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.contracts.TransactionState
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.copyToDirectory
import net.corda.core.internal.hash import net.corda.core.internal.hash
import net.corda.core.internal.mapToSet import net.corda.core.internal.mapToSet
import net.corda.core.internal.toPath import net.corda.core.internal.toPath
@ -23,30 +22,26 @@ import net.corda.finance.flows.CashIssueAndPaymentFlow
import net.corda.finance.issuedBy import net.corda.finance.issuedBy
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.ALICE_NAME 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.core.internal.JarSignatureTestUtils.unsignJar
import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.NodeParameters 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.DriverDSLImpl
import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP 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.enclosedCordapp
import net.corda.testing.node.internal.internalDriver import net.corda.testing.node.internal.internalDriver
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy import org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy
import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString import kotlin.io.path.absolutePathString
import kotlin.io.path.copyTo import kotlin.io.path.copyTo
import kotlin.io.path.createDirectories import kotlin.io.path.createDirectories
import kotlin.io.path.div
import kotlin.io.path.inputStream import kotlin.io.path.inputStream
import kotlin.io.path.isRegularFile import kotlin.io.path.isRegularFile
import kotlin.io.path.moveTo import kotlin.io.path.moveTo
@ -57,30 +52,16 @@ class TransactionBuilderDriverTest {
val legacyFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts-4.11.jar")!!.toPath() 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) @Test(timeout=300_000)
fun `adds CorDapp dependencies`() { fun `adds CorDapp dependencies`() {
internalDriver(cordappsForAllNodes = listOf(FINANCE_WORKFLOWS_CORDAPP), startNodesInProcess = false) { internalDriver(cordappsForAllNodes = listOf(FINANCE_WORKFLOWS_CORDAPP), startNodesInProcess = false) {
val (cordapp, dependency) = splitFinanceContractCordapp(currentFinanceContractsJar) val (cordapp, dependency) = splitFinanceContractCordapp(currentFinanceContractsJar)
cordapp.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment) cordapp.jarFile.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
dependency.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment) dependency.jarFile.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
// Start the node with the CorDapp but without the dependency // Start the node with the CorDapp but without the dependency
cordapp.copyToDirectory((baseDirectory(ALICE_NAME) / "cordapps").createDirectories()) val node = startNode(NodeParameters(ALICE_NAME, additionalCordapps = listOf(cordapp))).getOrThrow()
val node = startNode(NodeParameters(ALICE_NAME)).getOrThrow()
// First make sure the missing dependency causes an issue // First make sure the missing dependency causes an issue
assertThatThrownBy { assertThatThrownBy {
@ -88,10 +69,10 @@ class TransactionBuilderDriverTest {
}.hasMessageContaining("Transaction being built has a missing attachment for class net/corda/finance/contracts/asset/") }.hasMessageContaining("Transaction being built has a missing attachment for class net/corda/finance/contracts/asset/")
// Upload the missing dependency // Upload the missing dependency
dependency.inputStream().use(node.rpc::uploadAttachment) dependency.jarFile.inputStream().use(node.rpc::uploadAttachment)
val stx = createTransaction(node) 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) networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
) { ) {
val (legacyContracts, legacyDependency) = splitFinanceContractCordapp(legacyFinanceContractsJar) val (legacyContracts, legacyDependency) = splitFinanceContractCordapp(legacyFinanceContractsJar)
// Re-sign the current finance contracts CorDapp with the same key as the split legacy CorDapp val currentContracts = TestCordapp.of(currentFinanceContractsJar.toUri()).asSigned() as TestCordappInternal
val currentContracts = currentFinanceContractsJar.copyTo(Path("${currentFinanceContractsJar.toString().substringBeforeLast(".")}-RESIGNED.jar"), overwrite = true)
currentContracts.unsignJar()
signJar(currentContracts)
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 // Start the node with the legacy CorDapp but without the dependency
legacyContracts.copyToDirectory((baseDirectory(ALICE_NAME) / "legacy-contracts").createDirectories()) val node = startNode(NodeParameters(
currentContracts.copyToDirectory((baseDirectory(ALICE_NAME) / "cordapps").createDirectories()) ALICE_NAME,
val node = startNode(NodeParameters(ALICE_NAME)).getOrThrow() additionalCordapps = listOf(currentContracts),
legacyContracts = listOf(legacyContracts)
)).getOrThrow()
// First make sure the missing dependency causes an issue // First make sure the missing dependency causes an issue
assertThatThrownBy { assertThatThrownBy {
@ -121,10 +101,10 @@ class TransactionBuilderDriverTest {
}.hasMessageContaining("Transaction being built has a missing legacy attachment for class net/corda/finance/contracts/asset/") }.hasMessageContaining("Transaction being built has a missing legacy attachment for class net/corda/finance/contracts/asset/")
// Upload the missing dependency // Upload the missing dependency
legacyDependency.inputStream().use(node.rpc::uploadAttachment) legacyDependency.jarFile.inputStream().use(node.rpc::uploadAttachment)
val stx = createTransaction(node) 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 (currentCashContract, currentCpContract) = splitJar(currentFinanceContractsJar) { "CommercialPaper" in it.absolutePathString() }
val (legacyCashContract, _) = splitJar(legacyFinanceContractsJar) { "CommercialPaper" in it.absolutePathString() } val (legacyCashContract, _) = splitJar(legacyFinanceContractsJar) { "CommercialPaper" in it.absolutePathString() }
currentCashContract.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment) currentCashContract.jarFile.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
currentCpContract.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment) currentCpContract.jarFile.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
// The node has the legacy CommericalPaper contract missing // The node has the legacy CommericalPaper contract missing
val cordappsDir = (baseDirectory(ALICE_NAME) / "cordapps").createDirectories() val node = startNode(NodeParameters(
currentCashContract.copyToDirectory(cordappsDir) ALICE_NAME,
currentCpContract.copyToDirectory(cordappsDir) additionalCordapps = listOf(currentCashContract, currentCpContract),
legacyCashContract.copyToDirectory((baseDirectory(ALICE_NAME) / "legacy-contracts").createDirectories()) legacyContracts = listOf(legacyCashContract)
)).getOrThrow()
val node = startNode(NodeParameters(ALICE_NAME)).getOrThrow()
assertThatThrownBy { node.rpc.startFlow(::TwoContractTransactionFlow).returnValue.getOrThrow() } assertThatThrownBy { node.rpc.startFlow(::TwoContractTransactionFlow).returnValue.getOrThrow() }
.hasMessageContaining("Transaction being built has a missing legacy attachment") .hasMessageContaining("Transaction being built has a missing legacy attachment")
.hasMessageContaining("CommercialPaper") .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. * 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" } 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 jar1 = Files.createTempFile(driverDirectory, "jar1-", ".jar")
val jar2 = Files.createTempFile(driverDirectory, "jar2-", ".jar") val jar2 = Files.createTempFile(driverDirectory, "jar2-", ".jar")
@ -181,10 +161,10 @@ class TransactionBuilderDriverTest {
} }
jar1.unsignJar() jar1.unsignJar()
signJar(jar1) return Pair(
signJar(jar2) TestCordapp.of(jar1.toUri()).asSigned() as UriTestCordapp,
TestCordapp.of(jar2.toUri()).asSigned() as UriTestCordapp
return Pair(jar1, jar2) )
} }
private fun DriverDSLImpl.createTransaction(node: NodeHandle): SignedTransaction { 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.Attributes
import java.util.jar.JarOutputStream import java.util.jar.JarOutputStream
import java.util.jar.Manifest import java.util.jar.Manifest
import kotlin.io.path.exists
import kotlin.io.path.fileSize import kotlin.io.path.fileSize
import kotlin.io.path.inputStream import kotlin.io.path.inputStream
import kotlin.io.path.outputStream import kotlin.io.path.outputStream
@ -36,9 +37,10 @@ inline fun <T> Path.useZipFile(block: (FileSystem) -> T): T {
return FileSystems.newFileSystem(this).use(block) 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 -> return useZipFile { zipFs ->
val manifestFile = zipFs.getPath("META-INF", "MANIFEST.MF") val manifestFile = zipFs.getPath("META-INF", "MANIFEST.MF")
if (!manifestFile.exists()) return null
val manifest = manifestFile.inputStream().use(::Manifest) val manifest = manifestFile.inputStream().use(::Manifest)
val result = block(manifest) val result = block(manifest)
manifestFile.outputStream().use(manifest::write) manifestFile.outputStream().use(manifest::write)

View File

@ -21,6 +21,7 @@ import java.util.jar.JarOutputStream
import java.util.jar.Manifest import java.util.jar.Manifest
import kotlin.io.path.deleteExisting import kotlin.io.path.deleteExisting
import kotlin.io.path.div import kotlin.io.path.div
import kotlin.io.path.exists
import kotlin.io.path.listDirectoryEntries import kotlin.io.path.listDirectoryEntries
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -75,7 +76,7 @@ object JarSignatureTestUtils {
fun Path.unsignJar() { fun Path.unsignJar() {
// Remove the signatures // Remove the signatures
useZipFile { zipFs -> 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 // Remove all the hash information of the jar contents
modifyJarManifest { manifest -> modifyJarManifest { manifest ->

View File

@ -25,7 +25,7 @@ import net.corda.testing.node.User
* log level argument. * 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. * @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( data class NodeParameters(
val providedName: CordaX500Name? = null, val providedName: CordaX500Name? = null,
val rpcUsers: List<User> = emptyList(), val rpcUsers: List<User> = emptyList(),
@ -37,7 +37,8 @@ data class NodeParameters(
val flowOverrides: Map<out Class<out FlowLogic<*>>, Class<out FlowLogic<*>>> = emptyMap(), val flowOverrides: Map<out Class<out FlowLogic<*>>, Class<out FlowLogic<*>>> = emptyMap(),
val logLevelOverride: String? = null, val logLevelOverride: String? = null,
val rpcAddress: NetworkHostAndPort? = 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 * 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 withAdditionalCordapps(additionalCordapps: Set<TestCordapp>): NodeParameters = copy(additionalCordapps = additionalCordapps)
fun withFlowOverrides(flowOverrides: Map<Class<out FlowLogic<*>>, Class<out FlowLogic<*>>>): NodeParameters = copy(flowOverrides = flowOverrides) fun withFlowOverrides(flowOverrides: Map<Class<out FlowLogic<*>>, Class<out FlowLogic<*>>>): NodeParameters = copy(flowOverrides = flowOverrides)
fun withLogLevelOverride(logLevelOverride: String?): NodeParameters = copy(logLevelOverride = logLevelOverride) 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( constructor(
providedName: CordaX500Name?, providedName: CordaX500Name?,
@ -221,4 +225,58 @@ data class NodeParameters(
logLevelOverride = logLevelOverride, logLevelOverride = logLevelOverride,
rpcAddress = rpcAddress, rpcAddress = rpcAddress,
systemProperties = systemProperties) 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.core.DoNotImplement
import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.NodeParameters 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] * 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. */ /** Returns a copy of this [TestCordapp] but with the specified CorDapp config. */
abstract fun withConfig(config: Map<String, Any>): TestCordapp 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 { companion object {
/** /**
* Scans the current classpath to find the CorDapp that contains the given package. All the CorDapp's metdata present in its * 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. * @param scanPackage The package name used to find the CorDapp. This does not need to be the root package of the CorDapp.
*/ */
@JvmStatic @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.serialization.SerializationWhitelist
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug 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.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.nio.file.attribute.FileTime 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 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 = fun signed(keyStorePath: Path? = null, numberOfSignatures: Int = 1, keyAlgorithm: String = "RSA"): CustomCordapp =
copy(signingInfo = SigningInfo(keyStorePath, numberOfSignatures, keyAlgorithm)) 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 { private fun createTestManifest(name: String, versionId: Int, targetPlatformVersion: Int): Manifest {
val manifest = 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 { companion object {
private val logger = contextLogger() private val logger = contextLogger()
private val epochFileTime = FileTime.from(Instant.EPOCH) private val epochFileTime = FileTime.from(Instant.EPOCH)
private val cordappsDirectory: Path private val cordappsDirectory: Path
private val defaultJarSignerDirectory: Path
private val whitespace = "\\s++".toRegex() private val whitespace = "\\s++".toRegex()
private val cache = ConcurrentHashMap<CustomCordapp, Path>() private val cache = ConcurrentHashMap<CustomCordapp, Path>()
@ -174,7 +155,6 @@ data class CustomCordapp(
val buildDir = Paths.get("build").toAbsolutePath() val buildDir = Paths.get("build").toAbsolutePath()
val timeDirName = getTimestampAsDirectoryName() val timeDirName = getTimestampAsDirectoryName()
cordappsDirectory = buildDir / "generated-custom-cordapps" / timeDirName cordappsDirectory = buildDir / "generated-custom-cordapps" / timeDirName
defaultJarSignerDirectory = buildDir / "jar-signer" / timeDirName
} }
fun getJarFile(cordapp: CustomCordapp): Path { fun getJarFile(cordapp: CustomCordapp): Path {
@ -187,7 +167,9 @@ data class CustomCordapp(
} else if (it.packages.isNotEmpty() || it.classes.isNotEmpty()) { } else if (it.packages.isNotEmpty() || it.classes.isNotEmpty()) {
it.packageAsJar(jarFile) 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" } logger.debug { "$it packaged into $jarFile" }
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.map
import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.concurrent.transpose 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_LICENCE
import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_NAME 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_VENDOR
@ -55,6 +56,7 @@ import net.corda.node.internal.DataSourceFactory
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.internal.NodeWithInfo import net.corda.node.internal.NodeWithInfo
import net.corda.node.internal.clientSslOptionsCompatibleWith 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.Permissions
import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.FlowOverride import net.corda.node.services.config.FlowOverride
@ -716,6 +718,11 @@ class DriverDSLImpl(
extraCustomCordapps + (cordappsForAllNodes ?: emptySet()) 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 nodeFuture = if (parameters.startInSameProcess ?: startNodesInProcess) {
val nodeAndThreadFuture = startInProcessNode(executorService, config, allowHibernateToManageAppSchema) val nodeAndThreadFuture = startInProcessNode(executorService, config, allowHibernateToManageAppSchema)
shutdownManager.registerShutdown( 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. * You will probably need to use [FINANCE_CORDAPPS] instead to get access to the flows as well.
*/ */
@JvmField @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 * 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. * You will probably need to use [FINANCE_CORDAPPS] instead to get access to the contract classes as well.
*/ */
@JvmField @JvmField
val FINANCE_WORKFLOWS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.workflows") val FINANCE_WORKFLOWS_CORDAPP: ScanPackageTestCordapp = findCordapp("net.corda.finance.workflows")
@JvmField @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 * *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 * 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. */ /** Create a *custom* CorDapp which just contains the enclosed classes of the receiver class. */
fun Any.enclosedCordapp(): CustomCordapp { fun Any.enclosedCordapp(): CustomCordapp {

View File

@ -2,6 +2,7 @@ package net.corda.testing.node.internal
import io.github.classgraph.ClassGraph import io.github.classgraph.ClassGraph
import net.corda.core.internal.attributes import net.corda.core.internal.attributes
import net.corda.core.internal.mapToSet
import net.corda.core.internal.pooledScan import net.corda.core.internal.pooledScan
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.testing.node.TestCordapp 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 * 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. * 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() { data class ScanPackageTestCordapp(val scanPackage: String,
override fun withConfig(config: Map<String, Any>): TestCordappImpl = copy(config = config) 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 override fun withOnlyJarContents(): ScanPackageTestCordapp = copy(config = emptyMap(), signed = false)
get() {
val jars = findJars(scanPackage) override val jarFile: Path by lazy {
when (jars.size) { val jars = findJars()
0 -> throw IllegalArgumentException("There are no CorDapps containing the package $scanPackage on the classpath. Make sure " + val jar = when (jars.size) {
"the package name is correct and that the CorDapp is added as a gradle dependency.") 0 -> throw IllegalArgumentException("There are no CorDapps containing the package $scanPackage on the classpath. Make sure " +
1 -> return jars.first() "the package name is correct and that the CorDapp is added as a gradle dependency.")
else -> throw IllegalArgumentException("There is more than one CorDapp containing the package $scanPackage on the classpath " + 1 -> jars.first()
"$jars. Specify a package name which is unique to the CorDapp.") 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 { companion object {
private val packageToRootPaths = ConcurrentHashMap<String, Set<Path>>() private val packageToRootPaths = ConcurrentHashMap<String, Set<Path>>()
private val projectRootToBuiltJar = ConcurrentHashMap<Path, Path>() private val projectRootToBuiltJar = ConcurrentHashMap<Path, Path>()
private val log = contextLogger() 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> { private fun findRootPaths(scanPackage: String): Set<Path> {
return packageToRootPaths.computeIfAbsent(scanPackage) { return packageToRootPaths.computeIfAbsent(scanPackage) {
val classGraph = ClassGraph().acceptPaths(scanPackage.replace('.', '/')) 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
}
}
}
}