diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..37dc57e9fa --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,8 @@ +Thank you for choosing to report an issue with Corda. + +When reporting an issue please make sure it contains; + +* A clear description of the issue +* Any logs or stack traces that you can provide (use a site like https://pastebin.com for larger logs) +* Steps to reproduce the issue +* The version/tag/release or commit hash it occured on \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..62d77dd2d8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +Thank you for choosing to contribute to Corda. + +Your PR must be approved by one or more reviewers and all tests must be passed on TeamCity (https://ci.corda.r3cev.com) in order to be merged. + +Once you have submitted a PR you are responsible for keeping it up to date until the time it is merged. + +PR Checklist: + +1. Ensure any new code is tested as described in https://docs.corda.net/testing.html +2. Ensure you have done any relevant automated testing and manual testing +3. Add your changes to docs/source/changelog.rst +4. Update any documentation in docs/source relating to your changes and learn how to build them in https://docs.corda.net/building-the-docs.html +5. If you are contributing for the first time please read the agreement in CONTRIBUTING.md now and add to this Pull Request that you have read, and agreed to, the agreement. + +Please remove this message when you have read it. diff --git a/.gitignore b/.gitignore index 5a11950686..710d20ef3a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ local.properties **/build/* lib/dokka.jar +lib/quasar.jar **/logs/* diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 75643b8eeb..4edce38213 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -62,8 +62,9 @@ - - + + + @@ -91,13 +92,15 @@ - - - - - - - + + + + + + + + + diff --git a/build.gradle b/build.gradle index 63b95c82ca..dd7a6af5da 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { file("$projectDir/constants.properties").withInputStream { constants.load(it) } // Our version: bump this on release. - ext.corda_release_version = "0.15-SNAPSHOT" + ext.corda_release_version = "0.16-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 @@ -143,10 +143,6 @@ allprojects { mavenLocal() mavenCentral() jcenter() - // TODO: remove this once we eliminate Exposed - maven { - url 'https://dl.bintray.com/kotlin/exposed' - } maven { url 'https://jitpack.io' } } @@ -220,15 +216,15 @@ tasks.withType(Test) { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "CN=Controller,O=R3,OU=corda,L=London,C=GB" + networkMap "O=Controller,OU=corda,L=London,C=GB" node { - name "CN=Controller,O=R3,OU=corda,L=London,C=GB" + name "O=Controller,OU=corda,L=London,C=GB" advertisedServices = ["corda.notary.validating"] p2pPort 10002 cordapps = [] } node { - name "CN=Bank A,O=R3,OU=corda,L=London,C=GB" + name "O=Bank A,OU=corda,L=London,C=GB" advertisedServices = [] p2pPort 10012 rpcPort 10013 @@ -236,7 +232,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { cordapps = [] } node { - name "CN=Bank B,O=R3,OU=corda,L=London,C=GB" + name "O=Bank B,OU=corda,L=London,C=GB" advertisedServices = [] p2pPort 10007 rpcPort 10008 @@ -256,7 +252,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', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', '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-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver'] license { name = 'Apache-2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0' @@ -291,7 +287,7 @@ artifactory { 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-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver') + publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver') } } } diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt similarity index 99% rename from client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt rename to client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt index cf2ed02f52..a8418809da 100644 --- a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt @@ -1,4 +1,4 @@ -package net.corda.jackson +package net.corda.client.jackson import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty @@ -12,7 +12,7 @@ import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef import net.corda.core.crypto.* -import net.corda.core.crypto.composite.CompositeKey +import net.corda.core.crypto.CompositeKey import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party @@ -27,6 +27,8 @@ import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.parsePublicKeyBase58 +import net.corda.core.utilities.toBase58String import net.i2p.crypto.eddsa.EdDSAPublicKey import org.bouncycastle.asn1.x500.X500Name import java.math.BigDecimal diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/StringToMethodCallParser.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt similarity index 99% rename from client/jackson/src/main/kotlin/net/corda/jackson/StringToMethodCallParser.kt rename to client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt index 45454ef1d0..ddbf924c27 100644 --- a/client/jackson/src/main/kotlin/net/corda/jackson/StringToMethodCallParser.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt @@ -1,11 +1,11 @@ -package net.corda.jackson +package net.corda.client.jackson import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.google.common.collect.HashMultimap import com.google.common.collect.Multimap -import net.corda.jackson.StringToMethodCallParser.ParsedMethodCall +import net.corda.client.jackson.StringToMethodCallParser.ParsedMethodCall import org.slf4j.LoggerFactory import java.lang.reflect.Constructor import java.lang.reflect.Method @@ -223,4 +223,4 @@ open class StringToMethodCallParser @JvmOverloads constructor( Pair(name, argStr) }.toMap() } -} \ No newline at end of file +} diff --git a/client/jackson/src/test/kotlin/net/corda/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt similarity index 98% rename from client/jackson/src/test/kotlin/net/corda/jackson/JacksonSupportTest.kt rename to client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index 13acd147ab..fec036aeb2 100644 --- a/client/jackson/src/test/kotlin/net/corda/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -1,4 +1,4 @@ -package net.corda.jackson +package net.corda.client.jackson import com.fasterxml.jackson.databind.SerializationFeature import net.corda.core.contracts.Amount diff --git a/client/jackson/src/test/kotlin/net/corda/jackson/StringToMethodCallParserTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/StringToMethodCallParserTest.kt similarity index 98% rename from client/jackson/src/test/kotlin/net/corda/jackson/StringToMethodCallParserTest.kt rename to client/jackson/src/test/kotlin/net/corda/client/jackson/StringToMethodCallParserTest.kt index 7610fbf91b..a93052a48e 100644 --- a/client/jackson/src/test/kotlin/net/corda/jackson/StringToMethodCallParserTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/StringToMethodCallParserTest.kt @@ -1,4 +1,4 @@ -package net.corda.jackson +package net.corda.client.jackson import net.corda.core.crypto.SecureHash import org.junit.Assert.assertArrayEquals @@ -65,4 +65,4 @@ class StringToMethodCallParserTest { val args: Array = parser.parseArguments(clazz.name, names.zip(ctor.parameterTypes), "alternativeWord: Foo bar!") assertArrayEquals(args, arrayOf("Foo bar!")) } -} \ No newline at end of file +} diff --git a/client/jfx/build.gradle b/client/jfx/build.gradle index 8b8c677e4b..ef9602e810 100644 --- a/client/jfx/build.gradle +++ b/client/jfx/build.gradle @@ -50,6 +50,7 @@ dependencies { // Integration test helpers integrationTestCompile "junit:junit:$junit_version" + integrationTestCompile project(':node-driver') } task integrationTest(type: Test) { 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 83453feddd..20e9c8dda5 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 @@ -58,13 +58,13 @@ class NodeMonitorModelTest : DriverBasedTest() { startFlowPermission(), startFlowPermission()) ) - val aliceNodeFuture = startNode(ALICE.name, rpcUsers = listOf(cashUser)) - val notaryNodeFuture = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + val aliceNodeFuture = startNode(providedName = ALICE.name, rpcUsers = listOf(cashUser)) + val notaryNodeFuture = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) val aliceNodeHandle = aliceNodeFuture.getOrThrow() val notaryNodeHandle = notaryNodeFuture.getOrThrow() aliceNode = aliceNodeHandle.nodeInfo notaryNode = notaryNodeHandle.nodeInfo - newNode = { nodeName -> startNode(nodeName).getOrThrow().nodeInfo } + newNode = { nodeName -> startNode(providedName = nodeName).getOrThrow().nodeInfo } val monitor = NodeMonitorModel() stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed() stateMachineUpdates = monitor.stateMachineUpdates.bufferUntilSubscribed() @@ -76,7 +76,7 @@ class NodeMonitorModelTest : DriverBasedTest() { monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password, initialiseSerialization = false) rpc = monitor.proxyObservable.value!! - val bobNodeHandle = startNode(BOB.name, rpcUsers = listOf(cashUser)).getOrThrow() + val bobNodeHandle = startNode(providedName = BOB.name, rpcUsers = listOf(cashUser)).getOrThrow() bobNode = bobNodeHandle.nodeInfo val monitorBob = NodeMonitorModel() stateMachineUpdatesBob = monitorBob.stateMachineUpdates.bufferUntilSubscribed() diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ExchangeRateModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ExchangeRateModel.kt index 243d87843e..636603d1ab 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ExchangeRateModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ExchangeRateModel.kt @@ -3,25 +3,30 @@ package net.corda.client.jfx.model import javafx.beans.property.SimpleObjectProperty import javafx.beans.value.ObservableValue import net.corda.core.contracts.Amount +import java.math.BigDecimal +import java.math.RoundingMode import java.util.* - -interface ExchangeRate { - fun rate(from: Currency, to: Currency): Double -} - -fun ExchangeRate.exchangeAmount(amount: Amount, to: Currency) = - Amount(exchangeDouble(amount, to).toLong(), to) - -fun ExchangeRate.exchangeDouble(amount: Amount, to: Currency) = - rate(amount.token, to) * amount.quantity - /** * This model provides an exchange rate from arbitrary currency to arbitrary currency. - * TODO hook up an actual oracle */ +abstract class ExchangeRate { + /** + * Convert the given amount of a currency into the target currency. + * + * @return the original amount converted to an amount in the target currency. + */ + fun exchangeAmount(amount: Amount, to: Currency) = Amount.fromDecimal(amount.toDecimal().multiply(rate(amount.token, to)), to) + + abstract fun rate(from: Currency, to: Currency): BigDecimal +} + +/** + * Default implementation of an exchange rate model, which uses a fixed exchange rate. + */ +// TODO hook up an actual oracle class ExchangeRateModel { - val exchangeRate: ObservableValue = SimpleObjectProperty(object : ExchangeRate { - override fun rate(from: Currency, to: Currency) = 1.0 + val exchangeRate: ObservableValue = SimpleObjectProperty(object : ExchangeRate() { + override fun rate(from: Currency, to: Currency) = BigDecimal.ONE }) } diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt index ee47ed9947..923465b2b9 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NetworkIdentityModel.kt @@ -1,13 +1,14 @@ package net.corda.client.jfx.model +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader import javafx.beans.value.ObservableValue import javafx.collections.FXCollections import javafx.collections.ObservableList -import net.corda.client.jfx.utils.firstOrDefault -import net.corda.client.jfx.utils.firstOrNullObservable import net.corda.client.jfx.utils.fold import net.corda.client.jfx.utils.map -import net.corda.core.crypto.keys +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache.MapChange import java.security.PublicKey @@ -29,6 +30,12 @@ class NetworkIdentityModel { private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable) + private val identityCache = CacheBuilder.newBuilder() + .build>(CacheLoader.from { + publicKey -> + publicKey?.let { rpcProxy.map { it?.nodeIdentityFromParty(AnonymousParty(publicKey)) } } + }) + val parties: ObservableList = networkIdentities.filtered { !it.isCordaService() } val notaries: ObservableList = networkIdentities.filtered { it.advertisedServices.any { it.info.type.isNotary() } } val myIdentity = rpcProxy.map { it?.nodeIdentity() } @@ -38,8 +45,5 @@ class NetworkIdentityModel { return advertisedServices.any { it.info.type.isNetworkMap() || it.info.type.isNotary() } } - // TODO: Use Identity Service in service hub instead? - fun lookup(publicKey: PublicKey): ObservableValue = parties.firstOrDefault(notaries.firstOrNullObservable { it.notaryIdentity.owningKey.keys.any { it == publicKey } }) { - it.legalIdentity.owningKey.keys.any { it == publicKey } - } + fun partyFromPublicKey(publicKey: PublicKey): ObservableValue = identityCache[publicKey] } 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 33e160c742..7bd9623f52 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 @@ -89,7 +89,7 @@ class NodeMonitorModel { vaultUpdates.startWith(initialVaultUpdate).subscribe(vaultUpdatesSubject) // Transactions - val (transactions, newTransactions) = proxy.verifiedTransactionsFeed() + val (transactions, newTransactions) = proxy.internalVerifiedTransactionsFeed() newTransactions.startWith(transactions).subscribe(transactionsSubject) // SM -> TX mapping diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/AmountBindings.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/AmountBindings.kt index 26387c1663..49e76e227a 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/AmountBindings.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/utils/AmountBindings.kt @@ -29,7 +29,7 @@ object AmountBindings { return EasyBind.combine(observableCurrency, observableExchangeRate) { currency, exchangeRate -> Pair) -> Long>( currency, - { (quantity, _, token) -> (exchangeRate.rate(token, currency) * quantity).toLong() } + { amount -> exchangeRate.exchangeAmount(amount, currency).quantity } ) } } diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle index 70ebc5ed57..2b3c020f49 100644 --- a/client/rpc/build.gradle +++ b/client/rpc/build.gradle @@ -67,7 +67,7 @@ dependencies { testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:${assertj_version}" - testCompile project(':test-utils') + testCompile project(':node-driver') testCompile project(':client:mock') // Smoke tests do NOT have any Node code on the classpath! diff --git a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java index 2cd3ad6628..5a8a8349eb 100644 --- a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java +++ b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java @@ -6,12 +6,12 @@ import net.corda.core.messaging.CordaRPCOps; import net.corda.core.messaging.FlowHandle; import net.corda.core.node.NodeInfo; import net.corda.core.utilities.OpaqueBytes; +import net.corda.core.utilities.X500NameUtils; import net.corda.finance.flows.AbstractCashFlow; import net.corda.finance.flows.CashIssueFlow; import net.corda.nodeapi.User; import net.corda.smoketesting.NodeConfig; import net.corda.smoketesting.NodeProcess; -import org.bouncycastle.asn1.x500.X500Name; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -43,7 +43,7 @@ public class StandaloneCordaRPCJavaClientTest { private NodeInfo notaryNode; private NodeConfig notaryConfig = new NodeConfig( - new X500Name("CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), + X500NameUtils.getX500Name("Notary Service", "Zurich", "CH"), port.getAndIncrement(), port.getAndIncrement(), port.getAndIncrement(), 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 a8c670dc2a..4fab64c88f 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 @@ -9,10 +9,7 @@ import net.corda.core.messaging.* import net.corda.core.node.NodeInfo import net.corda.core.node.services.Vault import net.corda.core.node.services.vault.* -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.seconds +import net.corda.core.utilities.* import net.corda.finance.DOLLARS import net.corda.finance.POUNDS import net.corda.finance.SWISS_FRANCS @@ -26,7 +23,6 @@ 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 @@ -58,12 +54,12 @@ class StandaloneCordaRPClientTest { private lateinit var notaryNode: NodeInfo private val notaryConfig = NodeConfig( - legalName = X500Name("CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), - p2pPort = port.andIncrement, - rpcPort = port.andIncrement, - webPort = port.andIncrement, - extraServices = listOf("corda.notary.validating"), - users = listOf(user) + legalName = getX500Name(O = "Notary Service", OU = "corda", L = "Zurich", C = "CH"), + p2pPort = port.andIncrement, + rpcPort = port.andIncrement, + webPort = port.andIncrement, + extraServices = listOf("corda.notary.validating"), + users = listOf(user) ) @Before diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt new file mode 100644 index 0000000000..378a41f783 --- /dev/null +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCFailureTests.kt @@ -0,0 +1,64 @@ +package net.corda.client.rpc + +import com.esotericsoftware.kryo.KryoException +import net.corda.core.concurrent.CordaFuture +import net.corda.core.internal.concurrent.openFuture +import net.corda.core.messaging.* +import net.corda.core.utilities.getOrThrow +import net.corda.testing.rpcDriver +import net.corda.testing.startRpcClient +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Test + +class RPCFailureTests { + class Unserializable + interface Ops : RPCOps { + fun getUnserializable(): Unserializable + fun getUnserializableAsync(): CordaFuture + fun kotlinNPE() + fun kotlinNPEAsync(): CordaFuture + } + + class OpsImpl : Ops { + override val protocolVersion = 1 + override fun getUnserializable() = Unserializable() + override fun getUnserializableAsync(): CordaFuture { + return openFuture().apply { capture { getUnserializable() } } + } + + override fun kotlinNPE() { + (null as Any?)!!.hashCode() + } + + override fun kotlinNPEAsync(): CordaFuture { + return openFuture().apply { capture { kotlinNPE() } } + } + } + + private fun rpc(proc: (Ops) -> Any?): Unit = rpcDriver { + val server = startRpcServer(ops = OpsImpl()).getOrThrow() + proc(startRpcClient(server.broker.hostAndPort!!).getOrThrow()) + } + + @Test + fun `kotlin NPE`() = rpc { + assertThatThrownBy { it.kotlinNPE() }.isInstanceOf(KotlinNullPointerException::class.java) + } + + @Test + fun `kotlin NPE async`() = rpc { + val future = it.kotlinNPEAsync() + assertThatThrownBy { future.getOrThrow() }.isInstanceOf(KotlinNullPointerException::class.java) + } + + @Test + fun `unserializable`() = rpc { + assertThatThrownBy { it.getUnserializable() }.isInstanceOf(KryoException::class.java) + } + + @Test + fun `unserializable async`() = rpc { + val future = it.getUnserializableAsync() + assertThatThrownBy { future.getOrThrow() }.isInstanceOf(KryoException::class.java) + } +} diff --git a/config/dev/generalnodea.conf b/config/dev/generalnodea.conf index 94b08cfc0c..16eb90f526 100644 --- a/config/dev/generalnodea.conf +++ b/config/dev/generalnodea.conf @@ -1,4 +1,4 @@ -myLegalName : "CN=Bank A,O=Bank A,L=London,C=GB" +myLegalName : "O=Bank A,L=London,C=GB" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" p2pAddress : "localhost:10002" @@ -7,6 +7,6 @@ webAddress : "localhost:10004" extraAdvertisedServiceIds : [ "corda.interest_rates" ] networkMapService : { address : "localhost:10000" - legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=GB" + legalName : "O=Network Map Service,OU=corda,L=London,C=GB" } useHTTPS : false diff --git a/config/dev/generalnodeb.conf b/config/dev/generalnodeb.conf index 0fe9e66d9c..1eb839aece 100644 --- a/config/dev/generalnodeb.conf +++ b/config/dev/generalnodeb.conf @@ -7,6 +7,6 @@ webAddress : "localhost:10007" extraAdvertisedServiceIds : [ "corda.interest_rates" ] networkMapService : { address : "localhost:10000" - legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=GB" + legalName : "O=Network Map Service,OU=corda,L=London,C=GB" } useHTTPS : false diff --git a/config/dev/nameservernode.conf b/config/dev/nameservernode.conf index 154f88aad9..664f2b6816 100644 --- a/config/dev/nameservernode.conf +++ b/config/dev/nameservernode.conf @@ -1,4 +1,4 @@ -myLegalName : "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" +myLegalName : "O=Notary Service,OU=corda,L=London,C=GB" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" p2pAddress : "localhost:10000" diff --git a/constants.properties b/constants.properties index f8c76f96a6..68c33706ba 100644 --- a/constants.properties +++ b/constants.properties @@ -1,5 +1,5 @@ -gradlePluginsVersion=0.15.1 +gradlePluginsVersion=0.16.1 kotlinVersion=1.1.4 guavaVersion=21.0 bouncycastleVersion=1.57 -typesafeConfigVersion=1.3.1 \ No newline at end of file +typesafeConfigVersion=1.3.1 diff --git a/core/build.gradle b/core/build.gradle index ca33b3b725..655067d680 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -22,12 +22,15 @@ dependencies { // Bring in the MockNode infrastructure for writing protocol unit tests. testCompile project(":node") - testCompile project(":test-utils") + testCompile project(":node-driver") compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + // Quasar, for suspendable fibres. + compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8" + // Thread safety annotations compile "com.google.code.findbugs:jsr305:3.0.1" @@ -65,6 +68,15 @@ dependencies { compile "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final" } +// TODO Consider moving it to quasar-utils in the future (introduced with PR-1388) +task copyQuasarJar(type: Copy) { + from configurations.quasar + into "$project.rootProject.projectDir/lib" + rename { filename -> "quasar.jar"} +} + +jar.finalizedBy(copyQuasarJar) + configurations { testArtifacts.extendsFrom testRuntime } diff --git a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt index 6de842ff63..43101b2a3e 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt @@ -1,6 +1,6 @@ package net.corda.core.contracts -import net.corda.core.crypto.composite.CompositeKey +import net.corda.core.crypto.CompositeKey import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.exactAdd diff --git a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt new file mode 100644 index 0000000000..b55055e529 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt @@ -0,0 +1,42 @@ +package net.corda.core.contracts + +import net.corda.core.identity.Party +import net.corda.core.internal.extractFile +import java.io.FileNotFoundException +import java.io.InputStream +import java.io.OutputStream +import java.util.jar.JarInputStream + +/** + * An attachment is a ZIP (or an optionally signed JAR) that contains one or more files. Attachments are meant to + * contain public static data which can be referenced from transactions and utilised from contracts. Good examples + * of how attachments are meant to be used include: + * - Calendar data + * - Fixes (e.g. LIBOR) + * - Smart contract code + * - Legal documents + * - Facts generated by oracles which might be reused a lot + */ +interface Attachment : NamedByHash { + fun open(): InputStream + fun openAsJAR(): JarInputStream { + val stream = open() + try { + return JarInputStream(stream) + } catch (t: Throwable) { + stream.use { throw t } + } + } + + /** + * Finds the named file case insensitively and copies it to the output stream. + * @throws FileNotFoundException if the given path doesn't exist in the attachment. + */ + fun extractFile(path: String, outputTo: OutputStream) = openAsJAR().use { it.extractFile(path, outputTo) } + + /** + * The parties that have correctly signed the whole attachment. + * Can be empty, for example non-contract attachments won't be necessarily be signed. + */ + val signers: List +} diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt index c1d43020f3..51036f8dfe 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt @@ -32,33 +32,33 @@ inline fun requireThat(body: Requirements.() -> R) = Requirements.body() // TODO: Provide a version of select that interops with Java /** Filters the command list by type, party and public key all at once. */ -inline fun Collection>.select(signer: PublicKey? = null, +inline fun Collection>.select(signer: PublicKey? = null, party: AbstractParty? = null) = filter { it.value is T }. filter { if (signer == null) true else signer in it.signers }. filter { if (party == null) true else party in it.signingParties }. - map { AuthenticatedObject(it.signers, it.signingParties, it.value as T) } + map { CommandWithParties(it.signers, it.signingParties, it.value as T) } // TODO: Provide a version of select that interops with Java /** Filters the command list by type, parties and public keys all at once. */ -inline fun Collection>.select(signers: Collection?, +inline fun Collection>.select(signers: Collection?, parties: Collection?) = filter { it.value is T }. filter { if (signers == null) true else it.signers.containsAll(signers) }. filter { if (parties == null) true else it.signingParties.containsAll(parties) }. - map { AuthenticatedObject(it.signers, it.signingParties, it.value as T) } + map { CommandWithParties(it.signers, it.signingParties, it.value as T) } /** Ensures that a transaction has only one command that is of the given type, otherwise throws an exception. */ -inline fun Collection>.requireSingleCommand() = try { +inline fun Collection>.requireSingleCommand() = try { select().single() } catch (e: NoSuchElementException) { throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message. } /** Ensures that a transaction has only one command that is of the given type, otherwise throws an exception. */ -fun Collection>.requireSingleCommand(klass: Class) = - mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as AuthenticatedObject else null }.single() +fun Collection>.requireSingleCommand(klass: Class) = + mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as CommandWithParties else null }.single() /** * Simple functionality for verifying a move command. Verifies that each input has a signature from its owning key. @@ -67,7 +67,7 @@ fun Collection>.requireSingle */ @Throws(IllegalArgumentException::class) inline fun verifyMoveCommand(inputs: List, - commands: List>) + commands: List>) : MoveCommand { // Now check the digital signatures on the move command. Every input has an owning public key, and we must // see a signature from each of those keys. The actual signatures have been verified against the transaction 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 a877e98b9f..820d87fe6a 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -9,18 +9,11 @@ import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.MissingAttachmentsException -import net.corda.core.serialization.SerializeAsTokenContext import net.corda.core.serialization.serialize import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.OpaqueBytes -import java.io.FileNotFoundException -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream import java.security.PublicKey import java.time.Instant -import java.util.jar.JarInputStream /** Implemented by anything that can be named by a secure hash value (e.g. transactions, attachments). */ interface NamedByHash { @@ -219,11 +212,6 @@ interface LinearState : ContractState { * except at issuance/termination. */ val linearId: UniqueIdentifier - - /** - * True if this should be tracked by our vault(s). - */ - fun isRelevant(ourKeys: Set): Boolean } // DOCEND 2 @@ -289,7 +277,7 @@ abstract class TypeOnlyCommandData : CommandData { data class Command(val value: T, val signers: List) { // TODO Introduce NonEmptyList? init { - require(signers.isNotEmpty()) + require(signers.isNotEmpty()) { "The list of signers cannot be empty" } } constructor(data: T, key: PublicKey) : this(data, listOf(key)) @@ -312,9 +300,9 @@ interface MoveCommand : CommandData { data class UpgradeCommand(val upgradedContractClass: Class>) : CommandData // DOCSTART 6 -/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */ +/** A [Command] where the signing parties have been looked up if they have a well known/recognised institutional key. */ @CordaSerializable -data class AuthenticatedObject( +data class CommandWithParties( val signers: List, /** If any public keys were recognised, the looked up institutions are available here */ val signingParties: List, @@ -367,69 +355,6 @@ interface UpgradedContract ByteArray) : Attachment { - companion object { - fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray { - return { - val a = serviceHub.attachments.openAttachment(id) ?: throw MissingAttachmentsException(listOf(id)) - (a as? AbstractAttachment)?.attachmentData ?: a.open().use { it.readBytes() } - } - } - } - - protected val attachmentData: ByteArray by lazy(dataLoader) - override fun open(): InputStream = attachmentData.inputStream() - override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id - override fun hashCode() = id.hashCode() - override fun toString() = "${javaClass.simpleName}(id=$id)" -} - -@Throws(IOException::class) -fun JarInputStream.extractFile(path: String, outputTo: OutputStream) { - val p = path.toLowerCase().split('\\', '/') - while (true) { - val e = nextJarEntry ?: break - if (!e.isDirectory && e.name.toLowerCase().split('\\', '/') == p) { - copyTo(outputTo) - return - } - closeEntry() - } - throw FileNotFoundException(path) -} - /** * A privacy salt is required to compute nonces per transaction component in order to ensure that an adversary cannot * use brute force techniques and reveal the content of a Merkle-leaf hashed value. diff --git a/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt similarity index 89% rename from core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt rename to core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt index a1e31e2f01..3d7824751d 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt @@ -1,12 +1,8 @@ -package net.corda.core.crypto.composite +package net.corda.core.crypto -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.toStringShort -import net.corda.core.utilities.exactAdd +import net.corda.core.crypto.CompositeKey.NodeAndWeight import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.exactAdd import net.corda.core.utilities.sequence import org.bouncycastle.asn1.* import org.bouncycastle.asn1.x509.AlgorithmIdentifier @@ -15,8 +11,10 @@ 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. + * A tree data structure that enables the representation of composite public keys, which are used to represent + * the signing requirements for multisignature scenarios such as RAFT notary services. A composite key is a list + * of leaf keys and their contributing weight, and each leaf can be a conventional single key or a composite key. + * Keys contribute their weight to the total if they are matched by the signature. * * 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. @@ -26,9 +24,7 @@ import java.util.* * 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 + * @property 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 @@ -163,7 +159,7 @@ class CompositeKey private constructor(val threshold: Int, children: List get() = children.flatMap { it.node.keys }.toSet() // Uses PublicKey.keys extension. @@ -253,9 +249,14 @@ class CompositeKey private constructor(val threshold: Int, children: List 0) val n = children.size return if (n > 1) CompositeKey(threshold ?: children.map { (_, weight) -> weight }.sum(), children) @@ -265,15 +266,7 @@ class CompositeKey private constructor(val threshold: Int, children: List.expandedCompositeKeys: Set - get() = flatMap { it.keys }.toSet() \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/crypto/composite/KeyFactory.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeKeyFactory.kt similarity index 82% rename from core/src/main/kotlin/net/corda/core/crypto/composite/KeyFactory.kt rename to core/src/main/kotlin/net/corda/core/crypto/CompositeKeyFactory.kt index b933188d88..df7fc7c433 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/composite/KeyFactory.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeKeyFactory.kt @@ -1,11 +1,14 @@ -package net.corda.core.crypto.composite +package net.corda.core.crypto import java.security.* import java.security.spec.InvalidKeySpecException import java.security.spec.KeySpec import java.security.spec.X509EncodedKeySpec -class KeyFactory : KeyFactorySpi() { +/** + * Factory for generating composite keys from ASN.1 format key specifications. This is used by [CordaSecurityProvider]. + */ +class CompositeKeyFactory : KeyFactorySpi() { @Throws(InvalidKeySpecException::class) override fun engineGeneratePrivate(keySpec: KeySpec): PrivateKey { @@ -23,7 +26,7 @@ class KeyFactory : KeyFactorySpi() { @Throws(InvalidKeySpecException::class) override fun engineGetKeySpec(key: Key, keySpec: Class): T { - // Only support [X509EncodedKeySpec]. + // Only support X509EncodedKeySpec. throw InvalidKeySpecException("Not implemented yet $key $keySpec") } diff --git a/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt similarity index 94% rename from core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignature.kt rename to core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt index eb231cb7eb..0438a630ae 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt @@ -1,6 +1,6 @@ -package net.corda.core.crypto.composite +package net.corda.core.crypto -import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.composite.CompositeSignaturesWithKeys import net.corda.core.serialization.deserialize import java.io.ByteArrayOutputStream import java.security.* @@ -27,6 +27,7 @@ class CompositeSignature : Signature(SIGNATURE_ALGORITHM) { return signatureState!! } + @Deprecated("Deprecated in inherited API") @Throws(InvalidAlgorithmParameterException::class) override fun engineGetParameter(param: String?): Any { throw InvalidAlgorithmParameterException("Composite signatures do not support any parameters") @@ -46,6 +47,7 @@ class CompositeSignature : Signature(SIGNATURE_ALGORITHM) { } } + @Deprecated("Deprecated in inherited API") @Throws(InvalidAlgorithmParameterException::class) override fun engineSetParameter(param: String?, value: Any?) { throw InvalidAlgorithmParameterException("Composite signatures do not support any parameters") diff --git a/core/src/main/kotlin/net/corda/core/crypto/provider/CordaSecurityProvider.kt b/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt similarity index 72% rename from core/src/main/kotlin/net/corda/core/crypto/provider/CordaSecurityProvider.kt rename to core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt index 63f3abcf31..eaae5e2ffb 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/provider/CordaSecurityProvider.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CordaSecurityProvider.kt @@ -1,7 +1,5 @@ -package net.corda.core.crypto.provider +package net.corda.core.crypto -import net.corda.core.crypto.composite.CompositeKey -import net.corda.core.crypto.composite.CompositeSignature import org.bouncycastle.asn1.ASN1ObjectIdentifier import java.security.AccessController import java.security.PrivilegedAction @@ -17,20 +15,22 @@ class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME secur } 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") + put("KeyFactory.${CompositeKey.KEY_ALGORITHM}", "net.corda.core.crypto.CompositeKeyFactory") + put("Signature.${CompositeSignature.SIGNATURE_ALGORITHM}", "net.corda.core.crypto.CompositeSignature") val compositeKeyOID = CordaObjectIdentifier.COMPOSITE_KEY.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) + + val compositeSignatureOID = CordaObjectIdentifier.COMPOSITE_SIGNATURE.id + put("Alg.Alias.Signature.$compositeSignatureOID", CompositeSignature.SIGNATURE_ALGORITHM) + put("Alg.Alias.Signature.OID.$compositeSignatureOID", CompositeSignature.SIGNATURE_ALGORITHM) } } object CordaObjectIdentifier { // UUID-based OID - // TODO: Register for an OID space and issue our own shorter OID + // TODO: Register for an OID space and issue our own shorter OID. @JvmField val COMPOSITE_KEY = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002") @JvmField val COMPOSITE_SIGNATURE = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003") } 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 8b1d9fb542..c5f6b16256 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -1,9 +1,5 @@ package net.corda.core.crypto -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.corda.core.serialization.serialize import net.i2p.crypto.eddsa.EdDSAEngine import net.i2p.crypto.eddsa.EdDSAPrivateKey @@ -201,7 +197,7 @@ object Crypto { 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. + "BCPQC" to BouncyCastlePQCProvider()) // Unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it. private fun getBouncyCastleProvider() = BouncyCastleProvider().apply { putAll(EdDSASecurityProvider()) 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 f41fd55f6a..8e6af2cd37 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -2,8 +2,9 @@ package net.corda.core.crypto -import net.corda.core.crypto.composite.CompositeKey import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.toBase58 +import net.corda.core.utilities.toSHA256Bytes import java.math.BigInteger import net.corda.core.utilities.SgxSupport import java.security.* @@ -17,13 +18,9 @@ import java.security.* * @throws SignatureException if signing is not possible due to malformed data or private key. */ @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) -fun PrivateKey.sign(bytesToSign: ByteArray): DigitalSignature { - return DigitalSignature(Crypto.doSign(this, bytesToSign)) -} +fun PrivateKey.sign(bytesToSign: ByteArray): DigitalSignature = DigitalSignature(Crypto.doSign(this, bytesToSign)) -fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey { - return DigitalSignature.WithKey(publicKey, this.sign(bytesToSign).bytes) -} +fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey) = DigitalSignature.WithKey(publicKey, this.sign(bytesToSign).bytes) /** * Helper function to sign with a key pair. @@ -81,32 +78,27 @@ fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature) : Boolean /** Render a public key to its hash (in Base58) of its serialised form using the DL prefix. */ fun PublicKey.toStringShort(): String = "DL" + this.toSHA256Bytes().toBase58() -val PublicKey.keys: Set get() { - return if (this is CompositeKey) this.leafKeys - else setOf(this) -} +val PublicKey.keys: Set get() = (this as? CompositeKey)?.leafKeys ?: setOf(this) fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf(otherKey)) -fun PublicKey.isFulfilledBy(otherKeys: Iterable): Boolean { - return if (this is CompositeKey) this.isFulfilledBy(otherKeys) - else this in otherKeys -} +fun PublicKey.isFulfilledBy(otherKeys: Iterable): Boolean = (this as? CompositeKey)?.isFulfilledBy(otherKeys) ?: (this in otherKeys) -/** Checks whether any of the given [keys] matches a leaf on the CompositeKey tree or a single PublicKey */ +/** Checks whether any of the given [keys] matches a leaf on the [CompositeKey] tree or a single [PublicKey]. */ fun PublicKey.containsAny(otherKeys: Iterable): Boolean { return if (this is CompositeKey) keys.intersect(otherKeys).isNotEmpty() else this in otherKeys } -/** Returns the set of all [PublicKey]s of the signatures */ +/** Returns the set of all [PublicKey]s of the signatures. */ fun Iterable.byKeys() = map { it.by }.toSet() -// Allow Kotlin destructuring: val (private, public) = keyPair +// Allow Kotlin destructuring: +// val (private, public) = keyPair operator fun KeyPair.component1(): PrivateKey = this.private operator fun KeyPair.component2(): PublicKey = this.public -/** A simple wrapper that will make it easier to swap out the EC algorithm we use in future */ +/** A simple wrapper that will make it easier to swap out the EC algorithm we use in future. */ fun generateKeyPair(): KeyPair = Crypto.generateKeyPair() /** @@ -147,13 +139,11 @@ fun KeyPair.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Cr * @param numOfBytes how many random bytes to output. * @return a random [ByteArray]. * @throws NoSuchAlgorithmException thrown if "NativePRNGNonBlocking" is not supported on the JVM - * or if no strong SecureRandom implementations are available or if Security.getProperty("securerandom.strongAlgorithms") is null or empty, + * or if no strong [SecureRandom] implementations are available or if Security.getProperty("securerandom.strongAlgorithms") is null or empty, * which should never happen and suggests an unusual JVM or non-standard Java library. */ @Throws(NoSuchAlgorithmException::class) -fun secureRandomBytes(numOfBytes: Int): ByteArray { - return newSecureRandom().generateSeed(numOfBytes) -} +fun secureRandomBytes(numOfBytes: Int): ByteArray = newSecureRandom().generateSeed(numOfBytes) /** * This is a hack added because during deserialisation when no-param constructors are called sometimes default values 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 0e0f36c606..9e28be8a3a 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt @@ -6,8 +6,8 @@ import java.security.InvalidKeyException import java.security.PublicKey import java.security.SignatureException -// TODO: Is there a use-case for bare [DigitalSignature], or is everything a [DigitalSignature.WithKey]? If there's no -// actual use-case, we should merge the with key version into the parent class. In that case [CompositeSignatureWithKeys] +// TODO: Is there a use-case for bare DigitalSignature, or is everything a DigitalSignature.WithKey? If there's no +// actual use-case, we should merge the with key version into the parent class. In that case CompositeSignatureWithKeys // should be renamed to match. /** A wrapper around a digital signature. */ @CordaSerializable diff --git a/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt b/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt index 831e20758e..1555ed70fc 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt @@ -3,7 +3,7 @@ package net.corda.core.crypto import java.util.* /** - * Creation and verification of a Merkle Tree for a Wire Transaction. + * Creation and verification of a Merkle tree for a [WireTransaction]. * * See: https://en.wikipedia.org/wiki/Merkle_tree * @@ -49,7 +49,7 @@ sealed class MerkleTree { */ private tailrec fun buildMerkleTree(lastNodesList: List): MerkleTree { if (lastNodesList.size == 1) { - return lastNodesList[0] //Root reached. + return lastNodesList[0] // Root reached. } else { val newLevelHashes: MutableList = ArrayList() val n = lastNodesList.size diff --git a/core/src/main/kotlin/net/corda/core/crypto/NullKeys.kt b/core/src/main/kotlin/net/corda/core/crypto/NullKeys.kt new file mode 100644 index 0000000000..e5533895e5 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/crypto/NullKeys.kt @@ -0,0 +1,22 @@ +package net.corda.core.crypto + +import net.corda.core.identity.AnonymousParty +import net.corda.core.serialization.CordaSerializable +import java.security.PublicKey + +object NullKeys { + @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) + + /** A signature with a key and value of zero. Useful when you want a signature object that you know won't ever be used. */ + val NULL_SIGNATURE = TransactionSignature(ByteArray(32), NullPublicKey, SignatureMetadata(1, -1)) + +} \ No newline at end of file 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 af2114ab6d..837f27faea 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -12,7 +12,7 @@ import java.security.MessageDigest */ @CordaSerializable sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) { - /** SHA-256 is part of the SHA-2 hash function family. Generated hash is fixed size, 256-bits (32-bytes) */ + /** SHA-256 is part of the SHA-2 hash function family. Generated hash is fixed size, 256-bits (32-bytes). */ class SHA256(bytes: ByteArray) : SecureHash(bytes) { init { require(bytes.size == 32) diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt b/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt index a893381f7b..5027247980 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignableData.kt @@ -11,4 +11,3 @@ import net.corda.core.serialization.CordaSerializable */ @CordaSerializable data class SignableData(val txId: SecureHash, val signatureMetadata: SignatureMetadata) - 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 9ec9d26863..0176397c2f 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignatureScheme.kt @@ -29,4 +29,5 @@ data class SignatureScheme( val signatureName: String, val algSpec: AlgorithmParameterSpec?, val keySize: Int?, - val desc: String) + val desc: String +) diff --git a/core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt deleted file mode 100644 index 6edd82ca93..0000000000 --- a/core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt +++ /dev/null @@ -1,78 +0,0 @@ -@file:JvmName("X500NameUtils") -package net.corda.core.crypto - -import net.corda.core.internal.toX509CertHolder -import org.bouncycastle.asn1.ASN1Encodable -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x500.X500NameBuilder -import org.bouncycastle.asn1.x500.style.BCStyle -import org.bouncycastle.cert.X509CertificateHolder -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter -import java.security.KeyPair -import java.security.cert.X509Certificate - -/** - * Rebuild the distinguished name, adding a postfix to the common name. If no common name is present. - * @throws IllegalArgumentException if the distinguished name does not contain a common name element. - */ -fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName { attr -> attr.toString() + commonName } - -/** - * Rebuild the distinguished name, replacing the common name with the given value. If no common name is present, this - * adds one. - * @throws IllegalArgumentException if the distinguished name does not contain a common name element. - */ -fun X500Name.replaceCommonName(commonName: String): X500Name = mutateCommonName { _ -> commonName } - -/** - * Rebuild the distinguished name, replacing the common name with a value generated from the provided function. - * - * @param mutator a function to generate the new value from the previous one. - * @throws IllegalArgumentException if the distinguished name does not contain a common name element. - */ -private fun X500Name.mutateCommonName(mutator: (ASN1Encodable) -> String): X500Name { - val builder = X500NameBuilder(BCStyle.INSTANCE) - var matched = false - this.rdNs.forEach { rdn -> - rdn.typesAndValues.forEach { typeAndValue -> - when (typeAndValue.type) { - BCStyle.CN -> { - matched = true - builder.addRDN(typeAndValue.type, mutator(typeAndValue.value)) - } - else -> { - builder.addRDN(typeAndValue) - } - } - } - } - require(matched) { "Input X.500 name must include a common name (CN) attribute: ${this}" } - return builder.build() -} - -val X500Name.commonName: String get() = getRDNs(BCStyle.CN).first().first.value.toString() -val X500Name.orgName: String? get() = getRDNs(BCStyle.O).firstOrNull()?.first?.value?.toString() -val X500Name.location: String get() = getRDNs(BCStyle.L).first().first.value.toString() -val X500Name.locationOrNull: String? get() = try { - location -} catch (e: Exception) { - null -} -val X509Certificate.subject: X500Name get() = toX509CertHolder().subject -val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this) - -/** - * Generate a distinguished name from the provided values. - */ -@JvmOverloads -fun getX509Name(myLegalName: String, nearestCity: String, email: String, country: String? = null): X500Name { - return X500NameBuilder(BCStyle.INSTANCE).let { builder -> - builder.addRDN(BCStyle.CN, myLegalName) - builder.addRDN(BCStyle.L, nearestCity) - country?.let { builder.addRDN(BCStyle.C, it) } - builder.addRDN(BCStyle.E, email) - builder.build() - } -} - -data class CertificateAndKeyPair(val certificate: X509CertificateHolder, val keyPair: KeyPair) diff --git a/core/src/main/kotlin/net/corda/core/crypto/testing/NullKeys.kt b/core/src/main/kotlin/net/corda/core/crypto/testing/NullKeys.kt deleted file mode 100644 index de2bc3ee42..0000000000 --- a/core/src/main/kotlin/net/corda/core/crypto/testing/NullKeys.kt +++ /dev/null @@ -1,21 +0,0 @@ -package net.corda.core.crypto.testing - -import net.corda.core.crypto.SignatureMetadata -import net.corda.core.crypto.TransactionSignature -import net.corda.core.identity.AnonymousParty -import net.corda.core.serialization.CordaSerializable -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) - -/** A signature with a key and value of zero. Useful when you want a signature object that you know won't ever be used. */ -val NULL_SIGNATURE = TransactionSignature(ByteArray(32), NullPublicKey, SignatureMetadata(1, -1)) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt b/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt index c4a84da857..26e8b79874 100644 --- a/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt @@ -138,6 +138,7 @@ abstract class AbstractStateReplacementFlow { // We use Void? instead of Unit? as that's what you'd use in Java. abstract class Acceptor(val otherSide: Party, override val progressTracker: ProgressTracker = Acceptor.tracker()) : FlowLogic() { + constructor(otherSide: Party) : this(otherSide, Acceptor.tracker()) companion object { object VERIFYING : ProgressTracker.Step("Verifying state replacement proposal") object APPROVING : ProgressTracker.Step("State replacement approved") diff --git a/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt b/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt index 9f95d897a5..723a585cae 100644 --- a/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt @@ -3,7 +3,7 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy -import net.corda.core.crypto.toBase58String +import net.corda.core.utilities.toBase58String import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.ServiceHub diff --git a/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt index d6e7024811..c9a1cbee02 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ContractUpgradeFlow.kt @@ -1,72 +1,146 @@ package net.corda.core.flows +import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* +import net.corda.core.identity.Party import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey /** - * A flow to be used for upgrading state objects of an old contract to a new contract. + * A flow to be used for authorising and upgrading state objects of an old contract to a new contract. * * This assembles the transaction for contract upgrade and sends out change proposals to all participants * of that state. If participants agree to the proposed change, they each sign the transaction. * Finally, the transaction containing all signatures is sent back to each participant so they can record it and * use the new updated state for future transactions. */ -@InitiatingFlow -@StartableByRPC -class ContractUpgradeFlow( - originalState: StateAndRef, - newContractClass: Class> -) : AbstractStateReplacementFlow.Instigator>>(originalState, newContractClass) { +object ContractUpgradeFlow { - companion object { - @JvmStatic - fun verify(tx: LedgerTransaction) { - // Contract Upgrade transaction should have 1 input, 1 output and 1 command. - verify( - tx.inputStates.single(), - tx.outputStates.single(), - tx.commandsOfType().single()) + /** + * Authorise a contract state upgrade. + * This will store the upgrade authorisation in persistent store, and will be queried by [ContractUpgradeFlow.Acceptor] during contract upgrade process. + * Invoking this flow indicates the node is willing to upgrade the [StateAndRef] using the [UpgradedContract] class. + * This method will NOT initiate the upgrade process. To start the upgrade process, see [Initiator]. + */ + @StartableByRPC + class Authorise( + val stateAndRef: StateAndRef<*>, + private val upgradedContractClass: Class> + ) : FlowLogic() { + @Suspendable + override fun call(): Void? { + val upgrade = upgradedContractClass.newInstance() + if (upgrade.legacyContract != stateAndRef.state.data.contract.javaClass) { + throw FlowException("The contract state cannot be upgraded using provided UpgradedContract.") + } + serviceHub.contractUpgradeService.storeAuthorisedContractUpgrade(stateAndRef.ref, upgradedContractClass) + return null } - @JvmStatic - fun verify(input: ContractState, output: ContractState, commandData: Command) { - val command = commandData.value - val participantKeys: Set = input.participants.map { it.owningKey }.toSet() - val keysThatSigned: Set = commandData.signers.toSet() - @Suppress("UNCHECKED_CAST") - val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract - requireThat { - "The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys) - "Inputs state reference the legacy contract" using (input.contract.javaClass == upgradedContract.legacyContract) - "Outputs state reference the upgraded contract" using (output.contract.javaClass == command.upgradedContractClass) - "Output state must be an upgraded version of the input state" using (output == upgradedContract.upgrade(input)) + } + + /** + * Deauthorise a contract state upgrade. + * This will remove the upgrade authorisation from persistent store (and prevent any further upgrade) + */ + @StartableByRPC + class Deauthorise( + val stateRef: StateRef + ) : FlowLogic< Void?>() { + @Suspendable + override fun call(): Void? { + serviceHub.contractUpgradeService.removeAuthorisedContractUpgrade(stateRef) + return null + } + } + + @InitiatingFlow + @StartableByRPC + class Initiator( + originalState: StateAndRef, + newContractClass: Class> + ) : AbstractStateReplacementFlow.Instigator>>(originalState, newContractClass) { + + companion object { + fun assembleBareTx( + stateRef: StateAndRef, + upgradedContractClass: Class>, + privacySalt: PrivacySalt + ): TransactionBuilder { + val contractUpgrade = upgradedContractClass.newInstance() + return TransactionBuilder(stateRef.state.notary) + .withItems( + stateRef, + contractUpgrade.upgrade(stateRef.state.data), + Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants.map { it.owningKey }), + privacySalt + ) } } - fun assembleBareTx( - stateRef: StateAndRef, - upgradedContractClass: Class>, - privacySalt: PrivacySalt - ): TransactionBuilder { - val contractUpgrade = upgradedContractClass.newInstance() - return TransactionBuilder(stateRef.state.notary) - .withItems( - stateRef, - contractUpgrade.upgrade(stateRef.state.data), - Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants.map { it.owningKey }), - privacySalt - ) + @Suspendable + override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { + val baseTx = assembleBareTx(originalState, modification, PrivacySalt()) + 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) } } - override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { - val baseTx = assembleBareTx(originalState, modification, PrivacySalt()) - 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) + @StartableByRPC + @InitiatedBy(ContractUpgradeFlow.Initiator::class) + class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor>>(otherSide) { + + companion object { + @JvmStatic + fun verify(tx: LedgerTransaction) { + // Contract Upgrade transaction should have 1 input, 1 output and 1 command. + verify(tx.inputStates.single(), + tx.outputStates.single(), + tx.commandsOfType().single()) + } + + @JvmStatic + fun verify(input: ContractState, output: ContractState, commandData: Command) { + val command = commandData.value + val participantKeys: Set = input.participants.map { it.owningKey }.toSet() + val keysThatSigned: Set = commandData.signers.toSet() + @Suppress("UNCHECKED_CAST") + val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract + requireThat { + "The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys) + "Inputs state reference the legacy contract" using (input.contract.javaClass == upgradedContract.legacyContract) + "Outputs state reference the upgraded contract" using (output.contract.javaClass == command.upgradedContractClass) + "Output state must be an upgraded version of the input state" using (output == upgradedContract.upgrade(input)) + } + } + } + + @Suspendable + @Throws(StateReplacementException::class) + override fun verifyProposal(stx: SignedTransaction, proposal: AbstractStateReplacementFlow.Proposal>>) { + // Retrieve signed transaction from our side, we will apply the upgrade logic to the transaction on our side, and + // verify outputs matches the proposed upgrade. + val ourSTX = serviceHub.validatedTransactions.getTransaction(proposal.stateRef.txhash) + requireNotNull(ourSTX) { "We don't have a copy of the referenced state" } + val oldStateAndRef = ourSTX!!.tx.outRef(proposal.stateRef.index) + val authorisedUpgrade = serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?: + throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}") + val proposedTx = stx.tx + val expectedTx = ContractUpgradeFlow.Initiator.assembleBareTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt).toWireTransaction() + requireThat { + "The instigator is one of the participants" using (otherSide in oldStateAndRef.state.data.participants) + "The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" using (proposal.modification.name == authorisedUpgrade) + "The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx) + } + ContractUpgradeFlow.Acceptor.verify( + oldStateAndRef.state.data, + expectedTx.outRef(0).state.data, + expectedTx.toLedgerTransaction(serviceHub).commandsOfType().single()) + } } } diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index 584692bd3d..74d6b2562c 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -72,7 +72,7 @@ object NotaryFlow { val tx: Any = if (stx.isNotaryChangeTransaction()) { stx.notaryChangeTx } else { - stx.tx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow }) + stx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow }) } sendAndReceiveWithRetry(notaryParty, tx) } 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 5652fc2688..f25d6446d1 100644 --- a/core/src/main/kotlin/net/corda/core/identity/Party.kt +++ b/core/src/main/kotlin/net/corda/core/identity/Party.kt @@ -2,7 +2,7 @@ package net.corda.core.identity import net.corda.core.contracts.PartyAndReference import net.corda.core.crypto.Crypto -import net.corda.core.crypto.composite.CompositeKey +import net.corda.core.crypto.CompositeKey import net.corda.core.utilities.OpaqueBytes import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.cert.X509CertificateHolder diff --git a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt new file mode 100644 index 0000000000..2f402f43e5 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt @@ -0,0 +1,69 @@ +package net.corda.core.internal + +import net.corda.core.contracts.Attachment +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.serialization.MissingAttachmentsException +import net.corda.core.serialization.SerializeAsTokenContext +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.security.CodeSigner +import java.util.jar.JarInputStream + +abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment { + companion object { + fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray { + return { + val a = serviceHub.attachments.openAttachment(id) ?: throw MissingAttachmentsException(listOf(id)) + (a as? AbstractAttachment)?.attachmentData ?: a.open().use { it.readBytes() } + } + } + + /** @see */ + private val unsignableEntryName = "META-INF/(?:.*[.](?:SF|DSA|RSA)|SIG-.*)".toRegex() + private val shredder = ByteArray(1024) + } + + protected val attachmentData: ByteArray by lazy(dataLoader) + override fun open(): InputStream = attachmentData.inputStream() + override val signers by lazy { + // Can't start with empty set if we're doing intersections. Logically the null means "all possible signers": + var attachmentSigners: MutableSet? = null + openAsJAR().use { jar -> + while (true) { + val entry = jar.nextJarEntry ?: break + if (entry.isDirectory || unsignableEntryName.matches(entry.name)) continue + while (jar.read(shredder) != -1) { // Must read entry fully for codeSigners to be valid. + // Do nothing. + } + val entrySigners = entry.codeSigners ?: emptyArray() + attachmentSigners?.retainAll(entrySigners) ?: run { attachmentSigners = entrySigners.toMutableSet() } + if (attachmentSigners!!.isEmpty()) break // Performance short-circuit. + } + } + (attachmentSigners ?: emptySet()).map { + Party(it.signerCertPath.certificates[0].toX509CertHolder()) + }.sortedBy { it.name.toString() } // Determinism. + } + + override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id + override fun hashCode() = id.hashCode() + override fun toString() = "${javaClass.simpleName}(id=$id)" +} + +@Throws(IOException::class) +fun JarInputStream.extractFile(path: String, outputTo: OutputStream) { + fun String.norm() = toLowerCase().split('\\', '/') // XXX: Should this really be locale-sensitive? + val p = path.norm() + while (true) { + val e = nextJarEntry ?: break + if (!e.isDirectory && e.name.norm() == p) { + copyTo(outputTo) + return + } + closeEntry() + } + throw FileNotFoundException(path) +} diff --git a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt index 5006843121..ce95ee9838 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FetchDataFlow.kt @@ -1,7 +1,6 @@ package net.corda.core.internal import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.AbstractAttachment import net.corda.core.contracts.Attachment import net.corda.core.contracts.NamedByHash import net.corda.core.crypto.SecureHash 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 3133e1c3b0..6470493af9 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -123,11 +123,11 @@ interface CordaRPCOps : RPCOps { * * Generic vault query function which takes a [QueryCriteria] object to define filters, * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), - * and returns a [Vault.PageAndUpdates] object containing - * 1) a snapshot as a [Vault.Page] (described previously in [queryBy]) + * and returns a [DataFeed] object containing + * 1) a snapshot as a [Vault.Page] (described previously in [CordaRPCOps.vaultQueryBy]) * 2) an [Observable] of [Vault.Update] * - * Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function. + * Notes: the snapshot part of the query adheres to the same behaviour as the [CordaRPCOps.vaultQueryBy] function. * the [QueryCriteria] applies to both snapshot and deltas (streaming updates). */ // DOCSTART VaultTrackByAPI @@ -157,15 +157,21 @@ interface CordaRPCOps : RPCOps { // DOCEND VaultTrackAPIHelpers /** - * Returns a list of all recorded transactions. + * @suppress Returns a list of all recorded transactions. + * + * TODO This method should be removed once SGX work is finalised and the design of the corresponding API using [FilteredTransaction] can be started */ - fun verifiedTransactionsSnapshot(): List + @Deprecated("This method is intended only for internal use and will be removed from the public API soon.") + fun internalVerifiedTransactionsSnapshot(): List /** - * Returns a data feed of all recorded transactions and an observable of future recorded ones. + * @suppress Returns a data feed of all recorded transactions and an observable of future recorded ones. + * + * TODO This method should be removed once SGX work is finalised and the design of the corresponding API using [FilteredTransaction] can be started */ + @Deprecated("This method is intended only for internal use and will be removed from the public API soon.") @RPCReturnsObservables - fun verifiedTransactionsFeed(): DataFeed, SignedTransaction> + fun internalVerifiedTransactionsFeed(): DataFeed, SignedTransaction> /** * Returns a snapshot list of existing state machine id - recorded transaction hash mappings. @@ -194,14 +200,14 @@ interface CordaRPCOps : RPCOps { * Start the given flow with the given arguments. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC]. */ @RPCReturnsObservables - fun startFlowDynamic(logicType: Class>, vararg args: Any?): FlowHandle + fun startFlowDynamic(logicType: Class>, vararg args: Any?): FlowHandle /** * Start the given flow with the given arguments, returning an [Observable] with a single observation of the * result of running the flow. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC]. */ @RPCReturnsObservables - fun startTrackedFlowDynamic(logicType: Class>, vararg args: Any?): FlowProgressHandle + fun startTrackedFlowDynamic(logicType: Class>, vararg args: Any?): FlowProgressHandle /** * Returns Node's identity, assuming this will not change while the node is running. @@ -233,20 +239,6 @@ interface CordaRPCOps : RPCOps { */ fun uploadAttachment(jar: InputStream): SecureHash - /** - * Authorise a contract state upgrade. - * This will store the upgrade authorisation in the vault, and will be queried by [ContractUpgradeFlow.Acceptor] during contract upgrade process. - * Invoking this method indicate the node is willing to upgrade the [state] using the [upgradedContractClass]. - * This method will NOT initiate the upgrade process. To start the upgrade process, see [ContractUpgradeFlow.Instigator]. - */ - fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class>) - - /** - * Authorise a contract state upgrade. - * This will remove the upgrade authorisation from the vault. - */ - fun deauthoriseContractUpgrade(state: StateAndRef<*>) - /** * Returns the node's current time. */ @@ -257,7 +249,7 @@ interface CordaRPCOps : RPCOps { * complete with an exception if it is unable to. */ @RPCReturnsObservables - fun waitUntilRegisteredWithNetworkMap(): CordaFuture + fun waitUntilNetworkReady(): CordaFuture // TODO These need rethinking. Instead of these direct calls we should have a way of replicating a subset of // the node's state locally and query that directly. @@ -299,6 +291,11 @@ interface CordaRPCOps : RPCOps { * @return the node info if available. */ fun nodeIdentityFromParty(party: AbstractParty): NodeInfo? + + /** + * Clear all network map data from local node cache. + */ + fun clearNetworkMapCache() } inline fun CordaRPCOps.vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), @@ -322,25 +319,25 @@ inline fun CordaRPCOps.vaultTrackBy(criteria: QueryC * Note that the passed in constructor function is only used for unification of other type parameters and reification of * the Class instance of the flow. This could be changed to use the constructor function directly. */ -inline fun > CordaRPCOps.startFlow( +inline fun > CordaRPCOps.startFlow( @Suppress("UNUSED_PARAMETER") flowConstructor: () -> R ): FlowHandle = startFlowDynamic(R::class.java) -inline fun > CordaRPCOps.startFlow( +inline fun > CordaRPCOps.startFlow( @Suppress("UNUSED_PARAMETER") flowConstructor: (A) -> R, arg0: A ): FlowHandle = startFlowDynamic(R::class.java, arg0) -inline fun > CordaRPCOps.startFlow( +inline fun > CordaRPCOps.startFlow( @Suppress("UNUSED_PARAMETER") flowConstructor: (A, B) -> R, arg0: A, arg1: B ): FlowHandle = startFlowDynamic(R::class.java, arg0, arg1) -inline fun > CordaRPCOps.startFlow( +inline fun > CordaRPCOps.startFlow( @Suppress("UNUSED_PARAMETER") flowConstructor: (A, B, C) -> R, arg0: A, @@ -348,7 +345,7 @@ inline fun > CordaRPCOps.startFlow( arg2: C ): FlowHandle = startFlowDynamic(R::class.java, arg0, arg1, arg2) -inline fun > CordaRPCOps.startFlow( +inline fun > CordaRPCOps.startFlow( @Suppress("UNUSED_PARAMETER") flowConstructor: (A, B, C, D) -> R, arg0: A, @@ -357,7 +354,7 @@ inline fun > CordaRPCOps.startFlow arg3: D ): FlowHandle = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3) -inline fun > CordaRPCOps.startFlow( +inline fun > CordaRPCOps.startFlow( @Suppress("UNUSED_PARAMETER") flowConstructor: (A, B, C, D, E) -> R, arg0: A, @@ -367,7 +364,7 @@ inline fun > CordaRPCOps.startF arg4: E ): FlowHandle = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4) -inline fun > CordaRPCOps.startFlow( +inline fun > CordaRPCOps.startFlow( @Suppress("UNUSED_PARAMETER") flowConstructor: (A, B, C, D, E, F) -> R, arg0: A, @@ -382,20 +379,20 @@ inline fun > CordaRPCOps.sta * Same again, except this time with progress-tracking enabled. */ @Suppress("unused") -inline fun > CordaRPCOps.startTrackedFlow( +inline fun > CordaRPCOps.startTrackedFlow( @Suppress("unused_parameter") flowConstructor: () -> R ): FlowProgressHandle = startTrackedFlowDynamic(R::class.java) @Suppress("unused") -inline fun > CordaRPCOps.startTrackedFlow( +inline fun > CordaRPCOps.startTrackedFlow( @Suppress("unused_parameter") flowConstructor: (A) -> R, arg0: A ): FlowProgressHandle = startTrackedFlowDynamic(R::class.java, arg0) @Suppress("unused") -inline fun > CordaRPCOps.startTrackedFlow( +inline fun > CordaRPCOps.startTrackedFlow( @Suppress("unused_parameter") flowConstructor: (A, B) -> R, arg0: A, @@ -403,7 +400,7 @@ inline fun > CordaRPCOps.startTrackedFlo ): FlowProgressHandle = startTrackedFlowDynamic(R::class.java, arg0, arg1) @Suppress("unused") -inline fun > CordaRPCOps.startTrackedFlow( +inline fun > CordaRPCOps.startTrackedFlow( @Suppress("unused_parameter") flowConstructor: (A, B, C) -> R, arg0: A, @@ -412,7 +409,7 @@ inline fun > CordaRPCOps.startTracked ): FlowProgressHandle = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2) @Suppress("unused") -inline fun > CordaRPCOps.startTrackedFlow( +inline fun > CordaRPCOps.startTrackedFlow( @Suppress("unused_parameter") flowConstructor: (A, B, C, D) -> R, arg0: A, @@ -422,7 +419,7 @@ inline fun > CordaRPCOps.startTrac ): FlowProgressHandle = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3) @Suppress("unused") -inline fun > CordaRPCOps.startTrackedFlow( +inline fun > CordaRPCOps.startTrackedFlow( @Suppress("unused_parameter") flowConstructor: (A, B, C, D, E) -> R, arg0: A, @@ -433,7 +430,7 @@ inline fun > CordaRPCOps.startT ): FlowProgressHandle = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4) @Suppress("unused") -inline fun > CordaRPCOps.startTrackedFlow( +inline fun > CordaRPCOps.startTrackedFlow( @Suppress("unused_parameter") flowConstructor: (A, B, C, D, E, F) -> R, arg0: A, 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 d7f30a4f50..322fcf3d5a 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -6,7 +6,7 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.NonEmptySet +import net.corda.core.utilities.locality /** * Information for an advertised service including the service specific identity information. @@ -21,11 +21,13 @@ data class ServiceEntry(val info: ServiceInfo, val identity: PartyAndCertificate // TODO We currently don't support multi-IP/multi-identity nodes, we only left slots in the data structures. @CordaSerializable 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: NonEmptySet, + // TODO After removing of services these two fields will be merged together and made NonEmptySet. + val legalIdentityAndCert: PartyAndCertificate, + val legalIdentitiesAndCerts: Set, val platformVersion: Int, val advertisedServices: List = emptyList(), - val worldMapLocation: WorldMapLocation? = null) { + val serial: Long +) { init { require(advertisedServices.none { it.identity == legalIdentityAndCert }) { "Service identities must be different from node legal identity" @@ -37,4 +39,12 @@ data class NodeInfo(val addresses: List, fun serviceIdentities(type: ServiceType): List { return advertisedServices.mapNotNull { if (it.info.type.isSubTypeOf(type)) it.identity.party else null } } + + /** + * Uses node's owner X500 name to infer the node's location. Used in Explorer in map view. + */ + fun getWorldMapLocation(): WorldMapLocation? { + val nodeOwnerLocation = legalIdentity.name.locality + return nodeOwnerLocation.let { CityDatabase[it] } + } } 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 fb882eb4ad..695e32a0a4 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -7,6 +7,7 @@ import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken +import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey @@ -46,6 +47,7 @@ interface ServiceHub : ServicesForResolution { val vaultService: VaultService val vaultQueryService: VaultQueryService val keyManagementService: KeyManagementService + val contractUpgradeService: ContractUpgradeService /** * A map of hash->tx where tx has been signature/contract validated and the states are known to be correct. @@ -170,7 +172,7 @@ interface ServiceHub : ServicesForResolution { /** * Helper method to construct an initial partially signed transaction from a TransactionBuilder - * using the default identity key contained in the node. The legal Indentity key is used to sign. + * using the default identity key contained in the node. The legal identity key is used to sign. * @param builder The TransactionBuilder to seal with the node's signature. * Any existing signatures on the builder will be preserved. * @return Returns a SignedTransaction with the new node signature attached. @@ -202,7 +204,9 @@ interface ServiceHub : ServicesForResolution { } /** - * Helper method to create an additional signature for an existing (partially) [SignedTransaction]. + * Helper method to create an additional signature for an existing (partially) [SignedTransaction]. Additional + * [SignatureMetadata], including the + * platform version used during signing and the cryptographic signature scheme use, is added to the signature. * @param signedTransaction The [SignedTransaction] to which the signature will apply. * @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 @@ -213,10 +217,12 @@ interface ServiceHub : ServicesForResolution { createSignature(signedTransaction, publicKey, SignatureMetadata(myInfo.platformVersion, Crypto.findSignatureScheme(publicKey).schemeNumberID)) /** - * Helper method to create an additional signature for an existing (partially) [SignedTransaction] - * using the default identity signing key of the node. The legal identity key is used to sign. + * Helper method to create a signature for an existing (partially) [SignedTransaction] + * using the default identity signing key of the node. The legal identity key is used to sign. Additional + * [SignatureMetadata], including the + * platform version used during signing and the cryptographic signature scheme use, is added to the signature. * @param signedTransaction The SignedTransaction to which the signature will apply. - * @return The DigitalSignature.WithKey generated by signing with the internally held identity PrivateKey. + * @return the TransactionSignature generated by signing with the internally held identity PrivateKey. */ fun createSignature(signedTransaction: SignedTransaction): TransactionSignature { return createSignature(signedTransaction, legalIdentityKey) @@ -242,6 +248,36 @@ interface ServiceHub : ServicesForResolution { */ fun addSignature(signedTransaction: SignedTransaction): SignedTransaction = addSignature(signedTransaction, legalIdentityKey) + // Helper method to create a signature for a FilteredTransaction. + private fun createSignature(filteredTransaction: FilteredTransaction, publicKey: PublicKey, signatureMetadata: SignatureMetadata): TransactionSignature { + val signableData = SignableData(filteredTransaction.id, signatureMetadata) + return keyManagementService.sign(signableData, publicKey) + } + + /** + * Helper method to create a signature for a FilteredTransaction. Additional [SignatureMetadata], including the + * platform version used during signing and the cryptographic signature scheme use, is added to the signature. + * @param filteredTransaction the [FilteredTransaction] to which the signature will apply. + * @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 [TransactionSignature] generated by signing with the internally held [java.security.PrivateKey]. + */ + fun createSignature(filteredTransaction: FilteredTransaction, publicKey: PublicKey) = + createSignature(filteredTransaction, publicKey, SignatureMetadata(myInfo.platformVersion, Crypto.findSignatureScheme(publicKey).schemeNumberID)) + + /** + * Helper method to create a signature for a FilteredTransaction + * using the default identity signing key of the node. The legal identity key is used to sign. Additional + * [SignatureMetadata], including the platform version used during signing and the cryptographic signature scheme use, + * is added to the signature. + * @param filteredTransaction the FilteredTransaction to which the signature will apply. + * @return the [TransactionSignature] generated by signing with the internally held identity [java.security.PrivateKey]. + */ + fun createSignature(filteredTransaction: FilteredTransaction): TransactionSignature { + return createSignature(filteredTransaction, legalIdentityKey) + } + /** * Exposes a JDBC connection (session) object using the currently configured database. * Applications can use this to execute arbitrary SQL queries (native, direct, prepared, callable) diff --git a/core/src/main/kotlin/net/corda/core/node/services/ContractUpgradeService.kt b/core/src/main/kotlin/net/corda/core/node/services/ContractUpgradeService.kt new file mode 100644 index 0000000000..42d1689796 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/node/services/ContractUpgradeService.kt @@ -0,0 +1,22 @@ +package net.corda.core.node.services + +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.UpgradedContract +import net.corda.core.flows.ContractUpgradeFlow + +/** + * The [ContractUpgradeService] is responsible for securely upgrading contract state objects according to + * a specified and mutually agreed (amongst participants) contract version. + * See also [ContractUpgradeFlow] to understand the workflow associated with contract upgrades. + */ +interface ContractUpgradeService { + + /** Get contracts we would be willing to upgrade the suggested contract to. */ + fun getAuthorisedContractUpgrade(ref: StateRef): String? + + /** Store authorised state ref and associated UpgradeContract class */ + fun storeAuthorisedContractUpgrade(ref: StateRef, upgradedContractClass: Class>) + + /** Remove a previously authorised state ref */ + fun removeAuthorisedContractUpgrade(ref: StateRef) +} 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 33fbd1f10e..3f854a44fc 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 @@ -8,6 +8,7 @@ import net.corda.core.internal.randomOrNull import net.corda.core.messaging.DataFeed import net.corda.core.node.NodeInfo import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.NetworkHostAndPort import org.bouncycastle.asn1.x500.X500Name import rx.Observable import java.security.PublicKey @@ -44,7 +45,7 @@ interface NetworkMapCache { /** Tracks changes to the network map cache */ val changed: Observable /** Future to track completion of the NetworkMapService registration. */ - val mapServiceRegistered: CordaFuture + val nodeReady: CordaFuture /** * Atomically get the current party nodes and a stream of updates. Note that the Observable buffers updates until the @@ -76,7 +77,10 @@ interface NetworkMapCache { 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 } + fun getNodeByLegalName(principal: X500Name): NodeInfo? + + /** Look up the node info for a host and port. */ + fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? /** * In general, nodes can advertise multiple identities: a legal identity, and separate identities for each of @@ -144,4 +148,9 @@ interface NetworkMapCache { "Your options are: ${notaryNodes.map { "\"${it.notaryIdentity.name}\"" }.joinToString()}.") return notary.advertisedServices.any { it.info.type.isValidatingNotary() } } + + /** + * Clear all network map data from local node cache. + */ + fun clearNetworkMapCache() } diff --git a/core/src/main/kotlin/net/corda/core/node/services/ServiceType.kt b/core/src/main/kotlin/net/corda/core/node/services/ServiceType.kt index 80111a3cdb..97d2888e88 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/ServiceType.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/ServiceType.kt @@ -30,6 +30,7 @@ class ServiceType private constructor(val id: String) { val regulator: ServiceType = corda.getSubType("regulator") val networkMap: ServiceType = corda.getSubType("network_map") + @JvmStatic fun getServiceType(namespace: String, typeId: String): ServiceType { require(!namespace.startsWith("corda")) { "Corda namespace is protected" } return baseWithSubType(namespace, typeId) diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index ebd70ae93d..0b7aeb862f 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -173,17 +173,6 @@ interface VaultService { */ val updatesPublisher: PublishSubject> - /** - * Possibly update the vault by marking as spent states that these transactions consume, and adding any relevant - * new states that they create. You should only insert transactions that have been successfully verified here! - * - * TODO: Consider if there's a good way to enforce the must-be-verified requirement in the type system. - */ - fun notifyAll(txns: Iterable) - - /** Same as notifyAll but with a single transaction. */ - fun notify(tx: CoreTransaction) = notifyAll(listOf(tx)) - /** * Provide a [CordaFuture] for when a [StateRef] is consumed, which can be very useful in building tests. */ @@ -191,24 +180,6 @@ interface VaultService { return updates.filter { it.consumed.any { it.ref == ref } }.toFuture() } - /** Get contracts we would be willing to upgrade the suggested contract to. */ - // TODO: We need a better place to put business logic functions - fun getAuthorisedContractUpgrade(ref: StateRef): Class>? - - /** - * Authorise a contract state upgrade. - * This will store the upgrade authorisation in the vault, and will be queried by [ContractUpgradeFlow.Acceptor] during contract upgrade process. - * Invoking this method indicate the node is willing to upgrade the [state] using the [upgradedContractClass]. - * This method will NOT initiate the upgrade process. To start the upgrade process, see [ContractUpgradeFlow.Instigator]. - */ - fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class>) - - /** - * Authorise a contract state upgrade. - * This will remove the upgrade authorisation from the vault. - */ - fun deauthoriseContractUpgrade(stateAndRef: StateAndRef<*>) - /** * Add a note to an existing [LedgerTransaction] given by its unique [SecureHash] id * Multiple notes may be attached to the same [LedgerTransaction]. 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 5a55d9cc92..0f9d985791 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt @@ -4,7 +4,6 @@ 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 java.util.* import javax.persistence.* diff --git a/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt new file mode 100644 index 0000000000..df8016115a --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/schemas/NodeInfoSchema.kt @@ -0,0 +1,125 @@ +package net.corda.core.schemas + +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.node.NodeInfo +import net.corda.core.node.ServiceEntry +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.toBase58String +import java.io.Serializable +import javax.persistence.* + +object NodeInfoSchema + +object NodeInfoSchemaV1 : MappedSchema( + schemaFamily = NodeInfoSchema.javaClass, + version = 1, + mappedTypes = listOf(PersistentNodeInfo::class.java, DBPartyAndCertificate::class.java, DBHostAndPort::class.java) +) { + @Entity + @Table(name = "node_infos") + class PersistentNodeInfo( + @Id + @GeneratedValue + @Column(name = "node_info_id") + var id: Int, + + @Column(name = "addresses") + @OneToMany(cascade = arrayOf(CascadeType.ALL), orphanRemoval = true) + val addresses: List, + + @Column(name = "legal_identities_certs") + @ManyToMany(cascade = arrayOf(CascadeType.ALL)) + @JoinTable(name = "link_nodeinfo_party", + joinColumns = arrayOf(JoinColumn(name="node_info_id")), + inverseJoinColumns = arrayOf(JoinColumn(name="party_name"))) + val legalIdentitiesAndCerts: Set, + + @Column(name = "platform_version") + val platformVersion: Int, + + @Column(name = "advertised_services") + @ElementCollection + var advertisedServices: List = emptyList(), + + /** + * serial is an increasing value which represents the version of [NodeInfo]. + * Not expected to be sequential, but later versions of the registration must have higher values + * Similar to the serial number on DNS records. + */ + @Column(name = "serial") + val serial: Long + ) { + fun toNodeInfo(): NodeInfo { + return NodeInfo( + this.addresses.map { it.toHostAndPort() }, + this.legalIdentitiesAndCerts.filter { it.isMain }.single().toLegalIdentityAndCert(), // TODO Workaround, it will be changed after PR with services removal. + this.legalIdentitiesAndCerts.filter { !it.isMain }.map { it.toLegalIdentityAndCert() }.toSet(), + this.platformVersion, + this.advertisedServices.map { + it.serviceEntry?.deserialize() ?: throw IllegalStateException("Service entry shouldn't be null") + }, + this.serial + ) + } + } + + @Embeddable + data class PKHostAndPort( + val host: String? = null, + val port: Int? = null + ) : Serializable + + @Entity + data class DBHostAndPort( + @EmbeddedId + private val pk: PKHostAndPort + ) { + companion object { + fun fromHostAndPort(hostAndPort: NetworkHostAndPort) = DBHostAndPort( + PKHostAndPort(hostAndPort.host, hostAndPort.port) + ) + } + fun toHostAndPort(): NetworkHostAndPort { + return NetworkHostAndPort(this.pk.host!!, this.pk.port!!) + } + } + + @Embeddable // TODO To be removed with services. + data class DBServiceEntry( + @Column(length = 65535) + val serviceEntry: ByteArray? = null + ) + + /** + * PartyAndCertificate entity (to be replaced by referencing final Identity Schema). + */ + @Entity + @Table(name = "node_info_party_cert") + data class DBPartyAndCertificate( + @Id + @Column(name = "owning_key", length = 65535, nullable = false) + val owningKey: String, + + //@Id // TODO Do we assume that names are unique? Note: We can't have it as Id, because our toString on X500 is inconsistent. + @Column(name = "party_name", nullable = false) + val name: String, + + @Column(name = "party_cert_binary") + @Lob + val partyCertBinary: ByteArray, + + val isMain: Boolean, + + @ManyToMany(mappedBy = "legalIdentitiesAndCerts", cascade = arrayOf(CascadeType.ALL)) // ManyToMany because of distributed services. + private val persistentNodeInfos: Set = emptySet() + ) { + constructor(partyAndCert: PartyAndCertificate, isMain: Boolean = false) + : this(partyAndCert.party.owningKey.toBase58String(), partyAndCert.party.name.toString(), partyAndCert.serialize().bytes, isMain) + + fun toLegalIdentityAndCert(): PartyAndCertificate { + return partyCertBinary.deserialize() + } + } +} diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt index b9f2094394..9440851fdd 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -3,8 +3,6 @@ package net.corda.core.serialization import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.internal.WriteOnceProperty -import net.corda.core.serialization.SerializationDefaults.P2P_CONTEXT -import net.corda.core.serialization.SerializationDefaults.SERIALIZATION_FACTORY import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.sequence @@ -13,7 +11,7 @@ import net.corda.core.utilities.sequence * An abstraction for serializing and deserializing objects, with support for versioning of the wire format via * a header / prefix in the bytes. */ -interface SerializationFactory { +abstract class SerializationFactory { /** * Deserialize the bytes in to an object, using the prefixed bytes to determine the format. * @@ -21,7 +19,7 @@ interface SerializationFactory { * @param clazz The class or superclass or the object to be deserialized, or [Any] or [Object] if unknown. * @param context A context that configures various parameters to deserialization. */ - fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T + abstract fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T /** * Serialize an object to bytes using the preferred serialization format version from the context. @@ -29,7 +27,63 @@ interface SerializationFactory { * @param obj The object to be serialized. * @param context A context that configures various parameters to serialization, including the serialization format version. */ - fun serialize(obj: T, context: SerializationContext): SerializedBytes + abstract fun serialize(obj: T, context: SerializationContext): SerializedBytes + + /** + * If there is a need to nest serialization/deserialization with a modified context during serialization or deserialization, + * this will return the current context used to start serialization/deserialization. + */ + val currentContext: SerializationContext? get() = _currentContext.get() + + /** + * A context to use as a default if you do not require a specially configured context. It will be the current context + * if the use is somehow nested (see [currentContext]). + */ + val defaultContext: SerializationContext get() = currentContext ?: SerializationDefaults.P2P_CONTEXT + + private val _currentContext = ThreadLocal() + + /** + * Change the current context inside the block to that supplied. + */ + fun withCurrentContext(context: SerializationContext?, block: () -> T): T { + val priorContext = _currentContext.get() + if (context != null) _currentContext.set(context) + try { + return block() + } finally { + if (context != null) _currentContext.set(priorContext) + } + } + + /** + * Allow subclasses to temporarily mark themselves as the current factory for the current thread during serialization/deserialization. + * Will restore the prior context on exiting the block. + */ + protected fun asCurrent(block: SerializationFactory.() -> T): T { + val priorContext = _currentFactory.get() + _currentFactory.set(this) + try { + return block() + } finally { + _currentFactory.set(priorContext) + } + } + + companion object { + private val _currentFactory = ThreadLocal() + + /** + * A default factory for serialization/deserialization, taking into account the [currentFactory] if set. + */ + val defaultFactory: SerializationFactory get() = currentFactory ?: SerializationDefaults.SERIALIZATION_FACTORY + + /** + * If there is a need to nest serialization/deserialization with a modified context during serialization or deserialization, + * this will return the current factory used to start serialization/deserialization. + */ + val currentFactory: SerializationFactory? get() = _currentFactory.get() + } } /** @@ -76,6 +130,12 @@ interface SerializationContext { */ fun withClassLoader(classLoader: ClassLoader): SerializationContext + /** + * Helper method to return a new context based on this context with the appropriate class loader constructed from the passed attachment identifiers. + * (Requires the attachment storage to have been enabled). + */ + fun withAttachmentsClassLoader(attachmentHashes: List): SerializationContext + /** * Helper method to return a new context based on this context with the given class specifically whitelisted. */ @@ -107,26 +167,26 @@ object SerializationDefaults { /** * Convenience extension method for deserializing a ByteSequence, utilising the defaults. */ -inline fun ByteSequence.deserialize(serializationFactory: SerializationFactory = SERIALIZATION_FACTORY, context: SerializationContext = P2P_CONTEXT): T { +inline fun ByteSequence.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T { return serializationFactory.deserialize(this, T::class.java, context) } /** * Convenience extension method for deserializing SerializedBytes with type matching, utilising the defaults. */ -inline fun SerializedBytes.deserialize(serializationFactory: SerializationFactory = SERIALIZATION_FACTORY, context: SerializationContext = P2P_CONTEXT): T { +inline fun SerializedBytes.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T { return serializationFactory.deserialize(this, T::class.java, context) } /** * Convenience extension method for deserializing a ByteArray, utilising the defaults. */ -inline fun ByteArray.deserialize(serializationFactory: SerializationFactory = SERIALIZATION_FACTORY, context: SerializationContext = P2P_CONTEXT): T = this.sequence().deserialize(serializationFactory, context) +inline fun ByteArray.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T = this.sequence().deserialize(serializationFactory, context) /** * Convenience extension method for serializing an object of type T, utilising the defaults. */ -fun T.serialize(serializationFactory: SerializationFactory = SERIALIZATION_FACTORY, context: SerializationContext = P2P_CONTEXT): SerializedBytes { +fun T.serialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): SerializedBytes { return serializationFactory.serialize(this, context) } @@ -142,4 +202,4 @@ class SerializedBytes(bytes: ByteArray) : OpaqueBytes(bytes) { interface ClassWhitelist { fun hasListed(type: Class<*>): Boolean -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomization.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomization.kt index e051f732c3..91c5dea265 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomization.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomization.kt @@ -1,6 +1,6 @@ package net.corda.core.serialization interface SerializationCustomization { - fun addToWhitelist(type: Class<*>) + fun addToWhitelist(vararg types: Class<*>) } diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt index 276415b041..da0d98dbe7 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt @@ -27,8 +27,8 @@ abstract class BaseTransaction : NamedByHash { } private fun checkNotarySetIfInputsPresent() { - if (notary == null) { - check(inputs.isEmpty()) { "The notary must be specified explicitly for any transaction that has inputs" } + if (inputs.isNotEmpty()) { + check(notary != null) { "The notary must be specified explicitly for any transaction that has inputs" } } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 59d3e8515d..5c4eb7fa2b 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -14,7 +14,7 @@ import java.util.function.Predicate * - Downloading and locally storing all the dependencies of the transaction. * - Resolving the input states and loading them into memory. * - Doing some basic key lookups on the [Command]s to see if any keys are from a recognised party, thus converting the - * [Command] objects into [AuthenticatedObject]. + * [Command] objects into [CommandWithParties]. * - Deserialising the output states. * * All the above refer to inputs using a (txhash, output index) pair. @@ -28,7 +28,7 @@ data class LedgerTransaction( override val inputs: List>, override val outputs: List>, /** Arbitrary data passed to the program of each input state. */ - val commands: List>, + val commands: List>, /** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */ val attachments: List, /** The hash of the original serialised WireTransaction. */ diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt index 38b418e877..b23800baec 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -4,7 +4,7 @@ import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SerializationDefaults.P2P_CONTEXT +import net.corda.core.serialization.SerializationFactory import net.corda.core.serialization.serialize import java.nio.ByteBuffer import java.util.function.Predicate @@ -22,12 +22,12 @@ fun serializedHash(x: T, privacySalt: PrivacySalt?, index: Int): Secur fun serializedHash(x: T, nonce: SecureHash): SecureHash { return if (x !is PrivacySalt) // PrivacySalt is not required to have an accompanied nonce. - (x.serialize(context = P2P_CONTEXT.withoutReferences()).bytes + nonce.bytes).sha256() + (x.serialize(context = SerializationFactory.defaultFactory.defaultContext.withoutReferences()).bytes + nonce.bytes).sha256() else serializedHash(x) } -fun serializedHash(x: T): SecureHash = x.serialize(context = P2P_CONTEXT.withoutReferences()).bytes.sha256() +fun serializedHash(x: T): SecureHash = x.serialize(context = SerializationFactory.defaultFactory.defaultContext.withoutReferences()).bytes.sha256() /** The nonce is computed as Hash(privacySalt || index). */ fun computeNonce(privacySalt: PrivacySalt, index: Int) = (privacySalt.bytes + ByteBuffer.allocate(4).putInt(index).array()).sha256() @@ -139,13 +139,13 @@ class FilteredLeaves( /** * Class representing merkleized filtered transaction. - * @param rootHash Merkle tree root hash. + * @param id Merkle tree root hash. * @param filteredLeaves Leaves included in a filtered transaction. * @param partialMerkleTree Merkle branch needed to verify filteredLeaves. */ @CordaSerializable class FilteredTransaction private constructor( - val rootHash: SecureHash, + val id: SecureHash, val filteredLeaves: FilteredLeaves, val partialMerkleTree: PartialMerkleTree ) { @@ -159,21 +159,84 @@ class FilteredTransaction private constructor( fun buildMerkleTransaction(wtx: WireTransaction, filtering: Predicate ): FilteredTransaction { - val filteredLeaves = wtx.filterWithFun(filtering) + val filteredLeaves = filterWithFun(wtx, filtering) val merkleTree = wtx.merkleTree val pmt = PartialMerkleTree.build(merkleTree, filteredLeaves.availableComponentHashes) return FilteredTransaction(merkleTree.hash, filteredLeaves, pmt) } + + /** + * Construction of partial transaction from WireTransaction based on filtering. + * Note that list of nonces to be sent is updated on the fly, based on the index of the filtered tx component. + * @param filtering filtering over the whole WireTransaction + * @returns FilteredLeaves used in PartialMerkleTree calculation and verification. + */ + private fun filterWithFun(wtx: WireTransaction, filtering: Predicate): FilteredLeaves { + val nonces: MutableList = mutableListOf() + val offsets = indexOffsets(wtx) + fun notNullFalseAndNoncesUpdate(elem: Any?, index: Int): Any? { + return if (elem == null || !filtering.test(elem)) { + null + } else { + nonces.add(computeNonce(wtx.privacySalt, index)) + elem + } + } + + fun filterAndNoncesUpdate(t: T, index: Int): Boolean { + return if (filtering.test(t)) { + nonces.add(computeNonce(wtx.privacySalt, index)) + true + } else { + false + } + } + + // TODO: We should have a warning (require) if all leaves (excluding salt) are visible after filtering. + // Consider the above after refactoring FilteredTransaction to implement TraversableTransaction, + // so that a WireTransaction can be used when required to send a full tx (e.g. RatesFixFlow in Oracles). + return FilteredLeaves( + wtx.inputs.filterIndexed { index, it -> filterAndNoncesUpdate(it, index) }, + wtx.attachments.filterIndexed { index, it -> filterAndNoncesUpdate(it, index + offsets[0]) }, + wtx.outputs.filterIndexed { index, it -> filterAndNoncesUpdate(it, index + offsets[1]) }, + wtx.commands.filterIndexed { index, it -> filterAndNoncesUpdate(it, index + offsets[2]) }, + notNullFalseAndNoncesUpdate(wtx.notary, offsets[3]) as Party?, + notNullFalseAndNoncesUpdate(wtx.timeWindow, offsets[4]) as TimeWindow?, + nonces + ) + } + + // We use index offsets, to get the actual leaf-index per transaction component required for nonce computation. + private fun indexOffsets(wtx: WireTransaction): List { + // There is no need to add an index offset for inputs, because they are the first components in the + // transaction format and it is always zero. Thus, offsets[0] corresponds to attachments, + // offsets[1] to outputs, offsets[2] to commands and so on. + val offsets = mutableListOf(wtx.inputs.size, wtx.inputs.size + wtx.attachments.size) + offsets.add(offsets.last() + wtx.outputs.size) + offsets.add(offsets.last() + wtx.commands.size) + if (wtx.notary != null) { + offsets.add(offsets.last() + 1) + } else { + offsets.add(offsets.last()) + } + if (wtx.timeWindow != null) { + offsets.add(offsets.last() + 1) + } else { + offsets.add(offsets.last()) + } + // No need to add offset for privacySalt as it doesn't require a nonce. + return offsets + } } /** - * Runs verification of Partial Merkle Branch against [rootHash]. + * Runs verification of partial Merkle branch against [id]. */ @Throws(MerkleTreeException::class) fun verify(): Boolean { val hashes: List = filteredLeaves.availableComponentHashes if (hashes.isEmpty()) throw MerkleTreeException("Transaction without included leaves.") - return partialMerkleTree.verify(rootHash, hashes) + return partialMerkleTree.verify(id, hashes) } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index 705e5553ca..0148421383 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -3,7 +3,7 @@ package net.corda.core.transactions import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature -import net.corda.core.crypto.toBase58String +import net.corda.core.utilities.toBase58String import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import java.security.PublicKey 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 c2afc3f2ec..7da67105f1 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -14,6 +14,7 @@ import java.security.KeyPair import java.security.PublicKey import java.security.SignatureException import java.util.* +import java.util.function.Predicate /** * SignedTransaction wraps a serialized WireTransaction. It contains one or more signatures, each one for @@ -29,6 +30,7 @@ import java.util.* * sign. */ // DOCSTART 1 +@CordaSerializable data class SignedTransaction(val txBits: SerializedBytes, override val sigs: List ) : TransactionWithSignatures { @@ -50,12 +52,18 @@ data class SignedTransaction(val txBits: SerializedBytes, /** The id of the contained [WireTransaction]. */ override val id: SecureHash get() = transaction.id - /** Returns the contained [WireTransaction], or throws if this is a notary change transaction */ + /** Returns the contained [WireTransaction], or throws if this is a notary change transaction. */ val tx: WireTransaction get() = transaction as WireTransaction - /** Returns the contained [NotaryChangeWireTransaction], or throws if this is a normal transaction */ + /** Returns the contained [NotaryChangeWireTransaction], or throws if this is a normal transaction. */ val notaryChangeTx: NotaryChangeWireTransaction get() = transaction as NotaryChangeWireTransaction + /** + * Helper function to directly build a [FilteredTransaction] using provided filtering functions, + * without first accessing the [WireTransaction] [tx]. + */ + fun buildFilteredTransaction(filtering: Predicate) = tx.buildFilteredTransaction(filtering) + /** Helper to access the inputs of the contained transaction */ val inputs: List get() = transaction.inputs /** Helper to access the notary of the contained transaction */ 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 c3492655f1..44a3797f75 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -33,8 +33,9 @@ data class WireTransaction( ) : CoreTransaction(), TraversableTransaction { init { checkBaseInvariants() + check(inputs.isNotEmpty() || outputs.isNotEmpty()) { "A transaction must contain at least one input or output state" } + check(commands.isNotEmpty()) { "A transaction must contain at least one command" } if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" } - check(availableComponents.isNotEmpty()) { "A WireTransaction cannot be empty" } } /** The transaction id is represented by the root hash of Merkle tree over the transaction components. */ @@ -83,7 +84,7 @@ data class WireTransaction( // Look up public keys to authenticated identities. This is just a stub placeholder and will all change in future. val authenticatedArgs = commands.map { val parties = it.signers.mapNotNull { pk -> resolveIdentity(pk) } - AuthenticatedObject(it.signers, parties, it.value) + CommandWithParties(it.signers, parties, it.value) } // Open attachments specified in this transaction. If we haven't downloaded them, we fail. val attachments = attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) } @@ -105,69 +106,6 @@ data class WireTransaction( */ val merkleTree: MerkleTree by lazy { MerkleTree.getMerkleTree(availableComponentHashes) } - /** - * Construction of partial transaction from WireTransaction based on filtering. - * Note that list of nonces to be sent is updated on the fly, based on the index of the filtered tx component. - * @param filtering filtering over the whole WireTransaction - * @returns FilteredLeaves used in PartialMerkleTree calculation and verification. - */ - fun filterWithFun(filtering: Predicate): FilteredLeaves { - val nonces: MutableList = mutableListOf() - val offsets = indexOffsets() - fun notNullFalseAndNoncesUpdate(elem: Any?, index: Int): Any? { - return if (elem == null || !filtering.test(elem)) { - null - } else { - nonces.add(computeNonce(privacySalt, index)) - elem - } - } - - fun filterAndNoncesUpdate(t: T, index: Int): Boolean { - return if (filtering.test(t)) { - nonces.add(computeNonce(privacySalt, index)) - true - } else { - false - } - } - - // TODO: We should have a warning (require) if all leaves (excluding salt) are visible after filtering. - // Consider the above after refactoring FilteredTransaction to implement TraversableTransaction, - // so that a WireTransaction can be used when required to send a full tx (e.g. RatesFixFlow in Oracles). - return FilteredLeaves( - inputs.filterIndexed { index, it -> filterAndNoncesUpdate(it, index) }, - attachments.filterIndexed { index, it -> filterAndNoncesUpdate(it, index + offsets[0]) }, - outputs.filterIndexed { index, it -> filterAndNoncesUpdate(it, index + offsets[1]) }, - commands.filterIndexed { index, it -> filterAndNoncesUpdate(it, index + offsets[2]) }, - notNullFalseAndNoncesUpdate(notary, offsets[3]) as Party?, - notNullFalseAndNoncesUpdate(timeWindow, offsets[4]) as TimeWindow?, - nonces - ) - } - - // We use index offsets, to get the actual leaf-index per transaction component required for nonce computation. - private fun indexOffsets(): List { - // There is no need to add an index offset for inputs, because they are the first components in the - // transaction format and it is always zero. Thus, offsets[0] corresponds to attachments, - // offsets[1] to outputs, offsets[2] to commands and so on. - val offsets = mutableListOf(inputs.size, inputs.size + attachments.size) - offsets.add(offsets.last() + outputs.size) - offsets.add(offsets.last() + commands.size) - if (notary != null) { - offsets.add(offsets.last() + 1) - } else { - offsets.add(offsets.last()) - } - if (timeWindow != null) { - offsets.add(offsets.last() + 1) - } else { - offsets.add(offsets.last()) - } - // No need to add offset for privacySalt as it doesn't require a nonce. - return offsets - } - /** * Checks that the given signature matches one of the commands and that it is a correct signature over the tx. * diff --git a/core/src/main/kotlin/net/corda/core/utilities/CountryCode.kt b/core/src/main/kotlin/net/corda/core/utilities/CountryCode.kt new file mode 100644 index 0000000000..94f0770279 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/utilities/CountryCode.kt @@ -0,0 +1,253 @@ +package net.corda.core.utilities + +val countryCodes = hashSetOf( + "AF", + "AX", + "AL", + "DZ", + "AS", + "AD", + "AO", + "AI", + "AQ", + "AG", + "AR", + "AM", + "AW", + "AU", + "AT", + "AZ", + "BS", + "BH", + "BD", + "BB", + "BY", + "BE", + "BZ", + "BJ", + "BM", + "BT", + "BO", + "BQ", + "BA", + "BW", + "BV", + "BR", + "IO", + "BN", + "BG", + "BF", + "BI", + "KH", + "CM", + "CA", + "CV", + "KY", + "CF", + "TD", + "CL", + "CN", + "CX", + "CC", + "CO", + "KM", + "CG", + "CD", + "CK", + "CR", + "CI", + "HR", + "CU", + "CW", + "CY", + "CZ", + "DK", + "DJ", + "DM", + "DO", + "EC", + "EG", + "SV", + "GQ", + "ER", + "EE", + "ET", + "FK", + "FO", + "FJ", + "FI", + "FR", + "GF", + "PF", + "TF", + "GA", + "GM", + "GE", + "DE", + "GH", + "GI", + "GR", + "GL", + "GD", + "GP", + "GU", + "GT", + "GG", + "GN", + "GW", + "GY", + "HT", + "HM", + "VA", + "HN", + "HK", + "HU", + "IS", + "IN", + "ID", + "IR", + "IQ", + "IE", + "IM", + "IL", + "IT", + "JM", + "JP", + "JE", + "JO", + "KZ", + "KE", + "KI", + "KP", + "KR", + "KW", + "KG", + "LA", + "LV", + "LB", + "LS", + "LR", + "LY", + "LI", + "LT", + "LU", + "MO", + "MK", + "MG", + "MW", + "MY", + "MV", + "ML", + "MT", + "MH", + "MQ", + "MR", + "MU", + "YT", + "MX", + "FM", + "MD", + "MC", + "MN", + "ME", + "MS", + "MA", + "MZ", + "MM", + "NA", + "NR", + "NP", + "NL", + "NC", + "NZ", + "NI", + "NE", + "NG", + "NU", + "NF", + "MP", + "NO", + "OM", + "PK", + "PW", + "PS", + "PA", + "PG", + "PY", + "PE", + "PH", + "PN", + "PL", + "PT", + "PR", + "QA", + "RE", + "RO", + "RU", + "RW", + "BL", + "SH", + "KN", + "LC", + "MF", + "PM", + "VC", + "WS", + "SM", + "ST", + "SA", + "SN", + "RS", + "SC", + "SL", + "SG", + "SX", + "SK", + "SI", + "SB", + "SO", + "ZA", + "GS", + "SS", + "ES", + "LK", + "SD", + "SR", + "SJ", + "SZ", + "SE", + "CH", + "SY", + "TW", + "TJ", + "TZ", + "TH", + "TL", + "TG", + "TK", + "TO", + "TT", + "TN", + "TR", + "TM", + "TC", + "TV", + "UG", + "UA", + "AE", + "GB", + "US", + "UM", + "UY", + "UZ", + "VU", + "VE", + "VN", + "VG", + "VI", + "WF", + "EH", + "YE", + "ZM", + "ZW" +) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/crypto/EncodingUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt similarity index 96% rename from core/src/main/kotlin/net/corda/core/crypto/EncodingUtils.kt rename to core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt index 0219142656..1cc4e53807 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/EncodingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/EncodingUtils.kt @@ -1,7 +1,9 @@ @file:JvmName("EncodingUtils") -package net.corda.core.crypto +package net.corda.core.utilities +import net.corda.core.crypto.Base58 +import net.corda.core.crypto.sha256 import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import java.nio.charset.Charset 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 bea9a6241d..8081a16e8e 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt @@ -2,8 +2,8 @@ package net.corda.core.utilities -import net.corda.core.crypto.commonName import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x500.style.BCStyle import java.lang.Character.UnicodeScript.* import java.text.Normalizer import java.util.regex.Pattern @@ -22,16 +22,9 @@ import javax.security.auth.x500.X500Principal * @throws IllegalArgumentException if the name does not meet the required rules. The message indicates why not. */ fun validateLegalName(normalizedLegalName: String) { - legalNameRules.forEach { it.validate(normalizedLegalName) } + Rule.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() - /** * The normalize function will trim the input string, replace any multiple spaces with a single space, * and normalize the string according to NFKC normalization form. @@ -41,82 +34,137 @@ fun normaliseLegalName(legalName: String): String { return Normalizer.normalize(trimmedLegalName, Normalizer.Form.NFKC) } -private val legalNameRules: List> = listOf( - UnicodeNormalizationRule(), - CharacterRule(',', '=', '$', '"', '\'', '\\'), - WordRule("node", "server"), - LengthRule(maxLength = 255), - // TODO: Implement confusable character detection if we add more scripts. - UnicodeRangeRule(LATIN, COMMON, INHERITED), - CapitalLetterRule(), - X500NameRule(), - MustHaveAtLeastTwoLettersRule() -) +val WHITESPACE = "\\s++".toRegex() -private class UnicodeNormalizationRule : Rule { - override fun validate(legalName: String) { - require(legalName == normaliseLegalName(legalName)) { "Legal name must be normalized. Please use 'normaliseLegalName' to normalize the legal name before validation." } +private val mandatoryAttributes = setOf(BCStyle.O, BCStyle.C, BCStyle.L) +private val supportedAttributes = mandatoryAttributes + setOf(BCStyle.CN, BCStyle.ST, BCStyle.OU) + +/** + * Validate X500Name according to Corda X500Name specification + * + * Supported attributes: + * - organisation (O) – VARCHAR(127) + * - state (ST) – VARCHAR(64) nullable + * - locality (L) – VARCHAR(64) + * - country (C) – VARCHAR(2) - ISO code of the country in which it is registered + * - organizational-unit (OU) – VARCHAR(64) nullable + * - common name (CN) – VARCHAR(64) + * + * @throws IllegalArgumentException if the name does not meet the required rules. The message indicates why not. + * @see Design Doc. + */ +fun validateX500Name(x500Name: X500Name) { + val rDNs = x500Name.rdNs.flatMap { it.typesAndValues.toList() } + val attributes = rDNs.map { it.type } + + // Duplicate attribute value checks. + require(attributes.size == attributes.toSet().size) { "X500Name contain duplicate attribute." } + + // Mandatory attribute checks. + require(attributes.containsAll(mandatoryAttributes)) { + val missingAttributes = mandatoryAttributes.subtract(attributes).map { BCStyle.INSTANCE.oidToDisplayName(it) } + "The following attribute${if (missingAttributes.size > 1) "s are" else " is"} missing from the legal name : $missingAttributes" } + + // Supported attribute checks. + require(attributes.subtract(supportedAttributes).isEmpty()) { + val unsupportedAttributes = attributes.subtract(supportedAttributes).map { BCStyle.INSTANCE.oidToDisplayName(it) } + "The following attribute${if (unsupportedAttributes.size > 1) "s are" else " is"} not supported in Corda :$unsupportedAttributes" + } + // Legal name checks. + validateLegalName(x500Name.organisation) + + // Attribute data width checks. + require(x500Name.country.length == 2) { "Invalid country '${x500Name.country}' Country code must be 2 letters ISO code " } + require(x500Name.country.toUpperCase() == x500Name.country) { "Country code should be in upper case." } + require(countryCodes.contains(x500Name.country)) { "Invalid country code '${x500Name.country}'" } + + require(x500Name.organisation.length < 127) { "Organisation attribute (O) must contain less then 127 characters." } + require(x500Name.locality.length < 64) { "Locality attribute (L) must contain less then 64 characters." } + + x500Name.state?.let { require(it.length < 64) { "State attribute (ST) must contain less then 64 characters." } } + x500Name.organisationUnit?.let { require(x500Name.organisationUnit!!.length < 64) { "Organisation Unit attribute (OU) must contain less then 64 characters." } } + x500Name.commonName?.let { require(x500Name.commonName!!.length < 64) { "Common Name attribute (CN) must contain less then 64 characters." } } } -private class UnicodeRangeRule(vararg supportScripts: Character.UnicodeScript) : Rule { - private val pattern = supportScripts.map { "\\p{Is$it}" }.joinToString(separator = "", prefix = "[", postfix = "]*").let { Pattern.compile(it) } +private sealed class Rule { + companion object { + val legalNameRules: List> = listOf( + UnicodeNormalizationRule(), + CharacterRule(',', '=', '$', '"', '\'', '\\'), + WordRule("node", "server"), + LengthRule(maxLength = 255), + // TODO: Implement confusable character detection if we add more scripts. + UnicodeRangeRule(LATIN, COMMON, INHERITED), + CapitalLetterRule(), + X500NameRule(), + MustHaveAtLeastTwoLettersRule() + ) + } - override fun validate(legalName: String) { - require(pattern.matcher(legalName).matches()) { - val illegalChars = legalName.replace(pattern.toRegex(), "").toSet() - if (illegalChars.size > 1) { - "Forbidden characters $illegalChars in \"$legalName\"." - } else { - "Forbidden character $illegalChars in \"$legalName\"." + abstract fun validate(legalName: T) + + private class UnicodeNormalizationRule : Rule() { + override fun validate(legalName: String) { + require(legalName == normaliseLegalName(legalName)) { "Legal name must be normalized. Please use 'normaliseLegalName' to normalize the legal name before validation." } + } + } + + private class UnicodeRangeRule(vararg supportScripts: Character.UnicodeScript) : Rule() { + private val pattern = supportScripts.map { "\\p{Is$it}" }.joinToString(separator = "", prefix = "[", postfix = "]*").let { Pattern.compile(it) } + + override fun validate(legalName: String) { + require(pattern.matcher(legalName).matches()) { + val illegalChars = legalName.replace(pattern.toRegex(), "").toSet() + if (illegalChars.size > 1) { + "Forbidden characters $illegalChars in \"$legalName\"." + } else { + "Forbidden character $illegalChars in \"$legalName\"." + } } } } -} -private class CharacterRule(vararg val bannedChars: Char) : Rule { - override fun validate(legalName: String) { - bannedChars.forEach { - require(!legalName.contains(it, true)) { "Character not allowed in legal names: $it" } + private class CharacterRule(vararg val bannedChars: Char) : Rule() { + override fun validate(legalName: String) { + bannedChars.forEach { + require(!legalName.contains(it, true)) { "Character not allowed in legal names: $it" } + } + } + } + + private class WordRule(vararg val bannedWords: String) : Rule() { + override fun validate(legalName: String) { + bannedWords.forEach { + require(!legalName.contains(it, ignoreCase = true)) { "Word not allowed in legal names: $it" } + } + } + } + + private class LengthRule(val maxLength: Int) : Rule() { + override fun validate(legalName: String) { + require(legalName.length <= maxLength) { "Legal name longer then $maxLength characters." } + } + } + + private class CapitalLetterRule : Rule() { + override fun validate(legalName: String) { + val capitalizedLegalName = legalName.capitalize() + require(legalName == capitalizedLegalName) { "Legal name should be capitalized. i.e. '$capitalizedLegalName'" } + } + } + + private class X500NameRule : Rule() { + override fun validate(legalName: String) { + // This will throw IllegalArgumentException if the name does not comply with X500 name format. + X500Principal("CN=$legalName") + } + } + + private class MustHaveAtLeastTwoLettersRule : Rule() { + override fun validate(legalName: String) { + // Try to exclude names like "/", "£", "X" etc. + require(legalName.count { it.isLetter() } >= 2) { "Illegal input legal name '$legalName'. Legal name must have at least two letters" } } } } - -private class WordRule(vararg val bannedWords: String) : Rule { - override fun validate(legalName: String) { - bannedWords.forEach { - require(!legalName.contains(it, ignoreCase = true)) { "Word not allowed in legal names: $it" } - } - } -} - -private class LengthRule(val maxLength: Int) : Rule { - override fun validate(legalName: String) { - require(legalName.length <= maxLength) { "Legal name longer then $maxLength characters." } - } -} - -private class CapitalLetterRule : Rule { - override fun validate(legalName: String) { - val capitalizedLegalName = legalName.capitalize() - require(legalName == capitalizedLegalName) { "Legal name should be capitalized. i.e. '$capitalizedLegalName'" } - } -} - -private class X500NameRule : Rule { - override fun validate(legalName: String) { - // This will throw IllegalArgumentException if the name does not comply with X500 name format. - X500Principal("CN=$legalName") - } -} - -private class MustHaveAtLeastTwoLettersRule : Rule { - override fun validate(legalName: String) { - // Try to exclude names like "/", "£", "X" etc. - require(legalName.count { it.isLetter() } >= 2) { "Illegal input legal name '$legalName'. Legal name must have at least two letters" } - } -} - -private interface Rule { - fun validate(legalName: T) -} diff --git a/core/src/main/kotlin/net/corda/core/utilities/X500NameUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/X500NameUtils.kt new file mode 100644 index 0000000000..a1cf8f8933 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/utilities/X500NameUtils.kt @@ -0,0 +1,58 @@ +@file:JvmName("X500NameUtils") + +package net.corda.core.utilities + +import net.corda.core.internal.toX509CertHolder +import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x500.X500NameBuilder +import org.bouncycastle.asn1.x500.style.BCStyle +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import java.security.KeyPair +import java.security.cert.X509Certificate + +val X500Name.commonName: String? get() = getRDNValueString(BCStyle.CN) +val X500Name.organisationUnit: String? get() = getRDNValueString(BCStyle.OU) +val X500Name.state: String? get() = getRDNValueString(BCStyle.ST) +val X500Name.organisation: String get() = getRDNValueString(BCStyle.O) ?: throw IllegalArgumentException("Malformed X500 name, organisation attribute (O) cannot be empty.") +val X500Name.locality: String get() = getRDNValueString(BCStyle.L) ?: throw IllegalArgumentException("Malformed X500 name, locality attribute (L) cannot be empty.") +val X500Name.country: String get() = getRDNValueString(BCStyle.C) ?: throw IllegalArgumentException("Malformed X500 name, country attribute (C) cannot be empty.") + +private fun X500Name.getRDNValueString(identifier: ASN1ObjectIdentifier): String? = getRDNs(identifier).firstOrNull()?.first?.value?.toString() + +val X509Certificate.subject: X500Name get() = toX509CertHolder().subject +val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this) + +/** + * Generate a distinguished name from the provided X500 . + * + * @param O organisation name. + * @param L locality. + * @param C county. + * @param CN common name. + * @param OU organisation unit. + * @param ST state. + */ +@JvmOverloads +fun getX500Name(O: String, L: String, C: String, CN: String? = null, OU: String? = null, ST: String? = null): X500Name { + return X500NameBuilder(BCStyle.INSTANCE).apply { + addRDN(BCStyle.C, C) + ST?.let { addRDN(BCStyle.ST, it) } + addRDN(BCStyle.L, L) + addRDN(BCStyle.O, O) + OU?.let { addRDN(BCStyle.OU, it) } + CN?.let { addRDN(BCStyle.CN, it) } + }.build() +} + +fun X500Name.withCommonName(commonName: String?): X500Name { + return getX500Name(organisation, locality, country, commonName, organisationUnit, state) +} + +fun X500Name.toWellFormattedName(): X500Name { + validateX500Name(this) + return getX500Name(organisation, locality, country, commonName, organisationUnit, state) +} + +data class CertificateAndKeyPair(val certificate: X509CertificateHolder, val keyPair: KeyPair) diff --git a/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java b/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java index 831d27549e..af255b2938 100644 --- a/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java +++ b/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java @@ -1,15 +1,18 @@ package net.corda.core.flows; import co.paralleluniverse.fibers.Suspendable; +import com.google.common.primitives.Primitives; import net.corda.core.identity.Party; import net.corda.testing.node.MockNetwork; import org.junit.After; import org.junit.Before; import org.junit.Test; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.Assert.fail; public class FlowsInJavaTest { @@ -18,11 +21,13 @@ public class FlowsInJavaTest { private MockNetwork.MockNode node2; @Before - public void setUp() { + public void setUp() throws Exception { MockNetwork.BasketOfNodes someNodes = mockNet.createSomeNodes(2); node1 = someNodes.getPartyNodes().get(0); node2 = someNodes.getPartyNodes().get(1); mockNet.runNetwork(); + // Ensure registration was successful + node1.getNodeReadyFuture().get(); } @After @@ -38,6 +43,30 @@ public class FlowsInJavaTest { assertThat(result.get()).isEqualTo("Hello"); } + @Test + public void primitiveClassForReceiveType() throws InterruptedException { + // Using the primitive classes causes problems with the checkpointing so we use the wrapper classes and convert + // to the primitive class at callsite. + for (Class receiveType : Primitives.allWrapperTypes()) { + primitiveReceiveTypeTest(receiveType); + } + } + + private void primitiveReceiveTypeTest(Class receiveType) throws InterruptedException { + PrimitiveReceiveFlow flow = new PrimitiveReceiveFlow(node2.getInfo().getLegalIdentity(), receiveType); + Future result = node1.getServices().startFlow(flow).getResultFuture(); + mockNet.runNetwork(); + try { + result.get(); + fail("ExecutionException should have been thrown"); + } catch (ExecutionException e) { + assertThat(e.getCause()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("primitive") + .hasMessageContaining(receiveType.getName()); + } + } + @InitiatingFlow private static class SendInUnwrapFlow extends FlowLogic { private final Party otherParty; @@ -71,4 +100,21 @@ public class FlowsInJavaTest { } } + @InitiatingFlow + private static class PrimitiveReceiveFlow extends FlowLogic { + private final Party otherParty; + private final Class receiveType; + + private PrimitiveReceiveFlow(Party otherParty, Class receiveType) { + this.otherParty = otherParty; + this.receiveType = receiveType; + } + + @Suspendable + @Override + public Void call() throws FlowException { + receive(Primitives.unwrap(receiveType), otherParty); + return null; + } + } } diff --git a/core/src/test/kotlin/net/corda/core/contracts/StructuresTests.kt b/core/src/test/kotlin/net/corda/core/contracts/StructuresTests.kt index b1d915a745..47f69945dd 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/StructuresTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/StructuresTests.kt @@ -30,6 +30,7 @@ class AttachmentTest { val attachment = object : Attachment { override val id get() = throw UnsupportedOperationException() override fun open() = inputStream + override val signers get() = throw UnsupportedOperationException() } try { attachment.openAsJAR() 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 c6c508b988..169677b3d6 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,7 @@ package net.corda.core.contracts import net.corda.core.crypto.* -import net.corda.core.crypto.composite.CompositeKey +import net.corda.core.crypto.CompositeKey import net.corda.core.identity.Party import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction @@ -96,7 +96,7 @@ class TransactionTests : TestDependencyInjectionBase() { val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DUMMY_NOTARY) val inputs = emptyList>() val outputs = listOf(baseOutState, baseOutState.copy(notary = ALICE), baseOutState.copy(notary = BOB)) - val commands = emptyList>() + val commands = emptyList>() val attachments = emptyList() val id = SecureHash.randomSHA256() val timeWindow: TimeWindow? = null @@ -137,7 +137,7 @@ class TransactionTests : TestDependencyInjectionBase() { val outState = inState.copy(notary = ALICE) val inputs = listOf(StateAndRef(inState, StateRef(SecureHash.randomSHA256(), 0))) val outputs = listOf(outState) - val commands = emptyList>() + val commands = emptyList>() val attachments = emptyList() val id = SecureHash.randomSHA256() val timeWindow: TimeWindow? = null 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 62169f8c21..873c3e5a97 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -1,16 +1,17 @@ package net.corda.core.crypto -import net.corda.core.crypto.composite.CompositeKey -import net.corda.core.crypto.composite.CompositeKey.NodeAndWeight -import net.corda.core.crypto.composite.CompositeSignature +import net.corda.core.crypto.CompositeKey.NodeAndWeight import net.corda.core.crypto.composite.CompositeSignaturesWithKeys import net.corda.core.internal.declaredField import net.corda.core.internal.div import net.corda.core.serialization.serialize import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.cert +import net.corda.core.utilities.getX500Name +import net.corda.core.utilities.toBase58String import net.corda.node.utilities.* import net.corda.testing.TestDependencyInjectionBase -import org.bouncycastle.asn1.x500.X500Name +import net.corda.testing.kryoSpecific import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -88,7 +89,7 @@ class CompositeKeyTests : TestDependencyInjectionBase() { val aliceAndBobOrCharlie = CompositeKey.Builder().addKeys(aliceAndBob, charliePublicKey).build(threshold = 1) val encoded = aliceAndBobOrCharlie.toBase58String() - val decoded = parsePublicKeyBase58(encoded) + val decoded = net.corda.core.utilities.parsePublicKeyBase58(encoded) assertEquals(decoded, aliceAndBobOrCharlie) } @@ -216,7 +217,7 @@ class CompositeKeyTests : TestDependencyInjectionBase() { } @Test() - fun `composite key validation with graph cycle detection`() { + fun `composite key validation with graph cycle detection`() = kryoSpecific("Cycle exists in the object graph which is not currently supported in AMQP mode") { 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 @@ -330,10 +331,10 @@ class CompositeKeyTests : TestDependencyInjectionBase() { // Create self sign CA. val caKeyPair = Crypto.generateKeyPair() - val ca = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Test CA"), caKeyPair) + val ca = X509Utilities.createSelfSignedCACertificate(getX500Name(CN = "Test CA", O = "R3", L = "London", C = "GB"), caKeyPair) // Sign the composite key with the self sign CA. - val compositeKeyCert = X509Utilities.createCertificate(CertificateType.IDENTITY, ca, caKeyPair, X500Name("CN=CompositeKey"), compositeKey) + val compositeKeyCert = X509Utilities.createCertificate(CertificateType.IDENTITY, ca, caKeyPair, getX500Name(CN = "CompositeKey", O = "R3", L = "London", C = "GB"), compositeKey) // Store certificate to keystore. val keystorePath = tempFolder.root.toPath() / "keystore.jks" diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt index 8d2589119c..09b31737d1 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt @@ -1,6 +1,8 @@ package net.corda.core.crypto import net.corda.core.internal.toTypedArray +import net.corda.core.utilities.cert +import net.corda.core.utilities.getX500Name import net.corda.node.utilities.* import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.GeneralName @@ -18,13 +20,13 @@ class X509NameConstraintsTest { private fun makeKeyStores(subjectName: X500Name, nameConstraints: NameConstraints): Pair { val rootKeys = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = X509Utilities.createSelfSignedCACertificate(getX509Name("Corda Root CA", "London", "demo@r3.com", null), rootKeys) + val rootCACert = X509Utilities.createSelfSignedCACertificate(getX500Name(CN = "Corda Root CA", O = "R3CEV", L = "London", C = "GB"), rootKeys) val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, getX509Name("Corda Intermediate CA", "London", "demo@r3.com", null), intermediateCAKeyPair.public) + val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, getX500Name(CN = "Corda Intermediate CA", O = "R3CEV", L = "London", C = "GB"), intermediateCAKeyPair.public) val clientCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, getX509Name("Corda Client CA", "London", "demo@r3.com", null), clientCAKeyPair.public, nameConstraints = nameConstraints) + val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, getX500Name(CN = "Corda Client CA", O = "R3CEV", L = "London", C = "GB"), clientCAKeyPair.public, nameConstraints = nameConstraints) val keyPass = "password" val trustStore = KeyStore.getInstance(KEYSTORE_TYPE) diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index b8b2900d98..378ed0b0f1 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -56,6 +56,11 @@ class AttachmentTests { val nodes = mockNet.createSomeNodes(2) val n0 = nodes.partyNodes[0] val n1 = nodes.partyNodes[1] + + // Ensure that registration was successful before progressing any further + mockNet.runNetwork() + n0.ensureRegistered() + n0.registerInitiatedFlow(FetchAttachmentsResponse::class.java) n1.registerInitiatedFlow(FetchAttachmentsResponse::class.java) @@ -89,6 +94,11 @@ class AttachmentTests { val nodes = mockNet.createSomeNodes(2) val n0 = nodes.partyNodes[0] val n1 = nodes.partyNodes[1] + + // Ensure that registration was successful before progressing any further + mockNet.runNetwork() + n0.ensureRegistered() + n0.registerInitiatedFlow(FetchAttachmentsResponse::class.java) n1.registerInitiatedFlow(FetchAttachmentsResponse::class.java) @@ -119,6 +129,10 @@ class AttachmentTests { }, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) val n1 = mockNet.createNode(n0.network.myAddress) + // Ensure that registration was successful before progressing any further + mockNet.runNetwork() + n0.ensureRegistered() + n0.registerInitiatedFlow(FetchAttachmentsResponse::class.java) n1.registerInitiatedFlow(FetchAttachmentsResponse::class.java) 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 09a9ae33a3..cf278dc307 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -36,6 +36,7 @@ class CollectSignaturesFlowTests { c = nodes.partyNodes[2] notary = nodes.notaryNode.info.notaryIdentity mockNet.runNetwork() + a.ensureRegistered() } @After @@ -140,9 +141,13 @@ class CollectSignaturesFlowTests { @Test fun `successfully collects two signatures`() { - val bConfidentialIdentity = b.services.keyManagementService.freshKeyAndCert(b.info.legalIdentityAndCert, false) - // Normally this is handled by TransactionKeyFlow, but here we have to manually let A know about the identity - a.services.identityService.verifyAndRegisterIdentity(bConfidentialIdentity) + val bConfidentialIdentity = b.database.transaction { + b.services.keyManagementService.freshKeyAndCert(b.info.legalIdentityAndCert, false) + } + a.database.transaction { + // Normally this is handled by TransactionKeyFlow, but here we have to manually let A know about the identity + a.services.identityService.verifyAndRegisterIdentity(bConfidentialIdentity) + } registerFlowOnAllNodes(TestFlowTwo.Responder::class) val magicNumber = 1337 val parties = listOf(a.info.legalIdentity, bConfidentialIdentity.party, c.info.legalIdentity) 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 951f28b9e3..ccf543c4e1 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -46,11 +46,21 @@ class ContractUpgradeFlowTest { val nodes = mockNet.createSomeNodes(notaryKeyPair = null) // prevent generation of notary override a = nodes.partyNodes[0] b = nodes.partyNodes[1] + + // Process registration + mockNet.runNetwork() + a.ensureRegistered() + notary = nodes.notaryNode.info.notaryIdentity val nodeIdentity = nodes.notaryNode.info.legalIdentitiesAndCerts.single { it.party == nodes.notaryNode.info.notaryIdentity } - a.services.identityService.verifyAndRegisterIdentity(nodeIdentity) - b.services.identityService.verifyAndRegisterIdentity(nodeIdentity) + a.database.transaction { + a.services.identityService.verifyAndRegisterIdentity(nodeIdentity) + } + b.database.transaction { + b.services.identityService.verifyAndRegisterIdentity(nodeIdentity) + } + } @After @@ -74,15 +84,24 @@ class ContractUpgradeFlowTest { requireNotNull(btx) // The request is expected to be rejected because party B hasn't authorised the upgrade yet. - val rejectedFuture = a.services.startFlow(ContractUpgradeFlow(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture + val rejectedFuture = a.services.startFlow(ContractUpgradeFlow.Initiator(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture mockNet.runNetwork() assertFailsWith(UnexpectedFlowEndException::class) { rejectedFuture.getOrThrow() } - // Party B authorise the contract state upgrade. - b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef(0), DummyContractV2::class.java) + // Party B authorise the contract state upgrade, and immediately deauthorise the same. + b.services.startFlow(ContractUpgradeFlow.Authorise(btx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture.getOrThrow() + b.services.startFlow(ContractUpgradeFlow.Deauthorise(btx.tx.outRef(0).ref)).resultFuture.getOrThrow() + + // The request is expected to be rejected because party B has subsequently deauthorised and a previously authorised upgrade. + val deauthorisedFuture = a.services.startFlow(ContractUpgradeFlow.Initiator(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture + mockNet.runNetwork() + assertFailsWith(UnexpectedFlowEndException::class) { deauthorisedFuture.getOrThrow() } + + // Party B authorise the contract state upgrade + b.services.startFlow(ContractUpgradeFlow.Authorise(btx.tx.outRef(0), DummyContractV2::class.java)).resultFuture.getOrThrow() // Party A initiates contract upgrade flow, expected to succeed this time. - val resultFuture = a.services.startFlow(ContractUpgradeFlow(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture + val resultFuture = a.services.startFlow(ContractUpgradeFlow.Initiator(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture mockNet.runNetwork() val result = resultFuture.getOrThrow() @@ -128,7 +147,10 @@ class ContractUpgradeFlowTest { val user = rpcTestUser.copy(permissions = setOf( startFlowPermission(), - startFlowPermission>() + startFlowPermission>(), + startFlowPermission(), + startFlowPermission(), + startFlowPermission() )) val rpcA = startProxy(a, user) val rpcB = startProxy(b, user) @@ -141,18 +163,35 @@ class ContractUpgradeFlowTest { requireNotNull(atx) requireNotNull(btx) - val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow(stateAndRef, upgrade) }, + val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiator(stateAndRef, upgrade) }, atx!!.tx.outRef(0), DummyContractV2::class.java).returnValue mockNet.runNetwork() assertFailsWith(UnexpectedFlowEndException::class) { rejectedFuture.getOrThrow() } + // Party B authorise the contract state upgrade, and immediately deauthorise the same. + rpcB.startFlow( { stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade ) }, + btx!!.tx.outRef(0), + DummyContractV2::class.java).returnValue + rpcB.startFlow( { stateRef -> ContractUpgradeFlow.Deauthorise(stateRef) }, + btx.tx.outRef(0).ref).returnValue + + // The request is expected to be rejected because party B has subsequently deauthorised and a previously authorised upgrade. + val deauthorisedFuture = rpcA.startFlow( {stateAndRef, upgrade -> ContractUpgradeFlow.Initiator(stateAndRef, upgrade) }, + atx.tx.outRef(0), + DummyContractV2::class.java).returnValue + + mockNet.runNetwork() + assertFailsWith(UnexpectedFlowEndException::class) { deauthorisedFuture.getOrThrow() } + // Party B authorise the contract state upgrade. - rpcB.authoriseContractUpgrade(btx!!.tx.outRef(0), DummyContractV2::class.java) + rpcB.startFlow( { stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade ) }, + btx.tx.outRef(0), + DummyContractV2::class.java).returnValue // Party A initiates contract upgrade flow, expected to succeed this time. - val resultFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow(stateAndRef, upgrade) }, + val resultFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiator(stateAndRef, upgrade) }, atx.tx.outRef(0), DummyContractV2::class.java).returnValue @@ -184,7 +223,7 @@ class ContractUpgradeFlowTest { val baseState = a.database.transaction { a.services.vaultQueryService.queryBy().states.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 + val upgradeResult = a.services.startFlow(ContractUpgradeFlow.Initiator(stateAndRef, CashV2::class.java)).resultFuture mockNet.runNetwork() upgradeResult.getOrThrow() // Get contract state from the vault. diff --git a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt index d4f96ab44a..add1d8e3ca 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -29,6 +29,7 @@ class FinalityFlowTests { nodeB = nodes.partyNodes[1] notary = nodes.notaryNode.info.notaryIdentity mockNet.runNetwork() + nodeA.ensureRegistered() } @After diff --git a/core/src/test/kotlin/net/corda/core/flows/IdentitySyncFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/IdentitySyncFlowTests.kt index 5bd4cdb349..048d1822b1 100644 --- a/core/src/test/kotlin/net/corda/core/flows/IdentitySyncFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/IdentitySyncFlowTests.kt @@ -41,10 +41,14 @@ class IdentitySyncFlowTests { 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.verifyAndRegisterIdentity(bobNode.info.legalIdentityAndCert) - aliceNode.services.identityService.verifyAndRegisterIdentity(notaryNode.info.legalIdentityAndCert) - bobNode.services.identityService.verifyAndRegisterIdentity(aliceNode.info.legalIdentityAndCert) - bobNode.services.identityService.verifyAndRegisterIdentity(notaryNode.info.legalIdentityAndCert) + aliceNode.database.transaction { + aliceNode.services.identityService.verifyAndRegisterIdentity(bobNode.info.legalIdentityAndCert) + aliceNode.services.identityService.verifyAndRegisterIdentity(notaryNode.info.legalIdentityAndCert) + } + bobNode.database.transaction { + bobNode.services.identityService.verifyAndRegisterIdentity(aliceNode.info.legalIdentityAndCert) + bobNode.services.identityService.verifyAndRegisterIdentity(notaryNode.info.legalIdentityAndCert) + } bobNode.registerInitiatedFlow(Receive::class.java) // Alice issues then pays some cash to a new confidential identity that Bob doesn't know about @@ -53,12 +57,16 @@ class IdentitySyncFlowTests { val issueFlow = aliceNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, alice, anonymous, notaryNode.services.myInfo.notaryIdentity)) val issueTx = issueFlow.resultFuture.getOrThrow().stx val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance().single().owner - assertNull(bobNode.services.identityService.partyFromAnonymous(confidentialIdentity)) + assertNull(bobNode.database.transaction { bobNode.services.identityService.partyFromAnonymous(confidentialIdentity) }) // Run the flow to sync up the identities aliceNode.services.startFlow(Initiator(bob, issueTx.tx)).resultFuture.getOrThrow() - val expected = aliceNode.services.identityService.partyFromAnonymous(confidentialIdentity) - val actual = bobNode.services.identityService.partyFromAnonymous(confidentialIdentity) + val expected = aliceNode.database.transaction { + aliceNode.services.identityService.partyFromAnonymous(confidentialIdentity) + } + val actual = bobNode.database.transaction { + bobNode.services.identityService.partyFromAnonymous(confidentialIdentity) + } assertEquals(expected, actual) } diff --git a/core/src/test/kotlin/net/corda/core/flows/ManualFinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/ManualFinalityFlowTests.kt index 2e4cc2429f..f872cecb8b 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ManualFinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ManualFinalityFlowTests.kt @@ -32,6 +32,7 @@ class ManualFinalityFlowTests { nodeC = nodes.partyNodes[2] notary = nodes.notaryNode.info.notaryIdentity mockNet.runNetwork() + nodeA.ensureRegistered() } @After diff --git a/core/src/test/kotlin/net/corda/core/flows/TransactionKeyFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/TransactionKeyFlowTests.kt index 83defd2fa2..c4042699fb 100644 --- a/core/src/test/kotlin/net/corda/core/flows/TransactionKeyFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/TransactionKeyFlowTests.kt @@ -26,10 +26,14 @@ class TransactionKeyFlowTests { 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.verifyAndRegisterIdentity(bobNode.info.legalIdentityAndCert) - aliceNode.services.identityService.verifyAndRegisterIdentity(notaryNode.info.legalIdentityAndCert) - bobNode.services.identityService.verifyAndRegisterIdentity(aliceNode.info.legalIdentityAndCert) - bobNode.services.identityService.verifyAndRegisterIdentity(notaryNode.info.legalIdentityAndCert) + aliceNode.database.transaction { + aliceNode.services.identityService.verifyAndRegisterIdentity(bobNode.info.legalIdentityAndCert) + aliceNode.services.identityService.verifyAndRegisterIdentity(notaryNode.info.legalIdentityAndCert) + } + bobNode.database.transaction { + bobNode.services.identityService.verifyAndRegisterIdentity(aliceNode.info.legalIdentityAndCert) + bobNode.services.identityService.verifyAndRegisterIdentity(notaryNode.info.legalIdentityAndCert) + } // Run the flows val requesterFlow = aliceNode.services.startFlow(TransactionKeyFlow(bob)) @@ -44,8 +48,8 @@ class TransactionKeyFlowTests { assertNotEquals(bob, bobAnonymousIdentity) // Verify that the anonymous identities look sane - assertEquals(alice.name, aliceNode.services.identityService.partyFromAnonymous(aliceAnonymousIdentity)!!.name) - assertEquals(bob.name, bobNode.services.identityService.partyFromAnonymous(bobAnonymousIdentity)!!.name) + assertEquals(alice.name, aliceNode.database.transaction { aliceNode.services.identityService.partyFromAnonymous(aliceAnonymousIdentity)!!.name }) + assertEquals(bob.name, bobNode.database.transaction { bobNode.services.identityService.partyFromAnonymous(bobAnonymousIdentity)!!.name }) // Verify that the nodes have the right anonymous identities assertTrue { aliceAnonymousIdentity.owningKey in aliceNode.services.keyManagementService.keys } diff --git a/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt b/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt index f6341d9253..17bfe21510 100644 --- a/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt +++ b/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt @@ -3,10 +3,10 @@ package net.corda.core.identity import net.corda.core.crypto.entropyToKeyPair import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.core.utilities.getX500Name import net.corda.testing.getTestPartyAndCertificate import net.corda.testing.withTestSerialization import org.assertj.core.api.Assertions.assertThat -import org.bouncycastle.asn1.x500.X500Name import org.junit.Test import java.math.BigInteger @@ -15,7 +15,7 @@ class PartyAndCertificateTest { fun `kryo serialisation`() { withTestSerialization { val original = getTestPartyAndCertificate(Party( - X500Name("CN=Test Corp,O=Test Corp,L=Madrid,C=ES"), + getX500Name(O = "Test Corp", L = "Madrid", C = "ES"), entropyToKeyPair(BigInteger.valueOf(83)).public)) val copy = original.serialize().deserialize() assertThat(copy).isEqualTo(original).isNotSameAs(original) diff --git a/core/src/test/kotlin/net/corda/core/internal/AbstractAttachmentTest.kt b/core/src/test/kotlin/net/corda/core/internal/AbstractAttachmentTest.kt new file mode 100644 index 0000000000..3c3f224091 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/internal/AbstractAttachmentTest.kt @@ -0,0 +1,118 @@ +package net.corda.core.internal + +import net.corda.testing.ALICE +import net.corda.testing.BOB +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.After +import org.junit.AfterClass +import org.junit.BeforeClass +import org.junit.Test +import java.nio.file.Files +import java.nio.file.Paths +import kotlin.test.assertEquals + +class AbstractAttachmentTest { + companion object { + private val dir = Files.createTempDirectory(AbstractAttachmentTest::class.simpleName) + private val bin = Paths.get(System.getProperty("java.home")).let { if (it.endsWith("jre")) it.parent else it } / "bin" + private val shredder = (dir / "_shredder").toFile() // No need to delete after each test. + fun execute(vararg command: String) { + assertEquals(0, ProcessBuilder() + .inheritIO() + .redirectOutput(shredder) + .directory(dir.toFile()) + .command((bin / command[0]).toString(), *command.sliceArray(1 until command.size)) + .start() + .waitFor()) + } + + @BeforeClass + @JvmStatic + fun beforeClass() { + execute("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", "RSA", "-alias", "alice", "-keypass", "alicepass", "-dname", ALICE.toString()) + execute("keytool", "-genkey", "-keystore", "_teststore", "-storepass", "storepass", "-keyalg", "RSA", "-alias", "bob", "-keypass", "bobpass", "-dname", BOB.toString()) + (dir / "_signable1").writeLines(listOf("signable1")) + (dir / "_signable2").writeLines(listOf("signable2")) + (dir / "_signable3").writeLines(listOf("signable3")) + } + + private fun load(name: String) = object : AbstractAttachment({ (dir / name).readAll() }) { + override val id get() = throw UnsupportedOperationException() + } + + @AfterClass + @JvmStatic + fun afterClass() { + dir.toFile().deleteRecursively() + } + } + + @After + fun tearDown() { + dir.toFile().listFiles().forEach { + if (!it.name.startsWith("_")) it.deleteRecursively() + } + assertEquals(5, dir.toFile().listFiles().size) + } + + @Test + fun `empty jar has no signers`() { + (dir / "META-INF").createDirectory() // At least one arg is required, and jar cvf conveniently ignores this. + execute("jar", "cvf", "attachment.jar", "META-INF") + assertEquals(emptyList(), load("attachment.jar").signers) + execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "alicepass", "attachment.jar", "alice") + assertEquals(emptyList(), load("attachment.jar").signers) // There needs to have been a file for ALICE to sign. + } + + @Test + fun `unsigned jar has no signers`() { + execute("jar", "cvf", "attachment.jar", "_signable1") + assertEquals(emptyList(), load("attachment.jar").signers) + execute("jar", "uvf", "attachment.jar", "_signable2") + assertEquals(emptyList(), load("attachment.jar").signers) + } + + @Test + fun `one signer`() { + execute("jar", "cvf", "attachment.jar", "_signable1", "_signable2") + execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "alicepass", "attachment.jar", "alice") + assertEquals(listOf(ALICE.name), load("attachment.jar").signers.map { it.name }) // We only reused ALICE's distinguished name, so the keys will be different. + (dir / "my-dir").createDirectory() + execute("jar", "uvf", "attachment.jar", "my-dir") + assertEquals(listOf(ALICE.name), load("attachment.jar").signers.map { it.name }) // Unsigned directory is irrelevant. + } + + @Test + fun `two signers`() { + execute("jar", "cvf", "attachment.jar", "_signable1", "_signable2") + execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "alicepass", "attachment.jar", "alice") + execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "bobpass", "attachment.jar", "bob") + assertEquals(listOf(ALICE.name, BOB.name), load("attachment.jar").signers.map { it.name }) + } + + @Test + fun `a party must sign all the files in the attachment to be a signer`() { + execute("jar", "cvf", "attachment.jar", "_signable1") + execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "alicepass", "attachment.jar", "alice") + assertEquals(listOf(ALICE.name), load("attachment.jar").signers.map { it.name }) + execute("jar", "uvf", "attachment.jar", "_signable2") + execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "bobpass", "attachment.jar", "bob") + assertEquals(listOf(BOB.name), load("attachment.jar").signers.map { it.name }) // ALICE hasn't signed the new file. + execute("jar", "uvf", "attachment.jar", "_signable3") + assertEquals(emptyList(), load("attachment.jar").signers) // Neither party has signed the new file. + } + + @Test + fun `bad signature is caught even if the party would not qualify as a signer`() { + (dir / "volatile").writeLines(listOf("volatile")) + execute("jar", "cvf", "attachment.jar", "volatile") + execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "alicepass", "attachment.jar", "alice") + assertEquals(listOf(ALICE.name), load("attachment.jar").signers.map { it.name }) + (dir / "volatile").writeLines(listOf("garbage")) + execute("jar", "uvf", "attachment.jar", "volatile", "_signable1") // ALICE's signature on volatile is now bad. + execute("jarsigner", "-keystore", "_teststore", "-storepass", "storepass", "-keypass", "bobpass", "attachment.jar", "bob") + val a = load("attachment.jar") + // The JDK doesn't care that BOB has correctly signed the whole thing, it won't let us process the entry with ALICE's bad signature: + assertThatThrownBy { a.signers }.isInstanceOf(SecurityException::class.java) + } +} diff --git a/core/src/test/kotlin/net/corda/core/internal/concurrent/CordaFutureImplTest.kt b/core/src/test/kotlin/net/corda/core/internal/concurrent/CordaFutureImplTest.kt index c176372a39..8b9efe991a 100644 --- a/core/src/test/kotlin/net/corda/core/internal/concurrent/CordaFutureImplTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/concurrent/CordaFutureImplTest.kt @@ -112,6 +112,18 @@ class CordaFutureTest { } verify(log).error(any(), same(throwable)) } + + @Test + fun `captureLater works`() { + val failingFuture = CordaFutureImpl() + val anotherFailingFuture = CordaFutureImpl() + anotherFailingFuture.captureLater(failingFuture) + + val exception = Exception() + failingFuture.setException(exception) + + Assertions.assertThatThrownBy { anotherFailingFuture.getOrThrow() }.isSameAs(exception) + } } class TransposeTest { diff --git a/core/src/test/kotlin/net/corda/core/node/ServiceInfoTests.kt b/core/src/test/kotlin/net/corda/core/node/ServiceInfoTests.kt index 7cf226d988..c7dfa52327 100644 --- a/core/src/test/kotlin/net/corda/core/node/ServiceInfoTests.kt +++ b/core/src/test/kotlin/net/corda/core/node/ServiceInfoTests.kt @@ -2,14 +2,14 @@ package net.corda.core.node import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType -import net.corda.testing.getTestX509Name +import net.corda.core.utilities.getX500Name import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith class ServiceInfoTests { val serviceType = ServiceType.getServiceType("test", "service").getSubType("subservice") - val name = getTestX509Name("service.name") + val name = getX500Name(O = "service.name", L = "London", C = "GB") @Test fun `type and name encodes correctly`() { 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 ed687c252a..ab38c1a545 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -46,13 +46,13 @@ private fun MockNetwork.MockNode.saveAttachment(content: String) = database.tran attachments.importAttachment(createAttachmentData(content).inputStream()) } private fun MockNetwork.MockNode.hackAttachment(attachmentId: SecureHash, content: String) = database.transaction { - attachments.updateAttachment(attachmentId, createAttachmentData(content)) + updateAttachment(attachmentId, createAttachmentData(content)) } /** * @see NodeAttachmentService.importAttachment */ -private fun NodeAttachmentService.updateAttachment(attachmentId: SecureHash, data: ByteArray) { +private fun updateAttachment(attachmentId: SecureHash, data: ByteArray) { val session = DatabaseTransactionManager.current().session val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) attachment?.let { @@ -73,6 +73,7 @@ class AttachmentSerializationTest { 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() + server.ensureRegistered() } @After @@ -112,6 +113,7 @@ class AttachmentSerializationTest { private class CustomAttachment(override val id: SecureHash, internal val customContent: String) : Attachment { override fun open() = throw UnsupportedOperationException("Not implemented.") + override val signers get() = throw UnsupportedOperationException() } private class CustomAttachmentLogic(server: MockNetwork.MockNode, private val attachmentId: SecureHash, private val customContent: String) : ClientLogic(server) { diff --git a/core/src/test/kotlin/net/corda/core/crypto/EncodingUtilsTest.kt b/core/src/test/kotlin/net/corda/core/utilities/EncodingUtilsTest.kt similarity index 97% rename from core/src/test/kotlin/net/corda/core/crypto/EncodingUtilsTest.kt rename to core/src/test/kotlin/net/corda/core/utilities/EncodingUtilsTest.kt index ae76f6e825..e3a6d0abaf 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/EncodingUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/utilities/EncodingUtilsTest.kt @@ -1,5 +1,6 @@ -package net.corda.core.crypto +package net.corda.core.utilities +import net.corda.core.crypto.AddressFormatException import org.junit.Test import kotlin.test.assertEquals import kotlin.test.fail diff --git a/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt b/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt index ae4a67c093..60bfdd8369 100644 --- a/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/utilities/KotlinUtilsTest.kt @@ -52,8 +52,7 @@ class KotlinUtilsTest : TestDependencyInjectionBase() { } @CordaSerializable - private class CapturingTransientProperty(prefix: String) { - private val seed = random63BitValue() + private class CapturingTransientProperty(val prefix: String, val seed: Long = random63BitValue()) { val transientVal by transient { prefix + seed + random63BitValue() } } } \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/utilities/LegalNameValidatorTest.kt b/core/src/test/kotlin/net/corda/core/utilities/LegalNameValidatorTest.kt index 899624c112..163381ca69 100644 --- a/core/src/test/kotlin/net/corda/core/utilities/LegalNameValidatorTest.kt +++ b/core/src/test/kotlin/net/corda/core/utilities/LegalNameValidatorTest.kt @@ -1,5 +1,6 @@ package net.corda.core.utilities +import org.bouncycastle.asn1.x500.X500Name import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -99,4 +100,33 @@ class LegalNameValidatorTest { validateLegalName("Legal Name With\n\rLine\nBreaks") } } + + @Test + fun `validate x500Name`() { + validateX500Name(X500Name("O=Bank A, L=New York, C=US, OU=Org Unit, CN=Service Name")) + validateX500Name(X500Name("O=Bank A, L=New York, C=US, CN=Service Name")) + validateX500Name(X500Name("O=Bank A, L=New York, C=US")) + validateX500Name(X500Name("O=Bank A, L=New York, C=US")) + + // Missing Organisation + assertFailsWith(IllegalArgumentException::class) { + validateX500Name(X500Name("L=New York, C=US, OU=Org Unit, CN=Service Name")) + } + // Missing Locality + assertFailsWith(IllegalArgumentException::class) { + validateX500Name(X500Name("O=Bank A, C=US, OU=Org Unit, CN=Service Name")) + } + // Missing Country + assertFailsWith(IllegalArgumentException::class) { + validateX500Name(X500Name("O=Bank A, L=New York, OU=Org Unit, CN=Service Name")) + } + // Wrong organisation name format + assertFailsWith(IllegalArgumentException::class) { + validateX500Name(X500Name("O=B, L=New York, C=US, OU=Org Unit, CN=Service Name")) + } + // Wrong organisation name format + assertFailsWith(IllegalArgumentException::class) { + validateX500Name(X500Name("O=B, L=New York, C=US, OU=Org Unit, CN=Service Name")) + } + } } \ No newline at end of file diff --git a/docs/build.gradle b/docs/build.gradle index b48623ac83..f4bf3cfa2e 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -9,7 +9,18 @@ dokka { moduleName = 'corda' outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/kotlin") processConfigurations = ['compile'] - sourceDirs = files('../core/src/main/kotlin', '../client/jfx/src/main/kotlin', '../client/mock/src/main/kotlin', '../client/rpc/src/main/kotlin', '../node/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin') + sourceDirs = files('../core/src/main/kotlin', '../client/jfx/src/main/kotlin', '../client/mock/src/main/kotlin', '../client/rpc/src/main/kotlin', '../node-api/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin') + jdkVersion = 8 + + externalDocumentationLink { + url = new URL("http://fasterxml.github.io/jackson-core/javadoc/2.8/") + } + externalDocumentationLink { + url = new URL("https://docs.oracle.com/javafx/2/api/") + } + externalDocumentationLink { + url = new URL("http://www.bouncycastle.org/docs/docs1.5on/") + } } task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) { @@ -17,8 +28,19 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) { outputFormat = "javadoc" outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/javadoc") processConfigurations = ['compile'] - sourceDirs = files('../core/src/main/kotlin', '../client/jfx/src/main/kotlin', '../client/mock/src/main/kotlin', '../client/rpc/src/main/kotlin', '../node/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin') + sourceDirs = files('../core/src/main/kotlin', '../client/jfx/src/main/kotlin', '../client/mock/src/main/kotlin', '../client/rpc/src/main/kotlin', '../node-api/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin') includes = ['packages.md'] + jdkVersion = 8 + + externalDocumentationLink { + url = new URL("http://fasterxml.github.io/jackson-core/javadoc/2.8/") + } + externalDocumentationLink { + url = new URL("https://docs.oracle.com/javafx/2/api/") + } + externalDocumentationLink { + url = new URL("http://www.bouncycastle.org/docs/docs1.5on/") + } } task buildDocs(dependsOn: ['apidocs', 'makeDocs']) diff --git a/docs/packages.md b/docs/packages.md index 6c620dede6..61a4b2cb93 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -1,3 +1,104 @@ +# Package net.corda.client.jackson + +Utilities and serialisers for working with JSON representations of basic types. This adds Jackson support for +the java.time API, some core types, and Kotlin data classes. + +# Package net.corda.client.jfx.model + +Data models for binding data feeds from Corda nodes into a JavaFX user interface, by presenting the data as [javafx.beans.Observable] +types. + +# Package net.corda.client.jfx.utils + +Utility classes (i.e. data classes) used by the Corda JavaFX client. + +# Package net.corda.client.mock + +Tools used by the client to produce mock data for testing purposes. + +# Package net.corda.client.rpc + +RPC client interface to Corda, for use both by user-facing client and integration with external systems. + +# Package net.corda.client.rpc.internal + +Internal, do not use. These APIs and implementations which are currently being revised and are subject to future change. + +# Package net.corda.core.concurrent + +Provides a simplified [java.util.concurrent.Future] class that allows registration of a callback to execute when the future +is complete. + +# Package net.corda.core.contracts + +This package contains the base data types for smarts contracts implemented in Corda. To implement a new contract start +with [Contract], or see the examples in [net.corda.finance.contracts]. + +Corda smart contracts are a combination of state held on the distributed ledger, and verification logic which defines +which transformations of state are valid. + +# Package net.corda.core.crypto + +Cryptography data and utility classes used for signing, verifying, key management and data integrity checks. + +# Package net.corda.core.flows + +Base data types and abstract classes for implementing Corda flows. To implement a new flow start with [FlowLogic], or +see [CollectSignaturesFlow] for a simple example flow. Flows are started via a node's [ServiceHub]. + +Corda flows are a tool for modelling the interactions between two or more nodes as they negotiate a workflow. +This can range from a simple case of completing a trade which has been agreed upon externally, to more complex +processes such as handling fixing of interest rate swaps. + +# Package net.corda.core.identity + +Data classes which model different forms of identity (potentially with supporting evidence) for legal entities and services. + +# Package net.corda.core.internal + +Internal, do not use. These APIs and implementations which are currently being revised and are subject to future change. + +# Package net.corda.core.messaging + +Data types used by the Corda messaging layer to manage state of messaging and sessions between nodes. + +# Package net.corda.core.node.services + +Services which run within a Corda node and provide various pieces of functionality such as identity management, transaction storage, etc. + +# Package net.corda.core.node.services.vault + +Supporting data types for the vault services. + +# Package net.corda.core.schemas + +Data types representing database schemas for storing Corda data via an object mapper such as Hibernate. Modifying Corda +state in the database directly is not a supported approach, however these can be used to read state for integrations with +external systems. + +# Package net.corda.core.serialization + +Supporting data types and classes for serialization of Corda data types. + +# Package net.corda.core.transactions + +Base data types for transactions which modify contract state on the distributed ledger. + +The core transaction on the ledger is [WireTransaction], which is constructed by [TransactionBuilder]. Once signed a transaction is stored +in [SignedTransaction] which encapsulates [WireTransaction]. Finally there is a special-case [LedgerTransaction] which is used by contracts +validating transactions, and is built from the wire transaction by resolving all references into their underlying data (i.e. inputs are +actual states rather than state references). + +# Package net.corda.core.utilities + +Corda utility classes, providing a broad range of functionality to help implement both Corda nodes and CorDapps. + +# Package net.corda.finance + +The finance module is a CorDapp containing sample cash and obligation contracts, as well as providing several +useful data types such as [Amount]. + # Package net.corda.finance.utils -A collection of utilities for summing financial states, for example, summing obligations to get total debts. \ No newline at end of file +A collection of utilities for summing financial states, for example, summing obligations to get total debts. + diff --git a/docs/source/api-contracts.rst b/docs/source/api-contracts.rst index 6da93efeb3..ea057bbce9 100644 --- a/docs/source/api-contracts.rst +++ b/docs/source/api-contracts.rst @@ -132,9 +132,9 @@ exception will cause the transaction to be rejected. Commands ^^^^^^^^ -``LedgerTransaction`` contains the commands as a list of ``AuthenticatedObject`` instances. -``AuthenticatedObject`` pairs an object with a list of signers. In this case, ``AuthenticatedObject`` pairs a command -with a list of the entities that are required to sign a transaction where this command is present: +``LedgerTransaction`` contains the commands as a list of ``CommandWithParties`` instances. +``CommandWithParties`` pairs a command with a list of the entities that are required to sign a transaction +where this command is present: .. container:: codeset @@ -153,7 +153,7 @@ Extracting commands ~~~~~~~~~~~~~~~~~~~ You can use the ``requireSingleCommand()`` helper method to extract commands. -`` Collection>.requireSingleCommand(klass: Class)`` asserts that +`` Collection>.requireSingleCommand(klass: Class)`` asserts that the transaction contains exactly one command of type ``T``, and returns it. If there is not exactly one command of this type in the transaction, an exception is thrown, rejecting the transaction. @@ -197,7 +197,7 @@ execution of ``verify()``: @Override public void verify(LedgerTransaction tx) { - final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Commands.class); + final CommandWithParties command = requireSingleCommand(tx.getCommands(), Commands.class); if (command.getValue() instanceof Commands.Issue) { // Issuance verification logic. diff --git a/docs/source/api-core-types.rst b/docs/source/api-core-types.rst index 2505068675..31593f990a 100644 --- a/docs/source/api-core-types.rst +++ b/docs/source/api-core-types.rst @@ -40,9 +40,9 @@ anonymous parties to full parties. .. note:: These types are provisional and will change significantly in future as the identity framework becomes more fleshed out. -AuthenticatedObject -------------------- -An ``AuthenticatedObject`` represents an object (like a command) and the list of associated signers. +CommandWithParties +------------------ +A ``CommandWithParties`` represents a command and the list of associated signers' identities. Multi-signature support ----------------------- diff --git a/docs/source/api-flows.rst b/docs/source/api-flows.rst index 57d46f6362..4c1ef7fd2b 100644 --- a/docs/source/api-flows.rst +++ b/docs/source/api-flows.rst @@ -196,8 +196,13 @@ updates. This section details the API for common tasks. Transaction building ^^^^^^^^^^^^^^^^^^^^ -The majority of the work performed during a flow will be to build, verify and sign a transaction. We cover this in -:doc:`api-transactions`. +The majority of the work performed during a flow will be to build, verify and sign a transaction. This is covered +in :doc:`api-transactions`. + +Extracting states from the vault +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +When building a transaction, you'll often need to extract the states you wish to consume from the vault. This is +covered in :doc:`api-vault-query`. Retrieving information about other nodes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -592,4 +597,4 @@ We then update the progress tracker's current step as we progress through the fl :language: java :start-after: DOCSTART 18 :end-before: DOCEND 18 - :dedent: 12 \ No newline at end of file + :dedent: 12 diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index 24db09fcaa..fbfb0c5d98 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -17,9 +17,9 @@ The ORM mapping is specified using the `Java Persistence API `) - custom JPA_/JPQL_ queries - custom 3rd party Data Access frameworks such as `Spring Data `_ @@ -62,7 +63,7 @@ There are four implementations of this interface which can be chained together t .. 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), uuid(s), and externalId(s). .. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those interfaces common state attributes to the **vault_linear_states** table. @@ -70,14 +71,14 @@ There are four implementations of this interface which can be chained together t .. 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`) -All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators, as also illustrated above. +All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators. All ``QueryCriteria`` implementations provide an explicitly specifiable set of common attributes: 1. State status attribute (``Vault.StateStatus``), which defaults to filtering on UNCONSUMED states. When chaining several criterias using AND / OR, the last value of this attribute will override any previous. -2. Contract state types (``>``), which will contain at minimum one type (by default this will be ``ContractState`` which resolves to all types). - When chaining several criteria using AND / OR, all specified contract state types are combined into a single set. +2. Contract state types (``>``), which will contain at minimum one type (by default this will be ``ContractState`` which resolves to all state types). + When chaining several criteria using ``and`` and ``or`` operators, all specified contract state types are combined into a single set. An example of a custom query is illustrated here: @@ -107,7 +108,7 @@ An example of a custom query in Java is illustrated here: :start-after: DOCSTART VaultJavaQueryExample3 :end-before: DOCEND VaultJavaQueryExample3 -.. 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). +.. note:: Queries by ``Party`` specify the ``AbstractParty`` which may be concrete or anonymous. In the later case, where an anonymous party does not resolve to an X500Name via the IdentityService, no query results will ever be produced. For performance reasons, queries do not use PublicKey as search criteria. Pagination ---------- @@ -154,8 +155,6 @@ Query for unconsumed states for a given notary: :start-after: DOCSTART VaultQueryExample4 :end-before: DOCEND VaultQueryExample4 -.. note:: We are using the notaries X500Name as our search identifier. - Query for unconsumed states for a given set of participants: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -170,7 +169,7 @@ Query for unconsumed states recorded between two time intervals: :start-after: DOCSTART VaultQueryExample6 :end-before: DOCEND VaultQueryExample6 -.. note:: This example illustrates usage of a Between ColumnPredicate. +.. note:: This example illustrates usage of a ``Between`` ``ColumnPredicate``. Query for all states with pagination specification (10 results per page): @@ -179,7 +178,14 @@ Query for all states with pagination specification (10 results per page): :start-after: DOCSTART VaultQueryExample7 :end-before: DOCEND VaultQueryExample7 -.. note:: The result set metadata field `totalStatesAvailable` allows you to further paginate accordingly. +.. note:: The result set metadata field `totalStatesAvailable` allows you to further paginate accordingly as demonstrated in the following example. + +Query for all states using pagination specification and iterate using `totalStatesAvailable` field until no further pages available: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExamplePaging + :end-before: DOCEND VaultQueryExamplePaging **LinearState and DealState queries using** ``LinearStateQueryCriteria`` @@ -389,77 +395,4 @@ The Corda Tutorials provide examples satisfying these additional Use Cases: .. _JPQL: http://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#hql .. _JPA: https://docs.spring.io/spring-data/jpa/docs/current/reference/html -Upgrading from previous releases ---------------------------------- -Here follows a selection of the most common upgrade scenarios: - -1. ServiceHub usage to obtain Unconsumed states for a given contract state type - - Previously: - -.. container:: codeset - - .. sourcecode:: kotlin - - val yoStates = b.vault.unconsumedStates() - -This query returned an ``Iterable>`` - - Now: - -.. container:: codeset - - .. sourcecode:: kotlin - - val yoStates = b.vault.queryBy().states - -The query returns a ``Vault.Page`` result containing: - - - 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. - - 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 - - Previously: - -.. container:: codeset - - .. sourcecode:: kotlin - - val iouStates = serviceHub.vaultService.linearHeadsOfType() - val iouToSettle = iouStates[linearId] ?: throw Exception("IOUState with linearId $linearId not found.") - - Now: - -.. container:: codeset - - .. sourcecode:: kotlin - - val criteria = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(linearId)) - val iouStates = serviceHub.vaultService.queryBy(criteria).states - - val iouToSettle = iouStates.singleOrNull() ?: throw Exception("IOUState with linearId $linearId not found.") - -3. RPC usage was limited to using the ``vaultAndUpdates`` RPC method, which returned a snapshot and streaming updates as an Observable. - In many cases, queries were not interested in the streaming updates. - - Previously: - -.. container:: codeset - - .. sourcecode:: kotlin - - val iouStates = services.vaultAndUpdates().first.filter { it.state.data is IOUState } - - Now: - -.. container:: codeset - - .. sourcecode:: kotlin - - val iouStates = services.vaultQueryBy() - diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index cd88037e0a..0f66efa1de 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,12 @@ from the previous milestone release. UNRELEASED ---------- +* About half of the code in test-utils has been moved to a new module ``node-driver``, + and the test scope modules are now located in a ``testing`` directory. + +* Contract Upgrades: deprecated RPC authorisation / deauthorisation API calls in favour of equivalent flows in ContractUpgradeFlow. + Implemented contract upgrade persistence using JDBC backed persistent map. + * Vault query common attributes (state status and contract state types) are now handled correctly when using composite criteria specifications. State status is overridable. Contract states types are aggregatable. @@ -34,7 +40,6 @@ UNRELEASED must use ``SendTransactionFlow`` at the correct place. There is also ``ReceiveStateAndRefFlow`` and ``SendStateAndRefFlow`` for dealing with ``StateAndRef``s. - * Vault query soft locking enhancements and deprecations * removed original ``VaultService`` ``softLockedStates` query mechanism. * introduced improved ``SoftLockingCondition`` filterable attribute in ``VaultQueryCriteria`` to enable specification @@ -68,6 +73,59 @@ UNRELEASED * Moved ``:finance`` gradle project files into a ``net.corda.finance`` package namespace. This may require adjusting imports of Cash flow references and also of ``StartFlow`` permission in ``gradle.build`` files. +* Removed the concept of relevancy from ``LinearState``. The ``ContractState``'s relevancy to the vault can be determined + by the flow context, the vault will process any transaction from a flow which is not derived from transaction resolution verification. + +* Removed the tolerance attribute from ``TimeWindowChecker`` and thus, there is no extra tolerance on the notary side anymore. + +* The ``FungibleAsset`` interface has been made simpler. The ``Commands`` grouping interface + that included the ``Move``, ``Issue`` and ``Exit`` interfaces have all been removed, while the ``move`` function has + been renamed to ``withNewOwnerAndAmount`` to be consistent with the ``withNewOwner`` function of the ``OwnableState``. + +* The ``IssueCommand`` interface has been removed from ``Structures``, because, due to the introduction of nonces per + transaction component, the issue command does not need a nonce anymore and it does not require any other attributes. + +* As a consequence of the above and the simpler ``FungibleAsset`` format, fungible assets like ``Cash`` now use + ``class Issue : TypeOnlyCommandData()``, because it's only its presence (``Issue``) that matters. + +* A new `PrivacySalt` transaction component is introduced, which is now an attribute in ``TraversableTransaction`` and + inherently in ``WireTransaction``. + +* A new ``nonces: List`` feature has been added to ``FilteredLeaves``. + +* Due to the ``nonces`` and ``PrivacySalt`` introduction, new functions have been added to ``MerkleTransaction``: + ``fun serializedHash(x: T, privacySalt: PrivacySalt?, index: Int): SecureHash`` + ``fun serializedHash(x: T, nonce: SecureHash): SecureHash`` + ``fun computeNonce(privacySalt: PrivacySalt, index: Int)``. + +* A new ``SignatureMetadata`` data class is introduced with two attributes, ``platformVersion: Int`` and + ``schemeNumberID: Int`` (the signature scheme used). + +* As part of the metadata support in signatures, a new ``data class SignableData(val txId: SecureHash, val signatureMetadata: SignatureMetadata)`` + is introduced, which represents the object actually signed. + +* The unused ``MetaData`` and ``SignatureType`` in ``crypto`` package have been removed. + +* The ``class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata): DigitalSignature(bytes)`` + class is now utilised Vs the old ``DigitalSignature.WithKey`` for Corda transaction signatures. Practically, it takes + the ``signatureMetadata`` as an extra input, in order to support signing both the transaction and the extra metadata. + +* To reflect changes in the signing process, the ``Crypto`` object is now equipped with the: + ``fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature`` and + ``fun doVerify(txId: SecureHash, transactionSignature: TransactionSignature): Boolean`` functions. + +* ``SerializationCustomization.addToWhitelist()` now accepts multiple classes via varargs. + +* Two functions to easily sign a ``FilteredTransaction`` have been added to ``ServiceHub``: + ``createSignature(filteredTransaction: FilteredTransaction, publicKey: PublicKey)`` and + ``createSignature(filteredTransaction: FilteredTransaction)`` to sign with the legal identity key. + +* A new helper method ``buildFilteredTransaction(filtering: Predicate)`` is added to ``SignedTransaction`` to + directly build a ``FilteredTransaction`` using provided filtering functions, without first accessing the + ``tx: WireTransaction``. + +* Test type ``NodeHandle`` now has method ``stop(): CordaFuture`` that terminates the referenced node. + Milestone 14 ------------ diff --git a/docs/source/clauses.rst b/docs/source/clauses.rst index d19552987c..93b6b097ad 100644 --- a/docs/source/clauses.rst +++ b/docs/source/clauses.rst @@ -21,7 +21,7 @@ Let's take a look at a simplified structure of the ``Clause`` class: abstract fun verify(tx: LedgerTransaction, inputs: List, outputs: List, - commands: List>, + commands: List>, groupingKey: K?): Set ... } @@ -235,7 +235,7 @@ Example from ``CommercialPaper.kt``: override fun verify(tx: LedgerTransaction, inputs: List, outputs: List, - commands: List>, + commands: List>, groupingKey: Issued?): Set { val consumedCommands = super.verify(tx, inputs, outputs, commands, groupingKey) ... diff --git a/docs/source/contract-upgrade.rst b/docs/source/contract-upgrade.rst index 5d7298b36b..a00b6b0870 100644 --- a/docs/source/contract-upgrade.rst +++ b/docs/source/contract-upgrade.rst @@ -21,23 +21,21 @@ Here's the workflow for contract upgrades: 1. Two banks, A and B negotiate a trade, off-platform -2. Banks A and B execute a protocol to construct a state object representing the trade, using contract X, and include it in a transaction (which is then signed and sent to the Uniqueness Service). +2. Banks A and B execute a protocol to construct a state object representing the trade, using contract X, and include it in a transaction (which is then signed and sent to the consensus service). 3. Time passes. -4. The developer of contract X discovers a bug in the contract code, and releases a new version, contract Y. -And notify the users (e.g. via a mailing list or CorDapp store). -At this point of time all nodes should stop issuing states of contract X. +4. The developer of contract X discovers a bug in the contract code, and releases a new version, contract Y. The developer will then notify all existing users (e.g. via a mailing list or CorDapp store) to stop their nodes from issuing further states with contract X. -5. Banks A and B review the new contract via standard change control processes and identify the contract states they agreed to upgrade, they can decide not to upgrade some contract states as they might be needed for other obligation contract. +5. Banks A and B review the new contract via standard change control processes and identify the contract states they agree to upgrade (they may decide not to upgrade some contract states as these might be needed for some other obligation contract). 6. Banks A and B instruct their Corda nodes (via RPC) to be willing to upgrade state objects of contract X, to state objects for contract Y using agreed upgrade path. -7. One of the parties ``Instigator`` initiates an upgrade of state objects referring to contract X, to a new state object referring to contract Y. +7. One of the parties initiates (``Initiator``) an upgrade of state objects referring to contract X, to a new state object referring to contract Y. 8. A proposed transaction ``Proposal``, taking in the old state and outputting the reissued version, is created and signed with the node's private key. -9. The node ``Instigator`` sends the proposed transaction, along with details of the new contract upgrade path it's proposing, to all participants of the state object. +9. The ``Initiator`` node sends the proposed transaction, along with details of the new contract upgrade path its proposing, to all participants of the state object. 10. Each counterparty ``Acceptor`` verifies the proposal, signs or rejects the state reissuance accordingly, and sends a signature or rejection notification back to the initiating node. @@ -48,32 +46,38 @@ Authorising upgrade ------------------- Each of the participants in the upgrading contract will have to instruct their node that they are willing to upgrade the state object before the upgrade. -Currently the vault service is used to manage the authorisation records. The administrator can use RPC to perform such instructions. +The ``ContractUpgradeFlow`` is used to manage the authorisation records. The administrator can use RPC to trigger either an ``Authorise`` or ``Deauthorise`` flow. .. container:: codeset .. sourcecode:: kotlin - - /** - * Authorise a contract state upgrade. - * This will store the upgrade authorisation in the vault, and will be queried by [ContractUpgradeFlow] during contract upgrade process. - * Invoking this method indicate the node is willing to upgrade the [state] using the [upgradedContractClass]. - * This method will NOT initiate the upgrade process. - */ - fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class>) - /** - * Authorise a contract state upgrade. - * This will remove the upgrade authorisation from the vault. - */ - fun deauthoriseContractUpgrade(state: StateAndRef<*>) + /** + * Authorise a contract state upgrade. + * This will store the upgrade authorisation in persistent store, and will be queried by [ContractUpgradeFlow.Acceptor] during contract upgrade process. + * Invoking this flow indicates the node is willing to upgrade the [StateAndRef] using the [UpgradedContract] class. + * This method will NOT initiate the upgrade process. To start the upgrade process, see [Initiator]. + */ + @StartableByRPC + class Authorise( + val stateAndRef: StateAndRef<*>, + private val upgradedContractClass: Class> + ) : FlowLogic() + /** + * Deauthorise a contract state upgrade. + * This will remove the upgrade authorisation from persistent store (and prevent any further upgrade) + */ + @StartableByRPC + class Deauthorise( + val stateRef: StateRef + ) : FlowLogic< Void?>() Proposing an upgrade -------------------- -After all parties have registered the intention of upgrading the contract state, one of the contract participant can initiate the upgrade process by running the contract upgrade flow. -The Instigator will create a new state and sent to each participant for signatures, each of the participants (Acceptor) will verify and sign the proposal and returns to the instigator. +After all parties have registered the intention of upgrading the contract state, one of the contract participants can initiate the upgrade process by triggering the ``Initiator`` contract upgrade flow. +The ``Initiator`` will create a new state and sent to each participant for signatures, each of the participants (Acceptor) will verify, sign the proposal and return to the initiator. The transaction will be notarised and persisted once every participant verified and signed the upgrade proposal. Examples @@ -81,7 +85,7 @@ Examples Lets assume Bank A has entered into an agreement with Bank B, and the contract is translated into contract code ``DummyContract`` with state object ``DummyContractState``. -Few days after the exchange of contracts, the developer of the contract code discovered a bug/misrepresentation in the contract code. +A few days after the exchange of contracts, the developer of the contract code discovered a bug/misrepresentation in the contract code. 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. @@ -99,7 +103,7 @@ Bank A and Bank B decided to upgrade the contract to ``DummyContractV2`` val rpcClient : CordaRPCClient = << Bank A's Corda RPC Client >> val rpcA = rpcClient.proxy() - rpcA.authoriseContractUpgrade(<>, DummyContractV2::class.java) + rpcA.startFlow(ContractUpgradeFlow.Authorise(<>, DummyContractV2::class.java)) 3. Bank B now initiate the upgrade Flow, this will send a upgrade proposal to all contract participants. Each of the participants of the contract state will sign and return the contract state upgrade proposal once they have validated and agreed with the upgrade. @@ -115,7 +119,7 @@ The upgraded transaction state will be recorded in every participant's node at t <>, DummyContractV2::class.java) -.. note:: See ``ContractUpgradeFlowTest.2 parties contract upgrade using RPC`` for more detailed code example. +.. note:: See ``ContractUpgradeFlowTest`` for more detailed code examples. diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 5b5d410c8f..f8e630afda 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -34,7 +34,7 @@ NetworkMapService plus Simple Notary configuration file. .. parsed-literal:: - myLegalName : "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" + myLegalName : "O=Notary Service,OU=corda,L=London,C=GB" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" p2pAddress : "localhost:12345" diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index eac18bd663..0517d61c4d 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -17,9 +17,9 @@ notary/network map node: task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK" + networkMap "O=Controller,OU=corda,L=London,C=UK" node { - name "CN=Controller,O=R3,OU=corda,L=London,C=UK" + name "O=Controller,OU=corda,L=London,C=UK" advertisedServices = ["corda.notary.validating"] p2pPort 10002 rpcPort 10003 diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index 9866eda06e..621dfffb76 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -10,9 +10,6 @@ repositories { maven { url 'http://oss.sonatype.org/content/repositories/snapshots' } - maven { - url 'https://dl.bintray.com/kotlin/exposed' - } } configurations { @@ -35,7 +32,7 @@ compileTestJava.dependsOn tasks.getByPath(':node:capsule:buildCordaJAR') dependencies { cordaCompile project(':core') cordaCompile project(':client:jfx') - cordaCompile project(':test-utils') + cordaCompile project(':node-driver') testCompile project(':verifier') compile "org.graphstream:gs-core:1.3" @@ -45,6 +42,9 @@ dependencies { cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + + // Corda Plugins: dependent flows and services + cordapp project(':finance') } mainClassName = "net.corda.docs.ClientRpcTutorialKt" @@ -70,9 +70,9 @@ task integrationTest(type: Test) { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" + networkMap "O=Notary Service,OU=corda,L=London,C=GB" node { - name "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" + name "O=Notary Service,OU=corda,L=London,C=GB" advertisedServices = ["corda.notary.validating"] p2pPort 10002 rpcPort 10003 @@ -80,7 +80,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { cordapps = [] } node { - name "CN=Alice Corp,O=Alice Corp,L=London,C=GB" + name "O=Alice Corp,L=London,C=GB" advertisedServices = [] p2pPort 10005 rpcPort 10006 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 288c0015e0..e14a4e4bbb 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 @@ -32,9 +32,9 @@ class IntegrationTestingTutorial { startFlowPermission() )) val (alice, bob, notary) = listOf( - startNode(ALICE.name, rpcUsers = listOf(aliceUser)), - startNode(BOB.name, rpcUsers = listOf(bobUser)), - startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))) + startNode(providedName = ALICE.name, rpcUsers = listOf(aliceUser)), + startNode(providedName = BOB.name, rpcUsers = listOf(bobUser)), + startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))) ).transpose().getOrThrow() // END 1 @@ -45,8 +45,8 @@ class IntegrationTestingTutorial { val bobClient = bob.rpcClientToNode() val bobProxy = bobClient.start("bobUser", "testPassword2").proxy - aliceProxy.waitUntilRegisteredWithNetworkMap().getOrThrow() - bobProxy.waitUntilRegisteredWithNetworkMap().getOrThrow() + aliceProxy.waitUntilNetworkReady().getOrThrow() + bobProxy.waitUntilNetworkReady().getOrThrow() // END 2 // START 3 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 bf91c9870d..998b6bb4d9 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 @@ -16,19 +16,17 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; 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.ProgressTracker; import net.corda.core.utilities.ProgressTracker.Step; import net.corda.core.utilities.UntrustworthyData; +import net.corda.core.utilities.X500NameUtils; import net.corda.finance.contracts.asset.Cash; import net.corda.testing.contracts.DummyContract; import net.corda.testing.contracts.DummyState; -import org.bouncycastle.asn1.x500.X500Name; import org.jetbrains.annotations.NotNull; import java.security.GeneralSecurityException; import java.security.PublicKey; -import java.security.SignatureException; import java.time.Duration; import java.time.Instant; import java.util.List; @@ -124,7 +122,7 @@ public class FlowCookbookJava { // - To serve as a timestamping authority if the transaction has a time-window // We retrieve a notary from the network map. // DOCSTART 1 - Party specificNotary = getServiceHub().getNetworkMapCache().getNotary(new X500Name("CN=Notary Service,O=R3,OU=corda,L=London,C=UK")); + Party specificNotary = getServiceHub().getNetworkMapCache().getNotary(X500NameUtils.getX500Name("Notary Service", "London", "UK")); Party anyNotary = getServiceHub().getNetworkMapCache().getAnyNotary(null); // Unlike the first two methods, ``getNotaryNodes`` returns a // ``List``. We have to extract the notary identity of @@ -135,7 +133,7 @@ public class FlowCookbookJava { // We may also need to identify a specific counterparty. // Again, we do so using the network map. // DOCSTART 2 - Party namedCounterparty = getServiceHub().getNetworkMapCache().getNodeByLegalName(new X500Name("CN=NodeA,O=NodeA,L=London,C=UK")).getLegalIdentity(); + Party namedCounterparty = getServiceHub().getNetworkMapCache().getNodeByLegalName(X500NameUtils.getX500Name("NodeA", "London", "UK")).getLegalIdentity(); Party keyedCounterparty = getServiceHub().getNetworkMapCache().getNodeByLegalIdentityKey(dummyPubKey).getLegalIdentity(); Party firstCounterparty = getServiceHub().getNetworkMapCache().getPartyNodes().get(0).getLegalIdentity(); // DOCEND 2 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 6f6c080322..f64503f2c0 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 @@ -49,8 +49,8 @@ fun main(args: Array) { startFlowPermission())) driver(driverDirectory = baseDirectory) { - startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))) - val node = startNode(ALICE.name, rpcUsers = listOf(user)).get() + startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))) + val node = startNode(providedName = ALICE.name, rpcUsers = listOf(user)).get() // END 1 // START 2 @@ -63,7 +63,7 @@ fun main(args: Array) { // END 2 // START 3 - val (transactions: List, futureTransactions: Observable) = proxy.verifiedTransactionsFeed() + val (transactions: List, futureTransactions: Observable) = proxy.internalVerifiedTransactionsFeed() // END 3 // START 4 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 904d4fa95d..1824302eb6 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 @@ -16,16 +16,12 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.* import net.corda.core.utilities.ProgressTracker.Step -import net.corda.core.utilities.UntrustworthyData -import net.corda.core.utilities.seconds -import net.corda.core.utilities.unwrap import net.corda.finance.contracts.asset.Cash import net.corda.testing.ALICE_PUBKEY import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState -import org.bouncycastle.asn1.x500.X500Name import java.security.PublicKey import java.time.Instant @@ -63,6 +59,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() @@ -105,7 +102,7 @@ object FlowCookbook { // - To serve as a timestamping authority if the transaction has a time-window // We retrieve the notary from the network map. // DOCSTART 1 - val specificNotary: Party? = serviceHub.networkMapCache.getNotary(X500Name("CN=Notary Service,O=R3,OU=corda,L=London,C=UK")) + val specificNotary: Party? = serviceHub.networkMapCache.getNotary(getX500Name(O = "Notary Service", OU = "corda", L = "London", C = "UK")) val anyNotary: Party? = serviceHub.networkMapCache.getAnyNotary() // Unlike the first two methods, ``getNotaryNodes`` returns a // ``List``. We have to extract the notary identity of @@ -116,7 +113,7 @@ object FlowCookbook { // We may also need to identify a specific counterparty. Again, we // do so using the network map. // DOCSTART 2 - val namedCounterparty: Party? = serviceHub.networkMapCache.getNodeByLegalName(X500Name("CN=NodeA,O=NodeA,L=London,C=UK"))?.legalIdentity + val namedCounterparty: Party? = serviceHub.networkMapCache.getNodeByLegalName(getX500Name(O = "NodeA", L = "London", C = "UK"))?.legalIdentity val keyedCounterparty: Party? = serviceHub.networkMapCache.getNodeByLegalIdentityKey(dummyPubKey)?.legalIdentity val firstCounterparty: Party = serviceHub.networkMapCache.partyNodes[0].legalIdentity // DOCEND 2 @@ -389,7 +386,7 @@ object FlowCookbook { subFlow(SendTransactionFlow(counterparty, twiceSignedTx)) // Optional request verification to further restrict data access. - subFlow(object :SendTransactionFlow(counterparty, twiceSignedTx){ + subFlow(object : SendTransactionFlow(counterparty, twiceSignedTx) { override fun verifyDataRequest(dataRequest: FetchDataFlow.Request.Data) { // Extra request verification. } 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 759cabb4d9..13d6b13acc 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 @@ -109,10 +109,10 @@ class ForeignExchangeFlow(val tradeId: String, } else throw IllegalArgumentException("Our identity must be one of the parties in the trade.") // Call the helper method to identify suitable inputs and make the outputs - val (outInputStates, ourOutputStates) = prepareOurInputsAndOutputs(serviceHub, runId.uuid, localRequest) + val (ourInputStates, ourOutputStates) = prepareOurInputsAndOutputs(serviceHub, runId.uuid, localRequest) // identify the notary for our states - val notary = outInputStates.first().state.notary + val notary = ourInputStates.first().state.notary // ensure request to other side is for a consistent notary val remoteRequestWithNotary = remoteRequest.copy(notary = notary) @@ -142,7 +142,7 @@ class ForeignExchangeFlow(val tradeId: String, } // having collated the data create the full transaction. - val signedTransaction = buildTradeProposal(outInputStates, ourOutputStates, theirInputStates, theirOutputStates) + val signedTransaction = buildTradeProposal(ourInputStates, ourOutputStates, theirInputStates, theirOutputStates) // pass transaction details to the counterparty to revalidate and confirm with a signature // Allow otherParty to access our data to resolve the transaction. 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 daa6d6e149..8d42157936 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 @@ -3,7 +3,6 @@ package net.corda.docs import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* import net.corda.core.crypto.TransactionSignature -import net.corda.core.crypto.containsAny import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy @@ -18,7 +17,6 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.seconds import net.corda.core.utilities.unwrap -import java.security.PublicKey // Minimal state model of a manual approval process @CordaSerializable @@ -32,7 +30,7 @@ enum class WorkflowState { * Minimal contract to encode a simple workflow with one initial state and two possible eventual states. * It is assumed one party unilaterally submits and the other manually retrieves the deal and completes it. */ -data class TradeApprovalContract(private val blank: Void? = null) : Contract { +data class TradeApprovalContract(val blank: Unit? = null) : Contract { interface Commands : CommandData { class Issue : TypeOnlyCommandData(), Commands // Record receipt of deal details @@ -51,10 +49,6 @@ data class TradeApprovalContract(private val blank: Void? = null) : Contract { val parties: List get() = listOf(source, counterparty) override val participants: List get() = parties - - override fun isRelevant(ourKeys: Set): Boolean { - return participants.any { it.owningKey.containsAny(ourKeys) } - } } /** diff --git a/docs/source/example-code/src/main/resources/example-network-map-node.conf b/docs/source/example-code/src/main/resources/example-network-map-node.conf index 1b425c1bd7..fe00d9aa38 100644 --- a/docs/source/example-code/src/main/resources/example-network-map-node.conf +++ b/docs/source/example-code/src/main/resources/example-network-map-node.conf @@ -1,4 +1,4 @@ -myLegalName : "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" +myLegalName : "O=Notary Service,OU=corda,L=London,C=GB" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" p2pAddress : "my-network-map:10000" diff --git a/docs/source/example-code/src/main/resources/example-node.conf b/docs/source/example-code/src/main/resources/example-node.conf index bb7ce35947..e6a5a706f5 100644 --- a/docs/source/example-code/src/main/resources/example-node.conf +++ b/docs/source/example-code/src/main/resources/example-node.conf @@ -1,4 +1,4 @@ -myLegalName : "CN=Bank A,O=Bank A,L=London,C=GB" +myLegalName : "O=Bank A,L=London,C=GB" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" dataSourceProperties : { @@ -13,7 +13,7 @@ webAddress : "localhost:10004" extraAdvertisedServiceIds : [ "corda.interest_rates" ] networkMapService : { address : "my-network-map:10000" - legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=GB" + legalName : "O=Network Map Service,OU=corda,L=London,C=GB" } useHTTPS : false rpcUsers : [ diff --git a/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf b/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf index 713da39f55..8d43f51a9f 100644 --- a/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf +++ b/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf @@ -1,8 +1,8 @@ -myLegalName : "CN=Bank A,O=Bank A,L=London,C=GB" +myLegalName : "O=Bank A,L=London,C=GB" p2pAddress : "my-corda-node:10002" webAddress : "localhost:10003" networkMapService : { address : "my-network-map:10000" - legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=GB" + legalName : "O=Network Map Service,OU=corda,L=London,C=GB" } verifierType: "OutOfProcess" diff --git a/docs/source/getting-set-up.rst b/docs/source/getting-set-up.rst index 9bea246253..510dcc43ae 100644 --- a/docs/source/getting-set-up.rst +++ b/docs/source/getting-set-up.rst @@ -5,14 +5,12 @@ Software requirements --------------------- Corda uses industry-standard tools: -* **Oracle JDK 8 JVM** - supported version **8u131** +* **Oracle JDK 8 JVM** - minimum supported version **8u131** * **IntelliJ IDEA** - supported versions **2017.1**, **2017.2** and **2017.3** -* **Gradle** - supported version **3.4** -* **Kotlin** - supported version **1.1.2** * **Git** -You do not need to install Gradle or Kotlin. A standalone Gradle wrapper is provided, and it will download the correct -version of Kotlin. +We also use Gradle and Kotlin, but you do not need to install them. A standalone Gradle wrapper is provided, and it +will download the correct version of Kotlin. Please note: @@ -35,17 +33,25 @@ others to provide support. However, if you do use other tools, we'd be intereste Set-up instructions ------------------- -The instructions below will allow you to set up a Corda development environment and run a basic CorDapp on a Windows -or Mac machine. If you have any issues, please consult the :doc:`troubleshooting` page, or reach out on -`Slack `_ or the `forums `_. +The instructions below will allow you to set up a Corda development environment and run a basic CorDapp. If you have +any issues, please consult the :doc:`troubleshooting` page, or reach out on `Slack `_, +`Stack Overflow `_ or the `forums `_. -.. note:: The set-up instructions are also available in video form for both `Windows `_ and `Mac `_. +The set-up instructions are available for the following platforms: + +* :ref:`windows-label` (or `in video form `_) + +* :ref:`mac-label` (or `in video form `_) + +.. _windows-label: Windows -^^^^^^^ +------- + +.. warning:: If you are using a Mac machine, please follow the :ref:`mac-label` instructions instead. Java -~~~~ +^^^^ 1. Visit http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 2. Scroll down to "Java SE Development Kit 8uXXX" (where "XXX" is the latest minor version number) 3. Toggle "Accept License Agreement" @@ -54,34 +60,34 @@ Java 6. Open a new command prompt and run ``java -version`` to test that Java is installed correctly Git -~~~ +^^^ 1. Visit https://git-scm.com/download/win 2. Click the "64-bit Git for Windows Setup" download link. 3. Download and run the executable to install Git (use the default settings) 4. Open a new command prompt and type ``git --version`` to test that git is installed correctly IntelliJ -~~~~~~~~ +^^^^^^^^ 1. Visit https://www.jetbrains.com/idea/download/download-thanks.html?code=IIC 2. Download and run the executable to install IntelliJ Community Edition (use the default settings) Download a sample project -~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^ 1. Open a command prompt 2. Clone the CorDapp tutorial repo by running ``git clone https://github.com/corda/cordapp-tutorial`` 3. Move into the cordapp-tutorial folder by running ``cd cordapp-tutorial`` -4. Retrieve a list of all the milestone (i.e. stable) releases by running ``git branch -a --list *release-M*`` +4. Retrieve a list of all the releases by running ``git branch -a --list`` 5. Check out the latest milestone release by running ``git checkout release-MX`` (where "X" is the latest milestone) Run from the command prompt -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1. From the cordapp-tutorial folder, deploy the nodes by running ``gradlew deployNodes`` 2. Start the nodes by running ``call kotlin-source/build/nodes/runnodes.bat`` 3. Wait until all the terminal windows display either "Webserver started up in XX.X sec" or "Node for "NodeC" started up and registered in XX.XX sec" 4. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/ Run from IntelliJ -~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^ 1. Open IntelliJ Community Edition 2. On the splash screen, click "Open" (do NOT click "Import Project") and select the cordapp-tutorial folder @@ -94,11 +100,15 @@ Run from IntelliJ 7. Wait until the run windows displays the message "Webserver started up in XX.X sec" 8. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/ +.. _mac-label: + Mac -^^^ +--- + +.. warning:: If you are using a Windows machine, please follow the :ref:`windows-label` instructions instead. Java -~~~~ +^^^^ 1. Open "System Preferences > Java" 2. In the Java Control Panel, if an update is available, click "Update Now" 3. In the "Software Update" window, click "Install Update". If required, enter your password and click "Install Helper" when prompted @@ -106,27 +116,27 @@ Java 5. Open a new terminal and type ``java -version`` to test that Java is installed correctly IntelliJ -~~~~~~~~ +^^^^^^^^ 1. Visit https://www.jetbrains.com/idea/download/download-thanks.html?platform=mac&code=IIC 2. Download and run the executable to install IntelliJ Community Edition (use the default settings) Download a sample project -~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^ 1. Open a terminal 2. Clone the CorDapp tutorial repo by running ``git clone https://github.com/corda/cordapp-tutorial`` 3. Move into the cordapp-tutorial folder by running ``cd cordapp-tutorial`` -4. Retrieve a list of all the milestone (i.e. stable) releases by running ``git branch -a --list *release-M*`` +4. Retrieve a list of all the releases by running ``git branch -a --list`` 5. Check out the latest milestone release by running ``git checkout release-MX`` (where "X" is the latest milestone) Run from the terminal -~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^ 1. From the cordapp-tutorial folder, deploy the nodes by running ``./gradlew deployNodes`` 2. Start the nodes by running ``kotlin-source/build/nodes/runnodes``. Do not click while 8 additional terminal windows start up. 3. Wait until all the terminal windows display either "Webserver started up in XX.X sec" or "Node for "NodeC" started up and registered in XX.XX sec" 4. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/ Run from IntelliJ -~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^ 1. Open IntelliJ Community Edition 2. On the splash screen, click "Open" (do NOT click "Import Project") and select the cordapp-tutorial folder 3. Once the project is open, click "File > Project Structure". Under "Project SDK:", set the project SDK by clicking "New...", clicking "JDK", and navigating to /Library/Java/JavaVirtualMachines/jdk1.8.0_XXX (where "XXX" is the latest minor version number). Click "OK". diff --git a/docs/source/hello-world-contract.rst b/docs/source/hello-world-contract.rst index c54e5cf6da..d5e4de152f 100644 --- a/docs/source/hello-world-contract.rst +++ b/docs/source/hello-world-contract.rst @@ -114,7 +114,7 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi package com.template.contract; import com.template.state.IOUState; - import net.corda.core.contracts.AuthenticatedObject; + import net.corda.core.contracts.CommandWithParties; import net.corda.core.contracts.CommandData; import net.corda.core.contracts.Contract; import net.corda.core.transactions.LedgerTransaction; @@ -130,7 +130,7 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi @Override public void verify(LedgerTransaction tx) { - final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); + final CommandWithParties command = requireSingleCommand(tx.getCommands(), Create.class); requireThat(check -> { // Constraints on the shape of the transaction. diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst index 1adb34a1e5..229127b4e4 100644 --- a/docs/source/hello-world-flow.rst +++ b/docs/source/hello-world-flow.rst @@ -24,7 +24,7 @@ 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 steps 5, 6 and 7 using ``FinalityFlow``. +In our initiator flow, we can automate steps 4 and 5 using ``FinalityFlow``. All we need to do is write the steps to handle the creation and signing of the proposed transaction. @@ -162,7 +162,7 @@ We now have our own ``FlowLogic`` subclass that overrides ``FlowLogic.call``. Th * 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 + * ``@StartableByRPC`` allows the node owner to start this flow via an RPC call * We override the progress tracker, even though we are not providing any progress tracker steps yet. The progress tracker is required for the node shell to establish when the flow has ended @@ -204,7 +204,7 @@ the following transaction: So we'll need the following: * The output ``IOUState`` -* A ``Create`` command listing the IOU's borrower as a signer +* A ``Create`` command listing the IOU's lender as a signer 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. diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 78788090c3..2e15aaf74e 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -25,9 +25,9 @@ Let's take a look at the nodes we're going to deploy. Open the project's ``build task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK" + networkMap "O=Controller,OU=corda,L=London,C=UK" node { - name "CN=Controller,O=R3,OU=corda,L=London,C=UK" + name "O=Controller,OU=corda,L=London,C=UK" advertisedServices = ["corda.notary.validating"] p2pPort 10002 rpcPort 10003 @@ -156,7 +156,7 @@ The vaults of Node A and Node B should both display the following output: participants: - "CN=NodeA,O=NodeA,L=London,C=GB" - "CN=NodeB,O=NodeB,L=New York,C=US" - notary: "CN=Controller,O=R3,OU=corda,L=London,C=GB,OU=corda.notary.validating" + notary: "O=Controller,OU=corda,L=London,C=GB,OU=corda.notary.validating" encumbrance: null ref: txhash: "656A1BF64D5AEEC6F6C944E287F34EF133336F5FC2C5BFB9A0BFAE25E826125F" diff --git a/docs/source/release-process-index.rst b/docs/source/release-process-index.rst index 9e5276f786..1b59977668 100644 --- a/docs/source/release-process-index.rst +++ b/docs/source/release-process-index.rst @@ -6,4 +6,5 @@ Release process release-notes changelog - codestyle \ No newline at end of file + codestyle + testing \ No newline at end of file diff --git a/docs/source/shell.rst b/docs/source/shell.rst index dc08c27cb8..8cc68a4313 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -68,7 +68,7 @@ Yaml (yet another markup language) is a simple JSON-like way to describe object that make it helpful for our use case, like a lightweight syntax and support for "bare words" which mean you can often skip the quotes around strings. Here is an example of how this syntax is used: -``flow start CashIssue amount: $1000, issueRef: 1234, recipient: "CN=Bank A,O=Bank A,L=London,C=GB", notary: "CN=Notary Service,O=R3,OU=corda,L=London,C=GB"`` +``flow start CashIssue amount: $1000, issueRef: 1234, recipient: "O=Bank A,L=London,C=GB", notary: "O=Notary Service,OU=corda,L=London,C=GB"`` This invokes a constructor of a flow with the following prototype in the code: diff --git a/docs/source/testing.rst b/docs/source/testing.rst new file mode 100644 index 0000000000..c082361d2d --- /dev/null +++ b/docs/source/testing.rst @@ -0,0 +1,41 @@ +Testing Corda +============= + +Automated Tests +--------------- + +Corda has a maintained suite of tests that any contributing developers must maintain and add to if new code has been added. + +There are several distinct test suites each with a different purpose; + +**Unit tests**: These are traditional unit tests that should only test a single code unit, typically a method or class. + +**Integration tests**: These tests should test the integration of small numbers of units, preferably with mocked out services. + +**Smoke tests**: These are full end to end tests which start a full set of Corda nodes and verify broader behaviour. + +**Other**: These include tests such as performance tests, stress tests, etc, and may be in an external repo. + +These tests are mostly written with JUnit and can be run via ``gradle``. On windows run ``gradlew test integrationTest +smokeTest`` and on unix run ``./gradlw test integrationTest smokeTest`` or any combination of these three arguments. + +Before creating a pull request please make sure these pass. + +Manual Testing +-------------- + +Manual testing would ideally be done for every set of changes merged into master, but practically you should manually test +anything that would be impacted by your changes. The areas that usually need to be manually tested and when are below; + +**Node startup** - changes in the ``node`` or ``node:capsule`` project in both the Kotlin or gradle or the ``cordformation`` gradle plugin. + +**Sample project** - changes in the ``samples`` project. eg; changing the IRS demo means you should manually test the IRS demo. + +**Explorer** - changes to the ``tools/explorer`` project. + +**Demobench** - changes to the ``tools/demobench`` project. + +How to manually test each of these areas differs and is currently not fully specified. For now the best thing to do is +ensure the program starts, that you can interact with it, and that no exceptions are generated in normal operation. + +TODO: Add instructions on manual testing \ No newline at end of file diff --git a/docs/source/tut-two-party-introduction.rst b/docs/source/tut-two-party-introduction.rst index fb99b07af0..e49de7dd9e 100644 --- a/docs/source/tut-two-party-introduction.rst +++ b/docs/source/tut-two-party-introduction.rst @@ -2,8 +2,9 @@ Introduction ============ .. note:: This tutorial extends the CorDapp built during the :doc:`Hello, World tutorial `. You can - find the final version of the CorDapp produced in that tutorial - `here `_. + download the final version of the CorDapp produced in that tutorial for + `Java `_ or + `Kotlin `_. In the Hello, World tutorial, we built a CorDapp allowing us to model IOUs on ledger. Our CorDapp was made up of three elements: @@ -22,4 +23,4 @@ IOU onto the ledger. We'll need to make two changes: signature (as well as the lender's) to become valid ledger updates * The ``IOUFlow`` will need to be updated to allow for the gathering of the borrower's signature -We'll start by updating the contract. \ No newline at end of file +We'll start by updating the contract. diff --git a/docs/source/tutorial-contract-clauses.rst b/docs/source/tutorial-contract-clauses.rst index 97ad1ba675..6ddb5035af 100644 --- a/docs/source/tutorial-contract-clauses.rst +++ b/docs/source/tutorial-contract-clauses.rst @@ -124,7 +124,7 @@ and is included in the ``CommercialPaper.kt`` code. override fun verify(tx: LedgerTransaction, inputs: List, outputs: List, - commands: List>, + commands: List>, groupingKey: Issued?): Set { val command = commands.requireSingleCommand() val input = inputs.single() @@ -154,9 +154,9 @@ and is included in the ``CommercialPaper.kt`` code. public Set verify(@NotNull LedgerTransaction tx, @NotNull List inputs, @NotNull List outputs, - @NotNull List> commands, + @NotNull List> commands, @NotNull State groupingKey) { - AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class); + CommandWithParties cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class); // There should be only a single input due to aggregation above State input = single(inputs); diff --git a/docs/source/tutorial-contract.rst b/docs/source/tutorial-contract.rst index 1cffea1982..3768c34fc9 100644 --- a/docs/source/tutorial-contract.rst +++ b/docs/source/tutorial-contract.rst @@ -303,7 +303,7 @@ run two contracts one time each: Cash and CommercialPaper. @Override public void verify(LedgerTransaction tx) { List> groups = tx.groupStates(State.class, State::withoutOwner); - AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.class); + CommandWithParties cmd = requireSingleCommand(tx.getCommands(), Commands.class); We start by using the ``groupStates`` method, which takes a type and a function. State grouping is a way of ensuring your contract can handle multiple unrelated states of the same type in the same transaction, which is needed for diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index 7b96013513..9b1b965941 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -279,11 +279,11 @@ and adds an RPC user for all but the "Controller" node (which serves as the nota // No permissions required as we are not invoking flows. val user = User("user1", "test", permissions = setOf()) driver(isDebug = true) { - startNode(X500Name("CN=Controller,O=R3,OU=corda,L=London,C=UK"), setOf(ServiceInfo(ValidatingNotaryService.type))) + startNode(getX500Name(O="Controller",OU="corda",L="London",C='UK"), setOf(ServiceInfo(ValidatingNotaryService.type))) val (nodeA, nodeB, nodeC) = Futures.allAsList( - startNode(X500Name("CN=NodeA,O=NodeA,L=London,C=UK"), rpcUsers = listOf(user)), - startNode(X500Name("CN=NodeB,O=NodeB,L=New York,C=US"), rpcUsers = listOf(user)), - startNode(X500Name("CN=NodeC,O=NodeC,L=Paris,C=FR"), rpcUsers = listOf(user))).getOrThrow() + startNode(getX500Name(O="NodeA",L="London",C="UK"), rpcUsers = listOf(user)), + startNode(getX500Name(O="NodeB",L="New York",C="US"), rpcUsers = listOf(user)), + startNode(getX500Name(O="NodeC",L="Paris",C="FR"), rpcUsers = listOf(user))).getOrThrow() startWebserver(nodeA) startWebserver(nodeB) @@ -454,7 +454,7 @@ We can see a list of the states in our node's vault using ``run vaultAndUpdates` participants: - "CN=NodeB,O=NodeB,L=New York,C=US" - "CN=NodeA,O=NodeA,L=London,C=UK" - notary: "CN=Controller,O=R3,OU=corda,L=London,C=UK,OU=corda.notary.validating" + notary: "O=Controller,OU=corda,L=London,C=UK,OU=corda.notary.validating" encumbrance: null ref: txhash: "52A1B18E6ABD535EF36B2075469B01D2EF888034F721C4BECD26F40355C8C9DC" @@ -488,14 +488,14 @@ abbreviated the output below): participants: - "CN=NodeB,O=NodeB,L=New York,C=US" - "CN=NodeA,O=NodeA,L=London,C=UK" - notary: "CN=Controller,O=R3,OU=corda,L=London,C=UK,OU=corda.notary.validating" + notary: "O=Controller,OU=corda,L=London,C=UK,OU=corda.notary.validating" encumbrance: null commands: - value: {} signers: - "8Kqd4oWdx4KQAVc3u5qvHZTGJxMtrShFudAzLUTdZUzbF9aPQcCZD5KXViC" - "8Kqd4oWdx4KQAVcBx98LBHwXwC3a7hNptQomrg9mq2ScY7t1Qqsyk5dCNAr" - notary: "CN=Controller,O=R3,OU=corda,L=London,C=UK,OU=corda.notary.validating" + notary: "O=Controller,OU=corda,L=London,C=UK,OU=corda.notary.validating" type: {} timeWindow: null mustSign: @@ -531,7 +531,7 @@ Green Arrow to run the client. You can edit the run configuration to connect on Run the following gradle task: -``./gradlew runExampleClientRPC localhost:10007`` +``./gradlew runExampleClientRPCKotlin localhost:10007`` You can close the application using ``ctrl+C``. @@ -578,11 +578,11 @@ Debugging is done via IntelliJ as follows: // No permissions required as we are not invoking flows. val user = User("user1", "test", permissions = setOf()) driver(isDebug = true) { - startNode(X500Name("CN=Controller,O=R3,OU=corda,L=London,C=UK"), setOf(ServiceInfo(ValidatingNotaryService.type))) + startNode(getX500Name(O="Controller",OU="corda",L="London",C="UK"), setOf(ServiceInfo(ValidatingNotaryService.type))) val (nodeA, nodeB, nodeC) = Futures.allAsList( - startNode(X500Name("CN=NodeA,O=NodeA,L=London,C=UK"), rpcUsers = listOf(user)), - startNode(X500Name("CN=NodeB,O=NodeB,L=New York,C=US"), rpcUsers = listOf(user)), - startNode(X500Name("CN=NodeC,O=NodeC,L=Paris,C=FR"), rpcUsers = listOf(user))).getOrThrow() + startNode(getX500Name(O="NodeA",L=London,C=UK"), rpcUsers = listOf(user)), + startNode(getX500Name(O="NodeB",L=New York,C=US"), rpcUsers = listOf(user)), + startNode(getX500Name(O="NodeC",L=Paris,C=FR"), rpcUsers = listOf(user))).getOrThrow() startWebserver(nodeA) startWebserver(nodeB) diff --git a/docs/source/tutorial-tear-offs.rst b/docs/source/tutorial-tear-offs.rst index 4e247f733d..5187aaac42 100644 --- a/docs/source/tutorial-tear-offs.rst +++ b/docs/source/tutorial-tear-offs.rst @@ -45,7 +45,7 @@ In the Oracle example this step takes place in ``RatesFixFlow`` by overriding `` // Direct accsess to included commands, inputs, outputs, attachments etc. val cmds: List = ftx.filteredLeaves.commands val ins: List = ftx.filteredLeaves.inputs - val timestamp: Timestamp? = ftx.filteredLeaves.timestamp + val timeWindow: TimeWindow? = ftx.filteredLeaves.timeWindow ... .. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt diff --git a/docs/source/using-a-notary.rst b/docs/source/using-a-notary.rst index 2c9f5c2fef..3f4bddd7b9 100644 --- a/docs/source/using-a-notary.rst +++ b/docs/source/using-a-notary.rst @@ -33,7 +33,7 @@ The first step is to obtain the notary identity -- ``Party``: .. sourcecode:: kotlin - val ourNotary: Party = serviceHub.networkMapCache.getNotary("Central Bank Notary") + val ourNotary: Party = serviceHub.networkMapCache.getNotary("Notary A") Then we initialise the transaction builder: @@ -86,7 +86,7 @@ we just use a helper that handles it for us. We also assume that we already have .. sourcecode:: kotlin - val inputState = StateAndRef(sate, stateRef) + val inputState = StateAndRef(state, stateRef) val moveTransactionBuilder = DummyContract.withNewOwnerAndAmount(inputState, newOwner = aliceParty.owningKey) The ``DummyContract.withNewOwnerAndAmount()`` method will a new transaction builder with our old state as the input, a new state diff --git a/experimental/build.gradle b/experimental/build.gradle index 42acd2995d..c5a39773ee 100644 --- a/experimental/build.gradle +++ b/experimental/build.gradle @@ -10,9 +10,6 @@ repositories { url 'http://oss.sonatype.org/content/repositories/snapshots' } jcenter() - maven { - url 'https://dl.bintray.com/kotlin/exposed' - } } compileKotlin { @@ -29,6 +26,8 @@ dependencies { // ObjectWeb Asm: a library for synthesising and working with JVM bytecode. compile "org.ow2.asm:asm:5.0.4" + compile "com.google.guava:guava:$guava_version" + testCompile "junit:junit:$junit_version" - testCompile project(':test-utils') + testCompile project(':node-driver') } diff --git a/experimental/quasar-hook/README.md b/experimental/quasar-hook/README.md index 43a253a5af..b0b1de0c99 100644 --- a/experimental/quasar-hook/README.md +++ b/experimental/quasar-hook/README.md @@ -5,17 +5,30 @@ This is a javaagent that may be used while running applications using quasar. It methods are scanned, instrumented and used at runtime, and generates an exclude pattern that may be passed in to quasar to stop it from scanning classes unnecessarily. -Example usage -============= +Arguments +=== + +`expand`, `alwaysExcluded` and `truncate` tweak the output exclude pattern. `expand` is a list of packages to always expand (for example +instead of generating `com.*` generate `com.google.*,com.typesafe.*` etc.), `alwaysExcluded` is a list of packages under +which all classes are considered excluded irregardless of instrumentation, `truncate` is a list of packages that should +not be included in the exclude pattern. Truncating `net.corda` means nothing should be excluded from instrumentation in +Corda. + +How to generate an exclude pattern for Corda +==== + +In order to generate a good exclude pattern we need to exercise the Corda code so that most classes get loaded and +inspected by quasar and quasar-hook. For this we can run tests using the 'Quasar exclude pattern extraction (...)' +intellij run configuration, which includes the hook. In addition we should run the tool on a bare corda.jar, as some +additional classes are used when the jar is invoked directly. To do this we'll use a node in samples: ``` ./gradlew experimental:quasar-hook:jar -java -javaagent:experimental/quasar-hook/build/libs/quasar-hook.jar="expand=com,de,org,co;truncate=net.corda" -jar path/to/corda.jar +./gradlew samples:irs-demo:deployNodes +cd samples/irs-demo/build/nodes/NotaryService +java -javaagent:../../../../../experimental/quasar-hook/build/libs/quasar-hook.jar=expand=com,de,org,co,io;truncate=net.corda;alwaysExcluded=com.opengamma,io.atomix -jar corda.jar ``` -The above will run corda.jar and on exit will print information about what classes were scanned/instrumented. +Once the node is started just exit the node. -`expand` and `truncate` tweak the output exclude pattern. `expand` is a list of packages to always expand (for example -instead of generating `com.*` generate `com.google.*,com.typesafe.*` etc.), `truncate` is a list of packages that should -not be included in the exclude pattern. Truncating `net.corda` means nothing should be excluded from instrumentation in -Corda. \ No newline at end of file +We can take the union of the two generated exclude patterns to get a final one. \ No newline at end of file diff --git a/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt b/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt index 960688379b..c32328ba4a 100644 --- a/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt +++ b/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt @@ -7,14 +7,15 @@ import java.lang.instrument.ClassFileTransformer import java.lang.instrument.Instrumentation import java.security.ProtectionDomain import java.util.* +import java.util.concurrent.ConcurrentHashMap /** * Used to collect classes through instrumentation. */ class ClassRecorder { - val usedInstrumentedClasses = HashSet() - val instrumentedClasses = HashSet() - val scannedClasses = HashSet() + val usedInstrumentedClasses = ConcurrentHashMap() + val instrumentedClasses = ConcurrentHashMap() + val scannedClasses = ConcurrentHashMap() } /** @@ -39,13 +40,12 @@ fun recordUsedInstrumentedCallStack() { index++ } index++ - while (true) { - require (index < throwable.stackTrace.size) { "Can't find Fiber call" } + while (index < throwable.stackTrace.size) { val stackElement = throwable.stackTrace[index] if (stackElement.className.startsWith("co.paralleluniverse")) { break } - classRecorder.usedInstrumentedClasses.add(stackElement.className) + classRecorder.usedInstrumentedClasses[stackElement.className] = Unit index++ } } @@ -55,7 +55,7 @@ fun recordUsedInstrumentedCallStack() { * instrumentation will happen. */ fun recordInstrumentedClass(className: String) { - classRecorder.instrumentedClasses.add(className) + classRecorder.instrumentedClasses[className] = Unit } /** @@ -63,7 +63,7 @@ fun recordInstrumentedClass(className: String) { */ fun recordScannedClass(className: String?) { if (className != null) { - classRecorder.scannedClasses.add(className) + classRecorder.scannedClasses[className] = Unit } } @@ -74,11 +74,13 @@ fun recordScannedClass(className: String?) { * @param expand A comma-separated list of packages to expand in the glob output. This is useful for certain top-level * domains that we don't want to completely exclude, because later on classes may be loaded from those namespaces * that require instrumentation. + * @param alwaysExcluded A comma-separated list of packages under which all touched classes will be excluded. * @param separator The package part separator character used in the above lists. */ data class Arguments( val truncate: List? = null, val expand: List? = null, + val alwaysExcluded: List? = null, val separator: Char = '.' ) @@ -98,6 +100,7 @@ class QuasarInstrumentationHookAgent { when (key) { "truncate" -> arguments = arguments.copy(truncate = value.split(",")) "expand" -> arguments = arguments.copy(expand = value.split(",")) + "alwaysExcluded" -> arguments = arguments.copy(alwaysExcluded = value.split(",")) "separator" -> arguments = arguments.copy(separator = value.toCharArray()[0]) } } @@ -113,19 +116,21 @@ class QuasarInstrumentationHookAgent { println(" $it") } println("Scanned classes: ${classRecorder.scannedClasses.size}") - classRecorder.scannedClasses.take(20).forEach { + classRecorder.scannedClasses.keys.take(20).forEach { println(" $it") } println(" (...)") - val scannedTree = PackageTree.fromStrings(classRecorder.scannedClasses.toList(), '/') - val instrumentedTree = PackageTree.fromStrings(classRecorder.instrumentedClasses.toList(), '/') + val scannedTree = PackageTree.fromStrings(classRecorder.scannedClasses.keys.toList(), '/') + val instrumentedTree = PackageTree.fromStrings(classRecorder.instrumentedClasses.keys.toList(), '/') + val alwaysExclude = arguments.alwaysExcluded?.let { PackageTree.fromStrings(it, arguments.separator) } + val alwaysExcludedTree = alwaysExclude?.let { instrumentedTree.truncate(it) } ?: instrumentedTree println("Suggested exclude globs:") val truncate = arguments.truncate?.let { PackageTree.fromStrings(it, arguments.separator) } // The separator append is a hack, it causes a package with an empty name to be added to the exclude tree, // which practically causes that level of the tree to be always expanded in the output globs. val expand = arguments.expand?.let { PackageTree.fromStrings(it.map { "$it${arguments.separator}" }, arguments.separator) } val truncatedTree = truncate?.let { scannedTree.truncate(it)} ?: scannedTree - val expandedTree = expand?.let { instrumentedTree.merge(it) } ?: instrumentedTree + val expandedTree = expand?.let { alwaysExcludedTree.merge(it) } ?: alwaysExcludedTree val globs = truncatedTree.toGlobs(expandedTree) globs.forEach { println(" $it") diff --git a/experimental/src/main/kotlin/net/corda/finance/contracts/universal/PrettyPrint.kt b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/PrettyPrint.kt index 615316a6bb..0db2bfc0ef 100644 --- a/experimental/src/main/kotlin/net/corda/finance/contracts/universal/PrettyPrint.kt +++ b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/PrettyPrint.kt @@ -1,6 +1,6 @@ package net.corda.finance.contracts.universal -import net.corda.core.crypto.commonName +import net.corda.core.utilities.organisation import net.corda.core.crypto.toStringShort import net.corda.core.identity.Party import java.math.BigDecimal @@ -48,7 +48,7 @@ private class PrettyPrint(arr : Arrangement) { fun createPartyName(party : Party) : String { - val parts = party.name.commonName.toLowerCase().split(' ') + val parts = party.name.organisation.toLowerCase().split(' ') var camelName = parts.drop(1).fold(parts.first()) { s, i -> s + i.first().toUpperCase() + i.drop(1) @@ -66,7 +66,7 @@ private class PrettyPrint(arr : Arrangement) { init { parties.forEach { - println( "val ${createPartyName(it)} = Party(\"${it.name.commonName}\", \"${it.owningKey.toStringShort()}\")" ) + println("val ${createPartyName(it)} = Party(\"${it.name.organisation}\", \"${it.owningKey.toStringShort()}\")") } } diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt index e4fa27ae78..91740eb915 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt @@ -169,8 +169,6 @@ class Cap { output { stateInitial } timeWindow(TEST_TX_TIME_1) - this `fails with` "transaction has a single command" - tweak { command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() } this `fails with` "the transaction is signed by all liable parties" diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt index 99d75414f9..908ef49061 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt @@ -56,8 +56,6 @@ class Caplet { output { stateStart } timeWindow(TEST_TX_TIME_1) - this `fails with` "transaction has a single command" - tweak { command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() } this `fails with` "the transaction is signed by all liable parties" diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt index df72fddfa5..4e76908c77 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt @@ -53,8 +53,6 @@ class FXFwdTimeOption output { inState } timeWindow(TEST_TX_TIME_1) - this `fails with` "transaction has a single command" - tweak { command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() } this `fails with` "the transaction is signed by all liable parties" diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt index ae9a4b822e..a34239017e 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt @@ -45,8 +45,6 @@ class FXSwap { output { inState } timeWindow(TEST_TX_TIME_1) - this `fails with` "transaction has a single command" - tweak { command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() } this `fails with` "the transaction is signed by all liable parties" diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt index e64850cf99..9427a9f087 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt @@ -136,8 +136,6 @@ class IRS { output { stateInitial } timeWindow(TEST_TX_TIME_1) - this `fails with` "transaction has a single command" - tweak { command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() } this `fails with` "the transaction is signed by all liable parties" diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt index 4db190640d..31d18cd810 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt @@ -145,8 +145,6 @@ class RollOutTests { output { stateStart } timeWindow(TEST_TX_TIME_1) - this `fails with` "transaction has a single command" - tweak { command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() } this `fails with` "the transaction is signed by all liable parties" diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt index 5a29086fae..270e807f84 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt @@ -62,8 +62,6 @@ class Swaption { output { stateInitial } timeWindow(TEST_TX_TIME_1) - this `fails with` "transaction has a single command" - tweak { command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() } this `fails with` "the transaction is signed by all liable parties" diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt index 7590505327..c208128996 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt @@ -48,12 +48,9 @@ class ZeroCouponBond { @Test fun `issue - signature`() { - transaction { output { inState } - this `fails with` "transaction has a single command" - tweak { command(acmeCorp.owningKey) { UniversalContract.Commands.Issue() } this `fails with` "the transaction is signed by all liable parties" diff --git a/finance/build.gradle b/finance/build.gradle index f4d43eeff7..e67a674738 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -15,8 +15,6 @@ dependencies { // and CorDapps using :finance features should use 'cordapp' not 'compile' linkage. cordaCompile project(':core') - compile "com.google.guava:guava:$guava_version" - testCompile project(':test-utils') testCompile project(path: ':core', configuration: 'testArtifacts') testCompile "junit:junit:$junit_version" diff --git a/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java index 2182999e56..0f2d081043 100644 --- a/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java @@ -1,11 +1,9 @@ package net.corda.finance.contracts; import co.paralleluniverse.fibers.Suspendable; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import kotlin.Unit; import net.corda.core.contracts.*; -import net.corda.core.crypto.testing.NullPublicKey; +import net.corda.core.crypto.NullKeys.NullPublicKey; import net.corda.core.identity.AbstractParty; import net.corda.core.identity.AnonymousParty; import net.corda.core.identity.Party; @@ -20,6 +18,7 @@ import org.jetbrains.annotations.Nullable; import java.time.Instant; import java.util.Collections; import java.util.Currency; +import java.util.Iterator; import java.util.List; import java.util.stream.Collectors; @@ -83,7 +82,7 @@ public class JavaCommercialPaper implements Contract { return owner; } - Amount> getFaceValue() { + public Amount> getFaceValue() { return faceValue; } @@ -128,7 +127,7 @@ public class JavaCommercialPaper implements Contract { @NotNull @Override public List getParticipants() { - return ImmutableList.of(this.owner); + return Collections.singletonList(this.owner); } } @@ -156,11 +155,11 @@ public class JavaCommercialPaper implements Contract { } @NotNull - private List> extractCommands(@NotNull LedgerTransaction tx) { + private List> extractCommands(@NotNull LedgerTransaction tx) { return tx.getCommands() .stream() - .filter((AuthenticatedObject command) -> command.getValue() instanceof Commands) - .map((AuthenticatedObject command) -> new AuthenticatedObject<>(command.getSigners(), command.getSigningParties(), (Commands) command.getValue())) + .filter((CommandWithParties command) -> command.getValue() instanceof Commands) + .map((CommandWithParties command) -> new CommandWithParties<>(command.getSigners(), command.getSigningParties(), (Commands) command.getValue())) .collect(Collectors.toList()); } @@ -172,19 +171,19 @@ public class JavaCommercialPaper implements Contract { // There are two possible things that can be done with this CP. The first is trading it. The second is redeeming // it for cash on or after the maturity date. - final List> commands = tx.getCommands().stream().filter( + final List> commands = tx.getCommands().stream().filter( it -> it.getValue() instanceof Commands ).collect(Collectors.toList()); - final AuthenticatedObject command = Iterables.getOnlyElement(commands); + final CommandWithParties command = onlyElementOf(commands); final TimeWindow timeWindow = tx.getTimeWindow(); for (final LedgerTransaction.InOutGroup group : groups) { final List inputs = group.getInputs(); final List outputs = group.getOutputs(); if (command.getValue() instanceof Commands.Move) { - final AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class); + final CommandWithParties cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class); // There should be only a single input due to aggregation above - final State input = Iterables.getOnlyElement(inputs); + final State input = onlyElementOf(inputs); if (!cmd.getSigners().contains(input.getOwner().getOwningKey())) throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); @@ -194,10 +193,10 @@ public class JavaCommercialPaper implements Contract { throw new IllegalStateException("the state is propagated"); } } else if (command.getValue() instanceof Commands.Redeem) { - final AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class); + final CommandWithParties cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class); // There should be only a single input due to aggregation above - final State input = Iterables.getOnlyElement(inputs); + final State input = onlyElementOf(inputs); if (!cmd.getSigners().contains(input.getOwner().getOwningKey())) throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); @@ -217,8 +216,8 @@ public class JavaCommercialPaper implements Contract { return Unit.INSTANCE; }); } else if (command.getValue() instanceof Commands.Issue) { - final AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class); - final State output = Iterables.getOnlyElement(outputs); + final CommandWithParties cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class); + final State output = onlyElementOf(outputs); final Instant time = null == timeWindow ? null : timeWindow.getUntilTime(); @@ -257,4 +256,13 @@ public class JavaCommercialPaper implements Contract { tx.addOutputState(new TransactionState<>(new State(paper.getState().getData().getIssuance(), newOwner, paper.getState().getData().getFaceValue(), paper.getState().getData().getMaturityDate()), paper.getState().getNotary(), paper.getState().getEncumbrance())); tx.addCommand(new Command<>(new Commands.Move(), paper.getState().getData().getOwner().getOwningKey())); } + + private static T onlyElementOf(Iterable iterable) { + Iterator iter = iterable.iterator(); + T item = iter.next(); + if (iter.hasNext()) { + throw new IllegalArgumentException("Iterable has more than one element!"); + } + return item; + } } diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt index c05cc175a6..8626da9034 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt @@ -2,8 +2,8 @@ package net.corda.finance.contracts import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* -import net.corda.core.crypto.testing.NULL_PARTY -import net.corda.core.crypto.toBase58String +import net.corda.core.crypto.NullKeys.NULL_PARTY +import net.corda.core.utilities.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.internal.Emoji @@ -40,10 +40,11 @@ import java.util.* * which may need to be tracked. That, in turn, requires validation logic (there is a bean validator that knows how * to do this in the Apache BVal project). */ -val CP_PROGRAM_ID = CommercialPaper() - // TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance. class CommercialPaper : Contract { + companion object { + val CP_PROGRAM_ID = CommercialPaper() + } data class State( val issuance: PartyAndReference, override val owner: AbstractParty, diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt b/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt index a2d4538aa7..f35b374f0f 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt @@ -83,7 +83,7 @@ data class Tenor(val name: String) { } // Move date to the closest business day when it falls on a weekend/holiday val adjustedMaturityDate = calendar.applyRollConvention(maturityDate, DateRollConvention.ModifiedFollowing) - val daysToMaturity = calculateDaysBetween(startDate, adjustedMaturityDate, DayCountBasisYear.Y360, DayCountBasisDay.DActual) + val daysToMaturity = BusinessCalendar.calculateDaysBetween(startDate, adjustedMaturityDate, DayCountBasisYear.Y360, DayCountBasisDay.DActual) return daysToMaturity } @@ -190,10 +190,6 @@ enum class Frequency(val annualCompoundCount: Int, val offset: LocalDate.(Long) Daily(365, { plusDays(1 * it) }); } - -@Suppress("unused") // This utility may be useful in future. TODO: Review before API stability guarantees in place. -fun LocalDate.isWorkingDay(accordingToCalendar: BusinessCalendar): Boolean = accordingToCalendar.isWorkingDay(this) - // TODO: Make Calendar data come from an oracle /** @@ -213,10 +209,26 @@ open class BusinessCalendar (val holidayDates: List) { it to BusinessCalendar::class.java.getResourceAsStream("${it}HolidayCalendar.txt").bufferedReader().readText() }.toMap() + @JvmStatic + fun calculateDaysBetween(startDate: LocalDate, + endDate: LocalDate, + dcbYear: DayCountBasisYear, + dcbDay: DayCountBasisDay): Int { + // Right now we are only considering Actual/360 and 30/360 .. We'll do the rest later. + // TODO: The rest. + return when { + dcbDay == DayCountBasisDay.DActual -> (endDate.toEpochDay() - startDate.toEpochDay()).toInt() + dcbDay == DayCountBasisDay.D30 && dcbYear == DayCountBasisYear.Y360 -> ((endDate.year - startDate.year) * 360.0 + (endDate.monthValue - startDate.monthValue) * 30.0 + endDate.dayOfMonth - startDate.dayOfMonth).toInt() + else -> TODO("Can't calculate days using convention $dcbDay / $dcbYear") + } + } + /** Parses a date of the form YYYY-MM-DD, like 2016-01-10 for 10th Jan. */ + @JvmStatic fun parseDateFromString(it: String): LocalDate = LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE) /** Returns a business calendar that combines all the named holiday calendars into one list of holiday dates. */ + @JvmStatic fun getInstance(vararg calname: String) = BusinessCalendar( calname.flatMap { (TEST_CALENDAR_DATA[it] ?: throw UnknownCalendar(it)).split(",") }. toSet(). @@ -225,6 +237,7 @@ open class BusinessCalendar (val holidayDates: List) { ) /** Calculates an event schedule that moves events around to ensure they fall on working days. */ + @JvmStatic fun createGenericSchedule(startDate: LocalDate, period: Frequency, calendar: BusinessCalendar = getInstance(), @@ -248,9 +261,11 @@ open class BusinessCalendar (val holidayDates: List) { return ret } - /** Calculates the date from @startDate moving forward @steps of time size @period. Does not apply calendar + /** + * Calculates the date from @startDate moving forward 'steps' of time size 'period'. Does not apply calendar * logic / roll conventions. */ + @JvmStatic fun getOffsetDate(startDate: LocalDate, period: Frequency, steps: Int = 1): LocalDate { if (steps == 0) return startDate return period.offset(startDate, steps.toLong()) @@ -316,19 +331,6 @@ open class BusinessCalendar (val holidayDates: List) { } } -fun calculateDaysBetween(startDate: LocalDate, - endDate: LocalDate, - dcbYear: DayCountBasisYear, - dcbDay: DayCountBasisDay): Int { - // Right now we are only considering Actual/360 and 30/360 .. We'll do the rest later. - // TODO: The rest. - return when { - dcbDay == DayCountBasisDay.DActual -> (endDate.toEpochDay() - startDate.toEpochDay()).toInt() - dcbDay == DayCountBasisDay.D30 && dcbYear == DayCountBasisYear.Y360 -> ((endDate.year - startDate.year) * 360.0 + (endDate.monthValue - startDate.monthValue) * 30.0 + endDate.dayOfMonth - startDate.dayOfMonth).toInt() - else -> TODO("Can't calculate days using convention $dcbDay / $dcbYear") - } -} - /** A common netting command for contracts whose states can be netted. */ interface NetCommand : CommandData { /** The type of netting to apply, see [NetType] for options. */ diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt index 792e8e6fdb..366b5ee666 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt @@ -1,13 +1,13 @@ -@file:JvmName("CashUtilities") // So the static extension functions get put into a class with a better name than CashKt +@file:JvmName("CashUtilities") + +// So the static extension functions get put into a class with a better name than CashKt package net.corda.finance.contracts.asset import co.paralleluniverse.fibers.Suspendable -import net.corda.finance.contracts.asset.cash.selection.CashSelectionH2Impl import net.corda.core.contracts.* import net.corda.core.contracts.Amount.Companion.sumOrThrow +import net.corda.core.crypto.NullKeys.NULL_PARTY import net.corda.core.crypto.entropyToKeyPair -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.internal.Emoji @@ -18,11 +18,13 @@ import net.corda.core.schemas.QueryableState import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getX500Name +import net.corda.core.utilities.toBase58String +import net.corda.finance.contracts.asset.cash.selection.CashSelectionH2Impl import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.utils.sumCash import net.corda.finance.utils.sumCashOrNull import net.corda.finance.utils.sumCashOrZero -import org.bouncycastle.asn1.x500.X500Name import java.math.BigInteger import java.security.PublicKey import java.sql.DatabaseMetaData @@ -56,7 +58,7 @@ interface CashSelection { instance.set(cashSelectionAlgo) cashSelectionAlgo } ?: throw ClassNotFoundException("\nUnable to load compatible cash selection algorithm implementation for JDBC driver ($_metadata)." + - "\nPlease specify an implementation in META-INF/services/net.corda.finance.contracts.asset.CashSelection") + "\nPlease specify an implementation in META-INF/services/net.corda.finance.contracts.asset.CashSelection") }.invoke() } } @@ -106,7 +108,7 @@ interface CashSelection { * vaults can ignore the issuer/depositRefs and just examine the amount fields. */ class Cash : OnLedgerAsset() { - override fun extractCommands(commands: Collection>): List> + override fun extractCommands(commands: Collection>): List> = commands.select() // DOCSTART 1 @@ -238,7 +240,7 @@ class Cash : OnLedgerAsset() { private fun verifyIssueCommand(inputs: List, outputs: List, tx: LedgerTransaction, - issueCommand: AuthenticatedObject, + issueCommand: CommandWithParties, currency: Currency, issuer: PartyAndReference) { // If we have an issue command, perform special processing: the group is allowed to have no inputs, @@ -345,7 +347,7 @@ class Cash : OnLedgerAsset() { /** A randomly generated key. */ val DUMMY_CASH_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) } /** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */ -val DUMMY_CASH_ISSUER by lazy { Party(X500Name("CN=Snake Oil Issuer,O=R3,OU=corda,L=London,C=GB"), DUMMY_CASH_ISSUER_KEY.public).ref(1) } +val DUMMY_CASH_ISSUER by lazy { Party(getX500Name(O = "Snake Oil Issuer", OU = "corda", L = "London", C = "GB"), DUMMY_CASH_ISSUER_KEY.public).ref(1) } /** An extension property that lets you write 100.DOLLARS.CASH */ val Amount.CASH: Cash.State get() = Cash.State(Amount(quantity, Issued(DUMMY_CASH_ISSUER, token)), NULL_PARTY) /** An extension property that lets you get a cash state from an issued token, under the [NULL_PARTY] */ diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt index 2ce6cc0855..dac289882d 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt @@ -19,9 +19,6 @@ import java.util.* // Commodity // -// Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode. -val COMMODITY_PROGRAM_ID = CommodityContract() - /** * A commodity contract represents an amount of some commodity, tracked on a distributed ledger. The design of this * contract is intentionally similar to the [Cash] contract, and the same commands (issue, move, exit) apply, the @@ -34,6 +31,11 @@ val COMMODITY_PROGRAM_ID = CommodityContract() */ // TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc. class CommodityContract : OnLedgerAsset() { + companion object { + // Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode. + val COMMODITY_PROGRAM_ID = CommodityContract() + } + /** A state representing a commodity claim against some party */ data class State( override val amount: Amount>, @@ -121,7 +123,7 @@ class CommodityContract : OnLedgerAsset, outputs: List, tx: LedgerTransaction, - issueCommand: AuthenticatedObject, + issueCommand: CommandWithParties, commodity: Commodity, issuer: PartyAndReference) { // If we have an issue command, perform special processing: the group is allowed to have no inputs, @@ -145,7 +147,7 @@ class CommodityContract : OnLedgerAsset>): List> + override fun extractCommands(commands: Collection>): List> = commands.select() /** diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt index e41b8c6389..b7968bc755 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt @@ -1,9 +1,5 @@ package net.corda.finance.contracts.asset -import net.corda.finance.contracts.NetCommand -import net.corda.finance.contracts.NetType -import net.corda.finance.contracts.NettableState -import net.corda.finance.contracts.asset.Obligation.Lifecycle.NORMAL import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair @@ -16,7 +12,12 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NonEmptySet +import net.corda.core.utilities.getX500Name import net.corda.core.utilities.seconds +import net.corda.finance.contracts.NetCommand +import net.corda.finance.contracts.NetType +import net.corda.finance.contracts.NettableState +import net.corda.finance.contracts.asset.Obligation.Lifecycle.NORMAL import net.corda.finance.utils.sumFungibleOrNull import net.corda.finance.utils.sumObligations import net.corda.finance.utils.sumObligationsOrNull @@ -283,7 +284,7 @@ class Obligation

: Contract { private fun verifyIssueCommand(tx: LedgerTransaction, inputs: List>>, outputs: List>>, - issueCommand: AuthenticatedObject, + issueCommand: CommandWithParties, key: Issued>) { // If we have an issue command, perform special processing: the group is allowed to have no inputs, // and the output states must have a deposit reference owned by the signer. @@ -311,7 +312,7 @@ class Obligation

: Contract { private fun verifySettleCommand(tx: LedgerTransaction, inputs: List>>, outputs: List>>, - command: AuthenticatedObject>, + command: CommandWithParties>, groupingKey: Issued>) { val obligor = groupingKey.issuer.party val template = groupingKey.product @@ -394,7 +395,7 @@ class Obligation

: Contract { } } - private fun verifyNetCommand(tx: LedgerTransaction, command: AuthenticatedObject) { + private fun verifyNetCommand(tx: LedgerTransaction, command: CommandWithParties) { val groups = when (command.value.type) { NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State

-> it.bilateralNetState } NetType.PAYMENT -> tx.groupStates { it: Obligation.State

-> it.multilateralNetState } @@ -434,7 +435,7 @@ class Obligation

: Contract { private fun verifySetLifecycleCommand(inputs: List>>, outputs: List>>, tx: LedgerTransaction, - setLifecycleCommand: AuthenticatedObject) { + setLifecycleCommand: CommandWithParties) { // Default must not change anything except lifecycle, so number of inputs and outputs must match // exactly. require(inputs.size == outputs.size) { "Number of inputs and outputs must match" } @@ -793,4 +794,4 @@ infix fun Obligation.State.`issued by`(party: AbstractParty) = copy /** A randomly generated key. */ 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 DUMMY_OBLIGATION_ISSUER by lazy { Party(getX500Name(O = "Snake Oil Issuer", OU = "corda", L = "London", C = "GB"), DUMMY_OBLIGATION_ISSUER_KEY.public) } diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt index 7205ce5366..a1b5a7a1d9 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt @@ -282,7 +282,7 @@ abstract class OnLedgerAsset> : C } } - abstract fun extractCommands(commands: Collection>): Collection> + abstract fun extractCommands(commands: Collection>): Collection> /** * Generate an transaction exiting assets from the ledger. diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt index 0ef6053ec2..e402103630 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt @@ -7,7 +7,6 @@ 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.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.ServiceHub @@ -30,8 +29,8 @@ class CashSelectionH2Impl : CashSelection { val log = loggerFor() } - override fun isCompatible(metaData: DatabaseMetaData): Boolean { - return metaData.driverName == JDBC_DRIVER_NAME + override fun isCompatible(metadata: DatabaseMetaData): Boolean { + return metadata.driverName == JDBC_DRIVER_NAME } // coin selection retry loop counter, sleep (msecs) and lock for selecting states diff --git a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt index b408708da4..62245f11d0 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt @@ -4,11 +4,9 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.requireThat import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature -import net.corda.core.flows.CollectSignaturesFlow -import net.corda.core.flows.FinalityFlow -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.SignTransactionFlow +import net.corda.core.flows.* 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.node.services.ServiceType @@ -29,9 +27,14 @@ import java.security.PublicKey // TODO: Also, the term Deal is used here where we might prefer Agreement. // TODO: Make this flow more generic. object TwoPartyDealFlow { - // This object is serialised to the network and is the first flow message the seller sends to the buyer. + /** + * This object is serialised to the network and is the first flow message the seller sends to the buyer. + * + * @param primaryIdentity the (anonymised) identity of the participant that initiates communication/handshake. + * @param secondaryIdentity the (anonymised) identity of the participant that is recipient of initial communication. + */ @CordaSerializable - data class Handshake(val payload: T, val publicKey: PublicKey) + data class Handshake(val payload: T, val primaryIdentity: AnonymousParty, val secondaryIdentity: AnonymousParty) /** * Abstracted bilateral deal flow participant that initiates communication/handshake. @@ -39,19 +42,23 @@ object TwoPartyDealFlow { abstract class Primary(override val progressTracker: ProgressTracker = Primary.tracker()) : FlowLogic() { companion object { + object GENERATING_ID : ProgressTracker.Step("Generating anonymous identities") object SENDING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal.") - fun tracker() = ProgressTracker(SENDING_PROPOSAL) + fun tracker() = ProgressTracker(GENERATING_ID, SENDING_PROPOSAL) } abstract val payload: Any abstract val notaryNode: NodeInfo abstract val otherParty: Party - abstract val myKey: PublicKey @Suspendable override fun call(): SignedTransaction { + progressTracker.currentStep = GENERATING_ID + val txIdentities = subFlow(TransactionKeyFlow(otherParty)) + val anonymousMe = txIdentities.get(serviceHub.myInfo.legalIdentity) ?: serviceHub.myInfo.legalIdentity.anonymise() + val anonymousCounterparty = txIdentities.get(otherParty) ?: otherParty.anonymise() progressTracker.currentStep = SENDING_PROPOSAL // Make the first message we'll send to kick off the flow. - val hello = Handshake(payload, serviceHub.myInfo.legalIdentity.owningKey) + val hello = Handshake(payload, anonymousMe, anonymousCounterparty) // Wait for the FinalityFlow to finish on the other side and return the tx when it's available. send(otherParty, hello) @@ -105,7 +112,7 @@ object TwoPartyDealFlow { progressTracker.currentStep = COLLECTING_SIGNATURES // DOCSTART 1 - val stx = subFlow(CollectSignaturesFlow(ptx)) + val stx = subFlow(CollectSignaturesFlow(ptx, additionalSigningPubKeys)) // DOCEND 1 logger.trace { "Got signatures from other party, verifying ... " } @@ -138,7 +145,14 @@ object TwoPartyDealFlow { val handshake = receive>(otherParty) progressTracker.currentStep = VERIFYING - return handshake.unwrap { validateHandshake(it) } + return handshake.unwrap { + // Verify the transaction identities represent the correct parties + val wellKnownOtherParty = serviceHub.identityService.partyFromAnonymous(it.primaryIdentity) + val wellKnownMe = serviceHub.identityService.partyFromAnonymous(it.secondaryIdentity) + require(wellKnownOtherParty == otherParty) + require(wellKnownMe == serviceHub.myInfo.legalIdentity) + validateHandshake(it) + } } @Suspendable protected abstract fun validateHandshake(handshake: Handshake): Handshake @@ -153,9 +167,7 @@ object TwoPartyDealFlow { */ open class Instigator(override val otherParty: Party, override val payload: AutoOffer, - override val myKey: PublicKey, override val progressTracker: ProgressTracker = Primary.tracker()) : Primary() { - override val notaryNode: NodeInfo get() = serviceHub.networkMapCache.notaryNodes.filter { it.notaryIdentity == payload.notary }.single() diff --git a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt index 215946ae9b..bac6013f74 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt @@ -5,10 +5,10 @@ import net.corda.core.contracts.Amount import net.corda.core.contracts.OwnableState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.withoutIssuer +import net.corda.core.contracts.* import net.corda.core.flows.* -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.NodeInfo import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction @@ -48,18 +48,22 @@ object TwoPartyTradeFlow { override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName" } - // This object is serialised to the network and is the first flow message the seller sends to the buyer. + /** + * This object is serialised to the network and is the first flow message the seller sends to the buyer. + * + * @param payToIdentity anonymous identity of the seller, for payment to be sent to. + */ @CordaSerializable data class SellerTradeInfo( val price: Amount, - val sellerOwner: AbstractParty + val payToIdentity: PartyAndCertificate ) open class Seller(val otherParty: Party, val notaryNode: NodeInfo, val assetToSell: StateAndRef, val price: Amount, - val me: AbstractParty, + val me: PartyAndCertificate, override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic() { companion object { @@ -84,12 +88,26 @@ object TwoPartyTradeFlow { // SendTransactionFlow allows otherParty to access our data to resolve the transaction. subFlow(SendStateAndRefFlow(otherParty, listOf(assetToSell))) send(otherParty, hello) + // Verify and sign the transaction. progressTracker.currentStep = VERIFYING_AND_SIGNING + + // Sync identities to ensure we know all of the identities involved in the transaction we're about to + // be asked to sign + subFlow(IdentitySyncFlow.Receive(otherParty)) + // DOCSTART 5 val signTransactionFlow = object : SignTransactionFlow(otherParty, VERIFYING_AND_SIGNING.childProgressTracker()) { override fun checkTransaction(stx: SignedTransaction) { - if (stx.tx.outputStates.sumCashBy(me).withoutIssuer() != price) + // Verify that we know who all the participants in the transaction are + val states: Iterable = (stx.tx.inputs.map { serviceHub.loadState(it).data } + stx.tx.outputs.map { it.data }) + states.forEach { state -> + state.participants.forEach { anon -> + require(serviceHub.identityService.partyFromAnonymous(anon) != null) { "Transaction state ${state} involves unknown participant ${anon}" } + } + } + + if (stx.tx.outputStates.sumCashBy(me.party).withoutIssuer() != price) throw FlowException("Transaction is not sending us the right amount of cash") } } @@ -114,7 +132,9 @@ object TwoPartyTradeFlow { open class Buyer(val otherParty: Party, val notary: Party, val acceptablePrice: Amount, - val typeToBuy: Class) : FlowLogic() { + val typeToBuy: Class, + val anonymous: Boolean) : FlowLogic() { + constructor(otherParty: Party, notary: Party, acceptablePrice: Amount, typeToBuy: Class): this(otherParty, notary, acceptablePrice, typeToBuy, true) // DOCSTART 2 object RECEIVING : ProgressTracker.Step("Waiting for seller trading info") @@ -139,16 +159,27 @@ object TwoPartyTradeFlow { progressTracker.currentStep = RECEIVING val (assetForSale, tradeRequest) = receiveAndValidateTradeRequest() + // Create the identity we'll be paying to, and send the counterparty proof we own the identity + val buyerAnonymousIdentity = if (anonymous) + serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, false) + else + serviceHub.myInfo.legalIdentityAndCert + // Put together a proposed transaction that performs the trade, and sign it. progressTracker.currentStep = SIGNING - val (ptx, cashSigningPubKeys) = assembleSharedTX(assetForSale, tradeRequest) + val (ptx, cashSigningPubKeys) = assembleSharedTX(assetForSale, tradeRequest, buyerAnonymousIdentity) + // Now sign the transaction with whatever keys we need to move the cash. val partSignedTx = serviceHub.signInitialTransaction(ptx, cashSigningPubKeys) + // Sync up confidential identities in the transaction with our counterparty + subFlow(IdentitySyncFlow.Send(otherParty, ptx.toWireTransaction())) + // Send the signed transaction to the seller, who must then sign it themselves and commit // it to the ledger by sending it to the notary. progressTracker.currentStep = COLLECTING_SIGNATURES - val twiceSignedTx = subFlow(CollectSignaturesFlow(partSignedTx, COLLECTING_SIGNATURES.childProgressTracker())) + val twiceSignedTx = subFlow(CollectSignaturesFlow(partSignedTx, cashSigningPubKeys, COLLECTING_SIGNATURES.childProgressTracker())) + // Notarise and record the transaction. progressTracker.currentStep = RECORDING return subFlow(FinalityFlow(twiceSignedTx)).single() @@ -159,33 +190,40 @@ object TwoPartyTradeFlow { val assetForSale = subFlow(ReceiveStateAndRefFlow(otherParty)).single() return assetForSale to receive(otherParty).unwrap { progressTracker.currentStep = VERIFYING + // What is the seller trying to sell us? val asset = assetForSale.state.data val assetTypeName = asset.javaClass.name + + // The asset must either be owned by the well known identity of the counterparty, or we must be able to + // prove the owner is a confidential identity of the counterparty. + val assetForSaleIdentity = serviceHub.identityService.partyFromAnonymous(asset.owner) + require(assetForSaleIdentity == otherParty) + + // Register the identity we're about to send payment to. This shouldn't be the same as the asset owner + // identity, so that anonymity is enforced. + val wellKnownPayToIdentity = serviceHub.identityService.verifyAndRegisterIdentity(it.payToIdentity) + require(wellKnownPayToIdentity?.party == otherParty) { "Well known identity to pay to must match counterparty identity" } + if (it.price > acceptablePrice) throw UnacceptablePriceException(it.price) if (!typeToBuy.isInstance(asset)) throw AssetMismatchException(typeToBuy.name, assetTypeName) + it } } - @Suspendable - private fun assembleSharedTX(assetForSale: StateAndRef, tradeRequest: SellerTradeInfo): Pair> { + private fun assembleSharedTX(assetForSale: StateAndRef, tradeRequest: SellerTradeInfo, buyerAnonymousIdentity: PartyAndCertificate): SharedTx { val ptx = TransactionBuilder(notary) // Add input and output states for the movement of cash, by using the Cash contract to generate the states - val (tx, cashSigningPubKeys) = Cash.generateSpend(serviceHub, ptx, tradeRequest.price, tradeRequest.sellerOwner) + val (tx, cashSigningPubKeys) = Cash.generateSpend(serviceHub, ptx, tradeRequest.price, tradeRequest.payToIdentity.party) // Add inputs/outputs/a command for the movement of the asset. tx.addInputState(assetForSale) - // Just pick some new public key for now. This won't be linked with our identity in any way, which is what - // we want for privacy reasons: the key is here ONLY to manage and control ownership, it is not intended to - // reveal who the owner actually is. The key management service is expected to derive a unique key from some - // initial seed in order to provide privacy protection. - val freshKey = serviceHub.keyManagementService.freshKey() - val (command, state) = assetForSale.state.data.withNewOwner(AnonymousParty(freshKey)) + val (command, state) = assetForSale.state.data.withNewOwner(buyerAnonymousIdentity.party) tx.addOutputState(state, assetForSale.state.notary) tx.addCommand(command, assetForSale.state.data.owner.owningKey) @@ -193,8 +231,11 @@ object TwoPartyTradeFlow { // But it can't hurt to have one. val currentTime = serviceHub.clock.instant() tx.setTimeWindow(currentTime, 30.seconds) - return Pair(tx, cashSigningPubKeys) + + return SharedTx(tx, cashSigningPubKeys) } // DOCEND 1 + + data class SharedTx(val tx: TransactionBuilder, val cashSigningPubKeys: List) } } diff --git a/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java b/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java index e1f8d92f9c..fabf51ef34 100644 --- a/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java +++ b/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java @@ -1,14 +1,15 @@ package net.corda.finance.contracts.asset; -import kotlin.Unit; import net.corda.core.contracts.PartyAndReference; import net.corda.core.identity.AnonymousParty; import net.corda.core.utilities.OpaqueBytes; +import net.corda.testing.DummyCommandData; import org.junit.Test; import static net.corda.finance.CurrencyUtils.DOLLARS; import static net.corda.finance.CurrencyUtils.issuedBy; import static net.corda.testing.CoreTestUtils.*; +import static net.corda.testing.NodeTestUtils.*; /** * This is an incomplete Java replica of CashTests.kt to show how to use the Java test DSL @@ -21,43 +22,41 @@ public class CashTestsJava { @Test public void trivial() { - ledger(lg -> { - lg.transaction(tx -> { - tx.input(inState); - tx.failsWith("the amounts balance"); + transaction(tx -> { + tx.input(inState); - tx.tweak(tw -> { - tw.output(new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), new AnonymousParty(getMINI_CORP_PUBKEY()))); - return tw.failsWith("the amounts balance"); - }); - - tx.tweak(tw -> { - tw.output(outState); - // No command arguments - return tw.failsWith("required net.corda.finance.contracts.asset.Cash.Commands.Move command"); - }); - tx.tweak(tw -> { - tw.output(outState); - tw.command(getMINI_CORP_PUBKEY(), new Cash.Commands.Move()); - return tw.failsWith("the owning keys are a subset of the signing keys"); - }); - tx.tweak(tw -> { - tw.output(outState); - // issuedBy() can't be directly imported because it conflicts with other identically named functions - // with different overloads (for some reason). - tw.output(outState.issuedBy(getMINI_CORP())); - tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); - return tw.failsWith("at least one cash input"); - }); - - // Simple reallocation works. - return tx.tweak(tw -> { - tw.output(outState); - tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); - return tw.verifies(); - }); + tx.tweak(tw -> { + tw.output(new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), new AnonymousParty(getMINI_CORP_PUBKEY()))); + tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); + return tw.failsWith("the amounts balance"); + }); + + tx.tweak(tw -> { + tw.output(outState); + tw.command(getMEGA_CORP_PUBKEY(), DummyCommandData.INSTANCE); + // Invalid command + return tw.failsWith("required net.corda.finance.contracts.asset.Cash.Commands.Move command"); + }); + tx.tweak(tw -> { + tw.output(outState); + tw.command(getMINI_CORP_PUBKEY(), new Cash.Commands.Move()); + return tw.failsWith("the owning keys are a subset of the signing keys"); + }); + tx.tweak(tw -> { + tw.output(outState); + // issuedBy() can't be directly imported because it conflicts with other identically named functions + // with different overloads (for some reason). + tw.output(outState.issuedBy(getMINI_CORP())); + tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); + return tw.failsWith("at least one cash input"); + }); + + // Simple reallocation works. + return tx.tweak(tw -> { + tw.output(outState); + tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); + return tw.verifies(); }); - return Unit.INSTANCE; }); } } diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt index 7953afa693..e72638806f 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -15,7 +15,7 @@ import net.corda.finance.contracts.asset.* import net.corda.testing.* import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestDatabaseAndMockServices +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index 2f4227472c..4d7843f707 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -22,7 +22,7 @@ import net.corda.testing.* import net.corda.testing.contracts.DummyState import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestDatabaseAndMockServices +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices import org.junit.After import org.junit.Before import org.junit.Test @@ -85,15 +85,16 @@ class CashTests : TestDependencyInjectionBase() { fun trivial() { transaction { input { inState } - this `fails with` "the amounts balance" tweak { output { outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer) } + command(ALICE_PUBKEY) { Cash.Commands.Move() } this `fails with` "the amounts balance" } tweak { output { outState } - // No command arguments + command(ALICE_PUBKEY) { DummyCommandData } + // Invalid command this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command" } tweak { @@ -279,12 +280,14 @@ class CashTests : TestDependencyInjectionBase() { transaction { input { inState } input { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) } + command(ALICE_PUBKEY) { Cash.Commands.Move() } this `fails with` "zero sized inputs" } transaction { input { inState } output { inState } output { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) } + command(ALICE_PUBKEY) { Cash.Commands.Move() } this `fails with` "zero sized outputs" } } @@ -295,6 +298,7 @@ class CashTests : TestDependencyInjectionBase() { transaction { input { inState } output { outState `issued by` MINI_CORP } + command(ALICE_PUBKEY) { Cash.Commands.Move() } this `fails with` "the amounts balance" } // Can't change deposit reference when splitting. @@ -302,6 +306,7 @@ class CashTests : TestDependencyInjectionBase() { val splits2 = inState.amount.splitEvenly(2) input { inState } for (i in 0..1) output { outState.copy(amount = splits2[i]).editDepositRef(i.toByte()) } + command(ALICE_PUBKEY) { Cash.Commands.Move() } this `fails with` "the amounts balance" } // Can't mix currencies. @@ -309,6 +314,7 @@ class CashTests : TestDependencyInjectionBase() { input { inState } output { outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer) } output { outState.copy(amount = 200.POUNDS `issued by` defaultIssuer) } + command(ALICE_PUBKEY) { Cash.Commands.Move() } this `fails with` "the amounts balance" } transaction { @@ -320,6 +326,7 @@ class CashTests : TestDependencyInjectionBase() { ) } output { outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer) } + command(ALICE_PUBKEY) { Cash.Commands.Move() } this `fails with` "the amounts balance" } // Can't have superfluous input states from different issuers. @@ -335,6 +342,7 @@ class CashTests : TestDependencyInjectionBase() { input { inState } input { inState.editDepositRef(3) } output { outState.copy(amount = inState.amount * 2).editDepositRef(3) } + command(ALICE_PUBKEY) { Cash.Commands.Move() } this `fails with` "for reference [01]" } } @@ -404,6 +412,7 @@ class CashTests : TestDependencyInjectionBase() { // Gather 2000 dollars from two different issuers. input { inState } input { inState `issued by` MINI_CORP } + command(ALICE_PUBKEY) { Cash.Commands.Move() } // Can't merge them together. tweak { @@ -420,7 +429,6 @@ class CashTests : TestDependencyInjectionBase() { // This works. output { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) } output { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) `issued by` MINI_CORP } - command(ALICE_PUBKEY) { Cash.Commands.Move() } this.verifies() } } diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/DummyFungibleContract.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/DummyFungibleContract.kt index 9817e20a70..be5ff9a189 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/DummyFungibleContract.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/DummyFungibleContract.kt @@ -1,7 +1,7 @@ package net.corda.finance.contracts.asset import net.corda.core.contracts.* -import net.corda.core.crypto.toBase58String +import net.corda.core.utilities.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.internal.Emoji import net.corda.core.schemas.MappedSchema @@ -18,7 +18,7 @@ import java.security.PublicKey import java.util.* class DummyFungibleContract : OnLedgerAsset() { - override fun extractCommands(commands: Collection>): List> + override fun extractCommands(commands: Collection>): List> = commands.select() data class State( @@ -127,7 +127,7 @@ class DummyFungibleContract : OnLedgerAsset, outputs: List, tx: LedgerTransaction, - issueCommand: AuthenticatedObject, + issueCommand: CommandWithParties, currency: Currency, issuer: PartyAndReference) { // If we have an issue command, perform special processing: the group is allowed to have no inputs, diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index de7d0e68d5..7bd7fbe4e6 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -3,7 +3,7 @@ package net.corda.finance.contracts.asset import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 -import net.corda.core.crypto.testing.NULL_PARTY +import net.corda.core.crypto.NullKeys.NULL_PARTY import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.transactions.TransactionBuilder 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 59b495879e..235570cf74 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 @@ -71,7 +71,7 @@ class Cordformation implements Plugin { 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.')) { + if(it.group && it.group.contains('net.corda.')) { logger.warn("You appear to have included a Corda platform component ($it) using a 'compile' or 'runtime' dependency." + "This can cause node stability problems. Please use 'corda' instead." + "See http://docs.corda.net/cordapp-build-systems.html") 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 f530928695..c96a937fef 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 @@ -90,7 +90,7 @@ class Node extends CordformNode { def dirName try { X500Name x500Name = new X500Name(name) - dirName = x500Name.getRDNs(BCStyle.CN).getAt(0).getFirst().getValue().toString() + dirName = x500Name.getRDNs(BCStyle.O).getAt(0).getFirst().getValue().toString() } catch(IllegalArgumentException ignore) { // Can't parse as an X500 name, use the full string dirName = name 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 5083554337..6a4ffaf25a 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 @@ -14,7 +14,7 @@ class QuasarPlugin implements Plugin { // To add a local .jar dependency: // project.dependencies.add("quasar", project.files("${project.rootProject.projectDir}/lib/quasar.jar")) project.dependencies.add("quasar", "co.paralleluniverse:quasar-core:${project.rootProject.ext.quasar_version}:jdk8@jar") - project.dependencies.add("compile", project.configurations.getByName("quasar")) + project.dependencies.add("runtime", project.configurations.getByName("quasar")) project.tasks.withType(Test) { jvmArgs "-javaagent:${project.configurations.quasar.singleFile}" diff --git a/lib/quasar.jar b/lib/quasar.jar index 286f065e43..c9f0010e79 100644 Binary files a/lib/quasar.jar and b/lib/quasar.jar differ diff --git a/node-api/build.gradle b/node-api/build.gradle index a0844f5f13..88892c4eff 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -32,7 +32,7 @@ dependencies { compile "de.javakaffee:kryo-serializers:0.41" // For AMQP serialisation. - compile "org.apache.qpid:proton-j:0.19.0" + compile "org.apache.qpid:proton-j:0.21.0" // Unit testing helpers. testCompile "junit:junit:$junit_version" 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 f1ad6582a1..7c87c19cbb 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt @@ -1,6 +1,6 @@ package net.corda.nodeapi -import net.corda.core.crypto.toBase58String +import net.corda.core.utilities.toBase58String import net.corda.core.messaging.MessageRecipientGroup import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient 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 e3376e482c..93ba7b3a27 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt @@ -4,14 +4,6 @@ import net.corda.core.serialization.SerializationContext 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 -import net.corda.nodeapi.RPCApi.ServerToClient import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.management.CoreNotificationType @@ -20,41 +12,43 @@ import org.apache.activemq.artemis.reader.MessageUtil import rx.Notification import java.util.* +// The RPC protocol: +// +// The server consumes the queue "RPC_SERVER_QUEUE_NAME" and receives RPC requests (ClientToServer.RpcRequest) on it. +// When a client starts up it should create a queue for its inbound messages, this should be of the form +// "RPC_CLIENT_QUEUE_NAME_PREFIX.$username.$nonce". Each RPC request contains this address (in +// ClientToServer.RpcRequest.clientAddress), this is where the server will send the reply to the request as well as +// subsequent Observations rooted in the RPC. The requests/replies are muxed using a unique RpcRequestId generated by +// the client for each request. +// +// If an RPC reply's payload (ServerToClient.RpcReply.result) contains observables then the server will generate a +// unique ObservableId for each and serialise them in place of the observables themselves. Subsequently the client +// should be prepared to receive observations (ServerToClient.Observation), muxed by the relevant ObservableId. +// In addition each observation itself may contain further observables, this case should behave the same as before. +// +// Additionally the client may send ClientToServer.ObservablesClosed messages indicating that certain observables +// aren't consumed anymore, which should subsequently stop the stream from the server. Note that some observations may +// already be in flight when this is sent, the client should handle this gracefully. +// +// An example session: +// Client Server +// ----------RpcRequest(RID0)-----------> // Client makes RPC request with ID "RID0" +// <----RpcReply(RID0, Payload(OID0))---- // Server sends reply containing an observable with ID "OID0" +// <---------Observation(OID0)----------- // Server sends observation onto "OID0" +// <---Observation(OID0, Payload(OID1))-- // Server sends another observation, this time containing another observable +// <---------Observation(OID1)----------- // Observation onto new "OID1" +// <---------Observation(OID0)----------- +// -----ObservablesClosed(OID0, OID1)---> // Client indicates it stopped consuming the observables. +// <---------Observation(OID1)----------- // Observation was already in-flight before the previous message was processed +// (FIN) +// +// Note that multiple sessions like the above may interleave in an arbitrary fashion. +// +// Additionally the server may listen on client binding removals for cleanup using RPC_CLIENT_BINDING_REMOVALS. This +// requires the server to create a filter on the Artemis notification address using RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION + /** - * The RPC protocol: - * - * The server consumes the queue "[RPC_SERVER_QUEUE_NAME]" and receives RPC requests ([ClientToServer.RpcRequest]) on it. - * When a client starts up it should create a queue for its inbound messages, this should be of the form - * "[RPC_CLIENT_QUEUE_NAME_PREFIX].$username.$nonce". Each RPC request contains this address (in - * [ClientToServer.RpcRequest.clientAddress]), this is where the server will send the reply to the request as well as - * subsequent Observations rooted in the RPC. The requests/replies are muxed using a unique [RpcRequestId] generated by - * the client for each request. - * - * If an RPC reply's payload ([ServerToClient.RpcReply.result]) contains [Observable]s then the server will generate a - * unique [ObservableId] for each and serialise them in place of the [Observable]s themselves. Subsequently the client - * should be prepared to receive observations ([ServerToClient.Observation]), muxed by the relevant [ObservableId]. - * In addition each observation itself may contain further [Observable]s, this case should behave the same as before. - * - * Additionally the client may send [ClientToServer.ObservablesClosed] messages indicating that certain observables - * aren't consumed anymore, which should subsequently stop the stream from the server. Note that some observations may - * already be in flight when this is sent, the client should handle this gracefully. - * - * An example session: - * Client Server - * ----------RpcRequest(RID0)-----------> // Client makes RPC request with ID "RID0" - * <----RpcReply(RID0, Payload(OID0))---- // Server sends reply containing an observable with ID "OID0" - * <---------Observation(OID0)----------- // Server sends observation onto "OID0" - * <---Observation(OID0, Payload(OID1))-- // Server sends another observation, this time containing another observable - * <---------Observation(OID1)----------- // Observation onto new "OID1" - * <---------Observation(OID0)----------- - * -----ObservablesClosed(OID0, OID1)---> // Client indicates it stopped consuming the Observables. - * <---------Observation(OID1)----------- // Observation was already in-flight before the previous message was processed - * (FIN) - * - * Note that multiple sessions like the above may interleave in an arbitrary fashion. - * - * Additionally the server may listen on client binding removals for cleanup using [RPC_CLIENT_BINDING_REMOVALS]. This - * requires the server to create a filter on the artemis notification address using [RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION] + * Constants and data types used by the RPC API. */ object RPCApi { private val TAG_FIELD_NAME = "tag" @@ -62,10 +56,15 @@ object RPCApi { private val OBSERVABLE_ID_FIELD_NAME = "observable-id" private val METHOD_NAME_FIELD_NAME = "method-name" - val RPC_SERVER_QUEUE_NAME = "rpc.server" - val RPC_CLIENT_QUEUE_NAME_PREFIX = "rpc.client" - val RPC_CLIENT_BINDING_REMOVALS = "rpc.clientqueueremovals" - val RPC_CLIENT_BINDING_ADDITIONS = "rpc.clientqueueadditions" + /** Name of the Artemis queue on which the server receives RPC requests (as [ClientToServer.RpcRequest]). */ + const val RPC_SERVER_QUEUE_NAME = "rpc.server" + /** + * Prefix to Artemis queue names used by clients to receive communication back from a server. The full queue name + * should be of the form "rpc.client.<username>.<nonce>". + */ + const val RPC_CLIENT_QUEUE_NAME_PREFIX = "rpc.client" + const val RPC_CLIENT_BINDING_REMOVALS = "rpc.clientqueueremovals" + const val RPC_CLIENT_BINDING_ADDITIONS = "rpc.clientqueueadditions" val RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION = "${ManagementHelper.HDR_NOTIFICATION_TYPE} = '${CoreNotificationType.BINDING_REMOVED.name}' AND " + @@ -83,12 +82,23 @@ object RPCApi { return ByteArray(bodySize).apply { bodyBuffer.readBytes(this) } } + /** + * Message content types which can be sent from a Corda client to a server. + */ sealed class ClientToServer { private enum class Tag { RPC_REQUEST, OBSERVABLES_CLOSED } + /** + * Request to a server to trigger the specified method with the provided arguments. + * + * @param clientAddress return address to contact the client at. + * @param id a unique ID for the request, which the server will use to identify its response with. + * @param methodName name of the method (procedure) to be called. + * @param arguments arguments to pass to the method, if any. + */ data class RpcRequest( val clientAddress: SimpleString, val id: RpcRequestId, @@ -141,6 +151,9 @@ object RPCApi { } } + /** + * Message content types which can be sent from a Corda server back to a client. + */ sealed class ServerToClient { private enum class Tag { RPC_REPLY, @@ -149,6 +162,7 @@ object RPCApi { abstract fun writeToClientMessage(context: SerializationContext, message: ClientMessage) + /** Reply in response to an [ClientToServer.RpcRequest]. */ data class RpcReply( val id: RpcRequestId, val result: Try @@ -156,7 +170,7 @@ object RPCApi { override fun writeToClientMessage(context: SerializationContext, message: ClientMessage) { message.putIntProperty(TAG_FIELD_NAME, Tag.RPC_REPLY.ordinal) message.putLongProperty(RPC_ID_FIELD_NAME, id.toLong) - message.bodyBuffer.writeBytes(result.serialize(context = context).bytes) + message.bodyBuffer.writeBytes(result.safeSerialize(context) { Try.Failure(it) }.bytes) } } @@ -167,11 +181,17 @@ object RPCApi { override fun writeToClientMessage(context: SerializationContext, message: ClientMessage) { message.putIntProperty(TAG_FIELD_NAME, Tag.OBSERVATION.ordinal) message.putLongProperty(OBSERVABLE_ID_FIELD_NAME, id.toLong) - message.bodyBuffer.writeBytes(content.serialize(context = context).bytes) + message.bodyBuffer.writeBytes(content.safeSerialize(context) { Notification.createOnError(it) }.bytes) } } companion object { + private fun Any.safeSerialize(context: SerializationContext, wrap: (Throwable) -> Any) = try { + serialize(context = context) + } catch (t: Throwable) { + wrap(t).serialize(context = context) + } + fun fromClientMessage(context: SerializationContext, message: ClientMessage): ServerToClient { val tag = Tag.values()[message.getIntProperty(TAG_FIELD_NAME)] return when (tag) { 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 d99f17dab7..3911943969 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt @@ -41,10 +41,17 @@ open class RPCException(message: String?, cause: Throwable?) : CordaRuntimeExcep constructor(msg: String) : this(msg, null) } +/** + * Thrown to indicate that the calling user does not have permission for something they have requested (for example + * calling a method). + */ @CordaSerializable class PermissionException(msg: String) : RuntimeException(msg) -// The Kryo used for the RPC wire protocol. Every type in the wire protocol is listed here explicitly. +/** + * The Kryo used for the RPC wire protocol. + */ +// Every type in the wire protocol is listed here explicitly. // This is annoying to write out, but will make it easier to formalise the wire protocol when the time comes, // because we can see everything we're using in one place. class RPCKryo(observableSerializer: Serializer>, serializationContext: SerializationContext) : CordaKryo(CordaClassResolver(serializationContext)) { 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 34a7bbe87e..20f8fb8513 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 @@ -3,7 +3,6 @@ package net.corda.nodeapi.config import com.typesafe.config.Config import com.typesafe.config.ConfigUtil import net.corda.core.internal.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 diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt index d4a38efe6a..bc0b5e1ee0 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt @@ -1,20 +1,35 @@ package net.corda.nodeapi.internal.serialization -import net.corda.core.serialization.ClassWhitelist -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.SerializedBytes +import net.corda.core.node.CordaPluginRegistry +import net.corda.core.serialization.* import net.corda.core.utilities.ByteSequence import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0 import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import java.util.* import java.util.concurrent.ConcurrentHashMap -internal val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == AmqpHeaderV1_0 +val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == AmqpHeaderV1_0 + +class AMQPSerializationCustomization(val factory: SerializerFactory) : SerializationCustomization { + override fun addToWhitelist(vararg types: Class<*>) { + factory.addToWhitelist(*types) + } +} + +fun SerializerFactory.addToWhitelist(vararg types: Class<*>) { + for (type in types) { + (this.whitelist as? MutableClassWhitelist)?.add(type) + } +} abstract class AbstractAMQPSerializationScheme : SerializationScheme { internal companion object { + private val pluginRegistries: List by lazy { + ServiceLoader.load(CordaPluginRegistry::class.java, this::class.java.classLoader).toList() + } + fun registerCustomSerializers(factory: SerializerFactory) { factory.apply { register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer) @@ -40,6 +55,8 @@ abstract class AbstractAMQPSerializationScheme : SerializationScheme { register(net.corda.nodeapi.internal.serialization.amqp.custom.X509CertificateHolderSerializer) register(net.corda.nodeapi.internal.serialization.amqp.custom.PartyAndCertificateSerializer(factory)) } + val customizer = AMQPSerializationCustomization(factory) + pluginRegistries.forEach { it.customizeSerialization(customizer) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt index de21b06ac0..5fa3d61e9c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt @@ -6,7 +6,10 @@ import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.serializers.FieldSerializer import com.esotericsoftware.kryo.util.DefaultClassResolver import com.esotericsoftware.kryo.util.Util -import net.corda.core.serialization.* +import net.corda.core.serialization.AttachmentsClassLoader +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializationContext import net.corda.core.utilities.loggerFor import java.io.PrintWriter import java.lang.reflect.Modifier.isAbstract @@ -16,8 +19,10 @@ import java.nio.file.Paths import java.nio.file.StandardOpenOption import java.util.* -fun Kryo.addToWhitelist(type: Class<*>) { - ((classResolver as? CordaClassResolver)?.whitelist as? MutableClassWhitelist)?.add(type) +fun Kryo.addToWhitelist(vararg types: Class<*>) { + for (type in types) { + ((classResolver as? CordaClassResolver)?.whitelist as? MutableClassWhitelist)?.add(type) + } } /** @@ -134,7 +139,11 @@ object EmptyWhitelist : ClassWhitelist { } class BuiltInExceptionsWhitelist : ClassWhitelist { - override fun hasListed(type: Class<*>): Boolean = Throwable::class.java.isAssignableFrom(type) && type.`package`.name.startsWith("java.") + companion object { + private val packageName = "^(?:java|kotlin)(?:[.]|$)".toRegex() + } + + override fun hasListed(type: Class<*>) = Throwable::class.java.isAssignableFrom(type) && packageName.containsMatchIn(type.`package`.name) } object AllWhitelist : ClassWhitelist { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt index 9cb3b8ea57..13d0d08f5b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt @@ -11,7 +11,7 @@ import de.javakaffee.kryoserializers.BitSetSerializer import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer import de.javakaffee.kryoserializers.guava.* import net.corda.core.contracts.PrivacySalt -import net.corda.core.crypto.composite.CompositeKey +import net.corda.core.crypto.CompositeKey import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.CordaPluginRegistry import net.corda.core.serialization.SerializeAsToken diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt index ae10b24d4b..f2e3f3835e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultWhitelist.kt @@ -18,54 +18,54 @@ import java.util.* class DefaultWhitelist : CordaPluginRegistry() { override fun customizeSerialization(custom: SerializationCustomization): Boolean { custom.apply { - // TODO: Turn this into an array and use map {} - addToWhitelist(Array(0, {}).javaClass) - addToWhitelist(Notification::class.java) - addToWhitelist(Notification.Kind::class.java) - addToWhitelist(ArrayList::class.java) - addToWhitelist(listOf().javaClass) // EmptyList - addToWhitelist(Pair::class.java) - addToWhitelist(ByteArray::class.java) - addToWhitelist(UUID::class.java) - addToWhitelist(LinkedHashSet::class.java) - addToWhitelist(setOf().javaClass) // EmptySet - addToWhitelist(Currency::class.java) - addToWhitelist(listOf(Unit).javaClass) // SingletonList - addToWhitelist(setOf(Unit).javaClass) // SingletonSet - addToWhitelist(mapOf(Unit to Unit).javaClass) // SingletonSet - addToWhitelist(NetworkHostAndPort::class.java) - addToWhitelist(SimpleString::class.java) - addToWhitelist(KryoException::class.java) - addToWhitelist(StringBuffer::class.java) - addToWhitelist(Unit::class.java) - addToWhitelist(java.io.ByteArrayInputStream::class.java) - addToWhitelist(java.lang.Class::class.java) - addToWhitelist(java.math.BigDecimal::class.java) - addToWhitelist(java.security.KeyPair::class.java) + addToWhitelist(Array(0, {}).javaClass, + Notification::class.java, + Notification.Kind::class.java, + ArrayList::class.java, + listOf().javaClass, // EmptyList + Pair::class.java, + ByteArray::class.java, + UUID::class.java, + LinkedHashSet::class.java, + setOf().javaClass, // EmptySet + Currency::class.java, + listOf(Unit).javaClass, // SingletonList + setOf(Unit).javaClass, // SingletonSet + mapOf(Unit to Unit).javaClass, // SingletonSet + NetworkHostAndPort::class.java, + SimpleString::class.java, + KryoException::class.java, + StringBuffer::class.java, + Unit::class.java, + java.io.ByteArrayInputStream::class.java, + java.lang.Class::class.java, + java.math.BigDecimal::class.java, + java.security.KeyPair::class.java, - // Matches the list in TimeSerializers.addDefaultSerializers: - addToWhitelist(java.time.Duration::class.java) - addToWhitelist(java.time.Instant::class.java) - addToWhitelist(java.time.LocalDate::class.java) - addToWhitelist(java.time.LocalDateTime::class.java) - addToWhitelist(java.time.ZoneOffset::class.java) - addToWhitelist(java.time.ZoneId::class.java) - addToWhitelist(java.time.OffsetTime::class.java) - addToWhitelist(java.time.OffsetDateTime::class.java) - addToWhitelist(java.time.ZonedDateTime::class.java) - addToWhitelist(java.time.Year::class.java) - addToWhitelist(java.time.YearMonth::class.java) - addToWhitelist(java.time.MonthDay::class.java) - addToWhitelist(java.time.Period::class.java) - addToWhitelist(java.time.DayOfWeek::class.java) // No custom serialiser but it's an enum. + // Matches the list in TimeSerializers.addDefaultSerializers: + java.time.Duration::class.java, + java.time.Instant::class.java, + java.time.LocalDate::class.java, + java.time.LocalDateTime::class.java, + java.time.ZoneOffset::class.java, + java.time.ZoneId::class.java, + java.time.OffsetTime::class.java, + java.time.OffsetDateTime::class.java, + java.time.ZonedDateTime::class.java, + java.time.Year::class.java, + java.time.YearMonth::class.java, + java.time.MonthDay::class.java, + java.time.Period::class.java, + java.time.DayOfWeek::class.java, // No custom serialiser but it's an enum. - addToWhitelist(java.util.Collections.singletonMap("A", "B").javaClass) - addToWhitelist(java.util.LinkedHashMap::class.java) - addToWhitelist(BigDecimal::class.java) - addToWhitelist(LocalDate::class.java) - addToWhitelist(Period::class.java) - addToWhitelist(BitSet::class.java) - addToWhitelist(OnErrorNotImplementedException::class.java) + java.util.Collections.singletonMap("A", "B").javaClass, + java.util.LinkedHashMap::class.java, + BigDecimal::class.java, + LocalDate::class.java, + Period::class.java, + BitSet::class.java, + OnErrorNotImplementedException::class.java, + StackTraceElement::class.java) } return true } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt index 7bd6886278..63089cfe31 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt @@ -8,12 +8,11 @@ import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer import com.esotericsoftware.kryo.serializers.FieldSerializer import com.esotericsoftware.kryo.util.MapReferenceResolver import net.corda.core.contracts.* +import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature -import net.corda.core.crypto.composite.CompositeKey import net.corda.core.identity.Party -import net.corda.core.internal.VisibleForTesting import net.corda.core.serialization.AttachmentsClassLoader import net.corda.core.serialization.MissingAttachmentsException import net.corda.core.serialization.SerializeAsTokenContext @@ -245,9 +244,6 @@ fun Input.readBytesWithLength(): ByteArray { /** 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) @@ -259,7 +255,7 @@ object WireTransactionSerializer : Serializer() { } private fun attachmentsClassLoader(kryo: Kryo, attachmentHashes: List): ClassLoader? { - kryo.context[attachmentsClassLoaderEnabled] as? Boolean ?: false || return null + kryo.context[attachmentsClassLoaderEnabledPropertyName] as? Boolean ?: false || return null val serializationContext = kryo.serializationContext() ?: return null // Some tests don't set one. val missing = ArrayList() val attachments = ArrayList() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoSerializationCustomization.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoSerializationCustomization.kt index 6b1ab209b8..1ae01e271e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoSerializationCustomization.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoSerializationCustomization.kt @@ -4,7 +4,7 @@ import com.esotericsoftware.kryo.Kryo import net.corda.core.serialization.SerializationCustomization class KryoSerializationCustomization(val kryo: Kryo) : SerializationCustomization { - override fun addToWhitelist(type: Class<*>) { - kryo.addToWhitelist(type) + override fun addToWhitelist(vararg types: Class<*>) { + kryo.addToWhitelist(*types) } } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt index f08f66e9fb..9cdd9c49e5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt @@ -8,6 +8,10 @@ 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.cache.Cache +import com.google.common.cache.CacheBuilder +import net.corda.core.contracts.Attachment +import net.corda.core.crypto.SecureHash import net.corda.core.internal.LazyPool import net.corda.core.serialization.* import net.corda.core.utilities.ByteSequence @@ -17,6 +21,8 @@ import java.io.NotSerializableException import java.util.* import java.util.concurrent.ConcurrentHashMap +val attachmentsClassLoaderEnabledPropertyName = "attachments.class.loader.enabled" + object NotSupportedSeralizationScheme : SerializationScheme { private fun doThrow(): Nothing = throw UnsupportedOperationException("Serialization scheme not supported.") @@ -33,6 +39,24 @@ data class SerializationContextImpl(override val preferredSerializationVersion: override val properties: Map, override val objectReferencesEnabled: Boolean, override val useCase: SerializationContext.UseCase) : SerializationContext { + + private val cache: Cache, AttachmentsClassLoader> = CacheBuilder.newBuilder().weakValues().maximumSize(1024).build() + + // We need to cache the AttachmentClassLoaders to avoid too many contexts, since the class loader is part of cache key for the context. + override fun withAttachmentsClassLoader(attachmentHashes: List): SerializationContext { + properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean ?: false || return this + val serializationContext = properties[serializationContextKey] as? SerializeAsTokenContextImpl ?: return this // Some tests don't set one. + return withClassLoader(cache.get(attachmentHashes) { + val missing = ArrayList() + val attachments = ArrayList() + attachmentHashes.forEach { id -> + serializationContext.serviceHub.attachments.openAttachment(id)?.let { attachments += it } ?: run { missing += id } + } + missing.isNotEmpty() && throw MissingAttachmentsException(missing) + AttachmentsClassLoader(attachments) + }) + } + override fun withProperty(property: Any, value: Any): SerializationContext { return copy(properties = properties + (property to value)) } @@ -56,7 +80,7 @@ data class SerializationContextImpl(override val preferredSerializationVersion: private const val HEADER_SIZE: Int = 8 -open class SerializationFactoryImpl : SerializationFactory { +open class SerializationFactoryImpl : SerializationFactory() { private val creator: List = Exception().stackTrace.asList() private val registeredSchemes: MutableCollection = Collections.synchronizedCollection(mutableListOf()) @@ -75,10 +99,12 @@ open class SerializationFactoryImpl : SerializationFactory { } @Throws(NotSerializableException::class) - override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T = schemeFor(byteSequence, context.useCase).deserialize(byteSequence, clazz, context) + override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T { + return asCurrent { withCurrentContext(context) { schemeFor(byteSequence, context.useCase).deserialize(byteSequence, clazz, context) } } + } override fun serialize(obj: T, context: SerializationContext): SerializedBytes { - return schemeFor(context.preferredSerializationVersion, context.useCase).serialize(obj, context) + return asCurrent { withCurrentContext(context) { schemeFor(context.preferredSerializationVersion, context.useCase).serialize(obj, context) } } } fun registerScheme(scheme: SerializationScheme) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPPrimitiveSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPPrimitiveSerializer.kt index 11f91005ed..02a4379fc8 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPPrimitiveSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPPrimitiveSerializer.kt @@ -1,6 +1,7 @@ package net.corda.nodeapi.internal.serialization.amqp import org.apache.qpid.proton.amqp.Binary +import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data import java.lang.reflect.Type @@ -10,7 +11,7 @@ import java.lang.reflect.Type * [ByteArray] is automatically marshalled to/from the Proton-J wrapper, [Binary]. */ class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer { - override val typeDescriptor: String = SerializerFactory.primitiveTypeName(clazz)!! + override val typeDescriptor = Symbol.valueOf(SerializerFactory.primitiveTypeName(clazz)!!) override val type: Type = clazz // NOOP since this is a primitive type. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializer.kt index 85cdc24e11..707cefa44f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPSerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data import java.lang.reflect.Type @@ -18,7 +19,7 @@ interface AMQPSerializer { * * This should be unique enough that we can use one global cache of [AMQPSerializer]s and use this as the look up key. */ - val typeDescriptor: String + val typeDescriptor: Symbol /** * Add anything required to the AMQP schema via [SerializationOutput.writeTypeNotations] and any dependent serializers diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt index f1595d70d6..e49eafaa96 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException import java.lang.reflect.Type @@ -31,12 +32,12 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) "${type.componentType().typeName}$arrayType" } - override val typeDescriptor by lazy { "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" } + override val typeDescriptor by lazy { Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") } internal val elementType: Type by lazy { type.componentType() } internal open val typeName by lazy { calcTypeName(type) } internal val typeNotation: TypeNotation by lazy { - RestrictedType(typeName, null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList()) + RestrictedType(typeName, null, emptyList(), "list", Descriptor(typeDescriptor), emptyList()) } override fun writeClassInfo(output: SerializationOutput) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt index 5d218a1a2a..667b75e818 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt @@ -1,6 +1,7 @@ package net.corda.nodeapi.internal.serialization.amqp import net.corda.core.utilities.NonEmptySet +import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException import java.lang.reflect.ParameterizedType @@ -14,27 +15,51 @@ import kotlin.collections.Set * Serialization / deserialization of predefined set of supported [Collection] types covering mostly [List]s and [Set]s. */ class CollectionSerializer(val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer { - override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(declaredType.toString()) - override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" + override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType)) + override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") companion object { - private val supportedTypes: Map>, (List<*>) -> Collection<*>> = mapOf( + // NB: Order matters in this map, the most specific classes should be listed at the end + private val supportedTypes: Map>, (List<*>) -> Collection<*>> = Collections.unmodifiableMap(linkedMapOf( Collection::class.java to { list -> Collections.unmodifiableCollection(list) }, List::class.java to { list -> Collections.unmodifiableList(list) }, Set::class.java to { list -> Collections.unmodifiableSet(LinkedHashSet(list)) }, SortedSet::class.java to { list -> Collections.unmodifiableSortedSet(TreeSet(list)) }, NavigableSet::class.java to { list -> Collections.unmodifiableNavigableSet(TreeSet(list)) }, NonEmptySet::class.java to { list -> NonEmptySet.copyOf(list) } - ) + )) private fun findConcreteType(clazz: Class<*>): (List<*>) -> Collection<*> { return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported collection type $clazz.") } + + fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType { + if(supportedTypes.containsKey(declaredClass)) { + // Simple case - it is already known to be a collection. + @Suppress("UNCHECKED_CAST") + return deriveParametrizedType(declaredType, declaredClass as Class>) + } + else if (actualClass != null && Collection::class.java.isAssignableFrom(actualClass)) { + // Declared class is not collection, but [actualClass] is - represent it accordingly. + val collectionClass = findMostSuitableCollectionType(actualClass) + return deriveParametrizedType(declaredType, collectionClass) + } + + throw NotSerializableException("Cannot derive collection type for declaredType: '$declaredType', declaredClass: '$declaredClass', actualClass: '$actualClass'") + } + + private fun deriveParametrizedType(declaredType: Type, collectionClass: Class>): ParameterizedType = + (declaredType as? ParameterizedType) ?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType)) + + + private fun findMostSuitableCollectionType(actualClass: Class<*>): Class> = + supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!! + } private val concreteBuilder: (List<*>) -> Collection<*> = findConcreteType(declaredType.rawType as Class<*>) - private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList()) + private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor), emptyList()) override fun writeClassInfo(output: SerializationOutput) { if (output.writeTypeNotations(typeNotation)) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt index 032c453751..fdb5896444 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt @@ -1,7 +1,7 @@ package net.corda.nodeapi.internal.serialization.amqp -import net.corda.core.serialization.SerializationDefaults import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.nameForType +import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data import java.lang.reflect.Type @@ -50,8 +50,8 @@ abstract class CustomSerializer : AMQPSerializer { 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 val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}") + private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(clazz), null, emptyList(), SerializerFactory.nameForType(superClassSerializer.type), Descriptor(typeDescriptor), emptyList()) override fun writeClassInfo(output: SerializationOutput) { output.writeTypeNotations(typeNotation) } @@ -73,7 +73,7 @@ abstract class CustomSerializer : AMQPSerializer { */ abstract class CustomSerializerImp(protected val clazz: Class, protected val withInheritance: Boolean) : CustomSerializer() { override val type: Type get() = clazz - override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}" + override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${nameForType(clazz)}") override fun writeClassInfo(output: SerializationOutput) {} override val descriptor: Descriptor = Descriptor(typeDescriptor) override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz @@ -160,11 +160,11 @@ abstract class CustomSerializer : AMQPSerializer { descriptor, emptyList()))) override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) { - data.putObject(unmaker(obj)) + data.putString(unmaker(obj)) } override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T { - val proxy = input.readObject(obj, schema, String::class.java) as String + val proxy = obj as String return maker(proxy) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt index 612d196433..7522844db0 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt @@ -6,12 +6,12 @@ import net.corda.core.utilities.ByteSequence 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.amqp.UnsignedInteger import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.nio.ByteBuffer -import java.util.* data class ObjectAndEnvelope(val obj: T, val envelope: Envelope) @@ -22,11 +22,10 @@ data class ObjectAndEnvelope(val obj: T, val envelope: Envelope) * instances and threads. */ class DeserializationInput(internal val serializerFactory: SerializerFactory) { - // TODO: we're not supporting object refs yet - private val objectHistory: MutableList = ArrayList() + private val objectHistory: MutableList = mutableListOf() internal companion object { - val BYTES_NEEDED_TO_PEEK: Int = 23 + private val BYTES_NEEDED_TO_PEEK: Int = 23 fun peekSize(bytes: ByteArray): Int { // There's an 8 byte header, and then a 0 byte plus descriptor followed by constructor @@ -58,7 +57,6 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) { inline internal fun deserializeAndReturnEnvelope(bytes: SerializedBytes): ObjectAndEnvelope = deserializeAndReturnEnvelope(bytes, T::class.java) - @Throws(NotSerializableException::class) private fun getEnvelope(bytes: ByteSequence): Envelope { // Check that the lead bytes match expected header @@ -95,40 +93,53 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) { * be deserialized and a schema describing the types of the objects. */ @Throws(NotSerializableException::class) - fun deserialize(bytes: ByteSequence, clazz: Class): T { - return des { - val envelope = getEnvelope(bytes) - clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)) - } + fun deserialize(bytes: ByteSequence, clazz: Class): T = des { + val envelope = getEnvelope(bytes) + clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)) } @Throws(NotSerializableException::class) - internal fun deserializeAndReturnEnvelope(bytes: SerializedBytes, clazz: Class): ObjectAndEnvelope { - return des { - val envelope = getEnvelope(bytes) - // Now pick out the obj and schema from the envelope. - ObjectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)), envelope) - } + fun deserializeAndReturnEnvelope(bytes: SerializedBytes, clazz: Class): ObjectAndEnvelope = des { + val envelope = getEnvelope(bytes) + // Now pick out the obj and schema from the envelope. + ObjectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)), envelope) } internal fun readObjectOrNull(obj: Any?, schema: Schema, type: Type): Any? { return if (obj == null) null else readObject(obj, schema, type) } - internal fun readObject(obj: Any, schema: Schema, type: Type): Any { - if (obj is DescribedType) { - // Look up serializer in factory by descriptor - val serializer = serializerFactory.get(obj.descriptor, schema) - if (serializer.type != type && with(serializer.type) { !isSubClassOf(type) && !materiallyEquivalentTo(type) }) - throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " + - "expected to be of type $type but was ${serializer.type}") - return serializer.readObject(obj.described, schema, this) - } else if (obj is Binary) { - return obj.array - } else { - return obj - } - } + internal fun readObject(obj: Any, schema: Schema, type: Type): Any = + if (obj is DescribedType && ReferencedObject.DESCRIPTOR == obj.descriptor) { + // It must be a reference to an instance that has already been read, cheaply and quickly returning it by reference. + val objectIndex = (obj.described as UnsignedInteger).toInt() + if (objectIndex !in 0..objectHistory.size) + throw NotSerializableException("Retrieval of existing reference failed. Requested index $objectIndex " + + "is outside of the bounds for the list of size: ${objectHistory.size}") + + val objectRetrieved = objectHistory[objectIndex] + if (!objectRetrieved::class.java.isSubClassOf(type.asClass()!!)) + throw NotSerializableException("Existing reference type mismatch. Expected: '$type', found: '${objectRetrieved::class.java}'") + objectRetrieved + } else { + val objectRead = when (obj) { + is DescribedType -> { + // Look up serializer in factory by descriptor + val serializer = serializerFactory.get(obj.descriptor, schema) + if (SerializerFactory.AnyType != type && serializer.type != type && with(serializer.type) { !isSubClassOf(type) && !materiallyEquivalentTo(type) }) + throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " + + "expected to be of type $type but was ${serializer.type}") + serializer.readObject(obj.described, schema, this) + } + is Binary -> obj.array + else -> obj // this will be the case for primitive types like [boolean] et al. + } + + // Store the reference in case we need it later on. + // Skip for primitive types as they are too small and overhead of referencing them will be much higher than their content + if (suitableForObjectReference(objectRead.javaClass)) objectHistory.add(objectRead) + objectRead + } /** * TODO: Currently performs rather basic checks aimed in particular at [java.util.List>] and @@ -136,5 +147,5 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) { * In the future tighter control might be needed */ private fun Type.materiallyEquivalentTo(that: Type): Boolean = - asClass() == that.asClass() && that is ParameterizedType -} \ No newline at end of file + asClass() == that.asClass() && that is ParameterizedType +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedType.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedType.kt index 3209866821..96b2d729d4 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedType.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedType.kt @@ -53,6 +53,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p var typeStart = 0 var needAType = true var skippingWhitespace = false + while (pos < params.length) { if (params[pos] == '<') { val typeEnd = pos++ @@ -102,7 +103,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p } else if (!skippingWhitespace && (params[pos] == '.' || params[pos].isJavaIdentifierPart())) { pos++ } else { - throw NotSerializableException("Invalid character in middle of type: ${params[pos]}") + throw NotSerializableException("Invalid character ${params[pos]} in middle of type $params at idx $pos") } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt new file mode 100644 index 0000000000..a11c547a90 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt @@ -0,0 +1,52 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import org.apache.qpid.proton.amqp.Symbol +import org.apache.qpid.proton.codec.Data +import java.io.NotSerializableException +import java.lang.reflect.Type + +/** + * Our definition of an enum with the AMQP spec is a list (of two items, a string and an int) that is + * a restricted type with a number of choices associated with it + */ +class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: SerializerFactory) : AMQPSerializer { + override val type: Type = declaredType + override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") + private val typeNotation: TypeNotation + + init { + typeNotation = RestrictedType( + SerializerFactory.nameForType(declaredType), + null, emptyList(), "list", Descriptor(typeDescriptor), + declaredClass.enumConstants.zip(IntRange(0, declaredClass.enumConstants.size)).map { + Choice(it.first.toString(), it.second.toString()) + }) + } + + override fun writeClassInfo(output: SerializationOutput) { + output.writeTypeNotations(typeNotation) + } + + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any { + val enumName = (obj as List<*>)[0] as String + val enumOrd = obj[1] as Int + val fromOrd = type.asClass()!!.enumConstants[enumOrd] + + if (enumName != fromOrd?.toString()) { + throw NotSerializableException("Deserializing obj as enum $type with value $enumName.$enumOrd but " + + "ordinality has changed") + } + return fromOrd + } + + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { + if (obj !is Enum<*>) throw NotSerializableException("Serializing $obj as enum when it isn't") + + data.withDescribed(typeNotation.descriptor) { + withList { + data.putString(obj.name) + data.putInt(obj.ordinal) + } + } + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt index 8b74459fe5..ba033241aa 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException import java.lang.reflect.ParameterizedType @@ -10,15 +11,18 @@ import kotlin.collections.Map import kotlin.collections.iterator import kotlin.collections.map +private typealias MapCreationFunction = (Map<*, *>) -> Map<*, *> + /** * Serialization / deserialization of certain supported [Map] types. */ -class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer { - override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(declaredType.toString()) - override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" +class MapSerializer(private val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer { + override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType)) + override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") companion object { - private val supportedTypes: Map, (Map<*, *>) -> Map<*, *>> = mapOf( + // NB: Order matters in this map, the most specific classes should be listed at the end + private val supportedTypes: Map>, MapCreationFunction> = Collections.unmodifiableMap(linkedMapOf( // Interfaces Map::class.java to { map -> Collections.unmodifiableMap(map) }, SortedMap::class.java to { map -> Collections.unmodifiableSortedMap(TreeMap(map)) }, @@ -26,15 +30,38 @@ class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFact // concrete classes for user convenience LinkedHashMap::class.java to { map -> LinkedHashMap(map) }, TreeMap::class.java to { map -> TreeMap(map) } - ) - private fun findConcreteType(clazz: Class<*>): (Map<*, *>) -> Map<*, *> { + )) + + private fun findConcreteType(clazz: Class<*>): MapCreationFunction { return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.") } + + fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType { + if(supportedTypes.containsKey(declaredClass)) { + // Simple case - it is already known to be a map. + @Suppress("UNCHECKED_CAST") + return deriveParametrizedType(declaredType, declaredClass as Class>) + } + else if (actualClass != null && Map::class.java.isAssignableFrom(actualClass)) { + // Declared class is not map, but [actualClass] is - represent it accordingly. + val mapClass = findMostSuitableMapType(actualClass) + return deriveParametrizedType(declaredType, mapClass) + } + + throw NotSerializableException("Cannot derive map type for declaredType: '$declaredType', declaredClass: '$declaredClass', actualClass: '$actualClass'") + } + + private fun deriveParametrizedType(declaredType: Type, collectionClass: Class>): ParameterizedType = + (declaredType as? ParameterizedType) ?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType, SerializerFactory.AnyType)) + + + private fun findMostSuitableMapType(actualClass: Class<*>): Class> = + MapSerializer.supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!! } - private val concreteBuilder: (Map<*, *>) -> Map<*, *> = findConcreteType(declaredType.rawType as Class<*>) + private val concreteBuilder: MapCreationFunction = findConcreteType(declaredType.rawType as Class<*>) - private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor, null), emptyList()) + private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor), emptyList()) override fun writeClassInfo(output: SerializationOutput) { if (output.writeTypeNotations(typeNotation)) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt index 3b99e84371..488150a14b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt @@ -1,7 +1,9 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.utilities.debug +import net.corda.core.utilities.loggerFor import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.nameForType -import org.apache.qpid.proton.amqp.UnsignedInteger +import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException import java.lang.reflect.Type @@ -13,7 +15,9 @@ import kotlin.reflect.jvm.javaConstructor open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer { override val type: Type get() = clazz open val kotlinConstructor = constructorForDeserialization(clazz) - val javaConstructor by lazy { kotlinConstructor?.javaConstructor } + val javaConstructor by lazy { kotlinConstructor?.javaConstructor?.apply { isAccessible = true } } + + private val logger = loggerFor() open internal val propertySerializers: Collection by lazy { propertiesForSerialization(kotlinConstructor, clazz, factory) @@ -21,10 +25,10 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS private val typeName = nameForType(clazz) - override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" + override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") private val interfaces = interfacesForSerialization(clazz, factory) // We restrict to only those annotated or whitelisted - open internal val typeNotation : TypeNotation by lazy {CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor, null), generateFields()) } + open internal val typeNotation: TypeNotation by lazy { CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor), generateFields()) } override fun writeClassInfo(output: SerializationOutput) { if (output.writeTypeNotations(typeNotation)) { @@ -50,10 +54,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS } override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any { - if (obj is UnsignedInteger) { - // TODO: Object refs - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } else if (obj is List<*>) { + if (obj is List<*>) { if (obj.size > propertySerializers.size) throw NotSerializableException("Too many properties in described type $typeName") val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schema, input) } return construct(params) @@ -66,6 +67,12 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS private fun generateProvides(): List = interfaces.map { nameForType(it) } - fun construct(properties: List) = javaConstructor?.newInstance(*properties.toTypedArray()) ?: - throw NotSerializableException("Attempt to deserialize an interface: $clazz. Serialized form is invalid.") + fun construct(properties: List): Any { + + logger.debug { "Calling constructor: '$javaConstructor' with properties '$properties'" } + + return javaConstructor?.newInstance(*properties.toTypedArray()) ?: + throw NotSerializableException("Attempt to deserialize an interface: $clazz. Serialized form is invalid.") + } + } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt index e82df6443e..2e27fbfd9a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt @@ -2,17 +2,18 @@ package net.corda.nodeapi.internal.serialization.amqp import com.google.common.hash.Hasher import com.google.common.hash.Hashing -import net.corda.core.crypto.toBase64 import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.toBase64 import org.apache.qpid.proton.amqp.DescribedType +import org.apache.qpid.proton.amqp.Symbol +import org.apache.qpid.proton.amqp.UnsignedInteger import org.apache.qpid.proton.amqp.UnsignedLong import org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.DescribedTypeConstructor import java.io.NotSerializableException import java.lang.reflect.* import java.util.* - import net.corda.nodeapi.internal.serialization.carpenter.Field as CarpenterField import net.corda.nodeapi.internal.serialization.carpenter.Schema as CarpenterSchema @@ -24,6 +25,21 @@ val DESCRIPTOR_DOMAIN: String = "net.corda" // "corda" + majorVersionByte + minorVersionMSB + minorVersionLSB val AmqpHeaderV1_0: OpaqueBytes = OpaqueBytes("corda\u0001\u0000\u0000".toByteArray()) +private enum class DescriptorRegistry(val id: Long) { + + ENVELOPE(1), + SCHEMA(2), + OBJECT_DESCRIPTOR(3), + FIELD(4), + COMPOSITE_TYPE(5), + RESTRICTED_TYPE(6), + CHOICE(7), + REFERENCED_OBJECT(8), + ; + + val amqpDescriptor = UnsignedLong(id or DESCRIPTOR_TOP_32BITS) +} + /** * This class wraps all serialized data, so that the schema can be carried along with it. We will provide various internal utilities * to decompose and recompose with/without schema etc so that e.g. we can store objects with a (relationally) normalised out schema to @@ -32,7 +48,7 @@ val AmqpHeaderV1_0: OpaqueBytes = OpaqueBytes("corda\u0001\u0000\u0000".toByteAr // TODO: make the schema parsing lazy since mostly schemas will have been seen before and we only need it if we don't recognise a type descriptor. data class Envelope(val obj: Any?, val schema: Schema) : DescribedType { companion object : DescribedTypeConstructor { - val DESCRIPTOR = UnsignedLong(1L or DESCRIPTOR_TOP_32BITS) + val DESCRIPTOR = DescriptorRegistry.ENVELOPE.amqpDescriptor val DESCRIPTOR_OBJECT = Descriptor(null, DESCRIPTOR) fun get(data: Data): Envelope { @@ -63,7 +79,7 @@ data class Envelope(val obj: Any?, val schema: Schema) : DescribedType { */ data class Schema(val types: List) : DescribedType { companion object : DescribedTypeConstructor { - val DESCRIPTOR = UnsignedLong(2L or DESCRIPTOR_TOP_32BITS) + val DESCRIPTOR = DescriptorRegistry.SCHEMA.amqpDescriptor fun get(obj: Any): Schema { val describedType = obj as DescribedType @@ -90,9 +106,11 @@ data class Schema(val types: List) : DescribedType { override fun toString(): String = types.joinToString("\n") } -data class Descriptor(val name: String?, val code: UnsignedLong? = null) : DescribedType { +data class Descriptor(val name: Symbol?, val code: UnsignedLong? = null) : DescribedType { + constructor(name: String?) : this(Symbol.valueOf(name)) + companion object : DescribedTypeConstructor { - val DESCRIPTOR = UnsignedLong(3L or DESCRIPTOR_TOP_32BITS) + val DESCRIPTOR = DescriptorRegistry.OBJECT_DESCRIPTOR.amqpDescriptor fun get(obj: Any): Descriptor { val describedType = obj as DescribedType @@ -106,7 +124,7 @@ data class Descriptor(val name: String?, val code: UnsignedLong? = null) : Descr override fun newInstance(described: Any?): Descriptor { val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list") - return Descriptor(list[0] as? String, list[1] as? UnsignedLong) + return Descriptor(list[0] as? Symbol, list[1] as? UnsignedLong) } } @@ -130,7 +148,7 @@ data class Descriptor(val name: String?, val code: UnsignedLong? = null) : Descr data class Field(val name: String, val type: String, val requires: List, val default: String?, val label: String?, val mandatory: Boolean, val multiple: Boolean) : DescribedType { companion object : DescribedTypeConstructor { - val DESCRIPTOR = UnsignedLong(4L or DESCRIPTOR_TOP_32BITS) + val DESCRIPTOR = DescriptorRegistry.FIELD.amqpDescriptor fun get(obj: Any): Field { val describedType = obj as DescribedType @@ -175,12 +193,10 @@ sealed class TypeNotation : DescribedType { companion object { fun get(obj: Any): TypeNotation { val describedType = obj as DescribedType - if (describedType.descriptor == CompositeType.DESCRIPTOR) { - return CompositeType.get(describedType) - } else if (describedType.descriptor == RestrictedType.DESCRIPTOR) { - return RestrictedType.get(describedType) - } else { - throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.") + return when (describedType.descriptor) { + CompositeType.DESCRIPTOR -> CompositeType.get(describedType) + RestrictedType.DESCRIPTOR -> RestrictedType.get(describedType) + else -> throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.") } } } @@ -193,7 +209,7 @@ sealed class TypeNotation : DescribedType { data class CompositeType(override val name: String, override val label: String?, override val provides: List, override val descriptor: Descriptor, val fields: List) : TypeNotation() { companion object : DescribedTypeConstructor { - val DESCRIPTOR = UnsignedLong(5L or DESCRIPTOR_TOP_32BITS) + val DESCRIPTOR = DescriptorRegistry.COMPOSITE_TYPE.amqpDescriptor fun get(describedType: DescribedType): CompositeType { if (describedType.descriptor != DESCRIPTOR) { @@ -236,9 +252,14 @@ data class CompositeType(override val name: String, override val label: String?, } } -data class RestrictedType(override val name: String, override val label: String?, override val provides: List, val source: String, override val descriptor: Descriptor, val choices: List) : TypeNotation() { +data class RestrictedType(override val name: String, + override val label: String?, + override val provides: List, + val source: String, + override val descriptor: Descriptor, + val choices: List) : TypeNotation() { companion object : DescribedTypeConstructor { - val DESCRIPTOR = UnsignedLong(6L or DESCRIPTOR_TOP_32BITS) + val DESCRIPTOR = DescriptorRegistry.RESTRICTED_TYPE.amqpDescriptor fun get(describedType: DescribedType): RestrictedType { if (describedType.descriptor != DESCRIPTOR) { @@ -274,6 +295,9 @@ data class RestrictedType(override val name: String, override val label: String? } sb.append(">\n") sb.append(" $descriptor\n") + choices.forEach { + sb.append(" $it\n") + } sb.append("") return sb.toString() } @@ -281,7 +305,7 @@ data class RestrictedType(override val name: String, override val label: String? data class Choice(val name: String, val value: String) : DescribedType { companion object : DescribedTypeConstructor { - val DESCRIPTOR = UnsignedLong(7L or DESCRIPTOR_TOP_32BITS) + val DESCRIPTOR = DescriptorRegistry.CHOICE.amqpDescriptor fun get(obj: Any): Choice { val describedType = obj as DescribedType @@ -308,7 +332,35 @@ data class Choice(val name: String, val value: String) : DescribedType { } } +data class ReferencedObject(private val refCounter: Int) : DescribedType { + companion object : DescribedTypeConstructor { + val DESCRIPTOR = DescriptorRegistry.REFERENCED_OBJECT.amqpDescriptor + + fun get(obj: Any): ReferencedObject { + val describedType = obj as DescribedType + if (describedType.descriptor != DESCRIPTOR) { + throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.") + } + return newInstance(describedType.described) + } + + override fun getTypeClass(): Class<*> = ReferencedObject::class.java + + override fun newInstance(described: Any?): ReferencedObject { + val unInt = described as? UnsignedInteger ?: throw IllegalStateException("Was expecting an UnsignedInteger") + return ReferencedObject(unInt.toInt()) + } + } + + override fun getDescriptor(): Any = DESCRIPTOR + + override fun getDescribed(): UnsignedInteger = UnsignedInteger(refCounter) + + override fun toString(): String = "" +} + private val ARRAY_HASH: String = "Array = true" +private val ENUM_HASH: String = "Enum = true" private val ALREADY_SEEN_HASH: String = "Already seen = true" private val NULLABLE_HASH: String = "Nullable = true" private val NOT_NULLABLE_HASH: String = "Nullable = false" @@ -339,7 +391,7 @@ internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String { return hasher.hash().asBytes().toBase64() } -private fun Hasher.fingerprintWithCustomSerializerOrElse(factory: SerializerFactory, clazz: Class<*>, declaredType: Type, block: () -> Hasher) : Hasher { +private fun Hasher.fingerprintWithCustomSerializerOrElse(factory: SerializerFactory, clazz: Class<*>, declaredType: Type, block: () -> Hasher): Hasher { // Need to check if a custom serializer is applicable val customSerializer = factory.findCustomSerializer(clazz, declaredType) return if (customSerializer != null) { @@ -357,51 +409,58 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta } else { alreadySeen += type 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.fingerprintWithCustomSerializerOrElse(factory, type, type) { - 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, type, alreadySeen, hasher, factory) + when (type) { + is SerializerFactory.AnyType -> hasher.putUnencodedChars(ANY_TYPE_HASH) + 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 if (type.isEnum) { + // ensures any change to the enum (adding constants) will trigger the need for evolution + hasher.apply { + type.enumConstants.forEach { + putUnencodedChars(it.toString()) + } + }.putUnencodedChars(type.name).putUnencodedChars(ENUM_HASH) + } else { + hasher.fingerprintWithCustomSerializerOrElse(factory, type, type) { + 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, type, alreadySeen, hasher, factory) + } } } } - } 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 { - hasher.fingerprintWithCustomSerializerOrElse(factory, clazz, type) { - fingerprintForObject(type, type, alreadySeen, hasher, factory) + is ParameterizedType -> { + // Hash the rawType + params + val clazz = type.rawType as Class<*> + val startingHash = if (isCollectionOrMap(clazz)) { + hasher.putUnencodedChars(clazz.name) + } else { + hasher.fingerprintWithCustomSerializerOrElse(factory, clazz, type) { + 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) } } - // ... 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 if (type is WildcardType) { - hasher.putUnencodedChars(type.typeName).putUnencodedChars(WILDCARD_TYPE_HASH) + // Hash the element type + some array hash + is GenericArrayType -> fingerprintForType(type.genericComponentType, contextType, alreadySeen, + hasher, factory).putUnencodedChars(ARRAY_HASH) + // TODO: include bounds + is TypeVariable<*> -> hasher.putUnencodedChars(type.name).putUnencodedChars(TYPE_VARIABLE_HASH) + is WildcardType -> hasher.putUnencodedChars(type.typeName).putUnencodedChars(WILDCARD_TYPE_HASH) + else -> throw NotSerializableException("Don't know how to hash") } - else { - throw NotSerializableException("Don't know how to hash") - } - } catch(e: NotSerializableException) { + } catch (e: NotSerializableException) { val msg = "${e.message} -> $type" logger.error(msg, e) throw NotSerializableException(msg) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index f1a356a01d..8374a7b898 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import com.google.common.primitives.Primitives import com.google.common.reflect.TypeToken import org.apache.qpid.proton.codec.Data import java.beans.IndexedPropertyDescriptor @@ -36,7 +37,7 @@ internal fun constructorForDeserialization(type: Type): KFunction? { val kotlinConstructors = clazz.kotlin.constructors val hasDefault = kotlinConstructors.any { it.parameters.isEmpty() } for (kotlinConstructor in kotlinConstructors) { - if (preferredCandidate == null && kotlinConstructors.size == 1 && !hasDefault) { + if (preferredCandidate == null && kotlinConstructors.size == 1) { preferredCandidate = kotlinConstructor } else if (preferredCandidate == null && kotlinConstructors.size == 2 && hasDefault && kotlinConstructor.parameters.isNotEmpty()) { preferredCandidate = kotlinConstructor @@ -155,6 +156,19 @@ fun Data.withList(block: Data.() -> Unit) { exit() // exit list } +/** + * Extension helper for outputting reference to already observed object + */ +fun Data.writeReferencedObject(refObject: ReferencedObject) { + // Write described + putDescribed() + enter() + // Write descriptor + putObject(refObject.descriptor) + putUnsignedInteger(refObject.described) + exit() // exit described +} + 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 @@ -206,4 +220,10 @@ internal fun Type.asParameterizedType(): ParameterizedType { internal fun Type.isSubClassOf(type: Type): Boolean { return TypeToken.of(this).isSubtypeOf(type) +} + +// ByteArrays, primtives and boxed primitives are not stored in the object history +internal fun suitableForObjectReference(type: Type): Boolean { + val clazz = type.asClass() + return type != ByteArray::class.java && (clazz != null && !clazz.isPrimitive && !Primitives.unwrap(clazz).isPrimitive) } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt index 39b96b9679..5379b5ab84 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt @@ -8,6 +8,7 @@ import java.nio.ByteBuffer import java.util.* import kotlin.collections.LinkedHashSet + /** * Main entry point for serializing an object to AMQP. * @@ -15,40 +16,48 @@ import kotlin.collections.LinkedHashSet * instances and threads. */ open class SerializationOutput(internal val serializerFactory: SerializerFactory) { - // TODO: we're not supporting object refs yet + private val objectHistory: MutableMap = IdentityHashMap() private val serializerHistory: MutableSet> = LinkedHashSet() - private val schemaHistory: MutableSet = LinkedHashSet() + internal val schemaHistory: MutableSet = LinkedHashSet() /** * Serialize the given object to AMQP, wrapped in our [Envelope] wrapper which carries an AMQP 1.0 schema, and prefixed - * with a header to indicate that this is serialized with AMQP and not [Kryo], and what version of the Corda implementation - * of AMQP serialization contructed the serialized form. + * with a header to indicate that this is serialized with AMQP and not Kryo, and what version of the Corda implementation + * of AMQP serialization constructed the serialized form. */ @Throws(NotSerializableException::class) fun serialize(obj: T): SerializedBytes { try { - val data = Data.Factory.create() - data.withDescribed(Envelope.DESCRIPTOR_OBJECT) { - withList { - // Our object - writeObject(obj, this) - // The schema - writeSchema(Schema(schemaHistory.toList()), this) - } - } - val bytes = ByteArray(data.encodedSize().toInt() + 8) - val buf = ByteBuffer.wrap(bytes) - buf.put(AmqpHeaderV1_0.bytes) - data.encode(buf) - return SerializedBytes(bytes) + return _serialize(obj) } finally { - objectHistory.clear() - serializerHistory.clear() - schemaHistory.clear() + andFinally() } } + internal fun andFinally() { + objectHistory.clear() + serializerHistory.clear() + schemaHistory.clear() + } + + internal fun _serialize(obj: T): SerializedBytes { + val data = Data.Factory.create() + data.withDescribed(Envelope.DESCRIPTOR_OBJECT) { + withList { + // Our object + writeObject(obj, this) + // The schema + writeSchema(Schema(schemaHistory.toList()), this) + } + } + val bytes = ByteArray(data.encodedSize().toInt() + 8) + val buf = ByteBuffer.wrap(bytes) + buf.put(AmqpHeaderV1_0.bytes) + data.encode(buf) + return SerializedBytes(bytes) + } + internal fun writeObject(obj: Any, data: Data) { writeObject(obj, data, obj.javaClass) } @@ -71,7 +80,18 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory serializerHistory.add(serializer) serializer.writeClassInfo(this) } - serializer.writeObject(obj, data, type, this) + + val retrievedRefCount = objectHistory[obj] + if(retrievedRefCount == null) { + serializer.writeObject(obj, data, type, this) + // Important to do it after serialization such that dependent object will have preceding reference numbers + // assigned to them first as they will be first read from the stream on receiving end. + // Skip for primitive types as they are too small and overhead of referencing them will be much higher than their content + if (suitableForObjectReference(obj.javaClass)) objectHistory.put(obj, objectHistory.size) + } + else { + data.writeReferencedObject(ReferencedObject(retrievedRefCount)) + } } open internal fun writeTypeNotations(vararg typeNotation: TypeNotation): Boolean { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index c0b8b19c3f..c05587b4c7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -7,49 +7,43 @@ import net.corda.core.serialization.CordaSerializable import net.corda.nodeapi.internal.serialization.carpenter.* import org.apache.qpid.proton.amqp.* import java.io.NotSerializableException -import java.lang.reflect.GenericArrayType -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import java.lang.reflect.WildcardType +import java.lang.reflect.* import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import javax.annotation.concurrent.ThreadSafe -data class schemaAndDescriptor (val schema: Schema, val typeDescriptor: Any) +data class schemaAndDescriptor(val schema: Schema, val typeDescriptor: Any) /** * Factory of serializers designed to be shared across threads and invocations. */ -// TODO: enums -// 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. 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 // TODO: use guava caches etc so not unbounded // TODO: do we need to support a transient annotation to exclude certain properties? // TODO: allow definition of well known types that are left out of the schema. -// 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, cl : ClassLoader) { +class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) { private val serializersByType = ConcurrentHashMap>() private val serializersByDescriptor = ConcurrentHashMap>() private val customSerializers = CopyOnWriteArrayList>() private val classCarpenter = ClassCarpenter(cl) - val classloader : ClassLoader + val classloader: ClassLoader get() = classCarpenter.classloader - fun getEvolutionSerializer(typeNotation: TypeNotation, newSerializer: ObjectSerializer) : AMQPSerializer { + private fun getEvolutionSerializer(typeNotation: TypeNotation, newSerializer: AMQPSerializer): AMQPSerializer { return serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) { - EvolutionSerializer.make(typeNotation as CompositeType, newSerializer, this) + when (typeNotation) { + is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, this) + is RestrictedType -> throw NotSerializableException("Enum evolution is not currently supported") + } } } @@ -66,18 +60,29 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType - val serializer = if (Collection::class.java.isAssignableFrom(declaredClass)) { - serializersByType.computeIfAbsent(declaredType) { - CollectionSerializer(declaredType as? ParameterizedType ?: DeserializedParameterizedType( - declaredClass, arrayOf(AnyType), null), this) + val serializer = when { + // Declared class may not be set to Collection, but actual class could be a collection. + // In this case use of CollectionSerializer is perfectly appropriate. + (Collection::class.java.isAssignableFrom(declaredClass) || + (actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) -> { + val declaredTypeAmended= CollectionSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass) + serializersByType.computeIfAbsent(declaredTypeAmended) { + CollectionSerializer(declaredTypeAmended, this) + } } - } else if (Map::class.java.isAssignableFrom(declaredClass)) { - serializersByType.computeIfAbsent(declaredClass) { - makeMapSerializer(declaredType as? ParameterizedType ?: DeserializedParameterizedType( - declaredClass, arrayOf(AnyType, AnyType), null)) + // Declared class may not be set to Map, but actual class could be a map. + // In this case use of MapSerializer is perfectly appropriate. + (Map::class.java.isAssignableFrom(declaredClass) || + (actualClass != null && Map::class.java.isAssignableFrom(actualClass))) -> { + val declaredTypeAmended= MapSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass) + serializersByType.computeIfAbsent(declaredClass) { + makeMapSerializer(declaredTypeAmended) + } } - } else { - makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType) + Enum::class.java.isAssignableFrom(declaredClass) -> serializersByType.computeIfAbsent(declaredClass) { + EnumSerializer(actualType, actualClass ?: declaredClass, this) + } + else -> makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType) } serializersByDescriptor.putIfAbsent(serializer.typeDescriptor, serializer) @@ -90,17 +95,17 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { * 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 - } + private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, declaredType: Type): Type? = + when (declaredType) { + is ParameterizedType -> inferTypeVariables(actualClass, declaredClass, declaredType) + // Nothing to infer, otherwise we'd have ParameterizedType + is Class<*> -> actualClass + is GenericArrayType -> { + val declaredComponent = declaredType.genericComponentType + inferTypeVariables(actualClass?.componentType, declaredComponent.asClass()!!, declaredComponent)?.asArray() + } + else -> null + } /** * Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared @@ -117,8 +122,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { 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 resolver = rest.reversed().fold(TypeResolver().where(start, declaredType)) { resolved, chainEntry -> val newResolved = resolved.resolveType(chainEntry) TypeResolver().where(chainEntry, newResolved) } @@ -194,7 +198,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { // doesn't match that of the serialised object then we are dealing with different // instance of the class, as such we need to build an EvolutionSerialiser if (serialiser.typeDescriptor != typeNotation.descriptor.name) { - getEvolutionSerializer(typeNotation, serialiser as ObjectSerializer) + getEvolutionSerializer(typeNotation, serialiser) } } catch (e: ClassNotFoundException) { if (sentinel || (typeNotation !is CompositeType)) throw e @@ -210,16 +214,14 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { } private fun processSchemaEntry(typeNotation: TypeNotation) = when (typeNotation) { - is CompositeType -> processCompositeType(typeNotation) // java.lang.Class (whether a class or interface) - is RestrictedType -> processRestrictedType(typeNotation) // Collection / Map, possibly with generics - } - - private fun processRestrictedType(typeNotation: RestrictedType): AMQPSerializer { - // TODO: class loader logic, and compare the schema. - val type = typeForName(typeNotation.name, classloader) - return get(null, type) + is CompositeType -> processCompositeType(typeNotation) // java.lang.Class (whether a class or interface) + is RestrictedType -> processRestrictedType(typeNotation) // Collection / Map, possibly with generics } + // TODO: class loader logic, and compare the schema. + private fun processRestrictedType(typeNotation: RestrictedType) = get(null, + typeForName(typeNotation.name, classloader)) + private fun processCompositeType(typeNotation: CompositeType): AMQPSerializer { // TODO: class loader logic, and compare the schema. val type = typeForName(typeNotation.name, classloader) @@ -233,7 +235,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { findCustomSerializer(clazz, declaredType) ?: run { if (type.isArray()) { // Allow Object[] since this can be quite common (i.e. an untyped array) - if(type.componentType() != Object::class.java) whitelisted(type.componentType()) + if (type.componentType() != Object::class.java) whitelisted(type.componentType()) if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this) else ArraySerializer.make(type, this) } else if (clazz.kotlin.objectInstance != null) { @@ -248,8 +250,9 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { } 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? + // 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)) { val declaredSuperClass = declaredType.asClass()?.superclass @@ -258,7 +261,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { } else { // Make a subclass serializer for the subclass and return that... @Suppress("UNCHECKED_CAST") - return CustomSerializer.SubClass(clazz, customSerializer as CustomSerializer) + return CustomSerializer.SubClass(clazz, customSerializer as CustomSerializer) } } } @@ -277,7 +280,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { (!whitelist.hasListed(clazz) && !hasAnnotationInHierarchy(clazz)) // Recursively check the class, interfaces and superclasses for our annotation. - internal fun hasAnnotationInHierarchy(type: Class<*>): Boolean { + private fun hasAnnotationInHierarchy(type: Class<*>): Boolean { return type.isAnnotationPresent(CordaSerializable::class.java) || type.interfaces.any { hasAnnotationInHierarchy(it) } || (type.superclass != null && hasAnnotationInHierarchy(type.superclass)) @@ -334,7 +337,8 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { } is ParameterizedType -> "${nameForType(type.rawType)}<${type.actualTypeArguments.joinToString { nameForType(it) }}>" is GenericArrayType -> "${nameForType(type.genericComponentType)}[]" - is WildcardType -> "Any" + is WildcardType -> "?" + is TypeVariable<*> -> "?" else -> throw NotSerializableException("Unable to render type $type to a string.") } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt index dfc587a1dd..e38914cd4d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data import java.lang.reflect.Type @@ -9,12 +10,13 @@ import java.lang.reflect.Type * 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)}" + override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") + private val interfaces = interfacesForSerialization(type, factory) private fun generateProvides(): List = interfaces.map { it.typeName } - internal val typeNotation: TypeNotation = RestrictedType(type.typeName, "Singleton", generateProvides(), "boolean", Descriptor(typeDescriptor, null), emptyList()) + internal val typeNotation: TypeNotation = RestrictedType(type.typeName, "Singleton", generateProvides(), "boolean", Descriptor(typeDescriptor), emptyList()) override fun writeClassInfo(output: SerializationOutput) { output.writeTypeNotations(typeNotation) diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java new file mode 100644 index 0000000000..e0b65ad27c --- /dev/null +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java @@ -0,0 +1,36 @@ +package net.corda.nodeapi.internal.serialization.amqp; + +import org.junit.Test; + +import net.corda.nodeapi.internal.serialization.AllWhitelist; +import net.corda.core.serialization.SerializedBytes; + +import java.io.NotSerializableException; + +public class JavaSerialiseEnumTests { + + public enum Bras { + TSHIRT, UNDERWIRE, PUSHUP, BRALETTE, STRAPLESS, SPORTS, BACKLESS, PADDED + } + + private static class Bra { + private final Bras bra; + + private Bra(Bras bra) { + this.bra = bra; + } + + public Bras getBra() { + return this.bra; + } + } + + @Test + public void testJavaConstructorAnnotations() throws NotSerializableException { + Bra bra = new Bra(Bras.UNDERWIRE); + + SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader()); + SerializationOutput ser = new SerializationOutput(factory1); + SerializedBytes bytes = ser.serialize(bra); + } +} diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java index 171b9cbe72..34ca1abaa6 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java @@ -25,15 +25,17 @@ public class JavaSerializationOutputTests { } @ConstructorForDeserialization - public Foo(String fred, int count) { + private Foo(String fred, int count) { this.bob = fred; this.count = count; } + @SuppressWarnings("unused") public String getFred() { return bob; } + @SuppressWarnings("unused") public int getCount() { return count; } @@ -61,15 +63,17 @@ public class JavaSerializationOutputTests { private final String bob; private final int count; - public UnAnnotatedFoo(String fred, int count) { + private UnAnnotatedFoo(String fred, int count) { this.bob = fred; this.count = count; } + @SuppressWarnings("unused") public String getFred() { return bob; } + @SuppressWarnings("unused") public int getCount() { return count; } @@ -97,7 +101,7 @@ public class JavaSerializationOutputTests { private final String fred; private final Integer count; - public BoxedFoo(String fred, Integer count) { + private BoxedFoo(String fred, Integer count) { this.fred = fred; this.count = count; } @@ -134,7 +138,7 @@ public class JavaSerializationOutputTests { private final String fred; private final Integer count; - public BoxedFooNotNull(String fred, Integer count) { + private BoxedFooNotNull(String fred, Integer count) { this.fred = fred; this.count = count; } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/AttachmentClassLoaderTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/AttachmentClassLoaderTests.kt index 3773a0dee6..a81287b26f 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/AttachmentClassLoaderTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/AttachmentClassLoaderTests.kt @@ -10,19 +10,18 @@ import net.corda.core.internal.declaredField import net.corda.core.node.ServiceHub import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.* -import net.corda.core.serialization.SerializationDefaults.P2P_CONTEXT +import net.corda.core.serialization.SerializationFactory import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.loggerFor -import net.corda.nodeapi.internal.serialization.AMQP_ENABLED import net.corda.nodeapi.internal.serialization.SerializeAsTokenContextImpl -import net.corda.nodeapi.internal.serialization.WireTransactionSerializer +import net.corda.nodeapi.internal.serialization.attachmentsClassLoaderEnabledPropertyName import net.corda.nodeapi.internal.serialization.withTokenContext import net.corda.testing.DUMMY_NOTARY import net.corda.testing.MEGA_CORP import net.corda.testing.TestDependencyInjectionBase +import net.corda.testing.kryoSpecific import net.corda.testing.node.MockAttachmentStorage import org.apache.commons.io.IOUtils import org.junit.Assert @@ -52,7 +51,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { private fun SerializationContext.withAttachmentStorage(attachmentStorage: AttachmentStorage): SerializationContext { val serviceHub = mock() whenever(serviceHub.attachments).thenReturn(attachmentStorage) - return this.withTokenContext(SerializeAsTokenContextImpl(serviceHub) {}).withProperty(WireTransactionSerializer.attachmentsClassLoaderEnabled, true) + return this.withTokenContext(SerializeAsTokenContextImpl(serviceHub) {}).withProperty(attachmentsClassLoaderEnabledPropertyName, true) } } @@ -224,7 +223,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader) - val context = P2P_CONTEXT.withClassLoader(cl).withWhitelisted(contract.javaClass) + val context = SerializationFactory.defaultFactory.defaultContext.withClassLoader(cl).withWhitelisted(contract.javaClass) val state2 = bytes.deserialize(context = context) assertTrue(state2.javaClass.classLoader is AttachmentsClassLoader) assertNotNull(state2) @@ -240,7 +239,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { assertNotNull(data.contract) - val context2 = P2P_CONTEXT.withWhitelisted(data.contract.javaClass) + val context2 = SerializationFactory.defaultFactory.defaultContext.withWhitelisted(data.contract.javaClass) val bytes = data.serialize(context = context2) @@ -252,7 +251,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader) - val context = P2P_CONTEXT.withClassLoader(cl).withWhitelisted(Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, cl)) + val context = SerializationFactory.defaultFactory.defaultContext.withClassLoader(cl).withWhitelisted(Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, cl)) val state2 = bytes.deserialize(context = context) assertEquals(cl, state2.contract.javaClass.classLoader) @@ -261,7 +260,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { // We should be able to load same class from a different class loader and have them be distinct. val cl2 = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader) - val context3 = P2P_CONTEXT.withClassLoader(cl2).withWhitelisted(Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, cl2)) + val context3 = SerializationFactory.defaultFactory.defaultContext.withClassLoader(cl2).withWhitelisted(Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, cl2)) val state3 = bytes.deserialize(context = context3) assertEquals(cl2, state3.contract.javaClass.classLoader) @@ -313,7 +312,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { val contract = contractClass.newInstance() as DummyContractBackdoor val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY) val storage = MockAttachmentStorage() - val context = P2P_CONTEXT.withWhitelisted(contract.javaClass) + val context = SerializationFactory.defaultFactory.defaultContext.withWhitelisted(contract.javaClass) .withWhitelisted(Class.forName("net.corda.contracts.isolated.AnotherDummyContract\$State", true, child)) .withWhitelisted(Class.forName("net.corda.contracts.isolated.AnotherDummyContract\$Commands\$Create", true, child)) .withAttachmentStorage(storage) @@ -332,8 +331,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { } @Test - // Kryo verifies/loads attachments on deserialization, whereas AMQP currently does not - fun `test deserialize of WireTransaction where contract cannot be found`() = kryoSpecific { + fun `test deserialize of WireTransaction where contract cannot be found`() = kryoSpecific("Kryo verifies/loads attachments on deserialization, whereas AMQP currently does not") { val child = ClassLoaderForTests() val contractClass = Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, child) val contract = contractClass.newInstance() as DummyContractBackdoor @@ -348,13 +346,13 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { val wireTransaction = tx.toWireTransaction() - wireTransaction.serialize(context = P2P_CONTEXT.withAttachmentStorage(storage)) + wireTransaction.serialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentStorage(storage)) } // use empty attachmentStorage val e = assertFailsWith(MissingAttachmentsException::class) { val mockAttStorage = MockAttachmentStorage() - bytes.deserialize(context = P2P_CONTEXT.withAttachmentStorage(mockAttStorage)) + bytes.deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentStorage(mockAttStorage)) if(mockAttStorage.openAttachment(attachmentRef) == null) { throw MissingAttachmentsException(listOf(attachmentRef)) @@ -363,9 +361,20 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { assertEquals(attachmentRef, e.ids.single()) } - private fun kryoSpecific(function: () -> Unit) = if(!AMQP_ENABLED) { - function() - } else { - loggerFor().info("Ignoring Kryo specific test") + @Test + fun `test loading a class from attachment during deserialization`() { + val child = ClassLoaderForTests() + val contractClass = Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, child) + val contract = contractClass.newInstance() as DummyContractBackdoor + val storage = MockAttachmentStorage() + val attachmentRef = importJar(storage) + val outboundContext = SerializationFactory.defaultFactory.defaultContext.withClassLoader(child) + // We currently ignore annotations in attachments, so manually whitelist. + val inboundContext = SerializationFactory.defaultFactory.defaultContext.withWhitelisted(contract.javaClass).withAttachmentStorage(storage).withAttachmentsClassLoader(listOf(attachmentRef)) + + // Serialize with custom context to avoid populating the default context with the specially loaded class + val serialized = contract.serialize(context = outboundContext) + // Then deserialize with the attachment class loader associated with the attachment + serialized.deserialize(context = inboundContext) } } \ No newline at end of file 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 1d7b061fad..7264d57b5c 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 @@ -6,7 +6,7 @@ import com.typesafe.config.ConfigRenderOptions.defaults import com.typesafe.config.ConfigValueFactory import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort -import net.corda.testing.getTestX509Name +import net.corda.core.utilities.getX500Name import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x500.X500Name import org.junit.Test @@ -112,7 +112,7 @@ class ConfigParsingTest { @Test fun x500Name() { - testPropertyType(getTestX509Name("Mock Party"), getTestX509Name("Mock Party 2"), valuesToString = true) + testPropertyType(getX500Name(O = "Mock Party", L = "London", C = "GB"), getX500Name(O = "Mock Party 2", L = "London", C = "GB"), valuesToString = true) } @Test diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt index 11eb91de40..c3f0ab3aaf 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt @@ -76,7 +76,7 @@ class DefaultSerializableSerializer : Serializer() { } class CordaClassResolverTests { - val factory: SerializationFactory = object : SerializationFactory { + val factory: SerializationFactory = object : SerializationFactory() { override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt new file mode 100644 index 0000000000..2d01f3466c --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ListsSerializationTest.kt @@ -0,0 +1,58 @@ +package net.corda.nodeapi.internal.serialization + +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.node.services.statemachine.SessionData +import net.corda.testing.TestDependencyInjectionBase +import net.corda.testing.amqpSpecific +import org.assertj.core.api.Assertions +import org.junit.Test +import java.io.NotSerializableException +import kotlin.test.assertEquals + +class ListsSerializationTest : TestDependencyInjectionBase() { + + @Test + fun `check list can be serialized as root of serialization graph`() { + assertEqualAfterRoundTripSerialization(emptyList()) + assertEqualAfterRoundTripSerialization(listOf(1)) + assertEqualAfterRoundTripSerialization(listOf(1, 2)) + } + + @Test + fun `check list can be serialized as part of SessionData`() { + + run { + val sessionData = SessionData(123, listOf(1)) + assertEqualAfterRoundTripSerialization(sessionData) + } + + run { + val sessionData = SessionData(123, listOf(1, 2)) + assertEqualAfterRoundTripSerialization(sessionData) + } + } + + @CordaSerializable + data class WrongPayloadType(val payload: ArrayList) + + @Test + fun `check throws for forbidden declared type`() = amqpSpecific("Such exceptions are not expected in Kryo mode.") { + val payload = ArrayList() + payload.add(1) + payload.add(2) + val wrongPayloadType = WrongPayloadType(payload) + Assertions.assertThatThrownBy { wrongPayloadType.serialize() } + .isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive collection type for declaredType") + } +} + +internal inline fun assertEqualAfterRoundTripSerialization(obj: T) { + + val serializedForm: SerializedBytes = obj.serialize() + val deserializedInstance = serializedForm.deserialize() + + assertEquals(obj, deserializedInstance) +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt new file mode 100644 index 0000000000..78e0e10c31 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/MapsSerializationTest.kt @@ -0,0 +1,57 @@ +package net.corda.nodeapi.internal.serialization + +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.serialize +import net.corda.node.services.statemachine.SessionData +import net.corda.testing.TestDependencyInjectionBase +import net.corda.testing.amqpSpecific +import org.assertj.core.api.Assertions +import org.junit.Test +import org.bouncycastle.asn1.x500.X500Name +import java.io.NotSerializableException + +class MapsSerializationTest : TestDependencyInjectionBase() { + + private val smallMap = mapOf("foo" to "bar", "buzz" to "bull") + + @Test + fun `check EmptyMap serialization`() = amqpSpecific("kotlin.collections.EmptyMap is not enabled for Kryo serialization") { + assertEqualAfterRoundTripSerialization(emptyMap()) + } + + @Test + fun `check Map can be root of serialization graph`() { + assertEqualAfterRoundTripSerialization(smallMap) + } + + @Test + fun `check list can be serialized as part of SessionData`() { + val sessionData = SessionData(123, smallMap) + assertEqualAfterRoundTripSerialization(sessionData) + } + + @CordaSerializable + data class WrongPayloadType(val payload: HashMap) + + @Test + fun `check throws for forbidden declared type`() = amqpSpecific("Such exceptions are not expected in Kryo mode.") { + val payload = HashMap(smallMap) + val wrongPayloadType = WrongPayloadType(payload) + Assertions.assertThatThrownBy { wrongPayloadType.serialize() } + .isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive map type for declaredType") + } + + @CordaSerializable + data class MyKey(val keyContent: Double) + + @CordaSerializable + data class MyValue(val valueContent: X500Name) + + @Test + fun `check map serialization works with custom types`() { + val myMap = mapOf( + MyKey(1.0) to MyValue(X500Name("CN=one")), + MyKey(10.0) to MyValue(X500Name("CN=ten"))) + assertEqualAfterRoundTripSerialization(myMap) + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt index 5cc7bbc125..4f9b7b6872 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt @@ -1,8 +1,10 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.serialization.SerializedBytes import org.apache.qpid.proton.codec.Data import net.corda.nodeapi.internal.serialization.AllWhitelist import net.corda.nodeapi.internal.serialization.EmptyWhitelist +import java.io.NotSerializableException fun testDefaultFactory() = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) fun testDefaultFactoryWithWhitelist() = SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader()) @@ -17,3 +19,18 @@ class TestSerializationOutput( super.writeSchema(schema, data) } } + +fun testName(): String = Thread.currentThread().stackTrace[2].methodName + +data class BytesAndSchema(val obj: SerializedBytes, val schema: Schema) + +// Extension for the serialize routine that returns the scheme encoded into the +// bytes as well as the bytes for simple testing +@Throws(NotSerializableException::class) +fun SerializationOutput.serializeAndReturnSchema(obj: T): BytesAndSchema { + try { + return BytesAndSchema(_serialize(obj), Schema(schemaHistory.toList())) + } finally { + andFinally() + } +} diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt index db59a9c2c8..7b9c7a4e39 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt @@ -7,11 +7,9 @@ import kotlin.test.assertNotEquals import kotlin.test.assertTrue class DeserializeAndReturnEnvelopeTests { - - fun testName(): String = Thread.currentThread().stackTrace[2].methodName - + // the 'this' reference means we can't just move this to the common test utils @Suppress("NOTHING_TO_INLINE") - inline fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz" + inline private fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz" @Test fun oneType() { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt index 162756b06c..37dfb8d3f4 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt @@ -1,6 +1,8 @@ package net.corda.nodeapi.internal.serialization.amqp +import org.assertj.core.api.Assertions import org.junit.Test +import java.io.NotSerializableException import java.util.* class DeserializeCollectionTests { @@ -11,7 +13,7 @@ class DeserializeCollectionTests { private const val VERBOSE = false } - val sf = testDefaultFactory() + private val sf = testDefaultFactory() @Test fun mapTest() { @@ -57,7 +59,7 @@ class DeserializeCollectionTests { DeserializationInput(sf).deserialize(serialisedBytes) } - @Test(expected=java.io.NotSerializableException::class) + @Test fun dictionaryTest() { data class C(val c: Dictionary) val v : Hashtable = Hashtable() @@ -66,10 +68,11 @@ class DeserializeCollectionTests { val c = C(v) // expected to throw - TestSerializationOutput(VERBOSE, sf).serialize(c) + Assertions.assertThatThrownBy { TestSerializationOutput(VERBOSE, sf).serialize(c) } + .isInstanceOf(IllegalArgumentException::class.java).hasMessageContaining("Unable to serialise deprecated type class java.util.Dictionary.") } - @Test(expected=java.lang.IllegalArgumentException::class) + @Test fun hashtableTest() { data class C(val c: Hashtable) val v : Hashtable = Hashtable() @@ -78,24 +81,27 @@ class DeserializeCollectionTests { val c = C(v) // expected to throw - TestSerializationOutput(VERBOSE, sf).serialize(c) + Assertions.assertThatThrownBy { TestSerializationOutput(VERBOSE, sf).serialize(c) } + .isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive map type for declaredType") } - @Test(expected=java.lang.IllegalArgumentException::class) + @Test fun hashMapTest() { data class C(val c : HashMap) val c = C (HashMap (mapOf("A" to 1, "B" to 2))) // expect this to throw - TestSerializationOutput(VERBOSE, sf).serialize(c) + Assertions.assertThatThrownBy { TestSerializationOutput(VERBOSE, sf).serialize(c) } + .isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive map type for declaredType") } - @Test(expected=java.lang.IllegalArgumentException::class) + @Test fun weakHashMapTest() { data class C(val c : WeakHashMap) val c = C (WeakHashMap (mapOf("A" to 1, "B" to 2))) - TestSerializationOutput(VERBOSE, sf).serialize(c) + Assertions.assertThatThrownBy { TestSerializationOutput(VERBOSE, sf).serialize(c) } + .isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive map type for declaredType") } @Test diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt new file mode 100644 index 0000000000..d46ec93c5a --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt @@ -0,0 +1,189 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import org.junit.Test +import java.time.DayOfWeek + +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +import java.io.File +import java.io.NotSerializableException + +import net.corda.core.serialization.SerializedBytes + +class EnumTests { + enum class Bras { + TSHIRT, UNDERWIRE, PUSHUP, BRALETTE, STRAPLESS, SPORTS, BACKLESS, PADDED + } + + // The state of the OldBras enum when the tests in changedEnum1 were serialised + // - use if the test file needs regenerating + //enum class OldBras { + // TSHIRT, UNDERWIRE, PUSHUP, BRALETTE + //} + + // the new state, SPACER has been added to change the ordinality + enum class OldBras { + SPACER, TSHIRT, UNDERWIRE, PUSHUP, BRALETTE + } + + // The state of the OldBras2 enum when the tests in changedEnum2 were serialised + // - use if the test file needs regenerating + //enum class OldBras2 { + // TSHIRT, UNDERWIRE, PUSHUP, BRALETTE + //} + + // the new state, note in the test we serialised with value UNDERWIRE so the spacer + // occuring after this won't have changed the ordinality of our serialised value + // and thus should still be deserialisable + enum class OldBras2 { + TSHIRT, UNDERWIRE, PUSHUP, SPACER, BRALETTE, SPACER2 + } + + + enum class BrasWithInit (val someList: List) { + TSHIRT(emptyList()), + UNDERWIRE(listOf(1, 2, 3)), + PUSHUP(listOf(100, 200)), + BRALETTE(emptyList()) + } + + private val brasTestName = "${this.javaClass.name}\$Bras" + + companion object { + /** + * If you want to see the schema encoded into the envelope after serialisation change this to true + */ + private const val VERBOSE = false + } + + @Suppress("NOTHING_TO_INLINE") + inline private fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz" + + private val sf1 = testDefaultFactory() + + @Test + fun serialiseSimpleTest() { + data class C(val c: Bras) + + val schema = TestSerializationOutput(VERBOSE, sf1).serializeAndReturnSchema(C(Bras.UNDERWIRE)).schema + + assertEquals(2, schema.types.size) + val schema_c = schema.types.find { it.name == classTestName("C") } as CompositeType + val schema_bras = schema.types.find { it.name == brasTestName } as RestrictedType + + assertNotNull(schema_c) + assertNotNull(schema_bras) + + assertEquals(1, schema_c.fields.size) + assertEquals("c", schema_c.fields.first().name) + assertEquals(brasTestName, schema_c.fields.first().type) + + assertEquals(8, schema_bras.choices.size) + Bras.values().forEach { + val bra = it + assertNotNull (schema_bras.choices.find { it.name == bra.name }) + } + } + + @Test + fun deserialiseSimpleTest() { + data class C(val c: Bras) + + val objAndEnvelope = DeserializationInput(sf1).deserializeAndReturnEnvelope( + TestSerializationOutput(VERBOSE, sf1).serialize(C(Bras.UNDERWIRE))) + + val obj = objAndEnvelope.obj + val schema = objAndEnvelope.envelope.schema + + assertEquals(2, schema.types.size) + val schema_c = schema.types.find { it.name == classTestName("C") } as CompositeType + val schema_bras = schema.types.find { it.name == brasTestName } as RestrictedType + + assertEquals(1, schema_c.fields.size) + assertEquals("c", schema_c.fields.first().name) + assertEquals(brasTestName, schema_c.fields.first().type) + + assertEquals(8, schema_bras.choices.size) + Bras.values().forEach { + val bra = it + assertNotNull (schema_bras.choices.find { it.name == bra.name }) + } + + // Test the actual deserialised object + assertEquals(obj.c, Bras.UNDERWIRE) + } + + @Test + fun multiEnum() { + data class Support (val top: Bras, val day : DayOfWeek) + data class WeeklySupport (val tops: List) + + val week = WeeklySupport (listOf( + Support (Bras.PUSHUP, DayOfWeek.MONDAY), + Support (Bras.UNDERWIRE, DayOfWeek.WEDNESDAY), + Support (Bras.PADDED, DayOfWeek.SUNDAY))) + + val obj = DeserializationInput(sf1).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(week)) + + assertEquals(week.tops[0].top, obj.tops[0].top) + assertEquals(week.tops[0].day, obj.tops[0].day) + assertEquals(week.tops[1].top, obj.tops[1].top) + assertEquals(week.tops[1].day, obj.tops[1].day) + assertEquals(week.tops[2].top, obj.tops[2].top) + assertEquals(week.tops[2].day, obj.tops[2].day) + } + + @Test + fun enumWithInit() { + data class C(val c: BrasWithInit) + + val c = C (BrasWithInit.PUSHUP) + val obj = DeserializationInput(sf1).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(c)) + + assertEquals(c.c, obj.c) + } + + @Test(expected = NotSerializableException::class) + fun changedEnum1() { + val path = EnumTests::class.java.getResource("EnumTests.changedEnum1") + val f = File(path.toURI()) + + data class C (val a: OldBras) + + // Original version of the class for the serialised version of this class + // + // val a = OldBras.TSHIRT + // val sc = SerializationOutput(sf1).serialize(C(a)) + // f.writeBytes(sc.bytes) + // println(path) + + val sc2 = f.readBytes() + + // we expect this to throw + DeserializationInput(sf1).deserialize(SerializedBytes(sc2)) + } + + @Test(expected = NotSerializableException::class) + fun changedEnum2() { + val path = EnumTests::class.java.getResource("EnumTests.changedEnum2") + val f = File(path.toURI()) + + data class C (val a: OldBras2) + + // DO NOT CHANGE THIS, it's important we serialise with a value that doesn't + // change position in the upated enum class + + // Original version of the class for the serialised version of this class + // + // val a = OldBras2.UNDERWIRE + // val sc = SerializationOutput(sf1).serialize(C(a)) + // f.writeBytes(sc.bytes) + // println(path) + + val sc2 = f.readBytes() + + // we expect this to throw + DeserializationInput(sf1).deserialize(SerializedBytes(sc2)) + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt index fd8e6b3058..0bf23ae85d 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt @@ -1,8 +1,7 @@ package net.corda.nodeapi.internal.serialization.amqp -import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.DeprecatedConstructorForDeserialization - +import net.corda.core.serialization.SerializedBytes import org.junit.Test import java.io.File import java.io.NotSerializableException diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt index 3e83fe6f62..8945903f6b 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt @@ -12,15 +12,17 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.LedgerTransaction import net.corda.nodeapi.RPCException import net.corda.nodeapi.internal.serialization.AbstractAMQPSerializationScheme +import net.corda.nodeapi.internal.serialization.AllWhitelist import net.corda.nodeapi.internal.serialization.EmptyWhitelist import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.isPrimitive -import net.corda.nodeapi.internal.serialization.AllWhitelist import net.corda.testing.BOB_IDENTITY import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_PUBKEY import org.apache.qpid.proton.amqp.* import org.apache.qpid.proton.codec.DecoderImpl import org.apache.qpid.proton.codec.EncoderImpl +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertSame import org.junit.Ignore import org.junit.Test import java.io.IOException @@ -29,9 +31,7 @@ import java.math.BigDecimal import java.nio.ByteBuffer import java.time.* import java.time.temporal.ChronoUnit -import java.time.temporal.TemporalUnit import java.util.* -import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -140,13 +140,13 @@ class SerializationOutputTests { data class PolymorphicProperty(val foo: FooInterface?) - private fun serdes(obj: Any, + private inline fun serdes(obj: T, factory: SerializerFactory = SerializerFactory ( AllWhitelist, ClassLoader.getSystemClassLoader()), freshDeserializationFactory: SerializerFactory = SerializerFactory( AllWhitelist, ClassLoader.getSystemClassLoader()), expectedEqual: Boolean = true, - expectDeserializedEqual: Boolean = true): Any { + expectDeserializedEqual: Boolean = true): T { val ser = SerializationOutput(factory) val bytes = ser.serialize(obj) @@ -158,6 +158,7 @@ class SerializationOutputTests { this.register(CompositeType.DESCRIPTOR, CompositeType.Companion) this.register(Choice.DESCRIPTOR, Choice.Companion) this.register(RestrictedType.DESCRIPTOR, RestrictedType.Companion) + this.register(ReferencedObject.DESCRIPTOR, ReferencedObject.Companion) } EncoderImpl(decoder) decoder.setByteBuffer(ByteBuffer.wrap(bytes.bytes, 8, bytes.size - 8)) @@ -265,7 +266,7 @@ class SerializationOutputTests { serdes(obj) } - @Test(expected = NotSerializableException::class) + @Test fun `test top level list array`() { val obj = arrayOf(listOf("Fred", "Ginger"), listOf("Rogers", "Hammerstein")) serdes(obj) @@ -436,7 +437,7 @@ class SerializationOutputTests { throw IllegalStateException("Layer 2", t) } } catch(t: Throwable) { - val desThrowable = serdes(t, factory, factory2, false) as Throwable + val desThrowable = serdes(t, factory, factory2, false) assertSerializedThrowableEquivalent(t, desThrowable) } } @@ -468,7 +469,7 @@ class SerializationOutputTests { throw e } } catch(t: Throwable) { - val desThrowable = serdes(t, factory, factory2, false) as Throwable + val desThrowable = serdes(t, factory, factory2, false) assertSerializedThrowableEquivalent(t, desThrowable) } } @@ -538,7 +539,6 @@ class SerializationOutputTests { AbstractAMQPSerializationScheme.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) @@ -763,4 +763,107 @@ class SerializationOutputTests { val obj = StateRef(SecureHash.randomSHA256(), 0) serdes(obj, factory, factory2) } -} + + interface Container + + data class SimpleContainer(val one: String, val another: String) : Container + + data class ParentContainer(val left: SimpleContainer, val right: Container) + + @Test + fun `test object referenced multiple times`() { + val simple = SimpleContainer("Fred", "Ginger") + val parentContainer = ParentContainer(simple, simple) + assertSame(parentContainer.left, parentContainer.right) + + val parentCopy = serdes(parentContainer) + assertSame(parentCopy.left, parentCopy.right) + } + + data class TestNode(val content: String, val children: MutableCollection = ArrayList()) + + @Test + @Ignore("Ignored due to cyclic graphs not currently supported by AMQP serialization") + fun `test serialization of cyclic graph`() { + val nodeA = TestNode("A") + val nodeB = TestNode("B", ArrayList(Arrays.asList(nodeA))) + nodeA.children.add(nodeB) + + // Also blows with StackOverflow error + assertTrue(nodeB.hashCode() > 0) + + val bCopy = serdes(nodeB) + assertEquals("A", bCopy.children.single().content) + } + + data class Bob(val byteArrays: List) + + @Ignore("Causes DeserializedParameterizedType.make() to fail") + @Test + fun `test list of byte arrays`() { + val a = ByteArray(1) + val b = ByteArray(2) + val obj = Bob(listOf(a, b, a)) + + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + serdes(obj, factory, factory2) + } + + data class Vic(val a: List, val b: List) + + @Test + fun `test generics ignored from graph logic`() { + val a = listOf("a", "b") + val obj = Vic(a, a) + + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + val objCopy = serdes(obj, factory, factory2) + assertSame(objCopy.a, objCopy.b) + } + + data class Spike private constructor(val a: String) { + constructor() : this("a") + } + + @Test + fun `test private constructor`() { + val obj = Spike() + + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + serdes(obj, factory, factory2) + } + + data class BigDecimals(val a: BigDecimal, val b: BigDecimal) + + @Test + fun `test toString custom serializer`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer) + + val obj = BigDecimals(BigDecimal.TEN, BigDecimal.TEN) + val objCopy = serdes(obj, factory, factory2) + assertEquals(objCopy.a, objCopy.b) + } + + data class ByteArrays(val a: ByteArray, val b: ByteArray) + + @Test + fun `test byte arrays not reference counted`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer) + + val bytes = ByteArray(1) + val obj = ByteArrays(bytes, bytes) + val objCopy = serdes(obj, factory, factory2, false, false) + assertNotSame(objCopy.a, objCopy.b) + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializeAndReturnSchemaTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializeAndReturnSchemaTest.kt new file mode 100644 index 0000000000..86b769a14f --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializeAndReturnSchemaTest.kt @@ -0,0 +1,34 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class SerializeAndReturnSchemaTest { + // the 'this' reference means we can't just move this to the common test utils + @Suppress("NOTHING_TO_INLINE") + inline private fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz" + + val factory = testDefaultFactory() + + // just a simple test to verify the internal test extension for serialize does + // indeed give us the correct schema back. This is more useful in support of other + // tests rather than by itself but for those to be reliable this also needs + // testing + @Test + fun getSchema() { + data class C(val a: Int, val b: Int) + val a = 1 + val b = 2 + + val sc = SerializationOutput(factory).serializeAndReturnSchema(C(a, b)) + + assertEquals(1, sc.schema.types.size) + assertEquals(classTestName("C"), sc.schema.types.first().name) + assertTrue(sc.schema.types.first() is CompositeType) + assertEquals(2, (sc.schema.types.first() as CompositeType).fields.size) + assertNotNull((sc.schema.types.first() as CompositeType).fields.find { it.name == "a" }) + assertNotNull((sc.schema.types.first() as CompositeType).fields.find { it.name == "b" }) + } +} diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumTests.changedEnum1 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumTests.changedEnum1 new file mode 100644 index 0000000000..800c35bf17 Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumTests.changedEnum1 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumTests.changedEnum2 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumTests.changedEnum2 new file mode 100644 index 0000000000..5f42911837 Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumTests.changedEnum2 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParam b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParam index 375f8642c2..06c32d9eff 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParam and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParam differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory index eb7fdfc160..4b5eb1dda9 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters index 5a45bb3e39..d31d1b26fd 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor index a06dc1fbfc..6daa3c7714 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructorUnAnnotated b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructorUnAnnotated index f77b8e5c4d..e47d5270f8 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructorUnAnnotated and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructorUnAnnotated differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor index 7500dbd59b..290fad125a 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval index 32d79fc258..769ea67e2d 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.changeSubType b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.changeSubType index bcc0475b4a..1158e284fe 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.changeSubType and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.changeSubType differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.1 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.1 index 263bd776f7..46714457ec 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.1 and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.1 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.2 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.2 index e11895f9fc..3f467b2390 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.2 and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.2 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.3 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.3 index 1117d04f96..6b3e9a3a37 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.3 and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.3 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.1 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.1 index a411a89c16..fa424bf678 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.1 and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.1 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.2 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.2 index f0eac50a93..bc41d9c9d0 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.2 and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.2 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.3 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.3 index fdb3336d1c..f8a7705470 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.3 and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.3 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters index 375f8642c2..c0d7333de5 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapDifferentType b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapDifferentType index 4b6bf6a7b8..2e6a7098f6 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapDifferentType and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapDifferentType differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType index b3852fe5ff..6e04f4bdf3 100644 Binary files a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType differ diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index 0ce676a96b..e81eb844c0 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -36,10 +36,9 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) { capsuleManifest { applicationVersion = corda_release_version appClassPath = ["jolokia-agent-war-${project.rootProject.ext.jolokia_version}.war"] - // TODO add this once we upgrade quasar to 0.7.8 - // def quasarExcludeExpression = "x(rx**;io**;kotlin**;jdk**;reflectasm**;groovyjarjarasm**;groovy**;joptsimple**;groovyjarjarantlr**;javassist**;com.fasterxml**;com.typesafe**;com.google**;com.zaxxer**;com.jcabi**;com.codahale**;com.esotericsoftware**;de.javakaffee**;org.objectweb**;org.slf4j**;org.w3c**;org.codehaus**;org.h2**;org.crsh**;org.fusesource**;org.hibernate**;org.dom4j**;org.bouncycastle**;org.apache**;org.objenesis**;org.jboss**;org.xml**;org.jcp**;org.jetbrains**;org.yaml**;co.paralleluniverse**;net.i2p**)" - // javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"] - javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"] + // See experimental/quasar-hook/README.md for how to generate. + def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)" + javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"] systemProperties['visualvm.display.name'] = 'CordaEnterprise' minJavaVersion = '1.8.0' minUpdateVersion['1.8'] = java8_minUpdateVersion 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 f1bb21c2d0..67cf30d78c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -41,12 +41,12 @@ class BootTests { val logConfigFile = projectRootDir / "config" / "dev" / "log4j2.xml" assertThat(logConfigFile).isRegularFile() driver(isDebug = true, systemProperties = mapOf("log4j.configurationFile" to logConfigFile.toString())) { - val alice = startNode(ALICE.name).get() + val alice = startNode(providedName = ALICE.name).get() 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 { - startNode(ALICE.name).getOrThrow() + startNode(providedName = ALICE.name).getOrThrow() } // We count the number of nodes that wrote into the logfile by counting "Logs can be found in" val numberOfNodesThatLogged = Files.lines(logFile.toPath()).filter { NodeStartup.LOGS_CAN_BE_FOUND_IN_STRING in it }.count() @@ -58,7 +58,7 @@ class BootTests { 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) + val future = startNode(providedName = ALICE.name, advertisedServices = tooManyAdvertisedServices) assertFailsWith(ListenProcessDeathException::class) { future.getOrThrow() } } } 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 2417f1d329..926dfb1baa 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt @@ -25,8 +25,8 @@ class CordappScanningDriverTest { // The driver will automatically pick up the annotated flows below driver { val (alice, bob) = listOf( - startNode(ALICE.name, rpcUsers = listOf(user)), - startNode(BOB.name)).transpose().getOrThrow() + startNode(providedName = ALICE.name, rpcUsers = listOf(user)), + startNode(providedName = BOB.name)).transpose().getOrThrow() val initiatedFlowClass = alice.rpcClientToNode() .start(user.username, user.password) .proxy diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AdvertisedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AdvertisedServiceTests.kt index 204058c519..55a7ac3bf0 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AdvertisedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AdvertisedServiceTests.kt @@ -6,15 +6,15 @@ import net.corda.core.flows.StartableByRPC import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType +import net.corda.core.utilities.getX500Name import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.nodeapi.User import net.corda.testing.driver.driver -import org.bouncycastle.asn1.x500.X500Name import org.junit.Test import kotlin.test.assertTrue class AdvertisedServiceTests { - private val serviceName = X500Name("CN=Custom Service,O=R3,OU=corda,L=London,C=GB") + private val serviceName = getX500Name(O = "Custom Service", OU = "corda", L = "London", C = "GB") private val serviceType = ServiceType.corda.getSubType("custom") private val user = "bankA" private val pass = "passA" 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 bba066f2be..eaa4fcb2a2 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 @@ -3,18 +3,19 @@ package net.corda.node.services import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.composite.CompositeKey -import net.corda.core.internal.div +import net.corda.core.crypto.CompositeKey import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow import net.corda.core.identity.Party +import net.corda.core.internal.div import net.corda.core.node.services.ServiceInfo +import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.Try import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.getX500Name import net.corda.node.internal.AbstractNode import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.network.NetworkMapService @@ -23,8 +24,8 @@ import net.corda.node.services.transactions.minClusterSize import net.corda.node.services.transactions.minCorrectReplicas import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.testing.contracts.DummyContract +import net.corda.testing.dummyCommand import net.corda.testing.node.MockNetwork -import org.bouncycastle.asn1.x500.X500Name import org.junit.After import org.junit.Test import java.nio.file.Files @@ -33,7 +34,7 @@ import kotlin.test.assertTrue class BFTNotaryServiceTests { companion object { - private val clusterName = X500Name("CN=BFT,O=R3,OU=corda,L=Zurich,C=CH") + private val clusterName = getX500Name(O = "BFT", OU = "corda", L = "Zurich", C = "CH") private val serviceType = BFTNonValidatingNotaryService.type } @@ -70,7 +71,9 @@ class BFTNotaryServiceTests { fun `all replicas start even if there is a new consensus during startup`() { val notary = bftNotaryCluster(minClusterSize(1), true) // This true adds a sleep to expose the race. val f = node.run { - val trivialTx = signInitialTransaction(notary) {} + val trivialTx = signInitialTransaction(notary) { + addOutputState(DummyContract.SingleOwnerState(owner = info.legalIdentity)) + } // Create a new consensus while the redundant replica is sleeping: services.startFlow(NotaryFlow.Client(trivialTx)).resultFuture } @@ -99,7 +102,7 @@ class BFTNotaryServiceTests { services.recordTransactions(issueTx) } val spendTxs = (1..10).map { - signInitialTransaction(notary, true) { + signInitialTransaction(notary) { addInputState(issueTx.tx.outRef(0)) } } @@ -137,11 +140,11 @@ class BFTNotaryServiceTests { private fun AbstractNode.signInitialTransaction( notary: Party, - makeUnique: Boolean = false, block: TransactionBuilder.() -> Any? -) = services.signInitialTransaction(TransactionBuilder(notary).apply { - block() - if (makeUnique) { - addAttachment(SecureHash.randomSHA256()) - } -}) +): SignedTransaction { + return services.signInitialTransaction( + TransactionBuilder(notary).apply { + addCommand(dummyCommand(services.legalIdentityKey)) + block() + }) +} 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 26442c108c..1e6daa63e6 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 @@ -38,7 +38,7 @@ class DistributedServiceTests : DriverBasedTest() { startFlowPermission(), startFlowPermission()) ) - val aliceFuture = startNode(ALICE.name, rpcUsers = listOf(testUser)) + val aliceFuture = startNode(providedName = ALICE.name, rpcUsers = listOf(testUser)) val notariesFuture = startNotaryCluster( DUMMY_NOTARY.name, rpcUsers = listOf(testUser), 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 b999da66d2..f975a1225a 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 @@ -2,26 +2,27 @@ package net.corda.node.services import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef -import net.corda.testing.contracts.DummyContract -import net.corda.core.identity.Party -import net.corda.testing.DUMMY_BANK_A import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow +import net.corda.core.identity.Party import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.transpose -import net.corda.core.utilities.getOrThrow import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.getX500Name import net.corda.node.internal.AbstractNode +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.contracts.DummyContract +import net.corda.testing.dummyCommand import net.corda.testing.node.NodeBasedTest -import org.bouncycastle.asn1.x500.X500Name import org.junit.Test import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith class RaftNotaryServiceTests : NodeBasedTest() { - private val notaryName = X500Name("CN=RAFT Notary Service,O=R3,OU=corda,L=London,C=GB") + private val notaryName = getX500Name(O = "RAFT Notary Service", OU = "corda", L = "London", C = "GB") @Test fun `detect double spend`() { @@ -34,7 +35,9 @@ class RaftNotaryServiceTests : NodeBasedTest() { val inputState = issueState(bankA, notaryParty) - val firstTxBuilder = TransactionBuilder(notaryParty).withItems(inputState) + val firstTxBuilder = TransactionBuilder(notaryParty) + .addInputState(inputState) + .addCommand(dummyCommand(bankA.services.legalIdentityKey)) val firstSpendTx = bankA.services.signInitialTransaction(firstTxBuilder) val firstSpend = bankA.services.startFlow(NotaryFlow.Client(firstSpendTx)) @@ -43,6 +46,7 @@ class RaftNotaryServiceTests : NodeBasedTest() { val secondSpendBuilder = TransactionBuilder(notaryParty).withItems(inputState).run { val dummyState = DummyContract.SingleOwnerState(0, bankA.info.legalIdentity) addOutputState(dummyState) + addCommand(dummyCommand(bankA.services.legalIdentityKey)) this } val secondSpendTx = bankA.services.signInitialTransaction(secondSpendBuilder) 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 9ea00ba466..2dafbab440 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 @@ -30,8 +30,10 @@ class FlowVersioningTest : NodeBasedTest() { private class PretendInitiatingCoreFlow(val initiatedParty: Party) : FlowLogic>() { @Suspendable override fun call(): Pair { + // Execute receive() outside of the Pair constructor to avoid Kotlin/Quasar instrumentation bug. + val alicePlatformVersionAccordingToBob = receive(initiatedParty).unwrap { it } return Pair( - receive(initiatedParty).unwrap { it }, + alicePlatformVersionAccordingToBob, getFlowContext(initiatedParty).flowVersion ) } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt index caf80073d6..cf0e136b88 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt @@ -1,19 +1,18 @@ package net.corda.node.services.statemachine import co.paralleluniverse.fibers.Suspendable -import net.corda.core.internal.InputStreamAndHash import net.corda.core.crypto.SecureHash import net.corda.core.flows.* import net.corda.core.identity.Party +import net.corda.core.internal.InputStreamAndHash import net.corda.core.messaging.startFlow -import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.unwrap import net.corda.testing.BOB import net.corda.testing.DUMMY_NOTARY import net.corda.testing.aliceBobAndNotary import net.corda.testing.contracts.DummyState import net.corda.testing.driver.driver +import net.corda.testing.dummyCommand import org.junit.Test import kotlin.test.assertEquals @@ -28,6 +27,7 @@ class LargeTransactionsTest { override fun call() { val tx = TransactionBuilder(notary = DUMMY_NOTARY) .addOutputState(DummyState()) + .addCommand(dummyCommand(serviceHub.legalIdentityKey)) .addAttachment(hash1) .addAttachment(hash2) .addAttachment(hash3) 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 b16f87f806..af40eb7adf 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 @@ -3,7 +3,7 @@ package net.corda.services.messaging import co.paralleluniverse.fibers.Suspendable import net.corda.client.rpc.CordaRPCClient import net.corda.core.crypto.generateKeyPair -import net.corda.core.crypto.toBase58String +import net.corda.core.utilities.toBase58String import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow 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 7c96c484e4..dd79a907f8 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 @@ -12,6 +12,7 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.getX500Name import net.corda.core.utilities.seconds import net.corda.node.internal.Node import net.corda.node.services.messaging.* @@ -26,13 +27,12 @@ import org.junit.Test import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger class P2PMessagingTest : NodeBasedTest() { private companion object { - val DISTRIBUTED_SERVICE_NAME = getTestX509Name("DistributedService") - val SERVICE_2_NAME = getTestX509Name("Service 2") + val DISTRIBUTED_SERVICE_NAME = getX500Name(O = "DistributedService", L = "London", C = "GB") + val SERVICE_2_NAME = getX500Name(O = "Service 2", L = "London", C = "GB") } @Test @@ -43,6 +43,7 @@ class P2PMessagingTest : NodeBasedTest() { val startUpDuration = elapsedTime { startNodes().getOrThrow() } // Start the network map a second time - this will restore message queues from the journal. // This will hang and fail prior the fix. https://github.com/corda/corda/issues/37 + clearAllNodeInfoDb() // Clear network map data from nodes databases. stopAllNodes() startNodes().getOrThrow(timeout = startUpDuration * 3) } @@ -104,17 +105,23 @@ class P2PMessagingTest : NodeBasedTest() { val dummyTopic = "dummy.topic" val responseMessage = "response" - simulateCrashingNode(distributedServiceNodes, dummyTopic, responseMessage) + val crashingNodes = simulateCrashingNodes(distributedServiceNodes, dummyTopic, responseMessage) // Send a single request with retry - val response = with(alice.network) { + val responseFuture = with(alice.network) { val request = TestRequest(replyTo = myAddress) val responseFuture = onNext(dummyTopic, request.sessionID) val msg = createMessage(TopicSession(dummyTopic), data = request.serialize().bytes) send(msg, serviceAddress, retryId = request.sessionID) responseFuture - }.getOrThrow(10.seconds) + } + crashingNodes.firstRequestReceived.await(5, TimeUnit.SECONDS) + // The request wasn't successful. + assertThat(responseFuture.isDone).isFalse() + crashingNodes.ignoreRequests = false + // The retry should be successful. + val response = responseFuture.getOrThrow(10.seconds) assertThat(response).isEqualTo(responseMessage) } @@ -129,7 +136,7 @@ class P2PMessagingTest : NodeBasedTest() { val dummyTopic = "dummy.topic" val responseMessage = "response" - val (firstRequestReceived, requestsReceived) = simulateCrashingNode(distributedServiceNodes, dummyTopic, responseMessage) + val crashingNodes = simulateCrashingNodes(distributedServiceNodes, dummyTopic, responseMessage) val sessionId = random63BitValue() @@ -141,38 +148,48 @@ class P2PMessagingTest : NodeBasedTest() { } // Wait until the first request is received - firstRequestReceived.await(5, TimeUnit.SECONDS) - // Stop alice's node before the request is redelivered – the first request is ignored + crashingNodes.firstRequestReceived.await(5, TimeUnit.SECONDS) + // Stop alice's node after we ensured that the first request was delivered and ignored. alice.stop() - assertThat(requestsReceived.get()).isEqualTo(1) + val numberOfRequestsReceived = crashingNodes.requestsReceived.get() + assertThat(numberOfRequestsReceived).isGreaterThanOrEqualTo(1) + + crashingNodes.ignoreRequests = false // Restart the node and expect a response val aliceRestarted = startNode(ALICE.name, configOverrides = mapOf("messageRedeliveryDelaySeconds" to 1)).getOrThrow() val response = aliceRestarted.network.onNext(dummyTopic, sessionId).getOrThrow(5.seconds) - assertThat(requestsReceived.get()).isGreaterThanOrEqualTo(2) + assertThat(crashingNodes.requestsReceived.get()).isGreaterThan(numberOfRequestsReceived) assertThat(response).isEqualTo(responseMessage) } + data class CrashingNodes( + val firstRequestReceived: CountDownLatch, + val requestsReceived: AtomicInteger, + var ignoreRequests: Boolean + ) + /** - * Sets up the [distributedServiceNodes] to respond to [dummyTopic] requests. The first node in the service to - * receive a request will ignore it and all subsequent requests. This simulates the scenario where a node receives - * a request message, but crashes before sending back a response. The other nodes will respond to _all_ requests. + * Sets up the [distributedServiceNodes] to respond to [dummyTopic] requests. All nodes will receive requests and + * either ignore them or respond, depending on the value of [CrashingNodes.ignoreRequests], initially set to true. + * This may be used to simulate scenarios where nodes receive request messages but crash before sending back a response. */ - private fun simulateCrashingNode(distributedServiceNodes: List, dummyTopic: String, responseMessage: String): Pair { - val firstToReceive = AtomicBoolean(true) - val requestsReceived = AtomicInteger(0) - val firstRequestReceived = CountDownLatch(1) + private fun simulateCrashingNodes(distributedServiceNodes: List, dummyTopic: String, responseMessage: String): CrashingNodes { + val crashingNodes = CrashingNodes( + requestsReceived = AtomicInteger(0), + firstRequestReceived = CountDownLatch(1), + ignoreRequests = true + ) + distributedServiceNodes.forEach { val nodeName = it.info.legalIdentity.name - var ignoreRequests = false it.network.addMessageHandler(dummyTopic) { netMessage, _ -> - requestsReceived.incrementAndGet() - firstRequestReceived.countDown() + crashingNodes.requestsReceived.incrementAndGet() + crashingNodes.firstRequestReceived.countDown() // The node which receives the first request will ignore all requests - if (firstToReceive.getAndSet(false)) ignoreRequests = true print("$nodeName: Received request - ") - if (ignoreRequests) { + if (crashingNodes.ignoreRequests) { println("ignoring") // Requests are ignored to simulate a service node crashing before sending back a response. // A retry by the client will result in the message being redelivered to another node in the service cluster. @@ -184,7 +201,7 @@ class P2PMessagingTest : NodeBasedTest() { } } } - return Pair(firstRequestReceived, requestsReceived) + return crashingNodes } private fun assertAllNodesAreUsed(participatingServiceNodes: List, serviceName: X500Name, originatingNode: Node) { 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 0e32802348..76918e4a74 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 @@ -2,12 +2,9 @@ package net.corda.services.messaging import com.nhaarman.mockito_kotlin.whenever import net.corda.core.concurrent.CordaFuture -import net.corda.core.crypto.cert import net.corda.core.crypto.random63BitValue import net.corda.core.node.NodeInfo -import net.corda.core.utilities.seconds -import net.corda.core.utilities.NonEmptySet -import net.corda.core.utilities.getOrThrow +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 @@ -30,7 +27,7 @@ class P2PSecurityTest : NodeBasedTest() { @Test fun `incorrect legal name for the network map service config`() { - val incorrectNetworkMapName = getTestX509Name("NetworkMap-${random63BitValue()}") + val incorrectNetworkMapName = getX500Name(O = "NetworkMap-${random63BitValue()}", L = "London", C = "GB") val node = startNode(BOB.name, configOverrides = mapOf( "networkMapService" to mapOf( "address" to networkMapNode.configuration.p2pAddress.toString(), @@ -67,7 +64,7 @@ class P2PSecurityTest : NodeBasedTest() { private fun SimpleNode.registerWithNetworkMap(registrationName: X500Name): CordaFuture { val legalIdentity = getTestPartyAndCertificate(registrationName, identity.public) - val nodeInfo = NodeInfo(listOf(MOCK_HOST_AND_PORT), legalIdentity, NonEmptySet.of(legalIdentity), 1) + val nodeInfo = NodeInfo(listOf(MOCK_HOST_AND_PORT), legalIdentity, NonEmptySet.of(legalIdentity), 1, serial = 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/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt b/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt new file mode 100644 index 0000000000..b6389806de --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt @@ -0,0 +1,160 @@ +package net.corda.test.node + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Command +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.LinearState +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.contracts.requireSingleCommand +import net.corda.core.contracts.requireThat +import net.corda.core.flows.FinalityFlow +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.startFlow +import net.corda.core.node.services.ServiceInfo +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.transactions.LedgerTransaction +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.getOrThrow +import net.corda.node.services.FlowPermissions +import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.nodeapi.User +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.driver.driver +import org.junit.Test +import java.lang.management.ManagementFactory +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table +import kotlin.test.assertEquals + +class NodeStatePersistenceTests { + + @Test + fun `persistent state survives node restart`() { + val user = User("mark", "dadada", setOf(FlowPermissions.startFlowPermission())) + val message = Message("Hello world!") + driver(isDebug = true, startNodesInProcess = isQuasarAgentSpecified()) { + + startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow() + var nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow() + val nodeName = nodeHandle.nodeInfo.legalIdentity.name + nodeHandle.rpcClientToNode().start(user.username, user.password).use { + it.proxy.startFlow(::SendMessageFlow, message).returnValue.getOrThrow() + } + nodeHandle.stop().getOrThrow() + + nodeHandle = startNode(providedName = nodeName, rpcUsers = listOf(user)).getOrThrow() + nodeHandle.rpcClientToNode().start(user.username, user.password).use { + val page = it.proxy.vaultQuery(MessageState::class.java) + val retrievedMessage = page.states.singleOrNull()?.state?.data?.message + assertEquals(message, retrievedMessage) + } + } + } +} + +fun isQuasarAgentSpecified(): Boolean { + val jvmArgs = ManagementFactory.getRuntimeMXBean().inputArguments + return jvmArgs.any { it.startsWith("-javaagent:") && it.endsWith("quasar.jar") } +} + +@CordaSerializable +data class Message(val value: String) + +data class MessageState(val message: Message, val by: Party, override val linearId: UniqueIdentifier = UniqueIdentifier()) : LinearState, QueryableState { + override val contract = MessageContract() + override val participants: List = listOf(by) + + override fun generateMappedObject(schema: MappedSchema): PersistentState { + return when (schema) { + is MessageSchemaV1 -> MessageSchemaV1.PersistentMessage( + by = by.name.toString(), + value = message.value + ) + else -> throw IllegalArgumentException("Unrecognised schema $schema") + } + } + + override fun supportedSchemas(): Iterable = listOf(MessageSchemaV1) +} + +object MessageSchema +object MessageSchemaV1 : MappedSchema( + schemaFamily = MessageSchema.javaClass, + version = 1, + mappedTypes = listOf(PersistentMessage::class.java)) { + + @Entity + @Table(name = "messages") + class PersistentMessage( + @Column(name = "by") + var by: String, + + @Column(name = "value") + var value: String + ) : PersistentState() +} + +open class MessageContract : Contract { + override fun verify(tx: LedgerTransaction) { + val command = tx.commands.requireSingleCommand() + requireThat { + // Generic constraints around the IOU transaction. + "No inputs should be consumed when sending a message." using (tx.inputs.isEmpty()) + "Only one output state should be created." using (tx.outputs.size == 1) + val out = tx.outputsOfType().single() + "Message sender must sign." using (command.signers.containsAll(out.participants.map { it.owningKey })) + + "Message value must not be empty." using (out.message.value.isNotBlank()) + } + } + + interface Commands : CommandData { + class Send : Commands + } +} + +@StartableByRPC +class SendMessageFlow(private val message: Message) : FlowLogic() { + companion object { + object GENERATING_TRANSACTION : ProgressTracker.Step("Generating transaction based on the message.") + object VERIFYING_TRANSACTION : ProgressTracker.Step("Verifying contract constraints.") + object SIGNING_TRANSACTION : ProgressTracker.Step("Signing transaction with our private key.") + object FINALISING_TRANSACTION : ProgressTracker.Step("Obtaining notary signature and recording transaction.") { + override fun childProgressTracker() = FinalityFlow.tracker() + } + + fun tracker() = ProgressTracker(GENERATING_TRANSACTION, VERIFYING_TRANSACTION, SIGNING_TRANSACTION, FINALISING_TRANSACTION) + } + + override val progressTracker = tracker() + + @Suspendable + override fun call(): SignedTransaction { + val notary = serviceHub.networkMapCache.getAnyNotary() + + progressTracker.currentStep = GENERATING_TRANSACTION + + val messageState = MessageState(message = message, by = serviceHub.myInfo.legalIdentity) + val txCommand = Command(MessageContract.Commands.Send(), messageState.participants.map { it.owningKey }) + val txBuilder = TransactionBuilder(notary).withItems(messageState, txCommand) + + progressTracker.currentStep = VERIFYING_TRANSACTION + txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify() + + progressTracker.currentStep = SIGNING_TRANSACTION + val signedTx = serviceHub.signInitialTransaction(txBuilder) + + progressTracker.currentStep = FINALISING_TRANSACTION + return subFlow(FinalityFlow(signedTx, FINALISING_TRANSACTION.childProgressTracker())).single() + } +} \ No newline at end of file diff --git a/node/src/main/java/net/corda/node/shell/RunShellCommand.java b/node/src/main/java/net/corda/node/shell/RunShellCommand.java index 1b5d0efbb4..c388ccfe5f 100644 --- a/node/src/main/java/net/corda/node/shell/RunShellCommand.java +++ b/node/src/main/java/net/corda/node/shell/RunShellCommand.java @@ -1,7 +1,7 @@ package net.corda.node.shell; import net.corda.core.messaging.*; -import net.corda.jackson.*; +import net.corda.client.jackson.*; import org.crsh.cli.*; import org.crsh.command.*; 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 f75f050837..b03cd9e989 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -1,7 +1,6 @@ package net.corda.node.internal import com.codahale.metrics.MetricRegistry -import net.corda.core.internal.VisibleForTesting import com.google.common.collect.Lists import com.google.common.collect.MutableClassToInstanceMap import com.google.common.util.concurrent.MoreExecutors @@ -10,42 +9,43 @@ import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.* import net.corda.core.flows.* +import net.corda.core.flows.ContractUpgradeFlow.Acceptor import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.* +import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.openFuture import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient -import net.corda.core.node.* +import net.corda.core.node.CordaPluginRegistry +import net.corda.core.node.NodeInfo +import net.corda.core.node.PluginServiceHub +import net.corda.core.node.ServiceEntry import net.corda.core.node.services.* import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.debug -import net.corda.core.utilities.toNonEmptySet -import net.corda.node.services.ContractUpgradeHandler +import net.corda.core.utilities.* import net.corda.node.services.NotaryChangeHandler import net.corda.node.services.NotifyTransactionHandler import net.corda.node.services.TransactionKeyHandler import net.corda.node.services.api.* import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate -import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.events.ScheduledActivityObserver -import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.PersistentKeyManagementService 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.PersistentNetworkMapCache import net.corda.node.services.network.PersistentNetworkMapService import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.persistence.DBTransactionMappingStorage @@ -57,6 +57,7 @@ import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.statemachine.flowVersionAndInitiatingClass import net.corda.node.services.transactions.* +import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.HibernateVaultQueryImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager @@ -140,10 +141,15 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, var isPreviousCheckpointsPresent = false private set - protected val _networkMapRegistrationFuture = openFuture() - /** Completes once the node has successfully registered with the network map service */ - val networkMapRegistrationFuture: CordaFuture - get() = _networkMapRegistrationFuture + protected val _nodeReadyFuture = openFuture() + /** Completes once the node has successfully registered with the network map service + * or has loaded network map data from local database */ + val nodeReadyFuture: CordaFuture + get() = _nodeReadyFuture + + protected val myLegalName: X500Name by lazy { + loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword).getX509Certificate(X509Utilities.CORDA_CLIENT_CA).subject.withCommonName(null) + } /** Fetch CordaPluginRegistry classes registered in META-INF/services/net.corda.core.node.CordaPluginRegistry files that exist in the classpath */ open val pluginRegistries: List by lazy { @@ -157,10 +163,6 @@ 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() { require(!started) { "Node has already been started" } @@ -210,7 +212,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } runOnStop += network::stop - _networkMapRegistrationFuture.captureLater(registerWithNetworkMapIfConfigured()) + } + // If we successfully loaded network data from database, we set this future to Unit. + _nodeReadyFuture.captureLater(registerWithNetworkMapIfConfigured()) + database.transaction { smm.start() // Shut down the SMM so no Fibers are scheduled. runOnStop += { smm.stop(acceptableLiveFiberCountOnStop()) } @@ -380,7 +385,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, .filter { it.isUserInvokable() } + // Add any core flows here listOf( - ContractUpgradeFlow::class.java) + ContractUpgradeFlow.Initiator::class.java) } /** @@ -401,7 +406,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, private fun installCoreFlows() { installCoreFlow(BroadcastTransactionFlow::class, ::NotifyTransactionHandler) installCoreFlow(NotaryChangeFlow::class, ::NotaryChangeHandler) - installCoreFlow(ContractUpgradeFlow::class, ::ContractUpgradeHandler) + installCoreFlow(ContractUpgradeFlow.Initiator::class, ::Acceptor) installCoreFlow(TransactionKeyFlow::class, ::TransactionKeyHandler) } @@ -413,7 +418,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, checkpointStorage = DBCheckpointStorage() _services = ServiceHubInternalImpl() attachments = NodeAttachmentService(services.monitoringService.metrics) - val legalIdentity = obtainIdentity("identity", configuration.myLegalName) + val legalIdentity = obtainIdentity() network = makeMessagingService(legalIdentity) info = makeInfo(legalIdentity) @@ -485,9 +490,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, private fun makeInfo(legalIdentity: PartyAndCertificate): NodeInfo { val advertisedServiceEntries = makeServiceEntries() - val allIdentities = (advertisedServiceEntries.map { it.identity } + legalIdentity).toNonEmptySet() + val allIdentities = advertisedServiceEntries.map { it.identity }.toSet() // TODO Add node's legalIdentity (after services removal). val addresses = myAddresses() // TODO There is no support for multiple IP addresses yet. - return NodeInfo(addresses, legalIdentity, allIdentities, platformVersion, advertisedServiceEntries, findMyLocation()) + return NodeInfo(addresses, legalIdentity, allIdentities, platformVersion, advertisedServiceEntries, platformClock.instant().toEpochMilli()) } /** @@ -496,9 +501,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, */ protected open fun makeServiceEntries(): List { return advertisedServices.map { - val serviceId = it.type.id - val serviceName = it.name ?: X500Name("${configuration.myLegalName},OU=$serviceId") - val identity = obtainIdentity(serviceId, serviceName) + val identity = obtainIdentity(it) ServiceEntry(it, identity) } } @@ -524,12 +527,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, "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 = 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." - } } // Specific class so that MockNode can catch it. @@ -582,7 +579,15 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, services.networkMapCache.runWithoutMapService() noNetworkMapConfigured() // TODO This method isn't needed as runWithoutMapService sets the Future in the cache } else { - registerWithNetworkMap() + val netMapRegistration = registerWithNetworkMap() + // We may want to start node immediately with database data and not wait for network map registration (but send it either way). + // So we are ready to go. + if (services.networkMapCache.loadDBSuccess) { + log.info("Node successfully loaded network map data from the database.") + doneFuture(Unit) + } else { + netMapRegistration + } } } @@ -597,8 +602,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, 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" } + return sendNetworkMapRegistration(address).flatMap { response: RegistrationResponse -> + check(response.error == null) { "Unable to register with the network map service: ${response.error}" } // The future returned addMapService will complete on the same executor as sendNetworkMapRegistration, namely the one used by net services.networkMapCache.addMapService(network, address, true, null) } @@ -608,7 +613,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // Register this node against the network val instant = platformClock.instant() val expires = instant + NetworkMapService.DEFAULT_EXPIRATION_PERIOD - val reg = NodeRegistration(info, instant.toEpochMilli(), ADD, expires) + val reg = NodeRegistration(info, info.serial, ADD, expires) val request = RegistrationRequest(reg.toWire(services.keyManagementService, info.legalIdentityAndCert.owningKey), network.myAddress) return network.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapAddress) } @@ -618,9 +623,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, /** This is overriden by the mock node implementation to enable operation without any network map service */ protected open fun noNetworkMapConfigured(): CordaFuture { - // TODO: There should be a consistent approach to configuration error exceptions. - throw IllegalStateException("Configuration error: this node isn't being asked to act as the network map, nor " + - "has any other map node been configured.") + if (services.networkMapCache.loadDBSuccess) { + return doneFuture(Unit) + } else { + // TODO: There should be a consistent approach to configuration error exceptions. + throw IllegalStateException("Configuration error: this node isn't being asked to act as the network map, nor " + + "has any other map node been configured.") + } } protected open fun makeKeyManagementService(identityService: IdentityService): KeyManagementService { @@ -648,7 +657,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val caCertificates: Array = listOf(legalIdentity.certificate.cert, clientCa?.certificate?.cert) .filterNotNull() .toTypedArray() - val service = InMemoryIdentityService(setOf(info.legalIdentityAndCert), trustRoot = trustRoot, caCertificates = *caCertificates) + val service = PersistentIdentityService(setOf(info.legalIdentityAndCert), trustRoot = trustRoot, caCertificates = *caCertificates) services.networkMapCache.partyNodes.forEach { service.verifyAndRegisterIdentity(it.legalIdentityAndCert) } services.networkMapCache.changed.subscribe { mapChange -> // TODO how should we handle network map removal @@ -679,15 +688,23 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected abstract fun startMessagingService(rpcOps: RPCOps) - private fun obtainIdentity(id: String, name: X500Name): PartyAndCertificate { + private fun obtainIdentity(serviceInfo: ServiceInfo? = null): PartyAndCertificate { // Load the private identity key, creating it if necessary. The identity key is a long term well known key that // is distributed to other peers and we use it (or a key signed by it) when we need to do something // "permissioned". The identity file is what gets distributed and contains the node's legal name along with // the public key. Obviously in a real system this would need to be a certificate chain of some kind to ensure // the legal name is actually validated in some way. + val keyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) + + val (id, name) = if (serviceInfo == null) { + // Create node identity if service info = null + Pair("identity", myLegalName.withCommonName(null)) + } else { + val name = serviceInfo.name ?: myLegalName.withCommonName(serviceInfo.type.id) + Pair(serviceInfo.type.id, name) + } // TODO: Integrate with Key management service? - val keyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) val privateKeyAlias = "$id-private-key" val compositeKeyAlias = "$id-composite-key" @@ -702,7 +719,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, migrateKeysFromFile(keyStore, name, pubIdentityFile, privKeyFile, compositeKeyFile, privateKeyAlias, compositeKeyAlias) } else { log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!") - keyStore.saveNewKeyPair(name, privateKeyAlias, generateKeyPair()) + keyStore.signAndSaveNewKeyPair(name, privateKeyAlias, generateKeyPair()) } } @@ -738,7 +755,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // Load the private key. val publicKey = Crypto.decodePublicKey(pubKeyFile.readAll()) val privateKey = Crypto.decodePrivateKey(privKeyFile.readAll()) - keyStore.saveNewKeyPair(serviceName, privateKeyAlias, KeyPair(publicKey, privateKey)) + keyStore.signAndSaveNewKeyPair(serviceName, privateKeyAlias, KeyPair(publicKey, privateKey)) // Store composite key separately. if (compositeKeyFile.exists()) { keyStore.savePublicKey(serviceName, compositeKeyAlias, Crypto.decodePublicKey(compositeKeyFile.readAll())) @@ -756,8 +773,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, override val monitoringService = MonitoringService(MetricRegistry()) override val validatedTransactions = makeTransactionStorage() override val transactionVerifierService by lazy { makeTransactionVerifierService() } - override val networkMapCache by lazy { InMemoryNetworkMapCache(this) } + override val schemaService by lazy { NodeSchemaService(pluginRegistries.flatMap { it.requiredSchemas }.toSet()) } + override val networkMapCache by lazy { PersistentNetworkMapCache(this) } override val vaultService by lazy { NodeVaultService(this) } + override val contractUpgradeService by lazy { ContractUpgradeServiceImpl() } override val vaultQueryService by lazy { HibernateVaultQueryImpl(database.hibernateConfig, vaultService) } @@ -778,7 +797,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, 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: CordaPersistence get() = this@AbstractNode.database override val configuration: NodeConfiguration get() = this@AbstractNode.configuration 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 79ca9d2f22..550e22f791 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -1,6 +1,7 @@ package net.corda.node.internal import net.corda.client.rpc.notUsed +import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.UpgradedContract @@ -18,10 +19,10 @@ 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.transactions.SignedTransaction +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.messaging.getRpcContext import net.corda.node.services.messaging.requirePermission -import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.utilities.CordaPersistence @@ -71,13 +72,13 @@ class CordaRPCOpsImpl( } } - override fun verifiedTransactionsSnapshot(): List { - val (snapshot, updates) = verifiedTransactionsFeed() + override fun internalVerifiedTransactionsSnapshot(): List { + val (snapshot, updates) = internalVerifiedTransactionsFeed() updates.notUsed() return snapshot } - override fun verifiedTransactionsFeed(): DataFeed, SignedTransaction> { + override fun internalVerifiedTransactionsFeed(): DataFeed, SignedTransaction> { return database.transaction { services.validatedTransactions.track() } @@ -127,7 +128,7 @@ class CordaRPCOpsImpl( } } - override fun startTrackedFlowDynamic(logicType: Class>, vararg args: Any?): FlowProgressHandle { + override fun startTrackedFlowDynamic(logicType: Class>, vararg args: Any?): FlowProgressHandle { val stateMachine = startFlow(logicType, args) return FlowProgressHandleImpl( id = stateMachine.id, @@ -136,12 +137,12 @@ class CordaRPCOpsImpl( ) } - override fun startFlowDynamic(logicType: Class>, vararg args: Any?): FlowHandle { + override fun startFlowDynamic(logicType: Class>, vararg args: Any?): FlowHandle { val stateMachine = startFlow(logicType, args) return FlowHandleImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture) } - private fun startFlow(logicType: Class>, args: Array): FlowStateMachineImpl { + private fun startFlow(logicType: Class>, args: Array): FlowStateMachineImpl { require(logicType.isAnnotationPresent(StartableByRPC::class.java)) { "${logicType.name} was not designed for RPC" } val rpcContext = getRpcContext() rpcContext.requirePermission(startFlowPermission(logicType)) @@ -170,18 +171,52 @@ class CordaRPCOpsImpl( } } - override fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class>) = services.vaultService.authoriseContractUpgrade(state, upgradedContractClass) - override fun deauthoriseContractUpgrade(state: StateAndRef<*>) = services.vaultService.deauthoriseContractUpgrade(state) override fun currentNodeTime(): Instant = Instant.now(services.clock) - override fun waitUntilRegisteredWithNetworkMap() = services.networkMapCache.mapServiceRegistered - override fun partyFromAnonymous(party: AbstractParty): Party? = services.identityService.partyFromAnonymous(party) - override fun partyFromKey(key: PublicKey) = services.identityService.partyFromKey(key) - 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 waitUntilNetworkReady(): CordaFuture { + return database.transaction { + services.networkMapCache.nodeReady + } + } + + override fun partyFromAnonymous(party: AbstractParty): Party? { + return database.transaction { + services.identityService.partyFromAnonymous(party) + } + } + + override fun partyFromKey(key: PublicKey): Party? { + return database.transaction { + services.identityService.partyFromKey(key) + } + } + + override fun partyFromX500Name(x500Name: X500Name): Party? { + return database.transaction { + services.identityService.partyFromX500Name(x500Name) + } + } + + override fun partiesFromName(query: String, exactMatch: Boolean): Set { + return database.transaction { + services.identityService.partiesFromName(query, exactMatch) + } + } + + override fun nodeIdentityFromParty(party: AbstractParty): NodeInfo? { + return database.transaction { + services.networkMapCache.getNodeByLegalIdentity(party) + } + } override fun registeredFlows(): List = services.rpcFlows.map { it.name }.sorted() + override fun clearNetworkMapCache() { + database.transaction { + services.networkMapCache.clearNetworkMapCache() + } + } + companion object { private fun stateMachineInfoFromFlowLogic(flowLogic: FlowLogic<*>): StateMachineInfo { return StateMachineInfo(flowLogic.runId, flowLogic.javaClass.name, flowLogic.stateMachine.flowInitiator, flowLogic.track()) diff --git a/node/src/main/kotlin/net/corda/node/internal/MutableClock.kt b/node/src/main/kotlin/net/corda/node/internal/MutableClock.kt new file mode 100644 index 0000000000..3a52cf377c --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/MutableClock.kt @@ -0,0 +1,45 @@ +package net.corda.node.internal + +import rx.Observable +import rx.Subscriber +import rx.subscriptions.Subscriptions +import java.time.Clock +import java.util.concurrent.CopyOnWriteArraySet +import java.util.concurrent.atomic.AtomicLong + +/** + * An abstract class with helper methods for a type of Clock that might have it's concept of "now" + * adjusted externally. + * + * e.g. for testing (so unit tests do not have to wait for timeouts in realtime) or for demos and simulations. + */ +abstract class MutableClock : Clock() { + private val _version = AtomicLong(0L) + + /** + * This is an observer on the mutation count of this [Clock], which reflects the occurence of mutations. + */ + val mutations: Observable by lazy { + Observable.create({ subscriber: Subscriber -> + if (!subscriber.isUnsubscribed) { + mutationObservers.add(subscriber) + // This is not very intuitive, but subscribing to a subscriber observes unsubscribes. + subscriber.add(Subscriptions.create { mutationObservers.remove(subscriber) }) + } + }) + } + + private val mutationObservers = CopyOnWriteArraySet>() + + /** + * Must be called by subclasses when they mutate (but not just with the passage of time as per the "wall clock"). + */ + protected fun notifyMutationObservers() { + val version = _version.incrementAndGet() + for (observer in mutationObservers) { + if (!observer.isUnsubscribed) { + observer.onNext(version) + } + } + } +} \ No newline at end of file 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 6ca08f393b..a306515f65 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -155,7 +155,7 @@ open class Node(override val configuration: FullNodeConfiguration, myIdentityOrNullIfNetworkMapService, serverThread, database, - networkMapRegistrationFuture, + nodeReadyFuture, services.monitoringService, advertisedAddress) } @@ -214,15 +214,17 @@ open class Node(override val configuration: FullNodeConfiguration, 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 { - initialConnectAttempts = 5 - retryInterval = 5.seconds.toMillis() + initialConnectAttempts = 2 // TODO Public host discovery needs rewriting, as we may start nodes without network map, and we don't want to wait that long on startup. + retryInterval = 2.seconds.toMillis() retryIntervalMultiplier = 1.5 maxRetryInterval = 3.minutes.toMillis() } val clientFactory = try { locator.createSessionFactory() } catch (e: ActiveMQNotConnectedException) { - throw IOException("Unable to connect to the Network Map Service at $serverAddress for IP address discovery", e) + log.warn("Unable to connect to the Network Map Service at $serverAddress for IP address discovery. " + + "Using the provided \"${configuration.p2pAddress.host}\" as the advertised address.") + return null } val session = clientFactory.createSession(PEER_USER, PEER_USER, false, true, true, locator.isPreAcknowledge, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE) @@ -310,7 +312,7 @@ open class Node(override val configuration: FullNodeConfiguration, } super.start() - networkMapRegistrationFuture.thenMatch({ + nodeReadyFuture.thenMatch({ serverThread.execute { // Begin exporting our own metrics via JMX. These can be monitored using any agent, e.g. Jolokia: // @@ -332,7 +334,9 @@ open class Node(override val configuration: FullNodeConfiguration, _startupComplete.set(Unit) } - }, {}) + }, + { th -> logger.error("Unexpected exception", th)} + ) shutdownHook = addShutdownHook { stop() } 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 68e9292e3a..1fed51f1b0 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -5,21 +5,16 @@ import com.jcraft.jsch.JSch import com.jcraft.jsch.JSchException import com.typesafe.config.ConfigException import joptsimple.OptionException -import net.corda.core.crypto.commonName -import net.corda.core.crypto.orgName -import net.corda.core.internal.concurrent.thenMatch -import net.corda.core.internal.createDirectories -import net.corda.core.internal.div import net.corda.core.internal.* +import net.corda.core.internal.concurrent.thenMatch import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.organisation import net.corda.node.* -import net.corda.node.serialization.NodeClock import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.RelayConfiguration import net.corda.node.services.transactions.bftSMaRtSerialFilter import net.corda.node.shell.InteractiveShell -import net.corda.node.utilities.TestClock import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper import net.corda.nodeapi.internal.addShutdownHook @@ -106,10 +101,9 @@ open class NodeStartup(val args: Array) { node.start() printPluginsAndServices(node) - node.networkMapRegistrationFuture.thenMatch({ + node.nodeReadyFuture.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 + val name = node.info.legalIdentity.name.organisation Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec") // Don't start the shell if there's no console attached. @@ -121,7 +115,10 @@ open class NodeStartup(val args: Array) { logger.error("Shell failed to start", e) } } - }, {}) + }, + { + th -> logger.error("Unexpected exception during registration", th) + }) 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 bbded81fa8..3cf2c341ff 100644 --- a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt +++ b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt @@ -49,31 +49,6 @@ class NotaryChangeHandler(otherSide: Party) : AbstractStateReplacementFlow.Accep } } -class ContractUpgradeHandler(otherSide: Party) : AbstractStateReplacementFlow.Acceptor>>(otherSide) { - @Suspendable - @Throws(StateReplacementException::class) - override fun verifyProposal(stx: SignedTransaction, proposal: AbstractStateReplacementFlow.Proposal>>) { - // Retrieve signed transaction from our side, we will apply the upgrade logic to the transaction on our side, and - // verify outputs matches the proposed upgrade. - val ourSTX = serviceHub.validatedTransactions.getTransaction(proposal.stateRef.txhash) - requireNotNull(ourSTX) { "We don't have a copy of the referenced state" } - val oldStateAndRef = ourSTX!!.tx.outRef(proposal.stateRef.index) - val authorisedUpgrade = serviceHub.vaultService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?: - throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}") - val proposedTx = stx.tx - val expectedTx = ContractUpgradeFlow.assembleBareTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt).toWireTransaction() - requireThat { - "The instigator is one of the participants" using (otherSide in oldStateAndRef.state.data.participants) - "The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" using (proposal.modification == authorisedUpgrade) - "The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx) - } - ContractUpgradeFlow.verify( - oldStateAndRef.state.data, - expectedTx.outRef(0).state.data, - expectedTx.toLedgerTransaction(serviceHub).commandsOfType().single()) - } -} - class TransactionKeyHandler(val otherSide: Party, val revocationEnabled: Boolean) : FlowLogic() { constructor(otherSide: Party) : this(otherSide, false) companion object { 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 69e010892b..5aee5ca7b9 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 @@ -22,6 +22,7 @@ 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 net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.CordaPersistence interface NetworkMapCacheInternal : NetworkMapCache { @@ -53,6 +54,9 @@ interface NetworkMapCacheInternal : NetworkMapCache { /** For testing where the network map cache is manipulated marks the service as immediately ready. */ @VisibleForTesting fun runWithoutMapService() + + /** Indicates if loading network map data from database was successful. */ + val loadDBSuccess: Boolean } @CordaSerializable @@ -97,7 +101,7 @@ interface ServiceHubInternal : PluginServiceHub { if (notifyVault) { val toNotify = recordedTransactions.map { if (it.isNotaryChangeTransaction()) it.notaryChangeTx else it.tx } - vaultService.notifyAll(toNotify) + (vaultService as NodeVaultService).notifyAll(toNotify) } } @@ -122,7 +126,7 @@ interface ServiceHubInternal : PluginServiceHub { * @throws net.corda.core.flows.IllegalFlowLogicException or IllegalArgumentException if there are problems with the * [logicType] or [args]. */ - fun invokeFlowAsync( + fun invokeFlowAsync( logicType: Class>, flowInitiator: FlowInitiator, vararg args: Any?): FlowStateMachineImpl { diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index f66e53a7be..b070bc3565 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -11,6 +11,8 @@ import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.exists import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.toWellFormattedName +import net.corda.core.utilities.withCommonName import net.corda.node.utilities.* import net.corda.nodeapi.config.SSLConfiguration import org.bouncycastle.asn1.x500.X500Name @@ -86,11 +88,18 @@ fun createKeystoreForCordaNode(sslKeyStorePath: Path, val (intermediateCACert, intermediateCAKeyPair) = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA, caKeyPassword) val clientKey = Crypto.generateKeyPair(signatureScheme) - val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName))), arrayOf()) - val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, legalName, clientKey.public, nameConstraints = nameConstraints) + val clientName = legalName.toWellFormattedName().withCommonName(null) + + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, clientName))), arrayOf()) + val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, + intermediateCACert, + intermediateCAKeyPair, + clientName.withCommonName(X509Utilities.CORDA_CLIENT_CA_CN), + clientKey.public, + nameConstraints = nameConstraints) val tlsKey = Crypto.generateKeyPair(signatureScheme) - val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKey, legalName, tlsKey.public) + val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKey, clientName, tlsKey.public) val keyPass = keyPassword.toCharArray() 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 8c51097c8d..4ce237b51a 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 @@ -19,6 +19,8 @@ data class BFTSMaRtConfiguration(val replicaId: Int, val debug: Boolean, val exp } interface NodeConfiguration : NodeSSLConfiguration { + // myLegalName should be only used in the initial network registration, we should use the name from the certificate instead of this. + // TODO: Remove this so we don't accidentally use this identity in the code? val myLegalName: X500Name val networkMapService: NetworkMapInfo? val minimumPlatformVersion: Int 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 908c20c399..1d0b5e3d54 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 @@ -1,26 +1,39 @@ package net.corda.node.services.events -import com.google.common.util.concurrent.SettableFuture -import net.corda.core.contracts.* -import net.corda.core.internal.ThreadBox +import co.paralleluniverse.fibers.Suspendable +import co.paralleluniverse.strands.SettableFuture as QuasarSettableFuture +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.SettableFuture as GuavaSettableFuture +import net.corda.core.contracts.SchedulableState +import net.corda.core.contracts.ScheduledActivity +import net.corda.core.contracts.ScheduledStateRef +import net.corda.core.contracts.StateRef import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic +import net.corda.core.internal.ThreadBox +import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.until import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace +import net.corda.node.internal.MutableClock import net.corda.node.services.api.SchedulerService import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl -import net.corda.node.utilities.* +import net.corda.node.utilities.AffinityExecutor +import net.corda.node.utilities.NODE_DATABASE_PREFIX +import net.corda.node.utilities.PersistentMap import org.apache.activemq.artemis.utils.ReusableLatch +import java.time.Clock import java.time.Instant import java.util.* -import java.util.concurrent.Executor -import java.util.concurrent.Executors +import java.util.concurrent.* import javax.annotation.concurrent.ThreadSafe -import javax.persistence.* +import javax.persistence.Column +import javax.persistence.EmbeddedId +import javax.persistence.Entity /** * A first pass of a simple [SchedulerService] that works with [MutableClock]s for testing, demonstrations and simulations @@ -48,6 +61,48 @@ class NodeSchedulerService(private val services: ServiceHubInternal, companion object { private val log = loggerFor() + /** + * Wait until the given [Future] is complete or the deadline is reached, with support for [MutableClock] implementations + * used in demos or testing. This will substitute a Fiber compatible Future so the current + * [co.paralleluniverse.strands.Strand] is not blocked. + * + * @return true if the [Future] is complete, false if the deadline was reached. + */ + // We should try to make the Clock used in our code injectable (for tests etc) and to use the extension below + // to wait in our code, rather than Thread.sleep() or other time-based pauses. + @Suspendable + @VisibleForTesting + // We specify full classpath on SettableFuture to differentiate it from the Quasar class of the same name + fun awaitWithDeadline(clock: Clock, deadline: Instant, future: Future<*> = GuavaSettableFuture.create()): Boolean { + var nanos: Long + do { + val originalFutureCompleted = makeStrandFriendlySettableFuture(future) + val subscription = if (clock is MutableClock) { + clock.mutations.first().subscribe { + originalFutureCompleted.set(false) + } + } else { + null + } + nanos = (clock.instant() until deadline).toNanos() + if (nanos > 0) { + try { + // This will return when it times out, or when the clock mutates or when when the original future completes. + originalFutureCompleted.get(nanos, TimeUnit.NANOSECONDS) + } catch(e: ExecutionException) { + // No need to take action as will fall out of the loop due to future.isDone + } catch(e: CancellationException) { + // No need to take action as will fall out of the loop due to future.isDone + } catch(e: TimeoutException) { + // No need to take action as will fall out of the loop due to future.isDone + } + } + subscription?.unsubscribe() + originalFutureCompleted.cancel(false) + } while (nanos > 0 && !future.isDone) + return future.isDone + } + fun createMap(): PersistentMap { return PersistentMap( toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) }, @@ -67,6 +122,21 @@ class NodeSchedulerService(private val services: ServiceHubInternal, persistentEntityClass = PersistentScheduledState::class.java ) } + + /** + * Convert a Guava [ListenableFuture] or JDK8 [CompletableFuture] to Quasar implementation and set to true when a result + * or [Throwable] is available in the original. + * + * We need this so that we do not block the actual thread when calling get(), but instead allow a Quasar context + * switch. There's no need to checkpoint our [Fiber]s as there's no external effect of waiting. + */ + private fun makeStrandFriendlySettableFuture(future: Future) = QuasarSettableFuture().also { g -> + when (future) { + is ListenableFuture -> future.addListener(Runnable { g.set(true) }, Executor { it.run() }) + is CompletionStage<*> -> future.whenComplete { _, _ -> g.set(true) } + else -> throw IllegalArgumentException("Cannot make future $future Strand friendly.") + } + } } @Entity @@ -84,7 +154,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, var scheduledStatesQueue: PriorityQueue = PriorityQueue( { a, b -> a.scheduledAt.compareTo(b.scheduledAt) } ) - var rescheduled: SettableFuture? = null + var rescheduled: GuavaSettableFuture? = null } private val mutex = ThreadBox(InnerState()) @@ -145,7 +215,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, // Note, we already have the mutex but we need the scope again here val (scheduledState, ourRescheduledFuture) = mutex.alreadyLocked { rescheduled?.cancel(false) - rescheduled = SettableFuture.create() + rescheduled = GuavaSettableFuture.create() Pair(scheduledStatesQueue.peek(), rescheduled!!) } if (scheduledState != null) { @@ -153,7 +223,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, log.trace { "Scheduling as next $scheduledState" } // This will block the scheduler single thread until the scheduled time (returns false) OR // the Future is cancelled due to rescheduling (returns true). - if (!services.clock.awaitWithDeadline(scheduledState.scheduledAt, ourRescheduledFuture)) { + if (!awaitWithDeadline(services.clock, scheduledState.scheduledAt, ourRescheduledFuture)) { log.trace { "Invoking as next $scheduledState" } onTimeReached(scheduledState) } else { @@ -165,14 +235,20 @@ class NodeSchedulerService(private val services: ServiceHubInternal, private fun onTimeReached(scheduledState: ScheduledStateRef) { serverThread.execute { - services.database.transaction { - val scheduledFlow = getScheduledFlow(scheduledState) - if (scheduledFlow != null) { - val future = services.startFlow(scheduledFlow, FlowInitiator.Scheduled(scheduledState)).resultFuture - future.then { - unfinishedSchedules.countDown() + var flowName: String? = "(unknown)" + try { + services.database.transaction { + val scheduledFlow = getScheduledFlow(scheduledState) + if (scheduledFlow != null) { + flowName = scheduledFlow.javaClass.name + val future = services.startFlow(scheduledFlow, FlowInitiator.Scheduled(scheduledState)).resultFuture + future.then { + unfinishedSchedules.countDown() + } } } + } catch (e: Exception) { + log.error("Failed to start scheduled flow $flowName for $scheduledState due to an internal error", e) } } } diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index 80812edd1e..312d5895ee 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -1,7 +1,6 @@ package net.corda.node.services.identity import net.corda.core.contracts.PartyAndReference -import net.corda.core.crypto.cert import net.corda.core.crypto.toStringShort import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty @@ -11,7 +10,9 @@ import net.corda.core.internal.toX509CertHolder import net.corda.core.node.services.IdentityService import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.utilities.cert import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.subject import net.corda.core.utilities.trace import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.cert.X509CertificateHolder @@ -65,8 +66,17 @@ class InMemoryIdentityService(identities: Iterable = emptyS @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class) override fun verifyAndRegisterIdentity(identity: PartyAndCertificate): PartyAndCertificate? { // Validate the chain first, before we do anything clever with it - identity.verify(trustAnchor) - + try { + identity.verify(trustAnchor) + } catch (e: CertPathValidatorException) { + log.error("Certificate validation failed for ${identity.name} against trusted root ${trustAnchor.trustedCert.subject}.") + log.error("Certificate path :") + identity.certPath.certificates.reversed().forEachIndexed { index, certificate -> + val space = (0 until index).map { " " }.joinToString("") + log.error("$space${certificate.toX509CertHolder().subject}") + } + throw e + } log.trace { "Registering identity $identity" } keyToParties[identity.owningKey] = identity // Always keep the first party we registered, as that's the well known identity diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt new file mode 100644 index 0000000000..6774883ab9 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -0,0 +1,203 @@ +package net.corda.node.services.identity + +import net.corda.core.contracts.PartyAndReference +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.toStringShort +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.internal.toX509CertHolder +import net.corda.core.node.services.IdentityService +import net.corda.core.node.services.UnknownAnonymousPartyException +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.utilities.cert +import net.corda.core.utilities.loggerFor +import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.node.utilities.NODE_DATABASE_PREFIX +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.X509CertificateHolder +import java.io.ByteArrayInputStream +import java.security.InvalidAlgorithmParameterException +import java.security.PublicKey +import java.security.cert.* +import javax.annotation.concurrent.ThreadSafe +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Lob + +@ThreadSafe +class PersistentIdentityService(identities: Iterable = emptySet(), + confidentialIdentities: Iterable = emptySet(), + override val trustRoot: X509Certificate, + vararg caCertificates: X509Certificate) : SingletonSerializeAsToken(), IdentityService { + constructor(wellKnownIdentities: Iterable = emptySet(), + confidentialIdentities: Iterable = emptySet(), + trustRoot: X509CertificateHolder) : this(wellKnownIdentities, confidentialIdentities, trustRoot.cert) + + companion object { + private val log = loggerFor() + private val certFactory: CertificateFactory = CertificateFactory.getInstance("X.509") + + fun createPKMap(): AppendOnlyPersistentMap { + return AppendOnlyPersistentMap( + toPersistentEntityKey = { it.toString() }, + fromPersistentEntity = { + Pair(SecureHash.parse(it.publicKeyHash), + PartyAndCertificate(ByteArrayInputStream(it.identity).use { + certFactory.generateCertPath(it) + })) + }, + toPersistentEntity = { key: SecureHash, value: PartyAndCertificate -> + PersistentIdentity(key.toString(), value.certPath.encoded) + }, + persistentEntityClass = PersistentIdentity::class.java + ) + } + + fun createX500Map(): AppendOnlyPersistentMap { + return AppendOnlyPersistentMap( + toPersistentEntityKey = { it.toString() }, + fromPersistentEntity = { Pair(X500Name(it.name), SecureHash.parse(it.publicKeyHash)) }, + toPersistentEntity = { key: X500Name, value: SecureHash -> + PersistentIdentityNames(key.toString(), value.toString()) + }, + persistentEntityClass = PersistentIdentityNames::class.java + ) + } + + private fun mapToKey(owningKey: PublicKey) = SecureHash.sha256(owningKey.encoded) + private fun mapToKey(party: PartyAndCertificate) = mapToKey(party.owningKey) + } + + @Entity + @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}identities") + class PersistentIdentity( + @Id + @Column(name = "pk_hash", length = 64) + var publicKeyHash: String = "", + + @Lob + @Column + var identity: ByteArray = ByteArray(0) + ) + + @Entity + @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}named_identities") + class PersistentIdentityNames( + @Id + @Column(name = "name", length = 128) + var name: String = "", + + @Column(name = "pk_hash", length = 64) + var publicKeyHash: String = "" + ) + + override val caCertStore: CertStore + override val trustRootHolder = trustRoot.toX509CertHolder() + override val trustAnchor: TrustAnchor = TrustAnchor(trustRoot, null) + + private val keyToParties = createPKMap() + private val principalToParties = createX500Map() + + init { + val caCertificatesWithRoot: Set = caCertificates.toSet() + trustRoot + caCertStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(caCertificatesWithRoot)) + identities.forEach { + val key = mapToKey(it) + keyToParties.addWithDuplicatesAllowed(key, it) + principalToParties.addWithDuplicatesAllowed(it.name, key) + } + confidentialIdentities.forEach { + principalToParties.addWithDuplicatesAllowed(it.name, mapToKey(it)) + } + } + + override fun registerIdentity(party: PartyAndCertificate) { + verifyAndRegisterIdentity(party) + } + + // TODO: Check the certificate validation logic + @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class) + override fun verifyAndRegisterIdentity(identity: PartyAndCertificate): PartyAndCertificate? { + // Validate the chain first, before we do anything clever with it + identity.verify(trustAnchor) + + log.info("Registering identity $identity") + val key = mapToKey(identity) + keyToParties.addWithDuplicatesAllowed(key, identity) + // Always keep the first party we registered, as that's the well known identity + principalToParties.addWithDuplicatesAllowed(identity.name, key) + val parentId = mapToKey(identity.certPath.certificates[1].publicKey) + return keyToParties[parentId] + } + + override fun certificateFromKey(owningKey: PublicKey): PartyAndCertificate? = keyToParties[mapToKey(owningKey)] + private fun certificateFromX500Name(name: X500Name): PartyAndCertificate? { + val partyId = principalToParties[name] + return if (partyId != null) { + keyToParties[partyId] + } else null + } + + override fun certificateFromParty(party: Party): PartyAndCertificate = certificateFromX500Name(party.name) ?: throw IllegalArgumentException("Unknown identity ${party.name}") + + // We give the caller a copy of the data set to avoid any locking problems + override fun getAllIdentities(): Iterable = keyToParties.allPersisted().map { it.second }.asIterable() + + override fun partyFromKey(key: PublicKey): Party? = certificateFromKey(key)?.party + override fun partyFromX500Name(principal: X500Name): Party? = certificateFromX500Name(principal)?.party + override fun partyFromAnonymous(party: AbstractParty): Party? { + // Expand the anonymous party to a full party (i.e. has a name) if possible + val candidate = party as? Party ?: partyFromKey(party.owningKey) + // TODO: This should be done via the network map cache, which is the authoritative source of well known identities + // Look up the well known identity for that name + return if (candidate != null) { + // If we have a well known identity by that name, use it in preference to the candidate. Otherwise default + // back to the candidate. + val res = partyFromX500Name(candidate.name) ?: candidate + res + } else { + null + } + } + + override fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party) + override fun requirePartyFromAnonymous(party: AbstractParty): Party { + return partyFromAnonymous(party) ?: throw IllegalStateException("Could not deanonymise party ${party.owningKey.toStringShort()}") + } + + override fun partiesFromName(query: String, exactMatch: Boolean): Set { + val results = LinkedHashSet() + for ((x500name, partyId) in principalToParties.allPersisted()) { + val party = keyToParties[partyId]!!.party + for (rdn in x500name.rdNs) { + val component = rdn.first.value.toString() + if (exactMatch && component == query) { + results += party + } else if (!exactMatch) { + // We can imagine this being a query over a lucene index in future. + // + // Kostas says: We can easily use the Jaro-Winkler distance metric as it is best suited for short + // strings such as entity/company names, and to detect small typos. We can also apply it for city + // or any keyword related search in lists of records (not raw text - for raw text we need indexing) + // and we can return results in hierarchical order (based on normalised String similarity 0.0-1.0). + if (component.contains(query, ignoreCase = true)) + results += party + } + } + } + return results + } + + @Throws(UnknownAnonymousPartyException::class) + override fun assertOwnership(party: Party, anonymousParty: AnonymousParty) { + val anonymousIdentity = certificateFromKey(anonymousParty.owningKey) ?: + throw UnknownAnonymousPartyException("Unknown $anonymousParty") + val issuingCert = anonymousIdentity.certPath.certificates[1] + require(issuingCert.publicKey == party.owningKey) { + "Issuing certificate's public key must match the party key ${party.owningKey.toStringShort()}." + } + } +} \ No newline at end of file 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 36a2c4f34d..00b293c539 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 @@ -1,12 +1,12 @@ package net.corda.node.services.keys -import net.corda.core.crypto.ContentSignerBuilder import net.corda.core.crypto.Crypto -import net.corda.core.crypto.cert +import net.corda.core.utilities.cert import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService import net.corda.core.utilities.days import net.corda.node.utilities.CertificateType +import net.corda.node.utilities.ContentSignerBuilder import net.corda.node.utilities.X509Utilities import org.bouncycastle.operator.ContentSigner import java.security.KeyPair 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 95c4e0083e..5117edd8c1 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 @@ -8,6 +8,8 @@ import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.core.utilities.parsePublicKeyBase58 +import net.corda.core.utilities.toBase58String import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.NODE_DATABASE_PREFIX import org.bouncycastle.operator.ContentSigner 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 829c36bfbf..2e375d78f9 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 @@ -5,7 +5,7 @@ import io.netty.handler.ssl.SslHandler import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.AddressFormatException import net.corda.core.crypto.newSecureRandom -import net.corda.core.crypto.parsePublicKeyBase58 +import net.corda.core.utilities.parsePublicKeyBase58 import net.corda.core.crypto.random63BitValue import net.corda.core.internal.ThreadBox import net.corda.core.internal.concurrent.openFuture @@ -338,6 +338,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, * TODO : Create the bridge directly from the list of queues on start up when we have a persisted network map service. */ private fun updateBridgesOnNetworkChange(change: MapChange) { + log.debug { "Updating bridges on network map change: ${change.node}" } fun gatherAddresses(node: NodeInfo): Sequence { val peerAddress = getArtemisPeerAddress(node) val addresses = mutableListOf(peerAddress) 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 8bde1f046a..4d4bbd65b4 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 @@ -244,7 +244,8 @@ class NodeMessagingClient(override val config: NodeConfiguration, } }, {}) - rpcServer = RPCServer(rpcOps, NODE_USER, NODE_USER, locator, userService, config.myLegalName) + val myLegalName = loadKeyStore(config.sslKeystore, config.keyStorePassword).getX509Certificate(X509Utilities.CORDA_CLIENT_TLS).subject + rpcServer = RPCServer(rpcOps, NODE_USER, NODE_USER, locator, userService, myLegalName) fun checkVerifierCount() { if (session.queueQuery(SimpleString(VERIFICATION_REQUESTS_QUEUE_NAME)).consumerCount == 0) { 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 deleted file mode 100644 index 774e93f499..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt +++ /dev/null @@ -1,185 +0,0 @@ -package net.corda.node.services.network - -import net.corda.core.concurrent.CordaFuture -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.Party -import net.corda.core.internal.VisibleForTesting -import net.corda.core.internal.bufferUntilSubscribed -import net.corda.core.internal.concurrent.map -import net.corda.core.internal.concurrent.openFuture -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.NetworkMapCache.MapChange -import net.corda.core.node.services.PartyInfo -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.NetworkCacheError -import net.corda.node.services.api.NetworkMapCacheInternal -import net.corda.node.services.messaging.MessagingService -import net.corda.node.services.messaging.createMessage -import net.corda.node.services.messaging.sendRequest -import net.corda.node.services.network.NetworkMapService.FetchMapResponse -import net.corda.node.services.network.NetworkMapService.SubscribeResponse -import net.corda.node.utilities.AddOrRemove -import net.corda.node.utilities.bufferUntilDatabaseCommit -import net.corda.node.utilities.wrapWithDatabaseTransaction -import rx.Observable -import rx.subjects.PublishSubject -import java.security.PublicKey -import java.security.SignatureException -import java.util.* -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(private val serviceHub: ServiceHub?) : SingletonSerializeAsToken(), NetworkMapCacheInternal { - companion object { - val logger = loggerFor() - } - - override val partyNodes: List get() = registeredNodes.map { it.value } - override val networkMapNodes: List get() = getNodesWithService(NetworkMapService.type) - private val _changed = PublishSubject.create() - // We use assignment here so that multiple subscribers share the same wrapped Observable. - override val changed: Observable = _changed.wrapWithDatabaseTransaction() - private val changePublisher: rx.Observer get() = _changed.bufferUntilDatabaseCommit() - - private val _registrationFuture = openFuture() - override val mapServiceRegistered: CordaFuture get() = _registrationFuture - - private var registeredForPush = false - protected var registeredNodes: MutableMap = Collections.synchronizedMap(HashMap()) - - override fun getPartyInfo(party: Party): PartyInfo? { - val node = registeredNodes[party.owningKey] - if (node != null) { - return PartyInfo.Node(node) - } - for ((_, value) in registeredNodes) { - for (service in value.advertisedServices) { - if (service.identity.party == party) { - return PartyInfo.Service(service) - } - } - } - return null - } - - 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 - } - - return wellKnownParty?.let { - getNodeByLegalIdentityKey(it.owningKey) - } - } - - override fun track(): DataFeed, MapChange> { - synchronized(_changed) { - return DataFeed(partyNodes, _changed.bufferUntilSubscribed().wrapWithDatabaseTransaction()) - } - } - - override fun addMapService(network: MessagingService, networkMapAddress: SingleMessageRecipient, subscribe: Boolean, - ifChangedSinceVer: Int?): CordaFuture { - if (subscribe && !registeredForPush) { - // Add handler to the network, for updates received from the remote network map service. - network.addMessageHandler(NetworkMapService.PUSH_TOPIC) { message, _ -> - try { - val req = message.data.deserialize() - val ackMessage = network.createMessage(NetworkMapService.PUSH_ACK_TOPIC, - data = NetworkMapService.UpdateAcknowledge(req.mapVersion, network.myAddress).serialize().bytes) - network.send(ackMessage, req.replyTo) - processUpdatePush(req) - } catch(e: NodeMapError) { - logger.warn("Failure during node map update due to bad update: ${e.javaClass.name}") - } catch(e: Exception) { - logger.error("Exception processing update from network map service", e) - } - } - registeredForPush = true - } - - // Fetch the network map and register for updates at the same time - val req = NetworkMapService.FetchMapRequest(subscribe, ifChangedSinceVer, network.myAddress) - val future = network.sendRequest(NetworkMapService.FETCH_TOPIC, req, networkMapAddress).map { (nodes) -> - // We may not receive any nodes back, if the map hasn't changed since the version specified - nodes?.forEach { processRegistration(it) } - Unit - } - _registrationFuture.captureLater(future.map { null }) - - return future - } - - override fun addNode(node: NodeInfo) { - synchronized(_changed) { - val previousNode = registeredNodes.put(node.legalIdentity.owningKey, node) - if (previousNode == null) { - changePublisher.onNext(MapChange.Added(node)) - } else if (previousNode != node) { - changePublisher.onNext(MapChange.Modified(node, previousNode)) - } - } - } - - override fun removeNode(node: NodeInfo) { - synchronized(_changed) { - registeredNodes.remove(node.legalIdentity.owningKey) - changePublisher.onNext(MapChange.Removed(node)) - } - } - - /** - * Unsubscribes from updates from the given map service. - * @param service the network map service to listen to updates from. - */ - override fun deregisterForUpdates(network: MessagingService, service: NodeInfo): CordaFuture { - // Fetch the network map and register for updates at the same time - val req = NetworkMapService.SubscribeRequest(false, network.myAddress) - // `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.captureLater(future.map { null }) - return future - } - - fun processUpdatePush(req: NetworkMapService.Update) { - try { - val reg = req.wireReg.verified() - processRegistration(reg) - } catch (e: SignatureException) { - throw NodeMapError.InvalidSignature() - } - } - - private fun processRegistration(reg: NodeRegistration) { - // TODO: Implement filtering by sequence number, so we only accept changes that are - // more recent than the latest change we've processed. - when (reg.type) { - AddOrRemove.ADD -> addNode(reg.node) - AddOrRemove.REMOVE -> removeNode(reg.node) - } - } - - @VisibleForTesting - override fun runWithoutMapService() { - _registrationFuture.set(null) - } -} 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 21b728e6ff..18352367fe 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 @@ -35,6 +35,7 @@ import net.corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_ import net.corda.node.utilities.AddOrRemove import net.corda.node.utilities.AddOrRemove.ADD import net.corda.node.utilities.AddOrRemove.REMOVE +import java.io.IOException import java.security.PublicKey import java.security.SignatureException import java.time.Instant @@ -243,6 +244,10 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, request.wireReg.verified() } catch (e: SignatureException) { return RegistrationResponse("Invalid signature on request") + } catch (e: IOException) { + val msg = "Unexpected IO exception: ${e.message}" + logger.error(msg, e) + return RegistrationResponse(msg) } val node = change.node diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt new file mode 100644 index 0000000000..fa57922e1e --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -0,0 +1,340 @@ +package net.corda.node.services.network + +import net.corda.core.concurrent.CordaFuture +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.bufferUntilSubscribed +import net.corda.core.internal.concurrent.map +import net.corda.core.internal.concurrent.openFuture +import net.corda.core.messaging.DataFeed +import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.node.NodeInfo +import net.corda.core.node.services.NetworkMapCache.MapChange +import net.corda.core.node.services.PartyInfo +import net.corda.core.schemas.NodeInfoSchemaV1 +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.parsePublicKeyBase58 +import net.corda.core.utilities.toBase58String +import net.corda.node.services.api.NetworkCacheError +import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.messaging.MessagingService +import net.corda.node.services.messaging.createMessage +import net.corda.node.services.messaging.sendRequest +import net.corda.node.services.network.NetworkMapService.FetchMapResponse +import net.corda.node.services.network.NetworkMapService.SubscribeResponse +import net.corda.node.utilities.AddOrRemove +import net.corda.node.utilities.DatabaseTransactionManager +import net.corda.node.utilities.bufferUntilDatabaseCommit +import net.corda.node.utilities.wrapWithDatabaseTransaction +import org.bouncycastle.asn1.x500.X500Name +import org.hibernate.Session +import rx.Observable +import rx.subjects.PublishSubject +import java.security.PublicKey +import java.security.SignatureException +import java.util.* +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 PersistentNetworkMapCache(private val serviceHub: ServiceHubInternal) : SingletonSerializeAsToken(), NetworkMapCacheInternal { + companion object { + val logger = loggerFor() + } + + private var registeredForPush = false + // TODO Small explanation, partyNodes and registeredNodes is left in memory as it was before, because it will be removed in + // next PR that gets rid of services. These maps are used only for queries by service. + override val partyNodes: List get() = registeredNodes.map { it.value } + override val networkMapNodes: List get() = getNodesWithService(NetworkMapService.type) + private val _changed = PublishSubject.create() + // We use assignment here so that multiple subscribers share the same wrapped Observable. + override val changed: Observable = _changed.wrapWithDatabaseTransaction() + private val changePublisher: rx.Observer get() = _changed.bufferUntilDatabaseCommit() + + private val _registrationFuture = openFuture() + override val nodeReady: CordaFuture get() = _registrationFuture + protected val registeredNodes: MutableMap = Collections.synchronizedMap(HashMap()) + private var _loadDBSuccess: Boolean = false + override val loadDBSuccess get() = _loadDBSuccess + + init { + serviceHub.database.transaction { loadFromDB() } + } + + override fun getPartyInfo(party: Party): PartyInfo? { + val nodes = serviceHub.database.transaction { queryByIdentityKey(party.owningKey) } + if (nodes.size == 1 && nodes[0].legalIdentity == party) { + return PartyInfo.Node(nodes[0]) + } + for (node in nodes) { + for (service in node.advertisedServices) { + if (service.identity.party == party) { + return PartyInfo.Service(service) + } + } + } + return null + } + + // TODO See comment to queryByLegalName why it's left like that. + override fun getNodeByLegalName(principal: X500Name): NodeInfo? = partyNodes.singleOrNull { it.legalIdentity.name == principal } + //serviceHub!!.database.transaction { queryByLegalName(principal).firstOrNull() } + override fun getNodeByLegalIdentityKey(identityKey: PublicKey): NodeInfo? = + serviceHub.database.transaction { queryByIdentityKey(identityKey).firstOrNull() } + override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { + val wellKnownParty = serviceHub.identityService.partyFromAnonymous(party) + return wellKnownParty?.let { + getNodeByLegalIdentityKey(it.owningKey) + } + } + + override fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? = serviceHub.database.transaction { queryByAddress(address) } + + override fun track(): DataFeed, MapChange> { + synchronized(_changed) { + return DataFeed(partyNodes, _changed.bufferUntilSubscribed().wrapWithDatabaseTransaction()) + } + } + + override fun addMapService(network: MessagingService, networkMapAddress: SingleMessageRecipient, subscribe: Boolean, + ifChangedSinceVer: Int?): CordaFuture { + if (subscribe && !registeredForPush) { + // Add handler to the network, for updates received from the remote network map service. + network.addMessageHandler(NetworkMapService.PUSH_TOPIC) { message, _ -> + try { + val req = message.data.deserialize() + val ackMessage = network.createMessage(NetworkMapService.PUSH_ACK_TOPIC, + data = NetworkMapService.UpdateAcknowledge(req.mapVersion, network.myAddress).serialize().bytes) + network.send(ackMessage, req.replyTo) + processUpdatePush(req) + } catch(e: NodeMapError) { + logger.warn("Failure during node map update due to bad update: ${e.javaClass.name}") + } catch(e: Exception) { + logger.error("Exception processing update from network map service", e) + } + } + registeredForPush = true + } + + // Fetch the network map and register for updates at the same time + val req = NetworkMapService.FetchMapRequest(subscribe, ifChangedSinceVer, network.myAddress) + val future = network.sendRequest(NetworkMapService.FETCH_TOPIC, req, networkMapAddress).map { (nodes) -> + // We may not receive any nodes back, if the map hasn't changed since the version specified + nodes?.forEach { processRegistration(it) } + Unit + } + _registrationFuture.captureLater(future.map { null }) + + return future + } + + override fun addNode(node: NodeInfo) { + synchronized(_changed) { + val previousNode = registeredNodes.put(node.legalIdentity.owningKey, node) + if (previousNode == null) { + serviceHub.database.transaction { + updateInfoDB(node) + changePublisher.onNext(MapChange.Added(node)) + } + } else if (previousNode != node) { + serviceHub.database.transaction { + updateInfoDB(node) + changePublisher.onNext(MapChange.Modified(node, previousNode)) + } + } + } + } + + override fun removeNode(node: NodeInfo) { + synchronized(_changed) { + registeredNodes.remove(node.legalIdentity.owningKey) + serviceHub.database.transaction { + removeInfoDB(node) + changePublisher.onNext(MapChange.Removed(node)) + } + } + } + + /** + * Unsubscribes from updates from the given map service. + * @param service the network map service to listen to updates from. + */ + override fun deregisterForUpdates(network: MessagingService, service: NodeInfo): CordaFuture { + // Fetch the network map and register for updates at the same time + val req = NetworkMapService.SubscribeRequest(false, network.myAddress) + // `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.captureLater(future.map { null }) + return future + } + + fun processUpdatePush(req: NetworkMapService.Update) { + try { + val reg = req.wireReg.verified() + processRegistration(reg) + } catch (e: SignatureException) { + throw NodeMapError.InvalidSignature() + } + } + + private fun processRegistration(reg: NodeRegistration) { + // TODO: Implement filtering by sequence number, so we only accept changes that are + // more recent than the latest change we've processed. + when (reg.type) { + AddOrRemove.ADD -> addNode(reg.node) + AddOrRemove.REMOVE -> removeNode(reg.node) + } + } + + @VisibleForTesting + override fun runWithoutMapService() { + _registrationFuture.set(null) + } + + // Changes related to NetworkMap redesign + // TODO It will be properly merged into network map cache after services removal. + + private inline fun createSession(block: (Session) -> T): T { + return DatabaseTransactionManager.current().session.let { block(it) } + } + + private fun getAllInfos(session: Session): List { + val criteria = session.criteriaBuilder.createQuery(NodeInfoSchemaV1.PersistentNodeInfo::class.java) + criteria.select(criteria.from(NodeInfoSchemaV1.PersistentNodeInfo::class.java)) + return session.createQuery(criteria).resultList + } + + /** + * Load NetworkMap data from the database if present. Node can start without having NetworkMapService configured. + */ + private fun loadFromDB() { + logger.info("Loading network map from database...") + createSession { + val result = getAllInfos(it) + for (nodeInfo in result) { + try { + logger.info("Loaded node info: $nodeInfo") + val publicKey = parsePublicKeyBase58(nodeInfo.legalIdentitiesAndCerts.single { it.isMain }.owningKey) + val node = nodeInfo.toNodeInfo() + registeredNodes.put(publicKey, node) + changePublisher.onNext(MapChange.Added(node)) // Redeploy bridges after reading from DB on startup. + _loadDBSuccess = true // This is used in AbstractNode to indicate that node is ready. + } catch (e: Exception) { + logger.warn("Exception parsing network map from the database.", e) + } + } + if (loadDBSuccess) { + _registrationFuture.set(null) // Useful only if we don't have NetworkMapService configured so StateMachineManager can start. + } + } + } + + private fun updateInfoDB(nodeInfo: NodeInfo) { + // TODO Temporary workaround to force isolated transaction (otherwise it causes race conditions when processing + // network map registration on network map node) + serviceHub.database.dataSource.connection.use { + val session = serviceHub.database.entityManagerFactory.withOptions().connection(it.apply { + transactionIsolation = 1 + }).openSession() + session.use { + val tx = session.beginTransaction() + // TODO For now the main legal identity is left in NodeInfo, this should be set comparision/come up with index for NodeInfo? + val info = findByIdentityKey(session, nodeInfo.legalIdentity.owningKey) + val nodeInfoEntry = generateMappedObject(nodeInfo) + if (info.isNotEmpty()) { + nodeInfoEntry.id = info[0].id + } + session.merge(nodeInfoEntry) + tx.commit() + } + } + } + + private fun removeInfoDB(nodeInfo: NodeInfo) { + createSession { + val info = findByIdentityKey(it, nodeInfo.legalIdentity.owningKey).single() + it.remove(info) + } + } + + private fun findByIdentityKey(session: Session, identityKey: PublicKey): List { + val query = session.createQuery( + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.owningKey = :owningKey", + NodeInfoSchemaV1.PersistentNodeInfo::class.java) + query.setParameter("owningKey", identityKey.toBase58String()) + return query.resultList + } + + private fun queryByIdentityKey(identityKey: PublicKey): List { + createSession { + val result = findByIdentityKey(it, identityKey) + return result.map { it.toNodeInfo() } + } + } + + // TODO It's useless for now, because toString on X500 names is inconsistent and we have: + // C=ES,L=Madrid,O=Alice Corp,CN=Alice Corp + // CN=Alice Corp,O=Alice Corp,L=Madrid,C=ES + private fun queryByLegalName(name: X500Name): List { + createSession { + val query = it.createQuery( + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.name = :name", + NodeInfoSchemaV1.PersistentNodeInfo::class.java) + query.setParameter("name", name.toString()) + val result = query.resultList + return result.map { it.toNodeInfo() } + } + } + + private fun queryByAddress(hostAndPort: NetworkHostAndPort): NodeInfo? { + createSession { + val query = it.createQuery( + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.addresses a WHERE a.pk.host = :host AND a.pk.port = :port", + NodeInfoSchemaV1.PersistentNodeInfo::class.java) + query.setParameter("host", hostAndPort.host) + query.setParameter("port", hostAndPort.port) + val result = query.resultList + return if (result.isEmpty()) null + else result.map { it.toNodeInfo() }.singleOrNull() ?: throw IllegalStateException("More than one node with the same host and port") + } + } + + /** Object Relational Mapping support. */ + private fun generateMappedObject(nodeInfo: NodeInfo): NodeInfoSchemaV1.PersistentNodeInfo { + return NodeInfoSchemaV1.PersistentNodeInfo( + id = 0, + addresses = nodeInfo.addresses.map { NodeInfoSchemaV1.DBHostAndPort.fromHostAndPort(it) }, + legalIdentitiesAndCerts = nodeInfo.legalIdentitiesAndCerts.map { NodeInfoSchemaV1.DBPartyAndCertificate(it) }.toSet() + // TODO It's workaround to keep the main identity, will be removed in future PR getting rid of services. + + NodeInfoSchemaV1.DBPartyAndCertificate(nodeInfo.legalIdentityAndCert, isMain = true), + platformVersion = nodeInfo.platformVersion, + advertisedServices = nodeInfo.advertisedServices.map { NodeInfoSchemaV1.DBServiceEntry(it.serialize().bytes) }, + serial = nodeInfo.serial + ) + } + + override fun clearNetworkMapCache() { + serviceHub.database.transaction { + createSession { + val result = getAllInfos(it) + for (nodeInfo in result) it.remove(nodeInfo) + } + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt index 2409d8a178..f62748dcba 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt @@ -1,6 +1,6 @@ package net.corda.node.services.network -import net.corda.core.crypto.toBase58String +import net.corda.core.utilities.toBase58String import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.ThreadBox import net.corda.core.messaging.SingleMessageRecipient diff --git a/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt similarity index 75% rename from node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt rename to node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt index 03fe0aed2d..4d194f9f90 100644 --- a/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/HibernateConfiguration.kt @@ -1,21 +1,26 @@ -package net.corda.node.services.database +package net.corda.node.services.persistence import net.corda.core.internal.castIfPossible import net.corda.core.node.services.IdentityService import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.converters.AbstractPartyToX500NameAsStringConverter import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.toHexString import net.corda.node.services.api.SchemaService import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.parserTransactionIsolationLevel import org.hibernate.SessionFactory import org.hibernate.boot.MetadataSources -import org.hibernate.boot.model.naming.* +import org.hibernate.boot.model.naming.Identifier +import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder import org.hibernate.cfg.Configuration import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment import org.hibernate.service.UnknownUnwrapTypeException +import org.hibernate.type.AbstractSingleColumnStandardBasicType +import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor +import org.hibernate.type.descriptor.sql.BlobTypeDescriptor import java.sql.Connection import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -57,7 +62,7 @@ class HibernateConfiguration(createSchemaService: () -> SchemaService, private v // We set a connection provider as the auto schema generation requires it. The auto schema generation will not // necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though. // TODO: replace auto schema generation as it isn't intended for production use, according to Hibernate docs. - val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", HibernateConfiguration.NodeDatabaseConnectionProvider::class.java.name) + val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", NodeDatabaseConnectionProvider::class.java.name) .setProperty("hibernate.hbm2ddl.auto", if (databaseProperties.getProperty("initDatabase","true") == "true") "update" else "validate") .setProperty("hibernate.format_sql", "true") .setProperty("hibernate.connection.isolation", transactionIsolationLevel.toString()) @@ -83,7 +88,9 @@ class HibernateConfiguration(createSchemaService: () -> SchemaService, private v }) // register custom converters applyAttributeConverter(AbstractPartyToX500NameAsStringConverter(createIdentityScervice)) - + // Register a tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages. + // to avoid OOM when large blobs might get logged. + applyBasicType(CordaMaterializedBlobType, CordaMaterializedBlobType.name) build() } @@ -117,4 +124,26 @@ class HibernateConfiguration(createSchemaService: () -> SchemaService, private v override fun isUnwrappableAs(unwrapType: Class<*>?): Boolean = unwrapType == NodeDatabaseConnectionProvider::class.java } + + // A tweaked version of `org.hibernate.type.MaterializedBlobType` that truncates logged messages. Also logs in hex. + private object CordaMaterializedBlobType : AbstractSingleColumnStandardBasicType(BlobTypeDescriptor.DEFAULT, CordaPrimitiveByteArrayTypeDescriptor) { + override fun getName(): String { + return "materialized_blob" + } + } + + // A tweaked version of `org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor` that truncates logged messages. + private object CordaPrimitiveByteArrayTypeDescriptor : PrimitiveByteArrayTypeDescriptor() { + private val LOG_SIZE_LIMIT = 1024 + + override fun extractLoggableRepresentation(value: ByteArray?): String { + return if (value == null) super.extractLoggableRepresentation(value) else { + if (value.size <= LOG_SIZE_LIMIT) { + return "[size=${value.size}, value=${value.toHexString()}]" + } else { + return "[size=${value.size}, value=${value.copyOfRange(0, LOG_SIZE_LIMIT).toHexString()}...truncated...]" + } + } + } + } } \ No newline at end of file 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 28125f40ff..94c00f52ca 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 @@ -6,7 +6,7 @@ import com.google.common.hash.HashCode import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import com.google.common.io.CountingInputStream -import net.corda.core.contracts.AbstractAttachment +import net.corda.core.internal.AbstractAttachment import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash import net.corda.core.node.services.AttachmentStorage diff --git a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt index 58fbe944a1..9afc740949 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt @@ -8,7 +8,7 @@ import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentStateRef import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor -import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.persistence.HibernateConfiguration import net.corda.node.utilities.DatabaseTransactionManager import org.hibernate.FlushMode import rx.Observable 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 331ef4461d..9adcace424 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 @@ -5,11 +5,13 @@ import net.corda.core.contracts.FungibleAsset import net.corda.core.contracts.LinearState import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.NodeInfoSchemaV1 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.events.NodeSchedulerService +import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.PersistentKeyManagementService import net.corda.node.services.messaging.NodeMessagingClient import net.corda.node.services.network.PersistentNetworkMapService @@ -20,6 +22,7 @@ import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.services.transactions.RaftUniquenessProvider +import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.VaultSchemaV1 /** @@ -50,7 +53,10 @@ class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaS NodeMessagingClient.RetryMessage::class.java, NodeAttachmentService.DBAttachment::class.java, RaftUniquenessProvider.RaftState::class.java, - BFTNonValidatingNotaryService.PersistedCommittedState::class.java + BFTNonValidatingNotaryService.PersistedCommittedState::class.java, + PersistentIdentityService.PersistentIdentity::class.java, + PersistentIdentityService.PersistentIdentityNames::class.java, + ContractUpgradeServiceImpl.DBContractUpgrade::class.java )) // Required schemas are those used by internal Corda services @@ -58,6 +64,7 @@ class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaS val requiredSchemas: Map = mapOf(Pair(CommonSchemaV1, SchemaService.SchemaOptions()), Pair(VaultSchemaV1, SchemaService.SchemaOptions()), + Pair(NodeInfoSchemaV1, SchemaService.SchemaOptions()), Pair(NodeServicesV1, SchemaService.SchemaOptions())) override val schemaOptions: Map = requiredSchemas.plus(customSchemas.map { 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 977e1b416e..d8fa038ed2 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 @@ -1,9 +1,11 @@ package net.corda.node.services.statemachine import co.paralleluniverse.fibers.Fiber +import co.paralleluniverse.fibers.Fiber.parkAndSerialize import co.paralleluniverse.fibers.FiberScheduler import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.strands.Strand +import com.google.common.primitives.Primitives import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash import net.corda.core.crypto.random63BitValue @@ -165,24 +167,26 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, payload: Any, sessionFlow: FlowLogic<*>, retrySend: Boolean): UntrustworthyData { + requireNonPrimitive(receiveType) logger.debug { "sendAndReceive(${receiveType.name}, $otherParty, ${payload.toString().abbreviate(300)}) ..." } val session = getConfirmedSessionIfPresent(otherParty, sessionFlow) - val sessionData = if (session == null) { + val receivedSessionData: ReceivedSessionMessage = if (session == null) { val newSession = startNewSession(otherParty, sessionFlow, payload, waitForConfirmation = true, retryable = retrySend) // Only do a receive here as the session init has carried the payload - receiveInternal(newSession, receiveType) + receiveInternal(newSession, receiveType) } else { val sendData = createSessionData(session, payload) - sendAndReceiveInternal(session, sendData, receiveType) + sendAndReceiveInternal(session, sendData, receiveType) } - logger.debug { "Received ${sessionData.message.payload.toString().abbreviate(300)}" } - return sessionData.checkPayloadIs(receiveType) + logger.debug { "Received ${receivedSessionData.message.payload.toString().abbreviate(300)}" } + return receivedSessionData.checkPayloadIs(receiveType) } @Suspendable override fun receive(receiveType: Class, otherParty: Party, sessionFlow: FlowLogic<*>): UntrustworthyData { + requireNonPrimitive(receiveType) logger.debug { "receive(${receiveType.name}, $otherParty) ..." } val session = getConfirmedSession(otherParty, sessionFlow) val sessionData = receiveInternal(session, receiveType) @@ -190,6 +194,12 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, return sessionData.checkPayloadIs(receiveType) } + private fun requireNonPrimitive(receiveType: Class<*>) { + require(!receiveType.isPrimitive) { + "Use the wrapper type ${Primitives.wrap(receiveType).name} instead of the primitive $receiveType.class" + } + } + @Suspendable override fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>) { logger.debug { "send($otherParty, ${payload.toString().abbreviate(300)})" } 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 4c2e3b0368..f9d561a43e 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 @@ -2,6 +2,7 @@ package net.corda.node.services.statemachine import co.paralleluniverse.fibers.Fiber import co.paralleluniverse.fibers.FiberExecutorScheduler +import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.instrument.SuspendableHelper import co.paralleluniverse.strands.Strand import com.codahale.metrics.Gauge @@ -39,6 +40,7 @@ import net.corda.node.utilities.wrapWithDatabaseTransaction import net.corda.nodeapi.internal.serialization.SerializeAsTokenContextImpl import net.corda.nodeapi.internal.serialization.withTokenContext import org.apache.activemq.artemis.utils.ReusableLatch +import org.bouncycastle.asn1.x500.X500Name import org.slf4j.Logger import rx.Observable import rx.subjects.PublishSubject @@ -170,7 +172,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, checkQuasarJavaAgentPresence() restoreFibersFromCheckpoints() listenToLedgerTransactions() - serviceHub.networkMapCache.mapServiceRegistered.then { executor.execute(this::resumeRestoredFibers) } + serviceHub.networkMapCache.nodeReady.then { executor.execute(this::resumeRestoredFibers) } } private fun checkQuasarJavaAgentPresence() { @@ -426,6 +428,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } private fun initFiber(fiber: FlowStateMachineImpl<*>) { + verifyFlowLogicIsSuspendable(fiber.logic) fiber.database = database fiber.serviceHub = serviceHub fiber.actionOnSuspend = { ioRequest -> @@ -457,6 +460,19 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } } + private fun verifyFlowLogicIsSuspendable(logic: FlowLogic) { + // Quasar requires (in Java 8) that at least the call method be annotated suspendable. Unfortunately, it's + // easy to forget to add this when creating a new flow, so we check here to give the user a better error. + // + // The Kotlin compiler can sometimes generate a synthetic bridge method from a single call declaration, which + // forwards to the void method and then returns Unit. However annotations do not get copied across to this + // bridge, so we have to do a more complex scan here. + val call = logic.javaClass.methods.first { !it.isSynthetic && it.name == "call" && it.parameterCount == 0 } + if (call.getAnnotation(Suspendable::class.java) == null) { + throw FlowException("${logic.javaClass.name}.call() is not annotated as @Suspendable. Please fix this.") + } + } + private fun endAllFiberSessions(fiber: FlowStateMachineImpl<*>, result: Try<*>, propagated: Boolean) { openSessions.values.removeIf { session -> if (session.fiber == 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 feb4e83e55..70c7cde877 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 @@ -14,10 +14,7 @@ import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.FilteredTransaction -import net.corda.core.utilities.debug -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.unwrap +import net.corda.core.utilities.* import net.corda.node.services.api.ServiceHubInternal import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.NODE_DATABASE_PREFIX @@ -85,7 +82,7 @@ class BFTNonValidatingNotaryService(override val services: ServiceHubInternal, c when (response) { is BFTSMaRt.ClusterResponse.Error -> throw NotaryException(response.error) is BFTSMaRt.ClusterResponse.Signatures -> { - log.debug("All input states of transaction ${stx.rootHash} have been committed") + log.debug("All input states of transaction ${stx.id} have been committed") return response.txSignatures } } @@ -139,16 +136,13 @@ class BFTNonValidatingNotaryService(override val services: ServiceHubInternal, c fun verifyAndCommitTx(ftx: FilteredTransaction, callerIdentity: Party): BFTSMaRt.ReplicaResponse { return try { - val id = ftx.rootHash + val id = ftx.id val inputs = ftx.filteredLeaves.inputs validateTimeWindow(ftx.filteredLeaves.timeWindow) commitInputStates(inputs, id, callerIdentity) - log.debug { "Inputs committed successfully, signing $id" } - val signableData = SignableData(id, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(services.notaryIdentityKey).schemeNumberID)) - val sig = sign(signableData) - BFTSMaRt.ReplicaResponse.Signature(sig) + BFTSMaRt.ReplicaResponse.Signature(sign(ftx)) } catch (e: NotaryException) { log.debug { "Error processing transaction: ${e.error}" } BFTSMaRt.ReplicaResponse.Error(e.error) 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 ba39a29689..072a07bd04 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 @@ -251,8 +251,8 @@ object BFTSMaRt { return services.database.transaction { services.keyManagementService.sign(bytes, services.notaryIdentityKey) } } - protected fun sign(signableData: SignableData): TransactionSignature { - return services.database.transaction { services.keyManagementService.sign(signableData, services.notaryIdentityKey) } + protected fun sign(filteredTransaction: FilteredTransaction): TransactionSignature { + return services.database.transaction { services.createSignature(filteredTransaction, services.notaryIdentityKey) } } // TODO: 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 97bedc4c46..2546d44afc 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 @@ -24,7 +24,7 @@ class NonValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryS when (it) { is FilteredTransaction -> { it.verify() - TransactionParts(it.rootHash, it.filteredLeaves.inputs, it.filteredLeaves.timeWindow) + TransactionParts(it.id, it.filteredLeaves.inputs, it.filteredLeaves.timeWindow) } is NotaryChangeWireTransaction -> TransactionParts(it.id, it.inputs, null) else -> { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt index 8c4d0f18e1..507b3d65e2 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt @@ -2,8 +2,8 @@ package net.corda.node.services.transactions import net.corda.core.contracts.StateRef import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.parsePublicKeyBase58 -import net.corda.core.crypto.toBase58String +import net.corda.core.utilities.parsePublicKeyBase58 +import net.corda.core.utilities.toBase58String import net.corda.core.identity.Party import net.corda.core.internal.ThreadBox import net.corda.core.node.services.UniquenessException 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 69254beeee..ed349f1cc6 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,5 +1,6 @@ package net.corda.node.services.transactions +import com.codahale.metrics.Gauge import io.atomix.catalyst.buffer.BufferInput import io.atomix.catalyst.buffer.BufferOutput import io.atomix.catalyst.serializer.Serializer @@ -45,7 +46,7 @@ import javax.persistence.Lob * to the cluster leader to be actioned. */ @ThreadSafe -class RaftUniquenessProvider(services: ServiceHubInternal) : UniquenessProvider, SingletonSerializeAsToken() { +class RaftUniquenessProvider(private val services: ServiceHubInternal) : UniquenessProvider, SingletonSerializeAsToken() { companion object { private val log = loggerFor() @@ -94,6 +95,8 @@ class RaftUniquenessProvider(services: ServiceHubInternal) : UniquenessProvider, private lateinit var _clientFuture: CompletableFuture private lateinit var server: CopycatServer + + /** * Copycat clients are responsible for connecting to the cluster and submitting commands and queries that operate * on the cluster's replicated state machine. @@ -148,6 +151,8 @@ class RaftUniquenessProvider(services: ServiceHubInternal) : UniquenessProvider, server.bootstrap() } + registerMonitoring() + val client = CopycatClient.builder(address) .withTransport(transport) // TODO: use local transport for client-server communications .withConnectionStrategy(ConnectionStrategies.EXPONENTIAL_BACKOFF) @@ -179,6 +184,21 @@ class RaftUniquenessProvider(services: ServiceHubInternal) : UniquenessProvider, .build() } + private fun registerMonitoring() { + services.monitoringService.metrics.register("RaftCluster.ThisServerStatus", Gauge { + server.state().name + }) + + services.monitoringService.metrics.register("RaftCluster.MembersCount", Gauge { + server.cluster().members().size + }) + + services.monitoringService.metrics.register("RaftCluster.Members", Gauge> { + server.cluster().members().map { it.address().toString() } + }) + } + + override fun commit(states: List, txId: SecureHash, callerIdentity: Party) { val entries = states.mapIndexed { i, stateRef -> stateRef to UniquenessProvider.ConsumingTx(txId, i, callerIdentity) } diff --git a/node/src/main/kotlin/net/corda/node/services/upgrade/ContractUpgradeServiceImpl.kt b/node/src/main/kotlin/net/corda/node/services/upgrade/ContractUpgradeServiceImpl.kt new file mode 100644 index 0000000000..0a7ef3f1c9 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/upgrade/ContractUpgradeServiceImpl.kt @@ -0,0 +1,51 @@ +package net.corda.node.services.upgrade + +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.UpgradedContract +import net.corda.core.node.services.ContractUpgradeService +import net.corda.node.utilities.NODE_DATABASE_PREFIX +import net.corda.node.utilities.PersistentMap +import javax.persistence.* + +class ContractUpgradeServiceImpl : ContractUpgradeService { + + @Entity + @Table(name = "${NODE_DATABASE_PREFIX}contract_upgrades") + class DBContractUpgrade( + @Id + @Column(name = "state_ref", length = 96) + var stateRef: String = "", + + /** refers to the UpgradedContract class name*/ + @Column(name = "contract_class_name") + var upgradedContractClassName: String = "" + ) + + private companion object { + fun createContractUpgradesMap(): PersistentMap { + return PersistentMap( + toPersistentEntityKey = { it }, + fromPersistentEntity = { Pair(it.stateRef, it.upgradedContractClassName) }, + toPersistentEntity = { key: String, value: String -> + DBContractUpgrade().apply { + stateRef = key + upgradedContractClassName = value + } + }, + persistentEntityClass = DBContractUpgrade::class.java + ) + } + } + + private val authorisedUpgrade = createContractUpgradesMap() + + override fun getAuthorisedContractUpgrade(ref: StateRef) = authorisedUpgrade[ref.toString()] + + override fun storeAuthorisedContractUpgrade(ref: StateRef, upgradedContractClass: Class>) { + authorisedUpgrade.put(ref.toString(), upgradedContractClass.name) + } + + override fun removeAuthorisedContractUpgrade(ref: StateRef) { + authorisedUpgrade.remove(ref.toString()) + } +} \ 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 b6cfebe4cd..76c4ba0f60 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 @@ -1,11 +1,11 @@ package net.corda.node.services.vault -import net.corda.core.internal.ThreadBox import net.corda.core.contracts.ContractState 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.internal.ThreadBox import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.messaging.DataFeed import net.corda.core.node.services.Vault @@ -20,7 +20,7 @@ import net.corda.core.serialization.deserialize import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace -import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.persistence.HibernateConfiguration import net.corda.node.utilities.DatabaseTransactionManager import org.hibernate.Session import rx.Observable 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 64fb9c81fa..bf8cc64458 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 @@ -4,7 +4,6 @@ import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.strands.Strand import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.containsAny import net.corda.core.internal.ThreadBox import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.tee @@ -115,7 +114,7 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT * indicate whether an update consists entirely of regular or notary change transactions, which may require * different processing logic. */ - override fun notifyAll(txns: Iterable) { + fun notifyAll(txns: Iterable) { // It'd be easier to just group by type, but then we'd lose ordering. val regularTxns = mutableListOf() val notaryChangeTxns = mutableListOf() @@ -143,11 +142,14 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT if (notaryChangeTxns.isNotEmpty()) notifyNotaryChange(notaryChangeTxns.toList()) } + /** Same as notifyAll but with a single transaction. */ + fun notify(tx: CoreTransaction) = notifyAll(listOf(tx)) + private fun notifyRegular(txns: Iterable) { - val ourKeys = services.keyManagementService.keys fun makeUpdate(tx: WireTransaction): Vault.Update { + val myKeys = services.keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }) val ourNewStates = tx.outputs. - filter { isRelevant(it.data, ourKeys) }. + filter { isRelevant(it.data, myKeys.toSet()) }. map { tx.outRef(it.data) } // Retrieve all unconsumed states for this transaction's inputs @@ -167,19 +169,15 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT } private fun notifyNotaryChange(txns: Iterable) { - val ourKeys = services.keyManagementService.keys fun makeUpdate(tx: NotaryChangeWireTransaction): Vault.Update { // We need to resolve the full transaction here because outputs are calculated from inputs // We also can't do filtering beforehand, since output encumbrance pointers get recalculated based on // input positions val ltx = tx.resolve(services, emptyList()) - + val myKeys = services.keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } }) val (consumedStateAndRefs, producedStates) = ltx.inputs. zip(ltx.outputs). - filter { - (_, output) -> - isRelevant(output.data, ourKeys) - }. + filter { (_, output) -> isRelevant(output.data, myKeys.toSet()) }. unzip() val producedStateAndRefs = producedStates.map { ltx.outRef(it.data) } @@ -373,27 +371,14 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT return claimedStates } - // TODO : Persists this in DB. - private val authorisedUpgrade = mutableMapOf>>() - override fun getAuthorisedContractUpgrade(ref: StateRef) = authorisedUpgrade[ref] - - override fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class>) { - val upgrade = upgradedContractClass.newInstance() - if (upgrade.legacyContract != stateAndRef.state.data.contract.javaClass) { - throw IllegalArgumentException("The contract state cannot be upgraded using provided UpgradedContract.") - } - authorisedUpgrade.put(stateAndRef.ref, upgradedContractClass) - } - - override fun deauthoriseContractUpgrade(stateAndRef: StateAndRef<*>) { - authorisedUpgrade.remove(stateAndRef.ref) - } @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() + internal fun isRelevant(state: ContractState, myKeys: Set): Boolean { + val keysToCheck = when (state) { + is OwnableState -> listOf(state.owner.owningKey) + else -> state.participants.map { it.owningKey } + } + return keysToCheck.any { it in myKeys } } } 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 7211d06d9b..72ae711a93 100644 --- a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt +++ b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt @@ -1,6 +1,6 @@ package net.corda.node.shell -import net.corda.core.crypto.commonName +import net.corda.core.utilities.organisation import net.corda.core.flows.FlowInitiator import net.corda.core.flows.StateMachineRunId import net.corda.core.internal.concurrent.openFuture @@ -110,7 +110,7 @@ class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Sub return when (flowInitiator) { is FlowInitiator.Scheduled -> flowInitiator.scheduledState.ref.toString() is FlowInitiator.Shell -> "Shell" // TODO Change when we will have more information on shell user. - is FlowInitiator.Peer -> flowInitiator.party.name.commonName + is FlowInitiator.Peer -> flowInitiator.party.name.organisation is FlowInitiator.RPC -> "RPC: " + flowInitiator.username } } 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 37386f42f0..cc863b3193 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt +++ b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt @@ -21,8 +21,8 @@ import net.corda.core.internal.* import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.StateMachineUpdate import net.corda.core.utilities.loggerFor -import net.corda.jackson.JacksonSupport -import net.corda.jackson.StringToMethodCallParser +import net.corda.client.jackson.JacksonSupport +import net.corda.client.jackson.StringToMethodCallParser import net.corda.node.internal.Node import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT import net.corda.node.services.messaging.RpcContext @@ -293,6 +293,10 @@ object InteractiveShell { continue } val flow = ctor.newInstance(*args) as FlowLogic<*> + if (flow.progressTracker == null) { + errors.add("A flow must override the progress tracker in order to be run from the shell") + continue + } return invoke(flow) } catch(e: StringToMethodCallParser.UnparseableCallException.MissingParameter) { errors.add("${getPrototype()}: missing parameter ${e.paramName}") diff --git a/node/src/main/kotlin/net/corda/node/utilities/ClockUtils.kt b/node/src/main/kotlin/net/corda/node/utilities/ClockUtils.kt deleted file mode 100644 index 44f3d0ec2c..0000000000 --- a/node/src/main/kotlin/net/corda/node/utilities/ClockUtils.kt +++ /dev/null @@ -1,115 +0,0 @@ -package net.corda.node.utilities - -import co.paralleluniverse.fibers.Suspendable -import co.paralleluniverse.strands.SettableFuture -import com.google.common.util.concurrent.ListenableFuture -import net.corda.core.internal.until -import rx.Observable -import rx.Subscriber -import rx.subscriptions.Subscriptions -import java.time.Clock -import java.time.Instant -import java.util.concurrent.* -import java.util.concurrent.atomic.AtomicLong -import com.google.common.util.concurrent.SettableFuture as GuavaSettableFuture - -/** - * The classes and methods in this file allow the use of custom Clocks in demos, simulations and testing - * that might not follow "real time" or "wall clock time". i.e. they allow time to be fast forwarded. - * - * We should try to make the Clock used in our code injectable (for tests etc) and to use the extensions below - * to wait in our code, rather than Thread.sleep() or Object.wait() etc. - */ - -/** - * An abstract class with helper methods for a type of Clock that might have it's concept of "now" - * adjusted externally. - * - * e.g. for testing (so unit tests do not have to wait for timeouts in realtime) or for demos and simulations. - */ -abstract class MutableClock : Clock() { - - private val _version = AtomicLong(0L) - - /** - * This is an observer on the mutation count of this [Clock], which reflects the occurence of mutations. - */ - val mutations: Observable by lazy { - Observable.create({ subscriber: Subscriber -> - if (!subscriber.isUnsubscribed) { - mutationObservers.add(subscriber) - // This is not very intuitive, but subscribing to a subscriber observes unsubscribes. - subscriber.add(Subscriptions.create { mutationObservers.remove(subscriber) }) - } - }) - } - - private val mutationObservers = CopyOnWriteArraySet>() - - /** - * Must be called by subclasses when they mutate (but not just with the passage of time as per the "wall clock"). - */ - protected fun notifyMutationObservers() { - val version = _version.incrementAndGet() - for (observer in mutationObservers) { - if (!observer.isUnsubscribed) { - observer.onNext(version) - } - } - } -} - -/** - * Wait until the given [Future] is complete or the deadline is reached, with support for [MutableClock] implementations - * used in demos or testing. This will substitute a Fiber compatible Future so the current - * [co.paralleluniverse.strands.Strand] is not blocked. - * - * @return true if the [Future] is complete, false if the deadline was reached. - */ -@Suspendable -fun Clock.awaitWithDeadline(deadline: Instant, future: Future<*> = GuavaSettableFuture.create()): Boolean { - var nanos: Long - do { - val originalFutureCompleted = makeStrandFriendlySettableFuture(future) - val subscription = if (this is MutableClock) { - mutations.first().subscribe { - originalFutureCompleted.set(false) - } - } else { - null - } - nanos = (instant() until deadline).toNanos() - if (nanos > 0) { - try { - // This will return when it times out, or when the clock mutates or when when the original future completes. - originalFutureCompleted.get(nanos, TimeUnit.NANOSECONDS) - } catch(e: ExecutionException) { - // No need to take action as will fall out of the loop due to future.isDone - } catch(e: CancellationException) { - // No need to take action as will fall out of the loop due to future.isDone - } catch(e: TimeoutException) { - // No need to take action as will fall out of the loop due to future.isDone - } - } - subscription?.unsubscribe() - originalFutureCompleted.cancel(false) - } while (nanos > 0 && !future.isDone) - return future.isDone -} - -/** - * Convert a Guava [ListenableFuture] or JDK8 [CompletableFuture] to Quasar implementation and set to true when a result - * or [Throwable] is available in the original. - * - * We need this so that we do not block the actual thread when calling get(), but instead allow a Quasar context - * switch. There's no need to checkpoint our Fibers as there's no external effect of waiting. - */ -private fun makeStrandFriendlySettableFuture(future: Future) = SettableFuture().also { g -> - when (future) { - is ListenableFuture -> future.addListener(Runnable { g.set(true) }, Executor { it.run() }) - is CompletionStage<*> -> future.whenComplete { _, _ -> g.set(true) } - else -> throw IllegalArgumentException("Cannot make future $future Fiber friendly.") - } -} - - diff --git a/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt b/node/src/main/kotlin/net/corda/node/utilities/ContentSignerBuilder.kt similarity index 95% rename from core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt rename to node/src/main/kotlin/net/corda/node/utilities/ContentSignerBuilder.kt index 8a62f308c4..f2a365bf89 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/ContentSignerBuilder.kt @@ -1,5 +1,6 @@ -package net.corda.core.crypto +package net.corda.node.utilities +import net.corda.core.crypto.SignatureScheme import org.bouncycastle.asn1.x509.AlgorithmIdentifier import org.bouncycastle.operator.ContentSigner import java.io.OutputStream diff --git a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt index b8add3a202..de78ac1071 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt @@ -4,7 +4,7 @@ import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import net.corda.core.node.services.IdentityService import net.corda.node.services.api.SchemaService -import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.services.persistence.HibernateConfiguration import net.corda.node.services.schema.NodeSchemaService import org.hibernate.SessionFactory diff --git a/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt b/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt index fbe2947a70..03427411e9 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt @@ -1,8 +1,8 @@ package net.corda.node.utilities -import net.corda.core.crypto.CertificateAndKeyPair +import net.corda.core.utilities.CertificateAndKeyPair import net.corda.core.crypto.Crypto -import net.corda.core.crypto.cert +import net.corda.core.utilities.cert import net.corda.core.internal.exists import net.corda.core.internal.read import net.corda.core.internal.toX509CertHolder @@ -183,10 +183,11 @@ class KeyStoreWrapper(private val storePath: Path, private val storePassword: St val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey) val certPath = CertificateFactory.getInstance("X509").generateCertPath(listOf(cert.cert) + clientCertPath) require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" } + // TODO: X509Utilities.validateCertificateChain() return certPath } - fun saveNewKeyPair(serviceName: X500Name, privateKeyAlias: String, keyPair: KeyPair) { + fun signAndSaveNewKeyPair(serviceName: X500Name, privateKeyAlias: String, keyPair: KeyPair) { val certPath = createCertificate(serviceName, keyPair.public) // Assume key password = store password. keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), certPath.certificates.toTypedArray()) diff --git a/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt index 2d43aae33c..55acb14efc 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt @@ -57,7 +57,7 @@ class PersistentMap ( } fun all(): Sequence> { - return cache.asMap().asSequence().map { Pair(it.key, it.value.get()) } + return cache.asMap().asSequence().filter { it.value.isPresent }.map { Pair(it.key, it.value.get()) } } override val size get() = cache.size().toInt() 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 fc87226b3d..81d10b45b6 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt @@ -1,6 +1,6 @@ package net.corda.node.utilities -import net.corda.core.crypto.composite.CompositeKey +import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.Party import net.corda.core.utilities.loggerFor diff --git a/node/src/main/kotlin/net/corda/node/utilities/TestClock.kt b/node/src/main/kotlin/net/corda/node/utilities/TestClock.kt index 3d1c57d312..e582f45147 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/TestClock.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/TestClock.kt @@ -4,6 +4,7 @@ import net.corda.core.internal.until import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializeAsTokenContext import net.corda.core.serialization.SingletonSerializationToken.Companion.singletonSerializationToken +import net.corda.node.internal.MutableClock import java.time.Clock import java.time.Instant import java.time.LocalDate diff --git a/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt b/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt index f10261826f..7f67a4a8ac 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt @@ -1,12 +1,17 @@ package net.corda.node.utilities -import net.corda.core.crypto.* +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SignatureScheme +import net.corda.core.crypto.random63BitValue +import net.corda.core.utilities.cert import net.corda.core.utilities.days import net.corda.core.utilities.millis import org.bouncycastle.asn1.ASN1EncodableVector import org.bouncycastle.asn1.ASN1Sequence import org.bouncycastle.asn1.DERSequence +import org.bouncycastle.asn1.DERUTF8String import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x500.style.BCStyle import org.bouncycastle.asn1.x509.* import org.bouncycastle.asn1.x509.Extension import org.bouncycastle.cert.X509CertificateHolder @@ -43,6 +48,8 @@ object X509Utilities { val CORDA_CLIENT_TLS = "cordaclienttls" val CORDA_CLIENT_CA = "cordaclientca" + val CORDA_CLIENT_CA_CN = "Corda Client CA Certificate" + private val DEFAULT_VALIDITY_WINDOW = Pair(0.millis, 3650.days) /** * Helper function to return the latest out of an instant and an optional date. @@ -106,6 +113,7 @@ object X509Utilities { return createCertificate(certificateType, issuerCertificate.subject, issuerKeyPair, subject, subjectPublicKey, window, nameConstraints) } + @Throws(CertPathValidatorException::class) fun validateCertificateChain(trustedRoot: X509CertificateHolder, vararg certificates: Certificate) { require(certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" } val certFactory = CertificateFactory.getInstance("X509") @@ -224,12 +232,12 @@ object X509Utilities { /** * Create certificate signing request using provided information. */ - fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair, signatureScheme: SignatureScheme): PKCS10CertificationRequest { + fun createCertificateSigningRequest(subject: X500Name, email: String, keyPair: KeyPair, signatureScheme: SignatureScheme): PKCS10CertificationRequest { val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.findProvider(signatureScheme.providerName)) - return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).build(signer) + return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).addAttribute(BCStyle.E, DERUTF8String(email)).build(signer) } - fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair) = createCertificateSigningRequest(subject, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME) + fun createCertificateSigningRequest(subject: X500Name, email: String, keyPair: KeyPair) = createCertificateSigningRequest(subject, email, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME) } diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 268b89fa53..bcd1c17674 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -1,7 +1,7 @@ package net.corda.node.utilities.registration import net.corda.core.crypto.Crypto -import net.corda.core.crypto.cert +import net.corda.core.utilities.cert import net.corda.core.internal.* import net.corda.core.utilities.seconds import net.corda.core.utilities.validateX500Name @@ -19,13 +19,10 @@ import java.security.cert.Certificate import kotlin.system.exitProcess /** - * This checks the config.certificatesDirectory field for certificates required to connect to a Corda network. - * If the certificates are not found, a [org.bouncycastle.pkcs.PKCS10CertificationRequest] will be submitted to - * Corda network permissioning server using [NetworkRegistrationService]. This process will enter a polling loop until - * the request has been approved, and then the certificate chain will be downloaded and stored in [KeyStore] reside in - * the certificates directory. + * Helper for managing the node registration process, which checks for any existing certificates and requests them if + * needed. */ -class NetworkRegistrationHelper(val config: NodeConfiguration, val certService: NetworkRegistrationService) { +class NetworkRegistrationHelper(private val config: NodeConfiguration, private val certService: NetworkRegistrationService) { companion object { val pollInterval = 10.seconds val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key" @@ -36,6 +33,17 @@ class NetworkRegistrationHelper(val config: NodeConfiguration, val certService: // TODO: Use different password for private key. private val privateKeyPassword = config.keyStorePassword + /** + * Ensure the initial keystore for a node is set up; note that this function may cause the process to exit under + * some circumstances. + * + * This checks the "config.certificatesDirectory" field for certificates required to connect to a Corda network. + * If the certificates are not found, a PKCS #10 certification request will be submitted to the + * Corda network permissioning server via [NetworkRegistrationService]. This process will enter a polling loop until + * the request has been approved, and then the certificate chain will be downloaded and stored in [KeyStore] reside in + * the certificates directory. + */ + // TODO: Stop killing the calling process from within a called function. fun buildKeystore() { validateX500Name(config.myLegalName) config.certificatesDirectory.createDirectories() @@ -92,7 +100,6 @@ class NetworkRegistrationHelper(val config: NodeConfiguration, val certService: } } - /** * Poll Certificate Signing Server for approved certificate, * enter a slow polling loop if server return null. @@ -119,7 +126,7 @@ class NetworkRegistrationHelper(val config: NodeConfiguration, val certService: private fun submitOrResumeCertificateSigningRequest(keyPair: KeyPair): String { // Retrieve request id from file if exists, else post a request to server. return if (!requestIdStore.exists()) { - val request = X509Utilities.createCertificateSigningRequest(config.myLegalName, keyPair) + val request = X509Utilities.createCertificateSigningRequest(config.myLegalName, config.emailAddress, keyPair) val writer = StringWriter() JcaPEMWriter(writer).use { it.writeObject(PemObject("CERTIFICATE REQUEST", request.encoded)) diff --git a/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt b/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt index 4093f6b935..26a913db24 100644 --- a/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt +++ b/node/src/smoke-test/kotlin/net/corda/node/CordappSmokeTest.kt @@ -9,11 +9,11 @@ import net.corda.core.internal.div import net.corda.core.internal.list import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.getX500Name 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 @@ -28,7 +28,7 @@ class CordappSmokeTest { private val factory = NodeProcess.Factory() private val aliceConfig = NodeConfig( - legalName = X500Name("CN=Alice Corp,O=Alice Corp,L=Madrid,C=ES"), + legalName = getX500Name(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 455f22ab3a..e14acce986 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 @@ -4,7 +4,6 @@ import com.google.common.collect.ImmutableSet; import kotlin.Pair; import kotlin.Triple; import net.corda.core.contracts.*; -import net.corda.core.crypto.EncodingUtils; import net.corda.core.identity.AbstractParty; import net.corda.core.messaging.DataFeed; import net.corda.core.node.services.IdentityService; @@ -16,6 +15,7 @@ import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria; import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; import net.corda.core.schemas.MappedSchema; +import net.corda.core.utilities.EncodingUtils; import net.corda.core.utilities.OpaqueBytes; import net.corda.finance.contracts.DealState; import net.corda.finance.contracts.asset.Cash; @@ -49,8 +49,8 @@ import static net.corda.finance.contracts.asset.CashUtilities.getDUMMY_CASH_ISSU import static net.corda.testing.CoreTestUtils.*; import static net.corda.testing.TestConstants.getDUMMY_NOTARY; import static net.corda.testing.TestConstants.getDUMMY_NOTARY_KEY; -import static net.corda.testing.node.MockServicesKt.makeTestDatabaseAndMockServices; -import static net.corda.testing.node.MockServicesKt.makeTestIdentityService; +import static net.corda.testing.node.MockServices.makeTestDatabaseAndMockServices; +import static net.corda.testing.node.MockServices.makeTestIdentityService; import static org.assertj.core.api.Assertions.assertThat; public class VaultQueryJavaTests extends TestDependencyInjectionBase { diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 158a8ce8b3..ebfd43b407 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -7,6 +7,7 @@ import net.corda.core.contracts.Issued import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.keys import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StateMachineRunId import net.corda.core.messaging.* import net.corda.core.node.services.ServiceInfo @@ -44,6 +45,7 @@ import rx.Observable import java.io.ByteArrayOutputStream import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNull import kotlin.test.assertTrue class CordaRPCOpsImplTest { @@ -72,11 +74,8 @@ class CordaRPCOpsImplTest { startFlowPermission() )))) - aliceNode.database.transaction { - stateMachineUpdates = rpc.stateMachinesFeed().updates - transactions = rpc.verifiedTransactionsFeed().updates - vaultTrackCash = rpc.vaultTrackBy().updates - } + mockNet.runNetwork() + networkMap.ensureRegistered() } @After @@ -86,6 +85,11 @@ class CordaRPCOpsImplTest { @Test fun `cash issue accepted`() { + aliceNode.database.transaction { + stateMachineUpdates = rpc.stateMachinesFeed().updates + vaultTrackCash = rpc.vaultTrackBy().updates + } + val quantity = 1000L val ref = OpaqueBytes(ByteArray(1) { 1 }) @@ -131,6 +135,12 @@ class CordaRPCOpsImplTest { @Test fun `issue and move`() { + aliceNode.database.transaction { + stateMachineUpdates = rpc.stateMachinesFeed().updates + transactions = rpc.internalVerifiedTransactionsFeed().updates + vaultTrackCash = rpc.vaultTrackBy().updates + } + val anonymous = false val result = rpc.startFlow(::CashIssueFlow, 100.DOLLARS, @@ -248,4 +258,20 @@ class CordaRPCOpsImplTest { @Suspendable override fun call() = Unit } + + @Test + fun `attempt to start RPC flow with void return`() { + CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = setOf( + startFlowPermission() + )))) + val result = rpc.startFlow(::VoidRPCFlow) + mockNet.runNetwork() + assertNull(result.returnValue.getOrThrow()) + } + + @StartableByRPC + class VoidRPCFlow : FlowLogic() { + @Suspendable + override fun call() : Void? = null + } } diff --git a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt index 6a70ea30a0..a7cdbe34d7 100644 --- a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt +++ b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt @@ -10,7 +10,8 @@ import net.corda.core.internal.FlowStateMachine import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.UntrustworthyData -import net.corda.jackson.JacksonSupport +import net.corda.client.jackson.JacksonSupport +import net.corda.core.utilities.ProgressTracker import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.shell.InteractiveShell import net.corda.testing.DUMMY_CA @@ -30,6 +31,7 @@ class InteractiveShellTest { constructor(pair: Pair, SecureHash.SHA256>) : this(pair.toString()) constructor(party: Party) : this(party.name.toString()) + override val progressTracker = ProgressTracker() override fun call() = a } @@ -94,4 +96,4 @@ class InteractiveShellTest { override fun flowStackSnapshot(flowClass: Class>): FlowStackSnapshot? = null override fun persistFlowStackSnapshot(flowClass: Class>) = Unit } -} \ No newline at end of file +} 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 55b1ff406b..4d0c8c60da 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -20,6 +20,7 @@ import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.node.NodeInfo import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault +import net.corda.core.serialization.CordaSerializable import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder @@ -145,19 +146,18 @@ class TwoPartyTradeFlowTests { val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) - val cashIssuer = bankNode.info.legalIdentity.ref(1) - val cpIssuer = bankNode.info.legalIdentity.ref(1, 2, 3) + val issuer = bankNode.info.legalIdentity.ref(1) aliceNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop() val cashStates = bobNode.database.transaction { bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, notaryNode.info.notaryIdentity, 3, 3, - issuedBy = cashIssuer) + issuedBy = issuer) } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, cpIssuer, aliceNode.info.legalIdentity, + fillUpForSeller(false, issuer, aliceNode.info.legalIdentity, 1200.DOLLARS `issued by` bankNode.info.legalIdentity.ref(0), null, notaryNode.info.notaryIdentity).second } @@ -199,11 +199,22 @@ class TwoPartyTradeFlowTests { val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) var bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) - val cashIssuer = bankNode.info.legalIdentity.ref(1) - val cpIssuer = bankNode.info.legalIdentity.ref(1, 2, 3) + val issuer = bankNode.info.legalIdentity.ref(1, 2, 3) - aliceNode.services.identityService.verifyAndRegisterIdentity(bobNode.info.legalIdentityAndCert) - bobNode.services.identityService.verifyAndRegisterIdentity(aliceNode.info.legalIdentityAndCert) + // Let the nodes know about each other - normally the network map would handle this + val allNodes = listOf(notaryNode, aliceNode, bobNode, bankNode) + allNodes.forEach { node -> + node.database.transaction { + allNodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> node.services.identityService.registerIdentity(identity) } + } + } + + aliceNode.database.transaction { + aliceNode.services.identityService.verifyAndRegisterIdentity(bobNode.info.legalIdentityAndCert) + } + bobNode.database.transaction { + bobNode.services.identityService.verifyAndRegisterIdentity(aliceNode.info.legalIdentityAndCert) + } aliceNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop() @@ -214,10 +225,10 @@ class TwoPartyTradeFlowTests { bobNode.database.transaction { bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, outputNotary = notaryNode.info.notaryIdentity, - issuedBy = cashIssuer) + issuedBy = issuer) } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, cpIssuer, aliceNode.info.legalIdentity, + fillUpForSeller(false, issuer, aliceNode.info.legalIdentity, 1200.DOLLARS `issued by` bankNode.info.legalIdentity.ref(0), null, notaryNode.info.notaryIdentity).second } insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode) @@ -253,6 +264,9 @@ class TwoPartyTradeFlowTests { // She will wait around until Bob comes back. assertThat(aliceNode.pumpReceive()).isNotNull() + // FIXME: Knowledge of confidential identities is lost on node shutdown, so Bob's node now refuses to sign the + // transaction because it has no idea who the parties are. + // ... bring the node back up ... the act of constructing the SMM will re-register the message handlers // that Bob was waiting on before the reboot occurred. bobNode = mockNet.createNode(networkMapAddress, bobAddr.id, object : MockNetwork.Factory { @@ -325,9 +339,14 @@ class TwoPartyTradeFlowTests { val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name) val issuer = bankNode.info.legalIdentity.ref(1, 2, 3) + mockNet.runNetwork() + notaryNode.ensureRegistered() + val allNodes = listOf(notaryNode, aliceNode, bobNode, bankNode) allNodes.forEach { node -> - allNodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> node.services.identityService.verifyAndRegisterIdentity(identity) } + node.database.transaction { + allNodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> node.services.identityService.verifyAndRegisterIdentity(identity) } + } } ledger(aliceNode.services, initialiseSerialization = false) { @@ -431,13 +450,19 @@ class TwoPartyTradeFlowTests { val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name) val issuer = bankNode.info.legalIdentity.ref(1, 2, 3) + mockNet.runNetwork() + notaryNode.ensureRegistered() + val allNodes = listOf(notaryNode, aliceNode, bobNode, bankNode) allNodes.forEach { node -> - allNodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> node.services.identityService.verifyAndRegisterIdentity(identity) } + node.database.transaction { + allNodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> + node.services.identityService.verifyAndRegisterIdentity(identity) + } + } } ledger(aliceNode.services, initialiseSerialization = false) { - // Insert a prospectus type attachment into the commercial paper transaction. val stream = ByteArrayOutputStream() JarOutputStream(stream).use { @@ -529,13 +554,11 @@ class TwoPartyTradeFlowTests { private fun runBuyerAndSeller(notaryNode: MockNetwork.MockNode, sellerNode: MockNetwork.MockNode, buyerNode: MockNetwork.MockNode, - assetToSell: StateAndRef): RunResult { - val anonymousSeller = sellerNode.services.let { serviceHub -> - serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, false) - }.party.anonymise() - val buyerFlows: Observable = buyerNode.registerInitiatedFlow(BuyerAcceptor::class.java) + assetToSell: StateAndRef, + anonymous: Boolean = true): RunResult { + val buyerFlows: Observable> = buyerNode.registerInitiatedFlow(BuyerAcceptor::class.java) val firstBuyerFiber = buyerFlows.toFuture().map { it.stateMachine } - val seller = SellerInitiator(buyerNode.info.legalIdentity, notaryNode.info, assetToSell, 1000.DOLLARS, anonymousSeller) + val seller = SellerInitiator(buyerNode.info.legalIdentity, notaryNode.info, assetToSell, 1000.DOLLARS, anonymous) val sellerResult = sellerNode.services.startFlow(seller).resultFuture return RunResult(firstBuyerFiber, sellerResult, seller.stateMachine.id) } @@ -545,10 +568,15 @@ class TwoPartyTradeFlowTests { val notary: NodeInfo, val assetToSell: StateAndRef, val price: Amount, - val me: AnonymousParty) : FlowLogic() { + val anonymous: Boolean) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { - send(buyer, Pair(notary.notaryIdentity, price)) + val me = if (anonymous) { + serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, false) + } else { + serviceHub.myInfo.legalIdentityAndCert + } + send(buyer, TestTx(notary.notaryIdentity, price, anonymous)) return subFlow(Seller( buyer, notary, @@ -562,14 +590,17 @@ class TwoPartyTradeFlowTests { class BuyerAcceptor(val seller: Party) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { - val (notary, price) = receive>>(seller).unwrap { - require(serviceHub.networkMapCache.isNotary(it.first)) { "${it.first} is not a notary" } + val (notary, price, anonymous) = receive(seller).unwrap { + require(serviceHub.networkMapCache.isNotary(it.notaryIdentity)) { "${it.notaryIdentity} is not a notary" } it } - return subFlow(Buyer(seller, notary, price, CommercialPaper.State::class.java)) + return subFlow(Buyer(seller, notary, price, CommercialPaper.State::class.java, anonymous)) } } + @CordaSerializable + data class TestTx(val notaryIdentity: Party, val price: Amount, val anonymous: Boolean) + private fun LedgerDSL.runWithError( bobError: Boolean, aliceError: Boolean, @@ -581,6 +612,17 @@ class TwoPartyTradeFlowTests { val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name) val issuer = bankNode.info.legalIdentity.ref(1, 2, 3) + mockNet.runNetwork() + notaryNode.ensureRegistered() + + // Let the nodes know about each other - normally the network map would handle this + val allNodes = listOf(notaryNode, aliceNode, bobNode, bankNode) + allNodes.forEach { node -> + node.database.transaction { + allNodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> node.services.identityService.registerIdentity(identity) } + } + } + val bobsBadCash = bobNode.database.transaction { fillUpForBuyer(bobError, issuer, bobNode.info.legalIdentity, notaryNode.info.notaryIdentity).second 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 3ffcee67e6..696e27ee96 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -9,6 +9,7 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.getX500Name import net.corda.core.utilities.seconds import net.corda.node.internal.AbstractNode import net.corda.node.services.network.NetworkMapService @@ -46,6 +47,7 @@ class NotaryChangeTests { newNotaryNode = mockNet.createNode(networkMapAddress = oldNotaryNode.network.myAddress, advertisedServices = ServiceInfo(SimpleNotaryService.type)) mockNet.runNetwork() // Clear network map registration messages + oldNotaryNode.ensureRegistered() } @After @@ -85,7 +87,7 @@ class NotaryChangeTests { @Test fun `should throw when a participant refuses to change Notary`() { val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode) - val newEvilNotary = getTestPartyAndCertificate(X500Name("CN=Evil Notary,O=Evil R3,OU=corda,L=London,C=GB"), generateKeyPair().public) + val newEvilNotary = getTestPartyAndCertificate(getX500Name(OU="Evil Notary",O="Evil R3",L="London",C="GB"), generateKeyPair().public) val flow = NotaryChangeFlow(state, newEvilNotary.party) val future = clientNodeA.services.startFlow(flow) 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 03adc6102d..2510504774 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,11 +1,11 @@ package net.corda.node.services.config -import net.corda.core.crypto.commonName +import net.corda.core.utilities.organisation import net.corda.core.utilities.NetworkHostAndPort -import net.corda.testing.ALICE import net.corda.nodeapi.User -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties +import net.corda.testing.ALICE +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test import java.net.URL @@ -21,7 +21,7 @@ class FullNodeConfigurationTest { emailAddress = "", keyStorePassword = "cordacadevpass", trustStorePassword = "trustpass", - dataSourceProperties = makeTestDataSourceProperties(ALICE.name.commonName), + dataSourceProperties = makeTestDataSourceProperties(ALICE.name.organisation), database = makeTestDatabaseProperties(), certificateSigningService = URL("http://localhost"), rpcUsers = emptyList(), 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 deleted file mode 100644 index e69de29bb2..0000000000 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 df17f414c2..370d7667a1 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 @@ -1,30 +1,33 @@ package net.corda.node.services.events +import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* -import net.corda.core.utilities.days import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogicRef import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.node.services.VaultService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.TransactionBuilder -import net.corda.node.services.MockServiceHubInternal -import net.corda.node.services.database.HibernateConfiguration +import net.corda.core.utilities.days +import net.corda.core.utilities.getX500Name import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.persistence.DBCheckpointStorage -import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.vault.NodeVaultService +import net.corda.node.testing.MockServiceHubInternal import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase import net.corda.testing.* import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockKeyManagementService -import net.corda.testing.node.* +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties +import net.corda.testing.node.MockServices.Companion.makeTestIdentityService import net.corda.testing.node.TestClock import org.assertj.core.api.Assertions.assertThat import org.bouncycastle.asn1.x500.X500Name @@ -32,7 +35,6 @@ import org.junit.After import org.junit.Before import org.junit.Test import java.nio.file.Paths -import java.security.PublicKey import java.time.Clock import java.time.Instant import java.util.concurrent.CountDownLatch @@ -87,7 +89,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { database) services = object : MockServiceHubInternal( database, - testNodeConfiguration(Paths.get("."), getTestX509Name("Alice")), + testNodeConfiguration(Paths.get("."), getX500Name(O = "Alice", L = "London", C = "GB")), overrideClock = testClock, keyManagement = kms, network = mockMessagingService), TestReference { @@ -120,14 +122,12 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { resetTestSerialization() } - class TestState(val flowLogicRef: FlowLogicRef, val instant: Instant) : LinearState, SchedulableState { + class TestState(val flowLogicRef: FlowLogicRef, val instant: Instant, private val myIdentity: Party) : LinearState, SchedulableState { override val participants: List - get() = throw UnsupportedOperationException() + get() = listOf(myIdentity) override val linearId = UniqueIdentifier() - override fun isRelevant(ourKeys: Set): Boolean = true - override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? { return ScheduledActivity(flowLogicRef, instant) } @@ -137,6 +137,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { } class TestFlowLogic(val increment: Int = 1) : FlowLogic() { + @Suspendable override fun call() { (serviceHub as TestReference).testReference.calls += increment (serviceHub as TestReference).testReference.countDown.countDown() @@ -279,7 +280,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { database.transaction { apply { val freshKey = services.keyManagementService.freshKey() - val state = TestState(FlowLogicRefFactoryImpl.createForRPC(TestFlowLogic::class.java, increment), instant) + val state = TestState(FlowLogicRefFactoryImpl.createForRPC(TestFlowLogic::class.java, increment), instant, services.myInfo.legalIdentity) val builder = TransactionBuilder(null).apply { addOutputState(state, DUMMY_NOTARY) addCommand(Command(), freshKey) 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 b57af97ed6..a567ce1464 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 @@ -58,10 +58,6 @@ class ScheduledFlowTests { } override val participants: List = listOf(source, destination) - - override fun isRelevant(ourKeys: Set): Boolean { - return participants.any { it.owningKey.containsAny(ourKeys) } - } } class InsertInitialStateFlow(val destination: Party) : FlowLogic() { @@ -109,6 +105,9 @@ class ScheduledFlowTests { advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type))) nodeA = mockNet.createNode(notaryNode.network.myAddress, start = false) nodeB = mockNet.createNode(notaryNode.network.myAddress, start = false) + + notaryNode.ensureRegistered() + mockNet.startNodes() } @@ -182,6 +181,7 @@ class ScheduledFlowTests { * @return states ordered by the transaction ID. */ private fun queryStatesWithPaging(vaultQueryService: VaultQueryService): List> { + // DOCSTART VaultQueryExamplePaging var pageNumber = DEFAULT_PAGE_NUM val states = mutableListOf>() do { @@ -190,6 +190,7 @@ class ScheduledFlowTests { states.addAll(results.states) pageNumber++ } while ((pageSpec.pageSize * (pageNumber)) <= results.totalStatesAvailable) + // DOCEND VaultQueryExamplePaging return states.toList() } } 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 098b7c41de..3d1942c01c 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 @@ -12,17 +12,18 @@ import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.api.MonitoringService import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate -import net.corda.node.services.network.InMemoryNetworkMapCache +import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.PersistentUniquenessProvider +import net.corda.node.testing.MockServiceHubInternal import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase import net.corda.testing.* -import net.corda.testing.node.MOCK_VERSION_INFO -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties -import net.corda.testing.node.makeTestIdentityService +import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties +import net.corda.testing.node.MockServices.Companion.makeTestIdentityService import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.After @@ -54,8 +55,7 @@ class ArtemisMessagingTests : TestDependencyInjectionBase() { var messagingClient: NodeMessagingClient? = null var messagingServer: ArtemisMessagingServer? = null - // TODO: We should have a dummy service hub rather than change behaviour in tests - val networkMapCache = InMemoryNetworkMapCache(serviceHub = null) + lateinit var networkMapCache: PersistentNetworkMapCache val rpcOps = object : RPCOps { override val protocolVersion: Int get() = throw UnsupportedOperationException() @@ -71,6 +71,7 @@ class ArtemisMessagingTests : TestDependencyInjectionBase() { LogHelper.setLevel(PersistentUniquenessProvider::class) database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) networkMapRegistrationFuture = doneFuture(Unit) + networkMapCache = PersistentNetworkMapCache(serviceHub = object : MockServiceHubInternal(database, config) {}) } @After 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 c817fca191..30c0de2843 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 @@ -6,6 +6,7 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.services.ServiceInfo import net.corda.core.serialization.deserialize import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.getX500Name import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.send @@ -46,7 +47,7 @@ abstract class AbstractNetworkMapServiceTest lateinit var alice: MockNode companion object { - val subscriberLegalName = X500Name("CN=Subscriber,OU=Corda QA Department,O=R3 CEV,L=New York,C=US") + val subscriberLegalName = getX500Name(O="Subscriber",L="New York",C="US") } @Before 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 c7809739fb..43e917b73d 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 @@ -1,13 +1,14 @@ package net.corda.node.services.network -import net.corda.core.crypto.CertificateAndKeyPair import net.corda.core.crypto.Crypto -import net.corda.core.crypto.cert import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.UnknownAnonymousPartyException +import net.corda.core.utilities.CertificateAndKeyPair +import net.corda.core.utilities.cert +import net.corda.core.utilities.getX500Name import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.utilities.CertificateType import net.corda.node.utilities.X509Utilities @@ -62,7 +63,7 @@ class InMemoryIdentityServiceTests { val service = InMemoryIdentityService(trustRoot = trustRoot.certificate) service.verifyAndRegisterIdentity(ALICE_IDENTITY) service.verifyAndRegisterIdentity(BOB_IDENTITY) - val alicente = getTestPartyAndCertificate(X500Name("O=Alicente Worldwide,L=London,C=GB"), generateKeyPair().public) + val alicente = getTestPartyAndCertificate(getX500Name(O = "Alicente Worldwide", L = "London", C = "GB"), generateKeyPair().public) service.verifyAndRegisterIdentity(alicente) assertEquals(setOf(ALICE, alicente.party), service.partiesFromName("Alice", false)) assertEquals(setOf(ALICE), service.partiesFromName("Alice Corp", true)) @@ -73,7 +74,7 @@ class InMemoryIdentityServiceTests { fun `get identity by name`() { val service = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate) val identities = listOf("Node A", "Node B", "Node C") - .map { getTestPartyAndCertificate(X500Name("CN=$it,O=R3,OU=corda,L=London,C=GB"), generateKeyPair().public) } + .map { getTestPartyAndCertificate(getX500Name(O = it, OU = "corda", L = "London", C = "GB"), generateKeyPair().public) } assertNull(service.partyFromX500Name(identities.first().name)) identities.forEach { service.verifyAndRegisterIdentity(it) } identities.forEach { assertEquals(it.party, service.partyFromX500Name(it.name)) } diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt similarity index 70% rename from node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt rename to node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt index 0b4cdb9a72..706cd10ae8 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt @@ -6,13 +6,14 @@ import net.corda.core.utilities.getOrThrow import net.corda.testing.ALICE import net.corda.testing.BOB 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.math.BigInteger import kotlin.test.assertEquals -class InMemoryNetworkMapCacheTest { +class NetworkMapCacheTest { lateinit var mockNet: MockNetwork @Before @@ -47,9 +48,7 @@ class InMemoryNetworkMapCacheTest { // Node A currently knows only about itself, so this returns node A assertEquals(nodeA.services.networkMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeA.info) - nodeA.database.transaction { - nodeA.services.networkMapCache.addNode(nodeB.info) - } + nodeA.services.networkMapCache.addNode(nodeB.info) // The details of node B write over those for node A assertEquals(nodeA.services.networkMapCache.getNodeByLegalIdentityKey(nodeA.info.legalIdentity.owningKey), nodeB.info) } @@ -63,9 +62,25 @@ class InMemoryNetworkMapCacheTest { val expected = n1.info mockNet.runNetwork() - val actual = node0Cache.getNodeByLegalIdentity(n1.info.legalIdentity) + val actual = n0.database.transaction { node0Cache.getNodeByLegalIdentity(n1.info.legalIdentity) } assertEquals(expected, actual) // TODO: Should have a test case with anonymous lookup } + + @Test + fun `remove node from cache`() { + val nodes = mockNet.createSomeNodes(1) + val n0 = nodes.mapNode + val n1 = nodes.partyNodes[0] + val node0Cache = n0.services.networkMapCache as PersistentNetworkMapCache + mockNet.runNetwork() + n0.database.transaction { + assertThat(node0Cache.getNodeByLegalIdentity(n1.info.legalIdentity) != null) + node0Cache.removeNode(n1.info) + assertThat(node0Cache.getNodeByLegalIdentity(n1.info.legalIdentity) == null) + assertThat(node0Cache.getNodeByLegalIdentity(n0.info.legalIdentity) != null) + assertThat(node0Cache.getNodeByLegalName(n1.info.legalIdentity.name) == null) + } + } } diff --git a/node/src/test/kotlin/net/corda/node/services/network/PersistentIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/network/PersistentIdentityServiceTests.kt new file mode 100644 index 0000000000..1d936a0135 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/network/PersistentIdentityServiceTests.kt @@ -0,0 +1,280 @@ +package net.corda.node.services.network + +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.generateKeyPair +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.node.services.UnknownAnonymousPartyException +import net.corda.core.utilities.CertificateAndKeyPair +import net.corda.core.utilities.cert +import net.corda.core.utilities.getX500Name +import net.corda.node.services.identity.PersistentIdentityService +import net.corda.node.utilities.CertificateType +import net.corda.node.utilities.CordaPersistence +import net.corda.node.utilities.X509Utilities +import net.corda.testing.* +import net.corda.testing.node.MockServices +import org.bouncycastle.asn1.x500.X500Name +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.security.cert.CertificateFactory +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNull + +/** + * Tests for the in memory identity service. + */ +class PersistentIdentityServiceTests { + + lateinit var database: CordaPersistence + lateinit var services: MockServices + lateinit var identityService: IdentityService + + @Before + fun setup() { + val databaseAndServices = MockServices.makeTestDatabaseAndMockServices(keys = emptyList(), createIdentityService = { PersistentIdentityService(trustRoot = DUMMY_CA.certificate) }) + database = databaseAndServices.first + services = databaseAndServices.second + identityService = services.identityService + } + + @After + fun shutdown() { + database.close() + } + + @Test + fun `get all identities`() { + // Nothing registered, so empty set + database.transaction { + assertNull(identityService.getAllIdentities().firstOrNull()) + } + + database.transaction { + identityService.verifyAndRegisterIdentity(ALICE_IDENTITY) + } + var expected = setOf(ALICE) + var actual = database.transaction { + identityService.getAllIdentities().map { it.party }.toHashSet() + } + assertEquals(expected, actual) + + // Add a second party and check we get both back + database.transaction { + identityService.verifyAndRegisterIdentity(BOB_IDENTITY) + } + expected = setOf(ALICE, BOB) + actual = database.transaction { + identityService.getAllIdentities().map { it.party }.toHashSet() + } + assertEquals(expected, actual) + } + + @Test + fun `get identity by key`() { + database.transaction { + assertNull(identityService.partyFromKey(ALICE_PUBKEY)) + identityService.verifyAndRegisterIdentity(ALICE_IDENTITY) + assertEquals(ALICE, identityService.partyFromKey(ALICE_PUBKEY)) + assertNull(identityService.partyFromKey(BOB_PUBKEY)) + } + } + + @Test + fun `get identity by name with no registered identities`() { + database.transaction { + assertNull(identityService.partyFromX500Name(ALICE.name)) + } + } + + @Test + fun `get identity by substring match`() { + database.transaction { + identityService.verifyAndRegisterIdentity(ALICE_IDENTITY) + identityService.verifyAndRegisterIdentity(BOB_IDENTITY) + } + val alicente = getTestPartyAndCertificate(getX500Name(O = "Alicente Worldwide", L = "London", C = "GB"), generateKeyPair().public) + database.transaction { + identityService.verifyAndRegisterIdentity(alicente) + assertEquals(setOf(ALICE, alicente.party), identityService.partiesFromName("Alice", false)) + assertEquals(setOf(ALICE), identityService.partiesFromName("Alice Corp", true)) + assertEquals(setOf(BOB), identityService.partiesFromName("Bob Plc", true)) + } + } + + @Test + fun `get identity by name`() { + val identities = listOf("Node A", "Node B", "Node C") + .map { getTestPartyAndCertificate(getX500Name(O = it, OU = "corda", L = "London", C = "GB"), generateKeyPair().public) } + database.transaction { + assertNull(identityService.partyFromX500Name(identities.first().name)) + } + identities.forEach { + database.transaction { + identityService.verifyAndRegisterIdentity(it) + } + } + identities.forEach { + database.transaction { + assertEquals(it.party, identityService.partyFromX500Name(it.name)) + } + } + } + + /** + * Generate a certificate path from a root CA, down to a transaction key, store and verify the association. + */ + @Test + fun `assert unknown anonymous key is unrecognised`() { + withTestSerialization { + val rootKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val rootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootKey) + val txKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_IDENTITY_SIGNATURE_SCHEME) + val identity = Party(rootCert) + val txIdentity = AnonymousParty(txKey.public) + + assertFailsWith { + database.transaction { + identityService.assertOwnership(identity, txIdentity) + } + } + } + } + + /** + * Generate a pair of certificate paths from a root CA, down to a transaction key, store and verify the associations. + * Also checks that incorrect associations are rejected. + */ + @Test + fun `get anonymous identity by key`() { + val trustRoot = DUMMY_CA + val (alice, aliceTxIdentity) = createParty(ALICE.name, trustRoot) + val (_, bobTxIdentity) = createParty(ALICE.name, trustRoot) + + // Now we have identities, construct the service and let it know about both + database.transaction { + identityService.verifyAndRegisterIdentity(alice) + identityService.verifyAndRegisterIdentity(aliceTxIdentity) + } + + var actual = database.transaction { + identityService.certificateFromKey(aliceTxIdentity.party.owningKey) + } + assertEquals(aliceTxIdentity, actual!!) + + database.transaction { + assertNull(identityService.certificateFromKey(bobTxIdentity.party.owningKey)) + } + database.transaction { + identityService.verifyAndRegisterIdentity(bobTxIdentity) + } + actual = database.transaction { + identityService.certificateFromKey(bobTxIdentity.party.owningKey) + } + assertEquals(bobTxIdentity, actual!!) + } + + /** + * Generate a pair of certificate paths from a root CA, down to a transaction key, store and verify the associations. + * Also checks that incorrect associations are rejected. + */ + @Test + fun `assert ownership`() { + withTestSerialization { + val trustRoot = DUMMY_CA + val (alice, anonymousAlice) = createParty(ALICE.name, trustRoot) + val (bob, anonymousBob) = createParty(BOB.name, trustRoot) + + database.transaction { + // Now we have identities, construct the service and let it know about both + identityService.verifyAndRegisterIdentity(anonymousAlice) + identityService.verifyAndRegisterIdentity(anonymousBob) + } + + // Verify that paths are verified + database.transaction { + identityService.assertOwnership(alice.party, anonymousAlice.party.anonymise()) + identityService.assertOwnership(bob.party, anonymousBob.party.anonymise()) + } + assertFailsWith { + database.transaction { + identityService.assertOwnership(alice.party, anonymousBob.party.anonymise()) + } + } + assertFailsWith { + database.transaction { + identityService.assertOwnership(bob.party, anonymousAlice.party.anonymise()) + } + } + + assertFailsWith { + val owningKey = Crypto.decodePublicKey(trustRoot.certificate.subjectPublicKeyInfo.encoded) + database.transaction { + identityService.assertOwnership(Party(trustRoot.certificate.subject, owningKey), anonymousAlice.party.anonymise()) + } + } + } + } + + @Test + fun `Test Persistence`() { + val trustRoot = DUMMY_CA + val (alice, anonymousAlice) = createParty(ALICE.name, trustRoot) + val (bob, anonymousBob) = createParty(BOB.name, trustRoot) + + database.transaction { + // Register well known identities + identityService.verifyAndRegisterIdentity(alice) + identityService.verifyAndRegisterIdentity(bob) + // Register an anonymous identities + identityService.verifyAndRegisterIdentity(anonymousAlice) + identityService.verifyAndRegisterIdentity(anonymousBob) + } + + // Create new identity service mounted onto same DB + val newPersistentIdentityService = database.transaction { + PersistentIdentityService(trustRoot = DUMMY_CA.certificate) + } + + database.transaction { + newPersistentIdentityService.assertOwnership(alice.party, anonymousAlice.party.anonymise()) + newPersistentIdentityService.assertOwnership(bob.party, anonymousBob.party.anonymise()) + } + + val aliceParent = database.transaction { + newPersistentIdentityService.partyFromAnonymous(anonymousAlice.party.anonymise()) + } + assertEquals(alice.party, aliceParent!!) + + val bobReload = database.transaction { + newPersistentIdentityService.certificateFromKey(anonymousBob.party.owningKey) + } + assertEquals(anonymousBob, bobReload!!) + } + + 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, PartyAndCertificate(txCertPath)) + } + + /** + * Ensure if we feed in a full identity, we get the same identity back. + */ + @Test + fun `deanonymising a well known identity`() { + val expected = ALICE + val actual = database.transaction { + identityService.partyFromAnonymous(expected) + } + assertEquals(expected, actual) + } +} diff --git a/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt new file mode 100644 index 0000000000..06d4f37a54 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt @@ -0,0 +1,194 @@ +package net.corda.node.services.network + +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.identity.Party +import net.corda.core.node.NodeInfo +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.seconds +import net.corda.core.utilities.toBase58String +import net.corda.core.utilities.unwrap +import net.corda.node.internal.Node +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.CHARLIE +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.node.NodeBasedTest +import org.assertj.core.api.Assertions.assertThat +import org.bouncycastle.asn1.x500.X500Name +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFails + +class PersistentNetworkMapCacheTest : NodeBasedTest() { + val partiesList = listOf(DUMMY_NOTARY, ALICE, BOB) + val addressesMap: HashMap = HashMap() + val infos: MutableSet = HashSet() + + @Before + fun start() { + val nodes = startNodesWithPort(partiesList) + nodes.forEach { it.nodeReadyFuture.get() } // Need to wait for network map registration, as these tests are ran without waiting. + nodes.forEach { + infos.add(it.info) + addressesMap[it.info.legalIdentity.name] = it.info.addresses[0] + it.stop() // We want them to communicate with NetworkMapService to save data to cache. + } + } + + @Test + fun `get nodes by owning key and by name, no network map service`() { + val alice = startNodesWithPort(listOf(ALICE), noNetworkMap = true)[0] + val netCache = alice.services.networkMapCache as PersistentNetworkMapCache + alice.database.transaction { + val res = netCache.getNodeByLegalIdentity(alice.info.legalIdentity) + assertEquals(alice.info, res) + val res2 = netCache.getNodeByLegalName(DUMMY_NOTARY.name) + assertEquals(infos.filter { it.legalIdentity.name == DUMMY_NOTARY.name }.singleOrNull(), res2) + } + } + + @Test + fun `get nodes by address no network map service`() { + val alice = startNodesWithPort(listOf(ALICE), noNetworkMap = true)[0] + val netCache = alice.services.networkMapCache as PersistentNetworkMapCache + alice.database.transaction { + val res = netCache.getNodeByAddress(alice.info.addresses[0]) + assertEquals(alice.info, res) + } + } + + @Test + fun `restart node with DB map cache and no network map`() { + val alice = startNodesWithPort(listOf(ALICE), noNetworkMap = true)[0] + val partyNodes = alice.services.networkMapCache.partyNodes + assert(NetworkMapService.type !in alice.info.advertisedServices.map { it.info.type }) + assertEquals(null, alice.inNodeNetworkMapService) + assertEquals(infos.size, partyNodes.size) + assertEquals(infos.map { it.legalIdentity }.toSet(), partyNodes.map { it.legalIdentity }.toSet()) + } + + @Test + fun `start 2 nodes without pointing at NetworkMapService and communicate with each other`() { + val parties = partiesList.subList(1, partiesList.size) + val nodes = startNodesWithPort(parties, noNetworkMap = true) + assert(nodes.all { it.inNodeNetworkMapService == null }) + assert(nodes.all { NetworkMapService.type !in it.info.advertisedServices.map { it.info.type } }) + nodes.forEach { + val partyNodes = it.services.networkMapCache.partyNodes + assertEquals(infos.size, partyNodes.size) + assertEquals(infos.map { it.legalIdentity }.toSet(), partyNodes.map { it.legalIdentity }.toSet()) + } + checkConnectivity(nodes) + } + + @Test + fun `start 2 nodes pointing at NetworkMapService but don't start network map node`() { + val parties = partiesList.subList(1, partiesList.size) + val nodes = startNodesWithPort(parties, noNetworkMap = false) + assert(nodes.all { it.inNodeNetworkMapService == null }) + assert(nodes.all { NetworkMapService.type !in it.info.advertisedServices.map { it.info.type } }) + nodes.forEach { + val partyNodes = it.services.networkMapCache.partyNodes + assertEquals(infos.size, partyNodes.size) + assertEquals(infos.map { it.legalIdentity }.toSet(), partyNodes.map { it.legalIdentity }.toSet()) + } + checkConnectivity(nodes) + } + + @Test + fun `start node and network map communicate`() { + val parties = partiesList.subList(0, 2) + val nodes = startNodesWithPort(parties, noNetworkMap = false) + checkConnectivity(nodes) + } + + @Test + fun `start node without networkMapService and no database - fail`() { + assertFails { startNode(CHARLIE.name, noNetworkMap = true).getOrThrow(2.seconds) } + } + + @Test + fun `new node joins network without network map started`() { + val parties = partiesList.subList(1, partiesList.size) + // Start 2 nodes pointing at network map, but don't start network map service. + val otherNodes = startNodesWithPort(parties, noNetworkMap = false) + otherNodes.forEach { node -> + assert(infos.any { it.legalIdentity == node.info.legalIdentity }) + } + // Start node that is not in databases of other nodes. Point to NMS. Which has't started yet. + val charlie = startNodesWithPort(listOf(CHARLIE), noNetworkMap = false)[0] + otherNodes.forEach { + assert(charlie.info.legalIdentity !in it.services.networkMapCache.partyNodes.map { it.legalIdentity }) + } + // Start Network Map and see that charlie node appears in caches. + val nms = startNodesWithPort(listOf(DUMMY_NOTARY), noNetworkMap = false)[0] + nms.startupComplete.get() + assert(nms.inNodeNetworkMapService != null) + assert(infos.any {it.legalIdentity == nms.info.legalIdentity}) + otherNodes.forEach { + assert(nms.info.legalIdentity in it.services.networkMapCache.partyNodes.map { it.legalIdentity }) + } + charlie.nodeReadyFuture.get() // Finish registration. + checkConnectivity(listOf(otherNodes[0], nms)) // Checks connectivity from A to NMS. + val cacheA = otherNodes[0].services.networkMapCache.partyNodes + val cacheB = otherNodes[1].services.networkMapCache.partyNodes + val cacheC = charlie.services.networkMapCache.partyNodes + assertEquals(4, cacheC.size) // Charlie fetched data from NetworkMap + assert(charlie.info.legalIdentity in cacheB.map { it.legalIdentity }) // Other nodes also fetched data from Network Map with node C. + assertEquals(cacheA.toSet(), cacheB.toSet()) + assertEquals(cacheA.toSet(), cacheC.toSet()) + } + + // HELPERS + // Helper function to restart nodes with the same host and port. + private fun startNodesWithPort(nodesToStart: List, noNetworkMap: Boolean = false): List { + return nodesToStart.map { party -> + val configOverrides = addressesMap[party.name]?.let { mapOf("p2pAddress" to it.toString()) } ?: emptyMap() + if (party == DUMMY_NOTARY) { + startNetworkMapNode(party.name, configOverrides = configOverrides) + } + else { + startNode(party.name, + configOverrides = configOverrides, + noNetworkMap = noNetworkMap, + waitForConnection = false).getOrThrow() + } + } + } + + // Check that nodes are functional, communicate each with each. + private fun checkConnectivity(nodes: List) { + nodes.forEach { node1 -> + nodes.forEach { node2 -> + node2.registerInitiatedFlow(SendBackFlow::class.java) + val resultFuture = node1.services.startFlow(SendFlow(node2.info.legalIdentity)).resultFuture + assertThat(resultFuture.getOrThrow()).isEqualTo("Hello!") + } + } + } + + @InitiatingFlow + private class SendFlow(val otherParty: Party) : FlowLogic() { + @Suspendable + override fun call(): String { + println("SEND FLOW to $otherParty") + println("Party key ${otherParty.owningKey.toBase58String()}") + return sendAndReceive(otherParty, "Hi!").unwrap { it } + } + } + + @InitiatedBy(SendFlow::class) + private class SendBackFlow(val otherParty: Party) : FlowLogic() { + @Suspendable + override fun call() { + println("SEND BACK FLOW to $otherParty") + println("Party key ${otherParty.owningKey.toBase58String()}") + send(otherParty, "Hello!") + } + } +} diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt index 88b0012435..aaa8e49db1 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt @@ -9,9 +9,9 @@ import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase import net.corda.testing.LogHelper import net.corda.testing.TestDependencyInjectionBase -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties -import net.corda.testing.node.makeTestIdentityService +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties +import net.corda.testing.node.MockServices.Companion.makeTestIdentityService import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index 063dbff380..6fbbf5e534 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -13,7 +13,6 @@ import net.corda.core.transactions.WireTransaction import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.schemas.SampleCashSchemaV2 import net.corda.finance.schemas.SampleCashSchemaV3 -import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.PersistentUniquenessProvider @@ -23,9 +22,9 @@ import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase import net.corda.testing.* import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties -import net.corda.testing.node.makeTestIdentityService +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties +import net.corda.testing.node.MockServices.Companion.makeTestIdentityService import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before @@ -65,7 +64,7 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() { 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 }) + (vaultService as NodeVaultService).notifyAll(txs.map { it.tx }) } } } diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt similarity index 98% rename from node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt rename to node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index 6cbad996e3..a11454dc13 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -1,11 +1,11 @@ -package net.corda.node.services.database +package net.corda.node.services.persistence import net.corda.core.contracts.ContractState 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.crypto.toBase58String +import net.corda.core.utilities.toBase58String import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.schemas.CommonSchemaV1 @@ -25,6 +25,7 @@ import net.corda.finance.schemas.SampleCashSchemaV3 import net.corda.finance.utils.sumCash 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.VaultSchemaV1 import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -34,9 +35,9 @@ import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.contracts.fillWithSomeTestDeals import net.corda.testing.contracts.fillWithSomeTestLinearStates import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties -import net.corda.testing.node.makeTestIdentityService +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties +import net.corda.testing.node.MockServices.Companion.makeTestIdentityService import net.corda.testing.schemas.DummyLinearStateSchemaV1 import net.corda.testing.schemas.DummyLinearStateSchemaV2 import org.assertj.core.api.Assertions @@ -88,7 +89,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { 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 }) + (vaultService as NodeVaultService).notifyAll(txs.map { it.tx }) } override fun jdbcSession() = database.createSession() } @@ -870,7 +871,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { val nativeQuery = "SELECT v.transaction_id, v.output_index FROM vault_states v WHERE v.state_status = 0" database.transaction { - val jdbcSession = database.createSession() + val jdbcSession = services.jdbcSession() val prepStatement = jdbcSession.prepareStatement(nativeQuery) val rs = prepStatement.executeQuery() // DOCEND JdbcSession diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt index f1c55ca1a7..f979021fce 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt @@ -14,9 +14,9 @@ import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.configureDatabase import net.corda.testing.LogHelper -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties -import net.corda.testing.node.makeTestIdentityService +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties +import net.corda.testing.node.MockServices.Companion.makeTestIdentityService import org.junit.After import org.junit.Before import org.junit.Test diff --git a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt index 66c3d5dad0..bbade152b5 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt @@ -12,9 +12,9 @@ import net.corda.node.services.api.SchemaService import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.configureDatabase import net.corda.testing.MEGA_CORP -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties -import net.corda.testing.node.makeTestIdentityService +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties +import net.corda.testing.node.MockServices.Companion.makeTestIdentityService import org.hibernate.annotations.Cascade import org.hibernate.annotations.CascadeType import org.junit.After diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 64d21ded83..7da73c2258 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -21,11 +21,8 @@ import net.corda.core.serialization.serialize import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.* import net.corda.core.utilities.ProgressTracker.Change -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.unwrap import net.corda.finance.DOLLARS import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow @@ -73,9 +70,13 @@ class FlowFrameworkTests { fun start() { node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) node2 = mockNet.createNode(networkMapAddress = node1.network.myAddress) + + mockNet.runNetwork() + node1.ensureRegistered() + // We intentionally create our own notary and ignore the one provided by the network val notaryKeyPair = generateKeyPair() - val notaryService = ServiceInfo(ValidatingNotaryService.type, getTestX509Name("notary-service-2000")) + val notaryService = ServiceInfo(ValidatingNotaryService.type, getX500Name(O = "notary-service-2000", L = "London", C = "GB")) val overrideServices = mapOf(Pair(notaryService, notaryKeyPair)) // Note that these notaries don't operate correctly as they don't share their state. They are only used for testing // service addressing. @@ -88,8 +89,10 @@ class FlowFrameworkTests { // We don't create a network map, so manually handle registrations val nodes = listOf(node1, node2, notary1, notary2) nodes.forEach { node -> - nodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> - node.services.identityService.verifyAndRegisterIdentity(identity) + node.database.transaction { + nodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> + node.services.identityService.verifyAndRegisterIdentity(identity) + } } } } @@ -166,6 +169,7 @@ class FlowFrameworkTests { node3.services.startFlow(flow) assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet node3.disableDBCloseOnStop() + node3.services.networkMapCache.clearNetworkMapCache() // zap persisted NetworkMapCache to force use of network. node3.stop() node3 = mockNet.createNode(node1.network.myAddress, node3.id) @@ -175,6 +179,7 @@ class FlowFrameworkTests { node3.smm.executor.flush() assertEquals(true, restoredFlow.flowStarted) // Now we should have run the flow and hopefully cleared the init checkpoint node3.disableDBCloseOnStop() + node3.services.networkMapCache.clearNetworkMapCache() // zap persisted NetworkMapCache to force use of network. node3.stop() // Now it is completed the flow should leave no Checkpoint. @@ -220,6 +225,7 @@ class FlowFrameworkTests { node2.stop() node2.database.transaction { assertEquals(1, node2.checkpointStorage.checkpoints().size) // confirm checkpoint + node2.services.networkMapCache.clearNetworkMapCache() } val node2b = mockNet.createNode(node1.network.myAddress, node2.id, advertisedServices = *node2.advertisedServices.toTypedArray()) node2.manuallyCloseDB() @@ -912,6 +918,7 @@ class FlowFrameworkTests { override val progressTracker: ProgressTracker = ProgressTracker(START_STEP) lateinit var exceptionThrown: E + @Suspendable override fun call(): Nothing { progressTracker.currentStep = START_STEP exceptionThrown = exception() 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 f06897bae5..bd4c3358f3 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 @@ -13,9 +13,9 @@ import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.DatabaseTransaction import net.corda.node.utilities.configureDatabase import net.corda.testing.* -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties -import net.corda.testing.node.makeTestIdentityService +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties +import net.corda.testing.node.MockServices.Companion.makeTestIdentityService import org.junit.After import org.junit.Before import org.junit.Test 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 4eb85f9f01..45a8bb66dd 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 @@ -40,6 +40,7 @@ class NotaryServiceTests { advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) clientNode = mockNet.createNode(notaryNode.network.myAddress) mockNet.runNetwork() // Clear network map registration messages + notaryNode.ensureRegistered() } @After 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 289bc695ac..1c1124cb1f 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 @@ -5,9 +5,9 @@ import net.corda.core.node.services.UniquenessException import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase import net.corda.testing.* -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties -import net.corda.testing.node.makeTestIdentityService +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties +import net.corda.testing.node.MockServices.Companion.makeTestIdentityService import org.junit.After import org.junit.Before import org.junit.Test 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 dadecbc5f7..f65a9f2ef9 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 @@ -42,6 +42,7 @@ class ValidatingNotaryServiceTests { ) clientNode = mockNet.createNode(notaryNode.network.myAddress) mockNet.runNetwork() // Clear network map registration messages + notaryNode.ensureRegistered() } @After 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 3d481851ae..cdf6d06fc5 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 @@ -28,7 +28,7 @@ import net.corda.node.utilities.CordaPersistence import net.corda.testing.* import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestDatabaseAndMockServices +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.After @@ -96,7 +96,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { val originalVault = vaultSvc val originalVaultQuery = vaultQuery val services2 = object : MockServices() { - override val vaultService: VaultService get() = originalVault + override val vaultService: NodeVaultService get() = originalVault as NodeVaultService override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { for (stx in txs) { validatedTransactions.addTransaction(stx) @@ -476,15 +476,18 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { 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 myKeys = services.keyManagementService.filterMyKeys(listOf(wellKnownCash.owner.owningKey)) + assertTrue { service.isRelevant(wellKnownCash, myKeys.toSet()) } val anonymousIdentity = services.keyManagementService.freshKeyAndCert(services.myInfo.legalIdentityAndCert, false) val anonymousCash = Cash.State(amount, anonymousIdentity.party) - assertTrue { service.isRelevant(anonymousCash, services.keyManagementService.keys) } + val anonymousKeys = services.keyManagementService.filterMyKeys(listOf(anonymousCash.owner.owningKey)) + assertTrue { service.isRelevant(anonymousCash, anonymousKeys.toSet()) } val thirdPartyIdentity = AnonymousParty(generateKeyPair().public) val thirdPartyCash = Cash.State(amount, thirdPartyIdentity) - assertFalse { service.isRelevant(thirdPartyCash, services.keyManagementService.keys) } + val thirdPartyKeys = services.keyManagementService.filterMyKeys(listOf(thirdPartyCash.owner.owningKey)) + assertFalse { service.isRelevant(thirdPartyCash, thirdPartyKeys.toSet()) } } // TODO: Unit test linear state relevancy checks 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 707361794c..16780717e0 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,7 +3,6 @@ package net.corda.node.services.vault import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair -import net.corda.core.crypto.toBase58String import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.* @@ -26,14 +25,13 @@ import net.corda.node.utilities.configureDatabase import net.corda.testing.* import net.corda.testing.contracts.* import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestDatabaseAndMockServices -import net.corda.testing.node.makeTestDatabaseProperties -import net.corda.testing.node.makeTestIdentityService +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties +import net.corda.testing.node.MockServices.Companion.makeTestIdentityService 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.junit.* import org.junit.rules.ExpectedException import java.lang.Thread.sleep @@ -56,7 +54,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { // test cash notary val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(21)) } - val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public) + val CASH_NOTARY: Party get() = Party(getX500Name(O = "Cash Notary Service", OU = "corda", L = "Zurich", C = "CH"), CASH_NOTARY_KEY.public) val CASH_NOTARY_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CASH_NOTARY.nameOrNull(), CASH_NOTARY_KEY.public) @Before @@ -279,7 +277,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { 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))) + Sort.SortColumn(sortAttributeIndex, Sort.Direction.ASC))) val criteria = VaultQueryCriteria() val results = vaultQuerySvc.queryBy(criteria, sortBy) @@ -296,11 +294,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed states for state refs`() { val stateRefs = - database.transaction { - services.fillWithSomeTestLinearStates(8) - val issuedStates = services.fillWithSomeTestLinearStates(2) - issuedStates.states.map { it.ref }.toList() - } + database.transaction { + services.fillWithSomeTestLinearStates(8) + val issuedStates = services.fillWithSomeTestLinearStates(2) + issuedStates.states.map { it.ref }.toList() + } database.transaction { // DOCSTART VaultQueryExample2 val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID) @@ -473,15 +471,15 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed states with soft locking`() { val (lockId1, lockId2) = - database.transaction { - val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, notaryServices, CASH_NOTARY, 10, 10, Random(0L)).states.toList() - vaultSvc.softLockReserve(UUID.randomUUID(), NonEmptySet.of(issuedStates[1].ref, issuedStates[2].ref, issuedStates[3].ref)) - val lockId1 = UUID.randomUUID() - vaultSvc.softLockReserve(lockId1, NonEmptySet.of(issuedStates[4].ref, issuedStates[5].ref)) - val lockId2 = UUID.randomUUID() - vaultSvc.softLockReserve(lockId2, NonEmptySet.of(issuedStates[6].ref)) - Pair(lockId1, lockId2) - } + database.transaction { + val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, notaryServices, CASH_NOTARY, 10, 10, Random(0L)).states.toList() + vaultSvc.softLockReserve(UUID.randomUUID(), NonEmptySet.of(issuedStates[1].ref, issuedStates[2].ref, issuedStates[3].ref)) + val lockId1 = UUID.randomUUID() + vaultSvc.softLockReserve(lockId1, NonEmptySet.of(issuedStates[4].ref, issuedStates[5].ref)) + val lockId2 = UUID.randomUUID() + vaultSvc.softLockReserve(lockId2, NonEmptySet.of(issuedStates[6].ref)) + Pair(lockId1, lockId2) + } database.transaction { // excluding soft locked states val criteriaExclusive = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY)) @@ -740,10 +738,10 @@ class VaultQueryTests : TestDependencyInjectionBase() { val avgCriteria = VaultCustomQueryCriteria(avg) val results = vaultQuerySvc.queryBy>(sumCriteria - .and(countCriteria) - .and(maxCriteria) - .and(minCriteria) - .and(avgCriteria)) + .and(countCriteria) + .and(maxCriteria) + .and(minCriteria) + .and(avgCriteria)) // DOCEND VaultQueryExample21 assertThat(results.otherResults).hasSize(5) @@ -779,9 +777,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { val avgCriteria = VaultCustomQueryCriteria(avg) val results = vaultQuerySvc.queryBy>(sumCriteria - .and(maxCriteria) - .and(minCriteria) - .and(avgCriteria)) + .and(maxCriteria) + .and(minCriteria) + .and(avgCriteria)) // DOCEND VaultQueryExample22 assertThat(results.otherResults).hasSize(24) @@ -827,9 +825,10 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { // DOCSTART VaultQueryExample23 - val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::issuerParty, - CashSchemaV1.PersistentCashState::currency), - orderBy = Sort.Direction.DESC) + 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)) @@ -881,16 +880,16 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `aggregate functions count by contract type and state status`() { - val (linearStatesJKL,linearStatesXYZ,dealStates) = - database.transaction { - // create new states - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 10, 10, Random(0L)) - val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ") - val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL") - services.fillWithSomeTestLinearStates(3, "ABC") - val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")) - Triple(linearStatesJKL,linearStatesXYZ,dealStates) - } + val (linearStatesJKL, linearStatesXYZ, dealStates) = + database.transaction { + // create new states + services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 10, 10, Random(0L)) + val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ") + val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL") + services.fillWithSomeTestLinearStates(3, "ABC") + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")) + Triple(linearStatesJKL, linearStatesXYZ, dealStates) + } val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } database.transaction { // count fungible assets @@ -907,15 +906,15 @@ class VaultQueryTests : TestDependencyInjectionBase() { assertThat(dealStateCount).isEqualTo(3L) } val cashUpdates = - database.transaction { - // consume some states - services.consumeLinearStates(linearStatesXYZ.states.toList(), DUMMY_NOTARY) - services.consumeLinearStates(linearStatesJKL.states.toList(), DUMMY_NOTARY) - services.consumeDeals(dealStates.states.filter { it.state.data.linearId.externalId == "456" }, DUMMY_NOTARY) - services.consumeCash(50.DOLLARS, notary = DUMMY_NOTARY) + database.transaction { + // consume some states + services.consumeLinearStates(linearStatesXYZ.states.toList(), DUMMY_NOTARY) + services.consumeLinearStates(linearStatesJKL.states.toList(), DUMMY_NOTARY) + services.consumeDeals(dealStates.states.filter { it.state.data.linearId.externalId == "456" }, DUMMY_NOTARY) + services.consumeCash(50.DOLLARS, notary = DUMMY_NOTARY) - // UNCONSUMED states (default) - } + // UNCONSUMED states (default) + } database.transaction { // count fungible assets val countCriteriaUnconsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.UNCONSUMED) @@ -1060,7 +1059,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { } database.transaction { @Suppress("EXPECTED_CONDITION") - val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE + 1) // overflow = -2147483648 + val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, @Suppress("INTEGER_OVERFLOW")MAX_PAGE_SIZE + 1) // overflow = -2147483648 val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) vaultQuerySvc.queryBy(criteria, paging = pagingSpec) } @@ -1229,9 +1228,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed linear heads for linearId without external Id`() { val issuedStates = - database.transaction { - services.fillWithSomeTestLinearStates(10) - } + database.transaction { + services.fillWithSomeTestLinearStates(10) + } database.transaction { // DOCSTART VaultQueryExample8 val linearIds = issuedStates.states.map { it.state.data.linearId }.toList() @@ -1245,12 +1244,12 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed linear heads by linearId`() { val (linearState1, linearState3) = - database.transaction { - val linearState1 = services.fillWithSomeTestLinearStates(1, "ID1") - services.fillWithSomeTestLinearStates(1, "ID2") - val linearState3 = services.fillWithSomeTestLinearStates(1, "ID3") - Pair(linearState1, linearState3) - } + database.transaction { + val linearState1 = services.fillWithSomeTestLinearStates(1, "ID1") + services.fillWithSomeTestLinearStates(1, "ID2") + val linearState3 = services.fillWithSomeTestLinearStates(1, "ID3") + Pair(linearState1, linearState3) + } database.transaction { val linearIds = listOf(linearState1.states.first().state.data.linearId, linearState3.states.first().state.data.linearId) val criteria = LinearStateQueryCriteria(linearId = linearIds) @@ -1279,14 +1278,14 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `all linear states for a given linear id`() { val linearId = - database.transaction { - val txns = services.fillWithSomeTestLinearStates(1, "TEST") - val linearState = txns.states.first() - services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference - services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference - services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference - linearState.state.data.linearId - } + database.transaction { + val txns = services.fillWithSomeTestLinearStates(1, "TEST") + val linearState = txns.states.first() + services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference + services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference + services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference + linearState.state.data.linearId + } database.transaction { // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" // DOCSTART VaultQueryExample9 @@ -1301,14 +1300,14 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `all linear states for a given id sorted by uuid`() { val linearStates = - database.transaction { - val txns = services.fillWithSomeTestLinearStates(2, "TEST") - val linearStates = txns.states.toList() - services.evolveLinearStates(linearStates, DUMMY_NOTARY) // consume current and produce new state reference - services.evolveLinearStates(linearStates, DUMMY_NOTARY) // consume current and produce new state reference - services.evolveLinearStates(linearStates, DUMMY_NOTARY) // consume current and produce new state reference - linearStates - } + database.transaction { + val txns = services.fillWithSomeTestLinearStates(2, "TEST") + val linearStates = txns.states.toList() + services.evolveLinearStates(linearStates, DUMMY_NOTARY) // consume current and produce new state reference + services.evolveLinearStates(linearStates, DUMMY_NOTARY) // consume current and produce new state reference + services.evolveLinearStates(linearStates, DUMMY_NOTARY) // consume current and produce new state reference + linearStates + } database.transaction { // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" val linearStateCriteria = LinearStateQueryCriteria(uuid = linearStates.map { it.state.data.linearId.id }, status = Vault.StateStatus.ALL) @@ -1341,11 +1340,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed deal states sorted`() { val uid = - database.transaction { - val linearStates = services.fillWithSomeTestLinearStates(10) - services.fillWithSomeTestDeals(listOf("123", "456", "789")) - linearStates.states.first().state.data.linearId.id - } + database.transaction { + val linearStates = services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + linearStates.states.first().state.data.linearId.id + } database.transaction { val linearStateCriteria = LinearStateQueryCriteria(uuid = listOf(uid)) val dealStateCriteria = LinearStateQueryCriteria(externalId = listOf("123", "456", "789")) @@ -1379,14 +1378,14 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `return consumed linear states for a given linear id`() { val txns = - database.transaction { - val txns = services.fillWithSomeTestLinearStates(1, "TEST") - val linearState = txns.states.first() - val linearState2 = services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference - val linearState3 = services.evolveLinearState(linearState2, DUMMY_NOTARY) // consume current and produce new state reference - services.evolveLinearState(linearState3, DUMMY_NOTARY) // consume current and produce new state reference - txns - } + database.transaction { + val txns = services.fillWithSomeTestLinearStates(1, "TEST") + val linearState = txns.states.first() + val linearState2 = services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference + val linearState3 = services.evolveLinearState(linearState2, DUMMY_NOTARY) // consume current and produce new state reference + services.evolveLinearState(linearState3, DUMMY_NOTARY) // consume current and produce new state reference + txns + } database.transaction { // 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 }, status = Vault.StateStatus.CONSUMED) @@ -1485,15 +1484,15 @@ class VaultQueryTests : TestDependencyInjectionBase() { fun `unconsumed fungible assets for selected issuer parties`() { // GBP issuer val gbpCashIssuerKey = entropyToKeyPair(BigInteger.valueOf(1001)) - val gbpCashIssuer = Party(X500Name("CN=British Pounds Cash Issuer,O=R3,OU=corda,L=London,C=GB"), gbpCashIssuerKey.public).ref(1) + val gbpCashIssuer = Party(getX500Name(O = "British Pounds Cash Issuer", OU = "corda", L = "London", C = "GB"), gbpCashIssuerKey.public).ref(1) val gbpCashIssuerServices = MockServices(gbpCashIssuerKey) // USD issuer val usdCashIssuerKey = entropyToKeyPair(BigInteger.valueOf(1002)) - val usdCashIssuer = Party(X500Name("CN=US Dollars Cash Issuer,O=R3,OU=corda,L=New York,C=US"), usdCashIssuerKey.public).ref(1) + val usdCashIssuer = Party(getX500Name(O = "US Dollars Cash Issuer", OU = "corda", L = "New York", C = "US"), usdCashIssuerKey.public).ref(1) val usdCashIssuerServices = MockServices(usdCashIssuerKey) // CHF issuer val chfCashIssuerKey = entropyToKeyPair(BigInteger.valueOf(1003)) - val chfCashIssuer = Party(X500Name("CN=Swiss Francs Cash Issuer,O=R3,OU=corda,L=Zurich,C=CH"), chfCashIssuerKey.public).ref(1) + val chfCashIssuer = Party(getX500Name(O = "Swiss Francs Cash Issuer", OU = "corda", L = "Zurich", C = "CH"), chfCashIssuerKey.public).ref(1) val chfCashIssuerServices = MockServices(chfCashIssuerKey) database.transaction { @@ -1872,14 +1871,14 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed linear heads for a given external id or uuid`() { val uuid = - database.transaction { - services.fillWithSomeTestLinearStates(1, "TEST1") - val aState = services.fillWithSomeTestLinearStates(1, "TEST2").states - services.consumeLinearStates(aState.toList(), DUMMY_NOTARY) - services.fillWithSomeTestLinearStates(1, "TEST1").states.first().state.data.linearId.id + database.transaction { + services.fillWithSomeTestLinearStates(1, "TEST1") + val aState = services.fillWithSomeTestLinearStates(1, "TEST2").states + services.consumeLinearStates(aState.toList(), DUMMY_NOTARY) + services.fillWithSomeTestLinearStates(1, "TEST1").states.first().state.data.linearId.id - // 2 unconsumed states with same external ID, 1 consumed with different external ID - } + // 2 unconsumed states with same external ID, 1 consumed with different external ID + } database.transaction { val results = builder { val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.equal("TEST1") @@ -1920,12 +1919,12 @@ class VaultQueryTests : TestDependencyInjectionBase() { identitySvc.verifyAndRegisterIdentity(BOB_IDENTITY) identitySvc.verifyAndRegisterIdentity(CHARLIE_IDENTITY) - services.fillWithSomeTestLinearStates(1, "TEST1", listOf(ALICE,BOB,CHARLIE)) + services.fillWithSomeTestLinearStates(1, "TEST1", listOf(ALICE, BOB, CHARLIE)) services.fillWithSomeTestLinearStates(1) services.fillWithSomeTestLinearStates(1, "TEST3") } database.transaction { - val linearStateCriteria = LinearStateQueryCriteria(participants = listOf(ALICE,BOB,CHARLIE)) + val linearStateCriteria = LinearStateQueryCriteria(participants = listOf(ALICE, BOB, CHARLIE)) val results = vaultQuerySvc.queryBy(linearStateCriteria) assertThat(results.states).hasSize(1) @@ -1985,12 +1984,12 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { // Base criteria val baseCriteria = VaultQueryCriteria(notary = listOf(DUMMY_NOTARY), - status = Vault.StateStatus.CONSUMED) + status = Vault.StateStatus.CONSUMED) // Enrich and override QueryCriteria with additional default attributes (such as soft locks) - val enrichedCriteria = VaultQueryCriteria(contractStateTypes = setOf(DealState::class.java), // enrich - softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(UUID.randomUUID())), - status = Vault.StateStatus.UNCONSUMED) // override + val enrichedCriteria = VaultQueryCriteria(contractStateTypes = setOf(DealState::class.java), // enrich + softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(UUID.randomUUID())), + status = Vault.StateStatus.UNCONSUMED) // override // Sorting val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))) @@ -2008,28 +2007,28 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun trackCashStates_unconsumed() { val updates = - database.transaction { - // DOCSTART VaultQueryExample15 - vaultQuerySvc.trackBy().updates // UNCONSUMED default - // DOCEND VaultQueryExample15 - } - val (linearStates,dealStates) = - database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 5, 5, Random(0L)) - val linearStates = services.fillWithSomeTestLinearStates(10).states - val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states - // add more cash - services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - // add another deal - services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) - Pair(linearStates,dealStates) - } + database.transaction { + // DOCSTART VaultQueryExample15 + vaultQuerySvc.trackBy().updates // UNCONSUMED default + // DOCEND VaultQueryExample15 + } + val (linearStates, dealStates) = + database.transaction { + services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 5, 5, Random(0L)) + val linearStates = services.fillWithSomeTestLinearStates(10).states + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states + // add more cash + services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) + // add another deal + services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) + Pair(linearStates, dealStates) + } database.transaction { // consume stuff services.consumeCash(100.DOLLARS, notary = DUMMY_NOTARY) services.consumeDeals(dealStates.toList(), DUMMY_NOTARY) services.consumeLinearStates(linearStates.toList(), DUMMY_NOTARY) - } + } updates.expectEvents { sequence( @@ -2050,22 +2049,22 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun trackCashStates_consumed() { val updates = - database.transaction { - val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - vaultQuerySvc.trackBy(criteria).updates - } - val (linearStates,dealStates) = - database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 5, 5, Random(0L)) - val linearStates = services.fillWithSomeTestLinearStates(10).states - val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states + database.transaction { + val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) + vaultQuerySvc.trackBy(criteria).updates + } + val (linearStates, dealStates) = + database.transaction { + services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 5, 5, Random(0L)) + val linearStates = services.fillWithSomeTestLinearStates(10).states + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states - // add more cash - services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - // add another deal - services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) - Pair(linearStates,dealStates) - } + // add more cash + services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) + // add another deal + services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) + Pair(linearStates, dealStates) + } database.transaction { // consume stuff services.consumeCash(100.POUNDS, notary = DUMMY_NOTARY) @@ -2096,21 +2095,21 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun trackCashStates_all() { val updates = - database.transaction { - val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - vaultQuerySvc.trackBy(criteria).updates - } - val (linearStates,dealStates) = - database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 5, 5, Random(0L)) - val linearStates = services.fillWithSomeTestLinearStates(10).states - val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states - // add more cash - services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - // add another deal - services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) - Pair(linearStates,dealStates) - } + database.transaction { + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + vaultQuerySvc.trackBy(criteria).updates + } + val (linearStates, dealStates) = + database.transaction { + services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 5, 5, Random(0L)) + val linearStates = services.fillWithSomeTestLinearStates(10).states + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states + // add more cash + services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) + // add another deal + services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) + Pair(linearStates, dealStates) + } database.transaction { // consume stuff services.consumeCash(99.POUNDS, notary = DUMMY_NOTARY) @@ -2151,24 +2150,24 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun trackLinearStates() { val updates = - database.transaction { - // DOCSTART VaultQueryExample16 - val (snapshot, updates) = vaultQuerySvc.trackBy() - // DOCEND VaultQueryExample16 - assertThat(snapshot.states).hasSize(0) - updates - } - val (linearStates,dealStates) = - database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) - val linearStates = services.fillWithSomeTestLinearStates(10).states - val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states - // add more cash - services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - // add another deal - services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) - Pair(linearStates,dealStates) - } + database.transaction { + // DOCSTART VaultQueryExample16 + val (snapshot, updates) = vaultQuerySvc.trackBy() + // DOCEND VaultQueryExample16 + assertThat(snapshot.states).hasSize(0) + updates + } + val (linearStates, dealStates) = + database.transaction { + services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) + val linearStates = services.fillWithSomeTestLinearStates(10).states + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states + // add more cash + services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) + // add another deal + services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) + Pair(linearStates, dealStates) + } database.transaction { // consume stuff services.consumeCash(100.DOLLARS, notary = DUMMY_NOTARY) @@ -2200,24 +2199,24 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun trackDealStates() { val updates = - database.transaction { - // DOCSTART VaultQueryExample17 - val (snapshot, updates) = vaultQuerySvc.trackBy() - // DOCEND VaultQueryExample17 - assertThat(snapshot.states).hasSize(0) - updates - } - val (linearStates,dealStates) = - database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) - val linearStates = services.fillWithSomeTestLinearStates(10).states - val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states - // add more cash - services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - // add another deal - services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) - Pair(linearStates,dealStates) - } + database.transaction { + // DOCSTART VaultQueryExample17 + val (snapshot, updates) = vaultQuerySvc.trackBy() + // DOCEND VaultQueryExample17 + assertThat(snapshot.states).hasSize(0) + updates + } + val (linearStates, dealStates) = + database.transaction { + services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) + val linearStates = services.fillWithSomeTestLinearStates(10).states + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states + // add more cash + services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) + // add another deal + services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) + Pair(linearStates, dealStates) + } database.transaction { // consume stuff services.consumeCash(100.DOLLARS, notary = DUMMY_NOTARY) 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 66bf35ba86..250b4fab5f 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 @@ -20,7 +20,7 @@ import net.corda.node.utilities.CordaPersistence import net.corda.testing.* import net.corda.testing.contracts.* import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestDatabaseAndMockServices +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.After diff --git a/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt b/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt index 23723f70f2..783564a664 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt @@ -8,6 +8,7 @@ import com.google.common.util.concurrent.SettableFuture import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.hours import net.corda.core.utilities.minutes +import net.corda.node.services.events.NodeSchedulerService import net.corda.testing.node.TestClock import org.junit.After import org.junit.Before @@ -41,24 +42,24 @@ class ClockUtilsTest { @Test fun `test waiting no time for a deadline`() { - assertFalse(stoppedClock.awaitWithDeadline(stoppedClock.instant()), "Should have reached deadline") + assertFalse(NodeSchedulerService.awaitWithDeadline(stoppedClock, stoppedClock.instant()), "Should have reached deadline") } @Test fun `test waiting negative time for a deadline`() { - assertFalse(stoppedClock.awaitWithDeadline(stoppedClock.instant().minus(1.hours)), "Should have reached deadline") + assertFalse(NodeSchedulerService.awaitWithDeadline(stoppedClock, stoppedClock.instant().minus(1.hours)), "Should have reached deadline") } @Test fun `test waiting no time for a deadline with incomplete future`() { val future = SettableFuture.create() - assertFalse(stoppedClock.awaitWithDeadline(stoppedClock.instant(), future), "Should have reached deadline") + assertFalse(NodeSchedulerService.awaitWithDeadline(stoppedClock, stoppedClock.instant(), future), "Should have reached deadline") } @Test fun `test waiting negative time for a deadline with incomplete future`() { val future = SettableFuture.create() - assertFalse(stoppedClock.awaitWithDeadline(stoppedClock.instant().minus(1.hours), future), "Should have reached deadline") + assertFalse(NodeSchedulerService.awaitWithDeadline(stoppedClock, stoppedClock.instant().minus(1.hours), future), "Should have reached deadline") } @@ -67,7 +68,7 @@ class ClockUtilsTest { val advancedClock = Clock.offset(stoppedClock, 1.hours) val future = SettableFuture.create() completeNow(future) - assertTrue(stoppedClock.awaitWithDeadline(advancedClock.instant(), future), "Should not have reached deadline") + assertTrue(NodeSchedulerService.awaitWithDeadline(stoppedClock, advancedClock.instant(), future), "Should not have reached deadline") } @Test @@ -75,7 +76,7 @@ class ClockUtilsTest { val advancedClock = Clock.offset(stoppedClock, 1.hours) val future = SettableFuture.create() completeAfterWaiting(future) - assertTrue(stoppedClock.awaitWithDeadline(advancedClock.instant(), future), "Should not have reached deadline") + assertTrue(NodeSchedulerService.awaitWithDeadline(stoppedClock, advancedClock.instant(), future), "Should not have reached deadline") } @Test @@ -83,7 +84,7 @@ class ClockUtilsTest { val advancedClock = Clock.offset(stoppedClock, 1.hours) val testClock = TestClock(stoppedClock) advanceClockAfterWait(testClock, 1.hours) - assertFalse(testClock.awaitWithDeadline(advancedClock.instant()), "Should have reached deadline") + assertFalse(NodeSchedulerService.awaitWithDeadline(testClock, advancedClock.instant()), "Should have reached deadline") } @Test @@ -92,7 +93,7 @@ class ClockUtilsTest { val testClock = TestClock(stoppedClock) val future = SettableFuture.create() advanceClockAfterWait(testClock, 1.hours) - assertFalse(testClock.awaitWithDeadline(advancedClock.instant(), future), "Should have reached deadline") + assertFalse(NodeSchedulerService.awaitWithDeadline(testClock, advancedClock.instant(), future), "Should have reached deadline") } @Test @@ -102,7 +103,7 @@ class ClockUtilsTest { val future = SettableFuture.create() advanceClockAfterWait(testClock, 1.hours) completeAfterWaiting(future) - assertTrue(testClock.awaitWithDeadline(advancedClock.instant(), future), "Should not have reached deadline") + assertTrue(NodeSchedulerService.awaitWithDeadline(testClock, advancedClock.instant(), future), "Should not have reached deadline") } @Test @@ -113,7 +114,7 @@ class ClockUtilsTest { for (advance in 1..6) { advanceClockAfterWait(testClock, 10.minutes) } - assertFalse(testClock.awaitWithDeadline(advancedClock.instant(), future), "Should have reached deadline") + assertFalse(NodeSchedulerService.awaitWithDeadline(testClock, advancedClock.instant(), future), "Should have reached deadline") } @Test @@ -131,7 +132,7 @@ class ClockUtilsTest { val advancedClock = Clock.offset(stoppedClock, 10.hours) try { - testClock.awaitWithDeadline(advancedClock.instant(), SettableFuture.create()) + NodeSchedulerService.awaitWithDeadline(testClock, advancedClock.instant(), SettableFuture.create()) fail("Expected InterruptedException") } catch (exception: InterruptedException) { } @@ -145,7 +146,7 @@ class ClockUtilsTest { val future = CompletableFuture() val scheduler = FiberExecutorScheduler("test", executor) val fiber = scheduler.newFiber(@Suspendable { - future.complete(testClock.awaitWithDeadline(advancedClock.instant(), future)) + future.complete(NodeSchedulerService.awaitWithDeadline(testClock, advancedClock.instant(), future)) }).start() for (advance in 1..6) { scheduler.newFiber(@Suspendable { @@ -167,7 +168,7 @@ class ClockUtilsTest { val future = SettableFuture.create() val scheduler = FiberExecutorScheduler("test", executor) val fiber = scheduler.newFiber(@Suspendable { - future.set(testClock.awaitWithDeadline(advancedClock.instant(), future)) + future.set(NodeSchedulerService.awaitWithDeadline(testClock, advancedClock.instant(), future)) }).start() for (advance in 1..6) { scheduler.newFiber(@Suspendable { 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 900c00e482..ffac359f49 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt @@ -3,9 +3,9 @@ package net.corda.node.utilities import com.google.common.util.concurrent.SettableFuture import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.tee -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties -import net.corda.testing.node.makeTestIdentityService +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties +import net.corda.testing.node.MockServices.Companion.makeTestIdentityService import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Test diff --git a/node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt b/node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt index 2c528cbfd5..930b8a8e78 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt @@ -3,21 +3,25 @@ package net.corda.node.utilities import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512 import net.corda.core.crypto.Crypto.generateKeyPair -import net.corda.core.crypto.cert -import net.corda.core.crypto.commonName -import net.corda.core.crypto.getX509Name import net.corda.core.internal.div import net.corda.core.internal.toTypedArray import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.core.utilities.cert +import net.corda.core.utilities.commonName +import net.corda.core.utilities.getX500Name +import net.corda.core.utilities.organisation import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.node.services.config.createKeystoreForCordaNode import net.corda.nodeapi.internal.serialization.AllWhitelist import net.corda.nodeapi.internal.serialization.KryoHeaderV0_1 import net.corda.nodeapi.internal.serialization.SerializationContextImpl import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl -import net.corda.testing.* +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.BOB_PUBKEY +import net.corda.testing.MEGA_CORP import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.BasicConstraints import org.bouncycastle.asn1.x509.Extension @@ -54,7 +58,7 @@ class X509UtilitiesTest { @Test fun `create valid self-signed CA certificate`() { val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val caCert = X509Utilities.createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey) + val caCert = X509Utilities.createSelfSignedCACertificate(getX500Name(CN = "Test Cert", O = "R3 Ltd", L = "London", C = "GB"), caKey) assertTrue { caCert.subject.commonName == "Test Cert" } // using our subject common name assertEquals(caCert.issuer, caCert.subject) //self-signed caCert.isValidOn(Date()) // throws on verification problems @@ -69,7 +73,7 @@ class X509UtilitiesTest { fun `load and save a PEM file certificate`() { val tmpCertificateFile = tempFile("cacert.pem") val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val caCert = X509Utilities.createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey) + val caCert = X509Utilities.createSelfSignedCACertificate(getX500Name(CN = "Test Cert", O = "R3 Ltd", L = "London", C = "GB"), caKey) X509Utilities.saveCertificateAsPEMFile(caCert, tmpCertificateFile) val readCertificate = X509Utilities.loadCertificateFromPEMFile(tmpCertificateFile) assertEquals(caCert, readCertificate) @@ -78,8 +82,8 @@ class X509UtilitiesTest { @Test fun `create valid server certificate chain`() { val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val caCert = X509Utilities.createSelfSignedCACertificate(getTestX509Name("Test CA Cert"), caKey) - val subject = getTestX509Name("Server Cert") + val caCert = X509Utilities.createSelfSignedCACertificate(getX500Name(CN = "Test CA Cert", O = "R3 Ltd", L = "London", C = "GB"), caKey) + val subject = getX500Name(CN = "Server Cert", O = "R3 Ltd", L = "London", C = "GB") val keyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val serverCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, caKey, subject, keyPair.public) assertTrue { serverCert.subject.toString().contains("CN=Server Cert") } // using our subject common name @@ -208,7 +212,7 @@ class X509UtilitiesTest { serverCertAndKey.certificate.isValidOn(Date()) serverCertAndKey.certificate.isSignatureValid(JcaContentVerifierProviderBuilder().build(caCertAndKey.certificate.subjectPublicKeyInfo)) - assertTrue { serverCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.commonName) } + assertTrue { serverCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.organisation) } // Load back server certificate val sslKeyStore = loadKeyStore(tmpSSLKeyStore, "serverstorepass") @@ -217,7 +221,7 @@ class X509UtilitiesTest { sslCertAndKey.certificate.isValidOn(Date()) sslCertAndKey.certificate.isSignatureValid(JcaContentVerifierProviderBuilder().build(serverCertAndKey.certificate.subjectPublicKeyInfo)) - assertTrue { sslCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.commonName) } + assertTrue { sslCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.organisation) } // Now sign something with private key and verify against certificate public key val testData = "123456".toByteArray() val signature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData) @@ -355,10 +359,10 @@ class X509UtilitiesTest { trustStorePassword: String ): KeyStore { val rootCAKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = X509Utilities.createSelfSignedCACertificate(getX509Name("Corda Node Root CA", "London", "demo@r3.com", null), rootCAKey) + val rootCACert = X509Utilities.createSelfSignedCACertificate(getX500Name(CN = "Corda Node Root CA", O = "R3CEV", L = "London", C = "GB"), rootCAKey) val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, getX509Name("Corda Node Intermediate CA", "London", "demo@r3.com", null), intermediateCAKeyPair.public) + val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, getX500Name(CN = "Corda Node Intermediate CA", O = "R3CEV", L = "London", C = "GB"), intermediateCAKeyPair.public) val keyPass = keyPassword.toCharArray() val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword) 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 da5569e280..38e8dabf99 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 @@ -5,15 +5,15 @@ import com.nhaarman.mockito_kotlin.eq import com.nhaarman.mockito_kotlin.mock import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.cert -import net.corda.core.crypto.commonName import net.corda.core.internal.exists import net.corda.core.internal.toTypedArray import net.corda.core.internal.toX509CertHolder +import net.corda.core.utilities.cert +import net.corda.core.utilities.commonName +import net.corda.core.utilities.getX500Name import net.corda.node.utilities.X509Utilities import net.corda.node.utilities.loadKeyStore import net.corda.testing.ALICE -import net.corda.testing.getTestX509Name import net.corda.testing.testNodeConfiguration import org.junit.Rule import org.junit.Test @@ -34,7 +34,7 @@ class NetworkRegistrationHelperTest { val identities = listOf("CORDA_CLIENT_CA", "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA") - .map { getTestX509Name(it) } + .map { getX500Name(CN = it, O = "R3 Ltd", L = "London", C = "GB") } val certs = identities.stream().map { X509Utilities.createSelfSignedCACertificate(it, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) } .map { it.cert }.toTypedArray() diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index 050eb95246..7d1587af73 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -30,16 +30,16 @@ dependencies { cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') cordaCompile project(':core') cordaCompile project(':webserver') - cordaCompile project(':test-utils') + cordaCompile project(':node-driver') } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': ["StartFlow.net.corda.attachmentdemo.AttachmentDemoFlow"]]] directory "./build/nodes" - networkMap "CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH" + networkMap "O=Notary Service,OU=corda,L=Zurich,C=CH" node { - name "CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH" + name "O=Notary Service,OU=corda,L=Zurich,C=CH" advertisedServices["corda.notary.validating"] p2pPort 10002 rpcPort 10003 @@ -47,7 +47,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { rpcUsers = ext.rpcUsers } node { - name "CN=Bank A,O=Bank A,L=London,C=GB" + name "O=Bank A,L=London,C=GB" advertisedServices = [] p2pPort 10005 rpcPort 10006 @@ -55,7 +55,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { rpcUsers = ext.rpcUsers } node { - name "CN=Bank B,O=Bank B,L=New York,C=US" + name "O=Bank B,L=New York,C=US" advertisedServices = [] p2pPort 10008 rpcPort 10009 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 a8e2787c3a..347cb75c78 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 @@ -1,17 +1,16 @@ package net.corda.attachmentdemo import net.corda.core.node.services.ServiceInfo -import net.corda.core.internal.concurrent.transpose import net.corda.core.utilities.getOrThrow +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission +import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.nodeapi.User 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.FlowPermissions.Companion.startFlowPermission -import net.corda.node.services.transactions.SimpleNotaryService -import net.corda.nodeapi.User import org.junit.Test -import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletableFuture.supplyAsync class AttachmentDemoTest { // run with a 10,000,000 bytes in-memory zip file. In practice, a slightly bigger file will be used (~10,002,000 bytes). @@ -19,19 +18,18 @@ class AttachmentDemoTest { val numOfExpectedBytes = 10_000_000 driver(dsl = { val demoUser = listOf(User("demo", "demo", setOf(startFlowPermission()))) - val (nodeA, nodeB) = listOf( - startNode(DUMMY_BANK_A.name, rpcUsers = demoUser), - startNode(DUMMY_BANK_B.name, rpcUsers = demoUser), - startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) - ).transpose().getOrThrow() + val notaryFuture = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + val nodeAFuture = startNode(providedName = DUMMY_BANK_A.name, rpcUsers = demoUser) + val nodeBFuture = startNode(providedName = DUMMY_BANK_B.name, rpcUsers = demoUser) + val (nodeA, nodeB) = listOf(nodeAFuture, nodeBFuture, notaryFuture).map { it.getOrThrow() } - val senderThread = CompletableFuture.supplyAsync { + val senderThread = supplyAsync { nodeA.rpcClientToNode().start(demoUser[0].username, demoUser[0].password).use { sender(it.proxy, numOfExpectedBytes) } }.exceptionally { it.printStackTrace() } - val recipientThread = CompletableFuture.supplyAsync { + val recipientThread = supplyAsync { nodeB.rpcClientToNode().start(demoUser[0].username, demoUser[0].password).use { recipient(it.proxy) } 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 adc945e56b..1b24d48640 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 @@ -6,6 +6,7 @@ import net.corda.client.rpc.CordaRPCClient import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.Contract import net.corda.core.contracts.ContractState +import net.corda.core.contracts.TypeOnlyCommandData import net.corda.core.crypto.SecureHash import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowLogic @@ -115,8 +116,9 @@ class AttachmentDemoFlow(val otherSide: Party, val notary: Party, val hash: Secu override fun call(): SignedTransaction { // Create a trivial transaction with an output that describes the attachment, and the attachment itself val ptx = TransactionBuilder(notary) - ptx.addOutputState(AttachmentContract.State(hash)) - ptx.addAttachment(hash) + .addOutputState(AttachmentContract.State(hash)) + .addCommand(AttachmentContract.Command, serviceHub.legalIdentityKey) + .addAttachment(hash) progressTracker.currentStep = SIGNING @@ -129,7 +131,7 @@ class AttachmentDemoFlow(val otherSide: Party, val notary: Party, val hash: Secu fun recipient(rpc: CordaRPCOps) { println("Waiting to receive transaction ...") - val stx = rpc.verifiedTransactionsFeed().updates.toBlocking().first() + val stx = rpc.internalVerifiedTransactionsFeed().updates.toBlocking().first() val wtx = stx.tx if (wtx.attachments.isNotEmpty()) { if (wtx.outputs.isNotEmpty()) { @@ -183,6 +185,8 @@ class AttachmentContract : Contract { require(state.hash == attachment.id) } + object Command : TypeOnlyCommandData() + data class State(val hash: SecureHash.SHA256) : ContractState { override val contract: Contract = AttachmentContract() override val participants: List = emptyList() 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 bd586c7542..9f9c269e13 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 @@ -16,9 +16,9 @@ import net.corda.testing.driver.driver fun main(args: Array) { val demoUser = listOf(User("demo", "demo", setOf("StartFlow.net.corda.flows.FinalityFlow"))) driver(isDebug = true, driverDirectory = "build" / "attachment-demo-nodes") { - startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) - startNode(DUMMY_BANK_A.name, rpcUsers = demoUser) - startNode(DUMMY_BANK_B.name, rpcUsers = demoUser) + startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + startNode(providedName = DUMMY_BANK_A.name, rpcUsers = demoUser) + startNode(providedName = DUMMY_BANK_B.name, rpcUsers = demoUser) waitForAllNodesToFinish() } } diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index f1485103f6..16bb935396 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -34,7 +34,7 @@ dependencies { cordaCompile project(':client:jfx') cordaCompile project(':client:rpc') cordaCompile project(':webserver') - cordaCompile project(':test-utils') + cordaCompile project(':node-driver') // Javax is required for webapis compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" @@ -50,16 +50,16 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" // This name "Notary" is hard-coded into BankOfCordaClientApi so if you change it here, change it there too. // In this demo the node that runs a standalone notary also acts as the network map server. - networkMap "CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH" + networkMap "O=Notary Service,OU=corda,L=Zurich,C=CH" node { - name "CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH" + name "O=Notary Service,OU=corda,L=Zurich,C=CH" advertisedServices = ["corda.notary.validating"] p2pPort 10002 rpcPort 10003 cordapps = ["net.corda:finance:$corda_release_version"] } node { - name "CN=BankOfCorda,O=R3,L=New York,C=US" + name "O=BankOfCorda,L=New York,C=US" advertisedServices = ["corda.issuer.USD"] p2pPort 10005 rpcPort 10006 @@ -74,7 +74,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { ] } node { - name "CN=BigCorporation,O=R3,OU=corda,L=London,C=GB" + name "O=BigCorporation,OU=corda,L=London,C=GB" advertisedServices = [] p2pPort 10008 rpcPort 10009 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 d6d24385bf..716587966d 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 @@ -3,11 +3,10 @@ package net.corda.bank import net.corda.bank.api.BankOfCordaClientApi import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams import net.corda.core.node.services.ServiceInfo -import net.corda.core.internal.concurrent.transpose import net.corda.core.utilities.getOrThrow -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 @@ -15,10 +14,9 @@ class BankOfCordaHttpAPITest { @Test fun `issuer flow via Http`() { driver(dsl = { - val (nodeBankOfCorda) = listOf( - startNode(BOC.name, setOf(ServiceInfo(SimpleNotaryService.type))), - startNode(BIGCORP_LEGAL_NAME) - ).transpose().getOrThrow() + val bigCorpNodeFuture = startNode(providedName = BIGCORP_LEGAL_NAME) + val nodeBankOfCordaFuture = startNode(providedName = BOC.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + val (nodeBankOfCorda) = listOf(nodeBankOfCordaFuture, bigCorpNodeFuture).map { it.getOrThrow() } val anonymous = false val nodeBankOfCordaApiAddr = startWebserver(nodeBankOfCorda).getOrThrow().listenAddress assertTrue(BankOfCordaClientApi(nodeBankOfCordaApiAddr).requestWebIssue(IssueRequestParams(1000, "USD", BIGCORP_LEGAL_NAME, "1", BOC.name, BOC.name, anonymous))) 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 4df2768692..4a4bfadacb 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,6 +1,5 @@ package net.corda.bank -import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault @@ -23,10 +22,9 @@ class BankOfCordaRPCClientTest { val bocManager = User("bocManager", "password1", permissions = setOf( startFlowPermission())) val bigCorpCFO = User("bigCorpCFO", "password2", permissions = emptySet()) - val (nodeBankOfCorda, nodeBigCorporation) = listOf( - startNode(BOC.name, setOf(ServiceInfo(SimpleNotaryService.type)), listOf(bocManager)), - startNode(BIGCORP_LEGAL_NAME, rpcUsers = listOf(bigCorpCFO)) - ).transpose().getOrThrow() + val nodeBankOfCordaFuture = startNode(providedName = BOC.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)), rpcUsers = listOf(bocManager)) + val nodeBigCorporationFuture = startNode(providedName = BIGCORP_LEGAL_NAME, rpcUsers = listOf(bigCorpCFO)) + val (nodeBankOfCorda, nodeBigCorporation) = listOf(nodeBankOfCordaFuture, nodeBigCorporationFuture).map { it.getOrThrow() } // Bank of Corda RPC Client val bocClient = nodeBankOfCorda.rpcClientToNode() 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 0f61b602e0..1c0f6af795 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 @@ -6,6 +6,7 @@ import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.getX500Name import net.corda.finance.flows.CashExitFlow import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.finance.flows.CashPaymentFlow @@ -15,7 +16,6 @@ import net.corda.nodeapi.User import net.corda.testing.BOC import net.corda.testing.DUMMY_NOTARY import net.corda.testing.driver.driver -import org.bouncycastle.asn1.x500.X500Name import kotlin.system.exitProcess /** @@ -28,7 +28,7 @@ fun main(args: Array) { val BANK_USERNAME = "bankUser" val BIGCORP_USERNAME = "bigCorpUser" -val BIGCORP_LEGAL_NAME = X500Name("CN=BigCorporation,O=R3,OU=corda,L=London,C=GB") +val BIGCORP_LEGAL_NAME = getX500Name(O = "BigCorporation", OU = "corda", L = "London", C = "GB") private class BankOfCordaDriver { enum class Role { @@ -71,12 +71,13 @@ private class BankOfCordaDriver { val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf( startFlowPermission())) - startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) + startNode(providedName = DUMMY_NOTARY.name, + advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) val bankOfCorda = startNode( - BOC.name, + providedName = BOC.name, rpcUsers = listOf(bankUser), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.USD")))) - startNode(BIGCORP_LEGAL_NAME, rpcUsers = listOf(bigCorpUser)) + startNode(providedName = BIGCORP_LEGAL_NAME, rpcUsers = listOf(bigCorpUser)) startWebserver(bankOfCorda.get()) waitForAllNodesToFinish() }, isDebug = true) diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index e9f572b106..1eef8f0ff1 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -43,16 +43,16 @@ dependencies { // Specify your cordapp's dependencies below, including dependent cordapps compile "com.squareup.okhttp3:okhttp:$okhttp_version" - testCompile project(':test-utils') + testCompile project(':node-driver') testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:${assertj_version}" } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH" + networkMap "O=Notary Service,OU=corda,L=Zurich,C=CH" node { - name "CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH" + name "O=Notary Service,OU=corda,L=Zurich,C=CH" advertisedServices = ["corda.notary.validating", "corda.interest_rates"] p2pPort 10002 rpcPort 10003 @@ -61,7 +61,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { useTestClock true } node { - name "CN=Bank A,O=Bank A,L=London,C=GB" + name "O=Bank A,L=London,C=GB" advertisedServices = [] p2pPort 10005 rpcPort 10006 @@ -70,7 +70,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { useTestClock true } node { - name "CN=Bank B,O=Bank B,L=New York,C=US" + name "O=Bank B,L=New York,C=US" advertisedServices = [] p2pPort 10008 rpcPort 10009 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 1116623f2a..a523e5f273 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,7 +1,6 @@ package net.corda.irs import net.corda.client.rpc.CordaRPCClient -import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.vaultTrackBy import net.corda.core.node.services.ServiceInfo import net.corda.core.toFuture @@ -44,24 +43,25 @@ class IRSDemoTest : IntegrationTestCategory { @Test fun `runs IRS demo`() { driver(useTestClock = true, isDebug = true) { - val (controller, nodeA, nodeB) = listOf( - startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.Oracle.type))), - startNode(DUMMY_BANK_A.name, rpcUsers = listOf(rpcUser)), - startNode(DUMMY_BANK_B.name) - ).transpose().getOrThrow() + val controllerFuture = startNode( + providedName = DUMMY_NOTARY.name, + advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.Oracle.type))) + val nodeAFuture = startNode(providedName = DUMMY_BANK_A.name, rpcUsers = listOf(rpcUser)) + val nodeBFuture = startNode(providedName = DUMMY_BANK_B.name) + val (controller, nodeA, nodeB) = listOf(controllerFuture, nodeAFuture, nodeBFuture).map { it.getOrThrow() } log.info("All nodes started") - val (controllerAddr, nodeAAddr, nodeBAddr) = listOf( - startWebserver(controller), - startWebserver(nodeA), - startWebserver(nodeB) - ).transpose().getOrThrow().map { it.listenAddress } + val controllerAddrFuture = startWebserver(controller) + val nodeAAddrFuture = startWebserver(nodeA) + val nodeBAddrFuture = startWebserver(nodeB) + val (controllerAddr, nodeAAddr, nodeBAddr) = + listOf(controllerAddrFuture, nodeAAddrFuture, nodeBAddrFuture).map { it.getOrThrow().listenAddress } log.info("All webservers started") val (_, nodeAApi, nodeBApi) = listOf(controller, nodeA, nodeB).zip(listOf(controllerAddr, nodeAAddr, nodeBAddr)).map { - val mapper = net.corda.jackson.JacksonSupport.createDefaultMapper(it.first.rpc) + val mapper = net.corda.client.jackson.JacksonSupport.createDefaultMapper(it.first.rpc) registerFinanceJSONMappers(mapper) HttpApi.fromHostAndPort(it.second, "api/irs", mapper = mapper) } 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 f9ec94b819..695ab9d2e3 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 @@ -175,9 +175,7 @@ object NodeInterestRates { // Note that we will happily sign an invalid transaction, as we are only being presented with a filtered // 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 signableData = SignableData(ftx.rootHash, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(signingKey).schemeNumberID)) - val signature = services.keyManagementService.sign(signableData, signingKey) - return TransactionSignature(signature.bytes, signingKey, signableData.signatureMetadata) + return services.createSignature(ftx, signingKey) } // DOCEND 1 diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt index aa901cfe6b..8a3d074e24 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt @@ -2,7 +2,6 @@ package net.corda.irs.contract import com.fasterxml.jackson.annotation.JsonIgnoreProperties import net.corda.core.contracts.* -import net.corda.core.crypto.containsAny import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party @@ -18,7 +17,6 @@ import org.apache.commons.jexl3.JexlBuilder import org.apache.commons.jexl3.MapContext import java.math.BigDecimal import java.math.RoundingMode -import java.security.PublicKey import java.time.LocalDate import java.util.* @@ -76,7 +74,7 @@ abstract class RatePaymentEvent(date: LocalDate, abstract val flow: Amount - val days: Int get() = calculateDaysBetween(accrualStartDate, accrualEndDate, dayCountBasisYear, dayCountBasisDay) + val days: Int get() = BusinessCalendar.calculateDaysBetween(accrualStartDate, accrualEndDate, dayCountBasisYear, dayCountBasisDay) // TODO : Fix below (use daycount convention for division, not hardcoded 360 etc) val dayCountFactor: BigDecimal get() = (BigDecimal(days).divide(BigDecimal(360.0), 8, RoundingMode.HALF_UP)).setScale(4, RoundingMode.HALF_UP) @@ -513,7 +511,7 @@ class InterestRateSwap : Contract { checkLegDates(listOf(irs.fixedLeg, irs.floatingLeg)) } - private fun verifyFixCommand(inputs: List, outputs: List, command: AuthenticatedObject) { + private fun verifyFixCommand(inputs: List, outputs: List, command: CommandWithParties) { val irs = outputs.filterIsInstance().single() val prevIrs = inputs.filterIsInstance().single() val paymentDifferences = getFloatingLegPaymentsDifferences(prevIrs.calculation.floatingLegPaymentSchedule, irs.calculation.floatingLegPaymentSchedule) @@ -618,10 +616,6 @@ class InterestRateSwap : Contract { override val participants: List get() = listOf(fixedLeg.fixedRatePayer, floatingLeg.floatingRatePayer) - override fun isRelevant(ourKeys: Set): Boolean { - return fixedLeg.fixedRatePayer.owningKey.containsAny(ourKeys) || floatingLeg.floatingRatePayer.owningKey.containsAny(ourKeys) - } - override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? { val nextFixingOf = nextFixingOf() ?: return null diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt index 099d82c73a..bb7b27e9fc 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt @@ -52,11 +52,9 @@ object AutoOfferFlow { // need to pick which ever party is not us val otherParty = notUs(dealToBeOffered.participants).map { serviceHub.identityService.partyFromAnonymous(it) }.requireNoNulls().single() progressTracker.currentStep = DEALING - val myKey = serviceHub.legalIdentityKey val instigator = Instigator( otherParty, AutoOffer(notary, dealToBeOffered), - myKey, progressTracker.getChildProgressTracker(DEALING)!! ) val stx = subFlow(instigator) 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 65b6bb6ad7..c95dafe25a 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 @@ -3,11 +3,12 @@ package net.corda.irs.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* import net.corda.core.crypto.TransactionSignature -import net.corda.core.crypto.toBase58String +import net.corda.core.utilities.toBase58String import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.SchedulableFlow +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.node.services.ServiceType @@ -102,18 +103,12 @@ object FixingFlow { class Floater(override val otherParty: Party, override val payload: FixingSession, override val progressTracker: ProgressTracker = TwoPartyDealFlow.Primary.tracker()) : TwoPartyDealFlow.Primary() { - @Suppress("UNCHECKED_CAST") internal val dealToFix: StateAndRef by transient { val state = serviceHub.loadState(payload.ref) as TransactionState StateAndRef(state, payload.ref) } - override val myKey: PublicKey get() { - dealToFix.state.data.participants.single { it.owningKey == serviceHub.myInfo.legalIdentity.owningKey } - return serviceHub.legalIdentityKey - } - override val notaryNode: NodeInfo get() { return serviceHub.networkMapCache.notaryNodes.single { it.notaryIdentity == dealToFix.state.notary } } diff --git a/samples/irs-demo/src/main/resources/irsweb/js/viewmodel/FixedLeg.js b/samples/irs-demo/src/main/resources/irsweb/js/viewmodel/FixedLeg.js index a6d25e2ec1..e8e6fc6486 100644 --- a/samples/irs-demo/src/main/resources/irsweb/js/viewmodel/FixedLeg.js +++ b/samples/irs-demo/src/main/resources/irsweb/js/viewmodel/FixedLeg.js @@ -2,7 +2,7 @@ define(['utils/dayCountBasisLookup'], (dayCountBasisLookup) => { return { - fixedRatePayer: "CN=Bank A,O=Bank A,L=London,C=GB", + fixedRatePayer: "O=Bank A,L=London,C=GB", notional: { quantity: 2500000000 }, diff --git a/samples/irs-demo/src/main/resources/irsweb/js/viewmodel/FloatingLeg.js b/samples/irs-demo/src/main/resources/irsweb/js/viewmodel/FloatingLeg.js index 51f38c4f68..a103cafa86 100644 --- a/samples/irs-demo/src/main/resources/irsweb/js/viewmodel/FloatingLeg.js +++ b/samples/irs-demo/src/main/resources/irsweb/js/viewmodel/FloatingLeg.js @@ -2,7 +2,7 @@ define(['utils/dayCountBasisLookup'], (dayCountBasisLookup) => { return { - floatingRatePayer: "CN=Bank B,O=Bank B,L=New York,C=US", + floatingRatePayer: "O=Bank B,L=New York,C=US", notional: { quantity: 2500000000 }, diff --git a/samples/irs-demo/src/main/resources/net/corda/irs/simulation/example-irs-trade.json b/samples/irs-demo/src/main/resources/net/corda/irs/simulation/example-irs-trade.json index 829c38d6a5..389fa1618c 100644 --- a/samples/irs-demo/src/main/resources/net/corda/irs/simulation/example-irs-trade.json +++ b/samples/irs-demo/src/main/resources/net/corda/irs/simulation/example-irs-trade.json @@ -1,6 +1,6 @@ { "fixedLeg": { - "fixedRatePayer": "CN=Bank A,O=Bank A,L=London,C=GB", + "fixedRatePayer": "O=Bank A,L=London,C=GB", "notional": "€25000000", "paymentFrequency": "SemiAnnual", "effectiveDate": "2016-03-11", @@ -22,7 +22,7 @@ "interestPeriodAdjustment": "Adjusted" }, "floatingLeg": { - "floatingRatePayer": "CN=Bank B,O=Bank B,L=New York,C=US", + "floatingRatePayer": "O=Bank B,L=New York,C=US", "notional": "€25000000", "paymentFrequency": "Quarterly", "effectiveDate": "2016-03-11", 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 f66fbe7a96..4604da6b3e 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 @@ -1,6 +1,5 @@ package net.corda.irs -import net.corda.core.internal.concurrent.transpose import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.getOrThrow import net.corda.testing.DUMMY_BANK_A @@ -16,11 +15,12 @@ import net.corda.testing.driver.driver */ fun main(args: Array) { driver(dsl = { - val (controller, nodeA, nodeB) = listOf( - startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.Oracle.type))), - startNode(DUMMY_BANK_A.name), - startNode(DUMMY_BANK_B.name) - ).transpose().getOrThrow() + val controllerFuture = startNode( + providedName = DUMMY_NOTARY.name, + advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.Oracle.type))) + val nodeAFuture = startNode(providedName = DUMMY_BANK_A.name) + val nodeBFuture = startNode(providedName = DUMMY_BANK_B.name) + val (controller, nodeA, nodeB) = listOf(controllerFuture, nodeAFuture, nodeBFuture).map { it.getOrThrow() } startWebserver(controller) startWebserver(nodeA) 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 2d9e0577ef..714840f777 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 @@ -11,6 +11,7 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.getX500Name import net.corda.finance.DOLLARS import net.corda.finance.contracts.Fix import net.corda.finance.contracts.FixOf @@ -33,6 +34,9 @@ import java.util.function.Predicate import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties +import net.corda.testing.node.MockServices.Companion.makeTestIdentityService class NodeInterestRatesTest : TestDependencyInjectionBase() { val TEST_DATA = NodeInterestRates.parseFile(""" @@ -45,7 +49,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { """.trimIndent()) val DUMMY_CASH_ISSUER_KEY = generateKeyPair() - val DUMMY_CASH_ISSUER = Party(X500Name("CN=Cash issuer,O=R3,OU=corda,L=London,C=GB"), DUMMY_CASH_ISSUER_KEY.public) + val DUMMY_CASH_ISSUER = Party(getX500Name(O="Cash issuer",OU="corda",L="London",C="GB"), DUMMY_CASH_ISSUER_KEY.public) lateinit var oracle: NodeInterestRates.Oracle lateinit var database: CordaPersistence 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 d15c86bcec..03195cc5e6 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 @@ -23,6 +23,7 @@ object UpdateBusinessDayFlow { @InitiatedBy(Broadcast::class) private class UpdateBusinessDayHandler(val otherParty: Party) : FlowLogic() { + @Suspendable override fun call() { val message = receive(otherParty).unwrap { it } (serviceHub.clock as TestClock).updateDate(message.date) diff --git a/samples/network-visualiser/build.gradle b/samples/network-visualiser/build.gradle index 7450bcd816..d472b1e5b3 100644 --- a/samples/network-visualiser/build.gradle +++ b/samples/network-visualiser/build.gradle @@ -7,7 +7,7 @@ apply plugin: 'us.kirchmeier.capsule' dependencies { compile project(':samples:irs-demo') - compile project(':test-utils') + compile project(':node-driver') compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" testCompile "junit:junit:$junit_version" @@ -20,7 +20,7 @@ dependencies { // Cordapp dependencies // GraphStream: For visualisation - compile 'co.paralleluniverse:capsule:1.0.3' + compileOnly "co.paralleluniverse:capsule:$capsule_version" compile "org.graphstream:gs-core:1.3" compile "org.graphstream:gs-ui:1.3" } diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/NetworkMapVisualiser.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/NetworkMapVisualiser.kt index 93fa4b4e95..16bf49e595 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/NetworkMapVisualiser.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/NetworkMapVisualiser.kt @@ -11,7 +11,7 @@ import javafx.scene.input.KeyCodeCombination import javafx.scene.layout.VBox import javafx.stage.Stage import javafx.util.Duration -import net.corda.core.crypto.commonName +import net.corda.core.utilities.organisation import net.corda.core.serialization.deserialize import net.corda.core.utilities.ProgressTracker import net.corda.netmap.VisualiserViewModel.Style @@ -171,7 +171,7 @@ class NetworkMapVisualiser : Application() { onNextInvoked() } } - viewModel.simulation.networkInitialisationFinished.then { + viewModel.simulation.networkInitialisationFinished.thenAccept { view.simulateInitialisationCheckbox.isVisible = false } view.runPauseButton.setOnAction { @@ -233,7 +233,7 @@ class NetworkMapVisualiser : Application() { } else if (!viewModel.trackerBoxes.containsKey(tracker)) { // New flow started up; add. val extraLabel = viewModel.simulation.extraNodeLabels[node] - val label = if (extraLabel != null) "${node.info.legalIdentity.name.commonName}: $extraLabel" else node.info.legalIdentity.name.commonName + val label = if (extraLabel != null) "${node.info.legalIdentity.name.organisation}: $extraLabel" else node.info.legalIdentity.name.organisation val widget = view.buildProgressTrackerWidget(label, tracker.topLevelTracker) println("Added: $tracker, $widget") viewModel.trackerBoxes[tracker] = widget diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt index 547d29be7b..638f8bb168 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt @@ -7,9 +7,10 @@ import javafx.scene.layout.StackPane import javafx.scene.shape.Circle import javafx.scene.shape.Line import javafx.util.Duration -import net.corda.core.crypto.commonName import net.corda.core.node.ScreenCoordinate import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.getX500Name +import net.corda.core.utilities.organisation import net.corda.netmap.simulation.IRSSimulation import net.corda.testing.node.MockNetwork import org.bouncycastle.asn1.x500.X500Name @@ -127,7 +128,7 @@ class VisualiserViewModel { } } - fun makeNodeWidget(forNode: MockNetwork.MockNode, type: String, label: X500Name = X500Name("CN=Bank of Bologna,OU=Corda QA Department,O=R3 CEV,L=Bologna,C=IT"), + fun makeNodeWidget(forNode: MockNetwork.MockNode, type: String, label: X500Name = getX500Name(O = "Bank of Bologna", OU = "Corda QA Department", L = "Bologna", C = "IT"), nodeType: NetworkMapVisualiser.NodeType, index: Int): NodeWidget { fun emitRadarPulse(initialRadius: Double, targetRadius: Double, duration: Double): Pair { val pulse = Circle(initialRadius).apply { @@ -157,7 +158,7 @@ class VisualiserViewModel { view.root.children += longPulseOuterDot view.root.children += innerDot - val nameLabel = Label(label.commonName) + val nameLabel = Label(label.organisation) val nameLabelRect = StackPane(nameLabel).apply { styleClass += "node-label" alignment = Pos.CENTER_RIGHT 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 c18dac37b2..49b5d64c7c 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,14 +3,12 @@ package net.corda.netmap.simulation import co.paralleluniverse.fibers.Suspendable import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue -import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.StateAndRef 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.internal.FlowStateMachine -import net.corda.core.internal.concurrent.* import net.corda.core.node.services.queryBy import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction @@ -20,7 +18,7 @@ import net.corda.finance.flows.TwoPartyDealFlow.Instigator import net.corda.finance.plugin.registerFinanceJSONMappers import net.corda.irs.contract.InterestRateSwap import net.corda.irs.flows.FixingFlow -import net.corda.jackson.JacksonSupport +import net.corda.client.jackson.JacksonSupport import net.corda.node.services.identity.InMemoryIdentityService import net.corda.testing.DUMMY_CA import net.corda.testing.node.InMemoryMessagingNetwork @@ -28,6 +26,8 @@ import rx.Observable import java.security.PublicKey import java.time.LocalDate import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletableFuture.allOf /** @@ -42,46 +42,53 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten private val executeOnNextIteration = Collections.synchronizedList(LinkedList<() -> Unit>()) - override fun startMainSimulation(): CordaFuture { - // TODO: Determine why this isn't happening via the network map - mockNet.nodes.map { it.services.identityService }.forEach { service -> - mockNet.nodes.forEach { node -> service.registerIdentity(node.info.legalIdentityAndCert) } - } - - val future = openFuture() + override fun startMainSimulation(): CompletableFuture { om = JacksonSupport.createInMemoryMapper(InMemoryIdentityService((banks + regulators + networkMap).map { it.info.legalIdentityAndCert }, trustRoot = DUMMY_CA.certificate)) registerFinanceJSONMappers(om) - startIRSDealBetween(0, 1).thenMatch({ + return startIRSDealBetween(0, 1).thenCompose { + val future = CompletableFuture() // Next iteration is a pause. executeOnNextIteration.add {} executeOnNextIteration.add { // Keep fixing until there's no more left to do. val initialFixFuture = doNextFixing(0, 1) fun onFailure(t: Throwable) { - future.setException(t) // Propagate the error. + future.completeExceptionally(t) } - fun onSuccess(result: Unit?) { + fun onSuccess() { // Pause for an iteration. executeOnNextIteration.add {} executeOnNextIteration.add { val f = doNextFixing(0, 1) if (f != null) { - f.thenMatch(::onSuccess, ::onFailure) + f.handle { _, throwable -> + if (throwable == null) { + onSuccess() + } else { + onFailure(throwable) + } + } } else { // All done! - future.set(Unit) + future.complete(Unit) } } } - initialFixFuture!!.thenMatch(::onSuccess, ::onFailure) + initialFixFuture!!.handle { _, throwable -> + if (throwable == null) { + onSuccess() + } else { + onFailure(throwable) + } + } } - }, {}) - return future + future + } } - private fun doNextFixing(i: Int, j: Int): CordaFuture? { + private fun doNextFixing(i: Int, j: Int): CompletableFuture? { println("Doing a fixing between $i and $j") val node1: SimulatedNode = banks[i] val node2: SimulatedNode = banks[j] @@ -107,10 +114,10 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten if (nextFixingDate > currentDateAndTime.toLocalDate()) currentDateAndTime = nextFixingDate.atTime(15, 0) - return listOf(futA, futB).transpose().map { Unit } + return allOf(futA.toCompletableFuture(), futB.toCompletableFuture()) } - private fun startIRSDealBetween(i: Int, j: Int): CordaFuture { + private fun startIRSDealBetween(i: Int, j: Int): CompletableFuture { val node1: SimulatedNode = banks[i] val node2: SimulatedNode = banks[j] @@ -129,10 +136,9 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten @InitiatingFlow class StartDealFlow(val otherParty: Party, - val payload: AutoOffer, - val myKey: PublicKey) : FlowLogic() { + val payload: AutoOffer) : FlowLogic() { @Suspendable - override fun call(): SignedTransaction = subFlow(Instigator(otherParty, payload, myKey)) + override fun call(): SignedTransaction = subFlow(Instigator(otherParty, payload)) } @InitiatedBy(StartDealFlow::class) @@ -141,8 +147,8 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten val acceptDealFlows: Observable = node2.registerInitiatedFlow(AcceptDealFlow::class.java) @Suppress("UNCHECKED_CAST") - val acceptorTxFuture = acceptDealFlows.toFuture().flatMap { - (it.stateMachine as FlowStateMachine).resultFuture + val acceptorTxFuture = acceptDealFlows.toFuture().toCompletableFuture().thenCompose { + (it.stateMachine as FlowStateMachine).resultFuture.toCompletableFuture() } showProgressFor(listOf(node1, node2)) @@ -150,11 +156,10 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten val instigator = StartDealFlow( node2.info.legalIdentity, - AutoOffer(notary.info.notaryIdentity, irs), - node1.services.legalIdentityKey) + AutoOffer(notary.info.notaryIdentity, irs)) val instigatorTxFuture = node1.services.startFlow(instigator).resultFuture - return listOf(instigatorTxFuture, acceptorTxFuture).transpose().flatMap { instigatorTxFuture } + return allOf(instigatorTxFuture.toCompletableFuture(), acceptorTxFuture).thenCompose { instigatorTxFuture.toCompletableFuture() } } override fun iterate(): InMemoryMessagingNetwork.MessageTransfer? { 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 c70c383102..fbc3ab9304 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 @@ -1,31 +1,27 @@ package net.corda.netmap.simulation -import net.corda.core.concurrent.CordaFuture -import net.corda.core.crypto.locationOrNull import net.corda.core.flows.FlowLogic import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.CityDatabase import net.corda.core.node.WorldMapLocation import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.containsType -import net.corda.core.internal.concurrent.doneFuture -import net.corda.core.internal.concurrent.flatMap -import net.corda.core.internal.concurrent.transpose -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.core.utilities.getX500Name +import net.corda.core.utilities.locality import net.corda.irs.api.NodeInterestRates import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.network.NetworkMapService import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.testing.DUMMY_MAP +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.DUMMY_REGULATOR import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork import net.corda.testing.node.TestClock import net.corda.testing.node.setTo import net.corda.testing.testNodeConfiguration -import org.bouncycastle.asn1.x500.X500Name import rx.Observable import rx.subjects.PublishSubject import java.math.BigInteger @@ -34,6 +30,9 @@ import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneOffset import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletableFuture.allOf +import java.util.concurrent.Future /** * Base class for network simulations that are based on the unit test / mock environment. @@ -58,7 +57,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, entropyRoot: BigInteger) : MockNetwork.MockNode(config, mockNet, networkMapAddress, advertisedServices, id, overrideServices, entropyRoot) { override fun findMyLocation(): WorldMapLocation? { - return configuration.myLegalName.locationOrNull?.let { CityDatabase[it] } + return configuration.myLegalName.locality.let { CityDatabase[it] } } } @@ -73,7 +72,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, val cfg = testNodeConfiguration( baseDirectory = config.baseDirectory, - myLegalName = X500Name("CN=Bank $letter,O=Bank $letter,L=$city,C=$country")) + myLegalName = getX500Name(O = "Bank $letter", L = city, C = country)) return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) } @@ -113,7 +112,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, object RatesOracleFactory : MockNetwork.Factory { // TODO: Make a more realistic legal name - val RATES_SERVICE_NAME = X500Name("CN=Rates Service Provider,O=R3,OU=corda,L=Madrid,C=ES") + val RATES_SERVICE_NAME = getX500Name(O = "Rates Service Provider", OU = "corda", L = "Madrid", C = "ES") override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, overrideServices: Map?, @@ -261,21 +260,19 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, } } - val networkInitialisationFinished = mockNet.nodes.map { it.networkMapRegistrationFuture }.transpose() + val networkInitialisationFinished = allOf(*mockNet.nodes.map { it.nodeReadyFuture.toCompletableFuture() }.toTypedArray()) - fun start(): CordaFuture { + fun start(): Future { mockNet.startNodes() // Wait for all the nodes to have finished registering with the network map service. - return networkInitialisationFinished.flatMap { startMainSimulation() } + return networkInitialisationFinished.thenCompose { startMainSimulation() } } /** * Sub-classes should override this to trigger whatever they want to simulate. This method will be invoked once the * network bringup has been simulated. */ - protected open fun startMainSimulation(): CordaFuture { - return doneFuture(Unit) - } + protected abstract fun startMainSimulation(): CompletableFuture fun stop() { mockNet.stopNodes() diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index d6b9266937..b2d3f3e341 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -23,8 +23,7 @@ dependencies { cordaCompile project(':core') cordaCompile project(':client:jfx') cordaCompile project(':client:rpc') - cordaCompile project(':test-utils') - cordaCompile project(':cordform-common') + cordaCompile project(':node-driver') } idea { 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 b57011a62d..d5ffa5aa65 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,21 +1,21 @@ package net.corda.notarydemo -import net.corda.core.internal.div -import net.corda.core.node.services.ServiceInfo -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 -import net.corda.node.utilities.ServiceIdentityGenerator -import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformContext +import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode +import net.corda.core.internal.div import net.corda.core.internal.stream import net.corda.core.internal.toTypedArray +import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.getX500Name +import net.corda.demorun.runNodes +import net.corda.demorun.util.* +import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.minCorrectReplicas -import org.bouncycastle.asn1.x500.X500Name +import net.corda.node.utilities.ServiceIdentityGenerator +import net.corda.testing.ALICE +import net.corda.testing.BOB fun main(args: Array) = BFTNotaryCordform.runNodes() @@ -23,7 +23,7 @@ private val clusterSize = 4 // Minimum size that tolerates a faulty replica. private val notaryNames = createNotaryNames(clusterSize) object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0]) { - private val clusterName = X500Name("CN=BFT,O=R3,OU=corda,L=Zurich,C=CH") + private val clusterName = getX500Name(O = "BFT", OU = "corda", L = "Zurich", C = "CH") private val advertisedService = ServiceInfo(BFTNonValidatingNotaryService.type, clusterName) init { 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 d648ce6d71..cd8485ce57 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,19 +1,16 @@ package net.corda.notarydemo import net.corda.client.rpc.CordaRPCClient -import net.corda.client.rpc.notUsed -import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.toStringShort import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.transactions.SignedTransaction -import net.corda.core.internal.concurrent.map -import net.corda.core.internal.concurrent.transpose import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.notarydemo.flows.DummyIssueAndMove import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient import net.corda.testing.BOB +import java.util.concurrent.Future import kotlin.streams.asSequence fun main(args: Array) { @@ -53,9 +50,10 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) { * as it consumes the original asset and creates a copy with the new owner as its output. */ private fun buildTransactions(count: Int): List { - return (1..count).map { + val flowFutures = (1..count).map { rpc.startFlow(::DummyIssueAndMove, notary, counterpartyNode.legalIdentity, it).returnValue - }.transpose().getOrThrow() + } + return flowFutures.map { it.getOrThrow() } } /** @@ -64,9 +62,9 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) { * * @return a list of encoded signer public keys - one for every transaction */ - private fun notariseTransactions(transactions: List): List>> { + private fun notariseTransactions(transactions: List): List>> { return transactions.map { - rpc.startFlow(::RPCStartableNotaryFlowClient, it).returnValue.map { it.map { it.by.toStringShort() } } + rpc.startFlow(::RPCStartableNotaryFlowClient, it).returnValue.toCompletableFuture().thenApply { it.map { it.by.toStringShort() } } } } } 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 f0f5279def..4cc4878137 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,30 +1,27 @@ package net.corda.notarydemo -import net.corda.core.crypto.appendToCommonName +import net.corda.cordform.CordformContext +import net.corda.cordform.CordformDefinition +import net.corda.cordform.CordformNode import net.corda.core.internal.div import net.corda.core.node.services.ServiceInfo -import net.corda.testing.ALICE -import net.corda.testing.BOB -import net.corda.testing.DUMMY_NOTARY +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.getX500Name +import net.corda.demorun.runNodes 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 +import net.corda.testing.ALICE +import net.corda.testing.BOB fun main(args: Array) = RaftNotaryCordform.runNodes() -internal fun createNotaryNames(clusterSize: Int) = (0 until clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(" $it") } +internal fun createNotaryNames(clusterSize: Int) = (0 until clusterSize).map { getX500Name(CN = "Notary Service $it", O = "R3 Ltd", OU = "corda", L = "Zurich", C = "CH") } private val notaryNames = createNotaryNames(3) object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0]) { - private val clusterName = X500Name("CN=Raft,O=R3,OU=corda,L=Zurich,C=CH") + private val clusterName = getX500Name(O = "Raft", OU = "corda", L = "Zurich", C = "CH") private val advertisedService = ServiceInfo(RaftValidatingNotaryService.type, clusterName) init { 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 dd7731e872..82e62ba997 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,9 @@ package net.corda.notarydemo.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.Amount +import net.corda.core.contracts.CommandData import net.corda.core.contracts.Contract import net.corda.core.contracts.ContractState -import net.corda.core.contracts.Issued import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC import net.corda.core.identity.AbstractParty @@ -12,8 +11,6 @@ import net.corda.core.identity.Party import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.finance.GBP -import net.corda.finance.contracts.asset.Cash @StartableByRPC class DummyIssueAndMove(private val notary: Party, private val counterpartyNode: Party, private val discriminator: Int) : FlowLogic() { @@ -21,23 +18,27 @@ class DummyIssueAndMove(private val notary: Party, private val counterpartyNode: override fun verify(tx: LedgerTransaction) {} } + data class DummyCommand(val dummy: Int = 0): CommandData + data class State(override val participants: List, private val discriminator: Int) : ContractState { override val contract = DoNothingContract } @Suspendable - override fun call() = serviceHub.run { + override fun call(): SignedTransaction { // Self issue an asset - val amount = Amount(1000000, Issued(myInfo.legalIdentity.ref(0), GBP)) - val issueTxBuilder = TransactionBuilder(notary = notary) - val signers = Cash().generateIssue(issueTxBuilder, amount, serviceHub.myInfo.legalIdentity, notary) - val issueTx = serviceHub.signInitialTransaction(issueTxBuilder, signers) + val state = State(listOf(serviceHub.myInfo.legalIdentity), discriminator) + val issueTx = serviceHub.signInitialTransaction(TransactionBuilder(notary).apply { + addOutputState(state) + addCommand(DummyCommand(),listOf(serviceHub.myInfo.legalIdentity.owningKey)) + }) serviceHub.recordTransactions(issueTx) // Move ownership of the asset to the counterparty - val moveTxBuilder = TransactionBuilder(notary = notary) - - val (_, keys) = Cash.generateSpend(serviceHub, moveTxBuilder, Amount(amount.quantity, GBP), counterpartyNode) // We don't check signatures because we know that the notary's signature is missing - signInitialTransaction(moveTxBuilder, keys) + return serviceHub.signInitialTransaction(TransactionBuilder(notary).apply { + addInputState(issueTx.tx.outRef(0)) + addOutputState(state.copy(participants = listOf(counterpartyNode))) + addCommand(DummyCommand(),listOf(serviceHub.myInfo.legalIdentity.owningKey)) + }) } } diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 7c39385cb5..16dd0242f7 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -56,36 +56,36 @@ dependencies { compile "com.opengamma.strata:strata-math:${strata_version}" // Test dependencies - testCompile project(':test-utils') + testCompile project(':node-driver') testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:${assertj_version}" } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH" + networkMap "O=Notary Service,OU=corda,L=Zurich,C=CH" node { - name "CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH" + name "O=Notary Service,OU=corda,L=Zurich,C=CH" advertisedServices = ["corda.notary.validating"] p2pPort 10002 cordapps = ["net.corda:finance:$corda_release_version"] } node { - name "CN=Bank A,O=Bank A,L=London,C=GB" + name "O=Bank A,L=London,C=GB" advertisedServices = [] p2pPort 10004 webPort 10005 cordapps = ["net.corda:finance:$corda_release_version"] } node { - name "CN=Bank B,O=Bank B,L=New York,C=US" + name "O=Bank B,L=New York,C=US" advertisedServices = [] p2pPort 10006 webPort 10007 cordapps = ["net.corda:finance:$corda_release_version"] } node { - name "CN=Bank C,O=Bank C,L=Tokyo,C=Japan" + name "O=Bank C,L=Tokyo,C=Japan" advertisedServices = [] p2pPort 10008 webPort 10009 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 b10b1ea541..444e1aa7a8 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 @@ -2,7 +2,6 @@ package net.corda.vega import com.opengamma.strata.product.common.BuySell import net.corda.core.node.services.ServiceInfo -import net.corda.core.internal.concurrent.transpose import net.corda.core.utilities.getOrThrow import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_B @@ -33,11 +32,14 @@ class SimmValuationTest : IntegrationTestCategory { @Test fun `runs SIMM valuation demo`() { driver(isDebug = true) { - startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow() - val (nodeA, nodeB) = listOf(startNode(nodeALegalName), startNode(nodeBLegalName)).transpose().getOrThrow() - val (nodeAApi, nodeBApi) = listOf(startWebserver(nodeA), startWebserver(nodeB)).transpose() - .getOrThrow() - .map { HttpApi.fromHostAndPort(it.listenAddress, "api/simmvaluationdemo") } + startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow() + val nodeAFuture = startNode(providedName = nodeALegalName) + val nodeBFuture = startNode(providedName = nodeBLegalName) + val (nodeA, nodeB) = listOf(nodeAFuture, nodeBFuture).map { it.getOrThrow() } + val nodeAWebServerFuture = startWebserver(nodeA) + val nodeBWebServerFuture = startWebserver(nodeB) + val nodeAApi = HttpApi.fromHostAndPort(nodeAWebServerFuture.getOrThrow().listenAddress, "api/simmvaluationdemo") + val nodeBApi = HttpApi.fromHostAndPort(nodeBWebServerFuture.getOrThrow().listenAddress, "api/simmvaluationdemo") val nodeBParty = getPartyWithName(nodeAApi, nodeBLegalName) val nodeAParty = getPartyWithName(nodeBApi, nodeALegalName) @@ -55,7 +57,7 @@ class SimmValuationTest : IntegrationTestCategory { } private fun getAvailablePartiesFor(partyApi: HttpApi): PortfolioApi.AvailableParties { - return partyApi.getJson("whoami") + return partyApi.getJson("whoami") } private fun createTradeBetween(partyApi: HttpApi, counterparty: PortfolioApi.ApiParty, tradeId: String): Boolean { diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/analytics/OGUtils.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/analytics/OGUtils.kt index ff48322f55..65c28f48d5 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/analytics/OGUtils.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/analytics/OGUtils.kt @@ -1,13 +1,7 @@ package net.corda.vega.analytics fun compareIMTriples(a: InitialMarginTriple, b: InitialMarginTriple): Boolean { - if (a.first is Double && a.second is Double && a.third is Double && - b.first is Double && b.second is Double && b.third is Double) { - if (withinTolerance(a.first, b.first) && withinTolerance(a.second, b.second) && withinTolerance(a.third, b.third)) { - return true - } - } - return false + return withinTolerance(a.first, b.first) && withinTolerance(a.second, b.second) && withinTolerance(a.third, b.third) } // TODO: Do this correctly 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 66dd387c22..a5bd412d7a 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 @@ -2,8 +2,8 @@ package net.corda.vega.api import com.opengamma.strata.basics.currency.MultiCurrencyAmount import net.corda.core.contracts.StateAndRef -import net.corda.core.crypto.parsePublicKeyBase58 -import net.corda.core.crypto.toBase58String +import net.corda.core.utilities.parsePublicKeyBase58 +import net.corda.core.utilities.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt index fb13e86d76..f4d4ad5bdc 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt @@ -5,11 +5,11 @@ import com.opengamma.strata.product.swap.IborRateCalculation import com.opengamma.strata.product.swap.RateCalculationSwapLeg import com.opengamma.strata.product.swap.SwapLegType import net.corda.core.contracts.hash -import net.corda.core.crypto.commonName import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.crypto.toBase58String import net.corda.core.messaging.CordaRPCOps +import net.corda.core.utilities.organisation +import net.corda.core.utilities.toBase58String import net.corda.vega.contracts.IRSState import net.corda.vega.contracts.PortfolioState import net.corda.vega.portfolio.Portfolio @@ -136,7 +136,7 @@ class PortfolioApiUtils(private val ownParty: Party) { return TradeView( fixedLeg = mapOf( - "fixedRatePayer" to (fixedRatePayer.nameOrNull()?.commonName ?: fixedRatePayer.owningKey.toBase58String()), + "fixedRatePayer" to (fixedRatePayer.nameOrNull()?.organisation ?: fixedRatePayer.owningKey.toBase58String()), "notional" to mapOf( "token" to fixedLeg.currency.code, "quantity" to fixedLeg.notionalSchedule.amount.initialValue @@ -152,7 +152,7 @@ class PortfolioApiUtils(private val ownParty: Party) { "paymentCalendar" to mapOf() // TODO ), floatingLeg = mapOf( - "floatingRatePayer" to (floatingRatePayer.nameOrNull()?.commonName ?: floatingRatePayer.owningKey.toBase58String()), + "floatingRatePayer" to (floatingRatePayer.nameOrNull()?.organisation ?: floatingRatePayer.owningKey.toBase58String()), "notional" to mapOf( "token" to floatingLeg.currency.code, "quantity" to floatingLeg.notionalSchedule.amount.initialValue diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/IRSState.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/IRSState.kt index a7b02c55a0..db1eff6755 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/IRSState.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/IRSState.kt @@ -22,10 +22,6 @@ data class IRSState(val swap: SwapData, val ref: String get() = linearId.externalId!! // Same as the constructor for UniqueIdentified override val participants: List get() = listOf(buyer, seller) - override fun isRelevant(ourKeys: Set): Boolean { - return participants.flatMap { it.owningKey.keys }.intersect(ourKeys).isNotEmpty() - } - override fun generateAgreement(notary: Party): TransactionBuilder { val state = IRSState(swap, buyer, seller, OGTrade()) return TransactionBuilder(notary).withItems(state, Command(OGTrade.Commands.Agree(), participants.map { it.owningKey })) diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt index ee351737c5..02b1367d14 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt @@ -37,10 +37,6 @@ data class PortfolioState(val portfolio: List, return ScheduledActivity(flow, LocalDate.now().plus(1, ChronoUnit.DAYS).atStartOfDay().toInstant(ZoneOffset.UTC)) } - override fun isRelevant(ourKeys: Set): Boolean { - return participants.flatMap { it.owningKey.keys }.intersect(ourKeys).isNotEmpty() - } - override fun generateAgreement(notary: Party): TransactionBuilder { return TransactionBuilder(notary).withItems(copy(), Command(PortfolioSwap.Commands.Agree(), participants.map { it.owningKey })) } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/SwapData.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/SwapData.kt index 09e5d9cbf4..c27a6d4ef0 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/SwapData.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/SwapData.kt @@ -10,7 +10,7 @@ import com.opengamma.strata.product.swap.SwapTrade import com.opengamma.strata.product.swap.type.FixedIborSwapConvention import com.opengamma.strata.product.swap.type.FixedIborSwapConventions import net.corda.core.identity.AbstractParty -import net.corda.core.crypto.toBase58String +import net.corda.core.utilities.toBase58String import net.corda.core.serialization.CordaSerializable import java.math.BigDecimal import java.security.PublicKey diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt index ad8c949331..d167e347ae 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt @@ -40,8 +40,7 @@ object IRSTradeFlow { return subFlow(TwoPartyDealFlow.Instigator( otherParty, - TwoPartyDealFlow.AutoOffer(notary, offer), - serviceHub.legalIdentityKey)) + TwoPartyDealFlow.AutoOffer(notary, offer))) } } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt index 31acaa837d..e8565f65ec 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt @@ -291,8 +291,7 @@ object SimmFlow { val seller = TwoPartyDealFlow.Instigator( replyToParty, - TwoPartyDealFlow.AutoOffer(offer.notary, offer.dealBeingOffered), - serviceHub.legalIdentityKey) + TwoPartyDealFlow.AutoOffer(offer.notary, offer.dealBeingOffered)) logger.info("Starting two party deal initiator with: ${replyToParty.name}") return subFlow(seller) } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt index b965550b9a..2cbd235203 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPluginRegistry.kt @@ -26,18 +26,18 @@ class SimmPluginRegistry : CordaPluginRegistry() { override fun customizeSerialization(custom: SerializationCustomization): Boolean { custom.apply { // OpenGamma classes. - addToWhitelist(MultiCurrencyAmount::class.java) - addToWhitelist(Ordering.natural>().javaClass) - addToWhitelist(CurrencyAmount::class.java) - addToWhitelist(Currency::class.java) - addToWhitelist(InitialMarginTriple::class.java) - addToWhitelist(CordaMarketData::class.java) - addToWhitelist(CurrencyParameterSensitivities::class.java) - addToWhitelist(CurrencyParameterSensitivity::class.java) - addToWhitelist(DoubleArray::class.java) - addToWhitelist(CurveName::class.java) - addToWhitelist(TenorDateParameterMetadata::class.java) - addToWhitelist(Tenor::class.java) + addToWhitelist(MultiCurrencyAmount::class.java, + Ordering.natural>().javaClass, + CurrencyAmount::class.java, + Currency::class.java, + InitialMarginTriple::class.java, + CordaMarketData::class.java, + CurrencyParameterSensitivities::class.java, + CurrencyParameterSensitivity::class.java, + DoubleArray::class.java, + CurveName::class.java, + TenorDateParameterMetadata::class.java, + Tenor::class.java) } return true } diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js index 9ac11fba23..793a319f1b 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js @@ -1,7 +1,7 @@ "use strict"; var FixedLegViewModel = (function () { function FixedLegViewModel() { - this.fixedRatePayer = "CN=Bank A,O=Bank A,L=London,C=GB"; + this.fixedRatePayer = "O=Bank A,L=London,C=GB"; this.notional = { quantity: 2500000000 }; diff --git a/samples/simm-valuation-demo/src/main/web/src/app/viewmodel/FixedLegViewModel.ts b/samples/simm-valuation-demo/src/main/web/src/app/viewmodel/FixedLegViewModel.ts index c088492179..7f02629e3c 100644 --- a/samples/simm-valuation-demo/src/main/web/src/app/viewmodel/FixedLegViewModel.ts +++ b/samples/simm-valuation-demo/src/main/web/src/app/viewmodel/FixedLegViewModel.ts @@ -1,7 +1,7 @@ export class FixedLegViewModel { constructor() { } - fixedRatePayer = "CN=Bank A,O=Bank A,L=London,C=GB"; + fixedRatePayer = "O=Bank A,L=London,C=GB"; notional: Object = { quantity: 2500000000 }; 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 2ad314e6c6..c31048985d 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 @@ -1,6 +1,5 @@ package net.corda.vega -import net.corda.core.internal.concurrent.transpose import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.getOrThrow import net.corda.testing.DUMMY_BANK_A @@ -17,12 +16,11 @@ import net.corda.testing.driver.driver */ fun main(args: Array) { driver(dsl = { - startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) - val (nodeA, nodeB, nodeC) = listOf( - startNode(DUMMY_BANK_A.name), - startNode(DUMMY_BANK_B.name), - startNode(DUMMY_BANK_C.name) - ).transpose().getOrThrow() + val notaryFuture = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + val nodeAFuture = startNode(providedName = DUMMY_BANK_A.name) + val nodeBFuture = startNode(providedName = DUMMY_BANK_B.name) + val nodeCFuture = startNode(providedName = DUMMY_BANK_C.name) + val (nodeA, nodeB, nodeC) = listOf(nodeAFuture, nodeBFuture, nodeCFuture, notaryFuture).map { it.getOrThrow() } startWebserver(nodeA) startWebserver(nodeB) diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index 36d77c5797..36928a8807 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -51,15 +51,15 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" // This name "Notary" is hard-coded into TraderDemoClientApi so if you change it here, change it there too. // In this demo the node that runs a standalone notary also acts as the network map server. - networkMap "CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH" + networkMap "O=Notary Service,OU=corda,L=Zurich,C=CH" node { - name "CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH" + name "O=Notary Service,OU=corda,L=Zurich,C=CH" advertisedServices = ["corda.notary.validating"] p2pPort 10002 cordapps = ["net.corda:finance:$corda_release_version"] } node { - name "CN=Bank A,O=Bank A,L=London,C=GB" + name "O=Bank A,L=London,C=GB" advertisedServices = [] p2pPort 10005 rpcPort 10006 @@ -67,7 +67,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { rpcUsers = ext.rpcUsers } node { - name "CN=Bank B,O=Bank B,L=New York,C=US" + name "O=Bank B,L=New York,C=US" advertisedServices = [] p2pPort 10008 rpcPort 10009 @@ -75,7 +75,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { rpcUsers = ext.rpcUsers } node { - name "CN=BankOfCorda,O=R3,L=New York,C=US" + name "O=BankOfCorda,L=New York,C=US" advertisedServices = [] p2pPort 10011 rpcPort 10012 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 7db63b4f77..778554688b 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 @@ -1,7 +1,6 @@ package net.corda.traderdemo import net.corda.client.rpc.CordaRPCClient -import net.corda.core.internal.concurrent.transpose import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.millis @@ -32,12 +31,11 @@ class TraderDemoTest : NodeBasedTest() { startFlowPermission(), startFlowPermission(), startFlowPermission())) - val (nodeA, nodeB, bankNode) = listOf( - startNode(DUMMY_BANK_A.name, rpcUsers = listOf(demoUser)), - startNode(DUMMY_BANK_B.name, rpcUsers = listOf(demoUser)), - startNode(BOC.name, rpcUsers = listOf(bankUser)), - startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) - ).transpose().getOrThrow() + val notaryFuture = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + val nodeAFuture = startNode(DUMMY_BANK_A.name, rpcUsers = listOf(demoUser)) + val nodeBFuture = startNode(DUMMY_BANK_B.name, rpcUsers = listOf(demoUser)) + val bankNodeFuture = startNode(BOC.name, rpcUsers = listOf(bankUser)) + val (nodeA, nodeB, bankNode) = listOf(nodeAFuture, nodeBFuture, bankNodeFuture, notaryFuture).map { it.getOrThrow() } nodeA.registerInitiatedFlow(BuyerFlow::class.java) 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 a249b87339..9656d93227 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 @@ -6,7 +6,6 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic 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.node.NodeInfo import net.corda.core.transactions.SignedTransaction @@ -42,10 +41,9 @@ class SellerFlow(val otherParty: Party, progressTracker.currentStep = SELF_ISSUING val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0] - val cpOwnerKey = serviceHub.keyManagementService.freshKey() + val cpOwner = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, false) val commercialPaper = serviceHub.vaultQueryService.queryBy(CommercialPaper.State::class.java).states.first() - progressTracker.currentStep = TRADING // Send the offered amount. @@ -55,7 +53,7 @@ class SellerFlow(val otherParty: Party, notary, commercialPaper, amount, - AnonymousParty(cpOwnerKey), + cpOwner, progressTracker.getChildProgressTracker(TRADING)!!) return subFlow(seller) } 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 1661b70ad7..7ed7f689cd 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 @@ -27,10 +27,10 @@ fun main(args: Array) { val user = User("user1", "test", permissions = setOf(startFlowPermission(), startFlowPermission(), startFlowPermission())) - startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) - startNode(DUMMY_BANK_A.name, rpcUsers = demoUser) - startNode(DUMMY_BANK_B.name, rpcUsers = demoUser) - startNode(BOC.name, rpcUsers = listOf(user)) + startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) + startNode(providedName = DUMMY_BANK_A.name, rpcUsers = demoUser) + startNode(providedName = DUMMY_BANK_B.name, rpcUsers = demoUser) + startNode(providedName = BOC.name, rpcUsers = listOf(user)) waitForAllNodesToFinish() } } diff --git a/settings.gradle b/settings.gradle index 215e3aac52..08166fb237 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,6 +22,11 @@ include 'verifier' include 'test-common' include 'test-utils' include 'smoke-test-utils' +include 'node-driver' +// Avoid making 'testing' a project, and allow build.gradle files to refer to these by their simple names: +['test-common', 'test-utils', 'smoke-test-utils', 'node-driver'].each { + project(":$it").projectDir = new File("$settingsDir/testing/$it") +} include 'tools:explorer' include 'tools:explorer:capsule' include 'tools:demobench' diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt b/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt deleted file mode 100644 index 319dba7157..0000000000 --- a/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt +++ /dev/null @@ -1,131 +0,0 @@ -@file:JvmName("TestConstants") - -package net.corda.testing - -import net.corda.core.contracts.Command -import net.corda.core.contracts.TypeOnlyCommandData -import net.corda.core.crypto.CertificateAndKeyPair -import net.corda.core.crypto.entropyToKeyPair -import net.corda.core.crypto.generateKeyPair -import net.corda.core.identity.Party -import net.corda.core.identity.PartyAndCertificate -import net.corda.core.internal.concurrent.transpose -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.node.services.ServiceInfo -import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.node.utilities.X509Utilities -import net.corda.nodeapi.User -import net.corda.testing.driver.DriverDSLExposedInterface -import org.bouncycastle.asn1.x500.X500Name -import java.math.BigInteger -import java.security.KeyPair -import java.security.PublicKey -import java.time.Instant - -// A dummy time at which we will be pretending test transactions are created. -val TEST_TX_TIME: Instant get() = Instant.parse("2015-04-17T12:00:00.00Z") - -val DUMMY_KEY_1: KeyPair by lazy { generateKeyPair() } -val DUMMY_KEY_2: KeyPair by lazy { generateKeyPair() } - -val DUMMY_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) } -/** Dummy notary identity for tests and simulations */ -val DUMMY_NOTARY_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_NOTARY) -val DUMMY_NOTARY: Party get() = Party(X500Name("CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), DUMMY_NOTARY_KEY.public) - -val DUMMY_MAP_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(30)) } -/** Dummy network map service identity for tests and simulations */ -val DUMMY_MAP: Party get() = Party(X500Name("CN=Network Map Service,O=R3,OU=corda,L=Amsterdam,C=NL"), DUMMY_MAP_KEY.public) - -val DUMMY_BANK_A_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(40)) } -/** Dummy bank identity for tests and simulations */ -val DUMMY_BANK_A: Party get() = Party(X500Name("CN=Bank A,O=Bank A,L=London,C=GB"), DUMMY_BANK_A_KEY.public) - -val DUMMY_BANK_B_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(50)) } -/** Dummy bank identity for tests and simulations */ -val DUMMY_BANK_B: Party get() = Party(X500Name("CN=Bank B,O=Bank B,L=New York,C=US"), DUMMY_BANK_B_KEY.public) - -val DUMMY_BANK_C_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(60)) } -/** Dummy bank identity for tests and simulations */ -val DUMMY_BANK_C: Party get() = Party(X500Name("CN=Bank C,O=Bank C,L=Tokyo,C=JP"), DUMMY_BANK_C_KEY.public) - -val ALICE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(70)) } -/** Dummy individual identity for tests and simulations */ -val ALICE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(ALICE) -val ALICE: Party get() = Party(X500Name("CN=Alice Corp,O=Alice Corp,L=Madrid,C=ES"), ALICE_KEY.public) - -val BOB_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(80)) } -/** Dummy individual identity for tests and simulations */ -val BOB_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(BOB) -val BOB: Party get() = Party(X500Name("CN=Bob Plc,O=Bob Plc,L=Rome,C=IT"), BOB_KEY.public) - -val CHARLIE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(90)) } -/** Dummy individual identity for tests and simulations */ -val CHARLIE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CHARLIE) -val CHARLIE: Party get() = Party(X500Name("CN=Charlie Ltd,O=Charlie Ltd,L=Athens,C=GR"), CHARLIE_KEY.public) - -val DUMMY_REGULATOR_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(100)) } -/** Dummy regulator for tests and simulations */ -val DUMMY_REGULATOR: Party get() = Party(X500Name("CN=Regulator A,OU=Corda,O=AMF,L=Paris,C=FR"), DUMMY_REGULATOR_KEY.public) - -val DUMMY_CA_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(110)) } -val DUMMY_CA: CertificateAndKeyPair by lazy { - // TODO: Should be identity scheme - val cert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Dummy CA,OU=Corda,O=R3 Ltd,L=London,C=GB"), DUMMY_CA_KEY) - CertificateAndKeyPair(cert, DUMMY_CA_KEY) -} - -fun dummyCommand(vararg signers: PublicKey = arrayOf(generateKeyPair().public) ) = Command(DummyCommandData, signers.toList()) - -object DummyCommandData : TypeOnlyCommandData() - -val DUMMY_IDENTITY_1: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_PARTY) -val DUMMY_PARTY: Party get() = Party(X500Name("CN=Dummy,O=Dummy,L=Madrid,C=ES"), DUMMY_KEY_1.public) - -// -// Extensions to the Driver DSL to auto-manufacture nodes by name. -// - -/** - * A simple wrapper for objects provided by the integration test driver DSL. The fields are lazy so - * node construction won't start until you access the members. You can get one of these from the - * [alice], [bob] and [aliceBobAndNotary] functions. - */ -class PredefinedTestNode internal constructor(party: Party, driver: DriverDSLExposedInterface, services: Set) { - val rpcUsers = listOf(User("admin", "admin", setOf("ALL"))) // TODO: Randomize? - val nodeFuture by lazy { driver.startNode(party.name, rpcUsers = rpcUsers, advertisedServices = services) } - val node by lazy { nodeFuture.get()!! } - val rpc by lazy { node.rpcClientToNode() } - - fun useRPC(block: (CordaRPCOps) -> R) = rpc.use(rpcUsers[0].username, rpcUsers[0].password) { block(it.proxy) } -} - -// TODO: Probably we should inject the above keys through the driver to make the nodes use it, rather than have the warnings below. - -/** - * Returns a plain, entirely stock node pre-configured with the [ALICE] identity. Note that a random key will be generated - * for it: you won't have [ALICE_KEY]. - */ -fun DriverDSLExposedInterface.alice(): PredefinedTestNode = PredefinedTestNode(ALICE, this, emptySet()) -/** - * Returns a plain, entirely stock node pre-configured with the [BOB] identity. Note that a random key will be generated - * for it: you won't have [BOB_KEY]. - */ -fun DriverDSLExposedInterface.bob(): PredefinedTestNode = PredefinedTestNode(BOB, this, emptySet()) -/** - * Returns a plain single node notary pre-configured with the [DUMMY_NOTARY] identity. Note that a random key will be generated - * for it: you won't have [DUMMY_NOTARY_KEY]. - */ -fun DriverDSLExposedInterface.notary(): PredefinedTestNode = PredefinedTestNode(DUMMY_NOTARY, this, setOf(ServiceInfo(ValidatingNotaryService.type))) - -/** - * Returns plain, entirely stock nodes pre-configured with the [ALICE], [BOB] and [DUMMY_NOTARY] X.500 names in that - * order. They have been started up in parallel and are now ready to use. - */ -fun DriverDSLExposedInterface.aliceBobAndNotary(): List { - val alice = alice() - val bob = bob() - val notary = notary() - listOf(alice.nodeFuture, bob.nodeFuture, notary.nodeFuture).transpose().get() - return listOf(alice, bob, notary) -} \ No newline at end of file diff --git a/testing/node-driver/build.gradle b/testing/node-driver/build.gradle new file mode 100644 index 0000000000..2196330ae4 --- /dev/null +++ b/testing/node-driver/build.gradle @@ -0,0 +1,49 @@ +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' + +//noinspection GroovyAssignabilityCheck +configurations { + // we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment + runtime.exclude module: 'isolated' + + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +sourceSets { + integrationTest { + kotlin { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/kotlin') + } + resources { + srcDir file('src/integration-test/resources') + } + } +} + +dependencies { + compile project(':test-utils') + compile project(':cordform-common') + + // Integration test helpers + integrationTestCompile "org.assertj:assertj-core:${assertj_version}" + integrationTestCompile "junit:junit:$junit_version" +} + +task integrationTest(type: Test) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath +} + +jar { + baseName 'corda-node-driver' +} + +publish { + name jar.baseName +} diff --git a/test-utils/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt similarity index 99% rename from test-utils/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt rename to testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt index b1a1dba4fc..0aa4bec5e4 100644 --- a/test-utils/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt @@ -1,6 +1,7 @@ package net.corda.testing import co.paralleluniverse.fibers.Suspendable +import net.corda.client.jackson.JacksonSupport import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowStackSnapshot import net.corda.core.flows.StartableByRPC @@ -10,7 +11,6 @@ import net.corda.core.internal.list import net.corda.core.internal.read import net.corda.core.messaging.startFlow import net.corda.core.serialization.CordaSerializable -import net.corda.jackson.JacksonSupport import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.nodeapi.User import net.corda.testing.driver.driver @@ -34,7 +34,7 @@ fun convertToStackSnapshotFrames(snapshot: FlowStackSnapshot): List lines.anyMatch { line -> line.startsWith("[DEBUG]") } } assertThat(debugLinesPresent).isTrue() } } - } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt b/testing/node-driver/src/main/kotlin/net/corda/demorun/DemoRunner.kt similarity index 100% rename from samples/notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt rename to testing/node-driver/src/main/kotlin/net/corda/demorun/DemoRunner.kt diff --git a/samples/notary-demo/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt similarity index 100% rename from samples/notary-demo/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt rename to testing/node-driver/src/main/kotlin/net/corda/demorun/util/DemoUtils.kt diff --git a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt b/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt similarity index 92% rename from node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt rename to testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt index 1994ed04c8..d8ce9c23f7 100644 --- a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/node/testing/MockServiceHubInternal.kt @@ -1,4 +1,4 @@ -package net.corda.node.services +package net.corda.node.testing import com.codahale.metrics.MetricRegistry import net.corda.core.flows.FlowInitiator @@ -42,6 +42,7 @@ open class MockServiceHubInternal( val scheduler: SchedulerService? = null, val overrideClock: Clock? = NodeClock(), val schemas: SchemaService? = NodeSchemaService(), + val customContractUpgradeService: ContractUpgradeService? = null, val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2) ) : ServiceHubInternal { override val vaultQueryService: VaultQueryService @@ -50,6 +51,8 @@ open class MockServiceHubInternal( get() = customTransactionVerifierService ?: throw UnsupportedOperationException() override val vaultService: VaultService get() = customVault ?: throw UnsupportedOperationException() + override val contractUpgradeService: ContractUpgradeService + get() = customContractUpgradeService ?: throw UnsupportedOperationException() override val keyManagementService: KeyManagementService get() = keyManagement ?: throw UnsupportedOperationException() override val identityService: IdentityService @@ -63,7 +66,7 @@ open class MockServiceHubInternal( override val clock: Clock get() = overrideClock ?: throw UnsupportedOperationException() override val myInfo: NodeInfo - get() = NodeInfo(listOf(MOCK_HOST_AND_PORT), DUMMY_IDENTITY_1, NonEmptySet.of(DUMMY_IDENTITY_1), 1) // Required to get a dummy platformVersion when required for tests. + get() = NodeInfo(listOf(MOCK_HOST_AND_PORT), DUMMY_IDENTITY_1, NonEmptySet.of(DUMMY_IDENTITY_1), 1, serial = 1L) // Required to get a dummy platformVersion when required for tests. override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) override val rpcFlows: List>> get() = throw UnsupportedOperationException() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt new file mode 100644 index 0000000000..709f48da8a --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/DriverConstants.kt @@ -0,0 +1,59 @@ +@file:JvmName("DriverConstants") + +package net.corda.testing + +import net.corda.core.identity.Party +import net.corda.core.internal.concurrent.transpose +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.node.services.ServiceInfo +import net.corda.node.services.transactions.ValidatingNotaryService +import net.corda.nodeapi.User +import net.corda.testing.driver.DriverDSLExposedInterface + +// +// Extensions to the Driver DSL to auto-manufacture nodes by name. +// + +/** + * A simple wrapper for objects provided by the integration test driver DSL. The fields are lazy so + * node construction won't start until you access the members. You can get one of these from the + * [alice], [bob] and [aliceBobAndNotary] functions. + */ +class PredefinedTestNode internal constructor(party: Party, driver: DriverDSLExposedInterface, services: Set) { + val rpcUsers = listOf(User("admin", "admin", setOf("ALL"))) // TODO: Randomize? + val nodeFuture by lazy { driver.startNode(providedName = party.name, rpcUsers = rpcUsers, advertisedServices = services) } + val node by lazy { nodeFuture.get()!! } + val rpc by lazy { node.rpcClientToNode() } + + fun useRPC(block: (CordaRPCOps) -> R) = rpc.use(rpcUsers[0].username, rpcUsers[0].password) { block(it.proxy) } +} + +// TODO: Probably we should inject the above keys through the driver to make the nodes use it, rather than have the warnings below. + +/** + * Returns a plain, entirely stock node pre-configured with the [ALICE] identity. Note that a random key will be generated + * for it: you won't have [ALICE_KEY]. + */ +fun DriverDSLExposedInterface.alice(): PredefinedTestNode = PredefinedTestNode(ALICE, this, emptySet()) +/** + * Returns a plain, entirely stock node pre-configured with the [BOB] identity. Note that a random key will be generated + * for it: you won't have [BOB_KEY]. + */ +fun DriverDSLExposedInterface.bob(): PredefinedTestNode = PredefinedTestNode(BOB, this, emptySet()) +/** + * Returns a plain single node notary pre-configured with the [DUMMY_NOTARY] identity. Note that a random key will be generated + * for it: you won't have [DUMMY_NOTARY_KEY]. + */ +fun DriverDSLExposedInterface.notary(): PredefinedTestNode = PredefinedTestNode(DUMMY_NOTARY, this, setOf(ServiceInfo(ValidatingNotaryService.type))) + +/** + * Returns plain, entirely stock nodes pre-configured with the [ALICE], [BOB] and [DUMMY_NOTARY] X.500 names in that + * order. They have been started up in parallel and are now ready to use. + */ +fun DriverDSLExposedInterface.aliceBobAndNotary(): List { + val alice = alice() + val bob = bob() + val notary = notary() + listOf(alice.nodeFuture, bob.nodeFuture, notary.nodeFuture).transpose().get() + return listOf(alice, bob, notary) +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt new file mode 100644 index 0000000000..daded934cb --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt @@ -0,0 +1,74 @@ +@file:JvmName("NodeTestUtils") + +package net.corda.testing + +import com.nhaarman.mockito_kotlin.spy +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.node.ServiceHub +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.commonName +import net.corda.core.utilities.organisation +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.VerifierType +import net.corda.testing.node.MockServices +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties +import org.bouncycastle.asn1.x500.X500Name +import java.net.URL +import java.nio.file.Path + +/** + * Creates and tests a ledger built by the passed in dsl. The provided services can be customised, otherwise a default + * of a freshly built [MockServices] is used. + */ +@JvmOverloads fun ledger( + services: ServiceHub = MockServices(), + initialiseSerialization: Boolean = true, + dsl: LedgerDSL.() -> Unit +): LedgerDSL { + if (initialiseSerialization) initialiseTestSerialization() + try { + val ledgerDsl = LedgerDSL(TestLedgerDSLInterpreter(services)) + dsl(ledgerDsl) + return ledgerDsl + } finally { + if (initialiseSerialization) resetTestSerialization() + } +} + +/** + * Creates a ledger with a single transaction, built by the passed in dsl. + * + * @see LedgerDSLInterpreter._transaction + */ +@JvmOverloads fun transaction( + transactionLabel: String? = null, + transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY), + initialiseSerialization: Boolean = true, + dsl: TransactionDSL.() -> EnforceVerifyOrFail +) = ledger(initialiseSerialization = initialiseSerialization) { + dsl(TransactionDSL(TestTransactionDSLInterpreter(this.interpreter, transactionBuilder))) +} + +fun testNodeConfiguration( + baseDirectory: Path, + myLegalName: X500Name): NodeConfiguration { + abstract class MockableNodeConfiguration : NodeConfiguration // Otherwise Mockito is defeated by val getters. + val nc = spy() + whenever(nc.baseDirectory).thenReturn(baseDirectory) + whenever(nc.myLegalName).thenReturn(myLegalName) + whenever(nc.minimumPlatformVersion).thenReturn(1) + whenever(nc.keyStorePassword).thenReturn("cordacadevpass") + whenever(nc.trustStorePassword).thenReturn("trustpass") + whenever(nc.rpcUsers).thenReturn(emptyList()) + whenever(nc.dataSourceProperties).thenReturn(makeTestDataSourceProperties(myLegalName.organisation)) + whenever(nc.database).thenReturn(makeTestDatabaseProperties()) + whenever(nc.emailAddress).thenReturn("") + whenever(nc.exportJMXto).thenReturn("") + whenever(nc.devMode).thenReturn(true) + whenever(nc.certificateSigningService).thenReturn(URL("http://localhost")) + whenever(nc.certificateChainCheckPolicies).thenReturn(emptyList()) + whenever(nc.verifierType).thenReturn(VerifierType.InMemory) + whenever(nc.messageRedeliveryDelaySeconds).thenReturn(5) + return nc +} diff --git a/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/RPCDriver.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt similarity index 78% rename from test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index ab849e220f..a704ad5646 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -11,9 +11,6 @@ import net.corda.cordform.CordformNode import net.corda.cordform.NodeDefinition import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf -import net.corda.core.crypto.appendToCommonName -import net.corda.core.crypto.commonName -import net.corda.core.crypto.getX509Name import net.corda.core.identity.Party import net.corda.core.internal.ThreadBox import net.corda.core.internal.concurrent.* @@ -36,7 +33,7 @@ import net.corda.nodeapi.config.SSLConfiguration import net.corda.nodeapi.config.parseAs import net.corda.nodeapi.internal.addShutdownHook import net.corda.testing.* -import net.corda.testing.node.MOCK_VERSION_INFO +import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import okhttp3.OkHttpClient import okhttp3.Request import org.bouncycastle.asn1.x500.X500Name @@ -78,6 +75,8 @@ interface DriverDSLExposedInterface : CordformContext { /** * Starts a [net.corda.node.internal.Node] in a separate process. * + * @param defaultParameters The default parameters for the node. Allows the node to be configured in builder style + * when called from Java code. * @param providedName Optional name of the node, which will be its legal name in [Party]. Defaults to something * random. Note that this must be unique as the driver uses it as a primary key! * @param advertisedServices The set of services to be advertised by the node. Defaults to empty set. @@ -87,12 +86,25 @@ interface DriverDSLExposedInterface : CordformContext { * in. If null the Driver-level value will be used. * @return The [NodeInfo] of the started up node retrieved from the network map service. */ - fun startNode(providedName: X500Name? = null, - advertisedServices: Set = emptySet(), - rpcUsers: List = emptyList(), - verifierType: VerifierType = VerifierType.InMemory, - customOverrides: Map = emptyMap(), - startInSameProcess: Boolean? = null): CordaFuture + fun startNode( + defaultParameters: NodeParameters = NodeParameters(), + providedName: X500Name? = defaultParameters.providedName, + advertisedServices: Set = defaultParameters.advertisedServices, + rpcUsers: List = defaultParameters.rpcUsers, + verifierType: VerifierType = defaultParameters.verifierType, + customOverrides: Map = defaultParameters.customOverrides, + startInSameProcess: Boolean? = defaultParameters.startInSameProcess): CordaFuture + + /** + * Helper function for starting a [node] with custom parameters from Java. + * + * @param defaultParameters The default parameters for the driver. + * @param dsl The dsl itself. + * @return The value returned in the [dsl] closure. + */ + fun startNode(parameters: NodeParameters): CordaFuture { + return startNode(defaultParameters = parameters) + } fun startNodes( nodes: List, @@ -146,6 +158,7 @@ interface DriverDSLExposedInterface : CordformContext { * @return A future that completes with the non-null value [check] has returned. */ fun pollUntilNonNull(pollName: String, pollInterval: Duration = 500.millis, warnCount: Int = 120, check: () -> A?): CordaFuture + /** * Polls the given function until it returns true. * @see pollUntilNonNull @@ -175,7 +188,15 @@ sealed class NodeHandle { override val webAddress: NetworkHostAndPort, val debugPort: Int?, val process: Process - ) : NodeHandle() + ) : NodeHandle() { + override fun stop(): CordaFuture { + with(process) { + destroy() + waitFor() + } + return doneFuture(Unit) + } + } data class InProcess( override val nodeInfo: NodeInfo, @@ -184,9 +205,23 @@ sealed class NodeHandle { override val webAddress: NetworkHostAndPort, val node: Node, val nodeThread: Thread - ) : NodeHandle() + ) : NodeHandle() { + override fun stop(): CordaFuture { + node.stop() + with(nodeThread) { + interrupt() + join() + } + return doneFuture(Unit) + } + } fun rpcClientToNode(): CordaRPCClient = CordaRPCClient(configuration.rpcAddress!!, initialiseSerialization = false) + + /** + * Stops the referenced node. + */ + abstract fun stop(): CordaFuture } data class WebserverHandle( @@ -213,20 +248,42 @@ sealed class PortAllocation { } } +/** + * Helper builder for configuring a [node] from Java. + */ +data class NodeParameters( + val providedName: X500Name? = null, + val advertisedServices: Set = emptySet(), + val rpcUsers: List = emptyList(), + val verifierType: VerifierType = VerifierType.InMemory, + val customOverrides: Map = emptyMap(), + val startInSameProcess: Boolean? = null +) { + fun setProvidedName(providedName: X500Name?) = copy(providedName = providedName) + fun setAdvertisedServices(advertisedServices: Set) = copy(advertisedServices = advertisedServices) + fun setRpcUsers(rpcUsers: List) = copy(rpcUsers = rpcUsers) + fun setVerifierType(verifierType: VerifierType) = copy(verifierType = verifierType) + fun setCustomerOverrides(customOverrides: Map) = copy(customOverrides = customOverrides) + fun setStartInSameProcess(startInSameProcess: Boolean?) = copy(startInSameProcess = startInSameProcess) +} + /** * [driver] allows one to start up nodes like this: * driver { - * val noService = startNode(DUMMY_BANK_A.name) - * val notary = startNode(DUMMY_NOTARY.name) + * val noService = startNode(providedName = DUMMY_BANK_A.name) + * val notary = startNode(providedName = DUMMY_NOTARY.name) * * (...) * } * * Note that [DriverDSL.startNode] does not wait for the node to start up synchronously, but rather returns a [CordaFuture] - * of the [NodeInfo] that may be waited on, which completes when the new node registered with the network map service. + * of the [NodeInfo] that may be waited on, which completes when the new node registered with the network map service or + * loaded node data from database. * * The driver implicitly bootstraps a [NetworkMapService]. * + * @param defaultParameters The default parameters for the driver. Allows the driver to be configured in builder style + * when called from Java code. * @param isDebug Indicates whether the spawned nodes should start in jdwt debug mode and have debug level logging. * @param driverDirectory The base directory node directories go into, defaults to "build//". The node * directories themselves are "//", where legalName defaults to "-" @@ -241,33 +298,74 @@ sealed class PortAllocation { * @param dsl The dsl itself. * @return The value returned in the [dsl] closure. */ -@JvmOverloads fun driver( - isDebug: Boolean = false, - driverDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()), - portAllocation: PortAllocation = PortAllocation.Incremental(10000), - debugPortAllocation: PortAllocation = PortAllocation.Incremental(5005), - systemProperties: Map = emptyMap(), - useTestClock: Boolean = false, - initialiseSerialization: Boolean = true, - networkMapStartStrategy: NetworkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = true), - startNodesInProcess: Boolean = false, + defaultParameters: DriverParameters = DriverParameters(), + isDebug: Boolean = defaultParameters.isDebug, + driverDirectory: Path = defaultParameters.driverDirectory, + portAllocation: PortAllocation = defaultParameters.portAllocation, + debugPortAllocation: PortAllocation = defaultParameters.debugPortAllocation, + systemProperties: Map = defaultParameters.systemProperties, + useTestClock: Boolean = defaultParameters.useTestClock, + initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, + networkMapStartStrategy: NetworkMapStartStrategy = defaultParameters.networkMapStartStrategy, + startNodesInProcess: Boolean = defaultParameters.startNodesInProcess, dsl: DriverDSLExposedInterface.() -> A -) = genericDriver( - driverDsl = DriverDSL( - portAllocation = portAllocation, - debugPortAllocation = debugPortAllocation, - systemProperties = systemProperties, - driverDirectory = driverDirectory.toAbsolutePath(), - useTestClock = useTestClock, - networkMapStartStrategy = networkMapStartStrategy, - startNodesInProcess = startNodesInProcess, - isDebug = isDebug - ), - coerce = { it }, - dsl = dsl, - initialiseSerialization = initialiseSerialization -) +): A { + return genericDriver( + driverDsl = DriverDSL( + portAllocation = portAllocation, + debugPortAllocation = debugPortAllocation, + systemProperties = systemProperties, + driverDirectory = driverDirectory.toAbsolutePath(), + useTestClock = useTestClock, + networkMapStartStrategy = networkMapStartStrategy, + startNodesInProcess = startNodesInProcess, + isDebug = isDebug + ), + coerce = { it }, + dsl = dsl, + initialiseSerialization = initialiseSerialization + ) +} + +/** + * Helper function for starting a [driver] with custom parameters from Java. + * + * @param defaultParameters The default parameters for the driver. + * @param dsl The dsl itself. + * @return The value returned in the [dsl] closure. + */ +fun driver( + parameters: DriverParameters, + dsl: DriverDSLExposedInterface.() -> A +): A { + return driver(defaultParameters = parameters, dsl = dsl) +} + +/** + * Helper builder for configuring a [driver] from Java. + */ +data class DriverParameters( + val isDebug: Boolean = false, + val driverDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()), + val portAllocation: PortAllocation = PortAllocation.Incremental(10000), + val debugPortAllocation: PortAllocation = PortAllocation.Incremental(5005), + val systemProperties: Map = emptyMap(), + val useTestClock: Boolean = false, + val initialiseSerialization: Boolean = true, + val networkMapStartStrategy: NetworkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = true), + val startNodesInProcess: Boolean = false +) { + fun setIsDebug(isDebug: Boolean) = copy(isDebug = isDebug) + fun setDriverDirectory(driverDirectory: Path) = copy(driverDirectory = driverDirectory) + fun setPortAllocation(portAllocation: PortAllocation) = copy(portAllocation = portAllocation) + fun setDebugPortAllocation(debugPortAllocation: PortAllocation) = copy(debugPortAllocation = debugPortAllocation) + fun setSystemProperties(systemProperties: Map) = copy(systemProperties = systemProperties) + fun setUseTestClock(useTestClock: Boolean) = copy(useTestClock = useTestClock) + fun setInitialiseSerialization(initialiseSerialization: Boolean) = copy(initialiseSerialization = initialiseSerialization) + fun setNetworkMapStartStrategy(networkMapStartStrategy: NetworkMapStartStrategy) = copy(networkMapStartStrategy = networkMapStartStrategy) + fun setStartNodesInProcess(startNodesInProcess: Boolean) = copy(startNodesInProcess = startNodesInProcess) +} /** * This is a helper method to allow extending of the DSL, along the lines of @@ -325,8 +423,13 @@ fun addressMustBeBoundFuture(executorService: ScheduledExecutorService, hostAndP } } -fun addressMustNotBeBound(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort) { - addressMustNotBeBoundFuture(executorService, hostAndPort).getOrThrow() +/* + * The default timeout value of 40 seconds have been chosen based on previous node shutdown time estimate. + * It's been observed that nodes can take up to 30 seconds to shut down, so just to stay on the safe side the 40 seconds + * timeout has been chosen. + */ +fun addressMustNotBeBound(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, timeout: Duration = 40.seconds) { + addressMustNotBeBoundFuture(executorService, hostAndPort).getOrThrow(timeout) } fun addressMustNotBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort): CordaFuture { @@ -402,15 +505,17 @@ class ShutdownManager(private val executorService: ExecutorService) { } } 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) - } } + 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: CordaFuture<() -> Unit>) { @@ -419,6 +524,7 @@ class ShutdownManager(private val executorService: ExecutorService) { registeredShutdowns.add(shutdown) } } + fun registerShutdown(shutdown: () -> Unit) = registerShutdown(doneFuture(shutdown)) fun registerProcessShutdown(processFuture: CordaFuture) { @@ -555,6 +661,7 @@ class DriverDSL( } override fun startNode( + defaultParameters: NodeParameters, providedName: X500Name?, advertisedServices: Set, rpcUsers: List, @@ -566,7 +673,7 @@ class DriverDSL( val rpcAddress = portAllocation.nextHostAndPort() val webAddress = portAllocation.nextHostAndPort() // TODO: Derive name from the full picked name, don't just wrap the common name - val name = providedName ?: getX509Name("${oneOf(names).commonName}-${p2pAddress.port}", "London", "demo@r3.com", null) + val name = providedName ?: getX500Name(O = "${oneOf(names).organisation}-${p2pAddress.port}", L = "London", C = "GB") val networkMapServiceConfigLookup = networkMapServiceConfigLookup(listOf(object : NodeDefinition { override fun getName() = name.toString() override fun getConfig() = configOf("p2pAddress" to p2pAddress.toString()) @@ -618,7 +725,7 @@ class DriverDSL( rpcUsers: List, startInSameProcess: Boolean? ): CordaFuture>> { - val nodeNames = (0 until clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(" $it") } + val nodeNames = (0 until clusterSize).map { getX500Name(O = "Notary Service $it", OU = "corda", L = "Zurich", C = "CH") } val paths = nodeNames.map { baseDirectory(it) } ServiceIdentityGenerator.generateToDisk(paths, type.id, notaryName) val advertisedServices = setOf(ServiceInfo(type, notaryName)) @@ -631,7 +738,7 @@ class DriverDSL( rpcUsers = rpcUsers, verifierType = verifierType, customOverrides = mapOf("notaryNodeAddress" to notaryClusterAddress.toString(), - "database.serverNameTablePrefix" to if (nodeNames.isNotEmpty()) nodeNames.first().toString().replace(Regex("[^0-9A-Za-z]+"),"") else ""), + "database.serverNameTablePrefix" to if (nodeNames.isNotEmpty()) nodeNames.first().toString().replace(Regex("[^0-9A-Za-z]+"), "") else ""), startInSameProcess = startInSameProcess ) // All other nodes will join the cluster @@ -639,7 +746,7 @@ class DriverDSL( val nodeAddress = portAllocation.nextHostAndPort() val configOverride = mapOf("notaryNodeAddress" to nodeAddress.toString(), "notaryClusterAddresses" to listOf(notaryClusterAddress.toString()), "database.serverNameTablePrefix" to it.toString().replace(Regex("[^0-9A-Za-z]+"), "")) - startNode(it, advertisedServices, rpcUsers, verifierType, configOverride) + startNode(providedName = it, advertisedServices = advertisedServices, rpcUsers = rpcUsers, verifierType = verifierType, customOverrides = configOverride) } return firstNotaryFuture.flatMap { firstNotary -> @@ -660,7 +767,7 @@ class DriverDSL( if (response.isSuccessful && (response.body().string() == "started")) { return WebserverHandle(handle.webAddress, process) } - } catch(e: ConnectException) { + } catch (e: ConnectException) { log.debug("Retrying webserver info at ${handle.webAddress}") } @@ -684,7 +791,7 @@ class DriverDSL( } } - override fun baseDirectory(nodeName: X500Name): Path = driverDirectory / nodeName.commonName.replace(WHITESPACE, "") + override fun baseDirectory(nodeName: X500Name): Path = driverDirectory / nodeName.organisation.replace(WHITESPACE, "") override fun startDedicatedNetworkMapService(startInProcess: Boolean?): CordaFuture { val webAddress = portAllocation.nextHostAndPort() @@ -698,7 +805,8 @@ class DriverDSL( // node port numbers to be shifted, so all demos and docs need to be updated accordingly. "webAddress" to webAddress.toString(), "p2pAddress" to dedicatedNetworkMapAddress.toString(), - "useTestClock" to useTestClock + "useTestClock" to useTestClock, + "extraAdvertisedServiceIds" to listOf(ServiceInfo(NetworkMapService.type).toString()) ) ) return startNodeInternal(config, webAddress, startInProcess) @@ -709,14 +817,16 @@ class DriverDSL( if (startInProcess ?: startNodesInProcess) { val nodeAndThreadFuture = startInProcessNode(executorService, nodeConfiguration, config) shutdownManager.registerShutdown( - nodeAndThreadFuture.map { (node, thread) -> { - node.stop() - thread.interrupt() - } } + nodeAndThreadFuture.map { (node, thread) -> + { + node.stop() + thread.interrupt() + } + } ) return nodeAndThreadFuture.flatMap { (node, thread) -> establishRpc(nodeConfiguration.p2pAddress, nodeConfiguration, openFuture()).flatMap { rpc -> - rpc.waitUntilRegisteredWithNetworkMap().map { + rpc.waitUntilNetworkReady().map { NodeHandle.InProcess(rpc.nodeIdentity(), rpc, nodeConfiguration, webAddress, node, thread) } } @@ -731,9 +841,9 @@ class DriverDSL( } // We continue to use SSL enabled port for RPC when its for node user. establishRpc(nodeConfiguration.p2pAddress, nodeConfiguration, processDeathFuture).flatMap { rpc -> - // Call waitUntilRegisteredWithNetworkMap in background in case RPC is failing over: + // Call waitUntilNetworkReady in background in case RPC is failing over: val networkMapFuture = executorService.fork { - rpc.waitUntilRegisteredWithNetworkMap() + rpc.waitUntilNetworkReady() }.flatMap { it } firstOf(processDeathFuture, networkMapFuture) { if (it == processDeathFuture) { @@ -768,13 +878,13 @@ class DriverDSL( config: Config ): CordaFuture> { return executorService.fork { - log.info("Starting in-process Node ${nodeConf.myLegalName.commonName}") + log.info("Starting in-process Node ${nodeConf.myLegalName.organisation}") // Write node.conf writeConfig(nodeConf.baseDirectory, "node.conf", config) // TODO pass the version in? val node = Node(nodeConf, nodeConf.calculateServices(), MOCK_VERSION_INFO, initialiseSerialization = false) node.start() - val nodeThread = thread(name = nodeConf.myLegalName.commonName) { + val nodeThread = thread(name = nodeConf.myLegalName.organisation) { node.run() } node to nodeThread @@ -791,7 +901,7 @@ class DriverDSL( callerPackage: String ): CordaFuture { val processFuture = executorService.fork { - log.info("Starting out-of-process Node ${nodeConf.myLegalName.commonName}") + log.info("Starting out-of-process Node ${nodeConf.myLegalName.organisation}") // Write node.conf writeConfig(nodeConf.baseDirectory, "node.conf", config) @@ -801,12 +911,10 @@ class DriverDSL( "net.corda.node.cordapp.scan.package" to callerPackage, "java.io.tmpdir" to System.getProperty("java.io.tmpdir") // Inherit from parent process ) - // TODO Add this once we upgrade to quasar 0.7.8, this causes startup time to halve. - // val excludePattern = x(rx**;io**;kotlin**;jdk**;reflectasm**;groovyjarjarasm**;groovy**;joptsimple**;groovyjarjarantlr**;javassist**;com.fasterxml**;com.typesafe**;com.google**;com.zaxxer**;com.jcabi**;com.codahale**;com.esotericsoftware**;de.javakaffee**;org.objectweb**;org.slf4j**;org.w3c**;org.codehaus**;org.h2**;org.crsh**;org.fusesource**;org.hibernate**;org.dom4j**;org.bouncycastle**;org.apache**;org.objenesis**;org.jboss**;org.xml**;org.jcp**;org.jetbrains**;org.yaml**;co.paralleluniverse**;net.i2p**)" - // val extraJvmArguments = systemProperties.map { "-D${it.key}=${it.value}" } + - // "-javaagent:$quasarJarPath=$excludePattern" + // See experimental/quasar-hook/README.md for how to generate. + val excludePattern = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)" val extraJvmArguments = systemProperties.map { "-D${it.key}=${it.value}" } + - "-javaagent:$quasarJarPath" + "-javaagent:$quasarJarPath=$excludePattern" val loggingLevel = if (debugPort == null) "INFO" else "DEBUG" ProcessUtilities.startCordaProcess( @@ -822,8 +930,8 @@ class DriverDSL( workingDirectory = nodeConf.baseDirectory ) } - return processFuture.flatMap { - process -> addressMustBeBoundFuture(executorService, nodeConf.p2pAddress, process).map { process } + return processFuture.flatMap { process -> + addressMustBeBoundFuture(executorService, nodeConf.p2pAddress, process).map { process } } } @@ -839,8 +947,8 @@ class DriverDSL( arguments = listOf("--base-directory", handle.configuration.baseDirectory.toString()), jdwpPort = debugPort, extraJvmArguments = listOf( - "-Dname=node-${handle.configuration.p2pAddress}-webserver", - "-Djava.io.tmpdir=${System.getProperty("java.io.tmpdir")}" // Inherit from parent process + "-Dname=node-${handle.configuration.p2pAddress}-webserver", + "-Djava.io.tmpdir=${System.getProperty("java.io.tmpdir")}" // Inherit from parent process ), errorLogPath = Paths.get("error.$className.log") ) diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/driver/ProcessUtilities.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/DriverBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/DriverBasedTest.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/node/DriverBasedTest.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/node/DriverBasedTest.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt similarity index 99% rename from test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt index 3e9816910c..886043b644 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt @@ -3,7 +3,7 @@ package net.corda.testing.node 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.getX509Name +import net.corda.core.utilities.getX500Name import net.corda.core.internal.ThreadBox import net.corda.core.messaging.AllPossibleRecipients import net.corda.core.messaging.MessageRecipientGroup @@ -127,7 +127,7 @@ class InMemoryMessagingNetwork( id: Int, executor: AffinityExecutor, advertisedServices: List, - description: X500Name = getX509Name("In memory node $id", "London", "demo@r3.com", null), + description: X500Name = getX500Name(O = "In memory node $id", L = "London", C = "UK"), database: CordaPersistence) : MessagingServiceBuilder { val peerHandle = PeerHandle(id, description) diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt similarity index 73% rename from test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt index ae80cc56da..45ac7ae63b 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt @@ -4,13 +4,13 @@ import co.paralleluniverse.common.util.VisibleForTesting import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.Party import net.corda.core.node.NodeInfo -import net.corda.core.node.ServiceHub import net.corda.core.node.services.NetworkMapCache import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NonEmptySet -import net.corda.node.services.network.InMemoryNetworkMapCache +import net.corda.core.utilities.getX500Name +import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.testing.getTestPartyAndCertificate -import net.corda.testing.getTestX509Name import rx.Observable import rx.subjects.PublishSubject import java.math.BigInteger @@ -18,10 +18,10 @@ import java.math.BigInteger /** * Network map cache with no backing map service. */ -class MockNetworkMapCache(serviceHub: ServiceHub) : InMemoryNetworkMapCache(serviceHub) { +class MockNetworkMapCache(serviceHub: ServiceHubInternal) : PersistentNetworkMapCache(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 = getTestPartyAndCertificate(getX500Name(O = "Bank C", L = "London", C = "GB"), entropyToKeyPair(BigInteger.valueOf(1000)).public) + val BANK_D = getTestPartyAndCertificate(getX500Name(O = "Bank D", L = "London", C = "GB"), entropyToKeyPair(BigInteger.valueOf(2000)).public) val BANK_C_ADDR = NetworkHostAndPort("bankC", 8080) val BANK_D_ADDR = NetworkHostAndPort("bankD", 8080) } @@ -29,8 +29,8 @@ class MockNetworkMapCache(serviceHub: ServiceHub) : InMemoryNetworkMapCache(serv override val changed: Observable = PublishSubject.create() init { - val mockNodeA = NodeInfo(listOf(BANK_C_ADDR), BANK_C, NonEmptySet.of(BANK_C), 1) - val mockNodeB = NodeInfo(listOf(BANK_D_ADDR), BANK_D, NonEmptySet.of(BANK_D), 1) + val mockNodeA = NodeInfo(listOf(BANK_C_ADDR), BANK_C, NonEmptySet.of(BANK_C), 1, serial = 1L) + val mockNodeB = NodeInfo(listOf(BANK_D_ADDR), BANK_D, NonEmptySet.of(BANK_D), 1, serial = 1L) 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/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt similarity index 92% rename from test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 603de51d4c..d747d090cb 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -3,8 +3,6 @@ package net.corda.testing.node import com.google.common.jimfs.Configuration.unix import com.google.common.jimfs.Jimfs import com.nhaarman.mockito_kotlin.whenever -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 @@ -18,12 +16,10 @@ import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.ServiceEntry import net.corda.core.node.WorldMapLocation import net.corda.core.node.services.* -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.* import net.corda.node.internal.AbstractNode import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.messaging.MessagingService import net.corda.node.services.network.InMemoryNetworkMapService @@ -32,6 +28,7 @@ import net.corda.node.services.transactions.* import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.testing.* +import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.apache.activemq.artemis.utils.ReusableLatch import org.bouncycastle.asn1.x500.X500Name import org.slf4j.Logger @@ -68,7 +65,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, val messagingNetwork = InMemoryMessagingNetwork(networkSendManuallyPumped, servicePeerAllocationStrategy, busyLatch) // A unique identifier for this network to segregate databases with the same nodeID but different networks. private val networkId = random63BitValue() - private val identities = mutableListOf() private val _nodes = mutableListOf() /** A read only view of the current set of executing nodes. */ val nodes: List get() = _nodes @@ -156,7 +152,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, id, serverThread, makeServiceEntries(), - configuration.myLegalName, + myLegalName, database) .start() .getOrThrow() @@ -168,8 +164,17 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, val caCertificates: Array = listOf(legalIdentity.certificate.cert, clientCa?.certificate?.cert) .filterNotNull() .toTypedArray() - return InMemoryIdentityService((mockNet.identities + info.legalIdentityAndCert).toSet(), + val identityService = PersistentIdentityService(setOf(info.legalIdentityAndCert), trustRoot = trustRoot, caCertificates = *caCertificates) + services.networkMapCache.partyNodes.forEach { identityService.verifyAndRegisterIdentity(it.legalIdentityAndCert) } + services.networkMapCache.changed.subscribe { mapChange -> + // TODO how should we handle network map removal + if (mapChange is NetworkMapCache.MapChange.Added) { + identityService.verifyAndRegisterIdentity(mapChange.node.legalIdentityAndCert) + } + } + + return identityService } override fun makeKeyManagementService(identityService: IdentityService): KeyManagementService { @@ -211,17 +216,12 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override fun noNetworkMapConfigured() = doneFuture(Unit) // There is no need to slow down the unit tests by initialising CityDatabase - override fun findMyLocation(): WorldMapLocation? = null + open fun findMyLocation(): WorldMapLocation? = null // It's left only for NetworkVisualiserSimulation override fun makeTransactionVerifierService() = InMemoryTransactionVerifierService(1) override fun myAddresses() = emptyList() - override fun start() { - super.start() - mockNet.identities.add(info.legalIdentityAndCert) - } - // Allow unit tests to modify the plugin list before the node start, // so they don't have to ServiceLoad test plugins into all unit tests. val testPluginRegistries = super.pluginRegistries.toMutableList() @@ -267,6 +267,15 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } }) } + + /** + * Makes sure that the [MockNode] is correctly registered on the [MockNetwork] + * Please note that [MockNetwork.runNetwork] should be invoked to ensure that all the pending registration requests + * were duly processed + */ + fun ensureRegistered() { + _nodeReadyFuture.getOrThrow() + } } /** @@ -294,14 +303,14 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, val id = forcedID ?: nextNodeId++ val config = testNodeConfiguration( baseDirectory = baseDirectory(id).createDirectories(), - myLegalName = legalName ?: getTestX509Name("Mock Company $id")).also { + myLegalName = legalName ?: getX500Name(O = "Mock Company $id", L = "London", C = "GB")).also { whenever(it.dataSourceProperties).thenReturn(makeTestDataSourceProperties("node_${id}_net_$networkId")) configOverrides(it) } return nodeFactory.create(config, this, networkMapAddress, advertisedServices.toSet(), id, overrideServices, entropyRoot).apply { if (start) { start() - if (threadPerNode && networkMapAddress != null) networkMapRegistrationFuture.getOrThrow() // XXX: What about manually-started nodes? + if (threadPerNode && networkMapAddress != null) nodeReadyFuture.getOrThrow() // XXX: What about manually-started nodes? } _nodes.add(this) } @@ -355,9 +364,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, repeat(numPartyNodes) { nodes += createPartyNode(mapAddress) } - nodes.forEach { itNode -> - nodes.map { it.info.legalIdentityAndCert }.map(itNode.services.identityService::verifyAndRegisterIdentity) - } return BasketOfNodes(nodes, notaryNode, mapNode) } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt similarity index 59% rename from test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 4ac02a135b..a08d3b0de7 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -1,6 +1,5 @@ package net.corda.testing.node -import net.corda.core.contracts.Attachment import net.corda.core.crypto.* import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.PartyAndCertificate @@ -18,14 +17,15 @@ import net.corda.finance.schemas.CommercialPaperSchemaV1 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 import net.corda.node.services.keys.getSigner +import net.corda.node.services.persistence.HibernateConfiguration import net.corda.node.services.persistence.InMemoryStateMachineRecordedTransactionMappingStorage 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.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.HibernateVaultQueryImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.CordaPersistence @@ -35,17 +35,12 @@ import net.corda.testing.schemas.DummyLinearStateSchemaV1 import org.bouncycastle.operator.ContentSigner import rx.Observable import rx.subjects.PublishSubject -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.InputStream import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey import java.sql.Connection import java.time.Clock import java.util.* -import java.util.jar.JarInputStream // 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. @@ -56,6 +51,87 @@ import java.util.jar.JarInputStream */ open class MockServices(vararg val keys: KeyPair) : ServiceHub { + companion object { + + @JvmStatic + val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") + + /** + * Make properties appropriate for creating a DataSource for unit tests. + * + * @param nodeName Reflects the "instance" of the in-memory database. Defaults to a random string. + */ + // TODO: Can we use an X509 principal generator here? + @JvmStatic + fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties { + val props = Properties() + props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") + props.setProperty("dataSource.url", "jdbc:h2:mem:${nodeName}_persistence;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") + props.setProperty("dataSource.user", "sa") + props.setProperty("dataSource.password", "") + return props + } + + /** + * Make properties appropriate for creating a Database for unit tests. + * + * @param key (optional) key of a database property to be set. + * @param value (optional) value of a database property to be set. + */ + @JvmStatic + fun makeTestDatabaseProperties(key: String? = null, value: String? = null): Properties { + val props = Properties() + props.setProperty("transactionIsolationLevel", "repeatableRead") //for other possible values see net.corda.node.utilities.CordaPeristence.parserTransactionIsolationLevel(String) + if (key != null) { props.setProperty(key, value) } + return props + } + + /** + * Creates an instance of [InMemoryIdentityService] with [MOCK_IDENTITIES]. + */ + @JvmStatic + fun makeTestIdentityService() = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate) + + /** + * Makes database and mock services appropriate for unit tests. + * + * @param customSchemas a set of schemas being used by [NodeSchemaService] + * @param keys a lis of [KeyPair] instances to be used by [MockServices]. Defualts to [MEGA_CORP_KEY] + * @param createIdentityService a lambda function returning an instance of [IdentityService]. Defauts to [InMemoryIdentityService]. + * + * @return a pair where the first element is the instance of [CordaPersistence] and the second is [MockServices]. + */ + @JvmStatic + fun makeTestDatabaseAndMockServices(customSchemas: Set = setOf(CommercialPaperSchemaV1, DummyLinearStateSchemaV1, CashSchemaV1), + keys: List = listOf(MEGA_CORP_KEY), + createIdentityService: () -> IdentityService = { makeTestIdentityService() }): Pair { + val dataSourceProps = makeTestDataSourceProperties() + val databaseProperties = makeTestDatabaseProperties() + val createSchemaService = { NodeSchemaService(customSchemas) } + val identityServiceRef: IdentityService by lazy { createIdentityService() } + val database = configureDatabase(dataSourceProps, databaseProperties, createSchemaService, { identityServiceRef }) + val mockService = database.transaction { + object : MockServices(*(keys.toTypedArray())) { + override val identityService: IdentityService = database.transaction { identityServiceRef } + override val vaultService: VaultService = makeVaultService(database.hibernateConfig) + + override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { + for (stx in txs) { + validatedTransactions.addTransaction(stx) + } + // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. + (vaultService as NodeVaultService).notifyAll(txs.map { it.tx }) + } + + override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(database.hibernateConfig, vaultService) + + override fun jdbcSession(): Connection = database.createSession() + } + } + return Pair(database, mockService) + } + } + constructor() : this(generateKeyPair()) val key: KeyPair get() = keys.first() @@ -72,16 +148,17 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { 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) + override val identityService: IdentityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate) + override val keyManagementService: KeyManagementService by lazy { MockKeyManagementService(identityService, *keys) } override val vaultService: VaultService get() = throw UnsupportedOperationException() + override val contractUpgradeService: ContractUpgradeService get() = throw UnsupportedOperationException() 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() { val identity = getTestPartyAndCertificate(MEGA_CORP.name, key.public) - return NodeInfo(emptyList(), identity, NonEmptySet.of(identity), 1) + return NodeInfo(emptyList(), identity, NonEmptySet.of(identity), 1, serial = 1L) } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) @@ -136,35 +213,6 @@ class MockKeyManagementService(val identityService: IdentityService, } } -class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { - val files = HashMap() - - override fun openAttachment(id: SecureHash): Attachment? { - val f = files[id] ?: return null - return object : Attachment { - override fun open(): InputStream = ByteArrayInputStream(f) - override val id: SecureHash = id - } - } - - override fun importAttachment(jar: InputStream): SecureHash { - // JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here. - require(jar !is JarInputStream) - - val bytes = run { - val s = ByteArrayOutputStream() - jar.copyTo(s) - s.close() - s.toByteArray() - } - val sha256 = bytes.sha256() - if (files.containsKey(sha256)) - throw FileAlreadyExistsException(File("!! MOCK FILE NAME")) - files[sha256] = bytes - return sha256 - } -} - class MockStateMachineRecordedTransactionMappingStorage( val storage: StateMachineRecordedTransactionMappingStorage = InMemoryStateMachineRecordedTransactionMappingStorage() ) : StateMachineRecordedTransactionMappingStorage by storage @@ -193,56 +241,3 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali override fun getTransaction(id: SecureHash): SignedTransaction? = txns[id] } - -/** - * Make properties appropriate for creating a DataSource for unit tests. - * - * @param nodeName Reflects the "instance" of the in-memory database. Defaults to a random string. - */ -// TODO: Can we use an X509 principal generator here? -fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties { - val props = Properties() - props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") - props.setProperty("dataSource.url", "jdbc:h2:mem:${nodeName}_persistence;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE") - props.setProperty("dataSource.user", "sa") - props.setProperty("dataSource.password", "") - return props -} - -fun makeTestDatabaseProperties(key: String? = null, value: String? = null): Properties { - val props = Properties() - props.setProperty("transactionIsolationLevel", "repeatableRead") //for other possible values see net.corda.node.utilities.CordaPeristence.parserTransactionIsolationLevel(String) - if (key != null) { props.setProperty(key, value) } - return props -} - -fun makeTestIdentityService() = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate) - -fun makeTestDatabaseAndMockServices(customSchemas: Set = setOf(CommercialPaperSchemaV1, DummyLinearStateSchemaV1, CashSchemaV1), - keys: List = listOf(MEGA_CORP_KEY), - createIdentityService: () -> IdentityService = { makeTestIdentityService() }): Pair { - val dataSourceProps = makeTestDataSourceProperties() - val databaseProperties = makeTestDatabaseProperties() - val createSchemaService = { NodeSchemaService(customSchemas) } - val database = configureDatabase(dataSourceProps, databaseProperties, createSchemaService, createIdentityService) - val mockService = database.transaction { - object : MockServices(*(keys.toTypedArray())) { - override val vaultService: VaultService = makeVaultService(database.hibernateConfig) - - override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { - for (stx in txs) { - 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 }) - } - - override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(database.hibernateConfig, vaultService) - - override fun jdbcSession(): Connection = database.createSession() - } - } - return Pair(database, mockService) -} - -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/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt similarity index 71% rename from test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index 64a871db5f..2d5366a17f 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -1,24 +1,18 @@ package net.corda.testing.node import net.corda.core.concurrent.CordaFuture -import net.corda.core.crypto.appendToCommonName -import net.corda.core.crypto.commonName -import net.corda.core.crypto.getX509Name -import net.corda.core.internal.concurrent.flatMap -import net.corda.core.internal.concurrent.fork -import net.corda.core.internal.concurrent.map -import net.corda.core.internal.concurrent.transpose +import net.corda.core.internal.concurrent.* import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType -import net.corda.core.utilities.WHITESPACE -import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.* import net.corda.node.internal.Node import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.configOf import net.corda.node.services.config.plus +import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.nodeapi.User @@ -27,6 +21,7 @@ import net.corda.testing.DUMMY_MAP import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.driver.addressMustNotBeBoundFuture import net.corda.testing.getFreeLocalPorts +import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import org.apache.logging.log4j.Level import org.bouncycastle.asn1.x500.X500Name import org.junit.After @@ -75,6 +70,13 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { portNotBoundChecks.transpose().getOrThrow() } + /** + * Clear network map data from nodes' databases. + */ + fun clearAllNodeInfoDb() { + nodes.forEach { it.services.networkMapCache.clearNetworkMapCache() } + } + /** * You can use this method to start the network map node in a more customised manner. Otherwise it * will automatically be started with the default parameters. @@ -84,37 +86,51 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { advertisedServices: Set = emptySet(), rpcUsers: List = emptyList(), configOverrides: Map = emptyMap()): Node { - check(_networkMapNode == null) - return startNodeInternal(legalName, platformVersion, advertisedServices, rpcUsers, configOverrides).apply { + check(_networkMapNode == null || _networkMapNode!!.info.legalIdentity.name == legalName) + return startNodeInternal(legalName, platformVersion, advertisedServices + ServiceInfo(NetworkMapService.type), rpcUsers, configOverrides).apply { _networkMapNode = this } } + @JvmOverloads fun startNode(legalName: X500Name, platformVersion: Int = 1, advertisedServices: Set = emptySet(), rpcUsers: List = emptyList(), - configOverrides: Map = emptyMap()): CordaFuture { + configOverrides: Map = emptyMap(), + noNetworkMap: Boolean = false, + waitForConnection: Boolean = true): CordaFuture { + val networkMapConf = if (noNetworkMap) { + // Nonexistent network map service address. + mapOf( + "networkMapService" to mapOf( + "address" to "localhost:10000", + "legalName" to networkMapNode.info.legalIdentity.name.toString() + ) + ) + } else { + mapOf( + "networkMapService" to mapOf( + "address" to networkMapNode.configuration.p2pAddress.toString(), + "legalName" to networkMapNode.info.legalIdentity.name.toString() + ) + ) + } val node = startNodeInternal( legalName, platformVersion, advertisedServices, rpcUsers, - mapOf( - "networkMapService" to mapOf( - "address" to networkMapNode.configuration.p2pAddress.toString(), - "legalName" to networkMapNode.info.legalIdentity.name.toString() - ) - ) + configOverrides - ) - return node.networkMapRegistrationFuture.map { node } + networkMapConf + configOverrides, + noNetworkMap) + return if (waitForConnection) node.nodeReadyFuture.map { node } else doneFuture(node) } fun startNotaryCluster(notaryName: X500Name, clusterSize: Int, serviceType: ServiceType = RaftValidatingNotaryService.type): CordaFuture> { ServiceIdentityGenerator.generateToDisk( - (0 until clusterSize).map { baseDirectory(notaryName.appendToCommonName("-$it")) }, + (0 until clusterSize).map { baseDirectory(getX500Name(O = "${notaryName.organisation}-$it", L = notaryName.locality, C = notaryName.country)) }, serviceType.id, notaryName) @@ -122,19 +138,19 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { val nodeAddresses = getFreeLocalPorts("localhost", clusterSize).map { it.toString() } val masterNodeFuture = startNode( - getX509Name("${notaryName.commonName}-0", "London", "demo@r3.com", null), + getX500Name(O = "${notaryName.organisation}-0", L = notaryName.locality, C = notaryName.country), advertisedServices = setOf(serviceInfo), configOverrides = mapOf("notaryNodeAddress" to nodeAddresses[0], - "database" to mapOf("serverNameTablePrefix" to if (clusterSize > 1) "${notaryName.commonName}0".replace(Regex("[^0-9A-Za-z]+"),"") else ""))) + "database" to mapOf("serverNameTablePrefix" to if (clusterSize > 1) "${notaryName.organisation}0".replace(Regex("[^0-9A-Za-z]+"), "") else ""))) val remainingNodesFutures = (1 until clusterSize).map { startNode( - getX509Name("${notaryName.commonName}-$it", "London", "demo@r3.com", null), + getX500Name(O = "${notaryName.organisation}-$it", L = notaryName.locality, C = notaryName.country), advertisedServices = setOf(serviceInfo), configOverrides = mapOf( "notaryNodeAddress" to nodeAddresses[it], "notaryClusterAddresses" to listOf(nodeAddresses[0]), - "database" to mapOf("serverNameTablePrefix" to "${notaryName.commonName}$it".replace(Regex("[^0-9A-Za-z]+"), "")))) + "database" to mapOf("serverNameTablePrefix" to "${notaryName.organisation}$it".replace(Regex("[^0-9A-Za-z]+"), "")))) } return remainingNodesFutures.transpose().flatMap { remainingNodes -> @@ -142,24 +158,27 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { } } - protected fun baseDirectory(legalName: X500Name) = tempFolder.root.toPath() / legalName.commonName.replace(WHITESPACE, "") + protected fun baseDirectory(legalName: X500Name) = tempFolder.root.toPath() / legalName.organisation.replace(WHITESPACE, "") private fun startNodeInternal(legalName: X500Name, platformVersion: Int, advertisedServices: Set, rpcUsers: List, - configOverrides: Map): Node { + configOverrides: Map, + noNetworkMap: Boolean = false): Node { val baseDirectory = baseDirectory(legalName).createDirectories() val localPort = getFreeLocalPorts("localhost", 2) + val p2pAddress = configOverrides["p2pAddress"] ?: localPort[0].toString() val config = ConfigHelper.loadConfig( baseDirectory = baseDirectory, allowMissingConfig = true, configOverrides = configOf( "myLegalName" to legalName.toString(), - "p2pAddress" to localPort[0].toString(), + "p2pAddress" to p2pAddress, "rpcAddress" to localPort[1].toString(), "extraAdvertisedServiceIds" to advertisedServices.map { it.toString() }, - "rpcUsers" to rpcUsers.map { it.toMap() } + "rpcUsers" to rpcUsers.map { it.toMap() }, + "noNetworkMap" to noNetworkMap ) + configOverrides ) @@ -168,7 +187,7 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() { initialiseSerialization = false) node.start() nodes += node - thread(name = legalName.commonName) { + thread(name = legalName.organisation) { node.run() } return node diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt similarity index 87% rename from test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt index 4e45f47417..34d0372409 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/SimpleNode.kt @@ -1,13 +1,13 @@ package net.corda.testing.node import com.codahale.metrics.MetricRegistry -import net.corda.core.crypto.commonName import net.corda.core.crypto.generateKeyPair import net.corda.core.internal.concurrent.openFuture 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.core.utilities.organisation import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.api.MonitoringService import net.corda.node.services.config.NodeConfiguration @@ -15,12 +15,13 @@ import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.NodeMessagingClient -import net.corda.node.services.network.InMemoryNetworkMapCache import net.corda.node.services.schema.NodeSchemaService +import net.corda.node.testing.MockServiceHubInternal import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase import net.corda.testing.freeLocalHostAndPort +import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import java.security.KeyPair import java.security.cert.X509Certificate import kotlin.concurrent.thread @@ -39,9 +40,10 @@ class SimpleNode(val config: NodeConfiguration, val address: NetworkHostAndPort val identityService: IdentityService = InMemoryIdentityService(trustRoot = trustRoot) val database: CordaPersistence = configureDatabase(config.dataSourceProperties, config.database, { NodeSchemaService() }, { InMemoryIdentityService(trustRoot = trustRoot) }) val keyService: KeyManagementService = E2ETestKeyManagementService(identityService, setOf(identity)) - val executor = ServiceAffinityExecutor(config.myLegalName.commonName, 1) + val executor = ServiceAffinityExecutor(config.myLegalName.organisation, 1) // 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 broker = ArtemisMessagingServer(config, address.port, rpcAddress.port, + MockNetworkMapCache(serviceHub = object : MockServiceHubInternal(database = database, configuration = config) {}), userService) val networkMapRegistrationFuture = openFuture() val network = database.transaction { NodeMessagingClient( @@ -62,7 +64,7 @@ class SimpleNode(val config: NodeConfiguration, val address: NetworkHostAndPort override val protocolVersion = 0 }, userService) - thread(name = config.myLegalName.commonName) { + thread(name = config.myLegalName.organisation) { network.run(broker.serverControl) } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/TestClock.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt similarity index 96% rename from test-utils/src/main/kotlin/net/corda/testing/node/TestClock.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt index d0303085d8..690f765c3d 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/TestClock.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt @@ -4,8 +4,7 @@ import net.corda.core.internal.until import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializeAsTokenContext import net.corda.core.serialization.SingletonSerializationToken.Companion.singletonSerializationToken -import net.corda.core.internal.until -import net.corda.node.utilities.MutableClock +import net.corda.node.internal.MutableClock import java.time.Clock import java.time.Duration import java.time.Instant diff --git a/test-utils/src/main/kotlin/net/corda/testing/performance/Injectors.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/performance/Injectors.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/performance/Injectors.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/performance/Injectors.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/performance/Reporter.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/performance/Reporter.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/performance/Reporter.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/performance/Reporter.kt diff --git a/smoke-test-utils/build.gradle b/testing/smoke-test-utils/build.gradle similarity index 100% rename from smoke-test-utils/build.gradle rename to testing/smoke-test-utils/build.gradle diff --git a/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt similarity index 95% rename from smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt rename to testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt index 0df543cd5c..71ad5805ba 100644 --- a/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt @@ -5,7 +5,7 @@ import com.typesafe.config.ConfigFactory.empty 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.utilities.organisation import net.corda.nodeapi.User import org.bouncycastle.asn1.x500.X500Name @@ -22,7 +22,7 @@ class NodeConfig( val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) } - val commonName: String get() = legalName.commonName + val commonName: String get() = legalName.organisation /* * The configuration object depends upon the networkMap, diff --git a/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt similarity index 100% rename from smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt rename to testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt diff --git a/test-common/build.gradle b/testing/test-common/build.gradle similarity index 100% rename from test-common/build.gradle rename to testing/test-common/build.gradle diff --git a/test-common/src/main/resources/log4j2-test.xml b/testing/test-common/src/main/resources/log4j2-test.xml similarity index 100% rename from test-common/src/main/resources/log4j2-test.xml rename to testing/test-common/src/main/resources/log4j2-test.xml diff --git a/test-utils/build.gradle b/testing/test-utils/build.gradle similarity index 62% rename from test-utils/build.gradle rename to testing/test-utils/build.gradle index e67533db44..350a94d766 100644 --- a/test-utils/build.gradle +++ b/testing/test-utils/build.gradle @@ -10,22 +10,6 @@ description 'Testing utilities for Corda' configurations { // we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment runtime.exclude module: 'isolated' - - integrationTestCompile.extendsFrom testCompile - integrationTestRuntime.extendsFrom testRuntime -} - -sourceSets { - integrationTest { - kotlin { - compileClasspath += main.output + test.output - runtimeClasspath += main.output + test.output - srcDir file('src/integration-test/kotlin') - } - resources { - srcDir file('src/integration-test/resources') - } - } } dependencies { @@ -49,15 +33,6 @@ dependencies { // OkHTTP: Simple HTTP library. compile "com.squareup.okhttp3:okhttp:$okhttp_version" - - // Integration test helpers - integrationTestCompile "org.assertj:assertj-core:${assertj_version}" - integrationTestCompile "junit:junit:$junit_version" -} - -task integrationTest(type: Test) { - testClassesDirs = sourceSets.integrationTest.output.classesDirs - classpath = sourceSets.integrationTest.runtimeClasspath } jar { diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt similarity index 60% rename from test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 127ea3b186..88987a562e 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -3,34 +3,22 @@ package net.corda.testing -import com.nhaarman.mockito_kotlin.spy -import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.StateRef -import net.corda.core.crypto.* +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate -import net.corda.core.node.ServiceHub import net.corda.core.node.services.IdentityService -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.* import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER -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.node.utilities.CertificateType import net.corda.node.utilities.X509Utilities import net.corda.nodeapi.config.SSLConfiguration -import net.corda.testing.node.MockServices -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties +import net.corda.nodeapi.internal.serialization.AMQP_ENABLED import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x500.X500NameBuilder -import org.bouncycastle.asn1.x500.style.BCStyle -import java.net.URL import java.nio.file.Files -import java.nio.file.Path import java.security.KeyPair import java.security.PublicKey import java.security.cert.CertificateFactory @@ -70,20 +58,20 @@ val ALICE_PUBKEY: PublicKey get() = ALICE_KEY.public val BOB_PUBKEY: PublicKey get() = BOB_KEY.public val CHARLIE_PUBKEY: PublicKey get() = CHARLIE_KEY.public -val MEGA_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX509Name("MegaCorp", "London", "demo@r3.com", null), MEGA_CORP_PUBKEY) +val MEGA_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX500Name(O = "MegaCorp", L = "London", C = "GB"), MEGA_CORP_PUBKEY) val MEGA_CORP: Party get() = MEGA_CORP_IDENTITY.party -val MINI_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX509Name("MiniCorp", "London", "demo@r3.com", null), MINI_CORP_PUBKEY) +val MINI_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX500Name(O = "MiniCorp", L = "London", C = "GB"), MINI_CORP_PUBKEY) val MINI_CORP: Party get() = MINI_CORP_IDENTITY.party val BOC_KEY: KeyPair by lazy { generateKeyPair() } val BOC_PUBKEY: PublicKey get() = BOC_KEY.public -val BOC_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getTestX509Name("BankOfCorda"), BOC_PUBKEY) +val BOC_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX500Name(O = "BankOfCorda", L = "London", C = "GB"), BOC_PUBKEY) val BOC: Party get() = BOC_IDENTITY.party val BOC_PARTY_REF = BOC.ref(OpaqueBytes.of(1)).reference val BIG_CORP_KEY: KeyPair by lazy { generateKeyPair() } val BIG_CORP_PUBKEY: PublicKey get() = BIG_CORP_KEY.public -val BIG_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX509Name("BigCorporation", "London", "demo@r3.com", null), BIG_CORP_PUBKEY) +val BIG_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX500Name(O = "BigCorporation", L = "London", C = "GB"), BIG_CORP_PUBKEY) val BIG_CORP: Party get() = BIG_CORP_IDENTITY.party val BIG_CORP_PARTY_REF = BIG_CORP.ref(OpaqueBytes.of(1)).reference @@ -126,60 +114,6 @@ fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List.() -> Unit -): LedgerDSL { - if (initialiseSerialization) initialiseTestSerialization() - try { - val ledgerDsl = LedgerDSL(TestLedgerDSLInterpreter(services)) - dsl(ledgerDsl) - return ledgerDsl - } finally { - if (initialiseSerialization) resetTestSerialization() - } -} - -/** - * Creates a ledger with a single transaction, built by the passed in dsl. - * - * @see LedgerDSLInterpreter._transaction - */ -@JvmOverloads fun transaction( - transactionLabel: String? = null, - transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY), - initialiseSerialization: Boolean = true, - dsl: TransactionDSL.() -> EnforceVerifyOrFail -) = ledger(initialiseSerialization = initialiseSerialization) { this.transaction(transactionLabel, transactionBuilder, dsl) } - -fun testNodeConfiguration( - baseDirectory: Path, - myLegalName: X500Name): NodeConfiguration { - abstract class MockableNodeConfiguration : NodeConfiguration // Otherwise Mockito is defeated by val getters. - val nc = spy() - whenever(nc.baseDirectory).thenReturn(baseDirectory) - whenever(nc.myLegalName).thenReturn(myLegalName) - whenever(nc.minimumPlatformVersion).thenReturn(1) - whenever(nc.keyStorePassword).thenReturn("cordacadevpass") - whenever(nc.trustStorePassword).thenReturn("trustpass") - whenever(nc.rpcUsers).thenReturn(emptyList()) - whenever(nc.dataSourceProperties).thenReturn(makeTestDataSourceProperties(myLegalName.commonName)) - whenever(nc.database).thenReturn(makeTestDatabaseProperties()) - whenever(nc.emailAddress).thenReturn("") - whenever(nc.exportJMXto).thenReturn("") - whenever(nc.devMode).thenReturn(true) - whenever(nc.certificateSigningService).thenReturn(URL("http://localhost")) - whenever(nc.certificateChainCheckPolicies).thenReturn(emptyList()) - whenever(nc.verifierType).thenReturn(VerifierType.InMemory) - whenever(nc.messageRedeliveryDelaySeconds).thenReturn(5) - return nc -} - @JvmOverloads fun configureTestSSL(legalName: X500Name = MEGA_CORP.name): SSLConfiguration = object : SSLConfiguration { override val certificatesDirectory = Files.createTempDirectory("certs") @@ -191,21 +125,6 @@ fun configureTestSSL(legalName: X500Name = MEGA_CORP.name): SSLConfiguration = o } } - -/** - * Return a bogus X.509 for testing purposes. - */ -fun getTestX509Name(commonName: String): X500Name { - require(!commonName.startsWith("CN=")) - // TODO: Consider if we want to make these more variable, i.e. different locations? - val nameBuilder = X500NameBuilder(BCStyle.INSTANCE) - nameBuilder.addRDN(BCStyle.CN, commonName) - nameBuilder.addRDN(BCStyle.O, "R3") - nameBuilder.addRDN(BCStyle.L, "New York") - 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) @@ -218,4 +137,16 @@ fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair = */ fun getTestPartyAndCertificate(name: X500Name, publicKey: PublicKey, trustRoot: CertificateAndKeyPair = DUMMY_CA): PartyAndCertificate { return getTestPartyAndCertificate(Party(name, publicKey), trustRoot) +} + +inline fun kryoSpecific(reason: String, function: () -> Unit) = if(!AMQP_ENABLED) { + function() +} else { + loggerFor().info("Ignoring Kryo specific test, reason: $reason" ) +} + +inline fun amqpSpecific(reason: String, function: () -> Unit) = if(AMQP_ENABLED) { + function() +} else { + loggerFor().info("Ignoring AMQP specific test, reason: $reason" ) } \ No newline at end of file diff --git a/test-utils/src/main/kotlin/net/corda/testing/Expect.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/Expect.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/Expect.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/Expect.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt similarity index 99% rename from test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt index 9ee727f418..94eb3a7a12 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt @@ -15,7 +15,7 @@ import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.div import net.corda.core.internal.write import net.corda.core.serialization.SerializeAsToken -import net.corda.jackson.JacksonSupport +import net.corda.client.jackson.JacksonSupport import net.corda.node.services.statemachine.FlowStackSnapshotFactory import java.nio.file.Path import java.time.Instant diff --git a/test-utils/src/main/kotlin/net/corda/testing/IntegrationTestCategory.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/IntegrationTestCategory.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/IntegrationTestCategory.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/IntegrationTestCategory.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/LedgerDSLInterpreter.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/LogHelper.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/Measure.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/Measure.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/Measure.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/ProjectStructure.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/ProjectStructure.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/ProjectStructure.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/ProjectStructure.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt similarity index 95% rename from test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt index 9712bc866d..1d914914a2 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt @@ -1,6 +1,7 @@ package net.corda.testing import net.corda.client.rpc.serialization.KryoClientSerializationScheme +import net.corda.core.crypto.SecureHash import net.corda.core.serialization.* import net.corda.core.utilities.ByteSequence import net.corda.node.serialization.KryoServerSerializationScheme @@ -89,7 +90,7 @@ fun resetTestSerialization() { (SerializationDefaults.CHECKPOINT_CONTEXT as TestSerializationContext).delegate = null } -class TestSerializationFactory : SerializationFactory { +class TestSerializationFactory : SerializationFactory() { var delegate: SerializationFactory? = null set(value) { field = value @@ -150,4 +151,8 @@ class TestSerializationContext : SerializationContext { override fun withPreferredSerializationVersion(versionHeader: ByteSequence): SerializationContext { return TestSerializationContext().apply { delegate = this@TestSerializationContext.delegate!!.withPreferredSerializationVersion(versionHeader) } } + + override fun withAttachmentsClassLoader(attachmentHashes: List): SerializationContext { + return TestSerializationContext().apply { delegate = this@TestSerializationContext.delegate!!.withAttachmentsClassLoader(attachmentHashes) } + } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt new file mode 100644 index 0000000000..ba76178760 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt @@ -0,0 +1,77 @@ +@file:JvmName("TestConstants") + +package net.corda.testing + +import net.corda.core.contracts.Command +import net.corda.core.contracts.TypeOnlyCommandData +import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.crypto.generateKeyPair +import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.utilities.CertificateAndKeyPair +import net.corda.core.utilities.getX500Name +import net.corda.node.utilities.X509Utilities +import java.math.BigInteger +import java.security.KeyPair +import java.security.PublicKey +import java.time.Instant + +// A dummy time at which we will be pretending test transactions are created. +val TEST_TX_TIME: Instant get() = Instant.parse("2015-04-17T12:00:00.00Z") + +val DUMMY_KEY_1: KeyPair by lazy { generateKeyPair() } +val DUMMY_KEY_2: KeyPair by lazy { generateKeyPair() } + +val DUMMY_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) } +/** Dummy notary identity for tests and simulations */ +val DUMMY_NOTARY_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_NOTARY) +val DUMMY_NOTARY: Party get() = Party(getX500Name(O = "Notary Service", OU = "corda", L = "Zurich", C = "CH"), DUMMY_NOTARY_KEY.public) + +val DUMMY_MAP_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(30)) } +/** Dummy network map service identity for tests and simulations */ +val DUMMY_MAP: Party get() = Party(getX500Name(O = "Network Map Service", OU = "corda", L = "Amsterdam", C = "NL"), DUMMY_MAP_KEY.public) + +val DUMMY_BANK_A_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(40)) } +/** Dummy bank identity for tests and simulations */ +val DUMMY_BANK_A: Party get() = Party(getX500Name(O = "Bank A", L = "London", C = "GB"), DUMMY_BANK_A_KEY.public) + +val DUMMY_BANK_B_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(50)) } +/** Dummy bank identity for tests and simulations */ +val DUMMY_BANK_B: Party get() = Party(getX500Name(O = "Bank B", L = "New York", C = "US"), DUMMY_BANK_B_KEY.public) + +val DUMMY_BANK_C_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(60)) } +/** Dummy bank identity for tests and simulations */ +val DUMMY_BANK_C: Party get() = Party(getX500Name(O = "Bank C", L = "Tokyo", C = "JP"), DUMMY_BANK_C_KEY.public) + +val ALICE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(70)) } +/** Dummy individual identity for tests and simulations */ +val ALICE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(ALICE) +val ALICE: Party get() = Party(getX500Name(O = "Alice Corp", L = "Madrid", C = "ES"), ALICE_KEY.public) + +val BOB_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(80)) } +/** Dummy individual identity for tests and simulations */ +val BOB_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(BOB) +val BOB: Party get() = Party(getX500Name(O = "Bob Plc", L = "Rome", C = "IT"), BOB_KEY.public) + +val CHARLIE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(90)) } +/** Dummy individual identity for tests and simulations */ +val CHARLIE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CHARLIE) +val CHARLIE: Party get() = Party(getX500Name(O = "Charlie Ltd", L = "Athens", C = "GR"), CHARLIE_KEY.public) + +val DUMMY_REGULATOR_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(100)) } +/** Dummy regulator for tests and simulations */ +val DUMMY_REGULATOR: Party get() = Party(getX500Name(O = "Regulator A", OU = "Corda", L = "Paris", C = "FR"), DUMMY_REGULATOR_KEY.public) + +val DUMMY_CA_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(110)) } +val DUMMY_CA: CertificateAndKeyPair by lazy { + // TODO: Should be identity scheme + val cert = X509Utilities.createSelfSignedCACertificate(getX500Name(CN = "Dummy CA", OU = "Corda", O = "R3 Ltd", L = "London", C = "GB"), DUMMY_CA_KEY) + CertificateAndKeyPair(cert, DUMMY_CA_KEY) +} + +fun dummyCommand(vararg signers: PublicKey = arrayOf(generateKeyPair().public) ) = Command(DummyCommandData, signers.toList()) + +object DummyCommandData : TypeOnlyCommandData() + +val DUMMY_IDENTITY_1: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_PARTY) +val DUMMY_PARTY: Party get() = Party(getX500Name(O = "Dummy", L = "Madrid", C = "ES"), DUMMY_KEY_1.public) diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt similarity index 90% rename from test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index d3dd0e6bfb..c641c69b67 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/testing/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.NULL_SIGNATURE +import net.corda.core.crypto.NullKeys.NULL_SIGNATURE +import net.corda.core.crypto.CompositeKey import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction +import net.corda.testing.contracts.DummyContract import java.io.InputStream import java.security.KeyPair import java.security.PublicKey @@ -243,10 +244,13 @@ data class TestLedgerDSLInterpreter private constructor( transactionLabel: String?, transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> R, - transactionMap: HashMap = HashMap() + transactionMap: HashMap = HashMap(), + /** If set to true, will add dummy components to [transactionBuilder] to make it valid. */ + fillTransaction: Boolean = false ): WireTransaction { val transactionLocation = getCallerLocation() val transactionInterpreter = interpretTransactionDsl(transactionBuilder, dsl) + if (fillTransaction) fillTransaction(transactionBuilder) // Create the WireTransaction val wireTransaction = transactionInterpreter.toWireTransaction() // Record the output states @@ -262,6 +266,20 @@ data class TestLedgerDSLInterpreter private constructor( return wireTransaction } + /** + * This method fills the transaction builder with dummy components to satisfy the base transaction validity rules. + * + * A common pattern in our tests is using a base transaction and expressing the test cases using [tweak]s. + * The base transaction may not be valid, but it still gets recorded to the ledger. This causes a test failure, + * even though is not being used for anything afterwards. + */ + private fun fillTransaction(transactionBuilder: TransactionBuilder) { + if (transactionBuilder.commands().isEmpty()) transactionBuilder.addCommand(dummyCommand()) + if (transactionBuilder.inputStates().isEmpty() && transactionBuilder.outputStates().isEmpty()) { + transactionBuilder.addOutputState(DummyContract.SingleOwnerState(owner = ALICE)) + } + } + override fun _transaction( transactionLabel: String?, transactionBuilder: TransactionBuilder, @@ -272,7 +290,7 @@ data class TestLedgerDSLInterpreter private constructor( transactionLabel: String?, transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> Unit - ) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, nonVerifiedTransactionWithLocations) + ) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, nonVerifiedTransactionWithLocations, fillTransaction = true) override fun tweak( dsl: LedgerDSL get() = nonVerifiedTransactionWithLocations.values.map { it.transaction } } +/** + * 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() + /** * Signs all transactions passed in. * @param transactionsToSign Transactions to be signed. diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestDependencyInjectionBase.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestDependencyInjectionBase.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/TestDependencyInjectionBase.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/TestDependencyInjectionBase.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestTimestamp.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestTimestamp.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/TestTimestamp.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/TestTimestamp.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt similarity index 94% rename from test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt index 92b9604350..544d050f54 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt @@ -78,9 +78,6 @@ class TransactionDSL(val interpreter: T) : Tr fun input(state: ContractState) { val transaction = ledgerInterpreter._unverifiedTransaction(null, TransactionBuilder(notary = DUMMY_NOTARY)) { output { state } - // Add a dummy randomised output so that the transaction id differs when issuing the same state multiple times - val nonceState = DummyContract.SingleOwnerState(Random().nextInt(), DUMMY_NOTARY) - output { nonceState } } input(transaction.outRef(0).ref) } diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContract.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt similarity index 98% rename from test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt index 220e17d629..f8103acecc 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyContractV2.kt @@ -32,7 +32,7 @@ class DummyContractV2 : UpgradedContract = listOf(), ref: String) : this(contract, participants, UniqueIdentifier(ref)) - override fun isRelevant(ourKeys: Set): Boolean { - return participants.any { it.owningKey.containsAny(ourKeys) } - } - override fun generateAgreement(notary: Party): TransactionBuilder { throw UnsupportedOperationException("not implemented") } diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt similarity index 94% rename from test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt index c2bb95fd09..408fefffd3 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt @@ -39,10 +39,6 @@ class DummyLinearContract : Contract { val linearBoolean: Boolean = true, val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState, QueryableState { - override fun isRelevant(ourKeys: Set): Boolean { - return participants.any { it.owningKey.containsAny(ourKeys) } - } - override fun supportedSchemas(): Iterable = listOf(DummyLinearStateSchemaV1, DummyLinearStateSchemaV2) override fun generateMappedObject(schema: MappedSchema): PersistentState { diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt similarity index 95% rename from test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt index dbbe571af5..743934e921 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt @@ -30,7 +30,7 @@ class HttpApi(val root: URL, val mapper: ObjectMapper = defaultMapper) { 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() + net.corda.client.jackson.JacksonSupport.createNonRpcMapper() } } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt similarity index 96% rename from test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt index 94eef77537..6a65441726 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt @@ -22,7 +22,7 @@ object HttpUtils { .readTimeout(60, TimeUnit.SECONDS).build() } val defaultMapper: ObjectMapper by lazy { - net.corda.jackson.JacksonSupport.createNonRpcMapper() + net.corda.client.jackson.JacksonSupport.createNonRpcMapper() } fun putJson(url: URL, data: String): Boolean { diff --git a/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt similarity index 91% rename from test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt index 12ac044952..c14b750e0c 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt @@ -1,13 +1,13 @@ package net.corda.testing.messaging import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.getX500Name import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.config.SSLConfiguration import net.corda.testing.configureTestSSL import org.apache.activemq.artemis.api.core.client.* -import org.bouncycastle.asn1.x500.X500Name /** * As the name suggests this is a simple client for connecting to MQ brokers. @@ -15,7 +15,7 @@ import org.bouncycastle.asn1.x500.X500Name 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") + val DEFAULT_MQ_LEGAL_NAME = getX500Name(O = "SimpleMQClient", OU = "corda", L = "London", C = "GB") } lateinit var sessionFactory: ClientSessionFactory lateinit var session: ClientSession diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockAttachmentStorage.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockAttachmentStorage.kt new file mode 100644 index 0000000000..618060be0e --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockAttachmentStorage.kt @@ -0,0 +1,41 @@ +package net.corda.testing.node + +import net.corda.core.contracts.Attachment +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 +import net.corda.core.internal.AbstractAttachment +import net.corda.core.node.services.AttachmentStorage +import net.corda.core.serialization.SingletonSerializeAsToken +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.InputStream +import java.util.HashMap +import java.util.jar.JarInputStream + +class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { + val files = HashMap() + + override fun openAttachment(id: SecureHash): Attachment? { + val f = files[id] ?: return null + return object : AbstractAttachment({ f }) { + override val id = id + } + } + + override fun importAttachment(jar: InputStream): SecureHash { + // JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here. + require(jar !is JarInputStream) + + val bytes = run { + val s = ByteArrayOutputStream() + jar.copyTo(s) + s.close() + s.toByteArray() + } + val sha256 = bytes.sha256() + if (files.containsKey(sha256)) + throw FileAlreadyExistsException(File("!! MOCK FILE NAME")) + files[sha256] = bytes + return sha256 + } +} diff --git a/test-utils/src/main/kotlin/net/corda/testing/performance/Rate.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/performance/Rate.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/performance/Rate.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/performance/Rate.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt diff --git a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt similarity index 100% rename from test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt diff --git a/test-utils/src/main/resources/META-INF/services/net.corda.node.services.statemachine.FlowStackSnapshotFactory b/testing/test-utils/src/main/resources/META-INF/services/net.corda.node.services.statemachine.FlowStackSnapshotFactory similarity index 100% rename from test-utils/src/main/resources/META-INF/services/net.corda.node.services.statemachine.FlowStackSnapshotFactory rename to testing/test-utils/src/main/resources/META-INF/services/net.corda.node.services.statemachine.FlowStackSnapshotFactory diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index 5a9345ffd9..6a3452b8c9 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.tornadofx_version = '1.7.3' + ext.tornadofx_version = '1.7.10' ext.jna_version = '4.1.0' ext.purejavacomm_version = '0.0.18' ext.controlsfx_version = '8.40.12' 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 cfec8b3757..1bf02e2a52 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,6 +1,8 @@ package net.corda.demobench.model import com.typesafe.config.Config +import net.corda.core.node.services.ServiceInfo +import net.corda.core.node.services.ServiceType import net.corda.core.utilities.parseNetworkHostAndPort import org.bouncycastle.asn1.x500.X500Name import tornadofx.* @@ -51,14 +53,14 @@ class InstallFactory : Controller() { return port } - private fun Config.parseExtraServices(path: String): List { - val services = serviceController.services.toSortedSet() + private fun Config.parseExtraServices(path: String): MutableList { + val services = serviceController.services.toSortedSet() + ServiceInfo(ServiceType.networkMap).toString() return this.getStringList(path) .filter { !it.isNullOrEmpty() } .map { svc -> require(svc in services, { "Unknown service '$svc'." }) svc - }.toList() + }.toMutableList() } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt index 9aef0146f8..4aaccb3558 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt @@ -1,13 +1,11 @@ package net.corda.demobench.model -import net.corda.core.crypto.commonName +import net.corda.core.utilities.organisation import net.corda.core.utilities.WHITESPACE import org.bouncycastle.asn1.x500.X500Name open class NetworkMapConfig(val legalName: X500Name, val p2pPort: Int) { - - val key: String = legalName.commonName.toKey() - + val key: String = legalName.organisation.toKey() } fun String.stripWhitespace() = replace(WHITESPACE, "") diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index 64406da824..275ce76a45 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -1,7 +1,7 @@ package net.corda.demobench.model import com.typesafe.config.* -import net.corda.core.crypto.locationOrNull +import net.corda.core.utilities.locality import net.corda.nodeapi.User import org.bouncycastle.asn1.x500.X500Name import java.io.File @@ -16,7 +16,7 @@ class NodeConfig( val rpcPort: Int, val webPort: Int, val h2Port: Int, - val extraServices: List, + val extraServices: MutableList = mutableListOf(), val users: List = listOf(defaultUser), var networkMap: NetworkMapConfig? = null ) : NetworkMapConfig(legalName, p2pPort), HasPlugins { @@ -26,7 +26,7 @@ class NodeConfig( val defaultUser = user("guest") } - val nearestCity: String = legalName.locationOrNull ?: "Unknown location" + val nearestCity: String = legalName.locality val nodeDir: Path = baseDir.resolve(key) override val pluginDir: Path = nodeDir.resolve("plugins") val explorerDir: Path = baseDir.resolve("$key-explorer") diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index 0b484aacda..8fdb371618 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -1,6 +1,8 @@ package net.corda.demobench.model -import net.corda.core.crypto.getX509Name +import net.corda.core.node.services.ServiceInfo +import net.corda.core.node.services.ServiceType +import net.corda.core.utilities.getX500Name import net.corda.demobench.plugin.PluginController import net.corda.demobench.pty.R3Pty import tornadofx.* @@ -52,17 +54,16 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { val location = nodeData.nearestCity.value val config = NodeConfig( baseDir, - getX509Name( - myLegalName = nodeData.legalName.value.trim(), - email = "corda@city.${location.countryCode.toLowerCase()}.example", - nearestCity = location.description, - country = location.countryCode + getX500Name( + O = nodeData.legalName.value.trim(), + L = location.description, + C = location.countryCode ), nodeData.p2pPort.value, nodeData.rpcPort.value, nodeData.webPort.value, nodeData.h2Port.value, - nodeData.extraServices.value + nodeData.extraServices.toMutableList() ) if (nodes.putIfAbsent(config.key, config) != null) { @@ -98,6 +99,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { if (hasNetworkMap()) { config.networkMap = networkMapConfig } else { + config.extraServices.add(ServiceInfo(ServiceType.networkMap).toString()) networkMapConfig = config log.info("Network map provided by: ${config.legalName}") } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt index 64961f747a..869adc5735 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt @@ -4,8 +4,8 @@ import com.jediterm.terminal.ui.JediTermWidget import com.jediterm.terminal.ui.UIUtil import com.jediterm.terminal.ui.settings.SettingsProvider import com.pty4j.PtyProcess -import net.corda.core.crypto.commonName import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.organisation import org.bouncycastle.asn1.x500.X500Name import java.awt.Dimension import java.io.IOException @@ -34,7 +34,7 @@ class R3Pty(val name: X500Name, settings: SettingsProvider, dimension: Dimension val process = PtyProcess.exec(command, environment, workingDir) try { - return PtyProcessTtyConnector(name.commonName, process, UTF_8) + return PtyProcessTtyConnector(name.organisation, process, UTF_8) } catch (e: Exception) { process.destroyForcibly() process.waitFor(30, SECONDS) 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 cf9822f969..3597b530e2 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 @@ -14,15 +14,15 @@ import javafx.scene.layout.Pane import javafx.scene.layout.Priority import javafx.stage.FileChooser import javafx.util.StringConverter -import net.corda.core.crypto.commonName import net.corda.core.internal.div import net.corda.core.internal.exists +import net.corda.core.internal.readAllLines +import net.corda.core.internal.writeLines import net.corda.core.node.CityDatabase import net.corda.core.node.WorldMapLocation -import net.corda.core.internal.readAllLines import net.corda.core.utilities.normaliseLegalName +import net.corda.core.utilities.organisation import net.corda.core.utilities.validateLegalName -import net.corda.core.internal.writeLines import net.corda.demobench.model.* import net.corda.demobench.ui.CloseableTab import org.controlsfx.control.CheckListView @@ -267,7 +267,7 @@ class NodeTabView : Fragment() { if (countryCode != null) { nodeTab.graphic = ImageView(flags.get()[countryCode]).apply { fitWidth = 24.0; isPreserveRatio = true } } - nodeTab.text = config.legalName.commonName + nodeTab.text = config.legalName.organisation nodeTerminalView.open(config) { exitCode -> Platform.runLater { if (exitCode == 0) { 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 5c30ae9e2c..e5a8bff1fd 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 @@ -15,7 +15,7 @@ import javafx.scene.layout.VBox import javafx.util.Duration import net.corda.core.concurrent.match import net.corda.core.contracts.ContractState -import net.corda.core.crypto.commonName +import net.corda.core.utilities.organisation import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.vaultTrackBy import net.corda.core.node.services.vault.PageSpecification @@ -71,7 +71,7 @@ class NodeTerminalView : Fragment() { private lateinit var swingTerminal: SwingNode fun open(config: NodeConfig, onExit: (Int) -> Unit) { - nodeName.text = config.legalName.commonName + nodeName.text = config.legalName.organisation swingTerminal = SwingNode() swingTerminal.setOnMouseClicked { @@ -187,7 +187,7 @@ class NodeTerminalView : Fragment() { private fun initialise(config: NodeConfig, ops: CordaRPCOps) { try { - val (txInit, txNext) = ops.verifiedTransactionsFeed() + val (txInit, txNext) = ops.internalVerifiedTransactionsFeed() val (stateInit, stateNext) = ops.vaultTrackBy(paging = pageSpecification) txCount = txInit.size diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NetworkMapConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NetworkMapConfigTest.kt index 62ad3d41a6..324d09aa9c 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NetworkMapConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NetworkMapConfigTest.kt @@ -8,7 +8,7 @@ class NetworkMapConfigTest { @Test fun keyValue() { - val config = NetworkMapConfig(X500Name("CN=My\tNasty Little\rLabel\n"), 10000) + val config = NetworkMapConfig(X500Name("O=My\tNasty Little\rLabel\n"), 10000) assertEquals("mynastylittlelabel", config.key) } 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 6fee64384a..db2b61410c 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 @@ -7,11 +7,12 @@ import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigValueFactory import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort -import net.corda.testing.DUMMY_NOTARY +import net.corda.core.utilities.getX500Name import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.config.FullNodeConfiguration import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs +import net.corda.testing.DUMMY_NOTARY import net.corda.webserver.WebServerConfig import org.bouncycastle.asn1.x500.X500Name import org.junit.Test @@ -28,7 +29,7 @@ class NodeConfigTest { companion object { private val baseDir: Path = Paths.get(".").toAbsolutePath() - private val myLegalName = X500Name("CN=My Name,OU=Corda QA Department,O=R3 CEV,L=New York,C=US") + private val myLegalName = getX500Name(OU = "Corda QA Department", O = "My Name", L = "New York", C = "US") } @Test @@ -82,7 +83,7 @@ class NodeConfigTest { @Test fun `test services`() { - val config = createConfig(services = listOf("my.service")) + val config = createConfig(services = mutableListOf("my.service")) assertEquals(listOf("my.service"), config.extraServices) } @@ -107,13 +108,13 @@ class NodeConfigTest { @Test fun `test cash issuer`() { - val config = createConfig(services = listOf("corda.issuer.GBP")) + val config = createConfig(services = mutableListOf("corda.issuer.GBP")) assertTrue(config.isCashIssuer) } @Test fun `test not cash issuer`() { - val config = createConfig(services = listOf("corda.issuerubbish")) + val config = createConfig(services = mutableListOf("corda.issuerubbish")) assertFalse(config.isCashIssuer) } @@ -138,14 +139,14 @@ class NodeConfigTest { rpcPort = 40002, webPort = 20001, h2Port = 30001, - services = listOf("my.service"), + services = mutableListOf("my.service"), users = listOf(user("jenny")) ) assertEquals(prettyPrint("{" + "\"detectPublicIp\":false," + "\"extraAdvertisedServiceIds\":[\"my.service\"]," + "\"h2port\":30001," - + "\"myLegalName\":\"CN=My Name,OU=Corda QA Department,O=R3 CEV,L=New York,C=US\"," + + "\"myLegalName\":\"C=US,L=New York,O=My Name,OU=Corda QA Department\"," + "\"p2pAddress\":\"localhost:10001\"," + "\"rpcAddress\":\"localhost:40002\"," + "\"rpcUsers\":[" @@ -164,7 +165,7 @@ class NodeConfigTest { rpcPort = 40002, webPort = 20001, h2Port = 30001, - services = listOf("my.service"), + services = mutableListOf("my.service"), users = listOf(user("jenny")) ) config.networkMap = NetworkMapConfig(DUMMY_NOTARY.name, 12345) @@ -173,8 +174,8 @@ class NodeConfigTest { + "\"detectPublicIp\":false," + "\"extraAdvertisedServiceIds\":[\"my.service\"]," + "\"h2port\":30001," - + "\"myLegalName\":\"CN=My Name,OU=Corda QA Department,O=R3 CEV,L=New York,C=US\"," - + "\"networkMapService\":{\"address\":\"localhost:12345\",\"legalName\":\"CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH\"}," + + "\"myLegalName\":\"C=US,L=New York,O=My Name,OU=Corda QA Department\"," + + "\"networkMapService\":{\"address\":\"localhost:12345\",\"legalName\":\"C=CH,L=Zurich,O=Notary Service,OU=corda\"}," + "\"p2pAddress\":\"localhost:10001\"," + "\"rpcAddress\":\"localhost:40002\"," + "\"rpcUsers\":[" @@ -193,7 +194,7 @@ class NodeConfigTest { rpcPort = 40002, webPort = 20001, h2Port = 30001, - services = listOf("my.service"), + services = mutableListOf("my.service"), users = listOf(user("jenny")) ) config.networkMap = NetworkMapConfig(DUMMY_NOTARY.name, 12345) @@ -223,7 +224,7 @@ class NodeConfigTest { rpcPort = 40002, webPort = 20001, h2Port = 30001, - services = listOf("my.service"), + services = mutableListOf("my.service"), users = listOf(user("jenny")) ) config.networkMap = NetworkMapConfig(DUMMY_NOTARY.name, 12345) @@ -252,12 +253,12 @@ class NodeConfigTest { } private fun createConfig( - legalName: X500Name = X500Name("CN=Unknown,O=R3,OU=corda,L=Nowhere,C=GB"), + legalName: X500Name = getX500Name(O = "Unknown", OU = "corda", L = "Nowhere", C = "GB"), p2pPort: Int = -1, rpcPort: Int = -1, webPort: Int = -1, h2Port: Int = -1, - services: List = listOf("extra.service"), + services: MutableList = mutableListOf("extra.service"), users: List = listOf(user("guest")) ) = NodeConfig( baseDir, 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 d476ccc93a..2aaec00fb9 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,6 +1,6 @@ package net.corda.demobench.model -import net.corda.core.crypto.getX509Name +import net.corda.core.utilities.getX500Name import net.corda.nodeapi.User import net.corda.testing.DUMMY_NOTARY import org.junit.Test @@ -170,15 +170,14 @@ class NodeControllerTest { rpcPort: Int = -1, webPort: Int = -1, h2Port: Int = -1, - services: List = listOf("extra.service"), + services: MutableList = mutableListOf("extra.service"), users: List = listOf(user("guest")) ) = NodeConfig( baseDir, - legalName = getX509Name( - myLegalName = commonName, - nearestCity = "New York", - country = "US", - email = "corda@city.us.example" + legalName = getX500Name( + O = commonName, + L = "New York", + C = "US" ), p2pPort = p2pPort, rpcPort = rpcPort, diff --git a/tools/explorer/build.gradle b/tools/explorer/build.gradle index 3fac788e92..643f6463a3 100644 --- a/tools/explorer/build.gradle +++ b/tools/explorer/build.gradle @@ -1,8 +1,5 @@ repositories { mavenCentral() - maven { - url 'https://dl.bintray.com/kotlin/exposed' - } } apply plugin: 'java' @@ -30,7 +27,7 @@ dependencies { compile project(':core') compile project(':client:jfx') compile project(':client:mock') - compile project(':test-utils') + compile project(':node-driver') compile project(':finance') // Capsule is a library for building independently executable fat JARs. diff --git a/tools/explorer/capsule/build.gradle b/tools/explorer/capsule/build.gradle index 09a4005c6d..e872daf3b9 100644 --- a/tools/explorer/capsule/build.gradle +++ b/tools/explorer/capsule/build.gradle @@ -12,9 +12,6 @@ repositories { url 'http://oss.sonatype.org/content/repositories/snapshots' } jcenter() - maven { - url 'https://dl.bintray.com/kotlin/exposed' - } } // Force the Caplet to target Java 6. This ensures that running 'java -jar explorer.jar' on any Java 6 VM upwards 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 3eda29511e..5e6646d76f 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -16,6 +16,7 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.getX500Name import net.corda.finance.GBP import net.corda.finance.USD import net.corda.finance.contracts.asset.Cash @@ -34,7 +35,6 @@ 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.* @@ -71,20 +71,20 @@ class ExplorerSimulation(val options: OptionSet) { val portAllocation = PortAllocation.Incremental(20000) driver(portAllocation = portAllocation) { // TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo. - val notary = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)), + val notary = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)), customOverrides = mapOf("nearestCity" to "Zurich")) - val alice = startNode(ALICE.name, rpcUsers = arrayListOf(user), + val alice = startNode(providedName = ALICE.name, rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "Milan")) - val bob = startNode(BOB.name, rpcUsers = arrayListOf(user), + val bob = startNode(providedName = BOB.name, rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "Madrid")) - val ukBankName = X500Name("CN=UK Bank Plc,O=UK Bank Plc,L=London,C=GB") - val usaBankName = X500Name("CN=USA Bank Corp,O=USA Bank Corp,L=New York,C=USA") - val issuerGBP = startNode(ukBankName, rpcUsers = arrayListOf(manager), + val ukBankName = getX500Name(O = "UK Bank Plc", L = "London", C = "GB") + val usaBankName = getX500Name(O = "USA Bank Corp", L = "New York", C = "USA") + val issuerGBP = startNode(providedName = ukBankName, rpcUsers = arrayListOf(manager), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.GBP"))), customOverrides = mapOf("nearestCity" to "London")) - val issuerUSD = startNode(usaBankName, rpcUsers = arrayListOf(manager), + val issuerUSD = startNode(providedName = usaBankName, rpcUsers = arrayListOf(manager), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.USD"))), customOverrides = mapOf("nearestCity" to "New York")) diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/formatters/PartyNameFormatter.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/formatters/PartyNameFormatter.kt index 0347e9924f..2af31ad3ee 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/formatters/PartyNameFormatter.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/formatters/PartyNameFormatter.kt @@ -1,11 +1,11 @@ package net.corda.explorer.formatters -import net.corda.core.crypto.commonName +import net.corda.core.utilities.organisation import org.bouncycastle.asn1.x500.X500Name object PartyNameFormatter { val short = object : Formatter { - override fun format(value: X500Name) = value.commonName + override fun format(value: X500Name) = value.organisation } val full = object : Formatter { diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/GuiUtilities.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/GuiUtilities.kt index 1ea8ac1d5c..1dd067df54 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/GuiUtilities.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/GuiUtilities.kt @@ -14,10 +14,10 @@ import net.corda.client.jfx.model.Models import net.corda.client.jfx.model.NetworkIdentityModel import net.corda.client.jfx.utils.map import net.corda.core.contracts.StateAndRef -import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.finance.contracts.asset.Cash import tornadofx.* +import java.security.PublicKey /** * Helper method to reduce boiler plate code @@ -89,6 +89,6 @@ inline fun UIComponent.getModel(): M = Models.get(M::class, th fun Collection.cross(other: Collection) = this.flatMap { a -> other.map { b -> a to b } } // TODO: This is a temporary fix for the UI to show the correct issuer identity, this will break when we start randomizing keys. More work is needed here when the identity work is done. -fun StateAndRef.resolveIssuer(): ObservableValue = state.data.amount.token.issuer.party.resolveIssuer() +fun StateAndRef.resolveIssuer(): ObservableValue = state.data.amount.token.issuer.party.owningKey.toKnownParty() -fun AbstractParty.resolveIssuer(): ObservableValue = Models.get(NetworkIdentityModel::class, javaClass.kotlin).lookup(owningKey).map { it?.legalIdentity } +fun PublicKey.toKnownParty() = Models.get(NetworkIdentityModel::class, javaClass.kotlin).partyFromPublicKey(this).map { it?.legalIdentity } 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 9df6973b25..0481534d5b 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 @@ -27,10 +27,10 @@ import javafx.util.Duration import net.corda.client.jfx.model.* import net.corda.client.jfx.utils.* import net.corda.core.contracts.ContractState -import net.corda.core.crypto.toBase58String import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.node.ScreenCoordinate +import net.corda.core.utilities.toBase58String import net.corda.explorer.formatters.PartyNameFormatter import net.corda.explorer.model.CordaView import tornadofx.* @@ -79,7 +79,7 @@ class Network : CordaView() { .filterNotNull() .map { it.stateAndRef.state.data }.getParties() val outputParties = it.transaction.tx.outputStates.observable().getParties() - val signingParties = it.transaction.sigs.map { getModel().lookup(it.by) } + val signingParties = it.transaction.sigs.map { it.by.toKnownParty() } // Input parties fire a bullets to all output parties, and to the signing parties. !! This is a rough guess of how the message moves in the network. // TODO : Expose artemis queue to get real message information. inputParties.cross(outputParties) + inputParties.cross(signingParties) @@ -99,7 +99,7 @@ class Network : CordaView() { copyableLabel(SimpleObjectProperty(node.legalIdentity.owningKey.toBase58String())).apply { minWidth = 400.0 } } row("Services :") { label(node.advertisedServices.map { it.info }.joinToString(", ")) } - node.worldMapLocation?.apply { row("Location :") { label(this@apply.description) } } + node.getWorldMapLocation()?.apply { row("Location :") { label(this@apply.description) } } } } setOnMouseClicked { @@ -123,7 +123,7 @@ class Network : CordaView() { contentDisplay = ContentDisplay.TOP val coordinate = Bindings.createObjectBinding({ // These coordinates are obtained when we generate the map using TileMill. - node.worldMapLocation?.coordinate?.project(mapPane.width, mapPane.height, 85.0511, -85.0511, -180.0, 180.0) ?: ScreenCoordinate(0.0, 0.0) + node.getWorldMapLocation()?.coordinate?.project(mapPane.width, mapPane.height, 85.0511, -85.0511, -180.0, 180.0) ?: ScreenCoordinate(0.0, 0.0) }, arrayOf(mapPane.widthProperty(), mapPane.heightProperty())) // Center point of the label. layoutXProperty().bind(coordinate.map { it.screenX - width / 2 }) @@ -170,10 +170,10 @@ class Network : CordaView() { zoomOutButton.setOnAction { zoom(0.8) } lastTransactions.addListener { _, _, new -> - new?.forEach { - it.first.value?.let { a -> - it.second.value?.let { b -> - fireBulletBetweenNodes(a.legalIdentity, b.legalIdentity, "bank", "bank") + new?.forEach { (partyA, partyB) -> + partyA.value?.let { a -> + partyB.value?.let { b -> + fireBulletBetweenNodes(a, b, "bank", "bank") } } } @@ -209,7 +209,7 @@ class Network : CordaView() { return Point2D(x, y) } - private fun List.getParties() = map { it.participants.map { getModel().lookup(it.owningKey) } }.flatten() + private fun List.getParties() = map { it.participants.map { it.owningKey.toKnownParty() } }.flatten() private fun fireBulletBetweenNodes(senderNode: Party, destNode: Party, startType: String, endType: String) { val senderNodeComp = allComponentMap[senderNode] ?: return 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 ead286c435..159cc3a1eb 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 @@ -15,18 +15,13 @@ import javafx.scene.layout.GridPane import javafx.scene.layout.VBox import javafx.scene.text.FontWeight import javafx.scene.text.TextAlignment -import net.corda.client.jfx.model.StateMachineData -import net.corda.client.jfx.model.StateMachineDataModel -import net.corda.client.jfx.model.StateMachineStatus -import net.corda.client.jfx.model.observableList -import net.corda.client.jfx.model.observableValue -import net.corda.client.jfx.model.writableValue +import net.corda.client.jfx.model.* import net.corda.client.jfx.utils.map 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.core.utilities.toBase58String import net.corda.explorer.formatters.FlowInitiatorFormatter import net.corda.explorer.formatters.FlowNameFormatter import net.corda.explorer.formatters.PartyNameFormatter 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 b345c7f09c..83d1e69f0c 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 @@ -22,11 +22,12 @@ import net.corda.client.jfx.utils.map import net.corda.client.jfx.utils.sequence import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.commonName -import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toStringShort import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party import net.corda.core.node.NodeInfo +import net.corda.core.utilities.organisation +import net.corda.core.utilities.toBase58String import net.corda.explorer.AmountDiff import net.corda.explorer.formatters.AmountFormatter import net.corda.explorer.formatters.Formatter @@ -70,8 +71,8 @@ class TransactionViewer : CordaView("Transactions") { val id: SecureHash, val inputs: Inputs, val outputs: ObservableList>, - val inputParties: ObservableList>>, - val outputParties: ObservableList>>, + val inputParties: ObservableList>>, + val outputParties: ObservableList>>, val commandTypes: List>, val totalValueEquiv: ObservableValue> ) @@ -135,8 +136,8 @@ class TransactionViewer : CordaView("Transactions") { "Transaction ID" to { tx, s -> "${tx.id}".contains(s, true) }, "Input" to { tx, s -> tx.inputs.resolved.any { it.state.data.contract.javaClass.simpleName.contains(s, true) } }, "Output" to { tx, s -> tx.outputs.any { it.state.data.contract.javaClass.simpleName.contains(s, true) } }, - "Input Party" to { tx, s -> tx.inputParties.any { it.any { it.value?.legalIdentity?.name?.commonName?.contains(s, true) ?: false } } }, - "Output Party" to { tx, s -> tx.outputParties.any { it.any { it.value?.legalIdentity?.name?.commonName?.contains(s, true) ?: false } } }, + "Input Party" to { tx, s -> tx.inputParties.any { it.any { it.value?.name?.organisation?.contains(s, true) ?: false } } }, + "Output Party" to { tx, s -> tx.outputParties.any { it.any { it.value?.name?.organisation?.contains(s, true) ?: false } } }, "Command Type" to { tx, s -> tx.commandTypes.any { it.simpleName.contains(s, true) } } ) root.top = searchField.root @@ -199,13 +200,13 @@ class TransactionViewer : CordaView("Transactions") { }) } - private fun ObservableList>>.formatJoinPartyNames(separator: String = "", formatter: Formatter): String { + private fun ObservableList>>.formatJoinPartyNames(separator: String = ",", formatter: Formatter): String { return flatten().map { - it.value?.legalIdentity?.let { formatter.format(it.name) } + it.value?.let { formatter.format(it.name) } }.filterNotNull().toSet().joinToString(separator) } - private fun ObservableList>.getParties() = map { it.state.data.participants.map { getModel().lookup(it.owningKey) } } + private fun ObservableList>.getParties() = map { it.state.data.participants.map { it.owningKey.toKnownParty() } } private fun ObservableList>.toText() = map { it.contract().javaClass.simpleName }.groupBy { it }.map { "${it.key} (${it.value.size})" }.joinToString() private class TransactionWidget : BorderPane() { @@ -248,8 +249,8 @@ class TransactionViewer : CordaView("Transactions") { outputs.items = transaction.outputs.observable() signatures.children.addAll(signatureData.map { signature -> - val nodeInfo = getModel().lookup(signature) - copyableLabel(nodeInfo.map { "${signature.toStringShort()} (${it?.legalIdentity?.let { PartyNameFormatter.short.format(it.name)} ?: "???"})" }) + val party = signature.toKnownParty() + copyableLabel(party.map { "${signature.toStringShort()} (${it?.let { PartyNameFormatter.short.format(it.name) } ?: "Anonymous"})" }) }) } @@ -276,7 +277,7 @@ class TransactionViewer : CordaView("Transactions") { row { label("Issuer :") { gridpaneConstraints { hAlignment = HPos.RIGHT } } val anonymousIssuer: AbstractParty = data.amount.token.issuer.party - val issuer: AbstractParty = anonymousIssuer.resolveIssuer().value ?: anonymousIssuer + val issuer: AbstractParty = anonymousIssuer.owningKey.toKnownParty().value ?: anonymousIssuer // TODO: Anonymous should probably be italicised or similar label(issuer.nameOrNull()?.let { PartyNameFormatter.short.format(it) } ?: "Anonymous") { tooltip(anonymousIssuer.owningKey.toBase58String()) @@ -284,9 +285,8 @@ class TransactionViewer : CordaView("Transactions") { } row { label("Owner :") { gridpaneConstraints { hAlignment = HPos.RIGHT } } - val owner = data.owner - val nodeInfo = getModel().lookup(owner.owningKey) - label(nodeInfo.map { it?.legalIdentity?.let { PartyNameFormatter.short.format(it.name) } ?: "???" }) { + val owner = data.owner.owningKey.toKnownParty() + label(owner.map { it?.let { PartyNameFormatter.short.format(it.name) } ?: "Anonymous" }) { tooltip(data.owner.owningKey.toBase58String()) } } @@ -300,27 +300,28 @@ class TransactionViewer : CordaView("Transactions") { } private fun StateAndRef.contract() = this.state.data.contract + } /** * We calculate the total value by subtracting relevant input states and adding relevant output states, as long as they're cash */ -private fun calculateTotalEquiv(identity: NodeInfo?, +private fun calculateTotalEquiv(myIdentity: NodeInfo?, reportingCurrencyExchange: Pair) -> Amount>, inputs: List, outputs: List): AmountDiff { val (reportingCurrency, exchange) = reportingCurrencyExchange - val legalIdentity = identity?.legalIdentity + val myLegalIdentity = myIdentity?.legalIdentity fun List.sum() = this.map { it as? Cash.State } .filterNotNull() - .filter { legalIdentity == it.owner } + .filter { it.owner.owningKey.toKnownParty().value == myLegalIdentity } .map { exchange(it.amount.withoutIssuer()).quantity } .sum() // For issuing cash, if I am the issuer and not the owner (e.g. issuing cash to other party), count it as negative. val issuedAmount = if (inputs.isEmpty()) outputs.map { it as? Cash.State } .filterNotNull() - .filter { legalIdentity == it.amount.token.issuer.party && legalIdentity != it.owner } + .filter { it.amount.token.issuer.party.owningKey.toKnownParty().value == myLegalIdentity && it.owner.owningKey.toKnownParty().value != myLegalIdentity } .map { exchange(it.amount.withoutIssuer()).quantity } .sum() else 0 diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt index 48a2b188ea..ab5bdfb82a 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt @@ -22,8 +22,8 @@ import net.corda.client.jfx.utils.* import net.corda.core.contracts.Amount import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.withoutIssuer -import net.corda.core.crypto.commonName import net.corda.core.identity.AbstractParty +import net.corda.core.utilities.organisation import net.corda.explorer.formatters.AmountFormatter import net.corda.explorer.formatters.PartyNameFormatter import net.corda.explorer.identicon.identicon @@ -148,7 +148,7 @@ class CashViewer : CordaView("Cash") { */ val searchField = SearchField(cashStates, "Currency" to { state, text -> state.state.data.amount.token.product.toString().contains(text, true) }, - "Issuer" to { state, text -> state.resolveIssuer().value?.name?.commonName?.contains(text, true) ?: false } + "Issuer" to { state, text -> state.resolveIssuer().value?.name?.organisation?.contains(text, true) ?: false } ) root.top = hbox(5.0) { button("New Transaction", FontAwesomeIconView(FontAwesomeIcon.PLUS)) { @@ -174,11 +174,11 @@ class CashViewer : CordaView("Cash") { * Next we create subgroups based on currency. [memberStates] here is all states holding currency [currency] issued by [issuer] above. * Note that these states will not be displayed in the TreeTable, but rather in the side pane if the user clicks on the row. */ - val currencyNodes = AggregatedList(memberStates, { it.state.data.amount.token.product }) { currency, memberStates -> + val currencyNodes = AggregatedList(memberStates, { it.state.data.amount.token.product }) { currency, groupedMemberStates -> /** * We sum the states in the subgroup, to be displayed in the "Local Currency" column */ - val amounts = memberStates.map { it.state.data.amount.withoutIssuer() } + val amounts = groupedMemberStates.map { it.state.data.amount.withoutIssuer() } val sumAmount = amounts.foldObservable(Amount(0, currency), Amount::plus) /** @@ -190,7 +190,7 @@ class CashViewer : CordaView("Cash") { /** * Finally assemble the actual TreeTable Currency node. */ - TreeItem(ViewerNode.CurrencyNode(sumAmount, equivSumAmount, memberStates)) + TreeItem(ViewerNode.CurrencyNode(sumAmount, equivSumAmount, groupedMemberStates)) } /** @@ -205,7 +205,7 @@ class CashViewer : CordaView("Cash") { /** * Assemble the Issuer node. */ - val treeItem = TreeItem(ViewerNode.IssuerNode(issuer.resolveIssuer().value ?: issuer, equivSumAmount, memberStates)) + val treeItem = TreeItem(ViewerNode.IssuerNode(issuer.owningKey.toKnownParty().value ?: issuer, equivSumAmount, memberStates)) /** * Bind the children in the TreeTable structure. diff --git a/tools/loadtest/build.gradle b/tools/loadtest/build.gradle index 68a5a50774..af7012b2e6 100644 --- a/tools/loadtest/build.gradle +++ b/tools/loadtest/build.gradle @@ -6,7 +6,7 @@ mainClassName = 'net.corda.loadtest.MainKt' dependencies { compile project(':client:mock') compile project(':client:rpc') - compile project(':test-utils') + compile project(':node-driver') // https://mvnrepository.com/artifact/com.jcraft/jsch compile group: 'com.jcraft', name: 'jsch', version: '0.1.54' diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt index 254b8aacb2..bff6f4fd9b 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt @@ -2,7 +2,7 @@ package net.corda.loadtest import com.google.common.util.concurrent.RateLimiter import net.corda.client.mock.Generator -import net.corda.core.crypto.toBase58String +import net.corda.core.utilities.toBase58String import net.corda.node.services.network.NetworkMapService import net.corda.testing.driver.PortAllocation import org.slf4j.LoggerFactory @@ -185,7 +185,7 @@ fun runLoadTests(configuration: LoadTestConfiguration, tests: List): Generator = Generator.int().combine(publicKeyGenerator) { n, key -> - Party(getTestX509Name("Party$n"), key) + Party(getX500Name(O = "Party$n", L = "London", C = "GB"), key) } fun pickOneOrMaybeNew(from: Collection, generator: Generator): Generator { 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 b97f5845dc..d9aa442501 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -3,13 +3,14 @@ package net.corda.verifier import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import net.corda.core.concurrent.CordaFuture -import net.corda.core.crypto.commonName import net.corda.core.crypto.random63BitValue import net.corda.core.internal.concurrent.* import net.corda.core.internal.div import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.getX500Name import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.organisation import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.ArtemisTcpTransport @@ -18,7 +19,6 @@ import net.corda.nodeapi.VerifierApi import net.corda.nodeapi.config.NodeSSLConfiguration import net.corda.nodeapi.config.SSLConfiguration import net.corda.testing.driver.* -import net.corda.testing.getTestX509Name import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ClientProducer @@ -181,7 +181,7 @@ data class VerifierDriverDSL( } private fun startVerificationRequestorInternal(name: X500Name, hostAndPort: NetworkHostAndPort): VerificationRequestorHandle { - val baseDir = driverDSL.driverDirectory / name.commonName + val baseDir = driverDSL.driverDirectory / name.organisation val sslConfig = object : NodeSSLConfiguration { override val baseDirectory = baseDir override val keyStorePassword: String get() = "cordacadevpass" @@ -249,8 +249,8 @@ data class VerifierDriverDSL( val id = verifierCount.andIncrement val jdwpPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null val processFuture = driverDSL.executorService.fork { - val verifierName = getTestX509Name("verifier$id") - val baseDirectory = driverDSL.driverDirectory / verifierName.commonName + val verifierName = getX500Name(O = "Verifier$id", L = "London", C = "GB") + val baseDirectory = driverDSL.driverDirectory / verifierName.organisation val config = createConfiguration(baseDirectory, address) val configFilename = "verifier.conf" writeConfig(baseDirectory, configFilename, config) 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 3698019574..6fa26cf776 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -113,8 +113,8 @@ class VerifierTests { @Test fun `single verifier works with a node`() { verifierDriver(networkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = true)) { - val aliceFuture = startNode(ALICE.name) - val notaryFuture = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type)), verifierType = VerifierType.OutOfProcess) + val aliceFuture = startNode(providedName = ALICE.name) + val notaryFuture = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type)), verifierType = VerifierType.OutOfProcess) val alice = aliceFuture.get() val notary = notaryFuture.get() startVerifier(notary) diff --git a/verify-enclave/build.gradle b/verify-enclave/build.gradle index 85bbc5e646..27273ebe48 100644 --- a/verify-enclave/build.gradle +++ b/verify-enclave/build.gradle @@ -43,7 +43,7 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - testCompile project(':test-utils') + testCompile project(':node-driver') } jar { 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 a34a97b9eb..b00df8c7f8 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 @@ -6,6 +6,7 @@ import net.corda.finance.POUNDS import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER +import net.corda.testing.DummyCommandData import net.corda.testing.MEGA_CORP_PUBKEY import net.corda.testing.MINI_CORP_PUBKEY import net.corda.testing.ledger @@ -62,6 +63,7 @@ class EnclaveletTest { val wtx3 = transaction { input("c1") input("c2") + command(DUMMY_CASH_ISSUER.party.owningKey, DummyCommandData) output(Cash.State(3000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MINI_CORP_PUBKEY))) failsWith("Required ${Cash.Commands.Move::class.java.canonicalName} command") } diff --git a/webserver/build.gradle b/webserver/build.gradle index a4f729f54a..15fa109983 100644 --- a/webserver/build.gradle +++ b/webserver/build.gradle @@ -57,7 +57,7 @@ dependencies { // For rendering the index page. compile "org.jetbrains.kotlinx:kotlinx-html-jvm:0.6.3" - testCompile project(':test-utils') + integrationTestCompile project(':node-driver') testCompile "junit:junit:$junit_version" } 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 bac83b0eab..cd4f35577d 100644 --- a/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt +++ b/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt @@ -27,7 +27,7 @@ class DriverTests { @Test fun `starting a node and independent web server works`() { val addr = driver { - val node = startNode(DUMMY_BANK_A.name).getOrThrow() + val node = startNode(providedName = DUMMY_BANK_A.name).getOrThrow() val webserverHandle = startWebserver(node).getOrThrow() webserverMustBeUp(webserverHandle) webserverHandle.listenAddress diff --git a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt index 10fe8410e2..80d337cc39 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt @@ -1,10 +1,10 @@ package net.corda.webserver.internal import com.google.common.html.HtmlEscapers.htmlEscaper +import net.corda.client.jackson.JacksonSupport import net.corda.client.rpc.CordaRPCClient import net.corda.core.messaging.CordaRPCOps import net.corda.core.utilities.loggerFor -import net.corda.jackson.JacksonSupport import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.webserver.WebServerConfig import net.corda.webserver.services.WebServerPluginRegistry diff --git a/webserver/src/main/kotlin/net/corda/webserver/servlets/AttachmentDownloadServlet.kt b/webserver/src/main/kotlin/net/corda/webserver/servlets/AttachmentDownloadServlet.kt index 9171e02e60..9750f595ac 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/servlets/AttachmentDownloadServlet.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/servlets/AttachmentDownloadServlet.kt @@ -1,6 +1,6 @@ package net.corda.webserver.servlets -import net.corda.core.contracts.extractFile +import net.corda.core.internal.extractFile import net.corda.core.crypto.SecureHash import net.corda.core.messaging.CordaRPCOps import net.corda.core.utilities.loggerFor