ENT-11676: Support for testing backwards compatible transactions in the node driver (#7704)

* ENT-11676: Support for testing backwards compatible transactions in the node driver

* Introduction of a new way to reference CorDapps for the node driver: `TestCordapp.of(URI)`
* New `TestCordapp.asSigned()` method which creates a copy of the CorDapp jar but signed by a dev key.
* Added `NodeParameters.legacyContracts` for specifying legacy contract CorDapps for the node

`TransactionBuilderDriverTest` has been updated to use these new APIs.

* ENT-11676: Support for testing backwards compatible transactions in the node driver

* Introduction of a new way to reference CorDapps for the node driver: `TestCordapp.of(URI)`
* New `TestCordapp.asSigned()` method which creates a copy of the CorDapp jar but signed by a dev key.
* Added `NodeParameters.legacyContracts` for specifying legacy contract CorDapps for the node

`TransactionBuilderDriverTest` has been updated to use these new APIs.

* ENT-11676: Added removed api and fixed alias issue.



---------

Co-authored-by: Adel El-Beik <adel.el-beik@r3.com>
This commit is contained in:
Shams Asari
2024-04-18 09:41:26 +01:00
committed by GitHub
parent 0f713aaa44
commit 18e5f7d68f
11 changed files with 250 additions and 109 deletions

View File

@ -12,6 +12,7 @@ import java.nio.file.Path
import java.util.jar.Attributes
import java.util.jar.JarOutputStream
import java.util.jar.Manifest
import kotlin.io.path.exists
import kotlin.io.path.fileSize
import kotlin.io.path.inputStream
import kotlin.io.path.outputStream
@ -36,9 +37,10 @@ inline fun <T> Path.useZipFile(block: (FileSystem) -> T): T {
return FileSystems.newFileSystem(this).use(block)
}
inline fun <T> Path.modifyJarManifest(block: (Manifest) -> T): T {
inline fun <T> Path.modifyJarManifest(block: (Manifest) -> T): T? {
return useZipFile { zipFs ->
val manifestFile = zipFs.getPath("META-INF", "MANIFEST.MF")
if (!manifestFile.exists()) return null
val manifest = manifestFile.inputStream().use(::Manifest)
val result = block(manifest)
manifestFile.outputStream().use(manifest::write)

View File

@ -21,6 +21,7 @@ import java.util.jar.JarOutputStream
import java.util.jar.Manifest
import kotlin.io.path.deleteExisting
import kotlin.io.path.div
import kotlin.io.path.exists
import kotlin.io.path.listDirectoryEntries
import kotlin.test.assertEquals
@ -75,7 +76,7 @@ object JarSignatureTestUtils {
fun Path.unsignJar() {
// Remove the signatures
useZipFile { zipFs ->
zipFs.getPath("META-INF").listDirectoryEntries("*.{SF,DSA,RSA,EC}").forEach(Path::deleteExisting)
zipFs.getPath("META-INF").takeIf { it.exists() }?.listDirectoryEntries("*.{SF,DSA,RSA,EC}")?.forEach(Path::deleteExisting)
}
// Remove all the hash information of the jar contents
modifyJarManifest { manifest ->

View File

@ -25,7 +25,7 @@ import net.corda.testing.node.User
* log level argument.
* @property rpcAddress optional override for RPC address on which node will be accepting RPC connections from the clients. Port provided must be vacant.
*/
@Suppress("unused")
@Suppress("unused", "TooManyFunctions")
data class NodeParameters(
val providedName: CordaX500Name? = null,
val rpcUsers: List<User> = emptyList(),
@ -37,7 +37,8 @@ data class NodeParameters(
val flowOverrides: Map<out Class<out FlowLogic<*>>, Class<out FlowLogic<*>>> = emptyMap(),
val logLevelOverride: String? = null,
val rpcAddress: NetworkHostAndPort? = null,
val systemProperties: Map<String, String> = emptyMap()
val systemProperties: Map<String, String> = emptyMap(),
val legacyContracts: Collection<TestCordapp> = emptySet()
) {
/**
* Create a new node parameters object with default values. Each parameter can be specified with its wither method which returns a copy
@ -54,6 +55,9 @@ data class NodeParameters(
fun withAdditionalCordapps(additionalCordapps: Set<TestCordapp>): NodeParameters = copy(additionalCordapps = additionalCordapps)
fun withFlowOverrides(flowOverrides: Map<Class<out FlowLogic<*>>, Class<out FlowLogic<*>>>): NodeParameters = copy(flowOverrides = flowOverrides)
fun withLogLevelOverride(logLevelOverride: String?): NodeParameters = copy(logLevelOverride = logLevelOverride)
fun withRpcAddress(rpcAddress: NetworkHostAndPort?): NodeParameters = copy(rpcAddress = rpcAddress)
fun withSystemProperties(systemProperties: Map<String, String>): NodeParameters = copy(systemProperties = systemProperties)
fun withLegacyContracts(legacyContracts: Collection<TestCordapp>): NodeParameters = copy(legacyContracts = legacyContracts)
constructor(
providedName: CordaX500Name?,
@ -221,4 +225,58 @@ data class NodeParameters(
logLevelOverride = logLevelOverride,
rpcAddress = rpcAddress,
systemProperties = systemProperties)
constructor(
providedName: CordaX500Name?,
rpcUsers: List<User>,
verifierType: VerifierType,
customOverrides: Map<String, Any?>,
startInSameProcess: Boolean?,
maximumHeapSize: String,
additionalCordapps: Collection<TestCordapp> = emptySet(),
flowOverrides: Map<out Class<out FlowLogic<*>>, Class<out FlowLogic<*>>>,
logLevelOverride: String? = null,
rpcAddress: NetworkHostAndPort? = null,
systemProperties: Map<String, String> = emptyMap()
) : this(
providedName,
rpcUsers,
verifierType,
customOverrides,
startInSameProcess,
maximumHeapSize,
additionalCordapps,
flowOverrides,
logLevelOverride,
rpcAddress,
systemProperties,
legacyContracts = emptySet())
@Suppress("LongParameterList")
fun copy(
providedName: CordaX500Name?,
rpcUsers: List<User>,
verifierType: VerifierType,
customOverrides: Map<String, Any?>,
startInSameProcess: Boolean?,
maximumHeapSize: String,
additionalCordapps: Collection<TestCordapp> = emptySet(),
flowOverrides: Map<out Class<out FlowLogic<*>>, Class<out FlowLogic<*>>>,
logLevelOverride: String? = null,
rpcAddress: NetworkHostAndPort? = null,
systemProperties: Map<String, String> = emptyMap()
) = this.copy(
providedName = providedName,
rpcUsers = rpcUsers,
verifierType = verifierType,
customOverrides = customOverrides,
startInSameProcess = startInSameProcess,
maximumHeapSize = maximumHeapSize,
additionalCordapps = additionalCordapps,
flowOverrides = flowOverrides,
logLevelOverride = logLevelOverride,
rpcAddress = rpcAddress,
systemProperties = systemProperties,
legacyContracts = legacyContracts)
}

View File

@ -3,7 +3,10 @@ package net.corda.testing.node
import net.corda.core.DoNotImplement
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.NodeParameters
import net.corda.testing.node.internal.TestCordappImpl
import net.corda.testing.node.internal.ScanPackageTestCordapp
import net.corda.testing.node.internal.UriTestCordapp
import java.net.URI
import java.nio.file.Path
/**
* Encapsulates a CorDapp that exists on the current classpath, which can be pulled in for testing. Use [TestCordapp.findCordapp]
@ -25,6 +28,12 @@ abstract class TestCordapp {
/** Returns a copy of this [TestCordapp] but with the specified CorDapp config. */
abstract fun withConfig(config: Map<String, Any>): TestCordapp
/**
* Returns a copy of this [TestCordapp] signed with a development signing key. The same signing key will be used for all signed
* [TestCordapp]s. If the CorDapp jar is already signed, then the new jar created will its signing key replaced by the development key.
*/
abstract fun asSigned(): TestCordapp
companion object {
/**
* Scans the current classpath to find the CorDapp that contains the given package. All the CorDapp's metdata present in its
@ -34,6 +43,14 @@ abstract class TestCordapp {
* @param scanPackage The package name used to find the CorDapp. This does not need to be the root package of the CorDapp.
*/
@JvmStatic
fun findCordapp(scanPackage: String): TestCordapp = TestCordappImpl(scanPackage = scanPackage, config = emptyMap())
fun findCordapp(scanPackage: String): TestCordapp = ScanPackageTestCordapp(scanPackage)
/**
* [URI] location to a CorDapp jar. This may be a path on the local file system or a URL to an external resource.
*
* A [Path] can be converted into a [URI] with [Path.toUri].
*/
@JvmStatic
fun of(uri: URI): TestCordapp = UriTestCordapp(uri)
}
}

View File

@ -10,9 +10,6 @@ import net.corda.core.node.services.AttachmentFixup
import net.corda.core.serialization.SerializationWhitelist
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.testing.core.internal.JarSignatureTestUtils.containsKey
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.attribute.FileTime
@ -49,6 +46,8 @@ data class CustomCordapp(
override fun withOnlyJarContents(): CustomCordapp = CustomCordapp(packages = packages, classes = classes, fixups = fixups)
override fun asSigned(): CustomCordapp = signed()
fun signed(keyStorePath: Path? = null, numberOfSignatures: Int = 1, keyAlgorithm: String = "RSA"): CustomCordapp =
copy(signingInfo = SigningInfo(keyStorePath, numberOfSignatures, keyAlgorithm))
@ -114,23 +113,6 @@ data class CustomCordapp(
}
}
private fun signJar(jarFile: Path) {
if (signingInfo != null) {
val keyStorePathToUse = signingInfo.keyStorePath ?: defaultJarSignerDirectory.createDirectories()
for (i in 1 .. signingInfo.numberOfSignatures) {
val alias = "alias$i"
val pwd = "secret!"
if (!keyStorePathToUse.containsKey(alias, pwd)) {
keyStorePathToUse.generateKey(alias, pwd, "O=Test Company Ltd $i,OU=Test,L=London,C=GB", signingInfo.keyAlgorithm)
}
val pk = keyStorePathToUse.signJar(jarFile.toString(), alias, pwd)
logger.debug { "Signed Jar: $jarFile with public key $pk" }
}
} else {
logger.debug { "Unsigned Jar: $jarFile" }
}
}
private fun createTestManifest(name: String, versionId: Int, targetPlatformVersion: Int): Manifest {
val manifest = Manifest()
@ -160,13 +142,12 @@ data class CustomCordapp(
}
}
data class SigningInfo(val keyStorePath: Path?, val numberOfSignatures: Int, val keyAlgorithm: String)
data class SigningInfo(val keyStorePath: Path?, val signatureCount: Int, val algorithm: String)
companion object {
private val logger = contextLogger()
private val epochFileTime = FileTime.from(Instant.EPOCH)
private val cordappsDirectory: Path
private val defaultJarSignerDirectory: Path
private val whitespace = "\\s++".toRegex()
private val cache = ConcurrentHashMap<CustomCordapp, Path>()
@ -174,7 +155,6 @@ data class CustomCordapp(
val buildDir = Paths.get("build").toAbsolutePath()
val timeDirName = getTimestampAsDirectoryName()
cordappsDirectory = buildDir / "generated-custom-cordapps" / timeDirName
defaultJarSignerDirectory = buildDir / "jar-signer" / timeDirName
}
fun getJarFile(cordapp: CustomCordapp): Path {
@ -187,7 +167,9 @@ data class CustomCordapp(
} else if (it.packages.isNotEmpty() || it.classes.isNotEmpty()) {
it.packageAsJar(jarFile)
}
it.signJar(jarFile)
if (it.signingInfo != null) {
TestCordappSigner.signJar(jarFile, it.signingInfo.keyStorePath, it.signingInfo.signatureCount, it.signingInfo.algorithm)
}
logger.debug { "$it packaged into $jarFile" }
jarFile
}

View File

@ -23,6 +23,7 @@ import net.corda.core.internal.concurrent.fork
import net.corda.core.internal.concurrent.map
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.concurrent.transpose
import net.corda.core.internal.copyToDirectory
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
@ -55,6 +56,7 @@ import net.corda.node.internal.DataSourceFactory
import net.corda.node.internal.Node
import net.corda.node.internal.NodeWithInfo
import net.corda.node.internal.clientSslOptionsCompatibleWith
import net.corda.node.internal.cordapp.JarScanningCordappLoader.Companion.LEGACY_CONTRACTS_DIR_NAME
import net.corda.node.services.Permissions
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.FlowOverride
@ -716,6 +718,11 @@ class DriverDSLImpl(
extraCustomCordapps + (cordappsForAllNodes ?: emptySet())
)
if (parameters.legacyContracts.isNotEmpty()) {
val legacyContractsDir = (baseDirectory / LEGACY_CONTRACTS_DIR_NAME).createDirectories()
parameters.legacyContracts.forEach { (it as TestCordappInternal).jarFile.copyToDirectory(legacyContractsDir) }
}
val nodeFuture = if (parameters.startInSameProcess ?: startNodesInProcess) {
val nodeAndThreadFuture = startInProcessNode(executorService, config, allowHibernateToManageAppSchema)
shutdownManager.registerShutdown(

View File

@ -61,7 +61,7 @@ private val log = LoggerFactory.getLogger("net.corda.testing.internal.InternalTe
* You will probably need to use [FINANCE_CORDAPPS] instead to get access to the flows as well.
*/
@JvmField
val FINANCE_CONTRACTS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.contracts")
val FINANCE_CONTRACTS_CORDAPP: ScanPackageTestCordapp = findCordapp("net.corda.finance.contracts")
/**
* Reference to the finance-workflows CorDapp in this repo. The metadata is taken directly from finance/workflows/build.gradle, including the
@ -70,10 +70,10 @@ val FINANCE_CONTRACTS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.
* You will probably need to use [FINANCE_CORDAPPS] instead to get access to the contract classes as well.
*/
@JvmField
val FINANCE_WORKFLOWS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.workflows")
val FINANCE_WORKFLOWS_CORDAPP: ScanPackageTestCordapp = findCordapp("net.corda.finance.workflows")
@JvmField
val FINANCE_CORDAPPS: Set<TestCordappImpl> = setOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)
val FINANCE_CORDAPPS: Set<ScanPackageTestCordapp> = setOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)
/**
* *Custom* CorDapp containing the contents of the `net.corda.testing.contracts` package, i.e. the dummy contracts. This is not a real CorDapp
@ -105,9 +105,9 @@ fun cordappWithFixups(fixups: List<AttachmentFixup>) = CustomCordapp(fixups = fi
/**
* Find the single CorDapp jar on the current classpath which contains the given package. This is a convenience method for
* [TestCordapp.findCordapp] but returns the internal [TestCordappImpl].
* [TestCordapp.findCordapp] but returns the internal [ScanPackageTestCordapp].
*/
fun findCordapp(scanPackage: String): TestCordappImpl = TestCordapp.findCordapp(scanPackage) as TestCordappImpl
fun findCordapp(scanPackage: String): ScanPackageTestCordapp = TestCordapp.findCordapp(scanPackage) as ScanPackageTestCordapp
/** Create a *custom* CorDapp which just contains the enclosed classes of the receiver class. */
fun Any.enclosedCordapp(): CustomCordapp {

View File

@ -2,6 +2,7 @@ package net.corda.testing.node.internal
import io.github.classgraph.ClassGraph
import net.corda.core.internal.attributes
import net.corda.core.internal.mapToSet
import net.corda.core.internal.pooledScan
import net.corda.core.utilities.contextLogger
import net.corda.testing.node.TestCordapp
@ -23,39 +24,43 @@ import kotlin.io.path.useDirectoryEntries
* the [scanPackage] may reference a gradle CorDapp project on the local system. In this scenerio the project's "jar" task is executed to
* build the CorDapp jar. This allows us to inherit the CorDapp's MANIFEST information without having to do any extra processing.
*/
data class TestCordappImpl(val scanPackage: String, override val config: Map<String, Any>) : TestCordappInternal() {
override fun withConfig(config: Map<String, Any>): TestCordappImpl = copy(config = config)
data class ScanPackageTestCordapp(val scanPackage: String,
override val config: Map<String, Any> = emptyMap(),
val signed: Boolean = false) : TestCordappInternal() {
override fun withConfig(config: Map<String, Any>): ScanPackageTestCordapp = copy(config = config)
override fun withOnlyJarContents(): TestCordappImpl = copy(config = emptyMap())
override fun asSigned(): TestCordapp = copy(signed = true)
override val jarFile: Path
get() {
val jars = findJars(scanPackage)
when (jars.size) {
0 -> throw IllegalArgumentException("There are no CorDapps containing the package $scanPackage on the classpath. Make sure " +
"the package name is correct and that the CorDapp is added as a gradle dependency.")
1 -> return jars.first()
else -> throw IllegalArgumentException("There is more than one CorDapp containing the package $scanPackage on the classpath " +
"$jars. Specify a package name which is unique to the CorDapp.")
}
override fun withOnlyJarContents(): ScanPackageTestCordapp = copy(config = emptyMap(), signed = false)
override val jarFile: Path by lazy {
val jars = findJars()
val jar = when (jars.size) {
0 -> throw IllegalArgumentException("There are no CorDapps containing the package $scanPackage on the classpath. Make sure " +
"the package name is correct and that the CorDapp is added as a gradle dependency.")
1 -> jars.first()
else -> throw IllegalArgumentException("There is more than one CorDapp containing the package $scanPackage on the classpath " +
"$jars. Specify a package name which is unique to the CorDapp.")
}
if (signed) TestCordappSigner.signJarCopy(jar) else jar
}
private fun findJars(): Set<Path> {
val rootPaths = findRootPaths(scanPackage)
return if (rootPaths.all { it.toString().endsWith(".jar") }) {
// We don't need to do anything more if all the root paths are jars
rootPaths
} else {
// Otherwise we need to build those paths which are local projects and extract the built jar from them
rootPaths.mapToSet { if (it.toString().endsWith(".jar")) it else buildCordappJar(it) }
}
}
companion object {
private val packageToRootPaths = ConcurrentHashMap<String, Set<Path>>()
private val projectRootToBuiltJar = ConcurrentHashMap<Path, Path>()
private val log = contextLogger()
fun findJars(scanPackage: String): Set<Path> {
val rootPaths = findRootPaths(scanPackage)
return if (rootPaths.all { it.toString().endsWith(".jar") }) {
// We don't need to do anything more if all the root paths are jars
rootPaths
} else {
// Otherwise we need to build those paths which are local projects and extract the built jar from them
rootPaths.mapTo(HashSet()) { if (it.toString().endsWith(".jar")) it else buildCordappJar(it) }
}
}
private fun findRootPaths(scanPackage: String): Set<Path> {
return packageToRootPaths.computeIfAbsent(scanPackage) {
val classGraph = ClassGraph().acceptPaths(scanPackage.replace('.', '/'))

View File

@ -0,0 +1,50 @@
package net.corda.testing.node.internal
import net.corda.core.internal.JarSignatureCollector
import net.corda.core.internal.deleteRecursively
import net.corda.testing.core.internal.JarSignatureTestUtils.containsKey
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar
import java.nio.file.Files
import java.nio.file.Path
import java.util.jar.JarInputStream
import kotlin.io.path.absolutePathString
import kotlin.io.path.copyTo
import kotlin.io.path.inputStream
import kotlin.io.path.name
object TestCordappSigner {
private val defaultSignerDir = Files.createTempDirectory("testcordapp-signer")
init {
defaultSignerDir.generateKey(alias = "testcordapp")
Runtime.getRuntime().addShutdownHook(Thread(defaultSignerDir::deleteRecursively))
}
fun signJarCopy(jar: Path, signerDir: Path? = null, signatureCount: Int = 1, algorithm: String = "RSA"): Path {
val copy = Files.createTempFile(jar.name, ".jar")
copy.toFile().deleteOnExit()
jar.copyTo(copy, overwrite = true)
signJar(copy, signerDir, signatureCount, algorithm)
return copy
}
fun signJar(jar: Path, signerDir: Path? = null, signatureCount: Int = 1, algorithm: String = "RSA") {
jar.unsignJar()
val signerDirToUse = signerDir ?: defaultSignerDir
for (i in 1 .. signatureCount) {
println("On signer $i")
// Note in the jarsigner tool if -sigfile is not specified then the first 8 chars of alias are used as the file
// name for the .SF and .DSA files. (See jarsigner doc). So $i below needs to be at beginning so unique files are
// created.
val alias = "$i-testcordapp-$algorithm"
val password = "secret!"
if (!signerDirToUse.containsKey(alias, password)) {
signerDirToUse.generateKey(alias, password, "O=Test Company Ltd $i,OU=Test,L=London,C=GB", algorithm)
}
signerDirToUse.signJar(jar.absolutePathString(), alias, password)
println("Number of actual signers: ${JarInputStream(jar.inputStream()).use { JarSignatureCollector.collectSigners(it).size }}")
}
}
}

View File

@ -0,0 +1,39 @@
package net.corda.testing.node.internal
import net.corda.core.internal.copyTo
import net.corda.core.utilities.Try
import net.corda.core.utilities.Try.Failure
import net.corda.core.utilities.Try.Success
import net.corda.testing.node.TestCordapp
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
import kotlin.io.path.toPath
data class UriTestCordapp(val uri: URI,
override val config: Map<String, Any> = emptyMap(),
val signed: Boolean = false) : TestCordappInternal() {
override fun withConfig(config: Map<String, Any>): TestCordapp = copy(config = config)
override fun asSigned(): TestCordapp = copy(signed = true)
override fun withOnlyJarContents(): TestCordappInternal = copy(config = emptyMap(), signed = false)
override val jarFile: Path by lazy {
val toPathAttempt = Try.on(uri::toPath)
when (toPathAttempt) {
is Success -> if (signed) TestCordappSigner.signJarCopy(toPathAttempt.value) else toPathAttempt.value
is Failure -> {
// URI is not a local path, so we copy it to a temp file and use that.
val downloaded = Files.createTempFile("test-cordapp-${uri.path.substringAfterLast("/").substringBeforeLast(".jar")}", ".jar")
downloaded.toFile().deleteOnExit()
uri.toURL().openStream().use { it.copyTo(downloaded, REPLACE_EXISTING) }
if (signed) {
TestCordappSigner.signJar(downloaded)
}
downloaded
}
}
}
}