From 1a065ef13d5cd687a91099c5781292e351574074 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Fri, 14 Dec 2018 15:48:01 +0000 Subject: [PATCH] CORDA-1942: Moved minimumPlatformVersion and targetPlatformVersion to Cordapp (#4416) Otherwise ContractAndWorkflow ends up having these two duplicated three times. --- .../kotlin/net/corda/core/cordapp/Cordapp.kt | 55 +++++++------ .../core/internal/cordapp/CordappImpl.kt | 61 ++++++++------- .../internal/cordapp/CordappInfoResolver.kt | 30 ++++---- .../core/internal/cordapp/ManifestUtils.kt | 72 +---------------- .../net/corda/core/flows/FinalityFlowTests.kt | 4 +- .../cordapp/CordappInfoResolverTest.kt | 23 ++++-- .../net/corda/node/internal/AbstractNode.kt | 2 +- .../cordapp/JarScanningCordappLoader.kt | 77 +++++++++++++++++-- .../node/internal/cordapp/VirtualCordapps.kt | 8 +- .../cordapp/JarScanningCordappLoaderTest.kt | 14 ++-- .../node/services/FinalityHandlerTest.kt | 2 +- .../testing/internal/InternalTestUtils.kt | 4 +- .../testing/internal/MockCordappProvider.kt | 20 +---- 13 files changed, 188 insertions(+), 184 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt index a4dc38ee68..753e842fe6 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt @@ -2,12 +2,10 @@ package net.corda.core.cordapp import net.corda.core.DeleteForDJVM import net.corda.core.DoNotImplement +import net.corda.core.cordapp.Cordapp.Info.* import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic -import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_VERSION -import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_WORKFLOW_VERSION import net.corda.core.internal.cordapp.CordappImpl.Companion.UNKNOWN_VALUE -import net.corda.core.internal.cordapp.CordappImpl.Companion.parseVersion import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.SerializationWhitelist @@ -21,7 +19,8 @@ import java.net.URL * * This will only need to be constructed manually for certain kinds of tests. * - * @property name Cordapp name - derived from the base name of the Cordapp JAR (therefore may not be unique) + * @property name Cordapp name - derived from the base name of the Cordapp JAR (therefore may not be unique). This is different to + * [Cordapp.Info.shortName] which should be used instead. * @property contractClassNames List of contracts * @property initiatedFlows List of initiatable flow classes * @property rpcFlows List of RPC initiable flows classes @@ -33,7 +32,10 @@ import java.net.URL * @property customSchemas List of custom schemas * @property allFlows List of all flow classes * @property jarPath The path to the JAR for this CorDapp + * @property cordappClasses An exhaustive list of all classes relevant to the node within this CorDapp * @property jarHash Hash of the jar + * @property minimumPlatformVersion The minimum platform version this CorDapp will run on. + * @property targetPlatformVersion The target platform version this CorDapp was designed and tested on. */ @DoNotImplement @DeleteForDJVM @@ -53,11 +55,14 @@ interface Cordapp { val cordappClasses: List val info: Info val jarHash: SecureHash.SHA256 + val minimumPlatformVersion: Int + val targetPlatformVersion: Int /** - * CorDapp's information, including vendor and version. + * Further information about the CorDapp extracted from its MANIFEST. The sub-types [Default], [Contract] and [Workflow] determine what + * type of CorDapp this is. * - * @property shortName Cordapp's shortName + * @property shortName Cordapp's name * @property vendor Cordapp's vendor * @property version Cordapp's version */ @@ -67,40 +72,44 @@ interface Cordapp { val vendor: String val version: String val licence: String - val minimumPlatformVersion: Int - val targetPlatformVersion: Int fun hasUnknownFields(): Boolean - /** CorDapps that do not separate Contracts and Flows into separate jars (pre Corda 4) */ - data class Default(override val shortName: String, override val vendor: String, override val version: String, override val minimumPlatformVersion: Int, override val targetPlatformVersion: Int, override val licence: String = UNKNOWN_VALUE) - : Info { + /** + * CorDapps created on V3 which bundle contracts and flows into the same jar. This is effectively + * [ContractAndWorkflow] but without the integer versioning. + */ + data class Default(override val shortName: String, override val vendor: String, override val version: String, override val licence: String) : Info { override fun hasUnknownFields(): Boolean = arrayOf(shortName, vendor, version).any { it == UNKNOWN_VALUE } override fun toString() = "CorDapp $shortName version $version by $vendor with licence $licence" } - /** A Contract CorDapp contains contract definitions (state, commands) and verification logic */ - data class Contract(override val shortName: String, override val vendor: String, val versionId: Int, override val licence: String, override val minimumPlatformVersion: Int, override val targetPlatformVersion: Int) - : Info { + /** + * A contract CorDapp contains contract definitions (state, commands) and verification logic + * @property versionId integer version of the CorDapp. [version] is the toString of this. + */ + data class Contract(override val shortName: String, override val vendor: String, val versionId: Int, override val licence: String) : Info { override val version: String get() = versionId.toString() - override fun toString() = "Contract CorDapp: $shortName version $version by vendor $vendor with licence $licence" + override fun toString() = "Contract CorDapp: $shortName version $versionId by vendor $vendor with licence $licence" override fun hasUnknownFields(): Boolean = arrayOf(shortName, vendor, licence).any { it == UNKNOWN_VALUE } } - /** A Workflow CorDapp contains flows and services used to implement business transactions using contracts and states persisted to the immutable ledger */ - data class Workflow(override val shortName: String, override val vendor: String, val versionId: Int, override val licence: String, override val minimumPlatformVersion: Int, override val targetPlatformVersion: Int) - : Info { + /** + * A workflow CorDapp contains flows and services used to implement business transactions using contracts and states persisted to the + * immutable ledger + * @property versionId integer version of the CorDapp. [version] is the toString of this. + */ + data class Workflow(override val shortName: String, override val vendor: String, val versionId: Int, override val licence: String) : Info { override val version: String get() = versionId.toString() - override fun toString() = "Workflow CorDapp: $shortName version $version by vendor $vendor with licence $licence" + override fun toString() = "Workflow CorDapp: $shortName version $versionId by vendor $vendor with licence $licence" override fun hasUnknownFields(): Boolean = arrayOf(shortName, vendor, licence).any { it == UNKNOWN_VALUE } } - /** A CorDapp that includes both Contract and Workflow classes (not recommended) */ + /** A CorDapp that includes both contract and workflow classes, and defined in terms of [Contract] and [Workflow]. */ // TODO: future work in Gradle cordapp plugins to enforce separation of Contract and Workflow classes into separate jars - data class ContractAndWorkflow(val contract: Contract, val workflow: Workflow, override val minimumPlatformVersion: Int, override val targetPlatformVersion: Int) - : Info { + data class ContractAndWorkflow(val contract: Contract, val workflow: Workflow) : Info { override val shortName: String get() = "Contract: ${contract.shortName}, Workflow: ${workflow.shortName}" override val vendor: String @@ -108,7 +117,7 @@ interface Cordapp { override val licence: String get() = "Contract: ${contract.licence}, Workflow: ${workflow.licence}" override val version: String - get() = "Contract: ${contract.versionId}, Workflow: ${workflow.versionId}" + get() = "Contract: ${contract.version}, Workflow: ${workflow.version}" override fun toString() = "Combined CorDapp: $contract, $workflow" override fun hasUnknownFields(): Boolean = contract.hasUnknownFields() || workflow.hasUnknownFields() } diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt index 9b9e073b01..ac53d8ae5f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt @@ -2,9 +2,10 @@ package net.corda.core.internal.cordapp import net.corda.core.DeleteForDJVM import net.corda.core.cordapp.Cordapp -import net.corda.core.cordapp.CordappInvalidVersionException import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic +import net.corda.core.internal.PLATFORM_VERSION +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.toPath import net.corda.core.schemas.MappedSchema @@ -12,6 +13,7 @@ import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import java.net.URL +import java.nio.file.Paths @DeleteForDJVM data class CordappImpl( @@ -28,11 +30,20 @@ data class CordappImpl( override val jarPath: URL, override val info: Cordapp.Info, override val jarHash: SecureHash.SHA256, + override val minimumPlatformVersion: Int, + override val targetPlatformVersion: Int, val notaryService: Class?, /** Indicates whether the CorDapp is loaded from external sources, or generated on node startup (virtual). */ - val isLoaded: Boolean = true) : Cordapp { + val isLoaded: Boolean = true +) : Cordapp { override val name: String = jarName(jarPath) + // TODO: Also add [SchedulableFlow] as a Cordapp class + override val cordappClasses: List = run { + val classList = rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass } + notaryService + classList.mapNotNull { it?.name } + contractClassNames + } + companion object { fun jarName(url: URL): String = url.toPath().fileName.toString().removeSuffix(".jar") @@ -54,30 +65,26 @@ data class CordappImpl( const val DEFAULT_CORDAPP_VERSION = 1 /** used for CorDapps that do not explicitly define attributes */ - val UNKNOWN = Cordapp.Info.Default(UNKNOWN_VALUE, UNKNOWN_VALUE, UNKNOWN_VALUE,1, 1) + val UNKNOWN_INFO = Cordapp.Info.Default(UNKNOWN_VALUE, UNKNOWN_VALUE, UNKNOWN_VALUE, UNKNOWN_VALUE) - /** Helper method for version identifier parsing */ - fun parseVersion(versionStr: String?, attributeName: String): Int { - if (versionStr == null) - throw CordappInvalidVersionException("Target versionId attribute $attributeName not specified. Please specify a whole number starting from 1.") - return try { - val version = versionStr.toInt() - if (version < 1) { - throw CordappInvalidVersionException("Target versionId ($versionStr) for attribute $attributeName must not be smaller than 1.") - } - return version - } catch (e: NumberFormatException) { - throw CordappInvalidVersionException("Version identifier ($versionStr) for attribute $attributeName must be a whole number starting from 1.") - } - } + @VisibleForTesting + val TEST_INSTANCE = CordappImpl( + contractClassNames = emptyList(), + initiatedFlows = emptyList(), + rpcFlows = emptyList(), + serviceFlows = emptyList(), + schedulableFlows = emptyList(), + services = emptyList(), + serializationWhitelists = emptyList(), + serializationCustomSerializers = emptyList(), + customSchemas = emptySet(), + jarPath = Paths.get("").toUri().toURL(), + info = CordappImpl.UNKNOWN_INFO, + allFlows = emptyList(), + jarHash = SecureHash.allOnesHash, + minimumPlatformVersion = 1, + targetPlatformVersion = PLATFORM_VERSION, + notaryService = null + ) } - /** - * An exhaustive list of all classes relevant to the node within this CorDapp - * - * TODO: Also add [SchedulableFlow] as a Cordapp class - */ - override val cordappClasses: List = run { - val classList = rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass } + notaryService - classList.mapNotNull { it?.name } + contractClassNames - } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappInfoResolver.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappInfoResolver.kt index 257d22e37d..73abf873ac 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappInfoResolver.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappInfoResolver.kt @@ -11,10 +11,10 @@ import java.util.concurrent.ConcurrentHashMap */ object CordappInfoResolver { private val logger = loggerFor() - private val cordappClasses: ConcurrentHashMap> = ConcurrentHashMap() + private val cordappClasses: ConcurrentHashMap> = ConcurrentHashMap() // TODO Use the StackWalker API once we migrate to Java 9+ - private var cordappInfoResolver: () -> Cordapp.Info? = { + private var cordappResolver: () -> Cordapp? = { Exception().stackTrace .mapNotNull { cordappClasses[it.className] } // If there is more than one cordapp registered for a class name we can't determine the "correct" one and return null. @@ -26,8 +26,8 @@ object CordappInfoResolver { * This could happen when trying to run different versions of the same CorDapp on the same node. */ @Synchronized - fun register(classes: List, cordapp: Cordapp.Info) { - classes.forEach { + fun register(cordapp: Cordapp) { + cordapp.cordappClasses.forEach { if (cordappClasses.containsKey(it)) { logger.warn("More than one CorDapp registered for $it.") cordappClasses[it] = cordappClasses[it]!! + cordapp @@ -46,32 +46,28 @@ object CordappInfoResolver { * @return Information about the CorDapp from which the invoker is called, null if called outside a CorDapp or the * calling CorDapp cannot be reliably determined. */ - val currentCordappInfo: Cordapp.Info? get() = cordappInfoResolver() + val currentCordapp: Cordapp? get() = cordappResolver() /** * Returns the target version of the current calling CorDapp. Defaults to the current platform version if there isn't one. */ // TODO It may be the default is wrong and this should be Int? instead - val currentTargetVersion: Int get() = currentCordappInfo?.targetPlatformVersion ?: PLATFORM_VERSION + val currentTargetVersion: Int get() = currentCordapp?.targetPlatformVersion ?: PLATFORM_VERSION /** - * Temporarily apply a fake CorDapp.Info with the given parameters. For use in testing. + * Temporarily apply a fake CorDapp with the given parameters. For use in testing. */ @Synchronized @VisibleForTesting - fun withCordappInfo(shortName: String = "CordappInfoResolver.withCordappInfo", - vendor: String = "Corda", - version: String = "1", - licence: String = "Apache", - minimumPlatformVersion: Int = 1, - targetPlatformVersion: Int = PLATFORM_VERSION, - block: () -> T): T { - val currentResolver = cordappInfoResolver - cordappInfoResolver = { Cordapp.Info.Default(shortName, vendor, version, minimumPlatformVersion, targetPlatformVersion, licence) } + fun withCordapp(minimumPlatformVersion: Int = 1, targetPlatformVersion: Int = PLATFORM_VERSION, block: () -> T): T { + val currentResolver = cordappResolver + cordappResolver = { + CordappImpl.TEST_INSTANCE.copy(minimumPlatformVersion = minimumPlatformVersion, targetPlatformVersion = targetPlatformVersion) + } try { return block() } finally { - cordappInfoResolver = currentResolver + cordappResolver = currentResolver } } diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/ManifestUtils.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/ManifestUtils.kt index 13a280c38c..c824ed53e0 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/ManifestUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/ManifestUtils.kt @@ -1,28 +1,13 @@ package net.corda.core.internal.cordapp -import net.corda.core.cordapp.Cordapp -import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_LICENCE -import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_NAME -import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_VENDOR -import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_VERSION -import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_WORKFLOW_LICENCE -import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_WORKFLOW_NAME -import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_WORKFLOW_VENDOR -import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_WORKFLOW_VERSION import net.corda.core.internal.cordapp.CordappImpl.Companion.MIN_PLATFORM_VERSION import net.corda.core.internal.cordapp.CordappImpl.Companion.TARGET_PLATFORM_VERSION -import net.corda.core.internal.cordapp.CordappImpl.Companion.UNKNOWN_VALUE -import net.corda.core.internal.cordapp.CordappImpl.Companion.parseVersion import java.util.jar.Attributes import java.util.jar.Manifest -operator fun Manifest.set(key: String, value: String): String? { - return mainAttributes.putValue(key, value) -} +operator fun Manifest.set(key: String, value: String): String? = mainAttributes.putValue(key, value) -operator fun Manifest.set(key: Attributes.Name, value: String): Any? { - return mainAttributes.put(key, value) -} +operator fun Manifest.set(key: Attributes.Name, value: String): Any? = mainAttributes.put(key, value) operator fun Manifest.get(key: String): String? = mainAttributes.getValue(key) @@ -31,56 +16,3 @@ val Manifest.targetPlatformVersion: Int val minPlatformVersion = this[MIN_PLATFORM_VERSION]?.toIntOrNull() ?: 1 return this[TARGET_PLATFORM_VERSION]?.toIntOrNull() ?: minPlatformVersion } - -fun Manifest.toCordappInfo(defaultName: String): Cordapp.Info { - - /** Common attributes */ - val minPlatformVersion = this[MIN_PLATFORM_VERSION]?.toIntOrNull() ?: 1 - val targetPlatformVersion = this[TARGET_PLATFORM_VERSION]?.toIntOrNull() ?: minPlatformVersion - - /** new identifiers (Corda 4) */ - // is it a Contract Jar? - val contractInfo = - if (this[CORDAPP_CONTRACT_NAME] != null) { - Cordapp.Info.Contract(shortName = this[CORDAPP_CONTRACT_NAME] ?: defaultName, - vendor = this[CORDAPP_CONTRACT_VENDOR] ?: UNKNOWN_VALUE, - versionId = parseVersion(this[CORDAPP_CONTRACT_VERSION], CORDAPP_CONTRACT_VERSION), - licence = this[CORDAPP_CONTRACT_LICENCE] ?: UNKNOWN_VALUE, - minimumPlatformVersion = minPlatformVersion, - targetPlatformVersion = targetPlatformVersion - ) - } else null - - // is it a Workflow (flows and services) Jar? - val workflowInfo = - if (this[CORDAPP_WORKFLOW_NAME] != null) { - Cordapp.Info.Workflow(shortName = this[CORDAPP_WORKFLOW_NAME] ?: defaultName, - vendor = this[CORDAPP_WORKFLOW_VENDOR] ?: UNKNOWN_VALUE, - versionId = parseVersion(this[CORDAPP_WORKFLOW_VERSION],CORDAPP_WORKFLOW_VERSION), - licence = this[CORDAPP_WORKFLOW_LICENCE] ?: UNKNOWN_VALUE, - minimumPlatformVersion = minPlatformVersion, - targetPlatformVersion = targetPlatformVersion - ) - } else null - - // combined Contract and Workflow Jar ? - if (contractInfo != null && workflowInfo != null) { - return Cordapp.Info.ContractAndWorkflow(contractInfo, workflowInfo, minPlatformVersion, targetPlatformVersion) - } - else if (contractInfo != null) return contractInfo - else if (workflowInfo != null) return workflowInfo - - /** need to maintain backwards compatibility so use old identifiers if existent */ - val shortName = this["Name"] ?: defaultName - val vendor = this["Implementation-Vendor"] ?: UNKNOWN_VALUE - val version = this["Implementation-Version"] ?: UNKNOWN_VALUE - return Cordapp.Info.Default( - shortName = shortName, - vendor = vendor, - version = version, - licence = UNKNOWN_VALUE, - minimumPlatformVersion = minPlatformVersion, - targetPlatformVersion = targetPlatformVersion - ) -} - 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 d5cf4ab5f6..ba70549e05 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -65,7 +65,7 @@ class FinalityFlowTests : WithFinality { fun `prevent use of the old API if the CorDapp target version is 4`() { val bob = createBob() val stx = aliceNode.issuesCashTo(bob) - val resultFuture = CordappInfoResolver.withCordappInfo(targetPlatformVersion = 4) { + val resultFuture = CordappInfoResolver.withCordapp(targetPlatformVersion = 4) { @Suppress("DEPRECATION") aliceNode.startFlowAndRunNetwork(FinalityFlow(stx)).resultFuture } @@ -79,7 +79,7 @@ class FinalityFlowTests : WithFinality { // We need Bob to load at least one old CorDapp so that its FinalityHandler is enabled val bob = createBob(cordapps = listOf(cordappForPackages("com.template").withTargetVersion(3))) val stx = aliceNode.issuesCashTo(bob) - val resultFuture = CordappInfoResolver.withCordappInfo(targetPlatformVersion = 3) { + val resultFuture = CordappInfoResolver.withCordapp(targetPlatformVersion = 3) { @Suppress("DEPRECATION") aliceNode.startFlowAndRunNetwork(FinalityFlow(stx)).resultFuture } diff --git a/core/src/test/kotlin/net/corda/core/internal/cordapp/CordappInfoResolverTest.kt b/core/src/test/kotlin/net/corda/core/internal/cordapp/CordappInfoResolverTest.kt index bdd24b7111..6520b14e5e 100644 --- a/core/src/test/kotlin/net/corda/core/internal/cordapp/CordappInfoResolverTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/cordapp/CordappInfoResolverTest.kt @@ -1,6 +1,5 @@ package net.corda.core.internal.cordapp -import net.corda.core.cordapp.Cordapp import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before @@ -18,11 +17,15 @@ class CordappInfoResolverTest { fun `the correct cordapp resolver is used after calling withCordappInfo`() { val defaultTargetVersion = 222 - CordappInfoResolver.register(listOf(javaClass.name), Cordapp.Info.Default("test", "test", "2", 3, defaultTargetVersion)) + CordappInfoResolver.register(CordappImpl.TEST_INSTANCE.copy( + contractClassNames = listOf(javaClass.name), + minimumPlatformVersion = 3, + targetPlatformVersion = defaultTargetVersion + )) assertEquals(defaultTargetVersion, CordappInfoResolver.currentTargetVersion) val expectedTargetVersion = 555 - CordappInfoResolver.withCordappInfo(targetPlatformVersion = expectedTargetVersion) { + CordappInfoResolver.withCordapp(targetPlatformVersion = expectedTargetVersion) { val actualTargetVersion = CordappInfoResolver.currentTargetVersion assertEquals(expectedTargetVersion, actualTargetVersion) } @@ -31,8 +34,16 @@ class CordappInfoResolverTest { @Test fun `when more than one cordapp is registered for the same class, the resolver returns null`() { - CordappInfoResolver.register(listOf(javaClass.name), Cordapp.Info.Default("test", "test", "2", 3, 222)) - CordappInfoResolver.register(listOf(javaClass.name), Cordapp.Info.Default("test1", "test1", "1", 2, 456)) - assertThat(CordappInfoResolver.currentCordappInfo).isNull() + CordappInfoResolver.register(CordappImpl.TEST_INSTANCE.copy( + contractClassNames = listOf(javaClass.name), + minimumPlatformVersion = 3, + targetPlatformVersion = 222 + )) + CordappInfoResolver.register(CordappImpl.TEST_INSTANCE.copy( + contractClassNames = listOf(javaClass.name), + minimumPlatformVersion = 2, + targetPlatformVersion = 456 + )) + assertThat(CordappInfoResolver.currentCordapp).isNull() } } 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 bb358b6993..c2c5c49207 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -693,7 +693,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // TODO Add public API to allow the node operator to accept or reject private fun installFinalityHandler() { // Disable the insecure FinalityHandler if none of the loaded CorDapps are old enough to require it. - val cordappsNeedingFinalityHandler = cordappLoader.cordapps.filter { it.info.targetPlatformVersion < 4 } + val cordappsNeedingFinalityHandler = cordappLoader.cordapps.filter { it.targetPlatformVersion < 4 } if (cordappsNeedingFinalityHandler.isEmpty()) { log.info("FinalityHandler is disabled as there are no CorDapps loaded which require it") } else { 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 3a9aa1ccb7..ccf25163cf 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 @@ -4,14 +4,15 @@ import io.github.classgraph.ClassGraph import io.github.classgraph.ScanResult import net.corda.core.contracts.warnContractWithoutConstraintPropagation import net.corda.core.cordapp.Cordapp +import net.corda.core.cordapp.CordappInvalidVersionException import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.flows.* import net.corda.core.internal.* import net.corda.core.internal.cordapp.CordappImpl -import net.corda.core.internal.cordapp.CordappImpl.Companion.UNKNOWN +import net.corda.core.internal.cordapp.CordappImpl.Companion.UNKNOWN_INFO import net.corda.core.internal.cordapp.CordappInfoResolver -import net.corda.core.internal.cordapp.toCordappInfo +import net.corda.core.internal.cordapp.get import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.notary.SinglePartyNotaryService import net.corda.core.node.services.CordaService @@ -31,6 +32,7 @@ import java.net.URLClassLoader import java.nio.file.Path import java.util.* import java.util.jar.JarInputStream +import java.util.jar.Manifest import kotlin.reflect.KClass import kotlin.streams.toList @@ -89,7 +91,6 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName) private fun jarUrlsInDirectory(directory: Path): List { - return if (!directory.exists()) { emptyList() } else { @@ -100,13 +101,14 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: } } } + private fun loadCordapps(): List { val cordapps = cordappJarPaths .map { url -> scanCordapp(url).use { it.toCordapp(url) } } .filter { - if (it.info.minimumPlatformVersion > versionInfo.platformVersion) { + if (it.minimumPlatformVersion > versionInfo.platformVersion) { logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it requires minimum " + - "platform version ${it.info.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).") + "platform version ${it.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).") false } else { true @@ -127,11 +129,15 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: } } } - cordapps.forEach { CordappInfoResolver.register(it.cordappClasses, it.info) } + cordapps.forEach(CordappInfoResolver::register) return cordapps } + private fun RestrictedScanResult.toCordapp(url: RestrictedURL): CordappImpl { - val info = url.url.openStream().let(::JarInputStream).use { it.manifest?.toCordappInfo(CordappImpl.jarName(url.url)) ?: UNKNOWN } + val manifest: Manifest? = url.url.openStream().use { JarInputStream(it).manifest } + val info = parseCordappInfo(manifest, CordappImpl.jarName(url.url)) + val minPlatformVersion = manifest?.get(CordappImpl.MIN_PLATFORM_VERSION)?.toIntOrNull() ?: 1 + val targetPlatformVersion = manifest?.get(CordappImpl.TARGET_PLATFORM_VERSION)?.toIntOrNull() ?: minPlatformVersion return CordappImpl( findContractClassNames(this), findInitiatedFlows(this), @@ -146,10 +152,67 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: url.url, info, getJarHash(url.url), + minPlatformVersion, + targetPlatformVersion, findNotaryService(this) ) } + private fun parseCordappInfo(manifest: Manifest?, defaultName: String): Cordapp.Info { + if (manifest == null) return UNKNOWN_INFO + + /** new identifiers (Corda 4) */ + // is it a Contract Jar? + val contractInfo = if (manifest[CordappImpl.CORDAPP_CONTRACT_NAME] != null) { + Cordapp.Info.Contract( + shortName = manifest[CordappImpl.CORDAPP_CONTRACT_NAME] ?: defaultName, + vendor = manifest[CordappImpl.CORDAPP_CONTRACT_VENDOR] ?: CordappImpl.UNKNOWN_VALUE, + versionId = parseVersion(manifest[CordappImpl.CORDAPP_CONTRACT_VERSION], CordappImpl.CORDAPP_CONTRACT_VERSION), + licence = manifest[CordappImpl.CORDAPP_CONTRACT_LICENCE] ?: CordappImpl.UNKNOWN_VALUE + ) + } else { + null + } + + // is it a Workflow (flows and services) Jar? + val workflowInfo = if (manifest[CordappImpl.CORDAPP_WORKFLOW_NAME] != null) { + Cordapp.Info.Workflow( + shortName = manifest[CordappImpl.CORDAPP_WORKFLOW_NAME] ?: defaultName, + vendor = manifest[CordappImpl.CORDAPP_WORKFLOW_VENDOR] ?: CordappImpl.UNKNOWN_VALUE, + versionId = parseVersion(manifest[CordappImpl.CORDAPP_WORKFLOW_VERSION], CordappImpl.CORDAPP_WORKFLOW_VERSION), + licence = manifest[CordappImpl.CORDAPP_WORKFLOW_LICENCE] ?: CordappImpl.UNKNOWN_VALUE + ) + } else { + null + } + + when { + // combined Contract and Workflow Jar? + contractInfo != null && workflowInfo != null -> return Cordapp.Info.ContractAndWorkflow(contractInfo, workflowInfo) + contractInfo != null -> return contractInfo + workflowInfo != null -> return workflowInfo + } + + return Cordapp.Info.Default( + shortName = manifest["Name"] ?: defaultName, + vendor = manifest["Implementation-Vendor"] ?: CordappImpl.UNKNOWN_VALUE, + version = manifest["Implementation-Version"] ?: CordappImpl.UNKNOWN_VALUE, + licence = CordappImpl.UNKNOWN_VALUE + ) + } + + private fun parseVersion(versionStr: String?, attributeName: String): Int { + if (versionStr == null) { + throw CordappInvalidVersionException("Target versionId attribute $attributeName not specified. Please specify a whole number starting from 1.") + } + val version = versionStr.toIntOrNull() + ?: throw CordappInvalidVersionException("Version identifier ($versionStr) for attribute $attributeName must be a whole number starting from 1.") + if (version < 1) { + throw CordappInvalidVersionException("Target versionId ($versionStr) for attribute $attributeName must not be smaller than 1.") + } + return version + } + private fun findNotaryService(scanResult: RestrictedScanResult): Class? { // Note: we search for implementations of both NotaryService and TrustedAuthorityNotaryService as // the scanner won't find subclasses deeper down the hierarchy if any intermediate class is not diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/VirtualCordapps.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/VirtualCordapps.kt index 6d567830a9..6bc82c032a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/VirtualCordapps.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/VirtualCordapps.kt @@ -29,10 +29,12 @@ internal object VirtualCordapp { serializationWhitelists = listOf(), serializationCustomSerializers = listOf(), customSchemas = setOf(), - info = Cordapp.Info.Default("corda-core", versionInfo.vendor, versionInfo.releaseVersion, 1, versionInfo.platformVersion), + info = Cordapp.Info.Default("corda-core", versionInfo.vendor, versionInfo.releaseVersion, "Open Source (Apache 2)"), allFlows = listOf(), jarPath = ContractUpgradeFlow.javaClass.location, // Core JAR location jarHash = SecureHash.allOnesHash, + minimumPlatformVersion = 1, + targetPlatformVersion = versionInfo.platformVersion, notaryService = null, isLoaded = false ) @@ -50,10 +52,12 @@ internal object VirtualCordapp { serializationWhitelists = listOf(), serializationCustomSerializers = listOf(), customSchemas = setOf(NodeNotarySchemaV1), - info = Cordapp.Info.Default("corda-notary", versionInfo.vendor, versionInfo.releaseVersion, 1, versionInfo.platformVersion), + info = Cordapp.Info.Default("corda-notary", versionInfo.vendor, versionInfo.releaseVersion, "Open Source (Apache 2)"), allFlows = listOf(), jarPath = SimpleNotaryService::class.java.location, jarHash = SecureHash.allOnesHash, + minimumPlatformVersion = 1, + targetPlatformVersion = versionInfo.platformVersion, notaryService = SimpleNotaryService::class.java, isLoaded = false ) diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt index 9c0c418ea7..d11b1676e7 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt @@ -4,9 +4,9 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.* import net.corda.core.internal.packageName import net.corda.node.VersionInfo +import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES import net.corda.testing.node.internal.TestCordappDirectories import net.corda.testing.node.internal.cordappForPackages -import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES import org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.nio.file.Paths @@ -96,8 +96,8 @@ class JarScanningCordappLoaderTest { val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/no-min-or-target-version.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN) loader.cordapps.forEach { - assertThat(it.info.targetPlatformVersion).isEqualTo(1) - assertThat(it.info.minimumPlatformVersion).isEqualTo(1) + assertThat(it.targetPlatformVersion).isEqualTo(1) + assertThat(it.minimumPlatformVersion).isEqualTo(1) } } @@ -108,8 +108,8 @@ class JarScanningCordappLoaderTest { val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN) val cordapp = loader.cordapps.first() - assertThat(cordapp.info.targetPlatformVersion).isEqualTo(3) - assertThat(cordapp.info.minimumPlatformVersion).isEqualTo(2) + assertThat(cordapp.targetPlatformVersion).isEqualTo(3) + assertThat(cordapp.minimumPlatformVersion).isEqualTo(2) } @Test @@ -119,8 +119,8 @@ class JarScanningCordappLoaderTest { val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN) // exclude the core cordapp val cordapp = loader.cordapps.single { it.cordappClasses.contains("net.corda.core.internal.cordapp.CordappImpl") } - assertThat(cordapp.info.targetPlatformVersion).isEqualTo(2) - assertThat(cordapp.info.minimumPlatformVersion).isEqualTo(2) + assertThat(cordapp.targetPlatformVersion).isEqualTo(2) + assertThat(cordapp.minimumPlatformVersion).isEqualTo(2) } @Test diff --git a/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt b/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt index af526f905a..d5efb155bb 100644 --- a/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/FinalityHandlerTest.kt @@ -121,7 +121,7 @@ class FinalityHandlerTest { } private fun TestStartedNode.finaliseWithOldApi(stx: SignedTransaction): CordaFuture { - return CordappInfoResolver.withCordappInfo(targetPlatformVersion = 3) { + return CordappInfoResolver.withCordapp(targetPlatformVersion = 3) { @Suppress("DEPRECATION") services.startFlow(FinalityFlow(stx)).resultFuture.apply { mockNet.runNetwork() diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt index 7564e67c5c..1800b6bc9f 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -10,13 +10,13 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.NamedCacheFactory +import net.corda.core.internal.cordapp.set import net.corda.core.internal.createComponentGroups import net.corda.core.node.NodeInfo import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.internal.effectiveSerializationEnv import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.loggerFor -import net.corda.core.internal.cordapp.set import net.corda.node.internal.createCordaPersistence import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.internal.startHikariPool @@ -210,4 +210,4 @@ fun withTestSerializationEnvIfNotSet(block: () -> R): R { } else { createTestSerializationEnv().asTestContextEnv { block() } } -} \ No newline at end of file +} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt index 199082a29b..86f47de703 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt @@ -2,16 +2,13 @@ package net.corda.testing.internal import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp -import net.corda.core.crypto.SecureHash import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.cordapp.CordappImpl -import net.corda.core.internal.cordapp.CordappImpl.Companion.UNKNOWN import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage import net.corda.node.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.testing.services.MockAttachmentStorage -import java.nio.file.Paths import java.security.PublicKey import java.util.jar.Attributes @@ -30,22 +27,7 @@ class MockCordappProvider( signers: List = emptyList(), jarManifestAttributes: Map = emptyMap() ): AttachmentId { - val cordapp = CordappImpl( - contractClassNames = listOf(contractClassName), - initiatedFlows = emptyList(), - rpcFlows = emptyList(), - serviceFlows = emptyList(), - schedulableFlows = emptyList(), - services = emptyList(), - serializationWhitelists = emptyList(), - serializationCustomSerializers = emptyList(), - customSchemas = emptySet(), - jarPath = Paths.get("").toUri().toURL(), - info = UNKNOWN, - allFlows = emptyList(), - jarHash = SecureHash.allOnesHash, - notaryService = null - ) + val cordapp = CordappImpl.TEST_INSTANCE.copy(contractClassNames = listOf(contractClassName)) val jarManifestAttributesWithObligatoryElement = jarManifestAttributes.toMutableMap() jarManifestAttributesWithObligatoryElement.putIfAbsent(Attributes.Name.MANIFEST_VERSION.toString(), "1.0") if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) && it.second == contractHash }) {