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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 250 additions and 109 deletions

View File

@ -4,7 +4,6 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.TransactionState
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.copyToDirectory
import net.corda.core.internal.hash
import net.corda.core.internal.mapToSet
import net.corda.core.internal.toPath
@ -23,30 +22,26 @@ import net.corda.finance.flows.CashIssueAndPaymentFlow
import net.corda.finance.issuedBy
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.ALICE_NAME
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 net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.NodeParameters
import net.corda.testing.node.TestCordapp
import net.corda.testing.node.internal.DriverDSLImpl
import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP
import net.corda.testing.node.internal.TestCordappInternal
import net.corda.testing.node.internal.UriTestCordapp
import net.corda.testing.node.internal.enclosedCordapp
import net.corda.testing.node.internal.internalDriver
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import java.time.Instant
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.copyTo
import kotlin.io.path.createDirectories
import kotlin.io.path.div
import kotlin.io.path.inputStream
import kotlin.io.path.isRegularFile
import kotlin.io.path.moveTo
@ -57,30 +52,16 @@ class TransactionBuilderDriverTest {
val legacyFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts-4.11.jar")!!.toPath()
}
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@Before
fun initJarSigner() {
tempFolder.root.toPath().generateKey("testAlias", "testPassword", ALICE_NAME.toString())
}
private fun signJar(jar: Path) {
tempFolder.root.toPath().signJar(jar.absolutePathString(), "testAlias", "testPassword")
}
@Test(timeout=300_000)
fun `adds CorDapp dependencies`() {
internalDriver(cordappsForAllNodes = listOf(FINANCE_WORKFLOWS_CORDAPP), startNodesInProcess = false) {
val (cordapp, dependency) = splitFinanceContractCordapp(currentFinanceContractsJar)
cordapp.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
dependency.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
cordapp.jarFile.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
dependency.jarFile.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
// Start the node with the CorDapp but without the dependency
cordapp.copyToDirectory((baseDirectory(ALICE_NAME) / "cordapps").createDirectories())
val node = startNode(NodeParameters(ALICE_NAME)).getOrThrow()
val node = startNode(NodeParameters(ALICE_NAME, additionalCordapps = listOf(cordapp))).getOrThrow()
// First make sure the missing dependency causes an issue
assertThatThrownBy {
@ -88,10 +69,10 @@ class TransactionBuilderDriverTest {
}.hasMessageContaining("Transaction being built has a missing attachment for class net/corda/finance/contracts/asset/")
// Upload the missing dependency
dependency.inputStream().use(node.rpc::uploadAttachment)
dependency.jarFile.inputStream().use(node.rpc::uploadAttachment)
val stx = createTransaction(node)
assertThat(stx.tx.attachments).contains(cordapp.hash, dependency.hash)
assertThat(stx.tx.attachments).contains(cordapp.jarFile.hash, dependency.jarFile.hash)
}
}
@ -103,17 +84,16 @@ class TransactionBuilderDriverTest {
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
) {
val (legacyContracts, legacyDependency) = splitFinanceContractCordapp(legacyFinanceContractsJar)
// Re-sign the current finance contracts CorDapp with the same key as the split legacy CorDapp
val currentContracts = currentFinanceContractsJar.copyTo(Path("${currentFinanceContractsJar.toString().substringBeforeLast(".")}-RESIGNED.jar"), overwrite = true)
currentContracts.unsignJar()
signJar(currentContracts)
val currentContracts = TestCordapp.of(currentFinanceContractsJar.toUri()).asSigned() as TestCordappInternal
currentContracts.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
currentContracts.jarFile.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
// Start the node with the legacy CorDapp but without the dependency
legacyContracts.copyToDirectory((baseDirectory(ALICE_NAME) / "legacy-contracts").createDirectories())
currentContracts.copyToDirectory((baseDirectory(ALICE_NAME) / "cordapps").createDirectories())
val node = startNode(NodeParameters(ALICE_NAME)).getOrThrow()
val node = startNode(NodeParameters(
ALICE_NAME,
additionalCordapps = listOf(currentContracts),
legacyContracts = listOf(legacyContracts)
)).getOrThrow()
// First make sure the missing dependency causes an issue
assertThatThrownBy {
@ -121,10 +101,10 @@ class TransactionBuilderDriverTest {
}.hasMessageContaining("Transaction being built has a missing legacy attachment for class net/corda/finance/contracts/asset/")
// Upload the missing dependency
legacyDependency.inputStream().use(node.rpc::uploadAttachment)
legacyDependency.jarFile.inputStream().use(node.rpc::uploadAttachment)
val stx = createTransaction(node)
assertThat(stx.tx.legacyAttachments).contains(legacyContracts.hash, legacyDependency.hash)
assertThat(stx.tx.legacyAttachments).contains(legacyContracts.jarFile.hash, legacyDependency.jarFile.hash)
}
}
@ -139,16 +119,16 @@ class TransactionBuilderDriverTest {
val (currentCashContract, currentCpContract) = splitJar(currentFinanceContractsJar) { "CommercialPaper" in it.absolutePathString() }
val (legacyCashContract, _) = splitJar(legacyFinanceContractsJar) { "CommercialPaper" in it.absolutePathString() }
currentCashContract.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
currentCpContract.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
currentCashContract.jarFile.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
currentCpContract.jarFile.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
// The node has the legacy CommericalPaper contract missing
val cordappsDir = (baseDirectory(ALICE_NAME) / "cordapps").createDirectories()
currentCashContract.copyToDirectory(cordappsDir)
currentCpContract.copyToDirectory(cordappsDir)
legacyCashContract.copyToDirectory((baseDirectory(ALICE_NAME) / "legacy-contracts").createDirectories())
val node = startNode(NodeParameters(
ALICE_NAME,
additionalCordapps = listOf(currentCashContract, currentCpContract),
legacyContracts = listOf(legacyCashContract)
)).getOrThrow()
val node = startNode(NodeParameters(ALICE_NAME)).getOrThrow()
assertThatThrownBy { node.rpc.startFlow(::TwoContractTransactionFlow).returnValue.getOrThrow() }
.hasMessageContaining("Transaction being built has a missing legacy attachment")
.hasMessageContaining("CommercialPaper")
@ -158,11 +138,11 @@ class TransactionBuilderDriverTest {
/**
* Split the given finance contracts jar into two such that the second jar becomes a dependency to the first.
*/
private fun DriverDSLImpl.splitFinanceContractCordapp(contractsJar: Path): Pair<Path, Path> {
private fun DriverDSLImpl.splitFinanceContractCordapp(contractsJar: Path): Pair<UriTestCordapp, UriTestCordapp> {
return splitJar(contractsJar) { it.absolutePathString() == "/net/corda/finance/contracts/asset/CashUtilities.class" }
}
private fun DriverDSLImpl.splitJar(path: Path, move: (Path) -> Boolean): Pair<Path, Path> {
private fun DriverDSLImpl.splitJar(path: Path, move: (Path) -> Boolean): Pair<UriTestCordapp, UriTestCordapp> {
val jar1 = Files.createTempFile(driverDirectory, "jar1-", ".jar")
val jar2 = Files.createTempFile(driverDirectory, "jar2-", ".jar")
@ -181,10 +161,10 @@ class TransactionBuilderDriverTest {
}
jar1.unsignJar()
signJar(jar1)
signJar(jar2)
return Pair(jar1, jar2)
return Pair(
TestCordapp.of(jar1.toUri()).asSigned() as UriTestCordapp,
TestCordapp.of(jar2.toUri()).asSigned() as UriTestCordapp
)
}
private fun DriverDSLImpl.createTransaction(node: NodeHandle): SignedTransaction {

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
}
}
}
}