mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
Merge pull request #7672 from corda/shams-tx-builder-cleanup
ENT-11355: Cleanup of TransactionBuilder and CorDapp loading
This commit is contained in:
commit
c2742ba6a5
@ -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) }
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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. */
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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?
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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: List<Pair<AttachmentId, List<TransactionState<ContractState>>?>> = allContracts.toSet()
|
||||
.map { ctr ->
|
||||
handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], services)
|
||||
}
|
||||
val contractAttachmentsAndResolvedOutputStates = allContracts.map { contract ->
|
||||
selectAttachmentAndResolveOutputStates(
|
||||
contract,
|
||||
inputContractGroups[contract],
|
||||
outputContractGroups[contract],
|
||||
explicitContractToAttachments[contract],
|
||||
serviceHub
|
||||
)
|
||||
}
|
||||
|
||||
val resolvedStates: List<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 *has* to be used by this transaction as it is explicit
|
||||
explicitContractAttachment
|
||||
} else {
|
||||
hashAttachment ?: selectAttachmentForContract()
|
||||
}
|
||||
|
||||
// This will contain the hash of the JAR that *has* to be used by this Transaction, because it is explicit. Or null if none.
|
||||
val forcedAttachmentId = explicitContractAttachment ?: hashAttachments.singleOrNull()?.id
|
||||
|
||||
// This will contain the hash of the JAR that will be used by this Transaction.
|
||||
val selectedAttachmentId = forcedAttachmentId ?: selectAttachment()
|
||||
|
||||
val attachmentToUse = services.attachments.openAttachment(selectedAttachmentId)?.let { it as ContractAttachment }
|
||||
?: throw IllegalArgumentException("Contract attachment $selectedAttachmentId for $contractClassName is missing.")
|
||||
|
||||
// For Exit transactions (no output states) there is no need to resolve the output constraints.
|
||||
if (outputStates == null) {
|
||||
return Pair(selectedAttachmentId, null)
|
||||
return Pair(selectedAttachment, emptyList())
|
||||
}
|
||||
|
||||
// If there are no automatic constraints, there is nothing to resolve.
|
||||
if (outputStates.none { it.constraint in automaticConstraints }) {
|
||||
return Pair(selectedAttachmentId, outputStates)
|
||||
return Pair(selectedAttachment, outputStates)
|
||||
}
|
||||
|
||||
// The final step is to resolve AutomaticPlaceholderConstraint.
|
||||
val automaticConstraintPropagation = contractClassName.contractHasAutomaticConstraintPropagation(inputsAndOutputs.first().data::class.java.classLoader)
|
||||
|
||||
// When automaticConstraintPropagation is disabled for a contract, output states must an explicit Constraint.
|
||||
require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." }
|
||||
require(automaticConstraintPropagation) {
|
||||
"Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly."
|
||||
}
|
||||
|
||||
// This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint.
|
||||
val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, attachmentToUse, services)
|
||||
val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, selectedAttachment, serviceHub)
|
||||
|
||||
// Sanity check that the selected attachment actually passes.
|
||||
val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations)
|
||||
require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachmentId" }
|
||||
val constraintAttachment = AttachmentWithContext(selectedAttachment, contractClassName, serviceHub.networkParameters.whitelistedContractImplementations)
|
||||
require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) {
|
||||
"Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachment"
|
||||
}
|
||||
|
||||
val resolvedOutputStates = outputStates.map {
|
||||
val outputConstraint = it.constraint
|
||||
@ -534,14 +506,16 @@ open class TransactionBuilder(
|
||||
} else {
|
||||
// If the constraint on the output state is already set, and is not a valid transition or can't be transitioned, then fail early.
|
||||
inputStates?.forEach { input ->
|
||||
require(outputConstraint.canBeTransitionedFrom(input.constraint, attachmentToUse)) { "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" }
|
||||
require(outputConstraint.canBeTransitionedFrom(input.constraint, selectedAttachment)) {
|
||||
"Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}"
|
||||
}
|
||||
}
|
||||
require(outputConstraint.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" }
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(selectedAttachmentId, resolvedOutputStates)
|
||||
return Pair(selectedAttachment, resolvedOutputStates)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -572,21 +546,25 @@ open class TransactionBuilder(
|
||||
contractClassName: ContractClassName,
|
||||
inputStates: List<TransactionState<ContractState>>?,
|
||||
attachmentToUse: ContractAttachment,
|
||||
services: ServicesForResolution): AttachmentConstraint = when {
|
||||
inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse, services)
|
||||
attachmentToUse.signerKeys.isNotEmpty() && services.networkParameters.minimumPlatformVersion < PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS -> {
|
||||
log.warnOnce("Signature constraints not available on network requiring a minimum platform version of ${PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS}. Current is: ${services.networkParameters.minimumPlatformVersion}.")
|
||||
if (useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters)) {
|
||||
log.warnOnce("Reverting back to using whitelisted zone constraints for contract $contractClassName")
|
||||
WhitelistedByZoneAttachmentConstraint
|
||||
} else {
|
||||
log.warnOnce("Reverting back to using hash constraints for contract $contractClassName")
|
||||
HashAttachmentConstraint(attachmentToUse.id)
|
||||
services: ServicesForResolution
|
||||
): AttachmentConstraint {
|
||||
return when {
|
||||
inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse, services)
|
||||
attachmentToUse.signerKeys.isNotEmpty() && services.networkParameters.minimumPlatformVersion < MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS -> {
|
||||
log.warnOnce("Signature constraints not available on network requiring a minimum platform version of " +
|
||||
"$MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS. Current is: ${services.networkParameters.minimumPlatformVersion}.")
|
||||
if (useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters)) {
|
||||
log.warnOnce("Reverting back to using whitelisted zone constraints for contract $contractClassName")
|
||||
WhitelistedByZoneAttachmentConstraint
|
||||
} else {
|
||||
log.warnOnce("Reverting back to using hash constraints for contract $contractClassName")
|
||||
HashAttachmentConstraint(attachmentToUse.id)
|
||||
}
|
||||
}
|
||||
attachmentToUse.signerKeys.isNotEmpty() -> makeSignatureAttachmentConstraint(attachmentToUse.signerKeys)
|
||||
useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint
|
||||
else -> HashAttachmentConstraint(attachmentToUse.id)
|
||||
}
|
||||
attachmentToUse.signerKeys.isNotEmpty() -> makeSignatureAttachmentConstraint(attachmentToUse.signerKeys)
|
||||
useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint
|
||||
else -> HashAttachmentConstraint(attachmentToUse.id)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -625,7 +603,7 @@ open class TransactionBuilder(
|
||||
// This ensures a smooth migration from a Whitelist Constraint to a Signature Constraint
|
||||
constraints.any { it is WhitelistedByZoneAttachmentConstraint } &&
|
||||
attachmentToUse.isSigned &&
|
||||
services.networkParameters.minimumPlatformVersion >= PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS ->
|
||||
services.networkParameters.minimumPlatformVersion >= MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS ->
|
||||
transitionToSignatureConstraint(constraints, attachmentToUse)
|
||||
|
||||
// This condition is hit when the current node has not installed the latest signed version but has already received states that have been migrated
|
||||
@ -651,16 +629,17 @@ open class TransactionBuilder(
|
||||
SignatureAttachmentConstraint.create(CompositeKey.Builder().addKeys(attachmentSigners)
|
||||
.build())
|
||||
|
||||
private fun getInstalledContractAttachmentId(
|
||||
private inline fun VerifyingServiceHub.getInstalledContractAttachment(
|
||||
contractClassName: String,
|
||||
states: List<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)
|
||||
|
@ -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 }
|
||||
|
@ -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>
|
||||
}
|
||||
/**
|
||||
* Returns all [MappedSchema] found inside the [Cordapp]s.
|
||||
*/
|
||||
val CordappLoader.cordappSchemas: Set<MappedSchema>
|
||||
get() = cordapps.flatMapToSet { it.customSchemas }
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
} catch (faee: FileAlreadyExistsException) {
|
||||
AttachmentId.create(faee.message!!)
|
||||
}
|
||||
} to cordapp.jarPath
|
||||
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)
|
||||
}
|
||||
// TODO We could remove this check if we had an import method for CorDapps, since it wouldn't need to hash the InputStream.
|
||||
// As it stands, we just have to double-check the hashes match, which should be the case (see NodeAttachmentService).
|
||||
check(attachmentId == cordapp.jarHash) {
|
||||
"Something has gone wrong. SHA-256 hash of ${cordapp.jarFile} (${cordapp.jarHash}) does not match attachment ID ($attachmentId)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContractAttachment(id: AttachmentId): ContractAttachment {
|
||||
return checkNotNull(attachmentStorage.openAttachment(id) as? ContractAttachment) { "Contract attachment $id has gone missing!" }
|
||||
}
|
||||
|
||||
private fun makeFlowToCordapp(): Map<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]
|
||||
}
|
||||
|
@ -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>,
|
||||
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()
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class JarScanningCordappLoader(private val cordappJars: Set<Path>,
|
||||
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
|
||||
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 ->
|
||||
if (cordapp.minimumPlatformVersion > versionInfo.platformVersion) {
|
||||
logger.warn("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it requires minimum " +
|
||||
"platform version ${cordapp.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).")
|
||||
invalidCordapps["CorDapp requires minimumPlatformVersion: ${cordapp.minimumPlatformVersion}, but was: ${versionInfo.platformVersion}"] = cordapp.jarFile
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}.filter { cordapp ->
|
||||
if (signerKeyFingerprintBlacklist.isEmpty()) {
|
||||
true //Nothing blacklisted, no need to check
|
||||
} else {
|
||||
val certificates = cordapp.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates)
|
||||
val blockedCertificates = certificates.filter { it.publicKey.hash.sha256() in signerKeyFingerprintBlacklist }
|
||||
if (certificates.isEmpty() || (certificates - blockedCertificates).isNotEmpty()) {
|
||||
true // Cordapp is not signed or it is signed by at least one non-blacklisted certificate
|
||||
} else {
|
||||
logger.warn("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it is signed by blacklisted key(s) only (probably development key): " +
|
||||
"${blockedCertificates.map { it.publicKey }}.")
|
||||
invalidCordapps["Corresponding contracts are signed by blacklisted key(s) only (probably development key),"] = cordapp.jarFile
|
||||
false
|
||||
}
|
||||
override val appClassLoader = URLClassLoader(cordappJars.stream().map { it.toUri().toURL() }.toTypedArray(), javaClass.classLoader)
|
||||
|
||||
private val internal by lazy(::InternalHolder)
|
||||
|
||||
override val cordapps: List<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.error("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it requires minimum " +
|
||||
"platform version ${cordapp.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).")
|
||||
invalidCordapps["CorDapp requires minimumPlatformVersion ${cordapp.minimumPlatformVersion}, but this node is running version ${versionInfo.platformVersion}"] = cordapp
|
||||
}
|
||||
if (signerKeyFingerprintBlacklist.isNotEmpty()) {
|
||||
val certificates = cordapp.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates)
|
||||
val blockedCertificates = certificates.filterTo(HashSet()) { it.publicKey.hash.sha256() in signerKeyFingerprintBlacklist }
|
||||
if (certificates.isNotEmpty() && (certificates - blockedCertificates).isEmpty()) {
|
||||
logger.error("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it is signed by blacklisted " +
|
||||
"key(s) only (probably development key): ${blockedCertificates.map { it.publicKey }}.")
|
||||
invalidCordapps["Corresponding contracts are signed by blacklisted key(s) only (probably development key),"] = cordapp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidCordapps.isNotEmpty()) {
|
||||
throw InvalidCordappException("Invalid Cordapps found, that couldn't be loaded: " +
|
||||
"${invalidCordapps.map { "Problem: ${it.key} in Cordapp ${it.value}" }}, ")
|
||||
if (invalidCordapps.isNotEmpty()) {
|
||||
throw InvalidCordappException("Invalid Cordapps found, that couldn't be loaded: " +
|
||||
"${invalidCordapps.map { "Problem: ${it.key} in Cordapp ${it.value.jarFile}" }}, ")
|
||||
}
|
||||
}
|
||||
|
||||
cordapps.forEach(::register)
|
||||
return cordapps
|
||||
}
|
||||
|
||||
private fun register(cordapp: Cordapp) {
|
||||
val contractClasses = cordapp.contractClassNames.toSet()
|
||||
val existingClasses = cordappClasses.keys
|
||||
val classesToRegister = cordapp.cordappClasses.toSet()
|
||||
val notAlreadyRegisteredClasses = classesToRegister - existingClasses
|
||||
val alreadyRegistered= HashMap(cordappClasses).apply { keys.retainAll(classesToRegister) }
|
||||
|
||||
notAlreadyRegisteredClasses.forEach { cordappClasses[it] = setOf(cordapp) }
|
||||
|
||||
for ((registeredClassName, registeredCordapps) in alreadyRegistered) {
|
||||
val duplicateCordapps = registeredCordapps.filter { it.jarHash == cordapp.jarHash }.toSet()
|
||||
|
||||
if (duplicateCordapps.isNotEmpty()) {
|
||||
throw DuplicateCordappsInstalledException(cordapp, duplicateCordapps)
|
||||
private fun checkDuplicateCordapps() {
|
||||
for (group in cordapps.groupBy { it.jarHash }.values) {
|
||||
if (group.size > 1) {
|
||||
throw DuplicateCordappsInstalledException(group[0], group.drop(1))
|
||||
}
|
||||
}
|
||||
if (registeredClassName in contractClasses) {
|
||||
throw IllegalStateException("More than one CorDapp installed on the node for contract $registeredClassName. " +
|
||||
}
|
||||
|
||||
private fun checkContractOverlap() {
|
||||
cordapps.groupByMultipleKeys(CordappImpl::contractClassNames) { contract, cordapp1, cordapp2 ->
|
||||
throw IllegalStateException("Contract $contract occuring in multiple CorDapps (${cordapp1.name}, ${cordapp2.name}). " +
|
||||
"Please remove the previous version when upgrading to a new version.")
|
||||
}
|
||||
cordappClasses[registeredClassName] = registeredCordapps + cordapp
|
||||
}
|
||||
}
|
||||
|
||||
private fun RestrictedScanResult.toCordapp(path: Path): CordappImpl {
|
||||
private fun ScanResult.toCordapp(path: Path): CordappImpl {
|
||||
val manifest: Manifest? = JarInputStream(path.inputStream()).use { it.manifest }
|
||||
val info = parseCordappInfo(manifest, CordappImpl.jarName(path))
|
||||
val minPlatformVersion = manifest?.get(CordappImpl.MIN_PLATFORM_VERSION)?.toIntOrNull() ?: 1
|
||||
val targetPlatformVersion = manifest?.get(CordappImpl.TARGET_PLATFORM_VERSION)?.toIntOrNull() ?: minPlatformVersion
|
||||
validateContractStateClassVersion(this)
|
||||
validateWhitelistClassVersion(this)
|
||||
return CordappImpl(
|
||||
path,
|
||||
findContractClassNamesWithVersionCheck(this),
|
||||
findContractClassNames(this),
|
||||
findInitiatedFlows(this),
|
||||
findRPCFlows(this),
|
||||
findServiceFlows(this),
|
||||
@ -206,10 +175,9 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set<
|
||||
findCustomSchemas(this),
|
||||
findAllFlows(this),
|
||||
info,
|
||||
path.hash,
|
||||
minPlatformVersion,
|
||||
targetPlatformVersion,
|
||||
findNotaryService(this),
|
||||
notaryService = findNotaryService(this),
|
||||
explicitCordappClasses = findAllCordappClasses(this)
|
||||
)
|
||||
}
|
||||
@ -278,27 +246,27 @@ class JarScanningCordappLoader private constructor(private val cordappJars: Set<
|
||||
return version
|
||||
}
|
||||
|
||||
private fun findNotaryService(scanResult: RestrictedScanResult): Class<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()
|
||||
.enableAllInfo()
|
||||
.pooledScan()
|
||||
return RestrictedScanResult(scanResult, cordappJar)
|
||||
return ClassGraph()
|
||||
.overrideClasspath(cordappJar.absolutePathString())
|
||||
.enableAllInfo()
|
||||
.pooledScan()
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -2,38 +2,42 @@ package net.corda.node.internal.cordapp
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.core.internal.hash
|
||||
import net.corda.core.internal.toPath
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.testing.core.internal.ContractJarTestUtils
|
||||
import net.corda.testing.core.internal.SelfCleaningDir
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.node.services.persistence.AttachmentStorageInternal
|
||||
import net.corda.node.services.persistence.toInternal
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar
|
||||
import net.corda.testing.internal.MockCordappConfigProvider
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.Deflater.NO_COMPRESSION
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipEntry.DEFLATED
|
||||
import java.util.zip.ZipEntry.STORED
|
||||
import kotlin.io.path.copyTo
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class CordappProviderImplTests {
|
||||
private companion object {
|
||||
val isolatedJAR = this::class.java.getResource("/isolated.jar")!!.toPath()
|
||||
// TODO: Cordapp name should differ from the JAR name
|
||||
const val isolatedCordappName = "isolated"
|
||||
val emptyJAR = this::class.java.getResource("empty.jar")!!.toPath()
|
||||
val validConfig: Config = ConfigFactory.parseString("key=value")
|
||||
val financeContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath()
|
||||
val financeWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath()
|
||||
|
||||
@JvmField
|
||||
val ID1 = AttachmentId.randomSHA256()
|
||||
@ -60,35 +64,29 @@ class CordappProviderImplTests {
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var attachmentStore: AttachmentStorage
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
|
||||
private lateinit var attachmentStore: AttachmentStorageInternal
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
attachmentStore = MockAttachmentStorage()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `isolated jar is loaded into the attachment store`() {
|
||||
val provider = newCordappProvider(isolatedJAR)
|
||||
val maybeAttachmentId = provider.getCordappAttachmentId(provider.cordapps.first())
|
||||
|
||||
assertNotNull(maybeAttachmentId)
|
||||
assertNotNull(attachmentStore.openAttachment(maybeAttachmentId!!))
|
||||
attachmentStore = MockAttachmentStorage().toInternal()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `empty jar is not loaded into the attachment store`() {
|
||||
val provider = newCordappProvider(emptyJAR)
|
||||
assertNull(provider.getCordappAttachmentId(provider.cordapps.first()))
|
||||
val provider = newCordappProvider(setOf(Companion::class.java.getResource("empty.jar")!!.toPath()))
|
||||
assertThat(attachmentStore.openAttachment(provider.cordapps.single().jarHash)).isNull()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `test that we find a cordapp class that is loaded into the store`() {
|
||||
val provider = newCordappProvider(isolatedJAR)
|
||||
val className = "net.corda.isolated.contracts.AnotherDummyContract"
|
||||
val provider = newCordappProvider(setOf(financeContractsJar))
|
||||
|
||||
val expected = provider.cordapps.first()
|
||||
val actual = provider.getCordappForClass(className)
|
||||
val actual = provider.getCordappForClass(Cash::class.java.name)
|
||||
|
||||
assertNotNull(actual)
|
||||
assertEquals(expected, actual)
|
||||
@ -96,33 +94,49 @@ class CordappProviderImplTests {
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `test that we find an attachment for a cordapp contract class`() {
|
||||
val provider = newCordappProvider(isolatedJAR)
|
||||
val className = "net.corda.isolated.contracts.AnotherDummyContract"
|
||||
val provider = newCordappProvider(setOf(financeContractsJar))
|
||||
val expected = provider.getAppContext(provider.cordapps.first()).attachmentId
|
||||
val actual = provider.getContractAttachmentID(className)
|
||||
val actual = provider.getContractAttachmentID(Cash::class.java.name)
|
||||
|
||||
assertNotNull(actual)
|
||||
assertEquals(actual!!, expected)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `test cordapp configuration`() {
|
||||
fun `test cordapp configuration`() {
|
||||
val configProvider = MockCordappConfigProvider()
|
||||
configProvider.cordappConfigs[isolatedCordappName] = validConfig
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR), VersionInfo.UNKNOWN)
|
||||
val provider = CordappProviderImpl(loader, configProvider, attachmentStore).apply { start() }
|
||||
configProvider.cordappConfigs["corda-finance-contracts"] = ConfigFactory.parseString("key=value")
|
||||
val provider = newCordappProvider(setOf(financeContractsJar), cordappConfigProvider = configProvider)
|
||||
|
||||
val expected = provider.getAppContext(provider.cordapps.first()).config
|
||||
|
||||
assertThat(expected.getString("key")).isEqualTo("value")
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun getCordappForFlow() {
|
||||
val provider = newCordappProvider(setOf(financeWorkflowsJar))
|
||||
val cashIssueFlow = CashIssueFlow(10.DOLLARS, OpaqueBytes.of(0x00), TestIdentity(ALICE_NAME).party)
|
||||
assertThat(provider.getCordappForFlow(cashIssueFlow)?.jarPath?.toPath()).isEqualTo(financeWorkflowsJar)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `does not load the same flow across different CorDapps`() {
|
||||
val unsignedJar = tempFolder.newFile("duplicate.jar").toPath()
|
||||
financeWorkflowsJar.copyTo(unsignedJar, overwrite = true)
|
||||
// We just need to change the file's hash and thus avoid the duplicate CorDapp check
|
||||
unsignedJar.unsignJar()
|
||||
assertThat(unsignedJar.hash).isNotEqualTo(financeWorkflowsJar.hash)
|
||||
assertFailsWith<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() }
|
||||
}
|
||||
}
|
||||
|
@ -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,10 +69,18 @@ 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`() {
|
||||
// Basedir will not be a corda node directory so the dummy flow shouldn't be recognised as a part of a cordapp
|
||||
@ -55,39 +89,42 @@ class JarScanningCordappLoaderTest {
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `isolated JAR contains a CorDapp with a contract and plugin`() {
|
||||
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!!.toPath()
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR))
|
||||
fun `constructed CordappImpls contains the right classes`() {
|
||||
val loader = JarScanningCordappLoader(setOf(financeContractsJar, financeWorkflowsJar))
|
||||
val (contractsCordapp, workflowsCordapp) = loader.cordapps
|
||||
|
||||
assertThat(loader.cordapps).hasSize(1)
|
||||
assertThat(contractsCordapp.contractClassNames).contains(Cash::class.java.name, CommercialPaper::class.java.name)
|
||||
assertThat(contractsCordapp.customSchemas).contains(CashSchemaV1, CommercialPaperSchemaV1)
|
||||
assertThat(contractsCordapp.info).isInstanceOf(Cordapp.Info.Contract::class.java)
|
||||
assertThat(contractsCordapp.allFlows).isEmpty()
|
||||
assertThat(contractsCordapp.jarFile).isEqualTo(financeContractsJar)
|
||||
|
||||
val actualCordapp = loader.cordapps.single()
|
||||
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId))
|
||||
assertThat(actualCordapp.initiatedFlows).isEmpty()
|
||||
assertThat(actualCordapp.rpcFlows.first().name).isEqualTo(isolatedFlowName)
|
||||
assertThat(actualCordapp.schedulableFlows).isEmpty()
|
||||
assertThat(actualCordapp.services).isEmpty()
|
||||
assertThat(actualCordapp.serializationWhitelists).hasSize(1)
|
||||
assertThat(actualCordapp.serializationWhitelists.first().javaClass.name).isEqualTo("net.corda.serialization.internal.DefaultWhitelist")
|
||||
assertThat(actualCordapp.jarFile).isEqualTo(isolatedJAR)
|
||||
assertThat(workflowsCordapp.allFlows).contains(CashIssueFlow::class.java, CashPaymentFlow::class.java)
|
||||
assertThat(workflowsCordapp.services).contains(ConfigHolder::class.java)
|
||||
assertThat(workflowsCordapp.info).isInstanceOf(Cordapp.Info.Workflow::class.java)
|
||||
assertThat(workflowsCordapp.contractClassNames).isEmpty()
|
||||
assertThat(workflowsCordapp.jarFile).isEqualTo(financeWorkflowsJar)
|
||||
|
||||
for (actualCordapp in loader.cordapps) {
|
||||
assertThat(actualCordapp.cordappClasses)
|
||||
.containsAll(actualCordapp.contractClassNames)
|
||||
.containsAll(actualCordapp.initiatedFlows.map { it.name })
|
||||
.containsAll(actualCordapp.rpcFlows.map { it.name })
|
||||
.containsAll(actualCordapp.serviceFlows.map { it.name })
|
||||
.containsAll(actualCordapp.schedulableFlows.map { it.name })
|
||||
.containsAll(actualCordapp.services.map { it.name })
|
||||
.containsAll(actualCordapp.telemetryComponents.map { it.name })
|
||||
.containsAll(actualCordapp.serializationCustomSerializers.map { it.javaClass.name })
|
||||
.containsAll(actualCordapp.checkpointCustomSerializers.map { it.javaClass.name })
|
||||
.containsAll(actualCordapp.customSchemas.map { it.name })
|
||||
assertThat(actualCordapp.serializationWhitelists).contains(DefaultWhitelist)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `constructed CordappImpl contains the right cordapp classes`() {
|
||||
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!!.toPath()
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR))
|
||||
|
||||
val actualCordapp = loader.cordapps.single()
|
||||
val cordappClasses = actualCordapp.cordappClasses
|
||||
assertThat(cordappClasses).contains(isolatedFlowName)
|
||||
val serializationWhitelistedClasses = actualCordapp.serializationWhitelists.flatMap { it.whitelist }.map { it.name }
|
||||
assertThat(cordappClasses).containsAll(serializationWhitelistedClasses)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `flows are loaded by loader`() {
|
||||
fun `flows are loaded by loader`() {
|
||||
val jarFile = cordappWithPackages(javaClass.packageName_).jarFile
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jarFile))
|
||||
val loader = JarScanningCordappLoader(setOf(jarFile))
|
||||
|
||||
// One cordapp from this source tree. In gradle it will also pick up the node jar.
|
||||
assertThat(loader.cordapps).isNotEmpty
|
||||
@ -101,18 +138,16 @@ class JarScanningCordappLoaderTest {
|
||||
// This test exists because the appClassLoader is used by serialisation and we need to ensure it is the classloader
|
||||
// being used internally. Later iterations will use a classloader per cordapp and this test can be retired.
|
||||
@Test(timeout=300_000)
|
||||
fun `cordapp classloader can load cordapp classes`() {
|
||||
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!!.toPath()
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR), VersionInfo.UNKNOWN)
|
||||
fun `cordapp classloader can load cordapp classes`() {
|
||||
val testJar = this::class.java.getResource("/testing-cashobservers-cordapp.jar")!!.toPath()
|
||||
val loader = JarScanningCordappLoader(setOf(testJar))
|
||||
|
||||
loader.appClassLoader.loadClass(isolatedContractId)
|
||||
loader.appClassLoader.loadClass(isolatedFlowName)
|
||||
loader.appClassLoader.loadClass("net.corda.finance.test.flows.CashIssueWithObserversFlow")
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `cordapp classloader sets target and min version to 1 if not specified`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/no-min-or-target-version.jar")!!.toPath()
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN)
|
||||
fun `sets target and min version to 1 if not specified`() {
|
||||
val loader = JarScanningCordappLoader(setOf(minAndTargetCordapp(minVersion = null, targetVersion = null)))
|
||||
loader.cordapps.forEach {
|
||||
assertThat(it.targetPlatformVersion).isEqualTo(1)
|
||||
assertThat(it.minimumPlatformVersion).isEqualTo(1)
|
||||
@ -120,21 +155,16 @@ class JarScanningCordappLoaderTest {
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `cordapp classloader returns correct values for minPlatformVersion and targetVersion`() {
|
||||
// load jar with min and target version in manifest
|
||||
// make sure classloader extracts correct values
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!.toPath()
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN)
|
||||
fun `returns correct values for minPlatformVersion and targetVersion`() {
|
||||
val loader = JarScanningCordappLoader(setOf(minAndTargetCordapp(minVersion = 2, targetVersion = 3)))
|
||||
val cordapp = loader.cordapps.first()
|
||||
assertThat(cordapp.targetPlatformVersion).isEqualTo(3)
|
||||
assertThat(cordapp.minimumPlatformVersion).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `cordapp classloader sets target version to min version if target version is not specified`() {
|
||||
// load jar with minVersion but not targetVersion in manifest
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!!.toPath()
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN)
|
||||
fun `sets target version to min version if target version is not specified`() {
|
||||
val loader = JarScanningCordappLoader(setOf(minAndTargetCordapp(minVersion = 2, targetVersion = null)))
|
||||
// exclude the core cordapp
|
||||
val cordapp = loader.cordapps.first()
|
||||
assertThat(cordapp.targetPlatformVersion).isEqualTo(2)
|
||||
@ -142,48 +172,99 @@ class JarScanningCordappLoaderTest {
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `cordapp classloader does not load apps when their min platform version is greater than the node platform version`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!!.toPath()
|
||||
val cordappLoader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1))
|
||||
fun `does not load apps when their min platform version is greater than the node platform version`() {
|
||||
val jar = minAndTargetCordapp(minVersion = 2, targetVersion = null)
|
||||
val cordappLoader = JarScanningCordappLoader(setOf(jar), versionInfo = VersionInfo.UNKNOWN.copy(platformVersion = 1))
|
||||
assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy {
|
||||
cordappLoader.cordapps
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `cordapp classloader does load apps when their min platform version is less than the platform version`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!.toPath()
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1000))
|
||||
fun `does load apps when their min platform version is less than the platform version`() {
|
||||
val jar = minAndTargetCordapp(minVersion = 2, targetVersion = 3)
|
||||
val loader = JarScanningCordappLoader(setOf(jar), versionInfo = VersionInfo.UNKNOWN.copy(platformVersion = 1000))
|
||||
assertThat(loader.cordapps).hasSize(1)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `cordapp classloader does load apps when their min platform version is equal to the platform version`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!.toPath()
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2))
|
||||
fun `does load apps when their min platform version is equal to the platform version`() {
|
||||
val jar = minAndTargetCordapp(minVersion = 2, targetVersion = 3)
|
||||
val loader = JarScanningCordappLoader(setOf(jar), versionInfo = VersionInfo.UNKNOWN.copy(platformVersion = 2))
|
||||
assertThat(loader.cordapps).hasSize(1)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `cordapp classloader loads app signed by allowed certificate`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!.toPath()
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = emptyList())
|
||||
fun `loads app signed by allowed certificate`() {
|
||||
val loader = JarScanningCordappLoader(setOf(financeContractsJar), signerKeyFingerprintBlacklist = emptyList())
|
||||
assertThat(loader.cordapps).hasSize(1)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `cordapp classloader does not load app signed by blacklisted certificate`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!.toPath()
|
||||
val cordappLoader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
|
||||
fun `does not load app signed by blacklisted certificate`() {
|
||||
val cordappLoader = JarScanningCordappLoader(setOf(financeContractsJar), signerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
|
||||
assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy {
|
||||
cordappLoader.cordapps
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `cordapp classloader loads app signed by both allowed and non-blacklisted certificate`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-two-keys.jar")!!.toPath()
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
|
||||
fun `does not load duplicate CorDapps`() {
|
||||
val duplicateJar = financeWorkflowsJar.duplicate()
|
||||
val loader = JarScanningCordappLoader(setOf(financeWorkflowsJar, duplicateJar))
|
||||
assertFailsWith<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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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 {
|
||||
|
@ -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?
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.")
|
||||
}
|
||||
|
||||
|
||||
|
@ -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) {}
|
||||
|
||||
|
@ -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) {
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user