Merge pull request #7672 from corda/shams-tx-builder-cleanup

ENT-11355: Cleanup of TransactionBuilder and CorDapp loading
This commit is contained in:
Adel El-Beik 2024-02-15 17:57:48 +00:00 committed by GitHub
commit c2742ba6a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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.node.services.AttachmentId
import net.corda.node.VersionInfo
import net.corda.node.internal.cordapp.JarScanningCordappLoader
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
@ -130,7 +129,7 @@ class AttachmentFixupsTest {
}
private fun newFixupService(vararg paths: Path): AttachmentFixups {
val loader = JarScanningCordappLoader.fromJarUrls(paths.toSet(), VersionInfo.UNKNOWN)
val loader = JarScanningCordappLoader(paths.toSet())
return AttachmentFixups().apply { load(loader.appClassLoader) }
}
}

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import net.corda.core.crypto.DigestService
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.HashAgility
import net.corda.core.internal.PLATFORM_VERSION
import net.corda.core.internal.RPC_UPLOADER
import net.corda.core.internal.digestService
import net.corda.core.node.ZoneVersionTooLowException
import net.corda.core.serialization.internal._driverSerializationEnv
@ -31,12 +32,14 @@ import net.corda.testing.node.MockServices
import net.corda.testing.node.internal.cordappWithPackages
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Assert.assertTrue
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import java.time.Instant
import kotlin.io.path.inputStream
import kotlin.test.assertFailsWith
class TransactionBuilderTest {
@ -298,4 +301,25 @@ class TransactionBuilderTest {
builder.toWireTransaction(services, schemeId)
}
}
@Test(timeout=300_000)
fun `contract overlap in explicit attachments`() {
val overlappingAttachmentId = cordappWithPackages("net.corda.testing").jarFile.inputStream().use {
services.attachments.importAttachment(it, RPC_UPLOADER, null)
}
val outputState = TransactionState(
data = DummyState(),
contract = DummyContract.PROGRAM_ID,
notary = notary
)
val builder = TransactionBuilder()
.addAttachment(contractAttachmentId)
.addAttachment(overlappingAttachmentId)
.addOutputState(outputState)
.addCommand(DummyCommandData, notary.owningKey)
assertThatIllegalArgumentException()
.isThrownBy { builder.toWireTransaction(services) }
.withMessageContaining("Multiple attachments specified for the same contract net.corda.testing.contracts.DummyContract")
}
}

View File

