[CORDA-1926] Implement target version and min platform version (#3899)

https://r3-cev.atlassian.net/browse/CORDA-1926
This commit is contained in:
Florian Friemel 2018-09-28 09:46:06 +01:00 committed by GitHub
parent 06150371ad
commit 842eac5c43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 240 additions and 23 deletions

View File

@ -40,10 +40,11 @@ data class CordappImpl(
override val cordappClasses: List<String> = (rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass }).map { it.name } + contractClassNames
// TODO Why a seperate Info class and not just have the fields directly in CordappImpl?
data class Info(val shortName: String, val vendor: String, val version: String) {
data class Info(val shortName: String, val vendor: String, val version: String, val minimumPlatformVersion: Int, val targetPlatformVersion: Int) {
companion object {
private const val UNKNOWN_VALUE = "Unknown"
val UNKNOWN = Info(UNKNOWN_VALUE, UNKNOWN_VALUE, UNKNOWN_VALUE)
val UNKNOWN = Info(UNKNOWN_VALUE, UNKNOWN_VALUE, UNKNOWN_VALUE, 1, 1)
}
fun hasUnknownFields(): Boolean = arrayOf(shortName, vendor, version).any { it == UNKNOWN_VALUE }

View File

@ -0,0 +1,65 @@
package net.corda.core.internal.cordapp
import net.corda.core.utilities.loggerFor
import java.util.concurrent.ConcurrentHashMap
/**
* Provides a way to acquire information about the calling CorDapp.
*/
object CordappInfoResolver {
private val logger = loggerFor<CordappInfoResolver>()
private val cordappClasses: ConcurrentHashMap<String, Set<CordappImpl.Info>> = ConcurrentHashMap()
// TODO use the StackWalker API once we migrate to Java 9+
private var cordappInfoResolver: () -> CordappImpl.Info? = {
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.
.firstOrNull { it.size < 2 }?.single()
}
/*
* Associates class names with CorDapps or logs a warning when a CorDapp is already registered for a given class.
* This could happen when trying to run different versions of the same CorDapp on the same node.
*/
@Synchronized
fun register(classes: List<String>, cordapp: CordappImpl.Info) {
classes.forEach {
if (cordappClasses.containsKey(it)) {
logger.warn("More than one CorDapp registered for $it.")
cordappClasses[it] = cordappClasses[it]!! + cordapp
} else {
cordappClasses[it] = setOf(cordapp)
}
}
}
/*
* This should only be used when making a change that would break compatibility with existing CorDapps. The change
* can then be version-gated, meaning the old behaviour is used if the calling CorDapp's target version is lower
* than the platform version that introduces the new behaviour.
* In situations where a `[CordappProvider]` is available the CorDapp context should be obtained from there.
*
* @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..
*/
fun getCorDappInfo(): CordappImpl.Info? = cordappInfoResolver()
/**
* Temporarily switch out the internal resolver for another one. For use in testing.
*/
@Synchronized
fun withCordappInfoResolution(tempResolver: () -> CordappImpl.Info?, block: () -> Unit) {
val resolver = cordappInfoResolver
cordappInfoResolver = tempResolver
try {
block()
} finally {
cordappInfoResolver = resolver
}
}
internal fun clear() {
cordappClasses.clear()
}
}

View File

@ -0,0 +1,42 @@
package net.corda.core.internal.cordapp
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
class CordappInfoResolverTest {
@Before
@After
fun clearCordappInfoResolver() {
CordappInfoResolver.clear()
}
@Test()
fun `The correct cordapp resolver is used after calling withCordappResolution`() {
val defaultTargetVersion = 222
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test", "test", "2", 3, defaultTargetVersion))
assertEquals(defaultTargetVersion, returnCallingTargetVersion())
val expectedTargetVersion = 555
CordappInfoResolver.withCordappInfoResolution( { CordappImpl.Info("foo", "bar", "1", 2, expectedTargetVersion) })
{
val actualTargetVersion = returnCallingTargetVersion()
assertEquals(expectedTargetVersion, actualTargetVersion)
}
assertEquals(defaultTargetVersion, returnCallingTargetVersion())
}
@Test()
fun `When more than one cordapp is registered for the same class, the resolver returns null`() {
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test", "test", "2", 3, 222))
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test1", "test1", "1", 2, 456))
assertEquals(0, returnCallingTargetVersion())
}
private fun returnCallingTargetVersion(): Int {
return CordappInfoResolver.getCorDappInfo()?.targetPlatformVersion ?: 0
}
}

View File

@ -7,6 +7,8 @@ release, see :doc:`upgrade-notes`.
Unreleased
----------
* Introduce minimum and target platform version for CorDapps.
* New overload for ``CordaRPCClient.start()`` method allowing to specify target legal identity to use for RPC call.
* Case insensitive vault queries can be specified via a boolean on applicable SQL criteria builder operators. By default queries will be case sensitive.

View File

@ -189,3 +189,30 @@ CorDapp configuration can be accessed from ``CordappContext::config`` whenever a
There is an example project that demonstrates in ``samples`` called ``cordapp-configuration`` and API documentation in
`<api/kotlin/corda/net.corda.core.cordapp/index.html>`_.
Minimum and target platform version
-----------------------------------
CorDapps can advertise their minimum and target platform version. The minimum platform version indicates that a node has to run at least this version in order to be able to run this CorDapp. The target platform version indicates that a CorDapp was tested with this version of the Corda Platform and should be run at this API level if possible. It provides a means of maintaining behavioural compatibility for the cases where the platform's behaviour has changed. These attributes are specified in the JAR manifest of the CorDapp, for example:
.. sourcecode:: groovy
'Min-Platform-Version': 3
'Target-Platform-Version': 4
In gradle, this can be achieved by modifying the jar task as shown in this example:
.. container:: codeset
.. sourcecode:: groovy
jar {
manifest {
attributes(
'Min-Platform-Version': 3
'Target-Platform-Version': 4
)
}
}

View File

@ -19,8 +19,4 @@ and the release version - a change in the major, minor or patch values may or ma
The Platform Version is part of the node's ``NodeInfo`` object, which is available from the ``ServiceHub``. This enables
a CorDapp to find out which version it's running on and determine whether a desired feature is available. When a node
registers with the Network Map Service it will use the node's Platform Version to enforce a minimum version requirement
for the network.
.. note:: A future release may introduce the concept of a target platform version, which would be similar to Android's
``targetSdkVersion``, and would provide a means of maintaining behavioural compatibility for the cases where the
platform's behaviour has changed.
for the network.

View File

@ -15,6 +15,7 @@ import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.node.VersionInfo
import net.corda.node.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.node.internal.cordapp.JarScanningCordappLoader
@ -110,7 +111,7 @@ class AttachmentsClassLoaderStaticContractTests {
val cordapps = cordappsForPackages(packages)
return testDirectory().let { directory ->
cordapps.packageInDirectory(directory)
JarScanningCordappLoader.fromDirectories(listOf(directory))
JarScanningCordappLoader.fromDirectories(listOf(directory), VersionInfo.UNKNOWN)
}
}

View File

@ -23,8 +23,10 @@ import net.corda.core.serialization.SerializationFactory
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.getOrThrow
import net.corda.node.VersionInfo
import net.corda.node.internal.cordapp.JarScanningCordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.nodeapi.internal.PLATFORM_VERSION
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.DUMMY_BANK_A_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
@ -50,7 +52,7 @@ class AttachmentLoadingTests {
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val attachments = MockAttachmentStorage()
private val provider = CordappProviderImpl(JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR)), MockCordappConfigProvider(), attachments).apply {
private val provider = CordappProviderImpl(JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN), MockCordappConfigProvider(), attachments).apply {
start(testNetworkParameters().whitelistedContractImplementations)
}
private val cordapp get() = provider.cordapps.first()

View File

@ -1,5 +1,7 @@
package net.corda.node
import net.corda.nodeapi.internal.PLATFORM_VERSION
/**
* Encapsulates various pieces of version information of the node.
*/
@ -17,6 +19,6 @@ data class VersionInfo(
val vendor: String) {
companion object {
val UNKNOWN = VersionInfo(1, "Unknown", "Unknown", "Unknown")
val UNKNOWN = VersionInfo(PLATFORM_VERSION, "Unknown", "Unknown", "Unknown")
}
}
}

View File

@ -8,6 +8,7 @@ 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.CordappInfoResolver
import net.corda.core.node.services.CordaService
import net.corda.core.schemas.MappedSchema
import net.corda.core.serialization.SerializationCustomSerializer
@ -35,7 +36,7 @@ import kotlin.streams.toList
* @property cordappJarPaths The classpath of cordapp JARs
*/
class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>,
versionInfo: VersionInfo = VersionInfo.UNKNOWN) : CordappLoaderTemplate() {
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN) : CordappLoaderTemplate() {
override val cordapps: List<CordappImpl> by lazy { loadCordapps() + coreCordapp }
@ -104,13 +105,25 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
serializationWhitelists = listOf(),
serializationCustomSerializers = listOf(),
customSchemas = setOf(),
info = CordappImpl.Info("corda-core", versionInfo.vendor, versionInfo.releaseVersion),
info = CordappImpl.Info("corda-core", versionInfo.vendor, versionInfo.releaseVersion, 1, versionInfo.platformVersion),
allFlows = listOf(),
jarPath = ContractUpgradeFlow.javaClass.location, // Core JAR location
jarHash = SecureHash.allOnesHash
)
private fun loadCordapps(): List<CordappImpl> = cordappJarPaths.map { scanCordapp(it).toCordapp(it) }
private fun loadCordapps(): List<CordappImpl> {
val cordapps = cordappJarPaths.map { scanCordapp(it).toCordapp(it) }
.filter {
if (it.info.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}).")
false
} else {
true
}
}
cordapps.forEach { CordappInfoResolver.register(it.cordappClasses, it.info) }
return cordapps
}
private fun RestrictedScanResult.toCordapp(url: RestrictedURL): CordappImpl {
val info = url.url.openStream().let(::JarInputStream).use { it.manifest }.toCordappInfo(CordappImpl.jarName(url.url))

View File

@ -38,5 +38,8 @@ fun Manifest?.toCordappInfo(defaultShortName: String): CordappImpl.Info {
this?.mainAttributes?.getValue("Implementation-Version")?.let { version ->
info = info.copy(version = version)
}
val minPlatformVersion = this?.mainAttributes?.getValue("Min-Platform-Version")?.toInt() ?: 1
val targetPlatformVersion = this?.mainAttributes?.getValue("Target-Platform-Version")?.toInt() ?: minPlatformVersion
info = info.copy(minimumPlatformVersion = minPlatformVersion, targetPlatformVersion = targetPlatformVersion)
return info
}
}

