diff --git a/docs/source/api-contract-constraints.rst b/docs/source/api-contract-constraints.rst index ad00dbc65f..f019212989 100644 --- a/docs/source/api-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -12,6 +12,9 @@ API: Contract Constraints .. note:: Before reading this page, you should be familiar with the key concepts of :doc:`key-concepts-contracts`. +.. note:: As of Corda |corda_version| the `minimumPlatformVersion` required to use these features is 4 + (see :ref:`Network Parameters ` and :doc:`features-versions` for more details). + .. contents:: Reasons for Contract Constraints diff --git a/docs/source/features-versions.rst b/docs/source/features-versions.rst new file mode 100644 index 0000000000..9ed8801926 --- /dev/null +++ b/docs/source/features-versions.rst @@ -0,0 +1,80 @@ +Corda Features to Versions +========================== + +New versions of Corda introduce new features. These fall into one of three categories which have subtle but important implications for +node owners, application developers and network operators. + +The first set are changes that have no impact on application developers or the Corda network protocol. An example would be support for +a new HSM or database system, for example, and which are of interest only to a node's operator. + +The second set are new or changed APIs, which are of interest to CorDapp developers. When a release of Corda ships such features, the +Platform Version of that node is incremented so that a CorDapp that relies on such a new or changed feature can detect this (eg to +prevent it from running on a node without the feature or to trigger an alternative optimised codepath if the feature is present). The +app developer should set the CorDapp's minimumPlatformVersion parameter to signal the minimum Platform Version against which the app +can run or has been tested. If the application has also been tested against a greater platform version and can exploit it if present, +the node can also set the targetPlatformVersion field. + +The third set of changes are those which could affect the operation of a Corda network. Examples would include a change to the +serialisation format or flow/wire protocol, or introduction of a new transaction component. These are changes to the core data model and +these features have the property that it is not safe for any node or application to take advantage of until all nodes on the network +are capable of understanding them. Such features are thus only enabled in a node if the network to which it is connected has published +a minimumPlatformVersion in its network parameters that is greater than or equal to the Corda Platform Version that introduced the +feature. For example, Corda 4.0 nodes, which implement Corda Platform Version 4, can only take advantage of the Corda Reference States +feature when connected to a network with mPV 4. + +If there is a Platform Version below which your application will not run or is not supported, then signal that with the application's +`CorDapp.mPV` field. This will prevent older nodes from running your app. Nodes which support newer Platform Versions may also use this +field to trigger code paths that emulate behaviours that were in force on that older Platform Version to maximise compatibility. However, +if you have tested your app against newer versions of Corda and know your node can take advantage of the new Platform Version behaviours +if present, you can signal this by using `CorDapp.targetPV` to declare the latest Platform Version against which the app has been tested +and is known to work. In this way, it is possible to ship CorDapps that can both run on all nodes supporting some minimum Platform Version +of Corda as well as opt in to newer features should they happen to be available on any given node. + +.. list-table:: Corda Features + :header-rows: 1 + + * - Feature + - Corda Platform Version (PV) + - Min Network Platform Version (network mPV) + - Introduced in OS version + - Introduced in Enterprise version + * - Observer Nodes + - 2 + - 2 + - 2.0 + - n/a + * - Corda Serialization Framework + - 3 + - 3 + - 3.0 + - 3.0 + * - Hash Constraints + - 1 + - 1 + - 1.0 + - 1.0 + * - Whitelist Constraints + - 3 + - 3 + - 3.0 + - 3.0 + * - Inline Finality Flow + - 4 + - 3 + - 4.0 + - 4.0 + * - Reference States + - 4 + - 4 + - 4.0 + - 4.0 + * - Signature Constraints + - 4 + - 4 + - 4.0 + - 4.0 + * - Underlying Support for Accounts + - 5 + - 4 + - 4.3 + - 4.3 diff --git a/docs/source/network-map.rst b/docs/source/network-map.rst index ac40b0ca62..90f428f6c5 100644 --- a/docs/source/network-map.rst +++ b/docs/source/network-map.rst @@ -77,6 +77,8 @@ cluster generated like this can be sized for the maximum size you may need, and More information can be found in :doc:`network-bootstrapper`. +.. _network-parameters: + Network parameters ------------------ @@ -132,6 +134,8 @@ The current set of network parameters: Encountering an owned contract in a JAR that is not signed by the rightful owner is most likely a sign of malicious behaviour, and should be reported. The transaction verification logic will throw an exception when this happens. +.. note:: To determine which `minimumPlatformVersion` a zone must mandate in order to permit all the features of Corda |corda_version| see :doc:`features-versions` + More parameters will be added in future releases to regulate things like allowed port numbers, whether or not IPv6 connectivity is required for zone members, required cryptographic algorithms and roll-out schedules (e.g. for moving to post quantum cryptography), parameters related to SGX and so on. diff --git a/node/src/integration-test/kotlin/net/corda/contracts/serialization/whitelist/WhitelistContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/serialization/whitelist/WhitelistContract.kt new file mode 100644 index 0000000000..b67d2134c9 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/contracts/serialization/whitelist/WhitelistContract.kt @@ -0,0 +1,38 @@ +package net.corda.contracts.serialization.whitelist + +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.identity.AbstractParty +import net.corda.core.transactions.LedgerTransaction + +class WhitelistContract : Contract { + companion object { + const val MAX_VALUE = 2000L + } + + override fun verify(tx: LedgerTransaction) { + val states = tx.outputsOfType() + require(states.isNotEmpty()) { + "Requires at least one data state" + } + + states.forEach { + require(it.whitelistData in WhitelistData(0)..WhitelistData(MAX_VALUE)) { + "WhitelistData $it exceeds maximum value!" + } + } + } + + @Suppress("CanBeParameter", "MemberVisibilityCanBePrivate") + class State(val owner: AbstractParty, val whitelistData: WhitelistData) : ContractState { + override val participants: List = listOf(owner) + + @Override + override fun toString(): String { + return whitelistData.toString() + } + } + + class Operate : CommandData +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/contracts/serialization/whitelist/WhitelistData.kt b/node/src/integration-test/kotlin/net/corda/contracts/serialization/whitelist/WhitelistData.kt new file mode 100644 index 0000000000..42c3e1e5c7 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/contracts/serialization/whitelist/WhitelistData.kt @@ -0,0 +1,15 @@ +package net.corda.contracts.serialization.whitelist + +import net.corda.core.serialization.SerializationWhitelist + +data class WhitelistData(val value: Long) : Comparable { + override fun compareTo(other: WhitelistData): Int { + return value.compareTo(other.value) + } + + override fun toString(): String = "$value things" +} + +class Whitelist : SerializationWhitelist { + override val whitelist = listOf(WhitelistData::class.java) +} diff --git a/node/src/integration-test/kotlin/net/corda/flows/djvm/attachment/SandboxAttachmentFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/djvm/attachment/SandboxAttachmentFlow.kt index e51313602e..ae277dfda2 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/djvm/attachment/SandboxAttachmentFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/djvm/attachment/SandboxAttachmentFlow.kt @@ -6,11 +6,9 @@ import net.corda.core.contracts.Command import net.corda.core.contracts.CommandData 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.transactions.TransactionBuilder -@InitiatingFlow @StartableByRPC class SandboxAttachmentFlow(private val command: CommandData) : FlowLogic() { @Suspendable diff --git a/node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt index ca9ddad1ef..076f17bdff 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt @@ -6,11 +6,9 @@ import net.corda.core.contracts.Command import net.corda.core.contracts.TypeOnlyCommandData 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.transactions.TransactionBuilder -@InitiatingFlow @StartableByRPC class NonDeterministicFlow(private val trouble: TypeOnlyCommandData) : FlowLogic() { @Suspendable diff --git a/node/src/integration-test/kotlin/net/corda/flows/djvm/crypto/DeterministicCryptoFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/djvm/crypto/DeterministicCryptoFlow.kt index 18fb73c380..65e0065eb1 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/djvm/crypto/DeterministicCryptoFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/djvm/crypto/DeterministicCryptoFlow.kt @@ -6,12 +6,10 @@ import net.corda.core.contracts.Command import net.corda.core.contracts.CommandData 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.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes -@InitiatingFlow @StartableByRPC class DeterministicCryptoFlow( private val command: CommandData, diff --git a/node/src/integration-test/kotlin/net/corda/flows/djvm/whitelist/DeterministicWhitelistFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/djvm/whitelist/DeterministicWhitelistFlow.kt index 8a5573f030..fe8824e03c 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/djvm/whitelist/DeterministicWhitelistFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/djvm/whitelist/DeterministicWhitelistFlow.kt @@ -7,11 +7,9 @@ import net.corda.contracts.djvm.whitelist.WhitelistData import net.corda.core.contracts.Command 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.transactions.TransactionBuilder -@InitiatingFlow @StartableByRPC class DeterministicWhitelistFlow(private val data: WhitelistData) : FlowLogic() { @Suspendable diff --git a/node/src/integration-test/kotlin/net/corda/flows/fixup/CordappFixupFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/fixup/CordappFixupFlow.kt index c6731ecd50..d35c556dc9 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/fixup/CordappFixupFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/fixup/CordappFixupFlow.kt @@ -7,11 +7,9 @@ import net.corda.contracts.fixup.dependent.DependentData import net.corda.core.contracts.Command 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.transactions.TransactionBuilder -@InitiatingFlow @StartableByRPC class CordappFixupFlow(private val data: DependentData) : FlowLogic() { @Suspendable diff --git a/node/src/integration-test/kotlin/net/corda/flows/serialization/custom/CustomSerializerFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/serialization/custom/CustomSerializerFlow.kt index e3491748e6..4435418bf9 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/serialization/custom/CustomSerializerFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/serialization/custom/CustomSerializerFlow.kt @@ -7,11 +7,9 @@ import net.corda.contracts.serialization.custom.CustomSerializerContract.Purchas import net.corda.core.contracts.Command 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.transactions.TransactionBuilder -@InitiatingFlow @StartableByRPC class CustomSerializerFlow( private val purchase: Currantsy diff --git a/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerBuilderFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerBuilderFlow.kt index 8353ee6bc5..b5ad46d01a 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerBuilderFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerBuilderFlow.kt @@ -7,11 +7,9 @@ import net.corda.contracts.serialization.missing.MissingSerializerContract.Opera import net.corda.core.contracts.Command 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.transactions.TransactionBuilder -@InitiatingFlow @StartableByRPC class MissingSerializerBuilderFlow(private val value: Long) : FlowLogic() { @Suspendable diff --git a/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerFlow.kt index 8f38f85f72..6bef6b071f 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerFlow.kt @@ -12,14 +12,12 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.internal.createComponentGroups import net.corda.core.internal.requiredContractClassName import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -@InitiatingFlow @StartableByRPC class MissingSerializerFlow(private val value: Long) : FlowLogic() { @Suspendable diff --git a/node/src/integration-test/kotlin/net/corda/flows/serialization/whitelist/WhitelistFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/serialization/whitelist/WhitelistFlow.kt new file mode 100644 index 0000000000..e623d7a00e --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/flows/serialization/whitelist/WhitelistFlow.kt @@ -0,0 +1,26 @@ +package net.corda.flows.serialization.whitelist + +import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.serialization.whitelist.WhitelistContract +import net.corda.contracts.serialization.whitelist.WhitelistContract.State +import net.corda.contracts.serialization.whitelist.WhitelistData +import net.corda.core.contracts.Command +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.transactions.TransactionBuilder + +@StartableByRPC +class WhitelistFlow(private val data: WhitelistData) : FlowLogic() { + @Suspendable + override fun call(): SecureHash { + val notary = serviceHub.networkMapCache.notaryIdentities[0] + val stx = serviceHub.signInitialTransaction( + TransactionBuilder(notary) + .addOutputState(State(ourIdentity, data)) + .addCommand(Command(WhitelistContract.Operate(), ourIdentity.owningKey)) + ) + stx.verify(serviceHub, checkSufficientSignatures = false) + return stx.id + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/ContractWithSerializationWhitelistTest.kt b/node/src/integration-test/kotlin/net/corda/node/ContractWithSerializationWhitelistTest.kt new file mode 100644 index 0000000000..2a9ae80195 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/ContractWithSerializationWhitelistTest.kt @@ -0,0 +1,83 @@ +package net.corda.node + +import net.corda.client.rpc.CordaRPCClient +import net.corda.contracts.serialization.whitelist.WhitelistData +import net.corda.core.contracts.TransactionVerificationException.ContractRejection +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.loggerFor +import net.corda.flows.serialization.whitelist.WhitelistFlow +import net.corda.node.services.Permissions +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import net.corda.testing.driver.internal.incrementalPortAllocation +import net.corda.testing.node.NotarySpec +import net.corda.testing.node.User +import net.corda.testing.node.internal.cordappWithPackages +import org.assertj.core.api.Assertions.assertThat +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters +import kotlin.test.assertFailsWith + +@RunWith(Parameterized::class) +@Suppress("FunctionName") +class ContractWithSerializationWhitelistTest(private val runInProcess: Boolean) { + companion object { + const val DATA = 123456L + + @JvmField + val logger = loggerFor() + + @JvmField + val contractCordapp = cordappWithPackages("net.corda.contracts.serialization.whitelist").signed() + + @JvmField + val workflowCordapp = cordappWithPackages("net.corda.flows.serialization.whitelist").signed() + + fun parametersFor(runInProcess: Boolean): DriverParameters { + return DriverParameters( + portAllocation = incrementalPortAllocation(), + startNodesInProcess = runInProcess, + notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)), + cordappsForAllNodes = listOf(contractCordapp, workflowCordapp) + ) + } + + @Parameters + @JvmStatic + fun modes(): List> = listOf(Array(1) { true }, Array(1) { false }) + + @BeforeClass + @JvmStatic + fun checkData() { + assertNotCordaSerializable() + } + } + + @Test(timeout = 300_000) + fun `test serialization whitelist`() { + logger.info("RUN-IN-PROCESS=$runInProcess") + + val user = User("u", "p", setOf(Permissions.all())) + driver(parametersFor(runInProcess = runInProcess)) { + val badData = WhitelistData(DATA) + val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + val ex = assertFailsWith { + CordaRPCClient(hostAndPort = alice.rpcAddress) + .start(user.username, user.password) + .use { client -> + client.proxy.startFlow(::WhitelistFlow, badData) + .returnValue + .getOrThrow() + } + } + assertThat(ex) + .hasMessageContaining("WhitelistData $badData exceeds maximum value!") + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 345876741e..0eba7d18ad 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -219,7 +219,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: // present in the CorDapp. val result = scanResult.getClassesWithSuperclass(NotaryService::class) + scanResult.getClassesWithSuperclass(SinglePartyNotaryService::class) - if(!result.isEmpty()) { + if (result.isNotEmpty()) { logger.info("Found notary service CorDapp implementations: " + result.joinToString(", ")) } return result.firstOrNull() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index cacaaac303..48c602d359 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -142,11 +142,7 @@ class DriverDSLImpl( private lateinit var _notaries: CordaFuture> override val notaryHandles: List get() = _notaries.getOrThrow() - override val cordappsClassLoader: ClassLoader? = if (!startNodesInProcess) { - createCordappsClassLoader(cordappsForAllNodes) - } else { - null - } + override val cordappsClassLoader: URLClassLoader? = createCordappsClassLoader(cordappsForAllNodes) interface Waitable { @Throws(InterruptedException::class) @@ -195,14 +191,15 @@ class DriverDSLImpl( } override fun shutdown() { - if (waitForAllNodesToFinish) { - state.locked { - processes.forEach { it.waitFor() } + cordappsClassLoader.use { _ -> + if (waitForAllNodesToFinish) { + state.locked { + processes.forEach { it.waitFor() } + } } + _shutdownManager?.shutdown() + _executorService?.shutdownNow() } - _shutdownManager?.shutdown() - _executorService?.shutdownNow() - (cordappsClassLoader as? AutoCloseable)?.close() } private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture): CordaFuture { @@ -992,7 +989,7 @@ class DriverDSLImpl( return config } - private fun createCordappsClassLoader(cordapps: Collection?): ClassLoader? { + private fun createCordappsClassLoader(cordapps: Collection?): URLClassLoader? { if (cordapps == null || cordapps.isEmpty()) { return null } diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt index 27c8f3472f..ee26e6016b 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt @@ -634,6 +634,10 @@ object InteractiveShell { InputStreamSerializer.invokeContext = null InputStreamDeserializer.closeAll() } + if (cmd == "shutdown") { + out.println("Called 'shutdown' on the node.\nQuitting the shell now.").also { out.flush() } + onExit.invoke() + } return result }