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.
This commit is contained in:
Shams Asari 2024-02-08 17:54:04 +00:00
parent c7514e1c60
commit 8fd3139df1
33 changed files with 665 additions and 700 deletions

View File

@ -2,7 +2,6 @@ package net.corda.coretests.internal.verification
import net.corda.core.internal.verification.AttachmentFixups import net.corda.core.internal.verification.AttachmentFixups
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.node.VersionInfo
import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.internal.cordapp.JarScanningCordappLoader
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
@ -130,7 +129,7 @@ class AttachmentFixupsTest {
} }
private fun newFixupService(vararg paths: Path): AttachmentFixups { 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) } return AttachmentFixups().apply { load(loader.appClassLoader) }
} }
} }

View File

@ -11,13 +11,13 @@ import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.isolated.contracts.DummyContractBackdoor import net.corda.isolated.contracts.DummyContractBackdoor
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator 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.common.internal.testNetworkParameters
import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.fakeAttachment import net.corda.testing.internal.fakeAttachment
import net.corda.testing.internal.services.InternalMockAttachmentStorage
import net.corda.testing.services.MockAttachmentStorage import net.corda.testing.services.MockAttachmentStorage
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@ -30,7 +30,7 @@ import kotlin.test.assertFailsWith
class AttachmentsClassLoaderSerializationTests { class AttachmentsClassLoaderSerializationTests {
companion object { 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" private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.isolated.contracts.AnotherDummyContract"
} }
@ -38,20 +38,19 @@ class AttachmentsClassLoaderSerializationTests {
@JvmField @JvmField
val testSerialization = SerializationEnvironmentRule() val testSerialization = SerializationEnvironmentRule()
private val storage = InternalMockAttachmentStorage(MockAttachmentStorage()) private val storage = MockAttachmentStorage().toInternal()
private val attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage, TestingNamedCacheFactory()) private val attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage, TestingNamedCacheFactory())
@Test(timeout=300_000) @Test(timeout=300_000)
fun `Can serialize and deserialize with an attachment classloader`() { fun `Can serialize and deserialize with an attachment classloader`() {
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
val isolatedId = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar") 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 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 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)!! }, arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! },
testNetworkParameters(), testNetworkParameters(),
SecureHash.zeroHash, SecureHash.zeroHash,
@ -64,7 +63,7 @@ class AttachmentsClassLoaderSerializationTests {
val txt = IOUtils.toString(classLoader.getResourceAsStream("file1.txt"), Charsets.UTF_8.name()) val txt = IOUtils.toString(classLoader.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
assertEquals("some data", txt) 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 serialisedState = state.serialize()
val state1 = serialisedState.deserialize() val state1 = serialisedState.deserialize()

View File

@ -25,6 +25,7 @@ import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator 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.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.ALICE_NAME 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.core.internal.ContractJarTestUtils.signContractJar
import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.fakeAttachment 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.node.internal.FINANCE_CONTRACTS_CORDAPP
import net.corda.testing.services.MockAttachmentStorage import net.corda.testing.services.MockAttachmentStorage
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
@ -87,7 +87,6 @@ class AttachmentsClassLoaderTests {
val testSerialization = SerializationEnvironmentRule() val testSerialization = SerializationEnvironmentRule()
private lateinit var storage: MockAttachmentStorage private lateinit var storage: MockAttachmentStorage
private lateinit var internalStorage: InternalMockAttachmentStorage
private lateinit var attachmentTrustCalculator: AttachmentTrustCalculator private lateinit var attachmentTrustCalculator: AttachmentTrustCalculator
private val networkParameters = testNetworkParameters() private val networkParameters = testNetworkParameters()
private val cacheFactory = TestingNamedCacheFactory(1) private val cacheFactory = TestingNamedCacheFactory(1)
@ -114,8 +113,7 @@ class AttachmentsClassLoaderTests {
@Before @Before
fun setup() { fun setup() {
storage = MockAttachmentStorage() storage = MockAttachmentStorage()
internalStorage = InternalMockAttachmentStorage(storage) attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage.toInternal(), cacheFactory)
attachmentTrustCalculator = NodeAttachmentTrustCalculator(internalStorage, cacheFactory)
} }
@Test(timeout=300_000) @Test(timeout=300_000)
@ -449,7 +447,7 @@ class AttachmentsClassLoaderTests {
val keyPairB = Crypto.generateKeyPair() val keyPairB = Crypto.generateKeyPair()
attachmentTrustCalculator = NodeAttachmentTrustCalculator( attachmentTrustCalculator = NodeAttachmentTrustCalculator(
InternalMockAttachmentStorage(storage), storage.toInternal(),
cacheFactory, cacheFactory,
blacklistedAttachmentSigningKeys = listOf(keyPairA.public.hash) blacklistedAttachmentSigningKeys = listOf(keyPairA.public.hash)
) )
@ -486,7 +484,7 @@ class AttachmentsClassLoaderTests {
val keyPairA = Crypto.generateKeyPair() val keyPairA = Crypto.generateKeyPair()
attachmentTrustCalculator = NodeAttachmentTrustCalculator( attachmentTrustCalculator = NodeAttachmentTrustCalculator(
InternalMockAttachmentStorage(storage), storage.toInternal(),
cacheFactory, cacheFactory,
blacklistedAttachmentSigningKeys = listOf(keyPairA.public.hash) blacklistedAttachmentSigningKeys = listOf(keyPairA.public.hash)
) )

View File

@ -13,6 +13,7 @@ import net.corda.core.crypto.DigestService
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.internal.HashAgility import net.corda.core.internal.HashAgility
import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.PLATFORM_VERSION
import net.corda.core.internal.RPC_UPLOADER
import net.corda.core.internal.digestService import net.corda.core.internal.digestService
import net.corda.core.node.ZoneVersionTooLowException import net.corda.core.node.ZoneVersionTooLowException
import net.corda.core.serialization.internal._driverSerializationEnv 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 net.corda.testing.node.internal.cordappWithPackages
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Ignore import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.time.Instant import java.time.Instant
import kotlin.io.path.inputStream
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
class TransactionBuilderTest { class TransactionBuilderTest {
@ -298,4 +301,25 @@ class TransactionBuilderTest {
builder.toWireTransaction(services, schemeId) 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")
}
} }

View File

@ -12,10 +12,7 @@ import net.corda.core.node.ServicesForResolution
import net.corda.core.node.ZoneVersionTooLowException import net.corda.core.node.ZoneVersionTooLowException
import net.corda.core.node.services.TransactionStorage import net.corda.core.node.services.TransactionStorage
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializationContext
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import org.slf4j.MDC import org.slf4j.MDC
import java.security.PublicKey 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. */ /** Checks if this flow is an idempotent flow. */
fun Class<out FlowLogic<*>>.isIdempotentFlow(): Boolean { fun Class<out FlowLogic<*>>.isIdempotentFlow(): Boolean {
return IdempotentFlow::class.java.isAssignableFrom(this) return IdempotentFlow::class.java.isAssignableFrom(this)

View File

@ -154,6 +154,24 @@ inline fun <T, R> Collection<T>.flatMapToSet(transform: (T) -> Iterable<R>): Set
return if (isEmpty()) emptySet() else flatMapTo(LinkedHashSet(), transform) 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 <K, V> Iterable<V>.groupByMultipleKeys(
keysSelector: (V) -> Iterable<K>,
onDuplicate: (K, V, V) -> Unit = { key, value1, value2 -> throw IllegalArgumentException("Duplicate mapping for $key ($value1, $value2)") }
): Map<K, V> {
val map = LinkedHashMap<K, V>()
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) fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options)
/** Same as [InputStream.readBytes] but also closes the stream. */ /** Same as [InputStream.readBytes] but also closes the stream. */

View File

@ -5,6 +5,7 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.PLATFORM_VERSION
import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.hash
import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.notary.NotaryService
import net.corda.core.internal.telemetry.TelemetryComponent import net.corda.core.internal.telemetry.TelemetryComponent
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
@ -32,9 +33,9 @@ data class CordappImpl(
override val customSchemas: Set<MappedSchema>, override val customSchemas: Set<MappedSchema>,
override val allFlows: List<Class<out FlowLogic<*>>>, override val allFlows: List<Class<out FlowLogic<*>>>,
override val info: Cordapp.Info, override val info: Cordapp.Info,
override val jarHash: SecureHash.SHA256,
override val minimumPlatformVersion: Int, override val minimumPlatformVersion: Int,
override val targetPlatformVersion: Int, override val targetPlatformVersion: Int,
override val jarHash: SecureHash.SHA256 = jarFile.hash,
val notaryService: Class<out NotaryService>? = null, val notaryService: Class<out NotaryService>? = null,
/** Indicates whether the CorDapp is loaded from external sources, or generated on node startup (virtual). */ /** Indicates whether the CorDapp is loaded from external sources, or generated on node startup (virtual). */
val isLoaded: Boolean = true, val isLoaded: Boolean = true,
@ -53,6 +54,10 @@ data class CordappImpl(
classList.mapNotNull { it?.name } + contractClassNames + explicitCordappClasses 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 { companion object {
fun jarName(url: Path): String = url.name.removeSuffix(".jar") fun jarName(url: Path): String = url.name.removeSuffix(".jar")

View File

@ -1,5 +1,7 @@
package net.corda.core.internal.cordapp 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.Cordapp
import net.corda.core.cordapp.CordappProvider import net.corda.core.cordapp.CordappProvider
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
@ -10,4 +12,9 @@ interface CordappProviderInternal : CordappProvider {
val attachmentFixups: AttachmentFixups val attachmentFixups: AttachmentFixups
val cordapps: List<CordappImpl> val cordapps: List<CordappImpl>
fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp? fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp?
/**
* Similar to [getContractAttachmentID] except it returns the [ContractAttachment] object.
*/
fun getContractAttachment(contractClassName: ContractClassName): ContractAttachment?
} }

View File

@ -100,7 +100,7 @@ interface NodeVerificationSupport : VerificationSupport {
val upgradedContractAttachment = getAttachment(wtx.upgradedContractAttachmentId) ?: throw MissingContractAttachments(emptyList()) val upgradedContractAttachment = getAttachment(wtx.upgradedContractAttachmentId) ?: throw MissingContractAttachments(emptyList())
val networkParameters = getNetworkParameters(wtx.networkParametersHash) ?: throw TransactionResolutionException(wtx.id) val networkParameters = getNetworkParameters(wtx.networkParametersHash) ?: throw TransactionResolutionException(wtx.id)
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( return AttachmentsClassLoaderBuilder.withAttachmentsClassLoaderContext(
listOf(legacyContractAttachment, upgradedContractAttachment), listOf(legacyContractAttachment, upgradedContractAttachment),
networkParameters, networkParameters,
wtx.id, wtx.id,

View File

@ -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. * @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") @Suppress("LongParameterList")
fun <T> withAttachmentsClassloaderContext(attachments: List<Attachment>, fun <T> withAttachmentsClassLoaderContext(attachments: List<Attachment>,
params: NetworkParameters, params: NetworkParameters,
txId: SecureHash, txId: SecureHash,
isAttachmentTrusted: (Attachment) -> Boolean, isAttachmentTrusted: (Attachment) -> Boolean,

View File

@ -255,7 +255,7 @@ private constructor(
internal fun verifyInternal(txAttachments: List<Attachment> = this.attachments) { internal fun verifyInternal(txAttachments: List<Attachment> = this.attachments) {
// Switch thread local deserialization context to using a cached attachments classloader. This classloader enforces various rules // 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. // like no-overlap, package namespace ownership and (in future) deterministic Java.
val verifier = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( val verifier = AttachmentsClassLoaderBuilder.withAttachmentsClassLoaderContext(
txAttachments, txAttachments,
getParamsWithGoo(), getParamsWithGoo(),
id, id,

View File

@ -2,13 +2,13 @@
package net.corda.core.transactions package net.corda.core.transactions
import co.paralleluniverse.strands.Strand import co.paralleluniverse.strands.Strand
import net.corda.core.CordaInternal
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.SignatureMetadata
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.* 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.VerifyingServiceHub
import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.internal.verification.toVerifyingServiceHub
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
@ -30,8 +30,6 @@ import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
@ -74,7 +72,10 @@ open class TransactionBuilder(
private fun defaultLockId() = (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID() private fun defaultLockId() = (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()
private val log = contextLogger() private val log = contextLogger()
private val MISSING_CLASS_DISABLED = java.lang.Boolean.getBoolean("net.corda.transactionbuilder.missingclass.disabled") 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 const val ID_PATTERN = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"
private val FQCP: Pattern = Pattern.compile("$ID_PATTERN(/$ID_PATTERN)+") private val FQCP: Pattern = Pattern.compile("$ID_PATTERN(/$ID_PATTERN)+")
private fun isValidJavaClass(identifier: String) = FQCP.matcher(identifier).matches() private fun isValidJavaClass(identifier: String) = FQCP.matcher(identifier).matches()
@ -86,7 +87,7 @@ open class TransactionBuilder(
private val inputsWithTransactionState = arrayListOf<StateAndRef<ContractState>>() private val inputsWithTransactionState = arrayListOf<StateAndRef<ContractState>>()
private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>() private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>()
private val excludedAttachments = arrayListOf<AttachmentId>() private var excludedAttachments: Set<AttachmentId> = emptySet()
/** /**
* Creates a copy of the builder. * 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 [ZoneVersionTooLowException] if there are reference states and the zone minimum platform version is less than 4.
*/ */
@Throws(MissingContractAttachments::class) @Throws(MissingContractAttachments::class)
fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services, null) fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransaction(services.toVerifyingServiceHub())
.apply { checkSupportedHashType() }
/** /**
* Generates a [WireTransaction] from this builder, resolves any [AutomaticPlaceholderConstraint], and selects the attachments to use for this transaction. * 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<Any, Any>): WireTransaction { fun toWireTransaction(services: ServicesForResolution, schemeId: Int, properties: Map<Any, Any>): WireTransaction {
val magic: SerializationMagic = getCustomSerializationMagicFromSchemeId(schemeId) val magic: SerializationMagic = getCustomSerializationMagicFromSchemeId(schemeId)
val serializationContext = SerializationDefaults.P2P_CONTEXT.withPreferredSerializationVersion(magic).withProperties(properties) val serializationContext = SerializationDefaults.P2P_CONTEXT.withPreferredSerializationVersion(magic).withProperties(properties)
return toWireTransactionWithContext(services, serializationContext).apply { checkSupportedHashType() } return toWireTransaction(services.toVerifyingServiceHub(), serializationContext)
} }
@CordaInternal private tailrec fun toWireTransaction(
@JvmSynthetic
internal fun toWireTransactionWithContext(
services: ServicesForResolution,
serializationContext: SerializationContext?
) : WireTransaction = toWireTransactionWithContext(services.toVerifyingServiceHub(), serializationContext, 0)
private tailrec fun toWireTransactionWithContext(
serviceHub: VerifyingServiceHub, serviceHub: VerifyingServiceHub,
serializationContext: SerializationContext?, serializationContext: SerializationContext? = null,
tryCount: Int tryCount: Int = 0
): WireTransaction { ): WireTransaction {
val referenceStates = referenceStates() val referenceStates = referenceStates()
if (referenceStates.isNotEmpty()) { if (referenceStates.isNotEmpty()) {
@ -193,8 +186,7 @@ open class TransactionBuilder(
} }
resolveNotary(serviceHub) resolveNotary(serviceHub)
val (allContractAttachments: Collection<AttachmentId>, resolvedOutputs: List<TransactionState<ContractState>>) val (allContractAttachments, resolvedOutputs) = selectContractAttachmentsAndOutputStateConstraints(serviceHub)
= selectContractAttachmentsAndOutputStateConstraints(serviceHub, serializationContext)
// Final sanity check that all states have the correct constraints. // Final sanity check that all states have the correct constraints.
for (state in (inputsWithTransactionState.map { it.state } + resolvedOutputs)) { for (state in (inputsWithTransactionState.map { it.state } + resolvedOutputs)) {
@ -202,17 +194,21 @@ open class TransactionBuilder(
} }
val wireTx = SerializationFactory.defaultFactory.withCurrentContext(serializationContext) { 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( WireTransaction(
createComponentGroups( createComponentGroups(
inputStates(), inputStates(),
resolvedOutputs, resolvedOutputs,
commands(), commands(),
// Sort the attachments to ensure transaction builds are stable. attachmentsBuilder.toList(),
((allContractAttachments + attachments).toSortedSet() - excludedAttachments).toList(),
notary, notary,
window, window,
referenceStates, referenceStates,
serviceHub.networkParametersService.currentHash), serviceHub.networkParametersService.currentHash
),
privacySalt, privacySalt,
serviceHub.digestService serviceHub.digestService
) )
@ -224,10 +220,11 @@ open class TransactionBuilder(
// TODO - remove once proper support for cordapp dependencies is added. // TODO - remove once proper support for cordapp dependencies is added.
val addedDependency = addMissingDependency(serviceHub, wireTx, tryCount) val addedDependency = addMissingDependency(serviceHub, wireTx, tryCount)
return if (addedDependency) return if (addedDependency) {
toWireTransactionWithContext(serviceHub, serializationContext, tryCount + 1) toWireTransaction(serviceHub, serializationContext, tryCount + 1)
else } else {
wireTx wireTx.apply { checkSupportedHashType() }
}
} }
// Returns the first exception in the hierarchy that matches one of the [types]. // Returns the first exception in the hierarchy that matches one of the [types].
@ -301,10 +298,7 @@ open class TransactionBuilder(
} }
attachments.addAll(extraAttachments) attachments.addAll(extraAttachments)
with(excludedAttachments) { excludedAttachments = (txAttachments - replacementAttachments).toSet()
clear()
addAll(txAttachments - replacementAttachments)
}
log.warn("Attempting to rebuild transaction with these extra attachments:{}{}and these attachments removed:{}", log.warn("Attempting to rebuild transaction with these extra attachments:{}{}and these attachments removed:{}",
extraAttachments.toPrettyString(), 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) * TODO also on the versions of the attachments of the transactions generating the input states. ( after we add versioning)
*/ */
private fun selectContractAttachmentsAndOutputStateConstraints( private fun selectContractAttachmentsAndOutputStateConstraints(
services: ServicesForResolution, serviceHub: VerifyingServiceHub
@Suppress("UNUSED_PARAMETER") serializationContext: SerializationContext? ): Pair<List<ContractAttachment>, List<TransactionState<*>>> {
): Pair<Collection<AttachmentId>, List<TransactionState<ContractState>>> {
// Determine the explicitly set contract attachments. // Determine the explicitly set contract attachments.
val explicitAttachmentContracts: List<Pair<ContractClassName, AttachmentId>> = this.attachments val explicitContractToAttachments = attachments
.map(services.attachments::openAttachment) .mapNotNull { serviceHub.attachments.openAttachment(it) as? ContractAttachment }
.mapNotNull { it as? ContractAttachment } .groupByMultipleKeys(ContractAttachment::allContracts) { contract, attachment1, attachment2 ->
.flatMap { attch -> throw IllegalArgumentException("Multiple attachments specified for the same contract $contract ($attachment1, $attachment2).")
attch.allContracts.map { it to attch.id }
} }
// 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<ContractClassName, AttachmentId> = explicitAttachmentContracts.toMap()
val inputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = inputsWithTransactionState.map { it.state } val inputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = inputsWithTransactionState.map { it.state }
.groupBy { it.contract } .groupBy { it.contract }
val outputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = outputs.groupBy { it.contract } val outputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = 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. // Filter out all contracts that might have been already used by 'normal' input or output states.
val referenceStateGroups: Map<ContractClassName, List<TransactionState<ContractState>>> val referenceStateGroups: Map<ContractClassName, List<TransactionState<ContractState>>>
= referencesWithTransactionState.groupBy { it.contract } = referencesWithTransactionState.groupBy { it.contract }
val refStateContractAttachments: List<AttachmentId> = referenceStateGroups val refStateContractAttachments = referenceStateGroups
.filterNot { it.key in allContracts } .filterNot { it.key in allContracts }
.map { refStateEntry -> .map { refStateEntry -> serviceHub.getInstalledContractAttachment(refStateEntry.key, refStateEntry::value) }
getInstalledContractAttachmentId(
refStateEntry.key,
refStateEntry.value,
services
)
}
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment. // For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
val contractAttachmentsAndResolvedOutputStates: List<Pair<AttachmentId, List<TransactionState<ContractState>>?>> = allContracts.toSet() val contractAttachmentsAndResolvedOutputStates = allContracts.map { contract ->
.map { ctr -> selectAttachmentAndResolveOutputStates(
handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], services) contract,
} inputContractGroups[contract],
outputContractGroups[contract],
explicitContractToAttachments[contract],
serviceHub
)
}
val resolvedStates: List<TransactionState<ContractState>> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second } val resolvedStates = contractAttachmentsAndResolvedOutputStates.flatMap { it.second }
.flatten()
// The output states need to preserve the order in which they were added. // The output states need to preserve the order in which they were added.
val resolvedOutputStatesInTheOriginalOrder: List<TransactionState<ContractState>> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance }!! } val resolvedOutputStatesInTheOriginalOrder: List<TransactionState<ContractState>> = outputStates().map { os ->
resolvedStates.first { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance }
}
val attachments: Collection<AttachmentId> = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments val attachments = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments
return Pair(attachments, resolvedOutputStatesInTheOriginalOrder) 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]. * 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. * * 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, contractClassName: ContractClassName,
inputStates: List<TransactionState<ContractState>>?, inputStates: List<TransactionState<ContractState>>?,
outputStates: List<TransactionState<ContractState>>?, outputStates: List<TransactionState<ContractState>>?,
explicitContractAttachment: AttachmentId?, explicitContractAttachment: ContractAttachment?,
services: ServicesForResolution serviceHub: VerifyingServiceHub
): Pair<AttachmentId, List<TransactionState<ContractState>>?> { ): Pair<ContractAttachment, List<TransactionState<*>>> {
val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList()) val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList())
fun selectAttachment() = getInstalledContractAttachmentId( fun selectAttachmentForContract() = serviceHub.getInstalledContractAttachment(contractClassName) {
contractClassName, inputsAndOutputs.filterNot { it.constraint in automaticConstraints }
inputsAndOutputs.filterNot { it.constraint in automaticConstraints }, }
services
)
/* /*
This block handles the very specific code path where a [HashAttachmentConstraint] can 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 This can only happen in a private network where all nodes have started with
a system parameter that disables the hash constraint check. a system parameter that disables the hash constraint check.
*/ */
if (canMigrateFromHashToSignatureConstraint(inputStates, outputStates, services)) { if (canMigrateFromHashToSignatureConstraint(inputStates, outputStates, serviceHub)) {
val attachmentId = selectAttachment() val attachment = selectAttachmentForContract()
val attachment = services.attachments.openAttachment(attachmentId) if (attachment.isSigned && (explicitContractAttachment == null || explicitContractAttachment.id == attachment.id)) {
require(attachment != null) { "Contract attachment $attachmentId for $contractClassName is missing." } val signatureConstraint = makeSignatureAttachmentConstraint(attachment.signerKeys)
if ((attachment as ContractAttachment).isSigned && (explicitContractAttachment == null || explicitContractAttachment == attachment.id)) {
val signatureConstraint =
makeSignatureAttachmentConstraint(attachment.signerKeys)
require(signatureConstraint.isSatisfiedBy(attachment)) { "Selected output constraint: $signatureConstraint not satisfying ${attachment.id}" } require(signatureConstraint.isSatisfiedBy(attachment)) { "Selected output constraint: $signatureConstraint not satisfying ${attachment.id}" }
val resolvedOutputStates = outputStates?.map { val resolvedOutputStates = outputStates?.map {
if (it.constraint in automaticConstraints) { if (it.constraint in automaticConstraints) it.copy(constraint = signatureConstraint) else it
it.copy(constraint = signatureConstraint) } ?: emptyList()
} else { return attachment to resolvedOutputStates
it
}
}
return attachment.id to resolvedOutputStates
} }
} }
// Determine if there are any HashConstraints that pin the version of a contract. If there are, check if we trust them. // 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<ContractAttachment> = inputsAndOutputs
.filter { it.constraint is HashAttachmentConstraint } .filter { it.constraint is HashAttachmentConstraint }
.mapToSet { state -> .mapToSet<TransactionState<*>, ContractAttachment> { state ->
val attachment = services.attachments.openAttachment((state.constraint as HashAttachmentConstraint).attachmentId) val attachment = serviceHub.attachments.openAttachment((state.constraint as HashAttachmentConstraint).attachmentId)
if (attachment == null || attachment !is ContractAttachment || !isUploaderTrusted(attachment.uploader)) { if (attachment !is ContractAttachment || !isUploaderTrusted(attachment.uploader)) {
// This should never happen because these are input states that should have been validated already. // This should never happen because these are input states that should have been validated already.
throw MissingContractAttachments(listOf(state)) 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. // Check that states with the HashConstraint don't conflict between themselves or with an explicitly set attachment.
require(hashAttachments.size <= 1) { 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) { val selectedAttachment = if (explicitContractAttachment != null) {
@Suppress("USELESS_CAST") // Because the external verifier uses Kotlin 1.2 if (hashAttachment != null) {
require(explicitContractAttachment == (hashAttachments.single() as ContractAttachment).attachment.id) { 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." "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. // For Exit transactions (no output states) there is no need to resolve the output constraints.
if (outputStates == null) { if (outputStates == null) {
return Pair(selectedAttachmentId, null) return Pair(selectedAttachment, emptyList())
} }
// If there are no automatic constraints, there is nothing to resolve. // If there are no automatic constraints, there is nothing to resolve.
if (outputStates.none { it.constraint in automaticConstraints }) { if (outputStates.none { it.constraint in automaticConstraints }) {
return Pair(selectedAttachmentId, outputStates) return Pair(selectedAttachment, outputStates)
} }
// The final step is to resolve AutomaticPlaceholderConstraint. // The final step is to resolve AutomaticPlaceholderConstraint.
val automaticConstraintPropagation = contractClassName.contractHasAutomaticConstraintPropagation(inputsAndOutputs.first().data::class.java.classLoader) val automaticConstraintPropagation = contractClassName.contractHasAutomaticConstraintPropagation(inputsAndOutputs.first().data::class.java.classLoader)
// When automaticConstraintPropagation is disabled for a contract, output states must an explicit Constraint. // 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. // 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. // Sanity check that the selected attachment actually passes.
val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations) val constraintAttachment = AttachmentWithContext(selectedAttachment, contractClassName, serviceHub.networkParameters.whitelistedContractImplementations)
require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachmentId" } require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) {
"Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachment"
}
val resolvedOutputStates = outputStates.map { val resolvedOutputStates = outputStates.map {
val outputConstraint = it.constraint val outputConstraint = it.constraint
@ -534,14 +506,16 @@ open class TransactionBuilder(
} else { } 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. // 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 -> 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" } require(outputConstraint.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" }
it it
} }
} }
return Pair(selectedAttachmentId, resolvedOutputStates) return Pair(selectedAttachment, resolvedOutputStates)
} }
/** /**
@ -572,21 +546,25 @@ open class TransactionBuilder(
contractClassName: ContractClassName, contractClassName: ContractClassName,
inputStates: List<TransactionState<ContractState>>?, inputStates: List<TransactionState<ContractState>>?,
attachmentToUse: ContractAttachment, attachmentToUse: ContractAttachment,
services: ServicesForResolution): AttachmentConstraint = when { services: ServicesForResolution
inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse, services) ): AttachmentConstraint {
attachmentToUse.signerKeys.isNotEmpty() && services.networkParameters.minimumPlatformVersion < PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS -> { return when {
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}.") inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse, services)
if (useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters)) { attachmentToUse.signerKeys.isNotEmpty() && services.networkParameters.minimumPlatformVersion < MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS -> {
log.warnOnce("Reverting back to using whitelisted zone constraints for contract $contractClassName") log.warnOnce("Signature constraints not available on network requiring a minimum platform version of " +
WhitelistedByZoneAttachmentConstraint "$MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS. Current is: ${services.networkParameters.minimumPlatformVersion}.")
} else { if (useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters)) {
log.warnOnce("Reverting back to using hash constraints for contract $contractClassName") log.warnOnce("Reverting back to using whitelisted zone constraints for contract $contractClassName")
HashAttachmentConstraint(attachmentToUse.id) 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 // This ensures a smooth migration from a Whitelist Constraint to a Signature Constraint
constraints.any { it is WhitelistedByZoneAttachmentConstraint } && constraints.any { it is WhitelistedByZoneAttachmentConstraint } &&
attachmentToUse.isSigned && attachmentToUse.isSigned &&
services.networkParameters.minimumPlatformVersion >= PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS -> services.networkParameters.minimumPlatformVersion >= MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS ->
transitionToSignatureConstraint(constraints, attachmentToUse) 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 // 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) SignatureAttachmentConstraint.create(CompositeKey.Builder().addKeys(attachmentSigners)
.build()) .build())
private fun getInstalledContractAttachmentId( private inline fun VerifyingServiceHub.getInstalledContractAttachment(
contractClassName: String, contractClassName: String,
states: List<TransactionState<ContractState>>, statesForException: () -> List<TransactionState<*>>
services: ServicesForResolution ): ContractAttachment {
): AttachmentId { return cordappProvider.getContractAttachment(contractClassName)
return services.cordappProvider.getContractAttachmentID(contractClassName) ?: throw MissingContractAttachments(statesForException(), contractClassName)
?: throw MissingContractAttachments(states, 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) @Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
fun toLedgerTransaction(services: ServiceHub) = toWireTransaction(services).toLedgerTransaction(services) fun toLedgerTransaction(services: ServiceHub) = toWireTransaction(services).toLedgerTransaction(services)

View File

@ -6,7 +6,11 @@ import net.corda.core.contracts.ContractClassName
import net.corda.core.contracts.UpgradedContract import net.corda.core.contracts.UpgradedContract
import net.corda.core.contracts.UpgradedContractWithLegacyConstraint import net.corda.core.contracts.UpgradedContractWithLegacyConstraint
import net.corda.core.crypto.SecureHash 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 org.slf4j.LoggerFactory
import java.io.InputStream import java.io.InputStream
import java.nio.file.Files 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 // 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. // 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 { interface ContractsJar {
val hash: SecureHash val hash: SecureHash
@ -32,7 +36,8 @@ class ContractsJarFile(private val file: Path) : ContractsJar {
return scanResult.use { result -> return scanResult.use { result ->
coreContractClasses coreContractClasses
.flatMap { result.getClassesImplementing(it.qualifiedName)} .asSequence()
.flatMap(result::getClassesImplementing)
.filterNot { it.isAbstract } .filterNot { it.isAbstract }
.filterNot { it.isInterface } .filterNot { it.isInterface }
.map { it.name } .map { it.name }

View File

@ -1,15 +1,14 @@
package net.corda.nodeapi.internal.cordapp package net.corda.nodeapi.internal.cordapp
import net.corda.core.cordapp.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.cordapp.CordappImpl
import net.corda.core.internal.flatMapToSet
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
/** /**
* Handles loading [Cordapp]s. * Handles loading [Cordapp]s.
*/ */
interface CordappLoader : AutoCloseable { interface CordappLoader : AutoCloseable {
/** /**
* Returns all [Cordapp]s found. * Returns all [Cordapp]s found.
*/ */
@ -19,15 +18,10 @@ interface CordappLoader : AutoCloseable {
* Returns a [ClassLoader] containing all types from all [Cordapp]s. * Returns a [ClassLoader] containing all types from all [Cordapp]s.
*/ */
val appClassLoader: ClassLoader 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<Class<out FlowLogic<*>>, Cordapp>
/**
* Returns all [MappedSchema] found inside the [Cordapp]s.
*/
val cordappSchemas: Set<MappedSchema>
} }
/**
* Returns all [MappedSchema] found inside the [Cordapp]s.
*/
val CordappLoader.cordappSchemas: Set<MappedSchema>
get() = cordapps.flatMapToSet { it.customSchemas }

View File

@ -80,6 +80,15 @@ processResources {
processTestResources { processTestResources {
from file("$rootDir/config/test/jolokia-access.xml") 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 // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in

View File

@ -147,6 +147,7 @@ import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.NodeStatus import net.corda.nodeapi.internal.NodeStatus
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.cordapp.CordappLoader 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.CryptoService
import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEvent import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEvent

View File

@ -1,32 +1,31 @@
package net.corda.node.internal.cordapp 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.contracts.ContractClassName
import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.Cordapp
import net.corda.core.cordapp.CordappContext import net.corda.core.cordapp.CordappContext
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.cordapp.CordappProviderInternal import net.corda.core.internal.cordapp.CordappProviderInternal
import net.corda.core.internal.groupByMultipleKeys
import net.corda.core.internal.verification.AttachmentFixups import net.corda.core.internal.verification.AttachmentFixups
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.persistence.AttachmentStorageInternal
import net.corda.nodeapi.internal.cordapp.CordappLoader import net.corda.nodeapi.internal.cordapp.CordappLoader
import java.net.URL
import java.nio.file.FileAlreadyExistsException
import java.util.concurrent.ConcurrentHashMap 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. * 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 cordappConfigProvider: CordappConfigProvider,
private val attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal { private val attachmentStorage: AttachmentStorageInternal) : SingletonSerializeAsToken(), CordappProviderInternal {
private val contextCache = ConcurrentHashMap<Cordapp, CordappContext>() private val contextCache = ConcurrentHashMap<Cordapp, CordappContext>()
private val cordappAttachments = HashBiMap.create<SecureHash, URL>() private lateinit var flowToCordapp: Map<Class<out FlowLogic<*>>, CordappImpl>
override val attachmentFixups = AttachmentFixups() override val attachmentFixups = AttachmentFixups()
@ -38,17 +37,12 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
override val cordapps: List<CordappImpl> get() = cordappLoader.cordapps override val cordapps: List<CordappImpl> get() = cordappLoader.cordapps
fun start() { fun start() {
cordappAttachments.putAll(loadContractsIntoAttachmentStore()) loadContractsIntoAttachmentStore(cordappLoader.cordapps)
verifyInstalledCordapps() flowToCordapp = makeFlowToCordapp()
// Load the fix-ups after uploading any new contracts into attachment storage. // Load the fix-ups after uploading any new contracts into attachment storage.
attachmentFixups.load(cordappLoader.appClassLoader) 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 { override fun getAppContext(): CordappContext {
// TODO: Use better supported APIs in Java 9 // TODO: Use better supported APIs in Java 9
Exception().stackTrace.forEach { stackFrame -> Exception().stackTrace.forEach { stackFrame ->
@ -62,41 +56,40 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
} }
override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? { 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
} }
/** override fun getContractAttachment(contractClassName: ContractClassName): ContractAttachment? {
* Gets the attachment ID of this CorDapp. Only CorDapps with contracts have an attachment ID return getContractAttachmentID(contractClassName)?.let(::getContractAttachment)
* }
* @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]
private fun loadContractsIntoAttachmentStore(): Map<SecureHash, URL> { private fun loadContractsIntoAttachmentStore(cordapps: List<CordappImpl>) {
return cordapps.filter { it.contractClassNames.isNotEmpty() }.associate { cordapp -> for (cordapp in cordapps) {
cordapp.jarPath.openStream().use { stream -> if (cordapp.contractClassNames.isEmpty()) continue
try { val attachmentId = cordapp.jarFile.inputStream().use { stream ->
// This code can be reached by [MockNetwork] tests which uses [MockAttachmentStorage] attachmentStorage.privilegedImportOrGetAttachment(stream, DEPLOYED_CORDAPP_UPLOADER, cordapp.info.shortName)
// [MockAttachmentStorage] cannot implement [AttachmentStorageInternal] because }
// doing so results in internal functions being exposed in the public API. // TODO We could remove this check if we had an import method for CorDapps, since it wouldn't need to hash the InputStream.
if (attachmentStorage is AttachmentStorageInternal) { // As it stands, we just have to double-check the hashes match, which should be the case (see NodeAttachmentService).
attachmentStorage.privilegedImportAttachment( check(attachmentId == cordapp.jarHash) {
stream, "Something has gone wrong. SHA-256 hash of ${cordapp.jarFile} (${cordapp.jarHash}) does not match attachment ID ($attachmentId)"
DEPLOYED_CORDAPP_UPLOADER, }
cordapp.info.shortName }
) }
} else {
attachmentStorage.importAttachment( private fun getContractAttachment(id: AttachmentId): ContractAttachment {
stream, return checkNotNull(attachmentStorage.openAttachment(id) as? ContractAttachment) { "Contract attachment $id has gone missing!" }
DEPLOYED_CORDAPP_UPLOADER, }
cordapp.info.shortName
) private fun makeFlowToCordapp(): Map<Class<out FlowLogic<*>>, CordappImpl> {
} return cordappLoader.cordapps.groupByMultipleKeys(CordappImpl::allFlows) { flowClass, _, _ ->
} catch (faee: FileAlreadyExistsException) { val overlappingCordapps = cordappLoader.cordapps.filter { flowClass in it.allFlows }
AttachmentId.create(faee.message!!) throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow ${flowClass.name}: " +
} "[ ${overlappingCordapps.joinToString { it.jarPath.toString() }} ].",
} to cordapp.jarPath flowClass.name,
overlappingCordapps.joinToString { it.jarFile.absolutePathString() }
)
} }
} }
@ -110,7 +103,7 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
return contextCache.computeIfAbsent(cordapp) { return contextCache.computeIfAbsent(cordapp) {
CordappContext.create( CordappContext.create(
cordapp, cordapp,
getCordappAttachmentId(cordapp), cordapp.jarHash.takeIf(attachmentStorage::hasAttachment), // Not all CorDapps are attachments
cordappLoader.appClassLoader, cordappLoader.appClassLoader,
TypesafeCordappConfig(cordappConfigProvider.getConfigByName(cordapp.name)) TypesafeCordappConfig(cordappConfigProvider.getConfigByName(cordapp.name))
) )
@ -123,7 +116,7 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
* @param className The class name * @param className The class name
* @return cordapp A cordapp or null if no cordapp has the given class loaded * @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]
} }

View File

@ -1,7 +1,7 @@
package net.corda.node.internal.cordapp package net.corda.node.internal.cordapp
import io.github.classgraph.ClassGraph import io.github.classgraph.ClassGraph
import io.github.classgraph.ClassInfo import io.github.classgraph.ClassInfoList
import io.github.classgraph.ScanResult import io.github.classgraph.ScanResult
import net.corda.common.logging.errorReporting.CordappErrors import net.corda.common.logging.errorReporting.CordappErrors
import net.corda.common.logging.errorReporting.ErrorCode 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.SchedulableFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.flows.StartableByService 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.JarSignatureCollector
import net.corda.core.internal.PlatformVersionSwitches import net.corda.core.internal.PlatformVersionSwitches
import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.cordapp.CordappImpl.Companion.UNKNOWN_INFO import net.corda.core.internal.cordapp.CordappImpl.Companion.UNKNOWN_INFO
import net.corda.core.internal.cordapp.get import net.corda.core.internal.cordapp.get
import net.corda.core.internal.flatMapToSet
import net.corda.core.internal.hash import net.corda.core.internal.hash
import net.corda.core.internal.isAbstractClass import net.corda.core.internal.isAbstractClass
import net.corda.core.internal.loadClassOfType import net.corda.core.internal.loadClassOfType
import net.corda.core.internal.location import net.corda.core.internal.location
import net.corda.core.internal.groupByMultipleKeys
import net.corda.core.internal.mapToSet import net.corda.core.internal.mapToSet
import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.notary.NotaryService
import net.corda.core.internal.notary.SinglePartyNotaryService 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.SerializationWhitelist
import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializeAsToken
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.node.VersionInfo import net.corda.node.VersionInfo
import net.corda.nodeapi.internal.cordapp.CordappLoader import net.corda.nodeapi.internal.cordapp.CordappLoader
import net.corda.nodeapi.internal.coreContractClasses import net.corda.nodeapi.internal.coreContractClasses
import net.corda.serialization.internal.DefaultWhitelist import net.corda.serialization.internal.DefaultWhitelist
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import java.math.BigInteger
import java.net.URLClassLoader import java.net.URLClassLoader
import java.nio.file.Path import java.nio.file.Path
import java.util.Random
import java.util.ServiceLoader import java.util.ServiceLoader
import java.util.concurrent.ConcurrentHashMap
import java.util.jar.JarInputStream import java.util.jar.JarInputStream
import java.util.jar.Manifest import java.util.jar.Manifest
import java.util.zip.ZipInputStream
import kotlin.io.path.absolutePathString import kotlin.io.path.absolutePathString
import kotlin.io.path.exists import kotlin.io.path.exists
import kotlin.io.path.inputStream import kotlin.io.path.inputStream
@ -67,27 +64,11 @@ import kotlin.reflect.KClass
* *
* @property cordappJars The classpath of cordapp JARs * @property cordappJars The classpath of cordapp JARs
*/ */
class JarScanningCordappLoader private constructor(private val cordappJars: Set<Path>, @Suppress("TooManyFunctions")
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN, class JarScanningCordappLoader(private val cordappJars: Set<Path>,
extraCordapps: List<CordappImpl>, private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
private val signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()) : CordappLoaderTemplate() { private val extraCordapps: List<CordappImpl> = emptyList(),
init { private val signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()) : CordappLoader {
if (cordappJars.isEmpty()) {
logger.info("No CorDapp paths provided")
} else {
logger.info("Loading CorDapps from ${cordappJars.joinToString()}")
}
}
private val cordappClasses: ConcurrentHashMap<String, Set<Cordapp>> = ConcurrentHashMap()
override val cordapps: List<CordappImpl> by lazy { loadCordapps() + extraCordapps }
override val appClassLoader: URLClassLoader = URLClassLoader(
cordappJars.stream().map { it.toUri().toURL() }.toTypedArray(),
javaClass.classLoader
)
override fun close() = appClassLoader.close()
companion object { companion object {
private val logger = contextLogger() private val logger = contextLogger()
@ -100,100 +81,88 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set<
versionInfo: VersionInfo = VersionInfo.UNKNOWN, versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl> = emptyList(), extraCordapps: List<CordappImpl> = emptyList(),
signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader { signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader {
logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}") logger.info("Looking for CorDapps in ${cordappDirs.toSet().joinToString(", ", "[", "]")}")
val paths = cordappDirs val cordappJars = cordappDirs
.asSequence() .asSequence()
.flatMap { if (it.exists()) it.listDirectoryEntries("*.jar") else emptyList() } .flatMap { if (it.exists()) it.listDirectoryEntries("*.jar") else emptyList() }
.toSet() .toSet()
return JarScanningCordappLoader(paths, versionInfo, extraCordapps, signerKeyFingerprintBlacklist) return JarScanningCordappLoader(cordappJars, 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<Path>,
versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl> = emptyList(),
cordappsSignerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader {
return JarScanningCordappLoader(scanJars, versionInfo, extraCordapps, cordappsSignerKeyFingerprintBlacklist)
} }
} }
private fun loadCordapps(): List<CordappImpl> { init {
val invalidCordapps = mutableMapOf<String, Path>() logger.debug { "cordappJars: $cordappJars" }
}
val cordapps = cordappJars override val appClassLoader = URLClassLoader(cordappJars.stream().map { it.toUri().toURL() }.toTypedArray(), javaClass.classLoader)
.map { path -> scanCordapp(path).use { it.toCordapp(path) } }
.filter { cordapp -> private val internal by lazy(::InternalHolder)
if (cordapp.minimumPlatformVersion > versionInfo.platformVersion) {
logger.warn("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it requires minimum " + override val cordapps: List<CordappImpl>
"platform version ${cordapp.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).") get() = internal.cordapps
invalidCordapps["CorDapp requires minimumPlatformVersion: ${cordapp.minimumPlatformVersion}, but was: ${versionInfo.platformVersion}"] = cordapp.jarFile
false override fun close() = appClassLoader.close()
} else {
true private inner class InternalHolder {
} val cordapps = cordappJars.mapTo(ArrayList(), ::scanCordapp)
}.filter { cordapp ->
if (signerKeyFingerprintBlacklist.isEmpty()) { init {
true //Nothing blacklisted, no need to check checkInvalidCordapps()
} else { checkDuplicateCordapps()
val certificates = cordapp.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates) checkContractOverlap()
val blockedCertificates = certificates.filter { it.publicKey.hash.sha256() in signerKeyFingerprintBlacklist } cordapps += extraCordapps
if (certificates.isEmpty() || (certificates - blockedCertificates).isNotEmpty()) { }
true // Cordapp is not signed or it is signed by at least one non-blacklisted certificate
} else { private fun checkInvalidCordapps() {
logger.warn("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it is signed by blacklisted key(s) only (probably development key): " + val invalidCordapps = LinkedHashMap<String, CordappImpl>()
"${blockedCertificates.map { it.publicKey }}.")
invalidCordapps["Corresponding contracts are signed by blacklisted key(s) only (probably development key),"] = cordapp.jarFile for (cordapp in cordapps) {
false 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()) { if (invalidCordapps.isNotEmpty()) {
throw InvalidCordappException("Invalid Cordapps found, that couldn't be loaded: " + throw InvalidCordappException("Invalid Cordapps found, that couldn't be loaded: " +
"${invalidCordapps.map { "Problem: ${it.key} in Cordapp ${it.value}" }}, ") "${invalidCordapps.map { "Problem: ${it.key} in Cordapp ${it.value.jarFile}" }}, ")
}
} }
cordapps.forEach(::register) private fun checkDuplicateCordapps() {
return cordapps for (group in cordapps.groupBy { it.jarHash }.values) {
} if (group.size > 1) {
throw DuplicateCordappsInstalledException(group[0], group.drop(1))
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)
} }
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.") "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 manifest: Manifest? = JarInputStream(path.inputStream()).use { it.manifest }
val info = parseCordappInfo(manifest, CordappImpl.jarName(path)) val info = parseCordappInfo(manifest, CordappImpl.jarName(path))
val minPlatformVersion = manifest?.get(CordappImpl.MIN_PLATFORM_VERSION)?.toIntOrNull() ?: 1 val minPlatformVersion = manifest?.get(CordappImpl.MIN_PLATFORM_VERSION)?.toIntOrNull() ?: 1
val targetPlatformVersion = manifest?.get(CordappImpl.TARGET_PLATFORM_VERSION)?.toIntOrNull() ?: minPlatformVersion val targetPlatformVersion = manifest?.get(CordappImpl.TARGET_PLATFORM_VERSION)?.toIntOrNull() ?: minPlatformVersion
validateContractStateClassVersion(this)
validateWhitelistClassVersion(this)
return CordappImpl( return CordappImpl(
path, path,
findContractClassNamesWithVersionCheck(this), findContractClassNames(this),
findInitiatedFlows(this), findInitiatedFlows(this),
findRPCFlows(this), findRPCFlows(this),
findServiceFlows(this), findServiceFlows(this),
@ -206,10 +175,9 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set<
findCustomSchemas(this), findCustomSchemas(this),
findAllFlows(this), findAllFlows(this),
info, info,
path.hash,
minPlatformVersion, minPlatformVersion,
targetPlatformVersion, targetPlatformVersion,
findNotaryService(this), notaryService = findNotaryService(this),
explicitCordappClasses = findAllCordappClasses(this) explicitCordappClasses = findAllCordappClasses(this)
) )
} }
@ -278,27 +246,27 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set<
return version return version
} }
private fun findNotaryService(scanResult: RestrictedScanResult): Class<out NotaryService>? { private fun findNotaryService(scanResult: ScanResult): Class<out NotaryService>? {
// Note: we search for implementations of both NotaryService and SinglePartyNotaryService as // 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 // the scanner won't find subclasses deeper down the hierarchy if any intermediate class is not
// present in the CorDapp. // present in the CorDapp.
val result = scanResult.getClassesWithSuperclass(NotaryService::class) + val result = scanResult.getClassesExtending(NotaryService::class) +
scanResult.getClassesWithSuperclass(SinglePartyNotaryService::class) scanResult.getClassesExtending(SinglePartyNotaryService::class)
if (result.isNotEmpty()) { if (result.isNotEmpty()) {
logger.info("Found notary service CorDapp implementations: " + result.joinToString(", ")) logger.info("Found notary service CorDapp implementations: " + result.joinToString(", "))
} }
return result.firstOrNull() return result.firstOrNull()
} }
private fun findServices(scanResult: RestrictedScanResult): List<Class<out SerializeAsToken>> { private fun findServices(scanResult: ScanResult): List<Class<out SerializeAsToken>> {
return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class) return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class)
} }
private fun findTelemetryComponents(scanResult: RestrictedScanResult): List<Class<out TelemetryComponent>> { private fun findTelemetryComponents(scanResult: ScanResult): List<Class<out TelemetryComponent>> {
return scanResult.getClassesImplementing(TelemetryComponent::class) return scanResult.getClassesImplementing(TelemetryComponent::class)
} }
private fun findInitiatedFlows(scanResult: RestrictedScanResult): List<Class<out FlowLogic<*>>> { private fun findInitiatedFlows(scanResult: ScanResult): List<Class<out FlowLogic<*>>> {
return scanResult.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class) 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)) return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers))
} }
private fun findRPCFlows(scanResult: RestrictedScanResult): List<Class<out FlowLogic<*>>> { private fun findRPCFlows(scanResult: ScanResult): List<Class<out FlowLogic<*>>> {
return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() } return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() }
} }
private fun findServiceFlows(scanResult: RestrictedScanResult): List<Class<out FlowLogic<*>>> { private fun findServiceFlows(scanResult: ScanResult): List<Class<out FlowLogic<*>>> {
return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByService::class) return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByService::class)
} }
private fun findSchedulableFlows(scanResult: RestrictedScanResult): List<Class<out FlowLogic<*>>> { private fun findSchedulableFlows(scanResult: ScanResult): List<Class<out FlowLogic<*>>> {
return scanResult.getClassesWithAnnotation(FlowLogic::class, SchedulableFlow::class) return scanResult.getClassesWithAnnotation(FlowLogic::class, SchedulableFlow::class)
} }
private fun findAllFlows(scanResult: RestrictedScanResult): List<Class<out FlowLogic<*>>> { private fun findAllFlows(scanResult: ScanResult): List<Class<out FlowLogic<*>>> {
return scanResult.getConcreteClassesOfType(FlowLogic::class) return scanResult.getClassesExtending(FlowLogic::class)
} }
private fun findAllCordappClasses(scanResult: RestrictedScanResult): List<String> { private fun findAllCordappClasses(scanResult: ScanResult): List<String> {
return scanResult.getAllStandardClasses() + scanResult.getAllInterfaces() val cordappClasses = ArrayList<String>()
scanResult.allStandardClasses.mapTo(cordappClasses) { it.name }
scanResult.allInterfaces.mapTo(cordappClasses) { it.name }
return cordappClasses
} }
private fun findContractClassNamesWithVersionCheck(scanResult: RestrictedScanResult): List<String> { private fun findContractClassNames(scanResult: ScanResult): List<String> {
val contractClasses = coreContractClasses.flatMapTo(LinkedHashSet()) { scanResult.getNamesOfClassesImplementingWithClassVersionCheck(it) }.toList() val contractClasses = coreContractClasses.flatMapToSet(scanResult::getClassesImplementing)
for (contractClass in contractClasses) { for (contractClass in contractClasses) {
contractClass.warnContractWithoutConstraintPropagation(appClassLoader) contractClass.name.warnContractWithoutConstraintPropagation(appClassLoader)
} }
return contractClasses return contractClasses.map { it.name }
}
private fun validateContractStateClassVersion(scanResult: RestrictedScanResult) {
coreContractClasses.forEach { scanResult.versionCheckClassesImplementing(it) }
}
private fun validateWhitelistClassVersion(scanResult: RestrictedScanResult) {
scanResult.versionCheckClassesImplementing(SerializationWhitelist::class)
} }
private fun findWhitelists(cordappJar: Path): List<SerializationWhitelist> { private fun findWhitelists(cordappJar: Path): List<SerializationWhitelist> {
@ -349,27 +312,25 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set<
} + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app. } + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app.
} }
private fun findSerializers(scanResult: RestrictedScanResult): List<SerializationCustomSerializer<*, *>> { private fun findSerializers(scanResult: ScanResult): List<SerializationCustomSerializer<*, *>> {
return scanResult.getClassesImplementingWithClassVersionCheck(SerializationCustomSerializer::class) return scanResult.getClassesImplementing(SerializationCustomSerializer::class).map { it.kotlin.objectOrNewInstance() }
} }
private fun findCheckpointSerializers(scanResult: RestrictedScanResult): List<CheckpointCustomSerializer<*, *>> { private fun findCheckpointSerializers(scanResult: ScanResult): List<CheckpointCustomSerializer<*, *>> {
return scanResult.getClassesImplementingWithClassVersionCheck(CheckpointCustomSerializer::class) return scanResult.getClassesImplementing(CheckpointCustomSerializer::class).map { it.kotlin.objectOrNewInstance() }
} }
private fun findCustomSchemas(scanResult: RestrictedScanResult): Set<MappedSchema> { private fun findCustomSchemas(scanResult: ScanResult): Set<MappedSchema> {
return scanResult.getClassesWithSuperclass(MappedSchema::class).mapToSet { it.kotlin.objectOrNewInstance() } 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()}") logger.info("Scanning CorDapp ${cordappJar.absolutePathString()}")
val scanResult = ClassGraph() return ClassGraph()
.filterClasspathElementsByURL { it.toPath().isSameFileAs(cordappJar) } .overrideClasspath(cordappJar.absolutePathString())
.overrideClassLoaders(appClassLoader) .enableAllInfo()
.ignoreParentClassLoaders() .pooledScan()
.enableAllInfo() .use { it.toCordapp(cordappJar) }
.pooledScan()
return RestrictedScanResult(scanResult, cordappJar)
} }
private fun <T : Any> loadClass(className: String, type: KClass<T>): Class<out T>? { private fun <T : Any> loadClass(className: String, type: KClass<T>): Class<out T>? {
@ -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 { private fun <T : Any> ScanResult.getClassesExtending(type: KClass<T>): List<Class<out T>> {
fun getNamesOfClassesImplementingWithClassVersionCheck(type: KClass<*>): List<String> { return getSubclasses(type.java).getAllConcreteClasses(type)
return scanResult.getClassesImplementing(type.java.name).map { }
validateClassFileVersion(it)
it.name
}
}
fun versionCheckClassesImplementing(type: KClass<*>) { private fun <T : Any> ScanResult.getClassesImplementing(type: KClass<T>): List<Class<out T>> {
return scanResult.getClassesImplementing(type.java.name).forEach { return getClassesImplementing(type.java).getAllConcreteClasses(type)
validateClassFileVersion(it) }
}
}
fun <T : Any> getClassesWithSuperclass(type: KClass<T>): List<Class<out T>> { private fun <T : Any> ScanResult.getClassesWithAnnotation(type: KClass<T>, annotation: KClass<out Annotation>): List<Class<out T>> {
return scanResult return getClassesWithAnnotation(annotation.java).getAllConcreteClasses(type)
.getSubclasses(type.java.name) }
.names
.mapNotNull { loadClass(it, type) }
.filterNot { it.isAbstractClass }
}
fun <T : Any> getClassesImplementingWithClassVersionCheck(type: KClass<T>): List<T> { private fun <T : Any> ClassInfoList.getAllConcreteClasses(type: KClass<T>): List<Class<out T>> {
return scanResult return mapNotNull { loadClass(it.name, type)?.takeUnless(Class<*>::isAbstractClass) }
.getClassesImplementing(type.java.name)
.mapNotNull {
validateClassFileVersion(it)
loadClass(it.name, type) }
.filterNot { it.isAbstractClass }
.map { it.kotlin.objectOrNewInstance() }
}
fun <T : Any> getClassesImplementing(type: KClass<T>): List<Class<out T>> {
return scanResult
.getClassesImplementing(type.java.name)
.mapNotNull { loadClass(it.name, type) }
.filterNot { it.isAbstractClass }
}
fun <T : Any> getClassesWithAnnotation(type: KClass<T>, annotation: KClass<out Annotation>): List<Class<out T>> {
return scanResult
.getClassesWithAnnotation(annotation.java.name)
.names
.mapNotNull { loadClass(it, type) }
.filterNot { Modifier.isAbstract(it.modifiers) }
}
fun <T : Any> getConcreteClassesOfType(type: KClass<T>): List<Class<out T>> {
return scanResult
.getSubclasses(type.java.name)
.names
.mapNotNull { loadClass(it, type) }
.filterNot { it.isAbstractClass }
}
fun getAllStandardClasses(): List<String> = scanResult.allStandardClasses.names
fun getAllInterfaces(): List<String> = 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()
} }
} }
@ -478,7 +386,7 @@ class CordappInvalidVersionException(
/** /**
* Thrown if duplicate CorDapps are installed on the node * Thrown if duplicate CorDapps are installed on the node
*/ */
class DuplicateCordappsInstalledException(app: Cordapp, duplicates: Set<Cordapp>) class DuplicateCordappsInstalledException(app: Cordapp, duplicates: Collection<Cordapp>)
: CordaRuntimeException("IllegalStateExcepion", "The CorDapp (name: ${app.info.shortName}, file: ${app.name}) " + : 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: " + "is installed multiple times on the node. The following files correspond to the exact same content: " +
"${duplicates.map { it.name }}", null), ErrorCode<CordappErrors> { "${duplicates.map { it.name }}", null), ErrorCode<CordappErrors> {
@ -490,40 +398,3 @@ class DuplicateCordappsInstalledException(app: Cordapp, duplicates: Set<Cordapp>
* Thrown if an exception occurs during loading cordapps. * Thrown if an exception occurs during loading cordapps.
*/ */
class InvalidCordappException(message: String) : CordaRuntimeException(message) class InvalidCordappException(message: String) : CordaRuntimeException(message)
abstract class CordappLoaderTemplate : CordappLoader {
companion object {
private val logger = contextLogger()
}
override val flowCordappMap: Map<Class<out FlowLogic<*>>, 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<MappedSchema> by lazy {
cordapps.flatMap { it.customSchemas }.toSet()
}
}

View File

@ -6,20 +6,29 @@ import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.nodeapi.exceptions.DuplicateAttachmentException import net.corda.nodeapi.exceptions.DuplicateAttachmentException
import java.io.InputStream import java.io.InputStream
import java.nio.file.FileAlreadyExistsException
import java.util.stream.Stream import java.util.stream.Stream
interface AttachmentStorageInternal : AttachmentStorage { interface AttachmentStorageInternal : AttachmentStorage {
/** /**
* This is the same as [importAttachment] expect there are no checks done on the uploader field. This API is internal * 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. * 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] * 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], * 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. * The [Stream] must be closed once used.
*/ */
fun getAllAttachmentsByCriteria(criteria: AttachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria()): Stream<Pair<String?, Attachment>> fun getAllAttachmentsByCriteria(
criteria: AttachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria()
): Stream<Pair<String?, Attachment>> {
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 {}
}
} }

View File

@ -32,11 +32,11 @@ import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.OpenFuture
import net.corda.core.internal.isIdempotentFlow import net.corda.core.internal.isIdempotentFlow
import net.corda.core.internal.location 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.ComponentTelemetryIds
import net.corda.core.internal.telemetry.SerializedTelemetry import net.corda.core.internal.telemetry.SerializedTelemetry
import net.corda.core.internal.telemetry.telemetryServiceInternal 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.SerializationDefaults
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.internal.CheckpointSerializationContext 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.Try
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.core.utilities.trace 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.FlowAppAuditEvent
import net.corda.node.services.api.FlowPermissionAuditEvent import net.corda.node.services.api.FlowPermissionAuditEvent
import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.ServiceHubInternal
@ -347,7 +346,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
// This sets the Cordapp classloader on the contextClassLoader of the current thread. // 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. // 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 // context.serializedTelemetry is from an rpc client, serializedTelemetry is from a peer, otherwise nothing
val serializedTelemetrySrc = context.serializedTelemetry ?: serializedTelemetry val serializedTelemetrySrc = context.serializedTelemetry ?: serializedTelemetry

View File

@ -2,38 +2,42 @@ package net.corda.node.internal.cordapp
import com.typesafe.config.Config import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import net.corda.core.internal.hash
import net.corda.core.internal.toPath import net.corda.core.internal.toPath
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage import net.corda.core.utilities.OpaqueBytes
import net.corda.node.VersionInfo import net.corda.finance.DOLLARS
import net.corda.testing.core.internal.ContractJarTestUtils import net.corda.finance.contracts.asset.Cash
import net.corda.testing.core.internal.SelfCleaningDir 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.internal.MockCordappConfigProvider
import net.corda.testing.services.MockAttachmentStorage import net.corda.testing.services.MockAttachmentStorage
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.util.jar.JarOutputStream import java.util.jar.JarOutputStream
import java.util.zip.Deflater.NO_COMPRESSION import java.util.zip.Deflater.NO_COMPRESSION
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipEntry.DEFLATED import java.util.zip.ZipEntry.DEFLATED
import java.util.zip.ZipEntry.STORED import java.util.zip.ZipEntry.STORED
import kotlin.io.path.copyTo
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
class CordappProviderImplTests { class CordappProviderImplTests {
private companion object { private companion object {
val isolatedJAR = this::class.java.getResource("/isolated.jar")!!.toPath() val financeContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath()
// TODO: Cordapp name should differ from the JAR name val financeWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath()
const val isolatedCordappName = "isolated"
val emptyJAR = this::class.java.getResource("empty.jar")!!.toPath()
val validConfig: Config = ConfigFactory.parseString("key=value")
@JvmField @JvmField
val ID1 = AttachmentId.randomSHA256() 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 @Before
fun setup() { fun setup() {
attachmentStore = MockAttachmentStorage() attachmentStore = MockAttachmentStorage().toInternal()
}
@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!!))
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `empty jar is not loaded into the attachment store`() { fun `empty jar is not loaded into the attachment store`() {
val provider = newCordappProvider(emptyJAR) val provider = newCordappProvider(setOf(Companion::class.java.getResource("empty.jar")!!.toPath()))
assertNull(provider.getCordappAttachmentId(provider.cordapps.first())) assertThat(attachmentStore.openAttachment(provider.cordapps.single().jarHash)).isNull()
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `test that we find a cordapp class that is loaded into the store`() { fun `test that we find a cordapp class that is loaded into the store`() {
val provider = newCordappProvider(isolatedJAR) val provider = newCordappProvider(setOf(financeContractsJar))
val className = "net.corda.isolated.contracts.AnotherDummyContract"
val expected = provider.cordapps.first() val expected = provider.cordapps.first()
val actual = provider.getCordappForClass(className) val actual = provider.getCordappForClass(Cash::class.java.name)
assertNotNull(actual) assertNotNull(actual)
assertEquals(expected, actual) assertEquals(expected, actual)
@ -96,33 +94,49 @@ class CordappProviderImplTests {
@Test(timeout=300_000) @Test(timeout=300_000)
fun `test that we find an attachment for a cordapp contract class`() { fun `test that we find an attachment for a cordapp contract class`() {
val provider = newCordappProvider(isolatedJAR) val provider = newCordappProvider(setOf(financeContractsJar))
val className = "net.corda.isolated.contracts.AnotherDummyContract"
val expected = provider.getAppContext(provider.cordapps.first()).attachmentId val expected = provider.getAppContext(provider.cordapps.first()).attachmentId
val actual = provider.getContractAttachmentID(className) val actual = provider.getContractAttachmentID(Cash::class.java.name)
assertNotNull(actual) assertNotNull(actual)
assertEquals(actual!!, expected) assertEquals(actual!!, expected)
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `test cordapp configuration`() { fun `test cordapp configuration`() {
val configProvider = MockCordappConfigProvider() val configProvider = MockCordappConfigProvider()
configProvider.cordappConfigs[isolatedCordappName] = validConfig configProvider.cordappConfigs["corda-finance-contracts"] = ConfigFactory.parseString("key=value")
val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR), VersionInfo.UNKNOWN) val provider = newCordappProvider(setOf(financeContractsJar), cordappConfigProvider = configProvider)
val provider = CordappProviderImpl(loader, configProvider, attachmentStore).apply { start() }
val expected = provider.getAppContext(provider.cordapps.first()).config val expected = provider.getAppContext(provider.cordapps.first()).config
assertThat(expected.getString("key")).isEqualTo("value") 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<MultipleCordappsForFlowException> {
newCordappProvider(setOf(financeWorkflowsJar, unsignedJar))
}
}
@Test(timeout=300_000) @Test(timeout=300_000)
fun `test fixup rule that adds attachment`() { fun `test fixup rule that adds attachment`() {
val fixupJar = File.createTempFile("fixup", ".jar") val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules("$ID1 => $ID2, $ID3") .writeFixupRules("$ID1 => $ID2, $ID3")
val fixedIDs = with(newCordappProvider(fixupJar.toPath())) { val fixedIDs = with(newCordappProvider(setOf(fixupJar.toPath()))) {
start()
attachmentFixups.fixupAttachmentIds(listOf(ID1)) attachmentFixups.fixupAttachmentIds(listOf(ID1))
} }
assertThat(fixedIDs).containsExactly(ID2, ID3) assertThat(fixedIDs).containsExactly(ID2, ID3)
@ -132,8 +146,7 @@ class CordappProviderImplTests {
fun `test fixup rule that deletes attachment`() { fun `test fixup rule that deletes attachment`() {
val fixupJar = File.createTempFile("fixup", ".jar") val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules("$ID1 =>") .writeFixupRules("$ID1 =>")
val fixedIDs = with(newCordappProvider(fixupJar.toPath())) { val fixedIDs = with(newCordappProvider(setOf(fixupJar.toPath()))) {
start()
attachmentFixups.fixupAttachmentIds(listOf(ID1)) attachmentFixups.fixupAttachmentIds(listOf(ID1))
} }
assertThat(fixedIDs).isEmpty() assertThat(fixedIDs).isEmpty()
@ -144,7 +157,7 @@ class CordappProviderImplTests {
val fixupJar = File.createTempFile("fixup", ".jar") val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules(" => $ID2") .writeFixupRules(" => $ID2")
val ex = assertFailsWith<IllegalArgumentException> { val ex = assertFailsWith<IllegalArgumentException> {
newCordappProvider(fixupJar.toPath()).start() newCordappProvider(setOf(fixupJar.toPath()))
} }
assertThat(ex).hasMessageContaining( assertThat(ex).hasMessageContaining(
"Forbidden empty list of source attachment IDs in '${fixupJar.absolutePath}'" "Forbidden empty list of source attachment IDs in '${fixupJar.absolutePath}'"
@ -157,7 +170,7 @@ class CordappProviderImplTests {
val fixupJar = File.createTempFile("fixup", ".jar") val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules(rule) .writeFixupRules(rule)
val ex = assertFailsWith<IllegalArgumentException> { val ex = assertFailsWith<IllegalArgumentException> {
newCordappProvider(fixupJar.toPath()).start() newCordappProvider(setOf(fixupJar.toPath()))
} }
assertThat(ex).hasMessageContaining( assertThat(ex).hasMessageContaining(
"Invalid fix-up line '${rule.trim()}' in '${fixupJar.absolutePath}'" "Invalid fix-up line '${rule.trim()}' in '${fixupJar.absolutePath}'"
@ -170,7 +183,7 @@ class CordappProviderImplTests {
val fixupJar = File.createTempFile("fixup", ".jar") val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules(rule) .writeFixupRules(rule)
val ex = assertFailsWith<IllegalArgumentException> { val ex = assertFailsWith<IllegalArgumentException> {
newCordappProvider(fixupJar.toPath()).start() newCordappProvider(setOf(fixupJar.toPath()))
} }
assertThat(ex).hasMessageContaining( assertThat(ex).hasMessageContaining(
"Invalid fix-up line '${rule.trim()}' in '${fixupJar.absolutePath}'" "Invalid fix-up line '${rule.trim()}' in '${fixupJar.absolutePath}'"
@ -186,44 +199,12 @@ class CordappProviderImplTests {
"", "",
"$ID3 => $ID4" "$ID3 => $ID4"
) )
val fixedIDs = with(newCordappProvider(fixupJar.toPath())) { val fixedIDs = with(newCordappProvider(setOf(fixupJar.toPath()))) {
start()
attachmentFixups.fixupAttachmentIds(listOf(ID2, ID1)) attachmentFixups.fixupAttachmentIds(listOf(ID2, ID1))
} }
assertThat(fixedIDs).containsExactlyInAnyOrder(ID2, ID4) 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<DuplicateCordappsInstalledException> {
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<IllegalStateException> {
CordappProviderImpl(it, stubConfigProvider, attachmentStore).apply { start() }
}
}
}
}
private fun File.writeFixupRules(vararg lines: String): File { private fun File.writeFixupRules(vararg lines: String): File {
JarOutputStream(FileOutputStream(this)).use { jar -> JarOutputStream(FileOutputStream(this)).use { jar ->
jar.setMethod(DEFLATED) jar.setMethod(DEFLATED)
@ -239,8 +220,8 @@ class CordappProviderImplTests {
return this return this
} }
private fun newCordappProvider(vararg paths: Path): CordappProviderImpl { private fun newCordappProvider(cordappJars: Set<Path>, cordappConfigProvider: CordappConfigProvider = stubConfigProvider): CordappProviderImpl {
val loader = JarScanningCordappLoader.fromJarUrls(paths.toSet(), VersionInfo.UNKNOWN) val loader = JarScanningCordappLoader(cordappJars)
return CordappProviderImpl(loader, stubConfigProvider, attachmentStore).apply { start() } return CordappProviderImpl(loader, cordappConfigProvider, attachmentStore).apply { start() }
} }
} }

View File

@ -1,6 +1,7 @@
package net.corda.node.internal.cordapp package net.corda.node.internal.cordapp
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.cordapp.Cordapp
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy 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.flows.StartableByRPC
import net.corda.core.internal.packageName_ import net.corda.core.internal.packageName_
import net.corda.core.internal.toPath 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.node.VersionInfo
import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES 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 net.corda.testing.node.internal.cordappWithPackages
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType 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.Test
import org.junit.rules.TemporaryFolder
import java.nio.file.Path
import java.nio.file.Paths 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 @InitiatingFlow
class DummyFlow : FlowLogic<Unit>() { class DummyFlow : FlowLogic<Unit>() {
@ -43,10 +69,18 @@ class DummyRPCFlow : FlowLogic<Unit>() {
class JarScanningCordappLoaderTest { class JarScanningCordappLoaderTest {
private companion object { private companion object {
const val isolatedContractId = "net.corda.isolated.contracts.AnotherDummyContract" val financeContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath()
const val isolatedFlowName = "net.corda.isolated.workflows.IsolatedIssuanceFlow" 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) @Test(timeout=300_000)
fun `classes that aren't in cordapps aren't loaded`() { 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 // 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) @Test(timeout=300_000)
fun `isolated JAR contains a CorDapp with a contract and plugin`() { fun `constructed CordappImpls contains the right classes`() {
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!!.toPath() val loader = JarScanningCordappLoader(setOf(financeContractsJar, financeWorkflowsJar))
val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR)) 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(workflowsCordapp.allFlows).contains(CashIssueFlow::class.java, CashPaymentFlow::class.java)
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId)) assertThat(workflowsCordapp.services).contains(ConfigHolder::class.java)
assertThat(actualCordapp.initiatedFlows).isEmpty() assertThat(workflowsCordapp.info).isInstanceOf(Cordapp.Info.Workflow::class.java)
assertThat(actualCordapp.rpcFlows.first().name).isEqualTo(isolatedFlowName) assertThat(workflowsCordapp.contractClassNames).isEmpty()
assertThat(actualCordapp.schedulableFlows).isEmpty() assertThat(workflowsCordapp.jarFile).isEqualTo(financeWorkflowsJar)
assertThat(actualCordapp.services).isEmpty()
assertThat(actualCordapp.serializationWhitelists).hasSize(1) for (actualCordapp in loader.cordapps) {
assertThat(actualCordapp.serializationWhitelists.first().javaClass.name).isEqualTo("net.corda.serialization.internal.DefaultWhitelist") assertThat(actualCordapp.cordappClasses)
assertThat(actualCordapp.jarFile).isEqualTo(isolatedJAR) .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) @Test(timeout=300_000)
fun `constructed CordappImpl contains the right cordapp classes`() { fun `flows are loaded by loader`() {
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`() {
val jarFile = cordappWithPackages(javaClass.packageName_).jarFile 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. // One cordapp from this source tree. In gradle it will also pick up the node jar.
assertThat(loader.cordapps).isNotEmpty 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 // 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. // being used internally. Later iterations will use a classloader per cordapp and this test can be retired.
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader can load cordapp classes`() { fun `cordapp classloader can load cordapp classes`() {
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!!.toPath() val testJar = this::class.java.getResource("/testing-cashobservers-cordapp.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR), VersionInfo.UNKNOWN) val loader = JarScanningCordappLoader(setOf(testJar))
loader.appClassLoader.loadClass(isolatedContractId) loader.appClassLoader.loadClass("net.corda.finance.test.flows.CashIssueWithObserversFlow")
loader.appClassLoader.loadClass(isolatedFlowName)
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader sets target and min version to 1 if not specified`() { fun `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(setOf(minAndTargetCordapp(minVersion = null, targetVersion = null)))
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN)
loader.cordapps.forEach { loader.cordapps.forEach {
assertThat(it.targetPlatformVersion).isEqualTo(1) assertThat(it.targetPlatformVersion).isEqualTo(1)
assertThat(it.minimumPlatformVersion).isEqualTo(1) assertThat(it.minimumPlatformVersion).isEqualTo(1)
@ -120,21 +155,16 @@ class JarScanningCordappLoaderTest {
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader returns correct values for minPlatformVersion and targetVersion`() { fun `returns correct values for minPlatformVersion and targetVersion`() {
// load jar with min and target version in manifest val loader = JarScanningCordappLoader(setOf(minAndTargetCordapp(minVersion = 2, targetVersion = 3)))
// 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)
val cordapp = loader.cordapps.first() val cordapp = loader.cordapps.first()
assertThat(cordapp.targetPlatformVersion).isEqualTo(3) assertThat(cordapp.targetPlatformVersion).isEqualTo(3)
assertThat(cordapp.minimumPlatformVersion).isEqualTo(2) assertThat(cordapp.minimumPlatformVersion).isEqualTo(2)
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader sets target version to min version if target version is not specified`() { fun `sets target version to min version if target version is not specified`() {
// load jar with minVersion but not targetVersion in manifest val loader = JarScanningCordappLoader(setOf(minAndTargetCordapp(minVersion = 2, targetVersion = null)))
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN)
// exclude the core cordapp // exclude the core cordapp
val cordapp = loader.cordapps.first() val cordapp = loader.cordapps.first()
assertThat(cordapp.targetPlatformVersion).isEqualTo(2) assertThat(cordapp.targetPlatformVersion).isEqualTo(2)
@ -142,48 +172,99 @@ class JarScanningCordappLoaderTest {
} }
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `cordapp classloader does not load apps when their min platform version is greater than the node platform version`() { fun `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 jar = minAndTargetCordapp(minVersion = 2, targetVersion = null)
val cordappLoader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1)) val cordappLoader = JarScanningCordappLoader(setOf(jar), versionInfo = VersionInfo.UNKNOWN.copy(platformVersion = 1))
assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy { assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy {
cordappLoader.cordapps cordappLoader.cordapps
} }
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader does load apps when their min platform version is less than the platform version`() { fun `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 jar = minAndTargetCordapp(minVersion = 2, targetVersion = 3)
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1000)) val loader = JarScanningCordappLoader(setOf(jar), versionInfo = VersionInfo.UNKNOWN.copy(platformVersion = 1000))
assertThat(loader.cordapps).hasSize(1) assertThat(loader.cordapps).hasSize(1)
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader does load apps when their min platform version is equal to the platform version`() { fun `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 jar = minAndTargetCordapp(minVersion = 2, targetVersion = 3)
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2)) val loader = JarScanningCordappLoader(setOf(jar), versionInfo = VersionInfo.UNKNOWN.copy(platformVersion = 2))
assertThat(loader.cordapps).hasSize(1) assertThat(loader.cordapps).hasSize(1)
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader loads app signed by allowed certificate`() { fun `loads app signed by allowed certificate`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!.toPath() val loader = JarScanningCordappLoader(setOf(financeContractsJar), signerKeyFingerprintBlacklist = emptyList())
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = emptyList())
assertThat(loader.cordapps).hasSize(1) assertThat(loader.cordapps).hasSize(1)
} }
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `cordapp classloader does not load app signed by blacklisted certificate`() { fun `does not load app signed by blacklisted certificate`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!.toPath() val cordappLoader = JarScanningCordappLoader(setOf(financeContractsJar), signerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
val cordappLoader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy { assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy {
cordappLoader.cordapps cordappLoader.cordapps
} }
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader loads app signed by both allowed and non-blacklisted certificate`() { fun `does not load duplicate CorDapps`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-two-keys.jar")!!.toPath() val duplicateJar = financeWorkflowsJar.duplicate()
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES) val loader = JarScanningCordappLoader(setOf(financeWorkflowsJar, duplicateJar))
assertFailsWith<DuplicateCordappsInstalledException> {
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) 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)
}
}
} }

Binary file not shown.

View File

@ -12,17 +12,18 @@ import com.esotericsoftware.kryo.util.MapReferenceResolver
import net.corda.core.contracts.TransactionVerificationException.UntrustedAttachmentsException import net.corda.core.contracts.TransactionVerificationException.UntrustedAttachmentsException
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.internal.AttachmentsClassLoader import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.core.serialization.internal.CheckpointSerializationContext import net.corda.core.serialization.internal.CheckpointSerializationContext
import net.corda.coretesting.internal.rigorousMock import net.corda.coretesting.internal.rigorousMock
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
import net.corda.node.services.persistence.toInternal
import net.corda.nodeapi.internal.serialization.kryo.CordaClassResolver import net.corda.nodeapi.internal.serialization.kryo.CordaClassResolver
import net.corda.nodeapi.internal.serialization.kryo.CordaKryo import net.corda.nodeapi.internal.serialization.kryo.CordaKryo
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.services.InternalMockAttachmentStorage
import net.corda.testing.services.MockAttachmentStorage import net.corda.testing.services.MockAttachmentStorage
import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.Test import org.junit.Test
@ -223,14 +224,21 @@ class CordaClassResolverTests {
} }
} }
private fun importJar(storage: AttachmentStorage, uploader: String = DEPLOYED_CORDAPP_UPLOADER) = ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it, uploader, "") } private fun importJar(storage: AttachmentStorage, uploader: String = DEPLOYED_CORDAPP_UPLOADER): AttachmentId {
return ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it, uploader, "") }
}
@Test(timeout=300_000) @Test(timeout=300_000)
fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() { fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() {
val storage = InternalMockAttachmentStorage(MockAttachmentStorage()) val storage = MockAttachmentStorage().toInternal()
val attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage, TestingNamedCacheFactory()) val attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage, TestingNamedCacheFactory())
val attachmentHash = importJar(storage) val attachmentHash = importJar(storage)
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }, testNetworkParameters(), SecureHash.zeroHash, { attachmentTrustCalculator.calculate(it) }) val classLoader = AttachmentsClassLoader(
arrayOf(attachmentHash).map { storage.openAttachment(it)!! },
testNetworkParameters(),
SecureHash.zeroHash,
{ attachmentTrustCalculator.calculate(it) }
)
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader) val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
assertThatExceptionOfType(KryoException::class.java).isThrownBy { assertThatExceptionOfType(KryoException::class.java).isThrownBy {
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass) CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
@ -239,7 +247,7 @@ class CordaClassResolverTests {
@Test(timeout=300_000) @Test(timeout=300_000)
fun `Attempt to load contract attachment with untrusted uploader should fail with UntrustedAttachmentsException`() { fun `Attempt to load contract attachment with untrusted uploader should fail with UntrustedAttachmentsException`() {
val storage = InternalMockAttachmentStorage(MockAttachmentStorage()) val storage = MockAttachmentStorage().toInternal()
val attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage, TestingNamedCacheFactory()) val attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage, TestingNamedCacheFactory())
val attachmentHash = importJar(storage, "some_uploader") val attachmentHash = importJar(storage, "some_uploader")
assertThatExceptionOfType(UntrustedAttachmentsException::class.java).isThrownBy { assertThatExceptionOfType(UntrustedAttachmentsException::class.java).isThrownBy {

View File

@ -5,10 +5,18 @@ import net.corda.coretesting.internal.stubs.CertificateStoreStubs
import net.corda.nodeapi.internal.config.MutualSslConfiguration import net.corda.nodeapi.internal.config.MutualSslConfiguration
import net.corda.nodeapi.internal.loadDevCaTrustStore import net.corda.nodeapi.internal.loadDevCaTrustStore
import net.corda.nodeapi.internal.registerDevP2pCertificates import net.corda.nodeapi.internal.registerDevP2pCertificates
import java.nio.file.FileSystem
import java.nio.file.FileSystems
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path
import java.util.jar.Attributes
import java.util.jar.JarOutputStream
import java.util.jar.Manifest
import kotlin.io.path.fileSize
import kotlin.io.path.inputStream
import kotlin.io.path.outputStream
fun configureTestSSL(legalName: CordaX500Name): MutualSslConfiguration { fun configureTestSSL(legalName: CordaX500Name): MutualSslConfiguration {
val certificatesDirectory = Files.createTempDirectory("certs") val certificatesDirectory = Files.createTempDirectory("certs")
val config = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory) val config = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)
if (config.trustStore.getOptional() == null) { if (config.trustStore.getOptional() == null) {
@ -19,3 +27,23 @@ fun configureTestSSL(legalName: CordaX500Name): MutualSslConfiguration {
} }
return config return config
} }
inline fun <T> 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 <T> 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?

View File

@ -3,11 +3,12 @@ package net.corda.testing.core.internal
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.JarSignatureCollector import net.corda.core.internal.JarSignatureCollector
import net.corda.core.internal.deleteRecursively 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 net.corda.nodeapi.internal.crypto.loadKeyStore
import java.io.Closeable import java.io.Closeable
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.nio.file.FileSystems
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.NoSuchFileException import java.nio.file.NoSuchFileException
import java.nio.file.Path import java.nio.file.Path
@ -20,9 +21,7 @@ import java.util.jar.JarOutputStream
import java.util.jar.Manifest import java.util.jar.Manifest
import kotlin.io.path.deleteExisting import kotlin.io.path.deleteExisting
import kotlin.io.path.div import kotlin.io.path.div
import kotlin.io.path.inputStream
import kotlin.io.path.listDirectoryEntries import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.outputStream
import kotlin.test.assertEquals import kotlin.test.assertEquals
/** /**
@ -74,12 +73,13 @@ object JarSignatureTestUtils {
} }
fun Path.unsignJar() { 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) 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) // Remove all the hash information of the jar contents
manifest.entries.clear() // Remove all the hash information of the jar contents modifyJarManifest { manifest ->
manifestFile.outputStream().use(manifest::write) manifest.entries.clear()
} }
} }

View File

@ -67,9 +67,11 @@ import net.corda.node.services.identity.PersistentIdentityService
import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.keys.BasicHSMKeyManagementService
import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.network.PersistentNetworkMapCache
import net.corda.node.services.persistence.PublicKeyToOwningIdentityCacheImpl 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.schema.NodeSchemaService
import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.NodeVaultService
import net.corda.nodeapi.internal.cordapp.CordappLoader 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.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.contextTransaction 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.MockCordappProvider
import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.configureDatabase 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.MockCryptoService
import net.corda.testing.node.internal.MockKeyManagementService import net.corda.testing.node.internal.MockKeyManagementService
import net.corda.testing.node.internal.MockNetworkParametersStorage import net.corda.testing.node.internal.MockNetworkParametersStorage
@ -128,7 +129,7 @@ open class MockServices private constructor(
) : ServiceHub { ) : ServiceHub {
companion object { companion object {
private fun cordappLoaderForPackages(packages: Iterable<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader { private fun cordappLoaderForPackages(packages: Iterable<String>, 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() { get() {
return NodeInfo(listOf(NetworkHostAndPort("mock.node.services", 10000)), listOf(initialIdentity.identity), 1, serial = 1L) 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() it.start()
} }
override val cordappProvider: CordappProvider get() = mockCordappProvider 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 { private class VerifyingView(private val mockServices: MockServices) : VerifyingServiceHub, ServiceHub by mockServices {
override val attachmentTrustCalculator = NodeAttachmentTrustCalculator( override val attachmentTrustCalculator = NodeAttachmentTrustCalculator(
attachmentStorage = InternalMockAttachmentStorage(mockServices.attachments), attachmentStorage = mockServices.attachments.toInternal(),
cacheFactory = TestingNamedCacheFactory() cacheFactory = TestingNamedCacheFactory()
) )
@ -577,7 +578,7 @@ open class MockServices private constructor(
override fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>> = mockServices.loadStates(stateRefs) override fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>> = mockServices.loadStates(stateRefs)
override val externalVerifierHandle: ExternalVerifierHandle 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.")
} }

View File

@ -27,11 +27,10 @@ import net.corda.core.transactions.WireTransaction
import net.corda.node.services.DbTransactionsResolver import net.corda.node.services.DbTransactionsResolver
import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.api.WritableTransactionStorage
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator 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.core.dummyCommand
import net.corda.testing.internal.MockCordappProvider import net.corda.testing.internal.MockCordappProvider
import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.services.InternalMockAttachmentStorage
import net.corda.testing.services.MockAttachmentStorage import net.corda.testing.services.MockAttachmentStorage
import java.io.InputStream import java.io.InputStream
import java.security.PublicKey import java.security.PublicKey
@ -113,14 +112,7 @@ data class TestTransactionDSLInterpreter private constructor(
ledgerInterpreter.services.attachments.let { ledgerInterpreter.services.attachments.let {
// Wrapping to a [InternalMockAttachmentStorage] is needed to prevent leaking internal api // Wrapping to a [InternalMockAttachmentStorage] is needed to prevent leaking internal api
// while still allowing the tests to work // while still allowing the tests to work
NodeAttachmentTrustCalculator( NodeAttachmentTrustCalculator(attachmentStorage = it.toInternal(), cacheFactory = TestingNamedCacheFactory())
attachmentStorage = if (it is MockAttachmentStorage) {
InternalMockAttachmentStorage(it)
} else {
it as AttachmentStorageInternal
},
cacheFactory = TestingNamedCacheFactory()
)
} }
override fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver = override fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver =
@ -129,6 +121,10 @@ data class TestTransactionDSLInterpreter private constructor(
override fun loadState(stateRef: StateRef) = override fun loadState(stateRef: StateRef) =
ledgerInterpreter.resolveStateRef<ContractState>(stateRef) ledgerInterpreter.resolveStateRef<ContractState>(stateRef)
override fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>> {
return ledgerInterpreter.services.loadStates(stateRefs)
}
override val cordappProvider: CordappProviderInternal override val cordappProvider: CordappProviderInternal
get() = ledgerInterpreter.services.cordappProvider as CordappProviderInternal get() = ledgerInterpreter.services.cordappProvider as CordappProviderInternal
@ -141,7 +137,7 @@ data class TestTransactionDSLInterpreter private constructor(
} }
override val externalVerifierHandle: ExternalVerifierHandle 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) {} override fun recordUnnotarisedTransaction(txn: SignedTransaction) {}

View File

@ -5,16 +5,16 @@ import net.corda.core.cordapp.Cordapp
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.node.services.AttachmentId 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.internal.cordapp.CordappProviderImpl
import net.corda.node.services.persistence.AttachmentStorageInternal
import net.corda.nodeapi.internal.cordapp.CordappLoader
import net.corda.testing.services.MockAttachmentStorage import net.corda.testing.services.MockAttachmentStorage
import java.security.PublicKey import java.security.PublicKey
import java.util.jar.Attributes import java.util.jar.Attributes
class MockCordappProvider( class MockCordappProvider(
cordappLoader: CordappLoader, cordappLoader: CordappLoader,
attachmentStorage: AttachmentStorage, attachmentStorage: AttachmentStorageInternal,
cordappConfigProvider: MockCordappConfigProvider = MockCordappConfigProvider() cordappConfigProvider: MockCordappConfigProvider = MockCordappConfigProvider()
) : CordappProviderImpl(cordappLoader, cordappConfigProvider, attachmentStorage) { ) : CordappProviderImpl(cordappLoader, cordappConfigProvider, attachmentStorage) {

View File

@ -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<Pair<String?, Attachment>> {
return queryAttachments(criteria)
.map(this::openAttachment)
.map { null as String? to it!! }
.stream()
}
}