diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index efd7fd2ac8..a698038242 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -212,7 +212,7 @@ data class Sort(val columns: Collection) : BaseSort() { data class AttachmentSort(val columns: Collection) : BaseSort() { enum class AttachmentSortAttribute(val columnName: String) { - INSERTION_DATE("insertion_date"), + INSERTION_DATE("insertionDate"), UPLOADER("uploader"), FILENAME("filename"), VERSION ("version") diff --git a/docs/source/api-contract-constraints.rst b/docs/source/api-contract-constraints.rst index 2fc81d0e98..295b3271c9 100644 --- a/docs/source/api-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -230,6 +230,20 @@ The contract attachment non-downgrade rule is enforced in two locations: A version number is stored in the manifest information of the enclosing JAR file. This version identifier should be a whole number starting from 1. This information should be set using the Gradle cordapp plugin, or manually, as described in :doc:`versioning`. + +Uniqueness requirement Contract and Version for Signature Constraint +-------------------------------------------------------------------- + +CorDapps in Corda 4 may be signed (to use new signature constraints functionality) or unsigned, and versioned. +The following controls are enforced for these different types of jars within the attachment store of a node: + +- Signed contract JARs must be uniquely versioned per contract class (or group of). + At runtime the node will throw a `DuplicateContractClassException`` exception if this condition is violated. + +- Unsigned contract JARs: there may exist multiple instances of the same contract jar. + At run-time the node will warn of duplicates encountered. + The most recent version given by insertionDate into the attachment storage will be used upon transaction building/resolution. + Issues when using the HashAttachmentConstraint ---------------------------------------------- diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/RpcExceptions.kt b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/RpcExceptions.kt index 0ef6e6b295..6f3be3883a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/RpcExceptions.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/RpcExceptions.kt @@ -11,6 +11,12 @@ import net.corda.core.serialization.CordaSerializable */ class DuplicateAttachmentException(attachmentHash: String) : java.nio.file.FileAlreadyExistsException(attachmentHash), ClientRelevantError +/** + * Thrown to indicate that a contract class name of the same version was already uploaded to a Corda node. + */ +class DuplicateContractClassException(contractClassName: String, version: Int, attachmentHashes: List) : + Exception("Contract $contractClassName version '$version' already present in the attachments $attachmentHashes"), ClientRelevantError + /** * Thrown to indicate that a flow was not designed for RPC and should be started from an RPC client. */ diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt index b97935f55a..1f6689f09c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt @@ -80,18 +80,24 @@ class FlowCheckpointVersionNodeStartupCheckTest { @Test fun `restart node with incompatible version of suspended flow due to different jar hash`() { driver(parametersForRestartingNodes()) { - val uniqueName = "different-jar-hash-test-${UUID.randomUUID()}" - val cordapp = defaultCordapp.copy(name = uniqueName) + val uniqueWorkflowJarName = "different-jar-hash-test-${UUID.randomUUID()}" + val uniqueContractJarName = "contract-$uniqueWorkflowJarName" + val defaultWorkflowJar = cordappWithPackages(SendMessageFlow::class.packageName) + val defaultContractJar = cordappWithPackages(MessageState::class.packageName) + val contractJar = defaultContractJar.copy(name = uniqueContractJarName) + val workflowJar = defaultWorkflowJar.copy(name = uniqueWorkflowJarName) - val bobBaseDir = createSuspendedFlowInBob(setOf(cordapp)) + val bobBaseDir = createSuspendedFlowInBob(setOf(workflowJar, contractJar)) val cordappsDir = bobBaseDir / "cordapps" - val cordappJar = cordappsDir.list().single { it.toString().endsWith(".jar") } + val cordappJar = cordappsDir.list().single { + ! it.toString().contains(uniqueContractJarName) && it.toString().endsWith(".jar") + } // Make sure we're dealing with right jar - assertThat(cordappJar.fileName.toString()).contains(uniqueName) + assertThat(cordappJar.fileName.toString()).contains(uniqueWorkflowJarName) // The name is part of the MANIFEST so changing it is sufficient to change the jar hash - val modifiedCordapp = cordapp.copy(name = "${cordapp.name}-modified") + val modifiedCordapp = workflowJar.copy(name = "${workflowJar.name}-modified") val modifiedCordappJar = CustomCordapp.getJarFile(modifiedCordapp) modifiedCordappJar.moveTo(cordappJar, REPLACE_EXISTING) @@ -125,8 +131,8 @@ class FlowCheckpointVersionNodeStartupCheckTest { )).getOrThrow() } - val logDir = baseDirectory(BOB_NAME) / NodeStartup.LOGS_DIRECTORY_NAME - val logFile = logDir.list { it.filter { it.fileName.toString().endsWith(".log") }.findAny().get() } + val logDir = baseDirectory(BOB_NAME) + val logFile = logDir.list { it.filter { it.fileName.toString().endsWith("out.log") }.findAny().get() } val matchingLineCount = logFile.readLines { it.filter { line -> logMessage in line }.count() } assertEquals(1, matchingLineCount) } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index 4ff0462744..e5fb69496f 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -29,6 +29,7 @@ import net.corda.node.utilities.InfrequentlyMutatedCache import net.corda.node.utilities.NonInvalidatingCache import net.corda.node.utilities.NonInvalidatingWeightBasedCache import net.corda.nodeapi.exceptions.DuplicateAttachmentException +import net.corda.nodeapi.exceptions.DuplicateContractClassException import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.nodeapi.internal.persistence.currentDBSession @@ -305,6 +306,22 @@ class NodeAttachmentService( currentDBSession().find(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) != null } + private fun verifyVersionUniquenessForSignedAttachments(contractClassNames: List, contractVersion: Int, signers: List?){ + if (signers != null && signers.isNotEmpty()) { + contractClassNames.forEach { + val existingContractsImplementations = queryAttachments(AttachmentQueryCriteria.AttachmentsQueryCriteria( + contractClassNamesCondition = Builder.equal(listOf(it)), + versionCondition = Builder.equal(contractVersion), + uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS), + isSignedCondition = Builder.equal(true)) + ) + if (existingContractsImplementations.isNotEmpty()) { + throw DuplicateContractClassException(it, contractVersion, existingContractsImplementations.map { it.toString() }) + } + } + } + } + // TODO: PLT-147: The attachment should be randomised to prevent brute force guessing and thus privacy leaks. private fun import(jar: InputStream, uploader: String?, filename: String?): AttachmentId { return database.transaction { @@ -324,6 +341,9 @@ class NodeAttachmentService( val jarSigners = getSigners(bytes) val contractVersion = getVersion(bytes) val session = currentDBSession() + + verifyVersionUniquenessForSignedAttachments(contractClassNames, contractVersion, jarSigners) + val attachment = NodeAttachmentService.DBAttachment( attId = id.toString(), content = bytes, @@ -344,6 +364,7 @@ class NodeAttachmentService( val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, id.toString()) // update the `uploader` field (as the existing attachment may have been resolved from a peer) if (attachment.uploader != uploader) { + verifyVersionUniquenessForSignedAttachments(contractClassNames, attachment.version, attachment.signers) attachment.uploader = uploader session.saveOrUpdate(attachment) log.info("Updated attachment $id with uploader $uploader") @@ -430,7 +451,8 @@ class NodeAttachmentService( private fun getContractAttachmentVersions(contractClassName: String): NavigableMap = contractsCache.get(contractClassName) { name -> val attachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf(name)), versionCondition = Builder.greaterThanOrEqual(0), uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS)) - val attachmentSort = AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC))) + val attachmentSort = AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC), + AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.INSERTION_DATE, Sort.Direction.DESC))) database.transaction { val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder @@ -447,15 +469,17 @@ class NodeAttachmentService( val query = session.createQuery(criteriaQuery) // execution - TreeMap(query.resultList.groupBy { it.version }.map { makeAttachmentIds(it) }.toMap()) + TreeMap(query.resultList.groupBy { it.version }.map { makeAttachmentIds(it, name) }.toMap()) } } - private fun makeAttachmentIds(it: Map.Entry>): Pair { - check(it.value.size <= 2) - val signed = it.value.filter { it.signers?.isNotEmpty() ?: false }.map { AttachmentId.parse(it.attId) }.singleOrNull() - val unsigned = it.value.filter { it.signers?.isEmpty() ?: true }.map { AttachmentId.parse(it.attId) }.singleOrNull() - return it.key to AttachmentIds(signed, unsigned) + private fun makeAttachmentIds(it: Map.Entry>, contractClassName: String): Pair { + val signed = it.value.filter { it.signers?.isNotEmpty() ?: false }.map { AttachmentId.parse(it.attId) } + check (signed.size <= 1) //sanity check + val unsigned = it.value.filter { it.signers?.isEmpty() ?: true }.map { AttachmentId.parse(it.attId) } + if (unsigned.size > 1) //TODO cater better for whiltelisted JARs - CORDA-2405 + log.warn("Selecting attachment ${unsigned.first()} from duplicated, unsigned attachments ${unsigned.map { it.toString() }} for contract $contractClassName version '${it.key}'.") + return it.key to AttachmentIds(signed.singleOrNull(), unsigned.firstOrNull()) } override fun getContractAttachmentWithHighestContractVersion(contractClassName: String, minContractVersion: Int): AttachmentId? { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 358c35a4cc..3e0a72fe67 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -231,11 +231,12 @@ class HibernateAttachmentQueryCriteriaParser(override val criteriaBuilder: Crite } criteria.isSignedCondition?.let { isSigned -> - val joinDBAttachmentToSigners = root.joinList("signers") - if (isSigned == Builder.equal(true)) + if (isSigned == Builder.equal(true)) { + val joinDBAttachmentToSigners = root.joinList("signers") predicateSet.add(criteriaBuilder.and(joinDBAttachmentToSigners.isNotNull)) - else - predicateSet.add(criteriaBuilder.and(joinDBAttachmentToSigners.isNull)) + } else { + predicateSet.add(criteriaBuilder.equal(criteriaBuilder.size(root.get?>("signers")),0)) + } } criteria.versionCondition?.let { diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index 3e060e1989..e01ae3575f 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -18,6 +18,7 @@ import net.corda.core.node.services.vault.Sort import net.corda.core.utilities.getOrThrow import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.nodeapi.exceptions.DuplicateAttachmentException +import net.corda.nodeapi.exceptions.DuplicateContractClassException import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.internal.ContractJarTestUtils.makeTestContractJar @@ -33,6 +34,7 @@ import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.startFlow import org.assertj.core.api.Assertions.* import org.junit.After +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Ignore import org.junit.Test @@ -268,10 +270,18 @@ class NodeAttachmentServiceTest { storage.queryAttachments(AttachmentsQueryCriteria(signersCondition = Builder.equal(listOf(publicKey)))).size ) - assertEquals( - 3, - storage.queryAttachments(AttachmentsQueryCriteria(isSignedCondition = Builder.equal(true))).size - ) + val allAttachments = storage.queryAttachments(AttachmentsQueryCriteria()) + assertEquals(6, allAttachments.size) + + val signedAttachments = storage.queryAttachments(AttachmentsQueryCriteria(isSignedCondition = Builder.equal(true))) + assertEquals(3, signedAttachments.size) + + val unsignedAttachments = storage.queryAttachments(AttachmentsQueryCriteria(isSignedCondition = Builder.equal(false))) + assertEquals(3, unsignedAttachments.size) + + assertNotEquals(signedAttachments.toSet(), unsignedAttachments.toSet()) + + assertEquals(signedAttachments.toSet() + unsignedAttachments.toSet(), allAttachments.toSet()) assertEquals( 1, @@ -315,6 +325,138 @@ class NodeAttachmentServiceTest { } } + @Test + fun `cannot import jar with duplicated contract class, version and signers for trusted uploader`() { + SelfCleaningDir().use { file -> + val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract") + val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar") + contractJar.read { storage.privilegedImportAttachment(it, "app", "sample.jar") } + + assertThatExceptionOfType(DuplicateContractClassException::class.java).isThrownBy { + anotherContractJar.read { storage.privilegedImportAttachment(it, "app", "another-sample.jar") } + } + } + } + + @Test + fun `can import jar with duplicated contract class, version and signers - when one uploader is trusted and other isnt`() { + SelfCleaningDir().use { file -> + val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract") + val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar") + val attachmentId = contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") } + val anotherAttachmentId = anotherContractJar.read { storage.privilegedImportAttachment(it, "app", "another-sample.jar") } + assertNotEquals(attachmentId, anotherAttachmentId) + } + } + + @Test + fun `can promote to trusted uploader for the same attachment`() { + SelfCleaningDir().use { file -> + val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract") + val attachmentId = contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") } + val reimportedAttachmentId = contractJar.read { storage.privilegedImportAttachment(it, "app", "sample.jar") } + assertEquals(attachmentId, reimportedAttachmentId) + } + } + + @Test + fun `cannot promote to trusted uploader if other trusted attachment already has duplicated contract class, version and signers`() { + SelfCleaningDir().use { file -> + val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract") + contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") } + val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar") + anotherContractJar.read { storage.privilegedImportAttachment(it, "app", "another-sample.jar") } + + assertThatExceptionOfType(DuplicateContractClassException::class.java).isThrownBy { + contractJar.read { storage.privilegedImportAttachment(it, "app", "sample.jar") } + } + + } + } + + @Test + fun `cannot promote to trusted uploder the same jar if other trusted uplodaer `() { + SelfCleaningDir().use { file -> + val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract") + val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar") + contractJar.read { storage.privilegedImportAttachment(it, "app", "sample.jar") } + + assertThatExceptionOfType(DuplicateContractClassException::class.java).isThrownBy { + anotherContractJar.read { storage.privilegedImportAttachment(it, "app", "another-sample.jar") } + } + } + } + + @Test + fun `can import duplicated contract class and signers if versions differ`() { + SelfCleaningDir().use { file -> + val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract", 2) + val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar") + contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") } + anotherContractJar.read { storage.importAttachment(it, "uploaderA", "another-sample.jar") } + + val attachments = storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract")))) + assertEquals(2, attachments.size) + attachments.forEach { + assertTrue("com.example.MyContract" in (storage.openAttachment(it) as ContractAttachment).allContracts) + } + } + } + + @Test + fun `can import duplicated contract class and version from unsiged attachment if a signed attachment already exists`() { + SelfCleaningDir().use { file -> + val (contractJar, _) = makeTestSignedContractJar(file.path, "com.example.MyContract") + val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), generateManifest = false, jarFileName = "another-sample.jar") + contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") } + anotherContractJar.read { storage.importAttachment(it, "uploaderB", "another-sample.jar") } + + val attachments = storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract")))) + assertEquals(2, attachments.size) + attachments.forEach { + val att = storage.openAttachment(it) + assertTrue(att is ContractAttachment) + assertTrue("com.example.MyContract" in (att as ContractAttachment).allContracts) + } + } + } + + @Test + fun `can import duplicated contract class and version from siged attachment if an unsigned attachment already exists`() { + SelfCleaningDir().use { file -> + val contractJar = makeTestContractJar(file.path, "com.example.MyContract") + val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), true, generateManifest = false, jarFileName = "another-sample.jar") + contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") } + anotherContractJar.read { storage.importAttachment(it, "uploaderB", "another-sample.jar") } + + val attachments = storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract")))) + assertEquals(2, attachments.size) + attachments.forEach { + val att = storage.openAttachment(it) + assertTrue(att is ContractAttachment) + assertTrue("com.example.MyContract" in (att as ContractAttachment).allContracts) + } + } + } + + @Test + fun `can import duplicated contract class and version for unsigned attachments`() { + SelfCleaningDir().use { file -> + val contractJar = makeTestContractJar(file.path, "com.example.MyContract") + val anotherContractJar = makeTestContractJar(file.path, listOf( "com.example.MyContract", "com.example.AnotherContract"), generateManifest = false, jarFileName = "another-sample.jar") + contractJar.read { storage.importAttachment(it, "uploaderA", "sample.jar") } + anotherContractJar.read { storage.importAttachment(it, "uploaderB", "another-sample.jar") } + + val attachments = storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract")))) + assertEquals(2, attachments.size) + attachments.forEach { + val att = storage.openAttachment(it) + assertTrue(att is ContractAttachment) + assertTrue("com.example.MyContract" in (att as ContractAttachment).allContracts) + } + } + } + @Test fun `sorting and compound conditions work`() { val (jarA, hashA) = makeTestJar(listOf(Pair("a", "a"))) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/ContractJarTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/ContractJarTestUtils.kt index 573aa77bb3..4bda623a87 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/ContractJarTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/ContractJarTestUtils.kt @@ -44,15 +44,20 @@ object ContractJarTestUtils { } } - @JvmOverloads - fun makeTestSignedContractJar(workingDir: Path, contractName: String, version: Int = 1): Pair { + private fun Path.signWithDummyKey(jarName: Path): PublicKey{ val alias = "testAlias" val pwd = "testPassword" - workingDir.generateKey(alias, pwd, ALICE_NAME.toString()) + this.generateKey(alias, pwd, ALICE_NAME.toString()) + val signer = this.signJar(jarName.toAbsolutePath().toString(), alias, pwd) + (this / "_shredder").delete() + (this / "_teststore").delete() + return signer + } + + @JvmOverloads + fun makeTestSignedContractJar(workingDir: Path, contractName: String, version: Int = 1): Pair { val jarName = makeTestContractJar(workingDir, contractName, true, version) - val signer = workingDir.signJar(jarName.toAbsolutePath().toString(), alias, pwd) - (workingDir / "_shredder").delete() - (workingDir / "_teststore").delete() + val signer = workingDir.signWithDummyKey(jarName) return workingDir.resolve(jarName) to signer } @@ -67,6 +72,23 @@ object ContractJarTestUtils { return workingDir.resolve(jarName) } + @JvmOverloads + fun makeTestContractJar(workingDir: Path, contractNames: List, signed: Boolean = false, version: Int = 1, generateManifest: Boolean = true, jarFileName : String? = null): Path { + contractNames.forEach { + val packages = it.split(".") + val className = packages.last() + createTestClass(workingDir, className, packages.subList(0, packages.size - 1)) + } + val packages = contractNames.first().split(".") + val jarName = jarFileName ?: "attachment-${packages.last()}-$version-${(if (signed) "signed" else "")}.jar" + workingDir.createJar(jarName, *contractNames.map{ "${it.replace(".", "/")}.class" }.toTypedArray() ) + if (generateManifest) + workingDir.addManifest(jarName, Pair(Attributes.Name(CORDAPP_CONTRACT_VERSION), version.toString())) + if (signed) + workingDir.signWithDummyKey(workingDir.resolve(jarName)) + return workingDir.resolve(jarName) + } + private fun createTestClass(workingDir: Path, className: String, packages: List): Path { val newClass = """package ${packages.joinToString(".")}; import net.corda.core.contracts.*; diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt index 4174cff65d..073bff3491 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt @@ -84,7 +84,7 @@ object JarSignatureTestUtils { manifest.mainAttributes[attributeName] = value } val output = JarOutputStream(FileOutputStream((this / fileName).toFile()), manifest) - var entry= input.nextEntry + var entry = input.nextEntry val buffer = ByteArray(1 shl 14) while (true) { output.putNextEntry(entry)