@ -12,10 +12,7 @@ import net.corda.core.node.ServicesForResolution
import net.corda.core.node.ZoneVersionTooLowException
import net.corda.core.node.services.TransactionStorage
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializationContext
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import org.slf4j.MDC
import java.security.PublicKey
@ -57,11 +54,6 @@ enum class JavaVersion(val versionString: String) {
}
}
/** Provide access to internal method for AttachmentClassLoaderTests. */
fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction {
return toWireTransactionWithContext(services, serializationContext)
}
/** Checks if this flow is an idempotent flow. */
fun Class<out FlowLogic<*>>.isIdempotentFlow(): Boolean {
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)
}
/**
* 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)
/** 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.internal.PLATFORM_VERSION
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.hash
import net.corda.core.internal.notary.NotaryService
import net.corda.core.internal.telemetry.TelemetryComponent
import net.corda.core.schemas.MappedSchema
@ -32,9 +33,9 @@ data class CordappImpl(
override val customSchemas: Set<MappedSchema>,
override val allFlows: List<Class<out FlowLogic<*>>>,
override val info: Cordapp.Info,
override val jarHash: SecureHash.SHA256,
override val minimumPlatformVersion: Int,
override val targetPlatformVersion: Int,
override val jarHash: SecureHash.SHA256 = jarFile.hash,
val notaryService: Class<out NotaryService>? = null,
/** Indicates whether the CorDapp is loaded from external sources, or generated on node startup (virtual). */
val isLoaded: Boolean = true,
@ -53,6 +54,10 @@ data class CordappImpl(
classList.mapNotNull { it?.name } + contractClassNames + explicitCordappClasses
}
override fun equals(other: Any?): Boolean = other is CordappImpl && this.jarHash == other.jarHash
override fun hashCode(): Int = 31 * jarHash.hashCode()
companion object {
fun jarName(url: Path): String = url.name.removeSuffix(".jar")

View File

@ -1,5 +1,7 @@
package net.corda.core.internal.cordapp
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.cordapp.Cordapp
import net.corda.core.cordapp.CordappProvider
import net.corda.core.flows.FlowLogic
@ -10,4 +12,9 @@ interface CordappProviderInternal : CordappProvider {
val attachmentFixups: AttachmentFixups
val cordapps: List<CordappImpl>
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 networkParameters = getNetworkParameters(wtx.networkParametersHash) ?: throw TransactionResolutionException(wtx.id)
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(
return AttachmentsClassLoaderBuilder.withAttachmentsClassLoaderContext(
listOf(legacyContractAttachment, upgradedContractAttachment),
networkParameters,
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.
*/
@Suppress("LongParameterList")
fun <T> withAttachmentsClassloaderContext(attachments: List<Attachment>,
fun <T> withAttachmentsClassLoaderContext(attachments: List<Attachment>,
params: NetworkParameters,
txId: SecureHash,
isAttachmentTrusted: (Attachment) -> Boolean,

View File

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

View File

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

View File

@ -6,7 +6,11 @@ import net.corda.core.contracts.ContractClassName
import net.corda.core.contracts.UpgradedContract
import net.corda.core.contracts.UpgradedContractWithLegacyConstraint
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.*
import net.corda.core.internal.copyTo
import net.corda.core.internal.hash
import net.corda.core.internal.logElapsedTime
import net.corda.core.internal.pooledScan
import net.corda.core.internal.read
import org.slf4j.LoggerFactory
import java.io.InputStream
import java.nio.file.Files
@ -17,7 +21,7 @@ import kotlin.io.path.deleteIfExists
// When scanning of the CorDapp Jar is performed without "corda-core.jar" being in the classpath, there is no way to appreciate
// relationships between those interfaces, therefore they have to be listed explicitly.
val coreContractClasses = setOf(Contract::class, UpgradedContractWithLegacyConstraint::class, UpgradedContract::class)
val coreContractClasses = setOf(Contract::class.java, UpgradedContractWithLegacyConstraint::class.java, UpgradedContract::class.java)
interface ContractsJar {
val hash: SecureHash
@ -32,7 +36,8 @@ class ContractsJarFile(private val file: Path) : ContractsJar {
return scanResult.use { result ->
coreContractClasses
.flatMap { result.getClassesImplementing(it.qualifiedName)}
.asSequence()
.flatMap(result::getClassesImplementing)
.filterNot { it.isAbstract }
.filterNot { it.isInterface }
.map { it.name }

View File

@ -1,15 +1,14 @@
package net.corda.nodeapi.internal.cordapp
import net.corda.core.cordapp.Cordapp
import net.corda.core.flows.FlowLogic
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.flatMapToSet
import net.corda.core.schemas.MappedSchema
/**
* Handles loading [Cordapp]s.
*/
interface CordappLoader : AutoCloseable {
/**
* Returns all [Cordapp]s found.
*/
@ -19,15 +18,10 @@ interface CordappLoader : AutoCloseable {
* Returns a [ClassLoader] containing all types from all [Cordapp]s.
*/
val appClassLoader: ClassLoader
/**
* Returns a map between flow class and owning [Cordapp].
* The mappings are unique, and the node will not start otherwise.
*/
val flowCordappMap: Map<Class<out FlowLogic<*>>, Cordapp>
}
/**
* Returns all [MappedSchema] found inside the [Cordapp]s.
*/
val cordappSchemas: Set<MappedSchema>
}
val CordappLoader.cordappSchemas: Set<MappedSchema>
get() = cordapps.flatMapToSet { it.customSchemas }

View File

@ -80,6 +80,15 @@ processResources {
processTestResources {
from file("$rootDir/config/test/jolokia-access.xml")
from(tasks.getByPath(":finance:contracts:jar")) {
rename 'corda-finance-contracts-.*.jar', 'corda-finance-contracts.jar'
}
from(tasks.getByPath(":finance:workflows:jar")) {
rename 'corda-finance-workflows-.*.jar', 'corda-finance-workflows.jar'
}
from(tasks.getByPath(":testing:cordapps:cashobservers:jar")) {
rename 'testing-cashobservers-cordapp-.*.jar', 'testing-cashobservers-cordapp.jar'
}
}
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in

View File

@ -147,6 +147,7 @@ import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.NodeStatus
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.cordapp.CordappLoader
import net.corda.nodeapi.internal.cordapp.cordappSchemas
import net.corda.nodeapi.internal.cryptoservice.CryptoService
import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService
import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEvent

View File

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

View File

@ -1,7 +1,7 @@
package net.corda.node.internal.cordapp
import io.github.classgraph.ClassGraph
import io.github.classgraph.ClassInfo
import io.github.classgraph.ClassInfoList
import io.github.classgraph.ScanResult
import net.corda.common.logging.errorReporting.CordappErrors
import net.corda.common.logging.errorReporting.ErrorCode
@ -14,17 +14,17 @@ import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.SchedulableFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.flows.StartableByService
import net.corda.core.internal.JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION
import net.corda.core.internal.JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION
import net.corda.core.internal.JarSignatureCollector
import net.corda.core.internal.PlatformVersionSwitches
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.cordapp.CordappImpl.Companion.UNKNOWN_INFO
import net.corda.core.internal.cordapp.get
import net.corda.core.internal.flatMapToSet
import net.corda.core.internal.hash
import net.corda.core.internal.isAbstractClass
import net.corda.core.internal.loadClassOfType
import net.corda.core.internal.location
import net.corda.core.internal.groupByMultipleKeys
import net.corda.core.internal.mapToSet
import net.corda.core.internal.notary.NotaryService
import net.corda.core.internal.notary.SinglePartyNotaryService
@ -41,20 +41,17 @@ import net.corda.core.serialization.SerializationCustomSerializer
import net.corda.core.serialization.SerializationWhitelist
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.node.VersionInfo
import net.corda.nodeapi.internal.cordapp.CordappLoader
import net.corda.nodeapi.internal.coreContractClasses
import net.corda.serialization.internal.DefaultWhitelist
import java.lang.reflect.Modifier
import java.math.BigInteger
import java.net.URLClassLoader
import java.nio.file.Path
import java.util.Random
import java.util.ServiceLoader
import java.util.concurrent.ConcurrentHashMap
import java.util.jar.JarInputStream
import java.util.jar.Manifest
import java.util.zip.ZipInputStream
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists
import kotlin.io.path.inputStream
@ -67,27 +64,11 @@ import kotlin.reflect.KClass
*
* @property cordappJars The classpath of cordapp JARs
*/
class JarScanningCordappLoader private constructor(private val cordappJars: Set<Path>,
@Suppress("TooManyFunctions")
class JarScanningCordappLoader(private val cordappJars: Set<Path>,
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl>,
private val signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()) : CordappLoaderTemplate() {
init {
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()
private val extraCordapps: List<CordappImpl> = emptyList(),
private val signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()) : CordappLoader {
companion object {
private val logger = contextLogger()
@ -100,100 +81,88 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set<
versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl> = emptyList(),
signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader {
logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}")
val paths = cordappDirs
logger.info("Looking for CorDapps in ${cordappDirs.toSet().joinToString(", ", "[", "]")}")
val cordappJars = cordappDirs
.asSequence()
.flatMap { if (it.exists()) it.listDirectoryEntries("*.jar") else emptyList() }
.toSet()
return JarScanningCordappLoader(paths, versionInfo, extraCordapps, signerKeyFingerprintBlacklist)
}
/**
* Creates a CordappLoader loader out of a list of JAR URLs.
*
* @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection.
*/
fun fromJarUrls(scanJars: Set<Path>,
versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl> = emptyList(),
cordappsSignerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader {
return JarScanningCordappLoader(scanJars, versionInfo, extraCordapps, cordappsSignerKeyFingerprintBlacklist)
return JarScanningCordappLoader(cordappJars, versionInfo, extraCordapps, signerKeyFingerprintBlacklist)
}
}
private fun loadCordapps(): List<CordappImpl> {
val invalidCordapps = mutableMapOf<String, Path>()
init {
logger.debug { "cordappJars: $cordappJars" }
}
val cordapps = cordappJars
.map { path -> scanCordapp(path).use { it.toCordapp(path) } }
.filter { cordapp ->
override val appClassLoader = URLClassLoader(cordappJars.stream().map { it.toUri().toURL() }.toTypedArray(), javaClass.classLoader)
private val internal by lazy(::InternalHolder)
override val cordapps: List<CordappImpl>
get() = internal.cordapps
override fun close() = appClassLoader.close()
private inner class InternalHolder {
val cordapps = cordappJars.mapTo(ArrayList(), ::scanCordapp)
init {
checkInvalidCordapps()
checkDuplicateCordapps()
checkContractOverlap()
cordapps += extraCordapps
}
private fun checkInvalidCordapps() {
val invalidCordapps = LinkedHashMap<String, CordappImpl>()
for (cordapp in cordapps) {
if (cordapp.minimumPlatformVersion > versionInfo.platformVersion) {
logger.warn("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it requires minimum " +
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 was: ${versionInfo.platformVersion}"] = cordapp.jarFile
false
} else {
true
invalidCordapps["CorDapp requires minimumPlatformVersion ${cordapp.minimumPlatformVersion}, but this node is running version ${versionInfo.platformVersion}"] = cordapp
}
}.filter { cordapp ->
if (signerKeyFingerprintBlacklist.isEmpty()) {
true //Nothing blacklisted, no need to check
} else {
if (signerKeyFingerprintBlacklist.isNotEmpty()) {
val certificates = cordapp.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates)
val blockedCertificates = certificates.filter { it.publicKey.hash.sha256() in signerKeyFingerprintBlacklist }
if (certificates.isEmpty() || (certificates - blockedCertificates).isNotEmpty()) {
true // Cordapp is not signed or it is signed by at least one non-blacklisted certificate
} else {
logger.warn("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it is signed by blacklisted key(s) only (probably development key): " +
"${blockedCertificates.map { it.publicKey }}.")
invalidCordapps["Corresponding contracts are signed by blacklisted key(s) only (probably development key),"] = cordapp.jarFile
false
val blockedCertificates = certificates.filterTo(HashSet()) { it.publicKey.hash.sha256() in signerKeyFingerprintBlacklist }
if (certificates.isNotEmpty() && (certificates - blockedCertificates).isEmpty()) {
logger.error("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it is signed by blacklisted " +
"key(s) only (probably development key): ${blockedCertificates.map { it.publicKey }}.")
invalidCordapps["Corresponding contracts are signed by blacklisted key(s) only (probably development key),"] = cordapp
}
}
}
if (invalidCordapps.isNotEmpty()) {
throw InvalidCordappException("Invalid Cordapps found, that couldn't be loaded: " +
"${invalidCordapps.map { "Problem: ${it.key} in Cordapp ${it.value}" }}, ")
"${invalidCordapps.map { "Problem: ${it.key} in Cordapp ${it.value.jarFile}" }}, ")
}
}
cordapps.forEach(::register)
return cordapps
private fun checkDuplicateCordapps() {
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.")
}
cordappClasses[registeredClassName] = registeredCordapps + cordapp
}
}
private fun RestrictedScanResult.toCordapp(path: Path): CordappImpl {
private fun ScanResult.toCordapp(path: Path): CordappImpl {
val manifest: Manifest? = JarInputStream(path.inputStream()).use { it.manifest }
val info = parseCordappInfo(manifest, CordappImpl.jarName(path))
val minPlatformVersion = manifest?.get(CordappImpl.MIN_PLATFORM_VERSION)?.toIntOrNull() ?: 1
val targetPlatformVersion = manifest?.get(CordappImpl.TARGET_PLATFORM_VERSION)?.toIntOrNull() ?: minPlatformVersion
validateContractStateClassVersion(this)
validateWhitelistClassVersion(this)
return CordappImpl(
path,
findContractClassNamesWithVersionCheck(this),
findContractClassNames(this),
findInitiatedFlows(this),
findRPCFlows(this),
findServiceFlows(this),
@ -206,10 +175,9 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set<
findCustomSchemas(this),
findAllFlows(this),
info,
path.hash,
minPlatformVersion,
targetPlatformVersion,
findNotaryService(this),
notaryService = findNotaryService(this),
explicitCordappClasses = findAllCordappClasses(this)
)
}
@ -278,27 +246,27 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set<
return version
}
private fun findNotaryService(scanResult: RestrictedScanResult): Class<out NotaryService>? {
private fun findNotaryService(scanResult: ScanResult): Class<out NotaryService>? {
// Note: we search for implementations of both NotaryService and SinglePartyNotaryService as
// the scanner won't find subclasses deeper down the hierarchy if any intermediate class is not
// present in the CorDapp.
val result = scanResult.getClassesWithSuperclass(NotaryService::class) +
scanResult.getClassesWithSuperclass(SinglePartyNotaryService::class)
val result = scanResult.getClassesExtending(NotaryService::class) +
scanResult.getClassesExtending(SinglePartyNotaryService::class)
if (result.isNotEmpty()) {
logger.info("Found notary service CorDapp implementations: " + result.joinToString(", "))
}
return result.firstOrNull()
}
private fun findServices(scanResult: RestrictedScanResult): List<Class<out SerializeAsToken>> {
private fun findServices(scanResult: ScanResult): List<Class<out SerializeAsToken>> {
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)
}
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)
}
@ -306,40 +274,35 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set<
return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers))
}
private fun findRPCFlows(scanResult: RestrictedScanResult): List<Class<out FlowLogic<*>>> {
private fun findRPCFlows(scanResult: ScanResult): List<Class<out FlowLogic<*>>> {
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)
}
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)
}
private fun findAllFlows(scanResult: RestrictedScanResult): List<Class<out FlowLogic<*>>> {
return scanResult.getConcreteClassesOfType(FlowLogic::class)
private fun findAllFlows(scanResult: ScanResult): List<Class<out FlowLogic<*>>> {
return scanResult.getClassesExtending(FlowLogic::class)
}
private fun findAllCordappClasses(scanResult: RestrictedScanResult): List<String> {
return scanResult.getAllStandardClasses() + scanResult.getAllInterfaces()
private fun findAllCordappClasses(scanResult: ScanResult): List<String> {
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> {
val contractClasses = coreContractClasses.flatMapTo(LinkedHashSet()) { scanResult.getNamesOfClassesImplementingWithClassVersionCheck(it) }.toList()
private fun findContractClassNames(scanResult: ScanResult): List<String> {
val contractClasses = coreContractClasses.flatMapToSet(scanResult::getClassesImplementing)
for (contractClass in contractClasses) {
contractClass.warnContractWithoutConstraintPropagation(appClassLoader)
contractClass.name.warnContractWithoutConstraintPropagation(appClassLoader)
}
return contractClasses
}
private fun validateContractStateClassVersion(scanResult: RestrictedScanResult) {
coreContractClasses.forEach { scanResult.versionCheckClassesImplementing(it) }
}
private fun validateWhitelistClassVersion(scanResult: RestrictedScanResult) {
scanResult.versionCheckClassesImplementing(SerializationWhitelist::class)
return contractClasses.map { it.name }
}
private fun findWhitelists(cordappJar: Path): List<SerializationWhitelist> {
@ -349,27 +312,25 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set<
} + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app.
}
private fun findSerializers(scanResult: RestrictedScanResult): List<SerializationCustomSerializer<*, *>> {
return scanResult.getClassesImplementingWithClassVersionCheck(SerializationCustomSerializer::class)
private fun findSerializers(scanResult: ScanResult): List<SerializationCustomSerializer<*, *>> {
return scanResult.getClassesImplementing(SerializationCustomSerializer::class).map { it.kotlin.objectOrNewInstance() }
}
private fun findCheckpointSerializers(scanResult: RestrictedScanResult): List<CheckpointCustomSerializer<*, *>> {
return scanResult.getClassesImplementingWithClassVersionCheck(CheckpointCustomSerializer::class)
private fun findCheckpointSerializers(scanResult: ScanResult): List<CheckpointCustomSerializer<*, *>> {
return scanResult.getClassesImplementing(CheckpointCustomSerializer::class).map { it.kotlin.objectOrNewInstance() }
}
private fun findCustomSchemas(scanResult: RestrictedScanResult): Set<MappedSchema> {
return scanResult.getClassesWithSuperclass(MappedSchema::class).mapToSet { it.kotlin.objectOrNewInstance() }
private fun findCustomSchemas(scanResult: ScanResult): Set<MappedSchema> {
return scanResult.getClassesExtending(MappedSchema::class).mapToSet { it.kotlin.objectOrNewInstance() }
}
private fun scanCordapp(cordappJar: Path): RestrictedScanResult {
private fun scanCordapp(cordappJar: Path): CordappImpl {
logger.info("Scanning CorDapp ${cordappJar.absolutePathString()}")
val scanResult = ClassGraph()
.filterClasspathElementsByURL { it.toPath().isSameFileAs(cordappJar) }
.overrideClassLoaders(appClassLoader)
.ignoreParentClassLoaders()
return ClassGraph()
.overrideClasspath(cordappJar.absolutePathString())
.enableAllInfo()
.pooledScan()
return RestrictedScanResult(scanResult, cordappJar)
.use { it.toCordapp(cordappJar) }
}
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 {
fun getNamesOfClassesImplementingWithClassVersionCheck(type: KClass<*>): List<String> {
return scanResult.getClassesImplementing(type.java.name).map {
validateClassFileVersion(it)
it.name
}
private fun <T : Any> ScanResult.getClassesExtending(type: KClass<T>): List<Class<out T>> {
return getSubclasses(type.java).getAllConcreteClasses(type)
}
fun versionCheckClassesImplementing(type: KClass<*>) {
return scanResult.getClassesImplementing(type.java.name).forEach {
validateClassFileVersion(it)
}
private fun <T : Any> ScanResult.getClassesImplementing(type: KClass<T>): List<Class<out T>> {
return getClassesImplementing(type.java).getAllConcreteClasses(type)
}
fun <T : Any> getClassesWithSuperclass(type: KClass<T>): List<Class<out T>> {
return scanResult
.getSubclasses(type.java.name)
.names
.mapNotNull { loadClass(it, type) }
.filterNot { it.isAbstractClass }
private fun <T : Any> ScanResult.getClassesWithAnnotation(type: KClass<T>, annotation: KClass<out Annotation>): List<Class<out T>> {
return getClassesWithAnnotation(annotation.java).getAllConcreteClasses(type)
}
fun <T : Any> getClassesImplementingWithClassVersionCheck(type: KClass<T>): List<T> {
return scanResult
.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()
private fun <T : Any> ClassInfoList.getAllConcreteClasses(type: KClass<T>): List<Class<out T>> {
return mapNotNull { loadClass(it.name, type)?.takeUnless(Class<*>::isAbstractClass) }
}
}
@ -478,7 +386,7 @@ class CordappInvalidVersionException(
/**
* Thrown if duplicate CorDapps are installed on the node
*/
class DuplicateCordappsInstalledException(app: Cordapp, duplicates: Set<Cordapp>)
class DuplicateCordappsInstalledException(app: Cordapp, duplicates: Collection<Cordapp>)
: CordaRuntimeException("IllegalStateExcepion", "The CorDapp (name: ${app.info.shortName}, file: ${app.name}) " +
"is installed multiple times on the node. The following files correspond to the exact same content: " +
"${duplicates.map { it.name }}", null), ErrorCode<CordappErrors> {
@ -490,40 +398,3 @@ class DuplicateCordappsInstalledException(app: Cordapp, duplicates: Set<Cordapp>
* Thrown if an exception occurs during loading cordapps.
*/
class InvalidCordappException(message: String) : CordaRuntimeException(message)
abstract class CordappLoaderTemplate : CordappLoader {
companion object {
private val logger = contextLogger()
}
override val flowCordappMap: Map<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.nodeapi.exceptions.DuplicateAttachmentException
import java.io.InputStream
import java.nio.file.FileAlreadyExistsException
import java.util.stream.Stream
interface AttachmentStorageInternal : AttachmentStorage {
/**
* This is the same as [importAttachment] expect there are no checks done on the uploader field. This API is internal
* and is only for the node.
*/
fun privilegedImportAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId
fun privilegedImportAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
// Default implementation is not privileged
return importAttachment(jar, uploader, filename)
}
/**
* Similar to above but returns existing [AttachmentId] instead of throwing [DuplicateAttachmentException]
*/
fun privilegedImportOrGetAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId
fun privilegedImportOrGetAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
return try {
privilegedImportAttachment(jar, uploader, filename)
} catch (faee: FileAlreadyExistsException) {
AttachmentId.create(faee.message!!)
}
}
/**
* Get all attachments as a [Stream], filtered by the input [AttachmentQueryCriteria],
@ -27,5 +36,16 @@ interface AttachmentStorageInternal : AttachmentStorage {
*
* The [Stream] must be closed once used.
*/
fun getAllAttachmentsByCriteria(criteria: AttachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria()): Stream<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.isIdempotentFlow
import net.corda.core.internal.location
import net.corda.core.internal.toPath
import net.corda.core.internal.uncheckedCast
import net.corda.core.internal.telemetry.ComponentTelemetryIds
import net.corda.core.internal.telemetry.SerializedTelemetry
import net.corda.core.internal.telemetry.telemetryServiceInternal
import net.corda.core.internal.toPath
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.internal.CheckpointSerializationContext
@ -46,7 +46,6 @@ import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.Try
import net.corda.core.utilities.debug
import net.corda.core.utilities.trace
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.node.services.api.FlowAppAuditEvent
import net.corda.node.services.api.FlowPermissionAuditEvent
import net.corda.node.services.api.ServiceHubInternal
@ -347,7 +346,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
// This sets the Cordapp classloader on the contextClassLoader of the current thread.
// Needed because in previous versions of the finance app we used Thread.contextClassLoader to resolve services defined in cordapps.
Thread.currentThread().contextClassLoader = (serviceHub.cordappProvider as CordappProviderImpl).cordappLoader.appClassLoader
Thread.currentThread().contextClassLoader = serviceHub.cordappProvider.appClassLoader
// context.serializedTelemetry is from an rpc client, serializedTelemetry is from a peer, otherwise nothing
val serializedTelemetrySrc = context.serializedTelemetry ?: serializedTelemetry

View File

@ -2,38 +2,42 @@ package net.corda.node.internal.cordapp
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import net.corda.core.internal.hash
import net.corda.core.internal.toPath
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import net.corda.node.VersionInfo
import net.corda.testing.core.internal.ContractJarTestUtils
import net.corda.testing.core.internal.SelfCleaningDir
import net.corda.core.utilities.OpaqueBytes
import net.corda.finance.DOLLARS
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueFlow
import net.corda.node.services.persistence.AttachmentStorageInternal
import net.corda.node.services.persistence.toInternal
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.TestIdentity
import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar
import net.corda.testing.internal.MockCordappConfigProvider
import net.corda.testing.services.MockAttachmentStorage
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
import java.io.FileOutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.util.jar.JarOutputStream
import java.util.zip.Deflater.NO_COMPRESSION
import java.util.zip.ZipEntry
import java.util.zip.ZipEntry.DEFLATED
import java.util.zip.ZipEntry.STORED
import kotlin.io.path.copyTo
import kotlin.test.assertFailsWith
class CordappProviderImplTests {
private companion object {
val isolatedJAR = this::class.java.getResource("/isolated.jar")!!.toPath()
// TODO: Cordapp name should differ from the JAR name
const val isolatedCordappName = "isolated"
val emptyJAR = this::class.java.getResource("empty.jar")!!.toPath()
val validConfig: Config = ConfigFactory.parseString("key=value")
val financeContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath()
val financeWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath()
@JvmField
val ID1 = AttachmentId.randomSHA256()
@ -60,35 +64,29 @@ class CordappProviderImplTests {
}
}
private lateinit var attachmentStore: AttachmentStorage
@Rule
@JvmField
val tempFolder = TemporaryFolder()
private lateinit var attachmentStore: AttachmentStorageInternal
@Before
fun setup() {
attachmentStore = MockAttachmentStorage()
}
@Test(timeout=300_000)
fun `isolated jar is loaded into the attachment store`() {
val provider = newCordappProvider(isolatedJAR)
val maybeAttachmentId = provider.getCordappAttachmentId(provider.cordapps.first())
assertNotNull(maybeAttachmentId)
assertNotNull(attachmentStore.openAttachment(maybeAttachmentId!!))
attachmentStore = MockAttachmentStorage().toInternal()
}
@Test(timeout=300_000)
fun `empty jar is not loaded into the attachment store`() {
val provider = newCordappProvider(emptyJAR)
assertNull(provider.getCordappAttachmentId(provider.cordapps.first()))
val provider = newCordappProvider(setOf(Companion::class.java.getResource("empty.jar")!!.toPath()))
assertThat(attachmentStore.openAttachment(provider.cordapps.single().jarHash)).isNull()
}
@Test(timeout=300_000)
fun `test that we find a cordapp class that is loaded into the store`() {
val provider = newCordappProvider(isolatedJAR)
val className = "net.corda.isolated.contracts.AnotherDummyContract"
val provider = newCordappProvider(setOf(financeContractsJar))
val expected = provider.cordapps.first()
val actual = provider.getCordappForClass(className)
val actual = provider.getCordappForClass(Cash::class.java.name)
assertNotNull(actual)
assertEquals(expected, actual)
@ -96,10 +94,9 @@ class CordappProviderImplTests {
@Test(timeout=300_000)
fun `test that we find an attachment for a cordapp contract class`() {
val provider = newCordappProvider(isolatedJAR)
val className = "net.corda.isolated.contracts.AnotherDummyContract"
val provider = newCordappProvider(setOf(financeContractsJar))
val expected = provider.getAppContext(provider.cordapps.first()).attachmentId
val actual = provider.getContractAttachmentID(className)
val actual = provider.getContractAttachmentID(Cash::class.java.name)
assertNotNull(actual)
assertEquals(actual!!, expected)
@ -108,21 +105,38 @@ class CordappProviderImplTests {
@Test(timeout=300_000)
fun `test cordapp configuration`() {
val configProvider = MockCordappConfigProvider()
configProvider.cordappConfigs[isolatedCordappName] = validConfig
val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR), VersionInfo.UNKNOWN)
val provider = CordappProviderImpl(loader, configProvider, attachmentStore).apply { start() }
configProvider.cordappConfigs["corda-finance-contracts"] = ConfigFactory.parseString("key=value")
val provider = newCordappProvider(setOf(financeContractsJar), cordappConfigProvider = configProvider)
val expected = provider.getAppContext(provider.cordapps.first()).config
assertThat(expected.getString("key")).isEqualTo("value")
}
@Test(timeout=300_000)
fun getCordappForFlow() {
val provider = newCordappProvider(setOf(financeWorkflowsJar))
val cashIssueFlow = CashIssueFlow(10.DOLLARS, OpaqueBytes.of(0x00), TestIdentity(ALICE_NAME).party)
assertThat(provider.getCordappForFlow(cashIssueFlow)?.jarPath?.toPath()).isEqualTo(financeWorkflowsJar)
}
@Test(timeout=300_000)
fun `does not load the same flow across different CorDapps`() {
val unsignedJar = tempFolder.newFile("duplicate.jar").toPath()
financeWorkflowsJar.copyTo(unsignedJar, overwrite = true)
// We just need to change the file's hash and thus avoid the duplicate CorDapp check
unsignedJar.unsignJar()
assertThat(unsignedJar.hash).isNotEqualTo(financeWorkflowsJar.hash)
assertFailsWith<MultipleCordappsForFlowException> {
newCordappProvider(setOf(financeWorkflowsJar, unsignedJar))
}
}
@Test(timeout=300_000)
fun `test fixup rule that adds attachment`() {
val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules("$ID1 => $ID2, $ID3")
val fixedIDs = with(newCordappProvider(fixupJar.toPath())) {
start()
val fixedIDs = with(newCordappProvider(setOf(fixupJar.toPath()))) {
attachmentFixups.fixupAttachmentIds(listOf(ID1))
}
assertThat(fixedIDs).containsExactly(ID2, ID3)
@ -132,8 +146,7 @@ class CordappProviderImplTests {
fun `test fixup rule that deletes attachment`() {
val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules("$ID1 =>")
val fixedIDs = with(newCordappProvider(fixupJar.toPath())) {
start()
val fixedIDs = with(newCordappProvider(setOf(fixupJar.toPath()))) {
attachmentFixups.fixupAttachmentIds(listOf(ID1))
}
assertThat(fixedIDs).isEmpty()
@ -144,7 +157,7 @@ class CordappProviderImplTests {
val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules(" => $ID2")
val ex = assertFailsWith<IllegalArgumentException> {
newCordappProvider(fixupJar.toPath()).start()
newCordappProvider(setOf(fixupJar.toPath()))
}
assertThat(ex).hasMessageContaining(
"Forbidden empty list of source attachment IDs in '${fixupJar.absolutePath}'"
@ -157,7 +170,7 @@ class CordappProviderImplTests {
val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules(rule)
val ex = assertFailsWith<IllegalArgumentException> {
newCordappProvider(fixupJar.toPath()).start()
newCordappProvider(setOf(fixupJar.toPath()))
}
assertThat(ex).hasMessageContaining(
"Invalid fix-up line '${rule.trim()}' in '${fixupJar.absolutePath}'"
@ -170,7 +183,7 @@ class CordappProviderImplTests {
val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules(rule)
val ex = assertFailsWith<IllegalArgumentException> {
newCordappProvider(fixupJar.toPath()).start()
newCordappProvider(setOf(fixupJar.toPath()))
}
assertThat(ex).hasMessageContaining(
"Invalid fix-up line '${rule.trim()}' in '${fixupJar.absolutePath}'"
@ -186,44 +199,12 @@ class CordappProviderImplTests {
"",
"$ID3 => $ID4"
)
val fixedIDs = with(newCordappProvider(fixupJar.toPath())) {
start()
val fixedIDs = with(newCordappProvider(setOf(fixupJar.toPath()))) {
attachmentFixups.fixupAttachmentIds(listOf(ID2, ID1))
}
assertThat(fixedIDs).containsExactlyInAnyOrder(ID2, ID4)
}
@Test(timeout=300_000)
fun `test an exception is raised when we have two jars with the same hash`() {
SelfCleaningDir().use { file ->
val jarAndSigner = ContractJarTestUtils.makeTestSignedContractJar(file.path, "com.example.MyContract")
val signedJarPath = jarAndSigner.first
val duplicateJarPath = signedJarPath.parent.resolve("duplicate-${signedJarPath.fileName}")
Files.copy(signedJarPath, duplicateJarPath)
val paths = setOf(signedJarPath, duplicateJarPath)
JarScanningCordappLoader.fromJarUrls(paths, VersionInfo.UNKNOWN).use {
assertFailsWith<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 {
JarOutputStream(FileOutputStream(this)).use { jar ->
jar.setMethod(DEFLATED)
@ -239,8 +220,8 @@ class CordappProviderImplTests {
return this
}
private fun newCordappProvider(vararg paths: Path): CordappProviderImpl {
val loader = JarScanningCordappLoader.fromJarUrls(paths.toSet(), VersionInfo.UNKNOWN)
return CordappProviderImpl(loader, stubConfigProvider, attachmentStore).apply { start() }
private fun newCordappProvider(cordappJars: Set<Path>, cordappConfigProvider: CordappConfigProvider = stubConfigProvider): CordappProviderImpl {
val loader = JarScanningCordappLoader(cordappJars)
return CordappProviderImpl(loader, cordappConfigProvider, attachmentStore).apply { start() }
}
}

View File

@ -1,6 +1,7 @@
package net.corda.node.internal.cordapp
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.cordapp.Cordapp
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
@ -9,13 +10,38 @@ import net.corda.core.flows.SchedulableFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.packageName_
import net.corda.core.internal.toPath
import net.corda.coretesting.internal.delete
import net.corda.coretesting.internal.modifyJarManifest
import net.corda.finance.contracts.CommercialPaper
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.finance.internal.ConfigHolder
import net.corda.finance.schemas.CashSchemaV1
import net.corda.finance.schemas.CommercialPaperSchemaV1
import net.corda.node.VersionInfo
import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES
import net.corda.serialization.internal.DefaultWhitelist
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.internal.ContractJarTestUtils.makeTestContractJar
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
import net.corda.testing.core.internal.JarSignatureTestUtils.getJarSigners
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
import net.corda.testing.internal.LogHelper
import net.corda.testing.node.internal.cordappWithPackages
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.assertj.core.api.Assertions.assertThatIllegalStateException
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.nio.file.Path
import java.nio.file.Paths
import java.util.jar.Manifest
import kotlin.io.path.absolutePathString
import kotlin.io.path.copyTo
import kotlin.io.path.name
import kotlin.test.assertFailsWith
@InitiatingFlow
class DummyFlow : FlowLogic<Unit>() {
@ -43,9 +69,17 @@ class DummyRPCFlow : FlowLogic<Unit>() {
class JarScanningCordappLoaderTest {
private companion object {
const val isolatedContractId = "net.corda.isolated.contracts.AnotherDummyContract"
const val isolatedFlowName = "net.corda.isolated.workflows.IsolatedIssuanceFlow"
val financeContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath()
val financeWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath()
init {
LogHelper.setLevel(JarScanningCordappLoaderTest::class)
}
}
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@Test(timeout=300_000)
fun `classes that aren't in cordapps aren't loaded`() {
@ -55,39 +89,42 @@ class JarScanningCordappLoaderTest {
}
@Test(timeout=300_000)
fun `isolated JAR contains a CorDapp with a contract and plugin`() {
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR))
fun `constructed CordappImpls contains the right classes`() {
val loader = JarScanningCordappLoader(setOf(financeContractsJar, financeWorkflowsJar))
val (contractsCordapp, workflowsCordapp) = loader.cordapps
assertThat(loader.cordapps).hasSize(1)
assertThat(contractsCordapp.contractClassNames).contains(Cash::class.java.name, CommercialPaper::class.java.name)
assertThat(contractsCordapp.customSchemas).contains(CashSchemaV1, CommercialPaperSchemaV1)
assertThat(contractsCordapp.info).isInstanceOf(Cordapp.Info.Contract::class.java)
assertThat(contractsCordapp.allFlows).isEmpty()
assertThat(contractsCordapp.jarFile).isEqualTo(financeContractsJar)
val actualCordapp = loader.cordapps.single()
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId))
assertThat(actualCordapp.initiatedFlows).isEmpty()
assertThat(actualCordapp.rpcFlows.first().name).isEqualTo(isolatedFlowName)
assertThat(actualCordapp.schedulableFlows).isEmpty()
assertThat(actualCordapp.services).isEmpty()
assertThat(actualCordapp.serializationWhitelists).hasSize(1)
assertThat(actualCordapp.serializationWhitelists.first().javaClass.name).isEqualTo("net.corda.serialization.internal.DefaultWhitelist")
assertThat(actualCordapp.jarFile).isEqualTo(isolatedJAR)
assertThat(workflowsCordapp.allFlows).contains(CashIssueFlow::class.java, CashPaymentFlow::class.java)
assertThat(workflowsCordapp.services).contains(ConfigHolder::class.java)
assertThat(workflowsCordapp.info).isInstanceOf(Cordapp.Info.Workflow::class.java)
assertThat(workflowsCordapp.contractClassNames).isEmpty()
assertThat(workflowsCordapp.jarFile).isEqualTo(financeWorkflowsJar)
for (actualCordapp in loader.cordapps) {
assertThat(actualCordapp.cordappClasses)
.containsAll(actualCordapp.contractClassNames)
.containsAll(actualCordapp.initiatedFlows.map { it.name })
.containsAll(actualCordapp.rpcFlows.map { it.name })
.containsAll(actualCordapp.serviceFlows.map { it.name })
.containsAll(actualCordapp.schedulableFlows.map { it.name })
.containsAll(actualCordapp.services.map { it.name })
.containsAll(actualCordapp.telemetryComponents.map { it.name })
.containsAll(actualCordapp.serializationCustomSerializers.map { it.javaClass.name })
.containsAll(actualCordapp.checkpointCustomSerializers.map { it.javaClass.name })
.containsAll(actualCordapp.customSchemas.map { it.name })
assertThat(actualCordapp.serializationWhitelists).contains(DefaultWhitelist)
}
@Test(timeout=300_000)
fun `constructed CordappImpl contains the right cordapp classes`() {
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR))
val actualCordapp = loader.cordapps.single()
val cordappClasses = actualCordapp.cordappClasses
assertThat(cordappClasses).contains(isolatedFlowName)
val serializationWhitelistedClasses = actualCordapp.serializationWhitelists.flatMap { it.whitelist }.map { it.name }
assertThat(cordappClasses).containsAll(serializationWhitelistedClasses)
}
@Test(timeout=300_000)
fun `flows are loaded by loader`() {
val jarFile = cordappWithPackages(javaClass.packageName_).jarFile
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jarFile))
val loader = JarScanningCordappLoader(setOf(jarFile))
// One cordapp from this source tree. In gradle it will also pick up the node jar.
assertThat(loader.cordapps).isNotEmpty
@ -102,17 +139,15 @@ class JarScanningCordappLoaderTest {
// being used internally. Later iterations will use a classloader per cordapp and this test can be retired.
@Test(timeout=300_000)
fun `cordapp classloader can load cordapp classes`() {
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR), VersionInfo.UNKNOWN)
val testJar = this::class.java.getResource("/testing-cashobservers-cordapp.jar")!!.toPath()
val loader = JarScanningCordappLoader(setOf(testJar))
loader.appClassLoader.loadClass(isolatedContractId)
loader.appClassLoader.loadClass(isolatedFlowName)
loader.appClassLoader.loadClass("net.corda.finance.test.flows.CashIssueWithObserversFlow")
}
@Test(timeout=300_000)
fun `cordapp classloader sets target and min version to 1 if not specified`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/no-min-or-target-version.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN)
fun `sets target and min version to 1 if not specified`() {
val loader = JarScanningCordappLoader(setOf(minAndTargetCordapp(minVersion = null, targetVersion = null)))
loader.cordapps.forEach {
assertThat(it.targetPlatformVersion).isEqualTo(1)
assertThat(it.minimumPlatformVersion).isEqualTo(1)
@ -120,21 +155,16 @@ class JarScanningCordappLoaderTest {
}
@Test(timeout=300_000)
fun `cordapp classloader returns correct values for minPlatformVersion and targetVersion`() {
// load jar with min and target version in manifest
// make sure classloader extracts correct values
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN)
fun `returns correct values for minPlatformVersion and targetVersion`() {
val loader = JarScanningCordappLoader(setOf(minAndTargetCordapp(minVersion = 2, targetVersion = 3)))
val cordapp = loader.cordapps.first()
assertThat(cordapp.targetPlatformVersion).isEqualTo(3)
assertThat(cordapp.minimumPlatformVersion).isEqualTo(2)
}
@Test(timeout=300_000)
fun `cordapp classloader sets target version to min version if target version is not specified`() {
// load jar with minVersion but not targetVersion in manifest
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN)
fun `sets target version to min version if target version is not specified`() {
val loader = JarScanningCordappLoader(setOf(minAndTargetCordapp(minVersion = 2, targetVersion = null)))
// exclude the core cordapp
val cordapp = loader.cordapps.first()
assertThat(cordapp.targetPlatformVersion).isEqualTo(2)
@ -142,48 +172,99 @@ class JarScanningCordappLoaderTest {
}
@Test(timeout = 300_000)
fun `cordapp classloader does not load apps when their min platform version is greater than the node platform version`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!!.toPath()
val cordappLoader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1))
fun `does not load apps when their min platform version is greater than the node platform version`() {
val jar = minAndTargetCordapp(minVersion = 2, targetVersion = null)
val cordappLoader = JarScanningCordappLoader(setOf(jar), versionInfo = VersionInfo.UNKNOWN.copy(platformVersion = 1))
assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy {
cordappLoader.cordapps
}
}
@Test(timeout=300_000)
fun `cordapp classloader does load apps when their min platform version is less than the platform version`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1000))
fun `does load apps when their min platform version is less than the platform version`() {
val jar = minAndTargetCordapp(minVersion = 2, targetVersion = 3)
val loader = JarScanningCordappLoader(setOf(jar), versionInfo = VersionInfo.UNKNOWN.copy(platformVersion = 1000))
assertThat(loader.cordapps).hasSize(1)
}
@Test(timeout=300_000)
fun `cordapp classloader does load apps when their min platform version is equal to the platform version`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2))
fun `does load apps when their min platform version is equal to the platform version`() {
val jar = minAndTargetCordapp(minVersion = 2, targetVersion = 3)
val loader = JarScanningCordappLoader(setOf(jar), versionInfo = VersionInfo.UNKNOWN.copy(platformVersion = 2))
assertThat(loader.cordapps).hasSize(1)
}
@Test(timeout=300_000)
fun `cordapp classloader loads app signed by allowed certificate`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = emptyList())
fun `loads app signed by allowed certificate`() {
val loader = JarScanningCordappLoader(setOf(financeContractsJar), signerKeyFingerprintBlacklist = emptyList())
assertThat(loader.cordapps).hasSize(1)
}
@Test(timeout = 300_000)
fun `cordapp classloader does not load app signed by blacklisted certificate`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!.toPath()
val cordappLoader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
fun `does not load app signed by blacklisted certificate`() {
val cordappLoader = JarScanningCordappLoader(setOf(financeContractsJar), signerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy {
cordappLoader.cordapps
}
}
@Test(timeout=300_000)
fun `cordapp classloader loads app signed by both allowed and non-blacklisted certificate`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-two-keys.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
fun `does not load duplicate CorDapps`() {
val duplicateJar = financeWorkflowsJar.duplicate()
val loader = JarScanningCordappLoader(setOf(financeWorkflowsJar, duplicateJar))
assertFailsWith<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)
}
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.crypto.SecureHash
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.serialization.CordaSerializable
import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.core.serialization.internal.CheckpointSerializationContext
import net.corda.coretesting.internal.rigorousMock
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.CordaKryo
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.services.InternalMockAttachmentStorage
import net.corda.testing.services.MockAttachmentStorage
import org.assertj.core.api.Assertions.assertThatExceptionOfType
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)
fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() {
val storage = InternalMockAttachmentStorage(MockAttachmentStorage())
val storage = MockAttachmentStorage().toInternal()
val attachmentTrustCalculator = NodeAttachmentTrustCalculator(storage, TestingNamedCacheFactory())
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)
assertThatExceptionOfType(KryoException::class.java).isThrownBy {
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
@ -239,7 +247,7 @@ class CordaClassResolverTests {
@Test(timeout=300_000)
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 attachmentHash = importJar(storage, "some_uploader")
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.loadDevCaTrustStore
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.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 {
val certificatesDirectory = Files.createTempDirectory("certs")
val config = CertificateStoreStubs.P2P.withCertificatesDirectory(certificatesDirectory)
if (config.trustStore.getOptional() == null) {
@ -19,3 +27,23 @@ fun configureTestSSL(legalName: CordaX500Name): MutualSslConfiguration {
}
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.internal.JarSignatureCollector
import net.corda.core.internal.deleteRecursively
import net.corda.coretesting.internal.modifyJarManifest
import net.corda.coretesting.internal.useZipFile
import net.corda.nodeapi.internal.crypto.loadKeyStore
import java.io.Closeable
import java.io.FileInputStream
import java.io.FileOutputStream
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.NoSuchFileException
import java.nio.file.Path
@ -20,9 +21,7 @@ import java.util.jar.JarOutputStream
import java.util.jar.Manifest
import kotlin.io.path.deleteExisting
import kotlin.io.path.div
import kotlin.io.path.inputStream
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.outputStream
import kotlin.test.assertEquals
/**
@ -74,12 +73,13 @@ object JarSignatureTestUtils {
}
fun Path.unsignJar() {
FileSystems.newFileSystem(this).use { zipFs ->
// Remove the signatures
useZipFile { zipFs ->
zipFs.getPath("META-INF").listDirectoryEntries("*.{SF,DSA,RSA,EC}").forEach(Path::deleteExisting)
val manifestFile = zipFs.getPath("META-INF", "MANIFEST.MF")
val manifest = manifestFile.inputStream().use(::Manifest)
manifest.entries.clear() // Remove all the hash information of the jar contents
manifestFile.outputStream().use(manifest::write)
}
// Remove all the hash information of the jar contents
modifyJarManifest { manifest ->
manifest.entries.clear()
}
}

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.network.PersistentNetworkMapCache
import net.corda.node.services.persistence.PublicKeyToOwningIdentityCacheImpl
import net.corda.node.services.persistence.toInternal
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.services.vault.NodeVaultService
import net.corda.nodeapi.internal.cordapp.CordappLoader
import net.corda.nodeapi.internal.cordapp.cordappSchemas
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.contextTransaction
@ -78,7 +80,6 @@ import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.MockCordappProvider
import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.configureDatabase
import net.corda.testing.internal.services.InternalMockAttachmentStorage
import net.corda.testing.node.internal.MockCryptoService
import net.corda.testing.node.internal.MockKeyManagementService
import net.corda.testing.node.internal.MockNetworkParametersStorage
@ -128,7 +129,7 @@ open class MockServices private constructor(
) : ServiceHub {
companion object {
private fun cordappLoaderForPackages(packages: Iterable<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() {
return NodeInfo(listOf(NetworkHostAndPort("mock.node.services", 10000)), listOf(initialIdentity.identity), 1, serial = 1L)
}
private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments).also {
private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments.toInternal()).also {
it.start()
}
override val cordappProvider: CordappProvider get() = mockCordappProvider
@ -562,7 +563,7 @@ open class MockServices private constructor(
*/
private class VerifyingView(private val mockServices: MockServices) : VerifyingServiceHub, ServiceHub by mockServices {
override val attachmentTrustCalculator = NodeAttachmentTrustCalculator(
attachmentStorage = InternalMockAttachmentStorage(mockServices.attachments),
attachmentStorage = mockServices.attachments.toInternal(),
cacheFactory = TestingNamedCacheFactory()
)
@ -577,7 +578,7 @@ open class MockServices private constructor(
override fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>> = mockServices.loadStates(stateRefs)
override val externalVerifierHandle: ExternalVerifierHandle
get() = throw UnsupportedOperationException("External verification is not supported by MockServices")
get() = throw UnsupportedOperationException("`Verification of legacy transactions is not supported by MockServices. Use MockNode instead.")
}

View File

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

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

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