From 8fd3139df1a660fbc7cafb29b019a4a40def9c32 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 8 Feb 2024 17:54:04 +0000 Subject: [PATCH] ENT-11355: Cleanup of TransactionBuilder and CorDapp loading This is code refactoring and cleanup that is required to add a new WireTransaction component group for 4.12+ attachments, and for supporting legacy (4.11 or older) contract CorDapps in the node. --- .../verification/AttachmentFixupsTest.kt | 3 +- ...ttachmentsClassLoaderSerializationTests.kt | 15 +- .../AttachmentsClassLoaderTests.kt | 10 +- .../transactions/TransactionBuilderTest.kt | 24 ++ .../net/corda/core/internal/CordaUtils.kt | 8 - .../net/corda/core/internal/InternalUtils.kt | 18 + .../core/internal/cordapp/CordappImpl.kt | 7 +- .../cordapp/CordappProviderInternal.kt | 7 + .../verification/NodeVerificationSupport.kt | 2 +- .../internal/AttachmentsClassLoader.kt | 2 +- .../core/transactions/LedgerTransaction.kt | 2 +- .../core/transactions/TransactionBuilder.kt | 257 ++++++------- .../nodeapi/internal/ContractsScanning.kt | 11 +- .../nodeapi/internal/cordapp/CordappLoader.kt | 20 +- node/build.gradle | 9 + .../net/corda/node/internal/AbstractNode.kt | 1 + .../internal/cordapp/CordappProviderImpl.kt | 93 +++-- .../cordapp/JarScanningCordappLoader.kt | 353 ++++++------------ .../persistence/AttachmentStorageInternal.kt | 30 +- .../statemachine/FlowStateMachineImpl.kt | 7 +- .../cordapp/CordappProviderImplTests.kt | 135 +++---- .../cordapp/JarScanningCordappLoaderTest.kt | 209 +++++++---- node/src/test/resources/isolated.jar | Bin 11209 -> 0 bytes .../cordapp/versions/min-2-no-target.jar | Bin 30781 -> 0 bytes .../cordapp/versions/min-2-target-3.jar | Bin 30790 -> 0 bytes .../versions/no-min-or-target-version.jar | Bin 12182 -> 0 bytes .../internal/CordaClassResolverTests.kt | 18 +- .../coretesting/internal/CoreTestUtils.kt | 30 +- .../core/internal/JarSignatureTestUtils.kt | 16 +- .../net/corda/testing/node/MockServices.kt | 11 +- .../kotlin/net/corda/testing/dsl/TestDSL.kt | 18 +- .../testing/internal/MockCordappProvider.kt | 6 +- .../services/InternalMockAttachmentStorage.kt | 43 --- 33 files changed, 665 insertions(+), 700 deletions(-) delete mode 100644 node/src/test/resources/isolated.jar delete mode 100644 node/src/test/resources/net/corda/node/internal/cordapp/versions/min-2-no-target.jar delete mode 100644 node/src/test/resources/net/corda/node/internal/cordapp/versions/min-2-target-3.jar delete mode 100644 node/src/test/resources/net/corda/node/internal/cordapp/versions/no-min-or-target-version.jar delete mode 100644 testing/test-utils/src/main/kotlin/net/corda/testing/internal/services/InternalMockAttachmentStorage.kt diff --git a/core-tests/src/test/kotlin/net/corda/coretests/internal/verification/AttachmentFixupsTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/internal/verification/AttachmentFixupsTest.kt index f007203309..4102ba9bc3 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/internal/verification/AttachmentFixupsTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/internal/verification/AttachmentFixupsTest.kt @@ -2,7 +2,6 @@ package net.corda.coretests.internal.verification import net.corda.core.internal.verification.AttachmentFixups import net.corda.core.node.services.AttachmentId -import net.corda.node.VersionInfo import net.corda.node.internal.cordapp.JarScanningCordappLoader import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -130,7 +129,7 @@ class AttachmentFixupsTest { } private fun newFixupService(vararg paths: Path): AttachmentFixups { - val loader = JarScanningCordappLoader.fromJarUrls(paths.toSet(), VersionInfo.UNKNOWN) + val loader = JarScanningCordappLoader(paths.toSet()) return AttachmentFixups().apply { load(loader.appClassLoader) } } } diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderSerializationTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderSerializationTests.kt index 63f5461e46..ce8b6571b9 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderSerializationTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderSerializationTests.kt @@ -11,13 +11,13 @@ import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.OpaqueBytes import net.corda.isolated.contracts.DummyContractBackdoor import net.corda.node.services.attachments.NodeAttachmentTrustCalculator +import net.corda.node.services.persistence.toInternal import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.fakeAttachment -import net.corda.testing.internal.services.InternalMockAttachmentStorage import net.corda.testing.services.MockAttachmentStorage import org.apache.commons.io.IOUtils import org.junit.Assert.assertEquals @@ -30,7 +30,7 @@ import kotlin.test.assertFailsWith class AttachmentsClassLoaderSerializationTests { companion object { - val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderSerializationTests::class.java.getResource("/isolated.jar") + val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderSerializationTests::class.java.getResource("/isolated.jar")!! private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.isolated.contracts.AnotherDummyContract" } @@ -38,20 +38,19 @@ class AttachmentsClassLoaderSerializationTests { @JvmField val testSerialization = SerializationEnvironmentRule() - private val storage = InternalMockAttachmentStorage(MockAttachmentStorage()) + private val storage = MockAttachmentStorage().toInternal() private val attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage, TestingNamedCacheFactory()) @Test(timeout=300_000) fun `Can serialize and deserialize with an attachment classloader`() { - - val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party - val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party + val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20).party + val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party val isolatedId = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar") val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar") val att2 = storage.importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar") - val serialisedState = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( + val serialisedState = AttachmentsClassLoaderBuilder.withAttachmentsClassLoaderContext( arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash, @@ -64,7 +63,7 @@ class AttachmentsClassLoaderSerializationTests { val txt = IOUtils.toString(classLoader.getResourceAsStream("file1.txt"), Charsets.UTF_8.name()) assertEquals("some data", txt) - val state = (contract as DummyContractBackdoor).generateInitial(MEGA_CORP.ref(1), 1, DUMMY_NOTARY).outputStates().first() + val state = (contract as DummyContractBackdoor).generateInitial(megaCorp.ref(1), 1, dummyNotary).outputStates().first() val serialisedState = state.serialize() val state1 = serialisedState.deserialize() diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt index fdbf0856f4..8c60b950be 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/AttachmentsClassLoaderTests.kt @@ -25,6 +25,7 @@ import net.corda.core.serialization.internal.AttachmentsClassLoader import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl import net.corda.core.transactions.LedgerTransaction import net.corda.node.services.attachments.NodeAttachmentTrustCalculator +import net.corda.node.services.persistence.toInternal import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.core.ALICE_NAME @@ -36,7 +37,6 @@ import net.corda.testing.core.internal.ContractJarTestUtils import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.fakeAttachment -import net.corda.testing.internal.services.InternalMockAttachmentStorage import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP import net.corda.testing.services.MockAttachmentStorage import org.apache.commons.io.IOUtils @@ -87,7 +87,6 @@ class AttachmentsClassLoaderTests { val testSerialization = SerializationEnvironmentRule() private lateinit var storage: MockAttachmentStorage - private lateinit var internalStorage: InternalMockAttachmentStorage private lateinit var attachmentTrustCalculator: AttachmentTrustCalculator private val networkParameters = testNetworkParameters() private val cacheFactory = TestingNamedCacheFactory(1) @@ -114,8 +113,7 @@ class AttachmentsClassLoaderTests { @Before fun setup() { storage = MockAttachmentStorage() - internalStorage = InternalMockAttachmentStorage(storage) - attachmentTrustCalculator = NodeAttachmentTrustCalculator(internalStorage, cacheFactory) + attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage.toInternal(), cacheFactory) } @Test(timeout=300_000) @@ -449,7 +447,7 @@ class AttachmentsClassLoaderTests { val keyPairB = Crypto.generateKeyPair() attachmentTrustCalculator = NodeAttachmentTrustCalculator( - InternalMockAttachmentStorage(storage), + storage.toInternal(), cacheFactory, blacklistedAttachmentSigningKeys = listOf(keyPairA.public.hash) ) @@ -486,7 +484,7 @@ class AttachmentsClassLoaderTests { val keyPairA = Crypto.generateKeyPair() attachmentTrustCalculator = NodeAttachmentTrustCalculator( - InternalMockAttachmentStorage(storage), + storage.toInternal(), cacheFactory, blacklistedAttachmentSigningKeys = listOf(keyPairA.public.hash) ) diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt index f1887ed00c..53788d5b70 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt @@ -13,6 +13,7 @@ import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash import net.corda.core.internal.HashAgility import net.corda.core.internal.PLATFORM_VERSION +import net.corda.core.internal.RPC_UPLOADER import net.corda.core.internal.digestService import net.corda.core.node.ZoneVersionTooLowException import net.corda.core.serialization.internal._driverSerializationEnv @@ -31,12 +32,14 @@ import net.corda.testing.node.MockServices import net.corda.testing.node.internal.cordappWithPackages import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.assertj.core.api.Assertions.assertThatIllegalArgumentException import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Assert.assertTrue import org.junit.Ignore import org.junit.Rule import org.junit.Test import java.time.Instant +import kotlin.io.path.inputStream import kotlin.test.assertFailsWith class TransactionBuilderTest { @@ -298,4 +301,25 @@ class TransactionBuilderTest { builder.toWireTransaction(services, schemeId) } } + + @Test(timeout=300_000) + fun `contract overlap in explicit attachments`() { + val overlappingAttachmentId = cordappWithPackages("net.corda.testing").jarFile.inputStream().use { + services.attachments.importAttachment(it, RPC_UPLOADER, null) + } + + val outputState = TransactionState( + data = DummyState(), + contract = DummyContract.PROGRAM_ID, + notary = notary + ) + val builder = TransactionBuilder() + .addAttachment(contractAttachmentId) + .addAttachment(overlappingAttachmentId) + .addOutputState(outputState) + .addCommand(DummyCommandData, notary.owningKey) + assertThatIllegalArgumentException() + .isThrownBy { builder.toWireTransaction(services) } + .withMessageContaining("Multiple attachments specified for the same contract net.corda.testing.contracts.DummyContract") + } } diff --git a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt index d0e039e279..16b362926f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt @@ -12,10 +12,7 @@ import net.corda.core.node.ServicesForResolution import net.corda.core.node.ZoneVersionTooLowException import net.corda.core.node.services.TransactionStorage import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SerializationContext import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.transactions.WireTransaction import org.slf4j.MDC import java.security.PublicKey @@ -57,11 +54,6 @@ enum class JavaVersion(val versionString: String) { } } -/** Provide access to internal method for AttachmentClassLoaderTests. */ -fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction { - return toWireTransactionWithContext(services, serializationContext) -} - /** Checks if this flow is an idempotent flow. */ fun Class>.isIdempotentFlow(): Boolean { return IdempotentFlow::class.java.isAssignableFrom(this) diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index c55a3c2503..95929aca32 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -154,6 +154,24 @@ inline fun Collection.flatMapToSet(transform: (T) -> Iterable): Set return if (isEmpty()) emptySet() else flatMapTo(LinkedHashSet(), transform) } +/** + * Map the elements of the [Iterable] to multiple keys. By default duplicate mappings are not allowed. The returned [Map] preserves the + * iteration order of the values. + */ +inline fun Iterable.groupByMultipleKeys( + keysSelector: (V) -> Iterable, + onDuplicate: (K, V, V) -> Unit = { key, value1, value2 -> throw IllegalArgumentException("Duplicate mapping for $key ($value1, $value2)") } +): Map { + val map = LinkedHashMap() + for (value in this) { + for (key in keysSelector(value)) { + val duplicate = map.put(key, value) ?: continue + onDuplicate(key, value, duplicate) + } + } + return map +} + fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options) /** Same as [InputStream.readBytes] but also closes the stream. */ diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt index c9205a9180..961607f086 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt @@ -5,6 +5,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.hash import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.telemetry.TelemetryComponent import net.corda.core.schemas.MappedSchema @@ -32,9 +33,9 @@ data class CordappImpl( override val customSchemas: Set, override val allFlows: List>>, override val info: Cordapp.Info, - override val jarHash: SecureHash.SHA256, override val minimumPlatformVersion: Int, override val targetPlatformVersion: Int, + override val jarHash: SecureHash.SHA256 = jarFile.hash, val notaryService: Class? = null, /** Indicates whether the CorDapp is loaded from external sources, or generated on node startup (virtual). */ val isLoaded: Boolean = true, @@ -53,6 +54,10 @@ data class CordappImpl( classList.mapNotNull { it?.name } + contractClassNames + explicitCordappClasses } + override fun equals(other: Any?): Boolean = other is CordappImpl && this.jarHash == other.jarHash + + override fun hashCode(): Int = 31 * jarHash.hashCode() + companion object { fun jarName(url: Path): String = url.name.removeSuffix(".jar") diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt index c7d14d4c7f..ea4a3d42e0 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt @@ -1,5 +1,7 @@ package net.corda.core.internal.cordapp +import net.corda.core.contracts.ContractAttachment +import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappProvider import net.corda.core.flows.FlowLogic @@ -10,4 +12,9 @@ interface CordappProviderInternal : CordappProvider { val attachmentFixups: AttachmentFixups val cordapps: List fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp? + + /** + * Similar to [getContractAttachmentID] except it returns the [ContractAttachment] object. + */ + fun getContractAttachment(contractClassName: ContractClassName): ContractAttachment? } diff --git a/core/src/main/kotlin/net/corda/core/internal/verification/NodeVerificationSupport.kt b/core/src/main/kotlin/net/corda/core/internal/verification/NodeVerificationSupport.kt index de76e987c3..f4b40dc1a0 100644 --- a/core/src/main/kotlin/net/corda/core/internal/verification/NodeVerificationSupport.kt +++ b/core/src/main/kotlin/net/corda/core/internal/verification/NodeVerificationSupport.kt @@ -100,7 +100,7 @@ interface NodeVerificationSupport : VerificationSupport { val upgradedContractAttachment = getAttachment(wtx.upgradedContractAttachmentId) ?: throw MissingContractAttachments(emptyList()) val networkParameters = getNetworkParameters(wtx.networkParametersHash) ?: throw TransactionResolutionException(wtx.id) - return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( + return AttachmentsClassLoaderBuilder.withAttachmentsClassLoaderContext( listOf(legacyContractAttachment, upgradedContractAttachment), networkParameters, wtx.id, diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt index 669b2ea777..dab65e4374 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt @@ -325,7 +325,7 @@ object AttachmentsClassLoaderBuilder { * @param txId The transaction ID that triggered this request; it's unused except for error messages and exceptions that can occur during setup. */ @Suppress("LongParameterList") - fun withAttachmentsClassloaderContext(attachments: List, + fun withAttachmentsClassLoaderContext(attachments: List, params: NetworkParameters, txId: SecureHash, isAttachmentTrusted: (Attachment) -> Boolean, diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 8845cfca4c..306e0f98b7 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -255,7 +255,7 @@ private constructor( internal fun verifyInternal(txAttachments: List = this.attachments) { // Switch thread local deserialization context to using a cached attachments classloader. This classloader enforces various rules // like no-overlap, package namespace ownership and (in future) deterministic Java. - val verifier = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( + val verifier = AttachmentsClassLoaderBuilder.withAttachmentsClassLoaderContext( txAttachments, getParamsWithGoo(), id, diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index ed97d740d8..5c37d7efe3 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -2,13 +2,13 @@ package net.corda.core.transactions import co.paralleluniverse.strands.Strand -import net.corda.core.CordaInternal import net.corda.core.contracts.* import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.identity.Party import net.corda.core.internal.* +import net.corda.core.internal.PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS import net.corda.core.internal.verification.VerifyingServiceHub import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.NetworkParameters @@ -30,8 +30,6 @@ import java.time.Duration import java.time.Instant import java.util.* import java.util.regex.Pattern -import kotlin.collections.component1 -import kotlin.collections.component2 import kotlin.reflect.KClass /** @@ -74,7 +72,10 @@ open class TransactionBuilder( private fun defaultLockId() = (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID() private val log = contextLogger() private val MISSING_CLASS_DISABLED = java.lang.Boolean.getBoolean("net.corda.transactionbuilder.missingclass.disabled") - + private val automaticConstraints = setOf( + AutomaticPlaceholderConstraint, + @Suppress("DEPRECATION") AutomaticHashConstraint + ) private const val ID_PATTERN = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*" private val FQCP: Pattern = Pattern.compile("$ID_PATTERN(/$ID_PATTERN)+") private fun isValidJavaClass(identifier: String) = FQCP.matcher(identifier).matches() @@ -86,7 +87,7 @@ open class TransactionBuilder( private val inputsWithTransactionState = arrayListOf>() private val referencesWithTransactionState = arrayListOf>() - private val excludedAttachments = arrayListOf() + private var excludedAttachments: Set = emptySet() /** * Creates a copy of the builder. @@ -137,8 +138,7 @@ open class TransactionBuilder( * @throws [ZoneVersionTooLowException] if there are reference states and the zone minimum platform version is less than 4. */ @Throws(MissingContractAttachments::class) - fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services, null) - .apply { checkSupportedHashType() } + fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransaction(services.toVerifyingServiceHub()) /** * Generates a [WireTransaction] from this builder, resolves any [AutomaticPlaceholderConstraint], and selects the attachments to use for this transaction. @@ -172,20 +172,13 @@ open class TransactionBuilder( fun toWireTransaction(services: ServicesForResolution, schemeId: Int, properties: Map): WireTransaction { val magic: SerializationMagic = getCustomSerializationMagicFromSchemeId(schemeId) val serializationContext = SerializationDefaults.P2P_CONTEXT.withPreferredSerializationVersion(magic).withProperties(properties) - return toWireTransactionWithContext(services, serializationContext).apply { checkSupportedHashType() } + return toWireTransaction(services.toVerifyingServiceHub(), serializationContext) } - @CordaInternal - @JvmSynthetic - internal fun toWireTransactionWithContext( - services: ServicesForResolution, - serializationContext: SerializationContext? - ) : WireTransaction = toWireTransactionWithContext(services.toVerifyingServiceHub(), serializationContext, 0) - - private tailrec fun toWireTransactionWithContext( + private tailrec fun toWireTransaction( serviceHub: VerifyingServiceHub, - serializationContext: SerializationContext?, - tryCount: Int + serializationContext: SerializationContext? = null, + tryCount: Int = 0 ): WireTransaction { val referenceStates = referenceStates() if (referenceStates.isNotEmpty()) { @@ -193,8 +186,7 @@ open class TransactionBuilder( } resolveNotary(serviceHub) - val (allContractAttachments: Collection, resolvedOutputs: List>) - = selectContractAttachmentsAndOutputStateConstraints(serviceHub, serializationContext) + val (allContractAttachments, resolvedOutputs) = selectContractAttachmentsAndOutputStateConstraints(serviceHub) // Final sanity check that all states have the correct constraints. for (state in (inputsWithTransactionState.map { it.state } + resolvedOutputs)) { @@ -202,17 +194,21 @@ open class TransactionBuilder( } val wireTx = SerializationFactory.defaultFactory.withCurrentContext(serializationContext) { + // Sort the attachments to ensure transaction builds are stable. + val attachmentsBuilder = allContractAttachments.mapTo(TreeSet()) { it.id } + attachmentsBuilder.addAll(attachments) + attachmentsBuilder.removeAll(excludedAttachments) WireTransaction( createComponentGroups( inputStates(), resolvedOutputs, commands(), - // Sort the attachments to ensure transaction builds are stable. - ((allContractAttachments + attachments).toSortedSet() - excludedAttachments).toList(), + attachmentsBuilder.toList(), notary, window, referenceStates, - serviceHub.networkParametersService.currentHash), + serviceHub.networkParametersService.currentHash + ), privacySalt, serviceHub.digestService ) @@ -224,10 +220,11 @@ open class TransactionBuilder( // TODO - remove once proper support for cordapp dependencies is added. val addedDependency = addMissingDependency(serviceHub, wireTx, tryCount) - return if (addedDependency) - toWireTransactionWithContext(serviceHub, serializationContext, tryCount + 1) - else - wireTx + return if (addedDependency) { + toWireTransaction(serviceHub, serializationContext, tryCount + 1) + } else { + wireTx.apply { checkSupportedHashType() } + } } // Returns the first exception in the hierarchy that matches one of the [types]. @@ -301,10 +298,7 @@ open class TransactionBuilder( } attachments.addAll(extraAttachments) - with(excludedAttachments) { - clear() - addAll(txAttachments - replacementAttachments) - } + excludedAttachments = (txAttachments - replacementAttachments).toSet() log.warn("Attempting to rebuild transaction with these extra attachments:{}{}and these attachments removed:{}", extraAttachments.toPrettyString(), @@ -352,26 +346,15 @@ open class TransactionBuilder( * TODO also on the versions of the attachments of the transactions generating the input states. ( after we add versioning) */ private fun selectContractAttachmentsAndOutputStateConstraints( - services: ServicesForResolution, - @Suppress("UNUSED_PARAMETER") serializationContext: SerializationContext? - ): Pair, List>> { - + serviceHub: VerifyingServiceHub + ): Pair, List>> { // Determine the explicitly set contract attachments. - val explicitAttachmentContracts: List> = this.attachments - .map(services.attachments::openAttachment) - .mapNotNull { it as? ContractAttachment } - .flatMap { attch -> - attch.allContracts.map { it to attch.id } + val explicitContractToAttachments = attachments + .mapNotNull { serviceHub.attachments.openAttachment(it) as? ContractAttachment } + .groupByMultipleKeys(ContractAttachment::allContracts) { contract, attachment1, attachment2 -> + throw IllegalArgumentException("Multiple attachments specified for the same contract $contract ($attachment1, $attachment2).") } - // And fail early if there's more than 1 for a contract. - require(explicitAttachmentContracts.isEmpty() - || explicitAttachmentContracts.groupBy { (ctr, _) -> ctr }.all { (_, groups) -> groups.size == 1 }) { - "Multiple attachments set for the same contract." - } - - val explicitAttachmentContractsMap: Map = explicitAttachmentContracts.toMap() - val inputContractGroups: Map>> = inputsWithTransactionState.map { it.state } .groupBy { it.contract } val outputContractGroups: Map>> = outputs.groupBy { it.contract } @@ -382,38 +365,33 @@ open class TransactionBuilder( // Filter out all contracts that might have been already used by 'normal' input or output states. val referenceStateGroups: Map>> = referencesWithTransactionState.groupBy { it.contract } - val refStateContractAttachments: List = referenceStateGroups + val refStateContractAttachments = referenceStateGroups .filterNot { it.key in allContracts } - .map { refStateEntry -> - getInstalledContractAttachmentId( - refStateEntry.key, - refStateEntry.value, - services - ) - } + .map { refStateEntry -> serviceHub.getInstalledContractAttachment(refStateEntry.key, refStateEntry::value) } // For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment. - val contractAttachmentsAndResolvedOutputStates: List>?>> = allContracts.toSet() - .map { ctr -> - handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], services) - } + val contractAttachmentsAndResolvedOutputStates = allContracts.map { contract -> + selectAttachmentAndResolveOutputStates( + contract, + inputContractGroups[contract], + outputContractGroups[contract], + explicitContractToAttachments[contract], + serviceHub + ) + } - val resolvedStates: List> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second } - .flatten() + val resolvedStates = contractAttachmentsAndResolvedOutputStates.flatMap { it.second } // The output states need to preserve the order in which they were added. - val resolvedOutputStatesInTheOriginalOrder: List> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance }!! } + val resolvedOutputStatesInTheOriginalOrder: List> = outputStates().map { os -> + resolvedStates.first { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance } + } - val attachments: Collection = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments + val attachments = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments return Pair(attachments, resolvedOutputStatesInTheOriginalOrder) } - private val automaticConstraints = setOf( - AutomaticPlaceholderConstraint, - @Suppress("DEPRECATION") AutomaticHashConstraint - ) - /** * Selects an attachment and resolves the constraints for the output states with [AutomaticPlaceholderConstraint]. * @@ -429,20 +407,18 @@ open class TransactionBuilder( * * * For input states with [WhitelistedByZoneAttachmentConstraint] or a [AlwaysAcceptAttachmentConstraint] implementations, then the currently installed cordapp version is used. */ - private fun handleContract( + private fun selectAttachmentAndResolveOutputStates( contractClassName: ContractClassName, inputStates: List>?, outputStates: List>?, - explicitContractAttachment: AttachmentId?, - services: ServicesForResolution - ): Pair>?> { + explicitContractAttachment: ContractAttachment?, + serviceHub: VerifyingServiceHub + ): Pair>> { val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList()) - fun selectAttachment() = getInstalledContractAttachmentId( - contractClassName, - inputsAndOutputs.filterNot { it.constraint in automaticConstraints }, - services - ) + fun selectAttachmentForContract() = serviceHub.getInstalledContractAttachment(contractClassName) { + inputsAndOutputs.filterNot { it.constraint in automaticConstraints } + } /* This block handles the very specific code path where a [HashAttachmentConstraint] can @@ -452,31 +428,24 @@ open class TransactionBuilder( This can only happen in a private network where all nodes have started with a system parameter that disables the hash constraint check. */ - if (canMigrateFromHashToSignatureConstraint(inputStates, outputStates, services)) { - val attachmentId = selectAttachment() - val attachment = services.attachments.openAttachment(attachmentId) - require(attachment != null) { "Contract attachment $attachmentId for $contractClassName is missing." } - if ((attachment as ContractAttachment).isSigned && (explicitContractAttachment == null || explicitContractAttachment == attachment.id)) { - val signatureConstraint = - makeSignatureAttachmentConstraint(attachment.signerKeys) + if (canMigrateFromHashToSignatureConstraint(inputStates, outputStates, serviceHub)) { + val attachment = selectAttachmentForContract() + if (attachment.isSigned && (explicitContractAttachment == null || explicitContractAttachment.id == attachment.id)) { + val signatureConstraint = makeSignatureAttachmentConstraint(attachment.signerKeys) require(signatureConstraint.isSatisfiedBy(attachment)) { "Selected output constraint: $signatureConstraint not satisfying ${attachment.id}" } val resolvedOutputStates = outputStates?.map { - if (it.constraint in automaticConstraints) { - it.copy(constraint = signatureConstraint) - } else { - it - } - } - return attachment.id to resolvedOutputStates + if (it.constraint in automaticConstraints) it.copy(constraint = signatureConstraint) else it + } ?: emptyList() + return attachment to resolvedOutputStates } } // Determine if there are any HashConstraints that pin the version of a contract. If there are, check if we trust them. - val hashAttachments = inputsAndOutputs + val hashAttachments: Set = inputsAndOutputs .filter { it.constraint is HashAttachmentConstraint } - .mapToSet { state -> - val attachment = services.attachments.openAttachment((state.constraint as HashAttachmentConstraint).attachmentId) - if (attachment == null || attachment !is ContractAttachment || !isUploaderTrusted(attachment.uploader)) { + .mapToSet, ContractAttachment> { state -> + val attachment = serviceHub.attachments.openAttachment((state.constraint as HashAttachmentConstraint).attachmentId) + if (attachment !is ContractAttachment || !isUploaderTrusted(attachment.uploader)) { // This should never happen because these are input states that should have been validated already. throw MissingContractAttachments(listOf(state)) } @@ -485,47 +454,50 @@ open class TransactionBuilder( // Check that states with the HashConstraint don't conflict between themselves or with an explicitly set attachment. require(hashAttachments.size <= 1) { - "Transaction was built with $contractClassName states with multiple HashConstraints. This is illegal, because it makes it impossible to validate with a single version of the contract code." + "Transaction was built with $contractClassName states with multiple HashConstraints. This is illegal, because it makes it " + + "impossible to validate with a single version of the contract code." } + val hashAttachment = hashAttachments.singleOrNull() - if (explicitContractAttachment != null && hashAttachments.singleOrNull() != null) { - @Suppress("USELESS_CAST") // Because the external verifier uses Kotlin 1.2 - require(explicitContractAttachment == (hashAttachments.single() as ContractAttachment).attachment.id) { - "An attachment has been explicitly set for contract $contractClassName in the transaction builder which conflicts with the HashConstraint of a state." + val selectedAttachment = if (explicitContractAttachment != null) { + if (hashAttachment != null) { + require(explicitContractAttachment.id == hashAttachment.id) { + "An attachment has been explicitly set for contract $contractClassName in the transaction builder which conflicts " + + "with the HashConstraint of a state." + } } + // This *has* to be used by this transaction as it is explicit + explicitContractAttachment + } else { + hashAttachment ?: selectAttachmentForContract() } - // This will contain the hash of the JAR that *has* to be used by this Transaction, because it is explicit. Or null if none. - val forcedAttachmentId = explicitContractAttachment ?: hashAttachments.singleOrNull()?.id - - // This will contain the hash of the JAR that will be used by this Transaction. - val selectedAttachmentId = forcedAttachmentId ?: selectAttachment() - - val attachmentToUse = services.attachments.openAttachment(selectedAttachmentId)?.let { it as ContractAttachment } - ?: throw IllegalArgumentException("Contract attachment $selectedAttachmentId for $contractClassName is missing.") - // For Exit transactions (no output states) there is no need to resolve the output constraints. if (outputStates == null) { - return Pair(selectedAttachmentId, null) + return Pair(selectedAttachment, emptyList()) } // If there are no automatic constraints, there is nothing to resolve. if (outputStates.none { it.constraint in automaticConstraints }) { - return Pair(selectedAttachmentId, outputStates) + return Pair(selectedAttachment, outputStates) } // The final step is to resolve AutomaticPlaceholderConstraint. val automaticConstraintPropagation = contractClassName.contractHasAutomaticConstraintPropagation(inputsAndOutputs.first().data::class.java.classLoader) // When automaticConstraintPropagation is disabled for a contract, output states must an explicit Constraint. - require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." } + require(automaticConstraintPropagation) { + "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." + } // This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint. - val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, attachmentToUse, services) + val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, selectedAttachment, serviceHub) // Sanity check that the selected attachment actually passes. - val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations) - require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachmentId" } + val constraintAttachment = AttachmentWithContext(selectedAttachment, contractClassName, serviceHub.networkParameters.whitelistedContractImplementations) + require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { + "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachment" + } val resolvedOutputStates = outputStates.map { val outputConstraint = it.constraint @@ -534,14 +506,16 @@ open class TransactionBuilder( } else { // If the constraint on the output state is already set, and is not a valid transition or can't be transitioned, then fail early. inputStates?.forEach { input -> - require(outputConstraint.canBeTransitionedFrom(input.constraint, attachmentToUse)) { "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" } + require(outputConstraint.canBeTransitionedFrom(input.constraint, selectedAttachment)) { + "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" + } } require(outputConstraint.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" } it } } - return Pair(selectedAttachmentId, resolvedOutputStates) + return Pair(selectedAttachment, resolvedOutputStates) } /** @@ -572,21 +546,25 @@ open class TransactionBuilder( contractClassName: ContractClassName, inputStates: List>?, attachmentToUse: ContractAttachment, - services: ServicesForResolution): AttachmentConstraint = when { - inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse, services) - attachmentToUse.signerKeys.isNotEmpty() && services.networkParameters.minimumPlatformVersion < PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS -> { - log.warnOnce("Signature constraints not available on network requiring a minimum platform version of ${PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS}. Current is: ${services.networkParameters.minimumPlatformVersion}.") - if (useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters)) { - log.warnOnce("Reverting back to using whitelisted zone constraints for contract $contractClassName") - WhitelistedByZoneAttachmentConstraint - } else { - log.warnOnce("Reverting back to using hash constraints for contract $contractClassName") - HashAttachmentConstraint(attachmentToUse.id) + services: ServicesForResolution + ): AttachmentConstraint { + return when { + inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse, services) + attachmentToUse.signerKeys.isNotEmpty() && services.networkParameters.minimumPlatformVersion < MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS -> { + log.warnOnce("Signature constraints not available on network requiring a minimum platform version of " + + "$MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS. Current is: ${services.networkParameters.minimumPlatformVersion}.") + if (useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters)) { + log.warnOnce("Reverting back to using whitelisted zone constraints for contract $contractClassName") + WhitelistedByZoneAttachmentConstraint + } else { + log.warnOnce("Reverting back to using hash constraints for contract $contractClassName") + HashAttachmentConstraint(attachmentToUse.id) + } } + attachmentToUse.signerKeys.isNotEmpty() -> makeSignatureAttachmentConstraint(attachmentToUse.signerKeys) + useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint + else -> HashAttachmentConstraint(attachmentToUse.id) } - attachmentToUse.signerKeys.isNotEmpty() -> makeSignatureAttachmentConstraint(attachmentToUse.signerKeys) - useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint - else -> HashAttachmentConstraint(attachmentToUse.id) } /** @@ -625,7 +603,7 @@ open class TransactionBuilder( // This ensures a smooth migration from a Whitelist Constraint to a Signature Constraint constraints.any { it is WhitelistedByZoneAttachmentConstraint } && attachmentToUse.isSigned && - services.networkParameters.minimumPlatformVersion >= PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS -> + services.networkParameters.minimumPlatformVersion >= MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS -> transitionToSignatureConstraint(constraints, attachmentToUse) // This condition is hit when the current node has not installed the latest signed version but has already received states that have been migrated @@ -651,16 +629,17 @@ open class TransactionBuilder( SignatureAttachmentConstraint.create(CompositeKey.Builder().addKeys(attachmentSigners) .build()) - private fun getInstalledContractAttachmentId( + private inline fun VerifyingServiceHub.getInstalledContractAttachment( contractClassName: String, - states: List>, - services: ServicesForResolution - ): AttachmentId { - return services.cordappProvider.getContractAttachmentID(contractClassName) - ?: throw MissingContractAttachments(states, contractClassName) + statesForException: () -> List> + ): ContractAttachment { + return cordappProvider.getContractAttachment(contractClassName) + ?: throw MissingContractAttachments(statesForException(), contractClassName) } - private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) = contractClassName in networkParameters.whitelistedContractImplementations.keys + private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters): Boolean { + return contractClassName in networkParameters.whitelistedContractImplementations.keys + } @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction(services: ServiceHub) = toWireTransaction(services).toLedgerTransaction(services) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ContractsScanning.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ContractsScanning.kt index 80e7871777..7f5f7c4021 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ContractsScanning.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ContractsScanning.kt @@ -6,7 +6,11 @@ import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.UpgradedContract import net.corda.core.contracts.UpgradedContractWithLegacyConstraint import net.corda.core.crypto.SecureHash -import net.corda.core.internal.* +import net.corda.core.internal.copyTo +import net.corda.core.internal.hash +import net.corda.core.internal.logElapsedTime +import net.corda.core.internal.pooledScan +import net.corda.core.internal.read import org.slf4j.LoggerFactory import java.io.InputStream import java.nio.file.Files @@ -17,7 +21,7 @@ import kotlin.io.path.deleteIfExists // When scanning of the CorDapp Jar is performed without "corda-core.jar" being in the classpath, there is no way to appreciate // relationships between those interfaces, therefore they have to be listed explicitly. -val coreContractClasses = setOf(Contract::class, UpgradedContractWithLegacyConstraint::class, UpgradedContract::class) +val coreContractClasses = setOf(Contract::class.java, UpgradedContractWithLegacyConstraint::class.java, UpgradedContract::class.java) interface ContractsJar { val hash: SecureHash @@ -32,7 +36,8 @@ class ContractsJarFile(private val file: Path) : ContractsJar { return scanResult.use { result -> coreContractClasses - .flatMap { result.getClassesImplementing(it.qualifiedName)} + .asSequence() + .flatMap(result::getClassesImplementing) .filterNot { it.isAbstract } .filterNot { it.isInterface } .map { it.name } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cordapp/CordappLoader.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cordapp/CordappLoader.kt index c58f212393..87eca433c4 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cordapp/CordappLoader.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cordapp/CordappLoader.kt @@ -1,15 +1,14 @@ package net.corda.nodeapi.internal.cordapp import net.corda.core.cordapp.Cordapp -import net.corda.core.flows.FlowLogic import net.corda.core.internal.cordapp.CordappImpl +import net.corda.core.internal.flatMapToSet import net.corda.core.schemas.MappedSchema /** * Handles loading [Cordapp]s. */ interface CordappLoader : AutoCloseable { - /** * Returns all [Cordapp]s found. */ @@ -19,15 +18,10 @@ interface CordappLoader : AutoCloseable { * Returns a [ClassLoader] containing all types from all [Cordapp]s. */ val appClassLoader: ClassLoader +} - /** - * Returns a map between flow class and owning [Cordapp]. - * The mappings are unique, and the node will not start otherwise. - */ - val flowCordappMap: Map>, Cordapp> - - /** - * Returns all [MappedSchema] found inside the [Cordapp]s. - */ - val cordappSchemas: Set -} \ No newline at end of file +/** + * Returns all [MappedSchema] found inside the [Cordapp]s. + */ +val CordappLoader.cordappSchemas: Set + get() = cordapps.flatMapToSet { it.customSchemas } diff --git a/node/build.gradle b/node/build.gradle index c177dc6ec0..da28a8798e 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -80,6 +80,15 @@ processResources { processTestResources { from file("$rootDir/config/test/jolokia-access.xml") + from(tasks.getByPath(":finance:contracts:jar")) { + rename 'corda-finance-contracts-.*.jar', 'corda-finance-contracts.jar' + } + from(tasks.getByPath(":finance:workflows:jar")) { + rename 'corda-finance-workflows-.*.jar', 'corda-finance-workflows.jar' + } + from(tasks.getByPath(":testing:cordapps:cashobservers:jar")) { + rename 'testing-cashobservers-cordapp-.*.jar', 'testing-cashobservers-cordapp.jar' + } } // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 84c2da76b0..7d9d670b0d 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -147,6 +147,7 @@ import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.NodeStatus import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.cordapp.CordappLoader +import net.corda.nodeapi.internal.cordapp.cordappSchemas import net.corda.nodeapi.internal.cryptoservice.CryptoService import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEvent diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt index f7464d8bbb..e870be4047 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt @@ -1,32 +1,31 @@ package net.corda.node.internal.cordapp -import com.google.common.collect.HashBiMap +import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappContext -import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappProviderInternal +import net.corda.core.internal.groupByMultipleKeys import net.corda.core.internal.verification.AttachmentFixups import net.corda.core.node.services.AttachmentId -import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.nodeapi.internal.cordapp.CordappLoader -import java.net.URL -import java.nio.file.FileAlreadyExistsException import java.util.concurrent.ConcurrentHashMap +import kotlin.io.path.absolutePathString +import kotlin.io.path.inputStream /** * Cordapp provider and store. For querying CorDapps for their attachment and vice versa. */ -open class CordappProviderImpl(val cordappLoader: CordappLoader, +open class CordappProviderImpl(private val cordappLoader: CordappLoader, private val cordappConfigProvider: CordappConfigProvider, - private val attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal { + private val attachmentStorage: AttachmentStorageInternal) : SingletonSerializeAsToken(), CordappProviderInternal { private val contextCache = ConcurrentHashMap() - private val cordappAttachments = HashBiMap.create() + private lateinit var flowToCordapp: Map>, CordappImpl> override val attachmentFixups = AttachmentFixups() @@ -38,17 +37,12 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader, override val cordapps: List get() = cordappLoader.cordapps fun start() { - cordappAttachments.putAll(loadContractsIntoAttachmentStore()) - verifyInstalledCordapps() + loadContractsIntoAttachmentStore(cordappLoader.cordapps) + flowToCordapp = makeFlowToCordapp() // Load the fix-ups after uploading any new contracts into attachment storage. attachmentFixups.load(cordappLoader.appClassLoader) } - private fun verifyInstalledCordapps() { - // This will invoke the lazy flowCordappMap property, thus triggering the MultipleCordappsForFlow check. - cordappLoader.flowCordappMap - } - override fun getAppContext(): CordappContext { // TODO: Use better supported APIs in Java 9 Exception().stackTrace.forEach { stackFrame -> @@ -62,41 +56,40 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader, } override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? { - return getCordappForClass(contractClassName)?.let(this::getCordappAttachmentId) + // loadContractsIntoAttachmentStore makes sure the jarHash is the attachment ID + return cordappLoader.cordapps.find { contractClassName in it.contractClassNames }?.jarHash } - /** - * Gets the attachment ID of this CorDapp. Only CorDapps with contracts have an attachment ID - * - * @param cordapp The cordapp to get the attachment ID - * @return An attachment ID if it exists, otherwise nothing - */ - fun getCordappAttachmentId(cordapp: Cordapp): SecureHash? = cordappAttachments.inverse()[cordapp.jarPath] + override fun getContractAttachment(contractClassName: ContractClassName): ContractAttachment? { + return getContractAttachmentID(contractClassName)?.let(::getContractAttachment) + } - private fun loadContractsIntoAttachmentStore(): Map { - return cordapps.filter { it.contractClassNames.isNotEmpty() }.associate { cordapp -> - cordapp.jarPath.openStream().use { stream -> - try { - // This code can be reached by [MockNetwork] tests which uses [MockAttachmentStorage] - // [MockAttachmentStorage] cannot implement [AttachmentStorageInternal] because - // doing so results in internal functions being exposed in the public API. - if (attachmentStorage is AttachmentStorageInternal) { - attachmentStorage.privilegedImportAttachment( - stream, - DEPLOYED_CORDAPP_UPLOADER, - cordapp.info.shortName - ) - } else { - attachmentStorage.importAttachment( - stream, - DEPLOYED_CORDAPP_UPLOADER, - cordapp.info.shortName - ) - } - } catch (faee: FileAlreadyExistsException) { - AttachmentId.create(faee.message!!) - } - } to cordapp.jarPath + private fun loadContractsIntoAttachmentStore(cordapps: List) { + for (cordapp in cordapps) { + if (cordapp.contractClassNames.isEmpty()) continue + val attachmentId = cordapp.jarFile.inputStream().use { stream -> + attachmentStorage.privilegedImportOrGetAttachment(stream, DEPLOYED_CORDAPP_UPLOADER, cordapp.info.shortName) + } + // TODO We could remove this check if we had an import method for CorDapps, since it wouldn't need to hash the InputStream. + // As it stands, we just have to double-check the hashes match, which should be the case (see NodeAttachmentService). + check(attachmentId == cordapp.jarHash) { + "Something has gone wrong. SHA-256 hash of ${cordapp.jarFile} (${cordapp.jarHash}) does not match attachment ID ($attachmentId)" + } + } + } + + private fun getContractAttachment(id: AttachmentId): ContractAttachment { + return checkNotNull(attachmentStorage.openAttachment(id) as? ContractAttachment) { "Contract attachment $id has gone missing!" } + } + + private fun makeFlowToCordapp(): Map>, CordappImpl> { + return cordappLoader.cordapps.groupByMultipleKeys(CordappImpl::allFlows) { flowClass, _, _ -> + val overlappingCordapps = cordappLoader.cordapps.filter { flowClass in it.allFlows } + throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow ${flowClass.name}: " + + "[ ${overlappingCordapps.joinToString { it.jarPath.toString() }} ].", + flowClass.name, + overlappingCordapps.joinToString { it.jarFile.absolutePathString() } + ) } } @@ -110,7 +103,7 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader, return contextCache.computeIfAbsent(cordapp) { CordappContext.create( cordapp, - getCordappAttachmentId(cordapp), + cordapp.jarHash.takeIf(attachmentStorage::hasAttachment), // Not all CorDapps are attachments cordappLoader.appClassLoader, TypesafeCordappConfig(cordappConfigProvider.getConfigByName(cordapp.name)) ) @@ -123,7 +116,7 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader, * @param className The class name * @return cordapp A cordapp or null if no cordapp has the given class loaded */ - fun getCordappForClass(className: String): Cordapp? = cordapps.find { it.cordappClasses.contains(className) } + fun getCordappForClass(className: String): CordappImpl? = cordapps.find { it.cordappClasses.contains(className) } - override fun getCordappForFlow(flowLogic: FlowLogic<*>) = cordappLoader.flowCordappMap[flowLogic.javaClass] + override fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp? = flowToCordapp[flowLogic.javaClass] } diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 49f903ecd2..0d80548cd3 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -1,7 +1,7 @@ package net.corda.node.internal.cordapp import io.github.classgraph.ClassGraph -import io.github.classgraph.ClassInfo +import io.github.classgraph.ClassInfoList import io.github.classgraph.ScanResult import net.corda.common.logging.errorReporting.CordappErrors import net.corda.common.logging.errorReporting.ErrorCode @@ -14,17 +14,17 @@ import net.corda.core.flows.InitiatedBy import net.corda.core.flows.SchedulableFlow import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByService -import net.corda.core.internal.JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION -import net.corda.core.internal.JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION import net.corda.core.internal.JarSignatureCollector import net.corda.core.internal.PlatformVersionSwitches import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl.Companion.UNKNOWN_INFO import net.corda.core.internal.cordapp.get +import net.corda.core.internal.flatMapToSet import net.corda.core.internal.hash import net.corda.core.internal.isAbstractClass import net.corda.core.internal.loadClassOfType import net.corda.core.internal.location +import net.corda.core.internal.groupByMultipleKeys import net.corda.core.internal.mapToSet import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.notary.SinglePartyNotaryService @@ -41,20 +41,17 @@ import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug import net.corda.node.VersionInfo import net.corda.nodeapi.internal.cordapp.CordappLoader import net.corda.nodeapi.internal.coreContractClasses import net.corda.serialization.internal.DefaultWhitelist import java.lang.reflect.Modifier -import java.math.BigInteger import java.net.URLClassLoader import java.nio.file.Path -import java.util.Random import java.util.ServiceLoader -import java.util.concurrent.ConcurrentHashMap import java.util.jar.JarInputStream import java.util.jar.Manifest -import java.util.zip.ZipInputStream import kotlin.io.path.absolutePathString import kotlin.io.path.exists import kotlin.io.path.inputStream @@ -67,27 +64,11 @@ import kotlin.reflect.KClass * * @property cordappJars The classpath of cordapp JARs */ -class JarScanningCordappLoader private constructor(private val cordappJars: Set, - private val versionInfo: VersionInfo = VersionInfo.UNKNOWN, - extraCordapps: List, - private val signerKeyFingerprintBlacklist: List = emptyList()) : CordappLoaderTemplate() { - init { - if (cordappJars.isEmpty()) { - logger.info("No CorDapp paths provided") - } else { - logger.info("Loading CorDapps from ${cordappJars.joinToString()}") - } - } - private val cordappClasses: ConcurrentHashMap> = ConcurrentHashMap() - override val cordapps: List by lazy { loadCordapps() + extraCordapps } - - override val appClassLoader: URLClassLoader = URLClassLoader( - cordappJars.stream().map { it.toUri().toURL() }.toTypedArray(), - javaClass.classLoader - ) - - override fun close() = appClassLoader.close() - +@Suppress("TooManyFunctions") +class JarScanningCordappLoader(private val cordappJars: Set, + private val versionInfo: VersionInfo = VersionInfo.UNKNOWN, + private val extraCordapps: List = emptyList(), + private val signerKeyFingerprintBlacklist: List = emptyList()) : CordappLoader { companion object { private val logger = contextLogger() @@ -100,100 +81,88 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set< versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList(), signerKeyFingerprintBlacklist: List = emptyList()): JarScanningCordappLoader { - logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}") - val paths = cordappDirs + logger.info("Looking for CorDapps in ${cordappDirs.toSet().joinToString(", ", "[", "]")}") + val cordappJars = cordappDirs .asSequence() .flatMap { if (it.exists()) it.listDirectoryEntries("*.jar") else emptyList() } .toSet() - return JarScanningCordappLoader(paths, versionInfo, extraCordapps, signerKeyFingerprintBlacklist) - } - - /** - * Creates a CordappLoader loader out of a list of JAR URLs. - * - * @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection. - */ - fun fromJarUrls(scanJars: Set, - versionInfo: VersionInfo = VersionInfo.UNKNOWN, - extraCordapps: List = emptyList(), - cordappsSignerKeyFingerprintBlacklist: List = emptyList()): JarScanningCordappLoader { - return JarScanningCordappLoader(scanJars, versionInfo, extraCordapps, cordappsSignerKeyFingerprintBlacklist) + return JarScanningCordappLoader(cordappJars, versionInfo, extraCordapps, signerKeyFingerprintBlacklist) } } - private fun loadCordapps(): List { - val invalidCordapps = mutableMapOf() + init { + logger.debug { "cordappJars: $cordappJars" } + } - val cordapps = cordappJars - .map { path -> scanCordapp(path).use { it.toCordapp(path) } } - .filter { cordapp -> - if (cordapp.minimumPlatformVersion > versionInfo.platformVersion) { - logger.warn("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it requires minimum " + - "platform version ${cordapp.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).") - invalidCordapps["CorDapp requires minimumPlatformVersion: ${cordapp.minimumPlatformVersion}, but was: ${versionInfo.platformVersion}"] = cordapp.jarFile - false - } else { - true - } - }.filter { cordapp -> - if (signerKeyFingerprintBlacklist.isEmpty()) { - true //Nothing blacklisted, no need to check - } else { - val certificates = cordapp.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates) - val blockedCertificates = certificates.filter { it.publicKey.hash.sha256() in signerKeyFingerprintBlacklist } - if (certificates.isEmpty() || (certificates - blockedCertificates).isNotEmpty()) { - true // Cordapp is not signed or it is signed by at least one non-blacklisted certificate - } else { - logger.warn("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it is signed by blacklisted key(s) only (probably development key): " + - "${blockedCertificates.map { it.publicKey }}.") - invalidCordapps["Corresponding contracts are signed by blacklisted key(s) only (probably development key),"] = cordapp.jarFile - false - } + override val appClassLoader = URLClassLoader(cordappJars.stream().map { it.toUri().toURL() }.toTypedArray(), javaClass.classLoader) + + private val internal by lazy(::InternalHolder) + + override val cordapps: List + get() = internal.cordapps + + override fun close() = appClassLoader.close() + + private inner class InternalHolder { + val cordapps = cordappJars.mapTo(ArrayList(), ::scanCordapp) + + init { + checkInvalidCordapps() + checkDuplicateCordapps() + checkContractOverlap() + cordapps += extraCordapps + } + + private fun checkInvalidCordapps() { + val invalidCordapps = LinkedHashMap() + + for (cordapp in cordapps) { + if (cordapp.minimumPlatformVersion > versionInfo.platformVersion) { + logger.error("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it requires minimum " + + "platform version ${cordapp.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).") + invalidCordapps["CorDapp requires minimumPlatformVersion ${cordapp.minimumPlatformVersion}, but this node is running version ${versionInfo.platformVersion}"] = cordapp + } + if (signerKeyFingerprintBlacklist.isNotEmpty()) { + val certificates = cordapp.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates) + val blockedCertificates = certificates.filterTo(HashSet()) { it.publicKey.hash.sha256() in signerKeyFingerprintBlacklist } + if (certificates.isNotEmpty() && (certificates - blockedCertificates).isEmpty()) { + logger.error("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it is signed by blacklisted " + + "key(s) only (probably development key): ${blockedCertificates.map { it.publicKey }}.") + invalidCordapps["Corresponding contracts are signed by blacklisted key(s) only (probably development key),"] = cordapp } } + } - if (invalidCordapps.isNotEmpty()) { - throw InvalidCordappException("Invalid Cordapps found, that couldn't be loaded: " + - "${invalidCordapps.map { "Problem: ${it.key} in Cordapp ${it.value}" }}, ") + if (invalidCordapps.isNotEmpty()) { + throw InvalidCordappException("Invalid Cordapps found, that couldn't be loaded: " + + "${invalidCordapps.map { "Problem: ${it.key} in Cordapp ${it.value.jarFile}" }}, ") + } } - cordapps.forEach(::register) - return cordapps - } - - private fun register(cordapp: Cordapp) { - val contractClasses = cordapp.contractClassNames.toSet() - val existingClasses = cordappClasses.keys - val classesToRegister = cordapp.cordappClasses.toSet() - val notAlreadyRegisteredClasses = classesToRegister - existingClasses - val alreadyRegistered= HashMap(cordappClasses).apply { keys.retainAll(classesToRegister) } - - notAlreadyRegisteredClasses.forEach { cordappClasses[it] = setOf(cordapp) } - - for ((registeredClassName, registeredCordapps) in alreadyRegistered) { - val duplicateCordapps = registeredCordapps.filter { it.jarHash == cordapp.jarHash }.toSet() - - if (duplicateCordapps.isNotEmpty()) { - throw DuplicateCordappsInstalledException(cordapp, duplicateCordapps) + private fun checkDuplicateCordapps() { + for (group in cordapps.groupBy { it.jarHash }.values) { + if (group.size > 1) { + throw DuplicateCordappsInstalledException(group[0], group.drop(1)) + } } - if (registeredClassName in contractClasses) { - throw IllegalStateException("More than one CorDapp installed on the node for contract $registeredClassName. " + + } + + private fun checkContractOverlap() { + cordapps.groupByMultipleKeys(CordappImpl::contractClassNames) { contract, cordapp1, cordapp2 -> + throw IllegalStateException("Contract $contract occuring in multiple CorDapps (${cordapp1.name}, ${cordapp2.name}). " + "Please remove the previous version when upgrading to a new version.") } - cordappClasses[registeredClassName] = registeredCordapps + cordapp } } - private fun RestrictedScanResult.toCordapp(path: Path): CordappImpl { + private fun ScanResult.toCordapp(path: Path): CordappImpl { val manifest: Manifest? = JarInputStream(path.inputStream()).use { it.manifest } val info = parseCordappInfo(manifest, CordappImpl.jarName(path)) val minPlatformVersion = manifest?.get(CordappImpl.MIN_PLATFORM_VERSION)?.toIntOrNull() ?: 1 val targetPlatformVersion = manifest?.get(CordappImpl.TARGET_PLATFORM_VERSION)?.toIntOrNull() ?: minPlatformVersion - validateContractStateClassVersion(this) - validateWhitelistClassVersion(this) return CordappImpl( path, - findContractClassNamesWithVersionCheck(this), + findContractClassNames(this), findInitiatedFlows(this), findRPCFlows(this), findServiceFlows(this), @@ -206,10 +175,9 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set< findCustomSchemas(this), findAllFlows(this), info, - path.hash, minPlatformVersion, targetPlatformVersion, - findNotaryService(this), + notaryService = findNotaryService(this), explicitCordappClasses = findAllCordappClasses(this) ) } @@ -278,27 +246,27 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set< return version } - private fun findNotaryService(scanResult: RestrictedScanResult): Class? { + private fun findNotaryService(scanResult: ScanResult): Class? { // Note: we search for implementations of both NotaryService and SinglePartyNotaryService as // the scanner won't find subclasses deeper down the hierarchy if any intermediate class is not // present in the CorDapp. - val result = scanResult.getClassesWithSuperclass(NotaryService::class) + - scanResult.getClassesWithSuperclass(SinglePartyNotaryService::class) + val result = scanResult.getClassesExtending(NotaryService::class) + + scanResult.getClassesExtending(SinglePartyNotaryService::class) if (result.isNotEmpty()) { logger.info("Found notary service CorDapp implementations: " + result.joinToString(", ")) } return result.firstOrNull() } - private fun findServices(scanResult: RestrictedScanResult): List> { + private fun findServices(scanResult: ScanResult): List> { return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class) } - private fun findTelemetryComponents(scanResult: RestrictedScanResult): List> { + private fun findTelemetryComponents(scanResult: ScanResult): List> { return scanResult.getClassesImplementing(TelemetryComponent::class) } - private fun findInitiatedFlows(scanResult: RestrictedScanResult): List>> { + private fun findInitiatedFlows(scanResult: ScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class) } @@ -306,40 +274,35 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set< return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers)) } - private fun findRPCFlows(scanResult: RestrictedScanResult): List>> { + private fun findRPCFlows(scanResult: ScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() } } - private fun findServiceFlows(scanResult: RestrictedScanResult): List>> { + private fun findServiceFlows(scanResult: ScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByService::class) } - private fun findSchedulableFlows(scanResult: RestrictedScanResult): List>> { + private fun findSchedulableFlows(scanResult: ScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, SchedulableFlow::class) } - private fun findAllFlows(scanResult: RestrictedScanResult): List>> { - return scanResult.getConcreteClassesOfType(FlowLogic::class) + private fun findAllFlows(scanResult: ScanResult): List>> { + return scanResult.getClassesExtending(FlowLogic::class) } - private fun findAllCordappClasses(scanResult: RestrictedScanResult): List { - return scanResult.getAllStandardClasses() + scanResult.getAllInterfaces() + private fun findAllCordappClasses(scanResult: ScanResult): List { + val cordappClasses = ArrayList() + scanResult.allStandardClasses.mapTo(cordappClasses) { it.name } + scanResult.allInterfaces.mapTo(cordappClasses) { it.name } + return cordappClasses } - private fun findContractClassNamesWithVersionCheck(scanResult: RestrictedScanResult): List { - val contractClasses = coreContractClasses.flatMapTo(LinkedHashSet()) { scanResult.getNamesOfClassesImplementingWithClassVersionCheck(it) }.toList() + private fun findContractClassNames(scanResult: ScanResult): List { + val contractClasses = coreContractClasses.flatMapToSet(scanResult::getClassesImplementing) for (contractClass in contractClasses) { - contractClass.warnContractWithoutConstraintPropagation(appClassLoader) + contractClass.name.warnContractWithoutConstraintPropagation(appClassLoader) } - return contractClasses - } - - private fun validateContractStateClassVersion(scanResult: RestrictedScanResult) { - coreContractClasses.forEach { scanResult.versionCheckClassesImplementing(it) } - } - - private fun validateWhitelistClassVersion(scanResult: RestrictedScanResult) { - scanResult.versionCheckClassesImplementing(SerializationWhitelist::class) + return contractClasses.map { it.name } } private fun findWhitelists(cordappJar: Path): List { @@ -349,27 +312,25 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set< } + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app. } - private fun findSerializers(scanResult: RestrictedScanResult): List> { - return scanResult.getClassesImplementingWithClassVersionCheck(SerializationCustomSerializer::class) + private fun findSerializers(scanResult: ScanResult): List> { + return scanResult.getClassesImplementing(SerializationCustomSerializer::class).map { it.kotlin.objectOrNewInstance() } } - private fun findCheckpointSerializers(scanResult: RestrictedScanResult): List> { - return scanResult.getClassesImplementingWithClassVersionCheck(CheckpointCustomSerializer::class) + private fun findCheckpointSerializers(scanResult: ScanResult): List> { + return scanResult.getClassesImplementing(CheckpointCustomSerializer::class).map { it.kotlin.objectOrNewInstance() } } - private fun findCustomSchemas(scanResult: RestrictedScanResult): Set { - return scanResult.getClassesWithSuperclass(MappedSchema::class).mapToSet { it.kotlin.objectOrNewInstance() } + private fun findCustomSchemas(scanResult: ScanResult): Set { + return scanResult.getClassesExtending(MappedSchema::class).mapToSet { it.kotlin.objectOrNewInstance() } } - private fun scanCordapp(cordappJar: Path): RestrictedScanResult { + private fun scanCordapp(cordappJar: Path): CordappImpl { logger.info("Scanning CorDapp ${cordappJar.absolutePathString()}") - val scanResult = ClassGraph() - .filterClasspathElementsByURL { it.toPath().isSameFileAs(cordappJar) } - .overrideClassLoaders(appClassLoader) - .ignoreParentClassLoaders() - .enableAllInfo() - .pooledScan() - return RestrictedScanResult(scanResult, cordappJar) + return ClassGraph() + .overrideClasspath(cordappJar.absolutePathString()) + .enableAllInfo() + .pooledScan() + .use { it.toCordapp(cordappJar) } } private fun loadClass(className: String, type: KClass): Class? { @@ -384,73 +345,20 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set< } } - private inner class RestrictedScanResult(private val scanResult: ScanResult, private val cordappJar: Path) : AutoCloseable { - fun getNamesOfClassesImplementingWithClassVersionCheck(type: KClass<*>): List { - return scanResult.getClassesImplementing(type.java.name).map { - validateClassFileVersion(it) - it.name - } - } + private fun ScanResult.getClassesExtending(type: KClass): List> { + return getSubclasses(type.java).getAllConcreteClasses(type) + } - fun versionCheckClassesImplementing(type: KClass<*>) { - return scanResult.getClassesImplementing(type.java.name).forEach { - validateClassFileVersion(it) - } - } + private fun ScanResult.getClassesImplementing(type: KClass): List> { + return getClassesImplementing(type.java).getAllConcreteClasses(type) + } - fun getClassesWithSuperclass(type: KClass): List> { - return scanResult - .getSubclasses(type.java.name) - .names - .mapNotNull { loadClass(it, type) } - .filterNot { it.isAbstractClass } - } + private fun ScanResult.getClassesWithAnnotation(type: KClass, annotation: KClass): List> { + return getClassesWithAnnotation(annotation.java).getAllConcreteClasses(type) + } - fun getClassesImplementingWithClassVersionCheck(type: KClass): List { - return scanResult - .getClassesImplementing(type.java.name) - .mapNotNull { - validateClassFileVersion(it) - loadClass(it.name, type) } - .filterNot { it.isAbstractClass } - .map { it.kotlin.objectOrNewInstance() } - } - - fun getClassesImplementing(type: KClass): List> { - return scanResult - .getClassesImplementing(type.java.name) - .mapNotNull { loadClass(it.name, type) } - .filterNot { it.isAbstractClass } - } - - fun getClassesWithAnnotation(type: KClass, annotation: KClass): List> { - return scanResult - .getClassesWithAnnotation(annotation.java.name) - .names - .mapNotNull { loadClass(it, type) } - .filterNot { Modifier.isAbstract(it.modifiers) } - } - - fun getConcreteClassesOfType(type: KClass): List> { - return scanResult - .getSubclasses(type.java.name) - .names - .mapNotNull { loadClass(it, type) } - .filterNot { it.isAbstractClass } - } - - fun getAllStandardClasses(): List = scanResult.allStandardClasses.names - - fun getAllInterfaces(): List = scanResult.allInterfaces.names - - private fun validateClassFileVersion(classInfo: ClassInfo) { - if (classInfo.classfileMajorVersion < JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION || - classInfo.classfileMajorVersion > JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION) - throw IllegalStateException("Class ${classInfo.name} from jar file $cordappJar has an invalid version of " + - "${classInfo.classfileMajorVersion}") - } - - override fun close() = scanResult.close() + private fun ClassInfoList.getAllConcreteClasses(type: KClass): List> { + return mapNotNull { loadClass(it.name, type)?.takeUnless(Class<*>::isAbstractClass) } } } @@ -478,7 +386,7 @@ class CordappInvalidVersionException( /** * Thrown if duplicate CorDapps are installed on the node */ -class DuplicateCordappsInstalledException(app: Cordapp, duplicates: Set) +class DuplicateCordappsInstalledException(app: Cordapp, duplicates: Collection) : CordaRuntimeException("IllegalStateExcepion", "The CorDapp (name: ${app.info.shortName}, file: ${app.name}) " + "is installed multiple times on the node. The following files correspond to the exact same content: " + "${duplicates.map { it.name }}", null), ErrorCode { @@ -490,40 +398,3 @@ class DuplicateCordappsInstalledException(app: Cordapp, duplicates: Set * Thrown if an exception occurs during loading cordapps. */ class InvalidCordappException(message: String) : CordaRuntimeException(message) - -abstract class CordappLoaderTemplate : CordappLoader { - companion object { - private val logger = contextLogger() - } - - override val flowCordappMap: Map>, Cordapp> by lazy { - cordapps.flatMap { corDapp -> corDapp.allFlows.map { flow -> flow to corDapp } } - .groupBy { it.first } - .mapValues { entry -> - if (entry.value.size > 1) { - logger.error("There are multiple CorDapp JARs on the classpath for flow " + - "${entry.value.first().first.name}: [ ${entry.value.joinToString { it.second.jarPath.toString() }} ].") - entry.value.forEach { (_, cordapp) -> - ZipInputStream(cordapp.jarPath.openStream()).use { zip -> - val ident = BigInteger(64, Random()).toString(36) - logger.error("Contents of: ${cordapp.jarPath} will be prefaced with: $ident") - var e = zip.nextEntry - while (e != null) { - logger.error("$ident\t ${e.name}") - e = zip.nextEntry - } - } - } - throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow " + - "${entry.value.first().first.name}: [ ${entry.value.joinToString { it.second.jarPath.toString() }} ].", - entry.value.first().first.name, - entry.value.joinToString { it.second.jarPath.toString() }) - } - entry.value.single().second - } - } - - override val cordappSchemas: Set by lazy { - cordapps.flatMap { it.customSchemas }.toSet() - } -} diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt b/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt index 11cb8b992b..cd90f301a0 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt @@ -6,20 +6,29 @@ import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.nodeapi.exceptions.DuplicateAttachmentException import java.io.InputStream +import java.nio.file.FileAlreadyExistsException import java.util.stream.Stream interface AttachmentStorageInternal : AttachmentStorage { - /** * This is the same as [importAttachment] expect there are no checks done on the uploader field. This API is internal * and is only for the node. */ - fun privilegedImportAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId + fun privilegedImportAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId { + // Default implementation is not privileged + return importAttachment(jar, uploader, filename) + } /** * Similar to above but returns existing [AttachmentId] instead of throwing [DuplicateAttachmentException] */ - fun privilegedImportOrGetAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId + fun privilegedImportOrGetAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId { + return try { + privilegedImportAttachment(jar, uploader, filename) + } catch (faee: FileAlreadyExistsException) { + AttachmentId.create(faee.message!!) + } + } /** * Get all attachments as a [Stream], filtered by the input [AttachmentQueryCriteria], @@ -27,5 +36,16 @@ interface AttachmentStorageInternal : AttachmentStorage { * * The [Stream] must be closed once used. */ - fun getAllAttachmentsByCriteria(criteria: AttachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria()): Stream> -} \ No newline at end of file + fun getAllAttachmentsByCriteria( + criteria: AttachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria() + ): Stream> { + return queryAttachments(criteria).stream().map { null to openAttachment(it)!! } + } +} + +fun AttachmentStorage.toInternal(): AttachmentStorageInternal { + return when (this) { + is AttachmentStorageInternal -> this + else -> object : AttachmentStorageInternal, AttachmentStorage by this {} + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 50f2c046e0..19df298f0f 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -32,11 +32,11 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.isIdempotentFlow import net.corda.core.internal.location -import net.corda.core.internal.toPath -import net.corda.core.internal.uncheckedCast import net.corda.core.internal.telemetry.ComponentTelemetryIds import net.corda.core.internal.telemetry.SerializedTelemetry import net.corda.core.internal.telemetry.telemetryServiceInternal +import net.corda.core.internal.toPath +import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.internal.CheckpointSerializationContext @@ -46,7 +46,6 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.Try import net.corda.core.utilities.debug import net.corda.core.utilities.trace -import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.services.api.FlowAppAuditEvent import net.corda.node.services.api.FlowPermissionAuditEvent import net.corda.node.services.api.ServiceHubInternal @@ -347,7 +346,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, // This sets the Cordapp classloader on the contextClassLoader of the current thread. // Needed because in previous versions of the finance app we used Thread.contextClassLoader to resolve services defined in cordapps. - Thread.currentThread().contextClassLoader = (serviceHub.cordappProvider as CordappProviderImpl).cordappLoader.appClassLoader + Thread.currentThread().contextClassLoader = serviceHub.cordappProvider.appClassLoader // context.serializedTelemetry is from an rpc client, serializedTelemetry is from a peer, otherwise nothing val serializedTelemetrySrc = context.serializedTelemetry ?: serializedTelemetry diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt index 4568b8a260..c61994d6d7 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt @@ -2,38 +2,42 @@ package net.corda.node.internal.cordapp import com.typesafe.config.Config import com.typesafe.config.ConfigFactory +import net.corda.core.internal.hash import net.corda.core.internal.toPath import net.corda.core.node.services.AttachmentId -import net.corda.core.node.services.AttachmentStorage -import net.corda.node.VersionInfo -import net.corda.testing.core.internal.ContractJarTestUtils -import net.corda.testing.core.internal.SelfCleaningDir +import net.corda.core.utilities.OpaqueBytes +import net.corda.finance.DOLLARS +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashIssueFlow +import net.corda.node.services.persistence.AttachmentStorageInternal +import net.corda.node.services.persistence.toInternal +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.TestIdentity +import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar import net.corda.testing.internal.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage import org.assertj.core.api.Assertions.assertThat import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder import java.io.File import java.io.FileOutputStream -import java.nio.file.Files import java.nio.file.Path import java.util.jar.JarOutputStream import java.util.zip.Deflater.NO_COMPRESSION import java.util.zip.ZipEntry import java.util.zip.ZipEntry.DEFLATED import java.util.zip.ZipEntry.STORED +import kotlin.io.path.copyTo import kotlin.test.assertFailsWith class CordappProviderImplTests { private companion object { - val isolatedJAR = this::class.java.getResource("/isolated.jar")!!.toPath() - // TODO: Cordapp name should differ from the JAR name - const val isolatedCordappName = "isolated" - val emptyJAR = this::class.java.getResource("empty.jar")!!.toPath() - val validConfig: Config = ConfigFactory.parseString("key=value") + val financeContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath() + val financeWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath() @JvmField val ID1 = AttachmentId.randomSHA256() @@ -60,35 +64,29 @@ class CordappProviderImplTests { } } - private lateinit var attachmentStore: AttachmentStorage + @Rule + @JvmField + val tempFolder = TemporaryFolder() + + private lateinit var attachmentStore: AttachmentStorageInternal @Before fun setup() { - attachmentStore = MockAttachmentStorage() - } - - @Test(timeout=300_000) - fun `isolated jar is loaded into the attachment store`() { - val provider = newCordappProvider(isolatedJAR) - val maybeAttachmentId = provider.getCordappAttachmentId(provider.cordapps.first()) - - assertNotNull(maybeAttachmentId) - assertNotNull(attachmentStore.openAttachment(maybeAttachmentId!!)) + attachmentStore = MockAttachmentStorage().toInternal() } @Test(timeout=300_000) fun `empty jar is not loaded into the attachment store`() { - val provider = newCordappProvider(emptyJAR) - assertNull(provider.getCordappAttachmentId(provider.cordapps.first())) + val provider = newCordappProvider(setOf(Companion::class.java.getResource("empty.jar")!!.toPath())) + assertThat(attachmentStore.openAttachment(provider.cordapps.single().jarHash)).isNull() } @Test(timeout=300_000) fun `test that we find a cordapp class that is loaded into the store`() { - val provider = newCordappProvider(isolatedJAR) - val className = "net.corda.isolated.contracts.AnotherDummyContract" + val provider = newCordappProvider(setOf(financeContractsJar)) val expected = provider.cordapps.first() - val actual = provider.getCordappForClass(className) + val actual = provider.getCordappForClass(Cash::class.java.name) assertNotNull(actual) assertEquals(expected, actual) @@ -96,33 +94,49 @@ class CordappProviderImplTests { @Test(timeout=300_000) fun `test that we find an attachment for a cordapp contract class`() { - val provider = newCordappProvider(isolatedJAR) - val className = "net.corda.isolated.contracts.AnotherDummyContract" + val provider = newCordappProvider(setOf(financeContractsJar)) val expected = provider.getAppContext(provider.cordapps.first()).attachmentId - val actual = provider.getContractAttachmentID(className) + val actual = provider.getContractAttachmentID(Cash::class.java.name) assertNotNull(actual) assertEquals(actual!!, expected) } @Test(timeout=300_000) - fun `test cordapp configuration`() { + fun `test cordapp configuration`() { val configProvider = MockCordappConfigProvider() - configProvider.cordappConfigs[isolatedCordappName] = validConfig - val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR), VersionInfo.UNKNOWN) - val provider = CordappProviderImpl(loader, configProvider, attachmentStore).apply { start() } + configProvider.cordappConfigs["corda-finance-contracts"] = ConfigFactory.parseString("key=value") + val provider = newCordappProvider(setOf(financeContractsJar), cordappConfigProvider = configProvider) val expected = provider.getAppContext(provider.cordapps.first()).config assertThat(expected.getString("key")).isEqualTo("value") } + @Test(timeout=300_000) + fun getCordappForFlow() { + val provider = newCordappProvider(setOf(financeWorkflowsJar)) + val cashIssueFlow = CashIssueFlow(10.DOLLARS, OpaqueBytes.of(0x00), TestIdentity(ALICE_NAME).party) + assertThat(provider.getCordappForFlow(cashIssueFlow)?.jarPath?.toPath()).isEqualTo(financeWorkflowsJar) + } + + @Test(timeout=300_000) + fun `does not load the same flow across different CorDapps`() { + val unsignedJar = tempFolder.newFile("duplicate.jar").toPath() + financeWorkflowsJar.copyTo(unsignedJar, overwrite = true) + // We just need to change the file's hash and thus avoid the duplicate CorDapp check + unsignedJar.unsignJar() + assertThat(unsignedJar.hash).isNotEqualTo(financeWorkflowsJar.hash) + assertFailsWith { + newCordappProvider(setOf(financeWorkflowsJar, unsignedJar)) + } + } + @Test(timeout=300_000) fun `test fixup rule that adds attachment`() { val fixupJar = File.createTempFile("fixup", ".jar") .writeFixupRules("$ID1 => $ID2, $ID3") - val fixedIDs = with(newCordappProvider(fixupJar.toPath())) { - start() + val fixedIDs = with(newCordappProvider(setOf(fixupJar.toPath()))) { attachmentFixups.fixupAttachmentIds(listOf(ID1)) } assertThat(fixedIDs).containsExactly(ID2, ID3) @@ -132,8 +146,7 @@ class CordappProviderImplTests { fun `test fixup rule that deletes attachment`() { val fixupJar = File.createTempFile("fixup", ".jar") .writeFixupRules("$ID1 =>") - val fixedIDs = with(newCordappProvider(fixupJar.toPath())) { - start() + val fixedIDs = with(newCordappProvider(setOf(fixupJar.toPath()))) { attachmentFixups.fixupAttachmentIds(listOf(ID1)) } assertThat(fixedIDs).isEmpty() @@ -144,7 +157,7 @@ class CordappProviderImplTests { val fixupJar = File.createTempFile("fixup", ".jar") .writeFixupRules(" => $ID2") val ex = assertFailsWith { - newCordappProvider(fixupJar.toPath()).start() + newCordappProvider(setOf(fixupJar.toPath())) } assertThat(ex).hasMessageContaining( "Forbidden empty list of source attachment IDs in '${fixupJar.absolutePath}'" @@ -157,7 +170,7 @@ class CordappProviderImplTests { val fixupJar = File.createTempFile("fixup", ".jar") .writeFixupRules(rule) val ex = assertFailsWith { - newCordappProvider(fixupJar.toPath()).start() + newCordappProvider(setOf(fixupJar.toPath())) } assertThat(ex).hasMessageContaining( "Invalid fix-up line '${rule.trim()}' in '${fixupJar.absolutePath}'" @@ -170,7 +183,7 @@ class CordappProviderImplTests { val fixupJar = File.createTempFile("fixup", ".jar") .writeFixupRules(rule) val ex = assertFailsWith { - newCordappProvider(fixupJar.toPath()).start() + newCordappProvider(setOf(fixupJar.toPath())) } assertThat(ex).hasMessageContaining( "Invalid fix-up line '${rule.trim()}' in '${fixupJar.absolutePath}'" @@ -186,44 +199,12 @@ class CordappProviderImplTests { "", "$ID3 => $ID4" ) - val fixedIDs = with(newCordappProvider(fixupJar.toPath())) { - start() + val fixedIDs = with(newCordappProvider(setOf(fixupJar.toPath()))) { attachmentFixups.fixupAttachmentIds(listOf(ID2, ID1)) } assertThat(fixedIDs).containsExactlyInAnyOrder(ID2, ID4) } - @Test(timeout=300_000) - fun `test an exception is raised when we have two jars with the same hash`() { - SelfCleaningDir().use { file -> - val jarAndSigner = ContractJarTestUtils.makeTestSignedContractJar(file.path, "com.example.MyContract") - val signedJarPath = jarAndSigner.first - val duplicateJarPath = signedJarPath.parent.resolve("duplicate-${signedJarPath.fileName}") - - Files.copy(signedJarPath, duplicateJarPath) - val paths = setOf(signedJarPath, duplicateJarPath) - JarScanningCordappLoader.fromJarUrls(paths, VersionInfo.UNKNOWN).use { - assertFailsWith { - CordappProviderImpl(it, stubConfigProvider, attachmentStore).apply { start() } - } - } - } - } - - @Test(timeout=300_000) - fun `test an exception is raised when two jars share a contract`() { - SelfCleaningDir().use { file -> - val jarA = ContractJarTestUtils.makeTestContractJar(file.path, listOf("com.example.MyContract", "com.example.AnotherContractForA"), generateManifest = false, jarFileName = "sampleA.jar") - val jarB = ContractJarTestUtils.makeTestContractJar(file.path, listOf("com.example.MyContract", "com.example.AnotherContractForB"), generateManifest = false, jarFileName = "sampleB.jar") - val paths = setOf(jarA, jarB) - JarScanningCordappLoader.fromJarUrls(paths, VersionInfo.UNKNOWN).use { - assertFailsWith { - CordappProviderImpl(it, stubConfigProvider, attachmentStore).apply { start() } - } - } - } - } - private fun File.writeFixupRules(vararg lines: String): File { JarOutputStream(FileOutputStream(this)).use { jar -> jar.setMethod(DEFLATED) @@ -239,8 +220,8 @@ class CordappProviderImplTests { return this } - private fun newCordappProvider(vararg paths: Path): CordappProviderImpl { - val loader = JarScanningCordappLoader.fromJarUrls(paths.toSet(), VersionInfo.UNKNOWN) - return CordappProviderImpl(loader, stubConfigProvider, attachmentStore).apply { start() } + private fun newCordappProvider(cordappJars: Set, cordappConfigProvider: CordappConfigProvider = stubConfigProvider): CordappProviderImpl { + val loader = JarScanningCordappLoader(cordappJars) + return CordappProviderImpl(loader, cordappConfigProvider, attachmentStore).apply { start() } } } diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt index a23d0a56af..23ef514454 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt @@ -1,6 +1,7 @@ package net.corda.node.internal.cordapp import co.paralleluniverse.fibers.Suspendable +import net.corda.core.cordapp.Cordapp import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.flows.InitiatedBy @@ -9,13 +10,38 @@ import net.corda.core.flows.SchedulableFlow import net.corda.core.flows.StartableByRPC import net.corda.core.internal.packageName_ import net.corda.core.internal.toPath +import net.corda.coretesting.internal.delete +import net.corda.coretesting.internal.modifyJarManifest +import net.corda.finance.contracts.CommercialPaper +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow +import net.corda.finance.internal.ConfigHolder +import net.corda.finance.schemas.CashSchemaV1 +import net.corda.finance.schemas.CommercialPaperSchemaV1 import net.corda.node.VersionInfo import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES +import net.corda.serialization.internal.DefaultWhitelist +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.internal.ContractJarTestUtils.makeTestContractJar +import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey +import net.corda.testing.core.internal.JarSignatureTestUtils.getJarSigners +import net.corda.testing.core.internal.JarSignatureTestUtils.signJar +import net.corda.testing.internal.LogHelper import net.corda.testing.node.internal.cordappWithPackages import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.assertj.core.api.Assertions.assertThatIllegalStateException +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.nio.file.Path import java.nio.file.Paths +import java.util.jar.Manifest +import kotlin.io.path.absolutePathString +import kotlin.io.path.copyTo +import kotlin.io.path.name +import kotlin.test.assertFailsWith @InitiatingFlow class DummyFlow : FlowLogic() { @@ -43,10 +69,18 @@ class DummyRPCFlow : FlowLogic() { class JarScanningCordappLoaderTest { private companion object { - const val isolatedContractId = "net.corda.isolated.contracts.AnotherDummyContract" - const val isolatedFlowName = "net.corda.isolated.workflows.IsolatedIssuanceFlow" + val financeContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath() + val financeWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath() + + init { + LogHelper.setLevel(JarScanningCordappLoaderTest::class) + } } + @Rule + @JvmField + val tempFolder = TemporaryFolder() + @Test(timeout=300_000) fun `classes that aren't in cordapps aren't loaded`() { // Basedir will not be a corda node directory so the dummy flow shouldn't be recognised as a part of a cordapp @@ -55,39 +89,42 @@ class JarScanningCordappLoaderTest { } @Test(timeout=300_000) - fun `isolated JAR contains a CorDapp with a contract and plugin`() { - val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!!.toPath() - val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR)) + fun `constructed CordappImpls contains the right classes`() { + val loader = JarScanningCordappLoader(setOf(financeContractsJar, financeWorkflowsJar)) + val (contractsCordapp, workflowsCordapp) = loader.cordapps - assertThat(loader.cordapps).hasSize(1) + assertThat(contractsCordapp.contractClassNames).contains(Cash::class.java.name, CommercialPaper::class.java.name) + assertThat(contractsCordapp.customSchemas).contains(CashSchemaV1, CommercialPaperSchemaV1) + assertThat(contractsCordapp.info).isInstanceOf(Cordapp.Info.Contract::class.java) + assertThat(contractsCordapp.allFlows).isEmpty() + assertThat(contractsCordapp.jarFile).isEqualTo(financeContractsJar) - val actualCordapp = loader.cordapps.single() - assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId)) - assertThat(actualCordapp.initiatedFlows).isEmpty() - assertThat(actualCordapp.rpcFlows.first().name).isEqualTo(isolatedFlowName) - assertThat(actualCordapp.schedulableFlows).isEmpty() - assertThat(actualCordapp.services).isEmpty() - assertThat(actualCordapp.serializationWhitelists).hasSize(1) - assertThat(actualCordapp.serializationWhitelists.first().javaClass.name).isEqualTo("net.corda.serialization.internal.DefaultWhitelist") - assertThat(actualCordapp.jarFile).isEqualTo(isolatedJAR) + assertThat(workflowsCordapp.allFlows).contains(CashIssueFlow::class.java, CashPaymentFlow::class.java) + assertThat(workflowsCordapp.services).contains(ConfigHolder::class.java) + assertThat(workflowsCordapp.info).isInstanceOf(Cordapp.Info.Workflow::class.java) + assertThat(workflowsCordapp.contractClassNames).isEmpty() + assertThat(workflowsCordapp.jarFile).isEqualTo(financeWorkflowsJar) + + for (actualCordapp in loader.cordapps) { + assertThat(actualCordapp.cordappClasses) + .containsAll(actualCordapp.contractClassNames) + .containsAll(actualCordapp.initiatedFlows.map { it.name }) + .containsAll(actualCordapp.rpcFlows.map { it.name }) + .containsAll(actualCordapp.serviceFlows.map { it.name }) + .containsAll(actualCordapp.schedulableFlows.map { it.name }) + .containsAll(actualCordapp.services.map { it.name }) + .containsAll(actualCordapp.telemetryComponents.map { it.name }) + .containsAll(actualCordapp.serializationCustomSerializers.map { it.javaClass.name }) + .containsAll(actualCordapp.checkpointCustomSerializers.map { it.javaClass.name }) + .containsAll(actualCordapp.customSchemas.map { it.name }) + assertThat(actualCordapp.serializationWhitelists).contains(DefaultWhitelist) + } } @Test(timeout=300_000) - fun `constructed CordappImpl contains the right cordapp classes`() { - val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!!.toPath() - val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR)) - - val actualCordapp = loader.cordapps.single() - val cordappClasses = actualCordapp.cordappClasses - assertThat(cordappClasses).contains(isolatedFlowName) - val serializationWhitelistedClasses = actualCordapp.serializationWhitelists.flatMap { it.whitelist }.map { it.name } - assertThat(cordappClasses).containsAll(serializationWhitelistedClasses) - } - - @Test(timeout=300_000) - fun `flows are loaded by loader`() { + fun `flows are loaded by loader`() { val jarFile = cordappWithPackages(javaClass.packageName_).jarFile - val loader = JarScanningCordappLoader.fromJarUrls(setOf(jarFile)) + val loader = JarScanningCordappLoader(setOf(jarFile)) // One cordapp from this source tree. In gradle it will also pick up the node jar. assertThat(loader.cordapps).isNotEmpty @@ -101,18 +138,16 @@ class JarScanningCordappLoaderTest { // This test exists because the appClassLoader is used by serialisation and we need to ensure it is the classloader // being used internally. Later iterations will use a classloader per cordapp and this test can be retired. @Test(timeout=300_000) - fun `cordapp classloader can load cordapp classes`() { - val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!!.toPath() - val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR), VersionInfo.UNKNOWN) + fun `cordapp classloader can load cordapp classes`() { + val testJar = this::class.java.getResource("/testing-cashobservers-cordapp.jar")!!.toPath() + val loader = JarScanningCordappLoader(setOf(testJar)) - loader.appClassLoader.loadClass(isolatedContractId) - loader.appClassLoader.loadClass(isolatedFlowName) + loader.appClassLoader.loadClass("net.corda.finance.test.flows.CashIssueWithObserversFlow") } @Test(timeout=300_000) - fun `cordapp classloader sets target and min version to 1 if not specified`() { - val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/no-min-or-target-version.jar")!!.toPath() - val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN) + fun `sets target and min version to 1 if not specified`() { + val loader = JarScanningCordappLoader(setOf(minAndTargetCordapp(minVersion = null, targetVersion = null))) loader.cordapps.forEach { assertThat(it.targetPlatformVersion).isEqualTo(1) assertThat(it.minimumPlatformVersion).isEqualTo(1) @@ -120,21 +155,16 @@ class JarScanningCordappLoaderTest { } @Test(timeout=300_000) - fun `cordapp classloader returns correct values for minPlatformVersion and targetVersion`() { - // load jar with min and target version in manifest - // make sure classloader extracts correct values - val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!.toPath() - val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN) + fun `returns correct values for minPlatformVersion and targetVersion`() { + val loader = JarScanningCordappLoader(setOf(minAndTargetCordapp(minVersion = 2, targetVersion = 3))) val cordapp = loader.cordapps.first() assertThat(cordapp.targetPlatformVersion).isEqualTo(3) assertThat(cordapp.minimumPlatformVersion).isEqualTo(2) } @Test(timeout=300_000) - fun `cordapp classloader sets target version to min version if target version is not specified`() { - // load jar with minVersion but not targetVersion in manifest - val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!!.toPath() - val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN) + fun `sets target version to min version if target version is not specified`() { + val loader = JarScanningCordappLoader(setOf(minAndTargetCordapp(minVersion = 2, targetVersion = null))) // exclude the core cordapp val cordapp = loader.cordapps.first() assertThat(cordapp.targetPlatformVersion).isEqualTo(2) @@ -142,48 +172,99 @@ class JarScanningCordappLoaderTest { } @Test(timeout = 300_000) - fun `cordapp classloader does not load apps when their min platform version is greater than the node platform version`() { - val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!!.toPath() - val cordappLoader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1)) + fun `does not load apps when their min platform version is greater than the node platform version`() { + val jar = minAndTargetCordapp(minVersion = 2, targetVersion = null) + val cordappLoader = JarScanningCordappLoader(setOf(jar), versionInfo = VersionInfo.UNKNOWN.copy(platformVersion = 1)) assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy { cordappLoader.cordapps } } @Test(timeout=300_000) - fun `cordapp classloader does load apps when their min platform version is less than the platform version`() { - val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!.toPath() - val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1000)) + fun `does load apps when their min platform version is less than the platform version`() { + val jar = minAndTargetCordapp(minVersion = 2, targetVersion = 3) + val loader = JarScanningCordappLoader(setOf(jar), versionInfo = VersionInfo.UNKNOWN.copy(platformVersion = 1000)) assertThat(loader.cordapps).hasSize(1) } @Test(timeout=300_000) - fun `cordapp classloader does load apps when their min platform version is equal to the platform version`() { - val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!.toPath() - val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2)) + fun `does load apps when their min platform version is equal to the platform version`() { + val jar = minAndTargetCordapp(minVersion = 2, targetVersion = 3) + val loader = JarScanningCordappLoader(setOf(jar), versionInfo = VersionInfo.UNKNOWN.copy(platformVersion = 2)) assertThat(loader.cordapps).hasSize(1) } @Test(timeout=300_000) - fun `cordapp classloader loads app signed by allowed certificate`() { - val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!.toPath() - val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = emptyList()) + fun `loads app signed by allowed certificate`() { + val loader = JarScanningCordappLoader(setOf(financeContractsJar), signerKeyFingerprintBlacklist = emptyList()) assertThat(loader.cordapps).hasSize(1) } @Test(timeout = 300_000) - fun `cordapp classloader does not load app signed by blacklisted certificate`() { - val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!.toPath() - val cordappLoader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES) + fun `does not load app signed by blacklisted certificate`() { + val cordappLoader = JarScanningCordappLoader(setOf(financeContractsJar), signerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES) assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy { cordappLoader.cordapps } } @Test(timeout=300_000) - fun `cordapp classloader loads app signed by both allowed and non-blacklisted certificate`() { - val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-two-keys.jar")!!.toPath() - val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES) + fun `does not load duplicate CorDapps`() { + val duplicateJar = financeWorkflowsJar.duplicate() + val loader = JarScanningCordappLoader(setOf(financeWorkflowsJar, duplicateJar)) + assertFailsWith { + loader.cordapps + } + } + + @Test(timeout=300_000) + fun `does not load contract shared across CorDapps`() { + val cordappJars = (1..2).map { + makeTestContractJar( + tempFolder.root.toPath(), + listOf("com.example.MyContract", "com.example.AnotherContractFor$it"), + generateManifest = false, + jarFileName = "sample$it.jar" + ) + }.toSet() + val loader = JarScanningCordappLoader(cordappJars) + assertThatIllegalStateException() + .isThrownBy { loader.cordapps } + .withMessageContaining("Contract com.example.MyContract occuring in multiple CorDapps") + } + + @Test(timeout=300_000) + fun `loads app signed by both allowed and non-blacklisted certificate`() { + val jar = financeWorkflowsJar.duplicate { + tempFolder.root.toPath().generateKey("testAlias", "testPassword", ALICE_NAME.toString()) + tempFolder.root.toPath().signJar(absolutePathString(), "testAlias", "testPassword") + } + assertThat(jar.parent.getJarSigners(jar.name)).hasSize(2) + val loader = JarScanningCordappLoader(setOf(jar), signerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES) assertThat(loader.cordapps).hasSize(1) } + + private inline fun Path.duplicate(name: String = "duplicate.jar", modify: Path.() -> Unit = { }): Path { + val copy = tempFolder.newFile(name).toPath() + copyTo(copy, overwrite = true) + modify(copy) + return copy + } + + private fun minAndTargetCordapp(minVersion: Int?, targetVersion: Int?): Path { + return financeWorkflowsJar.duplicate { + modifyJarManifest { manifest -> + manifest.setOrDeleteAttribute("Min-Platform-Version", minVersion?.toString()) + manifest.setOrDeleteAttribute("Target-Platform-Version", targetVersion?.toString()) + } + } + } + + private fun Manifest.setOrDeleteAttribute(name: String, value: String?) { + if (value != null) { + mainAttributes.putValue(name, value.toString()) + } else { + mainAttributes.delete(name) + } + } } diff --git a/node/src/test/resources/isolated.jar b/node/src/test/resources/isolated.jar deleted file mode 100644 index 3df99a710f0dde7d770de143c189ae6c19dc2fa5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11209 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 W48+qy6a)n2>AHT3sT~y0Z~q6@PDdR8 diff --git a/node/src/test/resources/net/corda/node/internal/cordapp/versions/min-2-no-target.jar b/node/src/test/resources/net/corda/node/internal/cordapp/versions/min-2-no-target.jar deleted file mode 100644 index 449691584132e7d716bb5276023ef57b16052a11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30781 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 diff --git a/node/src/test/resources/net/corda/node/internal/cordapp/versions/no-min-or-target-version.jar b/node/src/test/resources/net/corda/node/internal/cordapp/versions/no-min-or-target-version.jar deleted file mode 100644 index 408e70145dbb633f6b5460bb374aa1ac399f3bcf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12182 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

V!<)Yy>$Cenf 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 Path.useZipFile(block: (FileSystem) -> T): T { + if (fileSize() == 0L) { + // Need to first create an empty jar before it can be opened + JarOutputStream(outputStream()).close() + } + return FileSystems.newFileSystem(this).use(block) +} + +inline fun Path.modifyJarManifest(block: (Manifest) -> T): T { + return useZipFile { zipFs -> + val manifestFile = zipFs.getPath("META-INF", "MANIFEST.MF") + val manifest = manifestFile.inputStream().use(::Manifest) + val result = block(manifest) + manifestFile.outputStream().use(manifest::write) + result + } +} + +fun Attributes.delete(name: String): String? = remove(Attributes.Name(name)) as String? diff --git a/testing/core-test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt b/testing/core-test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt index a3e2d78d12..abf8d463bf 100644 --- a/testing/core-test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt +++ b/testing/core-test-utils/src/main/kotlin/net/corda/testing/core/internal/JarSignatureTestUtils.kt @@ -3,11 +3,12 @@ package net.corda.testing.core.internal import net.corda.core.identity.CordaX500Name import net.corda.core.internal.JarSignatureCollector import net.corda.core.internal.deleteRecursively +import net.corda.coretesting.internal.modifyJarManifest +import net.corda.coretesting.internal.useZipFile import net.corda.nodeapi.internal.crypto.loadKeyStore import java.io.Closeable import java.io.FileInputStream import java.io.FileOutputStream -import java.nio.file.FileSystems import java.nio.file.Files import java.nio.file.NoSuchFileException import java.nio.file.Path @@ -20,9 +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.inputStream import kotlin.io.path.listDirectoryEntries -import kotlin.io.path.outputStream import kotlin.test.assertEquals /** @@ -74,12 +73,13 @@ object JarSignatureTestUtils { } fun Path.unsignJar() { - FileSystems.newFileSystem(this).use { zipFs -> + // Remove the signatures + useZipFile { zipFs -> zipFs.getPath("META-INF").listDirectoryEntries("*.{SF,DSA,RSA,EC}").forEach(Path::deleteExisting) - val manifestFile = zipFs.getPath("META-INF", "MANIFEST.MF") - val manifest = manifestFile.inputStream().use(::Manifest) - manifest.entries.clear() // Remove all the hash information of the jar contents - manifestFile.outputStream().use(manifest::write) + } + // Remove all the hash information of the jar contents + modifyJarManifest { manifest -> + manifest.entries.clear() } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 20f2cae9c1..50febd80e1 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -67,9 +67,11 @@ import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.PublicKeyToOwningIdentityCacheImpl +import net.corda.node.services.persistence.toInternal import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.NodeVaultService import net.corda.nodeapi.internal.cordapp.CordappLoader +import net.corda.nodeapi.internal.cordapp.cordappSchemas import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.contextTransaction @@ -78,7 +80,6 @@ import net.corda.testing.core.TestIdentity import net.corda.testing.internal.MockCordappProvider import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.configureDatabase -import net.corda.testing.internal.services.InternalMockAttachmentStorage import net.corda.testing.node.internal.MockCryptoService import net.corda.testing.node.internal.MockKeyManagementService import net.corda.testing.node.internal.MockNetworkParametersStorage @@ -128,7 +129,7 @@ open class MockServices private constructor( ) : ServiceHub { companion object { private fun cordappLoaderForPackages(packages: Iterable, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader { - return JarScanningCordappLoader.fromJarUrls(cordappsForPackages(packages).mapToSet { it.jarFile }, versionInfo) + return JarScanningCordappLoader(cordappsForPackages(packages).mapToSet { it.jarFile }, versionInfo = versionInfo) } /** @@ -488,7 +489,7 @@ open class MockServices private constructor( get() { return NodeInfo(listOf(NetworkHostAndPort("mock.node.services", 10000)), listOf(initialIdentity.identity), 1, serial = 1L) } - private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments).also { + private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments.toInternal()).also { it.start() } override val cordappProvider: CordappProvider get() = mockCordappProvider @@ -562,7 +563,7 @@ open class MockServices private constructor( */ private class VerifyingView(private val mockServices: MockServices) : VerifyingServiceHub, ServiceHub by mockServices { override val attachmentTrustCalculator = NodeAttachmentTrustCalculator( - attachmentStorage = InternalMockAttachmentStorage(mockServices.attachments), + attachmentStorage = mockServices.attachments.toInternal(), cacheFactory = TestingNamedCacheFactory() ) @@ -577,7 +578,7 @@ open class MockServices private constructor( override fun loadStates(stateRefs: Set): Set> = mockServices.loadStates(stateRefs) override val externalVerifierHandle: ExternalVerifierHandle - get() = throw UnsupportedOperationException("External verification is not supported by MockServices") + get() = throw UnsupportedOperationException("`Verification of legacy transactions is not supported by MockServices. Use MockNode instead.") } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 4199288630..3280a456c8 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -27,11 +27,10 @@ import net.corda.core.transactions.WireTransaction import net.corda.node.services.DbTransactionsResolver import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.attachments.NodeAttachmentTrustCalculator -import net.corda.node.services.persistence.AttachmentStorageInternal +import net.corda.node.services.persistence.toInternal import net.corda.testing.core.dummyCommand import net.corda.testing.internal.MockCordappProvider import net.corda.testing.internal.TestingNamedCacheFactory -import net.corda.testing.internal.services.InternalMockAttachmentStorage import net.corda.testing.services.MockAttachmentStorage import java.io.InputStream import java.security.PublicKey @@ -113,14 +112,7 @@ data class TestTransactionDSLInterpreter private constructor( ledgerInterpreter.services.attachments.let { // Wrapping to a [InternalMockAttachmentStorage] is needed to prevent leaking internal api // while still allowing the tests to work - NodeAttachmentTrustCalculator( - attachmentStorage = if (it is MockAttachmentStorage) { - InternalMockAttachmentStorage(it) - } else { - it as AttachmentStorageInternal - }, - cacheFactory = TestingNamedCacheFactory() - ) + NodeAttachmentTrustCalculator(attachmentStorage = it.toInternal(), cacheFactory = TestingNamedCacheFactory()) } override fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver = @@ -129,6 +121,10 @@ data class TestTransactionDSLInterpreter private constructor( override fun loadState(stateRef: StateRef) = ledgerInterpreter.resolveStateRef(stateRef) + override fun loadStates(stateRefs: Set): Set> { + return ledgerInterpreter.services.loadStates(stateRefs) + } + override val cordappProvider: CordappProviderInternal get() = ledgerInterpreter.services.cordappProvider as CordappProviderInternal @@ -141,7 +137,7 @@ data class TestTransactionDSLInterpreter private constructor( } override val externalVerifierHandle: ExternalVerifierHandle - get() = throw UnsupportedOperationException("External verification is not supported by TestTransactionDSLInterpreter") + get() = throw UnsupportedOperationException("Verification of legacy transactions is not supported by TestTransactionDSLInterpreter") override fun recordUnnotarisedTransaction(txn: SignedTransaction) {} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt index 2ce9ef53ea..152dab8b11 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt @@ -5,16 +5,16 @@ import net.corda.core.cordapp.Cordapp import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.node.services.AttachmentId -import net.corda.core.node.services.AttachmentStorage -import net.corda.nodeapi.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.node.services.persistence.AttachmentStorageInternal +import net.corda.nodeapi.internal.cordapp.CordappLoader import net.corda.testing.services.MockAttachmentStorage import java.security.PublicKey import java.util.jar.Attributes class MockCordappProvider( cordappLoader: CordappLoader, - attachmentStorage: AttachmentStorage, + attachmentStorage: AttachmentStorageInternal, cordappConfigProvider: MockCordappConfigProvider = MockCordappConfigProvider() ) : CordappProviderImpl(cordappLoader, cordappConfigProvider, attachmentStorage) { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/services/InternalMockAttachmentStorage.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/services/InternalMockAttachmentStorage.kt deleted file mode 100644 index 26471237ec..0000000000 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/services/InternalMockAttachmentStorage.kt +++ /dev/null @@ -1,43 +0,0 @@ -package net.corda.testing.internal.services - -import net.corda.core.contracts.Attachment -import net.corda.core.node.services.AttachmentId -import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.vault.AttachmentQueryCriteria -import net.corda.node.services.persistence.AttachmentStorageInternal -import net.corda.testing.services.MockAttachmentStorage -import java.io.InputStream -import java.util.stream.Stream - -/** - * Internal version of [MockAttachmentStorage] that implements [AttachmentStorageInternal] for use - * in internal tests where [AttachmentStorageInternal] functions are needed. - */ -class InternalMockAttachmentStorage(storage: MockAttachmentStorage) : AttachmentStorageInternal, - AttachmentStorage by storage { - - override fun privilegedImportAttachment( - jar: InputStream, - uploader: String, - filename: String? - ): AttachmentId = importAttachment(jar, uploader, filename) - - override fun privilegedImportOrGetAttachment( - jar: InputStream, - uploader: String, - filename: String? - ): AttachmentId { - return try { - importAttachment(jar, uploader, filename) - } catch (faee: java.nio.file.FileAlreadyExistsException) { - AttachmentId.create(faee.message!!) - } - } - - override fun getAllAttachmentsByCriteria(criteria: AttachmentQueryCriteria): Stream> { - return queryAttachments(criteria) - .map(this::openAttachment) - .map { null as String? to it!! } - .stream() - } -} \ No newline at end of file