View File

@ -3,6 +3,8 @@ package net.corda.node.internal.cordapp
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import net.corda.core.node.services.AttachmentStorage
import net.corda.node.VersionInfo
import net.corda.nodeapi.internal.PLATFORM_VERSION
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.internal.MockCordappConfigProvider
import net.corda.testing.services.MockAttachmentStorage
@ -75,7 +77,7 @@ class CordappProviderImplTests {
fun `test cordapp configuration`() {
val configProvider = MockCordappConfigProvider()
configProvider.cordappConfigs[isolatedCordappName] = validConfig
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR))
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN)
val provider = CordappProviderImpl(loader, configProvider, attachmentStore).apply { start(whitelistedContractImplementations) }
val expected = provider.getAppContext(provider.cordapps.first()).config
@ -84,7 +86,7 @@ class CordappProviderImplTests {
}
private fun newCordappProvider(vararg urls: URL): CordappProviderImpl {
val loader = JarScanningCordappLoader.fromJarUrls(urls.toList())
val loader = JarScanningCordappLoader.fromJarUrls(urls.toList(), VersionInfo.UNKNOWN)
return CordappProviderImpl(loader, stubConfigProvider, attachmentStore).apply { start(whitelistedContractImplementations) }
}
}

View File

@ -2,7 +2,9 @@ package net.corda.node.internal.cordapp
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.*
import net.corda.node.VersionInfo
import net.corda.node.cordapp.CordappLoader
import net.corda.nodeapi.internal.PLATFORM_VERSION
import net.corda.testing.node.internal.cordappsForPackages
import net.corda.testing.node.internal.getTimestampAsDirectoryName
import net.corda.testing.node.internal.packageInDirectory
@ -101,13 +103,70 @@ class JarScanningCordappLoaderTest {
@Test
fun `cordapp classloader can load cordapp classes`() {
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("isolated.jar")!!
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR))
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN)
loader.appClassLoader.loadClass(isolatedContractId)
loader.appClassLoader.loadClass(isolatedFlowName)
}
private fun cordappLoaderForPackages(packages: Iterable<String>): CordappLoader {
@Test
fun `cordapp classloader sets target and min version to 1 if not specified`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/no-min-or-target-version.jar")!!
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN)
loader.cordapps.filter { !it.info.shortName.equals("corda-core") }.forEach {
assertThat(it.info.targetPlatformVersion).isEqualTo(1)
assertThat(it.info.minimumPlatformVersion).isEqualTo(1)
}
}
@Test
fun `cordapp classloader returns correct values for minPlatformVersion and targetVersion`() {
// load jar with min and target version in manifest
// make sure classloader extracts correct values
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN)
// exclude the core cordapp
val cordapp = loader.cordapps.filter { it.cordappClasses.contains("net.corda.core.internal.cordapp.CordappImpl")}.single()
assertThat(cordapp.info.targetPlatformVersion).isEqualTo(3)
assertThat(cordapp.info.minimumPlatformVersion).isEqualTo(2)
}
@Test
fun `cordapp classloader sets target version to min version if target version is not specified`() {
// load jar with minVersion but not targetVersion in manifest
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!!
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN)
// exclude the core cordapp
val cordapp = loader.cordapps.filter { it.cordappClasses.contains("net.corda.core.internal.cordapp.CordappImpl")}.single()
assertThat(cordapp.info.targetPlatformVersion).isEqualTo(2)
assertThat(cordapp.info.minimumPlatformVersion).isEqualTo(2)
}
@Test
fun `cordapp classloader does not load apps when their min platform version is greater than the platform version`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1))
// exclude the core cordapp
assertThat(loader.cordapps.size).isEqualTo(1)
}
@Test
fun `cordapp classloader does load apps when their min platform version is less than the platform version`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1000))
// exclude the core cordapp
assertThat(loader.cordapps.size).isEqualTo(2)
}
@Test
fun `cordapp classloader does load apps when their min platform version is equal to the platform version`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2))
// exclude the core cordapp
assertThat(loader.cordapps.size).isEqualTo(2)
}
private fun cordappLoaderForPackages(packages: Iterable<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader {
val cordapps = cordappsForPackages(packages)
return testDirectory().let { directory ->

View File

@ -18,6 +18,7 @@ import net.corda.core.node.services.*
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.VersionInfo
import net.corda.node.cordapp.CordappLoader
import net.corda.node.internal.ServicesForResolutionImpl
import net.corda.node.internal.configureDatabase
@ -27,6 +28,7 @@ import net.corda.node.services.identity.InMemoryIdentityService
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
import net.corda.node.services.vault.NodeVaultService
import net.corda.nodeapi.internal.PLATFORM_VERSION
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
@ -69,10 +71,10 @@ open class MockServices private constructor(
companion object {
private fun cordappLoaderForPackages(packages: Iterable<String>): CordappLoader {
private fun cordappLoaderForPackages(packages: Iterable<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader {
val cordappPaths = TestCordappDirectories.forPackages(packages)
return JarScanningCordappLoader.fromDirectories(cordappPaths)
return JarScanningCordappLoader.fromDirectories(cordappPaths, versionInfo)
}
/**

View File

@ -276,7 +276,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
}
}
open class MockNode(args: MockNodeArgs, cordappLoader: CordappLoader = JarScanningCordappLoader.fromDirectories(args.config.cordappDirectories)) : AbstractNode<TestStartedNode>(
open class MockNode(args: MockNodeArgs, cordappLoader: CordappLoader = JarScanningCordappLoader.fromDirectories(args.config.cordappDirectories, args.version)) : AbstractNode<TestStartedNode>(
args.config,
TestClock(Clock.systemUTC()),
DefaultNamedCacheFactory(),
@ -474,7 +474,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
val cordappDirectories = sharedCorDappsDirectories + TestCordappDirectories.cached(cordapps)
doReturn(cordappDirectories).whenever(config).cordappDirectories
val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version), JarScanningCordappLoader.fromDirectories(cordappDirectories))
val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version), JarScanningCordappLoader.fromDirectories(cordappDirectories, parameters.version))
_nodes += node
if (start) {
node.start()