diff --git a/.gitignore b/.gitignore index b2f09d9180..8fc24e7bad 100644 --- a/.gitignore +++ b/.gitignore @@ -88,6 +88,8 @@ docs/virtualenv/ # vim *.swp +*.swn +*.swo # Files you may find useful to have in your working directory. PLAN diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 4647555bea..175866f66d 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -91,6 +91,8 @@ + + diff --git a/TRADEMARK b/TRADEMARK index be0b5c8260..aa2799e5d3 100644 --- a/TRADEMARK +++ b/TRADEMARK @@ -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 \ No newline at end of file +For R3 HoldCo LLC's trademark and logo usage information, please consult our Trademark Usage Policy available at https://www.r3.com/trademark-policy diff --git a/build.gradle b/build.gradle index 4515bb43da..4904203fc2 100644 --- a/build.gradle +++ b/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') + } + } +} diff --git a/client/jackson/build.gradle b/client/jackson/build.gradle index 818b742f5d..234f7e1ae0 100644 --- a/client/jackson/build.gradle +++ b/client/jackson/build.gradle @@ -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') diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt index eaaeff5306..1ebb881d19 100644 --- a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt @@ -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 diff --git a/client/jfx/build.gradle b/client/jfx/build.gradle index 69f39972c6..5d12e01f56 100644 --- a/client/jfx/build.gradle +++ b/client/jfx/build.gradle @@ -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' diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index 678f4b3a50..5c93d9e820 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -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 diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt index 8505d1c621..e2b134bc8c 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt @@ -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( diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/StateMachineDataModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/StateMachineDataModel.kt index 139506d258..91167af17d 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/StateMachineDataModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/StateMachineDataModel.kt @@ -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 } } } diff --git a/client/mock/build.gradle b/client/mock/build.gradle index 3b278fcbba..d709d4c911 100644 --- a/client/mock/build.gradle +++ b/client/mock/build.gradle @@ -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' diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt index 771fca5fd1..6bd7b68e7c 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt @@ -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, val currencies: List 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, val currencies: List - 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, currencies: List, } 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( diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt index 74c052e723..9748e2a2ca 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/Generator.kt @@ -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 function. Keep the usage of this + * [Generator.flatMap] sequences two generators using an arbitrary A->Generator 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(val generate: (SplittableRandom) -> ErrorOr) { +class Generator(val generate: (SplittableRandom) -> Try) { // Functor fun map(function: (A) -> B): Generator = @@ -54,18 +54,19 @@ class Generator(val generate: (SplittableRandom) -> ErrorOr) { product(other1.product(other2.product(other3.product(other4.product(pure({ e -> { d -> { c -> { b -> { a -> function(a, b, c, d, e) } } } } })))))) // Monad - fun bind(function: (A) -> Generator) = - Generator { generate(it).bind { a -> function(a).generate(it) } } + fun flatMap(function: (A) -> Generator): Generator { + return Generator { random -> generate(random).flatMap { function(it).generate(random) } } + } companion object { - fun pure(value: A) = Generator { ErrorOr(value) } - fun impure(valueClosure: () -> A) = Generator { ErrorOr(valueClosure()) } - fun fail(error: Exception) = Generator { ErrorOr.of(error) } + fun pure(value: A) = Generator { Try.Success(value) } + fun impure(valueClosure: () -> A) = Generator { Try.Success(valueClosure()) } + fun fail(error: Exception) = Generator { Try.Failure(error) } // Alternative - fun choice(generators: List>) = intRange(0, generators.size - 1).bind { generators[it] } + fun choice(generators: List>) = intRange(0, generators.size - 1).flatMap { generators[it] } - fun success(generate: (SplittableRandom) -> A) = Generator { ErrorOr(generate(it)) } + fun success(generate: (SplittableRandom) -> A) = Generator { Try.Success(generate(it)) } fun frequency(generators: List>>): Generator { val ranges = mutableListOf>() var current = 0.0 @@ -74,11 +75,11 @@ class Generator(val generate: (SplittableRandom) -> ErrorOr) { 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(val generate: (SplittableRandom) -> ErrorOr) { val result = mutableListOf() 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 Generator.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 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 Generator.Companion.replicatePoisson(meanSize: Double, generator: Genera val result = mutableListOf() 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 Generator.Companion.pickOne(list: List) = Generator.intRange(0, list.size - 1).map { list[it] } @@ -211,7 +207,7 @@ fun Generator.Companion.pickN(number: Int, list: List) = Generator Generator.Companion.sampleBernoulli(maxRatio: Double = 1.0, vararg collection: A) = diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt b/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt index dd766cc4c9..7b35b8d5f8 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/Generators.kt @@ -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 { diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle index b18b563b0f..b2ab10dff4 100644 --- a/client/rpc/build.gradle +++ b/client/rpc/build.gradle @@ -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 -} \ No newline at end of file +} diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 72f3ef6316..40a179fd29 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -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 diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index 4cf43156d2..ac524995c7 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -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(HostAndPort.fromString("localhost:9999")).get() } + Try.on { startRpcClient(NetworkHostAndPort("localhost", 9999)).get() } val server = startRpcServer(ops = DummyOps) - ErrorOr.catch { startRpcClient( + Try.on { startRpcClient( server.get().broker.hostAndPort!!, configuration = RPCClientConfiguration.default.copy(minimumServerProtocolVersion = 1) ).get() } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index 199cdd6d67..78baa8d906 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -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 ) { diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index 3e52dbd946..a792c24faa 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -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( val rpcConfiguration: RPCClientConfiguration = RPCClientConfiguration.default ) { constructor( - hostAndPort: HostAndPort, + hostAndPort: NetworkHostAndPort, sslConfiguration: SSLConfiguration? = null, configuration: RPCClientConfiguration = RPCClientConfiguration.default ) : this(tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration), configuration) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index 86dcf7a7d1..e83363b7fc 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -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 -> { diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 3c0c055655..182ca10eea 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -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() - 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(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(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. diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/ValidateClasspathTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/ValidateClasspathTest.kt new file mode 100644 index 0000000000..ecc534ca8a --- /dev/null +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/ValidateClasspathTest.kt @@ -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 +} diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt index c9d3c65879..0117504c2e 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt @@ -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() 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() diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt index 2ffe065832..fb283773d1 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt @@ -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 diff --git a/config/dev/log4j2.xml b/config/dev/log4j2.xml index fa6ff74c5b..c0cb385396 100644 --- a/config/dev/log4j2.xml +++ b/config/dev/log4j2.xml @@ -13,7 +13,7 @@ - + @@ -27,7 +27,7 @@ fileName="${sys:log-path}/${log-name}.log" filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz"> - + diff --git a/constants.properties b/constants.properties index 1676023e4a..6993620217 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=0.12.4 +gradlePluginsVersion=0.13.2 kotlinVersion=1.1.1 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/cordform-common/build.gradle b/cordform-common/build.gradle index c3c1676b23..340a4b6ec6 100644 --- a/cordform-common/build.gradle +++ b/cordform-common/build.gradle @@ -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" diff --git a/cordform-common/src/main/java/net/corda/cordform/CordformNode.java b/cordform-common/src/main/java/net/corda/cordform/CordformNode.java index 66e0ba9ca0..80a9a3795a 100644 --- a/cordform-common/src/main/java/net/corda/cordform/CordformNode.java +++ b/cordform-common/src/main/java/net/corda/cordform/CordformNode.java @@ -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"; /** diff --git a/cordform-common/src/main/java/net/corda/cordform/NodeDefinition.java b/cordform-common/src/main/java/net/corda/cordform/NodeDefinition.java new file mode 100644 index 0000000000..0b86b98627 --- /dev/null +++ b/cordform-common/src/main/java/net/corda/cordform/NodeDefinition.java @@ -0,0 +1,9 @@ +package net.corda.cordform; + +import com.typesafe.config.Config; + +public interface NodeDefinition { + String getName(); + + Config getConfig(); +} diff --git a/core/src/main/kotlin/net/corda/core/utilities/CordaException.kt b/core/src/main/kotlin/net/corda/core/CordaException.kt similarity index 99% rename from core/src/main/kotlin/net/corda/core/utilities/CordaException.kt rename to core/src/main/kotlin/net/corda/core/CordaException.kt index 907bbee408..49ed6b6975 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/CordaException.kt +++ b/core/src/main/kotlin/net/corda/core/CordaException.kt @@ -1,4 +1,4 @@ -package net.corda.core.utilities +package net.corda.core import net.corda.core.serialization.CordaSerializable import java.util.* diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt index 2c26c227c2..24d8247df0 100644 --- a/core/src/main/kotlin/net/corda/core/Utils.kt +++ b/core/src/main/kotlin/net/corda/core/Utils.kt @@ -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 Future.getOrThrow(timeout: Duration? = null): T { return try { @@ -74,38 +69,20 @@ fun Future.getOrThrow(timeout: Duration? = null): T { } } -fun future(block: () -> T): ListenableFuture = CompletableToListenable(CompletableFuture.supplyAsync(block)) +fun future(block: () -> V): Future = CompletableFuture.supplyAsync(block) -private class CompletableToListenable(private val base: CompletableFuture) : Future by base, ListenableFuture { - override fun addListener(listener: Runnable, executor: Executor) { - base.whenCompleteAsync(BiConsumer { _, _ -> listener.run() }, executor) - } -} +fun , V> F.then(block: (F) -> V) = addListener(Runnable { block(this) }, MoreExecutors.directExecutor()) -// Some utilities for working with Guava listenable futures. -fun ListenableFuture.then(executor: Executor, body: () -> Unit) = addListener(Runnable(body), executor) - -fun ListenableFuture.success(executor: Executor, body: (T) -> Unit) = then(executor) { - val r = try { - get() - } catch(e: Throwable) { - return@then - } - body(r) -} - -fun ListenableFuture.failure(executor: Executor, body: (Throwable) -> Unit) = then(executor) { - try { +fun Future.match(success: (U) -> V, failure: (Throwable) -> V): V { + return success(try { getOrThrow() } catch (t: Throwable) { - body(t) - } + return failure(t) + }) } -infix fun ListenableFuture.then(body: () -> Unit): ListenableFuture = apply { then(RunOnCallerThread, body) } -infix fun ListenableFuture.success(body: (T) -> Unit): ListenableFuture = apply { success(RunOnCallerThread, body) } -infix fun ListenableFuture.failure(body: (Throwable) -> Unit): ListenableFuture = apply { failure(RunOnCallerThread, body) } -fun ListenableFuture<*>.andForget(log: Logger) = failure(RunOnCallerThread) { log.error("Background task failed:", it) } +fun ListenableFuture.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 ListenableFuture.map(mapper: (F) -> T): ListenableFuture = Futures.transform(this, { (mapper as (F?) -> T)(it) }) infix fun ListenableFuture.flatMap(mapper: (F) -> ListenableFuture): ListenableFuture = Futures.transformAsync(this) { mapper(it!!) } @@ -121,12 +98,12 @@ inline fun SettableFuture.catch(block: () -> T) { fun ListenableFuture.toObservable(): Observable { return Observable.create { subscriber -> - success { + thenMatch({ subscriber.onNext(it) subscriber.onCompleted() - } failure { + }, { subscriber.onError(it) - } + }) } } @@ -211,9 +188,6 @@ fun List.randomOrNull(): T? { /** Returns a random element in the list matching the given predicate, or null if none found */ fun List.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 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 catch(body: () -> T): ErrorOr { - return try { - ErrorOr(body()) - } catch (t: Throwable) { - ErrorOr.of(t) - } - } - - fun of(t: Throwable) = ErrorOr(null, t) - } - - fun 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 map(function: (A) -> B) = ErrorOr(value?.let(function), error) - - // Applicative - fun combine(other: ErrorOr, function: (A, B) -> C): ErrorOr { - val newError = error ?: other.error - return ErrorOr(if (newError != null) null else function(value as A, other.value as B), newError) - } - - // Monad - fun bind(function: (A) -> ErrorOr): ErrorOr { - 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 diff --git a/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt new file mode 100644 index 0000000000..8ab4d6f4e1 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/concurrent/ConcurrencyUtils.kt @@ -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 firstOf(vararg futures: ListenableFuture, handler: (ListenableFuture) -> T) = firstOf(futures, defaultLog, handler) + +private val defaultLog = LoggerFactory.getLogger("net.corda.core.concurrent") +@VisibleForTesting +internal val shortCircuitedTaskFailedMessage = "Short-circuited task failed:" + +internal fun firstOf(futures: Array>, log: Logger, handler: (ListenableFuture) -> T): ListenableFuture { + val resultFuture = SettableFuture.create() + 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 +} diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index d10d9cc2bb..0740a4a7e4 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -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 } @@ -126,7 +126,7 @@ infix fun 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(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() } } } diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionGraphSearch.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionGraphSearch.kt index 4e9bc3e006..4239b5772b 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionGraphSearch.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionGraphSearch.kt @@ -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) : Callable> { class Query( val withCommandOfType: Class? = null, diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt index b1e98a49e9..db388b5c8c 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt @@ -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 } } diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt deleted file mode 100644 index 1e0ae94678..0000000000 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt +++ /dev/null @@ -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) : 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, 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): 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 - 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 = 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): 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.expandedCompositeKeys: Set - get() = flatMap { it.keys }.toSet() \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt b/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt index ed3222bf18..cf679e8a7d 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt @@ -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) diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index c508c9a176..abfcaa964e 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -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 + = (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 = 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) { diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 28d4e18f4a..c77131522a 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -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 { - 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 { - 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 + } + } +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt index 01c0a0d2be..db7cf6473a 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt @@ -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) } diff --git a/core/src/main/kotlin/net/corda/core/crypto/EncodingUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/EncodingUtils.kt index b1681a19f9..a79821b760 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/EncodingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/EncodingUtils.kt @@ -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() 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). diff --git a/core/src/main/kotlin/net/corda/core/crypto/MetaData.kt b/core/src/main/kotlin/net/corda/core/crypto/MetaData.kt index a8dc49ae8e..edcf018e82 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/MetaData.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/MetaData.kt @@ -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 diff --git a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt index ee8f4a5afe..fcefe20a5b 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -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 /** diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt b/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt index 8f61f1b66d..49493f6d6f 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt @@ -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, val providerName: String, val algorithmName: String, val signatureName: String, val algSpec: AlgorithmParameterSpec?, - val keySize: Int, + val keySize: Int?, val desc: String) diff --git a/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt new file mode 100644 index 0000000000..51a13a076a --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt @@ -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) : 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) { + for ((node) in children) { + if (node is CompositeKey) { + val curVisitedMap = IdentityHashMap() + 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() + 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, 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): 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): 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 + 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 = 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): 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.expandedCompositeKeys: Set + get() = flatMap { it.keys }.toSet() \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignature.kt similarity index 87% rename from core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt rename to core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignature.kt index 328af22603..099875b39c 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignature.kt @@ -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 diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignaturesWithKeys.kt similarity index 83% rename from core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt rename to core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignaturesWithKeys.kt index 6edac6ce43..5a69484ffa 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignaturesWithKeys.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignaturesWithKeys.kt @@ -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 /** diff --git a/core/src/main/kotlin/net/corda/core/crypto/composite/KeyFactory.kt b/core/src/main/kotlin/net/corda/core/crypto/composite/KeyFactory.kt new file mode 100644 index 0000000000..b933188d88 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/composite/KeyFactory.kt @@ -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 engineGetKeySpec(key: Key, keySpec: Class): 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") + } +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/provider/CordaSecurityProvider.kt b/core/src/main/kotlin/net/corda/core/crypto/provider/CordaSecurityProvider.kt new file mode 100644 index 0000000000..7951142336 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/provider/CordaSecurityProvider.kt @@ -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 { 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") +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/testing/DummyKeys.kt b/core/src/main/kotlin/net/corda/core/crypto/testing/DummyKeys.kt new file mode 100644 index 0000000000..8b699ef38d --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/testing/DummyKeys.kt @@ -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 { + 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 { + 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)) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowException.kt b/core/src/main/kotlin/net/corda/core/flows/FlowException.kt index 153baeae46..e527f22c55 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowException.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowException.kt @@ -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 /** diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index a73cda8427..4583d14a62 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -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] implements a flow using direct, straight line blocking code. Thus you @@ -180,7 +180,7 @@ abstract class FlowLogic { * @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) = stateMachine.checkFlowPermission(permissionName, extraAuditData) + fun checkFlowPermission(permissionName: String, extraAuditData: Map) = stateMachine.checkFlowPermission(permissionName, extraAuditData) /** @@ -189,7 +189,7 @@ abstract class FlowLogic { * @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) = stateMachine.recordAuditEvent(eventType, comment, extraAuditData) + fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) = 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 { * * @return Returns null if this flow has no progress tracker. */ - fun track(): Pair>? { + fun track(): DataFeed? { // 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 { @Suspendable fun waitForLedgerCommit(hash: SecureHash): SignedTransaction = stateMachine.waitForLedgerCommit(hash, this) - //////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////// private var _stateMachine: FlowStateMachine<*>? = null /** diff --git a/core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt b/core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt index 25f2433ea0..a5f9c709a2 100644 --- a/core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt +++ b/core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt @@ -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. diff --git a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt index 5c81e0c4b2..7dc89ae4a5 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt @@ -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 diff --git a/core/src/main/kotlin/net/corda/core/identity/AnonymisedIdentity.kt b/core/src/main/kotlin/net/corda/core/identity/AnonymisedIdentity.kt new file mode 100644 index 0000000000..0048917443 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/identity/AnonymisedIdentity.kt @@ -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)) +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt index dc1ec16f58..33ffffb19b 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt @@ -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 diff --git a/core/src/main/kotlin/net/corda/core/identity/Party.kt b/core/src/main/kotlin/net/corda/core/identity/Party.kt index 7bc6ebab8b..e41c550c84 100644 --- a/core/src/main/kotlin/net/corda/core/identity/Party.kt +++ b/core/src/main/kotlin/net/corda/core/identity/Party.kt @@ -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 diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index bfe9910de8..bf99ebe570 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -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>? + val progressTrackerStepAndUpdates: DataFeed? ) { 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, Observable> + fun stateMachinesFeed(): DataFeed, 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 (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 vaultTrackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, - contractType: Class): Vault.PageAndUpdates + contractType: Class): DataFeed, Vault.Update> // DOCEND VaultTrackByAPI // Note: cannot apply @JvmOverloads to interfaces nor interface implementations // Java Helpers // DOCSTART VaultTrackAPIHelpers - fun vaultTrack(contractType: Class): Vault.PageAndUpdates { + fun vaultTrack(contractType: Class): DataFeed, Vault.Update> { return vaultTrackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) } - fun vaultTrackByCriteria(contractType: Class, criteria: QueryCriteria): Vault.PageAndUpdates { + fun vaultTrackByCriteria(contractType: Class, criteria: QueryCriteria): DataFeed, Vault.Update> { return vaultTrackBy(criteria, PageSpecification(), Sort(emptySet()), contractType) } - fun vaultTrackByWithPagingSpec(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates { + fun vaultTrackByWithPagingSpec(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { return vaultTrackBy(criteria, paging, Sort(emptySet()), contractType) } - fun vaultTrackByWithSorting(contractType: Class, criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates { + fun vaultTrackByWithSorting(contractType: Class, criteria: QueryCriteria, sorting: Sort): DataFeed, 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>, Observable> + fun vaultAndUpdates(): DataFeed>, 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, Observable> + fun verifiedTransactionsFeed(): DataFeed, 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, Observable> + fun stateMachineRecordedTransactionMappingFeed(): DataFeed, 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, Observable> + fun networkMapFeed(): DataFeed, 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 + + /** + * 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 CordaRPCOps.vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), @@ -292,7 +321,7 @@ inline fun CordaRPCOps.vaultQueryBy(criteria: QueryC inline fun CordaRPCOps.vaultTrackBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), paging: PageSpecification = PageSpecification(), - sorting: Sort = Sort(emptySet())): Vault.PageAndUpdates { + sorting: Sort = Sort(emptySet())): DataFeed, Vault.Update> { return vaultTrackBy(criteria, paging, sorting, T::class.java) } @@ -340,6 +369,27 @@ inline fun > CordaRPCOps.startFlow arg3: D ): FlowHandle = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3) +inline fun > CordaRPCOps.startFlow( + @Suppress("UNUSED_PARAMETER") + flowConstructor: (A, B, C, D, E) -> R, + arg0: A, + arg1: B, + arg2: C, + arg3: D, + arg4: E +): FlowHandle = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4) + +inline fun > 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 = 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 > CordaRPCOps.startTrac arg2: C, arg3: D ): FlowProgressHandle = 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(val snapshot: A, val updates: Observable) { + @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 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 get() = updates +} diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index 15f98be512..342e9997e1 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -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, + val legalIdentityAndCert: PartyAndCertificate, //TODO This field will be removed in future PR which gets rid of services. + val legalIdentitiesAndCerts: Set, val platformVersion: Int, var advertisedServices: List = 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 { return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity.party } } - fun servideIdentitiesAndCert(type: ServiceType): List { - return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity } - } } diff --git a/core/src/main/kotlin/net/corda/core/node/PhysicalLocationStructures.kt b/core/src/main/kotlin/net/corda/core/node/PhysicalLocationStructures.kt index 8d48e3a27f..049845432f 100644 --- a/core/src/main/kotlin/net/corda/core/node/PhysicalLocationStructures.kt +++ b/core/src/main/kotlin/net/corda/core/node/PhysicalLocationStructures.kt @@ -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() - val cityMap = HashMap() + private val caseInsensitiveLookups = HashMap() + val cityMap = HashMap() 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 } diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 13038dc558..081aea4d7f 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -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 cordaService(type: Class): 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) /** - * 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 toStateAndRef(ref: StateRef): StateAndRef { - val definingTx = storageService.validatedTransactions.getTransaction(ref.txhash) ?: throw TransactionResolutionException(ref.txhash) - return definingTx.tx.outRef(ref.index) + @Throws(TransactionResolutionException::class) + fun toStateAndRef(stateRef: StateRef): StateAndRef { + val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) + return stx.tx.outRef(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): 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] diff --git a/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt index 93ce39069d..af542a2b43 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt @@ -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 } diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index 027383031d..cda2fc7c5f 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -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, Observable> + fun track(): DataFeed, MapChange> /** Get the collection of nodes which advertise a specific service. */ fun getNodesWithService(serviceType: ServiceType): List { @@ -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 } diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt new file mode 100644 index 0000000000..aa3def742c --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt @@ -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 +} + +/** + * 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() + + // 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, 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) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index dfb7c4c5e1..6d1a486105 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -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(val states: Iterable>) { /** Checks whether the update contains a state of the specified type and state status */ fun containsType(clazz: Class, 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(val states: Iterable>) { * 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(val states: List>, val statesMetadata: List, - val pageable: PageSpecification, - val totalStatesAvailable: Int, - val stateTypes: StateStatus) + val totalStatesAvailable: Long, + val stateTypes: StateStatus, + val otherResults: List) @CordaSerializable data class StateMetadata(val ref: StateRef, @@ -140,9 +144,6 @@ class Vault(val states: Iterable>) { val notaryKey: String, val lockId: String?, val lockUpdateTime: Instant?) - - @CordaSerializable - data class PageAndUpdates (val current: Vault.Page, val future: Observable) } /** @@ -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, Observable> + fun track(): DataFeed, 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 states(clazzes: Set>, statuses: EnumSet, includeSoftLockedStates: Boolean = true): Iterable> // DOCEND VaultStatesQuery @@ -350,15 +351,18 @@ interface VaultQueryService { * and returns a [Vault.Page] object containing the following: * 1. states as a List of (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 _queryBy(criteria: QueryCriteria, @@ -381,7 +385,7 @@ interface VaultQueryService { fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, - contractType: Class): Vault.PageAndUpdates + contractType: Class): DataFeed, 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 trackBy(contractType: Class): Vault.Page { - return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) + fun trackBy(contractType: Class): DataFeed, Vault.Update> { + return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) } - fun trackBy(contractType: Class, criteria: QueryCriteria): Vault.PageAndUpdates { + fun trackBy(contractType: Class, criteria: QueryCriteria): DataFeed, Vault.Update> { return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractType) } - fun trackBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates { + fun trackBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { return _trackBy(criteria, paging, Sort(emptySet()), contractType) } - fun trackBy(contractType: Class, criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates { + fun trackBy(contractType: Class, criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { return _trackBy(criteria, PageSpecification(), sorting, contractType) } - fun trackBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates { + fun trackBy(contractType: Class, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed, Vault.Update> { return _trackBy(criteria, paging, sorting, contractType) } } @@ -439,23 +443,23 @@ inline fun VaultQueryService.queryBy(criteria: Query return _queryBy(criteria, paging, sorting, T::class.java) } -inline fun VaultQueryService.trackBy(): Vault.PageAndUpdates { +inline fun VaultQueryService.trackBy(): DataFeed, Vault.Update> { return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java) } -inline fun VaultQueryService.trackBy(criteria: QueryCriteria): Vault.PageAndUpdates { +inline fun VaultQueryService.trackBy(criteria: QueryCriteria): DataFeed, Vault.Update> { return _trackBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java) } -inline fun VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates { +inline fun VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> { return _trackBy(criteria, paging, Sort(emptySet()), T::class.java) } -inline fun VaultQueryService.trackBy(criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates { +inline fun VaultQueryService.trackBy(criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> { return _trackBy(criteria, PageSpecification(), sorting, T::class.java) } -inline fun VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates { +inline fun VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed, 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 + 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): Iterable + + /** + * 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 - - 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. */ diff --git a/core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt deleted file mode 100644 index 66171bd2ed..0000000000 --- a/core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt +++ /dev/null @@ -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, Observable> -} diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt index a6788cc1d2..8173616eda 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/TransactionStorage.kt @@ -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, Observable> -} - -/** - * 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, SignedTransaction> +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 9dd0e974d1..0bbce100e5 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -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) - /** - * VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates] - */ - data class VaultQueryCriteria @JvmOverloads constructor ( - val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - val contractStateTypes: Set>? = null, - val stateRefs: List? = null, - val notaryName: List? = 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 { 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>? = null, + val stateRefs: List? = null, + val notaryName: List? = null, + val includeSoftlockedStates: Boolean = true, + val timeCondition: TimeCondition? = null) : CommonQueryCriteria() { + override fun visit(parser: IQueryCriteriaParser): Collection { + 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? = null, - val linearId: List? = null, - val dealRef: List? = null) : QueryCriteria() { - + data class LinearStateQueryCriteria @JvmOverloads constructor(val participants: List? = null, + val linearId: List? = null, + val dealRef: List? = null, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { - 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? = null, - val owner: List? = null, - val quantity: ColumnPredicate? = null, - val issuerPartyName: List? = null, - val issuerRef: List? = null) : QueryCriteria() { - + data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List? = null, + val owner: List? = null, + val quantity: ColumnPredicate? = null, + val issuerPartyName: List? = null, + val issuerRef: List? = null, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { - 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(val expression: CriteriaExpression) : QueryCriteria() { + data class VaultCustomQueryCriteria @JvmOverloads constructor + (val expression: CriteriaExpression, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { - 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 { 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 { 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 fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection @@ -120,6 +129,3 @@ interface IQueryCriteriaParser { fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection fun parse(criteria: QueryCriteria, sorting: Sort? = null) : Collection } - -infix fun QueryCriteria.and(criteria: QueryCriteria): QueryCriteria = AndComposition(this, criteria) -infix fun QueryCriteria.or(criteria: QueryCriteria): QueryCriteria = OrComposition(this, criteria) diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index 4dcd9e6ed9..92817ac424 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -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 { data class BinaryLogical(val left: CriteriaExpression, val right: CriteriaExpression, val operator: BinaryLogicalOperator) : CriteriaExpression() data class Not(val expression: CriteriaExpression) : CriteriaExpression() data class ColumnPredicateExpression(val column: Column, val predicate: ColumnPredicate) : CriteriaExpression() + data class AggregateFunctionExpression(val column: Column, val predicate: ColumnPredicate, + val groupByColumns: List>?, + val orderBy: Sort.Direction?) : CriteriaExpression() } @CordaSerializable @@ -65,6 +79,7 @@ sealed class ColumnPredicate { data class CollectionExpression(val operator: CollectionOperator, val rightLiteral: Collection) : ColumnPredicate() data class Between>(val rightFromLiteral: C, val rightToLiteral: C) : ColumnPredicate() data class NullExpression(val operator: NullOperator) : ColumnPredicate() + data class AggregateFunction(val type: AggregateFunctionType) : ColumnPredicate() } fun resolveEnclosingObjectFromExpression(expression: CriteriaExpression): Class { @@ -72,9 +87,11 @@ fun resolveEnclosingObjectFromExpression(expression: CriteriaExpression resolveEnclosingObjectFromExpression(expression.left) is CriteriaExpression.Not -> resolveEnclosingObjectFromExpression(expression.expression) is CriteriaExpression.ColumnPredicateExpression -> resolveEnclosingObjectFromColumn(expression.column) + is CriteriaExpression.AggregateFunctionExpression -> resolveEnclosingObjectFromColumn(expression.column) } } +@Suppress("UNCHECKED_CAST") fun resolveEnclosingObjectFromColumn(column: Column): Class { return when (column) { is Column.Java -> column.field.declaringClass as Class @@ -102,21 +119,25 @@ fun getColumnName(column: Column): 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) { @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 > compare(operator: BinaryComparisonOperator, value: R) = ColumnPredicate.BinaryComparison(operator, value) - fun KProperty1.predicate(predicate: ColumnPredicate) = CriteriaExpression.ColumnPredicateExpression(Column.Kotlin(this), predicate) + fun Field.predicate(predicate: ColumnPredicate) = CriteriaExpression.ColumnPredicateExpression(Column.Java(this), predicate) + fun KProperty1.functionPredicate(predicate: ColumnPredicate, groupByColumns: List>? = null, orderBy: Sort.Direction? = null) + = CriteriaExpression.AggregateFunctionExpression(Column.Kotlin(this), predicate, groupByColumns, orderBy) + fun Field.functionPredicate(predicate: ColumnPredicate, groupByColumns: List>? = null, orderBy: Sort.Direction? = null) + = CriteriaExpression.AggregateFunctionExpression(Column.Java(this), predicate, groupByColumns, orderBy) + fun > KProperty1.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value)) fun > Field.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value)) @@ -200,15 +232,15 @@ object Builder { fun > KProperty1.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) fun > KProperty1.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) - fun Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) - fun Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) - fun > Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value) - fun > Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) - fun > Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value) - fun > Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) - fun > Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) - fun > Field.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) - fun > Field.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) + @JvmStatic fun Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) + @JvmStatic fun Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) + @JvmStatic fun > Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value) + @JvmStatic fun > Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) + @JvmStatic fun > Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value) + @JvmStatic fun > Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) + @JvmStatic fun > Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) + @JvmStatic fun > Field.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) + @JvmStatic fun > Field.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) fun equal(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value) fun notEqual(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value) @@ -221,14 +253,45 @@ object Builder { fun > notIn(collection: Collection) = ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection) fun KProperty1.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 KProperty1.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 KProperty1.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) - fun Field.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) + @JvmStatic fun Field.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) fun KProperty1.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) - fun Field.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) + @JvmStatic fun Field.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) + + /** aggregate functions */ + fun KProperty1.sum(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.SUM), groupByColumns?.map { Column.Kotlin(it) }, orderBy) + @JvmStatic @JvmOverloads + fun Field.sum(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.SUM), groupByColumns?.map { Column.Java(it) }, orderBy) + + fun KProperty1.count() = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.COUNT)) + @JvmStatic fun Field.count() = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.COUNT)) + + fun KProperty1.avg(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.AVG), groupByColumns?.map { Column.Kotlin(it) }, orderBy) + @JvmStatic + @JvmOverloads + fun Field.avg(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.AVG), groupByColumns?.map { Column.Java(it) }, orderBy) + + fun KProperty1.min(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MIN), groupByColumns?.map { Column.Kotlin(it) }, orderBy) + @JvmStatic + @JvmOverloads + fun Field.min(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MIN), groupByColumns?.map { Column.Java(it) }, orderBy) + + fun KProperty1.max(groupByColumns: List>? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MAX), groupByColumns?.map { Column.Kotlin(it) }, orderBy) + @JvmStatic + @JvmOverloads + fun Field.max(groupByColumns: List? = null, orderBy: Sort.Direction? = null) = + functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MAX), groupByColumns?.map { Column.Java(it) }, orderBy) } inline fun builder(block: Builder.() -> A) = block(Builder) diff --git a/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt index 1fe600300a..a74001d912 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt @@ -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()) } } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/schemas/DummyDealStateSchemaV1.kt b/core/src/main/kotlin/net/corda/core/schemas/DummyDealStateSchemaV1.kt deleted file mode 100644 index e26382143f..0000000000 --- a/core/src/main/kotlin/net/corda/core/schemas/DummyDealStateSchemaV1.kt +++ /dev/null @@ -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) -} diff --git a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt index b615e75b64..36a847eec3 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt @@ -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 diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt b/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt index d9755cec29..11f450cac9 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt @@ -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() { + override fun read(kryo: Kryo, input: Input, type: Class): 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 { diff --git a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt index 7963fb549e..9b1a18be0c 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt @@ -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() - 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()) 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 newInstantiatorOf(type: Class): ObjectInstantiator { + // 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) + } + } } diff --git a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt index 65de2949c5..b055c600da 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt @@ -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) : Exception() /** A serialisation engine that knows how to deserialise code inside a sandbox */ @ThreadSafe object WireTransactionSerializer : Serializer() { + @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() { } private fun attachmentsClassLoader(kryo: Kryo, attachmentHashes: List): 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() val attachments = ArrayList() 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() { } } +/** For serialising an ed25519 public key */ +@ThreadSafe +object ECPublicKeyImplSerializer : Serializer() { + 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 { + 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() { @@ -449,19 +468,6 @@ inline fun 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() { - override fun read(kryo: Kryo, input: Input, type: Class): 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 Kryo.register( return register( type.java, object : Serializer() { - override fun read(kryo: Kryo, input: Input, type: Class): T = read(kryo, input) + override fun read(kryo: Kryo, input: Input, clazz: Class): T = read(kryo, input) override fun write(kryo: Kryo, output: Output, obj: T) = write(kryo, output, obj) } ) @@ -625,7 +631,7 @@ object X500NameSerializer : Serializer() { */ @ThreadSafe object CertPathSerializer : Serializer() { - val factory = CertificateFactory.getInstance("X.509") + val factory: CertificateFactory = CertificateFactory.getInstance("X.509") override fun read(kryo: Kryo, input: Input, type: Class): CertPath { return factory.generateCertPath(input) } @@ -636,7 +642,7 @@ object CertPathSerializer : Serializer() { } /** - * For serialising an [CX509CertificateHolder] in an X.500 standard format. + * For serialising an [X509CertificateHolder] in an X.500 standard format. */ @ThreadSafe object X509CertificateSerializer : Serializer() { diff --git a/core/src/main/kotlin/net/corda/core/serialization/KryoAMQPSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/KryoAMQPSerializer.kt new file mode 100644 index 0000000000..42cb65aec7 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/KryoAMQPSerializer.kt @@ -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() { + 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 { + 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(allBytes), type) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPPrimitiveSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPPrimitiveSerializer.kt index 40f586a88e..b68d37c935 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPPrimitiveSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPPrimitiveSerializer.kt @@ -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 { - 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 { } 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 } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/ArraySerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/ArraySerializer.kt index 0cf705e16d..ca1612bc50 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/ArraySerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/ArraySerializer.kt @@ -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 { 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 List.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 diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/CollectionSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/CollectionSerializer.kt index 0f4421de6c..76ec0be975 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/CollectionSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/CollectionSerializer.kt @@ -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)) { diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/CustomSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/CustomSerializer.kt index e88230de3d..d08d3b8e88 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/CustomSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/CustomSerializer.kt @@ -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 : AMQPSerializer { /** * 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> + /** + * 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 : AMQPSerializer { 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(protected val clazz: Class<*>, protected val superClassSerializer: CustomSerializer) : CustomSerializer() { + override val additionalSerializers: Iterable> = 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(protected val clazz: Class) : CustomSerializer() { 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 : AMQPSerializer { abstract class Implements(protected val clazz: Class) : CustomSerializer() { 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 : AMQPSerializer { val withInheritance: Boolean = true) : CustomSerializer() { 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(CompositeType(type.typeName, null, emptyList(), descriptor, (proxySerializer.typeNotation as CompositeType).fields)) + val typeNotations = mutableSetOf(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 : AMQPSerializer { 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(clazz: Class, 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(clazz, String::class.java, /* Unused */ SerializerFactory(), withInheritance) { + + override val additionalSerializers: Iterable> = 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) + } + } } diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializationInput.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializationInput.kt index ccbe1fac20..2859dbb989 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializationInput.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializationInput.kt @@ -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(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 = 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 deserialize(bytes: SerializedBytes): T = deserialize(bytes, T::class.java) + inline fun deserialize(bytes: SerializedBytes): T = + deserialize(bytes, T::class.java) + + + @Throws(NotSerializableException::class) + inline internal fun deserializeAndReturnEnvelope(bytes: SerializedBytes): objectAndEnvelope = + deserializeAndReturnEnvelope(bytes, T::class.java) + + + @Throws(NotSerializableException::class) + private fun getEnvelope(bytes: SerializedBytes): 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 des(bytes: SerializedBytes, clazz: Class, generator: (SerializedBytes, Class) -> 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 deserialize(bytes: SerializedBytes, clazz: Class): 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(bytes, clazz) { bytes, clazz -> + var envelope = getEnvelope(bytes) + clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)) + } + } + + @Throws(NotSerializableException::class) + internal fun deserializeAndReturnEnvelope(bytes: SerializedBytes, clazz: Class): objectAndEnvelope { + return des>(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 - } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializedParameterizedType.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializedParameterizedType.kt index 9a0809d18d..8869d9c758 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializedParameterizedType.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializedParameterizedType.kt @@ -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, cl: ClassLoader): Type { diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/MapSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/MapSerializer.kt index 7991648f1a..95803f3070 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/MapSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/MapSerializer.kt @@ -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)) { diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/ObjectSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/ObjectSerializer.kt index 130d50d7a3..d22c968ef6 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/ObjectSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/ObjectSerializer.kt @@ -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 { +class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer { override val type: Type get() = clazz private val javaConstructor: Constructor? internal val propertySerializers: Collection @@ -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 { - return interfaces.map { it.typeName } + return interfaces.map { nameForType(it) } } diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/PropertySerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/PropertySerializer.kt index 2295a07b45..4020ca5cc5 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/PropertySerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/PropertySerializer.kt @@ -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 { - 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) : 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 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) + } } } } diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/Schema.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/Schema.kt index 5c627cc943..844f7ce51b 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/Schema.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/Schema.kt @@ -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, 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, 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, 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 +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationHelper.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationHelper.kt index 85082544a4..c77faa5119 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationHelper.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationHelper.kt @@ -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 constructorForDeserialization(clazz: Class): KFunction? { +internal fun constructorForDeserialization(type: Type): KFunction? { + val clazz: Class<*> = type.asClass()!! if (isConcrete(clazz)) { - var preferredCandidate: KFunction? = clazz.kotlin.primaryConstructor + var preferredCandidate: KFunction? = clazz.kotlin.primaryConstructor var annotatedCount = 0 val kotlinConstructors = clazz.kotlin.constructors val hasDefault = kotlinConstructors.any { it.parameters.isEmpty() } @@ -60,13 +59,14 @@ internal fun constructorForDeserialization(clazz: Class): 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 propertiesForSerialization(kotlinConstructor: KFunction?, clazz: Class<*>, factory: SerializerFactory): Collection { - return if (kotlinConstructor != null) propertiesForSerialization(kotlinConstructor, factory) else propertiesForSerialization(clazz, factory) +internal fun propertiesForSerialization(kotlinConstructor: KFunction?, type: Type, factory: SerializerFactory): Collection { + 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 propertiesForSerialization(kotlinConstructor: KFunction, factory: SerializerFactory): Collection { +private fun propertiesForSerializationFromConstructor(kotlinConstructor: KFunction, type: Type, factory: SerializerFactory): Collection { 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 propertiesForSerialization(kotlinConstructor: KFunction // 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 propertiesForSerialization(kotlinConstructor: KFunction private fun constructorParamTakesReturnTypeOfGetter(getter: Method, param: KParameter): Boolean = TypeToken.of(param.type.javaType).isSupertypeOf(getter.genericReturnType) -private fun propertiesForSerialization(clazz: Class<*>, factory: SerializerFactory): Collection { +private fun propertiesForSerializationFromAbstract(clazz: Class<*>, type: Type, factory: SerializerFactory): Collection { // 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 = 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 { +internal fun interfacesForSerialization(type: Type): List { val interfaces = LinkedHashSet() - exploreType(clazz, interfaces) + exploreType(type, interfaces) return interfaces.toList() } private fun exploreType(type: Type?, interfaces: MutableSet) { - 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) } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializerFactory.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializerFactory.kt index 3883aad9dd..a4f887be8b 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializerFactory.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializerFactory.kt @@ -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>() @@ -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 { - 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 { + 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? = findPathToDeclared(actualClass, declaredType, mutableListOf()) + 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): List? { + 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) { 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) { - 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 { + 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 { - 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? { + internal fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer? { + // 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(clazz, customSerializer as CustomSerializer) + } } } 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, 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> = 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 = "?" } } - diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/SingletonSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/SingletonSerializer.kt new file mode 100644 index 0000000000..ac7fca8d78 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/SingletonSerializer.kt @@ -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 { + override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" + private val interfaces = interfacesForSerialization(type) + + private fun generateProvides(): List = 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 + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/BigDecimalSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/BigDecimalSerializer.kt new file mode 100644 index 0000000000..68d02d2350 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/BigDecimalSerializer.kt @@ -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::class.java) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/CurrencySerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/CurrencySerializer.kt new file mode 100644 index 0000000000..cdad5b2242 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/CurrencySerializer.kt @@ -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::class.java, + withInheritance = false, + maker = { Currency.getInstance(it) }, + unmaker = { it.currencyCode }) diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/InstantSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/InstantSerializer.kt new file mode 100644 index 0000000000..aa0e32a927 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/InstantSerializer.kt @@ -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::class.java, InstantProxy::class.java, factory) { + override val additionalSerializers: Iterable> = 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) +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/PublicKeySerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/PublicKeySerializer.kt index 46536a1bed..747940eb4a 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/PublicKeySerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/PublicKeySerializer.kt @@ -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::class.java) { +/** + * A serializer that writes out a public key in X.509 format. + */ +object PublicKeySerializer : CustomSerializer.Implements(PublicKey::class.java) { override val additionalSerializers: Iterable> = 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) } } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/ThrowableSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/ThrowableSerializer.kt index ed267ed44d..7196667a41 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/ThrowableSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/ThrowableSerializer.kt @@ -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::class.java, ThrowableProxy::class.java, factory) { diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/X500NameSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/X500NameSerializer.kt new file mode 100644 index 0000000000..e45c45b5e9 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/X500NameSerializer.kt @@ -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::class.java) { + override val additionalSerializers: Iterable> = 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()) + } +} \ No newline at end of file diff --git a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt similarity index 53% rename from experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt rename to core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt index 7641b9c6ea..5d774df240 100644 --- a/experimental/src/main/kotlin/net/corda/carpenter/ClassCarpenter.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/carpenter/ClassCarpenter.kt @@ -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) { + 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): Field + abstract fun nullTest(mv: MethodVisitor, slot: Int) + } + + class NonNullableField(field: Class) : Field(field) { + override val nullabilityAnnotation = "Ljavax/annotation/Nonnull;" + + constructor(name: String, field: Class) : this(field) { + this.name = name + } + + override fun copy(name: String, field: Class) = 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) : Field(field) { + override val nullabilityAnnotation = "Ljavax/annotation/Nullable;" + + constructor(name: String, field: Class) : 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) = 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>, val superclass: Schema? = null, val interfaces: List> = 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, + val superclass: Schema? = null, + val interfaces: List> = emptyList()) + { + private fun Map.descriptors() = + LinkedHashMap(this.mapValues { it.value.descriptor }) - fun fieldsIncludingSuperclasses(): Map> = (superclass?.fieldsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(fields) - fun descriptorsIncludingSuperclasses(): Map = (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 = + (superclass?.fieldsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(fields) + + fun descriptorsIncludingSuperclasses(): Map = + (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, + superclass: Schema? = null, + interfaces: List> = emptyList() + ) : Schema(name, fields, superclass, interfaces) + + class InterfaceSchema( + name: String, + fields: Map, + superclass: Schema? = null, + interfaces: List> = 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>() @@ -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> = 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, "", "(" + 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, + "", + "(" + 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, "", "($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): 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") } } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index 00f83fdf3e..8a4453b2d6 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -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, operator fun plus(sigList: Collection) = 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)" } diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index e727ad7c7c..61a42d2dbb 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -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> = arrayListOf(), protected val commands: MutableList = arrayListOf(), protected val signers: MutableSet = 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) = 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 = ArrayList(inputs) + fun attachments(): List = ArrayList(attachments) + fun outputStates(): List> = ArrayList(outputs) + fun commands(): List = 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() @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) = addCommand(Command(data, keys)) - - // Accessors that yield immutable snapshots. - fun inputStates(): List = ArrayList(inputs) - - fun outputStates(): List> = ArrayList(outputs) - fun commands(): List = ArrayList(commands) - fun attachments(): List = ArrayList(attachments) -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index ecc2c58be9..684ad86f9b 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -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:") diff --git a/core/src/main/kotlin/net/corda/core/serialization/ByteArrays.kt b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt similarity index 90% rename from core/src/main/kotlin/net/corda/core/serialization/ByteArrays.kt rename to core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt index 9c36c9d19b..3102086b43 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt @@ -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 { diff --git a/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt b/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt index bc7fd86a14..f2a6a6b566 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt @@ -28,6 +28,7 @@ object Emoji { @JvmStatic val CODE_SKULL_AND_CROSSBONES: String = codePointsString(0x2620) @JvmStatic val CODE_BOOKS: String = codePointsString(0x1F4DA) @JvmStatic val CODE_SLEEPING_FACE: String = codePointsString(0x1F634) + @JvmStatic val CODE_LIGHTBULB: String = codePointsString(0x1F4A1) /** * When non-null, toString() methods are allowed to use emoji in the output as we're going to render them to a @@ -44,6 +45,7 @@ object Emoji { val coolGuy: String get() = if (emojiMode.get() != null) "$CODE_COOL_GUY " else "" val books: String get() = if (emojiMode.get() != null) "$CODE_BOOKS " else "" val sleepingFace: String get() = if (emojiMode.get() != null) "$CODE_SLEEPING_FACE " else "" + val lightBulb: String get() = if (emojiMode.get() != null) "$CODE_LIGHTBULB " else "" // These have old/non-emoji symbols with better platform support. val greenTick: String get() = if (emojiMode.get() != null) "$CODE_GREEN_TICK " else "✓" diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt new file mode 100644 index 0000000000..e6e656a199 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -0,0 +1,21 @@ +package net.corda.core.utilities + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * Get the [Logger] for a class using the syntax + * + * `val logger = loggerFor()` + */ +inline fun loggerFor(): Logger = LoggerFactory.getLogger(T::class.java) + +/** Log a TRACE level message produced by evaluating the given lamdba, but only if TRACE logging is enabled. */ +inline fun Logger.trace(msg: () -> String) { + if (isTraceEnabled) trace(msg()) +} + +/** Log a DEBUG level message produced by evaluating the given lamdba, but only if DEBUG logging is enabled. */ +inline fun Logger.debug(msg: () -> String) { + if (isDebugEnabled) debug(msg()) +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt b/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt index 804dd4ed7a..9ade89dfaf 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt @@ -2,6 +2,8 @@ package net.corda.core.utilities +import net.corda.core.crypto.commonName +import org.bouncycastle.asn1.x500.X500Name import java.lang.Character.UnicodeScript.* import java.text.Normalizer import java.util.regex.Pattern @@ -19,9 +21,13 @@ import javax.security.auth.x500.X500Principal * * @throws IllegalArgumentException if the name does not meet the required rules. The message indicates why not. */ -@Throws(IllegalArgumentException::class) fun validateLegalName(normalizedLegalName: String) { - rules.forEach { it.validate(normalizedLegalName) } + legalNameRules.forEach { it.validate(normalizedLegalName) } +} + +// TODO: Implement X500 attribute validation once the specification has been finalised. +fun validateX500Name(x500Name: X500Name) { + validateLegalName(x500Name.commonName) } val WHITESPACE = "\\s++".toRegex() @@ -35,7 +41,7 @@ fun normaliseLegalName(legalName: String): String { return Normalizer.normalize(trimmedLegalName, Normalizer.Form.NFKC) } -private val rules: List> = listOf( +private val legalNameRules: List> = listOf( UnicodeNormalizationRule(), CharacterRule(',', '=', '$', '"', '\'', '\\'), WordRule("node", "server"), @@ -107,7 +113,7 @@ private class X500NameRule : Rule { private class MustHaveAtLeastTwoLettersRule : Rule { override fun validate(legalName: String) { // Try to exclude names like "/", "£", "X" etc. - require(legalName.count { it.isLetter() } >= 3) { "Must have at least two letters" } + require(legalName.count { it.isLetter() } >= 3) { "Illegal input legal name '$legalName'. Legal name must have at least two letters" } } } diff --git a/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt b/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt new file mode 100644 index 0000000000..28d0e5c7dc --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt @@ -0,0 +1,37 @@ +package net.corda.core.utilities + +import java.net.URI + +/** + * Tuple of host and port. Use [parseNetworkHostAndPort] on untrusted data. + * @param host a hostname or IP address. IPv6 addresses must not be enclosed in square brackets. + * @param port a valid port number. + */ +data class NetworkHostAndPort(val host: String, val port: Int) { + companion object { + internal val invalidPortFormat = "Invalid port: %s" + internal val unparseableAddressFormat = "Unparseable address: %s" + internal val missingPortFormat = "Missing port: %s" + } + + init { + require(port in (0..0xffff)) { invalidPortFormat.format(port) } + } + + override fun toString() = if (':' in host) "[$host]:$port" else "$host:$port" +} + +/** + * Parses a string of the form host:port into a [NetworkHostAndPort]. + * The host part may be a hostname or IP address. If it's an IPv6 address, it must be enclosed in square brackets. + * Note this does not parse the toString of a resolved [java.net.InetSocketAddress], which is of a host/IP:port form. + * @throws IllegalArgumentException if the port is missing, the string is garbage, or the NetworkHostAndPort constructor rejected the parsed parts. + */ +fun String.parseNetworkHostAndPort() = run { + val uri = URI(null, this, null, null, null) + require(uri.host != null) { NetworkHostAndPort.unparseableAddressFormat.format(this) } + require(uri.port != -1) { NetworkHostAndPort.missingPortFormat.format(this) } + NetworkHostAndPort(bracketedHost.matchEntire(uri.host)?.groupValues?.get(1) ?: uri.host, uri.port) +} + +private val bracketedHost = "\\[(.*)]".toRegex() diff --git a/core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt b/core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt deleted file mode 100644 index d69ab38367..0000000000 --- a/core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt +++ /dev/null @@ -1,51 +0,0 @@ -package net.corda.core.utilities - -import java.nio.file.Path - -// TODO This doesn't belong in core and can be moved into node -object ProcessUtilities { - inline fun startJavaProcess( - arguments: List, - classpath: String = defaultClassPath, - jdwpPort: Int? = null, - extraJvmArguments: List = emptyList(), - inheritIO: Boolean = true, - errorLogPath: Path? = null, - workingDirectory: Path? = null - ): Process { - return startJavaProcess(C::class.java.name, arguments, classpath, jdwpPort, extraJvmArguments, inheritIO, errorLogPath, workingDirectory) - } - - fun startJavaProcess( - className: String, - arguments: List, - classpath: String = defaultClassPath, - jdwpPort: Int? = null, - extraJvmArguments: List = emptyList(), - inheritIO: Boolean = true, - errorLogPath: Path? = null, - workingDirectory: Path? = null - ): Process { - val separator = System.getProperty("file.separator") - val javaPath = System.getProperty("java.home") + separator + "bin" + separator + "java" - val debugPortArgument = if (jdwpPort != null) { - listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$jdwpPort") - } else { - emptyList() - } - - val allArguments = listOf(javaPath) + - debugPortArgument + - listOf("-Xmx200m", "-XX:+UseG1GC") + - extraJvmArguments + - listOf("-cp", classpath, className) + - arguments.toList() - return ProcessBuilder(allArguments).apply { - if (errorLogPath != null) redirectError(errorLogPath.toFile()) - if (inheritIO) inheritIO() - if (workingDirectory != null) directory(workingDirectory.toFile()) - }.start() - } - - val defaultClassPath: String get() = System.getProperty("java.class.path") -} diff --git a/core/src/main/kotlin/net/corda/core/utilities/Try.kt b/core/src/main/kotlin/net/corda/core/utilities/Try.kt new file mode 100644 index 0000000000..74c7833e66 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/utilities/Try.kt @@ -0,0 +1,74 @@ +package net.corda.core.utilities + +import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.Try.Failure +import net.corda.core.utilities.Try.Success + +/** + * Representation of an operation that has either succeeded with a result (represented by [Success]) or failed with an + * exception (represented by [Failure]). + */ +@CordaSerializable +sealed class Try { + companion object { + /** + * Executes the given block of code and returns a [Success] capturing the result, or a [Failure] if an exception + * is thrown. + */ + @JvmStatic + inline fun on(body: () -> T): Try { + return try { + Success(body()) + } catch (t: Throwable) { + Failure(t) + } + } + } + + /** Returns `true` iff the [Try] is a [Success]. */ + abstract val isFailure: Boolean + + /** Returns `true` iff the [Try] is a [Failure]. */ + abstract val isSuccess: Boolean + + /** Returns the value if a [Success] otherwise throws the exception if a [Failure]. */ + abstract fun getOrThrow(): A + + /** Maps the given function to the value from this [Success], or returns `this` if this is a [Failure]. */ + inline fun map(function: (A) -> B): Try = when (this) { + is Success -> Success(function(value)) + is Failure -> this + } + + /** Returns the given function applied to the value from this [Success], or returns `this` if this is a [Failure]. */ + inline fun flatMap(function: (A) -> Try): Try = when (this) { + is Success -> function(value) + is Failure -> this + } + + /** + * Maps the given function to the values from this [Success] and [other], or returns `this` if this is a [Failure] + * or [other] if [other] is a [Failure]. + */ + inline fun combine(other: Try, function: (A, B) -> C): Try = when (this) { + is Success -> when (other) { + is Success -> Success(function(value, other.value)) + is Failure -> other + } + is Failure -> this + } + + data class Success(val value: A) : Try() { + override val isSuccess: Boolean get() = true + override val isFailure: Boolean get() = false + override fun getOrThrow(): A = value + override fun toString(): String = "Success($value)" + } + + data class Failure(val exception: Throwable) : Try() { + override val isSuccess: Boolean get() = false + override val isFailure: Boolean get() = true + override fun getOrThrow(): Nothing = throw exception + override fun toString(): String = "Failure($exception)" + } +} diff --git a/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt b/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt index 48ecc3da4e..afa519fcec 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/UntrustworthyData.kt @@ -2,6 +2,7 @@ package net.corda.core.utilities import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowException +import java.io.Serializable /** * A small utility to approximate taint tracking: if a method gives you back one of these, it means the data came from @@ -23,11 +24,8 @@ class UntrustworthyData(private val fromUntrustedWorld: T) { @Throws(FlowException::class) fun unwrap(validator: Validator) = validator.validate(fromUntrustedWorld) - @Suppress("DEPRECATION") - @Deprecated("This old name was confusing, use unwrap instead", replaceWith = ReplaceWith("unwrap")) - inline fun validate(validator: (T) -> R) = validator(data) - - interface Validator { + @FunctionalInterface + interface Validator : Serializable { @Suspendable @Throws(FlowException::class) fun validate(data: T): R diff --git a/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt b/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt index f00941e786..a035bd256f 100644 --- a/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt @@ -9,6 +9,7 @@ import net.corda.core.crypto.isFulfilledBy import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction @@ -33,6 +34,15 @@ abstract class AbstractStateReplacementFlow { @CordaSerializable data class Proposal(val stateRef: StateRef, val modification: M, val stx: SignedTransaction) + /** + * The assembled transaction for upgrading a contract. + * + * @param stx signed transaction to do the upgrade. + * @param participants the parties involved in the upgrade transaction. + * @param myKey key + */ + data class UpgradeTx(val stx: SignedTransaction, val participants: Iterable, val myKey: PublicKey) + /** * The [Instigator] assembles the transaction for state replacement and sends out change proposals to all participants * ([Acceptor]) of that state. If participants agree to the proposed change, they each sign the transaction. @@ -57,17 +67,14 @@ abstract class AbstractStateReplacementFlow { @Suspendable @Throws(StateReplacementException::class) override fun call(): StateAndRef { - val (stx, participants) = assembleTx() + val (stx, participantKeys, myKey) = assembleTx() progressTracker.currentStep = SIGNING - val myKey = serviceHub.myInfo.legalIdentity - val me = listOf(myKey) - - val signatures = if (participants == me) { + val signatures = if (participantKeys.singleOrNull() == myKey) { getNotarySignatures(stx) } else { - collectSignatures((participants - me).map { it.owningKey }, stx) + collectSignatures(participantKeys - myKey, stx) } val finalTx = stx + signatures @@ -75,7 +82,13 @@ abstract class AbstractStateReplacementFlow { return finalTx.tx.outRef(0) } - abstract protected fun assembleTx(): Pair> + /** + * Build the upgrade transaction. + * + * @return a triple of the transaction, the public keys of all participants, and the participating public key of + * this node. + */ + abstract protected fun assembleTx(): UpgradeTx @Suspendable private fun collectSignatures(participants: Iterable, stx: SignedTransaction): List { diff --git a/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt b/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt index e876bcabbd..ce785ed6f2 100644 --- a/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt @@ -58,9 +58,12 @@ class ContractUpgradeFlow> { + override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { val baseTx = assembleBareTx(originalState, modification) - val stx = serviceHub.signInitialTransaction(baseTx) - return stx to originalState.state.data.participants + val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet() + // TODO: We need a much faster way of finding our key in the transaction + val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single() + val stx = serviceHub.signInitialTransaction(baseTx, myKey) + return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey) } } diff --git a/core/src/main/kotlin/net/corda/flows/FetchAttachmentsFlow.kt b/core/src/main/kotlin/net/corda/flows/FetchAttachmentsFlow.kt index 3ca8c8f695..805e25da14 100644 --- a/core/src/main/kotlin/net/corda/flows/FetchAttachmentsFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/FetchAttachmentsFlow.kt @@ -18,13 +18,13 @@ import net.corda.core.serialization.SerializeAsTokenContext class FetchAttachmentsFlow(requests: Set, otherSide: Party) : FetchDataFlow(requests, otherSide) { - override fun load(txid: SecureHash): Attachment? = serviceHub.storageService.attachments.openAttachment(txid) + override fun load(txid: SecureHash): Attachment? = serviceHub.attachments.openAttachment(txid) override fun convert(wire: ByteArray): Attachment = FetchedAttachment({ wire }) override fun maybeWriteToDisk(downloaded: List) { for (attachment in downloaded) { - serviceHub.storageService.attachments.importAttachment(attachment.open()) + serviceHub.attachments.importAttachment(attachment.open()) } } diff --git a/core/src/main/kotlin/net/corda/flows/FetchTransactionsFlow.kt b/core/src/main/kotlin/net/corda/flows/FetchTransactionsFlow.kt index 6e9c1055a8..0f99aad169 100644 --- a/core/src/main/kotlin/net/corda/flows/FetchTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/FetchTransactionsFlow.kt @@ -17,7 +17,5 @@ import net.corda.core.transactions.SignedTransaction class FetchTransactionsFlow(requests: Set, otherSide: Party) : FetchDataFlow(requests, otherSide) { - override fun load(txid: SecureHash): SignedTransaction? { - return serviceHub.storageService.validatedTransactions.getTransaction(txid) - } + override fun load(txid: SecureHash): SignedTransaction? = serviceHub.validatedTransactions.getTransaction(txid) } diff --git a/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt index e609c4f5ca..dfc3c3c20d 100644 --- a/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt @@ -74,16 +74,15 @@ class FinalityFlow(val transactions: Iterable, @Suspendable private fun notariseAndRecord(stxnsAndParties: List>>): List>> { - return stxnsAndParties.map { pair -> - val stx = pair.first + return stxnsAndParties.map { (stx, parties) -> val notarised = if (needsNotarySignature(stx)) { val notarySignatures = subFlow(NotaryFlow.Client(stx)) stx + notarySignatures } else { stx } - serviceHub.recordTransactions(listOf(notarised)) - Pair(notarised, pair.second) + serviceHub.recordTransactions(notarised) + Pair(notarised, parties) } } @@ -101,8 +100,7 @@ class FinalityFlow(val transactions: Iterable, } private fun lookupParties(ltxns: List>): List>> { - return ltxns.map { pair -> - val (stx, ltx) = pair + return ltxns.map { (stx, ltx) -> // Calculate who is meant to see the results based on the participants involved. val keys = ltx.outputs.flatMap { it.data.participants } + ltx.inputs.flatMap { it.state.data.participants } // TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them count as a reason to fail? diff --git a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt index f99bcd2f3a..ee5453d167 100644 --- a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt @@ -4,7 +4,6 @@ import net.corda.core.contracts.* import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker @@ -24,24 +23,25 @@ class NotaryChangeFlow( progressTracker: ProgressTracker = tracker()) : AbstractStateReplacementFlow.Instigator(originalState, newNotary, progressTracker) { - override fun assembleTx(): Pair> { + override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { val state = originalState.state val tx = TransactionType.NotaryChange.Builder(originalState.state.notary) - val participants: Iterable - - if (state.encumbrance == null) { + val participants: Iterable = if (state.encumbrance == null) { val modifiedState = TransactionState(state.data, modification) tx.addInputState(originalState) tx.addOutputState(modifiedState) - participants = state.data.participants + state.data.participants } else { - participants = resolveEncumbrances(tx) + resolveEncumbrances(tx) } val stx = serviceHub.signInitialTransaction(tx) + val participantKeys = participants.map { it.owningKey } + // TODO: We need a much faster way of finding our key in the transaction + val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single() - return Pair(stx, participants) + return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey) } /** @@ -53,7 +53,7 @@ class NotaryChangeFlow( private fun resolveEncumbrances(tx: TransactionBuilder): Iterable { val stateRef = originalState.ref val txId = stateRef.txhash - val issuingTx = serviceHub.storageService.validatedTransactions.getTransaction(txId) + val issuingTx = serviceHub.validatedTransactions.getTransaction(txId) ?: throw StateReplacementException("Transaction $txId not found") val outputs = issuingTx.tx.outputs diff --git a/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt index a12770ca1f..579f9c8125 100644 --- a/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt @@ -11,11 +11,8 @@ import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party -import net.corda.core.node.services.TimeWindowChecker -import net.corda.core.node.services.UniquenessException -import net.corda.core.node.services.UniquenessProvider +import net.corda.core.node.services.* import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap @@ -97,14 +94,13 @@ object NotaryFlow { * Additional transaction validation logic can be added when implementing [receiveAndVerifyTx]. */ // See AbstractStateReplacementFlow.Acceptor for why it's Void? - abstract class Service(val otherSide: Party, - val timeWindowChecker: TimeWindowChecker, - val uniquenessProvider: UniquenessProvider) : FlowLogic() { + abstract class Service(val otherSide: Party, val service: TrustedAuthorityNotaryService) : FlowLogic() { + @Suspendable override fun call(): Void? { val (id, inputs, timeWindow) = receiveAndVerifyTx() - validateTimeWindow(timeWindow) - commitInputStates(inputs, id) + service.validateTimeWindow(timeWindow) + service.commitInputStates(inputs, id, otherSide) signAndSendResponse(id) return null } @@ -118,44 +114,9 @@ object NotaryFlow { @Suspendable private fun signAndSendResponse(txId: SecureHash) { - val signature = sign(txId.bytes) + val signature = service.sign(txId.bytes) send(otherSide, listOf(signature)) } - - private 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. - */ - private fun commitInputStates(inputs: List, txId: SecureHash) { - try { - uniquenessProvider.commit(inputs, txId, otherSide) - } catch (e: UniquenessException) { - val conflicts = inputs.filterIndexed { i, stateRef -> - val consumingTx = e.error.stateHistory[stateRef] - consumingTx != null && consumingTx != UniquenessProvider.ConsumingTx(txId, i, otherSide) - } - if (conflicts.isNotEmpty()) { - // TODO: Create a new UniquenessException that only contains the conflicts filtered above. - logger.warn("Notary conflicts for $txId: $conflicts") - throw notaryException(txId, e) - } - } - } - - private fun sign(bits: ByteArray): DigitalSignature.WithKey { - return serviceHub.keyManagementService.sign(bits, serviceHub.notaryIdentityKey) - } - - 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)) - } } } diff --git a/core/src/main/kotlin/net/corda/flows/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/flows/ResolveTransactionsFlow.kt index 1b6d3d4b91..92f3b9ebd0 100644 --- a/core/src/main/kotlin/net/corda/flows/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/ResolveTransactionsFlow.kt @@ -193,7 +193,7 @@ class ResolveTransactionsFlow(private val txHashes: Set, private fun fetchMissingAttachments(downloads: List) { // TODO: This could be done in parallel with other fetches for extra speed. val missingAttachments = downloads.flatMap { wtx -> - wtx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null } + wtx.attachments.filter { serviceHub.attachments.openAttachment(it) == null } } if (missingAttachments.isNotEmpty()) subFlow(FetchAttachmentsFlow(missingAttachments.toSet(), otherSide)) diff --git a/core/src/main/kotlin/net/corda/flows/TransactionKeyFlow.kt b/core/src/main/kotlin/net/corda/flows/TransactionKeyFlow.kt new file mode 100644 index 0000000000..1989ef5d82 --- /dev/null +++ b/core/src/main/kotlin/net/corda/flows/TransactionKeyFlow.kt @@ -0,0 +1,50 @@ +package net.corda.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.unwrap + +/** + * Very basic flow which exchanges transaction key and certificate paths between two parties in a transaction. + * This is intended for use as a subflow of another flow. + */ +@StartableByRPC +@InitiatingFlow +class TransactionKeyFlow(val otherSide: Party, + val revocationEnabled: Boolean, + override val progressTracker: ProgressTracker) : FlowLogic>() { + constructor(otherSide: Party) : this(otherSide, false, tracker()) + + companion object { + object AWAITING_KEY : ProgressTracker.Step("Awaiting key") + + fun tracker() = ProgressTracker(AWAITING_KEY) + fun validateIdentity(otherSide: Party, anonymousOtherSide: AnonymisedIdentity): AnonymisedIdentity { + require(anonymousOtherSide.certificate.subject == otherSide.name) + return anonymousOtherSide + } + } + + @Suspendable + override fun call(): LinkedHashMap { + progressTracker.currentStep = AWAITING_KEY + val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) + serviceHub.identityService.registerAnonymousIdentity(legalIdentityAnonymous.identity, serviceHub.myInfo.legalIdentity, legalIdentityAnonymous.certPath) + + // Special case that if we're both parties, a single identity is generated + val identities = LinkedHashMap() + if (otherSide == serviceHub.myInfo.legalIdentity) { + identities.put(otherSide, legalIdentityAnonymous) + } else { + val otherSideAnonymous = sendAndReceive(otherSide, legalIdentityAnonymous).unwrap { validateIdentity(otherSide, it) } + identities.put(serviceHub.myInfo.legalIdentity, legalIdentityAnonymous) + identities.put(otherSide, otherSideAnonymous) + } + return identities + } + +} diff --git a/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt b/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt deleted file mode 100644 index ff50739ffc..0000000000 --- a/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt +++ /dev/null @@ -1,89 +0,0 @@ -package net.corda.flows - -import co.paralleluniverse.fibers.Suspendable -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatedBy -import net.corda.core.flows.InitiatingFlow -import net.corda.core.flows.StartableByRPC -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.Party -import net.corda.core.serialization.CordaSerializable -import net.corda.core.utilities.ProgressTracker -import net.corda.core.utilities.unwrap -import org.bouncycastle.cert.X509CertificateHolder -import java.security.cert.CertPath - -/** - * Very basic flow which exchanges transaction key and certificate paths between two parties in a transaction. - * This is intended for use as a subflow of another flow. - */ -object TxKeyFlow { - abstract class AbstractIdentityFlow(val otherSide: Party, val revocationEnabled: Boolean): FlowLogic() { - fun validateIdentity(untrustedIdentity: AnonymousIdentity): AnonymousIdentity { - val (certPath, theirCert, txIdentity) = untrustedIdentity - if (theirCert.subject == otherSide.name) { - serviceHub.identityService.registerAnonymousIdentity(txIdentity, otherSide, certPath) - return AnonymousIdentity(certPath, theirCert, txIdentity) - } else - throw IllegalStateException("Expected certificate subject to be ${otherSide.name} but found ${theirCert.subject}") - } - } - - @StartableByRPC - @InitiatingFlow - class Requester(otherSide: Party, - override val progressTracker: ProgressTracker) : AbstractIdentityFlow>(otherSide, false) { - constructor(otherSide: Party) : this(otherSide, tracker()) - companion object { - object AWAITING_KEY : ProgressTracker.Step("Awaiting key") - - fun tracker() = ProgressTracker(AWAITING_KEY) - } - - @Suspendable - override fun call(): Map { - progressTracker.currentStep = AWAITING_KEY - val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) - val myIdentity = AnonymousIdentity(myIdentityFragment) - val theirIdentity = receive(otherSide).unwrap { validateIdentity(it) } - send(otherSide, myIdentity) - return mapOf(Pair(otherSide, myIdentity), - Pair(serviceHub.myInfo.legalIdentity, theirIdentity)) - } - } - - /** - * Flow which waits for a key request from a counterparty, generates a new key and then returns it to the - * counterparty and as the result from the flow. - */ - @InitiatedBy(Requester::class) - class Provider(otherSide: Party) : AbstractIdentityFlow>(otherSide, false) { - companion object { - object SENDING_KEY : ProgressTracker.Step("Sending key") - } - - override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY) - - @Suspendable - override fun call(): Map { - val revocationEnabled = false - progressTracker.currentStep = SENDING_KEY - val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) - val myIdentity = AnonymousIdentity(myIdentityFragment) - send(otherSide, myIdentity) - val theirIdentity = receive(otherSide).unwrap { validateIdentity(it) } - return mapOf(Pair(otherSide, myIdentity), - Pair(serviceHub.myInfo.legalIdentity, theirIdentity)) - } - } - - @CordaSerializable - data class AnonymousIdentity( - val certPath: CertPath, - val certificate: X509CertificateHolder, - val identity: AnonymousParty) { - constructor(myIdentity: Pair) : this(myIdentity.second, - myIdentity.first, - AnonymousParty(myIdentity.second.certificates.first().publicKey)) - } -} diff --git a/core/src/test/kotlin/net/corda/core/utilities/CollectionExtensionTests.kt b/core/src/test/kotlin/net/corda/core/CollectionExtensionTests.kt similarity index 92% rename from core/src/test/kotlin/net/corda/core/utilities/CollectionExtensionTests.kt rename to core/src/test/kotlin/net/corda/core/CollectionExtensionTests.kt index 169fab9793..1fab9fceaa 100644 --- a/core/src/test/kotlin/net/corda/core/utilities/CollectionExtensionTests.kt +++ b/core/src/test/kotlin/net/corda/core/CollectionExtensionTests.kt @@ -1,7 +1,5 @@ -package net.corda.core.utilities +package net.corda.core -import net.corda.core.indexOfOrThrow -import net.corda.core.noneOrSingle import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith diff --git a/core/src/test/kotlin/net/corda/core/concurrent/ConcurrencyUtilsTest.kt b/core/src/test/kotlin/net/corda/core/concurrent/ConcurrencyUtilsTest.kt new file mode 100644 index 0000000000..722d67184e --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/concurrent/ConcurrencyUtilsTest.kt @@ -0,0 +1,78 @@ +package net.corda.core.concurrent + +import com.google.common.util.concurrent.SettableFuture +import com.nhaarman.mockito_kotlin.* +import net.corda.core.getOrThrow +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Test +import org.slf4j.Logger +import java.io.EOFException +import java.util.concurrent.CancellationException +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ConcurrencyUtilsTest { + private val f1 = SettableFuture.create() + private val f2 = SettableFuture.create() + private var invocations = 0 + private val log: Logger = mock() + @Test + fun `firstOf short circuit`() { + // Order not significant in this case: + val g = firstOf(arrayOf(f2, f1), log) { + ++invocations + it.getOrThrow() + } + f1.set(100) + assertEquals(100, g.getOrThrow()) + assertEquals(1, invocations) + verifyNoMoreInteractions(log) + val throwable = EOFException("log me") + f2.setException(throwable) + assertEquals(1, invocations) // Least astonishing to skip handler side-effects. + verify(log).error(eq(shortCircuitedTaskFailedMessage), same(throwable)) + } + + @Test + fun `firstOf re-entrant handler attempt due to cancel`() { + val futures = arrayOf(f1, f2) + val g = firstOf(futures, log) { + ++invocations + futures.forEach { it.cancel(false) } // One handler invocation queued here. + it.getOrThrow() + } + f1.set(100) + assertEquals(100, g.getOrThrow()) + assertEquals(1, invocations) // Handler didn't run as g was already done. + verifyNoMoreInteractions(log) // CancellationException is not logged (if due to cancel). + assertTrue(f2.isCancelled) + } + + @Test + fun `firstOf re-entrant handler attempt not due to cancel`() { + val futures = arrayOf(f1, f2) + val fakeCancel = CancellationException() + val g = firstOf(futures, log) { + ++invocations + futures.forEach { it.setException(fakeCancel) } // One handler attempt here. + it.getOrThrow() + } + f1.set(100) + assertEquals(100, g.getOrThrow()) + assertEquals(1, invocations) // Handler didn't run as g was already done. + verify(log).error(eq(shortCircuitedTaskFailedMessage), same(fakeCancel)) + assertThatThrownBy { f2.getOrThrow() }.isSameAs(fakeCancel) + } + + @Test + fun `firstOf cancel is not special`() { + val g = firstOf(arrayOf(f2, f1), log) { + ++invocations + it.getOrThrow() // This can always do something fancy if 'it' was cancelled. + } + f1.cancel(false) + assertThatThrownBy { g.getOrThrow() }.isInstanceOf(CancellationException::class.java) + assertEquals(1, invocations) + verifyNoMoreInteractions(log) + } +} diff --git a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt index dfe175ded2..e059dd2e1e 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt @@ -1,9 +1,10 @@ package net.corda.core.contracts +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyContractV2 import net.corda.core.crypto.SecureHash -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.testing.ALICE_PUBKEY +import net.corda.testing.ALICE +import net.corda.testing.DUMMY_NOTARY import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertTrue diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt index aee51ffd46..8f55aa7317 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt @@ -1,14 +1,17 @@ package net.corda.core.contracts +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyState import net.corda.core.crypto.newSecureRandom import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY import net.corda.testing.MEGA_CORP_KEY +import net.corda.testing.MEGA_CORP_PUBKEY +import net.corda.testing.node.MockServices import net.corda.testing.node.MockTransactionStorage import org.junit.Test -import java.security.KeyPair import kotlin.test.assertEquals class TransactionGraphSearchTests { @@ -28,24 +31,29 @@ class TransactionGraphSearchTests { * @param command the command to add to the origin transaction. * @param signer signer for the two transactions and their commands. */ - fun buildTransactions(command: CommandData, signer: KeyPair): GraphTransactionStorage { - val originTx = TransactionType.General.Builder(DUMMY_NOTARY).apply { - addOutputState(DummyState(random31BitValue())) - addCommand(command, signer.public) - signWith(signer) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction(false) - val inputTx = TransactionType.General.Builder(DUMMY_NOTARY).apply { - addInputState(originTx.tx.outRef(0)) - signWith(signer) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction(false) + fun buildTransactions(command: CommandData): GraphTransactionStorage { + val megaCorpServices = MockServices(MEGA_CORP_KEY) + val notaryServices = MockServices(DUMMY_NOTARY_KEY) + + val originBuilder = TransactionType.General.Builder(DUMMY_NOTARY) + originBuilder.addOutputState(DummyState(random31BitValue())) + originBuilder.addCommand(command, MEGA_CORP_PUBKEY) + + val originPtx = megaCorpServices.signInitialTransaction(originBuilder) + val originTx = notaryServices.addSignature(originPtx) + + val inputBuilder = TransactionType.General.Builder(DUMMY_NOTARY) + inputBuilder.addInputState(originTx.tx.outRef(0)) + + val inputPtx = megaCorpServices.signInitialTransaction(inputBuilder) + val inputTx = megaCorpServices.addSignature(inputPtx) + return GraphTransactionStorage(originTx, inputTx) } @Test fun `return empty from empty`() { - val storage = buildTransactions(DummyContract.Commands.Create(), MEGA_CORP_KEY) + val storage = buildTransactions(DummyContract.Commands.Create()) val search = TransactionGraphSearch(storage, emptyList()) search.query = TransactionGraphSearch.Query() val expected = emptyList() @@ -56,7 +64,7 @@ class TransactionGraphSearchTests { @Test fun `return empty from no match`() { - val storage = buildTransactions(DummyContract.Commands.Create(), MEGA_CORP_KEY) + val storage = buildTransactions(DummyContract.Commands.Create()) val search = TransactionGraphSearch(storage, listOf(storage.inputTx.tx)) search.query = TransactionGraphSearch.Query() val expected = emptyList() @@ -67,7 +75,7 @@ class TransactionGraphSearchTests { @Test fun `return origin on match`() { - val storage = buildTransactions(DummyContract.Commands.Create(), MEGA_CORP_KEY) + val storage = buildTransactions(DummyContract.Commands.Create()) val search = TransactionGraphSearch(storage, listOf(storage.inputTx.tx)) search.query = TransactionGraphSearch.Query(DummyContract.Commands.Create::class.java) val expected = listOf(storage.originTx.tx) diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt index 61750d7619..d4308446e2 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt @@ -1,7 +1,8 @@ package net.corda.core.contracts import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY -import net.corda.core.crypto.CompositeKey +import net.corda.testing.contracts.DummyContract +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.sign @@ -10,7 +11,7 @@ import net.corda.core.serialization.SerializedBytes import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.* +import net.corda.testing.* import org.junit.Test import java.security.KeyPair import kotlin.test.assertEquals diff --git a/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt b/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt index 5a7aba240c..4627f1baa2 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/clauses/VerifyClausesTests.kt @@ -1,6 +1,10 @@ package net.corda.core.contracts.clauses -import net.corda.core.contracts.* +import net.corda.core.contracts.AuthenticatedObject +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.TransactionForContract +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.SecureHash import org.junit.Test import kotlin.test.assertFailsWith diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index 8d2bed5a73..5294898acd 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -1,13 +1,25 @@ package net.corda.core.crypto -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.crypto.composite.CompositeKey +import net.corda.core.crypto.composite.CompositeSignature +import net.corda.core.crypto.composite.CompositeSignaturesWithKeys +import net.corda.core.div import net.corda.core.serialization.serialize +import net.corda.core.utilities.OpaqueBytes +import org.bouncycastle.asn1.x500.X500Name +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue class CompositeKeyTests { + @Rule + @JvmField + val tempFolder: TemporaryFolder = TemporaryFolder() + val aliceKey = generateKeyPair() val bobKey = generateKeyPair() val charlieKey = generateKeyPair() @@ -21,7 +33,6 @@ class CompositeKeyTests { val aliceSignature = aliceKey.sign(message) val bobSignature = bobKey.sign(message) val charlieSignature = charlieKey.sign(message) - val compositeAliceSignature = CompositeSignaturesWithKeys(listOf(aliceSignature)) @Test fun `(Alice) fulfilled by Alice signature`() { @@ -65,7 +76,7 @@ class CompositeKeyTests { } @Test - fun `encoded tree decodes correctly`() { + fun `kryo encoded tree decodes correctly`() { val aliceAndBob = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build() val aliceAndBobOrCharlie = CompositeKey.Builder().addKeys(aliceAndBob, charliePublicKey).build(threshold = 1) @@ -75,6 +86,35 @@ class CompositeKeyTests { assertEquals(decoded, aliceAndBobOrCharlie) } + @Test + fun `der encoded tree decodes correctly`() { + val aliceAndBob = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build() + val aliceAndBobOrCharlie = CompositeKey.Builder().addKeys(aliceAndBob, charliePublicKey).build(threshold = 1) + + val encoded = aliceAndBobOrCharlie.encoded + val decoded = CompositeKey.getInstance(encoded) + + assertEquals(decoded, aliceAndBobOrCharlie) + } + + @Test + fun `der encoded tree decodes correctly with weighting`() { + val aliceAndBob = CompositeKey.Builder() + .addKey(alicePublicKey, 2) + .addKey(bobPublicKey, 1) + .build(threshold = 2) + + val aliceAndBobOrCharlie = CompositeKey.Builder() + .addKey(aliceAndBob, 3) + .addKey(charliePublicKey, 2) + .build(threshold = 3) + + val encoded = aliceAndBobOrCharlie.encoded + val decoded = CompositeKey.getInstance(encoded) + + assertEquals(decoded, aliceAndBobOrCharlie) + } + @Test fun `tree canonical form`() { assertEquals(CompositeKey.Builder().addKeys(alicePublicKey).build(), alicePublicKey) @@ -124,4 +164,210 @@ class CompositeKeyTests { val brokenBobSignature = DigitalSignature.WithKey(bobSignature.by, aliceSignature.bytes) assertFalse { engine.verify(CompositeSignaturesWithKeys(listOf(aliceSignature, brokenBobSignature)).serialize().bytes) } } + + @Test() + fun `composite key constraints`() { + // Zero weight. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKey(alicePublicKey, 0) + } + // Negative weight. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKey(alicePublicKey, -1) + } + // Zero threshold. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKey(alicePublicKey).build(0) + } + // Negative threshold. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKey(alicePublicKey).build(-1) + } + // Threshold > Total-weight. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKey(alicePublicKey, 2).addKey(bobPublicKey, 2).build(5) + } + // Threshold value different than weight of single child node. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKey(alicePublicKey, 3).build(2) + } + // Aggregated weight integer overflow. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKey(alicePublicKey, Int.MAX_VALUE).addKey(bobPublicKey, Int.MAX_VALUE).build() + } + // Duplicated children. + assertFailsWith(IllegalArgumentException::class) { + CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey, alicePublicKey).build() + } + // Duplicated composite key children. + assertFailsWith(IllegalArgumentException::class) { + val compositeKey1 = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build() + val compositeKey2 = CompositeKey.Builder().addKeys(bobPublicKey, alicePublicKey).build() + CompositeKey.Builder().addKeys(compositeKey1, compositeKey2).build() + } + } + + @Test() + fun `composite key validation with graph cycle detection`() { + val key1 = CompositeKey.Builder().addKeys(alicePublicKey, bobPublicKey).build() as CompositeKey + val key2 = CompositeKey.Builder().addKeys(alicePublicKey, key1).build() as CompositeKey + val key3 = CompositeKey.Builder().addKeys(alicePublicKey, key2).build() as CompositeKey + val key4 = CompositeKey.Builder().addKeys(alicePublicKey, key3).build() as CompositeKey + val key5 = CompositeKey.Builder().addKeys(alicePublicKey, key4).build() as CompositeKey + val key6 = CompositeKey.Builder().addKeys(alicePublicKey, key5, key2).build() as CompositeKey + + // Initially, there is no any graph cycle. + key1.checkValidity() + key2.checkValidity() + key3.checkValidity() + key4.checkValidity() + key5.checkValidity() + // The fact that key6 has a direct reference to key2 and an indirect (via path key5->key4->key3->key2) + // does not imply a cycle, as expected (independent paths). + key6.checkValidity() + + // We will create a graph cycle between key5 and key3. Key5 has already a reference to key3 (via key4). + // To create a cycle, we add a reference (child) from key3 to key5. + // Children list is immutable, so reflection is used to inject key5 as an extra NodeAndWeight child of key3. + val field = key3.javaClass.getDeclaredField("children") + field.isAccessible = true + val fixedChildren = key3.children.plus(CompositeKey.NodeAndWeight(key5, 1)) + field.set(key3, fixedChildren) + + /* A view of the example graph cycle. + * + * key6 + * / \ + * key5 key2 + * / + * key4 + * / + * key3 + * / \ + * key2 key5 + * / + * key1 + * + */ + + // Detect the graph cycle starting from key3. + assertFailsWith(IllegalArgumentException::class) { + key3.checkValidity() + } + + // Detect the graph cycle starting from key4. + assertFailsWith(IllegalArgumentException::class) { + key4.checkValidity() + } + + // Detect the graph cycle starting from key5. + assertFailsWith(IllegalArgumentException::class) { + key5.checkValidity() + } + + // Detect the graph cycle starting from key6. + // Typically, one needs to test on the root tree-node only (thus, a validity check on key6 would be enough). + assertFailsWith(IllegalArgumentException::class) { + key6.checkValidity() + } + + // Key2 (and all paths below it, i.e. key1) are outside the graph cycle and thus, there is no impact on them. + key2.checkValidity() + key1.checkValidity() + } + + @Test + fun `CompositeKey from multiple signature schemes and signature verification`() { + val (privRSA, pubRSA) = Crypto.generateKeyPair(Crypto.RSA_SHA256) + val (privK1, pubK1) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val (privR1, pubR1) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val (privEd, pubEd) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val (privSP, pubSP) = Crypto.generateKeyPair(Crypto.SPHINCS256_SHA256) + + val RSASignature = privRSA.sign(message.bytes, pubRSA) + val K1Signature = privK1.sign(message.bytes, pubK1) + val R1Signature = privR1.sign(message.bytes, pubR1) + val EdSignature = privEd.sign(message.bytes, pubEd) + val SPSignature = privSP.sign(message.bytes, pubSP) + + val compositeKey = CompositeKey.Builder().addKeys(pubRSA, pubK1, pubR1, pubEd, pubSP).build() as CompositeKey + + val signatures = listOf(RSASignature, K1Signature, R1Signature, EdSignature, SPSignature) + assertTrue { compositeKey.isFulfilledBy(signatures.byKeys()) } + + // One signature is missing. + val signaturesWithoutRSA = listOf(K1Signature, R1Signature, EdSignature, SPSignature) + assertFalse { compositeKey.isFulfilledBy(signaturesWithoutRSA.byKeys()) } + } + + @Test + fun `Test save to keystore`() { + // From test case [CompositeKey from multiple signature schemes and signature verification] + val (privRSA, pubRSA) = Crypto.generateKeyPair(Crypto.RSA_SHA256) + val (privK1, pubK1) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val (privR1, pubR1) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val (privEd, pubEd) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val (privSP, pubSP) = Crypto.generateKeyPair(Crypto.SPHINCS256_SHA256) + + val RSASignature = privRSA.sign(message.bytes, pubRSA) + val K1Signature = privK1.sign(message.bytes, pubK1) + val R1Signature = privR1.sign(message.bytes, pubR1) + val EdSignature = privEd.sign(message.bytes, pubEd) + val SPSignature = privSP.sign(message.bytes, pubSP) + + val compositeKey = CompositeKey.Builder().addKeys(pubRSA, pubK1, pubR1, pubEd, pubSP).build() as CompositeKey + + val signatures = listOf(RSASignature, K1Signature, R1Signature, EdSignature, SPSignature) + assertTrue { compositeKey.isFulfilledBy(signatures.byKeys()) } + // One signature is missing. + val signaturesWithoutRSA = listOf(K1Signature, R1Signature, EdSignature, SPSignature) + assertFalse { compositeKey.isFulfilledBy(signaturesWithoutRSA.byKeys()) } + + // Create self sign CA. + val caKeyPair = Crypto.generateKeyPair() + val ca = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Test CA"), caKeyPair) + + // Sign the composite key with the self sign CA. + val compositeKeyCert = X509Utilities.createCertificate(CertificateType.IDENTITY, ca, caKeyPair, X500Name("CN=CompositeKey"), compositeKey) + + // Store certificate to keystore. + val keystorePath = tempFolder.root.toPath() / "keystore.jks" + val keystore = KeyStoreUtilities.loadOrCreateKeyStore(keystorePath, "password") + keystore.setCertificateEntry("CompositeKey", compositeKeyCert.cert) + keystore.save(keystorePath, "password") + + // Load keystore from disk. + val keystore2 = KeyStoreUtilities.loadKeyStore(keystorePath, "password") + assertTrue { keystore2.containsAlias("CompositeKey") } + + val key = keystore2.getCertificate("CompositeKey").publicKey + // Convert sun public key to Composite key. + val compositeKey2 = Crypto.toSupportedPublicKey(key) + assertTrue { compositeKey2 is CompositeKey } + + // Run the same composite key test again. + assertTrue { compositeKey2.isFulfilledBy(signatures.byKeys()) } + assertFalse { compositeKey2.isFulfilledBy(signaturesWithoutRSA.byKeys()) } + + // Ensure keys are the same before and after keystore. + assertEquals(compositeKey, compositeKey2) + } + + @Test + fun `CompositeKey deterministic children sorting`() { + val (_, pub1) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val (_, pub2) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val (_, pub3) = Crypto.generateKeyPair(Crypto.RSA_SHA256) + val (_, pub4) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val (_, pub5) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val (_, pub6) = Crypto.generateKeyPair(Crypto.SPHINCS256_SHA256) + val (_, pub7) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + + // Using default weight = 1, thus all weights are equal. + val composite1 = CompositeKey.Builder().addKeys(pub1, pub2, pub3, pub4, pub5, pub6, pub7).build() as CompositeKey + // Store in reverse order. + val composite2 = CompositeKey.Builder().addKeys(pub7, pub6, pub5, pub4, pub3, pub2, pub1).build() as CompositeKey + // There are 7! = 5040 permutations, but as sorting is deterministic the following should never fail. + assertEquals(composite1.children, composite2.children) + } } diff --git a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt index 7a45fe7ac3..5468886603 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt @@ -344,7 +344,7 @@ class CryptoUtilsTest { @Test fun `Check supported algorithms`() { val algList: List = Crypto.supportedSignatureSchemes.keys.toList() - val expectedAlgSet = setOf("RSA_SHA256", "ECDSA_SECP256K1_SHA256", "ECDSA_SECP256R1_SHA256", "EDDSA_ED25519_SHA512", "SPHINCS-256_SHA512") + val expectedAlgSet = setOf("RSA_SHA256", "ECDSA_SECP256K1_SHA256", "ECDSA_SECP256R1_SHA256", "EDDSA_ED25519_SHA512", "SPHINCS-256_SHA512", "COMPOSITE") assertTrue { Sets.symmetricDifference(expectedAlgSet, algList.toSet()).isEmpty(); } } diff --git a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt index 2452fc8466..6b69c32e13 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt @@ -9,9 +9,9 @@ import net.corda.core.identity.Party import net.corda.core.serialization.p2PKryo import net.corda.core.serialization.serialize import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_PUBKEY_1 -import net.corda.core.utilities.TEST_TX_TIME +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_PUBKEY_1 +import net.corda.testing.TEST_TX_TIME import net.corda.testing.* import org.junit.Test import java.security.PublicKey diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 1396799b8c..8bccd7666c 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -2,9 +2,9 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Command -import net.corda.core.contracts.DummyContract import net.corda.core.contracts.TransactionType import net.corda.core.contracts.requireThat +import net.corda.testing.contracts.DummyContract import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction @@ -14,6 +14,7 @@ import net.corda.flows.FinalityFlow import net.corda.flows.SignTransactionFlow import net.corda.testing.MINI_CORP_KEY import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockServices import org.junit.After import org.junit.Before import org.junit.Test @@ -26,6 +27,7 @@ class CollectSignaturesFlowTests { lateinit var b: MockNetwork.MockNode lateinit var c: MockNetwork.MockNode lateinit var notary: Party + val services = MockServices() @Before fun setup() { @@ -162,7 +164,8 @@ class CollectSignaturesFlowTests { @Test fun `fails when not signed by initiator`() { val onePartyDummyContract = DummyContract.generateInitial(1337, notary, a.info.legalIdentity.ref(1)) - val ptx = onePartyDummyContract.signWith(MINI_CORP_KEY).toSignedTransaction(false) + val miniCorpServices = MockServices(MINI_CORP_KEY) + val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract) val flow = a.services.startFlow(CollectSignaturesFlow(ptx)) mockNet.runNetwork() assertFailsWith("The Initiator of CollectSignaturesFlow must have signed the transaction.") { diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index d0335f7c44..1ee8601e34 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -3,6 +3,8 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.core.contracts.* +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyContractV2 import net.corda.core.crypto.SecureHash import net.corda.core.getOrThrow import net.corda.core.identity.AbstractParty @@ -10,7 +12,7 @@ import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.node.services.unconsumedStates -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.Emoji import net.corda.flows.CashIssueFlow @@ -64,8 +66,8 @@ class ContractUpgradeFlowTest { a.services.startFlow(FinalityFlow(stx, setOf(a.info.legalIdentity, b.info.legalIdentity))) mockNet.runNetwork() - val atx = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(stx.id) } - val btx = b.database.transaction { b.services.storageService.validatedTransactions.getTransaction(stx.id) } + val atx = a.database.transaction { a.services.validatedTransactions.getTransaction(stx.id) } + val btx = b.database.transaction { b.services.validatedTransactions.getTransaction(stx.id) } requireNotNull(atx) requireNotNull(btx) @@ -85,13 +87,13 @@ class ContractUpgradeFlowTest { fun check(node: MockNetwork.MockNode) { val nodeStx = node.database.transaction { - node.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) + node.services.validatedTransactions.getTransaction(result.ref.txhash) } requireNotNull(nodeStx) // Verify inputs. val input = node.database.transaction { - node.services.storageService.validatedTransactions.getTransaction(nodeStx!!.tx.inputs.single().txhash) + node.services.validatedTransactions.getTransaction(nodeStx!!.tx.inputs.single().txhash) } requireNotNull(input) assertTrue(input!!.tx.outputs.single().data is DummyContract.State) @@ -132,8 +134,8 @@ class ContractUpgradeFlowTest { mockNet.runNetwork() handle.returnValue.getOrThrow() - val atx = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(stx.id) } - val btx = b.database.transaction { b.services.storageService.validatedTransactions.getTransaction(stx.id) } + val atx = a.database.transaction { a.services.validatedTransactions.getTransaction(stx.id) } + val btx = b.database.transaction { b.services.validatedTransactions.getTransaction(stx.id) } requireNotNull(atx) requireNotNull(btx) @@ -156,11 +158,11 @@ class ContractUpgradeFlowTest { val result = resultFuture.getOrThrow() // Check results. listOf(a, b).forEach { - val signedTX = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) } + val signedTX = a.database.transaction { a.services.validatedTransactions.getTransaction(result.ref.txhash) } requireNotNull(signedTX) // Verify inputs. - val input = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(signedTX!!.tx.inputs.single().txhash) } + val input = a.database.transaction { a.services.validatedTransactions.getTransaction(signedTX!!.tx.inputs.single().txhash) } requireNotNull(input) assertTrue(input!!.tx.outputs.single().data is DummyContract.State) @@ -173,17 +175,19 @@ class ContractUpgradeFlowTest { @Test fun `upgrade Cash to v2`() { // Create some cash. - val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary)).resultFuture + val anonymous = false + val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary, anonymous)).resultFuture mockNet.runNetwork() - val stateAndRef = result.getOrThrow().tx.outRef(0) - val baseState = a.database.transaction { a.vault.unconsumedStates().single() } + val stx = result.getOrThrow().stx + val stateAndRef = stx.tx.outRef(0) + val baseState = a.database.transaction { a.services.vaultService.unconsumedStates().single() } assertTrue(baseState.state.data is Cash.State, "Contract state is old version.") // Starts contract upgrade flow. val upgradeResult = a.services.startFlow(ContractUpgradeFlow(stateAndRef, CashV2::class.java)).resultFuture mockNet.runNetwork() upgradeResult.getOrThrow() // Get contract state from the vault. - val firstState = a.database.transaction { a.vault.unconsumedStates().single() } + val firstState = a.database.transaction { a.services.vaultService.unconsumedStates().single() } assertTrue(firstState.state.data is CashV2.State, "Contract state is upgraded to the new version.") assertEquals(Amount(1000000, USD).`issued by`(a.info.legalIdentity.ref(1)), (firstState.state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.") assertEquals>(listOf(a.info.legalIdentity), (firstState.state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.") diff --git a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt index 887e1649dc..341f9b0d5d 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt @@ -1,19 +1,19 @@ package net.corda.core.flows -import net.corda.core.contracts.DummyContract -import net.corda.core.crypto.NullSignature +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.SecureHash import net.corda.core.getOrThrow import net.corda.core.identity.Party -import net.corda.core.serialization.opaque +import net.corda.core.utilities.opaque import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.testing.DUMMY_NOTARY_KEY import net.corda.flows.ResolveTransactionsFlow import net.corda.node.utilities.transaction import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_KEY import net.corda.testing.MINI_CORP import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockServices import org.junit.After import org.junit.Before import org.junit.Test @@ -32,6 +32,8 @@ class ResolveTransactionsFlowTest { lateinit var a: MockNetwork.MockNode lateinit var b: MockNetwork.MockNode lateinit var notary: Party + val megaCorpServices = MockServices(MEGA_CORP_KEY) + val notaryServices = MockServices(DUMMY_NOTARY_KEY) @Before fun setup() { @@ -58,8 +60,8 @@ class ResolveTransactionsFlowTest { val results = future.getOrThrow() assertEquals(listOf(stx1.id, stx2.id), results.map { it.id }) b.database.transaction { - assertEquals(stx1, b.storage.validatedTransactions.getTransaction(stx1.id)) - assertEquals(stx2, b.storage.validatedTransactions.getTransaction(stx2.id)) + assertEquals(stx1, b.services.validatedTransactions.getTransaction(stx1.id)) + assertEquals(stx2, b.services.validatedTransactions.getTransaction(stx2.id)) } } // DOCEND 1 @@ -81,9 +83,9 @@ class ResolveTransactionsFlowTest { mockNet.runNetwork() future.getOrThrow() b.database.transaction { - assertEquals(stx1, b.storage.validatedTransactions.getTransaction(stx1.id)) + assertEquals(stx1, b.services.validatedTransactions.getTransaction(stx1.id)) // But stx2 wasn't inserted, just stx1. - assertNull(b.storage.validatedTransactions.getTransaction(stx2.id)) + assertNull(b.services.validatedTransactions.getTransaction(stx2.id)) } } @@ -94,9 +96,8 @@ class ResolveTransactionsFlowTest { val count = 50 var cursor = stx2 repeat(count) { - val stx = DummyContract.move(cursor.tx.outRef(0), MINI_CORP) - .addSignatureUnchecked(NullSignature) - .toSignedTransaction(false) + val builder = DummyContract.move(cursor.tx.outRef(0), MINI_CORP) + val stx = megaCorpServices.signInitialTransaction(builder) a.database.transaction { a.services.recordTransactions(stx) } @@ -114,15 +115,13 @@ class ResolveTransactionsFlowTest { val stx1 = makeTransactions().first val stx2 = DummyContract.move(stx1.tx.outRef(0), MINI_CORP).run { - signWith(MEGA_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) - toSignedTransaction() + val ptx = megaCorpServices.signInitialTransaction(this) + notaryServices.addSignature(ptx) } val stx3 = DummyContract.move(listOf(stx1.tx.outRef(0), stx2.tx.outRef(0)), MINI_CORP).run { - signWith(MEGA_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) - toSignedTransaction() + val ptx = megaCorpServices.signInitialTransaction(this) + notaryServices.addSignature(ptx) } a.database.transaction { @@ -148,7 +147,7 @@ class ResolveTransactionsFlowTest { } // TODO: this operation should not require an explicit transaction val id = a.database.transaction { - a.services.storageService.attachments.importAttachment(makeJar()) + a.services.attachments.importAttachment(makeJar()) } val stx2 = makeTransactions(withAttachment = id).second val p = ResolveTransactionsFlow(stx2, a.info.legalIdentity) @@ -158,7 +157,7 @@ class ResolveTransactionsFlowTest { // TODO: this operation should not require an explicit transaction b.database.transaction { - assertNotNull(b.services.storageService.attachments.openAttachment(id)) + assertNotNull(b.services.attachments.openAttachment(id)) } } @@ -168,15 +167,19 @@ class ResolveTransactionsFlowTest { val dummy1: SignedTransaction = DummyContract.generateInitial(0, notary, MEGA_CORP.ref(1)).let { if (withAttachment != null) it.addAttachment(withAttachment) - if (signFirstTX) - it.signWith(MEGA_CORP_KEY) - it.signWith(DUMMY_NOTARY_KEY) - it.toSignedTransaction(false) + when (signFirstTX) { + true -> { + val ptx = megaCorpServices.signInitialTransaction(it) + notaryServices.addSignature(ptx) + } + false -> { + notaryServices.signInitialTransaction(it) + } + } } val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), MINI_CORP).let { - it.signWith(MEGA_CORP_KEY) - it.signWith(DUMMY_NOTARY_KEY) - it.toSignedTransaction() + val ptx = megaCorpServices.signInitialTransaction(it) + notaryServices.addSignature(ptx) } a.database.transaction { a.services.recordTransactions(dummy1, dummy2) diff --git a/core/src/test/kotlin/net/corda/core/identity/PartyTest.kt b/core/src/test/kotlin/net/corda/core/identity/PartyTest.kt index 37fe78f1ba..707e5b159b 100644 --- a/core/src/test/kotlin/net/corda/core/identity/PartyTest.kt +++ b/core/src/test/kotlin/net/corda/core/identity/PartyTest.kt @@ -1,7 +1,7 @@ package net.corda.core.identity import net.corda.core.crypto.entropyToKeyPair -import net.corda.core.utilities.ALICE +import net.corda.testing.ALICE import org.junit.Test import java.math.BigInteger import kotlin.test.assertEquals diff --git a/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt b/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt index 127103ae5f..e2af5fe66b 100644 --- a/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt +++ b/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt @@ -8,10 +8,9 @@ import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.StorageService import net.corda.core.serialization.* import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.MEGA_CORP import net.corda.testing.node.MockAttachmentStorage import org.apache.commons.io.IOUtils @@ -22,7 +21,6 @@ import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.net.URL import java.net.URLClassLoader -import java.security.PublicKey import java.util.jar.JarOutputStream import java.util.zip.ZipEntry import kotlin.test.assertEquals @@ -42,11 +40,9 @@ class AttachmentClassLoaderTests { val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentClassLoaderTests::class.java.getResource("isolated.jar") private fun Kryo.withAttachmentStorage(attachmentStorage: AttachmentStorage, block: () -> T) = run { + context.put(WireTransactionSerializer.attachmentsClassLoaderEnabled, true) val serviceHub = mock() - val storageService = mock() - whenever(serviceHub.storageService).thenReturn(storageService) - whenever(storageService.attachmentsClassLoaderEnabled).thenReturn(true) - whenever(storageService.attachments).thenReturn(attachmentStorage) + whenever(serviceHub.attachments).thenReturn(attachmentStorage) withSerializationContext(SerializeAsTokenContext(serviceHub) {}, block) } } diff --git a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt index 9b15ca200e..a2f2e81716 100644 --- a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt +++ b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt @@ -4,7 +4,7 @@ import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import org.junit.Test import kotlin.test.assertEquals diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 9756ba57fb..f1259a4f73 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -42,9 +42,12 @@ private fun createAttachmentData(content: String) = ByteArrayOutputStream().appl private fun Attachment.extractContent() = ByteArrayOutputStream().apply { extractFile("content", this) }.toString(UTF_8.name()) -private fun MockNetwork.MockNode.attachments() = services.storageService.attachments as NodeAttachmentService -private fun MockNetwork.MockNode.saveAttachment(content: String) = database.transaction { attachments().importAttachment(createAttachmentData(content).inputStream()) } -private fun MockNetwork.MockNode.hackAttachment(attachmentId: SecureHash, content: String) = database.transaction { attachments().updateAttachment(attachmentId, createAttachmentData(content)) } +private fun MockNetwork.MockNode.saveAttachment(content: String) = database.transaction { + attachments.importAttachment(createAttachmentData(content).inputStream()) +} +private fun MockNetwork.MockNode.hackAttachment(attachmentId: SecureHash, content: String) = database.transaction { + attachments.updateAttachment(attachmentId, createAttachmentData(content)) +} /** * @see NodeAttachmentService.importAttachment @@ -69,7 +72,7 @@ class AttachmentSerializationTest { fun setUp() { mockNet = MockNetwork() server = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - client = mockNet.createNode(server.info.address) + client = mockNet.createNode(server.network.myAddress) client.disableDBCloseOnStop() // Otherwise the in-memory database may disappear (taking the checkpoint with it) while we reboot the client. mockNet.runNetwork() } @@ -122,7 +125,7 @@ class AttachmentSerializationTest { private class OpenAttachmentLogic(server: MockNetwork.MockNode, private val attachmentId: SecureHash) : ClientLogic(server) { @Suspendable override fun getAttachmentContent(): String { - val localAttachment = serviceHub.storageService.attachments.openAttachment(attachmentId)!! + val localAttachment = serviceHub.attachments.openAttachment(attachmentId)!! communicate() return localAttachment.extractContent() } @@ -149,11 +152,11 @@ class AttachmentSerializationTest { private fun rebootClientAndGetAttachmentContent(checkAttachmentsOnLoad: Boolean = true): String { client.stop() - client = mockNet.createNode(server.info.address, client.id, object : MockNetwork.Factory { + client = mockNet.createNode(server.network.myAddress, client.id, object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, overrideServices: Map?, entropyRoot: BigInteger): MockNetwork.MockNode { return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { override fun startMessagingService(rpcOps: RPCOps) { - attachments().checkAttachmentsOnLoad = checkAttachmentsOnLoad + attachments.checkAttachmentsOnLoad = checkAttachmentsOnLoad super.startMessagingService(rpcOps) } } @@ -180,7 +183,7 @@ class AttachmentSerializationTest { @Test fun `only the hash of a regular attachment should be saved in checkpoint`() { val attachmentId = client.saveAttachment("genuine") - client.attachments().checkAttachmentsOnLoad = false // Cached by AttachmentImpl. + client.attachments.checkAttachmentsOnLoad = false // Cached by AttachmentImpl. launchFlow(OpenAttachmentLogic(server, attachmentId), 1) client.hackAttachment(attachmentId, "hacked") assertEquals("hacked", rebootClientAndGetAttachmentContent(false)) // Pass in false to allow non-genuine data to be loaded. diff --git a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt index 0852846808..e27932c757 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt @@ -3,10 +3,10 @@ package net.corda.core.serialization import com.esotericsoftware.kryo.Kryo import com.google.common.primitives.Ints import net.corda.core.crypto.* -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.node.services.messaging.Ack +import net.corda.core.utilities.opaque import net.corda.node.services.persistence.NodeAttachmentService +import net.corda.testing.ALICE +import net.corda.testing.BOB import net.corda.testing.BOB_PUBKEY import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -16,7 +16,8 @@ import org.junit.Test import org.slf4j.LoggerFactory import java.io.ByteArrayInputStream import java.io.InputStream -import java.security.cert.* +import java.security.cert.CertPath +import java.security.cert.CertificateFactory import java.time.Instant import java.util.* import kotlin.test.assertEquals @@ -92,11 +93,10 @@ class KryoTests { } @Test - fun `write and read Ack`() { - val tokenizableBefore = Ack - val serializedBytes = tokenizableBefore.serialize(kryo) - val tokenizableAfter = serializedBytes.deserialize(kryo) - assertThat(tokenizableAfter).isSameAs(tokenizableBefore) + fun `write and read Kotlin object singleton`() { + val serialised = TestSingleton.serialize(kryo) + val deserialised = serialised.deserialize(kryo) + assertThat(deserialised).isSameAs(TestSingleton) } @Test @@ -173,4 +173,7 @@ class KryoTests { override fun toString(): String = "Cyclic($value)" } + @CordaSerializable + private object TestSingleton + } diff --git a/core/src/test/kotlin/net/corda/core/serialization/SerializationTokenTest.kt b/core/src/test/kotlin/net/corda/core/serialization/SerializationTokenTest.kt index 450fbf1d18..9b2517c8d5 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/SerializationTokenTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/SerializationTokenTest.kt @@ -5,6 +5,7 @@ import com.esotericsoftware.kryo.KryoException import com.esotericsoftware.kryo.io.Output import com.nhaarman.mockito_kotlin.mock import net.corda.core.node.ServiceHub +import net.corda.core.utilities.OpaqueBytes import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index eed2941019..4e34ebfb07 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -5,14 +5,8 @@ import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.seconds import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.DUMMY_KEY_2 -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY -import net.corda.core.utilities.TEST_TX_TIME -import net.corda.testing.MEGA_CORP -import net.corda.testing.MEGA_CORP_KEY -import net.corda.testing.MINI_CORP -import net.corda.testing.generateStateRef +import net.corda.testing.* +import net.corda.testing.node.MockServices import org.junit.Before import org.junit.Test import java.security.SignatureException @@ -53,7 +47,8 @@ class TransactionSerializationTests { val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, MEGA_CORP), DUMMY_NOTARY) val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), DUMMY_NOTARY) - + val megaCorpServices = MockServices(MEGA_CORP_KEY) + val notaryServices = MockServices(DUMMY_NOTARY_KEY) lateinit var tx: TransactionBuilder @Before @@ -65,53 +60,47 @@ class TransactionSerializationTests { @Test fun signWireTX() { - tx.signWith(DUMMY_NOTARY_KEY) - tx.signWith(MEGA_CORP_KEY) - val signedTX = tx.toSignedTransaction() + val ptx = megaCorpServices.signInitialTransaction(tx) + val stx = notaryServices.addSignature(ptx) // Now check that the signature we just made verifies. - signedTX.verifySignatures() + stx.verifySignatures() // Corrupt the data and ensure the signature catches the problem. - signedTX.id.bytes[5] = signedTX.id.bytes[5].inc() + stx.id.bytes[5] = stx.id.bytes[5].inc() assertFailsWith(SignatureException::class) { - signedTX.verifySignatures() + stx.verifySignatures() } } @Test fun wrongKeys() { - // Can't convert if we don't have signatures for all commands - assertFailsWith(IllegalStateException::class) { - tx.toSignedTransaction() - } - - tx.signWith(MEGA_CORP_KEY) - tx.signWith(DUMMY_NOTARY_KEY) - val signedTX = tx.toSignedTransaction() + val ptx = megaCorpServices.signInitialTransaction(tx) + val stx = notaryServices.addSignature(ptx) // Cannot construct with an empty sigs list. assertFailsWith(IllegalArgumentException::class) { - signedTX.copy(sigs = emptyList()) + stx.copy(sigs = emptyList()) } // If the signature was replaced in transit, we don't like it. assertFailsWith(SignatureException::class) { val tx2 = TransactionType.General.Builder(DUMMY_NOTARY).withItems(inputState, outputState, changeState, Command(TestCash.Commands.Move(), DUMMY_KEY_2.public)) - tx2.signWith(DUMMY_NOTARY_KEY) - tx2.signWith(DUMMY_KEY_2) - signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verifySignatures() + val ptx2 = notaryServices.signInitialTransaction(tx2) + val dummyServices = MockServices(DUMMY_KEY_2) + val stx2 = dummyServices.addSignature(ptx2) + + stx.copy(sigs = stx2.sigs).verifySignatures() } } @Test fun timeWindow() { - tx.addTimeWindow(TEST_TX_TIME, 30.seconds) - tx.signWith(MEGA_CORP_KEY) - tx.signWith(DUMMY_NOTARY_KEY) - val stx = tx.toSignedTransaction() + tx.setTimeWindow(TEST_TX_TIME, 30.seconds) + val ptx = megaCorpServices.signInitialTransaction(tx) + val stx = notaryServices.addSignature(ptx) assertEquals(TEST_TX_TIME, stx.tx.timeWindow?.midpoint) } } diff --git a/core/src/test/kotlin/net/corda/core/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt b/core/src/test/kotlin/net/corda/core/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt new file mode 100644 index 0000000000..ca172680cf --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt @@ -0,0 +1,42 @@ +package net.corda.core.serialization.amqp + +import org.junit.Test +import kotlin.test.* + +class DeserializeAndReturnEnvelopeTests { + + fun testName() = Thread.currentThread().stackTrace[2].methodName + inline fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz" + + @Test + fun oneType() { + data class A(val a: Int, val b: String) + + val a = A(10, "20") + + var factory = SerializerFactory() + fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) + val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) + + assertTrue(obj.obj is A) + assertEquals(1, obj.envelope.schema.types.size) + assertEquals(classTestName("A"), obj.envelope.schema.types.first().name) + } + + @Test + fun twoTypes() { + data class A(val a: Int, val b: String) + data class B(val a: A, val b: Float) + + val b = B(A(10, "20"), 30.0F) + + var factory = SerializerFactory() + fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) + val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) + + assertTrue(obj.obj is B) + assertEquals(2, obj.envelope.schema.types.size) + assertNotEquals(null, obj.envelope.schema.types.find { it.name == classTestName("A") }) + assertNotEquals(null, obj.envelope.schema.types.find { it.name == classTestName("B") }) + } +} diff --git a/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt b/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt index 5896a3c292..54771ac805 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt @@ -1,17 +1,26 @@ package net.corda.core.serialization.amqp +import net.corda.core.contracts.* +import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException +import net.corda.core.identity.AbstractParty import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.EmptyWhitelist +import net.corda.core.serialization.KryoAMQPSerializer +import net.corda.core.CordaRuntimeException import net.corda.nodeapi.RPCException +import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_PUBKEY import org.apache.qpid.proton.codec.DecoderImpl import org.apache.qpid.proton.codec.EncoderImpl import org.junit.Test import java.io.IOException import java.io.NotSerializableException +import java.math.BigDecimal import java.nio.ByteBuffer +import java.time.Instant import java.util.* +import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -58,12 +67,38 @@ class SerializationOutputTests { @Suppress("AddVarianceModifier") data class GenericFoo(val bar: String, val pub: T) + data class ContainsGenericFoo(val contain: GenericFoo) + + data class NestedGenericFoo(val contain: GenericFoo) + + data class ContainsNestedGenericFoo(val contain: NestedGenericFoo) + data class TreeMapWrapper(val tree: TreeMap) data class NavigableMapWrapper(val tree: NavigableMap) data class SortedSetWrapper(val set: SortedSet) + open class InheritedGeneric(val foo: X) + + data class ExtendsGeneric(val bar: Int, val pub: String) : InheritedGeneric(pub) + + interface GenericInterface { + val pub: X + } + + data class ImplementsGenericString(val bar: Int, override val pub: String) : GenericInterface + + data class ImplementsGenericX(val bar: Int, override val pub: Y) : GenericInterface + + abstract class AbstractGenericX : GenericInterface + + data class InheritGenericX(val duke: Double, override val pub: A) : AbstractGenericX() + + data class CapturesGenericX(val foo: GenericInterface) + + object KotlinObject + class Mismatch(fred: Int) { val ginger: Int = fred @@ -85,7 +120,11 @@ class SerializationOutputTests { data class PolymorphicProperty(val foo: FooInterface?) - private fun serdes(obj: Any, factory: SerializerFactory = SerializerFactory(), freshDeserializationFactory: SerializerFactory = SerializerFactory(), expectedEqual: Boolean = true): Any { + private fun serdes(obj: Any, + factory: SerializerFactory = SerializerFactory(), + freshDeserializationFactory: SerializerFactory = SerializerFactory(), + expectedEqual: Boolean = true, + expectDeserializedEqual: Boolean = true): Any { val ser = SerializationOutput(factory) val bytes = ser.serialize(obj) @@ -103,6 +142,7 @@ class SerializationOutputTests { // Check that a vanilla AMQP decoder can deserialize without schema. val result = decoder.readObject() as Envelope assertNotNull(result) + println(result.schema) val des = DeserializationInput(freshDeserializationFactory) val desObj = des.deserialize(bytes) @@ -113,7 +153,7 @@ class SerializationOutputTests { val des2 = DeserializationInput(factory) val desObj2 = des2.deserialize(ser2.serialize(obj)) assertTrue(Objects.deepEquals(obj, desObj2) == expectedEqual) - assertTrue(Objects.deepEquals(desObj, desObj2)) + assertTrue(Objects.deepEquals(desObj, desObj2) == expectDeserializedEqual) // TODO: add some schema assertions to check correctly formed. return desObj2 @@ -155,7 +195,7 @@ class SerializationOutputTests { serdes(obj) } - @Test + @Test(expected = NotSerializableException::class) fun `test top level list array`() { val obj = arrayOf(listOf("Fred", "Ginger"), listOf("Rogers", "Hammerstein")) serdes(obj) @@ -197,12 +237,51 @@ class SerializationOutputTests { serdes(obj) } - @Test(expected = NotSerializableException::class) + @Test fun `test generic foo`() { val obj = GenericFoo("Fred", "Ginger") serdes(obj) } + @Test + fun `test generic foo as property`() { + val obj = ContainsGenericFoo(GenericFoo("Fred", "Ginger")) + serdes(obj) + } + + @Test + fun `test nested generic foo as property`() { + val obj = ContainsNestedGenericFoo(NestedGenericFoo(GenericFoo("Fred", "Ginger"))) + serdes(obj) + } + + // TODO: Generic interfaces / superclasses + + @Test + fun `test extends generic`() { + val obj = ExtendsGeneric(1, "Ginger") + serdes(obj) + } + + @Test + fun `test implements generic`() { + val obj = ImplementsGenericString(1, "Ginger") + serdes(obj) + } + + @Test + fun `test implements generic captured`() { + val obj = CapturesGenericX(ImplementsGenericX(1, "Ginger")) + serdes(obj) + } + + + @Test + fun `test inherits generic captured`() { + val obj = CapturesGenericX(InheritGenericX(1.0, "Ginger")) + serdes(obj) + } + @Test(expected = NotSerializableException::class) fun `test TreeMap`() { val obj = TreeMap() @@ -246,9 +325,9 @@ class SerializationOutputTests { @Test fun `test custom serializers on public key`() { val factory = SerializerFactory() - factory.register(net.corda.core.serialization.amqp.custom.PublicKeySerializer()) + factory.register(net.corda.core.serialization.amqp.custom.PublicKeySerializer) val factory2 = SerializerFactory() - factory2.register(net.corda.core.serialization.amqp.custom.PublicKeySerializer()) + factory2.register(net.corda.core.serialization.amqp.custom.PublicKeySerializer) val obj = MEGA_CORP_PUBKEY serdes(obj, factory, factory2) } @@ -267,8 +346,9 @@ class SerializationOutputTests { val factory2 = SerializerFactory() factory2.register(net.corda.core.serialization.amqp.custom.ThrowableSerializer(factory2)) - val obj = IllegalAccessException("message").fillInStackTrace() - serdes(obj, factory, factory2, false) + val t = IllegalAccessException("message").fillInStackTrace() + val desThrowable = serdes(t, factory, factory2, false) as Throwable + assertSerializedThrowableEquivalent(t, desThrowable) } @Test @@ -286,7 +366,19 @@ class SerializationOutputTests { throw IllegalStateException("Layer 2", t) } } catch(t: Throwable) { - serdes(t, factory, factory2, false) + val desThrowable = serdes(t, factory, factory2, false) as Throwable + assertSerializedThrowableEquivalent(t, desThrowable) + } + } + + fun assertSerializedThrowableEquivalent(t: Throwable, desThrowable: Throwable) { + assertTrue(desThrowable is CordaRuntimeException) // Since we don't handle the other case(s) yet + if (desThrowable is CordaRuntimeException) { + assertEquals("${t.javaClass.name}: ${t.message}", desThrowable.message) + assertTrue(desThrowable is CordaRuntimeException) + assertTrue(Objects.deepEquals(t.stackTrace, desThrowable.stackTrace)) + assertEquals(t.suppressed.size, desThrowable.suppressed.size) + t.suppressed.zip(desThrowable.suppressed).forEach { (before, after) -> assertSerializedThrowableEquivalent(before, after) } } } @@ -307,7 +399,8 @@ class SerializationOutputTests { throw e } } catch(t: Throwable) { - serdes(t, factory, factory2, false) + val desThrowable = serdes(t, factory, factory2, false) as Throwable + assertSerializedThrowableEquivalent(t, desThrowable) } } @@ -347,4 +440,88 @@ class SerializationOutputTests { serdes(obj) } + @Test + fun `test kotlin object`() { + serdes(KotlinObject) + } + + object FooContract : Contract { + override fun verify(tx: TransactionForContract) { + + } + + override val legalContractReference: SecureHash = SecureHash.Companion.sha256("FooContractLegal") + } + + class FooState : ContractState { + override val contract: Contract + get() = FooContract + override val participants: List + get() = emptyList() + } + + @Test + fun `test transaction state`() { + val state = TransactionState(FooState(), MEGA_CORP) + + val factory = SerializerFactory() + KryoAMQPSerializer.registerCustomSerializers(factory) + + val factory2 = SerializerFactory() + KryoAMQPSerializer.registerCustomSerializers(factory2) + + val desState = serdes(state, factory, factory2, expectedEqual = false, expectDeserializedEqual = false) + assertTrue(desState is TransactionState<*>) + assertTrue((desState as TransactionState<*>).data is FooState) + assertTrue(desState.notary == state.notary) + assertTrue(desState.encumbrance == state.encumbrance) + } + + @Test + fun `test currencies serialize`() { + val factory = SerializerFactory() + factory.register(net.corda.core.serialization.amqp.custom.CurrencySerializer) + + val factory2 = SerializerFactory() + factory2.register(net.corda.core.serialization.amqp.custom.CurrencySerializer) + + val obj = Currency.getInstance("USD") + serdes(obj, factory, factory2) + } + + @Test + fun `test big decimals serialize`() { + val factory = SerializerFactory() + factory.register(net.corda.core.serialization.amqp.custom.BigDecimalSerializer) + + val factory2 = SerializerFactory() + factory2.register(net.corda.core.serialization.amqp.custom.BigDecimalSerializer) + + val obj = BigDecimal("100000000000000000000000000000.00") + serdes(obj, factory, factory2) + } + + @Test + fun `test instants serialize`() { + val factory = SerializerFactory() + factory.register(net.corda.core.serialization.amqp.custom.InstantSerializer(factory)) + + val factory2 = SerializerFactory() + factory2.register(net.corda.core.serialization.amqp.custom.InstantSerializer(factory2)) + + val obj = Instant.now() + serdes(obj, factory, factory2) + } + + @Test + fun `test StateRef serialize`() { + val factory = SerializerFactory() + factory.register(net.corda.core.serialization.amqp.custom.InstantSerializer(factory)) + + val factory2 = SerializerFactory() + factory2.register(net.corda.core.serialization.amqp.custom.InstantSerializer(factory2)) + + val obj = StateRef(SecureHash.randomSHA256(), 0) + serdes(obj, factory, factory2) + } } \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt b/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt new file mode 100644 index 0000000000..6dc4f5b12a --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/serialization/carpenter/ClassCarpenterTest.kt @@ -0,0 +1,494 @@ +package net.corda.core.serialization.carpenter + + +import org.junit.Test +import java.lang.reflect.Field +import java.lang.reflect.Method +import kotlin.test.assertEquals +import kotlin.test.assertTrue + + +class ClassCarpenterTest { + interface DummyInterface { + val a: String + val b: Int + } + + val cc = ClassCarpenter() + + // We have to ignore synthetic fields even though ClassCarpenter doesn't create any because the JaCoCo + // coverage framework auto-magically injects one method and one field into every class loaded into the JVM. + val Class<*>.nonSyntheticFields: List get() = declaredFields.filterNot { it.isSynthetic } + val Class<*>.nonSyntheticMethods: List get() = declaredMethods.filterNot { it.isSynthetic } + + @Test + fun empty() { + val clazz = cc.build(ClassCarpenter.ClassSchema("gen.EmptyClass", emptyMap(), null)) + assertEquals(0, clazz.nonSyntheticFields.size) + assertEquals(2, clazz.nonSyntheticMethods.size) // get, toString + assertEquals(0, clazz.declaredConstructors[0].parameterCount) + clazz.newInstance() // just test there's no exception. + } + + @Test + fun prims() { + val clazz = cc.build(ClassCarpenter.ClassSchema( + "gen.Prims", + mapOf( + "anIntField" to Int::class.javaPrimitiveType!!, + "aLongField" to Long::class.javaPrimitiveType!!, + "someCharField" to Char::class.javaPrimitiveType!!, + "aShortField" to Short::class.javaPrimitiveType!!, + "doubleTrouble" to Double::class.javaPrimitiveType!!, + "floatMyBoat" to Float::class.javaPrimitiveType!!, + "byteMe" to Byte::class.javaPrimitiveType!!, + "booleanField" to Boolean::class.javaPrimitiveType!!).mapValues { + ClassCarpenter.NonNullableField (it.value) + })) + assertEquals(8, clazz.nonSyntheticFields.size) + assertEquals(10, clazz.nonSyntheticMethods.size) + assertEquals(8, clazz.declaredConstructors[0].parameterCount) + val i = clazz.constructors[0].newInstance(1, 2L, 'c', 4.toShort(), 1.23, 4.56F, 127.toByte(), true) + assertEquals(1, clazz.getMethod("getAnIntField").invoke(i)) + assertEquals(2L, clazz.getMethod("getALongField").invoke(i)) + assertEquals('c', clazz.getMethod("getSomeCharField").invoke(i)) + assertEquals(4.toShort(), clazz.getMethod("getAShortField").invoke(i)) + assertEquals(1.23, clazz.getMethod("getDoubleTrouble").invoke(i)) + assertEquals(4.56F, clazz.getMethod("getFloatMyBoat").invoke(i)) + assertEquals(127.toByte(), clazz.getMethod("getByteMe").invoke(i)) + assertEquals(true, clazz.getMethod("getBooleanField").invoke(i)) + + val sfa = i as SimpleFieldAccess + assertEquals(1, sfa["anIntField"]) + assertEquals(2L, sfa["aLongField"]) + assertEquals('c', sfa["someCharField"]) + assertEquals(4.toShort(), sfa["aShortField"]) + assertEquals(1.23, sfa["doubleTrouble"]) + assertEquals(4.56F, sfa["floatMyBoat"]) + assertEquals(127.toByte(), sfa["byteMe"]) + assertEquals(true, sfa["booleanField"]) + } + + private fun genPerson(): Pair, Any> { + val clazz = cc.build(ClassCarpenter.ClassSchema("gen.Person", mapOf( + "age" to Int::class.javaPrimitiveType!!, + "name" to String::class.java + ).mapValues { ClassCarpenter.NonNullableField (it.value) } )) + val i = clazz.constructors[0].newInstance(32, "Mike") + return Pair(clazz, i) + } + + @Test + fun objs() { + val (clazz, i) = genPerson() + assertEquals("Mike", clazz.getMethod("getName").invoke(i)) + assertEquals("Mike", (i as SimpleFieldAccess)["name"]) + } + + @Test + fun `generated toString`() { + val (_, i) = genPerson() + assertEquals("Person{age=32, name=Mike}", i.toString()) + } + + @Test(expected = ClassCarpenter.DuplicateNameException::class) + fun duplicates() { + cc.build(ClassCarpenter.ClassSchema("gen.EmptyClass", emptyMap(), null)) + cc.build(ClassCarpenter.ClassSchema("gen.EmptyClass", emptyMap(), null)) + } + + @Test + fun `can refer to each other`() { + val (clazz1, i) = genPerson() + val clazz2 = cc.build(ClassCarpenter.ClassSchema("gen.Referee", mapOf( + "ref" to ClassCarpenter.NonNullableField (clazz1) + ))) + val i2 = clazz2.constructors[0].newInstance(i) + assertEquals(i, (i2 as SimpleFieldAccess)["ref"]) + } + + @Test + fun superclasses() { + val schema1 = ClassCarpenter.ClassSchema( + "gen.A", + mapOf("a" to ClassCarpenter.NonNullableField (String::class.java))) + + val schema2 = ClassCarpenter.ClassSchema( + "gen.B", + mapOf("b" to ClassCarpenter.NonNullableField (String::class.java)), + schema1) + + val clazz = cc.build(schema2) + val i = clazz.constructors[0].newInstance("xa", "xb") as SimpleFieldAccess + assertEquals("xa", i["a"]) + assertEquals("xb", i["b"]) + assertEquals("B{a=xa, b=xb}", i.toString()) + } + + @Test + fun interfaces() { + val schema1 = ClassCarpenter.ClassSchema( + "gen.A", + mapOf("a" to ClassCarpenter.NonNullableField(String::class.java))) + + val schema2 = ClassCarpenter.ClassSchema("gen.B", + mapOf("b" to ClassCarpenter.NonNullableField(Int::class.java)), + schema1, + interfaces = listOf(DummyInterface::class.java)) + val clazz = cc.build(schema2) + val i = clazz.constructors[0].newInstance("xa", 1) as DummyInterface + assertEquals("xa", i.a) + assertEquals(1, i.b) + } + + @Test(expected = ClassCarpenter.InterfaceMismatchException::class) + fun `mismatched interface`() { + val schema1 = ClassCarpenter.ClassSchema( + "gen.A", + mapOf("a" to ClassCarpenter.NonNullableField(String::class.java))) + + val schema2 = ClassCarpenter.ClassSchema( + "gen.B", + mapOf("c" to ClassCarpenter.NonNullableField(Int::class.java)), + schema1, + interfaces = listOf(DummyInterface::class.java)) + + val clazz = cc.build(schema2) + val i = clazz.constructors[0].newInstance("xa", 1) as DummyInterface + assertEquals(1, i.b) + } + + @Test + fun `generate interface`() { + val schema1 = ClassCarpenter.InterfaceSchema( + "gen.Interface", + mapOf("a" to ClassCarpenter.NonNullableField (Int::class.java))) + + val iface = cc.build(schema1) + + assert(iface.isInterface) + assert(iface.constructors.isEmpty()) + assertEquals(iface.declaredMethods.size, 1) + assertEquals(iface.declaredMethods[0].name, "getA") + + val schema2 = ClassCarpenter.ClassSchema( + "gen.Derived", + mapOf("a" to ClassCarpenter.NonNullableField (Int::class.java)), + interfaces = listOf(iface)) + + val clazz = cc.build(schema2) + val testA = 42 + val i = clazz.constructors[0].newInstance(testA) as SimpleFieldAccess + + assertEquals(testA, i["a"]) + } + + @Test + fun `generate multiple interfaces`() { + val iFace1 = ClassCarpenter.InterfaceSchema( + "gen.Interface1", + mapOf( + "a" to ClassCarpenter.NonNullableField(Int::class.java), + "b" to ClassCarpenter.NonNullableField(String::class.java))) + + val iFace2 = ClassCarpenter.InterfaceSchema( + "gen.Interface2", + mapOf( + "c" to ClassCarpenter.NonNullableField(Int::class.java), + "d" to ClassCarpenter.NonNullableField(String::class.java))) + + val class1 = ClassCarpenter.ClassSchema( + "gen.Derived", + mapOf( + "a" to ClassCarpenter.NonNullableField(Int::class.java), + "b" to ClassCarpenter.NonNullableField(String::class.java), + "c" to ClassCarpenter.NonNullableField(Int::class.java), + "d" to ClassCarpenter.NonNullableField(String::class.java)), + interfaces = listOf(cc.build(iFace1), cc.build(iFace2))) + + val clazz = cc.build(class1) + val testA = 42 + val testB = "don't touch me, I'm scared" + val testC = 0xDEAD + val testD = "wibble" + val i = clazz.constructors[0].newInstance(testA, testB, testC, testD) as SimpleFieldAccess + + assertEquals(testA, i["a"]) + assertEquals(testB, i["b"]) + assertEquals(testC, i["c"]) + assertEquals(testD, i["d"]) + } + + @Test + fun `interface implementing interface`() { + val iFace1 = ClassCarpenter.InterfaceSchema( + "gen.Interface1", + mapOf( + "a" to ClassCarpenter.NonNullableField (Int::class.java), + "b" to ClassCarpenter.NonNullableField(String::class.java))) + + val iFace2 = ClassCarpenter.InterfaceSchema( + "gen.Interface2", + mapOf( + "c" to ClassCarpenter.NonNullableField(Int::class.java), + "d" to ClassCarpenter.NonNullableField(String::class.java)), + interfaces = listOf(cc.build(iFace1))) + + val class1 = ClassCarpenter.ClassSchema( + "gen.Derived", + mapOf( + "a" to ClassCarpenter.NonNullableField(Int::class.java), + "b" to ClassCarpenter.NonNullableField(String::class.java), + "c" to ClassCarpenter.NonNullableField(Int::class.java), + "d" to ClassCarpenter.NonNullableField(String::class.java)), + interfaces = listOf(cc.build(iFace2))) + + val clazz = cc.build(class1) + val testA = 99 + val testB = "green is not a creative colour" + val testC = 7 + val testD = "I like jam" + val i = clazz.constructors[0].newInstance(testA, testB, testC, testD) as SimpleFieldAccess + + assertEquals(testA, i["a"]) + assertEquals(testB, i["b"]) + assertEquals(testC, i["c"]) + assertEquals(testD, i["d"]) + } + + @Test(expected = java.lang.IllegalArgumentException::class) + fun `null parameter small int`() { + val className = "iEnjoySwede" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NonNullableField (Int::class.java))) + + val clazz = cc.build(schema) + + val a : Int? = null + clazz.constructors[0].newInstance(a) + } + + @Test(expected = ClassCarpenter.NullablePrimitiveException::class) + fun `nullable parameter small int`() { + val className = "iEnjoySwede" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NullableField (Int::class.java))) + + cc.build(schema) + } + + @Test + fun `nullable parameter integer`() { + val className = "iEnjoyWibble" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NullableField (Integer::class.java))) + + val clazz = cc.build(schema) + val a1 : Int? = null + clazz.constructors[0].newInstance(a1) + + val a2 : Int? = 10 + clazz.constructors[0].newInstance(a2) + } + + @Test + fun `non nullable parameter integer with non null`() { + val className = "iEnjoyWibble" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NonNullableField (Integer::class.java))) + + val clazz = cc.build(schema) + + val a : Int? = 10 + clazz.constructors[0].newInstance(a) + } + + @Test(expected = java.lang.reflect.InvocationTargetException::class) + fun `non nullable parameter integer with null`() { + val className = "iEnjoyWibble" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NonNullableField (Integer::class.java))) + + val clazz = cc.build(schema) + + val a : Int? = null + clazz.constructors[0].newInstance(a) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `int array`() { + val className = "iEnjoyPotato" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NonNullableField(IntArray::class.java))) + + val clazz = cc.build(schema) + + val i = clazz.constructors[0].newInstance(intArrayOf(1, 2, 3)) as SimpleFieldAccess + + val arr = clazz.getMethod("getA").invoke(i) + + assertEquals(1, (arr as IntArray)[0]) + assertEquals(2, arr[1]) + assertEquals(3, arr[2]) + assertEquals("$className{a=[1, 2, 3]}", i.toString()) + } + + @Test(expected = java.lang.reflect.InvocationTargetException::class) + fun `nullable int array throws`() { + val className = "iEnjoySwede" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NonNullableField(IntArray::class.java))) + + val clazz = cc.build(schema) + + val a : IntArray? = null + clazz.constructors[0].newInstance(a) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `integer array`() { + val className = "iEnjoyFlan" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NonNullableField(Array::class.java))) + + val clazz = cc.build(schema) + + val i = clazz.constructors[0].newInstance(arrayOf(1, 2, 3)) as SimpleFieldAccess + + val arr = clazz.getMethod("getA").invoke(i) + + assertEquals(1, (arr as Array)[0]) + assertEquals(2, arr[1]) + assertEquals(3, arr[2]) + assertEquals("$className{a=[1, 2, 3]}", i.toString()) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `int array with ints`() { + val className = "iEnjoyCrumble" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", mapOf( + "a" to Int::class.java, + "b" to IntArray::class.java, + "c" to Int::class.java).mapValues { ClassCarpenter.NonNullableField(it.value) }) + + val clazz = cc.build(schema) + + val i = clazz.constructors[0].newInstance(2, intArrayOf(4, 8), 16) as SimpleFieldAccess + + assertEquals(2, clazz.getMethod("getA").invoke(i)) + assertEquals(4, (clazz.getMethod("getB").invoke(i) as IntArray)[0]) + assertEquals(8, (clazz.getMethod("getB").invoke(i) as IntArray)[1]) + assertEquals(16, clazz.getMethod("getC").invoke(i)) + + assertEquals("$className{a=2, b=[4, 8], c=16}", i.toString()) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `multiple int arrays`() { + val className = "iEnjoyJam" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", mapOf( + "a" to IntArray::class.java, + "b" to Int::class.java, + "c" to IntArray::class.java).mapValues { ClassCarpenter.NonNullableField(it.value) }) + + val clazz = cc.build(schema) + val i = clazz.constructors[0].newInstance(intArrayOf(1, 2), 3, intArrayOf(4, 5, 6)) + + assertEquals(1, (clazz.getMethod("getA").invoke(i) as IntArray)[0]) + assertEquals(2, (clazz.getMethod("getA").invoke(i) as IntArray)[1]) + assertEquals(3, clazz.getMethod("getB").invoke(i)) + assertEquals(4, (clazz.getMethod("getC").invoke(i) as IntArray)[0]) + assertEquals(5, (clazz.getMethod("getC").invoke(i) as IntArray)[1]) + assertEquals(6, (clazz.getMethod("getC").invoke(i) as IntArray)[2]) + + assertEquals("$className{a=[1, 2], b=3, c=[4, 5, 6]}", i.toString()) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `string array`() { + val className = "iEnjoyToast" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NullableField(Array::class.java))) + + val clazz = cc.build(schema) + + val i = clazz.constructors[0].newInstance(arrayOf("toast", "butter", "jam")) + val arr = clazz.getMethod("getA").invoke(i) as Array + + assertEquals("toast", arr[0]) + assertEquals("butter", arr[1]) + assertEquals("jam", arr[2]) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `string arrays`() { + val className = "iEnjoyToast" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf( + "a" to Array::class.java, + "b" to String::class.java, + "c" to Array::class.java).mapValues { ClassCarpenter.NullableField (it.value) }) + + val clazz = cc.build(schema) + + val i = clazz.constructors[0].newInstance( + arrayOf("bread", "spread", "cheese"), + "and on the side", + arrayOf("some pickles", "some fries")) + + + val arr1 = clazz.getMethod("getA").invoke(i) as Array + val arr2 = clazz.getMethod("getC").invoke(i) as Array + + assertEquals("bread", arr1[0]) + assertEquals("spread", arr1[1]) + assertEquals("cheese", arr1[2]) + assertEquals("and on the side", clazz.getMethod("getB").invoke(i)) + assertEquals("some pickles", arr2[0]) + assertEquals("some fries", arr2[1]) + } + + @Test + fun `nullable sets annotations`() { + val className = "iEnjoyJam" + val schema = ClassCarpenter.ClassSchema( + "gen.$className", + mapOf("a" to ClassCarpenter.NullableField(String::class.java), + "b" to ClassCarpenter.NonNullableField(String::class.java))) + + val clazz = cc.build(schema) + + assertEquals (2, clazz.declaredFields.size) + + assertEquals (1, clazz.getDeclaredField("a").annotations.size) + assertEquals (javax.annotation.Nullable::class.java, clazz.getDeclaredField("a").annotations[0].annotationClass.java) + + assertEquals (1, clazz.getDeclaredField("b").annotations.size) + assertEquals (javax.annotation.Nonnull::class.java, clazz.getDeclaredField("b").annotations[0].annotationClass.java) + + assertEquals (1, clazz.getMethod("getA").annotations.size) + assertEquals (javax.annotation.Nullable::class.java, clazz.getMethod("getA").annotations[0].annotationClass.java) + + assertEquals (1, clazz.getMethod("getB").annotations.size) + assertEquals (javax.annotation.Nonnull::class.java, clazz.getMethod("getB").annotations[0].annotationClass.java) + } + +} diff --git a/core/src/test/kotlin/net/corda/core/testing/Generators.kt b/core/src/test/kotlin/net/corda/core/testing/Generators.kt index bf08440e51..591dbb13c1 100644 --- a/core/src/test/kotlin/net/corda/core/testing/Generators.kt +++ b/core/src/test/kotlin/net/corda/core/testing/Generators.kt @@ -9,7 +9,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.testing.getTestX509Name import org.bouncycastle.asn1.x500.X500Name import java.nio.ByteBuffer diff --git a/core/src/test/kotlin/net/corda/core/utilities/NetworkHostAndPortTest.kt b/core/src/test/kotlin/net/corda/core/utilities/NetworkHostAndPortTest.kt new file mode 100644 index 0000000000..925773e2b3 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/utilities/NetworkHostAndPortTest.kt @@ -0,0 +1,60 @@ +package net.corda.core.utilities + +import org.junit.Test +import kotlin.test.assertEquals +import org.assertj.core.api.Assertions.assertThatThrownBy + +class NetworkHostAndPortTest { + /** + * If a host isn't known-good it should go via the parser, which does some validation. + */ + @Test + fun `constructor is not fussy about host`() { + assertEquals("", NetworkHostAndPort("", 1234).host) + assertEquals("x", NetworkHostAndPort("x", 1234).host) + assertEquals("500", NetworkHostAndPort("500", 1234).host) + assertEquals(" yo yo\t", NetworkHostAndPort(" yo yo\t", 1234).host) + assertEquals("[::1]", NetworkHostAndPort("[::1]", 1234).host) // Don't do this. + } + + @Test + fun `constructor requires a valid port`() { + assertEquals(0, NetworkHostAndPort("example.com", 0).port) + assertEquals(65535, NetworkHostAndPort("example.com", 65535).port) + listOf(65536, -1).forEach { + assertThatThrownBy { + NetworkHostAndPort("example.com", it) + }.isInstanceOf(IllegalArgumentException::class.java).hasMessage(NetworkHostAndPort.invalidPortFormat.format(it)) + } + } + + @Test + fun `toString works`() { + assertEquals("example.com:1234", NetworkHostAndPort("example.com", 1234).toString()) + assertEquals("example.com:65535", NetworkHostAndPort("example.com", 65535).toString()) + assertEquals("1.2.3.4:1234", NetworkHostAndPort("1.2.3.4", 1234).toString()) + assertEquals("[::1]:1234", NetworkHostAndPort("::1", 1234).toString()) + // Brackets perhaps not necessary in unabbreviated case, but URI seems to need them for parsing: + assertEquals("[0:0:0:0:0:0:0:1]:1234", NetworkHostAndPort("0:0:0:0:0:0:0:1", 1234).toString()) + assertEquals(":1234", NetworkHostAndPort("", 1234).toString()) // URI won't parse this. + } + + @Test + fun `parseNetworkHostAndPort works`() { + assertEquals(NetworkHostAndPort("example.com", 1234), "example.com:1234".parseNetworkHostAndPort()) + assertEquals(NetworkHostAndPort("example.com", 65535), "example.com:65535".parseNetworkHostAndPort()) + assertEquals(NetworkHostAndPort("1.2.3.4", 1234), "1.2.3.4:1234".parseNetworkHostAndPort()) + assertEquals(NetworkHostAndPort("::1", 1234), "[::1]:1234".parseNetworkHostAndPort()) + assertEquals(NetworkHostAndPort("0:0:0:0:0:0:0:1", 1234), "[0:0:0:0:0:0:0:1]:1234".parseNetworkHostAndPort()) + listOf("0:0:0:0:0:0:0:1:1234", ":1234", "example.com:-1").forEach { + assertThatThrownBy { + it.parseNetworkHostAndPort() + }.isInstanceOf(IllegalArgumentException::class.java).hasMessage(NetworkHostAndPort.unparseableAddressFormat.format(it)) + } + listOf("example.com:", "example.com").forEach { + assertThatThrownBy { + it.parseNetworkHostAndPort() + }.isInstanceOf(IllegalArgumentException::class.java).hasMessage(NetworkHostAndPort.missingPortFormat.format(it)) + } + } +} diff --git a/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt b/core/src/test/kotlin/net/corda/flows/TransactionKeyFlowTests.kt similarity index 55% rename from core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt rename to core/src/test/kotlin/net/corda/flows/TransactionKeyFlowTests.kt index 7490819a48..486e24b3e8 100644 --- a/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt +++ b/core/src/test/kotlin/net/corda/flows/TransactionKeyFlowTests.kt @@ -3,16 +3,19 @@ package net.corda.flows import net.corda.core.getOrThrow import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.node.MockNetwork +import org.junit.After import org.junit.Before import org.junit.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotEquals +import kotlin.test.assertTrue -class TxKeyFlowTests { +class TransactionKeyFlowTests { lateinit var mockNet: MockNetwork @Before @@ -20,6 +23,11 @@ class TxKeyFlowTests { mockNet = MockNetwork(false) } + @After + fun cleanUp() { + mockNet.stopNodes() + } + @Test fun `issue key`() { // We run this in parallel threads to help catch any race conditions that may exist. @@ -27,8 +35,8 @@ class TxKeyFlowTests { // Set up values we'll need val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = mockNet.createPartyNode(notaryNode.info.address, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.info.address, BOB.name) + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val alice: Party = aliceNode.services.myInfo.legalIdentity val bob: Party = bobNode.services.myInfo.legalIdentity aliceNode.services.identityService.registerIdentity(bobNode.info.legalIdentityAndCert) @@ -37,15 +45,25 @@ class TxKeyFlowTests { bobNode.services.identityService.registerIdentity(notaryNode.info.legalIdentityAndCert) // Run the flows - val requesterFlow = aliceNode.services.startFlow(TxKeyFlow.Requester(bob)) + val requesterFlow = aliceNode.services.startFlow(TransactionKeyFlow(bob)) // Get the results - val actual: Map = requesterFlow.resultFuture.getOrThrow() + val actual: Map = requesterFlow.resultFuture.getOrThrow().toMap() assertEquals(2, actual.size) // Verify that the generated anonymous identities do not match the well known identities val aliceAnonymousIdentity = actual[alice] ?: throw IllegalStateException() val bobAnonymousIdentity = actual[bob] ?: throw IllegalStateException() assertNotEquals(alice, aliceAnonymousIdentity.identity) assertNotEquals(bob, bobAnonymousIdentity.identity) + + // Verify that the anonymous identities look sane + assertEquals(alice.name, aliceAnonymousIdentity.certificate.subject) + assertEquals(bob.name, bobAnonymousIdentity.certificate.subject) + + // Verify that the nodes have the right anonymous identities + assertTrue { aliceAnonymousIdentity.identity.owningKey in aliceNode.services.keyManagementService.keys } + assertTrue { bobAnonymousIdentity.identity.owningKey in bobNode.services.keyManagementService.keys } + assertFalse { aliceAnonymousIdentity.identity.owningKey in bobNode.services.keyManagementService.keys } + assertFalse { bobAnonymousIdentity.identity.owningKey in aliceNode.services.keyManagementService.keys } } } diff --git a/docs/source/_templates/layout_for_doc_website.html b/docs/source/_templates/layout_for_doc_website.html index 2ebb4d0cb6..841ca935b3 100644 --- a/docs/source/_templates/layout_for_doc_website.html +++ b/docs/source/_templates/layout_for_doc_website.html @@ -10,6 +10,32 @@ API reference: Kotlin/ Slack
+ +
{% endblock %} {% block footer %} diff --git a/docs/source/api-index.rst b/docs/source/api-index.rst index bdec7b5a9e..8498f42e9b 100644 --- a/docs/source/api-index.rst +++ b/docs/source/api-index.rst @@ -6,7 +6,7 @@ This section describes the APIs that are available for the development of CorDap * :doc:`api-states` * :doc:`api-persistence` * :doc:`api-contracts` -* :doc:`api-vault` +* :doc:`api-vault-query` * :doc:`api-transactions` * :doc:`api-flows` * :doc:`api-core-types` diff --git a/docs/source/api-service-hub.rst b/docs/source/api-service-hub.rst index 739dee94bd..d3cea14024 100644 --- a/docs/source/api-service-hub.rst +++ b/docs/source/api-service-hub.rst @@ -7,10 +7,12 @@ various services the node provides. The services offered by the ``ServiceHub`` a * Provides information on other nodes on the network (e.g. notaries…) * ``ServiceHub.identityService`` * Allows you to resolve anonymous identities to well-known identities if you have the required certificates +* ``ServiceHub.attachments`` + * Gives you access to the node's attachments +* ``ServiceHub.validatedTransactions`` + * Gives you access to the transactions stored in the node * ``ServiceHub.vaultService`` * Stores the node’s current and historic states -* ``ServiceHub.storageService`` - * Stores additional information such as transactions and attachments * ``ServiceHub.keyManagementService`` * Manages signing transactions and generating fresh public keys * ``ServiceHub.myInfo`` diff --git a/docs/source/api-transactions.rst b/docs/source/api-transactions.rst index 9b736b3ce8..a9a62e89e8 100644 --- a/docs/source/api-transactions.rst +++ b/docs/source/api-transactions.rst @@ -43,10 +43,10 @@ Transaction workflow -------------------- There are four states the transaction can occupy: -* ``TransactionBuilder``, a mutable transaction-in-construction +* ``TransactionBuilder``, a builder for a transaction in construction * ``WireTransaction``, an immutable transaction -* ``SignedTransaction``, a ``WireTransaction`` with 1+ associated signatures -* ``LedgerTransaction``, a resolved ``WireTransaction`` that can be checked for contract validity +* ``SignedTransaction``, an immutable transaction with 1+ associated signatures +* ``LedgerTransaction``, a transaction that can be checked for validity Here are the possible transitions between transaction states: @@ -56,25 +56,198 @@ TransactionBuilder ------------------ Creating a builder ^^^^^^^^^^^^^^^^^^ -The first step when building a transaction is to create a ``TransactionBuilder``: +The first step when creating a transaction is to instantiate a ``TransactionBuilder``. We can create a builder for each +transaction type as follows: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 19 + :end-before: DOCEND 19 + :dedent: 12 - // A general transaction builder. - val generalTxBuilder = TransactionType.General.Builder() + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 19 + :end-before: DOCEND 19 + :dedent: 12 - // A notary-change transaction builder. - val notaryChangeTxBuilder = TransactionType.NotaryChange.Builder() +Transaction components +^^^^^^^^^^^^^^^^^^^^^^ +Once we have a ``TransactionBuilder``, we need to gather together the various transaction components the transaction +will include. - .. sourcecode:: java +Input states +~~~~~~~~~~~~ +Input states are added to a transaction as ``StateAndRef`` instances. A ``StateAndRef`` combines: - // A general transaction builder. - final TransactionBuilder generalTxBuilder = new TransactionType.General.Builder(); +* A ``ContractState`` representing the input state itself +* A ``StateRef`` pointing to the input among the outputs of the transaction that created it - // A notary-change transaction builder. - final TransactionBuilder notaryChangeTxBuilder = new TransactionType.NotaryChange.Builder(); +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 21 + :end-before: DOCEND 21 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 21 + :end-before: DOCEND 21 + :dedent: 12 + +A ``StateRef`` uniquely identifies an input state, allowing the notary to mark it as historic. It is made up of: + +* The hash of the transaction that generated the state +* The state's index in the outputs of that transaction + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 20 + :end-before: DOCEND 20 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 20 + :end-before: DOCEND 20 + :dedent: 12 + +The ``StateRef`` create a chain of pointers from the input states back to the transactions that created them. This +allows a node to work backwards and verify the entirety of the transaction chain. + +Output states +~~~~~~~~~~~~~ +Since a transaction's output states do not exist until the transaction is committed, they cannot be referenced as the +outputs of previous transactions. Instead, we create the desired output states as ``ContractState`` instances, and +add them to the transaction: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 22 + :end-before: DOCEND 22 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 22 + :end-before: DOCEND 22 + :dedent: 12 + +In many cases (e.g. when we have a transaction that updates an existing state), we may want to create an output by +copying from the input state: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 23 + :end-before: DOCEND 23 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 23 + :end-before: DOCEND 23 + :dedent: 12 + +Commands +~~~~~~~~ +Commands are added to the transaction as ``Command`` instances. ``Command`` combines: + +* A ``CommandData`` instance representing the type of the command +* A list of the command's required signers + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 24 + :end-before: DOCEND 24 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 24 + :end-before: DOCEND 24 + :dedent: 12 + +Attachments +~~~~~~~~~~~ +Attachments are identified by their hash. The attachment with the corresponding hash must have been uploaded ahead of +time via the node's RPC interface: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 25 + :end-before: DOCEND 25 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 25 + :end-before: DOCEND 25 + :dedent: 12 + +Time-windows +~~~~~~~~~~~~ +Time windows represent the period of time during which the transaction must be notarised. They can have a start and an +end time, or be open at either end: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 26 + :end-before: DOCEND 26 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 26 + :end-before: DOCEND 26 + :dedent: 12 + +We can also define a time window as an ``Instant`` +/- a time tolerance (e.g. 30 seconds): + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 42 + :end-before: DOCEND 42 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 42 + :end-before: DOCEND 42 + :dedent: 12 + +Or as a start-time plus a duration: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 43 + :end-before: DOCEND 43 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 43 + :end-before: DOCEND 43 + :dedent: 12 Adding items ^^^^^^^^^^^^ @@ -95,56 +268,69 @@ The transaction builder is mutable. We add items to it using the ``TransactionBu Passing in objects of any other type will cause an ``IllegalArgumentException`` to be thrown. -You can also add the following items to the transaction: - -* ``TimeWindow`` objects, using ``TransactionBuilder.setTime`` -* ``SecureHash`` objects referencing the hash of an attachment stored on the node, using - ``TransactionBuilder.addAttachment`` - -Input states -~~~~~~~~~~~~ -Input states are added to a transaction as ``StateAndRef`` instances, rather than as ``ContractState`` instances. - -A ``StateAndRef`` combines a ``ContractState`` with a pointer to the transaction that created it. This series of -pointers from the input states back to the transactions that created them is what allows a node to work backwards and -verify the entirety of the transaction chain. It is defined as: +Here's an example usage of ``TransactionBuilder.withItems``: .. container:: codeset - .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 7 - :end-before: DOCEND 7 + :start-after: DOCSTART 27 + :end-before: DOCEND 27 + :dedent: 12 -Where ``StateRef`` is defined as: + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 27 + :end-before: DOCEND 27 + :dedent: 12 + +You can also pass in objects one-by-one. This is the only way to add attachments: .. container:: codeset - .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 8 - :end-before: DOCEND 8 + :start-after: DOCSTART 28 + :end-before: DOCEND 28 + :dedent: 12 -``StateRef.index`` is the state's position in the outputs of the transaction that created it. In this way, a -``StateRef`` allows a notary service to uniquely identify the existing states that a transaction is marking as historic. + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 28 + :end-before: DOCEND 28 + :dedent: 12 -Output states -~~~~~~~~~~~~~ -Since a transaction's output states do not exist until the transaction is committed, they cannot be referenced as the -outputs of previous transactions. Instead, we create the desired output states as ``ContractState`` instances, and -add them to the transaction. - -Commands -~~~~~~~~ -Commands are added to the transaction as ``Command`` instances. ``Command`` combines a ``CommandData`` -instance representing the type of the command with a list of the command's required signers. It is defined as: +To set the transaction builder's time-window, we can either set a time-window directly: .. container:: codeset - .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 9 - :end-before: DOCEND 9 + :start-after: DOCSTART 44 + :end-before: DOCEND 44 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 44 + :end-before: DOCEND 44 + :dedent: 12 + +Or define the time-window as a time plus a duration (e.g. 45 seconds): + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 45 + :end-before: DOCEND 45 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 45 + :end-before: DOCEND 45 + :dedent: 12 Signing the builder ^^^^^^^^^^^^^^^^^^^ @@ -152,32 +338,42 @@ Once the builder is ready, we finalize it by signing it and converting it into a .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 29 + :end-before: DOCEND 29 + :dedent: 12 - // Finalizes the builder by signing it with our primary signing key. - val signedTx1 = serviceHub.signInitialTransaction(unsignedTx) + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 29 + :end-before: DOCEND 29 + :dedent: 12 - // Finalizes the builder by signing it with a different key. - val signedTx2 = serviceHub.signInitialTransaction(unsignedTx, otherKey) +This will sign the transaction with your legal identity key. You can also choose to use another one of your public keys: - // Finalizes the builder by signing it with a set of keys. - val signedTx3 = serviceHub.signInitialTransaction(unsignedTx, otherKeys) +.. container:: codeset - .. sourcecode:: java + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 30 + :end-before: DOCEND 30 + :dedent: 12 - // Finalizes the builder by signing it with our primary signing key. - final SignedTransaction signedTx1 = getServiceHub().signInitialTransaction(unsignedTx); + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 30 + :end-before: DOCEND 30 + :dedent: 12 - // Finalizes the builder by signing it with a different key. - final SignedTransaction signedTx2 = getServiceHub().signInitialTransaction(unsignedTx, otherKey); - - // Finalizes the builder by signing it with a set of keys. - final SignedTransaction signedTx3 = getServiceHub().signInitialTransaction(unsignedTx, otherKeys); +Either way, the outcome of this process is to create a ``SignedTransaction``, which can no longer be modified. SignedTransaction ----------------- -A ``SignedTransaction`` is a combination of an immutable ``WireTransaction`` and a list of signatures over that -transaction: +A ``SignedTransaction`` is a combination of: + +* An immutable ``WireTransaction`` +* A list of signatures over that transaction .. container:: codeset @@ -186,114 +382,210 @@ transaction: :start-after: DOCSTART 1 :end-before: DOCEND 1 +Before adding our signature to the transaction, we'll want to verify both the transaction itself and its signatures. + +Verifying the transaction +^^^^^^^^^^^^^^^^^^^^^^^^^ +To verify a transaction, we need to retrieve any states in the transaction chain that our node doesn't +currently have in its local storage from the proposer(s) of the transaction. This process is handled by a built-in flow +called ``ResolveTransactionsFlow``. See :doc:`api-flows` for more details. + +When verifying a ``SignedTransaction``, we don't verify the ``SignedTransaction`` *per se*, but rather the +``WireTransaction`` it contains. We extract this ``WireTransaction`` as follows: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 31 + :end-before: DOCEND 31 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 31 + :end-before: DOCEND 31 + :dedent: 12 + +However, this still isn't enough. The ``WireTransaction`` holds its inputs as ``StateRef`` instances, and its +attachments as hashes. These do not provide enough information to properly validate the transaction's contents. To +resolve these into actual ``ContractState`` and ``Attachment`` instances, we need to use the ``ServiceHub`` to convert +the ``WireTransaction`` into a ``LedgerTransaction``: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 32 + :end-before: DOCEND 32 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 32 + :end-before: DOCEND 32 + :dedent: 12 + +We can now *verify* the transaction to ensure that it satisfies the contracts of all the transaction's input and output +states: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 33 + :end-before: DOCEND 33 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 33 + :end-before: DOCEND 33 + :dedent: 12 + +We will generally also want to conduct some additional validation of the transaction, beyond what is provided for in +the contract. Here's an example of how we might do this: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 34 + :end-before: DOCEND 34 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 34 + :end-before: DOCEND 34 + :dedent: 12 + Verifying the signatures ^^^^^^^^^^^^^^^^^^^^^^^^ -The signatures on a ``SignedTransaction`` have not necessarily been checked for validity. We check them using +We also need to verify the signatures over the transaction to prevent tampering. We do this using ``SignedTransaction.verifySignatures``: .. container:: codeset - .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt :language: kotlin - :start-after: DOCSTART 2 - :end-before: DOCEND 2 + :start-after: DOCSTART 35 + :end-before: DOCEND 35 + :dedent: 12 -``verifySignatures`` takes a ``vararg`` of the public keys for which the signatures are allowed to be missing. If the -transaction is missing any signatures without the corresponding public keys being passed in, a + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 35 + :end-before: DOCEND 35 + :dedent: 12 + +Optionally, we can pass ``verifySignatures`` a ``vararg`` of the public keys for which the signatures are allowed +to be missing: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 36 + :end-before: DOCEND 36 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 36 + :end-before: DOCEND 36 + :dedent: 12 + +If the transaction is missing any signatures without the corresponding public keys being passed in, a ``SignaturesMissingException`` is thrown. -Verifying the transaction -^^^^^^^^^^^^^^^^^^^^^^^^^ -Verifying a transaction is a multi-step process: - -* We check the transaction's signatures: +We can also choose to simply verify the signatures that are present: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 37 + :end-before: DOCEND 37 + :dedent: 12 - subFlow(ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain)) + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 37 + :end-before: DOCEND 37 + :dedent: 12 - .. sourcecode:: java - - subFlow(new ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain)); - -* Before verifying the transaction, we need to retrieve from the proposer(s) of the transaction any parts of the - transaction chain that our node doesn't currently have in its local storage: - -.. container:: codeset - - .. sourcecode:: kotlin - - subFlow(ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain)) - - .. sourcecode:: java - - subFlow(new ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain)); - -* To verify the transaction, we first need to resolve any state references and attachment hashes by converting the - ``SignedTransaction`` into a ``LedgerTransaction``. We can then verify the fully-resolved transaction: - -.. container:: codeset - - .. sourcecode:: kotlin - - partSignedTx.tx.toLedgerTransaction(serviceHub).verify() - - .. sourcecode:: java - - partSignedTx.getTx().toLedgerTransaction(getServiceHub()).verify(); - -* We will generally also want to conduct some custom validation of the transaction, beyond what is provided for in the - contract: - -.. container:: codeset - - .. sourcecode:: kotlin - - val ledgerTransaction = partSignedTx.tx.toLedgerTransaction(serviceHub) - val inputStateAndRef = ledgerTransaction.inputs.single() - val input = inputStateAndRef.state.data as MyState - if (input.value > 1000000) { - throw FlowException("Proposed input value too high!") - } - - .. sourcecode:: java - - final LedgerTransaction ledgerTransaction = partSignedTx.getTx().toLedgerTransaction(getServiceHub()); - final StateAndRef inputStateAndRef = ledgerTransaction.getInputs().get(0); - final MyState input = (MyState) inputStateAndRef.getState().getData(); - if (input.getValue() > 1000000) { - throw new FlowException("Proposed input value too high!"); - } +However, BE VERY CAREFUL - this function provides no guarantees that the signatures are correct, or that none are +missing. Signing the transaction ^^^^^^^^^^^^^^^^^^^^^^^ -We add an additional signature to an existing ``SignedTransaction`` using: +Once we are satisfied with the contents and existing signatures over the transaction, we can add our signature to the +``SignedTransaction`` using: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 38 + :end-before: DOCEND 38 + :dedent: 12 - val fullySignedTx = serviceHub.addSignature(partSignedTx) + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 38 + :end-before: DOCEND 38 + :dedent: 12 - .. sourcecode:: java +As with the ``TransactionBuilder``, we can also choose to sign using another one of our public keys: - SignedTransaction fullySignedTx = getServiceHub().addSignature(partSignedTx); +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 39 + :end-before: DOCEND 39 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 39 + :end-before: DOCEND 39 + :dedent: 12 We can also generate a signature over the transaction without adding it to the transaction directly by using: .. container:: codeset - .. sourcecode:: kotlin + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 40 + :end-before: DOCEND 40 + :dedent: 12 - val signature = serviceHub.createSignature(partSignedTx) + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 40 + :end-before: DOCEND 40 + :dedent: 12 - .. sourcecode:: java +Or using another one of our public keys, as follows: - DigitalSignature.WithKey signature = getServiceHub().createSignature(partSignedTx); +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 41 + :end-before: DOCEND 41 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 41 + :end-before: DOCEND 41 + :dedent: 12 Notarising and recording ^^^^^^^^^^^^^^^^^^^^^^^^ Notarising and recording a transaction is handled by a built-in flow called ``FinalityFlow``. See -:doc:`api-flows` for more details. \ No newline at end of file +:doc:`api-flows` for more details. diff --git a/docs/source/api-vault.rst b/docs/source/api-vault-query.rst similarity index 67% rename from docs/source/api-vault.rst rename to docs/source/api-vault-query.rst index 44706e1c15..84683b9763 100644 --- a/docs/source/api-vault.rst +++ b/docs/source/api-vault-query.rst @@ -1,5 +1,5 @@ -API: Vault -========== +API: Vault Query +================ Corda has been architected from the ground up to encourage usage of industry standard, proven query frameworks and libraries for accessing RDBMS backed transactional stores (including the Vault). @@ -48,29 +48,29 @@ The API provides both static (snapshot) and dynamic (snapshot with streaming upd .. note:: Streaming updates are only filtered based on contract type and state status (UNCONSUMED, CONSUMED, ALL) Simple pagination (page number and size) and sorting (directional ordering using standard or custom property attributes) is also specifiable. -Defaults are defined for Paging (pageNumber = 0, pageSize = 200) and Sorting (direction = ASC). +Defaults are defined for Paging (pageNumber = 1, pageSize = 200) and Sorting (direction = ASC). -The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of operators to include: binary logical (AND, OR), comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL), equality (EQUAL, NOT_EQUAL), likeness (LIKE, NOT_LIKE), nullability (IS_NULL, NOT_NULL), and collection based (IN, NOT_IN). +The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of operators to include: binary logical (AND, OR), comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL), equality (EQUAL, NOT_EQUAL), likeness (LIKE, NOT_LIKE), nullability (IS_NULL, NOT_NULL), and collection based (IN, NOT_IN). Standard SQL-92 aggregate functions (SUM, AVG, MIN, MAX, COUNT) are also supported. There are four implementations of this interface which can be chained together to define advanced filters. - 1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED). +1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED). - .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, includeSoftlockedStates = true). + .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, includeSoftlockedStates = true). - 2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s). +2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s). - .. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common state attributes to the **vault_fungible_states** table. + .. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common state attributes to the **vault_fungible_states** table. - 3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo). Filterable attributes include: participant(s), linearId(s), dealRef(s). +3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo). Filterable attributes include: participant(s), linearId(s), dealRef(s). - .. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those interfaces common state attributes to the **vault_linear_states** table. + .. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those interfaces common state attributes to the **vault_linear_states** table. - 4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the api-persistence_ documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression. The ColumnPredicateExpression allows for specification arbitrary criteria using the previously enumerated operator types. Furthermore, a rich DSL is provided to enable simple construction of custom criteria using any combination of ``ColumnPredicate``. +4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the :doc:`Persistence ` documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression, AggregateFunctionExpression. The ``ColumnPredicateExpression`` allows for specification arbitrary criteria using the previously enumerated operator types. The ``AggregateFunctionExpression`` allows for the specification of an aggregate function type (sum, avg, max, min, count) with optional grouping and sorting. Furthermore, a rich DSL is provided to enable simple construction of custom criteria using any combination of ``ColumnPredicate``. See the ``Builder`` object in ``QueryCriteriaUtils`` for a complete specification of the DSL. .. note:: It is a requirement to register any custom contract schemas to be used in Vault Custom queries in the associated `CordaPluginRegistry` configuration for the respective CorDapp using the ``requiredSchemas`` configuration field (which specifies a set of `MappedSchema`) - An example is illustrated here: +An example of a custom query is illustrated here: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt :language: kotlin @@ -78,6 +78,8 @@ There are four implementations of this interface which can be chained together t :end-before: DOCEND VaultQueryExample20 All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators, as also illustrated above. + +All ``QueryCriteria`` implementations provide an explicitly specifiable ``StateStatus`` attribute which defaults to filtering on UNCONSUMED states. .. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types ``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root ``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See ``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples. @@ -85,10 +87,9 @@ Examples of these ``QueryCriteria`` objects are presented below for Kotlin and J .. note:: When specifying the Contract Type as a parameterised type to the QueryCriteria in Kotlin, queries now include all concrete implementations of that type if this is an interface. Previously, it was only possible to query on Concrete types (or the universe of all Contract States). -The Vault Query API leverages the rich semantics of the underlying JPA Hibernate_ based Persistence_ framework adopted by Corda. +The Vault Query API leverages the rich semantics of the underlying JPA Hibernate_ based :doc:`Persistence ` framework adopted by Corda. .. _Hibernate: https://docs.jboss.org/hibernate/jpa/2.1/api/ -.. _Persistence: https://docs.corda.net/api-persistence.html .. note:: Permissioning at the database level will be enforced at a later date to ensure authenticated, role-based, read-only access to underlying Corda tables. @@ -103,6 +104,15 @@ An example of a custom query in Java is illustrated here: .. note:: Current queries by ``Party`` specify the ``AbstractParty`` which may be concrete or anonymous. In the later case, where an anonymous party does not have an associated X500Name, then no query results will ever be produced. For performance reasons, queries do not use PublicKey as search criteria. Ongoing design work on identity manangement is likely to enhance identity based queries (including composite key criteria selection). +Pagination +---------- +The API provides support for paging where large numbers of results are expected (by default, a page size is set to 200 results). +Defining a sensible default page size enables efficient checkpointing within flows, and frees the developer from worrying about pagination where +result sets are expected to be constrained to 200 or fewer entries. Where large result sets are expected (such as using the RPC API for reporting and/or UI display), it is strongly recommended to define a ``PageSpecification`` to correctly process results with efficient memory utilistion. A fail-fast mode is in place to alert API users to the need for pagination where a single query returns more than 200 results and no ``PageSpecification`` +has been supplied. + +.. note:: A pages maximum size ``MAX_PAGE_SIZE`` is defined as ``Int.MAX_VALUE`` and should be used with extreme caution as results returned may exceed your JVM's memory footprint. + Example usage ------------- @@ -235,6 +245,37 @@ Query for fungible assets for a specifc issuer party: :start-after: DOCSTART VaultQueryExample14 :end-before: DOCEND VaultQueryExample14 +**Aggregate Function queries using** ``VaultCustomQueryCriteria`` + +.. note:: Query results for aggregate functions are contained in the `otherResults` attribute of a results Page. + +Aggregations on cash using various functions: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample21 + :end-before: DOCEND VaultQueryExample21 + +.. note:: `otherResults` will contain 5 items, one per calculated aggregate function. + +Aggregations on cash grouped by currency for various functions: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample22 + :end-before: DOCEND VaultQueryExample22 + +.. note:: `otherResults` will contain 24 items, one result per calculated aggregate function per currency (the grouping attribute - currency in this case - is returned per aggregate result). + +Sum aggregation on cash grouped by issuer party and currency and sorted by sum: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample23 + :end-before: DOCEND VaultQueryExample23 + +.. note:: `otherResults` will contain 12 items sorted from largest summed cash amount to smallest, one result per calculated aggregate function per issuer party and currency (grouping attributes are returned per aggregate result). + **Dynamic queries** (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an additional ``QueryResults`` return type in the form of an ``Observable``. Refer to `ReactiveX Observable `_ for a detailed understanding and usage of this type. Track unconsumed cash states: @@ -252,7 +293,7 @@ Track unconsumed linear states: :end-before: DOCEND VaultQueryExample16 .. note:: This will return both Deal and Linear states. - + Track unconsumed deal states: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -300,6 +341,29 @@ Query for consumed deal states or linear ids, specify a paging specification and :start-after: DOCSTART VaultJavaQueryExample2 :end-before: DOCEND VaultJavaQueryExample2 +**Aggregate Function queries using** ``VaultCustomQueryCriteria`` + +Aggregations on cash using various functions: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt + :language: kotlin + :start-after: DOCSTART VaultJavaQueryExample21 + :end-before: DOCEND VaultJavaQueryExample21 + +Aggregations on cash grouped by currency for various functions: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt + :language: kotlin + :start-after: DOCSTART VaultJavaQueryExample22 + :end-before: DOCEND VaultJavaQueryExample22 + +Sum aggregation on cash grouped by issuer party and currency and sorted by sum: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt + :language: kotlin + :start-after: DOCSTART VaultJavaQueryExample23 + :end-before: DOCEND VaultJavaQueryExample23 + Track unconsumed cash states: .. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -314,6 +378,17 @@ Track unconsumed deal states or linear states (with snapshot including specifica :start-after: DOCSTART VaultJavaQueryExample4 :end-before: DOCEND VaultJavaQueryExample4 +Behavioural notes +----------------- +1. **TrackBy** updates do not take into account the full criteria specification due to different and more restrictive syntax + in `observables `_ filtering (vs full SQL-92 JDBC filtering as used in snapshot views). + Specifically, dynamic updates are filtered by ``contractType`` and ``stateType`` (UNCONSUMED, CONSUMED, ALL) only. +2. **QueryBy** and **TrackBy snapshot views** using pagination may return different result sets as each paging request is a + separate SQL query on the underlying database, and it is entirely conceivable that state modifications are taking + place in between and/or in parallel to paging requests. + When using pagination, always check the value of the ``totalStatesAvailable`` (from the ``Vault.Page`` result) and + adjust further paging requests appropriately. + Other use case scenarios ------------------------ @@ -355,10 +430,11 @@ This query returned an ``Iterable>`` The query returns a ``Vault.Page`` result containing: - - states as a ``List>`` sized according to the default Page specification of ``DEFAULT_PAGE_NUM`` (0) and ``DEFAULT_PAGE_SIZE`` (200). + - states as a ``List>`` up to a maximum of ``DEFAULT_PAGE_SIZE`` (200) where no ``PageSpecification`` provided, otherwise returns results according to the parameters ``pageNumber`` and ``pageSize`` specified in the supplied ``PageSpecification``. - states metadata as a ``List`` containing Vault State metadata held in the Vault states table. - - the ``PagingSpecification`` used in the query - - a ``total`` number of results available. This value can be used issue subsequent queries with appropriately specified ``PageSpecification`` (according to your paging needs and/or maximum memory capacity for holding large data sets). Note it is your responsibility to manage page numbers and sizes. + - a ``total`` number of results available if ``PageSpecification`` provided (otherwise returns -1). For pagination, this value can be used to issue subsequent queries with appropriately specified ``PageSpecification`` parameters (according to your paging needs and/or maximum memory capacity for holding large data sets). Note it is your responsibility to manage page numbers and sizes. + - status types used in this query: UNCONSUMED, CONSUMED, ALL + - other results as a [List] of any type (eg. aggregate function results with/without group by) 2. ServiceHub usage obtaining linear heads for a given contract state type diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index ec669aea03..7e4eb64739 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,8 +7,33 @@ from the previous milestone release. UNRELEASED ---------- +* Changes in ``NodeInfo``: + + * ``PhysicalLocation`` was renamed to ``WorldMapLocation`` to emphasise that it doesn't need to map to a truly physical + location of the node server. + * Slots for multiple IP addresses and ``legalIdentitiesAndCert``s were introduced. Addresses are no longer of type + ``SingleMessageRecipient``, but of ``NetworkHostAndPort``. + +* ``ServiceHub.storageService`` has been removed. ``attachments`` and ``validatedTransactions`` are now direct members of + ``ServiceHub``. + +* Mock identity constants used in tests, such as ``ALICE``, ``BOB``, ``DUMMY_NOTARY``, have moved to ``net.corda.testing`` + in the ``test-utils`` module. + +* ``DummyContract``, ``DummyContractV2``, ``DummyLinearContract`` and ``DummyState`` have moved to ``net.corda.testing.contracts`` + in the ``test-utils`` modules. + +* In Java, ``QueryCriteriaUtilsKt`` has moved to ``QueryCriteriaUtils``. Also ``and`` and ``or`` are now instance methods + of ``QueryCrtieria``. + +* ``random63BitValue()`` has moved to ``CryptoUtils`` + +* Added additional common Sort attributes (see ``Sort.CommandStateAttribute``) for use in Vault Query criteria + to include STATE_REF, STATE_REF_TXN_ID, STATE_REF_INDEX + Milestone 13 ----------- +------------ + Special thank you to `Frederic Dalibard `_, for his contribution which adds support for more currencies to the DemoBench and Explorer tools. @@ -78,7 +103,6 @@ support for more currencies to the DemoBench and Explorer tools. to specify for individual nodes. * Dependencies changes: - * Upgraded Kotlin to v1.1.2. * Upgraded Dokka to v0.9.14. * Upgraded Gradle Plugins to 0.12.4. * Upgraded Apache ActiveMQ Artemis to v2.1.0. diff --git a/docs/source/contract-upgrade.rst b/docs/source/contract-upgrade.rst index e448ade570..5d7298b36b 100644 --- a/docs/source/contract-upgrade.rst +++ b/docs/source/contract-upgrade.rst @@ -86,7 +86,7 @@ Bank A and Bank B decided to upgrade the contract to ``DummyContractV2`` 1. Developer will create a new contract extending the ``UpgradedContract`` class, and a new state object ``DummyContractV2.State`` referencing the new contract. -.. literalinclude:: /../../core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt +.. literalinclude:: /../../test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 1ae9174ab5..5b5d410c8f 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -128,11 +128,13 @@ path to the node's base directory. :password: The password :permissions: A list of permission strings which RPC methods can use to control access - If this field is absent or an empty list then RPC is effectively locked down. Alternatively, if it contains the string ``ALL`` then the user is permitted to use *any* RPC method. This value is intended for administrator users and for developers. + If this field is absent or an empty list then RPC is effectively locked down. Alternatively, if it contains the string + ``ALL`` then the user is permitted to use *any* RPC method. This value is intended for administrator users and for developers. -:devMode: This flag indicate if the node is running in development mode. On startup, if the keystore ``/certificates/sslkeystore.jks`` +:devMode: This flag sets the node to run in development mode. On startup, if the keystore ``/certificates/sslkeystore.jks`` does not exist, a developer keystore will be used if ``devMode`` is true. The node will exit if ``devMode`` is false - and keystore does not exist. + and the keystore does not exist. ``devMode`` also turns on background checking of flow checkpoints to shake out any + bugs in the checkpointing process. :detectPublicIp: This flag toggles the auto IP detection behaviour, it is enabled by default. On startup the node will attempt to discover its externally visible IP address first by looking for any public addresses on its network diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index af5ff519cd..c51e115f09 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -8,10 +8,10 @@ import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault -import net.corda.core.serialization.OpaqueBytes -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.OpaqueBytes +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow import net.corda.testing.driver.driver @@ -51,6 +51,9 @@ class IntegrationTestingTutorial { val bobClient = bob.rpcClientToNode() val bobProxy = bobClient.start("bobUser", "testPassword2").proxy + + aliceProxy.waitUntilRegisteredWithNetworkMap().getOrThrow() + bobProxy.waitUntilRegisteredWithNetworkMap().getOrThrow() // END 2 // START 3 @@ -67,7 +70,8 @@ class IntegrationTestingTutorial { i.DOLLARS, issueRef, bob.nodeInfo.legalIdentity, - notary.nodeInfo.notaryIdentity + notary.nodeInfo.notaryIdentity, + false // Not anonymised ).returnValue) } }.forEach(Thread::join) // Ensure the stack of futures is populated. @@ -90,7 +94,7 @@ class IntegrationTestingTutorial { // START 5 for (i in 1..10) { - bobProxy.startFlow(::CashPaymentFlow, i.DOLLARS, alice.nodeInfo.legalIdentity).returnValue.getOrThrow() + bobProxy.startFlow(::CashPaymentFlow, i.DOLLARS, alice.nodeInfo.legalIdentity, false).returnValue.getOrThrow() } aliceVaultUpdates.expectEvents { diff --git a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java index b50156b057..cf188f4e2c 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java @@ -7,6 +7,9 @@ import net.corda.contracts.asset.Cash; import net.corda.core.contracts.*; import net.corda.core.contracts.TransactionType.General; import net.corda.core.contracts.TransactionType.NotaryChange; +import net.corda.testing.contracts.DummyContract; +import net.corda.testing.contracts.DummyState; +import net.corda.core.crypto.DigitalSignature; import net.corda.core.crypto.SecureHash; import net.corda.core.flows.*; import net.corda.core.identity.Party; @@ -28,12 +31,14 @@ import net.corda.flows.SignTransactionFlow; import org.bouncycastle.asn1.x500.X500Name; import java.security.PublicKey; +import java.security.SignatureException; +import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.Set; import static net.corda.core.contracts.ContractsDSL.requireThat; -import static net.corda.core.utilities.TestConstants.getDUMMY_PUBKEY_1; +import static net.corda.testing.TestConstants.getDUMMY_PUBKEY_1; // We group our two flows inside a singleton object to indicate that they work // together. @@ -80,6 +85,7 @@ public class FlowCookbookJava { return CollectSignaturesFlow.Companion.tracker(); } }; + private static final Step VERIFYING_SIGS = new Step("Verifying a transaction's signatures."); private static final Step FINALISATION = new Step("Finalising a transaction.") { @Override public ProgressTracker childProgressTracker() { return FinalityFlow.Companion.tracker(); @@ -233,10 +239,16 @@ public class FlowCookbookJava { // When building a transaction, input states are passed in as // ``StateRef`` instances, which pair the hash of the transaction // that generated the state with the state's index in the outputs - // of that transaction. + // of that transaction. In practice, we'd pass the transaction hash + // or the ``StateRef`` as a parameter to the flow, or extract the + // ``StateRef`` from our vault. + // DOCSTART 20 StateRef ourStateRef = new StateRef(SecureHash.sha256("DummyTransactionHash"), 0); + // DOCEND 20 // A ``StateAndRef`` pairs a ``StateRef`` with the state it points to. + // DOCSTART 21 StateAndRef ourStateAndRef = getServiceHub().toStateAndRef(ourStateRef); + // DOCEND 21 /*------------------------------------------ * GATHERING OTHER TRANSACTION COMPONENTS * @@ -244,18 +256,24 @@ public class FlowCookbookJava { progressTracker.setCurrentStep(OTHER_TX_COMPONENTS); // Output states are constructed from scratch. + // DOCSTART 22 DummyState ourOutput = new DummyState(); + // DOCEND 22 // Or as copies of other states with some properties changed. + // DOCSTART 23 DummyState ourOtherOutput = ourOutput.copy(77); + // DOCEND 23 // Commands pair a ``CommandData`` instance with a list of // public keys. To be valid, the transaction requires a signature // matching every public key in all of the transaction's commands. + // DOCSTART 24 CommandData commandData = new DummyContract.Commands.Create(); PublicKey ourPubKey = getServiceHub().getLegalIdentityKey(); PublicKey counterpartyPubKey = counterparty.getOwningKey(); List requiredSigners = ImmutableList.of(ourPubKey, counterpartyPubKey); Command ourCommand = new Command(commandData, requiredSigners); + // DOCEND 24 // ``CommandData`` can either be: // 1. Of type ``TypeOnlyCommandData``, in which case it only @@ -269,12 +287,28 @@ public class FlowCookbookJava { // Attachments are identified by their hash. // The attachment with the corresponding hash must have been // uploaded ahead of time via the node's RPC interface. + // DOCSTART 25 SecureHash ourAttachment = SecureHash.sha256("DummyAttachment"); + // DOCEND 25 - // Time windows can have a start and end time, or be open at either end. + // Time windows represent the period of time during which a + // transaction must be notarised. They can have a start and an end + // time, or be open at either end. + // DOCSTART 26 TimeWindow ourTimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX); TimeWindow ourAfter = TimeWindow.fromOnly(Instant.MIN); TimeWindow ourBefore = TimeWindow.untilOnly(Instant.MAX); + // DOCEND 26 + + // We can also define a time window as an ``Instant`` +/- a time + // tolerance (e.g. 30 seconds): + // DOCSTART 42 + TimeWindow ourTimeWindow2 = TimeWindow.withTolerance(Instant.now(), Duration.ofSeconds(30)); + // DOCEND 42 + // Or as a start-time plus a duration: + // DOCSTART 43 + TimeWindow ourTimeWindow3 = TimeWindow.fromStartAndDuration(Instant.now(), Duration.ofSeconds(30)); + // DOCEND 43 /*------------------------ * TRANSACTION BUILDING * @@ -283,25 +317,40 @@ public class FlowCookbookJava { // There are two types of transaction (notary-change and general), // and therefore two types of transaction builder: + // DOCSTART 19 TransactionBuilder notaryChangeTxBuilder = new TransactionBuilder(NotaryChange.INSTANCE, specificNotary); TransactionBuilder regTxBuilder = new TransactionBuilder(General.INSTANCE, specificNotary); + // DOCEND 19 // We add items to the transaction builder using ``TransactionBuilder.withItems``: + // DOCSTART 27 regTxBuilder.withItems( // Inputs, as ``StateRef``s that reference to the outputs of previous transactions - ourStateRef, + ourStateAndRef, // Outputs, as ``ContractState``s ourOutput, // Commands, as ``Command``s ourCommand ); + // DOCEND 27 // We can also add items using methods for the individual components: + // DOCSTART 28 regTxBuilder.addInputState(ourStateAndRef); regTxBuilder.addOutputState(ourOutput); regTxBuilder.addCommand(ourCommand); regTxBuilder.addAttachment(ourAttachment); - regTxBuilder.addTimeWindow(ourTimeWindow); + // DOCEND 28 + + // There are several ways of setting the transaction's time-window. + // We can set a time-window directly: + // DOCSTART 44 + regTxBuilder.setTimeWindow(ourTimeWindow); + // DOCEND 44 + // Or as a start time plus a duration (e.g. 45 seconds): + // DOCSTART 45 + regTxBuilder.setTimeWindow(Instant.now(), Duration.ofSeconds(45)); + // DOCEND 45 /*----------------------- * TRANSACTION SIGNING * @@ -310,12 +359,40 @@ public class FlowCookbookJava { // We finalise the transaction by signing it, // converting it into a ``SignedTransaction``. + // DOCSTART 29 SignedTransaction onceSignedTx = getServiceHub().signInitialTransaction(regTxBuilder); + // DOCEND 29 + // We can also sign the transaction using a different public key: + // DOCSTART 30 + PublicKey otherKey = getServiceHub().getKeyManagementService().freshKey(); + SignedTransaction onceSignedTx2 = getServiceHub().signInitialTransaction(regTxBuilder, otherKey); + // DOCEND 30 // If instead this was a ``SignedTransaction`` that we'd received // from a counterparty and we needed to sign it, we would add our // signature using: - SignedTransaction twiceSignedTx = getServiceHub().addSignature(onceSignedTx, dummyPubKey); + // DOCSTART 38 + SignedTransaction twiceSignedTx = getServiceHub().addSignature(onceSignedTx); + // DOCEND 38 + // Or, if we wanted to use a different public key: + PublicKey otherKey2 = getServiceHub().getKeyManagementService().freshKey(); + // DOCSTART 39 + SignedTransaction twiceSignedTx2 = getServiceHub().addSignature(onceSignedTx, otherKey2); + // DOCEND 39 + + // We can also generate a signature over the transaction without + // adding it to the transaction itself. We may do this when + // sending just the signature in a flow instead of returning the + // entire transaction with our signature. This way, the receiving + // node does not need to check we haven't changed anything in the + // transaction. + // DOCSTART 40 + DigitalSignature.WithKey sig = getServiceHub().createSignature(onceSignedTx); + // DOCEND 40 + // And again, if we wanted to use a different public key: + // DOCSTART 41 + DigitalSignature.WithKey sig2 = getServiceHub().createSignature(onceSignedTx, otherKey2); + // DOCEND 41 /*---------------------------- * TRANSACTION VERIFICATION * @@ -336,34 +413,41 @@ public class FlowCookbookJava { subFlow(new ResolveTransactionsFlow(ImmutableSet.of(ourStateRef.getTxhash()), counterparty)); // DOCEND 14 - // We verify a transaction using the following one-liner: - twiceSignedTx.getTx().toLedgerTransaction(getServiceHub()).verify(); - - // Let's break that down... - // A ``SignedTransaction`` is a pairing of a ``WireTransaction`` // with signatures over this ``WireTransaction``. We don't verify // a signed transaction per se, but rather the ``WireTransaction`` // it contains. + // DOCSTART 31 WireTransaction wireTx = twiceSignedTx.getTx(); + // DOCEND 31 // Before we can verify the transaction, we need the // ``ServiceHub`` to use our node's local storage to resolve the // transaction's inputs and attachments into actual objects, // rather than just references. We do this by converting the // ``WireTransaction`` into a ``LedgerTransaction``. + // DOCSTART 32 LedgerTransaction ledgerTx = wireTx.toLedgerTransaction(getServiceHub()); + // DOCEND 32 // We can now verify the transaction. + // DOCSTART 33 ledgerTx.verify(); + // DOCEND 33 // We'll often want to perform our own additional verification // too. Just because a transaction is valid based on the contract // rules and requires our signature doesn't mean we have to // sign it! We need to make sure the transaction represents an // agreement we actually want to enter into. + // DOCSTART 34 DummyState outputState = (DummyState) wireTx.getOutputs().get(0).getData(); if (outputState.getMagicNumber() != 777) { + // ``FlowException`` is a special exception type. It will be + // propagated back to any counterparty flows waiting for a + // message from this flow, notifying them that the flow has + // failed. throw new FlowException("We expected a magic number of 777."); } + // DOCEND 34 // Of course, if you are not a required signer on the transaction, // you have no power to decide whether it is valid or not. If it @@ -384,6 +468,37 @@ public class FlowCookbookJava { SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(twiceSignedTx, SIGS_GATHERING.childProgressTracker())); // DOCEND 15 + /*------------------------ + * VERIFYING SIGNATURES * + ------------------------*/ + progressTracker.setCurrentStep(VERIFYING_SIGS); + + try { + + // We can verify that a transaction has all the required + // signatures, and that they're all valid, by running: + // DOCSTART 35 + fullySignedTx.verifySignatures(); + // DOCEND 35 + + // If the transaction is only partially signed, we have to pass in + // a list of the public keys corresponding to the missing + // signatures, explicitly telling the system not to check them. + // DOCSTART 36 + onceSignedTx.verifySignatures(counterpartyPubKey); + // DOCEND 36 + + // We can also choose to only check the signatures that are + // present. BE VERY CAREFUL - this function provides no guarantees + // that the signatures are correct, or that none are missing. + // DOCSTART 37 + twiceSignedTx.checkSignaturesAreValid(); + // DOCEND 37 + + } catch (SignatureException e) { + // Handle this as required. + } + /*------------------------------ * FINALISING THE TRANSACTION * ------------------------------*/ @@ -495,4 +610,4 @@ public class FlowCookbookJava { return null; } } -} \ No newline at end of file +} diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt index dd870c27d0..ee176d7847 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt @@ -9,11 +9,11 @@ import net.corda.core.messaging.startFlow import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.services.ServiceInfo import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.serialization.SerializationCustomization import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.CashExitFlow import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt new file mode 100644 index 0000000000..f5ec3116a4 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt @@ -0,0 +1,89 @@ +package net.corda.notarydemo + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.flows.FlowLogic +import net.corda.core.identity.Party +import net.corda.core.node.PluginServiceHub +import net.corda.core.node.services.CordaService +import net.corda.core.node.services.TimeWindowChecker +import net.corda.core.node.services.TrustedAuthorityNotaryService +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction +import net.corda.core.utilities.unwrap +import net.corda.flows.* +import net.corda.node.services.transactions.PersistentUniquenessProvider +import net.corda.node.services.transactions.ValidatingNotaryService +import java.security.SignatureException + +// START 1 +@CordaService +class MyCustomValidatingNotaryService(override val services: PluginServiceHub) : TrustedAuthorityNotaryService() { + companion object { + val type = ValidatingNotaryService.type.getSubType("mycustom") + } + + override val timeWindowChecker = TimeWindowChecker(services.clock) + override val uniquenessProvider = PersistentUniquenessProvider() + + override fun createServiceFlow(otherParty: Party, platformVersion: Int): FlowLogic { + return MyValidatingNotaryFlow(otherParty, this) + } + + override fun start() {} + override fun stop() {} +} +// END 1 + +// START 2 +class MyValidatingNotaryFlow(otherSide: Party, service: MyCustomValidatingNotaryService) : NotaryFlow.Service(otherSide, service) { + /** + * The received transaction is checked for contract-validity, which requires fully resolving it into a + * [TransactionForVerification], for which the caller also has to to reveal the whole transaction + * dependency chain. + */ + @Suspendable + override fun receiveAndVerifyTx(): TransactionParts { + val stx = receive(otherSide).unwrap { it } + checkSignatures(stx) + val wtx = stx.tx + validateTransaction(wtx) + val ltx = validateTransaction(wtx) + processTransaction(ltx) + + return TransactionParts(wtx.id, wtx.inputs, wtx.timeWindow) + } + + fun processTransaction(ltx: LedgerTransaction) { + // Add custom transaction processing logic here + } + + private fun checkSignatures(stx: SignedTransaction) { + try { + stx.verifySignatures(serviceHub.myInfo.notaryIdentity.owningKey) + } catch(e: SignedTransaction.SignaturesMissingException) { + throw NotaryException(NotaryError.SignaturesMissing(e)) + } + } + + @Suspendable + fun validateTransaction(wtx: WireTransaction): LedgerTransaction { + try { + resolveTransaction(wtx) + val ltx = wtx.toLedgerTransaction(serviceHub) + ltx.verify() + return ltx + } catch (e: Exception) { + throw when (e) { + is TransactionVerificationException -> NotaryException(NotaryError.TransactionInvalid(e.toString())) + is SignatureException -> NotaryException(NotaryError.SignaturesInvalid(e.toString())) + else -> e + } + } + } + + @Suspendable + private fun resolveTransaction(wtx: WireTransaction) = subFlow(ResolveTransactionsFlow(wtx, otherSide)) +} +// END 2 diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt index e906651f32..6414d50f9b 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt @@ -5,6 +5,9 @@ import net.corda.contracts.asset.Cash import net.corda.core.contracts.* import net.corda.core.contracts.TransactionType.General import net.corda.core.contracts.TransactionType.NotaryChange +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyState +import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.flows.* import net.corda.core.identity.Party @@ -16,7 +19,7 @@ import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_PUBKEY_1 +import net.corda.testing.DUMMY_PUBKEY_1 import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker.Step import net.corda.core.utilities.UntrustworthyData @@ -27,6 +30,7 @@ import net.corda.flows.ResolveTransactionsFlow import net.corda.flows.SignTransactionFlow import org.bouncycastle.asn1.x500.X500Name import java.security.PublicKey +import java.time.Duration import java.time.Instant // We group our two flows inside a singleton object to indicate that they work @@ -63,6 +67,7 @@ object FlowCookbook { // subflow's progress steps in our flow's progress tracker. override fun childProgressTracker() = CollectSignaturesFlow.tracker() } + object VERIFYING_SIGS : Step("Verifying a transaction's signatures.") object FINALISATION : Step("Finalising a transaction.") { override fun childProgressTracker() = FinalityFlow.tracker() } @@ -76,6 +81,7 @@ object FlowCookbook { TX_SIGNING, TX_VERIFICATION, SIGS_GATHERING, + VERIFYING_SIGS, FINALISATION ) } @@ -216,10 +222,16 @@ object FlowCookbook { // When building a transaction, input states are passed in as // ``StateRef`` instances, which pair the hash of the transaction // that generated the state with the state's index in the outputs - // of that transaction. + // of that transaction. In practice, we'd pass the transaction hash + // or the ``StateRef`` as a parameter to the flow, or extract the + // ``StateRef`` from our vault. + // DOCSTART 20 val ourStateRef: StateRef = StateRef(SecureHash.sha256("DummyTransactionHash"), 0) + // DOCEND 20 // A ``StateAndRef`` pairs a ``StateRef`` with the state it points to. + // DOCSTART 21 val ourStateAndRef: StateAndRef = serviceHub.toStateAndRef(ourStateRef) + // DOCEND 21 /**----------------------------------------- * GATHERING OTHER TRANSACTION COMPONENTS * @@ -227,18 +239,24 @@ object FlowCookbook { progressTracker.currentStep = OTHER_TX_COMPONENTS // Output states are constructed from scratch. + // DOCSTART 22 val ourOutput: DummyState = DummyState() + // DOCEND 22 // Or as copies of other states with some properties changed. + // DOCSTART 23 val ourOtherOutput: DummyState = ourOutput.copy(magicNumber = 77) + // DOCEND 23 // Commands pair a ``CommandData`` instance with a list of // public keys. To be valid, the transaction requires a signature // matching every public key in all of the transaction's commands. + // DOCSTART 24 val commandData: CommandData = DummyContract.Commands.Create() val ourPubKey: PublicKey = serviceHub.legalIdentityKey val counterpartyPubKey: PublicKey = counterparty.owningKey val requiredSigners: List = listOf(ourPubKey, counterpartyPubKey) val ourCommand: Command = Command(commandData, requiredSigners) + // DOCEND 24 // ``CommandData`` can either be: // 1. Of type ``TypeOnlyCommandData``, in which case it only @@ -252,12 +270,26 @@ object FlowCookbook { // Attachments are identified by their hash. // The attachment with the corresponding hash must have been // uploaded ahead of time via the node's RPC interface. + // DOCSTART 25 val ourAttachment: SecureHash = SecureHash.sha256("DummyAttachment") + // DOCEND 25 // Time windows can have a start and end time, or be open at either end. + // DOCSTART 26 val ourTimeWindow: TimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX) val ourAfter: TimeWindow = TimeWindow.fromOnly(Instant.MIN) val ourBefore: TimeWindow = TimeWindow.untilOnly(Instant.MAX) + // DOCEND 26 + + // We can also define a time window as an ``Instant`` +/- a time + // tolerance (e.g. 30 seconds): + // DOCSTART 42 + val ourTimeWindow2: TimeWindow = TimeWindow.withTolerance(Instant.now(), Duration.ofSeconds(30)) + // DOCEND 42 + // Or as a start-time plus a duration: + // DOCSTART 43 + val ourTimeWindow3: TimeWindow = TimeWindow.fromStartAndDuration(Instant.now(), Duration.ofSeconds(30)) + // DOCEND 43 /**----------------------- * TRANSACTION BUILDING * @@ -266,25 +298,40 @@ object FlowCookbook { // There are two types of transaction (notary-change and general), // and therefore two types of transaction builder: + // DOCSTART 19 val notaryChangeTxBuilder: TransactionBuilder = TransactionBuilder(NotaryChange, specificNotary) val regTxBuilder: TransactionBuilder = TransactionBuilder(General, specificNotary) + // DOCEND 19 // We add items to the transaction builder using ``TransactionBuilder.withItems``: + // DOCSTART 27 regTxBuilder.withItems( - // Inputs, as ``StateRef``s that reference to the outputs of previous transactions - ourStateRef, + // Inputs, as ``StateRef``s that reference the outputs of previous transactions + ourStateAndRef, // Outputs, as ``ContractState``s ourOutput, // Commands, as ``Command``s ourCommand ) + // DOCEND 27 // We can also add items using methods for the individual components: + // DOCSTART 28 regTxBuilder.addInputState(ourStateAndRef) regTxBuilder.addOutputState(ourOutput) regTxBuilder.addCommand(ourCommand) regTxBuilder.addAttachment(ourAttachment) - regTxBuilder.addTimeWindow(ourTimeWindow) + // DOCEND 28 + + // There are several ways of setting the transaction's time-window. + // We can set a time-window directly: + // DOCSTART 44 + regTxBuilder.setTimeWindow(ourTimeWindow) + // DOCEND 44 + // Or as a start time plus a duration (e.g. 45 seconds): + // DOCSTART 45 + regTxBuilder.setTimeWindow(serviceHub.clock.instant(), Duration.ofSeconds(45)) + // DOCEND 45 /**---------------------- * TRANSACTION SIGNING * @@ -293,12 +340,40 @@ object FlowCookbook { // We finalise the transaction by signing it, converting it into a // ``SignedTransaction``. + // DOCSTART 29 val onceSignedTx: SignedTransaction = serviceHub.signInitialTransaction(regTxBuilder) + // DOCEND 29 + // We can also sign the transaction using a different public key: + // DOCSTART 30 + val otherKey: PublicKey = serviceHub.keyManagementService.freshKey() + val onceSignedTx2: SignedTransaction = serviceHub.signInitialTransaction(regTxBuilder, otherKey) + // DOCEND 30 // If instead this was a ``SignedTransaction`` that we'd received // from a counterparty and we needed to sign it, we would add our // signature using: - val twiceSignedTx: SignedTransaction = serviceHub.addSignature(onceSignedTx, dummyPubKey) + // DOCSTART 38 + val twiceSignedTx: SignedTransaction = serviceHub.addSignature(onceSignedTx) + // DOCEND 38 + // Or, if we wanted to use a different public key: + val otherKey2: PublicKey = serviceHub.keyManagementService.freshKey() + // DOCSTART 39 + val twiceSignedTx2: SignedTransaction = serviceHub.addSignature(onceSignedTx, otherKey2) + // DOCEND 39 + + // We can also generate a signature over the transaction without + // adding it to the transaction itself. We may do this when + // sending just the signature in a flow instead of returning the + // entire transaction with our signature. This way, the receiving + // node does not need to check we haven't changed anything in the + // transaction. + // DOCSTART 40 + val sig: DigitalSignature.WithKey = serviceHub.createSignature(onceSignedTx) + // DOCEND 40 + // And again, if we wanted to use a different public key: + // DOCSTART 41 + val sig2: DigitalSignature.WithKey = serviceHub.createSignature(onceSignedTx, otherKey2) + // DOCEND 41 // In practice, however, the process of gathering every signature // but the first can be automated using ``CollectSignaturesFlow``. @@ -323,30 +398,32 @@ object FlowCookbook { subFlow(ResolveTransactionsFlow(setOf(ourStateRef.txhash), counterparty)) // DOCEND 14 - // We verify a transaction using the following one-liner: - twiceSignedTx.tx.toLedgerTransaction(serviceHub).verify() - - // Let's break that down... - // A ``SignedTransaction`` is a pairing of a ``WireTransaction`` // with signatures over this ``WireTransaction``. We don't verify // a signed transaction per se, but rather the ``WireTransaction`` // it contains. + // DOCSTART 31 val wireTx: WireTransaction = twiceSignedTx.tx + // DOCEND 31 // Before we can verify the transaction, we need the // ``ServiceHub`` to use our node's local storage to resolve the // transaction's inputs and attachments into actual objects, // rather than just references. We do this by converting the // ``WireTransaction`` into a ``LedgerTransaction``. + // DOCSTART 32 val ledgerTx: LedgerTransaction = wireTx.toLedgerTransaction(serviceHub) + // DOCEND 32 // We can now verify the transaction. + // DOCSTART 33 ledgerTx.verify() + // DOCEND 33 // We'll often want to perform our own additional verification // too. Just because a transaction is valid based on the contract // rules and requires our signature doesn't mean we have to // sign it! We need to make sure the transaction represents an // agreement we actually want to enter into. + // DOCSTART 34 val outputState: DummyState = wireTx.outputs.single().data as DummyState if (outputState.magicNumber == 777) { // ``FlowException`` is a special exception type. It will be @@ -355,6 +432,7 @@ object FlowCookbook { // failed. throw FlowException("We expected a magic number of 777.") } + // DOCEND 34 // Of course, if you are not a required signer on the transaction, // you have no power to decide whether it is valid or not. If it @@ -375,6 +453,31 @@ object FlowCookbook { val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, SIGS_GATHERING.childProgressTracker())) // DOCEND 15 + /**----------------------- + * VERIFYING SIGNATURES * + -----------------------**/ + progressTracker.currentStep = VERIFYING_SIGS + + // We can verify that a transaction has all the required + // signatures, and that they're all valid, by running: + // DOCSTART 35 + fullySignedTx.verifySignatures() + // DOCEND 35 + + // If the transaction is only partially signed, we have to pass in + // a list of the public keys corresponding to the missing + // signatures, explicitly telling the system not to check them. + // DOCSTART 36 + onceSignedTx.verifySignatures(counterpartyPubKey) + // DOCEND 36 + + // We can also choose to only check the signatures that are + // present. BE VERY CAREFUL - this function provides no guarantees + // that the signatures are correct, or that none are missing. + // DOCSTART 37 + twiceSignedTx.checkSignaturesAreValid() + // DOCEND 37 + /**----------------------------- * FINALISING THE TRANSACTION * -----------------------------**/ @@ -471,4 +574,4 @@ object FlowCookbook { // we be handled automatically. } } -} \ No newline at end of file +} diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt index 39b61b1b09..ce6ea008c6 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt @@ -12,10 +12,11 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party -import net.corda.core.identity.PartyAndCertificate -import net.corda.core.node.PluginServiceHub import net.corda.core.node.ServiceHub +import net.corda.core.node.services.Vault +import net.corda.core.node.services.queryBy import net.corda.core.node.services.unconsumedStates +import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.unwrap @@ -41,14 +42,15 @@ private fun gatherOurInputs(serviceHub: ServiceHub, amountRequired: Amount>, notary: Party?): Pair>, Long> { // Collect cash type inputs - val cashStates = serviceHub.vaultService.unconsumedStates() + val queryCriteria = QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, setOf(Cash.State::class.java)) + val cashStates = serviceHub.vaultQueryService.queryBy(queryCriteria).states // extract our identity for convenience - val ourIdentity = serviceHub.myInfo.legalIdentity + val ourKeys = serviceHub.keyManagementService.keys // Filter down to our own cash states with right currency and issuer val suitableCashStates = cashStates.filter { val state = it.state.data - (state.owner == ourIdentity) - && (state.amount.token == amountRequired.token) + // TODO: We may want to have the list of our states pre-cached somewhere for performance + (state.owner.owningKey in ourKeys) && (state.amount.token == amountRequired.token) } require(!suitableCashStates.isEmpty()) { "Insufficient funds" } var remaining = amountRequired.quantity @@ -134,9 +136,6 @@ class ForeignExchangeFlow(val tradeId: String, require(it.inputs.all { it.state.notary == notary }) { "notary of remote states must be same as for our states" } - require(it.inputs.all { it.state.data.owner == remoteRequestWithNotary.owner }) { - "The inputs are not owned by the correct counterparty" - } require(it.inputs.all { it.state.data.amount.token == remoteRequestWithNotary.amount.token }) { "Inputs not of the correct currency" } @@ -202,7 +201,7 @@ class ForeignExchangeFlow(val tradeId: String, // We have already validated their response and trust our own data // so we can sign. Note the returned SignedTransaction is still not fully signed // and would not pass full verification yet. - return serviceHub.signInitialTransaction(builder) + return serviceHub.signInitialTransaction(builder, ourSigners.single()) } // DOCEND 3 } @@ -236,10 +235,11 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic() { val ourResponse = prepareOurInputsAndOutputs(serviceHub, request) // Send back our proposed states and await the full transaction to verify + val ourKey = serviceHub.keyManagementService.filterMyKeys(ourResponse.inputs.flatMap { it.state.data.participants }.map { it.owningKey }).single() val proposedTrade = sendAndReceive(source, ourResponse).unwrap { val wtx = it.tx // check all signatures are present except our own and the notary - it.verifySignatures(serviceHub.myInfo.legalIdentity.owningKey, wtx.notary!!.owningKey) + it.verifySignatures(ourKey, wtx.notary!!.owningKey) // We need to fetch their complete input states and dependencies so that verify can operate checkDependencies(it) @@ -253,7 +253,7 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic() { } // assuming we have completed state and business level validation we can sign the trade - val ourSignature = serviceHub.createSignature(proposedTrade) + val ourSignature = serviceHub.createSignature(proposedTrade, ourKey) // send the other side our signature. send(source, ourSignature) diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt index 43c7b5b245..7e66e01fd7 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt @@ -123,7 +123,7 @@ class SubmitTradeApprovalFlow(val tradeId: String, // Create the TransactionBuilder and populate with the new state. val tx = TransactionType.General.Builder(notary) .withItems(tradeProposal, Command(TradeApprovalContract.Commands.Issue(), listOf(tradeProposal.source.owningKey))) - tx.addTimeWindow(serviceHub.clock.instant(), Duration.ofSeconds(60)) + tx.setTimeWindow(serviceHub.clock.instant(), Duration.ofSeconds(60)) // We can automatically sign as there is no untrusted data. val signedTx = serviceHub.signInitialTransaction(tx) // Notarise and distribute. @@ -184,7 +184,7 @@ class SubmitCompletionFlow(val ref: StateRef, val verdict: WorkflowState) : Flow newState, Command(TradeApprovalContract.Commands.Completed(), listOf(serviceHub.myInfo.legalIdentity.owningKey, latestRecord.state.data.source.owningKey))) - tx.addTimeWindow(serviceHub.clock.instant(), Duration.ofSeconds(60)) + tx.setTimeWindow(serviceHub.clock.instant(), Duration.ofSeconds(60)) // We can sign this transaction immediately as we have already checked all the fields and the decision // is ultimately a manual one from the caller. // As a SignedTransaction we can pass the data around certain that it cannot be modified, diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index 4210c57ce6..be1b5fa83f 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -3,10 +3,10 @@ package net.corda.docs import net.corda.core.contracts.* import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.toFuture -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY import net.corda.flows.CashIssueFlow import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.ValidatingNotaryService @@ -31,8 +31,8 @@ class FxTransactionBuildTutorialTest { legalName = DUMMY_NOTARY.name, overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY), advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) - nodeA = mockNet.createPartyNode(notaryNode.info.address) - nodeB = mockNet.createPartyNode(notaryNode.info.address) + nodeA = mockNet.createPartyNode(notaryNode.network.myAddress) + nodeB = mockNet.createPartyNode(notaryNode.network.myAddress) nodeB.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java) } @@ -48,7 +48,8 @@ class FxTransactionBuildTutorialTest { val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(DOLLARS(1000), OpaqueBytes.of(0x01), nodeA.info.legalIdentity, - notaryNode.info.notaryIdentity)) + notaryNode.info.notaryIdentity, + false)) // Wait for the flow to stop and print flowHandle1.resultFuture.getOrThrow() printBalances() @@ -57,7 +58,8 @@ class FxTransactionBuildTutorialTest { val flowHandle2 = nodeB.services.startFlow(CashIssueFlow(POUNDS(1000), OpaqueBytes.of(0x01), nodeB.info.legalIdentity, - notaryNode.info.notaryIdentity)) + notaryNode.info.notaryIdentity, + false)) // Wait for flow to come to an end and print flowHandle2.resultFuture.getOrThrow() printBalances() diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt index 17da495c0e..2a819c25cc 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt @@ -9,10 +9,9 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.node.services.vault.and import net.corda.core.toFuture -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.node.utilities.transaction @@ -43,8 +42,8 @@ class WorkflowTransactionBuildTutorialTest { legalName = DUMMY_NOTARY.name, overrideServices = mapOf(Pair(notaryService, DUMMY_NOTARY_KEY)), advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) - nodeA = mockNet.createPartyNode(notaryNode.info.address) - nodeB = mockNet.createPartyNode(notaryNode.info.address) + nodeA = mockNet.createPartyNode(notaryNode.network.myAddress) + nodeB = mockNet.createPartyNode(notaryNode.network.myAddress) nodeA.registerInitiatedFlow(RecordCompletionFlow::class.java) } diff --git a/docs/source/hello-world-contract.rst b/docs/source/hello-world-contract.rst index 3c0ab482f4..de523ddf8b 100644 --- a/docs/source/hello-world-contract.rst +++ b/docs/source/hello-world-contract.rst @@ -18,8 +18,8 @@ It's easy to imagine that most CorDapps will want to impose some constraints on * An asset-trading CorDapp would not want to allow users to finalise a trade without the agreement of their counterparty In Corda, we impose constraints on what transactions are allowed using contracts. These contracts are very different -to the smart contracts of other distributed ledger platforms. They do not represent the current state of the ledger. -Instead, like a real-world contract, they simply impose rules on what kinds of agreements are allowed. +to the smart contracts of other distributed ledger platforms. In Corda, contracts do not represent the current state of +the ledger. Instead, like a real-world contract, they simply impose rules on what kinds of agreements are allowed. Every state is associated with a contract. A transaction is invalid if it does not satisfy the contract of every input and output state in the transaction. @@ -42,11 +42,7 @@ Just as every Corda state must implement the ``ContractState`` interface, every val legalContractReference: SecureHash } -A few more Kotlinisms here: - -* ``fun`` declares a function -* The syntax ``fun funName(arg1Name: arg1Type): returnType`` declares that ``funName`` takes an argument of type - ``arg1Type`` and returns a value of type ``returnType`` +You can read about function declarations in Kotlin `here `_. We can see that ``Contract`` expresses its constraints in two ways: @@ -70,85 +66,121 @@ transfer them or redeem them for cash. One way to enforce this behaviour would b * For the transactions's output IOU state: * Its value must be non-negative - * Its sender and its recipient cannot be the same entity - * All the participants (i.e. both the sender and the recipient) must sign the transaction + * The lender and the borrower cannot be the same entity + * The IOU's borrower must sign the transaction We can picture this transaction as follows: - .. image:: resources/tutorial-transaction.png -:scale: 15% + .. image:: resources/simple-tutorial-transaction.png + :scale: 15% :align: center -Let's write a contract that enforces these constraints. We'll do this by modifying either ``TemplateContract.java`` or -``TemplateContract.kt`` and updating ``TemplateContract`` to define an ``IOUContract``. - Defining IOUContract -------------------- -The Create command -^^^^^^^^^^^^^^^^^^ -The first thing our contract needs is a *command*. Commands serve two purposes: - -* They indicate the transaction's intent, allowing us to perform different verification given the situation - - * For example, a transaction proposing the creation of an IOU could have to satisfy different constraints to one - redeeming an IOU - -* They allow us to define the required signers for the transaction - - * For example, IOU creation might require signatures from both the sender and the recipient, whereas the transfer - of an IOU might only require a signature from the IOUs current holder - -Let's update the definition of ``TemplateContract`` (in ``TemplateContract.java`` or ``TemplateContract.kt``) to -define an ``IOUContract`` with a ``Create`` command: +Let's write a contract that enforces these constraints. We'll do this by modifying either ``TemplateContract.java`` or +``TemplateContract.kt`` and updating ``TemplateContract`` to define an ``IOUContract``: .. container:: codeset .. code-block:: kotlin - package com.template + package com.iou import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash - import net.corda.core.crypto.SecureHash.Companion.sha256 open class IOUContract : Contract { - // Currently, verify() does no checking at all! - override fun verify(tx: TransactionForContract) {} - // Our Create command. class Create : CommandData + override fun verify(tx: TransactionForContract) { + val command = tx.commands.requireSingleCommand() + + requireThat { + // Constraints on the shape of the transaction. + "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) + "There should be one output state of type IOUState." using (tx.outputs.size == 1) + + // IOU-specific constraints. + val out = tx.outputs.single() as IOUState + "The IOU's value must be non-negative." using (out.value > 0) + "The lender and the borrower cannot be the same entity." using (out.lender != out.borrower) + + // Constraints on the signers. + "There must only be one signer." using (command.signers.toSet().size == 1) + "The signer must be the borrower." using (command.signers.contains(out.borrower.owningKey)) + } + } + // The legal contract reference - we'll leave this as a dummy hash for now. - override val legalContractReference = SecureHash.sha256("Prose contract.") + override val legalContractReference = SecureHash.zeroHash } .. code-block:: java - package com.template; + package com.iou; + import com.google.common.collect.ImmutableSet; + import net.corda.core.contracts.AuthenticatedObject; import net.corda.core.contracts.CommandData; import net.corda.core.contracts.Contract; + import net.corda.core.contracts.TransactionForContract; import net.corda.core.crypto.SecureHash; + import net.corda.core.identity.Party; + + import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; + import static net.corda.core.contracts.ContractsDSL.requireThat; public class IOUContract implements Contract { - @Override - // Currently, verify() does no checking at all! - public void verify(TransactionForContract tx) {} - // Our Create command. public static class Create implements CommandData {} + @Override + public void verify(TransactionForContract tx) { + final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); + + requireThat(check -> { + // Constraints on the shape of the transaction. + check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); + check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1); + + // IOU-specific constraints. + final IOUState out = (IOUState) tx.getOutputs().get(0); + final Party lender = out.getLender(); + final Party borrower = out.getBorrower(); + check.using("The IOU's value must be non-negative.",out.getValue() > 0); + check.using("The lender and the borrower cannot be the same entity.", lender != borrower); + + // Constraints on the signers. + check.using("There must only be one signer.", ImmutableSet.of(command.getSigners()).size() == 1); + check.using("The signer must be the borrower.", command.getSigners().contains(borrower.getOwningKey())); + + return null; + }); + } + // The legal contract reference - we'll leave this as a dummy hash for now. - private final SecureHash legalContractReference = SecureHash.sha256("Prose contract."); + private final SecureHash legalContractReference = SecureHash.Companion.getZeroHash(); @Override public final SecureHash getLegalContractReference() { return legalContractReference; } } -Aside from renaming ``TemplateContract`` to ``IOUContract``, we've also implemented the ``Create`` command. All -commands must implement the ``CommandData`` interface. +Let's walk through this code step by step. + +The Create command +^^^^^^^^^^^^^^^^^^ +The first thing we add to our contract is a *command*. Commands serve two functions: + +* They indicate the transaction's intent, allowing us to perform different verification given the situation. For + example, a transaction proposing the creation of an IOU could have to satisfy different constraints to one redeeming + an IOU +* They allow us to define the required signers for the transaction. For example, IOU creation might require signatures + from the borrower alone, whereas the transfer of an IOU might require signatures from both the IOU's borrower and lender + +Our contract has one command, a ``Create`` command. All commands must implement the ``CommandData`` interface. The ``CommandData`` interface is a simple marker interface for commands. In fact, its declaration is only two words -long (in Kotlin, interfaces do not require a body): +long (Kotlin interfaces do not require a body): .. container:: codeset @@ -158,8 +190,8 @@ long (in Kotlin, interfaces do not require a body): The verify logic ^^^^^^^^^^^^^^^^ -We now need to define the actual contract constraints. For our IOU CorDapp, we won't concern ourselves with writing -valid legal prose to enforce the IOU agreement in court. Instead, we'll focus on implementing ``verify``. +Our contract also needs to define the actual contract constraints. For our IOU CorDapp, we won't concern ourselves with +writing valid legal prose to enforce the IOU agreement in court. Instead, we'll focus on implementing ``verify``. Remember that our goal in writing the ``verify`` function is to write a function that, given a transaction: @@ -183,84 +215,25 @@ following are true: * The transaction has inputs * The transaction doesn't have exactly one output * The IOU itself is invalid -* The transaction doesn't require signatures from both the sender and the recipient - -Let's work through these constraints one-by-one. +* The transaction doesn't require the borrower's signature Command constraints ~~~~~~~~~~~~~~~~~~~ -To test for the presence of the ``Create`` command, we can use Corda's ``requireSingleCommand`` function: +Our first constraint is around the transaction's commands. We use Corda's ``requireSingleCommand`` function to test for +the presence of a single ``Create`` command. Here, ``requireSingleCommand`` performing a dual purpose: -.. container:: codeset - - .. code-block:: kotlin - - override fun verify(tx: TransactionForContract) { - val command = tx.commands.requireSingleCommand() - } - - .. code-block:: java - - // Additional imports. - import net.corda.core.contracts.AuthenticatedObject; - import net.corda.core.contracts.TransactionForContract; - import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; - - ... - - @Override - public void verify(TransactionForContract tx) { - final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); - } - -Here, ``requireSingleCommand`` performing a dual purpose: - -* It's asserting that there is exactly one ``Create`` command in the transaction -* It's extracting the command and returning it +* Asserting that there is exactly one ``Create`` command in the transaction +* Extracting the command and returning it If the ``Create`` command isn't present, or if the transaction has multiple ``Create`` commands, contract verification will fail. Transaction constraints ~~~~~~~~~~~~~~~~~~~~~~~ -We also wanted our transaction to have no inputs and only a single output. One way to impose this constraint is as -follows: +We also want our transaction to have no inputs and only a single output - an issuance transaction. -.. container:: codeset - - .. code-block:: kotlin - - override fun verify(tx: TransactionForContract) { - val command = tx.commands.requireSingleCommand() - - requireThat { - // Constraints on the shape of the transaction. - "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) - "Only one output state should be created." using (tx.outputs.size == 1) - } - } - - .. code-block:: java - - // Additional import. - import static net.corda.core.contracts.ContractsDSL.requireThat; - - ... - - @Override - public void verify(TransactionForContract tx) { - final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); - - requireThat(check -> { - // Constraints on the shape of the transaction. - check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); - check.using("Only one output state should be created.", tx.getOutputs().size() == 1); - - return null; - }); - } - -Note the use of Corda's built-in ``requireThat`` function. ``requireThat`` provides a terse way to write the following: +To impose this and the subsequent constraints, we are using Corda's built-in ``requireThat`` function. ``requireThat`` +provides a terse way to write the following: * If the condition on the right-hand side doesn't evaluate to true... * ...throw an ``IllegalArgumentException`` with the message on the left-hand side @@ -272,457 +245,18 @@ IOU constraints We want to impose two constraints on the ``IOUState`` itself: * Its value must be non-negative -* Its sender and its recipient cannot be the same entity +* The lender and the borrower cannot be the same entity -We can impose these constraints in the same ``requireThat`` block as before: - -.. container:: codeset - - .. code-block:: kotlin - - override fun verify(tx: TransactionForContract) { - val command = tx.commands.requireSingleCommand() - - requireThat { - // Constraints on the shape of the transaction. - "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) - "Only one output state should be created." using (tx.outputs.size == 1) - - // IOU-specific constraints. - val out = tx.outputs.single() as IOUState - "The IOU's value must be non-negative." using (out.value > 0) - "The sender and the recipient cannot be the same entity." using (out.sender != out.recipient) - } - } - - .. code-block:: java - - @Override - public void verify(TransactionForContract tx) { - final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); - - requireThat(check -> { - // Constraints on the shape of the transaction. - check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); - check.using("Only one output state should be created.", tx.getOutputs().size() == 1); - - // IOU-specific constraints. - final IOUState out = (IOUState) tx.getOutputs().get(0); - check.using("The IOU's value must be non-negative.",out.getValue() > 0); - check.using("The sender and the recipient cannot be the same entity.", out.getSender() != out.getRecipient()); - - return null; - }); - } +We impose these constraints in the same ``requireThat`` block as before. You can see that we're not restricted to only writing constraints in the ``requireThat`` block. We can also write other statements - in this case, we're extracting the transaction's single ``IOUState`` and assigning it to a variable. Signer constraints ~~~~~~~~~~~~~~~~~~ -Our final constraint is that the required signers on the transaction are the sender and the recipient only. A -transaction's required signers is equal to the union of all the signers listed on the commands. We can therefore -extract the signers from the ``Create`` command we retrieved earlier. - -.. container:: codeset - - .. code-block:: kotlin - - override fun verify(tx: TransactionForContract) { - val command = tx.commands.requireSingleCommand() - - requireThat { - // Constraints on the shape of the transaction. - "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) - "Only one output state should be created." using (tx.outputs.size == 1) - - // IOU-specific constraints. - val out = tx.outputs.single() as IOUState - "The IOU's value must be non-negative." using (out.value > 0) - "The sender and the recipient cannot be the same entity." using (out.sender != out.recipient) - - // Constraints on the signers. - "All of the participants must be signers." using (command.signers.toSet() == out.participants.map { it.owningKey }.toSet()) - } - } - - .. code-block:: java - - // Additional imports. - import com.google.common.collect.ImmutableList; - import java.security.PublicKey; - import java.util.List; - - ... - - @Override - public void verify(TransactionForContract tx) { - final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); - - requireThat(check -> { - // Constraints on the shape of the transaction. - check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); - check.using("Only one output state should be created.", tx.getOutputs().size() == 1); - - // IOU-specific constraints. - final IOUState out = (IOUState) tx.getOutputs().get(0); - final Party sender = out.getSender(); - final Party recipient = out.getRecipient(); - check.using("The IOU's value must be non-negative.",out.getValue() > 0); - check.using("The sender and the recipient cannot be the same entity.", out.getSender() != out.getRecipient()); - - // Constraints on the signers. - final Set requiredSigners = Sets.newHashSet(sender.getOwningKey(), recipient.getOwningKey()); - final Set signerSet = Sets.newHashSet(command.getSigners()); - check.using("All of the participants must be signers.", (signerSet.equals(requiredSigners))); - - return null; - }); - } - -Checkpoint ----------- -We've now defined the full contract logic of our ``IOUContract``. This contract means that transactions involving -``IOUState`` states will have to fulfill strict constraints to become valid ledger updates. - -Before we move on, let's go back and modify ``IOUState`` to point to the new ``IOUContract``: - -.. container:: codeset - - .. code-block:: kotlin - - class IOUState(val value: Int, - val sender: Party, - val recipient: Party) : ContractState { - override val contract: IOUContract = IOUContract() - - override val participants get() = listOf(sender, recipient) - } - - .. code-block:: java - - public class IOUState implements ContractState { - private final Integer value; - private final Party sender; - private final Party recipient; - private final IOUContract contract = new IOUContract(); - - public IOUState(Integer value, Party sender, Party recipient) { - this.value = value; - this.sender = sender; - this.recipient = recipient; - } - - public Integer getValue() { - return value; - } - - public Party getSender() { - return sender; - } - - public Party getRecipient() { - return recipient; - } - - @Override - public IOUContract getContract() { - return contract; - } - - @Override - public List getParticipants() { - return ImmutableList.of(sender, recipient); - } - } - -Transaction tests ------------------ -How can we ensure that we've defined our contract constraints correctly? - -One option would be to deploy the CorDapp onto a set of nodes, and test it manually. However, this is a relatively -slow process, and would take on the order of minutes to test each change. - -Instead, we can test our contract logic using Corda's ``ledgerDSL`` transaction-testing framework. This will allow us -to test our contract without the overhead of spinning up a set of nodes. - -Open either ``test/kotlin/com/template/contract/ContractTests.kt`` or -``test/java/com/template/contract/ContractTests.java``, and add the following as our first test: - -.. container:: codeset - - .. code-block:: kotlin - - package com.template - - import net.corda.testing.* - import org.junit.Test - - class IOUTransactionTests { - @Test - fun `transaction must include Create command`() { - ledger { - transaction { - output { IOUState(1, MINI_CORP, MEGA_CORP) } - fails() - command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } - verifies() - } - } - } - } - - .. code-block:: java - - package com.template; - - import net.corda.core.identity.Party; - import org.junit.Test; - import java.security.PublicKey; - import static net.corda.testing.CoreTestUtils.*; - - public class IOUTransactionTests { - static private final Party miniCorp = getMINI_CORP(); - static private final Party megaCorp = getMEGA_CORP(); - static private final PublicKey[] keys = new PublicKey[2]; - - { - keys[0] = getMEGA_CORP_PUBKEY(); - keys[1] = getMINI_CORP_PUBKEY(); - } - - @Test - public void transactionMustIncludeCreateCommand() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.output(new IOUState(1, miniCorp, megaCorp)); - txDSL.fails(); - txDSL.command(keys, IOUContract.Create::new); - txDSL.verifies(); - return null; - }); - return null; - }); - } - } - -This test uses Corda's built-in ``ledgerDSL`` to: - -* Create a fake transaction -* Add inputs, outputs, commands, etc. (using the DSL's ``output``, ``input`` and ``command`` methods) -* At any point, asserting that the transaction built so far is either contractually valid (by calling ``verifies``) or - contractually invalid (by calling ``fails``) - -In this instance: - -* We initially create a transaction with an output but no command -* We assert that this transaction is invalid (since the ``Create`` command is missing) -* We then add the ``Create`` command -* We assert that transaction is now valid - -Here is the full set of tests we'll be using to test the ``IOUContract``: - -.. container:: codeset - - .. code-block:: kotlin - - class IOUTransactionTests { - @Test - fun `transaction must include Create command`() { - ledger { - transaction { - output { IOUState(1, MINI_CORP, MEGA_CORP) } - fails() - command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } - verifies() - } - } - } - - @Test - fun `transaction must have no inputs`() { - ledger { - transaction { - input { IOUState(1, MINI_CORP, MEGA_CORP) } - output { IOUState(1, MINI_CORP, MEGA_CORP) } - command(MEGA_CORP_PUBKEY) { IOUContract.Create() } - `fails with`("No inputs should be consumed when issuing an IOU.") - } - } - } - - @Test - fun `transaction must have one output`() { - ledger { - transaction { - output { IOUState(1, MINI_CORP, MEGA_CORP) } - output { IOUState(1, MINI_CORP, MEGA_CORP) } - command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } - `fails with`("Only one output state should be created.") - } - } - } - - @Test - fun `sender must sign transaction`() { - ledger { - transaction { - output { IOUState(1, MINI_CORP, MEGA_CORP) } - command(MINI_CORP_PUBKEY) { IOUContract.Create() } - `fails with`("All of the participants must be signers.") - } - } - } - - @Test - fun `recipient must sign transaction`() { - ledger { - transaction { - output { IOUState(1, MINI_CORP, MEGA_CORP) } - command(MEGA_CORP_PUBKEY) { IOUContract.Create() } - `fails with`("All of the participants must be signers.") - } - } - } - - @Test - fun `sender is not recipient`() { - ledger { - transaction { - output { IOUState(1, MEGA_CORP, MEGA_CORP) } - command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } - `fails with`("The sender and the recipient cannot be the same entity.") - } - } - } - - @Test - fun `cannot create negative-value IOUs`() { - ledger { - transaction { - output { IOUState(-1, MINI_CORP, MEGA_CORP) } - command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } - `fails with`("The IOU's value must be non-negative.") - } - } - } - } - - .. code-block:: java - - public class IOUTransactionTests { - static private final Party miniCorp = getMINI_CORP(); - static private final Party megaCorp = getMEGA_CORP(); - static private final PublicKey[] keys = new PublicKey[2]; - - { - keys[0] = getMEGA_CORP_PUBKEY(); - keys[1] = getMINI_CORP_PUBKEY(); - } - - @Test - public void transactionMustIncludeCreateCommand() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.output(new IOUState(1, miniCorp, megaCorp)); - txDSL.fails(); - txDSL.command(keys, IOUContract.Create::new); - txDSL.verifies(); - return null; - }); - return null; - }); - } - - @Test - public void transactionMustHaveNoInputs() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.input(new IOUState(1, miniCorp, megaCorp)); - txDSL.output(new IOUState(1, miniCorp, megaCorp)); - txDSL.command(keys, IOUContract.Create::new); - txDSL.failsWith("No inputs should be consumed when issuing an IOU."); - return null; - }); - return null; - }); - } - - @Test - public void transactionMustHaveOneOutput() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.output(new IOUState(1, miniCorp, megaCorp)); - txDSL.output(new IOUState(1, miniCorp, megaCorp)); - txDSL.command(keys, IOUContract.Create::new); - txDSL.failsWith("Only one output state should be created."); - return null; - }); - return null; - }); - } - - @Test - public void senderMustSignTransaction() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.output(new IOUState(1, miniCorp, megaCorp)); - PublicKey[] keys = new PublicKey[1]; - keys[0] = getMINI_CORP_PUBKEY(); - txDSL.command(keys, IOUContract.Create::new); - txDSL.failsWith("All of the participants must be signers."); - return null; - }); - return null; - }); - } - - @Test - public void recipientMustSignTransaction() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.output(new IOUState(1, miniCorp, megaCorp)); - PublicKey[] keys = new PublicKey[1]; - keys[0] = getMEGA_CORP_PUBKEY(); - txDSL.command(keys, IOUContract.Create::new); - txDSL.failsWith("All of the participants must be signers."); - return null; - }); - return null; - }); - } - - @Test - public void senderIsNotRecipient() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.output(new IOUState(1, megaCorp, megaCorp)); - PublicKey[] keys = new PublicKey[1]; - keys[0] = getMEGA_CORP_PUBKEY(); - txDSL.command(keys, IOUContract.Create::new); - txDSL.failsWith("The sender and the recipient cannot be the same entity."); - return null; - }); - return null; - }); - } - - @Test - public void cannotCreateNegativeValueIOUs() { - ledger(ledgerDSL -> { - ledgerDSL.transaction(txDSL -> { - txDSL.output(new IOUState(-1, miniCorp, megaCorp)); - txDSL.command(keys, IOUContract.Create::new); - txDSL.failsWith("The IOU's value must be non-negative."); - return null; - }); - return null; - }); - } - } - -Copy these tests into the ContractTests file, and run them to ensure that the ``IOUState`` and ``IOUContract`` are -defined correctly. All the tests should pass. +Finally, we require the borrower's signature on the transaction. A transaction's required signers is equal to the union +of all the signers listed on the commands. We therefore extract the signers from the ``Create`` command we +retrieved earlier. Progress so far --------------- @@ -731,8 +265,10 @@ We've now written an ``IOUContract`` constraining the evolution of each ``IOUSta * An ``IOUState`` can only be created, not transferred or redeemed * Creating an ``IOUState`` requires an issuance transaction with no inputs, a single ``IOUState`` output, and a ``Create`` command -* The ``IOUState`` created by the issuance transaction must have a non-negative value, and its sender and recipient +* The ``IOUState`` created by the issuance transaction must have a non-negative value, and the lender and borrower must be different entities. -The final step in the creation of our CorDapp will be to write the ``IOUFlow`` that will allow nodes to orchestrate -the creation of a new ``IOUState`` on the ledger, while only sharing information on a need-to-know basis. \ No newline at end of file +Before we move on, make sure you go back and modify ``IOUState`` to point to the new ``IOUContract`` class. + +The final step in the creation of our CorDapp will be to write the ``IOUFlow`` that will allow a node to orchestrate +the creation of a new ``IOUState`` on the ledger, while only sharing information on a need-to-know basis. diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst index 8b71c7a494..3416be215d 100644 --- a/docs/source/hello-world-flow.rst +++ b/docs/source/hello-world-flow.rst @@ -7,1014 +7,246 @@ Writing the flow ================ A flow describes the sequence of steps for agreeing a specific ledger update. By installing new flows on our node, we -allow the node to handle new business processes. - -We'll have to define two flows to issue an ``IOUState`` onto the ledger: - -* One to be run by the node initiating the creation of the IOU -* One to be run by the node responding to an IOU creation request - -Let's start writing our flows. We'll do this by modifying either ``TemplateFlow.java`` or ``TemplateFlow.kt``. - -FlowLogic ---------- -Each flow is implemented as a ``FlowLogic`` subclass. You define the steps taken by the flow by overriding -``FlowLogic.call``. - -We will define two ``FlowLogic`` instances communicating as a pair. The first will be called ``Initiator``, and will -be run by the sender of the IOU. The other will be called ``Acceptor``, and will be run by the recipient. We group -them together using a class (in Java) or a singleton object (in Kotlin) to show that they are conceptually related. - -Overwrite the existing template code with the following: - -.. container:: codeset - - .. code-block:: kotlin - - package com.template - - import co.paralleluniverse.fibers.Suspendable - import net.corda.core.flows.FlowLogic - import net.corda.core.flows.InitiatedBy - import net.corda.core.flows.InitiatingFlow - import net.corda.core.flows.StartableByRPC - import net.corda.core.identity.Party - import net.corda.core.transactions.SignedTransaction - import net.corda.core.utilities.ProgressTracker - - object IOUFlow { - @InitiatingFlow - @StartableByRPC - class Initiator(val iouValue: Int, - val otherParty: Party): FlowLogic() { - - /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ - override val progressTracker = ProgressTracker() - - /** The flow logic is encapsulated within the call() method. */ - @Suspendable - override fun call(): SignedTransaction { } - } - - @InitiatedBy(Initiator::class) - class Acceptor(val otherParty: Party) : FlowLogic() { - - @Suspendable - override fun call() { } - } - } - - .. code-block:: java - - package com.template; - - import co.paralleluniverse.fibers.Suspendable; - import net.corda.core.flows.*; - import net.corda.core.identity.Party; - import net.corda.core.transactions.SignedTransaction; - import net.corda.core.utilities.ProgressTracker; - - public class IOUFlow { - @InitiatingFlow - @StartableByRPC - public static class Initiator extends FlowLogic { - private final Integer iouValue; - private final Party otherParty; - - /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ - private final ProgressTracker progressTracker = new ProgressTracker(); - - public Initiator(Integer iouValue, Party otherParty) { - this.iouValue = iouValue; - this.otherParty = otherParty; - } - - /** The flow logic is encapsulated within the call() method. */ - @Suspendable - @Override - public SignedTransaction call() throws FlowException { } - } - - @InitiatedBy(Initiator.class) - public static class Acceptor extends FlowLogic { - - private final Party otherParty; - - public Acceptor(Party otherParty) { - this.otherParty = otherParty; - } - - @Suspendable - @Override - public Void call() throws FlowException { } - } - } - -We can see that we have two ``FlowLogic`` subclasses, each overriding ``FlowLogic.call``. There's a few things to note: - -* ``FlowLogic.call`` has a return type that matches the type parameter passed to ``FlowLogic`` - this is the return - type of running the flow -* The ``FlowLogic`` subclasses can have constructor parameters, which can be used as arguments to ``FlowLogic.call`` -* ``FlowLogic.call`` is annotated ``@Suspendable`` - this means that the flow will be check-pointed and serialised to - disk when it encounters a long-running operation, allowing your node to move on to running other flows. Forgetting - this annotation out will lead to some very weird error messages -* There are also a few more annotations, on the ``FlowLogic`` subclasses themselves: - - * ``@InitiatingFlow`` means that this flow can be started directly by the node - * ``StartableByRPC`` allows the node owner to start this flow via an RPC call - * ``@InitiatedBy(myClass: Class)`` means that this flow will only start in response to a message sent by another - node running the ``myClass`` flow +allow the node to handle new business processes. Our flow will allow a node to issue an ``IOUState`` onto the ledger. Flow outline ------------ -Now that we've defined our ``FlowLogic`` subclasses, what are the steps we need to take to issue a new IOU onto -the ledger? - -On the initiator side, we need to: +Our flow needs to take the following steps for a borrower to issue a new IOU onto the ledger: 1. Create a valid transaction proposal for the creation of a new IOU 2. Verify the transaction 3. Sign the transaction ourselves - 4. Gather the acceptor's signature - 5. Optionally get the transaction notarised, to: - - * Protect against double-spends for transactions with inputs - * Timestamp transactions that have a ``TimeWindow`` - - 6. Record the transaction in our vault - 7. Send the transaction to the acceptor so that they can record it too - -On the acceptor side, we need to: - - 1. Receive the partially-signed transaction from the initiator - 2. Verify its contents and signatures - 3. Append our signature and send it back to the initiator - 4. Wait to receive back the transaction from the initiator - 5. Record the transaction in our vault + 4. Record the transaction in our vault + 5. Send the transaction to the IOU's lender so that they can record it too Subflows ^^^^^^^^ Although our flow requirements look complex, we can delegate to existing flows to handle many of these tasks. A flow that is invoked within the context of a larger flow to handle a repeatable task is called a *subflow*. -In our initiator flow, we can automate step 4 by invoking ``SignTransactionFlow``, and we can automate steps 5, 6 and -7 using ``FinalityFlow``. Meanwhile, the *entirety* of the acceptor's flow can be automated using -``CollectSignaturesFlow``. +In our initiator flow, we can automate steps 5, 6 and 7 using ``FinalityFlow``. -All we need to do is write the steps to handle the initiator creating and signing the proposed transaction. +All we need to do is write the steps to handle the creation and signing of the proposed transaction. -Writing the initiator's flow ----------------------------- -Let's work through the steps of the initiator's flow one-by-one. +FlowLogic +--------- +Flows are implemented as ``FlowLogic`` subclasses. You define the steps taken by the flow by overriding +``FlowLogic.call``. -Building the transaction -^^^^^^^^^^^^^^^^^^^^^^^^ -We'll approach building the transaction in three steps: - -* Creating a transaction builder -* Creating the transaction's components -* Adding the components to the builder - -TransactionBuilder -~~~~~~~~~~~~~~~~~~ -To start building the proposed transaction, we need a ``TransactionBuilder``. This is a mutable transaction class to -which we can add inputs, outputs, commands, and any other components the transaction needs. - -We create a ``TransactionBuilder`` in ``Initiator.call`` as follows: +We'll write our flow in either ``TemplateFlow.java`` or ``TemplateFlow.kt``. Overwrite the existing template code with +the following: .. container:: codeset .. code-block:: kotlin - // Additional import. + package com.iou + + import co.paralleluniverse.fibers.Suspendable + import net.corda.core.contracts.Command + import net.corda.core.flows.FlowLogic + import net.corda.core.flows.InitiatingFlow + import net.corda.core.flows.StartableByRPC + import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder + import net.corda.core.utilities.ProgressTracker + import net.corda.flows.FinalityFlow - ... + @InitiatingFlow + @StartableByRPC + class IOUFlow(val iouValue: Int, + val otherParty: Party) : FlowLogic() { - @Suspendable - override fun call(): SignedTransaction { - // We create a transaction builder - val txBuilder = TransactionBuilder() - val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() - txBuilder.notary = notaryIdentity + /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ + override val progressTracker = ProgressTracker() + + /** The flow logic is encapsulated within the call() method. */ + @Suspendable + override fun call() { + // We retrieve the required identities from the network map. + val me = serviceHub.myInfo.legalIdentity + val notary = serviceHub.networkMapCache.getAnyNotary() + + // We create a transaction builder + val txBuilder = TransactionBuilder(notary = notary) + + // We add the items to the builder. + val state = IOUState(iouValue, me, otherParty) + val cmd = Command(IOUContract.Create(), me.owningKey) + txBuilder.withItems(state, cmd) + + // Verifying the transaction. + txBuilder.verify(serviceHub) + + // Signing the transaction. + val signedTx = serviceHub.signInitialTransaction(txBuilder) + + // Finalising the transaction. + subFlow(FinalityFlow(signedTx)) + } } .. code-block:: java - // Additional import. + package com.iou; + + import co.paralleluniverse.fibers.Suspendable; + import net.corda.core.contracts.Command; + import net.corda.core.flows.FlowException; + import net.corda.core.flows.FlowLogic; + import net.corda.core.flows.InitiatingFlow; + import net.corda.core.flows.StartableByRPC; + import net.corda.core.identity.Party; + import net.corda.core.transactions.SignedTransaction; import net.corda.core.transactions.TransactionBuilder; + import net.corda.core.utilities.ProgressTracker; + import net.corda.flows.FinalityFlow; - ... + @InitiatingFlow + @StartableByRPC + public class IOUFlow extends FlowLogic { + private final Integer iouValue; + private final Party otherParty; - @Suspendable - @Override - public SignedTransaction call() throws FlowException { - // We create a transaction builder - final TransactionBuilder txBuilder = new TransactionBuilder(); - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - txBuilder.setNotary(notary); + /** + * The progress tracker provides checkpoints indicating the progress of the flow to observers. + */ + private final ProgressTracker progressTracker = new ProgressTracker(); + + public IOUFlow(Integer iouValue, Party otherParty) { + this.iouValue = iouValue; + this.otherParty = otherParty; + } + + /** + * The flow logic is encapsulated within the call() method. + */ + @Suspendable + @Override + public Void call() throws FlowException { + // We retrieve the required identities from the network map. + final Party me = getServiceHub().getMyInfo().getLegalIdentity(); + final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); + + // We create a transaction builder + final TransactionBuilder txBuilder = new TransactionBuilder(); + txBuilder.setNotary(notary); + + // We add the items to the builder. + IOUState state = new IOUState(iouValue, me, otherParty); + Command cmd = new Command(new IOUContract.Create(), me.getOwningKey()); + txBuilder.withItems(state, cmd); + + // Verifying the transaction. + txBuilder.verify(getServiceHub()); + + // Signing the transaction. + final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); + + // Finalising the transaction. + subFlow(new FinalityFlow(signedTx)); + + return null; + } } -In the first line, we create a ``TransactionBuilder``. We will also want our transaction to have a notary, in order -to prevent double-spends. In the second line, we retrieve the identity of the notary who will be notarising our -transaction and add it to the builder. +We now have our own ``FlowLogic`` subclass that overrides ``FlowLogic.call``. There's a few things to note: + +* ``FlowLogic.call`` has a return type that matches the type parameter passed to ``FlowLogic`` - this is type returned + by running the flow +* ``FlowLogic`` subclasses can have constructor parameters, which can be used as arguments to ``FlowLogic.call`` +* ``FlowLogic.call`` is annotated ``@Suspendable`` - this means that the flow will be check-pointed and serialised to + disk when it encounters a long-running operation, allowing your node to move on to running other flows. Forgetting + this annotation out will lead to some very weird error messages +* There are also a few more annotations, on the ``FlowLogic`` subclass itself: + + * ``@InitiatingFlow`` means that this flow can be started directly by the node + * ``StartableByRPC`` allows the node owner to start this flow via an RPC call + +Let's walk through the steps of ``FlowLogic.call`` one-by-one: + +Retrieving participant information +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The identity of our counterparty is passed in as a constructor argument. However, we need to use the ``ServiceHub`` to +retrieve our identity, as well as the identity of the notary we'll be using for our transaction. You can see that the notary's identity is being retrieved from the node's ``ServiceHub``. Whenever we need information within a flow - whether it's about our own node, its contents, or the rest of the network - we use the node's ``ServiceHub``. In particular, ``ServiceHub.networkMapCache`` provides information about the other nodes on the network and the services that they offer. -Transaction components -~~~~~~~~~~~~~~~~~~~~~~ -Now that we have our ``TransactionBuilder``, we need to create its components. Remember that we're trying to build +Building the transaction +^^^^^^^^^^^^^^^^^^^^^^^^ +We'll build our transaction proposal in two steps: + +* Creating a transaction builder +* Adding the desired items to the builder + +Creating a transaction builder +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +To start building the proposed transaction, we need a ``TransactionBuilder``. This is a mutable transaction class to +which we can add inputs, outputs, commands, and any other items the transaction needs. We create a +``TransactionBuilder`` that uses the notary we retrieved earlier. + +Transaction items +~~~~~~~~~~~~~~~~~ +Now that we have our ``TransactionBuilder``, we need to add the desired items. Remember that we're trying to build the following transaction: - .. image:: resources/tutorial-transaction.png -:scale: 15% + .. image:: resources/simple-tutorial-transaction.png + :scale: 15% :align: center So we'll need the following: * The output ``IOUState`` -* A ``Create`` command listing both the IOU's sender and recipient as signers +* A ``Create`` command listing the IOU's borrower as a signer -We create these components as follows: +The command we use pairs the ``IOUContract.Create`` command defined earlier with our public key. Including this command +in the transaction makes us one of the transaction's required signers. -.. container:: codeset +We add these items to the transaction using the ``TransactionBuilder.withItems`` method, which takes a ``vararg`` of: - .. code-block:: kotlin - - // Additional import. - import net.corda.core.contracts.Command - - ... - - @Suspendable - override fun call(): SignedTransaction { - // We create a transaction builder - val txBuilder = TransactionBuilder() - val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() - txBuilder.notary = notaryIdentity - - // We create the transaction's components. - val ourIdentity = serviceHub.myInfo.legalIdentity - val iou = IOUState(iouValue, ourIdentity, otherParty) - val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) - } - - .. code-block:: java - - // Additional imports. - import com.google.common.collect.ImmutableList; - import net.corda.core.contracts.Command; - import java.security.PublicKey; - import java.util.List; - - ... - - @Suspendable - @Override - public SignedTransaction call() throws FlowException { - // We create a transaction builder - final TransactionBuilder txBuilder = new TransactionBuilder(); - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - txBuilder.setNotary(notary); - - // We create the transaction's components. - final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); - final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); - final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); - final Command txCommand = new Command(new IOUContract.Create(), signers); - } - -To build the state, we start by retrieving our own identity (again, we get this information from the ``ServiceHub``, -via ``ServiceHub.myInfo``). We then build the ``IOUState``, using our identity, the ``IOUContract``, and the IOU -value and counterparty from the ``FlowLogic``'s constructor parameters. - -We also create the command, which pairs the ``IOUContract.Create`` command with the public keys of ourselves and the -counterparty. If this command is included in the transaction, both ourselves and the counterparty will be required -signers. - -Adding the components -~~~~~~~~~~~~~~~~~~~~~ -Finally, we add the items to the transaction using the ``TransactionBuilder.withItems`` method: - -.. container:: codeset - - .. code-block:: kotlin - - @Suspendable - override fun call(): SignedTransaction { - // We create a transaction builder - val txBuilder = TransactionBuilder() - val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() - txBuilder.notary = notaryIdentity - - // We create the transaction's components. - val ourIdentity = serviceHub.myInfo.legalIdentity - val iou = IOUState(iouValue, ourIdentity, otherParty) - val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand) - } - - .. code-block:: java - - @Suspendable - @Override - public SignedTransaction call() throws FlowException { - // We create a transaction builder - final TransactionBuilder txBuilder = new TransactionBuilder(); - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - txBuilder.setNotary(notary); - - // We create the transaction's components. - final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); - final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); - final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); - final Command txCommand = new Command(new IOUContract.Create(), signers); - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand); - } - -``TransactionBuilder.withItems`` takes a `vararg` of: - -* `ContractState` objects, which are added to the builder as output states -* `StateRef` objects (references to the outputs of previous transactions), which are added to the builder as input +* ``ContractState`` or ``TransactionState`` objects, which are added to the builder as output states +* ``StateRef`` objects (references to the outputs of previous transactions), which are added to the builder as input state references -* `Command` objects, which are added to the builder as commands +* ``Command`` objects, which are added to the builder as commands +* ``SecureHash`` objects, which are added to the builder as attachments +* ``TimeWindow`` objects, which set the time-window of the transaction It will modify the ``TransactionBuilder`` in-place to add these components to it. Verifying the transaction ^^^^^^^^^^^^^^^^^^^^^^^^^ We've now built our proposed transaction. Before we sign it, we should check that it represents a valid ledger update -proposal by verifying the transaction, which will execute each of the transaction's contracts: - -.. container:: codeset - - .. code-block:: kotlin - - @Suspendable - override fun call(): SignedTransaction { - // We create a transaction builder - val txBuilder = TransactionBuilder() - val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() - txBuilder.notary = notaryIdentity - - // We create the transaction's components. - val ourIdentity = serviceHub.myInfo.legalIdentity - val iou = IOUState(iouValue, ourIdentity, otherParty) - val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand) - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify() - } - - .. code-block:: java - - @Suspendable - @Override - public SignedTransaction call() throws FlowException { - // We create a transaction builder - final TransactionBuilder txBuilder = new TransactionBuilder(); - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - txBuilder.setNotary(notary); - - // We create the transaction's components. - final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); - final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); - final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); - final Command txCommand = new Command(new IOUContract.Create(), signers); - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand); - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify(); - } - -To verify the transaction, we must: - -* Convert the builder into an immutable ``WireTransaction`` -* Convert the ``WireTransaction`` into a ``LedgerTransaction`` using the ``ServiceHub``. This step resolves the - transaction's input state references and attachment references into actual states and attachments (in case their - contents are needed to verify the transaction -* Call ``LedgerTransaction.verify`` to test whether the transaction is valid based on the contract of every input and - output state in the transaction +proposal by verifying the transaction, which will execute each of the transaction's contracts. If the verification fails, we have built an invalid transaction. Our flow will then end, throwing a ``TransactionVerificationException``. Signing the transaction ^^^^^^^^^^^^^^^^^^^^^^^ -Now that we are satisfied that our transaction proposal is valid, we sign it. Once the transaction is signed, -no-one will be able to modify the transaction without invalidating our signature. This effectively makes the -transaction immutable. - -.. container:: codeset - - .. code-block:: kotlin - - @Suspendable - override fun call(): SignedTransaction { - // We create a transaction builder - val txBuilder = TransactionBuilder() - val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() - txBuilder.notary = notaryIdentity - - // We create the transaction's components. - val ourIdentity = serviceHub.myInfo.legalIdentity - val iou = IOUState(iouValue, ourIdentity, otherParty) - val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand) - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify() - - // Signing the transaction. - val partSignedTx = serviceHub.signInitialTransaction(txBuilder) - } - - .. code-block:: java - - @Suspendable - @Override - public SignedTransaction call() throws FlowException { - // We create a transaction builder - final TransactionBuilder txBuilder = new TransactionBuilder(); - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - txBuilder.setNotary(notary); - - // We create the transaction's components. - final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); - final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); - final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); - final Command txCommand = new Command(new IOUContract.Create(), signers); - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand); - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify(); - - // Signing the transaction. - final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder); - } +Now that we have a valid transaction proposal, we need to sign it. Once the transaction is signed, no-one will be able +to modify the transaction without invalidating our signature, effectively making the transaction immutable. The call to ``ServiceHub.signInitialTransaction`` returns a ``SignedTransaction`` - an object that pairs the transaction itself with a list of signatures over that transaction. -We can now safely send the builder to our counterparty. If the counterparty tries to modify the transaction, the -transaction's hash will change, our digital signature will no longer be valid, and the transaction will not be accepted -as a valid ledger update. - -Gathering counterparty signatures -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The final step in order to create a valid transaction proposal is to collect the counterparty's signature. As -discussed, we can automate this process by invoking the built-in ``CollectSignaturesFlow``: - -.. container:: codeset - - .. code-block:: kotlin - - // Additional import. - import net.corda.flows.CollectSignaturesFlow - - ... - - @Suspendable - override fun call(): SignedTransaction { - // We create a transaction builder - val txBuilder = TransactionBuilder() - val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() - txBuilder.notary = notaryIdentity - - // We create the transaction's components. - val ourIdentity = serviceHub.myInfo.legalIdentity - val iou = IOUState(iouValue, ourIdentity, otherParty) - val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand) - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify() - - // Signing the transaction. - val partSignedTx = serviceHub.signInitialTransaction(txBuilder) - - // Gathering the signatures. - val signedTx = subFlow(CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.tracker())) - } - - .. code-block:: java - - // Additional import. - import net.corda.flows.CollectSignaturesFlow; - - ... - - @Suspendable - @Override - public SignedTransaction call() throws FlowException { - // We create a transaction builder - final TransactionBuilder txBuilder = new TransactionBuilder(); - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - txBuilder.setNotary(notary); - - // We create the transaction's components. - final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); - final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); - final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); - final Command txCommand = new Command(new IOUContract.Create(), signers); - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand); - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify(); - - // Signing the transaction. - final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder); - - // Gathering the signatures. - final SignedTransaction signedTx = subFlow( - new CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.Companion.tracker())); - } - -``CollectSignaturesFlow`` gathers signatures from every participant listed on the transaction, and returns a -``SignedTransaction`` with all the required signatures. - Finalising the transaction ^^^^^^^^^^^^^^^^^^^^^^^^^^ -We now have a valid transaction signed by all the required parties. All that's left to do is to have it notarised and -recorded by all the relevant parties. From then on, it will become a permanent part of the ledger. Again, instead -of handling this process manually, we'll use a built-in flow called ``FinalityFlow``: - -.. container:: codeset - - .. code-block:: kotlin - - // Additional import. - import net.corda.flows.FinalityFlow - - ... - - @Suspendable - override fun call(): SignedTransaction { - // We create a transaction builder - val txBuilder = TransactionBuilder() - val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() - txBuilder.notary = notaryIdentity - - // We create the transaction's components. - val ourIdentity = serviceHub.myInfo.legalIdentity - val iou = IOUState(iouValue, ourIdentity, otherParty) - val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand) - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify() - - // Signing the transaction. - val partSignedTx = serviceHub.signInitialTransaction(txBuilder) - - // Gathering the signatures. - val signedTx = subFlow(CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.tracker())) - - // Finalising the transaction. - return subFlow(FinalityFlow(signedTx)).single() - } - - .. code-block:: java - - // Additional import. - import net.corda.flows.FinalityFlow; - - ... - - @Suspendable - @Override - public SignedTransaction call() throws FlowException { - // We create a transaction builder - final TransactionBuilder txBuilder = new TransactionBuilder(); - final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - txBuilder.setNotary(notary); - - // We create the transaction's components. - final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); - final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); - final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); - final Command txCommand = new Command(new IOUContract.Create(), signers); - - // Adding the item's to the builder. - txBuilder.withItems(iou, txCommand); - - // Verifying the transaction. - txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify(); - - // Signing the transaction. - final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder); - - // Gathering the signatures. - final SignedTransaction signedTx = subFlow( - new CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.Companion.tracker())); - - // Finalising the transaction. - return subFlow(new FinalityFlow(signedTx)).get(0); - } +Now that we have a valid signed transaction, all that's left to do is to have it notarised and recorded by all the +relevant parties. By doing so, it will become a permanent part of the ledger. As discussed, we'll handle this process +automatically using a built-in flow called ``FinalityFlow``: ``FinalityFlow`` completely automates the process of: -* Notarising the transaction +* Notarising the transaction if required (i.e. if the transaction contains inputs and/or a time-window) * Recording it in our vault -* Sending it to the counterparty for them to record as well +* Sending it to the other participants (i.e. the lender) for them to record as well -``FinalityFlow`` also returns a list of the notarised transactions. We extract the single item from this list and -return it. - -That completes the initiator side of the flow. - -Writing the acceptor's flow ---------------------------- -The acceptor's side of the flow is much simpler. We need to: - -1. Receive a signed transaction from the counterparty -2. Verify the transaction -3. Sign the transaction -4. Send the updated transaction back to the counterparty - -As we just saw, the process of building and finalising the transaction will be completely handled by the initiator flow. - -SignTransactionFlow -~~~~~~~~~~~~~~~~~~~ -We can automate all four steps of the acceptor's flow by invoking ``SignTransactionFlow``. ``SignTransactionFlow`` is -a flow that is registered by default on every node to respond to messages from ``CollectSignaturesFlow`` (which is -invoked by the initiator flow). - -As ``SignTransactionFlow`` is an abstract class, we have to subclass it and override -``SignTransactionFlow.checkTransaction``: - -.. container:: codeset - - .. code-block:: kotlin - - // Additional import. - import net.corda.flows.SignTransactionFlow - - ... - - @InitiatedBy(Initiator::class) - class Acceptor(val otherParty: Party) : FlowLogic() { - - @Suspendable - override fun call() { - // Stage 1 - Verifying and signing the transaction. - subFlow(object : SignTransactionFlow(otherParty, tracker()) { - override fun checkTransaction(stx: SignedTransaction) { - // Define custom verification logic here. - } - }) - } - } - - .. code-block:: java - - // Additional import. - import net.corda.flows.SignTransactionFlow; - - ... - - @InitiatedBy(Initiator.class) - public static class Acceptor extends FlowLogic { - - private final Party otherParty; - - public Acceptor(Party otherParty) { - this.otherParty = otherParty; - } - - @Suspendable - @Override - public Void call() throws FlowException { - // Stage 1 - Verifying and signing the transaction. - - class signTxFlow extends SignTransactionFlow { - private signTxFlow(Party otherParty, ProgressTracker progressTracker) { - super(otherParty, progressTracker); - } - - @Override - protected void checkTransaction(SignedTransaction signedTransaction) { - // Define custom verification logic here. - } - } - - subFlow(new signTxFlow(otherParty, SignTransactionFlow.Companion.tracker())); - - return null; - } - } - -``SignTransactionFlow`` already checks the transaction's signatures, and whether the transaction is contractually -valid. The purpose of ``SignTransactionFlow.checkTransaction`` is to define any additional verification of the -transaction that we wish to perform before we sign it. For example, we may want to: - -* Check that the transaction contains an ``IOUState`` -* Check that the IOU's value isn't too high - -Well done! You've finished the flows! - -Flow tests ----------- -As with contracts, deploying nodes to manually test flows is not efficient. Instead, we can use Corda's flow-test -DSL to quickly test our flows. The flow-test DSL works by creating a network of lightweight, "mock" node -implementations on which we run our flows. - -The first thing we need to do is create this mock network. Open either ``test/kotlin/com/template/flow/FlowTests.kt`` or -``test/java/com/template/contract/ContractTests.java``, and overwrite the existing code with: - -.. container:: codeset - - .. code-block:: kotlin - - package com.template - - import net.corda.core.contracts.TransactionVerificationException - import net.corda.core.getOrThrow - import net.corda.testing.node.MockNetwork - import net.corda.testing.node.MockNetwork.MockNode - import org.junit.After - import org.junit.Before - import org.junit.Test - import kotlin.test.assertEquals - import kotlin.test.assertFailsWith - - class IOUFlowTests { - lateinit var net: MockNetwork - lateinit var a: MockNode - lateinit var b: MockNode - lateinit var c: MockNode - - @Before - fun setup() { - net = MockNetwork() - val nodes = net.createSomeNodes(2) - a = nodes.partyNodes[0] - b = nodes.partyNodes[1] - b.registerInitiatedFlow(IOUFlow.Acceptor::class.java) - net.runNetwork() - } - - @After - fun tearDown() { - net.stopNodes() - } - } - - .. code-block:: java - - package com.template; - - import com.google.common.collect.ImmutableList; - import com.google.common.util.concurrent.ListenableFuture; - import net.corda.core.contracts.ContractState; - import net.corda.core.contracts.TransactionState; - import net.corda.core.contracts.TransactionVerificationException; - import net.corda.core.transactions.SignedTransaction; - import net.corda.testing.node.MockNetwork; - import net.corda.testing.node.MockNetwork.BasketOfNodes; - import net.corda.testing.node.MockNetwork.MockNode; - import org.junit.After; - import org.junit.Before; - import org.junit.Rule; - import org.junit.Test; - import org.junit.rules.ExpectedException; - - import java.util.List; - - import static org.hamcrest.CoreMatchers.instanceOf; - import static org.junit.Assert.assertEquals; - - public class IOUFlowTests { - private MockNetwork net; - private MockNode a; - private MockNode b; - - @Before - public void setup() { - net = new MockNetwork(); - BasketOfNodes nodes = net.createSomeNodes(2); - a = nodes.getPartyNodes().get(0); - b = nodes.getPartyNodes().get(1); - b.registerInitiatedFlow(IOUFlow.Acceptor.class); - net.runNetwork(); - } - - @After - public void tearDown() { - net.stopNodes(); - } - - @Rule - public final ExpectedException exception = ExpectedException.none(); - } - -This creates an in-memory network with mocked-out components. The network has two nodes, plus network map and notary -nodes. We register any responder flows (in our case, ``IOUFlow.Acceptor``) on our nodes as well. - -Our first test will be to check that the flow rejects invalid IOUs: - -.. container:: codeset - - .. code-block:: kotlin - - @Test - fun `flow rejects invalid IOUs`() { - val flow = IOUFlow.Initiator(-1, b.info.legalIdentity) - val future = a.services.startFlow(flow).resultFuture - net.runNetwork() - - // The IOUContract specifies that IOUs cannot have negative values. - assertFailsWith {future.getOrThrow()} - } - - .. code-block:: java - - @Test - public void flowRejectsInvalidIOUs() throws Exception { - IOUFlow.Initiator flow = new IOUFlow.Initiator(-1, b.info.getLegalIdentity()); - ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); - net.runNetwork(); - - exception.expectCause(instanceOf(TransactionVerificationException.class)); - future.get(); - } - -This code causes node A to run the ``IOUFlow.Initiator`` flow. The call to ``MockNetwork.runNetwork`` is required to -simulate the running of a real network. - -We then assert that because we passed in a negative IOU value to the flow's constructor, the flow should fail with a -``TransactionVerificationException``. In other words, we are asserting that at some point in flow, the transaction is -verified (remember that ``IOUContract`` forbids negative value IOUs), causing the flow to fail. - -Because flows need to be instrumented by a library called `Quasar `_ that -allows the flows to be checkpointed and serialized to disk, you need to run these tests using the provided -``Run Flow Tests - Java`` or ``Run Flow Tests - Kotlin`` run-configurations. - -Here is the full suite of tests we'll use for the ``IOUFlow``: - -.. container:: codeset - - .. code-block:: kotlin - - @Test - fun `flow rejects invalid IOUs`() { - val flow = IOUFlow.Initiator(-1, b.info.legalIdentity) - val future = a.services.startFlow(flow).resultFuture - net.runNetwork() - - // The IOUContract specifies that IOUs cannot have negative values. - assertFailsWith {future.getOrThrow()} - } - - @Test - fun `SignedTransaction returned by the flow is signed by the initiator`() { - val flow = IOUFlow.Initiator(1, b.info.legalIdentity) - val future = a.services.startFlow(flow).resultFuture - net.runNetwork() - - val signedTx = future.getOrThrow() - signedTx.verifySignatures(b.services.legalIdentityKey) - } - - @Test - fun `SignedTransaction returned by the flow is signed by the acceptor`() { - val flow = IOUFlow.Initiator(1, b.info.legalIdentity) - val future = a.services.startFlow(flow).resultFuture - net.runNetwork() - - val signedTx = future.getOrThrow() - signedTx.verifySignatures(a.services.legalIdentityKey) - } - - @Test - fun `flow records a transaction in both parties' vaults`() { - val flow = IOUFlow.Initiator(1, b.info.legalIdentity) - val future = a.services.startFlow(flow).resultFuture - net.runNetwork() - val signedTx = future.getOrThrow() - - // We check the recorded transaction in both vaults. - for (node in listOf(a, b)) { - assertEquals(signedTx, node.storage.validatedTransactions.getTransaction(signedTx.id)) - } - } - - @Test - fun `recorded transaction has no inputs and a single output, the input IOU`() { - val flow = IOUFlow.Initiator(1, b.info.legalIdentity) - val future = a.services.startFlow(flow).resultFuture - net.runNetwork() - val signedTx = future.getOrThrow() - - // We check the recorded transaction in both vaults. - for (node in listOf(a, b)) { - val recordedTx = node.storage.validatedTransactions.getTransaction(signedTx.id) - val txOutputs = recordedTx!!.tx.outputs - assert(txOutputs.size == 1) - - val recordedState = txOutputs[0].data as IOUState - assertEquals(recordedState.value, 1) - assertEquals(recordedState.sender, a.info.legalIdentity) - assertEquals(recordedState.recipient, b.info.legalIdentity) - } - } - - .. code-block:: java - - @Test - public void flowRejectsInvalidIOUs() throws Exception { - IOUFlow.Initiator flow = new IOUFlow.Initiator(-1, b.info.getLegalIdentity()); - ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); - net.runNetwork(); - - exception.expectCause(instanceOf(TransactionVerificationException.class)); - future.get(); - } - - @Test - public void signedTransactionReturnedByTheFlowIsSignedByTheInitiator() throws Exception { - IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity()); - ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); - net.runNetwork(); - - SignedTransaction signedTx = future.get(); - signedTx.verifySignatures(b.getServices().getLegalIdentityKey()); - } - - @Test - public void signedTransactionReturnedByTheFlowIsSignedByTheAcceptor() throws Exception { - IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity()); - ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); - net.runNetwork(); - - SignedTransaction signedTx = future.get(); - signedTx.verifySignatures(a.getServices().getLegalIdentityKey()); - } - - @Test - public void flowRecordsATransactionInBothPartiesVaults() throws Exception { - IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity()); - ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); - net.runNetwork(); - SignedTransaction signedTx = future.get(); - - for (MockNode node : ImmutableList.of(a, b)) { - assertEquals(signedTx, node.storage.getValidatedTransactions().getTransaction(signedTx.getId())); - } - } - - @Test - public void recordedTransactionHasNoInputsAndASingleOutputTheInputIOU() throws Exception { - IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity()); - ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); - net.runNetwork(); - SignedTransaction signedTx = future.get(); - - for (MockNode node : ImmutableList.of(a, b)) { - SignedTransaction recordedTx = node.storage.getValidatedTransactions().getTransaction(signedTx.getId()); - List> txOutputs = recordedTx.getTx().getOutputs(); - assert(txOutputs.size() == 1); - - IOUState recordedState = (IOUState) txOutputs.get(0).getData(); - assert(recordedState.getValue() == 1); - assertEquals(recordedState.getSender(), a.info.getLegalIdentity()); - assertEquals(recordedState.getRecipient(), b.info.getLegalIdentity()); - } - } - -Run these tests and make sure they all pass. If they do, its very likely that we have a working CorDapp. +Our flow, and our CorDapp, are now ready! Progress so far --------------- -We now have a flow that we can kick off on our node to completely automate the process of issuing an IOU onto the -ledger. Under the hood, this flow takes the form of two communicating ``FlowLogic`` subclasses. - -We now have a complete CorDapp, made up of: - -* The ``IOUState``, representing IOUs on the ledger -* The ``IOUContract``, controlling the evolution of ``IOUState`` objects over time -* The ``IOUFlow``, which transforms the creation of a new IOU on the ledger into a push-button process - -The final step is to spin up some nodes and test our CorDapp. \ No newline at end of file +We have now defined a flow that we can start on our node to completely automate the process of issuing an IOU onto the +ledger. The final step is to spin up some nodes and test our CorDapp. \ No newline at end of file diff --git a/docs/source/hello-world-introduction.rst b/docs/source/hello-world-introduction.rst index d66ffeb272..9760d91a6d 100644 --- a/docs/source/hello-world-introduction.rst +++ b/docs/source/hello-world-introduction.rst @@ -44,10 +44,10 @@ However, we can easily extend our CorDapp to handle additional use-cases later o Flow ^^^^ -Our flow will be the IOUFlow. It will allow two nodes to orchestrate the creation of a new IOU on the ledger, via the +Our flow will be the IOUFlow. It will allow a node to orchestrate the creation of a new IOU on the ledger, via the following steps: - .. image:: resources/tutorial-flow.png + .. image:: resources/simple-tutorial-flow.png :scale: 25% :align: center diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 8e604f8d11..b71eefd867 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -86,16 +86,12 @@ If we navigate to one of these folders, we'll see four node folder. Each node fo .. code:: python . - // The runnable node - |____corda.jar - // The node's webserver - |____corda-webserver.jar + |____corda.jar // The runnable node + |____corda-webserver.jar // The node's webserver |____dependencies - // The node's configuration file - |____node.conf + |____node.conf // The node's configuration file |____plugins - // Our IOU CorDapp - |____java/kotlin-source-0.1.jar + |____java/kotlin-source-0.1.jar // Our IOU CorDapp Let's start the nodes by running the following commands from the root of the project: @@ -132,9 +128,15 @@ commands. We want to create an IOU of 100 with Node B. We start the ``IOUFlow`` by typing: -.. code:: python +.. container:: codeset - start IOUFlow arg0: 99, arg1: "CN=NodeB,O=NodeB,L=New York,C=US" + .. code-block:: java + + start IOUFlow arg0: 99, arg1: "NodeB" + + .. code-block:: kotlin + + start IOUFlow iouValue: 99, otherParty: "NodeB" Node A and Node B will automatically agree an IOU. @@ -162,8 +164,8 @@ The vaults of Node A and Node B should both display the following output: - state: data: value: 99 - sender: "CN=NodeA,O=NodeA,L=London,C=GB" - recipient: "CN=NodeB,O=NodeB,L=New York,C=US" + lender: "CN=NodeA,O=NodeA,L=London,C=GB" + borrower: "CN=NodeB,O=NodeB,L=New York,C=US" contract: legalContractReference: "559322B95BCF7913E3113962DC3F3CBD71C818C66977721580C045DC41C813A5" participants: @@ -190,19 +192,26 @@ CorDapp is made up of three key parts: * The ``IOUState``, representing IOUs on the ledger * The ``IOUContract``, controlling the evolution of IOUs over time -* The ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger. +* The ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger Together, these three parts completely determine how IOUs are created and evolved on the ledger. Next steps ---------- -You should now be ready to develop your own CorDapps. There's -`a more fleshed-out version of the IOU CorDapp `_ -with an API and web front-end, and a set of example CorDapps in -`the main Corda repo `_, under ``samples``. An explanation of how to run these -samples :doc:`here `. +There are a number of improvements we could make to this CorDapp: + +* We could require signatures from the lender as well the borrower, to give both parties a say in the creation of a new + ``IOUState`` +* We should add unit tests, using the contract-test and flow-test frameworks +* We should change ``IOUState.value`` from an integer to a proper amount of a given currency +* We could add an API, to make it easier to interact with the CorDapp + +We will explore some of these improvements in future tutorials. But you should now be ready to develop your own +CorDapps. There's `a more fleshed-out version of the IOU CorDapp `_ with an +API and web front-end, and a set of example CorDapps in `the main Corda repo `_, under +``samples``. An explanation of how to run these samples :doc:`here `. As you write CorDapps, you can learn more about the API available :doc:`here `. If you get stuck at any point, please reach out on `Slack `_, -`Discourse `_, or `Stack Overflow `_. \ No newline at end of file +`Discourse `_, or `Stack Overflow `_. diff --git a/docs/source/hello-world-state.rst b/docs/source/hello-world-state.rst index 7196bcbd7a..b677366a08 100644 --- a/docs/source/hello-world-state.rst +++ b/docs/source/hello-world-state.rst @@ -38,11 +38,6 @@ If you do want to dive into Kotlin, there's an official `getting started guide `_, and a series of `Kotlin Koans `_. -If not, here's a quick primer on the Kotlinisms in the declaration of ``ContractState``: - -* ``val`` declares a read-only property, similar to Java's ``final`` keyword -* The syntax ``varName: varType`` declares ``varName`` as being of type ``varType`` - We can see that the ``ContractState`` interface declares two properties: * ``contract``: the contract controlling transactions involving this state @@ -53,15 +48,15 @@ Beyond this, our state is free to define any properties, methods, helpers or inn represent a given class of shared facts on the ledger. ``ContractState`` also has several child interfaces that you may wish to implement depending on your state, such as -``LinearState`` and ``OwnableState``. +``LinearState`` and ``OwnableState``. See :doc:`api-states` for more information. Modelling IOUs -------------- How should we define the ``IOUState`` representing IOUs on the ledger? Beyond implementing the ``ContractState`` interface, our ``IOUState`` will also need properties to track the relevant features of the IOU: -* The sender of the IOU -* The IOU's recipient +* The lender of the IOU +* The borrower of the IOU * The value of the IOU There are many more fields you could include, such as the IOU's currency. We'll abstract them away for now. If @@ -76,23 +71,22 @@ define an ``IOUState``: .. code-block:: kotlin - package com.template + package com.iou import net.corda.core.contracts.ContractState import net.corda.core.identity.Party class IOUState(val value: Int, - val sender: Party, - val recipient: Party, - // TODO: Once we've defined IOUContract, come back and update this. - override val contract: TemplateContract = TemplateContract()) : ContractState { + val lender: Party, + val borrower: Party) : ContractState { + override val contract: IOUContract = IOUContract() - override val participants get() = listOf(sender, recipient) + override val participants get() = listOf(lender, borrower) } .. code-block:: java - package com.template; + package com.iou; import com.google.common.collect.ImmutableList; import net.corda.core.contracts.ContractState; @@ -103,53 +97,52 @@ define an ``IOUState``: public class IOUState implements ContractState { private final int value; - private final Party sender; - private final Party recipient; - // TODO: Once we've defined IOUContract, come back and update this. - private final TemplateContract contract; + private final Party lender; + private final Party borrower; + private final IOUContract contract = new IOUContract(); - public IOUState(int value, Party sender, Party recipient, IOUContract contract) { + public IOUState(int value, Party lender, Party borrower) { this.value = value; - this.sender = sender; - this.recipient = recipient; - this.contract = contract; + this.lender = lender; + this.borrower = borrower; } public int getValue() { return value; } - public Party getSender() { - return sender; + public Party getLender() { + return lender; } - public Party getRecipient() { - return recipient; + public Party getBorrower() { + return borrower; } @Override // TODO: Once we've defined IOUContract, come back and update this. - public TemplateContract getContract() { + public IOUContract getContract() { return contract; } @Override public List getParticipants() { - return ImmutableList.of(sender, recipient); + return ImmutableList.of(lender, borrower); } } We've made the following changes: * We've renamed ``TemplateState`` to ``IOUState`` -* We've added properties for ``value``, ``sender`` and ``recipient`` (along with any getters and setters in Java): +* We've added properties for ``value``, ``lender`` and ``borrower`` (along with any getters and setters in Java): - * ``value`` is just a standard int (in Java)/Int (in Kotlin), but ``sender`` and ``recipient`` are of type - ``Party``. ``Party`` is a built-in Corda type that represents an entity on the network. + * ``value`` is just a standard int (in Java)/Int (in Kotlin) + * ``lender`` and ``borrower`` are of type ``Party``. ``Party`` is a built-in Corda type that represents an entity on + the network. -* We've overridden ``participants`` to return a list of the ``sender`` and ``recipient`` -* This means that actions such as changing the state's contract or its notary will require approval from both the - ``sender`` and the ``recipient`` +* We've overridden ``participants`` to return a list of the ``lender`` and ``borrower`` + + * Actions such as changing a state's contract or notary will require approval from all the ``participants`` We've left ``IOUState``'s contract as ``TemplateContract`` for now. We'll update this once we've defined the ``IOUContract``. diff --git a/docs/source/hello-world-template.rst b/docs/source/hello-world-template.rst index c298fb87c0..aac84d483d 100644 --- a/docs/source/hello-world-template.rst +++ b/docs/source/hello-world-template.rst @@ -24,13 +24,13 @@ Open a terminal window in the directory where you want to download the CorDapp t .. code-block:: text # Clone the template from GitHub: - git clone https://github.com/corda/cordapp-template.git & cd cordapp-template + git clone https://github.com/corda/cordapp-template.git ; cd cordapp-template # Retrieve a list of the stable Milestone branches using: git branch -a --list *release-M* # Check out the Milestone branch with the latest version number: - git checkout release-M[*version number*] & git pull + git checkout release-M[*version number*] ; git pull Template structure ------------------ @@ -38,7 +38,7 @@ We can write our CorDapp in either Java or Kotlin, and will be providing the cod you want to write the CorDapp in Java, you'll be modifying the files under ``java-source``. If you prefer to use Kotlin, you'll be modifying the files under ``kotlin-source``. -To implement our IOU CorDapp, we'll only need to modify five files: +To implement our IOU CorDapp, we'll only need to modify three files: .. container:: codeset @@ -53,13 +53,6 @@ To implement our IOU CorDapp, we'll only need to modify five files: // 3. The flow java-source/src/main/java/com/template/flow/TemplateFlow.java - // Tests for our contract and flow: - // 1. The contract tests - java-source/src/test/java/com/template/contract/ContractTests.java - - // 2. The flow tests - java-source/src/test/java/com/template/flow/FlowTests.java - .. code-block:: kotlin // 1. The state @@ -71,15 +64,8 @@ To implement our IOU CorDapp, we'll only need to modify five files: // 3. The flow kotlin-source/src/main/kotlin/com/template/flow/TemplateFlow.kt - // Tests for our contract and flow: - // 1. The contract tests - kotlin-source/src/test/kotlin/com/template/contract/ContractTests.kt - - // 2. The flow tests - kotlin-source/src/test/kotlin/com/template/flow/FlowTests.kt - Progress so far --------------- We now have a template that we can build upon to define our IOU CorDapp. -We'll begin writing the CorDapp proper by writing the definition of the ``IOUState``. \ No newline at end of file +We'll begin writing the CorDapp proper by writing the definition of the ``IOUState``. diff --git a/docs/source/node-administration.rst b/docs/source/node-administration.rst index 99ea389fdf..6410ed4076 100644 --- a/docs/source/node-administration.rst +++ b/docs/source/node-administration.rst @@ -39,9 +39,13 @@ not require any particular network protocol for export. So this data can be expo some monitoring systems provide a "Java Agent", which is essentially a JVM plugin that finds all the MBeans and sends them out to a statistics collector over the network. For those systems, follow the instructions provided by the vendor. -Sometimes though, you just want raw access to the data and operations itself. So nodes export them over HTTP on the -``/monitoring/json`` HTTP endpoint, using a program called `Jolokia `_. Jolokia defines the JSON -and REST formats for accessing MBeans, and provides client libraries to work with that protocol as well. +.. warning:: As of Corda M11, Java serialisation in the Corda node has been restricted, meaning MBeans access via the JMX + port will no longer work. Please use java agents instead, you can find details on how to use Jolokia JVM + agent `here `_. + +`Jolokia `_ allows you to access the raw data and operations without connecting to the JMX port +directly. The nodes export the data over HTTP on the ``/jolokia`` HTTP endpoint, Jolokia defines the JSON and REST +formats for accessing MBeans, and provides client libraries to work with that protocol as well. Here are a few ways to build dashboards and extract monitoring data for a node: @@ -51,12 +55,11 @@ Here are a few ways to build dashboards and extract monitoring data for a node: * `JMXTrans `_ is another tool for Graphite, this time, it's got its own agent (JVM plugin) which reads a custom config file and exports only the named data. It's more configurable than JMX2Graphite and doesn't require a separate process, as the JVM will write directly to Graphite. -* *Java Mission Control* is a desktop app that can connect to a target JVM that has the right command line flags set - (or always, if running locally). You can explore what data is available, create graphs of those metrics, and invoke - management operations like forcing a garbage collection. -* *VisualVM* is another desktop app that can do fine grained JVM monitoring and sampling. Very useful during development. * Cloud metrics services like New Relic also understand JMX, typically, by providing their own agent that uploads the data to their service on a regular schedule. +* `Telegraf `_ is a tool to collect, process, aggregate, and write metrics. + It can bridge any data input to any output using their plugin system, for example, Telegraf can + be configured to collect data from Jolokia and write to DataDog web api. Memory usage and tuning ----------------------- diff --git a/docs/source/node-internals-index.rst b/docs/source/node-internals-index.rst index 8fe4795988..e5a1c9157a 100644 --- a/docs/source/node-internals-index.rst +++ b/docs/source/node-internals-index.rst @@ -8,4 +8,3 @@ Node internals vault serialization messaging - persistence \ No newline at end of file diff --git a/docs/source/oracles.rst b/docs/source/oracles.rst index e77147893a..921562b671 100644 --- a/docs/source/oracles.rst +++ b/docs/source/oracles.rst @@ -112,7 +112,7 @@ Here is an extract from the ``NodeInterestRates.Oracle`` class and supporting ty class Oracle { fun query(queries: List, deadline: Instant): List - fun sign(ftx: FilteredTransaction, merkleRoot: SecureHash): DigitalSignature.LegallyIdentifiable + fun sign(ftx: FilteredTransaction, merkleRoot: SecureHash): DigitalSignature.WithKey } Because the fix contains a timestamp (the ``forDay`` field), that identifies the version of the data being requested, diff --git a/docs/source/publishing-corda.rst b/docs/source/publishing-corda.rst deleted file mode 100644 index 7b9f4b3c5e..0000000000 --- a/docs/source/publishing-corda.rst +++ /dev/null @@ -1,76 +0,0 @@ -Publishing Corda -================ - -Before Publishing ------------------ - -Before publishing you must make sure the version you plan to publish has a unique version number. Jcenter and Maven -Central will not allow overwriting old versions _unless_ the version is a snapshot. - -This guide assumes you are trying to publish to net.corda.*. Any other Maven coordinates require approval from Jcenter -and Maven Central. - -Publishing Locally ------------------- - -To publish the codebase locally to Maven Local you must run: - -.. code-block:: text - - gradlew install - -.. note:: This command is an alias for `publishToMavenLocal`. - -Publishing to Jcenter ---------------------- - -.. note:: The module you wish to publish must be linked to jcenter in Bintray. Only the founding account can do this. - -To publish to Jcenter you must first have the following; - -1. An account on Bintray in the R3 organisation -2. Our GPG key's passphrase for signing the binaries to publish - -Getting Setup -````````````` - -You must now set the following environment variables: - -* CORDA_BINTRAY_USER your Bintray username -* CORDA_BINTRAY_KEY to your Bintray API key (found at: https://bintray.com/profile/edit) -* CORDA_BINTRAY_GPG_PASSPHRASE to our GPG passphrase - -Publishing -`````````` - -Once you are setup you can upload all modules in a project with - -.. code-block:: text - - gradlew bintrayUpload - -Now login to Bintray and navigate to the corda repository, you will see a box stating you have published N files -and asking if you wish to publish. You can now publish to Bintray and Jcenter by clicking this button. - -.. warning:: Before publishing you should check that all of the files are uploaded and are signed. - -Within a minute your new version will be available to download and use. - -Publishing to Maven Central ---------------------------- - -To publish to Maven Central you need the following; - -1. An admin account on our Bintray R3 organisation -2. A published version in Bintray -3. An account with our Sonatype organisation (Maven Central's host) - -Publishing -`````````` - -1. Publish to Bintray -2. Navigate to the project you wish to publish -3. Click "Maven Central" -4. Enter your Sonatype credentials to publish a new version - -.. note:: The project you publish must be already published to Bintray and the project must be linked to Jcenter diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 6ea371d138..bd89729765 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -22,7 +22,7 @@ state schemas defined by CorDapp developers when modelling new contract types. C simple but sophisticated builder DSL (see ``QueryCriteriaUtils``). The new Vault Query service is usable by flows and by RPC clients alike via two simple API functions: ``queryBy()`` and ``trackBy()``. The former provides point-in-time snapshot queries whilst the later supplements the snapshot with dynamic streaming of updates. -See :doc:`vault-query` for full details. +See :doc:`api-vault-query` for full details. We have written a comprehensive Hello, World! tutorial, showing developers how to build a CorDapp from start to finish. The tutorial shows how the core elements of a CorDapp - states, contracts and flows - fit together diff --git a/docs/source/release-process-index.rst b/docs/source/release-process-index.rst index ff8d39ea52..9e5276f786 100644 --- a/docs/source/release-process-index.rst +++ b/docs/source/release-process-index.rst @@ -6,5 +6,4 @@ Release process release-notes changelog - publishing-corda codestyle \ No newline at end of file diff --git a/docs/source/resources/simple-tutorial-flow.png b/docs/source/resources/simple-tutorial-flow.png new file mode 100644 index 0000000000..7faf9a2462 Binary files /dev/null and b/docs/source/resources/simple-tutorial-flow.png differ diff --git a/docs/source/resources/simple-tutorial-transaction.png b/docs/source/resources/simple-tutorial-transaction.png new file mode 100644 index 0000000000..f881346b06 Binary files /dev/null and b/docs/source/resources/simple-tutorial-transaction.png differ diff --git a/docs/source/resources/tutorial-state.png b/docs/source/resources/tutorial-state.png index b394852fbe..11b0a0d385 100644 Binary files a/docs/source/resources/tutorial-state.png and b/docs/source/resources/tutorial-state.png differ diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index 97ee76af97..c17c81b874 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -262,22 +262,18 @@ folder has the following structure: . nodes ├── controller │   ├── corda.jar - │   ├── dependencies │   ├── node.conf │   └── plugins ├── nodea │   ├── corda.jar - │   ├── dependencies │   ├── node.conf │   └── plugins ├── nodeb │   ├── corda.jar - │   ├── dependencies │   ├── node.conf │   └── plugins ├── nodec │   ├── corda.jar - │   ├── dependencies │   ├── node.conf │   └── plugins ├── runnodes @@ -286,7 +282,7 @@ folder has the following structure: There will be one folder generated for each node you build (more on later when we get into the detail of the ``deployNodes`` Gradle task) and a ``runnodes`` shell script (batch file on Windows). -Each node folder contains the Corda JAR, a folder for dependencies and a folder for plugins (or CorDapps). There is also +Each node folder contains the Corda JAR and a folder for plugins (or CorDapps). There is also a node.conf file. See :doc:`Corda configuration files `. **Building from IntelliJ** @@ -340,7 +336,6 @@ When booted up, the node will generate a bunch of files and directories in addit ├── cache ├── certificates ├── corda.jar - ├── dependencies ├── identity-private-key ├── identity-public ├── logs diff --git a/docs/source/tutorial-custom-notary.rst b/docs/source/tutorial-custom-notary.rst new file mode 100644 index 0000000000..07be4fab06 --- /dev/null +++ b/docs/source/tutorial-custom-notary.rst @@ -0,0 +1,32 @@ +.. highlight:: kotlin + +Writing a custom notary service +=============================== + +.. warning:: Customising a notary service is an advanced feature and not recommended for most use-cases. Currently, + customising Raft or BFT notaries is not yet fully supported. If you want to write your own Raft notary you will have to + implement a custom database connector (or use a separate database for the notary), and use a custom configuration file. + +Similarly to writing an oracle service, the first step is to create a service class in your CorDapp and annotate it +with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The only requirement +is that the class provide a constructor with a single parameter of type ``PluginServiceHub``. + +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt + :language: kotlin + :start-after: START 1 + :end-before: END 1 + +The next step is to write a notary service flow. You are free to copy and modify the existing built-in flows such +as ``ValidatingNotaryFlow``, ``NonValidatingNotaryFlow``, or implement your own from scratch (following the +``NotaryFlow.Service`` template). Below is an example of a custom flow for a *validating* notary service: + +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt + :language: kotlin + :start-after: START 2 + :end-before: END 2 + +To ensure the custom notary is installed and advertised by the node, specify it in the configuration file: + +.. parsed-literal:: + + extraAdvertisedServiceIds : ["corda.notary.validating.mycustom"] diff --git a/docs/source/tutorial-integration-testing.rst b/docs/source/tutorial-integration-testing.rst index 3ab804b7b8..0cbed18a15 100644 --- a/docs/source/tutorial-integration-testing.rst +++ b/docs/source/tutorial-integration-testing.rst @@ -35,7 +35,9 @@ notary directly, so there's no need to pass in the test ``User``. The ``startNode`` function returns a future that completes once the node is fully started. This allows starting of the nodes to be parallel. We wait on these futures as we need the information -returned; their respective ``NodeHandles`` s. +returned; their respective ``NodeHandles`` s. After getting the handles we +wait for both parties to register with the network map to ensure we don't +have race conditions with network map registration. .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt :language: kotlin diff --git a/docs/source/vault.rst b/docs/source/vault.rst index c5a55638b6..37bf233843 100644 --- a/docs/source/vault.rst +++ b/docs/source/vault.rst @@ -46,7 +46,7 @@ Note the following: * the vault performs fungible state spending (and in future, fungible state optimisation management including merging, splitting and re-issuance) * vault extensions represent additional custom plugin code a developer may write to query specific custom contract state attributes. * customer "Off Ledger" (private store) represents internal organisational data that may be joined with the vault data to perform additional reporting or processing -* a :doc:`vault query API ` is exposed to developers using standard Corda RPC and CorDapp plugin mechanisms +* a :doc:`Vault Query API ` is exposed to developers using standard Corda RPC and CorDapp plugin mechanisms * a vault update API is internally used by transaction recording flows. * the vault database schemas are directly accessible via JDBC for customer joins and queries diff --git a/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt b/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt deleted file mode 100644 index 5ffe32e51d..0000000000 --- a/experimental/src/test/kotlin/net/corda/carpenter/ClassCarpenterTest.kt +++ /dev/null @@ -1,134 +0,0 @@ -package net.corda.carpenter - -import org.junit.Test -import java.lang.reflect.Field -import java.lang.reflect.Method -import kotlin.test.assertEquals - - -class ClassCarpenterTest { - interface DummyInterface { - val a: String - val b: Int - } - - val cc = ClassCarpenter() - - // We have to ignore synthetic fields even though ClassCarpenter doesn't create any because the JaCoCo - // coverage framework auto-magically injects one method and one field into every class loaded into the JVM. - val Class<*>.nonSyntheticFields: List get() = declaredFields.filterNot { it.isSynthetic } - val Class<*>.nonSyntheticMethods: List get() = declaredMethods.filterNot { it.isSynthetic } - - @Test - fun empty() { - val clazz = cc.build(ClassCarpenter.Schema("gen.EmptyClass", emptyMap(), null)) - assertEquals(0, clazz.nonSyntheticFields.size) - assertEquals(2, clazz.nonSyntheticMethods.size) // get, toString - assertEquals(0, clazz.declaredConstructors[0].parameterCount) - clazz.newInstance() // just test there's no exception. - } - - @Test - fun prims() { - val clazz = cc.build(ClassCarpenter.Schema("gen.Prims", mapOf( - "anIntField" to Int::class.javaPrimitiveType!!, - "aLongField" to Long::class.javaPrimitiveType!!, - "someCharField" to Char::class.javaPrimitiveType!!, - "aShortField" to Short::class.javaPrimitiveType!!, - "doubleTrouble" to Double::class.javaPrimitiveType!!, - "floatMyBoat" to Float::class.javaPrimitiveType!!, - "byteMe" to Byte::class.javaPrimitiveType!!, - "booleanField" to Boolean::class.javaPrimitiveType!! - ))) - assertEquals(8, clazz.nonSyntheticFields.size) - assertEquals(10, clazz.nonSyntheticMethods.size) - assertEquals(8, clazz.declaredConstructors[0].parameterCount) - val i = clazz.constructors[0].newInstance(1, 2L, 'c', 4.toShort(), 1.23, 4.56F, 127.toByte(), true) - assertEquals(1, clazz.getMethod("getAnIntField").invoke(i)) - assertEquals(2L, clazz.getMethod("getALongField").invoke(i)) - assertEquals('c', clazz.getMethod("getSomeCharField").invoke(i)) - assertEquals(4.toShort(), clazz.getMethod("getAShortField").invoke(i)) - assertEquals(1.23, clazz.getMethod("getDoubleTrouble").invoke(i)) - assertEquals(4.56F, clazz.getMethod("getFloatMyBoat").invoke(i)) - assertEquals(127.toByte(), clazz.getMethod("getByteMe").invoke(i)) - assertEquals(true, clazz.getMethod("getBooleanField").invoke(i)) - - val sfa = i as SimpleFieldAccess - assertEquals(1, sfa["anIntField"]) - assertEquals(2L, sfa["aLongField"]) - assertEquals('c', sfa["someCharField"]) - assertEquals(4.toShort(), sfa["aShortField"]) - assertEquals(1.23, sfa["doubleTrouble"]) - assertEquals(4.56F, sfa["floatMyBoat"]) - assertEquals(127.toByte(), sfa["byteMe"]) - assertEquals(true, sfa["booleanField"]) - } - - private fun genPerson(): Pair, Any> { - val clazz = cc.build(ClassCarpenter.Schema("gen.Person", mapOf( - "age" to Int::class.javaPrimitiveType!!, - "name" to String::class.java - ))) - val i = clazz.constructors[0].newInstance(32, "Mike") - return Pair(clazz, i) - } - - @Test - fun objs() { - val (clazz, i) = genPerson() - assertEquals("Mike", clazz.getMethod("getName").invoke(i)) - assertEquals("Mike", (i as SimpleFieldAccess)["name"]) - } - - @Test - fun `generated toString`() { - val (clazz, i) = genPerson() - assertEquals("Person{age=32, name=Mike}", i.toString()) - } - - @Test(expected = ClassCarpenter.DuplicateName::class) - fun duplicates() { - cc.build(ClassCarpenter.Schema("gen.EmptyClass", emptyMap(), null)) - cc.build(ClassCarpenter.Schema("gen.EmptyClass", emptyMap(), null)) - } - - @Test - fun `can refer to each other`() { - val (clazz1, i) = genPerson() - val clazz2 = cc.build(ClassCarpenter.Schema("gen.Referee", mapOf( - "ref" to clazz1 - ))) - val i2 = clazz2.constructors[0].newInstance(i) - assertEquals(i, (i2 as SimpleFieldAccess)["ref"]) - } - - @Test - fun superclasses() { - val schema1 = ClassCarpenter.Schema("gen.A", mapOf("a" to String::class.java)) - val schema2 = ClassCarpenter.Schema("gen.B", mapOf("b" to String::class.java), schema1) - val clazz = cc.build(schema2) - val i = clazz.constructors[0].newInstance("xa", "xb") as SimpleFieldAccess - assertEquals("xa", i["a"]) - assertEquals("xb", i["b"]) - assertEquals("B{a=xa, b=xb}", i.toString()) - } - - @Test - fun interfaces() { - val schema1 = ClassCarpenter.Schema("gen.A", mapOf("a" to String::class.java)) - val schema2 = ClassCarpenter.Schema("gen.B", mapOf("b" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java)) - val clazz = cc.build(schema2) - val i = clazz.constructors[0].newInstance("xa", 1) as DummyInterface - assertEquals("xa", i.a) - assertEquals(1, i.b) - } - - @Test(expected = ClassCarpenter.InterfaceMismatch::class) - fun `mismatched interface`() { - val schema1 = ClassCarpenter.Schema("gen.A", mapOf("a" to String::class.java)) - val schema2 = ClassCarpenter.Schema("gen.B", mapOf("c" to Int::class.java), schema1, interfaces = listOf(DummyInterface::class.java)) - val clazz = cc.build(schema2) - val i = clazz.constructors[0].newInstance("xa", 1) as DummyInterface - assertEquals(1, i.b) - } -} \ No newline at end of file diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt index e73e8d96db..c4c8767d95 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt @@ -4,7 +4,7 @@ import net.corda.contracts.BusinessCalendar import net.corda.contracts.FixOf import net.corda.contracts.Frequency import net.corda.contracts.Tenor -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt index 63e955647e..0e475216a3 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt @@ -2,7 +2,7 @@ package net.corda.contracts.universal import net.corda.contracts.FixOf import net.corda.contracts.Tenor -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/ContractDefinition.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/ContractDefinition.kt index 057c867490..e01f989b8d 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/ContractDefinition.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/ContractDefinition.kt @@ -2,7 +2,7 @@ package net.corda.contracts.universal import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.Party -import net.corda.core.utilities.ALICE +import net.corda.testing.ALICE import net.corda.testing.MEGA_CORP import net.corda.testing.MINI_CORP import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt index 73abc393cb..51c2ab4dd3 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt @@ -1,6 +1,6 @@ package net.corda.contracts.universal -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt index 755c9c2b98..e3cba5fdd7 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt @@ -1,6 +1,6 @@ package net.corda.contracts.universal -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt index 612dd47c25..abdc4bb445 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt @@ -3,7 +3,7 @@ package net.corda.contracts.universal import net.corda.contracts.FixOf import net.corda.contracts.Frequency import net.corda.contracts.Tenor -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt index a51fda60f4..aeccd75e75 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt @@ -1,7 +1,7 @@ package net.corda.contracts.universal import net.corda.contracts.Frequency -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Test import java.time.Instant diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt index 5e89376f77..e71bd6746c 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt @@ -2,7 +2,7 @@ package net.corda.contracts.universal import net.corda.contracts.Frequency import net.corda.contracts.Tenor -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt index 09b4aef290..3f41d0c260 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt @@ -1,6 +1,6 @@ package net.corda.contracts.universal -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Test import java.time.Instant diff --git a/finance/build.gradle b/finance/build.gradle index 6a554c33ec..3e4cae5813 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -5,6 +5,7 @@ apply plugin: 'kotlin-jpa' apply plugin: CanonicalizerPlugin apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.quasar-utils' +apply plugin: 'com.jfrog.artifactory' description 'Corda finance modules' diff --git a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java index bc1d505ff9..df6b29e5be 100644 --- a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java @@ -1,27 +1,36 @@ package net.corda.contracts; -import co.paralleluniverse.fibers.*; -import com.google.common.collect.*; -import kotlin.*; -import net.corda.contracts.asset.*; +import co.paralleluniverse.fibers.Suspendable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import kotlin.Pair; +import kotlin.Unit; +import net.corda.contracts.asset.CashKt; import net.corda.core.contracts.*; -import net.corda.core.contracts.Contract; -import net.corda.core.contracts.TransactionForContract.*; -import net.corda.core.contracts.clauses.*; -import net.corda.core.crypto.*; +import net.corda.core.contracts.TransactionForContract.InOutGroup; +import net.corda.core.contracts.clauses.AnyOf; +import net.corda.core.contracts.clauses.Clause; +import net.corda.core.contracts.clauses.ClauseVerifier; +import net.corda.core.contracts.clauses.GroupClauseVerifier; +import net.corda.core.crypto.SecureHash; +import net.corda.core.crypto.testing.NullPublicKey; import net.corda.core.identity.AbstractParty; import net.corda.core.identity.AnonymousParty; import net.corda.core.identity.Party; -import net.corda.core.node.services.*; -import net.corda.core.transactions.*; -import org.jetbrains.annotations.*; +import net.corda.core.node.services.VaultService; +import net.corda.core.transactions.TransactionBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.time.*; -import java.util.*; -import java.util.stream.*; +import java.time.Instant; +import java.util.Collections; +import java.util.Currency; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; -import static kotlin.collections.CollectionsKt.*; -import static net.corda.core.contracts.ContractsDSL.*; +import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; +import static net.corda.core.contracts.ContractsDSL.requireThat; /** @@ -166,7 +175,7 @@ public class JavaCommercialPaper implements Contract { State groupingKey) { AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class); // There should be only a single input due to aggregation above - State input = single(inputs); + State input = Iterables.getOnlyElement(inputs); if (!cmd.getSigners().contains(input.getOwner().getOwningKey())) throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); @@ -199,7 +208,7 @@ public class JavaCommercialPaper implements Contract { AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class); // There should be only a single input due to aggregation above - State input = single(inputs); + State input = Iterables.getOnlyElement(inputs); if (!cmd.getSigners().contains(input.getOwner().getOwningKey())) throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); @@ -240,7 +249,7 @@ public class JavaCommercialPaper implements Contract { @NotNull List> commands, State groupingKey) { AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class); - State output = single(outputs); + State output = Iterables.getOnlyElement(outputs); TimeWindow timeWindowCommand = tx.getTimeWindow(); Instant time = null == timeWindowCommand ? null diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt index 0bfeb4ea50..f3d2fcdb2e 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt @@ -13,7 +13,7 @@ import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.services.VaultService -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt index c8beea5c0e..0c3811556b 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt @@ -3,8 +3,8 @@ package net.corda.contracts import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.sumCashBy import net.corda.core.contracts.* -import net.corda.core.crypto.NULL_PARTY import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.services.VaultService diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt index f341aabe6c..f47fb51ec1 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt @@ -8,14 +8,17 @@ import net.corda.core.contracts.clauses.AllOf import net.corda.core.contracts.clauses.FirstOf import net.corda.core.contracts.clauses.GroupClauseVerifier import net.corda.core.contracts.clauses.verifyClause -import net.corda.core.crypto.* +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.newSecureRandom +import net.corda.core.crypto.testing.NULL_PARTY +import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.Emoji import net.corda.schemas.CashSchemaV1 diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt index a71f948fa9..dbc7cf54dc 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt @@ -8,19 +8,17 @@ import net.corda.contracts.asset.Obligation.Lifecycle.NORMAL import net.corda.contracts.clause.* import net.corda.core.contracts.* import net.corda.core.contracts.clauses.* -import net.corda.core.crypto.NULL_PARTY import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.Emoji import net.corda.core.utilities.NonEmptySet -import net.corda.core.utilities.TEST_TX_TIME -import net.corda.core.utilities.nonEmptySetOf import org.bouncycastle.asn1.x500.X500Name import java.math.BigInteger import java.security.PublicKey @@ -429,7 +427,7 @@ class Obligation

: Contract { /** * Generate a transaction performing close-out netting of two or more states. * - * @param signer the party who will sign the transaction. Must be one of the obligor or beneficiary. + * @param signer the party which will sign the transaction. Must be one of the obligor or beneficiary. * @param states two or more states, which must be compatible for bilateral netting (same issuance definitions, * and same parties involved). */ @@ -458,7 +456,7 @@ class Obligation

: Contract { * @param amountIssued the amount to be exited, represented as a quantity of issued currency. * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is * the responsibility of the caller to check that they do not exit funds held by others. - * @return the public keys who must sign the transaction for it to be valid. + * @return the public keys which must sign the transaction for it to be valid. */ @Suppress("unused") fun generateExit(tx: TransactionBuilder, amountIssued: Amount>>, @@ -543,7 +541,7 @@ class Obligation

: Contract { } tx.addCommand(Commands.SetLifecycle(lifecycle), partiesUsed.map { it.owningKey }.distinct()) } - tx.addTimeWindow(issuanceDef.dueBefore, issuanceDef.timeTolerance) + tx.setTimeWindow(issuanceDef.dueBefore, issuanceDef.timeTolerance) } /** @@ -727,8 +725,3 @@ infix fun Obligation.State.`issued by`(party: AbstractParty) = copy val DUMMY_OBLIGATION_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) } /** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */ val DUMMY_OBLIGATION_ISSUER by lazy { Party(X500Name("CN=Snake Oil Issuer,O=R3,OU=corda,L=London,C=GB"), DUMMY_OBLIGATION_ISSUER_KEY.public) } - -val Issued.OBLIGATION_DEF: Obligation.Terms - get() = Obligation.Terms(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME) -val Amount>.OBLIGATION: Obligation.State - get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER, token.OBLIGATION_DEF, quantity, NULL_PARTY) diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt b/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt index a6d3bd4185..383ddab54d 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt @@ -207,13 +207,15 @@ abstract class OnLedgerAsset> : C @JvmStatic fun , T: Any> generateIssue(tx: TransactionBuilder, transactionState: TransactionState, - issueCommand: CommandData) { + issueCommand: CommandData): Set { check(tx.inputStates().isEmpty()) check(tx.outputStates().map { it.data }.filterIsInstance(transactionState.javaClass).isEmpty()) require(transactionState.data.amount.quantity > 0) val at = transactionState.data.amount.token.issuer + val commandSigner = at.party.owningKey tx.addOutputState(transactionState) - tx.addCommand(issueCommand, at.party.owningKey) + tx.addCommand(issueCommand, commandSigner) + return setOf(commandSigner) } } diff --git a/finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt b/finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt index 7b32fd8afb..52bef42f21 100644 --- a/finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt @@ -3,30 +3,45 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker /** * Initiates a flow that produces an Issue/Move or Exit Cash transaction. */ -abstract class AbstractCashFlow(override val progressTracker: ProgressTracker) : FlowLogic() { +abstract class AbstractCashFlow(override val progressTracker: ProgressTracker) : FlowLogic() { companion object { + object GENERATING_ID : ProgressTracker.Step("Generating anonymous identities") object GENERATING_TX : ProgressTracker.Step("Generating transaction") object SIGNING_TX : ProgressTracker.Step("Signing transaction") object FINALISING_TX : ProgressTracker.Step("Finalising transaction") - fun tracker() = ProgressTracker(GENERATING_TX, SIGNING_TX, FINALISING_TX) + fun tracker() = ProgressTracker(GENERATING_ID, GENERATING_TX, SIGNING_TX, FINALISING_TX) } @Suspendable - internal fun finaliseTx(participants: Set, tx: SignedTransaction, message: String) { + protected fun finaliseTx(participants: Set, tx: SignedTransaction, message: String) { try { subFlow(FinalityFlow(tx, participants)) } catch (e: NotaryException) { throw CashException(message, e) } } + + /** + * Combined signed transaction and identity lookup map, which is the resulting data from regular cash flows. + * Specialised flows for unit tests differ from this. + * + * @param stx the signed transaction. + * @param recipient the identity used for the other side of the transaction, where applicable (i.e. this is + * null for exit transactions). For anonymous transactions this is the confidential identity generated for the + * transaction, otherwise this is the well known identity. + */ + @CordaSerializable + data class Result(val stx: SignedTransaction, val recipient: AbstractParty?) } class CashException(message: String, cause: Throwable) : FlowException(message, cause) \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt index 7a9e441668..9a363fffc2 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt @@ -8,8 +8,7 @@ import net.corda.core.contracts.TransactionType import net.corda.core.contracts.issuedBy import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party -import net.corda.core.serialization.OpaqueBytes -import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import java.util.* @@ -22,16 +21,20 @@ import java.util.* * issuer. */ @StartableByRPC -class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { +class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { constructor(amount: Amount, issueRef: OpaqueBytes) : this(amount, issueRef, tracker()) companion object { fun tracker() = ProgressTracker(GENERATING_TX, SIGNING_TX, FINALISING_TX) } + /** + * @return the signed transaction, and a mapping of parties to new anonymous identities generated + * (for this flow this map is always empty). + */ @Suspendable @Throws(CashException::class) - override fun call(): SignedTransaction { + override fun call(): AbstractCashFlow.Result { progressTracker.currentStep = GENERATING_TX val builder: TransactionBuilder = TransactionType.General.Builder(notary = null as Party?) val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) @@ -67,6 +70,6 @@ class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, prog // Commit the transaction progressTracker.currentStep = FINALISING_TX finaliseTx(participants, tx, "Unable to notarise exit") - return tx + return Result(tx, null) } } diff --git a/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt b/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt index 69fcdd91b9..446cca5b4d 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt @@ -5,15 +5,14 @@ import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow -import net.corda.core.serialization.OpaqueBytes -import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.OpaqueBytes import java.util.* /** * A command to initiate the cash flow with. */ sealed class CashFlowCommand { - abstract fun startFlow(proxy: CordaRPCOps): FlowHandle + abstract fun startFlow(proxy: CordaRPCOps): FlowHandle /** * A command to initiate the Cash flow with. @@ -21,8 +20,9 @@ sealed class CashFlowCommand { data class IssueCash(val amount: Amount, val issueRef: OpaqueBytes, val recipient: Party, - val notary: Party) : CashFlowCommand() { - override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashIssueFlow, amount, issueRef, recipient, notary) + val notary: Party, + val anonymous: Boolean) : CashFlowCommand() { + override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashIssueFlow, amount, issueRef, recipient, notary, anonymous) } /** @@ -31,8 +31,9 @@ sealed class CashFlowCommand { * @param amount the amount of currency to issue on to the ledger. * @param recipient the party to issue the cash to. */ - data class PayCash(val amount: Amount, val recipient: Party, val issuerConstraint: Party? = null) : CashFlowCommand() { - override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashPaymentFlow, amount, recipient) + data class PayCash(val amount: Amount, val recipient: Party, val issuerConstraint: Party? = null, + val anonymous: Boolean) : CashFlowCommand() { + override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashPaymentFlow, amount, recipient, anonymous) } /** diff --git a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt index 479e1a1d6d..ec9659f1d5 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt @@ -5,10 +5,9 @@ import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.TransactionType import net.corda.core.contracts.issuedBy -import net.corda.core.identity.Party import net.corda.core.flows.StartableByRPC -import net.corda.core.serialization.OpaqueBytes -import net.corda.core.transactions.SignedTransaction +import net.corda.core.identity.Party +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import java.util.* @@ -26,23 +25,35 @@ class CashIssueFlow(val amount: Amount, val issueRef: OpaqueBytes, val recipient: Party, val notary: Party, - progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { + val anonymous: Boolean, + progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { constructor(amount: Amount, issueRef: OpaqueBytes, recipient: Party, - notary: Party) : this(amount, issueRef, recipient, notary, tracker()) + notary: Party) : this(amount, issueRef, recipient, notary, true, tracker()) + constructor(amount: Amount, + issueRef: OpaqueBytes, + recipient: Party, + notary: Party, + anonymous: Boolean) : this(amount, issueRef, recipient, notary, anonymous, tracker()) @Suspendable - override fun call(): SignedTransaction { + override fun call(): AbstractCashFlow.Result { + progressTracker.currentStep = GENERATING_ID + val txIdentities = if (anonymous) { + subFlow(TransactionKeyFlow(recipient)) + } else { + emptyMap() + } + val anonymousRecipient = txIdentities.get(recipient)?.identity ?: recipient progressTracker.currentStep = GENERATING_TX val builder: TransactionBuilder = TransactionType.General.Builder(notary = notary) val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) - // TODO: Get a transaction key, don't just re-use the owning key - Cash().generateIssue(builder, amount.issuedBy(issuer), recipient, notary) + val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), anonymousRecipient, notary) progressTracker.currentStep = SIGNING_TX - val tx = serviceHub.signInitialTransaction(builder) + val tx = serviceHub.signInitialTransaction(builder, signers) progressTracker.currentStep = FINALISING_TX subFlow(FinalityFlow(tx)) - return tx + return Result(tx, anonymousRecipient) } } diff --git a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt index b7642d24c1..0567e2c77e 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt @@ -6,7 +6,6 @@ import net.corda.core.contracts.InsufficientBalanceException import net.corda.core.contracts.TransactionType import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party -import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import java.util.* @@ -17,18 +16,30 @@ import java.util.* * @param amount the amount of a currency to pay to the recipient. * @param recipient the party to pay the currency to. * @param issuerConstraint if specified, the payment will be made using only cash issued by the given parties. + * @param anonymous whether to anonymous the recipient party. Should be true for normal usage, but may be false + * for testing purposes. */ @StartableByRPC open class CashPaymentFlow( val amount: Amount, val recipient: Party, + val anonymous: Boolean, progressTracker: ProgressTracker, - val issuerConstraint: Set? = null) : AbstractCashFlow(progressTracker) { + val issuerConstraint: Set? = null) : AbstractCashFlow(progressTracker) { /** A straightforward constructor that constructs spends using cash states of any issuer. */ - constructor(amount: Amount, recipient: Party) : this(amount, recipient, tracker()) + constructor(amount: Amount, recipient: Party) : this(amount, recipient, true, tracker()) + /** A straightforward constructor that constructs spends using cash states of any issuer. */ + constructor(amount: Amount, recipient: Party, anonymous: Boolean) : this(amount, recipient, anonymous, tracker()) @Suspendable - override fun call(): SignedTransaction { + override fun call(): AbstractCashFlow.Result { + progressTracker.currentStep = GENERATING_ID + val txIdentities = if (anonymous) { + subFlow(TransactionKeyFlow(recipient)) + } else { + emptyMap() + } + val anonymousRecipient = txIdentities.get(recipient)?.identity ?: recipient progressTracker.currentStep = GENERATING_TX val builder: TransactionBuilder = TransactionType.General.Builder(null as Party?) // TODO: Have some way of restricting this to states the caller controls @@ -36,8 +47,7 @@ open class CashPaymentFlow( serviceHub.vaultService.generateSpend( builder, amount, - // TODO: Get a transaction key, don't just re-use the owning key - recipient, + anonymousRecipient, issuerConstraint) } catch (e: InsufficientBalanceException) { throw CashException("Insufficient cash for spend: ${e.message}", e) @@ -48,6 +58,6 @@ open class CashPaymentFlow( progressTracker.currentStep = FINALISING_TX finaliseTx(setOf(recipient), tx, "Unable to notarise spend") - return tx + return Result(tx, anonymousRecipient) } } \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt index 50bfe72327..72d11fe0c7 100644 --- a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt @@ -1,11 +1,12 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.asset.Cash import net.corda.core.contracts.* import net.corda.core.flows.* 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 net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap @@ -20,21 +21,42 @@ import java.util.* */ object IssuerFlow { @CordaSerializable - data class IssuanceRequestState(val amount: Amount, val issueToParty: Party, val issuerPartyRef: OpaqueBytes) + data class IssuanceRequestState(val amount: Amount, + val issueToParty: Party, + val issuerPartyRef: OpaqueBytes, + val notaryParty: Party, + val anonymous: Boolean) /** * IssuanceRequester should be used by a client to ask a remote node to issue some [FungibleAsset] with the given details. * Returns the transaction created by the Issuer to move the cash to the Requester. + * + * @param anonymous true if the issued asset should be sent to a new confidential identity, false to send it to the + * well known identity (generally this is only used in testing). */ @InitiatingFlow @StartableByRPC - class IssuanceRequester(val amount: Amount, val issueToParty: Party, val issueToPartyRef: OpaqueBytes, - val issuerBankParty: Party) : FlowLogic() { + class IssuanceRequester(val amount: Amount, + val issueToParty: Party, + val issueToPartyRef: OpaqueBytes, + val issuerBankParty: Party, + val notaryParty: Party, + val anonymous: Boolean) : FlowLogic() { @Suspendable @Throws(CashException::class) - override fun call(): SignedTransaction { - val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef) - return sendAndReceive(issuerBankParty, issueRequest).unwrap { it } + override fun call(): AbstractCashFlow.Result { + val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef, notaryParty, anonymous) + return sendAndReceive(issuerBankParty, issueRequest).unwrap { res -> + val tx = res.stx.tx + val expectedAmount = Amount(amount.quantity, Issued(issuerBankParty.ref(issueToPartyRef), amount.token)) + val cashOutputs = tx.outputs + .map { it.data} + .filterIsInstance() + .filter { state -> state.owner == res.recipient } + require(cashOutputs.size == 1) { "Require a single cash output paying ${res.recipient}, found ${tx.outputs}" } + require(cashOutputs.single().amount == expectedAmount) { "Require payment of $expectedAmount"} + res + } } } @@ -66,22 +88,22 @@ object IssuerFlow { it } // TODO: parse request to determine Asset to issue - val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef) + val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef, issueRequest.notaryParty, issueRequest.anonymous) progressTracker.currentStep = SENDING_CONFIRM send(otherParty, txn) - return txn + return txn.stx } @Suspendable private fun issueCashTo(amount: Amount, issueTo: Party, - issuerPartyRef: OpaqueBytes): SignedTransaction { - // TODO: pass notary in as request parameter - val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity + issuerPartyRef: OpaqueBytes, + notaryParty: Party, + anonymous: Boolean): AbstractCashFlow.Result { // invoke Cash subflow to issue Asset progressTracker.currentStep = ISSUING - val bankOfCordaParty = serviceHub.myInfo.legalIdentity - val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, bankOfCordaParty, notaryParty) + val issueRecipient = serviceHub.myInfo.legalIdentity + val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous) val issueTx = subFlow(issueCashFlow) // NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger) // short-circuit when issuing to self @@ -89,7 +111,7 @@ object IssuerFlow { return issueTx // now invoke Cash subflow to Move issued assetType to issue requester progressTracker.currentStep = TRANSFERRING - val moveCashFlow = CashPaymentFlow(amount, issueTo) + val moveCashFlow = CashPaymentFlow(amount, issueTo, anonymous) val moveTx = subFlow(moveCashFlow) // NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger) return moveTx diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt b/finance/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt index d9ebadad53..8509e55c9b 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt @@ -183,9 +183,9 @@ object TwoPartyDealFlow { val deal = handshake.payload.dealBeingOffered val ptx = deal.generateAgreement(handshake.payload.notary) - // And add a request for a time-window: it may be that none of the contracts need this! + // We set the transaction's time-window: it may be that none of the contracts need this! // But it can't hurt to have one. - ptx.addTimeWindow(serviceHub.clock.instant(), 30.seconds) + ptx.setTimeWindow(serviceHub.clock.instant(), 30.seconds) return Pair(ptx, arrayListOf(deal.participants.single { it == serviceHub.myInfo.legalIdentity as AbstractParty }.owningKey)) } } diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt index bb7f4eda86..d384e1255e 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt @@ -5,6 +5,7 @@ import net.corda.contracts.asset.sumCashBy import net.corda.core.contracts.* import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic +import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.NodeInfo @@ -49,14 +50,14 @@ object TwoPartyTradeFlow { data class SellerTradeInfo( val assetForSale: StateAndRef, val price: Amount, - val sellerOwnerKey: PublicKey + val sellerOwner: AbstractParty ) open class Seller(val otherParty: Party, val notaryNode: NodeInfo, val assetToSell: StateAndRef, val price: Amount, - val myKey: PublicKey, + val me: AbstractParty, override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic() { companion object { @@ -75,7 +76,7 @@ object TwoPartyTradeFlow { override fun call(): SignedTransaction { progressTracker.currentStep = AWAITING_PROPOSAL // Make the first message we'll send to kick off the flow. - val hello = SellerTradeInfo(assetToSell, price, myKey) + val hello = SellerTradeInfo(assetToSell, price, me) // What we get back from the other side is a transaction that *might* be valid and acceptable to us, // but we must check it out thoroughly before we sign! send(otherParty, hello) @@ -85,7 +86,7 @@ object TwoPartyTradeFlow { // DOCSTART 5 val signTransactionFlow = object : SignTransactionFlow(otherParty, VERIFYING_AND_SIGNING.childProgressTracker()) { override fun checkTransaction(stx: SignedTransaction) { - if (stx.tx.outputs.map { it.data }.sumCashBy(AnonymousParty(myKey)).withoutIssuer() != price) + if (stx.tx.outputs.map { it.data }.sumCashBy(me).withoutIssuer() != price) throw FlowException("Transaction is not sending us the right amount of cash") } } @@ -181,7 +182,7 @@ object TwoPartyTradeFlow { val ptx = TransactionType.General.Builder(notary) // Add input and output states for the movement of cash, by using the Cash contract to generate the states - val (tx, cashSigningPubKeys) = serviceHub.vaultService.generateSpend(ptx, tradeRequest.price, AnonymousParty(tradeRequest.sellerOwnerKey)) + val (tx, cashSigningPubKeys) = serviceHub.vaultService.generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwner) // Add inputs/outputs/a command for the movement of the asset. tx.addInputState(tradeRequest.assetForSale) @@ -195,10 +196,10 @@ object TwoPartyTradeFlow { tx.addOutputState(state, tradeRequest.assetForSale.state.notary) tx.addCommand(command, tradeRequest.assetForSale.state.data.owner.owningKey) - // And add a request for a time-window: it may be that none of the contracts need this! + // We set the transaction's time-window: it may be that none of the contracts need this! // But it can't hurt to have one. val currentTime = serviceHub.clock.instant() - tx.addTimeWindow(currentTime, 30.seconds) + tx.setTimeWindow(currentTime, 30.seconds) return Pair(tx, cashSigningPubKeys) } // DOCEND 1 diff --git a/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt b/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt index 05bc37c1c3..e2ede7a16e 100644 --- a/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt @@ -2,7 +2,6 @@ package net.corda.schemas import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 import javax.persistence.* /** diff --git a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java index 2c8b2d4595..ed091c6104 100644 --- a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java +++ b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java @@ -3,13 +3,13 @@ package net.corda.contracts.asset; import kotlin.Unit; import net.corda.core.contracts.PartyAndReference; import net.corda.core.identity.AnonymousParty; -import net.corda.core.serialization.OpaqueBytes; +import net.corda.core.utilities.OpaqueBytes; import org.junit.Test; import static net.corda.core.contracts.ContractsDSL.DOLLARS; import static net.corda.core.contracts.ContractsDSL.issuedBy; -import static net.corda.core.utilities.TestConstants.getDUMMY_PUBKEY_1; -import static net.corda.core.utilities.TestConstants.getDUMMY_PUBKEY_2; +import static net.corda.testing.TestConstants.getDUMMY_PUBKEY_1; +import static net.corda.testing.TestConstants.getDUMMY_PUBKEY_2; import static net.corda.testing.CoreTestUtils.*; /** diff --git a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt index 4b9c409a85..35bf37e01e 100644 --- a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt @@ -1,7 +1,7 @@ package net.corda.contracts import net.corda.contracts.asset.* -import net.corda.contracts.testing.fillWithSomeTestCash +import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.core.contracts.* import net.corda.core.days import net.corda.core.identity.AnonymousParty @@ -10,7 +10,6 @@ import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.* import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.testing.* @@ -205,6 +204,8 @@ class CommercialPaperTestsGeneric { private lateinit var aliceVaultService: VaultService private lateinit var alicesVault: Vault + private val notaryServices = MockServices(DUMMY_NOTARY_KEY) + private lateinit var moveTX: SignedTransaction @Test @@ -215,12 +216,12 @@ class CommercialPaperTestsGeneric { val databaseAlice = dataSourceAndDatabaseAlice.second databaseAlice.transaction { - aliceServices = object : MockServices() { + aliceServices = object : MockServices(ALICE_KEY) { override val vaultService: VaultService = makeVaultService(dataSourcePropsAlice) override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) @@ -235,12 +236,12 @@ class CommercialPaperTestsGeneric { val databaseBigCorp = dataSourceAndDatabaseBigCorp.second databaseBigCorp.transaction { - bigCorpServices = object : MockServices() { + bigCorpServices = object : MockServices(BIG_CORP_KEY) { override val vaultService: VaultService = makeVaultService(dataSourcePropsBigCorp) override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) @@ -251,35 +252,33 @@ class CommercialPaperTestsGeneric { } // Propagate the cash transactions to each side. - aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) - bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) + aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.validatedTransactions.getTransaction(it.ref.txhash)!! }) + bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.validatedTransactions.getTransaction(it.ref.txhash)!! }) // BigCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER val issuance = bigCorpServices.myInfo.legalIdentity.ref(1) - val issueTX: SignedTransaction = - CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - addTimeWindow(TEST_TX_TIME, 30.seconds) - signWith(bigCorpServices.key) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + val issueBuilder = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY) + issueBuilder.setTimeWindow(TEST_TX_TIME, 30.seconds) + val issuePtx = bigCorpServices.signInitialTransaction(issueBuilder) + val issueTx = notaryServices.addSignature(issuePtx) databaseAlice.transaction { // Alice pays $9000 to BigCorp to own some of their debt. moveTX = run { - val ptx = TransactionType.General.Builder(DUMMY_NOTARY) - aliceVaultService.generateSpend(ptx, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public)) - CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), AnonymousParty(aliceServices.key.public)) - ptx.signWith(bigCorpServices.key) - ptx.signWith(aliceServices.key) - ptx.signWith(DUMMY_NOTARY_KEY) - ptx.toSignedTransaction() + val builder = TransactionType.General.Builder(DUMMY_NOTARY) + aliceVaultService.generateSpend(builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public)) + CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public)) + val ptx = aliceServices.signInitialTransaction(builder) + val ptx2 = bigCorpServices.addSignature(ptx) + val stx = notaryServices.addSignature(ptx2) + stx } } databaseBigCorp.transaction { // Verify the txns are valid and insert into both sides. - listOf(issueTX, moveTX).forEach { + listOf(issueTx, moveTX).forEach { it.toLedgerTransaction(aliceServices).verify() aliceServices.recordTransactions(it) bigCorpServices.recordTransactions(it) @@ -288,13 +287,13 @@ class CommercialPaperTestsGeneric { databaseBigCorp.transaction { fun makeRedeemTX(time: Instant): Pair { - val ptx = TransactionType.General.Builder(DUMMY_NOTARY) - ptx.addTimeWindow(time, 30.seconds) - CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpVaultService) - ptx.signWith(aliceServices.key) - ptx.signWith(bigCorpServices.key) - ptx.signWith(DUMMY_NOTARY_KEY) - return Pair(ptx.toSignedTransaction(), ptx.lockId) + val builder = TransactionType.General.Builder(DUMMY_NOTARY) + builder.setTimeWindow(time, 30.seconds) + CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpVaultService) + val ptx = aliceServices.signInitialTransaction(builder) + val ptx2 = bigCorpServices.addSignature(ptx) + val stx = notaryServices.addSignature(ptx2) + return Pair(stx, builder.lockId) } val redeemTX = makeRedeemTX(TEST_TX_TIME + 10.days) diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index 637d5d62a5..5a0771ed3d 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -1,7 +1,8 @@ package net.corda.contracts.asset -import net.corda.contracts.testing.fillWithSomeTestCash +import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.core.contracts.* +import net.corda.testing.contracts.DummyState import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty @@ -9,10 +10,9 @@ import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.services.VaultService import net.corda.core.node.services.unconsumedStates -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.* import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction @@ -43,8 +43,8 @@ class CashTests { amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref)))) ) - lateinit var services: MockServices - val vault: VaultService get() = services.vaultService + lateinit var miniCorpServices: MockServices + val vault: VaultService get() = miniCorpServices.vaultService lateinit var dataSource: Closeable lateinit var database: Database lateinit var vaultStatesUnconsumed: List> @@ -57,29 +57,29 @@ class CashTests { dataSource = dataSourceAndDatabase.first database = dataSourceAndDatabase.second database.transaction { - services = object : MockServices() { + miniCorpServices = object : MockServices(MINI_CORP_KEY) { override val keyManagementService: MockKeyManagementService = MockKeyManagementService(identityService, MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY) override val vaultService: VaultService = makeVaultService(dataSourceProps) override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) } } - services.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, + miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_IDENTITY_1) - services.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, + miniCorpServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_IDENTITY_1) - services.fillWithSomeTestCash(howMuch = 80.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, + miniCorpServices.fillWithSomeTestCash(howMuch = 80.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_IDENTITY_1) - services.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1, + miniCorpServices.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_IDENTITY_1) - vaultStatesUnconsumed = services.vaultService.unconsumedStates().toList() + vaultStatesUnconsumed = miniCorpServices.vaultService.unconsumedStates().toList() } } @@ -160,8 +160,7 @@ class CashTests { // Test generation works. val tx: WireTransaction = TransactionType.General.Builder(notary = null).apply { Cash().generateIssue(this, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = AnonymousParty(DUMMY_PUBKEY_1), notary = DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertTrue(tx.inputs.isEmpty()) val s = tx.outputs[0].data as Cash.State assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount) @@ -177,8 +176,7 @@ class CashTests { val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34) val tx: WireTransaction = TransactionType.General.Builder(notary = null).apply { Cash().generateIssue(this, amount, owner = AnonymousParty(DUMMY_PUBKEY_1), notary = DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertTrue(tx.inputs.isEmpty()) assertEquals(tx.outputs[0], tx.outputs[0]) } @@ -250,8 +248,7 @@ class CashTests { var ptx = TransactionType.General.Builder(DUMMY_NOTARY) Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY) - ptx.signWith(MINI_CORP_KEY) - val tx = ptx.toSignedTransaction() + val tx = miniCorpServices.signInitialTransaction(ptx) // Include the previously issued cash in a new issuance command ptx = TransactionType.General.Builder(DUMMY_NOTARY) diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt index 58edc73e78..5d393f71db 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt @@ -4,15 +4,19 @@ import net.corda.contracts.Commodity import net.corda.contracts.NetType import net.corda.contracts.asset.Obligation.Lifecycle import net.corda.core.contracts.* -import net.corda.core.crypto.NULL_PARTY +import net.corda.testing.contracts.DummyState import net.corda.core.crypto.SecureHash +import net.corda.core.hours +import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.* import net.corda.testing.* +import net.corda.testing.node.MockServices import org.junit.Test import java.time.Duration +import java.time.Instant import java.time.temporal.ChronoUnit import java.util.* import kotlin.test.assertEquals @@ -21,14 +25,14 @@ import kotlin.test.assertNotEquals import kotlin.test.assertTrue class ObligationTests { - val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) + val defaultRef = OpaqueBytes.of(1) val defaultIssuer = MEGA_CORP.ref(defaultRef) val oneMillionDollars = 1000000.DOLLARS `issued by` defaultIssuer val trustedCashContract = nonEmptySetOf(SecureHash.randomSHA256() as SecureHash) val megaIssuedDollars = nonEmptySetOf(Issued(defaultIssuer, USD)) val megaIssuedPounds = nonEmptySetOf(Issued(defaultIssuer, GBP)) - val fivePm = TEST_TX_TIME.truncatedTo(ChronoUnit.DAYS).plus(17, ChronoUnit.HOURS) - val sixPm = fivePm.plus(1, ChronoUnit.HOURS) + val fivePm: Instant = TEST_TX_TIME.truncatedTo(ChronoUnit.DAYS) + 17.hours + val sixPm: Instant = fivePm + 1.hours val megaCorpDollarSettlement = Obligation.Terms(trustedCashContract, megaIssuedDollars, fivePm) val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuedProducts = megaIssuedPounds) val inState = Obligation.State( @@ -39,6 +43,8 @@ class ObligationTests { beneficiary = CHARLIE ) val outState = inState.copy(beneficiary = AnonymousParty(DUMMY_PUBKEY_2)) + val miniCorpServices = MockServices(MINI_CORP_KEY) + val notaryServices = MockServices(DUMMY_NOTARY_KEY) private fun cashObligationTestRoots( group: LedgerDSL @@ -125,8 +131,7 @@ class ObligationTests { val tx = TransactionType.General.Builder(notary = null).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, beneficiary = CHARLIE, notary = DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertTrue(tx.inputs.isEmpty()) val expected = Obligation.State( obligor = MINI_CORP, @@ -203,12 +208,12 @@ class ObligationTests { val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, beneficiary = MINI_CORP, notary = DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - }.toSignedTransaction() + }.toWireTransaction() + // Include the previously issued obligation in a new issuance command val ptx = TransactionType.General.Builder(DUMMY_NOTARY) - ptx.addInputState(tx.tx.outRef>(0)) + ptx.addInputState(tx.outRef>(0)) Obligation().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, beneficiary = MINI_CORP, notary = DUMMY_NOTARY) } @@ -220,9 +225,7 @@ class ObligationTests { val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateCloseOutNetting(this, ALICE, obligationAliceToBob, obligationBobToAlice) - signWith(ALICE_KEY) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertEquals(0, tx.outputs.size) } @@ -233,9 +236,7 @@ class ObligationTests { val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateCloseOutNetting(this, ALICE, obligationAliceToBob, obligationBobToAlice) - signWith(ALICE_KEY) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertEquals(1, tx.outputs.size) val actual = tx.outputs[0].data @@ -249,10 +250,7 @@ class ObligationTests { val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generatePaymentNetting(this, obligationAliceToBob.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) - signWith(ALICE_KEY) - signWith(BOB_KEY) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertEquals(0, tx.outputs.size) } @@ -263,9 +261,7 @@ class ObligationTests { val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE) val tx = TransactionType.General.Builder(null).apply { Obligation().generatePaymentNetting(this, obligationAliceToBob.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) - signWith(ALICE_KEY) - signWith(BOB_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertEquals(1, tx.outputs.size) val expected = obligationBobToAlice.copy(quantity = obligationBobToAlice.quantity - obligationAliceToBob.quantity) val actual = tx.outputs[0].data @@ -282,30 +278,31 @@ class ObligationTests { var tx = TransactionType.General.Builder(null).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement.copy(dueBefore = dueBefore), 100.DOLLARS.quantity, beneficiary = MINI_CORP, notary = DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - }.toSignedTransaction() - var stateAndRef = tx.tx.outRef>(0) + } + var stx = miniCorpServices.signInitialTransaction(tx) + var stateAndRef = stx.tx.outRef>(0) // Now generate a transaction marking the obligation as having defaulted tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateSetLifecycle(this, listOf(stateAndRef), Lifecycle.DEFAULTED, DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() - assertEquals(1, tx.tx.outputs.size) - assertEquals(stateAndRef.state.data.copy(lifecycle = Lifecycle.DEFAULTED), tx.tx.outputs[0].data) - tx.verifySignatures() + } + var ptx = miniCorpServices.signInitialTransaction(tx, MINI_CORP_PUBKEY) + stx = notaryServices.addSignature(ptx) + + assertEquals(1, stx.tx.outputs.size) + assertEquals(stateAndRef.state.data.copy(lifecycle = Lifecycle.DEFAULTED), stx.tx.outputs[0].data) + stx.verifySignatures() // And set it back - stateAndRef = tx.tx.outRef>(0) + stateAndRef = stx.tx.outRef>(0) tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateSetLifecycle(this, listOf(stateAndRef), Lifecycle.NORMAL, DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() - assertEquals(1, tx.tx.outputs.size) - assertEquals(stateAndRef.state.data.copy(lifecycle = Lifecycle.NORMAL), tx.tx.outputs[0].data) - tx.verifySignatures() + } + ptx = miniCorpServices.signInitialTransaction(tx) + stx = notaryServices.addSignature(ptx) + assertEquals(1, stx.tx.outputs.size) + assertEquals(stateAndRef.state.data.copy(lifecycle = Lifecycle.NORMAL), stx.tx.outputs[0].data) + stx.verifySignatures() } /** Test generating a transaction to settle an obligation. */ @@ -313,22 +310,18 @@ class ObligationTests { fun `generate settlement transaction`() { val cashTx = TransactionType.General.Builder(null).apply { Cash().generateIssue(this, 100.DOLLARS `issued by` defaultIssuer, MINI_CORP, DUMMY_NOTARY) - signWith(MEGA_CORP_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() // Generate a transaction issuing the obligation val obligationTx = TransactionType.General.Builder(null).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, beneficiary = MINI_CORP, notary = DUMMY_NOTARY) - signWith(MINI_CORP_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() // Now generate a transaction settling the obligation val settleTx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateSettle(this, listOf(obligationTx.outRef(0)), listOf(cashTx.outRef(0)), Cash.Commands.Move(), DUMMY_NOTARY) - signWith(DUMMY_NOTARY_KEY) - signWith(MINI_CORP_KEY) - }.toSignedTransaction().tx + }.toWireTransaction() assertEquals(2, settleTx.inputs.size) assertEquals(1, settleTx.outputs.size) } @@ -864,7 +857,7 @@ class ObligationTests { @Test fun `summing balances due between parties`() { val simple: Map, Amount> = mapOf(Pair(Pair(ALICE, BOB), Amount(100000000, GBP))) - val expected: Map = mapOf(Pair(ALICE, -100000000L), Pair(BOB, 100000000L)) + val expected: Map = mapOf(Pair(ALICE, -100000000L), Pair(BOB, 100000000L)) val actual = sumAmountsDue(simple) assertEquals(expected, actual) } @@ -880,4 +873,9 @@ class ObligationTests { val actual = sumAmountsDue(balanced) assertEquals(expected, actual) } + + val Issued.OBLIGATION_DEF: Obligation.Terms + get() = Obligation.Terms(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME) + val Amount>.OBLIGATION: Obligation.State + get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER, token.OBLIGATION_DEF, quantity, NULL_PARTY) } diff --git a/finance/src/test/kotlin/net/corda/contracts/testing/Generators.kt b/finance/src/test/kotlin/net/corda/contracts/testing/Generators.kt index fe65daa626..1ca4c5d998 100644 --- a/finance/src/test/kotlin/net/corda/contracts/testing/Generators.kt +++ b/finance/src/test/kotlin/net/corda/contracts/testing/Generators.kt @@ -9,7 +9,7 @@ import net.corda.core.contracts.Command import net.corda.core.contracts.CommandData import net.corda.core.contracts.ContractState import net.corda.core.contracts.TransactionType -import net.corda.core.crypto.NullSignature +import net.corda.core.crypto.testing.NullSignature import net.corda.core.identity.AnonymousParty import net.corda.core.testing.* import net.corda.core.transactions.SignedTransaction diff --git a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt index 9a09229162..b5ff01bff8 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt @@ -3,9 +3,9 @@ package net.corda.flows import net.corda.contracts.asset.Cash import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.`issued by` -import net.corda.core.identity.Party import net.corda.core.getOrThrow -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.identity.Party +import net.corda.core.utilities.OpaqueBytes import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode @@ -51,7 +51,7 @@ class CashExitFlowTests { val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount, ref)).resultFuture mockNet.runNetwork() - val exitTx = future.getOrThrow().tx + val exitTx = future.getOrThrow().stx.tx val expected = (initialBalance - exitAmount).`issued by`(bankOfCorda.ref(ref)) assertEquals(1, exitTx.inputs.size) assertEquals(1, exitTx.outputs.size) diff --git a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt index 5f81c03e40..db183cabff 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt @@ -3,9 +3,9 @@ package net.corda.flows import net.corda.contracts.asset.Cash import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.`issued by` -import net.corda.core.identity.Party import net.corda.core.getOrThrow -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.identity.Party +import net.corda.core.utilities.OpaqueBytes import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode @@ -46,7 +46,7 @@ class CashIssueFlowTests { bankOfCorda, notary)).resultFuture mockNet.runNetwork() - val issueTx = future.getOrThrow() + val issueTx = future.getOrThrow().stx val output = issueTx.tx.outputs.single().data as Cash.State assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), output.amount) } diff --git a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt index 58779cf1b7..3bdf21b4da 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt @@ -5,7 +5,13 @@ import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.`issued by` import net.corda.core.getOrThrow import net.corda.core.identity.Party -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.node.services.Vault +import net.corda.core.node.services.trackBy +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.utilities.OpaqueBytes +import net.corda.node.utilities.transaction +import net.corda.testing.expect +import net.corda.testing.expectEvents import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode @@ -32,7 +38,8 @@ class CashPaymentFlowTests { notary = notaryNode.info.notaryIdentity bankOfCorda = bankOfCordaNode.info.legalIdentity - mockNet.runNetwork() + notaryNode.services.identityService.registerIdentity(bankOfCordaNode.info.legalIdentityAndCert) + bankOfCordaNode.services.identityService.registerIdentity(notaryNode.info.legalIdentityAndCert) val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, bankOfCorda, notary)).resultFuture @@ -50,15 +57,39 @@ class CashPaymentFlowTests { val payTo = notaryNode.info.legalIdentity val expectedPayment = 500.DOLLARS val expectedChange = 1500.DOLLARS - val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment, - payTo)).resultFuture - mockNet.runNetwork() - val paymentTx = future.getOrThrow() - val states = paymentTx.tx.outputs.map { it.data }.filterIsInstance() - val ourState = states.single { it.owner.owningKey != payTo.owningKey } - val paymentState = states.single { it.owner.owningKey == payTo.owningKey } - assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), ourState.amount) - assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount) + + bankOfCordaNode.database.transaction { + // Register for vault updates + val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) + val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy(criteria) + val (_, vaultUpdatesBankClient) = notaryNode.services.vaultQueryService.trackBy(criteria) + + val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment, + payTo)).resultFuture + mockNet.runNetwork() + future.getOrThrow() + + // Check Bank of Corda vault updates - we take in some issued cash and split it into $500 to the notary + // and $1,500 back to us, so we expect to consume one state, produce one state for our own vault + vaultUpdatesBoc.expectEvents { + expect { update -> + require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" } + require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } + val changeState = update.produced.single().state.data as Cash.State + assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), changeState.amount) + } + } + + // Check notary node vault updates + vaultUpdatesBankClient.expectEvents { + expect { update -> + require(update.consumed.isEmpty()) { update.consumed.size } + require(update.produced.size == 1) { update.produced.size } + val paymentState = update.produced.single().state.data as Cash.State + assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount) + } + } + } } @Test diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt index dd812e30c2..bc98ce9943 100644 --- a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt +++ b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt @@ -1,33 +1,42 @@ package net.corda.flows import com.google.common.util.concurrent.ListenableFuture -import net.corda.contracts.testing.calculateRandomlySizedAmounts +import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.currency import net.corda.core.flows.FlowException -import net.corda.core.internal.FlowStateMachine import net.corda.core.getOrThrow import net.corda.core.identity.Party -import net.corda.core.map -import net.corda.core.serialization.OpaqueBytes -import net.corda.core.toFuture +import net.corda.core.node.services.Vault +import net.corda.core.node.services.trackBy +import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.OpaqueBytes import net.corda.flows.IssuerFlow.IssuanceRequester -import net.corda.testing.BOC -import net.corda.testing.MEGA_CORP +import net.corda.node.utilities.transaction +import net.corda.testing.* +import net.corda.testing.contracts.calculateRandomlySizedAmounts import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode import org.junit.After import org.junit.Before import org.junit.Test -import rx.Observable +import org.junit.runner.RunWith +import org.junit.runners.Parameterized import java.util.* -import kotlin.test.assertEquals import kotlin.test.assertFailsWith -class IssuerFlowTest { +@RunWith(Parameterized::class) +class IssuerFlowTest(val anonymous: Boolean) { + companion object { + @Parameterized.Parameters + @JvmStatic + fun data(): Collection> { + return listOf(arrayOf(false), arrayOf(true)) + } + } + lateinit var mockNet: MockNetwork lateinit var notaryNode: MockNode lateinit var bankOfCordaNode: MockNode @@ -37,8 +46,8 @@ class IssuerFlowTest { fun start() { mockNet = MockNetwork(threadPerNode = true) notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - bankOfCordaNode = mockNet.createPartyNode(notaryNode.info.address, BOC.name) - bankClientNode = mockNet.createPartyNode(notaryNode.info.address, MEGA_CORP.name) + bankOfCordaNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) + bankClientNode = mockNet.createPartyNode(notaryNode.network.myAddress, MEGA_CORP.name) } @After @@ -48,57 +57,110 @@ class IssuerFlowTest { @Test fun `test issuer flow`() { - // using default IssueTo Party Reference - val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS, - bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) - assertEquals(issuerResult.get(), issuer.get().resultFuture.get()) + val notary = notaryNode.services.myInfo.notaryIdentity + val (vaultUpdatesBoc, vaultUpdatesBankClient) = bankOfCordaNode.database.transaction { + // Register for vault updates + val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) + val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy(criteria) + val (_, vaultUpdatesBankClient) = bankClientNode.services.vaultQueryService.trackBy(criteria) + // using default IssueTo Party Reference + val issuerResult = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS, + bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary) + issuerResult.get() + + Pair(vaultUpdatesBoc, vaultUpdatesBankClient) + } + + // Check Bank of Corda Vault Updates + vaultUpdatesBoc.expectEvents { + sequence( + // ISSUE + expect { update -> + require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" } + require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } + val issued = update.produced.single().state.data as Cash.State + require(issued.owner.owningKey in bankOfCordaNode.services.keyManagementService.keys) + }, + // MOVE + expect { update -> + require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" } + require(update.produced.isEmpty()) { "Expected 0 produced states, actual: $update" } + } + ) + } + + // Check Bank Client Vault Updates + vaultUpdatesBankClient.expectEvents { + // MOVE + expect { update -> + require(update.consumed.isEmpty()) { update.consumed.size } + require(update.produced.size == 1) { update.produced.size } + val paidState = update.produced.single().state.data as Cash.State + require(paidState.owner.owningKey in bankClientNode.services.keyManagementService.keys) + } + } + } + + @Test + fun `test issuer flow rejects restricted`() { + val notary = notaryNode.services.myInfo.notaryIdentity // try to issue an amount of a restricted currency assertFailsWith { runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(100000L, currency("BRL")), - bankClientNode.info.legalIdentity, OpaqueBytes.of(123)).issueRequestResult.getOrThrow() + bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary).getOrThrow() } } @Test fun `test issue flow to self`() { - // using default IssueTo Party Reference - val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS, - bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123)) - assertEquals(issuerResult.get(), issuer.get().resultFuture.get()) + val notary = notaryNode.services.myInfo.notaryIdentity + val vaultUpdatesBoc = bankOfCordaNode.database.transaction { + val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) + val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy(criteria) + + // using default IssueTo Party Reference + runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS, + bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123), notary).getOrThrow() + vaultUpdatesBoc + } + + // Check Bank of Corda Vault Updates + vaultUpdatesBoc.expectEvents { + sequence( + // ISSUE + expect { update -> + require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" } + require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } + } + ) + } } @Test fun `test concurrent issuer flow`() { + val notary = notaryNode.services.myInfo.notaryIdentity // this test exercises the Cashflow issue and move subflows to ensure consistent spending of issued states val amount = 10000.DOLLARS val amounts = calculateRandomlySizedAmounts(10000.DOLLARS, 10, 10, Random()) val handles = amounts.map { pennies -> runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(pennies, amount.token), - bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) + bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary) } handles.forEach { - require(it.issueRequestResult.get() is SignedTransaction) + require(it.get().stx is SignedTransaction) } } private fun runIssuerAndIssueRequester(issuerNode: MockNode, issueToNode: MockNode, amount: Amount, - party: Party, - ref: OpaqueBytes): RunResult { - val issueToPartyAndRef = party.ref(ref) - val issuerFlows: Observable = issuerNode.registerInitiatedFlow(IssuerFlow.Issuer::class.java) - val firstIssuerFiber = issuerFlows.toFuture().map { it.stateMachine } - - val issueRequest = IssuanceRequester(amount, party, issueToPartyAndRef.reference, issuerNode.info.legalIdentity) - val issueRequestResultFuture = issueToNode.services.startFlow(issueRequest).resultFuture - - return IssuerFlowTest.RunResult(firstIssuerFiber, issueRequestResultFuture) + issueToParty: Party, + ref: OpaqueBytes, + notaryParty: Party): ListenableFuture { + val issueToPartyAndRef = issueToParty.ref(ref) + val issueRequest = IssuanceRequester(amount, issueToParty, issueToPartyAndRef.reference, issuerNode.info.legalIdentity, notaryParty, + anonymous) + return issueToNode.services.startFlow(issueRequest).resultFuture } - - private data class RunResult( - val issuer: ListenableFuture>, - val issueRequestResult: ListenableFuture - ) } \ No newline at end of file diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt index 08cda83d92..453dff0203 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt @@ -2,7 +2,6 @@ package net.corda.schemas import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 import javax.persistence.* /** diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt index a48b664023..83755a4110 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt @@ -2,7 +2,7 @@ package net.corda.schemas import net.corda.core.identity.AbstractParty import net.corda.core.schemas.MappedSchema -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import net.corda.core.schemas.CommonSchemaV1 import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Index diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt index 33bd6e2a19..d1d7e46d79 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt @@ -3,7 +3,7 @@ package net.corda.schemas import net.corda.core.identity.AbstractParty import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import net.corda.core.schemas.CommonSchemaV1 import javax.persistence.* /** diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt index e52df55695..735eda7afc 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt @@ -1,10 +1,8 @@ package net.corda.schemas -import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.schemas.MappedSchema -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 -import java.security.PublicKey +import net.corda.core.schemas.CommonSchemaV1 import java.time.Instant import javax.persistence.Column import javax.persistence.Entity diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy index 3151343f9a..6eafcdf3cd 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy @@ -13,6 +13,25 @@ class Cordformation implements Plugin { Configuration cordappConf = project.configurations.create("cordapp") cordappConf.transitive = false project.configurations.compile.extendsFrom cordappConf + + configureCordappJar(project) + } + + /** + * Configures this project's JAR as a Cordapp JAR + */ + private void configureCordappJar(Project project) { + // Note: project.afterEvaluate did not have full dependency resolution completed, hence a task is used instead + def task = project.task('configureCordappFatJar') { + doLast { + project.tasks.jar.from(getDirectNonCordaDependencies(project).collect { project.zipTree(it)}) { + exclude "META-INF/*.SF" + exclude "META-INF/*.DSA" + exclude "META-INF/*.RSA" + } + } + } + project.tasks.jar.dependsOn task } /** @@ -27,4 +46,27 @@ class Cordformation implements Plugin { it.name.contains('cordformation') }, filePathInJar).asFile() } + + private static def getDirectNonCordaDependencies(Project project) { + def coreCordaNames = ['jfx', 'mock', 'rpc', 'core', 'corda', 'cordform-common', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver', 'capsule', 'webcapsule'] + def excludes = coreCordaNames.collect { [group: 'net.corda', name: it] } + [ + [group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib'], + [group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jre8'], + [group: 'co.paralleluniverse', name: 'quasar-core'] + ] + // The direct dependencies of this project + def cordappDeps = project.configurations.cordapp.allDependencies + def directDeps = project.configurations.runtime.allDependencies - cordappDeps + // We want to filter out anything Corda related or provided by Corda, like kotlin-stdlib and quasar + def filteredDeps = directDeps.findAll { excludes.collect { exclude -> (exclude.group == it.group) && (exclude.name == it.name) }.findAll { it }.isEmpty() } + filteredDeps.each { + // net.corda may be a core dependency which shouldn't be included in this cordapp so give a warning + if(it.group.contains('net.corda')) { + project.logger.warn("Including a dependency with a net.corda group: $it") + } else { + project.logger.trace("Including dependency: $it") + } + } + return filteredDeps.collect { project.configurations.runtime.files it }.flatten().toSet() + } } diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy index 3f3a53c8f2..fb2ea88550 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy @@ -104,7 +104,6 @@ class Node extends CordformNode { installWebserverJar() installBuiltPlugin() installCordapps() - installDependencies() installConfig() } @@ -172,23 +171,6 @@ class Node extends CordformNode { } } - /** - * Installs other dependencies to this node's dependencies directory. - */ - private void installDependencies() { - def cordaJar = verifyAndGetCordaJar() - def webJar = verifyAndGetWebserverJar() - def depsDir = new File(nodeDir, "dependencies") - def coreDeps = project.zipTree(cordaJar).getFiles().collect { it.getName() } - def appDeps = project.configurations.runtime.filter { - (it != cordaJar) && (it != webJar) && !project.configurations.cordapp.contains(it) && !coreDeps.contains(it.getName()) - } - project.copy { - from appDeps - into depsDir - } - } - /** * Installs the configuration file to this node's directory and detokenises it. */ @@ -218,7 +200,7 @@ class Node extends CordformNode { */ private File verifyAndGetCordaJar() { def maybeCordaJAR = project.configurations.runtime.filter { - it.toString().contains("corda-${project.corda_release_version}.jar") + it.toString().contains("corda-${project.corda_release_version}.jar") || it.toString().contains("corda-enterprise-${project.corda_release_version}.jar") } if (maybeCordaJAR.size() == 0) { throw new RuntimeException("No Corda Capsule JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-${project.corda_release_version}.jar\"") diff --git a/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy b/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy index 0ed4fe5935..5083554337 100644 --- a/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy +++ b/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy @@ -10,11 +10,6 @@ import org.gradle.api.tasks.JavaExec */ class QuasarPlugin implements Plugin { void apply(Project project) { - - project.repositories { - mavenCentral() - } - project.configurations.create("quasar") // To add a local .jar dependency: // project.dependencies.add("quasar", project.files("${project.rootProject.projectDir}/lib/quasar.jar")) diff --git a/node-api/build.gradle b/node-api/build.gradle index 08ef79ae79..6bb83dcbf6 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -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 node Artemis API' diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt index d77de310db..a37a841f83 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt @@ -1,14 +1,15 @@ package net.corda.nodeapi -import com.google.common.annotations.VisibleForTesting -import com.google.common.net.HostAndPort import net.corda.core.crypto.toBase58String import net.corda.core.messaging.MessageRecipientGroup import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.node.NodeInfo +import net.corda.core.node.services.ServiceType import net.corda.core.read import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.config.SSLConfiguration import java.security.KeyStore import java.security.PublicKey @@ -34,18 +35,6 @@ abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() { const val P2P_QUEUE = "p2p.inbound" const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications" const val NETWORK_MAP_QUEUE = "${INTERNAL_PREFIX}networkmap" - - /** - * Assuming the passed in target address is actually an ArtemisAddress will extract the host and port of the node. This should - * only be used in unit tests and the internals of the messaging services to keep addressing opaque for the future. - * N.B. Marked as JvmStatic to allow use in the inherited classes. - */ - @JvmStatic - @VisibleForTesting - fun toHostAndPort(target: MessageRecipients): HostAndPort { - val addr = target as? ArtemisMessagingComponent.ArtemisPeerAddress ?: throw IllegalArgumentException("Not an Artemis address") - return addr.hostAndPort - } } interface ArtemisAddress : MessageRecipients { @@ -53,11 +42,11 @@ abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() { } interface ArtemisPeerAddress : ArtemisAddress, SingleMessageRecipient { - val hostAndPort: HostAndPort + val hostAndPort: NetworkHostAndPort } @CordaSerializable - data class NetworkMapAddress(override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisPeerAddress { + data class NetworkMapAddress(override val hostAndPort: NetworkHostAndPort) : ArtemisPeerAddress { override val queueName: String get() = NETWORK_MAP_QUEUE } @@ -73,13 +62,13 @@ abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() { * @param hostAndPort The address of the node. */ @CordaSerializable - data class NodeAddress(override val queueName: String, override val hostAndPort: HostAndPort) : ArtemisPeerAddress { + data class NodeAddress(override val queueName: String, override val hostAndPort: NetworkHostAndPort) : ArtemisPeerAddress { companion object { - fun asPeer(peerIdentity: PublicKey, hostAndPort: HostAndPort): NodeAddress { + fun asPeer(peerIdentity: PublicKey, hostAndPort: NetworkHostAndPort): NodeAddress { return NodeAddress("$PEERS_PREFIX${peerIdentity.toBase58String()}", hostAndPort) } - fun asService(serviceIdentity: PublicKey, hostAndPort: HostAndPort): NodeAddress { + fun asService(serviceIdentity: PublicKey, hostAndPort: NetworkHostAndPort): NodeAddress { return NodeAddress("$SERVICES_PREFIX${serviceIdentity.toBase58String()}", hostAndPort) } } @@ -116,4 +105,12 @@ abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() { KeyStore.getInstance("JKS").load(it, config.trustStorePassword.toCharArray()) } } + + fun getArtemisPeerAddress(nodeInfo: NodeInfo): ArtemisPeerAddress { + return if (nodeInfo.advertisedServices.any { it.info.type == ServiceType.networkMap }) { + NetworkMapAddress(nodeInfo.addresses.first()) + } else { + NodeAddress.asPeer(nodeInfo.legalIdentity.owningKey, nodeInfo.addresses.first()) + } + } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt index cdd0074a1b..5cda03946f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisTcpTransport.kt @@ -1,6 +1,6 @@ package net.corda.nodeapi -import com.google.common.net.HostAndPort +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.config.SSLConfiguration import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory @@ -36,7 +36,7 @@ class ArtemisTcpTransport { fun tcpTransport( direction: ConnectionDirection, - hostAndPort: HostAndPort, + hostAndPort: NetworkHostAndPort, config: SSLConfiguration?, enableSSL: Boolean = true ): TransportConfiguration { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt index 836315752a..8e5aaf8cfb 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt @@ -1,13 +1,14 @@ package net.corda.nodeapi import com.esotericsoftware.kryo.pool.KryoPool -import net.corda.core.ErrorOr import net.corda.core.serialization.KryoPoolWithContext import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.core.utilities.Try import net.corda.nodeapi.RPCApi.ClientToServer import net.corda.nodeapi.RPCApi.ObservableId import net.corda.nodeapi.RPCApi.RPC_CLIENT_BINDING_REMOVALS +import net.corda.nodeapi.RPCApi.RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION import net.corda.nodeapi.RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX import net.corda.nodeapi.RPCApi.RPC_SERVER_QUEUE_NAME import net.corda.nodeapi.RPCApi.RpcRequestId @@ -151,7 +152,7 @@ object RPCApi { data class RpcReply( val id: RpcRequestId, - val result: ErrorOr + val result: Try ) : ServerToClient() { override fun writeToClientMessage(kryoPool: KryoPool, message: ClientMessage) { message.putIntProperty(TAG_FIELD_NAME, Tag.RPC_REPLY.ordinal) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt index e5f279c692..0b5236f68f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt @@ -9,7 +9,7 @@ import net.corda.core.requireExternal import net.corda.core.serialization.* import net.corda.core.toFuture import net.corda.core.toObservable -import net.corda.core.utilities.CordaRuntimeException +import net.corda.core.CordaRuntimeException import net.corda.nodeapi.config.OldConfig import rx.Observable import java.io.InputStream diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt index 9726136af0..81455d6e6a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt @@ -1,9 +1,11 @@ package net.corda.nodeapi.config -import com.google.common.net.HostAndPort import com.typesafe.config.Config import com.typesafe.config.ConfigUtil import net.corda.core.noneOrSingle +import net.corda.core.utilities.validateX500Name +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.parseNetworkHostAndPort import org.bouncycastle.asn1.x500.X500Name import org.slf4j.LoggerFactory import java.net.Proxy @@ -67,11 +69,11 @@ private fun Config.getSingleValue(path: String, type: KType): Any? { Boolean::class -> getBoolean(path) LocalDate::class -> LocalDate.parse(getString(path)) Instant::class -> Instant.parse(getString(path)) - HostAndPort::class -> HostAndPort.fromString(getString(path)) + NetworkHostAndPort::class -> getString(path).parseNetworkHostAndPort() Path::class -> Paths.get(getString(path)) URL::class -> URL(getString(path)) Properties::class -> getConfig(path).toProperties() - X500Name::class -> X500Name(getString(path)) + X500Name::class -> X500Name(getString(path)).apply(::validateX500Name) else -> if (typeClass.java.isEnum) { parseEnum(typeClass.java, getString(path)) } else { @@ -95,7 +97,7 @@ private fun Config.getCollectionValue(path: String, type: KType): Collection getBooleanList(path) LocalDate::class -> getStringList(path).map(LocalDate::parse) Instant::class -> getStringList(path).map(Instant::parse) - HostAndPort::class -> getStringList(path).map(HostAndPort::fromString) + NetworkHostAndPort::class -> getStringList(path).map { it.parseNetworkHostAndPort() } Path::class -> getStringList(path).map { Paths.get(it) } URL::class -> getStringList(path).map(::URL) X500Name::class -> getStringList(path).map(::X500Name) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/serialization/DefaultWhitelist.kt b/node-api/src/main/kotlin/net/corda/nodeapi/serialization/DefaultWhitelist.kt index 31e9743eee..9e5b507cdb 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/serialization/DefaultWhitelist.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/serialization/DefaultWhitelist.kt @@ -1,9 +1,9 @@ package net.corda.nodeapi.serialization import com.esotericsoftware.kryo.KryoException -import com.google.common.net.HostAndPort import net.corda.core.node.CordaPluginRegistry import net.corda.core.serialization.SerializationCustomization +import net.corda.core.utilities.NetworkHostAndPort import org.apache.activemq.artemis.api.core.SimpleString import rx.Notification import rx.exceptions.OnErrorNotImplementedException @@ -33,7 +33,7 @@ class DefaultWhitelist : CordaPluginRegistry() { addToWhitelist(listOf(Unit).javaClass) // SingletonList addToWhitelist(setOf(Unit).javaClass) // SingletonSet addToWhitelist(mapOf(Unit to Unit).javaClass) // SingletonSet - addToWhitelist(HostAndPort::class.java) + addToWhitelist(NetworkHostAndPort::class.java) addToWhitelist(SimpleString::class.java) addToWhitelist(KryoException::class.java) addToWhitelist(StringBuffer::class.java) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt index 3a09140194..a3abc82492 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt @@ -1,11 +1,11 @@ package net.corda.nodeapi.config -import com.google.common.net.HostAndPort import com.typesafe.config.Config import com.typesafe.config.ConfigFactory.empty import com.typesafe.config.ConfigRenderOptions.defaults import com.typesafe.config.ConfigValueFactory import net.corda.core.div +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.getTestX509Name import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x500.X500Name @@ -59,10 +59,10 @@ class ConfigParsingTest { } @Test - fun `HostAndPort`() { - testPropertyType( - HostAndPort.fromParts("localhost", 2223), - HostAndPort.fromParts("localhost", 2225), + fun `NetworkHostAndPort`() { + testPropertyType( + NetworkHostAndPort("localhost", 2223), + NetworkHostAndPort("localhost", 2225), valuesToString = true) } @@ -112,7 +112,7 @@ class ConfigParsingTest { @Test fun x500Name() { - testPropertyType(getTestX509Name("Mock Node"), getTestX509Name("Mock Node 2"), valuesToString = true) + testPropertyType(getTestX509Name("Mock Party"), getTestX509Name("Mock Party 2"), valuesToString = true) } @Test @@ -223,8 +223,8 @@ class ConfigParsingTest { data class LocalDateListData(override val values: List) : ListData data class InstantData(override val value: Instant) : SingleData data class InstantListData(override val values: List) : ListData - data class HostAndPortData(override val value: HostAndPort) : SingleData - data class HostAndPortListData(override val values: List) : ListData + data class NetworkHostAndPortData(override val value: NetworkHostAndPort) : SingleData + data class NetworkHostAndPortListData(override val values: List) : ListData data class PathData(override val value: Path) : SingleData data class PathListData(override val values: List) : ListData data class URLData(override val value: URL) : SingleData diff --git a/node-schemas/build.gradle b/node-schemas/build.gradle index 5026633777..88a9102a8b 100644 --- a/node-schemas/build.gradle +++ b/node-schemas/build.gradle @@ -2,17 +2,20 @@ apply plugin: 'kotlin' apply plugin: 'kotlin-kapt' apply plugin: 'idea' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Corda node database schemas' dependencies { compile project(':core') - testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - testCompile "junit:junit:$junit_version" // Requery: SQL based query & persistence for Kotlin kapt "io.requery:requery-processor:$requery_version" + testCompile project(':test-utils') + testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testCompile "junit:junit:$junit_version" + // For H2 database support in persistence testCompile "com.h2database:h2:$h2_version" } diff --git a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt index b54cb0d961..58a7b3b95d 100644 --- a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt +++ b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt @@ -8,8 +8,8 @@ import io.requery.rx.KotlinRxEntityStore import io.requery.sql.* import io.requery.sql.platform.Generic import net.corda.core.contracts.* -import net.corda.core.contracts.TimeWindow -import net.corda.core.crypto.CompositeKey +import net.corda.testing.contracts.DummyContract +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.toBase58String @@ -21,8 +21,11 @@ import net.corda.core.schemas.requery.converters.VaultStateStatusConverter import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.LedgerTransaction -import net.corda.core.utilities.* import net.corda.node.services.vault.schemas.requery.* +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY import org.h2.jdbcx.JdbcDataSource import org.junit.After import org.junit.Assert diff --git a/node/build.gradle b/node/build.gradle index 8ee3ffa25c..2b453fb0cc 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -5,6 +5,7 @@ apply plugin: 'kotlin-jpa' apply plugin: 'java' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Corda node modules' diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index 9d60efab6c..fac1f538f5 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -4,6 +4,7 @@ */ apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'us.kirchmeier.capsule' +apply plugin: 'com.jfrog.artifactory' description 'Corda standalone node' diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index 1bae088182..b5366a7ace 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -6,11 +6,15 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow -import net.corda.core.utilities.ALICE +import net.corda.core.node.services.ServiceInfo +import net.corda.core.node.services.ServiceType +import net.corda.testing.ALICE import net.corda.testing.driver.driver import net.corda.node.internal.NodeStartup import net.corda.node.services.startFlowPermission import net.corda.nodeapi.User +import net.corda.testing.driver.ListenProcessDeathException +import net.corda.testing.driver.NetworkMapStartStrategy import net.corda.testing.ProjectStructure.projectRootDir import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -18,6 +22,7 @@ import org.junit.Test import java.io.* import java.nio.file.Files import kotlin.test.assertEquals +import kotlin.test.assertFailsWith class BootTests { @@ -37,7 +42,7 @@ class BootTests { assertThat(logConfigFile).isRegularFile() driver(isDebug = true, systemProperties = mapOf("log4j.configurationFile" to logConfigFile.toString())) { val alice = startNode(ALICE.name).get() - val logFolder = alice.configuration.baseDirectory / "logs" + val logFolder = alice.configuration.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME val logFile = logFolder.toFile().listFiles { _, name -> name.endsWith(".log") }.single() // Start second Alice, should fail assertThatThrownBy { @@ -48,6 +53,15 @@ class BootTests { assertEquals(1, numberOfNodesThatLogged) } } + + @Test + fun `node quits on failure to register with network map`() { + val tooManyAdvertisedServices = (1..100).map { ServiceInfo(ServiceType.regulator.getSubType("$it")) }.toSet() + driver(networkMapStartStrategy = NetworkMapStartStrategy.Nominated(ALICE.name)) { + val future = startNode(ALICE.name, advertisedServices = tooManyAdvertisedServices) + assertFailsWith(ListenProcessDeathException::class) { future.getOrThrow() } + } + } } @StartableByRPC diff --git a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt index 1e933ddaf5..0d4f786dcf 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt @@ -9,8 +9,8 @@ import net.corda.core.flows.StartableByRPC import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.messaging.startFlow -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB +import net.corda.testing.ALICE +import net.corda.testing.BOB import net.corda.core.utilities.unwrap import net.corda.node.services.startFlowPermission import net.corda.nodeapi.User diff --git a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt index 3b42fbeae0..c22c69ec34 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt @@ -9,7 +9,7 @@ import net.corda.core.flows.StartableByRPC import net.corda.core.messaging.startFlow import net.corda.core.minutes import net.corda.core.node.services.ServiceInfo -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.div import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index 5e8c9516c6..7e7060cbc6 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -1,13 +1,18 @@ package net.corda.node.services -import com.google.common.net.HostAndPort import com.nhaarman.mockito_kotlin.whenever -import net.corda.core.* -import net.corda.core.contracts.* -import net.corda.core.crypto.CompositeKey +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionType +import net.corda.testing.contracts.DummyContract +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.crypto.SecureHash +import net.corda.core.div +import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.Try import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.flows.NotaryFlow @@ -20,11 +25,11 @@ import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.node.utilities.transaction import net.corda.testing.node.MockNetwork import org.bouncycastle.asn1.x500.X500Name -import org.junit.Ignore import org.junit.After import org.junit.Test import java.nio.file.Files -import kotlin.test.* +import kotlin.test.assertEquals +import kotlin.test.assertTrue class BFTNotaryServiceTests { companion object { @@ -47,10 +52,10 @@ class BFTNotaryServiceTests { serviceType.id, clusterName) val bftNotaryService = ServiceInfo(serviceType, clusterName) - val notaryClusterAddresses = replicaIds.map { HostAndPort.fromParts("localhost", 11000 + it * 10) } + val notaryClusterAddresses = replicaIds.map { NetworkHostAndPort("localhost", 11000 + it * 10) } replicaIds.forEach { replicaId -> mockNet.createNode( - node.info.address, + node.network.myAddress, advertisedServices = bftNotaryService, configOverrides = { whenever(it.bftReplicaId).thenReturn(replicaId) @@ -61,13 +66,11 @@ class BFTNotaryServiceTests { } @Test - @Ignore("Under investigation due to failure on TC build server") fun `detect double spend 1 faulty`() { detectDoubleSpend(1) } @Test - @Ignore("Under investigation due to failure on TC build server") fun `detect double spend 2 faulty`() { detectDoubleSpend(2) } @@ -91,10 +94,10 @@ class BFTNotaryServiceTests { val flows = spendTxs.map { NotaryFlow.Client(it) } val stateMachines = flows.map { services.startFlow(it) } mockNet.runNetwork() - val results = stateMachines.map { ErrorOr.catch { it.resultFuture.getOrThrow() } } + val results = stateMachines.map { Try.on { it.resultFuture.getOrThrow() } } val successfulIndex = results.mapIndexedNotNull { index, result -> - if (result.error == null) { - val signers = result.getOrThrow().map { it.by } + if (result is Try.Success) { + val signers = result.value.map { it.by } assertEquals(minCorrectReplicas(clusterSize), signers.size) signers.forEach { assertTrue(it in (notary.owningKey as CompositeKey).leafKeys) @@ -105,8 +108,8 @@ class BFTNotaryServiceTests { } }.single() spendTxs.zip(results).forEach { (tx, result) -> - if (result.error != null) { - val error = (result.error as NotaryException).error as NotaryError.Conflict + if (result is Try.Failure) { + val error = (result.exception as NotaryException).error as NotaryError.Conflict assertEquals(tx.id, error.txId) val (stateRef, consumingTx) = error.conflict.verified().stateHistory.entries.single() assertEquals(StateRef(issueTx.id, 0), stateRef) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt index f5ec2cbeae..efb4bf8b80 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt @@ -9,9 +9,9 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.startFlow import net.corda.core.node.NodeInfo -import net.corda.core.serialization.OpaqueBytes -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.OpaqueBytes +import net.corda.testing.ALICE +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow import net.corda.testing.driver.NodeHandle diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index 0669214cbf..566c4674e1 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -1,14 +1,14 @@ package net.corda.node.services import com.google.common.util.concurrent.Futures -import net.corda.core.contracts.DummyContract import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType -import net.corda.core.identity.Party +import net.corda.testing.contracts.DummyContract import net.corda.core.getOrThrow +import net.corda.core.identity.Party import net.corda.core.map -import net.corda.core.utilities.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_A import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.flows.NotaryFlow @@ -26,28 +26,28 @@ class RaftNotaryServiceTests : NodeBasedTest() { @Test fun `detect double spend`() { - val (masterNode, alice) = Futures.allAsList( - startNotaryCluster(notaryName, 3).map { it.first() }, - startNode(DUMMY_BANK_A.name) + val (bankA) = Futures.allAsList( + startNode(DUMMY_BANK_A.name), + startNotaryCluster(notaryName, 3).map { it.first() } ).getOrThrow() - val notaryParty = alice.netMapCache.getNotary(notaryName)!! + val notaryParty = bankA.services.networkMapCache.getNotary(notaryName)!! - val inputState = issueState(alice, notaryParty) + val inputState = issueState(bankA, notaryParty) val firstTxBuilder = TransactionType.General.Builder(notaryParty).withItems(inputState) - val firstSpendTx = alice.services.signInitialTransaction(firstTxBuilder) + val firstSpendTx = bankA.services.signInitialTransaction(firstTxBuilder) - val firstSpend = alice.services.startFlow(NotaryFlow.Client(firstSpendTx)) + val firstSpend = bankA.services.startFlow(NotaryFlow.Client(firstSpendTx)) firstSpend.resultFuture.getOrThrow() val secondSpendBuilder = TransactionType.General.Builder(notaryParty).withItems(inputState).run { - val dummyState = DummyContract.SingleOwnerState(0, alice.info.legalIdentity) + val dummyState = DummyContract.SingleOwnerState(0, bankA.info.legalIdentity) addOutputState(dummyState) this } - val secondSpendTx = alice.services.signInitialTransaction(secondSpendBuilder) - val secondSpend = alice.services.startFlow(NotaryFlow.Client(secondSpendTx)) + val secondSpendTx = bankA.services.signInitialTransaction(secondSpendBuilder) + val secondSpend = bankA.services.startFlow(NotaryFlow.Client(secondSpendTx)) val ex = assertFailsWith(NotaryException::class) { secondSpend.resultFuture.getOrThrow() } val error = ex.error as NotaryError.Conflict @@ -58,7 +58,7 @@ class RaftNotaryServiceTests : NodeBasedTest() { return node.database.transaction { val builder = DummyContract.generateInitial(Random().nextInt(), notary, node.info.legalIdentity.ref(0)) val stx = node.services.signInitialTransaction(builder) - node.services.recordTransactions(listOf(stx)) + node.services.recordTransactions(stx) StateAndRef(builder.outputStates().first(), StateRef(stx.id, 0)) } } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt index a2cedb2ee0..459310ecf7 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt @@ -6,8 +6,8 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.getOrThrow import net.corda.core.identity.Party -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB +import net.corda.testing.ALICE +import net.corda.testing.BOB import net.corda.core.utilities.unwrap import net.corda.testing.node.NodeBasedTest import org.assertj.core.api.Assertions.assertThat diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index 7086d1a3db..9cae274260 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -1,7 +1,6 @@ package net.corda.services.messaging import co.paralleluniverse.fibers.Suspendable -import com.google.common.net.HostAndPort import net.corda.client.rpc.CordaRPCClient import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.toBase58String @@ -11,9 +10,10 @@ import net.corda.core.flows.InitiatingFlow import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps -import net.corda.core.random63BitValue -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.crypto.random63BitValue +import net.corda.testing.ALICE +import net.corda.testing.BOB import net.corda.core.utilities.unwrap import net.corda.node.internal.Node import net.corda.nodeapi.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX @@ -144,13 +144,13 @@ abstract class MQSecurityTest : NodeBasedTest() { assertAllQueueCreationAttacksFail(randomQueue) } - fun clientTo(target: HostAndPort, sslConfiguration: SSLConfiguration? = configureTestSSL()): SimpleMQClient { + fun clientTo(target: NetworkHostAndPort, sslConfiguration: SSLConfiguration? = configureTestSSL()): SimpleMQClient { val client = SimpleMQClient(target, sslConfiguration) clients += client return client } - fun loginToRPC(target: HostAndPort, rpcUser: User, sslConfiguration: SSLConfiguration? = null): CordaRPCOps { + fun loginToRPC(target: NetworkHostAndPort, rpcUser: User, sslConfiguration: SSLConfiguration? = null): CordaRPCOps { return CordaRPCClient(target, sslConfiguration).start(rpcUser.username, rpcUser.password).proxy } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index 9ba5bac762..d7e0dc2c3e 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -3,6 +3,7 @@ package net.corda.services.messaging import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import net.corda.core.* +import net.corda.core.crypto.random63BitValue import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.DEFAULT_SESSION_ID @@ -10,14 +11,12 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.core.utilities.* import net.corda.node.internal.Node import net.corda.node.services.messaging.* import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.utilities.ServiceIdentityGenerator -import net.corda.testing.freeLocalHostAndPort -import net.corda.testing.getTestX509Name +import net.corda.testing.* import net.corda.testing.node.NodeBasedTest import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x500.X500Name @@ -31,7 +30,7 @@ import java.util.concurrent.atomic.AtomicInteger class P2PMessagingTest : NodeBasedTest() { private companion object { val DISTRIBUTED_SERVICE_NAME = getTestX509Name("DistributedService") - val SERVICE_2_NAME = getTestX509Name("Service Node 2") + val SERVICE_2_NAME = getTestX509Name("Service 2") } @Test diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt index bca09c4a0f..5036db9077 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt @@ -6,9 +6,8 @@ import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.cert import net.corda.core.getOrThrow import net.corda.core.node.NodeInfo -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.seconds -import net.corda.core.utilities.* import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.messaging.sendRequest @@ -16,10 +15,9 @@ import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService.RegistrationRequest import net.corda.node.services.network.NodeRegistration import net.corda.node.utilities.AddOrRemove -import net.corda.testing.MOCK_VERSION_INFO +import net.corda.testing.* import net.corda.testing.node.NodeBasedTest import net.corda.testing.node.SimpleNode -import net.corda.testing.testNodeConfiguration import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.asn1.x500.X500Name @@ -32,7 +30,7 @@ class P2PSecurityTest : NodeBasedTest() { @Test fun `incorrect legal name for the network map service config`() { - val incorrectNetworkMapName = X509Utilities.getDevX509Name(random63BitValue().toString()) + val incorrectNetworkMapName = X509Utilities.getDevX509Name("NetworkMap-${random63BitValue()}") val node = startNode(BOB.name, configOverrides = mapOf( "networkMapService" to mapOf( "address" to networkMapNode.configuration.p2pAddress.toString(), @@ -69,7 +67,7 @@ class P2PSecurityTest : NodeBasedTest() { private fun SimpleNode.registerWithNetworkMap(registrationName: X500Name): ListenableFuture { val legalIdentity = getTestPartyAndCertificate(registrationName, identity.public) - val nodeInfo = NodeInfo(network.myAddress, legalIdentity, MOCK_VERSION_INFO.platformVersion) + val nodeInfo = NodeInfo(listOf(MOCK_HOST_AND_PORT), legalIdentity, setOf(legalIdentity), 1) val registration = NodeRegistration(nodeInfo, System.currentTimeMillis(), AddOrRemove.ADD, Instant.MAX) val request = RegistrationRequest(registration.toWire(keyService, identity.public), network.myAddress) return network.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapNode.network.myAddress) diff --git a/node/src/main/java/CordaCaplet.java b/node/src/main/java/CordaCaplet.java index de53b6ff08..83e76ae2ba 100644 --- a/node/src/main/java/CordaCaplet.java +++ b/node/src/main/java/CordaCaplet.java @@ -24,8 +24,7 @@ public class CordaCaplet extends Capsule { // defined as public static final fields on the Capsule class, therefore referential equality is safe. if (ATTR_APP_CLASS_PATH == attr) { T cp = super.attribute(attr); - List classpath = augmentClasspath((List) cp, "plugins"); - return (T) augmentClasspath(classpath, "dependencies"); + return (T) augmentClasspath((List) cp, "plugins"); } return super.attribute(attr); } diff --git a/core/src/main/kotlin/net/corda/core/node/VersionInfo.kt b/node/src/main/kotlin/net/corda/node/VersionInfo.kt similarity index 91% rename from core/src/main/kotlin/net/corda/core/node/VersionInfo.kt rename to node/src/main/kotlin/net/corda/node/VersionInfo.kt index f072eafe04..c51df32229 100644 --- a/core/src/main/kotlin/net/corda/core/node/VersionInfo.kt +++ b/node/src/main/kotlin/net/corda/node/VersionInfo.kt @@ -1,4 +1,4 @@ -package net.corda.core.node +package net.corda.node /** * Encapsulates various pieces of version information of the node. diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 18b02e4e53..b05b0d6c33 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -10,6 +10,7 @@ import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult import net.corda.core.* import net.corda.core.crypto.* +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate @@ -23,9 +24,8 @@ import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_CA +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug -import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.flows.* import net.corda.node.services.* import net.corda.node.services.api.* @@ -40,10 +40,14 @@ import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.sendRequest import net.corda.node.services.network.InMemoryNetworkMapCache import net.corda.node.services.network.NetworkMapService +import net.corda.node.services.network.NetworkMapService.RegistrationRequest import net.corda.node.services.network.NetworkMapService.RegistrationResponse import net.corda.node.services.network.NodeRegistration import net.corda.node.services.network.PersistentNetworkMapService -import net.corda.node.services.persistence.* +import net.corda.node.services.persistence.DBCheckpointStorage +import net.corda.node.services.persistence.DBTransactionMappingStorage +import net.corda.node.services.persistence.DBTransactionStorage +import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.statemachine.FlowStateMachineImpl @@ -66,9 +70,9 @@ import rx.Observable import java.io.IOException import java.lang.reflect.InvocationTargetException import java.lang.reflect.Modifier.* +import java.math.BigInteger import java.net.JarURLConnection import java.net.URI -import java.nio.file.FileAlreadyExistsException import java.nio.file.Path import java.nio.file.Paths import java.security.KeyPair @@ -118,69 +122,18 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, private val flowFactories = ConcurrentHashMap>, InitiatedFlowFactory<*>>() protected val partyKeys = mutableSetOf() - val services = object : ServiceHubInternal() { - override val networkService: MessagingService get() = network - override val networkMapCache: NetworkMapCacheInternal get() = netMapCache - override val storageService: TxWritableStorageService get() = storage - override val vaultService: VaultService get() = vault - override val vaultQueryService: VaultQueryService get() = vaultQuery - override val keyManagementService: KeyManagementService get() = keyManagement - override val identityService: IdentityService get() = identity - override val schedulerService: SchedulerService get() = scheduler - override val clock: Clock get() = platformClock - override val myInfo: NodeInfo get() = info - override val schemaService: SchemaService get() = schemas - override val transactionVerifierService: TransactionVerifierService get() = txVerifierService - override val auditService: AuditService get() = auditService - - override fun cordaService(type: Class): T { - require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } - return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") - } - - override val rpcFlows: List>> get() = this@AbstractNode.rpcFlows - - // Internal only - override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) - - override fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator): FlowStateMachineImpl { - return serverThread.fetchFrom { smm.add(logic, flowInitiator) } - } - - override fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? { - return flowFactories[initiatingFlowClass] - } - - override fun recordTransactions(txs: Iterable) { - database.transaction { - recordTransactionsInternal(storage, txs) - } - } - } - - open fun findMyLocation(): PhysicalLocation? { - return configuration.myLegalName.locationOrNull?.let { CityDatabase[it] } - } + val services: ServiceHubInternal get() = _services + private lateinit var _services: ServiceHubInternalImpl lateinit var info: NodeInfo - lateinit var storage: TxWritableStorageService lateinit var checkpointStorage: CheckpointStorage lateinit var smm: StateMachineManager - lateinit var vault: VaultService - lateinit var vaultQuery: VaultQueryService - lateinit var keyManagement: KeyManagementService + lateinit var attachments: NodeAttachmentService var inNodeNetworkMapService: NetworkMapService? = null - lateinit var txVerifierService: TransactionVerifierService - lateinit var identity: IdentityService lateinit var network: MessagingService - lateinit var netMapCache: NetworkMapCacheInternal - lateinit var scheduler: NodeSchedulerService - lateinit var schemas: SchemaService - lateinit var auditService: AuditService protected val runOnStop = ArrayList<() -> Any?>() lateinit var database: Database protected var dbCloser: (() -> Any?)? = null - private lateinit var rpcFlows: List>> var isPreviousCheckpointsPresent = false private set @@ -202,6 +155,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, /** The implementation of the [CordaRPCOps] interface used by this node. */ open val rpcOps: CordaRPCOps by lazy { CordaRPCOpsImpl(services, smm, database) } // Lazy to avoid init ordering issue with the SMM. + open fun findMyLocation(): WorldMapLocation? { + return configuration.myLegalName.locationOrNull?.let { CityDatabase[it] } + } + open fun start(): AbstractNode { require(!started) { "Node has already been started" } @@ -209,19 +166,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, log.warn("Corda node is running in dev mode.") configuration.configureWithDevSSLCertificate() } - require(hasSSLCertificates()) { - "Identity certificate not found. " + - "Please either copy your existing identity key and certificate from another node, " + - "or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " + - "Read more at: https://docs.corda.net/permissioning.html" - } + validateKeystore() log.info("Node starting up ...") // Do all of this in a database transaction so anything that might need a connection has one. initialiseDatabasePersistence { - val keyStoreWrapper = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword) - val tokenizableServices = makeServices(keyStoreWrapper) + val tokenizableServices = makeServices() smm = StateMachineManager(services, checkpointStorage, @@ -253,9 +204,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, if (scanResult != null) { installCordaServices(scanResult) registerInitiatedFlows(scanResult) - rpcFlows = findRPCFlows(scanResult) - } else { - rpcFlows = emptyList() + findRPCFlows(scanResult) } // TODO Remove this once the cash stuff is in its own CorDapp @@ -268,7 +217,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, smm.start() // Shut down the SMM so no Fibers are scheduled. runOnStop += { smm.stop(acceptableLiveFiberCountOnStop()) } - scheduler.start() + _services.schedulerService.start() } started = true return this @@ -286,7 +235,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } } - return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class) + scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class) .filter { val serviceType = getServiceType(it) if (serviceType != null && info.serviceIdentities(serviceType).isEmpty()) { @@ -325,10 +274,19 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } cordappServices.putInstance(serviceClass, service) smm.tokenizableServices += service + + if (service is NotaryService) handleCustomNotaryService(service) + log.info("Installed ${serviceClass.name} Corda service") return service } + private fun handleCustomNotaryService(service: NotaryService) { + runOnStop += service::stop + service.start() + installCoreFlow(NotaryFlow.Client::class, { party: Party, version: Int -> service.createServiceFlow(party, version) }) + } + private inline fun Class<*>.requireAnnotation(): A { return requireNotNull(getDeclaredAnnotation(A::class.java)) { "$name needs to be annotated with ${A::class.java.name}" } } @@ -410,19 +368,21 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return observable } - private fun findRPCFlows(scanResult: ScanResult): List>> { + private fun findRPCFlows(scanResult: ScanResult) { fun Class>.isUserInvokable(): Boolean { return isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || isStatic(modifiers)) } - return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() } + - // Add any core flows here - listOf( - ContractUpgradeFlow::class.java, - // TODO Remove all Cash flows from default list once they are split into separate CorDapp. - CashIssueFlow::class.java, - CashExitFlow::class.java, - CashPaymentFlow::class.java) + _services.rpcFlows += scanResult + .getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class) + .filter { it.isUserInvokable() } + + // Add any core flows here + listOf( + ContractUpgradeFlow::class.java, + // TODO Remove all Cash flows from default list once they are split into separate CorDapp. + CashIssueFlow::class.java, + CashExitFlow::class.java, + CashPaymentFlow::class.java) } /** @@ -446,40 +406,28 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, installCoreFlow(BroadcastTransactionFlow::class) { otherParty, _ -> NotifyTransactionHandler(otherParty) } installCoreFlow(NotaryChangeFlow::class) { otherParty, _ -> NotaryChangeHandler(otherParty) } installCoreFlow(ContractUpgradeFlow::class) { otherParty, _ -> ContractUpgradeHandler(otherParty) } + installCoreFlow(TransactionKeyFlow::class) { otherParty, _ -> TransactionKeyHandler(otherParty) } } /** * Builds node internal, advertised, and plugin services. * Returns a list of tokenizable services to be added to the serialisation context. */ - private fun makeServices(keyStoreWrapper: KeyStoreWrapper): MutableList { - val keyStore = keyStoreWrapper.keyStore - val storageServices = initialiseStorageService(configuration.baseDirectory) - storage = storageServices.first - checkpointStorage = storageServices.second - netMapCache = InMemoryNetworkMapCache() + private fun makeServices(): MutableList { + checkpointStorage = DBCheckpointStorage() + _services = ServiceHubInternalImpl() + attachments = createAttachmentStorage() network = makeMessagingService() - schemas = makeSchemaService() - vault = makeVaultService(configuration.dataSourceProperties) - vaultQuery = makeVaultQueryService(schemas) - txVerifierService = makeTransactionVerifierService() - auditService = DummyAuditService() - info = makeInfo() - identity = makeIdentityService(keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)!! as X509Certificate, - keyStoreWrapper.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA), - info.legalIdentityAndCert) - // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because - // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with - // the identity key. But the infrastructure to make that easy isn't here yet. - keyManagement = makeKeyManagementService(identity) - scheduler = NodeSchedulerService(services, database, unfinishedSchedules = busyNodeLatch) - val tokenizableServices = mutableListOf(storage, network, vault, keyManagement, identity, platformClock, scheduler) + val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.vaultQueryService, + services.keyManagementService, services.identityService, platformClock, services.schedulerService) makeAdvertisedServices(tokenizableServices) return tokenizableServices } + protected open fun makeTransactionStorage(): WritableTransactionStorage = DBTransactionStorage() + private fun scanCordapps(): ScanResult? { val scanPackage = System.getProperty("net.corda.node.cordapp.scan.package") val paths = if (scanPackage != null) { @@ -533,22 +481,23 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } private fun initUploaders() { - val uploaders: List = listOf(storage.attachments as NodeAttachmentService) + - cordappServices.values.filterIsInstance(AcceptsFileUpload::class.java) - (storage as StorageServiceImpl).initUploaders(uploaders) + _services.uploaders += attachments + cordappServices.values.filterIsInstanceTo(_services.uploaders, AcceptsFileUpload::class.java) } private fun makeVaultObservers() { - VaultSoftLockManager(vault, smm) + VaultSoftLockManager(services.vaultService, smm) CashBalanceAsMetricsObserver(services, database) ScheduledActivityObserver(services) - HibernateObserver(vault.rawUpdates, HibernateConfiguration(schemas)) + HibernateObserver(services.vaultService.rawUpdates, HibernateConfiguration(services.schemaService)) } private fun makeInfo(): NodeInfo { val advertisedServiceEntries = makeServiceEntries() val legalIdentity = obtainLegalIdentity() - return NodeInfo(network.myAddress, legalIdentity, platformVersion, advertisedServiceEntries, findMyLocation()) + val allIdentitiesSet = advertisedServiceEntries.map { it.identity }.toSet() + legalIdentity + val addresses = myAddresses() // TODO There is no support for multiple IP addresses yet. + return NodeInfo(addresses, legalIdentity, allIdentitiesSet, platformVersion, advertisedServiceEntries, findMyLocation()) } /** @@ -567,19 +516,30 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, @VisibleForTesting protected open fun acceptableLiveFiberCountOnStop(): Int = 0 - private fun hasSSLCertificates(): Boolean { - val (sslKeystore, keystore) = try { + private fun validateKeystore() { + val containCorrectKeys = try { // This will throw IOException if key file not found or KeyStoreException if keystore password is incorrect. - Pair( - KeyStoreUtilities.loadKeyStore(configuration.sslKeystore, configuration.keyStorePassword), - KeyStoreUtilities.loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword)) - } catch (e: IOException) { - return false + val sslKeystore = KeyStoreUtilities.loadKeyStore(configuration.sslKeystore, configuration.keyStorePassword) + val identitiesKeystore = KeyStoreUtilities.loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword) + sslKeystore.containsAlias(X509Utilities.CORDA_CLIENT_TLS) && identitiesKeystore.containsAlias(X509Utilities.CORDA_CLIENT_CA) } catch (e: KeyStoreException) { log.warn("Certificate key store found but key store password does not match configuration.") - return false + false + } catch (e: IOException) { + false + } + require(containCorrectKeys) { + "Identity certificate not found. " + + "Please either copy your existing identity key and certificate from another node, " + + "or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " + + "Read more at: https://docs.corda.net/permissioning.html" + } + val identitiesKeystore = KeyStoreUtilities.loadKeyStore(configuration.sslKeystore, configuration.keyStorePassword) + val tlsIdentity = identitiesKeystore.getX509Certificate(X509Utilities.CORDA_CLIENT_TLS).subject + + require(tlsIdentity == configuration.myLegalName) { + "Expected '${configuration.myLegalName}' but got '$tlsIdentity' from the keystore." } - return sslKeystore.containsAlias(X509Utilities.CORDA_CLIENT_TLS) && keystore.containsAlias(X509Utilities.CORDA_CLIENT_CA) } // Specific class so that MockNode can catch it. @@ -608,7 +568,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, * Run any tasks that are needed to ensure the node is in a correct state before running start(). */ open fun setup(): AbstractNode { - createNodeDir() + configuration.baseDirectory.createDirectories() return this } @@ -618,7 +578,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val notaryServiceType = serviceTypes.singleOrNull { it.isNotary() } if (notaryServiceType != null) { - makeNotaryService(notaryServiceType, tokenizableServices) + makeCoreNotaryService(notaryServiceType, tokenizableServices) } } @@ -641,7 +601,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, require(networkMapAddress != null || NetworkMapService.type in advertisedServices.map { it.type }) { "Initial network map address must indicate a node that provides a network map service" } - val address = networkMapAddress ?: info.address + val address: SingleMessageRecipient = networkMapAddress ?: + network.getAddressOfParty(PartyInfo.Node(info)) as SingleMessageRecipient // Register for updates, even if we're the one running the network map. return sendNetworkMapRegistration(address).flatMap { (error) -> check(error == null) { "Unable to register with the network map service: $error" } @@ -656,10 +617,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val expires = instant + NetworkMapService.DEFAULT_EXPIRATION_PERIOD val reg = NodeRegistration(info, instant.toEpochMilli(), ADD, expires) val legalIdentityKey = obtainLegalIdentityKey() - val request = NetworkMapService.RegistrationRequest(reg.toWire(keyManagement, legalIdentityKey.public), network.myAddress) + val request = RegistrationRequest(reg.toWire(services.keyManagementService, legalIdentityKey.public), network.myAddress) return network.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapAddress) } + /** Return list of node's addresses. It's overridden in MockNetwork as we don't have real addresses for MockNodes. */ + protected abstract fun myAddresses(): List + /** This is overriden by the mock node implementation to enable operation without any network map service */ protected open fun noNetworkMapConfigured(): ListenableFuture { // TODO: There should be a consistent approach to configuration error exceptions. @@ -675,35 +639,27 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, inNodeNetworkMapService = PersistentNetworkMapService(services, configuration.minimumPlatformVersion) } - open protected fun makeNotaryService(type: ServiceType, tokenizableServices: MutableList) { - val timeWindowChecker = TimeWindowChecker(platformClock, 30.seconds) - val uniquenessProvider = makeUniquenessProvider(type) - tokenizableServices.add(uniquenessProvider) - - val notaryService = when (type) { - SimpleNotaryService.type -> SimpleNotaryService(timeWindowChecker, uniquenessProvider) - ValidatingNotaryService.type -> ValidatingNotaryService(timeWindowChecker, uniquenessProvider) - RaftNonValidatingNotaryService.type -> RaftNonValidatingNotaryService(timeWindowChecker, uniquenessProvider as RaftUniquenessProvider) - RaftValidatingNotaryService.type -> RaftValidatingNotaryService(timeWindowChecker, uniquenessProvider as RaftUniquenessProvider) - BFTNonValidatingNotaryService.type -> with(configuration) { - val replicaId = bftReplicaId ?: throw IllegalArgumentException("bftReplicaId value must be specified in the configuration") - BFTSMaRtConfig(notaryClusterAddresses).use { config -> - BFTNonValidatingNotaryService(config, services, timeWindowChecker, replicaId, database).also { - tokenizableServices += it.client - runOnStop += it::dispose - } - } - } + open protected fun makeCoreNotaryService(type: ServiceType, tokenizableServices: MutableList) { + val service: NotaryService = when (type) { + SimpleNotaryService.type -> SimpleNotaryService(services) + ValidatingNotaryService.type -> ValidatingNotaryService(services) + RaftNonValidatingNotaryService.type -> RaftNonValidatingNotaryService(services) + RaftValidatingNotaryService.type -> RaftValidatingNotaryService(services) + BFTNonValidatingNotaryService.type -> BFTNonValidatingNotaryService(services) else -> { - throw IllegalArgumentException("Notary type ${type.id} is not handled by makeNotaryService.") + log.info("Notary type ${type.id} does not match any built-in notary types. " + + "It is expected to be loaded via a CorDapp") + return } } - - installCoreFlow(NotaryFlow.Client::class, notaryService.serviceFlowFactory) + service.apply { + tokenizableServices.add(this) + runOnStop += this::stop + start() + } + installCoreFlow(NotaryFlow.Client::class, { party: Party, version: Int -> service.createServiceFlow(party, version) }) } - protected abstract fun makeUniquenessProvider(type: ServiceType): UniquenessProvider - protected open fun makeIdentityService(trustRoot: X509Certificate, clientCa: CertificateAndKeyPair?, legalIdentity: PartyAndCertificate): IdentityService { @@ -712,7 +668,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, .toTypedArray() val service = InMemoryIdentityService(setOf(info.legalIdentityAndCert), trustRoot = trustRoot, caCertificates = *caCertificates) services.networkMapCache.partyNodes.forEach { service.registerIdentity(it.legalIdentityAndCert) } - netMapCache.changed.subscribe { mapChange -> + services.networkMapCache.changed.subscribe { mapChange -> // TODO how should we handle network map removal if (mapChange is MapChange.Added) { service.registerIdentity(mapChange.node.legalIdentityAndCert) @@ -721,13 +677,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return service } - // TODO: sort out ordering of open & protected modifiers of functions in this class. - protected open fun makeVaultService(dataSourceProperties: Properties): VaultService = NodeVaultService(services, dataSourceProperties) - - protected open fun makeVaultQueryService(schemas: SchemaService): VaultQueryService = HibernateVaultQueryImpl(HibernateConfiguration(schemas), vault.updatesPublisher) - - protected open fun makeSchemaService(): SchemaService = NodeSchemaService(pluginRegistries.flatMap { it.requiredSchemas }.toSet()) - protected abstract fun makeTransactionVerifierService(): TransactionVerifierService open fun stop() { @@ -748,22 +697,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected abstract fun startMessagingService(rpcOps: RPCOps) - protected open fun initialiseStorageService(dir: Path): Pair { - val attachments = makeAttachmentStorage(dir) - val checkpointStorage = DBCheckpointStorage() - val transactionStorage = DBTransactionStorage() - val stateMachineTransactionMappingStorage = DBTransactionMappingStorage() - return Pair( - constructStorageService(attachments, transactionStorage, stateMachineTransactionMappingStorage), - checkpointStorage - ) - } - - protected open fun constructStorageService(attachments: AttachmentStorage, - transactionStorage: TransactionStorage, - stateMachineRecordedTransactionMappingStorage: StateMachineRecordedTransactionMappingStorage) = - StorageServiceImpl(attachments, transactionStorage, stateMachineRecordedTransactionMappingStorage) - protected fun obtainLegalIdentity(): PartyAndCertificate = identityKeyPair.first protected fun obtainLegalIdentityKey(): KeyPair = identityKeyPair.second private val identityKeyPair by lazy { obtainKeyPair("identity", configuration.myLegalName) } @@ -808,9 +741,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, if (myIdentity.owningKey !is CompositeKey) { // TODO: Support case where owningKey is a composite key. keyStore.save(serviceName, privateKeyAlias, keyPair) } - val partyAndCertificate = getTestPartyAndCertificate(myIdentity) + val dummyCaKey = entropyToKeyPair(BigInteger.valueOf(111)) + val dummyCa = CertificateAndKeyPair( + X509Utilities.createSelfSignedCACertificate(X500Name("CN=Dummy CA,OU=Corda,O=R3 Ltd,L=London,C=GB"), dummyCaKey), + dummyCaKey) + val partyAndCertificate = getTestPartyAndCertificate(myIdentity, dummyCa) // Sanity check the certificate and path - val validatorParameters = PKIXParameters(setOf(TrustAnchor(DUMMY_CA.certificate.cert, null))) + val validatorParameters = PKIXParameters(setOf(TrustAnchor(dummyCa.certificate.cert, null))) val validator = CertPathValidator.getInstance("PKIX") validatorParameters.isRevocationEnabled = false validator.validate(partyAndCertificate.certPath, validatorParameters) as PKIXCertPathValidatorResult @@ -831,20 +768,73 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return identityCertPathAndKey } + private fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair): PartyAndCertificate { + val certFactory = CertificateFactory.getInstance("X509") + val certHolder = X509Utilities.createCertificate(CertificateType.IDENTITY, trustRoot.certificate, trustRoot.keyPair, party.name, party.owningKey) + val certPath = certFactory.generateCertPath(listOf(certHolder.cert, trustRoot.certificate.cert)) + return PartyAndCertificate(party, certHolder, certPath) + } + protected open fun generateKeyPair() = cryptoGenerateKeyPair() - protected fun makeAttachmentStorage(dir: Path): AttachmentStorage { - val attachmentsDir = dir / "attachments" - try { - attachmentsDir.createDirectory() - } catch (e: FileAlreadyExistsException) { - } + private fun createAttachmentStorage(): NodeAttachmentService { + val attachmentsDir = (configuration.baseDirectory / "attachments").createDirectories() return NodeAttachmentService(attachmentsDir, configuration.dataSourceProperties, services.monitoringService.metrics) } - protected fun createNodeDir() { - configuration.baseDirectory.createDirectories() + private inner class ServiceHubInternalImpl : ServiceHubInternal, SingletonSerializeAsToken() { + override val rpcFlows = ArrayList>>() + override val uploaders = ArrayList() + override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage() + override val auditService = DummyAuditService() + override val monitoringService = MonitoringService(MetricRegistry()) + override val validatedTransactions = makeTransactionStorage() + override val transactionVerifierService by lazy { makeTransactionVerifierService() } + override val networkMapCache by lazy { InMemoryNetworkMapCache(this) } + override val vaultService by lazy { NodeVaultService(this, configuration.dataSourceProperties) } + override val vaultQueryService by lazy { + HibernateVaultQueryImpl(HibernateConfiguration(schemaService), vaultService.updatesPublisher) + } + // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because + // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with + // the identity key. But the infrastructure to make that easy isn't here yet. + override val keyManagementService by lazy { makeKeyManagementService(identityService) } + override val schedulerService by lazy { NodeSchedulerService(this, unfinishedSchedules = busyNodeLatch) } + override val identityService by lazy { + val keyStoreWrapper = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword) + makeIdentityService( + keyStoreWrapper.keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA)!! as X509Certificate, + keyStoreWrapper.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA), + info.legalIdentityAndCert) + } + override val attachments: AttachmentStorage get() = this@AbstractNode.attachments + override val networkService: MessagingService get() = network + override val clock: Clock get() = platformClock + override val myInfo: NodeInfo get() = info + override val schemaService by lazy { NodeSchemaService(pluginRegistries.flatMap { it.requiredSchemas }.toSet()) } + override val database: Database get() = this@AbstractNode.database + override val configuration: NodeConfiguration get() = this@AbstractNode.configuration + + override fun cordaService(type: Class): T { + require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } + return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") + } + + override fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator): FlowStateMachineImpl { + return serverThread.fetchFrom { smm.add(logic, flowInitiator) } + } + + override fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? { + return flowFactories[initiatingFlowClass] + } + + override fun recordTransactions(txs: Iterable) { + database.transaction { + super.recordTransactions(txs) + } + } } + } private class KeyStoreWrapper(val keyStore: KeyStore, val storePath: Path, private val storePassword: String) { diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index 479b58c8db..f881064e27 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -8,13 +8,12 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.messaging.* 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.queryBy import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort @@ -43,16 +42,16 @@ class CordaRPCOpsImpl( private val smm: StateMachineManager, private val database: Database ) : CordaRPCOps { - override fun networkMapUpdates(): Pair, Observable> { + override fun networkMapFeed(): DataFeed, NetworkMapCache.MapChange> { return database.transaction { services.networkMapCache.track() } } - override fun vaultAndUpdates(): Pair>, Observable> { + override fun vaultAndUpdates(): DataFeed>, Vault.Update> { return database.transaction { val (vault, updates) = services.vaultService.track() - Pair(vault.states.toList(), updates) + DataFeed(vault.states.toList(), updates) } } @@ -69,31 +68,31 @@ class CordaRPCOpsImpl( override fun vaultTrackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, - contractType: Class): Vault.PageAndUpdates { + contractType: Class): DataFeed, Vault.Update> { return database.transaction { - services.vaultQueryService._trackBy(criteria, paging, sorting, contractType) + services.vaultQueryService._trackBy(criteria, paging, sorting, contractType) } } - override fun verifiedTransactions(): Pair, Observable> { + override fun verifiedTransactionsFeed(): DataFeed, SignedTransaction> { return database.transaction { - services.storageService.validatedTransactions.track() + services.validatedTransactions.track() } } - override fun stateMachinesAndUpdates(): Pair, Observable> { + override fun stateMachinesFeed(): DataFeed, StateMachineUpdate> { return database.transaction { val (allStateMachines, changes) = smm.track() - Pair( + DataFeed( allStateMachines.map { stateMachineInfoFromFlowLogic(it.logic) }, changes.map { stateMachineUpdateFromStateMachineChange(it) } ) } } - override fun stateMachineRecordedTransactionMapping(): Pair, Observable> { + override fun stateMachineRecordedTransactionMappingFeed(): DataFeed, StateMachineTransactionMapping> { return database.transaction { - services.storageService.stateMachineRecordedTransactionMapping.track() + services.stateMachineRecordedTransactionMapping.track() } } @@ -144,21 +143,21 @@ class CordaRPCOpsImpl( override fun attachmentExists(id: SecureHash): Boolean { // TODO: this operation should not require an explicit transaction return database.transaction { - services.storageService.attachments.openAttachment(id) != null + services.attachments.openAttachment(id) != null } } override fun openAttachment(id: SecureHash): InputStream { // TODO: this operation should not require an explicit transaction return database.transaction { - services.storageService.attachments.openAttachment(id)!!.open() + services.attachments.openAttachment(id)!!.open() } } override fun uploadAttachment(jar: InputStream): SecureHash { // TODO: this operation should not require an explicit transaction return database.transaction { - services.storageService.attachments.importAttachment(jar) + services.attachments.importAttachment(jar) } } @@ -167,7 +166,7 @@ class CordaRPCOpsImpl( override fun currentNodeTime(): Instant = Instant.now(services.clock) @Suppress("OverridingDeprecatedMember", "DEPRECATION") override fun uploadFile(dataType: String, name: String?, file: InputStream): String { - val acceptor = services.storageService.uploaders.firstOrNull { it.accepts(dataType) } + val acceptor = services.uploaders.firstOrNull { it.accepts(dataType) } return database.transaction { acceptor?.upload(file) ?: throw RuntimeException("Cannot find file upload acceptor for $dataType") } @@ -180,6 +179,7 @@ class CordaRPCOpsImpl( override fun partyFromName(name: String) = services.identityService.partyFromName(name) override fun partyFromX500Name(x500Name: X500Name) = services.identityService.partyFromX500Name(x500Name) override fun partiesFromName(query: String, exactMatch: Boolean): Set = services.identityService.partiesFromName(query, exactMatch) + override fun nodeIdentityFromParty(party: AbstractParty): NodeInfo? = services.networkMapCache.getNodeByLegalIdentity(party) override fun registeredFlows(): List = services.rpcFlows.map { it.name }.sorted() diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 256412301e..0fb3e90970 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -1,22 +1,19 @@ package net.corda.node.internal import com.codahale.metrics.JmxReporter -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture -import net.corda.core.flatMap +import net.corda.core.* import net.corda.core.messaging.RPCOps -import net.corda.core.minutes import net.corda.core.node.ServiceHub -import net.corda.core.node.VersionInfo import net.corda.core.node.services.ServiceInfo -import net.corda.core.node.services.ServiceType -import net.corda.core.node.services.UniquenessProvider import net.corda.core.seconds -import net.corda.core.success +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.parseNetworkHostAndPort import net.corda.core.utilities.trace +import net.corda.node.VersionInfo import net.corda.node.serialization.NodeClock import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserServiceImpl @@ -26,12 +23,9 @@ import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDete import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDetectResponseProperty import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.NodeMessagingClient -import net.corda.node.services.transactions.PersistentUniquenessProvider -import net.corda.node.services.transactions.RaftNonValidatingNotaryService -import net.corda.node.services.transactions.RaftUniquenessProvider -import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.AddressUtils import net.corda.node.utilities.AffinityExecutor +import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.nodeapi.ArtemisMessagingComponent.Companion.IP_REQUEST_PREFIX import net.corda.nodeapi.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.ArtemisMessagingComponent.NetworkMapAddress @@ -160,24 +154,24 @@ open class Node(override val configuration: FullNodeConfiguration, advertisedAddress) } - private fun makeLocalMessageBroker(): HostAndPort { + private fun makeLocalMessageBroker(): NetworkHostAndPort { with(configuration) { messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, userService) - return HostAndPort.fromParts("localhost", p2pAddress.port) + return NetworkHostAndPort("localhost", p2pAddress.port) } } - private fun getAdvertisedAddress(): HostAndPort { + private fun getAdvertisedAddress(): NetworkHostAndPort { return with(configuration) { if (relay != null) { - HostAndPort.fromParts(relay.relayHost, relay.remoteInboundPort) + NetworkHostAndPort(relay.relayHost, relay.remoteInboundPort) } else { val useHost = if (detectPublicIp) { tryDetectIfNotPublicHost(p2pAddress.host) ?: p2pAddress.host } else { p2pAddress.host } - HostAndPort.fromParts(useHost, p2pAddress.port) + NetworkHostAndPort(useHost, p2pAddress.port) } } } @@ -210,7 +204,7 @@ open class Node(override val configuration: FullNodeConfiguration, * it back to the queue. * - Once the message is received the session is closed and the queue deleted. */ - private fun discoverPublicHost(serverAddress: HostAndPort): String? { + private fun discoverPublicHost(serverAddress: NetworkHostAndPort): String? { log.trace { "Trying to detect public hostname through the Network Map Service at $serverAddress" } val tcpTransport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), serverAddress, configuration) val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { @@ -236,14 +230,14 @@ open class Node(override val configuration: FullNodeConfiguration, val consumer = session.createConsumer(queueName) val artemisMessage: ClientMessage = consumer.receive(10.seconds.toMillis()) ?: throw IOException("Did not receive a response from the Network Map Service at $serverAddress") - val publicHostAndPort = HostAndPort.fromString(artemisMessage.getStringProperty(ipDetectResponseProperty)) + val publicHostAndPort = artemisMessage.getStringProperty(ipDetectResponseProperty) log.info("Detected public address: $publicHostAndPort") consumer.close() session.deleteQueue(queueName) clientFactory.close() - return publicHostAndPort.host.removePrefix("/") + return publicHostAndPort.removePrefix("/").parseNetworkHostAndPort().host } override fun startMessagingService(rpcOps: RPCOps) { @@ -266,16 +260,9 @@ open class Node(override val configuration: FullNodeConfiguration, return networkMapConnection.flatMap { super.registerWithNetworkMap() } } - override fun makeUniquenessProvider(type: ServiceType): UniquenessProvider { - return when (type) { - RaftValidatingNotaryService.type, RaftNonValidatingNotaryService.type -> with(configuration) { - val provider = RaftUniquenessProvider(baseDirectory, notaryNodeAddress!!, notaryClusterAddresses, database, configuration) - provider.start() - runOnStop += provider::stop - provider - } - else -> PersistentUniquenessProvider() - } + override fun myAddresses(): List { + val address = network.myAddress as ArtemisMessagingComponent.ArtemisPeerAddress + return listOf(address.hostAndPort) } /** @@ -313,27 +300,29 @@ open class Node(override val configuration: FullNodeConfiguration, override fun start(): Node { super.start() - networkMapRegistrationFuture.success(serverThread) { - // Begin exporting our own metrics via JMX. These can be monitored using any agent, e.g. Jolokia: - // - // https://jolokia.org/agent/jvm.html - JmxReporter. - forRegistry(services.monitoringService.metrics). - inDomain("net.corda"). - createsObjectNamesWith { _, domain, name -> - // Make the JMX hierarchy a bit better organised. - val category = name.substringBefore('.') - val subName = name.substringAfter('.', "") - if (subName == "") - ObjectName("$domain:name=$category") - else - ObjectName("$domain:type=$category,name=$subName") - }. - build(). - start() + networkMapRegistrationFuture.thenMatch({ + serverThread.execute { + // Begin exporting our own metrics via JMX. These can be monitored using any agent, e.g. Jolokia: + // + // https://jolokia.org/agent/jvm.html + JmxReporter. + forRegistry(services.monitoringService.metrics). + inDomain("net.corda"). + createsObjectNamesWith { _, domain, name -> + // Make the JMX hierarchy a bit better organised. + val category = name.substringBefore('.') + val subName = name.substringAfter('.', "") + if (subName == "") + ObjectName("$domain:name=$category") + else + ObjectName("$domain:type=$category,name=$subName") + }. + build(). + start() - (startupComplete as SettableFuture).set(Unit) - } + (startupComplete as SettableFuture).set(Unit) + } + }, {}) shutdownHook = addShutdownHook { stop() } @@ -375,4 +364,4 @@ open class Node(override val configuration: FullNodeConfiguration, class ConfigurationException(message: String) : Exception(message) -data class NetworkMapInfo(val address: HostAndPort, val legalName: X500Name) +data class NetworkMapInfo(val address: NetworkHostAndPort, val legalName: X500Name) diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index cfef0dbe48..6e210f3c70 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -8,7 +8,7 @@ import joptsimple.OptionException import net.corda.core.* import net.corda.core.crypto.commonName import net.corda.core.crypto.orgName -import net.corda.core.node.VersionInfo +import net.corda.node.VersionInfo import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.Emoji import net.corda.core.utilities.loggerFor @@ -75,7 +75,7 @@ open class NodeStartup(val args: Array) { banJavaSerialisation(conf) // TODO: Move this to EnterpriseNode.Startup conf.relay?.let { connectToRelay(it, conf.p2pAddress.port) } - preNetworkRegistration() + preNetworkRegistration(conf) maybeRegisterWithNetworkAndExit(cmdlineOptions, conf) logStartupInfo(versionInfo, cmdlineOptions, conf) @@ -94,7 +94,7 @@ open class NodeStartup(val args: Array) { exitProcess(0) } - open protected fun preNetworkRegistration() = Unit + open protected fun preNetworkRegistration(conf: FullNodeConfiguration) = Unit open protected fun createNode(conf: FullNodeConfiguration, versionInfo: VersionInfo, services: Set): Node { return Node(conf, services, versionInfo, if (conf.useTestClock) TestClock() else NodeClock()) @@ -106,7 +106,7 @@ open class NodeStartup(val args: Array) { node.start() printPluginsAndServices(node) - node.networkMapRegistrationFuture.success { + node.networkMapRegistrationFuture.thenMatch({ val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 // TODO: Replace this with a standard function to get an unambiguous rendering of the X.500 name. val name = node.info.legalIdentity.name.orgName ?: node.info.legalIdentity.name.commonName @@ -114,17 +114,14 @@ open class NodeStartup(val args: Array) { // Don't start the shell if there's no console attached. val runShell = !cmdlineOptions.noLocalShell && System.console() != null - node.startupComplete then { + node.startupComplete.then { try { InteractiveShell.startShell(cmdlineOptions.baseDirectory, runShell, cmdlineOptions.sshdServer, node) } catch(e: Throwable) { logger.error("Shell failed to start", e) } } - } failure { - logger.error("Error during network map registration", it) - exitProcess(1) - } + }, {}) node.run() } diff --git a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt index e6b8c0e6e9..3359d55e58 100644 --- a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt +++ b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt @@ -10,6 +10,7 @@ import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap import net.corda.flows.* @@ -27,14 +28,14 @@ import net.corda.flows.* */ class FetchTransactionsHandler(otherParty: Party) : FetchDataHandler(otherParty) { override fun getData(id: SecureHash): SignedTransaction? { - return serviceHub.storageService.validatedTransactions.getTransaction(id) + return serviceHub.validatedTransactions.getTransaction(id) } } // TODO: Use Artemis message streaming support here, called "large messages". This avoids the need to buffer. class FetchAttachmentsHandler(otherParty: Party) : FetchDataHandler(otherParty) { override fun getData(id: SecureHash): ByteArray? { - return serviceHub.storageService.attachments.openAttachment(id)?.open()?.readBytes() + return serviceHub.attachments.openAttachment(id)?.open()?.readBytes() } } @@ -122,3 +123,24 @@ class ContractUpgradeHandler(otherSide: Party) : AbstractStateReplacementFlow.Ac ContractUpgradeFlow.verify(oldStateAndRef.state.data, expectedTx.outRef(0).state.data, expectedTx.commands.single()) } } + +class TransactionKeyHandler(val otherSide: Party, val revocationEnabled: Boolean) : FlowLogic() { + constructor(otherSide: Party) : this(otherSide, false) + companion object { + object SENDING_KEY : ProgressTracker.Step("Sending key") + } + + override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY) + + @Suspendable + override fun call(): Unit { + val revocationEnabled = false + progressTracker.currentStep = SENDING_KEY + val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) + val otherSideAnonymous = sendAndReceive(otherSide, legalIdentityAnonymous).unwrap { TransactionKeyFlow.validateIdentity(otherSide, it) } + val (certPath, theirCert, txIdentity) = otherSideAnonymous + // Validate then store their identity so that we can prove the key in the transaction is owned by the + // counterparty. + serviceHub.identityService.registerAnonymousIdentity(txIdentity, otherSide, certPath) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 5ad98740a3..861ff1db34 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -2,21 +2,28 @@ package net.corda.node.services.api import com.google.common.annotations.VisibleForTesting import com.google.common.util.concurrent.ListenableFuture +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.internal.FlowStateMachine +import net.corda.core.messaging.DataFeed import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.node.NodeInfo import net.corda.core.node.PluginServiceHub +import net.corda.core.node.services.FileUploader import net.corda.core.node.services.NetworkMapCache -import net.corda.core.node.services.TxWritableStorageService +import net.corda.core.node.services.TransactionStorage import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.loggerFor import net.corda.node.internal.InitiatedFlowFactory +import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowStateMachineImpl +import org.jetbrains.exposed.sql.Database interface NetworkMapCacheInternal : NetworkMapCache { /** @@ -55,32 +62,39 @@ sealed class NetworkCacheError : Exception() { class DeregistrationFailed : NetworkCacheError() } -abstract class ServiceHubInternal : PluginServiceHub { +interface ServiceHubInternal : PluginServiceHub { companion object { private val log = loggerFor() } - abstract val monitoringService: MonitoringService - abstract val schemaService: SchemaService - abstract override val networkMapCache: NetworkMapCacheInternal - abstract val schedulerService: SchedulerService - abstract val auditService: AuditService - abstract val rpcFlows: List>> - abstract val networkService: MessagingService - /** - * Given a list of [SignedTransaction]s, writes them to the given storage for validated transactions and then - * sends them to the vault for further processing. This is intended for implementations to call from - * [recordTransactions]. - * - * @param txs The transactions to record. + * 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. */ - internal fun recordTransactionsInternal(writableStorageService: TxWritableStorageService, txs: Iterable) { + override val validatedTransactions: WritableTransactionStorage + val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage + val monitoringService: MonitoringService + val schemaService: SchemaService + override val networkMapCache: NetworkMapCacheInternal + val schedulerService: SchedulerService + val auditService: AuditService + val rpcFlows: List>> + val networkService: MessagingService + val database: Database + val configuration: NodeConfiguration + + @Suppress("DEPRECATION") + @Deprecated("This service will be removed in a future milestone") + val uploaders: List + + override fun recordTransactions(txs: Iterable) { + val recordedTransactions = txs.filter { validatedTransactions.addTransaction(it) } + require(recordedTransactions.isNotEmpty()) { "No transactions passed in for recording" } val stateMachineRunId = FlowStateMachineImpl.currentStateMachine()?.id - val recordedTransactions = txs.filter { writableStorageService.validatedTransactions.addTransaction(it) } if (stateMachineRunId != null) { recordedTransactions.forEach { - storageService.stateMachineRecordedTransactionMapping.addMapping(stateMachineRunId, it.id) + stateMachineRecordedTransactionMapping.addMapping(stateMachineRunId, it.id) } } else { log.warn("Transactions recorded from outside of a state machine") @@ -99,7 +113,7 @@ abstract class ServiceHubInternal : PluginServiceHub { * Starts an already constructed flow. Note that you must be on the server thread to call this method. * @param flowInitiator indicates who started the flow, see: [FlowInitiator]. */ - abstract fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator): FlowStateMachineImpl + fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator): FlowStateMachineImpl /** * Will check [logicType] and [args] against a whitelist and if acceptable then construct and initiate the flow. @@ -119,5 +133,28 @@ abstract class ServiceHubInternal : PluginServiceHub { return startFlow(logic, flowInitiator) } - abstract fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? -} \ No newline at end of file + fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? +} + +/** + * Thread-safe storage of transactions. + */ +interface WritableTransactionStorage : TransactionStorage { + /** + * 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 +} + +/** + * 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(): DataFeed, StateMachineTransactionMapping> +} diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 8216d28467..b9afbc1553 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -1,7 +1,7 @@ package net.corda.node.services.config -import com.google.common.net.HostAndPort import net.corda.core.node.services.ServiceInfo +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.messaging.CertificateChainCheckPolicy import net.corda.node.services.network.NetworkMapService @@ -27,8 +27,8 @@ interface NodeConfiguration : NodeSSLConfiguration { val verifierType: VerifierType val messageRedeliveryDelaySeconds: Int val bftReplicaId: Int? - val notaryNodeAddress: HostAndPort? - val notaryClusterAddresses: List + val notaryNodeAddress: NetworkHostAndPort? + val notaryClusterAddresses: List } data class FullNodeConfiguration( @@ -50,16 +50,16 @@ data class FullNodeConfiguration( override val messageRedeliveryDelaySeconds: Int = 30, val useHTTPS: Boolean, @OldConfig("artemisAddress") - val p2pAddress: HostAndPort, - val rpcAddress: HostAndPort?, + val p2pAddress: NetworkHostAndPort, + val rpcAddress: NetworkHostAndPort?, val relay: RelayConfiguration?, // TODO This field is slightly redundant as p2pAddress is sufficient to hold the address of the node's MQ broker. // Instead this should be a Boolean indicating whether that broker is an internal one started by the node or an external one - val messagingServerAddress: HostAndPort?, + val messagingServerAddress: NetworkHostAndPort?, val extraAdvertisedServiceIds: List, override val bftReplicaId: Int?, - override val notaryNodeAddress: HostAndPort?, - override val notaryClusterAddresses: List, + override val notaryNodeAddress: NetworkHostAndPort?, + override val notaryClusterAddresses: List, override val certificateChainCheckPolicies: List, override val devMode: Boolean = false, val useTestClock: Boolean = false, diff --git a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt index 6faa38d735..b69906afbe 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt @@ -17,7 +17,6 @@ import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.utilities.* import org.apache.activemq.artemis.utils.ReusableLatch -import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.statements.InsertStatement import java.time.Instant @@ -43,7 +42,6 @@ import javax.annotation.concurrent.ThreadSafe */ @ThreadSafe class NodeSchedulerService(private val services: ServiceHubInternal, - private val database: Database, private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(), private val unfinishedSchedules: ReusableLatch = ReusableLatch()) : SchedulerService, SingletonSerializeAsToken() { @@ -159,7 +157,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, } private fun onTimeReached(scheduledState: ScheduledStateRef) { - database.transaction { + services.database.transaction { val scheduledFlow = getScheduledFlow(scheduledState) if (scheduledFlow != null) { // TODO Because the flow is executed asynchronously, there is a small window between this tx we're in diff --git a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt index fd7074833f..0222ba6fa5 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt @@ -5,11 +5,11 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.keys import net.corda.core.crypto.sign -import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.flows.AnonymisedIdentity import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.operator.ContentSigner import java.security.KeyPair @@ -58,7 +58,7 @@ class E2ETestKeyManagementService(val identityService: IdentityService, return keyPair.public } - override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair { + override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): AnonymisedIdentity { return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled) } @@ -71,6 +71,10 @@ class E2ETestKeyManagementService(val identityService: IdentityService, } } + override fun filterMyKeys(candidateKeys: Iterable): Iterable { + return mutex.locked { candidateKeys.filter { it in this.keys } } + } + override fun sign(bytes: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey { val keyPair = getSigningKeyPair(publicKey) val signature = keyPair.sign(bytes) diff --git a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt index 7f4dff1c5e..481e9e8246 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt @@ -4,6 +4,7 @@ import net.corda.core.crypto.* import net.corda.core.identity.AnonymousParty import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService +import net.corda.flows.AnonymisedIdentity import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.operator.ContentSigner import java.security.KeyPair @@ -30,7 +31,7 @@ fun freshCertificate(identityService: IdentityService, subjectPublicKey: PublicKey, issuer: PartyAndCertificate, issuerSigner: ContentSigner, - revocationEnabled: Boolean = false): Pair { + revocationEnabled: Boolean = false): AnonymisedIdentity { val issuerCertificate = issuer.certificate val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, Duration.ofDays(10 * 365), issuerCertificate) val ourCertificate = Crypto.createCertificate(CertificateType.IDENTITY, issuerCertificate.subject, issuerSigner, issuer.name, subjectPublicKey, window) @@ -39,7 +40,7 @@ fun freshCertificate(identityService: IdentityService, identityService.registerAnonymousIdentity(AnonymousParty(subjectPublicKey), issuer.party, ourCertPath) - return Pair(issuerCertificate, ourCertPath) + return AnonymisedIdentity(ourCertPath, issuerCertificate, subjectPublicKey) } fun getSigner(issuerKeyPair: KeyPair): ContentSigner { diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index c1f2c692a5..359239bc0f 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -9,15 +9,14 @@ import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.flows.AnonymisedIdentity import net.corda.node.utilities.* -import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.operator.ContentSigner import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.statements.InsertStatement import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey -import java.security.cert.CertPath /** * A persistent re-implementation of [E2ETestKeyManagementService] to support node re-start. @@ -60,6 +59,10 @@ class PersistentKeyManagementService(val identityService: IdentityService, override val keys: Set get() = mutex.locked { keys.keys } + override fun filterMyKeys(candidateKeys: Iterable): Iterable { + return mutex.locked { candidateKeys.filter { it in this.keys } } + } + override fun freshKey(): PublicKey { val keyPair = generateKeyPair() mutex.locked { @@ -68,7 +71,7 @@ class PersistentKeyManagementService(val identityService: IdentityService, return keyPair.public } - override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair { + override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): AnonymisedIdentity { return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled) } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 5b2d5f9c3c..66b2da18f6 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -1,6 +1,5 @@ package net.corda.node.services.messaging -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import io.netty.handler.ssl.SslHandler @@ -11,6 +10,7 @@ import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache.MapChange +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.node.internal.Node @@ -298,12 +298,8 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, fun deployBridgeToPeer(nodeInfo: NodeInfo) { log.debug("Deploying bridge for $queueName to $nodeInfo") - val address = nodeInfo.address - if (address is ArtemisPeerAddress) { - deployBridge(queueName, address.hostAndPort, nodeInfo.legalIdentity.name) - } else { - log.error("Don't know how to deal with $address for queue $queueName") - } + val address = nodeInfo.addresses.first() // TODO Load balancing. + deployBridge(queueName, address, nodeInfo.legalIdentity.name) } when { @@ -342,7 +338,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, */ private fun updateBridgesOnNetworkChange(change: MapChange) { fun gatherAddresses(node: NodeInfo): Sequence { - val peerAddress = node.address as ArtemisPeerAddress + val peerAddress = getArtemisPeerAddress(node) val addresses = mutableListOf(peerAddress) node.advertisedServices.mapTo(addresses) { NodeAddress.asService(it.identity.owningKey, peerAddress.hostAndPort) } return addresses.asSequence() @@ -380,7 +376,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, } private fun createTcpTransport(connectionDirection: ConnectionDirection, host: String, port: Int, enableSSL: Boolean = true) = - ArtemisTcpTransport.tcpTransport(connectionDirection, HostAndPort.fromParts(host, port), config, enableSSL = enableSSL) + ArtemisTcpTransport.tcpTransport(connectionDirection, NetworkHostAndPort(host, port), config, enableSSL = enableSSL) /** * All nodes are expected to have a public facing address called [ArtemisMessagingComponent.P2P_QUEUE] for receiving @@ -388,7 +384,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, * as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's * P2P address. */ - private fun deployBridge(queueName: String, target: HostAndPort, legalName: X500Name) { + private fun deployBridge(queueName: String, target: NetworkHostAndPort, legalName: X500Name) { val connectionDirection = ConnectionDirection.Outbound( connectorFactoryClassName = VerifyingNettyConnectorFactory::class.java.name, expectedCommonName = legalName @@ -424,7 +420,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, private val ArtemisPeerAddress.bridgeName: String get() = getBridgeName(queueName, hostAndPort) - private fun getBridgeName(queueName: String, hostAndPort: HostAndPort): String = "$queueName -> $hostAndPort" + private fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort" // This is called on one of Artemis' background threads internal fun hostVerificationFail(expectedLegalName: X500Name, errorMsg: String?) { diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt index d9003681d9..c5396a0067 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt @@ -8,7 +8,6 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.DEFAULT_SESSION_ID import net.corda.core.node.services.PartyInfo import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.DeserializeAsKotlinObjectDef import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import org.bouncycastle.asn1.x500.X500Name @@ -229,10 +228,3 @@ object TopicStringValidator { /** @throws IllegalArgumentException if the given topic contains invalid characters */ fun check(tag: String) = require(regex.matcher(tag).matches()) } - -/** - * A general Ack message that conveys no content other than it's presence for use when you want an acknowledgement - * from a recipient. Using [Unit] can be ambiguous as it is similar to [Void] and so could mean no response. - */ -@CordaSerializable -object Ack : DeserializeAsKotlinObjectDef diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index 56d56b8746..0b9a45d387 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -1,21 +1,20 @@ package net.corda.node.services.messaging -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture -import net.corda.core.ThreadBox +import net.corda.core.* +import net.corda.core.crypto.random63BitValue import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient -import net.corda.core.node.VersionInfo import net.corda.core.node.services.PartyInfo import net.corda.core.node.services.TransactionVerifierService -import net.corda.core.random63BitValue -import net.corda.core.serialization.opaque -import net.corda.core.success +import net.corda.core.utilities.opaque import net.corda.core.transactions.LedgerTransaction +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace +import net.corda.node.VersionInfo import net.corda.node.services.RPCUserService import net.corda.node.services.api.MonitoringService import net.corda.node.services.config.NodeConfiguration @@ -72,13 +71,13 @@ import javax.annotation.concurrent.ThreadSafe @ThreadSafe class NodeMessagingClient(override val config: NodeConfiguration, val versionInfo: VersionInfo, - val serverAddress: HostAndPort, + val serverAddress: NetworkHostAndPort, val myIdentity: PublicKey?, val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor, val database: Database, val networkMapRegistrationFuture: ListenableFuture, val monitoringService: MonitoringService, - advertisedAddress: HostAndPort = serverAddress + advertisedAddress: NetworkHostAndPort = serverAddress ) : ArtemisMessagingComponent(), MessagingService { companion object { private val log = loggerFor() @@ -160,7 +159,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, check(!started) { "start can't be called twice" } started = true - log.info("Connecting to server: $serverAddress") + log.info("Connecting to message broker: $serverAddress") // TODO Add broker CN to config for host verification in case the embedded broker isn't used val tcpTransport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), serverAddress, config) val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport) @@ -185,7 +184,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, // Create a queue, consumer and producer for handling P2P network messages. p2pConsumer = makeP2PConsumer(session, true) - networkMapRegistrationFuture.success { + networkMapRegistrationFuture.thenMatch({ state.locked { log.info("Network map is complete, so removing filter from P2P consumer.") try { @@ -195,7 +194,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, } p2pConsumer = makeP2PConsumer(session, false) } - } + }, {}) rpcServer = RPCServer(rpcOps, NODE_USER, NODE_USER, locator, userService, config.myLegalName) @@ -236,7 +235,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, } } - private var shutdownLatch = CountDownLatch(1) + private val shutdownLatch = CountDownLatch(1) private fun processMessage(consumer: ClientConsumer): Boolean { // Two possibilities here: @@ -286,6 +285,9 @@ class NodeMessagingClient(override val config: NodeConfiguration, while (!networkMapRegistrationFuture.isDone && processMessage(consumer)) { } + with(networkMapRegistrationFuture) { + if (isDone) getOrThrow() else andForget(log) // Trigger node shutdown here to avoid deadlock in shutdown hooks. + } } private fun runPostNetworkMap() { @@ -306,11 +308,14 @@ class NodeMessagingClient(override val config: NodeConfiguration, * consume all messages via a new consumer without a filter applied. */ fun run(serverControl: ActiveMQServerControl) { - // Build the network map. - runPreNetworkMap(serverControl) - // Process everything else once we have the network map. - runPostNetworkMap() - shutdownLatch.countDown() + try { + // Build the network map. + runPreNetworkMap(serverControl) + // Process everything else once we have the network map. + runPostNetworkMap() + } finally { + shutdownLatch.countDown() + } } private fun artemisToCordaMessage(message: ClientMessage): ReceivedMessage? { @@ -564,11 +569,10 @@ class NodeMessagingClient(override val config: NodeConfiguration, } } - override fun getAddressOfParty(partyInfo: PartyInfo): MessageRecipients { return when (partyInfo) { - is PartyInfo.Node -> partyInfo.node.address - is PartyInfo.Service -> ArtemisMessagingComponent.ServiceAddress(partyInfo.service.identity.owningKey) + is PartyInfo.Node -> getArtemisPeerAddress(partyInfo.node) + is PartyInfo.Service -> ServiceAddress(partyInfo.service.identity.owningKey) } } } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index 1306c4206b..71ee340dbb 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -12,15 +12,11 @@ import com.google.common.collect.HashMultimap import com.google.common.collect.Multimaps import com.google.common.collect.SetMultimap import com.google.common.util.concurrent.ThreadFactoryBuilder -import net.corda.core.ErrorOr +import net.corda.core.crypto.random63BitValue import net.corda.core.messaging.RPCOps -import net.corda.core.random63BitValue import net.corda.core.seconds import net.corda.core.serialization.KryoPoolWithContext -import net.corda.core.utilities.LazyStickyPool -import net.corda.core.utilities.LifeCycle -import net.corda.core.utilities.debug -import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.* import net.corda.node.services.RPCUserService import net.corda.nodeapi.* import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER @@ -43,7 +39,6 @@ import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.time.Duration import java.util.concurrent.* -import kotlin.collections.ArrayList data class RPCServerConfiguration( /** The number of threads to use for handling RPC requests */ @@ -270,14 +265,7 @@ class RPCServer( ) rpcExecutor!!.submit { val result = invokeRpc(rpcContext, clientToServer.methodName, clientToServer.arguments) - val resultWithExceptionUnwrapped = result.mapError { - if (it is InvocationTargetException) { - it.cause ?: RPCException("Caught InvocationTargetException without cause") - } else { - it - } - } - sendReply(clientToServer.id, clientToServer.clientAddress, resultWithExceptionUnwrapped) + sendReply(clientToServer.id, clientToServer.clientAddress, result) } } is RPCApi.ClientToServer.ObservablesClosed -> { @@ -287,25 +275,24 @@ class RPCServer( artemisMessage.acknowledge() } - private fun invokeRpc(rpcContext: RpcContext, methodName: String, arguments: List): ErrorOr { - return ErrorOr.catch { + private fun invokeRpc(rpcContext: RpcContext, methodName: String, arguments: List): Try { + return Try.on { try { CURRENT_RPC_CONTEXT.set(rpcContext) log.debug { "Calling $methodName" } val method = methodTable[methodName] ?: throw RPCException("Received RPC for unknown method $methodName - possible client/server version skew?") method.invoke(ops, *arguments.toTypedArray()) + } catch (e: InvocationTargetException) { + throw e.cause ?: RPCException("Caught InvocationTargetException without cause") } finally { CURRENT_RPC_CONTEXT.remove() } } } - private fun sendReply(requestId: RPCApi.RpcRequestId, clientAddress: SimpleString, resultWithExceptionUnwrapped: ErrorOr) { - val reply = RPCApi.ServerToClient.RpcReply( - id = requestId, - result = resultWithExceptionUnwrapped - ) + private fun sendReply(requestId: RPCApi.RpcRequestId, clientAddress: SimpleString, result: Try) { + val reply = RPCApi.ServerToClient.RpcReply(requestId, result) val observableContext = ObservableContext( requestId, observableMap, diff --git a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt index e1b2664b90..0d3cd6e818 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt @@ -4,11 +4,15 @@ import com.google.common.annotations.VisibleForTesting import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import net.corda.core.bufferUntilSubscribed +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.map +import net.corda.core.messaging.DataFeed import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo +import net.corda.core.node.ServiceHub import net.corda.core.node.services.DEFAULT_SESSION_ID +import net.corda.core.node.services.IdentityService import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.PartyInfo import net.corda.core.serialization.SingletonSerializeAsToken @@ -34,9 +38,13 @@ import javax.annotation.concurrent.ThreadSafe /** * Extremely simple in-memory cache of the network map. + * + * @param serviceHub an optional service hub from which we'll take the identity service. We take a service hub rather + * than the identity service directly, as this avoids problems with service start sequence (network map cache + * and identity services depend on each other). Should always be provided except for unit test cases. */ @ThreadSafe -open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCacheInternal { +open class InMemoryNetworkMapCache(private val serviceHub: ServiceHub?) : SingletonSerializeAsToken(), NetworkMapCacheInternal { companion object { val logger = loggerFor() } @@ -70,10 +78,21 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach } override fun getNodeByLegalIdentityKey(identityKey: PublicKey): NodeInfo? = registeredNodes[identityKey] + override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { + val wellKnownParty = if (serviceHub != null) { + serviceHub.identityService.partyFromAnonymous(party) + } else { + party + } - override fun track(): Pair, Observable> { + return wellKnownParty?.let { + getNodeByLegalIdentityKey(it.owningKey) + } + } + + override fun track(): DataFeed, MapChange> { synchronized(_changed) { - return Pair(partyNodes, _changed.bufferUntilSubscribed().wrapWithDatabaseTransaction()) + return DataFeed(partyNodes, _changed.bufferUntilSubscribed().wrapWithDatabaseTransaction()) } } @@ -134,7 +153,9 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach override fun deregisterForUpdates(network: MessagingService, service: NodeInfo): ListenableFuture { // Fetch the network map and register for updates at the same time val req = NetworkMapService.SubscribeRequest(false, network.myAddress) - val future = network.sendRequest(NetworkMapService.SUBSCRIPTION_TOPIC, req, service.address).map { + // `network.getAddressOfParty(partyInfo)` is a work-around for MockNetwork and InMemoryMessaging to get rid of SingleMessageRecipient in NodeInfo. + val address = network.getAddressOfParty(PartyInfo.Node(service)) + val future = network.sendRequest(NetworkMapService.SUBSCRIPTION_TOPIC, req, address).map { if (it.confirmed) Unit else throw NetworkCacheError.DeregistrationFailed() } _registrationFuture.setFuture(future) diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt index 035a1a506c..d0f51b534e 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt @@ -13,7 +13,7 @@ import net.corda.core.node.services.DEFAULT_SESSION_ID import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.ServiceType -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize @@ -273,11 +273,11 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, // subscribers when (change.type) { ADD -> { - logger.info("Added node ${node.address} to network map") + logger.info("Added node ${node.addresses} to network map") services.networkMapCache.addNode(change.node) } REMOVE -> { - logger.info("Removed node ${node.address} from network map") + logger.info("Removed node ${node.addresses} from network map") services.networkMapCache.removeNode(change.node) } } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt index 69cdf43e5c..b9c376b768 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt @@ -4,12 +4,12 @@ import net.corda.core.ThreadBox import net.corda.core.bufferUntilSubscribed import net.corda.core.crypto.SecureHash import net.corda.core.flows.StateMachineRunId -import net.corda.core.node.services.StateMachineRecordedTransactionMappingStorage -import net.corda.core.node.services.StateMachineTransactionMapping +import net.corda.core.messaging.DataFeed +import net.corda.core.messaging.StateMachineTransactionMapping +import net.corda.node.services.api.StateMachineRecordedTransactionMappingStorage import net.corda.node.utilities.* import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.statements.InsertStatement -import rx.Observable import rx.subjects.PublishSubject import javax.annotation.concurrent.ThreadSafe @@ -55,9 +55,9 @@ class DBTransactionMappingStorage : StateMachineRecordedTransactionMappingStorag } } - override fun track(): Pair, Observable> { + override fun track(): DataFeed, StateMachineTransactionMapping> { mutex.locked { - return Pair( + return DataFeed( stateMachineTransactionMap.map { StateMachineTransactionMapping(it.value, it.key) }, updates.bufferUntilSubscribed().wrapWithDatabaseTransaction() ) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index 04e42dcb13..219865b00e 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -3,8 +3,10 @@ package net.corda.node.services.persistence import com.google.common.annotations.VisibleForTesting import net.corda.core.bufferUntilSubscribed import net.corda.core.crypto.SecureHash -import net.corda.core.node.services.TransactionStorage +import net.corda.core.messaging.DataFeed +import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction +import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.utilities.* import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.exposedLogger @@ -13,7 +15,7 @@ import rx.Observable import rx.subjects.PublishSubject import java.util.Collections.synchronizedMap -class DBTransactionStorage : TransactionStorage { +class DBTransactionStorage : WritableTransactionStorage, SingletonSerializeAsToken() { private object Table : JDBCHashedTable("${NODE_DATABASE_PREFIX}transactions") { val txId = secureHash("tx_id") val transaction = blob("transaction") @@ -58,12 +60,12 @@ class DBTransactionStorage : TransactionStorage { } } - val updatesPublisher = PublishSubject.create().toSerialized() + private val updatesPublisher = PublishSubject.create().toSerialized() override val updates: Observable = updatesPublisher.wrapWithDatabaseTransaction() - override fun track(): Pair, Observable> { + override fun track(): DataFeed, SignedTransaction> { synchronized(txStorage) { - return Pair(txStorage.values.toList(), updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) + return DataFeed(txStorage.values.toList(), updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) } } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt index 301fde426f..168fee3bc8 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/InMemoryStateMachineRecordedTransactionMappingStorage.kt @@ -4,9 +4,9 @@ import net.corda.core.ThreadBox import net.corda.core.bufferUntilSubscribed import net.corda.core.crypto.SecureHash import net.corda.core.flows.StateMachineRunId -import net.corda.core.node.services.StateMachineRecordedTransactionMappingStorage -import net.corda.core.node.services.StateMachineTransactionMapping -import rx.Observable +import net.corda.core.messaging.DataFeed +import net.corda.core.messaging.StateMachineTransactionMapping +import net.corda.node.services.api.StateMachineRecordedTransactionMappingStorage import rx.subjects.PublishSubject import java.util.* import javax.annotation.concurrent.ThreadSafe @@ -32,9 +32,9 @@ class InMemoryStateMachineRecordedTransactionMappingStorage : StateMachineRecord } override fun track(): - Pair, Observable> { + DataFeed, StateMachineTransactionMapping> { mutex.locked { - return Pair( + return DataFeed( stateMachineTransactionMap.flatMap { entry -> entry.value.map { StateMachineTransactionMapping(entry.key, it) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index 0fecc0049f..bbb4ffc340 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -8,16 +8,10 @@ import com.google.common.hash.HashingInputStream import com.google.common.io.CountingInputStream import net.corda.core.contracts.AbstractAttachment import net.corda.core.contracts.Attachment -import net.corda.core.createDirectory import net.corda.core.crypto.SecureHash -import net.corda.core.div -import net.corda.core.extractZipFile import net.corda.core.isDirectory import net.corda.core.node.services.AttachmentStorage -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SerializationToken -import net.corda.core.serialization.SerializeAsToken -import net.corda.core.serialization.SerializeAsTokenContext +import net.corda.core.serialization.* import net.corda.core.utilities.loggerFor import net.corda.node.services.api.AcceptsFileUpload import net.corda.node.services.database.RequeryConfiguration @@ -38,8 +32,11 @@ import javax.annotation.concurrent.ThreadSafe * Stores attachments in H2 database. */ @ThreadSafe -class NodeAttachmentService(override var storePath: Path, dataSourceProperties: Properties, metrics: MetricRegistry) : AttachmentStorage, AcceptsFileUpload { - private val log = loggerFor() +class NodeAttachmentService(val storePath: Path, dataSourceProperties: Properties, metrics: MetricRegistry) + : AttachmentStorage, AcceptsFileUpload, SingletonSerializeAsToken() { + companion object { + private val log = loggerFor() + } val configuration = RequeryConfiguration(dataSourceProperties) val session = configuration.sessionForModel(Models.PERSISTENCE) @@ -48,7 +45,6 @@ class NodeAttachmentService(override var storePath: Path, dataSourceProperties: var checkAttachmentsOnLoad = true private val attachmentCount = metrics.counter("Attachments") - @Volatile override var automaticallyExtractAttachments = false init { require(storePath.isDirectory()) { "$storePath must be a directory" } @@ -183,19 +179,6 @@ class NodeAttachmentService(override var storePath: Path, dataSourceProperties: log.info("Stored new attachment $id") - if (automaticallyExtractAttachments) { - val extractTo = storePath / "$id.jar" - try { - extractTo.createDirectory() - extractZipFile(ByteArrayInputStream(bytes), extractTo) - } catch(e: FileAlreadyExistsException) { - log.trace("Did not extract attachment jar to directory because it already exists") - } catch(e: Exception) { - log.error("Failed to extract attachment jar $id, ", e) - // TODO: Delete the extractTo directory here. - } - } - return id } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/StorageServiceImpl.kt b/node/src/main/kotlin/net/corda/node/services/persistence/StorageServiceImpl.kt deleted file mode 100644 index c90cf27997..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/persistence/StorageServiceImpl.kt +++ /dev/null @@ -1,18 +0,0 @@ -package net.corda.node.services.persistence - -import net.corda.core.node.services.* -import net.corda.core.serialization.SingletonSerializeAsToken - -open class StorageServiceImpl(override val attachments: AttachmentStorage, - override val validatedTransactions: TransactionStorage, - override val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage) - : SingletonSerializeAsToken(), TxWritableStorageService { - override val attachmentsClassLoaderEnabled = false - - lateinit override var uploaders: List - - fun initUploaders(uploadersList: List) { - @Suppress("DEPRECATION") - uploaders = uploadersList - } -} diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 03c9f35a7d..1483ac4e9a 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -9,8 +9,8 @@ import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.node.services.api.SchemaService -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 -import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 +import net.corda.core.schemas.CommonSchemaV1 +import net.corda.node.services.vault.VaultSchemaV1 import net.corda.schemas.CashSchemaV1 /** diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 0716b52beb..b8b5825744 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -7,18 +7,14 @@ import co.paralleluniverse.strands.Strand import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import net.corda.core.DeclaredField.Companion.declaredField -import net.corda.core.ErrorOr import net.corda.core.abbreviate import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.random63BitValue import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine -import net.corda.core.random63BitValue 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 net.corda.core.utilities.trace +import net.corda.core.utilities.* import net.corda.node.services.api.FlowAppAuditEvent import net.corda.node.services.api.FlowPermissionAuditEvent import net.corda.node.services.api.ServiceHubInternal @@ -71,7 +67,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, @Transient override lateinit var serviceHub: ServiceHubInternal @Transient internal lateinit var database: Database @Transient internal lateinit var actionOnSuspend: (FlowIORequest) -> Unit - @Transient internal lateinit var actionOnEnd: (ErrorOr, Boolean) -> Unit + @Transient internal lateinit var actionOnEnd: (Try, Boolean) -> Unit @Transient internal var fromCheckpoint: Boolean = false @Transient private var txTrampoline: Transaction? = null @@ -125,7 +121,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, .filter { it.state is FlowSessionState.Initiating } .forEach { it.waitForConfirmation() } // This is to prevent actionOnEnd being called twice if it throws an exception - actionOnEnd(ErrorOr(result), false) + actionOnEnd(Try.Success(result), false) _resultFuture?.set(result) logic.progressTracker?.currentStep = ProgressTracker.DONE logger.debug { "Flow finished with result $result" } @@ -138,7 +134,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } private fun processException(exception: Throwable, propagated: Boolean) { - actionOnEnd(ErrorOr.of(exception), propagated) + actionOnEnd(Try.Failure(exception), propagated) _resultFuture?.setException(exception) logic.progressTracker?.endWithError(exception) } @@ -205,7 +201,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, override fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction { logger.debug { "waitForLedgerCommit($hash) ..." } suspend(WaitForLedgerCommit(hash, sessionFlow.stateMachine as FlowStateMachineImpl<*>)) - val stx = serviceHub.storageService.validatedTransactions.getTransaction(hash) + val stx = serviceHub.validatedTransactions.getTransaction(hash) if (stx != null) { logger.debug { "Transaction $hash committed to ledger" } return stx diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 595ac8dc47..27ce753cd3 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -13,12 +13,21 @@ import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.pool.KryoPool import com.google.common.collect.HashMultimap import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.MoreExecutors import io.requery.util.CloseableIterator -import net.corda.core.* +import net.corda.core.ThreadBox +import net.corda.core.bufferUntilSubscribed import net.corda.core.crypto.SecureHash -import net.corda.core.flows.* +import net.corda.core.crypto.random63BitValue +import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowInitiator +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.Party +import net.corda.core.messaging.DataFeed import net.corda.core.serialization.* +import net.corda.core.then +import net.corda.core.utilities.Try import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace @@ -31,15 +40,18 @@ import net.corda.node.services.messaging.TopicSession import net.corda.node.utilities.* import org.apache.activemq.artemis.utils.ReusableLatch import org.jetbrains.exposed.sql.Database +import org.slf4j.Logger import rx.Observable import rx.subjects.PublishSubject import java.util.* import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit.SECONDS import javax.annotation.concurrent.ThreadSafe import kotlin.collections.ArrayList /** - * A StateMachineManager is responsible for coordination and persistence of multiple [FlowStateMachine] objects. + * A StateMachineManager is responsible for coordination and persistence of multiple [FlowStateMachineImpl] objects. * Each such object represents an instantiation of a (two-party) flow that has reached a particular point. * * An implementation of this class will persist state machines to long term storage so they can survive process restarts @@ -71,11 +83,17 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, private val quasarKryoPool = KryoPool.Builder { val serializer = Fiber.getFiberSerializer(false) as KryoSerializer - DefaultKryoCustomizer.customize(serializer.kryo) - serializer.kryo.addDefaultSerializer(AutoCloseable::class.java, AutoCloseableSerialisationDetector) - serializer.kryo + val classResolver = makeNoWhitelistClassResolver().apply { setKryo(serializer.kryo) } + // TODO The ClassResolver can only be set in the Kryo constructor and Quasar doesn't provide us with a way of doing that + val field = Kryo::class.java.getDeclaredField("classResolver").apply { isAccessible = true } + serializer.kryo.apply { + field.set(this, classResolver) + DefaultKryoCustomizer.customize(this) + addDefaultSerializer(AutoCloseable::class.java, AutoCloseableSerialisationDetector) + } }.build() + // TODO Move this into the blacklist and upgrade the blacklist to allow custom messages private object AutoCloseableSerialisationDetector : Serializer() { override fun write(kryo: Kryo, output: Output, closeable: AutoCloseable) { val message = if (closeable is CloseableIterator<*>) { @@ -88,6 +106,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } throw UnsupportedOperationException(message) } + override fun read(kryo: Kryo, input: Input, type: Class) = throw IllegalStateException("Should not reach here!") } @@ -102,13 +121,11 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } } - val scheduler = FiberScheduler() - sealed class Change { abstract val logic: FlowLogic<*> - data class Add(override val logic: FlowLogic<*>): Change() - data class Removed(override val logic: FlowLogic<*>, val result: ErrorOr<*>): Change() + data class Add(override val logic: FlowLogic<*>) : Change() + data class Removed(override val logic: FlowLogic<*>, val result: Try<*>) : Change() } // A list of all the state machines being managed by this class. We expose snapshots of it via the stateMachines @@ -124,14 +141,18 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } } + private val scheduler = FiberScheduler() private val mutex = ThreadBox(InnerState()) + // This thread (only enabled in dev mode) deserialises checkpoints in the background to shake out bugs in checkpoint restore. + private val checkpointCheckerThread = if (serviceHub.configuration.devMode) Executors.newSingleThreadExecutor() else null + + @Volatile private var unrestorableCheckpoints = false // True if we're shutting down, so don't resume anything. @Volatile private var stopping = false // How many Fibers are running and not suspended. If zero and stopping is true, then we are halted. private val liveFibers = ReusableLatch() - // Monitoring support. private val metrics = serviceHub.monitoringService.metrics @@ -177,12 +198,12 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, fun start() { restoreFibersFromCheckpoints() listenToLedgerTransactions() - serviceHub.networkMapCache.mapServiceRegistered.then(executor) { resumeRestoredFibers() } + serviceHub.networkMapCache.mapServiceRegistered.then { executor.execute(this::resumeRestoredFibers) } } private fun listenToLedgerTransactions() { // Observe the stream of committed, validated transactions and resume fibers that are waiting for them. - serviceHub.storageService.validatedTransactions.updates.subscribe { stx -> + serviceHub.validatedTransactions.updates.subscribe { stx -> val hash = stx.id val fibers: Set> = mutex.locked { fibersWaitingForLedgerCommit.removeAll(hash) } if (fibers.isNotEmpty()) { @@ -220,26 +241,29 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, // Account for any expected Fibers in a test scenario. liveFibers.countDown(allowedUnsuspendedFiberCount) liveFibers.await() + checkpointCheckerThread?.let { MoreExecutors.shutdownAndAwaitTermination(it, 5, SECONDS) } + check(!unrestorableCheckpoints) { "Unrestorable checkpoints where created, please check the logs for details." } } /** * Atomic get snapshot + subscribe. This is needed so we don't miss updates between subscriptions to [changes] and * calls to [allStateMachines] */ - fun track(): Pair>, Observable> { + fun track(): DataFeed>, Change> { return mutex.locked { - Pair(stateMachines.keys.toList(), changesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) + DataFeed(stateMachines.keys.toList(), changesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) } } private fun restoreFibersFromCheckpoints() { mutex.locked { - checkpointStorage.forEach { + checkpointStorage.forEach { checkpoint -> // If a flow is added before start() then don't attempt to restore it - if (!stateMachines.containsValue(it)) { - val fiber = deserializeFiber(it) - initFiber(fiber) - stateMachines[fiber] = it + if (!stateMachines.containsValue(checkpoint)) { + deserializeFiber(checkpoint, logger)?.let { + initFiber(it) + stateMachines[it] = checkpoint + } } true } @@ -263,7 +287,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, if (waitingForResponse != null) { if (waitingForResponse is WaitForLedgerCommit) { val stx = database.transaction { - serviceHub.storageService.validatedTransactions.getTransaction(waitingForResponse.hash) + serviceHub.validatedTransactions.getTransaction(waitingForResponse.hash) } if (stx != null) { fiber.logger.info("Resuming fiber as tx ${waitingForResponse.hash} has committed") @@ -391,12 +415,17 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } } - private fun deserializeFiber(checkpoint: Checkpoint): FlowStateMachineImpl<*> { - return quasarKryoPool.run { kryo -> - // put the map of token -> tokenized into the kryo context - kryo.withSerializationContext(serializationContext) { - checkpoint.serializedFiber.deserialize(kryo) - }.apply { fromCheckpoint = true } + private fun deserializeFiber(checkpoint: Checkpoint, logger: Logger): FlowStateMachineImpl<*>? { + return try { + quasarKryoPool.run { kryo -> + // put the map of token -> tokenized into the kryo context + kryo.withSerializationContext(serializationContext) { + checkpoint.serializedFiber.deserialize(kryo) + }.apply { fromCheckpoint = true } + } + } catch (t: Throwable) { + logger.error("Encountered unrestorable checkpoint!", t) + null } } @@ -416,13 +445,13 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, processIORequest(ioRequest) decrementLiveFibers() } - fiber.actionOnEnd = { resultOrError, propagated -> + fiber.actionOnEnd = { result, propagated -> try { mutex.locked { stateMachines.remove(fiber)?.let { checkpointStorage.removeCheckpoint(it) } - notifyChangeObservers(Change.Removed(fiber.logic, resultOrError)) + notifyChangeObservers(Change.Removed(fiber.logic, result)) } - endAllFiberSessions(fiber, resultOrError.error, propagated) + endAllFiberSessions(fiber, result, propagated) } finally { fiber.commitTransaction() decrementLiveFibers() @@ -437,10 +466,10 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } } - private fun endAllFiberSessions(fiber: FlowStateMachineImpl<*>, exception: Throwable?, propagated: Boolean) { + private fun endAllFiberSessions(fiber: FlowStateMachineImpl<*>, result: Try<*>, propagated: Boolean) { openSessions.values.removeIf { session -> if (session.fiber == fiber) { - session.endSession(exception, propagated) + session.endSession((result as? Try.Failure)?.exception, propagated) true } else { false @@ -503,6 +532,14 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } checkpointStorage.addCheckpoint(newCheckpoint) checkpointingMeter.mark() + + checkpointCheckerThread?.execute { + // Immediately check that the checkpoint is valid by deserialising it. The idea is to plug any holes we have + // in our testing by failing any test where unrestorable checkpoints are created. + if (deserializeFiber(newCheckpoint, fiber.logger) == null) { + unrestorableCheckpoints = true + } + } } private fun resumeFiber(fiber: FlowStateMachineImpl<*>) { @@ -543,7 +580,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, private fun processWaitForCommitRequest(ioRequest: WaitForLedgerCommit) { // Is it already committed? val stx = database.transaction { - serviceHub.storageService.validatedTransactions.getTransaction(ioRequest.hash) + serviceHub.validatedTransactions.getTransaction(ioRequest.hash) } if (stx != null) { resumeFiber(ioRequest.fiber) diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt index 4dd60e71a9..a64cfaaa9d 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.flows.FlowLogic import net.corda.core.getOrThrow import net.corda.core.identity.Party +import net.corda.core.node.services.NotaryService import net.corda.core.node.services.TimeWindowChecker import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize @@ -15,7 +16,6 @@ import net.corda.core.utilities.loggerFor import net.corda.core.utilities.unwrap import net.corda.flows.NotaryException import net.corda.node.services.api.ServiceHubInternal -import org.jetbrains.exposed.sql.Database import kotlin.concurrent.thread /** @@ -23,40 +23,42 @@ import kotlin.concurrent.thread * * A transaction is notarised when the consensus is reached by the cluster on its uniqueness, and time-window validity. */ -class BFTNonValidatingNotaryService(config: BFTSMaRtConfig, - services: ServiceHubInternal, - timeWindowChecker: TimeWindowChecker, - replicaId: Int, - db: Database) : NotaryService { - val client = BFTSMaRt.Client(config, replicaId) // (Ab)use replicaId for clientId. - private val replicaHolder = SettableFuture.create() - - init { - // Replica startup must be in parallel with other replicas, otherwise the constructor may not return: - val configHandle = config.handle() - thread(name = "BFT SMaRt replica $replicaId init", isDaemon = true) { - configHandle.use { - replicaHolder.set(Replica(it, replicaId, db, "bft_smart_notary_committed_states", services, timeWindowChecker)) - log.info("BFT SMaRt replica $replicaId is running.") - } - } - } - - fun dispose() { - replicaHolder.getOrThrow().dispose() - client.dispose() - } - +class BFTNonValidatingNotaryService(override val services: ServiceHubInternal) : NotaryService() { companion object { val type = SimpleNotaryService.type.getSubType("bft") private val log = loggerFor() } - override val serviceFlowFactory: (Party, Int) -> FlowLogic = { otherParty, _ -> - ServiceFlow(otherParty, client) + private val client: BFTSMaRt.Client + private val replicaHolder = SettableFuture.create() + + init { + val replicaId = services.configuration.bftReplicaId ?: throw IllegalArgumentException("bftReplicaId value must be specified in the configuration") + val config = BFTSMaRtConfig(services.configuration.notaryClusterAddresses) + + client = config.use { + val configHandle = config.handle() + // Replica startup must be in parallel with other replicas, otherwise the constructor may not return: + thread(name = "BFT SMaRt replica $replicaId init", isDaemon = true) { + configHandle.use { + val timeWindowChecker = TimeWindowChecker(services.clock) + val replica = Replica(it, replicaId, "bft_smart_notary_committed_states", services, timeWindowChecker) + replicaHolder.set(replica) + log.info("BFT SMaRt replica $replicaId is running.") + } + } + + BFTSMaRt.Client(it, replicaId) + } } - private class ServiceFlow(val otherSide: Party, val client: BFTSMaRt.Client) : FlowLogic() { + fun commitTransaction(tx: Any, otherSide: Party) = client.commitTransaction(tx, otherSide) + + override fun createServiceFlow(otherParty: Party, platformVersion: Int): FlowLogic { + return ServiceFlow(otherParty, this) + } + + private class ServiceFlow(val otherSide: Party, val service: BFTNonValidatingNotaryService) : FlowLogic() { @Suspendable override fun call(): Void? { val stx = receive(otherSide).unwrap { it } @@ -66,7 +68,7 @@ class BFTNonValidatingNotaryService(config: BFTSMaRtConfig, } private fun commit(stx: FilteredTransaction): List { - val response = client.commitTransaction(stx, otherSide) + val response = service.commitTransaction(stx, otherSide) when (response) { is BFTSMaRt.ClusterResponse.Error -> throw NotaryException(response.error) is BFTSMaRt.ClusterResponse.Signatures -> { @@ -79,10 +81,9 @@ class BFTNonValidatingNotaryService(config: BFTSMaRtConfig, private class Replica(config: BFTSMaRtConfig, replicaId: Int, - db: Database, tableName: String, services: ServiceHubInternal, - timeWindowChecker: TimeWindowChecker) : BFTSMaRt.Replica(config, replicaId, db, tableName, services, timeWindowChecker) { + timeWindowChecker: TimeWindowChecker) : BFTSMaRt.Replica(config, replicaId, tableName, services, timeWindowChecker) { override fun executeCommand(command: ByteArray): ByteArray { val request = command.deserialize() @@ -107,5 +108,14 @@ class BFTNonValidatingNotaryService(config: BFTSMaRtConfig, BFTSMaRt.ReplicaResponse.Error(e.error) } } + + } + + override fun start() { + } + + override fun stop() { + replicaHolder.getOrThrow().dispose() + client.dispose() } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt index 2a13f0cab4..f910c0ef1e 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt @@ -37,7 +37,6 @@ import net.corda.node.services.transactions.BFTSMaRt.Client import net.corda.node.services.transactions.BFTSMaRt.Replica import net.corda.node.utilities.JDBCHashMap import net.corda.node.utilities.transaction -import org.jetbrains.exposed.sql.Database import java.nio.file.Path import java.util.* @@ -170,7 +169,6 @@ object BFTSMaRt { */ abstract class Replica(config: BFTSMaRtConfig, replicaId: Int, - private val db: Database, tableName: String, private val services: ServiceHubInternal, private val timeWindowChecker: TimeWindowChecker) : DefaultRecoverable() { @@ -180,9 +178,12 @@ object BFTSMaRt { // TODO: Use Requery with proper DB schema instead of JDBCHashMap. // Must be initialised before ServiceReplica is started - private val commitLog = db.transaction { JDBCHashMap(tableName) } - @Suppress("LeakingThis") - private val replica = CordaServiceReplica(replicaId, config.path, this) + private val commitLog = services.database.transaction { JDBCHashMap(tableName) } + private val replica = run { + config.waitUntilReplicaWillNotPrintStackTrace(replicaId) + @Suppress("LeakingThis") + CordaServiceReplica(replicaId, config.path, this) + } fun dispose() { replica.dispose() @@ -205,7 +206,7 @@ object BFTSMaRt { protected fun commitInputStates(states: List, txId: SecureHash, callerIdentity: Party) { log.debug { "Attempting to commit inputs for transaction: $txId" } val conflicts = mutableMapOf() - db.transaction { + services.database.transaction { states.forEach { state -> commitLog[state]?.let { conflicts[state] = it } } @@ -231,7 +232,7 @@ object BFTSMaRt { } protected fun sign(bytes: ByteArray): DigitalSignature.WithKey { - return db.transaction { services.keyManagementService.sign(bytes, services.notaryIdentityKey) } + return services.database.transaction { services.keyManagementService.sign(bytes, services.notaryIdentityKey) } } // TODO: @@ -240,7 +241,7 @@ object BFTSMaRt { override fun getSnapshot(): ByteArray { // LinkedHashMap for deterministic serialisation val m = LinkedHashMap() - db.transaction { + services.database.transaction { commitLog.forEach { m[it.key] = it.value } } return m.serialize().bytes @@ -248,7 +249,7 @@ object BFTSMaRt { override fun installSnapshot(bytes: ByteArray) { val m = bytes.deserialize>() - db.transaction { + services.database.transaction { commitLog.clear() commitLog.putAll(m) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt index 0643e4aa6a..72c0212eb2 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt @@ -1,24 +1,30 @@ package net.corda.node.services.transactions -import com.google.common.net.HostAndPort import net.corda.core.div +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.debug +import net.corda.core.utilities.loggerFor import java.io.FileWriter import java.io.PrintWriter import java.net.InetAddress +import java.net.Socket +import java.net.SocketException import java.nio.file.Files +import java.util.concurrent.TimeUnit.MILLISECONDS /** * BFT SMaRt can only be configured via files in a configHome directory. * Each instance of this class creates such a configHome, accessible via [path]. * The files are deleted on [close] typically via [use], see [PathManager] for details. */ -class BFTSMaRtConfig(private val replicaAddresses: List, debug: Boolean = false) : PathManager(Files.createTempDirectory("bft-smart-config")) { +class BFTSMaRtConfig(private val replicaAddresses: List, debug: Boolean = false) : PathManager(Files.createTempDirectory("bft-smart-config")) { companion object { + private val log = loggerFor() internal val portIsClaimedFormat = "Port %s is claimed by another replica: %s" } init { - val claimedPorts = mutableSetOf() + val claimedPorts = mutableSetOf() val n = replicaAddresses.size (0 until n).forEach { replicaId -> // Each replica claims the configured port and the next one: @@ -47,10 +53,36 @@ class BFTSMaRtConfig(private val replicaAddresses: List, debug: Boo } } - private fun replicaPorts(replicaId: Int): List { - val base = replicaAddresses[replicaId] - return (0..1).map { HostAndPort.fromParts(base.host, base.port + it) } + fun waitUntilReplicaWillNotPrintStackTrace(contextReplicaId: Int) { + // A replica will printStackTrace until all lower-numbered replicas are listening. + // But we can't probe a replica without it logging EOFException when our probe succeeds. + // So to keep logging to a minimum we only check the previous replica: + val peerId = contextReplicaId - 1 + if (peerId < 0) return + // The printStackTrace we want to avoid is in replica-replica communication code: + val address = BFTSMaRtPort.FOR_REPLICAS.ofReplica(replicaAddresses[peerId]) + log.debug { "Waiting for replica $peerId to start listening on: $address" } + while (!address.isListening()) MILLISECONDS.sleep(200) + log.debug { "Replica $peerId is ready for P2P." } } + + private fun replicaPorts(replicaId: Int): List { + val base = replicaAddresses[replicaId] + return BFTSMaRtPort.values().map { it.ofReplica(base) } + } +} + +private enum class BFTSMaRtPort(private val off: Int) { + FOR_CLIENTS(0), + FOR_REPLICAS(1); + + fun ofReplica(base: NetworkHostAndPort) = NetworkHostAndPort(base.host, base.port + off) +} + +private fun NetworkHostAndPort.isListening() = try { + Socket(host, port).use { true } // Will cause one error to be logged in the replica on success. +} catch (e: SocketException) { + false } fun maxFaultyReplicas(clusterSize: Int) = (clusterSize - 1) / 3 diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/InMemoryUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/InMemoryUniquenessProvider.kt deleted file mode 100644 index 17021d1c1b..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/transactions/InMemoryUniquenessProvider.kt +++ /dev/null @@ -1,36 +0,0 @@ -package net.corda.node.services.transactions - -import net.corda.core.ThreadBox -import net.corda.core.contracts.StateRef -import net.corda.core.identity.Party -import net.corda.core.crypto.SecureHash -import net.corda.core.node.services.UniquenessException -import net.corda.core.node.services.UniquenessProvider -import java.util.* -import javax.annotation.concurrent.ThreadSafe - -/** A dummy Uniqueness provider that stores the whole history of consumed states in memory */ -@ThreadSafe -class InMemoryUniquenessProvider : UniquenessProvider { - /** For each input state store the consuming transaction information */ - private val committedStates = ThreadBox(HashMap()) - - override fun commit(states: List, txId: SecureHash, callerIdentity: Party) { - committedStates.locked { - val conflictingStates = LinkedHashMap() - for (inputState in states) { - val consumingTx = get(inputState) - if (consumingTx != null) conflictingStates[inputState] = consumingTx - } - if (conflictingStates.isNotEmpty()) { - val conflict = UniquenessProvider.Conflict(conflictingStates) - throw UniquenessException(conflict) - } else { - states.forEachIndexed { i, stateRef -> - put(stateRef, UniquenessProvider.ConsumingTx(txId, i, callerIdentity)) - } - } - - } - } -} diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt index 470ad66a8e..354ef7799d 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt @@ -2,16 +2,13 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable import net.corda.core.identity.Party -import net.corda.core.node.services.TimeWindowChecker -import net.corda.core.node.services.UniquenessProvider +import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.transactions.FilteredTransaction import net.corda.core.utilities.unwrap import net.corda.flows.NotaryFlow import net.corda.flows.TransactionParts -class NonValidatingNotaryFlow(otherSide: Party, - timeWindowChecker: TimeWindowChecker, - uniquenessProvider: UniquenessProvider) : NotaryFlow.Service(otherSide, timeWindowChecker, uniquenessProvider) { +class NonValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSide, service) { /** * The received transaction is not checked for contract-validity, as that would require fully * resolving it into a [TransactionForVerification], for which the caller would have to reveal the whole transaction diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NotaryService.kt deleted file mode 100644 index 9abef4eb50..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NotaryService.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.corda.node.services.transactions - -import net.corda.core.flows.FlowLogic -import net.corda.core.identity.Party - -interface NotaryService { - - /** - * Factory for producing notary service flows which have 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. - */ - val serviceFlowFactory: (Party, Int) -> FlowLogic -} diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/OutOfProcessTransactionVerifierService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/OutOfProcessTransactionVerifierService.kt index e0d2999526..1539455fd9 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/OutOfProcessTransactionVerifierService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/OutOfProcessTransactionVerifierService.kt @@ -6,7 +6,7 @@ import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import net.corda.core.crypto.SecureHash import net.corda.core.node.services.TransactionVerifierService -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.loggerFor diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt index 614dfdeb36..05bcabe172 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt @@ -1,17 +1,29 @@ package net.corda.node.services.transactions -import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party +import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.TimeWindowChecker +import net.corda.flows.NotaryFlow +import net.corda.node.services.api.ServiceHubInternal /** A non-validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftNonValidatingNotaryService(val timeWindowChecker: TimeWindowChecker, - val uniquenessProvider: RaftUniquenessProvider) : NotaryService { +class RaftNonValidatingNotaryService(override val services: ServiceHubInternal) : TrustedAuthorityNotaryService() { companion object { val type = SimpleNotaryService.type.getSubType("raft") } - override val serviceFlowFactory: (Party, Int) -> FlowLogic = { otherParty, _ -> - NonValidatingNotaryFlow(otherParty, timeWindowChecker, uniquenessProvider) + override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock) + override val uniquenessProvider: RaftUniquenessProvider = RaftUniquenessProvider(services) + + override fun createServiceFlow(otherParty: Party, platformVersion: Int): NotaryFlow.Service { + return NonValidatingNotaryFlow(otherParty, this) } -} + + override fun start() { + uniquenessProvider.start() + } + + override fun stop() { + uniquenessProvider.stop() + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt index 87a09a22e3..b12c7ce477 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt @@ -1,6 +1,5 @@ package net.corda.node.services.transactions -import com.google.common.net.HostAndPort import io.atomix.catalyst.buffer.BufferInput import io.atomix.catalyst.buffer.BufferOutput import io.atomix.catalyst.serializer.Serializer @@ -23,6 +22,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.loggerFor +import net.corda.node.services.api.ServiceHubInternal import net.corda.nodeapi.config.SSLConfiguration import org.jetbrains.exposed.sql.Database import java.nio.file.Path @@ -36,27 +36,29 @@ import javax.annotation.concurrent.ThreadSafe * The uniqueness provider maintains both a Copycat cluster node (server) and a client through which it can submit * requests to the cluster. In Copycat, a client request is first sent to the server it's connected to and then redirected * to the cluster leader to be actioned. - * - * @param storagePath Directory storing the Raft log and state machine snapshots - * @param myAddress Address of the Copycat node run by this Corda node - * @param clusterAddresses List of node addresses in the existing Copycat cluster. At least one active node must be - * provided to join the cluster. If empty, a new cluster will be bootstrapped. - * @param db The database to store the state machine state in - * @param config SSL configuration */ @ThreadSafe -class RaftUniquenessProvider( - val storagePath: Path, - val myAddress: HostAndPort, - val clusterAddresses: List, - val db: Database, - val config: SSLConfiguration -) : UniquenessProvider, SingletonSerializeAsToken() { +class RaftUniquenessProvider(services: ServiceHubInternal) : UniquenessProvider, SingletonSerializeAsToken() { companion object { private val log = loggerFor() private val DB_TABLE_NAME = "notary_committed_states" } + /** Directory storing the Raft log and state machine snapshots */ + private val storagePath: Path = services.configuration.baseDirectory + /** Address of the Copycat node run by this Corda node */ + private val myAddress = services.configuration.notaryNodeAddress + ?: throw IllegalArgumentException("notaryNodeAddress must be specified in configuration") + /** + * List of node addresses in the existing Copycat cluster. At least one active node must be + * provided to join the cluster. If empty, a new cluster will be bootstrapped. + */ + private val clusterAddresses = services.configuration.notaryClusterAddresses + /** The database to store the state machine state in */ + private val db: Database = services.database + /** SSL configuration */ + private val transportConfiguration: SSLConfiguration = services.configuration + private lateinit var _clientFuture: CompletableFuture private lateinit var server: CopycatServer /** @@ -71,13 +73,21 @@ class RaftUniquenessProvider( val stateMachineFactory = { DistributedImmutableMap(db, DB_TABLE_NAME) } val address = Address(myAddress.host, myAddress.port) val storage = buildStorage(storagePath) - val transport = buildTransport(config) + val transport = buildTransport(transportConfiguration) val serializer = Serializer().apply { // Add serializers so Catalyst doesn't attempt to fall back on Java serialization for these types, which is disabled process-wide: register(DistributedImmutableMap.Commands.PutAll::class.java) { object : TypeSerializer> { - override fun write(obj: DistributedImmutableMap.Commands.PutAll<*, *>, buffer: BufferOutput>, serializer: Serializer) = writeMap(obj.entries, buffer, serializer) - override fun read(type: Class>, buffer: BufferInput>, serializer: Serializer) = DistributedImmutableMap.Commands.PutAll(readMap(buffer, serializer)) + override fun write(obj: DistributedImmutableMap.Commands.PutAll<*, *>, + buffer: BufferOutput>, + serializer: Serializer) { + writeMap(obj.entries, buffer, serializer) + } + override fun read(type: Class>, + buffer: BufferInput>, + serializer: Serializer): DistributedImmutableMap.Commands.PutAll { + return DistributedImmutableMap.Commands.PutAll(readMap(buffer, serializer)) + } } } register(LinkedHashMap::class.java) { @@ -170,4 +180,10 @@ private fun writeMap(map: Map<*, *>, buffer: BufferOutput>, } } -private fun readMap(buffer: BufferInput>, serializer: Serializer) = LinkedHashMap().apply { repeat(buffer.readInt()) { put(serializer.readObject(buffer), serializer.readObject(buffer)) } } +private fun readMap(buffer: BufferInput>, serializer: Serializer): LinkedHashMap { + return LinkedHashMap().apply { + repeat(buffer.readInt()) { + put(serializer.readObject(buffer), serializer.readObject(buffer)) + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt index ff0217d12d..deba64d1a3 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt @@ -1,17 +1,29 @@ package net.corda.node.services.transactions -import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party +import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.TimeWindowChecker +import net.corda.flows.NotaryFlow +import net.corda.node.services.api.ServiceHubInternal /** A validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftValidatingNotaryService(val timeWindowChecker: TimeWindowChecker, - val uniquenessProvider: RaftUniquenessProvider) : NotaryService { +class RaftValidatingNotaryService(override val services: ServiceHubInternal) : TrustedAuthorityNotaryService() { companion object { val type = ValidatingNotaryService.type.getSubType("raft") } - override val serviceFlowFactory: (Party, Int) -> FlowLogic = { otherParty, _ -> - ValidatingNotaryFlow(otherParty, timeWindowChecker, uniquenessProvider) + override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock) + override val uniquenessProvider: RaftUniquenessProvider = RaftUniquenessProvider(services) + + override fun createServiceFlow(otherParty: Party, platformVersion: Int): NotaryFlow.Service { + return ValidatingNotaryFlow(otherParty, this) + } + + override fun start() { + uniquenessProvider.start() + } + + override fun stop() { + uniquenessProvider.stop() } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt index 5ac707bc9e..c23d19532b 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt @@ -1,19 +1,25 @@ package net.corda.node.services.transactions -import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party +import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.ServiceType import net.corda.core.node.services.TimeWindowChecker -import net.corda.core.node.services.UniquenessProvider +import net.corda.flows.NotaryFlow +import net.corda.node.services.api.ServiceHubInternal /** A simple Notary service that does not perform transaction validation */ -class SimpleNotaryService(val timeWindowChecker: TimeWindowChecker, - val uniquenessProvider: UniquenessProvider) : NotaryService { +class SimpleNotaryService(override val services: ServiceHubInternal) : TrustedAuthorityNotaryService() { companion object { val type = ServiceType.notary.getSubType("simple") } - override val serviceFlowFactory: (Party, Int) -> FlowLogic = { otherParty, _ -> - NonValidatingNotaryFlow(otherParty, timeWindowChecker, uniquenessProvider) + override val timeWindowChecker = TimeWindowChecker(services.clock) + override val uniquenessProvider = PersistentUniquenessProvider() + + override fun createServiceFlow(otherParty: Party, platformVersion: Int): NotaryFlow.Service { + return NonValidatingNotaryFlow(otherParty, this) } -} + + override fun start() {} + override fun stop() {} +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index d30180db05..dca4e5f5ad 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -3,8 +3,7 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.TransactionVerificationException import net.corda.core.identity.Party -import net.corda.core.node.services.TimeWindowChecker -import net.corda.core.node.services.UniquenessProvider +import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.unwrap @@ -17,10 +16,7 @@ import java.security.SignatureException * has its input states "blocked" by a transaction from another party, and needs to establish whether that transaction was * indeed valid. */ -class ValidatingNotaryFlow(otherSide: Party, - timeWindowChecker: TimeWindowChecker, - uniquenessProvider: UniquenessProvider) : - NotaryFlow.Service(otherSide, timeWindowChecker, uniquenessProvider) { +class ValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSide, service) { /** * The received transaction is checked for contract-validity, which requires fully resolving it into a * [TransactionForVerification], for which the caller also has to to reveal the whole transaction diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt index 72b819e90a..c996a8979d 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt @@ -1,19 +1,25 @@ package net.corda.node.services.transactions -import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party +import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.ServiceType import net.corda.core.node.services.TimeWindowChecker -import net.corda.core.node.services.UniquenessProvider +import net.corda.flows.NotaryFlow +import net.corda.node.services.api.ServiceHubInternal /** A Notary service that validates the transaction chain of the submitted transaction before committing it */ -class ValidatingNotaryService(val timeWindowChecker: TimeWindowChecker, - val uniquenessProvider: UniquenessProvider) : NotaryService { +class ValidatingNotaryService(override val services: ServiceHubInternal) : TrustedAuthorityNotaryService() { companion object { val type = ServiceType.notary.getSubType("validating") } - override val serviceFlowFactory: (Party, Int) -> FlowLogic = { otherParty, _ -> - ValidatingNotaryFlow(otherParty, timeWindowChecker, uniquenessProvider) + override val timeWindowChecker = TimeWindowChecker(services.clock) + override val uniquenessProvider = PersistentUniquenessProvider() + + override fun createServiceFlow(otherParty: Party, platformVersion: Int): NotaryFlow.Service { + return ValidatingNotaryFlow(otherParty, this) } -} + + override fun start() {} + override fun stop() {} +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 9698ed58ad..6ad1d928f5 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -7,14 +7,14 @@ import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.vault.* +import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentStateRef -import net.corda.core.serialization.OpaqueBytes -import net.corda.core.serialization.toHexString +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.toHexString import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 -import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 +import net.corda.core.schemas.CommonSchemaV1 import org.bouncycastle.asn1.x500.X500Name import java.util.* import javax.persistence.Tuple @@ -34,6 +34,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, private val joinPredicates = mutableListOf() // incrementally build list of root entities (for later use in Sort parsing) private val rootEntities = mutableMapOf, Root<*>>() + private val aggregateExpressions = mutableListOf>() var stateTypes: Vault.StateStatus = Vault.StateStatus.UNCONSUMED @@ -41,13 +42,6 @@ class HibernateQueryCriteriaParser(val contractType: Class, log.trace { "Parsing VaultQueryCriteria: $criteria" } val predicateSet = mutableSetOf() - // state status - stateTypes = criteria.status - if (criteria.status == Vault.StateStatus.ALL) - predicateSet.add(vaultStates.get("stateStatus").`in`(setOf(Vault.StateStatus.UNCONSUMED, Vault.StateStatus.CONSUMED))) - else - predicateSet.add(criteriaBuilder.equal(vaultStates.get("stateStatus"), criteria.status)) - // contract State Types val combinedContractTypeTypes = criteria.contractStateTypes?.plus(contractType) ?: setOf(contractType) combinedContractTypeTypes.filter { it.name != ContractState::class.java.name }.let { @@ -84,7 +78,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, QueryCriteria.TimeInstantType.CONSUMED -> Column.Kotlin(VaultSchemaV1.VaultStates::consumedTime) } val expression = CriteriaExpression.ColumnPredicateExpression(timeColumn, timeCondition.predicate) - predicateSet.add(expressionToPredicate(vaultStates, expression)) + predicateSet.add(parseExpression(vaultStates, expression) as Predicate) } return predicateSet } @@ -100,6 +94,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, } is ColumnPredicate.BinaryComparison -> { column as Path?> + @Suppress("UNCHECKED_CAST") val literal = columnPredicate.rightLiteral as Comparable? when (columnPredicate.operator) { BinaryComparisonOperator.GREATER_THAN -> criteriaBuilder.greaterThan(column, literal) @@ -122,8 +117,11 @@ class HibernateQueryCriteriaParser(val contractType: Class, } } is ColumnPredicate.Between -> { + @Suppress("UNCHECKED_CAST") column as Path?> + @Suppress("UNCHECKED_CAST") val fromLiteral = columnPredicate.rightFromLiteral as Comparable? + @Suppress("UNCHECKED_CAST") val toLiteral = columnPredicate.rightToLiteral as Comparable? criteriaBuilder.between(column, fromLiteral, toLiteral) } @@ -133,32 +131,76 @@ class HibernateQueryCriteriaParser(val contractType: Class, NullOperator.NOT_NULL -> criteriaBuilder.isNotNull(column) } } + else -> throw VaultQueryException("Not expecting $columnPredicate") } } - /** - * @return : Expression -> : Predicate - */ - private fun expressionToExpression(root: Root, expression: CriteriaExpression): Expression { + private fun parseExpression(entityRoot: Root, expression: CriteriaExpression, predicateSet: MutableSet) { + if (expression is CriteriaExpression.AggregateFunctionExpression) { + parseAggregateFunction(entityRoot, expression) + } else { + predicateSet.add(parseExpression(entityRoot, expression) as Predicate) + } + } + + private fun parseExpression(root: Root, expression: CriteriaExpression): Expression { return when (expression) { is CriteriaExpression.BinaryLogical -> { - val leftPredicate = expressionToExpression(root, expression.left) - val rightPredicate = expressionToExpression(root, expression.right) + val leftPredicate = parseExpression(root, expression.left) + val rightPredicate = parseExpression(root, expression.right) when (expression.operator) { - BinaryLogicalOperator.AND -> criteriaBuilder.and(leftPredicate, rightPredicate) as Expression - BinaryLogicalOperator.OR -> criteriaBuilder.or(leftPredicate, rightPredicate) as Expression + BinaryLogicalOperator.AND -> criteriaBuilder.and(leftPredicate, rightPredicate) + BinaryLogicalOperator.OR -> criteriaBuilder.or(leftPredicate, rightPredicate) } } - is CriteriaExpression.Not -> criteriaBuilder.not(expressionToExpression(root, expression.expression)) as Expression + is CriteriaExpression.Not -> criteriaBuilder.not(parseExpression(root, expression.expression)) is CriteriaExpression.ColumnPredicateExpression -> { val column = root.get(getColumnName(expression.column)) - columnPredicateToPredicate(column, expression.predicate) as Expression + columnPredicateToPredicate(column, expression.predicate) } + else -> throw VaultQueryException("Unexpected expression: $expression") } } - private fun expressionToPredicate(root: Root, expression: CriteriaExpression): Predicate { - return expressionToExpression(root, expression) as Predicate + private fun parseAggregateFunction(root: Root, expression: CriteriaExpression.AggregateFunctionExpression): Expression? { + val column = root.get(getColumnName(expression.column)) + val columnPredicate = expression.predicate + when (columnPredicate) { + is ColumnPredicate.AggregateFunction -> { + @Suppress("UNCHECKED_CAST") + column as Path? + val aggregateExpression = + when (columnPredicate.type) { + AggregateFunctionType.SUM -> criteriaBuilder.sum(column) + AggregateFunctionType.AVG -> criteriaBuilder.avg(column) + AggregateFunctionType.COUNT -> criteriaBuilder.count(column) + AggregateFunctionType.MAX -> criteriaBuilder.max(column) + AggregateFunctionType.MIN -> criteriaBuilder.min(column) + } + aggregateExpressions.add(aggregateExpression) + // optionally order by this aggregate function + expression.orderBy?.let { + val orderCriteria = + when (expression.orderBy!!) { + Sort.Direction.ASC -> criteriaBuilder.asc(aggregateExpression) + Sort.Direction.DESC -> criteriaBuilder.desc(aggregateExpression) + } + criteriaQuery.orderBy(orderCriteria) + } + // add optional group by clauses + expression.groupByColumns?.let { columns -> + val groupByExpressions = + columns.map { column -> + val path = root.get(getColumnName(column)) + aggregateExpressions.add(path) + path + } + criteriaQuery.groupBy(groupByExpressions) + } + return aggregateExpression + } + else -> throw VaultQueryException("Not expecting $columnPredicate") + } } override fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria) : Collection { @@ -217,6 +259,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java) rootEntities.putIfAbsent(VaultSchemaV1.VaultLinearStates::class.java, vaultLinearStates) + val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultLinearStates.get("stateRef")) joinPredicates.add(joinPredicate) @@ -255,10 +298,12 @@ class HibernateQueryCriteriaParser(val contractType: Class, try { val entityRoot = criteriaQuery.from(entityClass) rootEntities.putIfAbsent(entityClass, entityRoot) + val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), entityRoot.get("stateRef")) joinPredicates.add(joinPredicate) - predicateSet.add(expressionToPredicate(entityRoot, criteria.expression)) + // resolve general criteria expressions + parseExpression(entityRoot, criteria.expression, predicateSet) } catch (e: Exception) { e.message?.let { message -> @@ -293,7 +338,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, val leftPredicates = parse(left) val rightPredicates = parse(right) - val andPredicate = criteriaBuilder.and(criteriaBuilder.and(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray())) + val andPredicate = criteriaBuilder.and(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray()) predicateSet.add(andPredicate) return predicateSet @@ -307,7 +352,11 @@ class HibernateQueryCriteriaParser(val contractType: Class, parse(sorting) } - val selections = listOf(vaultStates).plus(rootEntities.map { it.value }) + val selections = + if (aggregateExpressions.isEmpty()) + listOf(vaultStates).plus(rootEntities.map { it.value }) + else + aggregateExpressions criteriaQuery.multiselect(selections) val combinedPredicates = joinPredicates.plus(predicateSet) criteriaQuery.where(*combinedPredicates.toTypedArray()) @@ -315,17 +364,29 @@ class HibernateQueryCriteriaParser(val contractType: Class, return predicateSet } + override fun parseCriteria(criteria: CommonQueryCriteria): Collection { + log.trace { "Parsing CommonQueryCriteria: $criteria" } + val predicateSet = mutableSetOf() + + // state status + stateTypes = criteria.status + if (criteria.status != Vault.StateStatus.ALL) + predicateSet.add(criteriaBuilder.equal(vaultStates.get("stateStatus"), criteria.status)) + + return predicateSet + } + private fun parse(sorting: Sort) { log.trace { "Parsing sorting specification: $sorting" } var orderCriteria = mutableListOf() sorting.columns.map { (sortAttribute, direction) -> - val (entityStateClass, entityStateColumnName) = - when(sortAttribute) { - is SortAttribute.Standard -> parse(sortAttribute.attribute) - is SortAttribute.Custom -> Pair(sortAttribute.entityStateClass, sortAttribute.entityStateColumnName) - } + val (entityStateClass, entityStateAttributeParent, entityStateAttributeChild) = + when(sortAttribute) { + is SortAttribute.Standard -> parse(sortAttribute.attribute) + is SortAttribute.Custom -> Triple(sortAttribute.entityStateClass, sortAttribute.entityStateColumnName, null) + } val sortEntityRoot = rootEntities.getOrElse(entityStateClass) { // scenario where sorting on attributes not parsed as criteria @@ -337,10 +398,16 @@ class HibernateQueryCriteriaParser(val contractType: Class, } when (direction) { Sort.Direction.ASC -> { - orderCriteria.add(criteriaBuilder.asc(sortEntityRoot.get(entityStateColumnName))) + if (entityStateAttributeChild != null) + orderCriteria.add(criteriaBuilder.asc(sortEntityRoot.get(entityStateAttributeParent).get(entityStateAttributeChild))) + else + orderCriteria.add(criteriaBuilder.asc(sortEntityRoot.get(entityStateAttributeParent))) } Sort.Direction.DESC -> - orderCriteria.add(criteriaBuilder.desc(sortEntityRoot.get(entityStateColumnName))) + if (entityStateAttributeChild != null) + orderCriteria.add(criteriaBuilder.desc(sortEntityRoot.get(entityStateAttributeParent).get(entityStateAttributeChild))) + else + orderCriteria.add(criteriaBuilder.desc(sortEntityRoot.get(entityStateAttributeParent))) } } if (orderCriteria.isNotEmpty()) { @@ -349,20 +416,23 @@ class HibernateQueryCriteriaParser(val contractType: Class, } } - private fun parse(sortAttribute: Sort.Attribute): Pair, String> { - val entityClassAndColumnName : Pair, String> = - when(sortAttribute) { - is Sort.VaultStateAttribute -> { - Pair(VaultSchemaV1.VaultStates::class.java, sortAttribute.columnName) + private fun parse(sortAttribute: Sort.Attribute): Triple, String, String?> { + val entityClassAndColumnName : Triple, String, String?> = + when(sortAttribute) { + is Sort.CommonStateAttribute -> { + Triple(VaultSchemaV1.VaultStates::class.java, sortAttribute.attributeParent, sortAttribute.attributeChild) + } + is Sort.VaultStateAttribute -> { + Triple(VaultSchemaV1.VaultStates::class.java, sortAttribute.attributeName, null) + } + is Sort.LinearStateAttribute -> { + Triple(VaultSchemaV1.VaultLinearStates::class.java, sortAttribute.attributeName, null) + } + is Sort.FungibleStateAttribute -> { + Triple(VaultSchemaV1.VaultFungibleStates::class.java, sortAttribute.attributeName, null) + } + else -> throw VaultQueryException("Invalid sort attribute: $sortAttribute") } - is Sort.LinearStateAttribute -> { - Pair(VaultSchemaV1.VaultLinearStates::class.java, sortAttribute.columnName) - } - is Sort.FungibleStateAttribute -> { - Pair(VaultSchemaV1.VaultFungibleStates::class.java, sortAttribute.columnName) - } - else -> throw VaultQueryException("Invalid sort attribute: $sortAttribute") - } return entityClassAndColumnName } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt index a014a3b545..fe91e3f579 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt @@ -7,30 +7,28 @@ import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionState import net.corda.core.crypto.SecureHash +import net.corda.core.messaging.DataFeed import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.VaultQueryService -import net.corda.core.node.services.vault.MAX_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.node.services.vault.* +import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.storageKryo +import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.node.services.database.HibernateConfiguration -import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 -import net.corda.node.utilities.wrapWithDatabaseTransaction import org.jetbrains.exposed.sql.transactions.TransactionManager import rx.subjects.PublishSubject import java.lang.Exception +import java.util.* import javax.persistence.EntityManager import javax.persistence.Tuple class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, val updatesPublisher: PublishSubject) : SingletonSerializeAsToken(), VaultQueryService { - companion object { val log = loggerFor() } @@ -42,6 +40,15 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, override fun _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): Vault.Page { log.info("Vault Query for contract type: $contractType, criteria: $criteria, pagination: $paging, sorting: $sorting") + // calculate total results where a page specification has been defined + var totalStates = -1L + if (!paging.isDefault) { + val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } + val countCriteria = VaultCustomQueryCriteria(count, Vault.StateStatus.ALL) + val results = queryBy(contractType, criteria.and(countCriteria)) + totalStates = results.otherResults[0] as Long + } + val session = sessionFactory.withOptions(). connection(TransactionManager.current().connection). openSession() @@ -61,36 +68,45 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, // prepare query for execution val query = session.createQuery(criteriaQuery) - // pagination - if (paging.pageNumber < 0) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from 0]") - if (paging.pageSize < 0 || paging.pageSize > MAX_PAGE_SIZE) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [maximum page size is ${MAX_PAGE_SIZE}]") + // pagination checks + if (!paging.isDefault) { + // pagination + if (paging.pageNumber < DEFAULT_PAGE_NUM) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from $DEFAULT_PAGE_NUM]") + if (paging.pageSize < 1) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [must be a value between 1 and $MAX_PAGE_SIZE]") + } - // count total results available - val countQuery = criteriaBuilder.createQuery(Long::class.java) - countQuery.select(criteriaBuilder.count(countQuery.from(VaultSchemaV1.VaultStates::class.java))) - val totalStates = session.createQuery(countQuery).singleResult.toInt() - - if ((paging.pageNumber != 0) && (paging.pageSize * paging.pageNumber >= totalStates)) - throw VaultQueryException("Requested more results than available [${paging.pageSize} * ${paging.pageNumber} >= ${totalStates}]") - - query.firstResult = paging.pageNumber * paging.pageSize - query.maxResults = paging.pageSize + query.firstResult = (paging.pageNumber - 1) * paging.pageSize + query.maxResults = paging.pageSize + 1 // detection too many results // execution val results = query.resultList - val statesAndRefs: MutableList> = mutableListOf() + + // final pagination check (fail-fast on too many results when no pagination specified) + if (paging.isDefault && results.size > DEFAULT_PAGE_SIZE) + throw VaultQueryException("Please specify a `PageSpecification` as there are more results [${results.size}] than the default page size [$DEFAULT_PAGE_SIZE]") + + val statesAndRefs: MutableList> = mutableListOf() val statesMeta: MutableList = mutableListOf() + val otherResults: MutableList = mutableListOf() results.asSequence() - .forEach { it -> - val it = it[0] as VaultSchemaV1.VaultStates - val stateRef = StateRef(SecureHash.parse(it.stateRef!!.txId!!), it.stateRef!!.index!!) - val state = it.contractState.deserialize>(storageKryo()) - statesMeta.add(Vault.StateMetadata(stateRef, it.contractStateClassName, it.recordedTime, it.consumedTime, it.stateStatus, it.notaryName, it.notaryKey, it.lockId, it.lockUpdateTime)) - statesAndRefs.add(StateAndRef(state, stateRef)) + .forEachIndexed { index, result -> + if (result[0] is VaultSchemaV1.VaultStates) { + if (!paging.isDefault && index == paging.pageSize) // skip last result if paged + return@forEachIndexed + val vaultState = result[0] as VaultSchemaV1.VaultStates + val stateRef = StateRef(SecureHash.parse(vaultState.stateRef!!.txId!!), vaultState.stateRef!!.index!!) + val state = vaultState.contractState.deserialize>(storageKryo()) + statesMeta.add(Vault.StateMetadata(stateRef, vaultState.contractStateClassName, vaultState.recordedTime, vaultState.consumedTime, vaultState.stateStatus, vaultState.notaryName, vaultState.notaryKey, vaultState.lockId, vaultState.lockUpdateTime)) + statesAndRefs.add(StateAndRef(state, stateRef)) + } + else { + log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } + otherResults.addAll(result.toArray().asList()) + } } - return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, pageable = paging, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates) as Vault.Page + return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) } catch (e: Exception) { log.error(e.message) @@ -99,15 +115,14 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, } } - private val mutex = ThreadBox ({ updatesPublisher }) + private val mutex = ThreadBox({ updatesPublisher }) @Throws(VaultQueryException::class) - override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): Vault.PageAndUpdates { + override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): DataFeed, Vault.Update> { return mutex.locked { val snapshotResults = _queryBy(criteria, paging, sorting, contractType) - Vault.PageAndUpdates(snapshotResults, - updatesPublisher.bufferUntilSubscribed() - .filter { it.containsType(contractType, snapshotResults.stateTypes) } ) + val updates = updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractType, snapshotResults.stateTypes) } + DataFeed(snapshotResults, updates) } } @@ -115,7 +130,7 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, * Maintain a list of contract state interfaces to concrete types stored in the vault * for usage in generic queries of type queryBy or queryBy> */ - fun resolveUniqueContractStateTypes(session: EntityManager) : Map> { + fun resolveUniqueContractStateTypes(session: EntityManager): Map> { val criteria = criteriaBuilder.createQuery(String::class.java) val vaultStates = criteria.from(VaultSchemaV1.VaultStates::class.java) criteria.select(vaultStates.get("contractStateClassName")).distinct(true) @@ -125,6 +140,7 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, val contractInterfaceToConcreteTypes = mutableMapOf>() distinctTypes.forEach { it -> + @Suppress("UNCHECKED_CAST") val concreteType = Class.forName(it) as Class val contractInterfaces = deriveContractInterfaces(concreteType) contractInterfaces.map { @@ -135,10 +151,11 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, return contractInterfaceToConcreteTypes } - private fun deriveContractInterfaces(clazz: Class): Set> { + private fun deriveContractInterfaces(clazz: Class): Set> { val myInterfaces: MutableSet> = mutableSetOf() clazz.interfaces.forEach { if (!it.equals(ContractState::class.java)) { + @Suppress("UNCHECKED_CAST") myInterfaces.add(it as Class) myInterfaces.addAll(deriveContractInterfaces(it)) } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 0f014302c1..2ba51cbf06 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -2,6 +2,7 @@ package net.corda.node.services.vault import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.strands.Strand +import com.google.common.annotations.VisibleForTesting import io.requery.PersistenceException import io.requery.TransactionIsolation import io.requery.kotlin.`in` @@ -19,17 +20,27 @@ import net.corda.core.crypto.containsAny import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.messaging.DataFeed import net.corda.core.node.ServiceHub -import net.corda.core.node.services.* -import net.corda.core.serialization.* +import net.corda.core.node.services.StatesNotAvailableException +import net.corda.core.node.services.Vault +import net.corda.core.node.services.VaultService +import net.corda.core.node.services.unconsumedStates +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.core.serialization.storageKryo import net.corda.core.tee import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.toHexString import net.corda.core.utilities.trace import net.corda.node.services.database.RequeryConfiguration import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.vault.schemas.requery.* +import net.corda.node.services.vault.schemas.requery.VaultSchema import net.corda.node.utilities.bufferUntilDatabaseCommit import net.corda.node.utilities.wrapWithDatabaseTransaction import rx.Observable @@ -170,9 +181,9 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P override val updatesPublisher: PublishSubject get() = mutex.locked { _updatesPublisher } - override fun track(): Pair, Observable> { + override fun track(): DataFeed, Vault.Update> { return mutex.locked { - Pair(Vault(unconsumedStates()), _updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) + DataFeed(Vault(unconsumedStates()), _updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) } } @@ -458,7 +469,8 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P private fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty) = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) - private fun makeUpdate(tx: WireTransaction, ourKeys: Set): Vault.Update { + @VisibleForTesting + internal fun makeUpdate(tx: WireTransaction, ourKeys: Set): Vault.Update { val ourNewStates = tx.outputs. filter { isRelevant(it.data, ourKeys) }. map { tx.outRef(it.data) } @@ -505,7 +517,8 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P authorisedUpgrade.remove(stateAndRef.ref) } - private fun isRelevant(state: ContractState, ourKeys: Set) = when (state) { + @VisibleForTesting + internal fun isRelevant(state: ContractState, ourKeys: Set) = when (state) { is OwnableState -> state.owner.owningKey.containsAny(ourKeys) is LinearState -> state.isRelevant(ourKeys) else -> ourKeys.intersect(state.participants.map { it.owningKey }).isNotEmpty() diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index 24cd7baf0f..5ef516971a 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -1,13 +1,12 @@ -package net.corda.node.services.vault.schemas.jpa +package net.corda.node.services.vault import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.Vault +import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState -import net.corda.core.serialization.OpaqueBytes -import java.security.PublicKey +import net.corda.core.utilities.OpaqueBytes import java.time.Instant import java.util.* import javax.persistence.* diff --git a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt index 0f11a6a547..f249caa619 100644 --- a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt +++ b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt @@ -1,20 +1,22 @@ package net.corda.node.shell import com.google.common.util.concurrent.SettableFuture -import net.corda.core.ErrorOr import net.corda.core.crypto.commonName import net.corda.core.flows.FlowInitiator import net.corda.core.flows.StateMachineRunId import net.corda.core.messaging.StateMachineUpdate +import net.corda.core.messaging.StateMachineUpdate.Added +import net.corda.core.messaging.StateMachineUpdate.Removed import net.corda.core.then import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.Try import org.crsh.text.Color import org.crsh.text.Decoration import org.crsh.text.RenderPrintWriter import org.crsh.text.ui.LabelElement -import org.crsh.text.ui.TableElement import org.crsh.text.ui.Overflow import org.crsh.text.ui.RowElement +import org.crsh.text.ui.TableElement import rx.Subscriber class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Subscriber() { @@ -25,7 +27,7 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub init { // The future is public and can be completed by something else to indicate we don't wish to follow // anymore (e.g. the user pressing Ctrl-C). - future then { unsubscribe() } + future.then { unsubscribe() } } @Synchronized @@ -51,10 +53,10 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub future.setException(e) } - private fun stateColor(smmUpdate: StateMachineUpdate): Color { - return when(smmUpdate){ - is StateMachineUpdate.Added -> Color.blue - is StateMachineUpdate.Removed -> smmUpdate.result.match({ Color.green } , { Color.red }) + private fun stateColor(update: StateMachineUpdate): Color { + return when (update) { + is Added -> Color.blue + is Removed -> if (update.result.isSuccess) Color.green else Color.red } } @@ -68,7 +70,7 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub // TODO Add progress tracker? private fun createStateMachinesRow(smmUpdate: StateMachineUpdate) { when (smmUpdate) { - is StateMachineUpdate.Added -> { + is Added -> { table.add(RowElement().add( LabelElement(formatFlowId(smmUpdate.id)), LabelElement(formatFlowName(smmUpdate.stateMachineInfo.flowLogicClassName)), @@ -77,7 +79,7 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub ).style(stateColor(smmUpdate).fg())) indexMap[smmUpdate.id] = table.rows.size - 1 } - is StateMachineUpdate.Removed -> { + is Removed -> { val idx = indexMap[smmUpdate.id] if (idx != null) { val oldRow = table.rows[idx] @@ -114,7 +116,7 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub } } - private fun formatFlowResult(flowResult: ErrorOr<*>): String { + private fun formatFlowResult(flowResult: Try<*>): String { fun successFormat(value: Any?): String { return when(value) { is SignedTransaction -> "Tx ID: " + value.id.toString() @@ -123,6 +125,9 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub else -> value.toString() } } - return flowResult.match({ successFormat(it) }, { it.message ?: it.toString() }) + return when (flowResult) { + is Try.Success -> successFormat(flowResult.value) + is Try.Failure -> flowResult.exception.message ?: flowResult.exception.toString() + } } } diff --git a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt index 3290740576..553890b946 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt +++ b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt @@ -394,7 +394,7 @@ object InteractiveShell { init { // The future is public and can be completed by something else to indicate we don't wish to follow // anymore (e.g. the user pressing Ctrl-C). - future then { unsubscribe() } + future.then { unsubscribe() } } @Synchronized diff --git a/node/src/main/kotlin/net/corda/node/utilities/AddOrRemove.kt b/node/src/main/kotlin/net/corda/node/utilities/AddOrRemove.kt index 0cf5538a95..155e7853d2 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AddOrRemove.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AddOrRemove.kt @@ -1,6 +1,5 @@ package net.corda.node.utilities -import net.corda.core.ErrorOr import net.corda.core.serialization.CordaSerializable /** diff --git a/node/src/main/kotlin/net/corda/node/utilities/ConfigUtils.kt b/node/src/main/kotlin/net/corda/node/utilities/ConfigUtils.kt deleted file mode 100644 index 5048cb8981..0000000000 --- a/node/src/main/kotlin/net/corda/node/utilities/ConfigUtils.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.corda.node.utilities - -import com.google.common.net.HostAndPort -import com.typesafe.config.Config -import java.nio.file.Path -import java.nio.file.Paths - -fun Config.getHostAndPort(name: String): HostAndPort = HostAndPort.fromString(getString(name)) -fun Config.getPath(name: String): Path = Paths.get(getString(name)) \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt b/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt index 4ac4d90ada..2c50bca43f 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt @@ -6,7 +6,6 @@ import com.zaxxer.hikari.HikariDataSource import net.corda.core.crypto.SecureHash import net.corda.core.crypto.parsePublicKeyBase58 import net.corda.core.crypto.toBase58String -import net.corda.core.identity.PartyAndCertificate import net.corda.node.utilities.StrandLocalTransactionManager.Boundary import org.bouncycastle.cert.X509CertificateHolder import org.h2.jdbc.JdbcBlob @@ -23,7 +22,6 @@ import java.io.Closeable import java.security.PublicKey import java.security.cert.CertPath import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate import java.sql.Connection import java.time.Instant import java.time.LocalDate @@ -268,7 +266,7 @@ private class NoOpSubscriber(t: Subscriber) : Subscriber(t) { * that might be in place. */ fun rx.Observable.wrapWithDatabaseTransaction(db: Database? = null): rx.Observable { - val wrappingSubscriber = DatabaseTransactionWrappingSubscriber(db) + var wrappingSubscriber = DatabaseTransactionWrappingSubscriber(db) // Use lift to add subscribers to a special subscriber that wraps a database transaction around observations. // Each subscriber will be passed to this lambda when they subscribe, at which point we add them to wrapping subscriber. return this.lift { toBeWrappedInDbTx: Subscriber -> @@ -277,7 +275,13 @@ fun rx.Observable.wrapWithDatabaseTransaction(db: Database? = null) // If we are the first subscriber, return the shared subscriber, otherwise return a subscriber that does nothing. if (wrappingSubscriber.delegates.size == 1) wrappingSubscriber else NoOpSubscriber(toBeWrappedInDbTx) // Clean up the shared list of subscribers when they unsubscribe. - }.doOnUnsubscribe { wrappingSubscriber.cleanUp() } + }.doOnUnsubscribe { + wrappingSubscriber.cleanUp() + // If cleanup removed the last subscriber reset the system, as future subscribers might need the stream again + if (wrappingSubscriber.delegates.isEmpty()) { + wrappingSubscriber = DatabaseTransactionWrappingSubscriber(db) + } + } } // Composite columns for use with below Exposed helpers. diff --git a/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt b/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt index 6629f7ca26..2f2db9d09b 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt @@ -1,7 +1,6 @@ package net.corda.node.utilities -import net.corda.core.crypto.CompositeKey -import net.corda.core.crypto.X509Utilities +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.Party import net.corda.core.serialization.serialize diff --git a/node/src/smoke-test/kotlin/net/corda/node/CordappScanningNodeProcessTest.kt b/node/src/smoke-test/kotlin/net/corda/node/CordappScanningNodeProcessTest.kt index 1aaeb038ef..87b8faad10 100644 --- a/node/src/smoke-test/kotlin/net/corda/node/CordappScanningNodeProcessTest.kt +++ b/node/src/smoke-test/kotlin/net/corda/node/CordappScanningNodeProcessTest.kt @@ -3,11 +3,11 @@ package net.corda.node import net.corda.core.copyToDirectory import net.corda.core.createDirectories import net.corda.core.div -import net.corda.core.utilities.ALICE import net.corda.nodeapi.User import net.corda.smoketesting.NodeConfig import net.corda.smoketesting.NodeProcess import org.assertj.core.api.Assertions.assertThat +import org.bouncycastle.asn1.x500.X500Name import org.junit.Test import java.nio.file.Paths import java.util.concurrent.atomic.AtomicInteger @@ -21,7 +21,7 @@ class CordappScanningNodeProcessTest { private val factory = NodeProcess.Factory() private val aliceConfig = NodeConfig( - party = ALICE, + legalName = X500Name("CN=Alice Corp,O=Alice Corp,L=Madrid,C=ES"), p2pPort = port.andIncrement, rpcPort = port.andIncrement, webPort = port.andIncrement, diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 542c98a5db..f5b44a3487 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -7,16 +7,20 @@ import net.corda.contracts.asset.*; import net.corda.core.contracts.*; import net.corda.core.crypto.*; import net.corda.core.identity.*; +import net.corda.core.messaging.*; import net.corda.core.node.services.*; import net.corda.core.node.services.vault.*; import net.corda.core.node.services.vault.QueryCriteria.*; import net.corda.core.schemas.*; -import net.corda.core.serialization.*; import net.corda.core.transactions.*; +import net.corda.core.utilities.*; import net.corda.node.services.database.*; import net.corda.node.services.schema.*; import net.corda.schemas.*; +import net.corda.testing.*; +import net.corda.testing.contracts.*; import net.corda.testing.node.*; +import net.corda.testing.schemas.DummyLinearStateSchemaV1; import org.jetbrains.annotations.*; import org.jetbrains.exposed.sql.*; import org.junit.*; @@ -28,20 +32,19 @@ import java.util.*; import java.util.stream.*; import static net.corda.contracts.asset.CashKt.*; -import static net.corda.contracts.testing.VaultFiller.*; -import static net.corda.core.node.services.vault.QueryCriteriaKt.*; -import static net.corda.core.node.services.vault.QueryCriteriaUtilsKt.*; -import static net.corda.core.utilities.TestConstants.*; +import static net.corda.core.contracts.ContractsDSL.*; +import static net.corda.core.node.services.vault.QueryCriteriaUtils.*; import static net.corda.node.utilities.DatabaseSupportKt.*; import static net.corda.node.utilities.DatabaseSupportKt.transaction; import static net.corda.testing.CoreTestUtils.*; import static net.corda.testing.node.MockServicesKt.*; +import static net.corda.core.utilities.ByteArrays.toHexString; import static org.assertj.core.api.Assertions.*; public class VaultQueryJavaTests { private MockServices services; - VaultService vaultSvc; + private VaultService vaultSvc; private VaultQueryService vaultQuerySvc; private Closeable dataSource; private Database database; @@ -53,7 +56,7 @@ public class VaultQueryJavaTests { dataSource = dataSourceAndDatabase.getFirst(); database = dataSourceAndDatabase.getSecond(); - Set customSchemas = new HashSet<>(Arrays.asList(DummyLinearStateSchemaV1.INSTANCE)); + Set customSchemas = new HashSet<>(Collections.singletonList(DummyLinearStateSchemaV1.INSTANCE)); HibernateConfiguration hibernateConfig = new HibernateConfiguration(new NodeSchemaService(customSchemas)); transaction(database, statement -> { services = new MockServices(getMEGA_CORP_KEY()) { @@ -63,6 +66,7 @@ public class VaultQueryJavaTests { return makeVaultService(dataSourceProps, hibernateConfig); } + @NotNull @Override public VaultQueryService getVaultQueryService() { return new HibernateVaultQueryImpl(hibernateConfig, getVaultService().getUpdatesPublisher()); @@ -71,10 +75,10 @@ public class VaultQueryJavaTests { @Override public void recordTransactions(@NotNull Iterable txs) { for (SignedTransaction stx : txs) { - getStorageService().getValidatedTransactions().addTransaction(stx); + getValidatedTransactions().addTransaction(stx); } - Stream wtxn = StreamSupport.stream(txs.spliterator(), false).map(txn -> txn.getTx()); + Stream wtxn = StreamSupport.stream(txs.spliterator(), false).map(SignedTransaction::getTx); getVaultService().notifyAll(wtxn.collect(Collectors.toList())); } }; @@ -102,7 +106,7 @@ public class VaultQueryJavaTests { public void unconsumedLinearStates() throws VaultQueryException { transaction(database, tx -> { - fillWithSomeTestLinearStates(services, 3); + VaultFiller.fillWithSomeTestLinearStates(services, 3); // DOCSTART VaultJavaQueryExample0 Vault.Page results = vaultQuerySvc.queryBy(LinearState.class); @@ -114,15 +118,40 @@ public class VaultQueryJavaTests { }); } + @Test + public void unconsumedStatesForStateRefsSortedByTxnId() { + transaction(database, tx -> { + + VaultFiller.fillWithSomeTestLinearStates(services, 8); + Vault issuedStates = VaultFiller.fillWithSomeTestLinearStates(services, 2); + + Stream stateRefsStream = StreamSupport.stream(issuedStates.getStates().spliterator(), false).map(StateAndRef::getRef); + List stateRefs = stateRefsStream.collect(Collectors.toList()); + + SortAttribute.Standard sortAttribute = new SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID); + Sort sorting = new Sort(Collections.singletonList(new Sort.SortColumn(sortAttribute, Sort.Direction.ASC))); + VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, null, stateRefs); + Vault.Page results = vaultQuerySvc.queryBy(DummyLinearContract.State.class, criteria, sorting); + + assertThat(results.getStates()).hasSize(2); + + stateRefs.sort(Comparator.comparing(stateRef -> toHexString(stateRef.getTxhash().getBytes()))); + assertThat(results.getStates().get(0).getRef()).isEqualTo(stateRefs.get(0)); + assertThat(results.getStates().get(1).getRef()).isEqualTo(stateRefs.get(1)); + + return tx; + }); + } + @Test public void consumedCashStates() { transaction(database, tx -> { Amount amount = new Amount<>(100, Currency.getInstance("USD")); - fillWithSomeTestCash(services, + VaultFiller.fillWithSomeTestCash(services, new Amount<>(100, Currency.getInstance("USD")), - getDUMMY_NOTARY(), + TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(), @@ -131,7 +160,7 @@ public class VaultQueryJavaTests { getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY() ); - consumeCash(services, amount); + VaultFiller.consumeCash(services, amount); // DOCSTART VaultJavaQueryExample1 VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.CONSUMED); @@ -148,16 +177,16 @@ public class VaultQueryJavaTests { public void consumedDealStatesPagedSorted() throws VaultQueryException { transaction(database, tx -> { - Vault states = fillWithSomeTestLinearStates(services, 10, null); + Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); StateAndRef linearState = states.getStates().iterator().next(); UniqueIdentifier uid = linearState.component1().getData().getLinearId(); List dealIds = Arrays.asList("123", "456", "789"); - Vault dealStates = fillWithSomeTestDeals(services, dealIds); + Vault dealStates = VaultFiller.fillWithSomeTestDeals(services, dealIds); // consume states - consumeDeals(services, (List>) dealStates.getStates()); - consumeLinearStates(services, Arrays.asList(linearState)); + VaultFiller.consumeDeals(services, (List>) dealStates.getStates()); + VaultFiller.consumeLinearStates(services, Collections.singletonList(linearState)); // DOCSTART VaultJavaQueryExample2 Vault.StateStatus status = Vault.StateStatus.CONSUMED; @@ -166,14 +195,14 @@ public class VaultQueryJavaTests { QueryCriteria vaultCriteria = new VaultQueryCriteria(status, contractStateTypes); - List linearIds = Arrays.asList(uid); + List linearIds = Collections.singletonList(uid); QueryCriteria linearCriteriaAll = new LinearStateQueryCriteria(null, linearIds); QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(null, null, dealIds); - QueryCriteria compositeCriteria1 = or(dealCriteriaAll, linearCriteriaAll); - QueryCriteria compositeCriteria2 = and(vaultCriteria, compositeCriteria1); + QueryCriteria compositeCriteria1 = dealCriteriaAll.or(linearCriteriaAll); + QueryCriteria compositeCriteria2 = vaultCriteria.and(compositeCriteria1); - PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE()); + PageSpecification pageSpec = new PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); Vault.Page results = vaultQuerySvc.queryBy(LinearState.class, compositeCriteria2, pageSpec, sorting); @@ -186,6 +215,7 @@ public class VaultQueryJavaTests { } @Test + @SuppressWarnings("unchecked") public void customQueryForCashStatesWithAmountOfCurrencyGreaterOrEqualThanQuantity() { transaction(database, tx -> { @@ -194,10 +224,10 @@ public class VaultQueryJavaTests { Amount dollars10 = new Amount<>(10, Currency.getInstance("USD")); Amount dollars1 = new Amount<>(1, Currency.getInstance("USD")); - fillWithSomeTestCash(services, pounds, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); - fillWithSomeTestCash(services, dollars100, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); - fillWithSomeTestCash(services, dollars10, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); - fillWithSomeTestCash(services, dollars1, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, pounds, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars100, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars10, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars1, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); try { // DOCSTART VaultJavaQueryExample3 @@ -206,14 +236,14 @@ public class VaultQueryJavaTests { Field attributeCurrency = CashSchemaV1.PersistentCashState.class.getDeclaredField("currency"); Field attributeQuantity = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies"); - CriteriaExpression currencyIndex = Builder.INSTANCE.equal(attributeCurrency, "USD"); - CriteriaExpression quantityIndex = Builder.INSTANCE.greaterThanOrEqual(attributeQuantity, 10L); + CriteriaExpression currencyIndex = Builder.equal(attributeCurrency, "USD"); + CriteriaExpression quantityIndex = Builder.greaterThanOrEqual(attributeQuantity, 10L); QueryCriteria customCriteria2 = new VaultCustomQueryCriteria(quantityIndex); QueryCriteria customCriteria1 = new VaultCustomQueryCriteria(currencyIndex); - QueryCriteria criteria = QueryCriteriaKt.and(QueryCriteriaKt.and(generalCriteria, customCriteria1), customCriteria2); + QueryCriteria criteria = generalCriteria.and(customCriteria1).and(customCriteria2); Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); // DOCEND VaultJavaQueryExample3 @@ -232,9 +262,9 @@ public class VaultQueryJavaTests { @Test public void trackCashStates() { transaction(database, tx -> { - fillWithSomeTestCash(services, + VaultFiller.fillWithSomeTestCash(services, new Amount<>(100, Currency.getInstance("USD")), - getDUMMY_NOTARY(), + TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(), @@ -248,10 +278,10 @@ public class VaultQueryJavaTests { Set> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class)); VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, contractStateTypes); - Vault.PageAndUpdates results = vaultQuerySvc.trackBy(ContractState.class, criteria); + DataFeed, Vault.Update> results = vaultQuerySvc.trackBy(ContractState.class, criteria); - Vault.Page snapshot = results.getCurrent(); - Observable updates = results.getFuture(); + Vault.Page snapshot = results.getSnapshot(); + Observable updates = results.getUpdates(); // DOCEND VaultJavaQueryExample4 assertThat(snapshot.getStates()).hasSize(3); @@ -264,38 +294,34 @@ public class VaultQueryJavaTests { public void trackDealStatesPagedSorted() { transaction(database, tx -> { - Vault states = fillWithSomeTestLinearStates(services, 10, null); + Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); UniqueIdentifier uid = states.getStates().iterator().next().component1().getData().getLinearId(); List dealIds = Arrays.asList("123", "456", "789"); - fillWithSomeTestDeals(services, dealIds); + VaultFiller.fillWithSomeTestDeals(services, dealIds); // DOCSTART VaultJavaQueryExample5 @SuppressWarnings("unchecked") Set> contractStateTypes = new HashSet(Arrays.asList(DealState.class, LinearState.class)); QueryCriteria vaultCriteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, contractStateTypes); - List linearIds = Arrays.asList(uid); - List dealParty = Arrays.asList(getMEGA_CORP()); + List linearIds = Collections.singletonList(uid); + List dealParty = Collections.singletonList(getMEGA_CORP()); QueryCriteria dealCriteria = new LinearStateQueryCriteria(dealParty, null, dealIds); - QueryCriteria linearCriteria = new LinearStateQueryCriteria(dealParty, linearIds, null); + QueryCriteria dealOrLinearIdCriteria = dealCriteria.or(linearCriteria); + QueryCriteria compositeCriteria = dealOrLinearIdCriteria.and(vaultCriteria); - - QueryCriteria dealOrLinearIdCriteria = or(dealCriteria, linearCriteria); - - QueryCriteria compositeCriteria = and(dealOrLinearIdCriteria, vaultCriteria); - - PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE()); + PageSpecification pageSpec = new PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); - Vault.PageAndUpdates results = vaultQuerySvc.trackBy(ContractState.class, compositeCriteria, pageSpec, sorting); + DataFeed, Vault.Update> results = vaultQuerySvc.trackBy(ContractState.class, compositeCriteria, pageSpec, sorting); - Vault.Page snapshot = results.getCurrent(); - Observable updates = results.getFuture(); + Vault.Page snapshot = results.getSnapshot(); + Observable updates = results.getUpdates(); // DOCEND VaultJavaQueryExample5 - assertThat(snapshot.getStates()).hasSize(4); + assertThat(snapshot.getStates()).hasSize(13); return tx; }); @@ -308,10 +334,10 @@ public class VaultQueryJavaTests { @Test public void consumedStatesDeprecated() { transaction(database, tx -> { - Amount amount = new Amount<>(100, Currency.getInstance("USD")); - fillWithSomeTestCash(services, - new Amount<>(100, Currency.getInstance("USD")), - getDUMMY_NOTARY(), + Amount amount = new Amount<>(100, USD); + VaultFiller.fillWithSomeTestCash(services, + new Amount<>(100, USD), + TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(), @@ -320,7 +346,7 @@ public class VaultQueryJavaTests { getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY() ); - consumeCash(services, amount); + VaultFiller.consumeCash(services, amount); // DOCSTART VaultDeprecatedJavaQueryExample1 @SuppressWarnings("unchecked") @@ -341,10 +367,10 @@ public class VaultQueryJavaTests { public void consumedStatesForLinearIdDeprecated() { transaction(database, tx -> { - Vault linearStates = fillWithSomeTestLinearStates(services, 4,null); - UniqueIdentifier trackUid = linearStates.getStates().iterator().next().component1().getData().getLinearId(); + Vault linearStates = VaultFiller.fillWithSomeTestLinearStates(services, 4,null); + linearStates.getStates().iterator().next().component1().getData().getLinearId(); - consumeLinearStates(services, (List>) linearStates.getStates()); + VaultFiller.consumeLinearStates(services, (List>) linearStates.getStates()); // DOCSTART VaultDeprecatedJavaQueryExample0 @SuppressWarnings("unchecked") @@ -360,4 +386,172 @@ public class VaultQueryJavaTests { return tx; }); } + + /** + * Aggregation Functions + */ + + @Test + @SuppressWarnings("unchecked") + public void aggregateFunctionsWithoutGroupClause() { + transaction(database, tx -> { + + Amount dollars100 = new Amount<>(100, Currency.getInstance("USD")); + Amount dollars200 = new Amount<>(200, Currency.getInstance("USD")); + Amount dollars300 = new Amount<>(300, Currency.getInstance("USD")); + Amount pounds = new Amount<>(400, Currency.getInstance("GBP")); + Amount swissfrancs = new Amount<>(500, Currency.getInstance("CHF")); + + VaultFiller.fillWithSomeTestCash(services, dollars100, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars200, TestConstants.getDUMMY_NOTARY(), 2, 2, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars300, TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, pounds, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, swissfrancs, TestConstants.getDUMMY_NOTARY(), 5, 5, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + + try { + // DOCSTART VaultJavaQueryExample21 + Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies"); + + QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies)); + QueryCriteria countCriteria = new VaultCustomQueryCriteria(Builder.count(pennies)); + QueryCriteria maxCriteria = new VaultCustomQueryCriteria(Builder.max(pennies)); + QueryCriteria minCriteria = new VaultCustomQueryCriteria(Builder.min(pennies)); + QueryCriteria avgCriteria = new VaultCustomQueryCriteria(Builder.avg(pennies)); + + QueryCriteria criteria = sumCriteria.and(countCriteria).and(maxCriteria).and(minCriteria).and(avgCriteria); + Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); + // DOCEND VaultJavaQueryExample21 + + assertThat(results.getOtherResults()).hasSize(5); + assertThat(results.getOtherResults().get(0)).isEqualTo(1500L); + assertThat(results.getOtherResults().get(1)).isEqualTo(15L); + assertThat(results.getOtherResults().get(2)).isEqualTo(113L); + assertThat(results.getOtherResults().get(3)).isEqualTo(87L); + assertThat(results.getOtherResults().get(4)).isEqualTo(100.0); + + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + return tx; + }); + } + + @Test + @SuppressWarnings("unchecked") + public void aggregateFunctionsWithSingleGroupClause() { + transaction(database, tx -> { + + Amount dollars100 = new Amount<>(100, Currency.getInstance("USD")); + Amount dollars200 = new Amount<>(200, Currency.getInstance("USD")); + Amount dollars300 = new Amount<>(300, Currency.getInstance("USD")); + Amount pounds = new Amount<>(400, Currency.getInstance("GBP")); + Amount swissfrancs = new Amount<>(500, Currency.getInstance("CHF")); + + VaultFiller.fillWithSomeTestCash(services, dollars100, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars200, TestConstants.getDUMMY_NOTARY(), 2, 2, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars300, TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, pounds, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, swissfrancs, TestConstants.getDUMMY_NOTARY(), 5, 5, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + + try { + // DOCSTART VaultJavaQueryExample22 + Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies"); + Field currency = CashSchemaV1.PersistentCashState.class.getDeclaredField("currency"); + + QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies, Collections.singletonList(currency))); + QueryCriteria countCriteria = new VaultCustomQueryCriteria(Builder.count(pennies)); + QueryCriteria maxCriteria = new VaultCustomQueryCriteria(Builder.max(pennies, Collections.singletonList(currency))); + QueryCriteria minCriteria = new VaultCustomQueryCriteria(Builder.min(pennies, Collections.singletonList(currency))); + QueryCriteria avgCriteria = new VaultCustomQueryCriteria(Builder.avg(pennies, Collections.singletonList(currency))); + + QueryCriteria criteria = sumCriteria.and(countCriteria).and(maxCriteria).and(minCriteria).and(avgCriteria); + Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); + // DOCEND VaultJavaQueryExample22 + + assertThat(results.getOtherResults()).hasSize(27); + /** CHF */ + assertThat(results.getOtherResults().get(0)).isEqualTo(500L); + assertThat(results.getOtherResults().get(1)).isEqualTo("CHF"); + assertThat(results.getOtherResults().get(2)).isEqualTo(5L); + assertThat(results.getOtherResults().get(3)).isEqualTo(102L); + assertThat(results.getOtherResults().get(4)).isEqualTo("CHF"); + assertThat(results.getOtherResults().get(5)).isEqualTo(94L); + assertThat(results.getOtherResults().get(6)).isEqualTo("CHF"); + assertThat(results.getOtherResults().get(7)).isEqualTo(100.00); + assertThat(results.getOtherResults().get(8)).isEqualTo("CHF"); + /** GBP */ + assertThat(results.getOtherResults().get(9)).isEqualTo(400L); + assertThat(results.getOtherResults().get(10)).isEqualTo("GBP"); + assertThat(results.getOtherResults().get(11)).isEqualTo(4L); + assertThat(results.getOtherResults().get(12)).isEqualTo(103L); + assertThat(results.getOtherResults().get(13)).isEqualTo("GBP"); + assertThat(results.getOtherResults().get(14)).isEqualTo(93L); + assertThat(results.getOtherResults().get(15)).isEqualTo("GBP"); + assertThat(results.getOtherResults().get(16)).isEqualTo(100.0); + assertThat(results.getOtherResults().get(17)).isEqualTo("GBP"); + /** USD */ + assertThat(results.getOtherResults().get(18)).isEqualTo(600L); + assertThat(results.getOtherResults().get(19)).isEqualTo("USD"); + assertThat(results.getOtherResults().get(20)).isEqualTo(6L); + assertThat(results.getOtherResults().get(21)).isEqualTo(113L); + assertThat(results.getOtherResults().get(22)).isEqualTo("USD"); + assertThat(results.getOtherResults().get(23)).isEqualTo(87L); + assertThat(results.getOtherResults().get(24)).isEqualTo("USD"); + assertThat(results.getOtherResults().get(25)).isEqualTo(100.0); + assertThat(results.getOtherResults().get(26)).isEqualTo("USD"); + + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + return tx; + }); + } + + @Test + @SuppressWarnings("unchecked") + public void aggregateFunctionsSumByIssuerAndCurrencyAndSortByAggregateSum() { + transaction(database, tx -> { + + Amount dollars100 = new Amount<>(100, Currency.getInstance("USD")); + Amount dollars200 = new Amount<>(200, Currency.getInstance("USD")); + Amount pounds300 = new Amount<>(300, Currency.getInstance("GBP")); + Amount pounds400 = new Amount<>(400, Currency.getInstance("GBP")); + + VaultFiller.fillWithSomeTestCash(services, dollars100, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, dollars200, TestConstants.getDUMMY_NOTARY(), 2, 2, new Random(0L), new OpaqueBytes("1".getBytes()), null, getBOC().ref(new OpaqueBytes("1".getBytes())), getBOC_KEY()); + VaultFiller.fillWithSomeTestCash(services, pounds300, TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY()); + VaultFiller.fillWithSomeTestCash(services, pounds400, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getBOC().ref(new OpaqueBytes("1".getBytes())), getBOC_KEY()); + + try { + // DOCSTART VaultJavaQueryExample23 + Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies"); + Field currency = CashSchemaV1.PersistentCashState.class.getDeclaredField("currency"); + Field issuerParty = CashSchemaV1.PersistentCashState.class.getDeclaredField("issuerParty"); + + QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies, Arrays.asList(issuerParty, currency), Sort.Direction.DESC)); + + Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, sumCriteria); + // DOCEND VaultJavaQueryExample23 + + assertThat(results.getOtherResults()).hasSize(12); + + assertThat(results.getOtherResults().get(0)).isEqualTo(400L); + assertThat(results.getOtherResults().get(1)).isEqualTo(EncodingUtils.toBase58String(getBOC_PUBKEY())); + assertThat(results.getOtherResults().get(2)).isEqualTo("GBP"); + assertThat(results.getOtherResults().get(3)).isEqualTo(300L); + assertThat(results.getOtherResults().get(4)).isEqualTo(EncodingUtils.toBase58String(getDUMMY_CASH_ISSUER().getParty().getOwningKey())); + assertThat(results.getOtherResults().get(5)).isEqualTo("GBP"); + assertThat(results.getOtherResults().get(6)).isEqualTo(200L); + assertThat(results.getOtherResults().get(7)).isEqualTo(EncodingUtils.toBase58String(getBOC_PUBKEY())); + assertThat(results.getOtherResults().get(8)).isEqualTo("USD"); + assertThat(results.getOtherResults().get(9)).isEqualTo(100L); + assertThat(results.getOtherResults().get(10)).isEqualTo(EncodingUtils.toBase58String(getDUMMY_CASH_ISSUER().getParty().getOwningKey())); + assertThat(results.getOtherResults().get(11)).isEqualTo("USD"); + + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + return tx; + }); + } } diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 831f0b9d16..11009e3c42 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -7,11 +7,12 @@ import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.keys import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId +import net.corda.core.getOrThrow import net.corda.core.messaging.* import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault import net.corda.core.node.services.unconsumedStates -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow @@ -31,6 +32,7 @@ import net.corda.testing.node.MockNetwork.MockNode import net.corda.testing.sequence import org.apache.commons.io.IOUtils import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.After import org.junit.Assert.assertArrayEquals import org.junit.Before import org.junit.Test @@ -59,8 +61,8 @@ class CordaRPCOpsImplTest { fun setup() { mockNet = MockNetwork() val networkMap = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - aliceNode = mockNet.createNode(networkMapAddress = networkMap.info.address) - notaryNode = mockNet.createNode(advertisedServices = ServiceInfo(SimpleNotaryService.type), networkMapAddress = networkMap.info.address) + aliceNode = mockNet.createNode(networkMapAddress = networkMap.network.myAddress) + notaryNode = mockNet.createNode(advertisedServices = ServiceInfo(SimpleNotaryService.type), networkMapAddress = networkMap.network.myAddress) rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database) CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = setOf( startFlowPermission(), @@ -75,6 +77,11 @@ class CordaRPCOpsImplTest { } } + @After + fun cleanUp() { + mockNet.stopNodes() + } + @Test fun `cash issue accepted`() { val quantity = 1000L @@ -86,18 +93,11 @@ class CordaRPCOpsImplTest { } // Tell the monitoring service node to issue some cash + val anonymous = false val recipient = aliceNode.info.legalIdentity - rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, recipient, notaryNode.info.notaryIdentity) + val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, recipient, notaryNode.info.notaryIdentity, anonymous) mockNet.runNetwork() - val expectedState = Cash.State(Amount(quantity, - Issued(aliceNode.info.legalIdentity.ref(ref), GBP)), - recipient) - - // Query vault via RPC - val cash = rpc.vaultQueryBy() - assertEquals(expectedState, cash.states.first().state.data) - var issueSmId: StateMachineRunId? = null stateMachineUpdates.expectEvents { sequence( @@ -111,11 +111,14 @@ class CordaRPCOpsImplTest { ) } - transactions.expectEvents { - expect { tx -> - assertEquals(expectedState, tx.tx.outputs.single().data) - } - } + val tx = result.returnValue.getOrThrow() + val expectedState = Cash.State(Amount(quantity, + Issued(aliceNode.info.legalIdentity.ref(ref), GBP)), + recipient) + + // Query vault via RPC + val cash = rpc.vaultQueryBy() + assertEquals(expectedState, cash.states.first().state.data) // TODO: deprecated vaultUpdates.expectEvents { @@ -135,22 +138,24 @@ class CordaRPCOpsImplTest { @Test fun `issue and move`() { - rpc.startFlow(::CashIssueFlow, + val anonymous = false + val result = rpc.startFlow(::CashIssueFlow, Amount(100, USD), OpaqueBytes(ByteArray(1, { 1 })), aliceNode.info.legalIdentity, - notaryNode.info.notaryIdentity + notaryNode.info.notaryIdentity, + false ) mockNet.runNetwork() - rpc.startFlow(::CashPaymentFlow, Amount(100, USD), aliceNode.info.legalIdentity) + rpc.startFlow(::CashPaymentFlow, Amount(100, USD), aliceNode.info.legalIdentity, anonymous) mockNet.runNetwork() var issueSmId: StateMachineRunId? = null var moveSmId: StateMachineRunId? = null - stateMachineUpdates.expectEvents { + stateMachineUpdates.expectEvents() { sequence( // ISSUE expect { add: StateMachineUpdate.Added -> @@ -169,6 +174,7 @@ class CordaRPCOpsImplTest { ) } + val tx = result.returnValue.getOrThrow() transactions.expectEvents { sequence( // ISSUE @@ -233,7 +239,8 @@ class CordaRPCOpsImplTest { Amount(100, USD), OpaqueBytes(ByteArray(1, { 1 })), aliceNode.info.legalIdentity, - notaryNode.info.notaryIdentity + notaryNode.info.notaryIdentity, + false ) } } diff --git a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt index 210bc46e2e..4a2c2051de 100644 --- a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt +++ b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt @@ -11,7 +11,7 @@ import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_CA +import net.corda.testing.DUMMY_CA import net.corda.core.utilities.UntrustworthyData import net.corda.jackson.JacksonSupport import net.corda.node.services.identity.InMemoryIdentityService diff --git a/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt b/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt index 78408f57dd..a9fb24ce5b 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt @@ -11,13 +11,13 @@ import net.corda.flows.FetchDataFlow import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.database.RequeryConfiguration import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.schemas.requery.AttachmentEntity import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.testing.node.MockNetwork import net.corda.node.utilities.transaction +import net.corda.testing.node.MockNetwork import net.corda.testing.node.makeTestDataSourceProperties import org.jetbrains.exposed.sql.Database +import org.junit.After import org.junit.Before import org.junit.Test import java.io.ByteArrayInputStream @@ -39,12 +39,15 @@ class AttachmentTests { @Before fun setUp() { mockNet = MockNetwork() - val dataSourceProperties = makeTestDataSourceProperties() - configuration = RequeryConfiguration(dataSourceProperties) } + @After + fun cleanUp() { + mockNet.stopNodes() + } + fun fakeAttachment(): ByteArray { val bs = ByteArrayOutputStream() val js = JarOutputStream(bs) @@ -61,7 +64,7 @@ class AttachmentTests { // Insert an attachment into node zero's store directly. val id = n0.database.transaction { - n0.storage.attachments.importAttachment(ByteArrayInputStream(fakeAttachment())) + n0.attachments.importAttachment(ByteArrayInputStream(fakeAttachment())) } // Get node one to run a flow to fetch it and insert it. @@ -72,7 +75,7 @@ class AttachmentTests { // Verify it was inserted into node one's store. val attachment = n1.database.transaction { - n1.storage.attachments.openAttachment(id)!! + n1.attachments.openAttachment(id)!! } assertEquals(id, attachment.open().readBytes().sha256()) @@ -108,18 +111,18 @@ class AttachmentTests { return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { override fun start(): MockNetwork.MockNode { super.start() - (storage.attachments as NodeAttachmentService).checkAttachmentsOnLoad = false + attachments.checkAttachmentsOnLoad = false return this } } } }, true, null, null, ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type)) - val n1 = mockNet.createNode(n0.info.address) + val n1 = mockNet.createNode(n0.network.myAddress) val attachment = fakeAttachment() // Insert an attachment into node zero's store directly. val id = n0.database.transaction { - n0.storage.attachments.importAttachment(ByteArrayInputStream(attachment)) + n0.attachments.importAttachment(ByteArrayInputStream(attachment)) } // Corrupt its store. @@ -130,7 +133,7 @@ class AttachmentTests { corruptAttachment.attId = id corruptAttachment.content = attachment n0.database.transaction { - (n0.storage.attachments as NodeAttachmentService).session.update(corruptAttachment) + n0.attachments.session.update(corruptAttachment) } diff --git a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt index bf6c2755fe..b935635105 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt @@ -7,6 +7,7 @@ import net.corda.node.services.messaging.TopicStringValidator import net.corda.node.services.messaging.createMessage import net.corda.node.services.network.NetworkMapService import net.corda.testing.node.MockNetwork +import org.junit.After import org.junit.Test import java.util.* import kotlin.test.assertEquals @@ -16,8 +17,13 @@ import kotlin.test.assertTrue class InMemoryMessagingTests { val mockNet = MockNetwork() + @After + fun cleanUp() { + mockNet.stopNodes() + } + @Test - fun topicStringValidation() { + fun `topic string validation`() { TopicStringValidator.check("this.is.ok") TopicStringValidator.check("this.is.OkAlso") assertFails { @@ -34,15 +40,15 @@ class InMemoryMessagingTests { @Test fun basics() { val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - val node2 = mockNet.createNode(networkMapAddress = node1.info.address) - val node3 = mockNet.createNode(networkMapAddress = node1.info.address) + val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) + val node3 = mockNet.createNode(networkMapAddress = node1.network.myAddress) val bits = "test-content".toByteArray() var finalDelivery: Message? = null with(node2) { node2.network.addMessageHandler { msg, _ -> - node2.network.send(msg, node3.info.address) + node2.network.send(msg, node3.network.myAddress) } } @@ -53,7 +59,7 @@ class InMemoryMessagingTests { } // Node 1 sends a message and it should end up in finalDelivery, after we run the network - node1.network.send(node1.network.createMessage("test.topic", DEFAULT_SESSION_ID, bits), node2.info.address) + node1.network.send(node1.network.createMessage("test.topic", DEFAULT_SESSION_ID, bits), node2.network.myAddress) mockNet.runNetwork(rounds = 1) @@ -63,8 +69,8 @@ class InMemoryMessagingTests { @Test fun broadcast() { val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - val node2 = mockNet.createNode(networkMapAddress = node1.info.address) - val node3 = mockNet.createNode(networkMapAddress = node1.info.address) + val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) + val node3 = mockNet.createNode(networkMapAddress = node1.network.myAddress) val bits = "test-content".toByteArray() @@ -82,7 +88,7 @@ class InMemoryMessagingTests { @Test fun `skip unhandled messages`() { val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - val node2 = mockNet.createNode(networkMapAddress = node1.info.address) + val node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) var received: Int = 0 node1.network.addMessageHandler("valid_message") { _, _ -> diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 6256e5ec3e..049e44a463 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -3,34 +3,41 @@ package net.corda.node.messaging import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.CommercialPaper import net.corda.contracts.asset.* -import net.corda.contracts.testing.fillWithSomeTestCash import net.corda.core.* import net.corda.core.contracts.* import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sign -import net.corda.core.flows.* +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine +import net.corda.core.messaging.DataFeed import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.node.NodeInfo -import net.corda.core.node.services.* +import net.corda.core.node.services.ServiceInfo +import net.corda.core.node.services.Vault import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.* +import net.corda.testing.LogHelper +import net.corda.core.utilities.unwrap import net.corda.flows.TwoPartyTradeFlow.Buyer import net.corda.flows.TwoPartyTradeFlow.Seller import net.corda.node.internal.AbstractNode +import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.persistence.DBTransactionStorage -import net.corda.node.services.persistence.StorageServiceImpl import net.corda.node.services.persistence.checkpoints import net.corda.node.utilities.transaction import net.corda.testing.* +import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat @@ -70,6 +77,7 @@ class TwoPartyTradeFlowTests { @After fun after() { + mockNet.stopNodes() LogHelper.reset("platform.trade", "core.contract.TransactionGroup", "recordingmap") } @@ -127,8 +135,8 @@ class TwoPartyTradeFlowTests { ledger { val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = mockNet.createPartyNode(notaryNode.info.address, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.info.address, BOB.name) + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) aliceNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop() @@ -147,7 +155,7 @@ class TwoPartyTradeFlowTests { val cashLockId = UUID.randomUUID() bobNode.database.transaction { // lock the cash states with an arbitrary lockId (to prevent the Buyer flow from claiming the states) - bobNode.vault.softLockReserve(cashLockId, cashStates.states.map { it.ref }.toSet()) + bobNode.services.vaultService.softLockReserve(cashLockId, cashStates.states.map { it.ref }.toSet()) } val (bobStateMachine, aliceResult) = runBuyerAndSeller(notaryNode, aliceNode, bobNode, @@ -173,13 +181,13 @@ class TwoPartyTradeFlowTests { fun `shutdown and restore`() { ledger { val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = mockNet.createPartyNode(notaryNode.info.address, ALICE.name) - var bobNode = mockNet.createPartyNode(notaryNode.info.address, BOB.name) + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + var bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) aliceNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop() val bobAddr = bobNode.network.myAddress as InMemoryMessagingNetwork.PeerHandle - val networkMapAddr = notaryNode.info.address + val networkMapAddr = notaryNode.network.myAddress mockNet.runNetwork() // Clear network map registration messages @@ -210,7 +218,7 @@ class TwoPartyTradeFlowTests { assertThat(bobNode.checkpointStorage.checkpoints()).hasSize(1) } - val storage = bobNode.storage.validatedTransactions + val storage = bobNode.services.validatedTransactions val bobTransactionsBeforeCrash = bobNode.database.transaction { (storage as DBTransactionStorage).transactions } @@ -251,7 +259,9 @@ class TwoPartyTradeFlowTests { } bobNode.database.transaction { - val restoredBobTransactions = bobTransactionsBeforeCrash.filter { bobNode.storage.validatedTransactions.getTransaction(it.id) != null } + val restoredBobTransactions = bobTransactionsBeforeCrash.filter { + bobNode.services.validatedTransactions.getTransaction(it.id) != null + } assertThat(restoredBobTransactions).containsAll(bobTransactionsBeforeCrash) } @@ -275,13 +285,9 @@ class TwoPartyTradeFlowTests { overrideServices: Map?, entropyRoot: BigInteger): MockNetwork.MockNode { return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { - // That constructs the storage service object in a customised way ... - override fun constructStorageService( - attachments: AttachmentStorage, - transactionStorage: TransactionStorage, - stateMachineRecordedTransactionMappingStorage: StateMachineRecordedTransactionMappingStorage - ): StorageServiceImpl { - return StorageServiceImpl(attachments, RecordingTransactionStorage(database, transactionStorage), stateMachineRecordedTransactionMappingStorage) + // That constructs a recording tx storage + override fun makeTransactionStorage(): WritableTransactionStorage { + return RecordingTransactionStorage(database, super.makeTransactionStorage()) } } } @@ -291,8 +297,8 @@ class TwoPartyTradeFlowTests { @Test fun `check dependencies of sale asset are resolved`() { val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = makeNodeWithTracking(notaryNode.info.address, ALICE.name) - val bobNode = makeNodeWithTracking(notaryNode.info.address, BOB.name) + val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name) + val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name) ledger(aliceNode.services) { @@ -307,7 +313,7 @@ class TwoPartyTradeFlowTests { attachment(ByteArrayInputStream(stream.toByteArray())) } - val extraKey = bobNode.keyManagement.keys.single() + val extraKey = bobNode.services.keyManagementService.keys.single() val bobsFakeCash = fillUpForBuyer(false, AnonymousParty(extraKey), DUMMY_CASH_ISSUER.party, notaryNode.info.notaryIdentity).second @@ -325,7 +331,7 @@ class TwoPartyTradeFlowTests { mockNet.runNetwork() run { - val records = (bobNode.storage.validatedTransactions as RecordingTransactionStorage).records + val records = (bobNode.services.validatedTransactions as RecordingTransactionStorage).records // Check Bobs's database accesses as Bob's cash transactions are downloaded by Alice. records.expectEvents(isStrict = false) { sequence( @@ -343,7 +349,7 @@ class TwoPartyTradeFlowTests { // Bob has downloaded the attachment. bobNode.database.transaction { - bobNode.storage.attachments.openAttachment(attachmentID)!!.openAsJAR().use { + bobNode.services.attachments.openAttachment(attachmentID)!!.openAsJAR().use { it.nextJarEntry val contents = it.reader().readText() assertTrue(contents.contains("Our commercial paper is top notch stuff")) @@ -353,7 +359,7 @@ class TwoPartyTradeFlowTests { // And from Alice's perspective ... run { - val records = (aliceNode.storage.validatedTransactions as RecordingTransactionStorage).records + val records = (aliceNode.services.validatedTransactions as RecordingTransactionStorage).records records.expectEvents(isStrict = false) { sequence( // Seller Alice sends her seller info to Bob, who wants to check the asset for sale. @@ -390,8 +396,8 @@ class TwoPartyTradeFlowTests { @Test fun `track works`() { val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = makeNodeWithTracking(notaryNode.info.address, ALICE.name) - val bobNode = makeNodeWithTracking(notaryNode.info.address, BOB.name) + val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name) + val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name) ledger(aliceNode.services) { @@ -406,7 +412,7 @@ class TwoPartyTradeFlowTests { attachment(ByteArrayInputStream(stream.toByteArray())) } - val bobsKey = bobNode.keyManagement.keys.single() + val bobsKey = bobNode.services.keyManagementService.keys.single() val bobsFakeCash = fillUpForBuyer(false, AnonymousParty(bobsKey), DUMMY_CASH_ISSUER.party, notaryNode.info.notaryIdentity).second @@ -421,8 +427,10 @@ class TwoPartyTradeFlowTests { mockNet.runNetwork() // Clear network map registration messages - val aliceTxStream = aliceNode.storage.validatedTransactions.track().second - val aliceTxMappings = with(aliceNode) { database.transaction { storage.stateMachineRecordedTransactionMapping.track().second } } + val aliceTxStream = aliceNode.services.validatedTransactions.track().updates + val aliceTxMappings = with(aliceNode) { + database.transaction { services.stateMachineRecordedTransactionMapping.track().updates } + } val aliceSmId = runBuyerAndSeller(notaryNode, aliceNode, bobNode, "alice's paper".outputStateAndRef()).sellerId @@ -442,7 +450,7 @@ class TwoPartyTradeFlowTests { ) aliceTxStream.expectEvents { aliceTxExpectations } val aliceMappingExpectations = sequence( - expect { (stateMachineRunId, transactionId) -> + expect { (stateMachineRunId, transactionId) -> require(stateMachineRunId == aliceSmId) require(transactionId == bobsFakeCash[0].id) }, @@ -450,9 +458,9 @@ class TwoPartyTradeFlowTests { require(stateMachineRunId == aliceSmId) require(transactionId == bobsFakeCash[2].id) }, - expect { mapping: StateMachineTransactionMapping -> - require(mapping.stateMachineRunId == aliceSmId) - require(mapping.transactionId == bobsFakeCash[1].id) + expect { (stateMachineRunId, transactionId) -> + require(stateMachineRunId == aliceSmId) + require(transactionId == bobsFakeCash[1].id) } ) aliceTxMappings.expectEvents { aliceMappingExpectations } @@ -501,12 +509,13 @@ class TwoPartyTradeFlowTests { @Suspendable override fun call(): SignedTransaction { send(buyer, Pair(notary.notaryIdentity, price)) + val key = serviceHub.keyManagementService.freshKey() return subFlow(Seller( buyer, notary, assetToSell, price, - serviceHub.legalIdentityKey)) + AnonymousParty(key))) } } @@ -528,8 +537,8 @@ class TwoPartyTradeFlowTests { expectedMessageSubstring: String ) { val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = mockNet.createPartyNode(notaryNode.info.address, ALICE.name) - val bobNode = mockNet.createPartyNode(notaryNode.info.address, BOB.name) + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val issuer = MEGA_CORP.ref(1, 2, 3) val bobsBadCash = bobNode.database.transaction { @@ -588,7 +597,7 @@ class TwoPartyTradeFlowTests { } return node.database.transaction { node.services.recordTransactions(signed) - val validatedTransactions = node.services.storageService.validatedTransactions + val validatedTransactions = node.services.validatedTransactions if (validatedTransactions is RecordingTransactionStorage) { validatedTransactions.records.clear() } @@ -669,8 +678,8 @@ class TwoPartyTradeFlowTests { } - class RecordingTransactionStorage(val database: Database, val delegate: TransactionStorage) : TransactionStorage { - override fun track(): Pair, Observable> { + class RecordingTransactionStorage(val database: Database, val delegate: WritableTransactionStorage) : WritableTransactionStorage { + override fun track(): DataFeed, SignedTransaction> { return database.transaction { delegate.track() } diff --git a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt index 5b3194df6b..693abdc855 100644 --- a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt +++ b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt @@ -6,33 +6,41 @@ import net.corda.core.flows.FlowLogic import net.corda.core.node.NodeInfo import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken -import net.corda.core.transactions.SignedTransaction import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.serialization.NodeClock import net.corda.node.services.api.* +import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.testing.MOCK_IDENTITY_SERVICE +import net.corda.testing.node.MockAttachmentStorage import net.corda.testing.node.MockNetworkMapCache -import net.corda.testing.node.MockStorageService +import net.corda.testing.node.MockStateMachineRecordedTransactionMappingStorage +import net.corda.testing.node.MockTransactionStorage +import org.jetbrains.exposed.sql.Database import java.time.Clock open class MockServiceHubInternal( + override val database: Database, + override val configuration: NodeConfiguration, val customVault: VaultService? = null, val customVaultQuery: VaultQueryService? = null, val keyManagement: KeyManagementService? = null, val network: MessagingService? = null, val identity: IdentityService? = MOCK_IDENTITY_SERVICE, - val storage: TxWritableStorageService? = MockStorageService(), - val mapCache: NetworkMapCacheInternal? = MockNetworkMapCache(), + override val attachments: AttachmentStorage = MockAttachmentStorage(), + override val validatedTransactions: WritableTransactionStorage = MockTransactionStorage(), + override val uploaders: List = listOf(), + override val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage(), + val mapCache: NetworkMapCacheInternal? = null, val scheduler: SchedulerService? = null, val overrideClock: Clock? = NodeClock(), val schemas: SchemaService? = NodeSchemaService(), val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2) -) : ServiceHubInternal() { +) : ServiceHubInternal { override val vaultQueryService: VaultQueryService get() = customVaultQuery ?: throw UnsupportedOperationException() override val transactionVerifierService: TransactionVerifierService @@ -46,30 +54,22 @@ open class MockServiceHubInternal( override val networkService: MessagingService get() = network ?: throw UnsupportedOperationException() override val networkMapCache: NetworkMapCacheInternal - get() = mapCache ?: throw UnsupportedOperationException() - override val storageService: StorageService - get() = storage ?: throw UnsupportedOperationException() + get() = mapCache ?: MockNetworkMapCache(this) override val schedulerService: SchedulerService get() = scheduler ?: throw UnsupportedOperationException() override val clock: Clock get() = overrideClock ?: throw UnsupportedOperationException() override val myInfo: NodeInfo get() = throw UnsupportedOperationException() - override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) override val rpcFlows: List>> get() = throw UnsupportedOperationException() override val schemaService: SchemaService get() = schemas ?: throw UnsupportedOperationException() override val auditService: AuditService = DummyAuditService() - // We isolate the storage service with writable TXes so that it can't be accessed except via recordTransactions() - private val txStorageService: TxWritableStorageService - get() = storage ?: throw UnsupportedOperationException() lateinit var smm: StateMachineManager - override fun recordTransactions(txs: Iterable) = recordTransactionsInternal(txStorageService, txs) - override fun cordaService(type: Class): T = throw UnsupportedOperationException() override fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator): FlowStateMachineImpl { diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 84954fa218..8aef7f445e 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -1,22 +1,24 @@ package net.corda.node.services import net.corda.core.contracts.* +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.generateKeyPair import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.core.seconds import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.flows.NotaryChangeFlow import net.corda.flows.StateReplacementException import net.corda.node.internal.AbstractNode import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.getTestPartyAndCertificate import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.bouncycastle.asn1.x500.X500Name +import org.junit.After import org.junit.Before import org.junit.Test import java.time.Instant @@ -37,13 +39,18 @@ class NotaryChangeTests { oldNotaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) - clientNodeA = mockNet.createNode(networkMapAddress = oldNotaryNode.info.address) - clientNodeB = mockNet.createNode(networkMapAddress = oldNotaryNode.info.address) - newNotaryNode = mockNet.createNode(networkMapAddress = oldNotaryNode.info.address, advertisedServices = ServiceInfo(SimpleNotaryService.type)) + clientNodeA = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress) + clientNodeB = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress) + newNotaryNode = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress, advertisedServices = ServiceInfo(SimpleNotaryService.type)) mockNet.runNetwork() // Clear network map registration messages } + @After + fun cleanUp() { + mockNet.stopNodes() + } + @Test fun `should change notary for a state with single participant`() { val state = issueState(clientNodeA, oldNotaryNode) @@ -99,7 +106,7 @@ class NotaryChangeTests { val newState = future.resultFuture.getOrThrow() assertEquals(newState.state.notary, newNotary) - val notaryChangeTx = clientNodeA.services.storageService.validatedTransactions.getTransaction(newState.ref.txhash)!!.tx + val notaryChangeTx = clientNodeA.services.validatedTransactions.getTransaction(newState.ref.txhash)!!.tx // Check that all encumbrances have been propagated to the outputs val originalOutputs = issueTx.outputs.map { it.data } @@ -135,7 +142,7 @@ class NotaryChangeTests { addOutputState(stateB, notary, encumbrance = 1) // Encumbered by stateC } val stx = node.services.signInitialTransaction(tx) - node.services.recordTransactions(listOf(stx)) + node.services.recordTransactions(stx) return tx.toWireTransaction() } @@ -152,7 +159,7 @@ fun issueState(node: AbstractNode, notaryNode: AbstractNode): StateAndRef<*> { val tx = DummyContract.generateInitial(Random().nextInt(), notaryNode.info.notaryIdentity, node.info.legalIdentity.ref(0)) val signedByNode = node.services.signInitialTransaction(tx) val stx = notaryNode.services.addSignature(signedByNode, notaryNode.services.notaryIdentityKey) - node.services.recordTransactions(listOf(stx)) + node.services.recordTransactions(stx) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } @@ -163,16 +170,16 @@ fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode, notaryNode: A val signedByA = nodeA.services.signInitialTransaction(tx) val signedByAB = nodeB.services.addSignature(signedByA) val stx = notaryNode.services.addSignature(signedByAB, notaryNode.services.notaryIdentityKey) - nodeA.services.recordTransactions(listOf(stx)) - nodeB.services.recordTransactions(listOf(stx)) + nodeA.services.recordTransactions(stx) + nodeB.services.recordTransactions(stx) val stateAndRef = StateAndRef(state, StateRef(stx.id, 0)) return stateAndRef } fun issueInvalidState(node: AbstractNode, notary: Party): StateAndRef<*> { val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.legalIdentity.ref(0)) - tx.addTimeWindow(Instant.now(), 30.seconds) + tx.setTimeWindow(Instant.now(), 30.seconds) val stx = node.services.signInitialTransaction(tx) - node.services.recordTransactions(listOf(stx)) + node.services.recordTransactions(stx) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } diff --git a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt index 93ed430c6c..fcebec4411 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt @@ -1,8 +1,8 @@ package net.corda.node.services.config -import com.google.common.net.HostAndPort import net.corda.core.crypto.commonName -import net.corda.core.utilities.ALICE +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.testing.ALICE import net.corda.nodeapi.User import net.corda.testing.node.makeTestDataSourceProperties import org.assertj.core.api.Assertions.assertThatThrownBy @@ -25,8 +25,8 @@ class FullNodeConfigurationTest { rpcUsers = emptyList(), verifierType = VerifierType.InMemory, useHTTPS = false, - p2pAddress = HostAndPort.fromParts("localhost", 0), - rpcAddress = HostAndPort.fromParts("localhost", 1), + p2pAddress = NetworkHostAndPort("localhost", 0), + rpcAddress = NetworkHostAndPort("localhost", 1), messagingServerAddress = null, extraAdvertisedServiceIds = emptyList(), bftReplicaId = null, diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index cebee41ce1..cc8c9a98b1 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -1,36 +1,39 @@ package net.corda.node.services.database import net.corda.contracts.asset.Cash +import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DummyFungibleContract -import net.corda.contracts.testing.consumeCash -import net.corda.contracts.testing.fillWithSomeTestCash -import net.corda.contracts.testing.fillWithSomeTestDeals -import net.corda.contracts.testing.fillWithSomeTestLinearStates +import net.corda.testing.contracts.consumeCash +import net.corda.testing.contracts.fillWithSomeTestCash +import net.corda.testing.contracts.fillWithSomeTestDeals +import net.corda.testing.contracts.fillWithSomeTestLinearStates import net.corda.core.contracts.* import net.corda.core.crypto.toBase58String import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService -import net.corda.core.schemas.DummyLinearStateSchemaV1 -import net.corda.core.schemas.DummyLinearStateSchemaV2 import net.corda.core.schemas.PersistentStateRef +import net.corda.testing.schemas.DummyLinearStateSchemaV1 +import net.corda.testing.schemas.DummyLinearStateSchemaV2 import net.corda.core.serialization.deserialize import net.corda.core.serialization.storageKryo import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.BOB_KEY -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.BOB_KEY +import net.corda.testing.DUMMY_NOTARY import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.NodeVaultService -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 -import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 +import net.corda.core.schemas.CommonSchemaV1 +import net.corda.node.services.vault.VaultSchemaV1 import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.schemas.CashSchemaV1 import net.corda.schemas.SampleCashSchemaV2 import net.corda.schemas.SampleCashSchemaV3 import net.corda.testing.BOB_PUBKEY +import net.corda.testing.BOC +import net.corda.testing.BOC_KEY import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties import org.assertj.core.api.Assertions @@ -85,7 +88,7 @@ class HibernateConfigurationTest { override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) @@ -199,6 +202,67 @@ class HibernateConfigurationTest { queryResultsAsc.map { println(it.recordedTime) } } + @Test + fun `with sorting by state ref desc and asc`() { + // generate additional state ref indexes + database.transaction { + services.consumeCash(1.DOLLARS) + services.consumeCash(2.DOLLARS) + services.consumeCash(3.DOLLARS) + services.consumeCash(4.DOLLARS) + services.consumeCash(5.DOLLARS) + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + + val sortByStateRef = vaultStates.get("stateRef") + + // order by DESC + criteriaQuery.orderBy(criteriaBuilder.desc(sortByStateRef)) + val queryResults = entityManager.createQuery(criteriaQuery).resultList + println("DESC by stateRef") + queryResults.map { println(it.stateRef) } + + // order by ASC + criteriaQuery.orderBy(criteriaBuilder.asc(sortByStateRef)) + val queryResultsAsc = entityManager.createQuery(criteriaQuery).resultList + println("ASC by stateRef") + queryResultsAsc.map { println(it.stateRef) } + } + + @Test + fun `with sorting by state ref index and txId desc and asc`() { + // generate additional state ref indexes + database.transaction { + services.consumeCash(1.DOLLARS) + services.consumeCash(2.DOLLARS) + services.consumeCash(3.DOLLARS) + services.consumeCash(4.DOLLARS) + services.consumeCash(5.DOLLARS) + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + + val sortByIndex = vaultStates.get("stateRef").get("index") + val sortByTxId = vaultStates.get("stateRef").get("txId") + + // order by DESC + criteriaQuery.orderBy(criteriaBuilder.desc(sortByIndex), criteriaBuilder.desc(sortByTxId)) + val queryResults = entityManager.createQuery(criteriaQuery).resultList + println("DESC by index txId") + queryResults.map { println(it.stateRef) } + + // order by ASC + criteriaQuery.orderBy(criteriaBuilder.asc(sortByIndex), criteriaBuilder.asc(sortByTxId)) + val queryResultsAsc = entityManager.createQuery(criteriaQuery).resultList + println("ASC by index txId") + queryResultsAsc.map { println(it.stateRef) } + } + @Test fun `with pagination`() { // add 100 additional cash entries @@ -301,6 +365,109 @@ class HibernateConfigurationTest { } } + @Test + fun `calculate cash balances`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)) // +$100 = $200 + services.fillWithSomeTestCash(50.POUNDS, DUMMY_NOTARY, 5, 5, Random(0L)) // £50 = £50 + services.fillWithSomeTestCash(25.POUNDS, DUMMY_NOTARY, 5, 5, Random(0L)) // +£25 = £175 + services.fillWithSomeTestCash(500.SWISS_FRANCS, DUMMY_NOTARY, 10, 10, Random(0L)) // CHF500 = CHF500 + services.fillWithSomeTestCash(250.SWISS_FRANCS, DUMMY_NOTARY, 5, 5, Random(0L)) // +CHF250 = CHF750 + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) + val cashStates = criteriaQuery.from(CashSchemaV1.PersistentCashState::class.java) + + // aggregate function + criteriaQuery.multiselect(cashStates.get("currency"), + criteriaBuilder.sum(cashStates.get("pennies"))) + // group by + criteriaQuery.groupBy(cashStates.get("currency")) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + + queryResults.forEach { tuple -> println("${tuple.get(0)} = ${tuple.get(1)}") } + + assertThat(queryResults[0].get(0)).isEqualTo("CHF") + assertThat(queryResults[0].get(1)).isEqualTo(75000L) + assertThat(queryResults[1].get(0)).isEqualTo("GBP") + assertThat(queryResults[1].get(1)).isEqualTo(7500L) + assertThat(queryResults[2].get(0)).isEqualTo("USD") + assertThat(queryResults[2].get(1)).isEqualTo(20000L) + } + + @Test + fun `calculate cash balance for single currency`() { + database.transaction { + services.fillWithSomeTestCash(50.POUNDS, DUMMY_NOTARY, 5, 5, Random(0L)) // £50 = £50 + services.fillWithSomeTestCash(25.POUNDS, DUMMY_NOTARY, 5, 5, Random(0L)) // +£25 = £175 + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) + val cashStates = criteriaQuery.from(CashSchemaV1.PersistentCashState::class.java) + + // aggregate function + criteriaQuery.multiselect(cashStates.get("currency"), + criteriaBuilder.sum(cashStates.get("pennies"))) + + // where + criteriaQuery.where(criteriaBuilder.equal(cashStates.get("currency"), "GBP")) + + // group by + criteriaQuery.groupBy(cashStates.get("currency")) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + + queryResults.forEach { tuple -> println("${tuple.get(0)} = ${tuple.get(1)}") } + + assertThat(queryResults[0].get(0)).isEqualTo("GBP") + assertThat(queryResults[0].get(1)).isEqualTo(7500L) + } + + @Test + fun `calculate and order by cash balance for owner and currency`() { + database.transaction { + + services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), issuedBy = BOC.ref(1), issuerKey = BOC_KEY) + services.fillWithSomeTestCash(300.POUNDS, DUMMY_NOTARY, 3, 3, Random(0L), issuedBy = DUMMY_CASH_ISSUER) + services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L), issuedBy = BOC.ref(2), issuerKey = BOC_KEY) + } + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) + val cashStates = criteriaQuery.from(CashSchemaV1.PersistentCashState::class.java) + + // aggregate function + criteriaQuery.multiselect(cashStates.get("currency"), + criteriaBuilder.sum(cashStates.get("pennies"))) + + // group by + criteriaQuery.groupBy(cashStates.get("issuerParty"), cashStates.get("currency")) + + // order by + criteriaQuery.orderBy(criteriaBuilder.desc(criteriaBuilder.sum(cashStates.get("pennies")))) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + + queryResults.forEach { tuple -> println("${tuple.get(0)} = ${tuple.get(1)}") } + + assertThat(queryResults).hasSize(4) + assertThat(queryResults[0].get(0)).isEqualTo("GBP") + assertThat(queryResults[0].get(1)).isEqualTo(40000L) + assertThat(queryResults[1].get(0)).isEqualTo("GBP") + assertThat(queryResults[1].get(1)).isEqualTo(30000L) + assertThat(queryResults[2].get(0)).isEqualTo("USD") + assertThat(queryResults[2].get(1)).isEqualTo(20000L) + assertThat(queryResults[3].get(0)).isEqualTo("USD") + assertThat(queryResults[3].get(1)).isEqualTo(10000L) + } + /** * CashSchemaV2 = optimised Cash schema (extending FungibleState) */ @@ -482,7 +649,7 @@ class HibernateConfigurationTest { // search predicate val cashStatesSchema = criteriaQuery.from(SampleCashSchemaV3.PersistentCashState::class.java) - val joinCashToParty = cashStatesSchema.join("owner") + val joinCashToParty = cashStatesSchema.join("owner") val queryOwnerKey = BOB_PUBKEY.toBase58String() criteriaQuery.where(criteriaBuilder.equal(joinCashToParty.get("key"), queryOwnerKey)) diff --git a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt index b1b482261a..ec8f6a57d2 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt @@ -3,12 +3,12 @@ package net.corda.node.services.database import io.requery.Persistable import io.requery.kotlin.eq import io.requery.sql.KotlinEntityDataStore -import net.corda.core.contracts.DummyContract import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.DigitalSignature -import net.corda.core.crypto.NullPublicKey import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.testing.NullPublicKey import net.corda.core.crypto.toBase58String import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.Vault @@ -16,8 +16,8 @@ import net.corda.core.serialization.serialize import net.corda.core.serialization.storageKryo import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_PUBKEY_1 +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_PUBKEY_1 import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.vault.schemas.requery.Models import net.corda.node.services.vault.schemas.requery.VaultCashBalancesEntity diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index 71754dd4d5..46ef15d5c8 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -9,9 +9,9 @@ import net.corda.core.identity.AbstractParty import net.corda.core.node.ServiceHub import net.corda.core.node.services.VaultService import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.utilities.ALICE_KEY -import net.corda.core.utilities.DUMMY_CA -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE_KEY +import net.corda.testing.DUMMY_CA +import net.corda.testing.DUMMY_NOTARY import net.corda.node.services.MockServiceHubInternal import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.persistence.DBCheckpointStorage @@ -21,10 +21,12 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction +import net.corda.testing.getTestX509Name import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockKeyManagementService import net.corda.testing.node.TestClock import net.corda.testing.node.makeTestDataSourceProperties +import net.corda.testing.testNodeConfiguration import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x500.X500Name import org.jetbrains.exposed.sql.Database @@ -32,6 +34,7 @@ import org.junit.After import org.junit.Before import org.junit.Test import java.io.Closeable +import java.nio.file.Paths import java.security.PublicKey import java.time.Clock import java.time.Instant @@ -67,7 +70,6 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { val testReference: NodeSchedulerServiceTest } - @Before fun setup() { countDown = CountDownLatch(1) @@ -87,11 +89,16 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { InMemoryMessagingNetwork.PeerHandle(0, nullIdentity), AffinityExecutor.ServiceAffinityExecutor("test", 1), database) - services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, network = mockMessagingService), TestReference { + services = object : MockServiceHubInternal( + database, + testNodeConfiguration(Paths.get("."), getTestX509Name("Alice")), + overrideClock = testClock, + keyManagement = kms, + network = mockMessagingService), TestReference { override val vaultService: VaultService = NodeVaultService(this, dataSourceProps) override val testReference = this@NodeSchedulerServiceTest } - scheduler = NodeSchedulerService(services, database, schedulerGatedExecutor) + scheduler = NodeSchedulerService(services, schedulerGatedExecutor) smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) val mockSMM = StateMachineManager(services, DBCheckpointStorage(), smmExecutor, database) mockSMM.changes.subscribe { change -> diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index 10a31f2cbe..6fec905bae 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -2,6 +2,7 @@ package net.corda.node.services.events import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.containsAny import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic @@ -11,7 +12,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.linearHeadsOfType -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.FinalityFlow import net.corda.node.services.network.NetworkMapService import net.corda.node.services.statemachine.StateMachineManager @@ -94,8 +95,8 @@ class ScheduledFlowTests { notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type))) - nodeA = mockNet.createNode(notaryNode.info.address, start = false) - nodeB = mockNet.createNode(notaryNode.info.address, start = false) + nodeA = mockNet.createNode(notaryNode.network.myAddress, start = false) + nodeB = mockNet.createNode(notaryNode.network.myAddress, start = false) mockNet.startNodes() } diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index 174dab96f7..d92bec8a22 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -1,15 +1,15 @@ package net.corda.node.services.messaging import com.codahale.metrics.MetricRegistry -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import net.corda.core.crypto.generateKeyPair import net.corda.core.messaging.RPCOps import net.corda.core.node.services.DEFAULT_SESSION_ID -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.LogHelper +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.testing.ALICE +import net.corda.testing.LogHelper import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.api.MonitoringService @@ -21,9 +21,9 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction -import net.corda.testing.MOCK_VERSION_INFO import net.corda.testing.freeLocalHostAndPort import net.corda.testing.freePort +import net.corda.testing.node.MOCK_VERSION_INFO import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.testNodeConfiguration import org.assertj.core.api.Assertions.assertThat @@ -60,7 +60,8 @@ class ArtemisMessagingTests { var messagingClient: NodeMessagingClient? = null var messagingServer: ArtemisMessagingServer? = null - val networkMapCache = InMemoryNetworkMapCache() + // TODO: We should have a dummy service hub rather than change behaviour in tests + val networkMapCache = InMemoryNetworkMapCache(serviceHub = null) val rpcOps = object : RPCOps { override val protocolVersion: Int get() = throw UnsupportedOperationException() @@ -217,7 +218,7 @@ class ArtemisMessagingTests { return messagingClient } - private fun createMessagingClient(server: HostAndPort = HostAndPort.fromParts("localhost", serverPort)): NodeMessagingClient { + private fun createMessagingClient(server: NetworkHostAndPort = NetworkHostAndPort("localhost", serverPort)): NodeMessagingClient { return database.transaction { NodeMessagingClient( config, diff --git a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt index 47a45d8e9b..9c6df604e7 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt @@ -7,10 +7,10 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.services.DEFAULT_SESSION_ID import net.corda.core.node.services.ServiceInfo import net.corda.core.serialization.deserialize -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.CHARLIE -import net.corda.core.utilities.DUMMY_MAP +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.CHARLIE +import net.corda.testing.DUMMY_MAP import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.send import net.corda.node.services.messaging.sendRequest @@ -186,8 +186,8 @@ abstract class AbstractNetworkMapServiceTest } private fun MockNode.fetchMap(subscribe: Boolean = false, ifChangedSinceVersion: Int? = null): List { - val request = FetchMapRequest(subscribe, ifChangedSinceVersion, info.address) - val response = services.networkService.sendRequest(FETCH_TOPIC, request, mapServiceNode.info.address) + val request = FetchMapRequest(subscribe, ifChangedSinceVersion, network.myAddress) + val response = services.networkService.sendRequest(FETCH_TOPIC, request, mapServiceNode.network.myAddress) mockNet.runNetwork() return response.getOrThrow().nodes?.map { it.toChanged() } ?: emptyList() } @@ -198,8 +198,8 @@ abstract class AbstractNetworkMapServiceTest } private fun MockNode.identityQuery(): NodeInfo? { - val request = QueryIdentityRequest(info.legalIdentityAndCert, info.address) - val response = services.networkService.sendRequest(QUERY_TOPIC, request, mapServiceNode.info.address) + val request = QueryIdentityRequest(info.legalIdentityAndCert, network.myAddress) + val response = services.networkService.sendRequest(QUERY_TOPIC, request, mapServiceNode.network.myAddress) mockNet.runNetwork() return response.getOrThrow().node } @@ -216,39 +216,39 @@ abstract class AbstractNetworkMapServiceTest } val expires = Instant.now() + NetworkMapService.DEFAULT_EXPIRATION_PERIOD val nodeRegistration = NodeRegistration(info, distinctSerial, addOrRemove, expires) - val request = RegistrationRequest(nodeRegistration.toWire(services.keyManagementService, services.legalIdentityKey), info.address) - val response = services.networkService.sendRequest(REGISTER_TOPIC, request, mapServiceNode.info.address) + val request = RegistrationRequest(nodeRegistration.toWire(services.keyManagementService, services.legalIdentityKey), network.myAddress) + val response = services.networkService.sendRequest(REGISTER_TOPIC, request, mapServiceNode.network.myAddress) mockNet.runNetwork() return response } private fun MockNode.subscribe(): List { - val request = SubscribeRequest(true, info.address) + val request = SubscribeRequest(true, network.myAddress) val updates = BlockingArrayQueue() services.networkService.addMessageHandler(PUSH_TOPIC, DEFAULT_SESSION_ID) { message, _ -> updates += message.data.deserialize() } - val response = services.networkService.sendRequest(SUBSCRIPTION_TOPIC, request, mapServiceNode.info.address) + val response = services.networkService.sendRequest(SUBSCRIPTION_TOPIC, request, mapServiceNode.network.myAddress) mockNet.runNetwork() assertThat(response.getOrThrow().confirmed).isTrue() return updates } private fun MockNode.unsubscribe() { - val request = SubscribeRequest(false, info.address) - val response = services.networkService.sendRequest(SUBSCRIPTION_TOPIC, request, mapServiceNode.info.address) + val request = SubscribeRequest(false, network.myAddress) + val response = services.networkService.sendRequest(SUBSCRIPTION_TOPIC, request, mapServiceNode.network.myAddress) mockNet.runNetwork() assertThat(response.getOrThrow().confirmed).isTrue() } private fun MockNode.ackUpdate(mapVersion: Int) { val request = UpdateAcknowledge(mapVersion, services.networkService.myAddress) - services.networkService.send(PUSH_ACK_TOPIC, DEFAULT_SESSION_ID, request, mapServiceNode.info.address) + services.networkService.send(PUSH_ACK_TOPIC, DEFAULT_SESSION_ID, request, mapServiceNode.network.myAddress) mockNet.runNetwork() } private fun addNewNodeToNetworkMap(legalName: X500Name): MockNode { - val node = mockNet.createNode(networkMapAddress = mapServiceNode.info.address, legalName = legalName) + val node = mockNet.createNode(networkMapAddress = mapServiceNode.network.myAddress, legalName = legalName) mockNet.runNetwork() lastSerial = System.currentTimeMillis() return node diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt index 8eed029284..053d85bfaa 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt @@ -5,11 +5,9 @@ import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService -import net.corda.core.utilities.* -import net.corda.flows.TxKeyFlow +import net.corda.flows.AnonymisedIdentity import net.corda.node.services.identity.InMemoryIdentityService -import net.corda.testing.ALICE_PUBKEY -import net.corda.testing.BOB_PUBKEY +import net.corda.testing.* import org.bouncycastle.asn1.x500.X500Name import org.junit.Test import java.security.cert.CertificateFactory @@ -28,13 +26,13 @@ class InMemoryIdentityServiceTests { assertNull(service.getAllIdentities().firstOrNull()) service.registerIdentity(ALICE_IDENTITY) - var expected = setOf(ALICE) + var expected = setOf(ALICE) var actual = service.getAllIdentities().map { it.party }.toHashSet() assertEquals(expected, actual) // Add a second party and check we get both back service.registerIdentity(BOB_IDENTITY) - expected = setOf(ALICE, BOB) + expected = setOf(ALICE, BOB) actual = service.getAllIdentities().map { it.party }.toHashSet() assertEquals(expected, actual) } @@ -136,14 +134,14 @@ class InMemoryIdentityServiceTests { } } - private fun createParty(x500Name: X500Name, ca: CertificateAndKeyPair): Pair { + private fun createParty(x500Name: X500Name, ca: CertificateAndKeyPair): Pair { val certFactory = CertificateFactory.getInstance("X509") val issuerKeyPair = generateKeyPair() val issuer = getTestPartyAndCertificate(x500Name, issuerKeyPair.public, ca) val txKey = Crypto.generateKeyPair() val txCert = X509Utilities.createCertificate(CertificateType.IDENTITY, issuer.certificate, issuerKeyPair, x500Name, txKey.public) val txCertPath = certFactory.generateCertPath(listOf(txCert.cert) + issuer.certPath.certificates) - return Pair(issuer, TxKeyFlow.AnonymousIdentity(txCertPath, txCert, AnonymousParty(txKey.public))) + return Pair(issuer, AnonymisedIdentity(txCertPath, txCert, AnonymousParty(txKey.public))) } /** diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt index a341420963..948228acb8 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt @@ -1,11 +1,13 @@ package net.corda.node.services.network import net.corda.core.getOrThrow +import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB +import net.corda.testing.ALICE +import net.corda.testing.BOB import net.corda.node.utilities.transaction import net.corda.testing.node.MockNetwork +import org.junit.After import org.junit.Test import java.math.BigInteger import kotlin.test.assertEquals @@ -13,10 +15,15 @@ import kotlin.test.assertEquals class InMemoryNetworkMapCacheTest { private val mockNet = MockNetwork() + @After + fun teardown() { + mockNet.stopNodes() + } + @Test fun registerWithNetwork() { val (n0, n1) = mockNet.createTwoNodes() - val future = n1.services.networkMapCache.addMapService(n1.network, n0.info.address, false, null) + val future = n1.services.networkMapCache.addMapService(n1.network, n0.network.myAddress, false, null) mockNet.runNetwork() future.getOrThrow() } @@ -28,13 +35,28 @@ class InMemoryNetworkMapCacheTest { val nodeB = mockNet.createNode(null, -1, MockNetwork.DefaultFactory, true, BOB.name, null, entropy, ServiceInfo(NetworkMapService.type)) assertEquals(nodeA.info.legalIdentity, nodeB.info.legalIdentity) + mockNet.runNetwork() + // Node A currently knows only about itself, so this returns node A - assertEquals(nodeA.netMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeA.info) + assertEquals(nodeA.services.networkMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeA.info) nodeA.database.transaction { - nodeA.netMapCache.addNode(nodeB.info) + nodeA.services.networkMapCache.addNode(nodeB.info) } // The details of node B write over those for node A - assertEquals(nodeA.netMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeB.info) + assertEquals(nodeA.services.networkMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeB.info) + } + + @Test + fun `getNodeByLegalIdentity`() { + val (n0, n1) = mockNet.createTwoNodes() + val node0Cache: NetworkMapCache = n0.services.networkMapCache + val expected = n1.info + + mockNet.runNetwork() + val actual = node0Cache.getNodeByLegalIdentity(n1.info.legalIdentity) + assertEquals(expected, actual) + + // TODO: Should have a test case with anonymous lookup } } diff --git a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt index 2a3a8fd3b2..c5af8af7ae 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapServiceTest.kt @@ -5,7 +5,6 @@ import net.corda.core.node.services.ServiceInfo import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.config.NodeConfiguration import net.corda.node.utilities.transaction -import net.corda.testing.MOCK_VERSION_INFO import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode import java.math.BigInteger @@ -49,11 +48,11 @@ class PersistentNetworkMapServiceTest : AbstractNetworkMapServiceTest() { - val lazyTime by lazy { serviceHub.clock.instant() } - @Suspendable - override fun call() = Unit - } + val flow = LazyServiceHubAccessFlow() node1.services.startFlow(flow) assertThat(flow.lazyTime).isNotNull() } @@ -148,7 +149,7 @@ class FlowFrameworkTests { @Test fun `flow added before network map does run after init`() { - val node3 = mockNet.createNode(node1.info.address) //create vanilla node + val node3 = mockNet.createNode(node1.network.myAddress) //create vanilla node val flow = NoOpFlow() node3.services.startFlow(flow) assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet @@ -158,14 +159,14 @@ class FlowFrameworkTests { @Test fun `flow added before network map will be init checkpointed`() { - var node3 = mockNet.createNode(node1.info.address) //create vanilla node + var node3 = mockNet.createNode(node1.network.myAddress) //create vanilla node val flow = NoOpFlow() node3.services.startFlow(flow) assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet node3.disableDBCloseOnStop() node3.stop() - node3 = mockNet.createNode(node1.info.address, forcedID = node3.id) + node3 = mockNet.createNode(node1.network.myAddress, forcedID = node3.id) val restoredFlow = node3.getSingleFlow().first assertEquals(false, restoredFlow.flowStarted) // Not started yet as no network activity has been allowed yet mockNet.runNetwork() // Allow network map messages to flow @@ -175,7 +176,7 @@ class FlowFrameworkTests { node3.stop() // Now it is completed the flow should leave no Checkpoint. - node3 = mockNet.createNode(node1.info.address, forcedID = node3.id) + node3 = mockNet.createNode(node1.network.myAddress, forcedID = node3.id) mockNet.runNetwork() // Allow network map messages to flow node3.smm.executor.flush() assertTrue(node3.smm.findStateMachines(NoOpFlow::class.java).isEmpty()) @@ -201,7 +202,7 @@ class FlowFrameworkTests { var sentCount = 0 mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ } - val node3 = mockNet.createNode(node1.info.address) + val node3 = mockNet.createNode(node1.network.myAddress) val secondFlow = node3.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } mockNet.runNetwork() @@ -218,7 +219,7 @@ class FlowFrameworkTests { node2.database.transaction { assertEquals(1, node2.checkpointStorage.checkpoints().size) // confirm checkpoint } - val node2b = mockNet.createNode(node1.info.address, node2.id, advertisedServices = *node2.advertisedServices.toTypedArray()) + val node2b = mockNet.createNode(node1.network.myAddress, node2.id, advertisedServices = *node2.advertisedServices.toTypedArray()) node2.manuallyCloseDB() val (firstAgain, fut1) = node2b.getSingleFlow() // Run the network which will also fire up the second flow. First message should get deduped. So message data stays in sync. @@ -245,7 +246,7 @@ class FlowFrameworkTests { @Test fun `sending to multiple parties`() { - val node3 = mockNet.createNode(node1.info.address) + val node3 = mockNet.createNode(node1.network.myAddress) mockNet.runNetwork() node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() } node3.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() } @@ -277,7 +278,7 @@ class FlowFrameworkTests { @Test fun `receiving from multiple parties`() { - val node3 = mockNet.createNode(node1.info.address) + val node3 = mockNet.createNode(node1.network.myAddress) mockNet.runNetwork() val node2Payload = "Test 1" val node3Payload = "Test 2" @@ -328,10 +329,11 @@ class FlowFrameworkTests { 2000.DOLLARS, OpaqueBytes.of(0x01), node1.info.legalIdentity, - notary1.info.notaryIdentity)) + notary1.info.notaryIdentity, + anonymous = false)) // We pay a couple of times, the notary picking should go round robin for (i in 1..3) { - node1.services.startFlow(CashPaymentFlow(500.DOLLARS, node2.info.legalIdentity)) + node1.services.startFlow(CashPaymentFlow(500.DOLLARS, node2.info.legalIdentity, anonymous = false)) mockNet.runNetwork() } val endpoint = mockNet.messagingNetwork.endpoint(notary1.network.myAddress as InMemoryMessagingNetwork.PeerHandle)!! @@ -457,7 +459,7 @@ class FlowFrameworkTests { @Test fun `FlowException propagated in invocation chain`() { - val node3 = mockNet.createNode(node1.info.address) + val node3 = mockNet.createNode(node1.network.myAddress) mockNet.runNetwork() node3.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } @@ -471,7 +473,7 @@ class FlowFrameworkTests { @Test fun `FlowException thrown and there is a 3rd unrelated party flow`() { - val node3 = mockNet.createNode(node1.info.address) + val node3 = mockNet.createNode(node1.network.myAddress) mockNet.runNetwork() // Node 2 will send its payload and then block waiting for the receive from node 1. Meanwhile node 1 will move @@ -597,6 +599,21 @@ class FlowFrameworkTests { assertThatThrownBy { result.getOrThrow() }.hasMessageContaining("Vault").hasMessageContaining("private method") } + @Test + fun `verify vault query service is tokenizable by force checkpointing within a flow`() { + val ptx = TransactionBuilder(notary = notary1.info.notaryIdentity) + ptx.addOutputState(DummyState()) + val stx = node1.services.signInitialTransaction(ptx) + + node1.registerFlowFactory(VaultQueryFlow::class) { + WaitingFlows.Committer(it) + } + val result = node2.services.startFlow(VaultQueryFlow(stx, node1.info.legalIdentity)).resultFuture + + mockNet.runNetwork() + assertThat(result.getOrThrow()).isEmpty() + } + @Test fun `customised client flow`() { val receiveFlowFuture = node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it) } @@ -660,7 +677,7 @@ class FlowFrameworkTests { private inline fun > MockNode.restartAndGetRestoredFlow(networkMapNode: MockNode? = null): P { disableDBCloseOnStop() // Handover DB to new node copy stop() - val newNode = mockNet.createNode(networkMapNode?.info?.address, id, advertisedServices = *advertisedServices.toTypedArray()) + val newNode = mockNet.createNode(networkMapNode?.network?.myAddress, id, advertisedServices = *advertisedServices.toTypedArray()) newNode.acceptableLiveFiberCountOnStop = 1 manuallyCloseDB() mockNet.runNetwork() // allow NetworkMapService messages to stabilise and thus start the state machine @@ -735,6 +752,12 @@ class FlowFrameworkTests { .toFuture() } + private class LazyServiceHubAccessFlow : FlowLogic() { + val lazyTime: Instant by lazy { serviceHub.clock.instant() } + @Suspendable + override fun call() = Unit + } + private class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic() { @Transient var flowStarted = false @@ -863,6 +886,19 @@ class FlowFrameworkTests { } } + @InitiatingFlow + private class VaultQueryFlow(val stx: SignedTransaction, val otherParty: Party) : FlowLogic>>() { + @Suspendable + override fun call(): List> { + send(otherParty, stx) + // hold onto reference here to force checkpoint of vaultQueryService and thus + // prove it is registered as a tokenizableService in the node + val vaultQuerySvc = serviceHub.vaultQueryService + waitForLedgerCommit(stx.id) + return vaultQuerySvc.queryBy().states + } + } + @InitiatingFlow(version = 2) private class UpgradedFlow(val otherParty: Party) : FlowLogic() { @Suspendable diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt index 0dd0af75dc..5fdee29f25 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt @@ -1,6 +1,6 @@ package net.corda.node.services.transactions -import com.google.common.net.HostAndPort +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.transactions.BFTSMaRtConfig.Companion.portIsClaimedFormat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test @@ -28,7 +28,7 @@ class BFTSMaRtConfigTests { @Test fun `overlapping port ranges are rejected`() { - fun addresses(vararg ports: Int) = ports.map { HostAndPort.fromParts("localhost", it) } + fun addresses(vararg ports: Int) = ports.map { NetworkHostAndPort("localhost", it) } assertThatThrownBy { BFTSMaRtConfig(addresses(11000, 11001)).use {} } .isInstanceOf(IllegalArgumentException::class.java) .hasMessage(portIsClaimedFormat.format("localhost:11001", setOf("localhost:11000", "localhost:11001"))) diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt index 3500211147..7895f4defc 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt @@ -1,6 +1,5 @@ package net.corda.node.services.transactions -import com.google.common.net.HostAndPort import io.atomix.catalyst.transport.Address import io.atomix.copycat.client.ConnectionStrategies import io.atomix.copycat.client.CopycatClient @@ -8,9 +7,9 @@ import io.atomix.copycat.server.CopycatServer import io.atomix.copycat.server.storage.Storage import io.atomix.copycat.server.storage.StorageLevel import net.corda.core.getOrThrow -import net.corda.core.utilities.LogHelper +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.testing.LogHelper import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.transactions.DistributedImmutableMap import net.corda.node.utilities.configureDatabase import net.corda.testing.freeLocalHostAndPort import net.corda.testing.node.makeTestDataSourceProperties @@ -88,7 +87,7 @@ class DistributedImmutableMapTests { return cluster.map { it.getOrThrow() } } - private fun createReplica(myAddress: HostAndPort, clusterAddress: HostAndPort? = null): CompletableFuture { + private fun createReplica(myAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): CompletableFuture { val storage = Storage.builder().withStorageLevel(StorageLevel.MEMORY).build() val address = Address(myAddress.host, myAddress.port) diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/InMemoryUniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/InMemoryUniquenessProviderTests.kt deleted file mode 100644 index 8634d885be..0000000000 --- a/node/src/test/kotlin/net/corda/node/services/transactions/InMemoryUniquenessProviderTests.kt +++ /dev/null @@ -1,36 +0,0 @@ -package net.corda.node.services.transactions - -import net.corda.core.crypto.SecureHash -import net.corda.core.node.services.UniquenessException -import net.corda.testing.MEGA_CORP -import net.corda.testing.generateStateRef -import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -class InMemoryUniquenessProviderTests { - val identity = MEGA_CORP - val txID = SecureHash.randomSHA256() - - @Test fun `should commit a transaction with unused inputs without exception`() { - val provider = InMemoryUniquenessProvider() - val inputState = generateStateRef() - - provider.commit(listOf(inputState), txID, identity) - } - - @Test fun `should report a conflict for a transaction with previously used inputs`() { - val provider = InMemoryUniquenessProvider() - val inputState = generateStateRef() - - val inputs = listOf(inputState) - provider.commit(inputs, txID, identity) - - val ex = assertFailsWith { provider.commit(inputs, txID, identity) } - - val consumingTx = ex.error.stateHistory[inputState]!! - assertEquals(consumingTx.id, txID) - assertEquals(consumingTx.inputIndex, inputs.indexOf(inputState)) - assertEquals(consumingTx.requestingParty, identity) - } -} diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index 039ca933b6..c0da57821c 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -1,23 +1,24 @@ package net.corda.node.services.transactions import com.google.common.util.concurrent.ListenableFuture -import net.corda.core.contracts.DummyContract import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.DigitalSignature import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.flows.NotaryFlow import net.corda.node.internal.AbstractNode import net.corda.node.services.network.NetworkMapService +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat +import org.junit.After import org.junit.Before import org.junit.Test import java.time.Instant @@ -30,20 +31,27 @@ class NotaryServiceTests { lateinit var notaryNode: MockNetwork.MockNode lateinit var clientNode: MockNetwork.MockNode - @Before fun setup() { + @Before + fun setup() { mockNet = MockNetwork() notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) - clientNode = mockNet.createNode(networkMapAddress = notaryNode.info.address) + clientNode = mockNet.createNode(networkMapAddress = notaryNode.network.myAddress) mockNet.runNetwork() // Clear network map registration messages } - @Test fun `should sign a unique transaction with a valid time-window`() { + @After + fun cleanUp() { + mockNet.stopNodes() + } + + @Test + fun `should sign a unique transaction with a valid time-window`() { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.addTimeWindow(Instant.now(), 30.seconds) + tx.setTimeWindow(Instant.now(), 30.seconds) clientNode.services.signInitialTransaction(tx) } @@ -52,7 +60,8 @@ class NotaryServiceTests { signatures.forEach { it.verify(stx.id) } } - @Test fun `should sign a unique transaction without a time-window`() { + @Test + fun `should sign a unique transaction without a time-window`() { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) @@ -64,11 +73,12 @@ class NotaryServiceTests { signatures.forEach { it.verify(stx.id) } } - @Test fun `should report error for transaction with an invalid time-window`() { + @Test + fun `should report error for transaction with an invalid time-window`() { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.addTimeWindow(Instant.now().plusSeconds(3600), 30.seconds) + tx.setTimeWindow(Instant.now().plusSeconds(3600), 30.seconds) clientNode.services.signInitialTransaction(tx) } @@ -78,7 +88,8 @@ class NotaryServiceTests { assertThat(ex.error).isInstanceOf(NotaryError.TimeWindowInvalid::class.java) } - @Test fun `should sign identical transaction multiple times (signing is idempotent)`() { + @Test + fun `should sign identical transaction multiple times (signing is idempotent)`() { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) @@ -95,7 +106,8 @@ class NotaryServiceTests { assertEquals(f1.resultFuture.getOrThrow(), f2.resultFuture.getOrThrow()) } - @Test fun `should report conflict when inputs are reused across transactions`() { + @Test + fun `should report conflict when inputs are reused across transactions`() { val inputState = issueState(clientNode) val stx = run { val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) @@ -131,7 +143,7 @@ class NotaryServiceTests { val tx = DummyContract.generateInitial(Random().nextInt(), notaryNode.info.notaryIdentity, node.info.legalIdentity.ref(0)) val signedByNode = node.services.signInitialTransaction(tx) val stx = notaryNode.services.addSignature(signedByNode, notaryNode.services.notaryIdentityKey) - node.services.recordTransactions(listOf(stx)) + node.services.recordTransactions(stx) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt index 4f14caa074..1ff8c103f3 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt @@ -2,8 +2,7 @@ package net.corda.node.services.transactions import net.corda.core.crypto.SecureHash import net.corda.core.node.services.UniquenessException -import net.corda.core.utilities.LogHelper -import net.corda.node.services.transactions.PersistentUniquenessProvider +import net.corda.testing.LogHelper import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.testing.MEGA_CORP diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index aa8a417d81..3072677dfd 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -1,21 +1,26 @@ package net.corda.node.services.transactions import com.google.common.util.concurrent.ListenableFuture -import net.corda.core.contracts.* +import net.corda.core.contracts.Command +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionType +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.DigitalSignature import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.flows.NotaryFlow import net.corda.node.internal.AbstractNode import net.corda.node.services.issueInvalidState import net.corda.node.services.network.NetworkMapService +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.MEGA_CORP_KEY import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat +import org.junit.After import org.junit.Before import org.junit.Test import java.util.* @@ -27,17 +32,24 @@ class ValidatingNotaryServiceTests { lateinit var notaryNode: MockNetwork.MockNode lateinit var clientNode: MockNetwork.MockNode - @Before fun setup() { + @Before + fun setup() { mockNet = MockNetwork() notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type)) ) - clientNode = mockNet.createNode(networkMapAddress = notaryNode.info.address) + clientNode = mockNet.createNode(networkMapAddress = notaryNode.network.myAddress) mockNet.runNetwork() // Clear network map registration messages } - @Test fun `should report error for invalid transaction dependency`() { + @After + fun cleanUp() { + mockNet.stopNodes() + } + + @Test + fun `should report error for invalid transaction dependency`() { val stx = run { val inputState = issueInvalidState(clientNode, notaryNode.info.notaryIdentity) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) @@ -50,7 +62,8 @@ class ValidatingNotaryServiceTests { assertThat(ex.error).isInstanceOf(NotaryError.SignaturesInvalid::class.java) } - @Test fun `should report error for missing signatures`() { + @Test + fun `should report error for missing signatures`() { val expectedMissingKey = MEGA_CORP_KEY.public val stx = run { val inputState = issueState(clientNode) @@ -82,7 +95,7 @@ class ValidatingNotaryServiceTests { val tx = DummyContract.generateInitial(Random().nextInt(), notaryNode.info.notaryIdentity, node.info.legalIdentity.ref(0)) val signedByNode = node.services.signInitialTransaction(tx) val stx = notaryNode.services.addSignature(signedByNode, notaryNode.services.notaryIdentityKey) - node.services.recordTransactions(listOf(stx)) + node.services.recordTransactions(stx) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index a4164e3008..2e100edf28 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -2,23 +2,20 @@ package net.corda.node.services.vault import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.contracts.testing.fillWithSomeTestCash import net.corda.core.contracts.* +import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.StatesNotAvailableException -import net.corda.core.node.services.TxWritableStorageService +import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.node.services.unconsumedStates -import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.LogHelper +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction -import net.corda.testing.BOC -import net.corda.testing.BOC_KEY -import net.corda.testing.MEGA_CORP -import net.corda.testing.MEGA_CORP_KEY +import net.corda.testing.* +import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties import org.assertj.core.api.Assertions.assertThat @@ -32,7 +29,9 @@ import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNull +import kotlin.test.assertTrue class NodeVaultServiceTest { lateinit var services: MockServices @@ -53,7 +52,7 @@ class NodeVaultServiceTest { override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) @@ -77,17 +76,12 @@ class NodeVaultServiceTest { val w1 = vaultSvc.unconsumedStates() assertThat(w1).hasSize(3) - val originalStorage = services.storageService val originalVault = vaultSvc val services2 = object : MockServices() { override val vaultService: VaultService get() = originalVault - - // We need to be able to find the same transactions as before, too. - override val storageService: TxWritableStorageService get() = originalStorage - override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) vaultService.notify(stx.tx) } } @@ -400,17 +394,18 @@ class NodeVaultServiceTest { @Test fun addNoteToTransaction() { - database.transaction { + val megaCorpServices = MockServices(MEGA_CORP_KEY) + database.transaction { val freshKey = services.legalIdentityKey // Issue a txn to Send us some Money - val usefulTX = TransactionType.General.Builder(null).apply { + val usefulBuilder = TransactionType.General.Builder(null).apply { Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) - signWith(MEGA_CORP_KEY) - }.toSignedTransaction() + } + val usefulTX = megaCorpServices.signInitialTransaction(usefulBuilder) - services.recordTransactions(listOf(usefulTX)) + services.recordTransactions(usefulTX) vaultSvc.addNoteToTransaction(usefulTX.id, "USD Sample Note 1") vaultSvc.addNoteToTransaction(usefulTX.id, "USD Sample Note 2") @@ -418,15 +413,65 @@ class NodeVaultServiceTest { assertEquals(3, vaultSvc.getTransactionNotes(usefulTX.id).count()) // Issue more Money (GBP) - val anotherTX = TransactionType.General.Builder(null).apply { + val anotherBuilder = TransactionType.General.Builder(null).apply { Cash().generateIssue(this, 200.POUNDS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) - signWith(MEGA_CORP_KEY) - }.toSignedTransaction() + } + val anotherTX = megaCorpServices.signInitialTransaction(anotherBuilder) - services.recordTransactions(listOf(anotherTX)) + services.recordTransactions(anotherTX) - vaultSvc.addNoteToTransaction(anotherTX.id, "GPB Sample Note 1") + vaultSvc.addNoteToTransaction(anotherTX.id, "GBP Sample Note 1") assertEquals(1, vaultSvc.getTransactionNotes(anotherTX.id).count()) } } + + @Test + fun `is ownable state relevant`() { + val service = (services.vaultService as NodeVaultService) + val amount = Amount(1000, Issued(BOC.ref(1), GBP)) + val wellKnownCash = Cash.State(amount, services.myInfo.legalIdentity) + assertTrue { service.isRelevant(wellKnownCash, services.keyManagementService.keys) } + + val anonymousIdentity = services.keyManagementService.freshKeyAndCert(services.myInfo.legalIdentityAndCert, false) + val anonymousCash = Cash.State(amount, anonymousIdentity.identity) + assertTrue { service.isRelevant(anonymousCash, services.keyManagementService.keys) } + + val thirdPartyIdentity = AnonymousParty(generateKeyPair().public) + val thirdPartyCash = Cash.State(amount, thirdPartyIdentity) + assertFalse { service.isRelevant(thirdPartyCash, services.keyManagementService.keys) } + } + + // TODO: Unit test linear state relevancy checks + + @Test + fun `make update`() { + val service = (services.vaultService as NodeVaultService) + val anonymousIdentity = services.keyManagementService.freshKeyAndCert(services.myInfo.legalIdentityAndCert, false) + val thirdPartyIdentity = AnonymousParty(generateKeyPair().public) + val amount = Amount(1000, Issued(BOC.ref(1), GBP)) + + // Issue then move some cash + val issueTx = TransactionBuilder(TransactionType.General, services.myInfo.legalIdentity).apply { + Cash().generateIssue(this, + amount, anonymousIdentity.identity, services.myInfo.legalIdentity) + }.toWireTransaction() + val cashState = StateAndRef(issueTx.outputs.single(), StateRef(issueTx.id, 0)) + + database.transaction { + val expected = Vault.Update(emptySet(), setOf(cashState), null) + val actual = service.makeUpdate(issueTx, setOf(anonymousIdentity.identity.owningKey)) + assertEquals(expected, actual) + services.vaultService.notify(issueTx) + } + + database.transaction { + val moveTx = TransactionBuilder(TransactionType.General, services.myInfo.legalIdentity).apply { + services.vaultService.generateSpend(this, Amount(1000, GBP), thirdPartyIdentity) + }.toWireTransaction() + + val expected = Vault.Update(setOf(cashState), emptySet(), null) + val actual = service.makeUpdate(moveTx, setOf(anonymousIdentity.identity.owningKey)) + assertEquals(expected, actual) + } + } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 6db977b677..587e95081a 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -3,27 +3,22 @@ package net.corda.node.services.vault import net.corda.contracts.CommercialPaper import net.corda.contracts.Commodity import net.corda.contracts.DealState -import net.corda.contracts.DummyDealContract import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.contracts.testing.* import net.corda.core.contracts.* import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.toBase58String import net.corda.core.days import net.corda.core.identity.Party import net.corda.core.node.services.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* -import net.corda.core.schemas.DummyLinearStateSchemaV1 import net.corda.core.seconds -import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY -import net.corda.core.utilities.TEST_TX_TIME +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.toHexString import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1 import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.schemas.CashSchemaV1 @@ -31,17 +26,17 @@ import net.corda.schemas.CashSchemaV1.PersistentCashState import net.corda.schemas.CommercialPaperSchemaV1 import net.corda.schemas.SampleCashSchemaV3 import net.corda.testing.* +import net.corda.testing.contracts.* import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties +import net.corda.testing.schemas.DummyLinearStateSchemaV1 import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.asn1.x500.X500Name import org.jetbrains.exposed.sql.Database -import org.junit.After -import org.junit.Before -import org.junit.Ignore -import org.junit.Test +import org.junit.* +import org.junit.rules.ExpectedException import java.io.Closeable import java.lang.Thread.sleep import java.math.BigInteger @@ -51,7 +46,6 @@ import java.time.LocalDate import java.time.ZoneOffset import java.time.temporal.ChronoUnit import java.util.* -import kotlin.test.assertFails class VaultQueryTests { @@ -75,7 +69,7 @@ class VaultQueryTests { override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) @@ -93,9 +87,9 @@ class VaultQueryTests { /** * Helper method for generating a Persistent H2 test database */ - @Ignore //@Test + @Ignore + @Test fun createPersistentTestDb() { - val dataSourceAndDatabase = configureDatabase(makePersistentDataSourceProperties()) val dataSource = dataSourceAndDatabase.first val database = dataSourceAndDatabase.second @@ -190,6 +184,30 @@ class VaultQueryTests { } } + @Test + fun `unconsumed states with count`() { + database.transaction { + + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) + val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) + assertThat(resultsBeforeConsume.states).hasSize(4) + assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(4) + + services.consumeCash(75.DOLLARS) + + val consumedCriteria = VaultQueryCriteria(status = Vault.StateStatus.UNCONSUMED) + val resultsAfterConsume = vaultQuerySvc.queryBy(consumedCriteria, paging) + assertThat(resultsAfterConsume.states).hasSize(1) + assertThat(resultsAfterConsume.totalStatesAvailable).isEqualTo(1) + } + } + @Test fun `unconsumed cash states simple`() { database.transaction { @@ -222,21 +240,80 @@ class VaultQueryTests { } @Test - fun `unconsumed states for state refs`() { + fun `unconsumed cash states sorted by state ref`() { + database.transaction { + var stateRefs : MutableList = mutableListOf() + + val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)) + val issuedStateRefs = issuedStates.states.map { it.ref }.toList() + stateRefs.addAll(issuedStateRefs) + + val spentStates = services.consumeCash(25.DOLLARS) + var spentStateRefs = spentStates.states.map { it.ref }.toList() + stateRefs.addAll(spentStateRefs) + + val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) + val criteria = VaultQueryCriteria() + val results = vaultQuerySvc.queryBy(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))) + + // default StateRef sort is by index then txnId: + // order by + // vaultschem1_.output_index, + // vaultschem1_.transaction_id asc + assertThat(results.states).hasSize(8) // -3 CONSUMED + 1 NEW UNCONSUMED (change) + + val sortedStateRefs = stateRefs.sortedBy { it.index } + + assertThat(results.states.first().ref.index).isEqualTo(sortedStateRefs.first().index) // 0 + assertThat(results.states.last().ref.index).isEqualTo(sortedStateRefs.last().index) // 1 + } + } + + @Test + fun `unconsumed cash states sorted by state ref txnId and index`() { + database.transaction { + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)) + services.consumeCash(10.DOLLARS) + services.consumeCash(10.DOLLARS) + + val sortAttributeTxnId = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID) + val sortAttributeIndex = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_INDEX) + val sortBy = Sort(setOf(Sort.SortColumn(sortAttributeTxnId, Sort.Direction.ASC), + Sort.SortColumn(sortAttributeIndex, Sort.Direction.ASC))) + val criteria = VaultQueryCriteria() + val results = vaultQuerySvc.queryBy(criteria, sortBy) + + results.statesMetadata.forEach { + println(" ${it.ref}") + } + + // explicit sort order asc by txnId and then index: + // order by + // vaultschem1_.transaction_id asc, + // vaultschem1_.output_index asc + assertThat(results.states).hasSize(9) // -2 CONSUMED + 1 NEW UNCONSUMED (change) + } + } + + @Test + fun `unconsumed states for state refs`() { database.transaction { services.fillWithSomeTestLinearStates(8) val issuedStates = services.fillWithSomeTestLinearStates(2) val stateRefs = issuedStates.states.map { it.ref }.toList() // DOCSTART VaultQueryExample2 + val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID) val criteria = VaultQueryCriteria(stateRefs = listOf(stateRefs.first(), stateRefs.last())) - val results = vaultQuerySvc.queryBy(criteria) + val results = vaultQuerySvc.queryBy(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))) // DOCEND VaultQueryExample2 assertThat(results.states).hasSize(2) - assertThat(results.states.first().ref).isEqualTo(issuedStates.states.first().ref) - assertThat(results.states.last().ref).isEqualTo(issuedStates.states.last().ref) + + val sortedStateRefs = stateRefs.sortedBy { it.txhash.bytes.toHexString() } + assertThat(results.states.first().ref).isEqualTo(sortedStateRefs.first()) + assertThat(results.states.last().ref).isEqualTo(sortedStateRefs.last()) } } @@ -275,6 +352,30 @@ class VaultQueryTests { } } + @Test + fun `consumed states with count`() { + database.transaction { + + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(25.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) + val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) + assertThat(resultsBeforeConsume.states).hasSize(4) + assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(4) + + services.consumeCash(75.DOLLARS) + + val consumedCriteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) + val resultsAfterConsume = vaultQuerySvc.queryBy(consumedCriteria, paging) + assertThat(resultsAfterConsume.states).hasSize(3) + assertThat(resultsAfterConsume.totalStatesAvailable).isEqualTo(3) + } + } + @Test fun `all states`() { database.transaction { @@ -293,6 +394,25 @@ class VaultQueryTests { } } + @Test + fun `all states with count`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) + val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) + assertThat(resultsBeforeConsume.states).hasSize(1) + assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(1) + + services.consumeCash(50.DOLLARS) // consumed 100 (spent), produced 50 (change) + + val resultsAfterConsume = vaultQuerySvc.queryBy(criteria, paging) + assertThat(resultsAfterConsume.states).hasSize(2) + assertThat(resultsAfterConsume.totalStatesAvailable).isEqualTo(2) + } + } val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) } val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public) @@ -555,6 +675,143 @@ class VaultQueryTests { } } + @Test + fun `aggregate functions without group clause`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L)) + services.fillWithSomeTestCash(300.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L)) + services.fillWithSomeTestCash(500.SWISS_FRANCS, DUMMY_NOTARY, 5, 5, Random(0L)) + + // DOCSTART VaultQueryExample21 + val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum() } + val sumCriteria = VaultCustomQueryCriteria(sum) + + val count = builder { CashSchemaV1.PersistentCashState::pennies.count() } + val countCriteria = VaultCustomQueryCriteria(count) + + val max = builder { CashSchemaV1.PersistentCashState::pennies.max() } + val maxCriteria = VaultCustomQueryCriteria(max) + + val min = builder { CashSchemaV1.PersistentCashState::pennies.min() } + val minCriteria = VaultCustomQueryCriteria(min) + + val avg = builder { CashSchemaV1.PersistentCashState::pennies.avg() } + val avgCriteria = VaultCustomQueryCriteria(avg) + + val results = vaultQuerySvc.queryBy>(sumCriteria + .and(countCriteria) + .and(maxCriteria) + .and(minCriteria) + .and(avgCriteria)) + // DOCEND VaultQueryExample21 + + assertThat(results.otherResults).hasSize(5) + assertThat(results.otherResults[0]).isEqualTo(150000L) + assertThat(results.otherResults[1]).isEqualTo(15L) + assertThat(results.otherResults[2]).isEqualTo(11298L) + assertThat(results.otherResults[3]).isEqualTo(8702L) + assertThat(results.otherResults[4]).isEqualTo(10000.0) + } + } + + @Test + fun `aggregate functions with single group clause`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L)) + services.fillWithSomeTestCash(300.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L)) + services.fillWithSomeTestCash(500.SWISS_FRANCS, DUMMY_NOTARY, 5, 5, Random(0L)) + + // DOCSTART VaultQueryExample22 + val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val sumCriteria = VaultCustomQueryCriteria(sum) + + val max = builder { CashSchemaV1.PersistentCashState::pennies.max(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val maxCriteria = VaultCustomQueryCriteria(max) + + val min = builder { CashSchemaV1.PersistentCashState::pennies.min(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val minCriteria = VaultCustomQueryCriteria(min) + + val avg = builder { CashSchemaV1.PersistentCashState::pennies.avg(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val avgCriteria = VaultCustomQueryCriteria(avg) + + val results = vaultQuerySvc.queryBy>(sumCriteria + .and(maxCriteria) + .and(minCriteria) + .and(avgCriteria)) + // DOCEND VaultQueryExample22 + + assertThat(results.otherResults).hasSize(24) + /** CHF */ + assertThat(results.otherResults[0]).isEqualTo(50000L) + assertThat(results.otherResults[1]).isEqualTo("CHF") + assertThat(results.otherResults[2]).isEqualTo(10274L) + assertThat(results.otherResults[3]).isEqualTo("CHF") + assertThat(results.otherResults[4]).isEqualTo(9481L) + assertThat(results.otherResults[5]).isEqualTo("CHF") + assertThat(results.otherResults[6]).isEqualTo(10000.0) + assertThat(results.otherResults[7]).isEqualTo("CHF") + /** GBP */ + assertThat(results.otherResults[8]).isEqualTo(40000L) + assertThat(results.otherResults[9]).isEqualTo("GBP") + assertThat(results.otherResults[10]).isEqualTo(10343L) + assertThat(results.otherResults[11]).isEqualTo("GBP") + assertThat(results.otherResults[12]).isEqualTo(9351L) + assertThat(results.otherResults[13]).isEqualTo("GBP") + assertThat(results.otherResults[14]).isEqualTo(10000.0) + assertThat(results.otherResults[15]).isEqualTo("GBP") + /** USD */ + assertThat(results.otherResults[16]).isEqualTo(60000L) + assertThat(results.otherResults[17]).isEqualTo("USD") + assertThat(results.otherResults[18]).isEqualTo(11298L) + assertThat(results.otherResults[19]).isEqualTo("USD") + assertThat(results.otherResults[20]).isEqualTo(8702L) + assertThat(results.otherResults[21]).isEqualTo("USD") + assertThat(results.otherResults[22]).isEqualTo(10000.0) + assertThat(results.otherResults[23]).isEqualTo("USD") + } + } + + @Test + fun `aggregate functions sum by issuer and currency and sort by aggregate sum`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = DUMMY_CASH_ISSUER) + services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), issuedBy = BOC.ref(1), issuerKey = BOC_KEY) + services.fillWithSomeTestCash(300.POUNDS, DUMMY_NOTARY, 3, 3, Random(0L), issuedBy = DUMMY_CASH_ISSUER) + services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L), issuedBy = BOC.ref(2), issuerKey = BOC_KEY) + + // DOCSTART VaultQueryExample23 + val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::issuerParty, + CashSchemaV1.PersistentCashState::currency), + orderBy = Sort.Direction.DESC) + } + + val results = vaultQuerySvc.queryBy>(VaultCustomQueryCriteria(sum)) + // DOCEND VaultQueryExample23 + + assertThat(results.otherResults).hasSize(12) + + assertThat(results.otherResults[0]).isEqualTo(40000L) + assertThat(results.otherResults[1]).isEqualTo(BOC_PUBKEY.toBase58String()) + assertThat(results.otherResults[2]).isEqualTo("GBP") + assertThat(results.otherResults[3]).isEqualTo(30000L) + assertThat(results.otherResults[4]).isEqualTo(DUMMY_CASH_ISSUER.party.owningKey.toBase58String()) + assertThat(results.otherResults[5]).isEqualTo("GBP") + assertThat(results.otherResults[6]).isEqualTo(20000L) + assertThat(results.otherResults[7]).isEqualTo(BOC_PUBKEY.toBase58String()) + assertThat(results.otherResults[8]).isEqualTo("USD") + assertThat(results.otherResults[9]).isEqualTo(10000L) + assertThat(results.otherResults[10]).isEqualTo(DUMMY_CASH_ISSUER.party.owningKey.toBase58String()) + assertThat(results.otherResults[11]).isEqualTo("USD") + } + } + private val TODAY = LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC) @Test @@ -630,7 +887,7 @@ class VaultQueryTests { // Last page implies we need to perform a row count for the Query first, // and then re-query for a given offset defined by (count - pageSize) - val pagingSpec = PageSpecification(9, 10) + val pagingSpec = PageSpecification(10, 10) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) val results = vaultQuerySvc.queryBy(criteria, paging = pagingSpec) @@ -639,48 +896,54 @@ class VaultQueryTests { } } + @get:Rule + val expectedEx = ExpectedException.none()!! + // pagination: invalid page number - @Test(expected = VaultQueryException::class) + @Test fun `invalid page number`() { + expectedEx.expect(VaultQueryException::class.java) + expectedEx.expectMessage("Page specification: invalid page number") + database.transaction { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 100, 100, Random(0L)) - val pagingSpec = PageSpecification(-1, 10) + val pagingSpec = PageSpecification(0, 10) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultQuerySvc.queryBy(criteria, paging = pagingSpec) - assertThat(results.states).hasSize(10) // should retrieve states 90..99 + vaultQuerySvc.queryBy(criteria, paging = pagingSpec) } } // pagination: invalid page size - @Test(expected = VaultQueryException::class) + @Test fun `invalid page size`() { + expectedEx.expect(VaultQueryException::class.java) + expectedEx.expectMessage("Page specification: invalid page size") + database.transaction { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 100, 100, Random(0L)) - val pagingSpec = PageSpecification(0, MAX_PAGE_SIZE + 1) - + val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE + 1) // overflow = -2147483648 val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) vaultQuerySvc.queryBy(criteria, paging = pagingSpec) - assertFails { } } } - // pagination: out or range request (page number * page size) > total rows available - @Test(expected = VaultQueryException::class) - fun `out of range page request`() { + // pagination not specified but more than DEFAULT_PAGE_SIZE results available (fail-fast test) + @Test + fun `pagination not specified but more than default results available`() { + expectedEx.expect(VaultQueryException::class.java) + expectedEx.expectMessage("Please specify a `PageSpecification`") + database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 100, 100, Random(0L)) - - val pagingSpec = PageSpecification(10, 10) // this requests results 101 .. 110 + services.fillWithSomeTestCash(201.DOLLARS, DUMMY_NOTARY, 201, 201, Random(0L)) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultQuerySvc.queryBy(criteria, paging = pagingSpec) - assertFails { println("Query should throw an exception [${results.states.count()}]") } + vaultQuerySvc.queryBy(criteria) } } @@ -706,9 +969,9 @@ class VaultQueryTests { } assertThat(states).hasSize(20) - assertThat(metadata.first().contractStateClassName).isEqualTo("net.corda.core.contracts.DummyLinearContract\$State") + assertThat(metadata.first().contractStateClassName).isEqualTo("net.corda.testing.contracts.DummyLinearContract\$State") assertThat(metadata.first().status).isEqualTo(Vault.StateStatus.UNCONSUMED) // 0 = UNCONSUMED - assertThat(metadata.last().contractStateClassName).isEqualTo("net.corda.contracts.DummyDealContract\$State") + assertThat(metadata.last().contractStateClassName).isEqualTo("net.corda.contracts.asset.Cash\$State") assertThat(metadata.last().status).isEqualTo(Vault.StateStatus.CONSUMED) // 1 = CONSUMED } } @@ -753,6 +1016,20 @@ class VaultQueryTests { } } + @Test + fun `unconsumed cash fungible assets after spending`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.consumeCash(50.DOLLARS) + // should now have x2 CONSUMED + x2 UNCONSUMED (one spent + one change) + + val results = vaultQuerySvc.queryBy(FungibleAssetQueryCriteria()) + assertThat(results.statesMetadata).hasSize(2) + assertThat(results.states).hasSize(2) + } + } + @Test fun `consumed cash fungible assets`() { database.transaction { @@ -845,9 +1122,9 @@ class VaultQueryTests { // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" // DOCSTART VaultQueryExample9 - val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(linearId)) + val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(linearId), status = Vault.StateStatus.ALL) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria)) + val results = vaultQuerySvc.queryBy(linearStateCriteria and vaultCriteria) // DOCEND VaultQueryExample9 assertThat(results.states).hasSize(4) } @@ -864,7 +1141,7 @@ class VaultQueryTests { services.evolveLinearStates(linearStates) // consume current and produce new state reference // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" - val linearStateCriteria = LinearStateQueryCriteria(linearId = linearStates.map { it.state.data.linearId }) + val linearStateCriteria = LinearStateQueryCriteria(linearId = linearStates.map { it.state.data.linearId }, status = Vault.StateStatus.ALL) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC))) @@ -899,18 +1176,15 @@ class VaultQueryTests { val uid = linearStates.states.first().state.data.linearId services.fillWithSomeTestDeals(listOf("123", "456", "789")) - val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.UNCONSUMED) val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(uid)) val dealStateCriteria = LinearStateQueryCriteria(dealRef = listOf("123", "456", "789")) - val compositeCriteria = vaultCriteria.and(linearStateCriteria).or(dealStateCriteria) + val compositeCriteria = linearStateCriteria or dealStateCriteria val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.DEAL_REFERENCE), Sort.Direction.DESC))) val results = vaultQuerySvc.queryBy(compositeCriteria, sorting = sorting) - results.states.forEach { - if (it.state.data is DummyDealContract.State) - println("${(it.state.data as DealState).ref}, ${it.state.data.linearId}") } - assertThat(results.states).hasSize(4) + assertThat(results.statesMetadata).hasSize(13) + assertThat(results.states).hasSize(13) } } @@ -942,7 +1216,7 @@ class VaultQueryTests { services.evolveLinearState(linearState3) // consume current and produce new state reference // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" - val linearStateCriteria = LinearStateQueryCriteria(linearId = txns.states.map { it.state.data.linearId }) + val linearStateCriteria = LinearStateQueryCriteria(linearId = txns.states.map { it.state.data.linearId }, status = Vault.StateStatus.CONSUMED) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC))) val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria), sorting = sorting) @@ -966,8 +1240,12 @@ class VaultQueryTests { // DOCSTART VaultDeprecatedQueryExample1 val states = vaultSvc.linearHeadsOfType().filter { it.key == linearId } // DOCEND VaultDeprecatedQueryExample1 - assertThat(states).hasSize(1) + + // validate against new query api + val results = vaultQuerySvc.queryBy(LinearStateQueryCriteria(linearId = listOf(linearId))) + assertThat(results.statesMetadata).hasSize(1) + assertThat(results.states).hasSize(1) } } @@ -987,8 +1265,12 @@ class VaultQueryTests { // DOCSTART VaultDeprecatedQueryExample2 val states = vaultSvc.consumedStates().filter { it.state.data.linearId == linearId } // DOCEND VaultDeprecatedQueryExample2 - assertThat(states).hasSize(3) + + // validate against new query api + val results = vaultQuerySvc.queryBy(LinearStateQueryCriteria(linearId = listOf(linearId), status = Vault.StateStatus.CONSUMED)) + assertThat(results.statesMetadata).hasSize(3) + assertThat(results.states).hasSize(3) } } @@ -1009,8 +1291,12 @@ class VaultQueryTests { val states = vaultSvc.states(setOf(DummyLinearContract.State::class.java), EnumSet.of(Vault.StateStatus.CONSUMED, Vault.StateStatus.UNCONSUMED)).filter { it.state.data.linearId == linearId } // DOCEND VaultDeprecatedQueryExample3 - assertThat(states).hasSize(4) + + // validate against new query api + val results = vaultQuerySvc.queryBy(LinearStateQueryCriteria(linearId = listOf(linearId), status = Vault.StateStatus.ALL)) + assertThat(results.statesMetadata).hasSize(4) + assertThat(results.states).hasSize(4) } } @@ -1152,6 +1438,52 @@ class VaultQueryTests { } } + @Test + fun `unconsumed cash balance for single currency`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L)) + + val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val sumCriteria = VaultCustomQueryCriteria(sum) + + val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(USD.currencyCode) } + val ccyCriteria = VaultCustomQueryCriteria(ccyIndex) + + val results = vaultQuerySvc.queryBy>(sumCriteria.and(ccyCriteria)) + + assertThat(results.otherResults).hasSize(2) + assertThat(results.otherResults[0]).isEqualTo(30000L) + assertThat(results.otherResults[1]).isEqualTo("USD") + } + } + + @Test + fun `unconsumed cash balances for all currencies`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L)) + services.fillWithSomeTestCash(300.POUNDS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L)) + services.fillWithSomeTestCash(500.SWISS_FRANCS, DUMMY_NOTARY, 5, 5, Random(0L)) + services.fillWithSomeTestCash(600.SWISS_FRANCS, DUMMY_NOTARY, 6, 6, Random(0L)) + + val ccyIndex = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val criteria = VaultCustomQueryCriteria(ccyIndex) + val results = vaultQuerySvc.queryBy>(criteria) + + assertThat(results.otherResults).hasSize(6) + assertThat(results.otherResults[0]).isEqualTo(110000L) + assertThat(results.otherResults[1]).isEqualTo("CHF") + assertThat(results.otherResults[2]).isEqualTo(70000L) + assertThat(results.otherResults[3]).isEqualTo("GBP") + assertThat(results.otherResults[4]).isEqualTo(30000L) + assertThat(results.otherResults[5]).isEqualTo("USD") + } + } + @Test fun `unconsumed fungible assets for quantity greater than`() { database.transaction { @@ -1217,7 +1549,7 @@ class VaultQueryTests { val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER val commercialPaper = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - addTimeWindow(TEST_TX_TIME, 30.seconds) + setTimeWindow(TEST_TX_TIME, 30.seconds) signWith(MEGA_CORP_KEY) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() @@ -1227,7 +1559,7 @@ class VaultQueryTests { val faceValue2 = 10000.POUNDS `issued by` DUMMY_CASH_ISSUER val commercialPaper2 = CommercialPaper().generateIssue(issuance, faceValue2, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - addTimeWindow(TEST_TX_TIME, 30.seconds) + setTimeWindow(TEST_TX_TIME, 30.seconds) signWith(MEGA_CORP_KEY) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() @@ -1254,7 +1586,7 @@ class VaultQueryTests { val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER val commercialPaper = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - addTimeWindow(TEST_TX_TIME, 30.seconds) + setTimeWindow(TEST_TX_TIME, 30.seconds) signWith(MEGA_CORP_KEY) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() @@ -1264,7 +1596,7 @@ class VaultQueryTests { val faceValue2 = 5000.POUNDS `issued by` DUMMY_CASH_ISSUER val commercialPaper2 = CommercialPaper().generateIssue(issuance, faceValue2, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - addTimeWindow(TEST_TX_TIME, 30.seconds) + setTimeWindow(TEST_TX_TIME, 30.seconds) signWith(MEGA_CORP_KEY) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() @@ -1420,7 +1752,7 @@ class VaultQueryTests { services.fillWithSomeTestLinearStates(1, "TEST2") val uuid = services.fillWithSomeTestLinearStates(1, "TEST3").states.first().state.data.linearId.id - // 2 unconsumed states with same external ID + // 2 unconsumed states with same external ID, 1 with different external ID val results = builder { val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.equal("TEST2") @@ -1429,10 +1761,11 @@ class VaultQueryTests { val uuidCondition = VaultSchemaV1.VaultLinearStates::uuid.equal(uuid) val uuidCustomCriteria = VaultCustomQueryCriteria(uuidCondition) - val criteria = externalIdCustomCriteria.or(uuidCustomCriteria) + val criteria = externalIdCustomCriteria or uuidCustomCriteria vaultQuerySvc.queryBy(criteria) } - assertThat(results.states).hasSize(2) + assertThat(results.statesMetadata).hasSize(3) + assertThat(results.states).hasSize(3) } } @@ -1511,7 +1844,7 @@ class VaultQueryTests { updates } - updates?.expectEvents { + updates.expectEvents { sequence( expect { (consumed, produced, flowId) -> require(flowId == null) {} @@ -1558,7 +1891,7 @@ class VaultQueryTests { updates } - updates?.expectEvents { + updates.expectEvents { sequence( expect { (consumed, produced, flowId) -> require(flowId == null) {} @@ -1605,7 +1938,7 @@ class VaultQueryTests { updates } - updates?.expectEvents { + updates.expectEvents { sequence( expect { (consumed, produced, flowId) -> require(flowId == null) {} @@ -1661,7 +1994,7 @@ class VaultQueryTests { updates } - updates?.expectEvents { + updates.expectEvents { sequence( expect { (consumed, produced, flowId) -> require(flowId == null) {} @@ -1711,7 +2044,7 @@ class VaultQueryTests { updates } - updates?.expectEvents { + updates.expectEvents { sequence( expect { (consumed, produced, flowId) -> require(flowId == null) {} diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 7714a2186b..4ca05bb8e9 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -1,19 +1,22 @@ package net.corda.node.services.vault -import net.corda.contracts.DummyDealContract +import net.corda.testing.contracts.DummyDealContract import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.contracts.testing.* +import net.corda.testing.contracts.fillWithSomeTestCash +import net.corda.testing.contracts.fillWithSomeTestDeals +import net.corda.testing.contracts.fillWithSomeTestLinearStates import net.corda.core.contracts.* +import net.corda.testing.contracts.DummyLinearContract import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.VaultService import net.corda.core.node.services.consumedStates import net.corda.core.node.services.unconsumedStates import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY -import net.corda.core.utilities.LogHelper +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY +import net.corda.testing.LogHelper import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.testing.MEGA_CORP @@ -40,6 +43,7 @@ class VaultWithCashTest { val vault: VaultService get() = services.vaultService lateinit var dataSource: Closeable lateinit var database: Database + val notaryServices = MockServices(DUMMY_NOTARY_KEY) @Before fun setUp() { @@ -54,7 +58,7 @@ class VaultWithCashTest { override fun recordTransactions(txs: Iterable) { for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) @@ -89,32 +93,32 @@ class VaultWithCashTest { @Test fun `issue and spend total correctly and irrelevant ignored`() { + val megaCorpServices = MockServices(MEGA_CORP_KEY) + database.transaction { // A tx that sends us money. val freshKey = services.keyManagementService.freshKey() - val usefulTX = TransactionType.General.Builder(null).apply { - Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) - signWith(MEGA_CORP_KEY) - }.toSignedTransaction() + val usefulBuilder = TransactionType.General.Builder(null) + Cash().generateIssue(usefulBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) + val usefulTX = megaCorpServices.signInitialTransaction(usefulBuilder) assertNull(vault.cashBalances[USD]) services.recordTransactions(usefulTX) // A tx that spends our money. - val spendTXBuilder = TransactionType.General.Builder(DUMMY_NOTARY).apply { - vault.generateSpend(this, 80.DOLLARS, BOB) - signWith(DUMMY_NOTARY_KEY) - } - val spendTX = services.signInitialTransaction(spendTXBuilder, freshKey) + val spendTXBuilder = TransactionType.General.Builder(DUMMY_NOTARY) + vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB) + val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey) + val spendTX = notaryServices.addSignature(spendPTX) assertEquals(100.DOLLARS, vault.cashBalances[USD]) // A tx that doesn't send us anything. - val irrelevantTX = TransactionType.General.Builder(DUMMY_NOTARY).apply { - Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB, DUMMY_NOTARY) - signWith(MEGA_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + val irrelevantBuilder = TransactionType.General.Builder(DUMMY_NOTARY) + Cash().generateIssue(irrelevantBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB, DUMMY_NOTARY) + + val irrelevantPTX = megaCorpServices.signInitialTransaction(irrelevantBuilder) + val irrelevantTX = notaryServices.addSignature(irrelevantPTX) services.recordTransactions(irrelevantTX) assertEquals(100.DOLLARS, vault.cashBalances[USD]) @@ -148,12 +152,10 @@ class VaultWithCashTest { backgroundExecutor.submit { database.transaction { try { - val txn1Builder = - TransactionType.General.Builder(DUMMY_NOTARY).apply { - vault.generateSpend(this, 60.DOLLARS, BOB) - signWith(DUMMY_NOTARY_KEY) - } - val txn1 = services.signInitialTransaction(txn1Builder, freshKey) + val txn1Builder = TransactionType.General.Builder(DUMMY_NOTARY) + vault.generateSpend(txn1Builder, 60.DOLLARS, BOB) + val ptxn1 = notaryServices.signInitialTransaction(txn1Builder) + val txn1 = services.addSignature(ptxn1, freshKey) println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}") println("""txn1 states: UNCONSUMED: ${vault.unconsumedStates().count()} : ${vault.unconsumedStates()}, @@ -180,12 +182,10 @@ class VaultWithCashTest { backgroundExecutor.submit { database.transaction { try { - val txn2Builder = - TransactionType.General.Builder(DUMMY_NOTARY).apply { - vault.generateSpend(this, 80.DOLLARS, BOB) - signWith(DUMMY_NOTARY_KEY) - } - val txn2 = services.signInitialTransaction(txn2Builder, freshKey) + val txn2Builder = TransactionType.General.Builder(DUMMY_NOTARY) + vault.generateSpend(txn2Builder, 80.DOLLARS, BOB) + val ptxn2 = notaryServices.signInitialTransaction(txn2Builder) + val txn2 = services.addSignature(ptxn2, freshKey) println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}") println("""txn2 states: UNCONSUMED: ${vault.unconsumedStates().count()} : ${vault.unconsumedStates()}, @@ -227,9 +227,8 @@ class VaultWithCashTest { val dummyIssueBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) - signWith(DUMMY_NOTARY_KEY) } - val dummyIssue = services.signInitialTransaction(dummyIssueBuilder) + val dummyIssue = notaryServices.signInitialTransaction(dummyIssueBuilder) assertThatThrownBy { dummyIssue.toLedgerTransaction(services).verify() @@ -246,11 +245,10 @@ class VaultWithCashTest { val linearId = UniqueIdentifier() // Issue a linear state - val dummyIssueBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) - signWith(DUMMY_NOTARY_KEY) - } - val dummyIssue = services.signInitialTransaction(dummyIssueBuilder, services.legalIdentityKey) + val dummyIssueBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY) + dummyIssueBuilder.addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) + val dummyIssuePtx = notaryServices.signInitialTransaction(dummyIssueBuilder) + val dummyIssue = services.addSignature(dummyIssuePtx) dummyIssue.toLedgerTransaction(services).verify() @@ -258,11 +256,12 @@ class VaultWithCashTest { assertThat(vault.unconsumedStates()).hasSize(1) // Move the same state - val dummyMove = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { + val dummyMoveBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) addInputState(dummyIssue.tx.outRef(0)) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + } + + val dummyMove = notaryServices.signInitialTransaction(dummyMoveBuilder) dummyIssue.toLedgerTransaction(services).verify() @@ -289,11 +288,10 @@ class VaultWithCashTest { database.transaction { // A tx that spends our money. - val spendTXBuilder = TransactionType.General.Builder(DUMMY_NOTARY).apply { - vault.generateSpend(this, 80.DOLLARS, BOB) - signWith(DUMMY_NOTARY_KEY) - } - val spendTX = services.signInitialTransaction(spendTXBuilder, freshKey) + val spendTXBuilder = TransactionType.General.Builder(DUMMY_NOTARY) + vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB) + val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder) + val spendTX = services.addSignature(spendPTX, freshKey) services.recordTransactions(spendTX) val consumedStates = vault.consumedStates() @@ -320,13 +318,14 @@ class VaultWithCashTest { linearStates.forEach { println(it.state.data.linearId) } // Create a txn consuming different contract types - val dummyMove = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { + val dummyMoveBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { addOutputState(DummyLinearContract.State(participants = listOf(freshIdentity))) addOutputState(DummyDealContract.State(ref = "999", participants = listOf(freshIdentity))) addInputState(linearStates.first()) addInputState(deals.first()) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + } + + val dummyMove = notaryServices.signInitialTransaction(dummyMoveBuilder) dummyMove.toLedgerTransaction(services).verify() services.recordTransactions(dummyMove) diff --git a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt index 72213773fe..8e0c666796 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt @@ -237,4 +237,36 @@ class ObservablesTests { subscription2.unsubscribe() assertThat(unsubscribed).isTrue() } + + @Test + fun `check wrapping in db tx restarts if we pass through zero subscribers`() { + val database = createDatabase() + + val source = PublishSubject.create() + var unsubscribed = false + + val bufferedObservable: Observable = source.doOnUnsubscribe { unsubscribed = true } + val databaseWrappedObservable: Observable = bufferedObservable.wrapWithDatabaseTransaction(database) + + assertThat(unsubscribed).isFalse() + + val subscription1 = databaseWrappedObservable.subscribe { } + val subscription2 = databaseWrappedObservable.subscribe { } + + subscription1.unsubscribe() + assertThat(unsubscribed).isFalse() + + subscription2.unsubscribe() + assertThat(unsubscribed).isTrue() + + val event = SettableFuture.create() + val subscription3 = databaseWrappedObservable.subscribe { event.set(it) } + + source.onNext(1) + + assertThat(event.isDone).isTrue() + assertThat(event.get()).isEqualTo(1) + + subscription3.unsubscribe() + } } \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt index 6e2ea2f3d3..201fb206f4 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt @@ -6,7 +6,7 @@ import com.nhaarman.mockito_kotlin.mock import net.corda.core.crypto.* import net.corda.core.exists import net.corda.core.toTypedArray -import net.corda.core.utilities.ALICE +import net.corda.testing.ALICE import net.corda.testing.getTestX509Name import net.corda.testing.testNodeConfiguration import org.bouncycastle.cert.X509CertificateHolder diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index 936586894e..55be04795d 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -30,15 +30,6 @@ dependencies { compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':test-utils') - - // Javax is required for webapis - compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" - - // GraphStream: For visualisation (required by ExampleClientRPC app) - compile "org.graphstream:gs-core:1.3" - compile("org.graphstream:gs-ui:1.3") { - exclude group: "bouncycastle" - } } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { diff --git a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt index 61f57c93b0..a7e1f16027 100644 --- a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt +++ b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt @@ -3,9 +3,9 @@ package net.corda.attachmentdemo import com.google.common.util.concurrent.Futures import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.driver.driver import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt index 739bebc8f8..22f54033f3 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt @@ -1,7 +1,6 @@ package net.corda.attachmentdemo import co.paralleluniverse.fibers.Suspendable -import com.google.common.net.HostAndPort import joptsimple.OptionParser import net.corda.client.rpc.CordaRPCClient import net.corda.core.contracts.Contract @@ -20,6 +19,8 @@ import net.corda.core.sizedInputStreamAndHash import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.* import net.corda.flows.FinalityFlow +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.driver.poll import java.io.InputStream import java.net.HttpURLConnection @@ -51,14 +52,14 @@ fun main(args: Array) { val role = options.valueOf(roleArg)!! when (role) { Role.SENDER -> { - val host = HostAndPort.fromString("localhost:10006") + val host = NetworkHostAndPort("localhost", 10006) println("Connecting to sender node ($host)") CordaRPCClient(host).start("demo", "demo").use { sender(it.proxy) } } Role.RECIPIENT -> { - val host = HostAndPort.fromString("localhost:10009") + val host = NetworkHostAndPort("localhost", 10009) println("Connecting to the recipient node ($host)") CordaRPCClient(host).start("demo", "demo").use { recipient(it.proxy) @@ -108,11 +109,9 @@ class AttachmentDemoFlow(val otherSide: Party, val hash: SecureHash.SHA256) : Fl ptx.addAttachment(hash) progressTracker.currentStep = SIGNING - // Sign with the notary key - ptx.signWith(DUMMY_NOTARY_KEY) // Send the transaction to the other recipient - val stx = ptx.toSignedTransaction() + val stx = serviceHub.signInitialTransaction(ptx) return subFlow(FinalityFlow(stx, setOf(otherSide))).single() } diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt index bfd97e359a..4528c419c0 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt @@ -2,9 +2,9 @@ package net.corda.attachmentdemo import net.corda.core.div import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.driver.driver diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt index 2887b84435..afee0671f7 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt @@ -5,9 +5,9 @@ import net.corda.bank.api.BankOfCordaClientApi import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo -import net.corda.testing.driver.driver import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.BOC +import net.corda.testing.driver.driver import org.junit.Test import kotlin.test.assertTrue @@ -19,8 +19,9 @@ class BankOfCordaHttpAPITest { startNode(BOC.name, setOf(ServiceInfo(SimpleNotaryService.type))), startNode(BIGCORP_LEGAL_NAME) ).getOrThrow() + val anonymous = false val nodeBankOfCordaApiAddr = startWebserver(nodeBankOfCorda).getOrThrow().listenAddress - assertTrue(BankOfCordaClientApi(nodeBankOfCordaApiAddr).requestWebIssue(IssueRequestParams(1000, "USD", BIGCORP_LEGAL_NAME, "1", BOC.name))) + assertTrue(BankOfCordaClientApi(nodeBankOfCordaApiAddr).requestWebIssue(IssueRequestParams(1000, "USD", BIGCORP_LEGAL_NAME, "1", BOC.name, BOC.name, anonymous))) }, isDebug = true) } } diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index 7609ce2bb9..7159c077b4 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -1,10 +1,14 @@ package net.corda.bank import com.google.common.util.concurrent.Futures +import net.corda.contracts.asset.Cash import net.corda.core.contracts.DOLLARS import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo +import net.corda.core.node.services.Vault +import net.corda.core.node.services.trackBy +import net.corda.core.node.services.vault.QueryCriteria import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.testing.driver.driver import net.corda.node.services.startFlowPermission @@ -33,31 +37,35 @@ class BankOfCordaRPCClientTest { val bigCorpProxy = bigCorpClient.start("bigCorpCFO", "password2").proxy // Register for Bank of Corda Vault updates - val vaultUpdatesBoc = bocProxy.vaultAndUpdates().second + val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) + val (_, vaultUpdatesBoc) = bocProxy.vaultTrackByCriteria(Cash.State::class.java, criteria) // Register for Big Corporation Vault updates - val vaultUpdatesBigCorp = bigCorpProxy.vaultAndUpdates().second + val (_, vaultUpdatesBigCorp) = bigCorpProxy.vaultTrackByCriteria(Cash.State::class.java, criteria) // Kick-off actual Issuer Flow + val anonymous = true bocProxy.startFlow( ::IssuanceRequester, 1000.DOLLARS, nodeBigCorporation.nodeInfo.legalIdentity, BIG_CORP_PARTY_REF, - nodeBankOfCorda.nodeInfo.legalIdentity).returnValue.getOrThrow() + nodeBankOfCorda.nodeInfo.legalIdentity, + nodeBankOfCorda.nodeInfo.notaryIdentity, + anonymous).returnValue.getOrThrow() // Check Bank of Corda Vault Updates vaultUpdatesBoc.expectEvents { sequence( // ISSUE expect { update -> - require(update.consumed.isEmpty()) { update.consumed.size } - require(update.produced.size == 1) { update.produced.size } + require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" } + require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } }, // MOVE expect { update -> - require(update.consumed.size == 1) { update.consumed.size } - require(update.produced.isEmpty()) { update.produced.size } + require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" } + require(update.produced.isEmpty()) { "Expected 0 produced states, actual: $update" } } ) } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt index 8c54ac4804..ed44a63309 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -1,14 +1,13 @@ package net.corda.bank -import com.google.common.net.HostAndPort import joptsimple.OptionParser import net.corda.bank.api.BankOfCordaClientApi import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams -import net.corda.core.crypto.X509Utilities import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.CashExitFlow import net.corda.flows.CashPaymentFlow import net.corda.flows.IssuerFlow @@ -68,17 +67,18 @@ private class BankOfCordaDriver { }, isDebug = true) } else { try { - val requestParams = IssueRequestParams(options.valueOf(quantity), options.valueOf(currency), BIGCORP_LEGAL_NAME, "1", BOC.name) + val anonymous = true + val requestParams = IssueRequestParams(options.valueOf(quantity), options.valueOf(currency), BIGCORP_LEGAL_NAME, "1", BOC.name, DUMMY_NOTARY.name, anonymous) when (role) { Role.ISSUE_CASH_RPC -> { println("Requesting Cash via RPC ...") - val result = BankOfCordaClientApi(HostAndPort.fromString("localhost:10006")).requestRPCIssue(requestParams) + val result = BankOfCordaClientApi(NetworkHostAndPort("localhost", 10006)).requestRPCIssue(requestParams) if (result is SignedTransaction) println("Success!! You transaction receipt is ${result.tx.id}") } Role.ISSUE_CASH_WEB -> { println("Requesting Cash via Web ...") - val result = BankOfCordaClientApi(HostAndPort.fromString("localhost:10007")).requestWebIssue(requestParams) + val result = BankOfCordaClientApi(NetworkHostAndPort("localhost", 10007)).requestWebIssue(requestParams) if (result) println("Successfully processed Cash Issue request") } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt index b4fef75d86..2f4da02b39 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt @@ -1,21 +1,21 @@ package net.corda.bank.api -import com.google.common.net.HostAndPort import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams import net.corda.client.rpc.CordaRPCClient import net.corda.core.contracts.Amount import net.corda.core.contracts.currency import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.NetworkHostAndPort import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.testing.http.HttpApi /** * Interface for communicating with Bank of Corda node */ -class BankOfCordaClientApi(val hostAndPort: HostAndPort) { +class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { private val apiRoot = "api/bank" /** * HTTP API @@ -40,11 +40,14 @@ class BankOfCordaClientApi(val hostAndPort: HostAndPort) { ?: throw Exception("Unable to locate ${params.issueToPartyName} in Network Map Service") val issuerBankParty = proxy.partyFromX500Name(params.issuerBankName) ?: throw Exception("Unable to locate ${params.issuerBankName} in Network Map Service") + val notaryParty = proxy.partyFromX500Name(params.notaryName) + ?: throw Exception("Unable to locate ${params.notaryName} in Network Map Service") val amount = Amount(params.amount, currency(params.currency)) val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) - return proxy.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty).returnValue.getOrThrow() + return proxy.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, notaryParty, params.anonymous) + .returnValue.getOrThrow().stx } } } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt index 6aba4f1be5..31c8b3abf3 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt @@ -2,11 +2,10 @@ package net.corda.bank.api import net.corda.core.contracts.Amount import net.corda.core.contracts.currency -import net.corda.core.flows.FlowException import net.corda.core.getOrThrow import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor import net.corda.flows.IssuerFlow.IssuanceRequester import org.bouncycastle.asn1.x500.X500Name @@ -20,7 +19,9 @@ import javax.ws.rs.core.Response class BankOfCordaWebApi(val rpc: CordaRPCOps) { data class IssueRequestParams(val amount: Long, val currency: String, val issueToPartyName: X500Name, val issueToPartyRefAsString: String, - val issuerBankName: X500Name) + val issuerBankName: X500Name, + val notaryName: X500Name, + val anonymous: Boolean) private companion object { val logger = loggerFor() @@ -42,22 +43,25 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) { fun issueAssetRequest(params: IssueRequestParams): Response { // Resolve parties via RPC val issueToParty = rpc.partyFromX500Name(params.issueToPartyName) - ?: throw Exception("Unable to locate ${params.issueToPartyName} in Network Map Service") + ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.issueToPartyName} in Network Map Service").build() val issuerBankParty = rpc.partyFromX500Name(params.issuerBankName) - ?: throw Exception("Unable to locate ${params.issuerBankName} in Network Map Service") + ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.issuerBankName} in Network Map Service").build() + val notaryParty = rpc.partyFromX500Name(params.notaryName) + ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.notaryName} in Network Map Service").build() val amount = Amount(params.amount, currency(params.currency)) val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) + val anonymous = params.anonymous // invoke client side of Issuer Flow: IssuanceRequester // The line below blocks and waits for the future to resolve. - val status = try { - rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty).returnValue.getOrThrow() + return try { + rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, notaryParty, anonymous).returnValue.getOrThrow() logger.info("Issue request completed successfully: $params") - Response.Status.CREATED - } catch (e: FlowException) { - Response.Status.BAD_REQUEST + Response.status(Response.Status.CREATED).build() + } catch (e: Exception) { + logger.error("Issue request failed: ${e}", e) + Response.status(Response.Status.FORBIDDEN).build() } - return Response.status(status).build() } } \ No newline at end of file diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index c68008daf8..ac93b3ec0b 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -1,14 +1,14 @@ package net.corda.irs -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import net.corda.client.rpc.CordaRPCClient import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.core.toFuture -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.irs.api.NodeInterestRates import net.corda.irs.contract.InterestRateSwap import net.corda.irs.utilities.uploadFile @@ -101,7 +101,7 @@ class IRSDemoTest : IntegrationTestCategory { assertThat(nodeApi.postJson("deals", tradeFile)).isTrue() } - private fun runUploadRates(host: HostAndPort) { + private fun runUploadRates(host: NetworkHostAndPort) { println("Running upload rates against $host") val fileContents = loadResourceFile("net/corda/irs/simulation/example.rates.txt") val url = URL("http://$host/api/irs/fixes") diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt index e7c0b5ba8c..33604a7454 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt @@ -146,7 +146,7 @@ object NodeInterestRates { // Oracle gets signing request for only some of them with a valid partial tree? We sign over a whole transaction. // It will be fixed by adding partial signatures later. // DOCSTART 1 - fun sign(ftx: FilteredTransaction): DigitalSignature.LegallyIdentifiable { + fun sign(ftx: FilteredTransaction): DigitalSignature.WithKey { if (!ftx.verify()) { throw MerkleTreeException("Rate Fix Oracle: Couldn't verify partial Merkle tree.") } @@ -178,7 +178,7 @@ object NodeInterestRates { // version so we can't resolve or check it ourselves. However, that doesn't matter much, as if we sign // an invalid transaction the signature is worthless. val signature = services.keyManagementService.sign(ftx.rootHash.bytes, signingKey) - return DigitalSignature.LegallyIdentifiable(identity, signature.bytes) + return DigitalSignature.WithKey(signingKey, signature.bytes) } // DOCEND 1 diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt index f63e3c3217..57110870aa 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt @@ -72,9 +72,9 @@ object FixingFlow { override fun beforeSigning(fix: Fix) { newDeal.generateFix(ptx, StateAndRef(txState, handshake.payload.ref), fix) - // And add a request for a time-window: it may be that none of the contracts need this! + // We set the transaction's time-window: it may be that none of the contracts need this! // But it can't hurt to have one. - ptx.addTimeWindow(serviceHub.clock.instant(), 30.seconds) + ptx.setTimeWindow(serviceHub.clock.instant(), 30.seconds) } @Suspendable diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt index 61f4256ce4..0ab867f889 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt @@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.Fix import net.corda.contracts.FixOf import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.isFulfilledBy import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party @@ -111,13 +112,13 @@ open class RatesFixFlow(protected val tx: TransactionBuilder, @InitiatingFlow class FixSignFlow(val tx: TransactionBuilder, val oracle: Party, - val partialMerkleTx: FilteredTransaction) : FlowLogic() { + val partialMerkleTx: FilteredTransaction) : FlowLogic() { @Suspendable - override fun call(): DigitalSignature.LegallyIdentifiable { - val resp = sendAndReceive(oracle, SignRequest(partialMerkleTx)) + override fun call(): DigitalSignature.WithKey { + val resp = sendAndReceive(oracle, SignRequest(partialMerkleTx)) return resp.unwrap { sig -> - check(sig.signer == oracle) - tx.checkSignature(sig) + check(oracle.owningKey.isFulfilledBy(listOf(sig.by))) + tx.toWireTransaction().checkSignature(sig) sig } } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/IRSDemo.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/IRSDemo.kt index fa4a2f020a..5b9f023933 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/IRSDemo.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/IRSDemo.kt @@ -2,8 +2,8 @@ package net.corda.irs -import com.google.common.net.HostAndPort import joptsimple.OptionParser +import net.corda.core.utilities.NetworkHostAndPort import kotlin.system.exitProcess enum class Role { @@ -29,9 +29,9 @@ fun main(args: Array) { val role = options.valueOf(roleArg)!! val value = options.valueOf(valueArg) when (role) { - Role.UploadRates -> IRSDemoClientApi(HostAndPort.fromString("localhost:10004")).runUploadRates() - Role.Trade -> IRSDemoClientApi(HostAndPort.fromString("localhost:10007")).runTrade(value) - Role.Date -> IRSDemoClientApi(HostAndPort.fromString("localhost:10010")).runDateChange(value) + Role.UploadRates -> IRSDemoClientApi(NetworkHostAndPort("localhost", 10004)).runUploadRates() + Role.Trade -> IRSDemoClientApi(NetworkHostAndPort("localhost", 10007)).runTrade(value) + Role.Date -> IRSDemoClientApi(NetworkHostAndPort("localhost", 10010)).runDateChange(value) } } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/IrsDemoClientApi.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/IrsDemoClientApi.kt index c983115559..31b3ccf13b 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/IrsDemoClientApi.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/IrsDemoClientApi.kt @@ -1,6 +1,6 @@ package net.corda.irs -import com.google.common.net.HostAndPort +import net.corda.core.utilities.NetworkHostAndPort import net.corda.irs.utilities.uploadFile import net.corda.testing.http.HttpApi import org.apache.commons.io.IOUtils @@ -9,7 +9,7 @@ import java.net.URL /** * Interface for communicating with nodes running the IRS demo. */ -class IRSDemoClientApi(private val hostAndPort: HostAndPort) { +class IRSDemoClientApi(private val hostAndPort: NetworkHostAndPort) { private val api = HttpApi.fromHostAndPort(hostAndPort, apiRoot) fun runTrade(tradeId: String): Boolean { diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt index c229067712..321180d97d 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt @@ -3,9 +3,9 @@ package net.corda.irs import com.google.common.util.concurrent.Futures import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.irs.api.NodeInterestRates import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.driver.driver diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 4f25c1af8c..9d3035cef3 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -14,16 +14,12 @@ import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.LogHelper +import net.corda.testing.LogHelper import net.corda.core.utilities.ProgressTracker import net.corda.irs.flows.RatesFixFlow import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction -import net.corda.testing.ALICE_PUBKEY -import net.corda.testing.MEGA_CORP -import net.corda.testing.MEGA_CORP_KEY +import net.corda.testing.* import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties @@ -164,7 +160,7 @@ class NodeInterestRatesTest { val wtx = tx.toWireTransaction() val ftx = wtx.buildFilteredTransaction(Predicate { x -> fixCmdFilter(x) }) val signature = oracle.sign(ftx) - tx.checkAndAddSignature(signature) + wtx.checkSignature(signature) } } @@ -213,7 +209,7 @@ class NodeInterestRatesTest { fun `network tearoff`() { val mockNet = MockNetwork() val n1 = mockNet.createNotaryNode() - val n2 = mockNet.createNode(n1.info.address, advertisedServices = ServiceInfo(NodeInterestRates.Oracle.type)) + val n2 = mockNet.createNode(n1.network.myAddress, advertisedServices = ServiceInfo(NodeInterestRates.Oracle.type)) n2.registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java) n2.registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java) n2.database.transaction { @@ -228,10 +224,11 @@ class NodeInterestRatesTest { val future = n1.services.startFlow(flow).resultFuture mockNet.runNetwork() future.getOrThrow() - // We should now have a valid signature over our tx from the oracle. - val fix = tx.toSignedTransaction(true).tx.commands.map { it.value as Fix }.first() + // We should now have a valid fix of our tx from the oracle. + val fix = tx.toWireTransaction().commands.map { it.value as Fix }.first() assertEquals(fixOf, fix.of) assertEquals("0.678".bd, fix.value) + mockNet.stopNodes() } class FilteredRatesFlow(tx: TransactionBuilder, diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt index 78b2e7974e..fc01eddcaa 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt @@ -4,9 +4,9 @@ import net.corda.contracts.* import net.corda.core.contracts.* import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY -import net.corda.core.utilities.TEST_TX_TIME +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY +import net.corda.testing.TEST_TX_TIME import net.corda.testing.* import net.corda.testing.node.MockServices import org.junit.Test @@ -201,6 +201,10 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { } class IRSTests { + val megaCorpServices = MockServices(MEGA_CORP_KEY) + val miniCorpServices = MockServices(MINI_CORP_KEY) + val notaryServices = MockServices(DUMMY_NOTARY_KEY) + @Test fun ok() { trade().verifies() @@ -223,12 +227,11 @@ class IRSTests { calculation = dummyIRS.calculation, common = dummyIRS.common, notary = DUMMY_NOTARY).apply { - addTimeWindow(TEST_TX_TIME, 30.seconds) - signWith(MEGA_CORP_KEY) - signWith(MINI_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) + setTimeWindow(TEST_TX_TIME, 30.seconds) } - gtx.toSignedTransaction() + val ptx1 = megaCorpServices.signInitialTransaction(gtx) + val ptx2 = miniCorpServices.addSignature(ptx1) + notaryServices.addSignature(ptx2) } return genTX } @@ -308,13 +311,10 @@ class IRSTests { val tx = TransactionType.General.Builder(DUMMY_NOTARY) val fixing = Fix(nextFix, "0.052".percent.value) InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing) - with(tx) { - addTimeWindow(TEST_TX_TIME, 30.seconds) - signWith(MEGA_CORP_KEY) - signWith(MINI_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) - } - tx.toSignedTransaction() + tx.setTimeWindow(TEST_TX_TIME, 30.seconds) + val ptx1 = megaCorpServices.signInitialTransaction(tx) + val ptx2 = miniCorpServices.addSignature(ptx1) + notaryServices.addSignature(ptx2) } fixTX.toLedgerTransaction(services).verify() services.recordTransactions(fixTX) diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt index cce9a84d78..d15c86bcec 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt @@ -64,4 +64,4 @@ object UpdateBusinessDayFlow { send(recipient.legalIdentity, UpdateBusinessDayMessage(date)) } } -} +} \ No newline at end of file diff --git a/samples/network-visualiser/build.gradle b/samples/network-visualiser/build.gradle index 86337f2baa..7450bcd816 100644 --- a/samples/network-visualiser/build.gradle +++ b/samples/network-visualiser/build.gradle @@ -18,9 +18,6 @@ dependencies { compile project(':core') compile project(':finance') - // Javax is required for webapis - compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" - // Cordapp dependencies // GraphStream: For visualisation compile 'co.paralleluniverse:capsule:1.0.3' diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt index 468c269b02..c4148f403e 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt @@ -3,10 +3,7 @@ package net.corda.netmap.simulation import co.paralleluniverse.fibers.Suspendable import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue -import com.google.common.util.concurrent.FutureCallback -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture -import com.google.common.util.concurrent.SettableFuture +import com.google.common.util.concurrent.* import net.corda.core.* import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.UniqueIdentifier @@ -17,7 +14,7 @@ import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party import net.corda.core.node.services.linearHeadsOfType import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_CA +import net.corda.testing.DUMMY_CA import net.corda.flows.TwoPartyDealFlow.Acceptor import net.corda.flows.TwoPartyDealFlow.AutoOffer import net.corda.flows.TwoPartyDealFlow.Instigator @@ -49,7 +46,7 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten val future = SettableFuture.create() om = JacksonSupport.createInMemoryMapper(InMemoryIdentityService((banks + regulators + networkMap).map { it.info.legalIdentityAndCert }, trustRoot = DUMMY_CA.certificate)) - startIRSDealBetween(0, 1).success { + startIRSDealBetween(0, 1).thenMatch({ // Next iteration is a pause. executeOnNextIteration.add {} executeOnNextIteration.add { @@ -67,16 +64,16 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten executeOnNextIteration.add { val f = doNextFixing(0, 1) if (f != null) { - Futures.addCallback(f, this, RunOnCallerThread) + Futures.addCallback(f, this, MoreExecutors.directExecutor()) } else { // All done! future.set(Unit) } } } - }, RunOnCallerThread) + }, MoreExecutors.directExecutor()) } - } + }, {}) return future } diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index 89f4b7dfa9..1548e83ef6 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -7,12 +7,12 @@ import net.corda.core.flatMap import net.corda.core.flows.FlowLogic import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.CityDatabase -import net.corda.core.node.PhysicalLocation +import net.corda.core.node.WorldMapLocation import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.containsType -import net.corda.core.utilities.DUMMY_MAP -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_REGULATOR +import net.corda.testing.DUMMY_MAP +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_REGULATOR import net.corda.core.utilities.ProgressTracker import net.corda.irs.api.NodeInterestRates import net.corda.node.services.config.NodeConfiguration @@ -57,7 +57,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, advertisedServices: Set, id: Int, overrideServices: Map?, entropyRoot: BigInteger) : MockNetwork.MockNode(config, mockNet, networkMapAddress, advertisedServices, id, overrideServices, entropyRoot) { - override fun findMyLocation(): PhysicalLocation? { + override fun findMyLocation(): WorldMapLocation? { return configuration.myLegalName.locationOrNull?.let { CityDatabase[it] } } } @@ -80,7 +80,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, fun createAll(): List { return bankLocations.mapIndexed { i, _ -> // Use deterministic seeds so the simulation is stable. Needed so that party owning keys are stable. - mockNet.createNode(networkMap.info.address, start = false, nodeFactory = this, entropyRoot = BigInteger.valueOf(i.toLong())) as SimulatedNode + mockNet.createNode(networkMap.network.myAddress, start = false, nodeFactory = this, entropyRoot = BigInteger.valueOf(i.toLong())) as SimulatedNode } } } @@ -158,16 +158,16 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val networkMap: SimulatedNode = mockNet.createNode(null, nodeFactory = NetworkMapNodeFactory, advertisedServices = ServiceInfo(NetworkMapService.type)) as SimulatedNode val notary: SimulatedNode - = mockNet.createNode(networkMap.info.address, nodeFactory = NotaryNodeFactory, advertisedServices = ServiceInfo(SimpleNotaryService.type)) as SimulatedNode - val regulators: List = listOf(mockNet.createNode(networkMap.info.address, start = false, nodeFactory = RegulatorFactory) as SimulatedNode) + = mockNet.createNode(networkMap.network.myAddress, nodeFactory = NotaryNodeFactory, advertisedServices = ServiceInfo(SimpleNotaryService.type)) as SimulatedNode + val regulators: List = listOf(mockNet.createNode(networkMap.network.myAddress, start = false, nodeFactory = RegulatorFactory) as SimulatedNode) val ratesOracle: SimulatedNode - = mockNet.createNode(networkMap.info.address, start = false, nodeFactory = RatesOracleFactory, advertisedServices = ServiceInfo(NodeInterestRates.Oracle.type)) as SimulatedNode + = mockNet.createNode(networkMap.network.myAddress, start = false, nodeFactory = RatesOracleFactory, advertisedServices = ServiceInfo(NodeInterestRates.Oracle.type)) as SimulatedNode // All nodes must be in one of these two lists for the purposes of the visualiser tool. val serviceProviders: List = listOf(notary, ratesOracle, networkMap) val banks: List = bankFactory.createAll() - val clocks = (serviceProviders + regulators + banks).map { it.services.clock as TestClock } + val clocks = (serviceProviders + regulators + banks).map { it.platformClock as TestClock } // These are used from the network visualiser tool. private val _allFlowSteps = PublishSubject.create>() diff --git a/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt b/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt index 97777c9dbe..6fc24d24e3 100644 --- a/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt +++ b/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt @@ -1,7 +1,7 @@ package net.corda.netmap.simulation import net.corda.core.getOrThrow -import net.corda.core.utilities.LogHelper +import net.corda.testing.LogHelper import org.junit.Test class IRSSimulationTest { diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index 938b38cfd4..5f9204b252 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -25,9 +25,6 @@ dependencies { compile project(':client:rpc') compile project(':test-utils') compile project(':cordform-common') - - // Javax is required for webapis - compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" } idea { diff --git a/samples/notary-demo/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt b/samples/notary-demo/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt index 45d25fdb43..6aa3cc1aa4 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt @@ -1,9 +1,9 @@ package net.corda.demorun.util -import com.google.common.net.HostAndPort import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode import net.corda.core.node.services.ServiceInfo +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.User import org.bouncycastle.asn1.x500.X500Name @@ -21,6 +21,6 @@ fun CordformNode.advertisedServices(vararg services: ServiceInfo) { advertisedServices = services.map { it.toString() } } -fun CordformNode.notaryClusterAddresses(vararg addresses: HostAndPort) { +fun CordformNode.notaryClusterAddresses(vararg addresses: NetworkHostAndPort) { notaryClusterAddresses = addresses.map { it.toString() } } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 4a0a5632f9..3f6af7fc9f 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -1,10 +1,9 @@ package net.corda.notarydemo -import com.google.common.net.HostAndPort import net.corda.core.div import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB +import net.corda.testing.ALICE +import net.corda.testing.BOB import net.corda.demorun.util.* import net.corda.demorun.runNodes import net.corda.node.services.transactions.BFTNonValidatingNotaryService @@ -14,6 +13,7 @@ import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode import net.corda.core.stream import net.corda.core.toTypedArray +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.transactions.minCorrectReplicas import org.bouncycastle.asn1.x500.X500Name @@ -38,7 +38,7 @@ object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", not p2pPort(10005) rpcPort(10006) } - val clusterAddresses = (0 until clusterSize).stream().mapToObj { HostAndPort.fromParts("localhost", 11000 + it * 10) }.toTypedArray() + val clusterAddresses = (0 until clusterSize).stream().mapToObj { NetworkHostAndPort("localhost", 11000 + it * 10) }.toTypedArray() fun notaryNode(replicaId: Int, configure: CordformNode.() -> Unit) = node { name(notaryNames[replicaId]) advertisedServices(advertisedService) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt index 0a3b416022..855fd1dd27 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt @@ -1,6 +1,5 @@ package net.corda.notarydemo -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import net.corda.client.rpc.CordaRPCClient @@ -11,13 +10,14 @@ import net.corda.core.map import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.BOB +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.testing.BOB import net.corda.notarydemo.flows.DummyIssueAndMove import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient import kotlin.streams.asSequence fun main(args: Array) { - val address = HostAndPort.fromParts("localhost", 10003) + val address = NetworkHostAndPort("localhost", 10003) println("Connecting to the recipient node ($address)") CordaRPCClient(address).start(notaryDemoUser.username, notaryDemoUser.password).use { NotaryDemoClientApi(it.proxy).notarise(10) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index 7c2dd027bf..10a94ea6a4 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -1,18 +1,18 @@ package net.corda.notarydemo -import com.google.common.net.HostAndPort import net.corda.core.crypto.appendToCommonName import net.corda.core.div import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY import net.corda.demorun.util.* import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode +import net.corda.core.utilities.NetworkHostAndPort import net.corda.demorun.runNodes import net.corda.demorun.util.node import org.bouncycastle.asn1.x500.X500Name @@ -49,7 +49,7 @@ object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", no p2pPort(10009) rpcPort(10010) } - val clusterAddress = HostAndPort.fromParts("localhost", 10008) // Otherwise each notary forms its own cluster. + val clusterAddress = NetworkHostAndPort("localhost", 10008) // Otherwise each notary forms its own cluster. notaryNode(1) { notaryNodePort(10012) p2pPort(10013) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt index fcd2165253..bdd1c21093 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt @@ -2,9 +2,9 @@ package net.corda.notarydemo import net.corda.core.div import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY import net.corda.demorun.runNodes import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.ValidatingNotaryService diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt index cb7bb6196c..50f51bc9e1 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt @@ -1,10 +1,10 @@ package net.corda.notarydemo.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.DummyContract -import net.corda.core.identity.Party +import net.corda.testing.contracts.DummyContract import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction import java.util.* diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index f6c7a28aca..4be015ab67 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -35,9 +35,6 @@ dependencies { compile project(':webserver') compile project(':finance') - // Javax is required for webapis - compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" - // Cordapp dependencies // Specify your cordapp's dependencies below, including dependent cordapps compile "com.opengamma.strata:strata-basics:${strata_version}" diff --git a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt index 171d373603..ec255ccd88 100644 --- a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt +++ b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt @@ -4,9 +4,9 @@ import com.google.common.util.concurrent.Futures import com.opengamma.strata.product.common.BuySell import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.driver.driver import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.IntegrationTestCategory diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/analytics/example/OGSwapPricingCcpExample.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/analytics/example/OGSwapPricingCcpExample.kt index baffba45a0..3966488829 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/analytics/example/OGSwapPricingCcpExample.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/analytics/example/OGSwapPricingCcpExample.kt @@ -32,6 +32,10 @@ import com.opengamma.strata.product.common.BuySell import com.opengamma.strata.product.swap.type.FixedIborSwapConventions import com.opengamma.strata.report.ReportCalculationResults import com.opengamma.strata.report.trade.TradeReport +import net.corda.core.div +import net.corda.core.exists +import java.nio.file.Path +import java.nio.file.Paths import java.time.LocalDate /** @@ -59,31 +63,44 @@ class SwapPricingCcpExample { /** * The location of the data files. */ - private val PATH_CONFIG = "src/main/resources/" + private val resourcesUri = run { + // Find src/main/resources by walking up the directory tree starting at a classpath root: + var module = Paths.get(javaClass.getResource("/").toURI()) + val relative = "src" / "main" / "resources" + var path: Path + while (true) { + path = module.resolve(relative) + path.exists() && break + module = module.parent + } + path.toUri() + } + + private fun resourceLocator(uri: String) = ResourceLocator.ofUrl(resourcesUri.resolve(uri).toURL()) /** * The location of the curve calibration groups file for CCP1 and CCP2. */ - private val GROUPS_RESOURCE_CCP1 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/curves/groups.csv") - private val GROUPS_RESOURCE_CCP2 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/curves/groups-ccp2.csv") + private val GROUPS_RESOURCE_CCP1 = resourceLocator("example-calibration/curves/groups.csv") + private val GROUPS_RESOURCE_CCP2 = resourceLocator("example-calibration/curves/groups-ccp2.csv") /** * The location of the curve calibration settings file for CCP1 and CCP2. */ - private val SETTINGS_RESOURCE_CCP1 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/curves/settings.csv") - private val SETTINGS_RESOURCE_CCP2 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/curves/settings-ccp2.csv") + private val SETTINGS_RESOURCE_CCP1 = resourceLocator("example-calibration/curves/settings.csv") + private val SETTINGS_RESOURCE_CCP2 = resourceLocator("example-calibration/curves/settings-ccp2.csv") /** * The location of the curve calibration nodes file for CCP1 and CCP2. */ - private val CALIBRATION_RESOURCE_CCP1 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/curves/calibrations.csv") - private val CALIBRATION_RESOURCE_CCP2 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/curves/calibrations-ccp2.csv") + private val CALIBRATION_RESOURCE_CCP1 = resourceLocator("example-calibration/curves/calibrations.csv") + private val CALIBRATION_RESOURCE_CCP2 = resourceLocator("example-calibration/curves/calibrations-ccp2.csv") /** * The location of the market quotes file for CCP1 and CCP2. */ - private val QUOTES_RESOURCE_CCP1 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/quotes/quotes.csv") - private val QUOTES_RESOURCE_CCP2 = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-calibration/quotes/quotes-ccp2.csv") + private val QUOTES_RESOURCE_CCP1 = resourceLocator("example-calibration/quotes/quotes.csv") + private val QUOTES_RESOURCE_CCP2 = resourceLocator("example-calibration/quotes/quotes-ccp2.csv") /** * The location of the historical fixing file. */ - private val FIXINGS_RESOURCE = ResourceLocator.of(ResourceLocator.FILE_URL_PREFIX + PATH_CONFIG + "example-marketdata/historical-fixings/usd-libor-3m.csv") + private val FIXINGS_RESOURCE = resourceLocator("example-marketdata/historical-fixings/usd-libor-3m.csv") /** * The first counterparty. diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt index 9412b62262..38ec9efa02 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt @@ -12,8 +12,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow -import net.corda.core.utilities.DUMMY_MAP -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.node.services.ServiceType import net.corda.vega.analytics.InitialMarginTriple import net.corda.vega.contracts.IRSState import net.corda.vega.contracts.PortfolioState @@ -33,7 +32,6 @@ import javax.ws.rs.core.Response //TODO: Change import namespaces vega -> .... - @Path("simmvaluationdemo") class PortfolioApi(val rpc: CordaRPCOps) { private val ownParty: Party get() = rpc.nodeIdentity().legalIdentity @@ -228,7 +226,7 @@ class PortfolioApi(val rpc: CordaRPCOps) { return withParty(partyName) { party -> withPortfolio(party) { state -> if (state.valuation != null) { - val isValuer = state.valuer as AbstractParty == ownParty + val isValuer = state.valuer == ownParty val rawMtm = state.valuation.presentValues.map { it.value.amounts.first().amount }.reduce { a, b -> a + b } @@ -254,12 +252,11 @@ class PortfolioApi(val rpc: CordaRPCOps) { @Path("whoami") @Produces(MediaType.APPLICATION_JSON) fun getWhoAmI(): AvailableParties { - val (parties, partyUpdates) = rpc.networkMapUpdates() + val (parties, partyUpdates) = rpc.networkMapFeed() partyUpdates.notUsed() - val counterParties = parties.filter { - it.legalIdentity.name != DUMMY_MAP.name - && it.legalIdentity.name != DUMMY_NOTARY.name - && it.legalIdentity.name != ownParty.name + val counterParties = parties.filterNot { + it.advertisedServices.any { it.info.type in setOf(ServiceType.networkMap, ServiceType.notary) } + || it.legalIdentity == ownParty } return AvailableParties( @@ -280,8 +277,6 @@ class PortfolioApi(val rpc: CordaRPCOps) { return withParty(partyName) { otherParty -> val existingSwap = getPortfolioWith(otherParty) val flowHandle = if (existingSwap == null) { - // TODO: Remove this suppress when we upgrade to kotlin 1.1 or when JetBrain fixes the bug. - @Suppress("UNSUPPORTED_FEATURE") rpc.startFlow(SimmFlow::Requester, otherParty, params.valuationDate) } else { rpc.startFlow(SimmRevaluation::Initiator, getPortfolioStateAndRefWith(otherParty).ref, params.valuationDate) @@ -295,4 +290,3 @@ class PortfolioApi(val rpc: CordaRPCOps) { } } } - diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt index 889a7071b1..c9d56ae403 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt @@ -8,6 +8,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.flows.AbstractStateReplacementFlow import net.corda.flows.StateReplacementException import net.corda.vega.contracts.RevisionedState +import java.security.PublicKey /** * Flow that generates an update on a mutable deal state and commits the resulting transaction reaching consensus @@ -16,13 +17,16 @@ import net.corda.vega.contracts.RevisionedState object StateRevisionFlow { class Requester(curStateRef: StateAndRef>, updatedData: T) : AbstractStateReplacementFlow.Instigator, RevisionedState, T>(curStateRef, updatedData) { - override fun assembleTx(): Pair> { + override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { val state = originalState.state.data val tx = state.generateRevision(originalState.state.notary, originalState, modification) - tx.addTimeWindow(serviceHub.clock.instant(), 30.seconds) + tx.setTimeWindow(serviceHub.clock.instant(), 30.seconds) val stx = serviceHub.signInitialTransaction(tx) - return Pair(stx, state.participants) + val participantKeys = state.participants.map { it.owningKey } + // TODO: We need a much faster way of finding our key in the transaction + val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single() + return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey) } } diff --git a/samples/simm-valuation-demo/src/main/resources/log4j2.xml b/samples/simm-valuation-demo/src/main/resources/log4j2.xml index 0205e4fbfe..1c4164a050 100644 --- a/samples/simm-valuation-demo/src/main/resources/log4j2.xml +++ b/samples/simm-valuation-demo/src/main/resources/log4j2.xml @@ -11,11 +11,7 @@ - - - [%-5level] %date{HH:mm:ss.SSS} [%t] %c{2}.%method - %msg%n - > - + @@ -29,7 +25,7 @@ fileName="${log-path}/${log-name}.log" filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz"> - + diff --git a/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt b/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt index 3410f4ea48..b8a52397ee 100644 --- a/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt +++ b/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt @@ -3,10 +3,10 @@ package net.corda.vega import com.google.common.util.concurrent.Futures import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_BANK_C -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_BANK_C +import net.corda.testing.DUMMY_NOTARY import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.driver.driver diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index e21103cd7a..94dda10602 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -31,23 +31,11 @@ dependencies { compile project(':finance') // Corda Plugins: dependent flows and services - compile project(':samples:bank-of-corda-demo') - - // Javax is required for webapis - compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" - - // GraphStream: For visualisation (required by ExampleClientRPC app) - compile "org.graphstream:gs-core:1.3" - compile("org.graphstream:gs-ui:1.3") { - exclude group: "bouncycastle" - } + cordapp project(':samples:bank-of-corda-demo') testCompile project(':test-utils') testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:${assertj_version}" - - // Cordapp dependencies - // Specify your cordapp's dependencies below, including dependent cordapps } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index cc575dd8b1..a0198451a9 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -6,9 +6,9 @@ import net.corda.core.contracts.DOLLARS import net.corda.core.getOrThrow import net.corda.core.millis import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.IssuerFlow import net.corda.testing.driver.poll import net.corda.node.services.startFlowPermission @@ -51,7 +51,8 @@ class TraderDemoTest : NodeBasedTest() { val expectedBCash = clientB.cashCount + 1 val expectedPaper = listOf(clientA.commercialPaperCount + 1, clientB.commercialPaperCount) - clientA.runBuyer(amount = 100.DOLLARS) + // TODO: Enable anonymisation + clientA.runBuyer(amount = 100.DOLLARS, anonymous = false) clientB.runSeller(counterparty = nodeA.info.legalIdentity.name, amount = 5.DOLLARS) assertThat(clientA.cashCount).isGreaterThan(originalACash) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt index 82a0ab081c..7fba92d602 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt @@ -1,13 +1,11 @@ package net.corda.traderdemo -import com.google.common.net.HostAndPort import joptsimple.OptionParser import net.corda.client.rpc.CordaRPCClient import net.corda.core.contracts.DOLLARS -import net.corda.core.crypto.X509Utilities -import net.corda.core.utilities.DUMMY_BANK_A +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.testing.DUMMY_BANK_A import net.corda.core.utilities.loggerFor -import org.bouncycastle.asn1.x500.X500Name import org.slf4j.Logger import kotlin.system.exitProcess @@ -44,12 +42,12 @@ private class TraderDemo { // will contact the buyer and actually make something happen. val role = options.valueOf(roleArg)!! if (role == Role.BUYER) { - val host = HostAndPort.fromString("localhost:10006") + val host = NetworkHostAndPort("localhost", 10006) CordaRPCClient(host).start("demo", "demo").use { TraderDemoClientApi(it.proxy).runBuyer() } } else { - val host = HostAndPort.fromString("localhost:10009") + val host = NetworkHostAndPort("localhost", 10009) CordaRPCClient(host).use("demo", "demo") { TraderDemoClientApi(it.proxy).runSeller(1000.DOLLARS, DUMMY_BANK_A.name) } diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index aca2ca11da..7f2ddbee4e 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -4,7 +4,7 @@ import com.google.common.util.concurrent.Futures import net.corda.client.rpc.notUsed import net.corda.contracts.CommercialPaper import net.corda.contracts.asset.Cash -import net.corda.contracts.testing.calculateRandomlySizedAmounts +import net.corda.testing.contracts.calculateRandomlySizedAmounts import net.corda.core.contracts.Amount import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.USD @@ -12,11 +12,12 @@ import net.corda.core.contracts.filterStatesOfType import net.corda.core.getOrThrow import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.Emoji import net.corda.core.utilities.loggerFor import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.testing.BOC +import net.corda.testing.DUMMY_NOTARY import net.corda.traderdemo.flow.SellerFlow import org.bouncycastle.asn1.x500.X500Name import java.util.* @@ -43,14 +44,18 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) { return vault.filterStatesOfType().size } - fun runBuyer(amount: Amount = 30000.DOLLARS) { + fun runBuyer(amount: Amount = 30000.DOLLARS, anonymous: Boolean = true) { val bankOfCordaParty = rpc.partyFromX500Name(BOC.name) - ?: throw Exception("Unable to locate ${BOC.name} in Network Map Service") + ?: throw IllegalStateException("Unable to locate ${BOC.name} in Network Map Service") + val notaryLegalIdentity = rpc.partyFromX500Name(DUMMY_NOTARY.name) + ?: throw IllegalStateException("Unable to locate ${DUMMY_NOTARY.name} in Network Map Service") + val notaryNode = rpc.nodeIdentityFromParty(notaryLegalIdentity) + ?: throw IllegalStateException("Unable to locate notary node in network map cache") val me = rpc.nodeIdentity() val amounts = calculateRandomlySizedAmounts(amount, 3, 10, Random()) // issuer random amounts of currency totaling 30000.DOLLARS in parallel val resultFutures = amounts.map { pennies -> - rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty).returnValue + rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty, notaryNode.notaryIdentity, anonymous).returnValue } Futures.allAsList(resultFutures).getOrThrow() diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt index 53024ad9a5..10f2e030f0 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt @@ -4,7 +4,6 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.CommercialPaper import net.corda.core.contracts.Amount import net.corda.core.contracts.TransactionGraphSearch -import net.corda.core.div import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.identity.Party @@ -39,7 +38,7 @@ class BuyerFlow(val otherParty: Party) : FlowLogic() { // This invokes the trading flow and out pops our finished transaction. val tradeTX: SignedTransaction = subFlow(buyer) // TODO: This should be moved into the flow itself. - serviceHub.recordTransactions(listOf(tradeTX)) + serviceHub.recordTransactions(tradeTX) println("Purchase complete - we are a happy customer! Final transaction is: " + "\n\n${Emoji.renderIfSupported(tradeTX.tx)}") @@ -55,24 +54,17 @@ class BuyerFlow(val otherParty: Party) : FlowLogic() { private fun logIssuanceAttachment(tradeTX: SignedTransaction) { // Find the original CP issuance. - val search = TransactionGraphSearch(serviceHub.storageService.validatedTransactions, listOf(tradeTX.tx)) + val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx)) search.query = TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java, followInputsOfType = CommercialPaper.State::class.java) val cpIssuance = search.call().single() // Buyer will fetch the attachment from the seller automatically when it resolves the transaction. - // For demo purposes just extract attachment jars when saved to disk, so the user can explore them. - val attachmentsPath = (serviceHub.storageService.attachments).let { - it.automaticallyExtractAttachments = true - it.storePath - } cpIssuance.attachments.first().let { - val p = attachmentsPath / "$it.jar" println(""" -The issuance of the commercial paper came with an attachment. You can find it expanded in this directory: -$p +The issuance of the commercial paper came with an attachment. You can find it in the attachments directory: $it.jar ${Emoji.renderIfSupported(cpIssuance)}""") } diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt index 3a7ecba1d7..720f33ffe5 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt @@ -11,11 +11,13 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker +import net.corda.flows.FinalityFlow import net.corda.flows.NotaryFlow import net.corda.flows.TwoPartyTradeFlow import net.corda.testing.BOC @@ -49,7 +51,7 @@ class SellerFlow(val otherParty: Party, progressTracker.currentStep = SELF_ISSUING val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0] - val cpOwnerKey = serviceHub.legalIdentityKey + val cpOwnerKey = serviceHub.keyManagementService.freshKey() val commercialPaper = selfIssueSomeCommercialPaper(serviceHub.myInfo.legalIdentity, notary) progressTracker.currentStep = TRADING @@ -61,7 +63,7 @@ class SellerFlow(val otherParty: Party, notary, commercialPaper, amount, - cpOwnerKey, + AnonymousParty(cpOwnerKey), progressTracker.getChildProgressTracker(TRADING)!!) return subFlow(seller) } @@ -69,8 +71,7 @@ class SellerFlow(val otherParty: Party, @Suspendable fun selfIssueSomeCommercialPaper(ownedBy: AbstractParty, notaryNode: NodeInfo): StateAndRef { // Make a fake company that's issued its own paper. - val keyPair = generateKeyPair() - val party = Party(BOC.name, keyPair.public) + val party = Party(BOC.name, serviceHub.legalIdentityKey) val issuance: SignedTransaction = run { val tx = CommercialPaper().generateIssue(party.ref(1, 2, 3), 1100.DOLLARS `issued by` DUMMY_CASH_ISSUER, @@ -79,35 +80,23 @@ class SellerFlow(val otherParty: Party, // TODO: Consider moving these two steps below into generateIssue. // Attach the prospectus. - tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id) + tx.addAttachment(serviceHub.attachments.openAttachment(PROSPECTUS_HASH)!!.id) // Requesting a time-window to be set, all CP must have a validation window. - tx.addTimeWindow(Instant.now(), 30.seconds) + tx.setTimeWindow(Instant.now(), 30.seconds) // Sign it as ourselves. - tx.signWith(keyPair) + val stx = serviceHub.signInitialTransaction(tx) - // Get the notary to sign the time-window. - val notarySigs = subFlow(NotaryFlow.Client(tx.toSignedTransaction(false))) - notarySigs.forEach { tx.addSignatureUnchecked(it) } - - // Commit it to local storage. - val stx = tx.toSignedTransaction(true) - serviceHub.recordTransactions(listOf(stx)) - - stx + subFlow(FinalityFlow(stx)).single() } // Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works. val move: SignedTransaction = run { val builder = TransactionType.General.Builder(notaryNode.notaryIdentity) CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy) - builder.signWith(keyPair) - val notarySignature = subFlow(NotaryFlow.Client(builder.toSignedTransaction(false))) - notarySignature.forEach { builder.addSignatureUnchecked(it) } - val tx = builder.toSignedTransaction(true) - serviceHub.recordTransactions(listOf(tx)) - tx + val stx = serviceHub.signInitialTransaction(builder) + subFlow(FinalityFlow(stx)).single() } return move.tx.outRef(0) diff --git a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt index af4638e205..544055d907 100644 --- a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt +++ b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt @@ -2,9 +2,9 @@ package net.corda.traderdemo import net.corda.core.div import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.IssuerFlow import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService diff --git a/settings.gradle b/settings.gradle index aad7b883c0..a2405379d1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,6 +20,7 @@ include 'experimental:sandbox' include 'experimental:quasar-hook' include 'experimental:intellij-plugin' include 'verifier' +include 'test-common' include 'test-utils' include 'smoke-test-utils' include 'tools:explorer' diff --git a/smoke-test-utils/build.gradle b/smoke-test-utils/build.gradle index dcd94fae66..0868951903 100644 --- a/smoke-test-utils/build.gradle +++ b/smoke-test-utils/build.gradle @@ -4,5 +4,6 @@ description 'Utilities needed for smoke tests in Corda' dependencies { // Smoke tests do NOT have any Node code on the classpath! + compile project(':test-common') compile project(':client:rpc') } diff --git a/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt index 338c88d656..0df543cd5c 100644 --- a/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt +++ b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt @@ -6,35 +6,35 @@ import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigValue import com.typesafe.config.ConfigValueFactory import net.corda.core.crypto.commonName -import net.corda.core.identity.Party import net.corda.nodeapi.User +import org.bouncycastle.asn1.x500.X500Name class NodeConfig( - val party: Party, - val p2pPort: Int, - val rpcPort: Int, - val webPort: Int, - val extraServices: List, - val users: List, - var networkMap: NodeConfig? = null + val legalName: X500Name, + val p2pPort: Int, + val rpcPort: Int, + val webPort: Int, + val extraServices: List, + val users: List, + var networkMap: NodeConfig? = null ) { companion object { val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) } - val commonName: String get() = party.name.commonName + val commonName: String get() = legalName.commonName /* * The configuration object depends upon the networkMap, * which is mutable. */ fun toFileConfig(): Config = empty() - .withValue("myLegalName", valueFor(party.name.toString())) + .withValue("myLegalName", valueFor(legalName.toString())) .withValue("p2pAddress", addressValueFor(p2pPort)) .withValue("extraAdvertisedServiceIds", valueFor(extraServices)) .withFallback(optional("networkMapService", networkMap, { c, n -> c.withValue("address", addressValueFor(n.p2pPort)) - .withValue("legalName", valueFor(n.party.name.toString())) + .withValue("legalName", valueFor(n.legalName.toString())) })) .withValue("webAddress", addressValueFor(webPort)) .withValue("rpcAddress", addressValueFor(rpcPort)) diff --git a/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index c939bd4758..f7a56f2be9 100644 --- a/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt +++ b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -1,10 +1,10 @@ package net.corda.smoketesting -import com.google.common.net.HostAndPort import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCConnection import net.corda.core.createDirectories import net.corda.core.div +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import java.nio.file.Path import java.nio.file.Paths @@ -60,7 +60,7 @@ class NodeProcess( confFile.writeText(config.toText()) val process = startNode(nodeDir) - val client = CordaRPCClient(HostAndPort.fromParts("localhost", config.rpcPort)) + val client = CordaRPCClient(NetworkHostAndPort("localhost", config.rpcPort)) val user = config.users[0] val setupExecutor = Executors.newSingleThreadScheduledExecutor() diff --git a/test-common/build.gradle b/test-common/build.gradle new file mode 100644 index 0000000000..3a955f1691 --- /dev/null +++ b/test-common/build.gradle @@ -0,0 +1,9 @@ +apply plugin: 'net.corda.plugins.publish-utils' + +jar { + baseName 'corda-test-common' +} + +publish { + name = jar.baseName +} diff --git a/config/test/log4j2.xml b/test-common/src/main/resources/log4j2-test.xml similarity index 93% rename from config/test/log4j2.xml rename to test-common/src/main/resources/log4j2-test.xml index adcab4aeb5..739a4d6a17 100644 --- a/config/test/log4j2.xml +++ b/test-common/src/main/resources/log4j2-test.xml @@ -5,7 +5,7 @@ - + diff --git a/test-utils/build.gradle b/test-utils/build.gradle index 3c5e40f6fe..f6801db7ef 100644 --- a/test-utils/build.gradle +++ b/test-utils/build.gradle @@ -1,6 +1,8 @@ apply plugin: 'kotlin' +apply plugin: 'kotlin-jpa' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Testing utilities for Corda' @@ -27,6 +29,7 @@ sourceSets { } dependencies { + compile project(':test-common') compile project(':finance') compile project(':core') compile project(':node') diff --git a/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 5ac11f2139..a6ba60fc47 100644 --- a/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -6,13 +6,12 @@ import net.corda.core.getOrThrow import net.corda.core.list import net.corda.core.node.services.ServiceInfo import net.corda.core.readLines -import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_REGULATOR +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_REGULATOR import net.corda.node.internal.NodeStartup import net.corda.node.services.api.RegulatorService import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.testing.ProjectStructure.projectRootDir import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -26,13 +25,13 @@ class DriverTests { private val executorService: ScheduledExecutorService = Executors.newScheduledThreadPool(2) private fun nodeMustBeUp(handleFuture: ListenableFuture) = handleFuture.getOrThrow().apply { - val hostAndPort = ArtemisMessagingComponent.toHostAndPort(nodeInfo.address) + val hostAndPort = nodeInfo.addresses.first() // Check that the port is bound addressMustBeBound(executorService, hostAndPort, (this as? NodeHandle.OutOfProcess)?.process) } private fun nodeMustBeDown(handle: NodeHandle) { - val hostAndPort = ArtemisMessagingComponent.toHostAndPort(handle.nodeInfo.address) + val hostAndPort = handle.nodeInfo.addresses.first() // Check that the port is bound addressMustNotBeBound(executorService, hostAndPort) } diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index febd95d3dd..6c36d638c6 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -3,24 +3,20 @@ package net.corda.testing -import com.google.common.net.HostAndPort import com.nhaarman.mockito_kotlin.spy import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.StateRef -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.X509Utilities -import net.corda.core.crypto.commonName -import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.* import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.ServiceHub -import net.corda.core.node.VersionInfo import net.corda.core.node.services.IdentityService -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.* -import net.corda.node.services.config.* +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.VerifierType +import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.node.services.identity.InMemoryIdentityService import net.corda.nodeapi.config.SSLConfiguration import net.corda.testing.node.MockServices @@ -33,6 +29,7 @@ import java.nio.file.Files import java.nio.file.Path import java.security.KeyPair import java.security.PublicKey +import java.security.cert.CertificateFactory import java.util.concurrent.atomic.AtomicInteger /** @@ -53,6 +50,8 @@ import java.util.concurrent.atomic.AtomicInteger * - The Int.DOLLARS syntax doesn't work from Java. Use the DOLLARS(int) function instead. */ +// TODO: Refactor these dummies to work with the new identities framework. + // A few dummy values for testing. val MEGA_CORP_KEY: KeyPair by lazy { generateKeyPair() } val MEGA_CORP_PUBKEY: PublicKey get() = MEGA_CORP_KEY.public @@ -89,7 +88,7 @@ val ALL_TEST_KEYS: List get() = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, AL val MOCK_IDENTITIES = listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_NOTARY_IDENTITY) val MOCK_IDENTITY_SERVICE: IdentityService get() = InMemoryIdentityService(MOCK_IDENTITIES, emptyMap(), DUMMY_CA.certificate.cert) -val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") +val MOCK_HOST_AND_PORT = NetworkHostAndPort("mockHost", 30000) fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0) @@ -100,7 +99,7 @@ private val freePortCounter = AtomicInteger(30000) * Unsafe for getting multiple ports! * Use [getFreeLocalPorts] for getting multiple ports. */ -fun freeLocalHostAndPort(): HostAndPort = HostAndPort.fromParts("localhost", freePort()) +fun freeLocalHostAndPort() = NetworkHostAndPort("localhost", freePort()) /** * Returns a free port. @@ -116,9 +115,9 @@ fun freePort(): Int = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + ( * Unlikely, but in the time between running this function and handing the ports * to the Node, some other process else could allocate the returned ports. */ -fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List { +fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List { val freePort = freePortCounter.getAndAccumulate(0) { prev, _ -> 30000 + (prev - 30000 + numberToAlloc) % 10000 } - return (freePort .. freePort + numberToAlloc - 1).map { HostAndPort.fromParts(hostName, it) } + return (freePort .. freePort + numberToAlloc - 1).map { NetworkHostAndPort(hostName, it) } } /** @@ -192,3 +191,17 @@ fun getTestX509Name(commonName: String): X500Name { nameBuilder.addRDN(BCStyle.C, "US") return nameBuilder.build() } + +fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair = DUMMY_CA): PartyAndCertificate { + val certFactory = CertificateFactory.getInstance("X509") + val certHolder = X509Utilities.createCertificate(CertificateType.IDENTITY, trustRoot.certificate, trustRoot.keyPair, party.name, party.owningKey) + val certPath = certFactory.generateCertPath(listOf(certHolder.cert, trustRoot.certificate.cert)) + return PartyAndCertificate(party, certHolder, certPath) +} + +/** + * Build a test party with a nonsense certificate authority for testing purposes. + */ +fun getTestPartyAndCertificate(name: X500Name, publicKey: PublicKey, trustRoot: CertificateAndKeyPair = DUMMY_CA): PartyAndCertificate { + return getTestPartyAndCertificate(Party(name, publicKey), trustRoot) +} \ No newline at end of file diff --git a/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt b/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt index f68864dfb7..c3fb15115e 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt @@ -6,7 +6,6 @@ import net.corda.core.contracts.TransactionState import net.corda.core.crypto.SecureHash import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_NOTARY import java.io.InputStream /** diff --git a/core/src/main/kotlin/net/corda/core/utilities/Logging.kt b/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt similarity index 80% rename from core/src/main/kotlin/net/corda/core/utilities/Logging.kt rename to test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt index f17b1cc5e7..ad2e488e91 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Logging.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt @@ -1,26 +1,12 @@ -package net.corda.core.utilities +package net.corda.testing import org.apache.logging.log4j.Level import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.core.LoggerContext import org.apache.logging.log4j.core.config.Configurator import org.apache.logging.log4j.core.config.LoggerConfig -import org.slf4j.LoggerFactory import kotlin.reflect.KClass -// A couple of inlined utility functions: the first is just a syntax convenience, the second lets us use -// Kotlin's string interpolation efficiently: the message is never calculated/concatenated together unless -// logging at that level is enabled. -inline fun loggerFor(): org.slf4j.Logger = LoggerFactory.getLogger(T::class.java) - -inline fun org.slf4j.Logger.trace(msg: () -> String) { - if (isTraceEnabled) trace(msg()) -} - -inline fun org.slf4j.Logger.debug(msg: () -> String) { - if (isDebugEnabled) debug(msg()) -} - /** A configuration helper that allows modifying the log level for specific loggers */ object LogHelper { /** diff --git a/test-utils/src/main/kotlin/net/corda/testing/NodeApi.kt b/test-utils/src/main/kotlin/net/corda/testing/NodeApi.kt deleted file mode 100644 index c812beec84..0000000000 --- a/test-utils/src/main/kotlin/net/corda/testing/NodeApi.kt +++ /dev/null @@ -1,62 +0,0 @@ -package net.corda.testing - -import com.google.common.net.HostAndPort -import java.io.IOException -import java.io.InputStreamReader -import java.net.ConnectException -import java.net.HttpURLConnection -import java.net.SocketException -import java.net.URL -import kotlin.test.assertEquals - -class NodeApi { - class NodeDidNotStartException(message: String) : Exception(message) - - companion object { - // Increased timeout to two minutes. - val NODE_WAIT_RETRY_COUNT: Int = 600 - val NODE_WAIT_RETRY_DELAY_MS: Long = 200 - - fun ensureNodeStartsOrKill(proc: Process, nodeWebserverAddr: HostAndPort) { - try { - assertEquals(proc.isAlive, true) - waitForNodeStartup(nodeWebserverAddr) - } catch (e: Throwable) { - println("Forcibly killing node process") - proc.destroyForcibly() - throw e - } - } - - private fun waitForNodeStartup(nodeWebserverAddr: HostAndPort) { - val url = URL("http://$nodeWebserverAddr/api/status") - var retries = 0 - var respCode: Int - do { - retries++ - val err = try { - val conn = url.openConnection() as HttpURLConnection - conn.requestMethod = "GET" - respCode = conn.responseCode - InputStreamReader(conn.inputStream).readLines().joinToString { it } - } catch(e: ConnectException) { - // This is to be expected while it loads up - respCode = 404 - "Node hasn't started" - } catch(e: SocketException) { - respCode = -1 - "Could not connect: $e" - } catch (e: IOException) { - respCode = -1 - "IOException: $e" - } - - if (retries > NODE_WAIT_RETRY_COUNT) { - throw NodeDidNotStartException("The node did not start: " + err) - } - - Thread.sleep(NODE_WAIT_RETRY_DELAY_MS) - } while (respCode != 200) - } - } -} diff --git a/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt b/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt index 326d01fb74..af6844273b 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt @@ -1,6 +1,5 @@ package net.corda.testing -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture import net.corda.client.mock.Generator import net.corda.client.mock.generateOrFail @@ -11,8 +10,10 @@ import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.core.div import net.corda.core.map import net.corda.core.messaging.RPCOps -import net.corda.core.random63BitValue -import net.corda.core.utilities.ProcessUtilities +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.parseNetworkHostAndPort +import net.corda.core.crypto.random63BitValue +import net.corda.testing.driver.ProcessUtilities import net.corda.node.services.RPCUserService import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.RPCServer @@ -109,7 +110,7 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, configuration: RPCServerConfiguration = RPCServerConfiguration.default, - customPort: HostAndPort? = null, + customPort: NetworkHostAndPort? = null, ops : I ) : ListenableFuture @@ -124,7 +125,7 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { */ fun startRpcClient( rpcOpsClass: Class, - rpcAddress: HostAndPort, + rpcAddress: NetworkHostAndPort, username: String = rpcTestUser.username, password: String = rpcTestUser.password, configuration: RPCClientConfiguration = RPCClientConfiguration.default @@ -140,7 +141,7 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { */ fun startRandomRpcClient( rpcOpsClass: Class, - rpcAddress: HostAndPort, + rpcAddress: NetworkHostAndPort, username: String = rpcTestUser.username, password: String = rpcTestUser.password ): ListenableFuture @@ -153,7 +154,7 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { * @param password The password to authenticate with. */ fun startArtemisSession( - rpcAddress: HostAndPort, + rpcAddress: NetworkHostAndPort, username: String = rpcTestUser.username, password: String = rpcTestUser.password ): ClientSession @@ -163,7 +164,7 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { rpcUser: User = rpcTestUser, maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, - customPort: HostAndPort? = null + customPort: NetworkHostAndPort? = null ): ListenableFuture fun startInVmRpcBroker( @@ -186,12 +187,12 @@ inline fun RPCDriverExposedDSLInterface.startInVmRpcClient( configuration: RPCClientConfiguration = RPCClientConfiguration.default ) = startInVmRpcClient(I::class.java, username, password, configuration) inline fun RPCDriverExposedDSLInterface.startRandomRpcClient( - hostAndPort: HostAndPort, + hostAndPort: NetworkHostAndPort, username: String = rpcTestUser.username, password: String = rpcTestUser.password ) = startRandomRpcClient(I::class.java, hostAndPort, username, password) inline fun RPCDriverExposedDSLInterface.startRpcClient( - rpcAddress: HostAndPort, + rpcAddress: NetworkHostAndPort, username: String = rpcTestUser.username, password: String = rpcTestUser.password, configuration: RPCClientConfiguration = RPCClientConfiguration.default @@ -200,7 +201,7 @@ inline fun RPCDriverExposedDSLInterface.startRpcClient( interface RPCDriverInternalDSLInterface : DriverDSLInternalInterface, RPCDriverExposedDSLInterface data class RpcBrokerHandle( - val hostAndPort: HostAndPort?, /** null if this is an InVM broker */ + val hostAndPort: NetworkHostAndPort?,/** null if this is an InVM broker */ val clientTransportConfiguration: TransportConfiguration, val serverControl: ActiveMQServerControl ) @@ -306,7 +307,7 @@ data class RPCDriverDSL( configureCommonSettings(maxFileSize, maxBufferedBytesPerClient) } } - fun createRpcServerArtemisConfig(maxFileSize: Int, maxBufferedBytesPerClient: Long, baseDirectory: Path, hostAndPort: HostAndPort): Configuration { + fun createRpcServerArtemisConfig(maxFileSize: Int, maxBufferedBytesPerClient: Long, baseDirectory: Path, hostAndPort: NetworkHostAndPort): Configuration { val connectionDirection = ConnectionDirection.Inbound(acceptorFactoryClassName = NettyAcceptorFactory::class.java.name) return ConfigurationImpl().apply { val artemisDir = "$baseDirectory/artemis" @@ -318,7 +319,7 @@ data class RPCDriverDSL( } } val inVmClientTransportConfiguration = TransportConfiguration(InVMConnectorFactory::class.java.name) - fun createNettyClientTransportConfiguration(hostAndPort: HostAndPort): TransportConfiguration { + fun createNettyClientTransportConfiguration(hostAndPort: NetworkHostAndPort): TransportConfiguration { return ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), hostAndPort, null) } } @@ -366,7 +367,7 @@ data class RPCDriverDSL( maxFileSize: Int, maxBufferedBytesPerClient: Long, configuration: RPCServerConfiguration, - customPort: HostAndPort?, + customPort: NetworkHostAndPort?, ops: I ): ListenableFuture { return startRpcBroker(serverName, rpcUser, maxFileSize, maxBufferedBytesPerClient, customPort).map { broker -> @@ -376,7 +377,7 @@ data class RPCDriverDSL( override fun startRpcClient( rpcOpsClass: Class, - rpcAddress: HostAndPort, + rpcAddress: NetworkHostAndPort, username: String, password: String, configuration: RPCClientConfiguration @@ -391,7 +392,7 @@ data class RPCDriverDSL( } } - override fun startRandomRpcClient(rpcOpsClass: Class, rpcAddress: HostAndPort, username: String, password: String): ListenableFuture { + override fun startRandomRpcClient(rpcOpsClass: Class, rpcAddress: NetworkHostAndPort, username: String, password: String): ListenableFuture { val processFuture = driverDSL.executorService.submit { ProcessUtilities.startJavaProcess(listOf(rpcOpsClass.name, rpcAddress.toString(), username, password)) } @@ -399,7 +400,7 @@ data class RPCDriverDSL( return processFuture } - override fun startArtemisSession(rpcAddress: HostAndPort, username: String, password: String): ClientSession { + override fun startArtemisSession(rpcAddress: NetworkHostAndPort, username: String, password: String): ClientSession { val locator = ActiveMQClient.createServerLocatorWithoutHA(createNettyClientTransportConfiguration(rpcAddress)) val sessionFactory = locator.createSessionFactory() val session = sessionFactory.createSession(username, password, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) @@ -417,7 +418,7 @@ data class RPCDriverDSL( rpcUser: User, maxFileSize: Int, maxBufferedBytesPerClient: Long, - customPort: HostAndPort? + customPort: NetworkHostAndPort? ): ListenableFuture { val hostAndPort = customPort ?: driverDSL.portAllocation.nextHostAndPort() addressMustNotBeBound(driverDSL.executorService, hostAndPort) @@ -506,7 +507,7 @@ class RandomRpcUser { require(args.size == 4) @Suppress("UNCHECKED_CAST") val rpcClass = Class.forName(args[0]) as Class - val hostAndPort = HostAndPort.fromString(args[1]) + val hostAndPort = args[1].parseNetworkHostAndPort() val username = args[2] val password = args[3] val handle = RPCClient(hostAndPort, null).start(rpcClass, username, password) diff --git a/core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt b/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt similarity index 80% rename from core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt rename to test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt index 6861fa7fc0..d2ebdae030 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt @@ -1,15 +1,15 @@ @file:JvmName("TestConstants") -package net.corda.core.utilities +package net.corda.testing import net.corda.core.crypto.* +import net.corda.core.crypto.testing.DummyPublicKey import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import org.bouncycastle.asn1.x500.X500Name import java.math.BigInteger import java.security.KeyPair import java.security.PublicKey -import java.security.cert.CertificateFactory import java.time.Instant // A dummy time at which we will be pretending test transactions are created. @@ -67,14 +67,3 @@ val DUMMY_CA: CertificateAndKeyPair by lazy { CertificateAndKeyPair(cert, DUMMY_CA_KEY) } -/** - * Build a test party with a nonsense certificate authority for testing purposes. - */ -fun getTestPartyAndCertificate(name: X500Name, publicKey: PublicKey, trustRoot: CertificateAndKeyPair = DUMMY_CA) = getTestPartyAndCertificate(Party(name, publicKey), trustRoot) - -fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair = DUMMY_CA): PartyAndCertificate { - val certFactory = CertificateFactory.getInstance("X509") - val certHolder = X509Utilities.createCertificate(CertificateType.IDENTITY, trustRoot.certificate, trustRoot.keyPair, party.name, party.owningKey) - val certPath = certFactory.generateCertPath(listOf(certHolder.cert, trustRoot.certificate.cert)) - return PartyAndCertificate(party, certHolder, certPath) -} diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index 7a0e41094b..ea574f79a0 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -2,13 +2,14 @@ package net.corda.testing import net.corda.core.contracts.* import net.corda.core.crypto.* +import net.corda.core.crypto.composite.expandedCompositeKeys +import net.corda.core.crypto.testing.NullSignature import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.DUMMY_NOTARY_KEY import java.io.InputStream import java.security.KeyPair import java.security.PublicKey @@ -116,11 +117,12 @@ data class TestTransactionDSLInterpreter private constructor( } override fun _output(label: String?, notary: Party, encumbrance: Int?, contractState: ContractState) { - val outputIndex = transactionBuilder.addOutputState(contractState, notary, encumbrance) + transactionBuilder.addOutputState(contractState, notary, encumbrance) if (label != null) { if (label in labelToIndexMap) { throw DuplicateOutputLabel(label) } else { + val outputIndex = transactionBuilder.outputStates().size - 1 labelToIndexMap[label] = outputIndex } } @@ -139,14 +141,13 @@ data class TestTransactionDSLInterpreter private constructor( // Verify on a copy of the transaction builder, so if it's then further modified it doesn't error due to // the existing signature transactionBuilder.copy().apply { - signWith(DUMMY_NOTARY_KEY) toWireTransaction().toLedgerTransaction(services).verify() } return EnforceVerifyOrFail.Token } override fun timeWindow(data: TimeWindow) { - transactionBuilder.addTimeWindow(data) + transactionBuilder.setTimeWindow(data) } override fun tweak( @@ -211,8 +212,9 @@ data class TestLedgerDSLInterpreter private constructor( } } - internal fun resolveAttachment(attachmentId: SecureHash): Attachment = - services.storageService.attachments.openAttachment(attachmentId) ?: throw AttachmentResolutionException(attachmentId) + internal fun resolveAttachment(attachmentId: SecureHash): Attachment { + return services.attachments.openAttachment(attachmentId) ?: throw AttachmentResolutionException(attachmentId) + } private fun interpretTransactionDsl( transactionBuilder: TransactionBuilder, @@ -276,7 +278,7 @@ data class TestLedgerDSLInterpreter private constructor( dsl(LedgerDSL(copy())) override fun attachment(attachment: InputStream): SecureHash { - return services.storageService.attachments.importAttachment(attachment) + return services.attachments.importAttachment(attachment) } override fun verifies(): EnforceVerifyOrFail { diff --git a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt index b806e699de..cb973e022f 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt @@ -1,11 +1,11 @@ package net.corda.testing import net.corda.core.contracts.* +import net.corda.testing.contracts.DummyContract import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.seconds import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.DUMMY_NOTARY import java.security.PublicKey import java.time.Duration import java.time.Instant @@ -51,7 +51,7 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { fun _command(signers: List, commandData: CommandData) /** - * Adds a time-window to the transaction. + * Sets the time-window of the transaction. * @param data the [TimeWindow] (validation window). */ fun timeWindow(data: TimeWindow) @@ -115,7 +115,7 @@ class TransactionDSL(val interpreter: T) : Tr fun command(signer: PublicKey, commandData: CommandData) = _command(listOf(signer), commandData) /** - * Adds a [TimeWindow] command to the transaction. + * Sets the [TimeWindow] of the transaction. * @param time The [Instant] of the [TimeWindow]. * @param tolerance The tolerance of the [TimeWindow]. */ diff --git a/core/src/main/kotlin/net/corda/core/contracts/DummyContract.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt similarity index 91% rename from core/src/main/kotlin/net/corda/core/contracts/DummyContract.kt rename to test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt index 31e85f859c..e7a96d9a0b 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/DummyContract.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt @@ -1,5 +1,6 @@ -package net.corda.core.contracts +package net.corda.testing.contracts +import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party @@ -55,8 +56,8 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur } } - fun move(prior: StateAndRef, newOwner: AbstractParty) = move(listOf(prior), newOwner) - fun move(priors: List>, newOwner: AbstractParty): TransactionBuilder { + fun move(prior: StateAndRef, newOwner: AbstractParty) = move(listOf(prior), newOwner) + fun move(priors: List>, newOwner: AbstractParty): TransactionBuilder { require(priors.isNotEmpty()) val priorState = priors[0].state.data val (cmd, state) = priorState.withNewOwner(newOwner) diff --git a/core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt similarity index 91% rename from core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt rename to test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt index a0c4386236..b14b55937f 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt @@ -1,5 +1,6 @@ -package net.corda.core.contracts +package net.corda.testing.contracts +import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.transactions.WireTransaction @@ -25,8 +26,8 @@ class DummyContractV2 : UpgradedContract = listOf(), val linearString: String = "ABC", val linearNumber: Long = 123L, - val linearTimestamp: Instant = LocalDateTime.now().toInstant(ZoneOffset.UTC), + val linearTimestamp: java.time.Instant = LocalDateTime.now().toInstant(UTC), val linearBoolean: Boolean = true, val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState, QueryableState { - override fun isRelevant(ourKeys: Set): Boolean { + override fun isRelevant(ourKeys: Set): Boolean { return participants.any { it.owningKey.containsAny(ourKeys) } } @@ -39,12 +42,12 @@ class DummyLinearContract : Contract { override fun generateMappedObject(schema: MappedSchema): PersistentState { return when (schema) { is DummyLinearStateSchemaV1 -> DummyLinearStateSchemaV1.PersistentDummyLinearState( - externalId = linearId.externalId, - uuid = linearId.id, - linearString = linearString, - linearNumber = linearNumber, - linearTimestamp = linearTimestamp, - linearBoolean = linearBoolean + externalId = linearId.externalId, + uuid = linearId.id, + linearString = linearString, + linearNumber = linearNumber, + linearTimestamp = linearTimestamp, + linearBoolean = linearBoolean ) is DummyLinearStateSchemaV2 -> DummyLinearStateSchemaV2.PersistentDummyLinearState( uid = linearId, diff --git a/core/src/main/kotlin/net/corda/core/contracts/DummyState.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt similarity index 62% rename from core/src/main/kotlin/net/corda/core/contracts/DummyState.kt rename to test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt index 1498b8c379..c2e0696889 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/DummyState.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt @@ -1,5 +1,6 @@ -package net.corda.core.contracts +package net.corda.testing.contracts +import net.corda.core.contracts.ContractState import net.corda.core.identity.AbstractParty /** @@ -7,6 +8,5 @@ import net.corda.core.identity.AbstractParty */ data class DummyState(val magicNumber: Int = 0) : ContractState { override val contract = DUMMY_PROGRAM_ID - override val participants: List - get() = emptyList() + override val participants: List get() = emptyList() } diff --git a/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt similarity index 91% rename from finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt rename to test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt index b7fdd8c06f..0994505b0b 100644 --- a/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt @@ -1,10 +1,9 @@ @file:JvmName("VaultFiller") -package net.corda.contracts.testing +package net.corda.testing.contracts import net.corda.contracts.Commodity import net.corda.contracts.DealState -import net.corda.contracts.DummyDealContract import net.corda.contracts.asset.* import net.corda.core.contracts.* import net.corda.core.identity.AbstractParty @@ -12,11 +11,11 @@ import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.ServiceHub 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.CHARLIE -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.testing.CHARLIE +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY_KEY import java.security.KeyPair import java.security.PublicKey import java.time.Instant @@ -62,12 +61,13 @@ fun ServiceHub.fillWithSomeTestLinearStates(numberToCreate: Int, val transactions: List = (1..numberToCreate).map { // Issue a Linear state val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearContract.State(linearId = UniqueIdentifier(externalId), - participants = participants.plus(me), - linearString = linearString, - linearNumber = linearNumber, - linearBoolean = linearBoolean, - linearTimestamp = linearTimestamp)) + addOutputState(DummyLinearContract.State( + linearId = UniqueIdentifier(externalId), + participants = participants.plus(me), + linearString = linearString, + linearNumber = linearNumber, + linearBoolean = linearBoolean, + linearTimestamp = linearTimestamp)) signWith(DUMMY_NOTARY_KEY) } @@ -201,7 +201,7 @@ fun ServiceHub.consumeAndProduce(stateAndRef: StateAndRef): // Create a txn consuming different contract types val producedTx = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { addOutputState(DummyLinearContract.State(linearId = stateAndRef.state.data.linearId, - participants = stateAndRef.state.data.participants)) + participants = stateAndRef.state.data.participants)) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() @@ -222,7 +222,7 @@ fun ServiceHub.evolveLinearStates(linearStates: List>) fun ServiceHub.evolveLinearState(linearState: StateAndRef) : StateAndRef = consumeAndProduce(linearState) @JvmOverloads -fun ServiceHub.consumeCash(amount: Amount, to: Party = CHARLIE) { +fun ServiceHub.consumeCash(amount: Amount, to: Party = CHARLIE): Vault { // A tx that spends our money. val spendTX = TransactionType.General.Builder(DUMMY_NOTARY).apply { vaultService.generateSpend(this, amount, to) @@ -230,4 +230,10 @@ fun ServiceHub.consumeCash(amount: Amount, to: Party = CHARLIE) { }.toSignedTransaction(checkSufficientSignatures = false) recordTransactions(spendTX) + + // Get all the StateRefs of all the generated transactions. + val states = spendTX.tx.outputs.indices.map { i -> spendTX.tx.outRef(i) } + + return Vault(states) } + diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt index 5e77d52cbb..809f3fd4b6 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -2,14 +2,15 @@ package net.corda.testing.driver -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.* import com.typesafe.config.Config import com.typesafe.config.ConfigRenderOptions import net.corda.client.rpc.CordaRPCClient import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode +import net.corda.cordform.NodeDefinition import net.corda.core.* +import net.corda.core.concurrent.firstOf import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.appendToCommonName import net.corda.core.crypto.commonName @@ -31,9 +32,12 @@ import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.nodeapi.User import net.corda.nodeapi.config.SSLConfiguration import net.corda.nodeapi.config.parseAs -import net.corda.nodeapi.internal.ShutdownHook import net.corda.nodeapi.internal.addShutdownHook -import net.corda.testing.MOCK_VERSION_INFO +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.node.MOCK_VERSION_INFO import okhttp3.OkHttpClient import okhttp3.Request import org.bouncycastle.asn1.x500.X500Name @@ -161,13 +165,13 @@ sealed class NodeHandle { abstract val nodeInfo: NodeInfo abstract val rpc: CordaRPCOps abstract val configuration: FullNodeConfiguration - abstract val webAddress: HostAndPort + abstract val webAddress: NetworkHostAndPort data class OutOfProcess( override val nodeInfo: NodeInfo, override val rpc: CordaRPCOps, override val configuration: FullNodeConfiguration, - override val webAddress: HostAndPort, + override val webAddress: NetworkHostAndPort, val debugPort: Int?, val process: Process ) : NodeHandle() @@ -176,7 +180,7 @@ sealed class NodeHandle { override val nodeInfo: NodeInfo, override val rpc: CordaRPCOps, override val configuration: FullNodeConfiguration, - override val webAddress: HostAndPort, + override val webAddress: NetworkHostAndPort, val node: Node, val nodeThread: Thread ) : NodeHandle() @@ -185,13 +189,13 @@ sealed class NodeHandle { } data class WebserverHandle( - val listenAddress: HostAndPort, + val listenAddress: NetworkHostAndPort, val process: Process ) sealed class PortAllocation { abstract fun nextPort(): Int - fun nextHostAndPort(): HostAndPort = HostAndPort.fromParts("localhost", nextPort()) + fun nextHostAndPort() = NetworkHostAndPort("localhost", nextPort()) class Incremental(startingPort: Int) : PortAllocation() { val portCounter = AtomicInteger(startingPort) @@ -275,19 +279,16 @@ fun genericD coerce: (D) -> DI, dsl: DI.() -> A ): A { - var shutdownHook: ShutdownHook? = null + val shutdownHook = addShutdownHook(driverDsl::shutdown) try { driverDsl.start() - shutdownHook = addShutdownHook { - driverDsl.shutdown() - } return dsl(coerce(driverDsl)) } catch (exception: Throwable) { log.error("Driver shutting down because of exception", exception) throw exception } finally { driverDsl.shutdown() - shutdownHook?.cancel() + shutdownHook.cancel() } } @@ -295,19 +296,19 @@ fun getTimestampAsDirectoryName(): String { return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(UTC).format(Instant.now()) } -class ListenProcessDeathException(message: String) : Exception(message) +class ListenProcessDeathException(hostAndPort: NetworkHostAndPort, listenProcess: Process) : Exception("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}") /** * @throws ListenProcessDeathException if [listenProcess] dies before the check succeeds, i.e. the check can't succeed as intended. */ -fun addressMustBeBound(executorService: ScheduledExecutorService, hostAndPort: HostAndPort, listenProcess: Process? = null) { +fun addressMustBeBound(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, listenProcess: Process? = null) { addressMustBeBoundFuture(executorService, hostAndPort, listenProcess).getOrThrow() } -fun addressMustBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: HostAndPort, listenProcess: Process? = null): ListenableFuture { +fun addressMustBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, listenProcess: Process? = null): ListenableFuture { return poll(executorService, "address $hostAndPort to bind") { if (listenProcess != null && !listenProcess.isAlive) { - throw ListenProcessDeathException("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}") + throw ListenProcessDeathException(hostAndPort, listenProcess) } try { Socket(hostAndPort.host, hostAndPort.port).close() @@ -318,11 +319,11 @@ fun addressMustBeBoundFuture(executorService: ScheduledExecutorService, hostAndP } } -fun addressMustNotBeBound(executorService: ScheduledExecutorService, hostAndPort: HostAndPort) { +fun addressMustNotBeBound(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort) { addressMustNotBeBoundFuture(executorService, hostAndPort).getOrThrow() } -fun addressMustNotBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: HostAndPort): ListenableFuture { +fun addressMustNotBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort): ListenableFuture { return poll(executorService, "address $hostAndPort to unbind") { try { Socket(hostAndPort.host, hostAndPort.port).close() @@ -340,33 +341,27 @@ fun poll( warnCount: Int = 120, check: () -> A? ): ListenableFuture { - val initialResult = check() val resultFuture = SettableFuture.create() - if (initialResult != null) { - resultFuture.set(initialResult) - return resultFuture - } - var counter = 0 - fun schedulePoll() { - executorService.schedule(task@ { - counter++ - if (counter == warnCount) { + val task = object : Runnable { + var counter = -1 + override fun run() { + if (resultFuture.isCancelled) return // Give up, caller can no longer get the result. + if (++counter == warnCount) { log.warn("Been polling $pollName for ${pollInterval.multipliedBy(warnCount.toLong()).seconds} seconds...") } - val result = try { - check() + try { + val checkResult = check() + if (checkResult != null) { + resultFuture.set(checkResult) + } else { + executorService.schedule(this, pollInterval.toMillis(), MILLISECONDS) + } } catch (t: Throwable) { resultFuture.setException(t) - return@task } - if (result == null) { - schedulePoll() - } else { - resultFuture.set(result) - } - }, pollInterval.toMillis(), MILLISECONDS) + } } - schedulePoll() + executorService.submit(task) // The check may be expensive, so always run it in the background even the first time. return resultFuture } @@ -392,7 +387,7 @@ class ShutdownManager(private val executorService: ExecutorService) { } fun shutdown() { - val shutdownFutures = state.locked { + val shutdownActionFutures = state.locked { if (isShutdown) { emptyList Unit>>() } else { @@ -400,21 +395,16 @@ class ShutdownManager(private val executorService: ExecutorService) { registeredShutdowns } } - val shutdowns = shutdownFutures.map { ErrorOr.catch { it.getOrThrow(1.seconds) } } - shutdowns.reversed().forEach { errorOrShutdown -> - errorOrShutdown.match( - onValue = { shutdown -> - try { - shutdown() - } catch (throwable: Throwable) { - log.error("Exception while shutting down", throwable) - } - }, - onError = { error -> - log.error("Exception while getting shutdown method, disregarding", error) - } - ) - } + val shutdowns = shutdownActionFutures.map { Try.on { it.getOrThrow(1.seconds) } } + shutdowns.reversed().forEach { when (it) { + is Try.Success -> + try { + it.value() + } catch (t: Throwable) { + log.warn("Exception while shutting down", t) + } + is Try.Failure -> log.warn("Exception while getting shutdown method, disregarding", it.exception) + } } } fun registerShutdown(shutdown: ListenableFuture<() -> Unit>) { @@ -518,21 +508,28 @@ class DriverDSL( _executorService?.shutdownNow() } - private fun establishRpc(nodeAddress: HostAndPort, sslConfig: SSLConfiguration): ListenableFuture { + private fun establishRpc(nodeAddress: NetworkHostAndPort, sslConfig: SSLConfiguration, processDeathFuture: ListenableFuture): ListenableFuture { val client = CordaRPCClient(nodeAddress, sslConfig) - return poll(executorService, "for RPC connection") { + val connectionFuture = poll(executorService, "RPC connection") { try { - val connection = client.start(ArtemisMessagingComponent.NODE_USER, ArtemisMessagingComponent.NODE_USER) - shutdownManager.registerShutdown { connection.close() } - return@poll connection.proxy - } catch(e: Exception) { + client.start(ArtemisMessagingComponent.NODE_USER, ArtemisMessagingComponent.NODE_USER) + } catch (e: Exception) { + if (processDeathFuture.isDone) throw e log.error("Exception $e, Retrying RPC connection at $nodeAddress") null } } + return firstOf(connectionFuture, processDeathFuture) { + if (it == processDeathFuture) { + throw processDeathFuture.getOrThrow() + } + val connection = connectionFuture.getOrThrow() + shutdownManager.registerShutdown(connection::close) + connection.proxy + } } - private fun networkMapServiceConfigLookup(networkMapCandidates: List): (X500Name) -> Map? { + private fun networkMapServiceConfigLookup(networkMapCandidates: List): (X500Name) -> Map? { return networkMapStartStrategy.run { when (this) { is NetworkMapStartStrategy.Dedicated -> { @@ -541,9 +538,9 @@ class DriverDSL( } } is NetworkMapStartStrategy.Nominated -> { - serviceConfig(HostAndPort.fromString(networkMapCandidates.filter { + serviceConfig(networkMapCandidates.filter { it.name == legalName.toString() - }.single().config.getString("p2pAddress"))).let { + }.single().config.getString("p2pAddress").parseNetworkHostAndPort()).let { { nodeName: X500Name -> if (nodeName == legalName) null else it } } } @@ -564,6 +561,10 @@ class DriverDSL( val webAddress = portAllocation.nextHostAndPort() // TODO: Derive name from the full picked name, don't just wrap the common name val name = providedName ?: X509Utilities.getX509Name("${oneOf(names).commonName}-${p2pAddress.port}","London","demo@r3.com",null) + val networkMapServiceConfigLookup = networkMapServiceConfigLookup(listOf(object : NodeDefinition { + override fun getName() = name.toString() + override fun getConfig() = configOf("p2pAddress" to p2pAddress.toString()) + })) val config = ConfigHelper.loadConfig( baseDirectory = baseDirectory(name), allowMissingConfig = true, @@ -573,7 +574,7 @@ class DriverDSL( "rpcAddress" to rpcAddress.toString(), "webAddress" to webAddress.toString(), "extraAdvertisedServiceIds" to advertisedServices.map { it.toString() }, - "networkMapService" to networkMapServiceConfigLookup(emptyList())(name), + "networkMapService" to networkMapServiceConfigLookup(name), "useTestClock" to useTestClock, "rpcUsers" to rpcUsers.map { it.toMap() }, "verifierType" to verifierType.name @@ -697,7 +698,7 @@ class DriverDSL( return startNodeInternal(config, webAddress, startInProcess) } - private fun startNodeInternal(config: Config, webAddress: HostAndPort, startInProcess: Boolean?): ListenableFuture { + private fun startNodeInternal(config: Config, webAddress: NetworkHostAndPort, startInProcess: Boolean?): ListenableFuture { val nodeConfiguration = config.parseAs() if (startInProcess ?: startNodesInProcess) { val nodeAndThreadFuture = startInProcessNode(executorService, nodeConfiguration, config) @@ -708,7 +709,7 @@ class DriverDSL( } } ) return nodeAndThreadFuture.flatMap { (node, thread) -> - establishRpc(nodeConfiguration.p2pAddress, nodeConfiguration).flatMap { rpc -> + establishRpc(nodeConfiguration.p2pAddress, nodeConfiguration, SettableFuture.create()).flatMap { rpc -> rpc.waitUntilRegisteredWithNetworkMap().map { NodeHandle.InProcess(rpc.nodeIdentity(), rpc, nodeConfiguration, webAddress, node, thread) } @@ -719,9 +720,20 @@ class DriverDSL( val processFuture = startOutOfProcessNode(executorService, nodeConfiguration, config, quasarJarPath, debugPort, systemProperties, callerPackage) registerProcess(processFuture) return processFuture.flatMap { process -> + val processDeathFuture = poll(executorService, "process death") { + if (process.isAlive) null else ListenProcessDeathException(nodeConfiguration.p2pAddress, process) + } // We continue to use SSL enabled port for RPC when its for node user. - establishRpc(nodeConfiguration.p2pAddress, nodeConfiguration).flatMap { rpc -> - rpc.waitUntilRegisteredWithNetworkMap().map { + establishRpc(nodeConfiguration.p2pAddress, nodeConfiguration, processDeathFuture).flatMap { rpc -> + // Call waitUntilRegisteredWithNetworkMap in background in case RPC is failing over: + val networkMapFuture = executorService.submit(Callable { + rpc.waitUntilRegisteredWithNetworkMap() + }).flatMap { it } + firstOf(processDeathFuture, networkMapFuture) { + if (it == processDeathFuture) { + throw processDeathFuture.getOrThrow() + } + processDeathFuture.cancel(false) NodeHandle.OutOfProcess(rpc.nodeIdentity(), rpc, nodeConfiguration, webAddress, debugPort, process) } } @@ -792,7 +804,7 @@ class DriverDSL( "-javaagent:$quasarJarPath" val loggingLevel = if (debugPort == null) "INFO" else "DEBUG" - ProcessUtilities.startJavaProcess( + ProcessUtilities.startCordaProcess( className = "net.corda.node.Corda", // cannot directly get class for this, so just use string arguments = listOf( "--base-directory=${nodeConf.baseDirectory}", @@ -817,7 +829,7 @@ class DriverDSL( ): ListenableFuture { return executorService.submit { val className = "net.corda.webserver.WebServer" - ProcessUtilities.startJavaProcess( + ProcessUtilities.startCordaProcess( className = className, // cannot directly get class for this, so just use string arguments = listOf("--base-directory", handle.configuration.baseDirectory.toString()), jdwpPort = debugPort, diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt index 93a42686d4..e185930198 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt @@ -1,13 +1,13 @@ package net.corda.testing.driver -import com.google.common.net.HostAndPort -import net.corda.core.utilities.DUMMY_MAP +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.testing.DUMMY_MAP import org.bouncycastle.asn1.x500.X500Name sealed class NetworkMapStartStrategy { internal abstract val startDedicated: Boolean internal abstract val legalName: X500Name - internal fun serviceConfig(address: HostAndPort) = mapOf( + internal fun serviceConfig(address: NetworkHostAndPort) = mapOf( "address" to address.toString(), "legalName" to legalName.toString() ) diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt new file mode 100644 index 0000000000..7ac9eedf94 --- /dev/null +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt @@ -0,0 +1,57 @@ +package net.corda.testing.driver + +import net.corda.core.div +import net.corda.core.exists +import java.io.File.pathSeparator +import java.nio.file.Path + +object ProcessUtilities { + inline fun startJavaProcess( + arguments: List, + jdwpPort: Int? = null + ): Process { + return startJavaProcessImpl(C::class.java.name, arguments, defaultClassPath, jdwpPort, emptyList(), null, null) + } + + fun startCordaProcess( + className: String, + arguments: List, + jdwpPort: Int?, + extraJvmArguments: List, + errorLogPath: Path?, + workingDirectory: Path? = null + ): Process { + // FIXME: Instead of hacking our classpath, use the correct classpath for className. + val classpath = defaultClassPath.split(pathSeparator).filter { !(it / "log4j2-test.xml").exists() }.joinToString(pathSeparator) + return startJavaProcessImpl(className, arguments, classpath, jdwpPort, extraJvmArguments, errorLogPath, workingDirectory) + } + + fun startJavaProcessImpl( + className: String, + arguments: List, + classpath: String, + jdwpPort: Int?, + extraJvmArguments: List, + errorLogPath: Path?, + workingDirectory: Path? + ): Process { + val command = mutableListOf().apply { + add((System.getProperty("java.home") / "bin" / "java").toString()) + (jdwpPort != null) && add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$jdwpPort") + add("-Xmx200m") + add("-XX:+UseG1GC") + addAll(extraJvmArguments) + add("-cp") + add(classpath) + add(className) + addAll(arguments) + } + return ProcessBuilder(command).apply { + if (errorLogPath != null) redirectError(errorLogPath.toFile()) // FIXME: Undone by inheritIO. + inheritIO() + if (workingDirectory != null) directory(workingDirectory.toFile()) + }.start() + } + + val defaultClassPath: String get() = System.getProperty("java.class.path") +} diff --git a/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt b/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt index 42b3221264..dbbe571af5 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt @@ -1,7 +1,7 @@ package net.corda.testing.http import com.fasterxml.jackson.databind.ObjectMapper -import com.google.common.net.HostAndPort +import net.corda.core.utilities.NetworkHostAndPort import java.net.URL class HttpApi(val root: URL, val mapper: ObjectMapper = defaultMapper) { @@ -27,7 +27,7 @@ class HttpApi(val root: URL, val mapper: ObjectMapper = defaultMapper) { private fun toJson(any: Any) = any as? String ?: HttpUtils.defaultMapper.writeValueAsString(any) companion object { - fun fromHostAndPort(hostAndPort: HostAndPort, base: String, protocol: String = "http", mapper: ObjectMapper = defaultMapper): HttpApi + fun fromHostAndPort(hostAndPort: NetworkHostAndPort, base: String, protocol: String = "http", mapper: ObjectMapper = defaultMapper): HttpApi = HttpApi(URL("$protocol://$hostAndPort/$base/"), mapper) private val defaultMapper: ObjectMapper by lazy { net.corda.jackson.JacksonSupport.createNonRpcMapper() diff --git a/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt b/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt index dd521c8671..12ac044952 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt @@ -1,6 +1,6 @@ package net.corda.testing.messaging -import com.google.common.net.HostAndPort +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection @@ -12,7 +12,7 @@ import org.bouncycastle.asn1.x500.X500Name /** * As the name suggests this is a simple client for connecting to MQ brokers. */ -class SimpleMQClient(val target: HostAndPort, +class SimpleMQClient(val target: NetworkHostAndPort, override val config: SSLConfiguration? = configureTestSSL(DEFAULT_MQ_LEGAL_NAME)) : ArtemisMessagingComponent() { companion object { val DEFAULT_MQ_LEGAL_NAME = X500Name("CN=SimpleMQClient,O=R3,OU=corda,L=London,C=GB") diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt b/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt index ffe15ee54a..b5546e9fc4 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt @@ -82,6 +82,8 @@ class InMemoryMessagingNetwork( // Holds the mapping from services to peers advertising the service. private val serviceToPeersMapping = HashMap>() + // Holds the mapping from node's X500Name to PeerHandle. + private val peersMapping = HashMap() @Suppress("unused") // Used by the visualiser tool. /** A stream of (sender, message, recipients) triples */ @@ -127,10 +129,12 @@ class InMemoryMessagingNetwork( id: Int, executor: AffinityExecutor, advertisedServices: List, - description: X500Name? = null, + description: X500Name = X509Utilities.getX509Name("In memory node $id","London","demo@r3.com",null), database: Database) : MessagingServiceBuilder { - return Builder(manuallyPumped, PeerHandle(id, description ?: X509Utilities.getX509Name("In memory node $id","London","demo@r3.com",null)), advertisedServices.map(::ServiceHandle), executor, database = database) + val peerHandle = PeerHandle(id, description) + peersMapping[peerHandle.description] = peerHandle // Assume that the same name - the same entity in MockNetwork. + return Builder(manuallyPumped, peerHandle, advertisedServices.map(::ServiceHandle), executor, database = database) } interface LatencyCalculator { @@ -330,7 +334,7 @@ class InMemoryMessagingNetwork( override fun getAddressOfParty(partyInfo: PartyInfo): MessageRecipients { return when (partyInfo) { - is PartyInfo.Node -> partyInfo.node.address + is PartyInfo.Node -> peersMapping[partyInfo.party.name] ?: throw IllegalArgumentException("No MockNode for party ${partyInfo.party.name}") is PartyInfo.Service -> ServiceHandle(partyInfo.service) } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt index 47bd3bf6e2..a9b26e75bb 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt @@ -3,12 +3,12 @@ package net.corda.testing.node import co.paralleluniverse.common.util.VisibleForTesting import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.Party -import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo +import net.corda.core.node.ServiceHub import net.corda.core.node.services.NetworkMapCache -import net.corda.core.utilities.getTestPartyAndCertificate +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.network.InMemoryNetworkMapCache -import net.corda.testing.MOCK_VERSION_INFO +import net.corda.testing.getTestPartyAndCertificate import net.corda.testing.getTestX509Name import rx.Observable import rx.subjects.PublishSubject @@ -17,19 +17,19 @@ import java.math.BigInteger /** * Network map cache with no backing map service. */ -class MockNetworkMapCache : InMemoryNetworkMapCache() { +class MockNetworkMapCache(serviceHub: ServiceHub) : InMemoryNetworkMapCache(serviceHub) { private companion object { val BANK_C = getTestPartyAndCertificate(getTestX509Name("Bank C"), entropyToKeyPair(BigInteger.valueOf(1000)).public) val BANK_D = getTestPartyAndCertificate(getTestX509Name("Bank D"), entropyToKeyPair(BigInteger.valueOf(2000)).public) + val BANK_C_ADDR = NetworkHostAndPort("bankC", 8080) + val BANK_D_ADDR = NetworkHostAndPort("bankD", 8080) } override val changed: Observable = PublishSubject.create() - data class MockAddress(val id: String) : SingleMessageRecipient - init { - val mockNodeA = NodeInfo(MockAddress("bankC:8080"), BANK_C, MOCK_VERSION_INFO.platformVersion) - val mockNodeB = NodeInfo(MockAddress("bankD:8080"), BANK_D, MOCK_VERSION_INFO.platformVersion) + val mockNodeA = NodeInfo(listOf(BANK_C_ADDR), BANK_C, setOf(BANK_C), 1) + val mockNodeB = NodeInfo(listOf(BANK_D_ADDR), BANK_D, setOf(BANK_D), 1) registeredNodes[mockNodeA.legalIdentity.owningKey] = mockNodeA registeredNodes[mockNodeB.legalIdentity.owningKey] = mockNodeB runWithoutMapService() diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index 6a068d60db..aaf1abb4ad 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -9,18 +9,19 @@ import net.corda.core.* import net.corda.core.crypto.CertificateAndKeyPair import net.corda.core.crypto.cert import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.random63BitValue import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.CordaPluginRegistry -import net.corda.core.node.PhysicalLocation import net.corda.core.node.ServiceEntry -import net.corda.core.node.services.* -import net.corda.core.utilities.DUMMY_NOTARY_KEY -import net.corda.core.utilities.getTestPartyAndCertificate +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.node.WorldMapLocation +import net.corda.core.node.services.IdentityService +import net.corda.core.node.services.KeyManagementService +import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.loggerFor -import net.corda.flows.TxKeyFlow import net.corda.node.internal.AbstractNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.identity.InMemoryIdentityService @@ -29,20 +30,17 @@ import net.corda.node.services.messaging.MessagingService import net.corda.node.services.network.InMemoryNetworkMapService import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.InMemoryTransactionVerifierService -import net.corda.node.services.transactions.InMemoryUniquenessProvider import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor -import net.corda.testing.MOCK_VERSION_INFO -import net.corda.testing.getTestX509Name -import net.corda.testing.testNodeConfiguration +import net.corda.testing.* import org.apache.activemq.artemis.utils.ReusableLatch import org.bouncycastle.asn1.x500.X500Name import org.slf4j.Logger import java.math.BigInteger import java.nio.file.FileSystem +import java.nio.file.Path import java.security.KeyPair import java.security.cert.X509Certificate import java.util.* @@ -147,7 +145,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, AbstractNode(config, advertisedServices, TestClock(), mockNet.busyLatch) { var counter = entropyRoot override val log: Logger = loggerFor() - override val platformVersion: Int get() = MOCK_VERSION_INFO.platformVersion + override val platformVersion: Int get() = 1 override val serverThread: AffinityExecutor = if (mockNet.threadPerNode) ServiceAffinityExecutor("Mock node $id thread", 1) @@ -181,8 +179,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, trustRoot = trustRoot, caCertificates = *caCertificates) } - override fun makeVaultService(dataSourceProperties: Properties): VaultService = NodeVaultService(services, dataSourceProperties) - override fun makeKeyManagementService(identityService: IdentityService): KeyManagementService { return E2ETestKeyManagementService(identityService, partyKeys + (overrideServices?.values ?: emptySet())) } @@ -222,12 +218,12 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override fun noNetworkMapConfigured(): ListenableFuture = Futures.immediateFuture(Unit) // There is no need to slow down the unit tests by initialising CityDatabase - override fun findMyLocation(): PhysicalLocation? = null - - override fun makeUniquenessProvider(type: ServiceType): UniquenessProvider = InMemoryUniquenessProvider() + override fun findMyLocation(): WorldMapLocation? = null override fun makeTransactionVerifierService() = InMemoryTransactionVerifierService(1) + override fun myAddresses() = emptyList() + override fun start(): MockNode { super.start() mockNet.identities.add(info.legalIdentityAndCert) @@ -242,7 +238,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, // This does not indirect through the NodeInfo object so it can be called before the node is started. // It is used from the network visualiser tool. - @Suppress("unused") val place: PhysicalLocation get() = findMyLocation()!! + @Suppress("unused") val place: WorldMapLocation get() = findMyLocation()!! fun pumpReceive(block: Boolean = false): InMemoryMessagingNetwork.MessageTransfer? { return (network as InMemoryMessagingNetwork.InMemoryMessaging).pumpReceive(block) @@ -302,8 +298,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, val node = nodeFactory.create(config, this, networkMapAddress, advertisedServices.toSet(), id, overrideServices, entropyRoot) if (start) { node.setup().start() - // Register flows that are normally found via plugins - node.registerInitiatedFlow(TxKeyFlow.Provider::class.java) if (threadPerNode && networkMapAddress != null) node.networkMapRegistrationFuture.getOrThrow() // Block and wait for the node to register in the net map. } @@ -311,7 +305,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, return node } - fun baseDirectory(nodeId: Int) = filesystem.getPath("/nodes/$nodeId") + fun baseDirectory(nodeId: Int): Path = filesystem.getPath("/nodes/$nodeId") /** * Asks every node in order to process any queued up inbound messages. This may in turn result in nodes @@ -351,7 +345,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, null return Pair( createNode(null, -1, nodeFactory, true, firstNodeName, notaryOverride, BigInteger.valueOf(random63BitValue()), ServiceInfo(NetworkMapService.type), notaryServiceInfo), - createNode(nodes[0].info.address, -1, nodeFactory, true, secondNodeName) + createNode(nodes[0].network.myAddress, -1, nodeFactory, true, secondNodeName) ) } @@ -374,11 +368,12 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, else null val mapNode = createNode(null, nodeFactory = nodeFactory, advertisedServices = ServiceInfo(NetworkMapService.type)) - val notaryNode = createNode(mapNode.info.address, nodeFactory = nodeFactory, overrideServices = notaryOverride, + val mapAddress = mapNode.network.myAddress + val notaryNode = createNode(mapAddress, nodeFactory = nodeFactory, overrideServices = notaryOverride, advertisedServices = notaryServiceInfo) val nodes = ArrayList() repeat(numPartyNodes) { - nodes += createPartyNode(mapNode.info.address) + nodes += createPartyNode(mapAddress) } nodes.forEach { itNode -> nodes.map { it.info.legalIdentityAndCert }.forEach(itNode.services.identityService::registerIdentity) @@ -418,7 +413,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } fun stopNodes() { - require(nodes.isNotEmpty()) nodes.forEach { if (it.started) it.stop() } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index 2126f18897..9e1fba3192 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -4,15 +4,17 @@ import net.corda.core.contracts.Attachment import net.corda.core.crypto.* import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.PartyAndCertificate -import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.messaging.DataFeed import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_CA -import net.corda.core.utilities.getTestPartyAndCertificate +import net.corda.flows.AnonymisedIdentity +import net.corda.node.VersionInfo +import net.corda.node.services.api.StateMachineRecordedTransactionMappingStorage +import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.keys.freshCertificate @@ -22,10 +24,10 @@ import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.vault.NodeVaultService +import net.corda.testing.DUMMY_CA import net.corda.testing.MEGA_CORP import net.corda.testing.MOCK_IDENTITIES -import net.corda.testing.MOCK_VERSION_INFO -import org.bouncycastle.cert.X509CertificateHolder +import net.corda.testing.getTestPartyAndCertificate import org.bouncycastle.operator.ContentSigner import rx.Observable import rx.subjects.PublishSubject @@ -33,16 +35,12 @@ import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.File import java.io.InputStream -import java.nio.file.Path -import java.nio.file.Paths import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey -import java.security.cert.CertPath import java.time.Clock import java.util.* import java.util.jar.JarInputStream -import javax.annotation.concurrent.ThreadSafe // TODO: We need a single, rationalised unit testing environment that is usable for everything. Fix this! // That means it probably shouldn't be in the 'core' module, which lacks enough code to create a realistic test env. @@ -58,14 +56,16 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { override fun recordTransactions(txs: Iterable) { txs.forEach { - storageService.stateMachineRecordedTransactionMapping.addMapping(StateMachineRunId.createRandom(), it.id) + stateMachineRecordedTransactionMapping.addMapping(StateMachineRunId.createRandom(), it.id) } for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) + validatedTransactions.addTransaction(stx) } } - override val storageService: TxWritableStorageService = MockStorageService() + override val attachments: AttachmentStorage = MockAttachmentStorage() + override val validatedTransactions: WritableTransactionStorage = MockTransactionStorage() + val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage() override final val identityService: IdentityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate) override val keyManagementService: KeyManagementService = MockKeyManagementService(identityService, *keys) @@ -73,7 +73,10 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { override val vaultQueryService: VaultQueryService get() = throw UnsupportedOperationException() override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException() override val clock: Clock get() = Clock.systemUTC() - override val myInfo: NodeInfo get() = NodeInfo(object : SingleMessageRecipient {}, getTestPartyAndCertificate(MEGA_CORP.name, key.public), MOCK_VERSION_INFO.platformVersion) + override val myInfo: NodeInfo get() { + val identity = getTestPartyAndCertificate(MEGA_CORP.name, key.public) + return NodeInfo(emptyList(), identity, setOf(identity), 1) + } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) fun makeVaultService(dataSourceProps: Properties, hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService())): VaultService { @@ -99,7 +102,9 @@ class MockKeyManagementService(val identityService: IdentityService, return k.public } - override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair { + override fun filterMyKeys(candidateKeys: Iterable): Iterable = candidateKeys.filter { it in this.keys } + + override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): AnonymisedIdentity { return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled) } @@ -117,10 +122,8 @@ class MockKeyManagementService(val identityService: IdentityService, } } -class MockAttachmentStorage : AttachmentStorage { +class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { val files = HashMap() - override var automaticallyExtractAttachments = false - override var storePath: Path = Paths.get("") override fun openAttachment(id: SecureHash): Attachment? { val f = files[id] ?: return null @@ -152,9 +155,9 @@ class MockStateMachineRecordedTransactionMappingStorage( val storage: StateMachineRecordedTransactionMappingStorage = InMemoryStateMachineRecordedTransactionMappingStorage() ) : StateMachineRecordedTransactionMappingStorage by storage -open class MockTransactionStorage : TransactionStorage { - override fun track(): Pair, Observable> { - return Pair(txns.values.toList(), _updatesPublisher) +open class MockTransactionStorage : WritableTransactionStorage, SingletonSerializeAsToken() { + override fun track(): DataFeed, SignedTransaction> { + return DataFeed(txns.values.toList(), _updatesPublisher) } private val txns = HashMap() @@ -177,15 +180,6 @@ open class MockTransactionStorage : TransactionStorage { override fun getTransaction(id: SecureHash): SignedTransaction? = txns[id] } -@ThreadSafe -class MockStorageService(override val attachments: AttachmentStorage = MockAttachmentStorage(), - override val validatedTransactions: TransactionStorage = MockTransactionStorage(), - override val uploaders: List = listOf(), - override val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage()) - : SingletonSerializeAsToken(), TxWritableStorageService { - override val attachmentsClassLoaderEnabled = false -} - /** * Make properties appropriate for creating a DataSource for unit tests. * @@ -200,3 +194,5 @@ fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().to props.setProperty("dataSource.password", "") return props } + +val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index cadcc93871..81b11bd7fc 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -9,7 +9,6 @@ import net.corda.core.crypto.appendToCommonName import net.corda.core.crypto.commonName import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType -import net.corda.core.utilities.DUMMY_MAP import net.corda.core.utilities.WHITESPACE import net.corda.node.internal.Node import net.corda.node.serialization.NodeClock @@ -21,7 +20,7 @@ import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs -import net.corda.testing.MOCK_VERSION_INFO +import net.corda.testing.DUMMY_MAP import net.corda.testing.driver.addressMustNotBeBoundFuture import net.corda.testing.getFreeLocalPorts import org.apache.logging.log4j.Level diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt index 70f66961ca..d006e380ac 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt @@ -1,13 +1,13 @@ package net.corda.testing.node import com.codahale.metrics.MetricRegistry -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.SettableFuture import net.corda.core.crypto.commonName import net.corda.core.crypto.generateKeyPair import net.corda.core.messaging.RPCOps import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.api.MonitoringService import net.corda.node.services.config.NodeConfiguration @@ -19,7 +19,6 @@ import net.corda.node.services.network.InMemoryNetworkMapCache import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction -import net.corda.testing.MOCK_VERSION_INFO import net.corda.testing.freeLocalHostAndPort import org.jetbrains.exposed.sql.Database import java.io.Closeable @@ -31,8 +30,8 @@ import kotlin.concurrent.thread * This is a bare-bones node which can only send and receive messages. It doesn't register with a network map service or * any other such task that would make it functional in a network and thus left to the user to do so manually. */ -class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeLocalHostAndPort(), - rpcAddress: HostAndPort = freeLocalHostAndPort(), +class SimpleNode(val config: NodeConfiguration, val address: NetworkHostAndPort = freeLocalHostAndPort(), + rpcAddress: NetworkHostAndPort = freeLocalHostAndPort(), trustRoot: X509Certificate) : AutoCloseable { private val databaseWithCloseable: Pair = configureDatabase(config.dataSourceProperties) @@ -43,7 +42,8 @@ class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeL val identityService: IdentityService = InMemoryIdentityService(trustRoot = trustRoot) val keyService: KeyManagementService = E2ETestKeyManagementService(identityService, setOf(identity)) val executor = ServiceAffinityExecutor(config.myLegalName.commonName, 1) - val broker = ArtemisMessagingServer(config, address.port, rpcAddress.port, InMemoryNetworkMapCache(), userService) + // TODO: We should have a dummy service hub rather than change behaviour in tests + val broker = ArtemisMessagingServer(config, address.port, rpcAddress.port, InMemoryNetworkMapCache(serviceHub = null), userService) val networkMapRegistrationFuture: SettableFuture = SettableFuture.create() val network = database.transaction { NodeMessagingClient( diff --git a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt new file mode 100644 index 0000000000..c59a09b192 --- /dev/null +++ b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt @@ -0,0 +1,33 @@ +package net.corda.testing.schemas + +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.schemas.CommonSchemaV1 +import net.corda.core.schemas.MappedSchema +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table +import javax.persistence.Transient + +/** + * 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 : MappedSchema(schemaFamily = DummyDealStateSchema.javaClass, version = 1, mappedTypes = listOf(PersistentDummyDealState::class.java)) { + @Entity + @Table(name = "dummy_deal_states") + class PersistentDummyDealState( + + @Column(name = "deal_reference") + var dealReference: String, + + /** parent attributes */ + @Transient + val uid: UniqueIdentifier + + ) : CommonSchemaV1.LinearState(uid = uid) +} diff --git a/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV1.kt b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt similarity index 92% rename from core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV1.kt rename to test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt index a0c3c82649..b8e490b04f 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV1.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt @@ -1,5 +1,7 @@ -package net.corda.core.schemas +package net.corda.testing.schemas +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState import java.time.Instant import java.util.* import javax.persistence.Column diff --git a/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV2.kt b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt similarity index 64% rename from core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV2.kt rename to test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt index 247c061a0f..44b2df08e0 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/DummyLinearStateSchemaV2.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt @@ -1,6 +1,8 @@ -package net.corda.core.schemas +package net.corda.testing.schemas -import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1 +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.schemas.CommonSchemaV1 +import net.corda.core.schemas.MappedSchema import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Table @@ -9,8 +11,8 @@ import javax.persistence.Table * Second version of a cash contract ORM schema that extends the common * [VaultLinearState] abstract schema */ -object DummyLinearStateSchemaV2 : net.corda.core.schemas.MappedSchema(schemaFamily = DummyLinearStateSchema.javaClass, version = 2, - mappedTypes = listOf(PersistentDummyLinearState::class.java)) { +object DummyLinearStateSchemaV2 : MappedSchema(schemaFamily = DummyLinearStateSchema.javaClass, version = 2, + mappedTypes = listOf(PersistentDummyLinearState::class.java)) { @Entity @Table(name = "dummy_linear_states_v2") class PersistentDummyLinearState( @@ -24,6 +26,6 @@ object DummyLinearStateSchemaV2 : net.corda.core.schemas.MappedSchema(schemaFami /** parent attributes */ @Transient - val uid: net.corda.core.contracts.UniqueIdentifier + val uid: UniqueIdentifier ) : CommonSchemaV1.LinearState(uid = uid) } diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index 18c12a3b62..8d71db2137 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -66,7 +66,7 @@ dependencies { compile ':terminal-331a005d6793e52cefc9e2cec6774e62d5a546b1' compile ':pty4j-0.7.2' - testCompile project(':node') + testCompile project(':test-utils') testCompile project(':webserver') testCompile "junit:junit:$junit_version" diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt index c546657e7b..cfec8b3757 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt @@ -1,7 +1,7 @@ package net.corda.demobench.model -import com.google.common.net.HostAndPort import com.typesafe.config.Config +import net.corda.core.utilities.parseNetworkHostAndPort import org.bouncycastle.asn1.x500.X500Name import tornadofx.* import java.io.IOException @@ -46,7 +46,7 @@ class InstallFactory : Controller() { private fun Config.parsePort(path: String): Int { val address = this.getString(path) - val port = HostAndPort.fromString(address).port + val port = address.parseNetworkHostAndPort().port require(nodeController.isPortValid(port), { "Invalid port $port from '$path'." }) return port } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt index e3d1f95c13..baeb40ba59 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt @@ -1,9 +1,9 @@ package net.corda.demobench.rpc -import com.google.common.net.HostAndPort import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCConnection import net.corda.core.messaging.CordaRPCOps +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.demobench.model.NodeConfig import java.util.* @@ -16,7 +16,7 @@ class NodeRPC(config: NodeConfig, start: (NodeConfig, CordaRPCOps) -> Unit, invo val oneSecond = SECONDS.toMillis(1) } - private val rpcClient = CordaRPCClient(HostAndPort.fromParts("localhost", config.rpcPort)) + private val rpcClient = CordaRPCClient(NetworkHostAndPort("localhost", config.rpcPort)) private var rpcConnection: CordaRPCConnection? = null private val timer = Timer() diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt index d63c7ff366..0049c9e194 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt @@ -18,7 +18,7 @@ import net.corda.core.crypto.commonName import net.corda.core.div import net.corda.core.exists import net.corda.core.node.CityDatabase -import net.corda.core.node.PhysicalLocation +import net.corda.core.node.WorldMapLocation import net.corda.core.readAllLines import net.corda.core.utilities.normaliseLegalName import net.corda.core.utilities.validateLegalName @@ -211,7 +211,7 @@ class NodeTabView : Fragment() { CityDatabase.cityMap.values.map { it.countryCode }.toSet().map { it to Image(resources["/net/corda/demobench/flags/$it.png"]) }.toMap() } - private fun Pane.nearestCityField(): ComboBox { + private fun Pane.nearestCityField(): ComboBox { return combobox(model.nearestCity, CityDatabase.cityMap.values.toList().sortedBy { it.description }) { minWidth = textWidth styleClass += "city-picker" @@ -229,9 +229,9 @@ class NodeTabView : Fragment() { if (it == null) error("Please select a city") else null } - converter = object : StringConverter() { - override fun toString(loc: PhysicalLocation?) = loc?.description ?: "" - override fun fromString(string: String): PhysicalLocation? = CityDatabase[string] + converter = object : StringConverter() { + override fun toString(loc: WorldMapLocation?) = loc?.description ?: "" + override fun fromString(string: String): WorldMapLocation? = CityDatabase[string] } value = CityDatabase["London"] diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt index 7a44b60604..d2d81031c6 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt @@ -18,8 +18,7 @@ import javafx.scene.layout.HBox import javafx.scene.layout.VBox import javafx.util.Duration import net.corda.core.crypto.commonName -import net.corda.core.failure -import net.corda.core.success +import net.corda.core.match import net.corda.core.then import net.corda.core.messaging.CordaRPCOps import net.corda.demobench.explorer.ExplorerController @@ -157,19 +156,20 @@ class NodeTerminalView : Fragment() { launchWebButton.graphic = ProgressIndicator() log.info("Starting web server for ${config.legalName}") - webServer.open(config) then { + webServer.open(config).then { Platform.runLater { launchWebButton.graphic = null } - } success { - log.info("Web server for ${config.legalName} started on $it") - Platform.runLater { - webURL = it - launchWebButton.text = "Reopen\nweb site" - app.hostServices.showDocument(it.toString()) - } - } failure { - launchWebButton.text = oldLabel + it.match({ + log.info("Web server for ${config.legalName} started on $it") + Platform.runLater { + webURL = it + launchWebButton.text = "Reopen\nweb site" + app.hostServices.showDocument(it.toString()) + } + }, { + launchWebButton.text = oldLabel + }) } } } diff --git a/tools/demobench/src/main/resources/log4j2.xml b/tools/demobench/src/main/resources/log4j2.xml index 526b1bdb46..fc1846617f 100644 --- a/tools/demobench/src/main/resources/log4j2.xml +++ b/tools/demobench/src/main/resources/log4j2.xml @@ -18,7 +18,7 @@ fileName="${sys:log-path}/${log-name}.log" filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz"> - + diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index 6d9b04d3e5..904a90f717 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -3,11 +3,11 @@ package net.corda.demobench.model import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature -import com.google.common.net.HostAndPort import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigValueFactory import net.corda.core.div -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.testing.DUMMY_NOTARY import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.config.FullNodeConfiguration import net.corda.nodeapi.User @@ -270,5 +270,5 @@ class NodeConfigTest { users = users ) - private fun localPort(port: Int) = HostAndPort.fromParts("localhost", port) + private fun localPort(port: Int) = NetworkHostAndPort("localhost", port) } diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt index bf7f7e57a3..6c77fa3b05 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt @@ -1,7 +1,7 @@ package net.corda.demobench.model import net.corda.core.crypto.X509Utilities.getX509Name -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.DUMMY_NOTARY import net.corda.nodeapi.User import org.junit.Test import java.nio.file.Path diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 9a7ada66ea..1ea92e3f48 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -10,29 +10,23 @@ import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.GBP import net.corda.core.contracts.USD -import net.corda.core.failure import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.FlowHandle import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType -import net.corda.core.serialization.OpaqueBytes -import net.corda.core.success -import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.flows.CashExitFlow -import net.corda.flows.CashFlowCommand -import net.corda.flows.CashIssueFlow -import net.corda.flows.CashPaymentFlow -import net.corda.flows.IssuerFlow -import net.corda.testing.driver.NodeHandle -import net.corda.testing.driver.PortAllocation -import net.corda.testing.driver.driver +import net.corda.core.thenMatch +import net.corda.core.utilities.OpaqueBytes +import net.corda.flows.* import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.PortAllocation +import net.corda.testing.driver.driver import org.bouncycastle.asn1.x500.X500Name import java.time.Instant import java.util.* @@ -136,13 +130,13 @@ class ExplorerSimulation(val options: OptionSet) { private fun startSimulation(eventGenerator: EventGenerator, maxIterations: Int) { // Log to logger when flow finish. - fun FlowHandle.log(seq: Int, name: String) { + fun FlowHandle.log(seq: Int, name: String) { val out = "[$seq] $name $id :" - returnValue.success { - Main.log.info("$out ${it.id} ${(it.tx.outputs.first().data as Cash.State).amount}") - }.failure { + returnValue.thenMatch({ (stx) -> + Main.log.info("$out ${stx.id} ${(stx.tx.outputs.first().data as Cash.State).amount}") + }, { Main.log.info("$out ${it.message}") - } + }) } for (i in 0..maxIterations) { @@ -179,11 +173,12 @@ class ExplorerSimulation(val options: OptionSet) { currencies = listOf(GBP, USD) ) val maxIterations = 100_000 + val anonymous = true // Pre allocate some money to each party. eventGenerator.parties.forEach { for (ref in 0..1) { for ((currency, issuer) in issuers) { - CashFlowCommand.IssueCash(Amount(1_000_000, currency), OpaqueBytes(ByteArray(1, { ref.toByte() })), it, notaryNode.nodeInfo.notaryIdentity).startFlow(issuer) + CashFlowCommand.IssueCash(Amount(1_000_000, currency), OpaqueBytes(ByteArray(1, { ref.toByte() })), it, notaryNode.nodeInfo.notaryIdentity, anonymous).startFlow(issuer) } } } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt index df873028a2..ec0752d6c2 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt @@ -1,10 +1,10 @@ package net.corda.explorer.views -import com.google.common.net.HostAndPort import javafx.beans.property.SimpleIntegerProperty import javafx.scene.control.* import net.corda.client.jfx.model.NodeMonitorModel import net.corda.client.jfx.model.objectProperty +import net.corda.core.utilities.NetworkHostAndPort import net.corda.explorer.model.SettingsModel import org.controlsfx.dialog.ExceptionDialog import tornadofx.* @@ -27,8 +27,8 @@ class LoginView : View() { private val port by objectProperty(SettingsModel::portProperty) private val fullscreen by objectProperty(SettingsModel::fullscreenProperty) - fun login(host: String?, port: Int, username: String, password: String) { - getModel().register(HostAndPort.fromParts(host, port), username, password) + fun login(host: String, port: Int, username: String, password: String) { + getModel().register(NetworkHostAndPort(host, port), username, password) } fun login() { diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt index 6adfb1af73..44e08e45b2 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt @@ -98,7 +98,7 @@ class Network : CordaView() { copyableLabel(SimpleObjectProperty(node.legalIdentity.owningKey.toBase58String())).apply { minWidth = 400.0 } } row("Services :") { label(node.advertisedServices.map { it.info }.joinToString(", ")) } - node.physicalLocation?.apply { row("Location :") { label(this@apply.description) } } + node.worldMapLocation?.apply { row("Location :") { label(this@apply.description) } } } } setOnMouseClicked { @@ -122,7 +122,7 @@ class Network : CordaView() { contentDisplay = ContentDisplay.TOP val coordinate = Bindings.createObjectBinding({ // These coordinates are obtained when we generate the map using TileMill. - node.physicalLocation?.coordinate?.project(mapPane.width, mapPane.height, 85.0511, -85.0511, -180.0, 180.0) ?: Pair(0.0, 0.0) + node.worldMapLocation?.coordinate?.project(mapPane.width, mapPane.height, 85.0511, -85.0511, -180.0, 180.0) ?: Pair(0.0, 0.0) }, arrayOf(mapPane.widthProperty(), mapPane.heightProperty())) // Center point of the label. layoutXProperty().bind(coordinate.map { it.first - width / 2 }) diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/StateMachineViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/StateMachineViewer.kt index 92f5b38b23..ead286c435 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/StateMachineViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/StateMachineViewer.kt @@ -26,6 +26,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.toBase58String import net.corda.core.flows.FlowInitiator import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.Try import net.corda.explorer.formatters.FlowInitiatorFormatter import net.corda.explorer.formatters.FlowNameFormatter import net.corda.explorer.formatters.PartyNameFormatter @@ -120,7 +121,7 @@ class StateMachineViewer : CordaView("Flow Triage") { val addRm = it.first.value val progress = it.second.value.status ?: "No progress data" if (addRm is StateMachineStatus.Removed) { - if (addRm.result.error == null) { + if (addRm.result is Try.Success) { makeIconLabel(FontAwesomeIcon.CHECK, "Success", "-fx-fill: green") } else { makeIconLabel(FontAwesomeIcon.BOLT, progress, "-fx-fill: -color-4") @@ -143,13 +144,13 @@ class StateMachineViewer : CordaView("Flow Triage") { "Error" to { sm, _ -> val smAddRm = sm.smmStatus.first.value if (smAddRm is StateMachineStatus.Removed) - smAddRm.result.error != null + smAddRm.result is Try.Failure else false }, "Done" to { sm, _ -> val smAddRm = sm.smmStatus.first.value if (smAddRm is StateMachineStatus.Removed) - smAddRm.result.error == null + smAddRm.result is Try.Success else false }, "In progress" to { sm, _ -> sm.smmStatus.first.value !is StateMachineStatus.Removed }, @@ -177,7 +178,11 @@ class StateMachineViewer : CordaView("Flow Triage") { } val status = smmData.smmStatus.first.value if (status is StateMachineStatus.Removed) { - status.result.match(onValue = { makeResultVBox(flowResultVBox, it) }, onError = { makeErrorVBox(flowResultVBox, it) }) + val result = status.result + when (result) { + is Try.Success -> makeResultVBox(flowResultVBox, result.value) + is Try.Failure -> makeErrorVBox(flowResultVBox, result.exception) + } } } } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt index d97292ac13..858117de3f 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt @@ -24,7 +24,6 @@ import net.corda.contracts.asset.Cash import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty import net.corda.core.node.NodeInfo import net.corda.explorer.AmountDiff import net.corda.explorer.formatters.AmountFormatter diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt index e0883cc311..d2a59c1cea 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt @@ -20,15 +20,15 @@ import net.corda.client.jfx.utils.unique import net.corda.core.contracts.Amount import net.corda.core.contracts.sumOrNull import net.corda.core.contracts.withoutIssuer -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.Party -import net.corda.core.crypto.commonName import net.corda.core.flows.FlowException import net.corda.core.getOrThrow +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow import net.corda.core.node.NodeInfo -import net.corda.core.serialization.OpaqueBytes -import net.corda.core.then +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.transactions.SignedTransaction import net.corda.explorer.formatters.PartyNameFormatter import net.corda.explorer.model.CashTransaction import net.corda.explorer.model.IssuerModel @@ -36,6 +36,7 @@ import net.corda.explorer.model.ReportingCurrencyModel import net.corda.explorer.views.bigDecimalFormatter import net.corda.explorer.views.byteFormatter import net.corda.explorer.views.stringConverter +import net.corda.flows.AbstractCashFlow import net.corda.flows.CashFlowCommand import net.corda.flows.IssuerFlow.IssuanceRequester import org.controlsfx.dialog.ExceptionDialog @@ -93,18 +94,25 @@ class NewTransaction : Fragment() { initOwner(window) show() } - val handle = if (command is CashFlowCommand.IssueCash) { + val handle: FlowHandle = if (command is CashFlowCommand.IssueCash) { rpcProxy.value!!.startFlow(::IssuanceRequester, command.amount, command.recipient, command.issueRef, - myIdentity.value!!.legalIdentity) + myIdentity.value!!.legalIdentity, + command.notary, + command.anonymous) } else { command.startFlow(rpcProxy.value!!) } runAsync { - handle.returnValue.then { dialog.dialogPane.isDisable = false }.getOrThrow() - }.ui { + try { + handle.returnValue.getOrThrow() + } finally { + dialog.dialogPane.isDisable = false + } + }.ui { it -> + val stx: SignedTransaction = it.stx val type = when (command) { is CashFlowCommand.IssueCash -> "Cash Issued" is CashFlowCommand.ExitCash -> "Cash Exited" @@ -118,7 +126,7 @@ class NewTransaction : Fragment() { row { label(type) { font = Font.font(font.family, FontWeight.EXTRA_BOLD, font.size + 2) } } row { label("Transaction ID :") { GridPane.setValignment(this, VPos.TOP) } - label { text = Splitter.fixedLength(16).split("${it.id}").joinToString("\n") } + label { text = Splitter.fixedLength(16).split("${stx.id}").joinToString("\n") } } } dialog.dialogPane.scene.window.sizeToScene() @@ -142,14 +150,16 @@ class NewTransaction : Fragment() { dialogPane = root initOwner(window) setResultConverter { + // TODO: Enable confidential identities + val anonymous = false val defaultRef = OpaqueBytes.of(1) val issueRef = if (issueRef.value != null) OpaqueBytes.of(issueRef.value) else defaultRef when (it) { executeButton -> when (transactionTypeCB.value) { CashTransaction.Issue -> { - CashFlowCommand.IssueCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.legalIdentity, notaries.first().notaryIdentity) + CashFlowCommand.IssueCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.legalIdentity, notaries.first().notaryIdentity, anonymous) } - CashTransaction.Pay -> CashFlowCommand.PayCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), partyBChoiceBox.value.legalIdentity) + CashTransaction.Pay -> CashFlowCommand.PayCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), partyBChoiceBox.value.legalIdentity, anonymous = anonymous) CashTransaction.Exit -> CashFlowCommand.ExitCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef) else -> null } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt index ef84237cbb..80167f0416 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt @@ -1,6 +1,5 @@ package net.corda.loadtest -import com.google.common.net.HostAndPort import com.jcraft.jsch.Buffer import com.jcraft.jsch.Identity import com.jcraft.jsch.IdentityRepository @@ -8,6 +7,7 @@ import com.jcraft.jsch.JSch import com.jcraft.jsch.agentproxy.AgentProxy import com.jcraft.jsch.agentproxy.connector.SSHAgentConnector import com.jcraft.jsch.agentproxy.usocket.JNAUSocketFactory +import net.corda.core.utilities.NetworkHostAndPort import net.corda.testing.driver.PortAllocation import org.slf4j.LoggerFactory import java.util.* @@ -61,7 +61,7 @@ fun setupJSchWithSshAgent(): JSch { } class ConnectionManager(private val jSch: JSch) { - fun connectToNode(remoteNode: RemoteNode, localTunnelAddress: HostAndPort): NodeConnection { + fun connectToNode(remoteNode: RemoteNode, localTunnelAddress: NetworkHostAndPort): NodeConnection { val session = jSch.getSession(remoteNode.sshUserName, remoteNode.hostname, 22) // We don't check the host fingerprints because they may change often session.setConfig("StrictHostKeyChecking", "no") diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTestConfiguration.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTestConfiguration.kt index 8cf9b6568b..8e4a962002 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTestConfiguration.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTestConfiguration.kt @@ -33,7 +33,7 @@ data class LoadTestConfiguration( val remoteSystemdServiceName: String, val seed: Long?, val mode: TestMode = TestMode.LOAD_TEST, - val executionFrequency: Int = 20, + val executionFrequency: Int = 2, val generateCount: Int = 10000, val parallelism: Int = ForkJoinPool.getCommonPoolParallelism()) diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/Main.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/Main.kt index 0bce7c7f44..4a9c59d780 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/Main.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/Main.kt @@ -133,17 +133,17 @@ private fun runLoadTest(loadTestConfiguration: LoadTestConfiguration) { private fun runStabilityTest(loadTestConfiguration: LoadTestConfiguration) { runLoadTests(loadTestConfiguration, listOf( - // Self issue cash. - StabilityTest.selfIssueTest to LoadTest.RunParameters( + // Self issue cash. This is a pre test step to make sure vault have enough cash to work with. + StabilityTest.selfIssueTest(100) to LoadTest.RunParameters( parallelism = loadTestConfiguration.parallelism, - generateCount = loadTestConfiguration.generateCount, + generateCount = 1000, clearDatabaseBeforeRun = false, - executionFrequency = loadTestConfiguration.executionFrequency, + executionFrequency = 50, gatherFrequency = 100, disruptionPatterns = listOf(listOf()) // no disruptions ), // Send cash to a random party or exit cash, commands are generated randomly. - StabilityTest.crossCashTest to LoadTest.RunParameters( + StabilityTest.crossCashTest(100) to LoadTest.RunParameters( parallelism = loadTestConfiguration.parallelism, generateCount = loadTestConfiguration.generateCount, clearDatabaseBeforeRun = false, diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt index 55cd113430..7376ce4413 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt @@ -1,7 +1,5 @@ package net.corda.loadtest -import com.google.common.net.HostAndPort -import com.google.common.util.concurrent.ListenableFuture import com.jcraft.jsch.ChannelExec import com.jcraft.jsch.Session import net.corda.client.rpc.CordaRPCClient @@ -9,11 +7,13 @@ import net.corda.client.rpc.CordaRPCConnection import net.corda.core.future import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.nodeapi.internal.addShutdownHook import java.io.ByteArrayOutputStream import java.io.Closeable import java.io.OutputStream +import java.util.concurrent.Future /** * [NodeConnection] allows executing remote shell commands on the node as well as executing RPCs. @@ -22,7 +22,7 @@ import java.io.OutputStream * [doWhileClientStopped], otherwise the RPC link will be broken. * TODO: Auto reconnect has been enable for RPC connection, investigate if we still need [doWhileClientStopped]. */ -class NodeConnection(val remoteNode: RemoteNode, private val jSchSession: Session, private val localTunnelAddress: HostAndPort) : Closeable { +class NodeConnection(val remoteNode: RemoteNode, private val jSchSession: Session, private val localTunnelAddress: NetworkHostAndPort) : Closeable { companion object { val log = loggerFor() } @@ -85,7 +85,7 @@ class NodeConnection(val remoteNode: RemoteNode, private val jSchSession: Sessio return ShellCommandOutput(command, exitCode, stdoutStream.toString(), stderrStream.toString()) } - private fun runShellCommand(command: String, stdout: OutputStream, stderr: OutputStream): ListenableFuture { + private fun runShellCommand(command: String, stdout: OutputStream, stderr: OutputStream): Future { log.info("Running '$command' on ${remoteNode.hostname}") return future { val (exitCode, _) = withChannelExec(command) { channel -> diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt index dbe922e5a6..f2d90594ab 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt @@ -7,10 +7,9 @@ import net.corda.contracts.asset.Cash import net.corda.core.contracts.Issued import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.USD -import net.corda.core.failure import net.corda.core.identity.AbstractParty -import net.corda.core.serialization.OpaqueBytes -import net.corda.core.success +import net.corda.core.thenMatch +import net.corda.core.utilities.OpaqueBytes import net.corda.flows.CashFlowCommand import net.corda.loadtest.LoadTest import net.corda.loadtest.NodeConnection @@ -117,13 +116,14 @@ val crossCashTest = LoadTest( generate = { (nodeVaults), parallelism -> val nodeMap = simpleNodes.associateBy { it.info.legalIdentity } - Generator.pickN(parallelism, simpleNodes).bind { nodes -> + val anonymous = true + Generator.pickN(parallelism, simpleNodes).flatMap { nodes -> Generator.sequence( nodes.map { node -> val quantities = nodeVaults[node.info.legalIdentity] ?: mapOf() val possibleRecipients = nodeMap.keys.toList() val moves = quantities.map { - it.value.toDouble() / 1000 to generateMove(it.value, USD, node.info.legalIdentity, possibleRecipients) + it.value.toDouble() / 1000 to generateMove(it.value, USD, node.info.legalIdentity, possibleRecipients, anonymous) } val exits = quantities.mapNotNull { if (it.key == node.info.legalIdentity) { @@ -133,7 +133,7 @@ val crossCashTest = LoadTest( } } val command = Generator.frequency( - listOf(1.0 to generateIssue(10000, USD, notary.info.notaryIdentity, possibleRecipients)) + moves + exits + listOf(1.0 to generateIssue(10000, USD, notary.info.notaryIdentity, possibleRecipients, anonymous)) + moves + exits ) command.map { CrossCashCommand(it, nodeMap[node.info.legalIdentity]!!) } } @@ -206,12 +206,11 @@ val crossCashTest = LoadTest( execute = { command -> val result = command.command.startFlow(command.node.proxy).returnValue - result.failure { - log.error("Failure[$command]", it) - } - result.success { + result.thenMatch({ log.info("Success[$command]: $result") - } + }, { + log.error("Failure[$command]", it) + }) }, gatherRemoteState = { previousState -> diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/GenerateHelpers.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/GenerateHelpers.kt index 9794ddb9b9..e3ede62d99 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/GenerateHelpers.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/GenerateHelpers.kt @@ -7,7 +7,7 @@ import net.corda.core.contracts.Issued import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.withoutIssuer 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.* @@ -15,13 +15,14 @@ fun generateIssue( max: Long, currency: Currency, notary: Party, - possibleRecipients: List + possibleRecipients: List, + anonymous: Boolean ): Generator { return generateAmount(1, max, Generator.pure(currency)).combine( Generator.pure(OpaqueBytes.of(0)), Generator.pickOne(possibleRecipients) ) { amount, ref, recipient -> - CashFlowCommand.IssueCash(amount, ref, recipient, notary) + CashFlowCommand.IssueCash(amount, ref, recipient, notary, anonymous) } } @@ -29,12 +30,13 @@ fun generateMove( max: Long, currency: Currency, issuer: Party, - possibleRecipients: List + possibleRecipients: List, + anonymous: Boolean ): Generator { return generateAmount(1, max, Generator.pure(Issued(PartyAndReference(issuer, OpaqueBytes.of(0)), currency))).combine( Generator.pickOne(possibleRecipients) ) { amount, recipient -> - CashFlowCommand.PayCash(amount.withoutIssuer(), recipient, issuer) + CashFlowCommand.PayCash(amount.withoutIssuer(), recipient, issuer, anonymous) } } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt index 60c45a1cbf..f684b08bb4 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt @@ -6,14 +6,15 @@ import net.corda.client.mock.pickOne import net.corda.client.mock.replicate import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY -import net.corda.core.contracts.DummyContract import net.corda.core.flows.FlowException import net.corda.core.messaging.startFlow -import net.corda.core.success +import net.corda.core.thenMatch import net.corda.core.transactions.SignedTransaction import net.corda.flows.FinalityFlow import net.corda.loadtest.LoadTest import net.corda.loadtest.NodeConnection +import net.corda.testing.contracts.DummyContract +import net.corda.testing.node.MockServices import org.slf4j.LoggerFactory private val log = LoggerFactory.getLogger("NotaryTest") @@ -23,16 +24,15 @@ data class NotariseCommand(val issueTx: SignedTransaction, val moveTx: SignedTra val dummyNotarisationTest = LoadTest( "Notarising dummy transactions", generate = { _, _ -> - val generateTx = Generator.pickOne(simpleNodes).bind { node -> + val issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY) + val generateTx = Generator.pickOne(simpleNodes).flatMap { node -> Generator.int().map { - val issueTx = DummyContract.generateInitial(it, notary.info.notaryIdentity, DUMMY_CASH_ISSUER).apply { - signWith(DUMMY_CASH_ISSUER_KEY) - } - val asset = issueTx.toWireTransaction().outRef(0) - val moveTx = DummyContract.move(asset, DUMMY_CASH_ISSUER.party).apply { - signWith(DUMMY_CASH_ISSUER_KEY) - } - NotariseCommand(issueTx.toSignedTransaction(false), moveTx.toSignedTransaction(false), node) + val issueBuilder = DummyContract.generateInitial(it, notary.info.notaryIdentity, DUMMY_CASH_ISSUER) + val issueTx = issuerServices.signInitialTransaction(issueBuilder) + val asset = issueTx.tx.outRef(0) + val moveBuilder = DummyContract.move(asset, DUMMY_CASH_ISSUER.party) + val moveTx = issuerServices.signInitialTransaction(moveBuilder) + NotariseCommand(issueTx, moveTx, node) } } Generator.replicate(10, generateTx) @@ -42,9 +42,9 @@ val dummyNotarisationTest = LoadTest( try { val proxy = node.proxy val issueFlow = proxy.startFlow(::FinalityFlow, issueTx) - issueFlow.returnValue.success { + issueFlow.returnValue.thenMatch({ val moveFlow = proxy.startFlow(::FinalityFlow, moveTx) - } + }, {}) } catch (e: FlowException) { log.error("Failure", e) } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt index 6c298b981a..61194d7ea8 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt @@ -37,12 +37,12 @@ val selfIssueTest = LoadTest( "Self issuing cash randomly", generate = { _, parallelism -> - val generateIssue = Generator.pickOne(simpleNodes).bind { node -> - generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity)).map { + val generateIssue = Generator.pickOne(simpleNodes).flatMap { node -> + generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity), anonymous = true).map { SelfIssueCommand(it, node) } } - Generator.replicatePoisson(parallelism.toDouble(), generateIssue).bind { + Generator.replicatePoisson(parallelism.toDouble(), generateIssue).flatMap { // We need to generate at least one if (it.isEmpty()) { Generator.sequence(listOf(generateIssue)) diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt index 500283f630..b24f0216b4 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt @@ -1,61 +1,48 @@ package net.corda.loadtest.tests import net.corda.client.mock.Generator -import net.corda.client.mock.pickOne -import net.corda.client.mock.replicatePoisson +import net.corda.core.contracts.Amount import net.corda.core.contracts.USD -import net.corda.core.failure import net.corda.core.flows.FlowException import net.corda.core.getOrThrow -import net.corda.core.success +import net.corda.core.thenMatch +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor +import net.corda.flows.CashFlowCommand import net.corda.loadtest.LoadTest object StabilityTest { private val log = loggerFor() - val crossCashTest = LoadTest( - "Creating Cash transactions randomly", + fun crossCashTest(replication: Int) = LoadTest( + "Creating Cash transactions", generate = { _, _ -> - val nodeMap = simpleNodes.associateBy { it.info.legalIdentity } - Generator.sequence(simpleNodes.map { node -> - val possibleRecipients = nodeMap.keys.toList() - val moves = 0.5 to generateMove(1, USD, node.info.legalIdentity, possibleRecipients) - val exits = 0.5 to generateExit(1, USD) - val command = Generator.frequency(listOf(moves, exits)) - command.map { CrossCashCommand(it, nodeMap[node.info.legalIdentity]!!) } - }) + val payments = simpleNodes.flatMap { payer -> simpleNodes.map { payer to it } } + .filter { it.first != it.second } + .map { (payer, payee) -> CrossCashCommand(CashFlowCommand.PayCash(Amount(1, USD), payee.info.legalIdentity, anonymous = true), payer) } + Generator.pure(List(replication) { payments }.flatten()) }, interpret = { _, _ -> }, execute = { command -> val result = command.command.startFlow(command.node.proxy).returnValue - result.failure { - log.error("Failure[$command]", it) - } - result.success { + result.thenMatch({ log.info("Success[$command]: $result") - } + }, { + log.error("Failure[$command]", it) + }) }, gatherRemoteState = {} ) - val selfIssueTest = LoadTest( - "Self issuing cash randomly", - generate = { _, parallelism -> - val generateIssue = Generator.pickOne(simpleNodes).bind { node -> - generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity)).map { - SelfIssueCommand(it, node) - } - } - Generator.replicatePoisson(parallelism.toDouble(), generateIssue).bind { - // We need to generate at least one - if (it.isEmpty()) { - Generator.sequence(listOf(generateIssue)) - } else { - Generator.pure(it) - } - } + fun selfIssueTest(replication: Int) = LoadTest( + "Self issuing lot of cash", + generate = { _, _ -> + // Self issue cash is fast, its ok to flood the node with this command. + val generateIssue = + simpleNodes.map { issuer -> + SelfIssueCommand(CashFlowCommand.IssueCash(Amount(100000, USD), OpaqueBytes.of(0), issuer.info.legalIdentity, notary.info.notaryIdentity, anonymous = true), issuer) + } + Generator.pure(List(replication) { generateIssue }.flatten()) }, - interpret = { _, _ -> }, execute = { command -> try { diff --git a/verifier/build.gradle b/verifier/build.gradle index db63013972..e0035ffa33 100644 --- a/verifier/build.gradle +++ b/verifier/build.gradle @@ -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 verifier' diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt index 78a84a497d..3c0ccd7460 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt @@ -2,7 +2,11 @@ package net.corda.verifier import net.corda.client.mock.* import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.testing.contracts.DummyContract +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.X509Utilities +import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.sha256 import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party @@ -54,7 +58,7 @@ data class GeneratedLedger( * Invariants: The input list must be empty. */ val issuanceGenerator: Generator> by lazy { - val outputsGen = outputsGenerator.bind { outputs -> + val outputsGen = outputsGenerator.flatMap { outputs -> Generator.sequence( outputs.map { output -> pickOneOrMaybeNew(identities, partyGenerator).map { notary -> @@ -136,7 +140,7 @@ data class GeneratedLedger( fun notaryChangeTransactionGenerator(inputNotary: Party, inputsToChooseFrom: List>): Generator> { val newNotaryGen = pickOneOrMaybeNew(identities - inputNotary, partyGenerator) val inputsGen = Generator.sampleBernoulli(inputsToChooseFrom) - return inputsGen.bind { inputs -> + return inputsGen.flatMap { inputs -> val signers: List = (inputs.flatMap { it.state.data.participants } + inputNotary).map { it.owningKey } val outputsGen = Generator.sequence(inputs.map { input -> newNotaryGen.map { TransactionState(input.state.data, it, null) } }) outputsGen.combine(attachmentsGenerator) { outputs, txAttachments -> @@ -173,7 +177,7 @@ data class GeneratedLedger( if (availableOutputs.isEmpty()) { issuanceGenerator } else { - Generator.pickOne(availableOutputs.keys.toList()).bind { inputNotary -> + Generator.pickOne(availableOutputs.keys.toList()).flatMap { inputNotary -> val inputsToChooseFrom = availableOutputs[inputNotary]!! Generator.frequency( 0.3 to issuanceGenerator, @@ -227,7 +231,7 @@ fun pickOneOrMaybeNew(from: Collection, generator: Generator): Generat if (from.isEmpty()) { return generator } else { - return generator.bind { + return generator.flatMap { Generator.pickOne(from + it) } } diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index a343c3bd2c..66315907aa 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -1,6 +1,5 @@ package net.corda.verifier -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListeningScheduledExecutorService @@ -11,9 +10,10 @@ import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.commonName import net.corda.core.div import net.corda.core.map -import net.corda.core.random63BitValue +import net.corda.core.crypto.random63BitValue import net.corda.core.transactions.LedgerTransaction -import net.corda.core.utilities.ProcessUtilities +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.testing.driver.ProcessUtilities import net.corda.core.utilities.loggerFor import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER @@ -50,7 +50,7 @@ interface VerifierExposedDSLInterface : DriverDSLExposedInterface { fun startVerificationRequestor(name: X500Name): ListenableFuture /** Starts an out of process verifier connected to [address] */ - fun startVerifier(address: HostAndPort): ListenableFuture + fun startVerifier(address: NetworkHostAndPort): ListenableFuture /** * Waits until [number] verifiers are listening for verification requests coming from the Node. Check @@ -106,7 +106,7 @@ data class VerifierHandle( /** A handle for the verification requestor */ data class VerificationRequestorHandle( - val p2pAddress: HostAndPort, + val p2pAddress: NetworkHostAndPort, private val responseAddress: SimpleString, private val session: ClientSession, private val requestProducer: ClientProducer, @@ -143,7 +143,7 @@ data class VerifierDriverDSL( companion object { private val log = loggerFor() - fun createConfiguration(baseDirectory: Path, nodeHostAndPort: HostAndPort): Config { + fun createConfiguration(baseDirectory: Path, nodeHostAndPort: NetworkHostAndPort): Config { return ConfigFactory.parseMap( mapOf( "baseDirectory" to baseDirectory.toString(), @@ -152,7 +152,7 @@ data class VerifierDriverDSL( ) } - fun createVerificationRequestorArtemisConfig(baseDirectory: Path, responseAddress: String, hostAndPort: HostAndPort, sslConfiguration: SSLConfiguration): Configuration { + fun createVerificationRequestorArtemisConfig(baseDirectory: Path, responseAddress: String, hostAndPort: NetworkHostAndPort, sslConfiguration: SSLConfiguration): Configuration { val connectionDirection = ConnectionDirection.Inbound(acceptorFactoryClassName = NettyAcceptorFactory::class.java.name) return ConfigurationImpl().apply { val artemisDir = "$baseDirectory/artemis" @@ -183,7 +183,7 @@ data class VerifierDriverDSL( } } - private fun startVerificationRequestorInternal(name: X500Name, hostAndPort: HostAndPort): VerificationRequestorHandle { + private fun startVerificationRequestorInternal(name: X500Name, hostAndPort: NetworkHostAndPort): VerificationRequestorHandle { val baseDir = driverDSL.driverDirectory / name.commonName val sslConfig = object : NodeSSLConfiguration { override val baseDirectory = baseDir @@ -247,7 +247,7 @@ data class VerifierDriverDSL( ) } - override fun startVerifier(address: HostAndPort): ListenableFuture { + override fun startVerifier(address: NetworkHostAndPort): ListenableFuture { log.info("Starting verifier connecting to address $address") val id = verifierCount.andIncrement val jdwpPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt index 3ab800aaae..da594e5422 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -6,11 +6,11 @@ import net.corda.core.contracts.DOLLARS import net.corda.core.map import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo -import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.testing.ALICE +import net.corda.testing.DUMMY_NOTARY import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow import net.corda.testing.driver.NetworkMapStartStrategy diff --git a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt index a42b495072..b8df7f891b 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt @@ -1,12 +1,10 @@ package net.corda.verifier -import com.google.common.net.HostAndPort import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions -import net.corda.core.ErrorOr -import net.corda.nodeapi.internal.addShutdownHook import net.corda.core.div +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport @@ -15,6 +13,7 @@ import net.corda.nodeapi.VerifierApi import net.corda.nodeapi.VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME import net.corda.nodeapi.config.NodeSSLConfiguration import net.corda.nodeapi.config.getValue +import net.corda.nodeapi.internal.addShutdownHook import org.apache.activemq.artemis.api.core.client.ActiveMQClient import java.nio.file.Path import java.nio.file.Paths @@ -23,7 +22,7 @@ data class VerifierConfiguration( override val baseDirectory: Path, val config: Config ) : NodeSSLConfiguration { - val nodeHostAndPort: HostAndPort by config + val nodeHostAndPort: NetworkHostAndPort by config override val keyStorePassword: String by config override val trustStorePassword: String by config } @@ -61,14 +60,15 @@ class Verifier { consumer.setMessageHandler { val request = VerifierApi.VerificationRequest.fromClientMessage(it) log.debug { "Received verification request with id ${request.verificationId}" } - val result = ErrorOr.catch { + val error = try { request.transaction.verify() - } - if (result.error != null) { - log.debug { "Verification returned with error ${result.error}" } + null + } catch (t: Throwable) { + log.debug("Verification returned with error:", t) + t } val reply = session.createMessage(false) - val response = VerifierApi.VerificationResponse(request.verificationId, result.error) + val response = VerifierApi.VerificationResponse(request.verificationId, error) response.writeToClientMessage(reply) replyProducer.send(request.responseAddress, reply) it.acknowledge() diff --git a/verify-enclave/src/main/kotlin/com/r3/enclaves/txverify/Enclavelet.kt b/verify-enclave/src/main/kotlin/com/r3/enclaves/txverify/Enclavelet.kt index f2099fc8f1..bac47aaff2 100644 --- a/verify-enclave/src/main/kotlin/com/r3/enclaves/txverify/Enclavelet.kt +++ b/verify-enclave/src/main/kotlin/com/r3/enclaves/txverify/Enclavelet.kt @@ -3,31 +3,12 @@ package com.r3.enclaves.txverify import com.esotericsoftware.minlog.Log -import net.corda.core.contracts.Attachment -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionResolutionException -import net.corda.core.contracts.TransactionState -import net.corda.core.crypto.SecureHash -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.Party -import net.corda.core.identity.PartyAndCertificate -import net.corda.core.node.ServicesForResolution -import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.AttachmentsStorageService -import net.corda.core.node.services.IdentityService import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.createTestKryo import net.corda.core.serialization.deserialize import net.corda.core.transactions.WireTransaction -import org.bouncycastle.asn1.x500.X500Name import java.io.File -import java.io.InputStream -import java.nio.file.Path -import java.security.PublicKey -import java.security.cert.CertPath -import java.security.cert.X509Certificate // This file implements the functionality of the SGX transaction verification enclave. diff --git a/verify-enclave/src/test/kotlin/com/r3/enclaves/txverify/EnclaveletTest.kt b/verify-enclave/src/test/kotlin/com/r3/enclaves/txverify/EnclaveletTest.kt index 5f7ca36bdf..32e9a8aeba 100644 --- a/verify-enclave/src/test/kotlin/com/r3/enclaves/txverify/EnclaveletTest.kt +++ b/verify-enclave/src/test/kotlin/com/r3/enclaves/txverify/EnclaveletTest.kt @@ -4,8 +4,8 @@ import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.core.contracts.POUNDS import net.corda.core.contracts.`issued by` +import net.corda.core.crypto.random63BitValue import net.corda.core.identity.AnonymousParty -import net.corda.core.random63BitValue import net.corda.core.serialization.serialize import net.corda.testing.MEGA_CORP_PUBKEY import net.corda.testing.MINI_CORP_PUBKEY diff --git a/webserver/build.gradle b/webserver/build.gradle index dc2e70e676..0868a6ec8a 100644 --- a/webserver/build.gradle +++ b/webserver/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'kotlin' apply plugin: 'java' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Corda node web server' diff --git a/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt b/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt index dbbab3e2df..dca27b20f1 100644 --- a/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt +++ b/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt @@ -1,8 +1,8 @@ package net.corda.webserver -import com.google.common.net.HostAndPort import net.corda.core.getOrThrow -import net.corda.core.utilities.DUMMY_BANK_A +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.testing.DUMMY_BANK_A import net.corda.testing.driver.WebserverHandle import net.corda.testing.driver.addressMustBeBound import net.corda.testing.driver.addressMustNotBeBound @@ -19,7 +19,7 @@ class DriverTests { addressMustBeBound(executorService, webserverHandle.listenAddress, webserverHandle.process) } - fun webserverMustBeDown(webserverAddr: HostAndPort) { + fun webserverMustBeDown(webserverAddr: NetworkHostAndPort) { addressMustNotBeBound(executorService, webserverAddr) } } diff --git a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt index 194536869e..ab331c347e 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt @@ -1,7 +1,7 @@ package net.corda.webserver -import com.google.common.net.HostAndPort import com.typesafe.config.Config +import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.config.NodeSSLConfiguration import net.corda.nodeapi.config.getValue import java.nio.file.Path @@ -15,6 +15,6 @@ class WebServerConfig(override val baseDirectory: Path, val config: Config) : No val exportJMXto: String get() = "http" val useHTTPS: Boolean by config val myLegalName: String by config - val p2pAddress: HostAndPort by config // TODO: Use RPC port instead of P2P port (RPC requires authentication, P2P does not) - val webAddress: HostAndPort by config + val p2pAddress: NetworkHostAndPort by config // TODO: Use RPC port instead of P2P port (RPC requires authentication, P2P does not) + val webAddress: NetworkHostAndPort by config } diff --git a/webserver/webcapsule/build.gradle b/webserver/webcapsule/build.gradle index 08a8f0a9e1..9cfc1f8e0c 100644 --- a/webserver/webcapsule/build.gradle +++ b/webserver/webcapsule/build.gradle @@ -4,6 +4,7 @@ */ apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'us.kirchmeier.capsule' +apply plugin: 'com.jfrog.artifactory' description 'Corda node web server capsule'