CORDA-2083 verify transaction in AttachmentsClassloader (#4188)

CORDA-2083 fix tests

CORDA-2083 fix tests

CORDA-2083 fix tests

CORDA-2083 fix tests

CORDA-2083 fix tests

CORDA-2083 fix tests

CORDA-2083 fix tests

CORDA-2083 add support for explicit upgrade transactions

CORDA-2083 cleanup

CORDA-2083 cleanup

CORDA-2083 More cleanup

CORDA-2083 More cleanup

CORDA-2083 Clean up tests

CORDA-2083 Address code review comments

CORDA-2083 Fix merge

CORDA-2083 Fix merge

CORDA-2083 Address code review comments

revert file

CORDA-2083 Fix test

CORDA-2083 Add test

CORDA-2083 cleanup

CORDA-2083 Fix test

CORDA-2083 Address code review comments.

CORDA-2083 Remove unused functions.

CORDA-2083 Address code review comments.

CORDA-2083 Address code review comments.

CORDA-2083 Address code review comments.

CORDA-2083 Address code review comments.

CORDA-2083 Address code review comments.
This commit is contained in:
Tudor Malene 2018-11-19 13:42:12 +00:00 committed by GitHub
parent 38a4737764
commit 2d043828a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 839 additions and 792 deletions

View File

@ -27,6 +27,11 @@ fun isUploaderTrusted(uploader: String?): Boolean = uploader in TRUSTED_UPLOADER
@KeepForDJVM @KeepForDJVM
abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment { abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
companion object { companion object {
/**
* Returns a function that knows how to load an attachment.
*
* TODO - this code together with the rest of the Attachment handling (including [FetchedAttachment]) needs some refactoring as it is really hard to follow.
*/
@DeleteForDJVM @DeleteForDJVM
fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray { fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray {
return { return {

View File

@ -1,6 +1,10 @@
package net.corda.core.internal package net.corda.core.internal
import net.corda.core.DeleteForDJVM import net.corda.core.DeleteForDJVM
import net.corda.core.KeepForDJVM
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TransactionState
import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.Cordapp
import net.corda.core.cordapp.CordappConfig import net.corda.core.cordapp.CordappConfig
import net.corda.core.cordapp.CordappContext import net.corda.core.cordapp.CordappContext
@ -8,11 +12,14 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
import net.corda.core.node.ZoneVersionTooLowException import net.corda.core.node.ZoneVersionTooLowException
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializedBytes
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.OpaqueBytes
import org.slf4j.MDC import org.slf4j.MDC
// *Internal* Corda-specific utilities // *Internal* Corda-specific utilities
@ -73,3 +80,11 @@ class LazyMappedList<T, U>(val originalList: List<T>, val transform: (T, Int) ->
override fun get(index: Int) = partialResolvedList[index] override fun get(index: Int) = partialResolvedList[index]
?: transform(originalList[index], index).also { computed -> partialResolvedList[index] = computed } ?: transform(originalList[index], index).also { computed -> partialResolvedList[index] = computed }
} }
/**
* A SerializedStateAndRef is a pair (BinaryStateRepresentation, StateRef).
* The [serializedState] is the actual component from the original transaction.
*/
@KeepForDJVM
@CordaSerializable
data class SerializedStateAndRef(val serializedState: SerializedBytes<TransactionState<ContractState>>, val ref: StateRef)

View File

@ -221,15 +221,16 @@ data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHa
* Note that a slightly bigger than numOfExpectedBytes size is expected. * Note that a slightly bigger than numOfExpectedBytes size is expected.
*/ */
@DeleteForDJVM @DeleteForDJVM
fun createInMemoryTestZip(numOfExpectedBytes: Int, content: Byte): InputStreamAndHash { fun createInMemoryTestZip(numOfExpectedBytes: Int, content: Byte, entryName: String = "z"): InputStreamAndHash {
require(numOfExpectedBytes > 0){"Expected bytes must be greater than zero"} require(numOfExpectedBytes > 0){"Expected bytes must be greater than zero"}
require(numOfExpectedBytes > 0)
val baos = ByteArrayOutputStream() val baos = ByteArrayOutputStream()
ZipOutputStream(baos).use { zos -> ZipOutputStream(baos).use { zos ->
val arraySize = 1024 val arraySize = 1024
val bytes = ByteArray(arraySize) { content } val bytes = ByteArray(arraySize) { content }
val n = (numOfExpectedBytes - 1) / arraySize + 1 // same as Math.ceil(numOfExpectedBytes/arraySize). val n = (numOfExpectedBytes - 1) / arraySize + 1 // same as Math.ceil(numOfExpectedBytes/arraySize).
zos.setLevel(Deflater.NO_COMPRESSION) zos.setLevel(Deflater.NO_COMPRESSION)
zos.putNextEntry(ZipEntry("z")) zos.putNextEntry(ZipEntry(entryName))
for (i in 0 until n) { for (i in 0 until n) {
zos.write(bytes, 0, arraySize) zos.write(bytes, 0, arraySize)
} }
@ -501,3 +502,18 @@ fun <T : Any> SerializedBytes<Any>.checkPayloadIs(type: Class<T>): Untrustworthy
return type.castIfPossible(payloadData)?.let { UntrustworthyData(it) } return type.castIfPossible(payloadData)?.let { UntrustworthyData(it) }
?: throw IllegalArgumentException("We were expecting a ${type.name} but we instead got a ${payloadData.javaClass.name} ($payloadData)") ?: throw IllegalArgumentException("We were expecting a ${type.name} but we instead got a ${payloadData.javaClass.name} ($payloadData)")
} }
/**
* Simple Map structure that can be used as a cache in the DJVM.
*/
fun <K, V> createSimpleCache(maxSize: Int, onEject: (MutableMap.MutableEntry<K, V>) -> Unit = {}): MutableMap<K, V> {
return object : LinkedHashMap<K, V>() {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>?): Boolean {
val eject = size > maxSize
if (eject) onEject(eldest!!)
return eject
}
}
}
fun <K,V> MutableMap<K,V>.toSynchronised(): MutableMap<K,V> = Collections.synchronizedMap(this)

View File

@ -1,15 +1,23 @@
package net.corda.core.internal package net.corda.core.internal
import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.*
import net.corda.core.contracts.PrivacySalt
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.componentHash
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.MissingAttachmentsException
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.transactions.ComponentGroup
import net.corda.core.transactions.ContractUpgradeWireTransaction import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.FilteredComponentGroup
import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.lazyMapped
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.security.PublicKey
import kotlin.reflect.KClass
/** Constructs a [NotaryChangeWireTransaction]. */ /** Constructs a [NotaryChangeWireTransaction]. */
class NotaryChangeTransactionBuilder(val inputs: List<StateRef>, class NotaryChangeTransactionBuilder(val inputs: List<StateRef>,
@ -43,3 +51,74 @@ fun combinedHash(components: Iterable<SecureHash>): SecureHash {
} }
return stream.toByteArray().sha256() return stream.toByteArray().sha256()
} }
/**
* This function knows how to deserialize a transaction component group.
*
* In case the [componentGroups] is an instance of [LazyMappedList], this function will just use the original deserialized version, and avoid an unnecessary deserialization.
* The [forceDeserialize] will force deserialization. In can be used in case the SerializationContext changes.
*/
fun <T : Any> deserialiseComponentGroup(componentGroups: List<ComponentGroup>,
clazz: KClass<T>,
groupEnum: ComponentGroupEnum,
forceDeserialize: Boolean = false,
factory: SerializationFactory = SerializationFactory.defaultFactory,
context: SerializationContext = factory.defaultContext): List<T> {
val group = componentGroups.firstOrNull { it.groupIndex == groupEnum.ordinal }
if (group == null || group.components.isEmpty()) {
return emptyList()
}
// If the componentGroup is a [LazyMappedList] it means that the original deserialized version is already available.
val components = group.components
if (!forceDeserialize && components is LazyMappedList<*, OpaqueBytes>) {
return components.originalList as List<T>
}
return components.lazyMapped { component, internalIndex ->
try {
factory.deserialize(component, clazz.java, context)
} catch (e: MissingAttachmentsException) {
throw e
} catch (e: Exception) {
throw Exception("Malformed transaction, $groupEnum at index $internalIndex cannot be deserialised", e)
}
}
}
/**
* Method to deserialise Commands from its two groups:
* * COMMANDS_GROUP which contains the CommandData part
* * and SIGNERS_GROUP which contains the Signers part.
*
* This method used the [deserialiseComponentGroup] method.
*/
fun deserialiseCommands(componentGroups: List<ComponentGroup>,
forceDeserialize: Boolean = false,
factory: SerializationFactory = SerializationFactory.defaultFactory,
context: SerializationContext = factory.defaultContext): List<Command<*>> {
// TODO: we could avoid deserialising unrelated signers.
// However, current approach ensures the transaction is not malformed
// and it will throw if any of the signers objects is not List of public keys).
val signersList: List<List<PublicKey>> = uncheckedCast(deserialiseComponentGroup(componentGroups, List::class, ComponentGroupEnum.SIGNERS_GROUP, forceDeserialize))
val commandDataList: List<CommandData> = deserialiseComponentGroup(componentGroups, CommandData::class, ComponentGroupEnum.COMMANDS_GROUP, forceDeserialize)
val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal }
return if (group is FilteredComponentGroup) {
check(commandDataList.size <= signersList.size) {
"Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects"
}
val componentHashes = group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }
val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) }
if (leafIndices.isNotEmpty())
check(leafIndices.max()!! < signersList.size) { "Invalid Transaction. A command with no corresponding signer detected" }
commandDataList.lazyMapped { commandData, index -> Command(commandData, signersList[leafIndices[index]]) }
} else {
// It is a WireTransaction
// or a FilteredTransaction with no Commands (in which case group is null).
check(commandDataList.size == signersList.size) {
"Invalid Transaction. Sizes of CommandData (${commandDataList.size}) and Signers (${signersList.size}) do not match"
}
commandDataList.lazyMapped { commandData, index -> Command(commandData, signersList[index]) }
}
}

View File

@ -177,10 +177,10 @@ interface SerializationContext {
fun withClassLoader(classLoader: ClassLoader): SerializationContext fun withClassLoader(classLoader: ClassLoader): SerializationContext
/** /**
* Helper method to return a new context based on this context with the appropriate class loader constructed from the passed attachment identifiers. * Does not do anything.
* (Requires the attachment storage to have been enabled).
*/ */
@Throws(MissingAttachmentsException::class) @Throws(MissingAttachmentsException::class)
@Deprecated("There is no reason to call this. This method does not actually do anything.")
fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): SerializationContext fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): SerializationContext
/** /**

View File

@ -0,0 +1,161 @@
package net.corda.core.serialization.internal
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.isUploaderTrusted
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.toUrl
import net.corda.core.internal.createSimpleCache
import net.corda.core.internal.toSynchronised
import java.io.IOException
import java.io.InputStream
import java.net.*
/**
* A custom ClassLoader that knows how to load classes from a set of attachments. The attachments themselves only
* need to provide JAR streams, and so could be fetched from a database, local disk, etc. Constructing an
* AttachmentsClassLoader is somewhat expensive, as every attachment is scanned to ensure that there are no overlapping
* file paths.
*/
class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader = ClassLoader.getSystemClassLoader()) :
URLClassLoader(attachments.map(::toUrl).toTypedArray(), parent) {
companion object {
init {
// This is required to register the AttachmentURLStreamHandlerFactory.
URL.setURLStreamHandlerFactory(AttachmentURLStreamHandlerFactory)
}
private const val `META-INF` = "meta-inf"
private val excludeFromNoOverlapCheck = setOf(
"manifest.mf",
"license",
"license.txt",
"notice",
"notice.txt",
"index.list"
)
private fun shouldCheckForNoOverlap(path: String): Boolean {
if (!path.startsWith(`META-INF`)) return true
val p = path.substring(`META-INF`.length + 1)
if (p in excludeFromNoOverlapCheck) return false
if (p.endsWith(".sf") || p.endsWith(".dsa")) return false
return true
}
@CordaSerializable
class OverlappingAttachments(val path: String) : Exception() {
override fun toString() = "Multiple attachments define a file at path $path"
}
private fun requireNoDuplicates(attachments: List<Attachment>) {
val classLoaderEntries = mutableSetOf<String>()
for (attachment in attachments) {
attachment.openAsJAR().use { jar ->
while (true) {
val entry = jar.nextJarEntry ?: break
// We already verified that paths are not strange/game playing when we inserted the attachment
// into the storage service. So we don't need to repeat it here.
//
// We forbid files that differ only in case, or path separator to avoid issues for Windows/Mac developers where the
// filesystem tries to be case insensitive. This may break developers who attempt to use ProGuard.
//
// Also convert to Unix path separators as all resource/class lookups will expect this.
// If 2 entries have the same CRC, it means the same file is present in both attachments, so that is ok. TODO - Mike, wdyt?
val path = entry.name.toLowerCase().replace('\\', '/')
if (shouldCheckForNoOverlap(path)) {
if (path in classLoaderEntries) throw OverlappingAttachments(path)
classLoaderEntries.add(path)
}
}
}
}
}
}
init {
require(attachments.mapNotNull { it as? ContractAttachment }.all { isUploaderTrusted(it.uploader) }) {
"Attempting to load Contract Attachments downloaded from the network"
}
requireNoDuplicates(attachments)
}
}
/**
* This is just a factory that provides a cache to avoid constructing expensive [AttachmentsClassLoader]s.
*/
@VisibleForTesting
internal object AttachmentsClassLoaderBuilder {
private const val ATTACHMENT_CLASSLOADER_CACHE_SIZE = 1000
// This runs in the DJVM so it can't use caffeine.
private val cache: MutableMap<List<SecureHash>, AttachmentsClassLoader> = createSimpleCache<List<SecureHash>, AttachmentsClassLoader>(ATTACHMENT_CLASSLOADER_CACHE_SIZE)
.toSynchronised()
fun build(attachments: List<Attachment>): AttachmentsClassLoader {
return cache.computeIfAbsent(attachments.map { it.id }.sorted()) {
AttachmentsClassLoader(attachments)
}
}
fun <T> withAttachmentsClassloaderContext(attachments: List<Attachment>, block: (ClassLoader) -> T): T {
// Create classloader from the attachments.
val transactionClassLoader = AttachmentsClassLoaderBuilder.build(attachments)
// Create a new serializationContext for the current Transaction.
val transactionSerializationContext = SerializationFactory.defaultFactory.defaultContext.withClassLoader(transactionClassLoader)
// Deserialize all relevant classes in the transaction classloader.
return SerializationFactory.defaultFactory.withCurrentContext(transactionSerializationContext) {
block(transactionClassLoader)
}
}
}
/**
* Registers a new internal "attachment" protocol.
* This will not be exposed as an API.
*/
object AttachmentURLStreamHandlerFactory : URLStreamHandlerFactory {
private const val attachmentScheme = "attachment"
// TODO - what happens if this grows too large?
private val loadedAttachments = mutableMapOf<String, Attachment>().toSynchronised()
override fun createURLStreamHandler(protocol: String): URLStreamHandler? {
return if (attachmentScheme == protocol) {
AttachmentURLStreamHandler
} else null
}
fun toUrl(attachment: Attachment): URL {
val id = attachment.id.toString()
loadedAttachments[id] = attachment
return URL(attachmentScheme, "", -1, id, AttachmentURLStreamHandler)
}
private object AttachmentURLStreamHandler : URLStreamHandler() {
override fun openConnection(url: URL): URLConnection {
if (url.protocol != attachmentScheme) throw IOException("Cannot handle protocol: ${url.protocol}")
val attachment = loadedAttachments[url.path] ?: throw IOException("Could not load url: $url .")
return AttachmentURLConnection(url, attachment)
}
}
private class AttachmentURLConnection(url: URL, private val attachment: Attachment) : URLConnection(url) {
override fun getContentLengthLong(): Long = attachment.size.toLong()
override fun getInputStream(): InputStream = attachment.open()
override fun connect() {
connected = true
}
}
}

View File

@ -73,13 +73,6 @@ interface CheckpointSerializationContext {
*/ */
fun withClassLoader(classLoader: ClassLoader): CheckpointSerializationContext fun withClassLoader(classLoader: ClassLoader): CheckpointSerializationContext
/**
* Helper method to return a new context based on this context with the appropriate class loader constructed from the passed attachment identifiers.
* (Requires the attachment storage to have been enabled).
*/
@Throws(MissingAttachmentsException::class)
fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): CheckpointSerializationContext
/** /**
* Helper method to return a new context based on this context with the given class specifically whitelisted. * Helper method to return a new context based on this context with the given class specifically whitelisted.
*/ */

View File

@ -1,5 +1,6 @@
package net.corda.core.transactions package net.corda.core.transactions
import net.corda.core.CordaInternal
import net.corda.core.KeepForDJVM import net.corda.core.KeepForDJVM
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
@ -11,10 +12,12 @@ import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.combinedHash import net.corda.core.internal.combinedHash
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.*
import net.corda.core.serialization.deserialize import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
import net.corda.core.transactions.ContractUpgradeFilteredTransaction.FilteredComponent import net.corda.core.transactions.ContractUpgradeFilteredTransaction.FilteredComponent
import net.corda.core.transactions.ContractUpgradeWireTransaction.Companion.calculateUpgradedState
import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.* import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.*
import net.corda.core.transactions.WireTransaction.Companion.resolveStateRefBinaryComponent
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.toBase58String import net.corda.core.utilities.toBase58String
import java.security.PublicKey import java.security.PublicKey
@ -35,6 +38,32 @@ data class ContractUpgradeWireTransaction(
/** Required for hiding components in [ContractUpgradeFilteredTransaction]. */ /** Required for hiding components in [ContractUpgradeFilteredTransaction]. */
val privacySalt: PrivacySalt = PrivacySalt() val privacySalt: PrivacySalt = PrivacySalt()
) : CoreTransaction() { ) : CoreTransaction() {
companion object {
/**
* Runs the explicit upgrade logic.
*/
@CordaInternal
internal fun <T : ContractState, S : ContractState> calculateUpgradedState(state: TransactionState<T>, upgradedContract: UpgradedContract<T, S>, upgradedContractAttachment: Attachment): TransactionState<S> {
// TODO: if there are encumbrance states in the inputs, just copy them across without modifying
val upgradedState: S = upgradedContract.upgrade(state.data)
val inputConstraint = state.constraint
val outputConstraint = when (inputConstraint) {
is HashAttachmentConstraint -> HashAttachmentConstraint(upgradedContractAttachment.id)
WhitelistedByZoneAttachmentConstraint -> WhitelistedByZoneAttachmentConstraint
else -> throw IllegalArgumentException("Unsupported input contract constraint $inputConstraint")
}
// TODO: re-map encumbrance pointers
return TransactionState(
data = upgradedState,
contract = upgradedContract::class.java.name,
constraint = outputConstraint,
notary = state.notary,
encumbrance = state.encumbrance
)
}
}
override val inputs: List<StateRef> = serializedComponents[INPUTS.ordinal].deserialize() override val inputs: List<StateRef> = serializedComponents[INPUTS.ordinal].deserialize()
override val notary: Party by lazy { serializedComponents[NOTARY.ordinal].deserialize<Party>() } override val notary: Party by lazy { serializedComponents[NOTARY.ordinal].deserialize<Party>() }
val legacyContractAttachmentId: SecureHash by lazy { serializedComponents[LEGACY_ATTACHMENT.ordinal].deserialize<SecureHash>() } val legacyContractAttachmentId: SecureHash by lazy { serializedComponents[LEGACY_ATTACHMENT.ordinal].deserialize<SecureHash>() }
@ -90,6 +119,32 @@ data class ContractUpgradeWireTransaction(
) )
} }
private fun upgradedContract(className: ContractClassName, classLoader: ClassLoader): UpgradedContract<ContractState, ContractState> = try {
classLoader.loadClass(className).asSubclass(UpgradedContract::class.java as Class<UpgradedContract<ContractState, ContractState>>)
.newInstance()
} catch (e: Exception) {
throw TransactionVerificationException.ContractCreationError(id, className, e)
}
/**
* Creates a binary serialized component for a virtual output state serialised and executed with the attachments from the transaction.
*/
@CordaInternal
internal fun resolveOutputComponent(services: ServicesForResolution, stateRef: StateRef): SerializedBytes<TransactionState<ContractState>> {
val binaryInput = resolveStateRefBinaryComponent(inputs[stateRef.index], services)!!
val legacyAttachment = services.attachments.openAttachment(legacyContractAttachmentId)
?: throw MissingContractAttachments(emptyList())
val upgradedAttachment = services.attachments.openAttachment(upgradedContractAttachmentId)
?: throw MissingContractAttachments(emptyList())
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(listOf(legacyAttachment, upgradedAttachment)) { transactionClassLoader ->
val resolvedInput = binaryInput.deserialize<TransactionState<ContractState>>()
val upgradedContract = upgradedContract(upgradedContractClassName, transactionClassLoader)
val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment)
outputState.serialize()
}
}
/** Constructs a filtered transaction: the inputs and the notary party are always visible, while the rest are hidden. */ /** Constructs a filtered transaction: the inputs and the notary party are always visible, while the rest are hidden. */
fun buildFilteredTransaction(): ContractUpgradeFilteredTransaction { fun buildFilteredTransaction(): ContractUpgradeFilteredTransaction {
val totalComponents = (0 until serializedComponents.size).toSet() val totalComponents = (0 until serializedComponents.size).toSet()
@ -222,22 +277,7 @@ data class ContractUpgradeLedgerTransaction(
* Outputs are computed by running the contract upgrade logic on input states. This is done eagerly so that the * Outputs are computed by running the contract upgrade logic on input states. This is done eagerly so that the
* transaction is verified during construction. * transaction is verified during construction.
*/ */
override val outputs: List<TransactionState<ContractState>> = inputs.map { (state) -> override val outputs: List<TransactionState<ContractState>> = inputs.map { calculateUpgradedState(it.state, upgradedContract, upgradedContractAttachment) }
// TODO: if there are encumbrance states in the inputs, just copy them across without modifying
val upgradedState = upgradedContract.upgrade(state.data)
val inputConstraint = state.constraint
val outputConstraint = when (inputConstraint) {
is HashAttachmentConstraint -> HashAttachmentConstraint(upgradedContractAttachment.id)
WhitelistedByZoneAttachmentConstraint -> WhitelistedByZoneAttachmentConstraint
else -> throw IllegalArgumentException("Unsupported input contract constraint $inputConstraint")
}
// TODO: re-map encumbrance pointers
state.copy(
data = upgradedState,
contract = upgradedContractClassName,
constraint = outputConstraint
)
}
/** The required signers are the set of all input states' participants. */ /** The required signers are the set of all input states' participants. */
override val requiredSigningKeys: Set<PublicKey> override val requiredSigningKeys: Set<PublicKey>

View File

@ -1,22 +1,21 @@
package net.corda.core.transactions package net.corda.core.transactions
import net.corda.core.CordaInternal
import net.corda.core.KeepForDJVM import net.corda.core.KeepForDJVM
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.isFulfilledBy
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext import net.corda.core.internal.*
import net.corda.core.internal.castIfPossible
import net.corda.core.internal.checkMinimumPlatformVersion
import net.corda.core.internal.uncheckedCast
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.Try import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.warnOnce
import java.util.* import java.util.*
import java.util.function.Predicate import java.util.function.Predicate
import kotlin.collections.HashSet import kotlin.collections.HashSet
import net.corda.core.utilities.warnOnce
/** /**
* A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations: * A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations:
@ -34,7 +33,7 @@ import net.corda.core.utilities.warnOnce
// DOCSTART 1 // DOCSTART 1
@KeepForDJVM @KeepForDJVM
@CordaSerializable @CordaSerializable
data class LedgerTransaction @JvmOverloads constructor( data class LedgerTransaction private constructor(
/** The resolved input states which will be consumed/invalidated by the execution of this transaction. */ /** The resolved input states which will be consumed/invalidated by the execution of this transaction. */
override val inputs: List<StateAndRef<ContractState>>, override val inputs: List<StateAndRef<ContractState>>,
override val outputs: List<TransactionState<ContractState>>, override val outputs: List<TransactionState<ContractState>>,
@ -47,9 +46,38 @@ data class LedgerTransaction @JvmOverloads constructor(
override val notary: Party?, override val notary: Party?,
val timeWindow: TimeWindow?, val timeWindow: TimeWindow?,
val privacySalt: PrivacySalt, val privacySalt: PrivacySalt,
private val networkParameters: NetworkParameters? = null, private val networkParameters: NetworkParameters?,
override val references: List<StateAndRef<ContractState>> = emptyList() override val references: List<StateAndRef<ContractState>>,
val componentGroups: List<ComponentGroup>?,
val resolvedInputBytes: List<SerializedStateAndRef>?,
val resolvedReferenceBytes: List<SerializedStateAndRef>?
) : FullTransaction() { ) : FullTransaction() {
@Deprecated("Client code should not instantiate LedgerTransaction.")
constructor(
inputs: List<StateAndRef<ContractState>>,
outputs: List<TransactionState<ContractState>>,
commands: List<CommandWithParties<CommandData>>,
attachments: List<Attachment>,
id: SecureHash,
notary: Party?,
timeWindow: TimeWindow?,
privacySalt: PrivacySalt
) : this(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, null, emptyList(), null, null, null)
@Deprecated("Client code should not instantiate LedgerTransaction.")
constructor(
inputs: List<StateAndRef<ContractState>>,
outputs: List<TransactionState<ContractState>>,
commands: List<CommandWithParties<CommandData>>,
attachments: List<Attachment>,
id: SecureHash,
notary: Party?,
timeWindow: TimeWindow?,
privacySalt: PrivacySalt,
networkParameters: NetworkParameters?
) : this(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, emptyList(), null, null, null)
//DOCEND 1 //DOCEND 1
init { init {
checkBaseInvariants() checkBaseInvariants()
@ -58,19 +86,25 @@ data class LedgerTransaction @JvmOverloads constructor(
checkEncumbrancesValid() checkEncumbrancesValid()
} }
private companion object { companion object {
val logger = loggerFor<LedgerTransaction>() private val logger = loggerFor<LedgerTransaction>()
private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader?): Try<Class<out Contract>> {
return Try.on {
(classLoader ?: this::class.java.classLoader)
.loadClass(className)
.asSubclass(Contract::class.java)
}
}
private fun stateToContractClass(state: TransactionState<ContractState>): Try<Class<out Contract>> { @CordaInternal
return contractClassFor(state.contract, state.data::class.java.classLoader) internal fun makeLedgerTransaction(
} inputs: List<StateAndRef<ContractState>>,
outputs: List<TransactionState<ContractState>>,
commands: List<CommandWithParties<CommandData>>,
attachments: List<Attachment>,
id: SecureHash,
notary: Party?,
timeWindow: TimeWindow?,
privacySalt: PrivacySalt,
networkParameters: NetworkParameters?,
references: List<StateAndRef<ContractState>>,
componentGroups: List<ComponentGroup>,
resolvedInputBytes: List<SerializedStateAndRef>,
resolvedReferenceBytes: List<SerializedStateAndRef>
) = LedgerTransaction(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references, componentGroups, resolvedInputBytes, resolvedReferenceBytes)
} }
val inputStates: List<ContractState> get() = inputs.map { it.state.data } val inputStates: List<ContractState> get() = inputs.map { it.state.data }
@ -88,6 +122,12 @@ data class LedgerTransaction @JvmOverloads constructor(
/** /**
* Verifies this transaction and runs contract code. At this stage it is assumed that signatures have already been verified. * Verifies this transaction and runs contract code. At this stage it is assumed that signatures have already been verified.
* The contract verification logic is run in a custom [AttachmentsClassLoader] created for the current transaction.
* This classloader is only used during verification and does not leak to the client code.
*
* The reason for this is that classes (contract states) deserialized in this classloader would actually be a different type from what
* the calling code would expect.
* *
* @throws TransactionVerificationException if anything goes wrong. * @throws TransactionVerificationException if anything goes wrong.
*/ */
@ -95,12 +135,17 @@ data class LedgerTransaction @JvmOverloads constructor(
fun verify() { fun verify() {
val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = getUniqueContractAttachmentsByContract() val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = getUniqueContractAttachmentsByContract()
// TODO - verify for version downgrade AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments) { transactionClassLoader ->
validatePackageOwnership(contractAttachmentsByContract)
validateStatesAgainstContract() val internalTx = createInternalLedgerTransaction()
verifyConstraintsValidity(contractAttachmentsByContract)
verifyConstraints(contractAttachmentsByContract) // TODO - verify for version downgrade
verifyContracts() validatePackageOwnership(contractAttachmentsByContract)
validateStatesAgainstContract(internalTx)
verifyConstraintsValidity(internalTx, contractAttachmentsByContract, transactionClassLoader)
verifyConstraints(internalTx, contractAttachmentsByContract)
verifyContracts(internalTx)
}
} }
/** /**
@ -133,7 +178,7 @@ data class LedgerTransaction @JvmOverloads constructor(
* *
* A warning will be written to the log if any mismatch is detected. * A warning will be written to the log if any mismatch is detected.
*/ */
private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract) private fun validateStatesAgainstContract(internalTx: LedgerTransaction) = internalTx.allStates.forEach { validateStateAgainstContract(it) }
private fun validateStateAgainstContract(state: TransactionState<ContractState>) { private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
state.data.requiredContractClassName?.let { requiredContractClassName -> state.data.requiredContractClassName?.let { requiredContractClassName ->
@ -150,25 +195,25 @@ data class LedgerTransaction @JvmOverloads constructor(
* * Constraints should be one of the valid supported ones. * * Constraints should be one of the valid supported ones.
* * Constraints should propagate correctly if not marked otherwise. * * Constraints should propagate correctly if not marked otherwise.
*/ */
private fun verifyConstraintsValidity(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) { private fun verifyConstraintsValidity(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>, transactionClassLoader: ClassLoader) {
// First check that the constraints are valid. // First check that the constraints are valid.
for (state in allStates) { for (state in internalTx.allStates) {
checkConstraintValidity(state) checkConstraintValidity(state)
} }
// Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic. // Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic.
// This is not required for reference states as there is nothing to propagate. // This is not required for reference states as there is nothing to propagate.
val inputContractGroups = inputs.groupBy { it.state.contract } val inputContractGroups = internalTx.inputs.groupBy { it.state.contract }
val outputContractGroups = outputs.groupBy { it.contract } val outputContractGroups = internalTx.outputs.groupBy { it.contract }
for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) { for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) {
if (contractClassName.contractHasAutomaticConstraintPropagation()) { if (contractClassName.contractHasAutomaticConstraintPropagation(transactionClassLoader)) {
// Verify that the constraints of output states have at least the same level of restriction as the constraints of the corresponding input states. // Verify that the constraints of output states have at least the same level of restriction as the constraints of the corresponding input states.
val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet() val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet()
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet() val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
outputConstraints?.forEach { outputConstraint -> outputConstraints?.forEach { outputConstraint ->
inputConstraints?.forEach { inputConstraint -> inputConstraints?.forEach { inputConstraint ->
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachmentsByContract[contractClassName]!! ))) { if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachmentsByContract[contractClassName]!!))) {
throw TransactionVerificationException.ConstraintPropagationRejection(id, contractClassName, inputConstraint, outputConstraint) throw TransactionVerificationException.ConstraintPropagationRejection(id, contractClassName, inputConstraint, outputConstraint)
} }
} }
@ -186,8 +231,8 @@ data class LedgerTransaction @JvmOverloads constructor(
* *
* @throws TransactionVerificationException if the constraints fail to verify * @throws TransactionVerificationException if the constraints fail to verify
*/ */
private fun verifyConstraints(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) { private fun verifyConstraints(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
for (state in allStates) { for (state in internalTx.allStates) {
val contractAttachment = contractAttachmentsByContract[state.contract] val contractAttachment = contractAttachmentsByContract[state.contract]
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract) ?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
@ -226,38 +271,64 @@ data class LedgerTransaction @JvmOverloads constructor(
return result return result
} }
private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader): Class<out Contract> = try {
classLoader.loadClass(className).asSubclass(Contract::class.java)
} catch (e: Exception) {
throw TransactionVerificationException.ContractCreationError(id, className, e)
}
private fun createInternalLedgerTransaction(): LedgerTransaction {
return if (resolvedInputBytes != null && resolvedReferenceBytes != null && componentGroups != null) {
// Deserialize all relevant classes in the transaction classloader.
val resolvedDeserializedInputs = resolvedInputBytes.map { StateAndRef(it.serializedState.deserialize(), it.ref) }
val resolvedDeserializedReferences = resolvedReferenceBytes.map { StateAndRef(it.serializedState.deserialize(), it.ref) }
val deserializedOutputs = deserialiseComponentGroup(componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true)
val deserializedCommands = deserialiseCommands(this.componentGroups, forceDeserialize = true)
val authenticatedArgs = deserializedCommands.map { cmd ->
val parties = commands.find { it.value.javaClass.name == cmd.value.javaClass.name }!!.signingParties
CommandWithParties(cmd.signers, parties, cmd.value)
}
val ledgerTransactionToVerify = this.copy(
inputs = resolvedDeserializedInputs,
outputs = deserializedOutputs,
commands = authenticatedArgs,
references = resolvedDeserializedReferences)
ledgerTransactionToVerify
} else {
// This branch is only present for backwards compatibility.
// TODO - it should be removed once the constructor of LedgerTransaction is no longer public api.
logger.warn("The LedgerTransaction should not be instantiated directly from client code. Please use WireTransaction.toLedgerTransaction. The result of the verify method might not be accurate.")
this
}
}
/** /**
* Check the transaction is contract-valid by running the verify() for each input and output state contract. * Check the transaction is contract-valid by running the verify() for each input and output state contract.
* If any contract fails to verify, the whole transaction is considered to be invalid. * If any contract fails to verify, the whole transaction is considered to be invalid.
*/ */
private fun verifyContracts() = inputAndOutputStates.forEach { ts -> private fun verifyContracts(internalTx: LedgerTransaction) {
val contractClass = getContractClass(ts) val contractClasses = (internalTx.inputs.map { it.state } + internalTx.outputs).toSet()
val contract = createContractInstance(contractClass) .map { it.contract to contractClassFor(it.contract, it.data.javaClass.classLoader) }
try { val contractInstances = contractClasses.map { (contractClassName, contractClass) ->
contract.verify(this)
} catch (e: Exception) {
throw TransactionVerificationException.ContractRejection(id, contract, e)
}
}
// Obtain the contract class from the class name, wrapping any exception as a [ContractCreationError]
private fun getContractClass(ts: TransactionState<ContractState>): Class<out Contract> =
try {
(ts.data::class.java.classLoader ?: this::class.java.classLoader)
.loadClass(ts.contract)
.asSubclass(Contract::class.java)
} catch (e: Exception) {
throw TransactionVerificationException.ContractCreationError(id, ts.contract, e)
}
// Obtain an instance of the contract class, wrapping any exception as a [ContractCreationError]
private fun createContractInstance(contractClass: Class<out Contract>): Contract =
try { try {
contractClass.newInstance() contractClass.newInstance()
} catch (e: Exception) { } catch (e: Exception) {
throw TransactionVerificationException.ContractCreationError(id, contractClass.name, e) throw TransactionVerificationException.ContractCreationError(id, contractClassName, e)
} }
}
contractInstances.forEach { contract ->
try {
contract.verify(internalTx)
} catch (e: Exception) {
throw TransactionVerificationException.ContractRejection(id, contract, e)
}
}
}
/** /**
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there * Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
@ -286,7 +357,8 @@ data class LedgerTransaction @JvmOverloads constructor(
// b) the number of outputs can contain the encumbrance // b) the number of outputs can contain the encumbrance
// c) the bi-directionality (full cycle) property is satisfied // c) the bi-directionality (full cycle) property is satisfied
// d) encumbered output states are assigned to the same notary. // d) encumbered output states are assigned to the same notary.
val statesAndEncumbrance = outputs.withIndex().filter { it.value.encumbrance != null }.map { Pair(it.index, it.value.encumbrance!!) } val statesAndEncumbrance = outputs.withIndex().filter { it.value.encumbrance != null }
.map { Pair(it.index, it.value.encumbrance!!) }
if (!statesAndEncumbrance.isEmpty()) { if (!statesAndEncumbrance.isEmpty()) {
checkBidirectionalOutputEncumbrances(statesAndEncumbrance) checkBidirectionalOutputEncumbrances(statesAndEncumbrance)
checkNotariesOutputEncumbrance(statesAndEncumbrance) checkNotariesOutputEncumbrance(statesAndEncumbrance)

View File

@ -6,14 +6,14 @@ import net.corda.core.contracts.*
import net.corda.core.contracts.ComponentGroupEnum.* import net.corda.core.contracts.ComponentGroupEnum.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.LazyMappedList import net.corda.core.internal.deserialiseCommands
import net.corda.core.internal.uncheckedCast import net.corda.core.internal.deserialiseComponentGroup
import net.corda.core.serialization.* import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.lazyMapped
import java.security.PublicKey import java.security.PublicKey
import java.util.function.Predicate import java.util.function.Predicate
import kotlin.reflect.KClass
/** /**
* Implemented by [WireTransaction] and [FilteredTransaction]. A TraversableTransaction allows you to iterate * Implemented by [WireTransaction] and [FilteredTransaction]. A TraversableTransaction allows you to iterate
@ -23,27 +23,27 @@ import kotlin.reflect.KClass
*/ */
abstract class TraversableTransaction(open val componentGroups: List<ComponentGroup>) : CoreTransaction() { abstract class TraversableTransaction(open val componentGroups: List<ComponentGroup>) : CoreTransaction() {
/** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */ /** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */
val attachments: List<SecureHash> = deserialiseComponentGroup(SecureHash::class, ATTACHMENTS_GROUP) val attachments: List<SecureHash> = deserialiseComponentGroup(componentGroups, SecureHash::class, ATTACHMENTS_GROUP)
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */ /** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
override val inputs: List<StateRef> = deserialiseComponentGroup(StateRef::class, INPUTS_GROUP) override val inputs: List<StateRef> = deserialiseComponentGroup(componentGroups, StateRef::class, INPUTS_GROUP)
/** Pointers to reference states, identified by (tx identity hash, output index). */ /** Pointers to reference states, identified by (tx identity hash, output index). */
override val references: List<StateRef> = deserialiseComponentGroup(StateRef::class, REFERENCES_GROUP) override val references: List<StateRef> = deserialiseComponentGroup(componentGroups, StateRef::class, REFERENCES_GROUP)
override val outputs: List<TransactionState<ContractState>> = deserialiseComponentGroup(TransactionState::class, OUTPUTS_GROUP, attachmentsContext = true) override val outputs: List<TransactionState<ContractState>> = deserialiseComponentGroup(componentGroups, TransactionState::class, OUTPUTS_GROUP)
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */ /** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
val commands: List<Command<*>> = deserialiseCommands() val commands: List<Command<*>> = deserialiseCommands(componentGroups)
override val notary: Party? = let { override val notary: Party? = let {
val notaries: List<Party> = deserialiseComponentGroup(Party::class, NOTARY_GROUP) val notaries: List<Party> = deserialiseComponentGroup(componentGroups, Party::class, NOTARY_GROUP)
check(notaries.size <= 1) { "Invalid Transaction. More than 1 notary party detected." } check(notaries.size <= 1) { "Invalid Transaction. More than 1 notary party detected." }
notaries.firstOrNull() notaries.firstOrNull()
} }
val timeWindow: TimeWindow? = let { val timeWindow: TimeWindow? = let {
val timeWindows: List<TimeWindow> = deserialiseComponentGroup(TimeWindow::class, TIMEWINDOW_GROUP) val timeWindows: List<TimeWindow> = deserialiseComponentGroup(componentGroups, TimeWindow::class, TIMEWINDOW_GROUP)
check(timeWindows.size <= 1) { "Invalid Transaction. More than 1 time-window detected." } check(timeWindows.size <= 1) { "Invalid Transaction. More than 1 time-window detected." }
timeWindows.firstOrNull() timeWindows.firstOrNull()
} }
@ -66,65 +66,6 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
timeWindow?.let { result += listOf(it) } timeWindow?.let { result += listOf(it) }
return result return result
} }
// Helper function to return a meaningful exception if deserialisation of a component fails.
private fun <T : Any> deserialiseComponentGroup(clazz: KClass<T>,
groupEnum: ComponentGroupEnum,
attachmentsContext: Boolean = false): List<T> {
val group = componentGroups.firstOrNull { it.groupIndex == groupEnum.ordinal }
if (group == null || group.components.isEmpty()) {
return emptyList()
}
// If the componentGroup is a [LazyMappedList] it means that the original deserialized version is already available.
val components = group.components
if (components is LazyMappedList<*, OpaqueBytes>) {
return components.originalList as List<T>
}
val factory = SerializationFactory.defaultFactory
val context = factory.defaultContext.let { if (attachmentsContext) it.withAttachmentsClassLoader(attachments) else it }
return components.lazyMapped { component, internalIndex ->
try {
factory.deserialize(component, clazz.java , context)
} catch (e: MissingAttachmentsException) {
throw e
} catch (e: Exception) {
throw Exception("Malformed transaction, $groupEnum at index $internalIndex cannot be deserialised", e)
}
}
}
// Method to deserialise Commands from its two groups:
// COMMANDS_GROUP which contains the CommandData part
// and SIGNERS_GROUP which contains the Signers part.
private fun deserialiseCommands(): List<Command<*>> {
// TODO: we could avoid deserialising unrelated signers.
// However, current approach ensures the transaction is not malformed
// and it will throw if any of the signers objects is not List of public keys).
val signersList: List<List<PublicKey>> = uncheckedCast(deserialiseComponentGroup(List::class, SIGNERS_GROUP))
val commandDataList: List<CommandData> = deserialiseComponentGroup(CommandData::class, COMMANDS_GROUP, attachmentsContext = true)
val group = componentGroups.firstOrNull { it.groupIndex == COMMANDS_GROUP.ordinal }
return if (group is FilteredComponentGroup) {
check(commandDataList.size <= signersList.size) {
"Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects"
}
val componentHashes = group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }
val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) }
if (leafIndices.isNotEmpty())
check(leafIndices.max()!! < signersList.size) { "Invalid Transaction. A command with no corresponding signer detected" }
commandDataList.lazyMapped { commandData, index -> Command(commandData, signersList[leafIndices[index]]) }
} else {
// It is a WireTransaction
// or a FilteredTransaction with no Commands (in which case group is null).
check(commandDataList.size == signersList.size) {
"Invalid Transaction. Sizes of CommandData (${commandDataList.size}) and Signers (${signersList.size}) do not match"
}
commandDataList.lazyMapped { commandData, index -> Command(commandData, signersList[index]) }
}
}
} }
/** /**

View File

@ -1,5 +1,6 @@
package net.corda.core.transactions package net.corda.core.transactions
import net.corda.core.CordaInternal
import net.corda.core.DeleteForDJVM import net.corda.core.DeleteForDJVM
import net.corda.core.KeepForDJVM import net.corda.core.KeepForDJVM
import net.corda.core.contracts.* import net.corda.core.contracts.*
@ -10,6 +11,7 @@ import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.transactions.NotaryChangeWireTransaction.Component.* import net.corda.core.transactions.NotaryChangeWireTransaction.Component.*
@ -75,6 +77,20 @@ data class NotaryChangeWireTransaction(
@DeleteForDJVM @DeleteForDJVM
fun resolve(services: ServiceHub, sigs: List<TransactionSignature>) = resolve(services as ServicesForResolution, sigs) fun resolve(services: ServiceHub, sigs: List<TransactionSignature>) = resolve(services as ServicesForResolution, sigs)
/**
* This should return a serialized virtual output state, that will be used to verify spending transactions.
* The binary output should not depend on the classpath of the node that is verifying the transaction.
*
* Ideally the serialization engine would support partial deserialization so that only the Notary ( and the encumbrance can be replaced from the binary input state)
*
*
* TODO - currently this uses the main classloader.
*/
@CordaInternal
internal fun resolveOutputComponent(services: ServicesForResolution, stateRef: StateRef): SerializedBytes<TransactionState<ContractState>> {
return services.loadState(stateRef).serialize()
}
enum class Component { enum class Component {
INPUTS, NOTARY, NEW_NOTARY INPUTS, NOTARY, NEW_NOTARY
} }

View File

@ -270,7 +270,7 @@ open class TransactionBuilder @JvmOverloads constructor(
} }
// The final step is to resolve AutomaticPlaceholderConstraint. // The final step is to resolve AutomaticPlaceholderConstraint.
val automaticConstraintPropagation = contractClassName.contractHasAutomaticConstraintPropagation(serializationContext?.deserializationClassLoader) val automaticConstraintPropagation = contractClassName.contractHasAutomaticConstraintPropagation(inputsAndOutputs.first().data::class.java.classLoader)
// When automaticConstraintPropagation is disabled for a contract, output states must an explicit Constraint. // When automaticConstraintPropagation is disabled for a contract, output states must an explicit Constraint.
require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." } require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." }

View File

@ -7,11 +7,15 @@ import net.corda.core.contracts.*
import net.corda.core.contracts.ComponentGroupEnum.* import net.corda.core.contracts.ComponentGroupEnum.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.SerializedStateAndRef
import net.corda.core.internal.Emoji import net.corda.core.internal.Emoji
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.lazyMapped import net.corda.core.utilities.lazyMapped
@ -99,7 +103,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
return toLedgerTransactionInternal( return toLedgerTransactionInternal(
resolveIdentity = { services.identityService.partyFromKey(it) }, resolveIdentity = { services.identityService.partyFromKey(it) },
resolveAttachment = { services.attachments.openAttachment(it) }, resolveAttachment = { services.attachments.openAttachment(it) },
resolveStateRef = { services.loadState(it) }, resolveStateRefComponent = { resolveStateRefBinaryComponent(it, services) },
networkParameters = services.networkParameters networkParameters = services.networkParameters
) )
} }
@ -119,13 +123,14 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
resolveStateRef: (StateRef) -> TransactionState<*>?, resolveStateRef: (StateRef) -> TransactionState<*>?,
@Suppress("UNUSED_PARAMETER") resolveContractAttachment: (TransactionState<ContractState>) -> AttachmentId? @Suppress("UNUSED_PARAMETER") resolveContractAttachment: (TransactionState<ContractState>) -> AttachmentId?
): LedgerTransaction { ): LedgerTransaction {
return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, resolveStateRef, null) // This reverts to serializing the resolved transaction state.
return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, { stateRef -> resolveStateRef(stateRef)?.serialize() }, null)
} }
private fun toLedgerTransactionInternal( private fun toLedgerTransactionInternal(
resolveIdentity: (PublicKey) -> Party?, resolveIdentity: (PublicKey) -> Party?,
resolveAttachment: (SecureHash) -> Attachment?, resolveAttachment: (SecureHash) -> Attachment?,
resolveStateRef: (StateRef) -> TransactionState<*>?, resolveStateRefComponent: (StateRef) -> SerializedBytes<TransactionState<ContractState>>?,
networkParameters: NetworkParameters? networkParameters: NetworkParameters?
): LedgerTransaction { ): LedgerTransaction {
// Look up public keys to authenticated identities. // Look up public keys to authenticated identities.
@ -133,20 +138,38 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
val parties = cmd.signers.mapNotNull { pk -> resolveIdentity(pk) } val parties = cmd.signers.mapNotNull { pk -> resolveIdentity(pk) }
CommandWithParties(cmd.signers, parties, cmd.value) CommandWithParties(cmd.signers, parties, cmd.value)
} }
val resolvedInputs = inputs.lazyMapped { ref, _ ->
resolveStateRef(ref)?.let { StateAndRef(it, ref) } ?: throw TransactionResolutionException(ref.txhash) val resolvedInputBytes = inputs.map { ref ->
SerializedStateAndRef(resolveStateRefComponent(ref)
?: throw TransactionResolutionException(ref.txhash), ref)
} }
val resolvedReferences = references.lazyMapped { ref, _ -> val resolvedInputs = resolvedInputBytes.lazyMapped { (serialized, ref), _ ->
resolveStateRef(ref)?.let { StateAndRef(it, ref) } ?: throw TransactionResolutionException(ref.txhash) StateAndRef(serialized.deserialize(), ref)
} }
val resolvedReferenceBytes = references.map { ref ->
SerializedStateAndRef(resolveStateRefComponent(ref)
?: throw TransactionResolutionException(ref.txhash), ref)
}
val resolvedReferences = resolvedReferenceBytes.lazyMapped { (serialized, ref), _ ->
StateAndRef(serialized.deserialize(), ref)
}
val attachments = attachments.lazyMapped { att, _ -> val attachments = attachments.lazyMapped { att, _ ->
resolveAttachment(att) ?: throw AttachmentResolutionException(att) resolveAttachment(att) ?: throw AttachmentResolutionException(att)
} }
val ltx = LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt, networkParameters, resolvedReferences)
checkTransactionSize(ltx, networkParameters?.maxTransactionSize ?: 10485760) val ltx = LedgerTransaction.makeLedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt, networkParameters, resolvedReferences, componentGroups, resolvedInputBytes, resolvedReferenceBytes)
checkTransactionSize(ltx, networkParameters?.maxTransactionSize ?: DEFAULT_MAX_TX_SIZE)
return ltx return ltx
} }
/**
* Deterministic function that checks if the transaction is below the maximum allowed size.
* It uses the binary representation of transactions.
*/
private fun checkTransactionSize(ltx: LedgerTransaction, maxTransactionSize: Int) { private fun checkTransactionSize(ltx: LedgerTransaction, maxTransactionSize: Int) {
var remainingTransactionSize = maxTransactionSize var remainingTransactionSize = maxTransactionSize
@ -164,9 +187,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
// it's likely that the same underlying Attachment CorDapp will occur more than once so we dedup on the attachment id. // it's likely that the same underlying Attachment CorDapp will occur more than once so we dedup on the attachment id.
ltx.attachments.distinctBy { it.id }.forEach { minus(it.size) } ltx.attachments.distinctBy { it.id }.forEach { minus(it.size) }
// TODO - these can be optimized by creating a LazyStateAndRef class, that just stores (a pointer) the serialized output componentGroup from the previous transaction. minus(ltx.resolvedInputBytes!!.sumBy { it.serializedState.size })
minus(ltx.references.serialize().size) minus(ltx.resolvedReferenceBytes!!.sumBy { it.serializedState.size })
minus(ltx.inputs.serialize().size)
// For Commands and outputs we can use the component groups as they are already serialized. // For Commands and outputs we can use the component groups as they are already serialized.
minus(componentGroupSize(COMMANDS_GROUP)) minus(componentGroupSize(COMMANDS_GROUP))
@ -253,6 +275,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
} }
companion object { companion object {
private const val DEFAULT_MAX_TX_SIZE = 10485760
/** /**
* Creating list of [ComponentGroup] used in one of the constructors of [WireTransaction] required * Creating list of [ComponentGroup] used in one of the constructors of [WireTransaction] required
* for backwards compatibility purposes. * for backwards compatibility purposes.
@ -281,6 +305,28 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers }.lazyMapped(serialize))) if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers }.lazyMapped(serialize)))
return componentGroupMap return componentGroupMap
} }
/**
* This is the main logic that knows how to retrieve the binary representation of [StateRef]s.
*
* For [ContractUpgradeWireTransaction] or [NotaryChangeWireTransaction] it knows how to recreate the output state in the correct classloader independent of the node's classpath.
*/
@CordaInternal
fun resolveStateRefBinaryComponent(stateRef: StateRef, services: ServicesForResolution): SerializedBytes<TransactionState<ContractState>>? {
return if (services is ServiceHub) {
val coreTransaction = services.validatedTransactions.getTransaction(stateRef.txhash)?.coreTransaction
?: throw TransactionResolutionException(stateRef.txhash)
when (coreTransaction) {
is WireTransaction -> coreTransaction.componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.OUTPUTS_GROUP.ordinal }?.components?.get(stateRef.index) as SerializedBytes<TransactionState<ContractState>>?
is ContractUpgradeWireTransaction -> coreTransaction.resolveOutputComponent(services, stateRef)
is NotaryChangeWireTransaction -> coreTransaction.resolveOutputComponent(services, stateRef)
else -> throw UnsupportedOperationException("Attempting to resolve input ${stateRef.index} of a ${coreTransaction.javaClass} transaction. This is not supported.")
}
} else {
// For backwards compatibility revert to using the node classloader.
services.loadState(stateRef).serialize()
}
}
} }
@DeleteForDJVM @DeleteForDJVM

View File

@ -6,6 +6,7 @@ import net.corda.core.DeleteForDJVM
import net.corda.core.KeepForDJVM import net.corda.core.KeepForDJVM
import net.corda.core.internal.LazyMappedList import net.corda.core.internal.LazyMappedList
import net.corda.core.internal.concurrent.get import net.corda.core.internal.concurrent.get
import net.corda.core.internal.createSimpleCache
import net.corda.core.internal.uncheckedCast import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import org.slf4j.Logger import org.slf4j.Logger
@ -149,9 +150,7 @@ fun <V> Future<V>.getOrThrow(timeout: Duration? = null): V = try {
fun <T, U> List<T>.lazyMapped(transform: (T, Int) -> U): List<U> = LazyMappedList(this, transform) fun <T, U> List<T>.lazyMapped(transform: (T, Int) -> U): List<U> = LazyMappedList(this, transform)
private const val MAX_SIZE = 100 private const val MAX_SIZE = 100
private val warnings = Collections.newSetFromMap(object : LinkedHashMap<String, Boolean>() { private val warnings = Collections.newSetFromMap(createSimpleCache<String, Boolean>(MAX_SIZE))
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Boolean>?) = size > MAX_SIZE
})
/** /**
* Utility to help log a warning message only once. * Utility to help log a warning message only once.

View File

@ -14,18 +14,13 @@ import net.corda.core.internal.FetchAttachmentsFlow
import net.corda.core.internal.FetchDataFlow import net.corda.core.internal.FetchDataFlow
import net.corda.core.internal.hash import net.corda.core.internal.hash
import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.*
import net.corda.testing.core.BOB_NAME import net.corda.testing.internal.fakeAttachment
import net.corda.testing.core.makeUnique
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.InternalMockNodeParameters
import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.TestStartedNode
import org.junit.AfterClass import org.junit.AfterClass
import org.junit.Test import org.junit.Test
import java.io.ByteArrayOutputStream
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
class AttachmentTests : WithMockNet { class AttachmentTests : WithMockNet {
companion object { companion object {
@ -46,7 +41,7 @@ class AttachmentTests : WithMockNet {
@Test @Test
fun `download and store`() { fun `download and store`() {
// Insert an attachment into node zero's store directly. // Insert an attachment into node zero's store directly.
val id = aliceNode.importAttachment(fakeAttachment()) val id = aliceNode.importAttachment(fakeAttachment("file1.txt", "Some useful content"))
// Get node one to run a flow to fetch it and insert it. // Get node one to run a flow to fetch it and insert it.
assert.that( assert.that(
@ -87,7 +82,7 @@ class AttachmentTests : WithMockNet {
val badAlice = badAliceNode.info.singleIdentity() val badAlice = badAliceNode.info.singleIdentity()
// Insert an attachment into node zero's store directly. // Insert an attachment into node zero's store directly.
val attachment = fakeAttachment() val attachment = fakeAttachment("file1.txt", "Some useful content")
val id = badAliceNode.importAttachment(attachment) val id = badAliceNode.importAttachment(attachment)
// Corrupt its store. // Corrupt its store.
@ -134,18 +129,6 @@ class AttachmentTests : WithMockNet {
} }
}).apply { registerInitiatedFlow(FetchAttachmentsResponse::class.java) } }).apply { registerInitiatedFlow(FetchAttachmentsResponse::class.java) }
private fun fakeAttachment(): ByteArray =
ByteArrayOutputStream().use { baos ->
JarOutputStream(baos).use { jos ->
jos.putNextEntry(ZipEntry("file1.txt"))
jos.writer().apply {
append("Some useful content")
flush()
}
jos.closeEntry()
}
baos.toByteArray()
}
//endregion //endregion
//region Operations //region Operations

View File

@ -0,0 +1,98 @@
package net.corda.core.transactions
import net.corda.core.contracts.Contract
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.declaredField
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
import net.corda.core.serialization.serialize
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.OpaqueBytes
import net.corda.nodeapi.DummyContractBackdoor
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.fakeAttachment
import net.corda.testing.services.MockAttachmentStorage
import org.apache.commons.io.IOUtils
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import java.io.NotSerializableException
import java.net.URL
import kotlin.test.assertFailsWith
class AttachmentsClassLoaderSerializationTests {
companion object {
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderSerializationTests::class.java.getResource("isolated.jar")
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
val storage = MockAttachmentStorage()
@Test
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 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(arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! }) { classLoader ->
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader)
val contract = contractClass.newInstance() as Contract
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
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 serialisedState = state.serialize()
val state1 = serialisedState.deserialize()
assertEquals(state, state1)
serialisedState
}
assertFailsWith<NotSerializableException> {
serialisedState.deserialize()
}
}
// These tests are not Attachment specific. Should they be removed?
@Test
fun `test serialization of SecureHash`() {
val secureHash = SecureHash.randomSHA256()
val bytes = secureHash.serialize()
val copiedSecuredHash = bytes.deserialize()
assertEquals(secureHash, copiedSecuredHash)
}
@Test
fun `test serialization of OpaqueBytes`() {
val opaqueBytes = OpaqueBytes("0123456789".toByteArray())
val bytes = opaqueBytes.serialize()
val copiedOpaqueBytes = bytes.deserialize()
assertEquals(opaqueBytes, copiedOpaqueBytes)
}
@Test
fun `test serialization of sub-sequence OpaqueBytes`() {
val bytesSequence = ByteSequence.of("0123456789".toByteArray(), 3, 2)
val bytes = bytesSequence.serialize()
val copiedBytesSequence = bytes.deserialize()
assertEquals(bytesSequence, copiedBytesSequence)
}
}

View File

@ -0,0 +1,102 @@
package net.corda.core.transactions
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.Contract
import net.corda.core.internal.declaredField
import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.testing.internal.fakeAttachment
import net.corda.testing.services.MockAttachmentStorage
import org.apache.commons.io.IOUtils
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.net.URL
import kotlin.test.assertFailsWith
class AttachmentsClassLoaderTests {
companion object {
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderTests::class.java.getResource("isolated.jar")
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
ByteArrayOutputStream().use {
attachment.extractFile(filepath, it)
return it.toByteArray()
}
}
}
val storage = MockAttachmentStorage()
@Test
fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() {
assertFailsWith<ClassNotFoundException> {
Class.forName(ISOLATED_CONTRACT_CLASS_NAME)
}
}
@Test
fun `Dynamically load AnotherDummyContract from isolated contracts jar using the AttachmentsClassLoader`() {
val isolatedId = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
val classloader = AttachmentsClassLoader(listOf(storage.openAttachment(isolatedId)!!))
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader)
val contract = contractClass.newInstance() as Contract
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
}
@Test
fun `Load text resources from AttachmentsClassLoader`() {
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 cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
assertEquals("some data", txt)
val txt1 = IOUtils.toString(cl.getResourceAsStream("file2.txt"), Charsets.UTF_8.name())
assertEquals("some other data", txt1)
}
@Test
fun `Test overlapping file exception`() {
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
val att2 = storage.importAttachment(fakeAttachment("file1.txt", "some other data").inputStream(), "app", "file2.jar")
assertFailsWith(AttachmentsClassLoader.Companion.OverlappingAttachments::class) {
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
}
}
@Test
fun `No overlapping exception thrown on certain META-INF files`() {
listOf("meta-inf/manifest.mf", "meta-inf/license", "meta-inf/test.dsa", "meta-inf/test.sf").forEach { path ->
val att1 = storage.importAttachment(fakeAttachment(path, "some data").inputStream(), "app", "file1.jar")
val att2 = storage.importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar")
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
}
}
@Test
fun `Check platform independent path handling in attachment jars`() {
val storage = MockAttachmentStorage()
val att1 = storage.importAttachment(fakeAttachment("/folder1/foldera/file1.txt", "some data").inputStream(), "app", "file1.jar")
val att2 = storage.importAttachment(fakeAttachment("\\folder1\\folderb\\file2.txt", "some other data").inputStream(), "app", "file2.jar")
val data1a = readAttachment(storage.openAttachment(att1)!!, "/folder1/foldera/file1.txt")
assertArrayEquals("some data".toByteArray(), data1a)
val data1b = readAttachment(storage.openAttachment(att1)!!, "\\folder1\\foldera\\file1.txt")
assertArrayEquals("some data".toByteArray(), data1b)
val data2a = readAttachment(storage.openAttachment(att2)!!, "\\folder1\\folderb\\file2.txt")
assertArrayEquals("some other data".toByteArray(), data2a)
val data2b = readAttachment(storage.openAttachment(att2)!!, "/folder1/folderb/file2.txt")
assertArrayEquals("some other data".toByteArray(), data2b)
}
}

View File

@ -10,6 +10,7 @@ import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.* import net.corda.testing.core.*
import net.corda.testing.internal.createWireTransaction import net.corda.testing.internal.createWireTransaction
import net.corda.testing.internal.fakeAttachment
import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.rigorousMock
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -118,7 +119,8 @@ class TransactionTests {
val commands = emptyList<CommandWithParties<CommandData>>() val commands = emptyList<CommandWithParties<CommandData>>()
val attachments = listOf<Attachment>(ContractAttachment(rigorousMock<Attachment>().also { val attachments = listOf<Attachment>(ContractAttachment(rigorousMock<Attachment>().also {
doReturn(SecureHash.zeroHash).whenever(it).id doReturn(SecureHash.zeroHash).whenever(it).id
}, DummyContract.PROGRAM_ID)) doReturn(fakeAttachment("nothing", "nada").inputStream()).whenever(it).open()
}, DummyContract.PROGRAM_ID, uploader = "app"))
val id = SecureHash.randomSHA256() val id = SecureHash.randomSHA256()
val timeWindow: TimeWindow? = null val timeWindow: TimeWindow? = null
val privacySalt = PrivacySalt() val privacySalt = PrivacySalt()

View File

@ -7,6 +7,11 @@ release, see :doc:`upgrade-notes`.
Unreleased Unreleased
---------- ----------
* Deprecated `SerializationContext.withAttachmentsClassLoader`. This functionality has always been disabled by flags
and there is no reason for a CorDapp developer to use it. It is just an internal implementation detail of Corda.
* Deprecated the `LedgerTransaction` constructor. No client code should call it directly. LedgerTransactions can be created from WireTransactions if required.
* Introduced new optional network bootstrapper command line options (--register-package-owner, --unregister-package-owner) * Introduced new optional network bootstrapper command line options (--register-package-owner, --unregister-package-owner)
to register/unregister a java package namespace with an associated owner in the network parameter packageOwnership whitelist. to register/unregister a java package namespace with an associated owner in the network parameter packageOwnership whitelist.

View File

@ -25,6 +25,7 @@ import net.corda.testing.contracts.DummyState
import net.corda.testing.core.* import net.corda.testing.core.*
import net.corda.testing.dsl.* import net.corda.testing.dsl.*
import net.corda.testing.internal.TEST_TX_TIME import net.corda.testing.internal.TEST_TX_TIME
import net.corda.testing.internal.fakeAttachment
import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.vault.CommodityState import net.corda.testing.internal.vault.CommodityState
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
@ -565,7 +566,7 @@ class ObligationTests {
@Test @Test
fun `commodity settlement`() { fun `commodity settlement`() {
val commodityContractBytes = "https://www.big-book-of-banking-law.gov/commodity-claims.html".toByteArray() val commodityContractBytes = fakeAttachment("file1.txt", "https://www.big-book-of-banking-law.gov/commodity-claims.html")
val defaultFcoj = Issued(defaultIssuer, Commodity.getInstance("FCOJ")!!) val defaultFcoj = Issued(defaultIssuer, Commodity.getInstance("FCOJ")!!)
val oneUnitFcoj = Amount(1, defaultFcoj) val oneUnitFcoj = Amount(1, defaultFcoj)
val obligationDef = Obligation.Terms(NonEmptySet.of(commodityContractBytes.sha256() as SecureHash), NonEmptySet.of(defaultFcoj), TEST_TX_TIME) val obligationDef = Obligation.Terms(NonEmptySet.of(commodityContractBytes.sha256() as SecureHash), NonEmptySet.of(defaultFcoj), TEST_TX_TIME)
@ -957,7 +958,7 @@ class ObligationTests {
assertEquals(expected, actual) assertEquals(expected, actual)
} }
private val cashContractBytes = "https://www.big-book-of-banking-law.gov/cash-claims.html".toByteArray() private val cashContractBytes = fakeAttachment("file1.txt", "https://www.big-book-of-banking-law.gov/cash-claims.html")
private val Issued<Currency>.OBLIGATION_DEF: Obligation.Terms<Currency> private val Issued<Currency>.OBLIGATION_DEF: Obligation.Terms<Currency>
get() = Obligation.Terms(NonEmptySet.of(cashContractBytes.sha256() as SecureHash), NonEmptySet.of(this), TEST_TX_TIME) get() = Obligation.Terms(NonEmptySet.of(cashContractBytes.sha256() as SecureHash), NonEmptySet.of(this), TEST_TX_TIME)
private val Amount<Issued<Currency>>.OBLIGATION: Obligation.State<Currency> private val Amount<Issued<Currency>>.OBLIGATION: Obligation.State<Currency>

View File

@ -61,7 +61,8 @@ class CordaPersistence(
schemas: Set<MappedSchema>, schemas: Set<MappedSchema>,
val jdbcUrl: String, val jdbcUrl: String,
cacheFactory: NamedCacheFactory, cacheFactory: NamedCacheFactory,
attributeConverters: Collection<AttributeConverter<*, *>> = emptySet() attributeConverters: Collection<AttributeConverter<*, *>> = emptySet(),
customClassLoader: ClassLoader? = null
) : Closeable { ) : Closeable {
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()
@ -70,7 +71,7 @@ class CordaPersistence(
private val defaultIsolationLevel = databaseConfig.transactionIsolationLevel private val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
val hibernateConfig: HibernateConfiguration by lazy { val hibernateConfig: HibernateConfiguration by lazy {
transaction { transaction {
HibernateConfiguration(schemas, databaseConfig, attributeConverters, jdbcUrl, cacheFactory) HibernateConfiguration(schemas, databaseConfig, attributeConverters, jdbcUrl, cacheFactory, customClassLoader)
} }
} }

View File

@ -32,7 +32,7 @@ class HibernateConfiguration(
private val attributeConverters: Collection<AttributeConverter<*, *>>, private val attributeConverters: Collection<AttributeConverter<*, *>>,
private val jdbcUrl: String, private val jdbcUrl: String,
cacheFactory: NamedCacheFactory, cacheFactory: NamedCacheFactory,
val cordappClassLoader: ClassLoader? = null val customClassLoader: ClassLoader? = null
) { ) {
companion object { companion object {
private val logger = contextLogger() private val logger = contextLogger()
@ -86,7 +86,7 @@ class HibernateConfiguration(
schema.mappedTypes.forEach { config.addAnnotatedClass(it) } schema.mappedTypes.forEach { config.addAnnotatedClass(it) }
} }
val sessionFactory = buildSessionFactory(config, metadataSources, cordappClassLoader) val sessionFactory = buildSessionFactory(config, metadataSources, customClassLoader)
logger.info("Created session factory for schemas: $schemas") logger.info("Created session factory for schemas: $schemas")
// export Hibernate JMX statistics // export Hibernate JMX statistics
@ -112,13 +112,13 @@ class HibernateConfiguration(
} }
} }
private fun buildSessionFactory(config: Configuration, metadataSources: MetadataSources, cordappClassLoader: ClassLoader?): SessionFactory { private fun buildSessionFactory(config: Configuration, metadataSources: MetadataSources, customClassLoader: ClassLoader?): SessionFactory {
config.standardServiceRegistryBuilder.applySettings(config.properties) config.standardServiceRegistryBuilder.applySettings(config.properties)
if (cordappClassLoader != null) { if (customClassLoader != null) {
config.standardServiceRegistryBuilder.addService( config.standardServiceRegistryBuilder.addService(
ClassLoaderService::class.java, ClassLoaderService::class.java,
ClassLoaderServiceImpl(cordappClassLoader)) ClassLoaderServiceImpl(customClassLoader))
} }
val metadataBuilder = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build()) val metadataBuilder = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build())

View File

@ -69,10 +69,10 @@ class LargeTransactionsTest {
fun checkCanSendLargeTransactions() { fun checkCanSendLargeTransactions() {
// These 4 attachments yield a transaction that's got >10mb attached, so it'd push us over the Artemis // These 4 attachments yield a transaction that's got >10mb attached, so it'd push us over the Artemis
// max message size. // max message size.
val bigFile1 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 0) val bigFile1 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 0, "a")
val bigFile2 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 1) val bigFile2 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 1, "b")
val bigFile3 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 2) val bigFile3 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 2, "c")
val bigFile4 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 3) val bigFile4 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 3, "d")
driver(DriverParameters( driver(DriverParameters(
startNodesInProcess = true, startNodesInProcess = true,
extraCordappPackagesToScan = listOf("net.corda.testing.contracts"), extraCordappPackagesToScan = listOf("net.corda.testing.contracts"),

View File

@ -112,8 +112,6 @@ public class CordaCaplet extends Capsule {
// If it fails, just return the existing class path. The main Corda jar will detect the error and fail gracefully. // If it fails, just return the existing class path. The main Corda jar will detect the error and fail gracefully.
return cp; return cp;
} }
// Add additional directories of JARs to the classpath (at the end), e.g., for JDBC drivers.
augmentClasspath((List<Path>) cp, cordappsDir);
try { try {
List<String> jarDirs = nodeConfig.getStringList("jarDirs"); List<String> jarDirs = nodeConfig.getStringList("jarDirs");
log(LOG_VERBOSE, "Configured JAR directories = " + jarDirs); log(LOG_VERBOSE, "Configured JAR directories = " + jarDirs);

View File

@ -155,7 +155,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
identityService::wellKnownPartyFromAnonymous, identityService::wellKnownPartyFromAnonymous,
schemaService, schemaService,
configuration.dataSourceProperties, configuration.dataSourceProperties,
cacheFactory) cacheFactory,
this.cordappLoader.appClassLoader)
init { init {
// TODO Break cyclic dependency // TODO Break cyclic dependency
@ -748,7 +749,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
protected open fun startDatabase() { protected open fun startDatabase() {
val props = configuration.dataSourceProperties val props = configuration.dataSourceProperties
if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.") if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
database.startHikariPool(props, configuration.database, schemaService.internalSchemas(), metricRegistry) database.startHikariPool(props, configuration.database, schemaService.internalSchemas(), metricRegistry, this.cordappLoader.appClassLoader)
// Now log the vendor string as this will also cause a connection to be tested eagerly. // Now log the vendor string as this will also cause a connection to be tested eagerly.
logVendorString(database, log) logVendorString(database, log)
} }
@ -1061,7 +1062,8 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
wellKnownPartyFromAnonymous: (AbstractParty) -> Party?, wellKnownPartyFromAnonymous: (AbstractParty) -> Party?,
schemaService: SchemaService, schemaService: SchemaService,
hikariProperties: Properties, hikariProperties: Properties,
cacheFactory: NamedCacheFactory): CordaPersistence { cacheFactory: NamedCacheFactory,
customClassLoader: ClassLoader?): CordaPersistence {
// Register the AbstractPartyDescriptor so Hibernate doesn't warn when encountering AbstractParty. Unfortunately // Register the AbstractPartyDescriptor so Hibernate doesn't warn when encountering AbstractParty. Unfortunately
// Hibernate warns about not being able to find a descriptor if we don't provide one, but won't use it by default // Hibernate warns about not being able to find a descriptor if we don't provide one, but won't use it by default
// so we end up providing both descriptor and converter. We should re-examine this in later versions to see if // so we end up providing both descriptor and converter. We should re-examine this in later versions to see if
@ -1069,13 +1071,13 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous)) JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
val attributeConverters = listOf(PublicKeyToTextConverter(), AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous)) val attributeConverters = listOf(PublicKeyToTextConverter(), AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
val jdbcUrl = hikariProperties.getProperty("dataSource.url", "") val jdbcUrl = hikariProperties.getProperty("dataSource.url", "")
return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, cacheFactory, attributeConverters) return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, cacheFactory, attributeConverters, customClassLoader)
} }
fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set<MappedSchema>, metricRegistry: MetricRegistry? = null) { fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set<MappedSchema>, metricRegistry: MetricRegistry? = null, classloader: ClassLoader = Thread.currentThread().contextClassLoader) {
try { try {
val dataSource = DataSourceFactory.createDataSource(hikariProperties, metricRegistry = metricRegistry) val dataSource = DataSourceFactory.createDataSource(hikariProperties, metricRegistry = metricRegistry)
val schemaMigration = SchemaMigration(schemas, dataSource, databaseConfig) val schemaMigration = SchemaMigration(schemas, dataSource, databaseConfig, classloader)
schemaMigration.nodeStartup(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L }) schemaMigration.nodeStartup(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L })
start(dataSource) start(dataSource)
} catch (ex: Exception) { } catch (ex: Exception) {

View File

@ -11,7 +11,7 @@ import net.corda.core.internal.writer
import net.corda.core.serialization.internal.CheckpointSerializationContext import net.corda.core.serialization.internal.CheckpointSerializationContext
import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.ClassWhitelist
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.serialization.internal.AttachmentsClassLoader import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.serialization.internal.MutableClassWhitelist import net.corda.serialization.internal.MutableClassWhitelist
import net.corda.serialization.internal.TransientClassWhiteList import net.corda.serialization.internal.TransientClassWhiteList
import net.corda.serialization.internal.amqp.hasCordaSerializable import net.corda.serialization.internal.amqp.hasCordaSerializable

View File

@ -229,7 +229,7 @@ class NodeAttachmentService(
val attachmentImpl = AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad).let { val attachmentImpl = AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad).let {
val contracts = attachment.contractClassNames val contracts = attachment.contractClassNames
if (contracts != null && contracts.isNotEmpty()) { if (contracts != null && contracts.isNotEmpty()) {
ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader, attachment.signers ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader, attachment.signers?.toList()
?: emptyList()) ?: emptyList())
} else { } else {
it it

View File

@ -54,10 +54,10 @@ class MaxTransactionSizeTests {
@Test @Test
fun `check transaction will fail when exceed max transaction size limit`() { fun `check transaction will fail when exceed max transaction size limit`() {
// These 4 attachments yield a transaction that's got ~ 4mb, which will exceed the 3mb max transaction size limit // These 4 attachments yield a transaction that's got ~ 4mb, which will exceed the 3mb max transaction size limit
val bigFile1 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 0) val bigFile1 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 0, "a")
val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 1) val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 1, "b")
val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 2) val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 2, "c")
val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 3) val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 3, "d")
val flow = aliceNode.transaction { val flow = aliceNode.transaction {
val hash1 = aliceNode.importAttachment(bigFile1.inputStream) val hash1 = aliceNode.importAttachment(bigFile1.inputStream)
val hash2 = aliceNode.importAttachment(bigFile2.inputStream) val hash2 = aliceNode.importAttachment(bigFile2.inputStream)
@ -77,10 +77,10 @@ class MaxTransactionSizeTests {
@Test @Test
fun `check transaction will be rejected by counterparty when exceed max transaction size limit`() { fun `check transaction will be rejected by counterparty when exceed max transaction size limit`() {
// These 4 attachments yield a transaction that's got ~ 4mb, which will exceed the 3mb max transaction size limit // These 4 attachments yield a transaction that's got ~ 4mb, which will exceed the 3mb max transaction size limit
val bigFile1 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 0) val bigFile1 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 0, "a")
val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 1) val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 1, "b")
val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 2) val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 2, "c")
val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 3) val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 3, "c")
val flow = aliceNode.transaction { val flow = aliceNode.transaction {
val hash1 = aliceNode.importAttachment(bigFile1.inputStream) val hash1 = aliceNode.importAttachment(bigFile1.inputStream)
val hash2 = aliceNode.importAttachment(bigFile2.inputStream) val hash2 = aliceNode.importAttachment(bigFile2.inputStream)

View File

@ -1,12 +0,0 @@
package net.corda.serialization.internal
import net.corda.core.crypto.SecureHash
/**
* Drop-in replacement for [AttachmentsClassLoaderBuilder] in the serialization module.
* This version is not strongly-coupled to [net.corda.core.node.ServiceHub].
*/
@Suppress("UNUSED", "UNUSED_PARAMETER")
internal class AttachmentsClassLoaderBuilder() {
fun build(attachmentHashes: List<SecureHash>, properties: Map<Any, Any>, deserializationClassLoader: ClassLoader): AttachmentsClassLoader? = null
}

View File

@ -1,115 +0,0 @@
package net.corda.serialization.internal
import net.corda.core.KeepForDJVM
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractAttachment
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.isUploaderTrusted
import net.corda.core.serialization.CordaSerializable
import java.io.ByteArrayOutputStream
import java.io.FileNotFoundException
import java.io.InputStream
import java.net.URL
import java.net.URLConnection
import java.net.URLStreamHandler
import java.security.CodeSigner
import java.security.CodeSource
import java.security.SecureClassLoader
import java.util.*
/**
* A custom ClassLoader that knows how to load classes from a set of attachments. The attachments themselves only
* need to provide JAR streams, and so could be fetched from a database, local disk, etc. Constructing an
* AttachmentsClassLoader is somewhat expensive, as every attachment is scanned to ensure that there are no overlapping
* file paths.
*/
@KeepForDJVM
class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader = ClassLoader.getSystemClassLoader()) : SecureClassLoader(parent) {
private val pathsToAttachments = HashMap<String, Attachment>()
private val idsToAttachments = HashMap<SecureHash, Attachment>()
@CordaSerializable
class OverlappingAttachments(val path: String) : Exception() {
override fun toString() = "Multiple attachments define a file at path $path"
}
init {
require(attachments.mapNotNull { it as? ContractAttachment }.all { isUploaderTrusted(it.uploader) }) {
"Attempting to load Contract Attachments downloaded from the network"
}
for (attachment in attachments) {
attachment.openAsJAR().use { jar ->
while (true) {
val entry = jar.nextJarEntry ?: break
// We already verified that paths are not strange/game playing when we inserted the attachment
// into the storage service. So we don't need to repeat it here.
//
// We forbid files that differ only in case, or path separator to avoid issues for Windows/Mac developers where the
// filesystem tries to be case insensitive. This may break developers who attempt to use ProGuard.
//
// Also convert to Unix path separators as all resource/class lookups will expect this.
val path = entry.name.toLowerCase().replace('\\', '/')
if (path in pathsToAttachments)
throw OverlappingAttachments(path)
pathsToAttachments[path] = attachment
}
}
idsToAttachments[attachment.id] = attachment
}
}
// Example: attachment://0b4fc1327f3bbebf1bfe98330ea402ae035936c3cb6da9bd3e26eeaa9584e74d/some/file.txt
//
// We have to provide a fake stream handler to satisfy the URL class that the scheme is known. But it's not
// a real scheme and we don't register it. It's just here to ensure that there aren't codepaths that could
// lead to data loading that we don't control right here in this class (URLs can have evil security properties!)
private val fakeStreamHandler = object : URLStreamHandler() {
override fun openConnection(u: URL?): URLConnection? {
throw UnsupportedOperationException()
}
}
private fun Attachment.toURL(path: String?) = URL(null, "attachment://$id/" + (path ?: ""), fakeStreamHandler)
override fun findClass(name: String): Class<*> {
val path = name.replace('.', '/').toLowerCase() + ".class"
val attachment = pathsToAttachments[path] ?: throw ClassNotFoundException(name)
val stream = ByteArrayOutputStream()
try {
attachment.extractFile(path, stream)
} catch (e: FileNotFoundException) {
throw ClassNotFoundException(name)
}
val bytes = stream.toByteArray()
// We don't attempt to propagate signatures from the JAR into the codesource, because our sandbox does not
// depend on external policy files to specify what it can do, so the data wouldn't be useful.
val codesource = CodeSource(attachment.toURL(null), emptyArray<CodeSigner>())
// TODO: Define an empty ProtectionDomain to start enforcing the standard Java sandbox.
// The standard Java sandbox is insufficient for our needs and a much more sophisticated sandboxing
// ClassLoader will appear here in future, but it can't hurt to use the default one too: defence in depth!
return defineClass(name, bytes, 0, bytes.size, codesource)
}
override fun findResource(name: String): URL? {
val attachment = pathsToAttachments[name.toLowerCase()] ?: return null
return attachment.toURL(name)
}
override fun getResourceAsStream(name: String): InputStream? {
val url = getResource(name) ?: return null // May check parent classloaders, for example.
if (url.protocol != "attachment") return null
val attachment = idsToAttachments[SecureHash.parse(url.host)] ?: return null
val path = url.path?.substring(1) ?: return null // Chop off the leading slash.
return try {
val stream = ByteArrayOutputStream()
attachment.extractFile(path, stream)
stream.toByteArray().inputStream()
} catch (e: FileNotFoundException) {
null
}
}
}

View File

@ -16,15 +16,6 @@ data class CheckpointSerializationContextImpl @JvmOverloads constructor(
override val objectReferencesEnabled: Boolean, override val objectReferencesEnabled: Boolean,
override val encoding: SerializationEncoding?, override val encoding: SerializationEncoding?,
override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist) : CheckpointSerializationContext { override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist) : CheckpointSerializationContext {
/**
* {@inheritDoc}
*
* Unsupported for checkpoints.
*/
override fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): CheckpointSerializationContext {
throw UnsupportedOperationException()
}
override fun withProperty(property: Any, value: Any): CheckpointSerializationContext { override fun withProperty(property: Any, value: Any): CheckpointSerializationContext {
return copy(properties = properties + (property to value)) return copy(properties = properties + (property to value))
} }

View File

@ -8,6 +8,7 @@ import net.corda.core.contracts.Attachment
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.internal.copyBytes import net.corda.core.internal.copyBytes
import net.corda.core.serialization.* import net.corda.core.serialization.*
import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.ByteSequence
import net.corda.serialization.internal.amqp.amqpMagic import net.corda.serialization.internal.amqp.amqpMagic
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -31,20 +32,12 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe
override val useCase: SerializationContext.UseCase, override val useCase: SerializationContext.UseCase,
override val encoding: SerializationEncoding?, override val encoding: SerializationEncoding?,
override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist, override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist,
override val lenientCarpenterEnabled: Boolean = false, override val lenientCarpenterEnabled: Boolean = false) : SerializationContext {
private val builder: AttachmentsClassLoaderBuilder = AttachmentsClassLoaderBuilder()
) : SerializationContext {
/** /**
* {@inheritDoc} * {@inheritDoc}
*
* We need to cache the AttachmentClassLoaders to avoid too many contexts, since the class loader is part of cache key for the context.
*/ */
override fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): SerializationContext { override fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): SerializationContext {
properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean == true || return this return this
val classLoader = builder.build(attachmentHashes, properties, deserializationClassLoader) ?: return this
return withClassLoader(classLoader)
} }
override fun withProperty(property: Any, value: Any): SerializationContext { override fun withProperty(property: Any, value: Any): SerializationContext {
@ -72,34 +65,6 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe
override fun withEncodingWhitelist(encodingWhitelist: EncodingWhitelist) = copy(encodingWhitelist = encodingWhitelist) override fun withEncodingWhitelist(encodingWhitelist: EncodingWhitelist) = copy(encodingWhitelist = encodingWhitelist)
} }
/*
* This class is internal rather than private so that serialization-deterministic
* can replace it with an alternative version.
*/
@DeleteForDJVM
class AttachmentsClassLoaderBuilder() {
private val cache: Cache<Pair<List<SecureHash>, ClassLoader>, AttachmentsClassLoader> = Caffeine.newBuilder().weakValues().maximumSize(1024).build()
fun build(attachmentHashes: List<SecureHash>, properties: Map<Any, Any>, deserializationClassLoader: ClassLoader): AttachmentsClassLoader? {
val serializationContext = properties[serializationContextKey] as? SerializeAsTokenContext ?: return null // Some tests don't set one.
try {
return cache.get(Pair(attachmentHashes, deserializationClassLoader)) {
val missing = ArrayList<SecureHash>()
val attachments = ArrayList<Attachment>()
attachmentHashes.forEach { id ->
serializationContext.serviceHub.attachments.openAttachment(id)?.let { attachments += it }
?: run { missing += id }
}
missing.isNotEmpty() && throw MissingAttachmentsException(missing)
AttachmentsClassLoader(attachments, parent = deserializationClassLoader)
}!!
} catch (e: ExecutionException) {
// Caught from within the cache get, so unwrap.
throw e.cause!!
}
}
}
@KeepForDJVM @KeepForDJVM
open class SerializationFactoryImpl( open class SerializationFactoryImpl(
// TODO: This is read-mostly. Probably a faster implementation to be found. // TODO: This is read-mostly. Probably a faster implementation to be found.

View File

@ -1,382 +0,0 @@
package net.corda.serialization.internal
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.Contract
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.declaredField
import net.corda.core.internal.toWireTransaction
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.*
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.OpaqueBytes
import net.corda.node.internal.cordapp.JarScanningCordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.nodeapi.DummyContractBackdoor
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.MockCordappConfigProvider
import net.corda.testing.internal.kryoSpecific
import net.corda.testing.internal.rigorousMock
import net.corda.testing.services.MockAttachmentStorage
import org.apache.commons.io.IOUtils
import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.net.URL
import java.net.URLClassLoader
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
import kotlin.test.assertFailsWith
class AttachmentsClassLoaderTests {
companion object {
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderTests::class.java.getResource("isolated.jar")
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
private val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
private val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
private fun SerializationContext.withAttachmentStorage(attachmentStorage: AttachmentStorage): SerializationContext {
val serviceHub = rigorousMock<ServiceHub>()
doReturn(attachmentStorage).whenever(serviceHub).attachments
return this.withServiceHub(serviceHub)
}
private fun SerializationContext.withServiceHub(serviceHub: ServiceHub): SerializationContext {
return this.withTokenContext(SerializeAsTokenContextImpl(serviceHub) {}).withProperty(attachmentsClassLoaderEnabledPropertyName, true)
}
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val attachments = MockAttachmentStorage()
private val networkParameters = testNetworkParameters()
private val cordappProvider = CordappProviderImpl(JarScanningCordappLoader.fromJarUrls(listOf(ISOLATED_CONTRACTS_JAR_PATH)), MockCordappConfigProvider(), attachments).apply {
start(networkParameters.whitelistedContractImplementations)
}
private val cordapp get() = cordappProvider.cordapps.first()
private val attachmentId get() = cordappProvider.getCordappAttachmentId(cordapp)!!
private val appContext get() = cordappProvider.getAppContext(cordapp)
private val serviceHub = rigorousMock<ServiceHub>().also {
doReturn(attachments).whenever(it).attachments
doReturn(cordappProvider).whenever(it).cordappProvider
doReturn(networkParameters).whenever(it).networkParameters
}
// These ClassLoaders work together to load 'AnotherDummyContract' in a disposable way, such that even though
// the class may be on the unit test class path (due to default IDE settings, etc), it won't be loaded into the
// regular app classloader but rather than ClassLoaderForTests. This helps keep our environment clean and
// ensures we have precise control over where it's loaded.
object FilteringClassLoader : ClassLoader() {
@Throws(ClassNotFoundException::class)
override fun loadClass(name: String, resolve: Boolean): Class<*> {
if ("AnotherDummyContract" in name) {
throw ClassNotFoundException(name)
}
return super.loadClass(name, resolve)
}
}
class ClassLoaderForTests : URLClassLoader(arrayOf(ISOLATED_CONTRACTS_JAR_PATH), FilteringClassLoader)
@Test
fun `dynamically load AnotherDummyContract from isolated contracts jar`() {
ClassLoaderForTests().use { child ->
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, child)
val contract = contractClass.newInstance() as Contract
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
}
}
private fun fakeAttachment(filepath: String, content: String): ByteArray {
val bs = ByteArrayOutputStream()
JarOutputStream(bs).use { js ->
js.putNextEntry(ZipEntry(filepath))
js.writer().apply { append(content); flush() }
js.closeEntry()
}
return bs.toByteArray()
}
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
ByteArrayOutputStream().use {
attachment.extractFile(filepath, it)
return it.toByteArray()
}
}
@Test
fun `test MockAttachmentStorage open as jar`() {
val storage = attachments
val key = attachmentId
val attachment = storage.openAttachment(key)!!
val jar = attachment.openAsJAR()
assertNotNull(jar.nextEntry)
}
@Test
@Suppress("DEPRECATION")
fun `test overlapping file exception`() {
val storage = attachments
val att0 = attachmentId
val att1 = storage.importAttachment(fakeAttachment("file.txt", "some data").inputStream())
val att2 = storage.importAttachment(fakeAttachment("file.txt", "some other data").inputStream())
assertFailsWith(AttachmentsClassLoader.OverlappingAttachments::class) {
AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! })
}
}
@Test
@Suppress("DEPRECATION")
fun basic() {
val storage = attachments
val att0 = attachmentId
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream())
val att2 = storage.importAttachment(fakeAttachment("file2.txt", "some other data").inputStream())
val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! })
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
assertEquals("some data", txt)
}
@Test
@Suppress("DEPRECATION")
fun `Check platform independent path handling in attachment jars`() {
val storage = MockAttachmentStorage()
val att1 = storage.importAttachment(fakeAttachment("/folder1/foldera/file1.txt", "some data").inputStream())
val att2 = storage.importAttachment(fakeAttachment("\\folder1\\folderb\\file2.txt", "some other data").inputStream())
val data1a = readAttachment(storage.openAttachment(att1)!!, "/folder1/foldera/file1.txt")
assertArrayEquals("some data".toByteArray(), data1a)
val data1b = readAttachment(storage.openAttachment(att1)!!, "\\folder1\\foldera\\file1.txt")
assertArrayEquals("some data".toByteArray(), data1b)
val data2a = readAttachment(storage.openAttachment(att2)!!, "\\folder1\\folderb\\file2.txt")
assertArrayEquals("some other data".toByteArray(), data2a)
val data2b = readAttachment(storage.openAttachment(att2)!!, "/folder1/folderb/file2.txt")
assertArrayEquals("some other data".toByteArray(), data2b)
}
@Test
@Suppress("DEPRECATION")
fun `loading class AnotherDummyContract`() {
val storage = attachments
val att0 = attachmentId
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream())
val att2 = storage.importAttachment(fakeAttachment("file2.txt", "some other data").inputStream())
val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader)
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, cl)
val contract = contractClass.newInstance() as Contract
assertEquals(cl, contract.javaClass.classLoader)
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
}
private fun createContract2Cash(): Contract {
ClassLoaderForTests().use { cl ->
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, cl)
return contractClass.newInstance() as Contract
}
}
@Test
@Suppress("DEPRECATION")
fun `testing Kryo with ClassLoader (with top level class name)`() {
val contract = createContract2Cash()
val bytes = contract.serialize()
val storage = attachments
val att0 = attachmentId
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream())
val att2 = storage.importAttachment(fakeAttachment("file2.txt", "some other data").inputStream())
val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader)
val context = SerializationFactory.defaultFactory.defaultContext.withClassLoader(cl).withWhitelisted(contract.javaClass)
val state2 = bytes.deserialize(context = context)
assertTrue(state2.javaClass.classLoader is AttachmentsClassLoader)
assertNotNull(state2)
}
// top level wrapper
@CordaSerializable
class Data(val contract: Contract)
@Test
@Suppress("DEPRECATION")
fun `testing Kryo with ClassLoader (without top level class name)`() {
val data = Data(createContract2Cash())
assertNotNull(data.contract)
val context2 = SerializationFactory.defaultFactory.defaultContext.withWhitelisted(data.contract.javaClass)
val bytes = data.serialize(context = context2)
val storage = attachments
val att0 = attachmentId
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream())
val att2 = storage.importAttachment(fakeAttachment("file2.txt", "some other data").inputStream())
val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader)
val context = SerializationFactory.defaultFactory.defaultContext.withClassLoader(cl).withWhitelisted(Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, cl))
val state2 = bytes.deserialize(context = context)
assertEquals(cl, state2.contract.javaClass.classLoader)
assertNotNull(state2)
// We should be able to load same class from a different class loader and have them be distinct.
val cl2 = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader)
val context3 = SerializationFactory.defaultFactory.defaultContext.withClassLoader(cl2).withWhitelisted(Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, cl2))
val state3 = bytes.deserialize(context = context3)
assertEquals(cl2, state3.contract.javaClass.classLoader)
assertNotNull(state3)
}
@Test
fun `test serialization of SecureHash`() {
val secureHash = SecureHash.randomSHA256()
val bytes = secureHash.serialize()
val copiedSecuredHash = bytes.deserialize()
assertEquals(secureHash, copiedSecuredHash)
}
@Test
fun `test serialization of OpaqueBytes`() {
val opaqueBytes = OpaqueBytes("0123456789".toByteArray())
val bytes = opaqueBytes.serialize()
val copiedOpaqueBytes = bytes.deserialize()
assertEquals(opaqueBytes, copiedOpaqueBytes)
}
@Test
fun `test serialization of sub-sequence OpaqueBytes`() {
val bytesSequence = ByteSequence.of("0123456789".toByteArray(), 3, 2)
val bytes = bytesSequence.serialize()
val copiedBytesSequence = bytes.deserialize()
assertEquals(bytesSequence, copiedBytesSequence)
}
@Test
fun `test serialization of WireTransaction with dynamically loaded contract`() {
val child = appContext.classLoader
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, child)
val contract = contractClass.newInstance() as DummyContractBackdoor
val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
val context = SerializationFactory.defaultFactory.defaultContext
.withWhitelisted(contract.javaClass)
.withWhitelisted(Class.forName("$ISOLATED_CONTRACT_CLASS_NAME\$State", true, child))
.withWhitelisted(Class.forName("$ISOLATED_CONTRACT_CLASS_NAME\$Commands\$Create", true, child))
.withServiceHub(serviceHub)
.withClassLoader(child)
val bytes = run {
val wireTransaction = tx.toWireTransaction(serviceHub, context)
wireTransaction.serialize(context = context)
}
val copiedWireTransaction = bytes.deserialize(context = context)
assertEquals(1, copiedWireTransaction.outputs.size)
// Contracts need to be loaded by the same classloader as the ContractState itself
val contractClassloader = copiedWireTransaction.getOutput(0).javaClass.classLoader
val contract2 = contractClassloader.loadClass(copiedWireTransaction.outputs.first().contract).newInstance() as DummyContractBackdoor
assertEquals(contract2.javaClass.classLoader, copiedWireTransaction.outputs[0].data.javaClass.classLoader)
assertEquals(42, contract2.inspectState(copiedWireTransaction.outputs[0].data))
}
@Test
fun `test deserialize of WireTransaction where contract cannot be found`() {
kryoSpecific("Kryo verifies/loads attachments on deserialization, whereas AMQP currently does not") {
ClassLoaderForTests().use { child ->
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, child)
val contract = contractClass.newInstance() as DummyContractBackdoor
val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
val attachmentRef = attachmentId
val bytes = run {
val outboundContext = SerializationFactory.defaultFactory.defaultContext
.withServiceHub(serviceHub)
.withClassLoader(child)
val wireTransaction = tx.toWireTransaction(serviceHub, outboundContext)
wireTransaction.serialize(context = outboundContext)
}
// use empty attachmentStorage
val e = assertFailsWith(MissingAttachmentsException::class) {
val mockAttStorage = MockAttachmentStorage()
val inboundContext = SerializationFactory.defaultFactory.defaultContext
.withAttachmentStorage(mockAttStorage)
.withAttachmentsClassLoader(listOf(attachmentRef))
bytes.deserialize(context = inboundContext)
if (mockAttStorage.openAttachment(attachmentRef) == null) {
throw MissingAttachmentsException(listOf(attachmentRef))
}
}
assertEquals(attachmentRef, e.ids.single())
}
}
}
@Test
fun `test loading a class from attachment during deserialization`() {
ClassLoaderForTests().use { child ->
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, child)
val contract = contractClass.newInstance() as DummyContractBackdoor
val outboundContext = SerializationFactory.defaultFactory.defaultContext.withClassLoader(child)
val attachmentRef = attachmentId
// We currently ignore annotations in attachments, so manually whitelist.
val inboundContext = SerializationFactory
.defaultFactory
.defaultContext
.withWhitelisted(contract.javaClass)
.withServiceHub(serviceHub)
.withAttachmentsClassLoader(listOf(attachmentRef))
// Serialize with custom context to avoid populating the default context with the specially loaded class
val serialized = contract.serialize(context = outboundContext)
// Then deserialize with the attachment class loader associated with the attachment
serialized.deserialize(context = inboundContext)
}
}
@Test
fun `test loading a class with attachment missing during deserialization`() {
ClassLoaderForTests().use { child ->
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, child)
val contract = contractClass.newInstance() as DummyContractBackdoor
val attachmentRef = SecureHash.randomSHA256()
val outboundContext = SerializationFactory.defaultFactory.defaultContext.withClassLoader(child)
// Serialize with custom context to avoid populating the default context with the specially loaded class
val serialized = contract.serialize(context = outboundContext)
// Then deserialize with the attachment class loader associated with the attachment
val e = assertFailsWith(MissingAttachmentsException::class) {
// We currently ignore annotations in attachments, so manually whitelist.
val inboundContext = SerializationFactory
.defaultFactory
.defaultContext
.withWhitelisted(contract.javaClass)
.withServiceHub(serviceHub)
.withAttachmentsClassLoader(listOf(attachmentRef))
serialized.deserialize(context = inboundContext)
}
assertEquals(attachmentRef, e.ids.single())
}
}
}

View File

@ -14,6 +14,7 @@ import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.internal.CheckpointSerializationContext import net.corda.core.serialization.internal.CheckpointSerializationContext
import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.node.serialization.kryo.CordaClassResolver import net.corda.node.serialization.kryo.CordaClassResolver
import net.corda.node.serialization.kryo.CordaKryo import net.corda.node.serialization.kryo.CordaKryo
import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.rigorousMock
@ -22,6 +23,7 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.ExpectedException import org.junit.rules.ExpectedException
import java.lang.IllegalStateException import java.lang.IllegalStateException
import java.net.URL
import java.sql.Connection import java.sql.Connection
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -112,6 +114,7 @@ class CordaClassResolverTests {
val emptyListClass = listOf<Any>().javaClass val emptyListClass = listOf<Any>().javaClass
val emptySetClass = setOf<Any>().javaClass val emptySetClass = setOf<Any>().javaClass
val emptyMapClass = mapOf<Any, Any>().javaClass val emptyMapClass = mapOf<Any, Any>().javaClass
val ISOLATED_CONTRACTS_JAR_PATH: URL = CordaClassResolverTests::class.java.getResource("isolated.jar")
} }
private val emptyWhitelistContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, null) private val emptyWhitelistContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, null)
@ -201,7 +204,7 @@ class CordaClassResolverTests {
CordaClassResolver(emptyWhitelistContext).getRegistration(DefaultSerializable::class.java) CordaClassResolver(emptyWhitelistContext).getRegistration(DefaultSerializable::class.java)
} }
private fun importJar(storage: AttachmentStorage, uploader: String = DEPLOYED_CORDAPP_UPLOADER) = AttachmentsClassLoaderTests.ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it, uploader, "") } private fun importJar(storage: AttachmentStorage, uploader: String = DEPLOYED_CORDAPP_UPLOADER) = ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it, uploader, "") }
@Test(expected = KryoException::class) @Test(expected = KryoException::class)
fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() { fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() {

View File

@ -161,3 +161,4 @@ fun NodeInfo.singleIdentityAndCert(): PartyAndCertificate = legalIdentitiesAndCe
* Extract a single identity from the node info. Throws an error if the node has multiple identities. * Extract a single identity from the node info. Throws an error if the node has multiple identities.
*/ */
fun NodeInfo.singleIdentity(): Party = singleIdentityAndCert().party fun NodeInfo.singleIdentity(): Party = singleIdentityAndCert().party

View File

@ -33,10 +33,13 @@ import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.registerDevP2pCertificates import net.corda.nodeapi.internal.registerDevP2pCertificates
import net.corda.serialization.internal.amqp.AMQP_ENABLED import net.corda.serialization.internal.amqp.AMQP_ENABLED
import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.internal.stubs.CertificateStoreStubs
import java.io.ByteArrayOutputStream
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.security.KeyPair import java.security.KeyPair
import java.util.* import java.util.*
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
@Suppress("unused") @Suppress("unused")
@ -169,7 +172,20 @@ fun configureDatabase(hikariProperties: Properties,
schemaService: SchemaService = NodeSchemaService(), schemaService: SchemaService = NodeSchemaService(),
internalSchemas: Set<MappedSchema> = NodeSchemaService().internalSchemas(), internalSchemas: Set<MappedSchema> = NodeSchemaService().internalSchemas(),
cacheFactory: NamedCacheFactory = TestingNamedCacheFactory()): CordaPersistence { cacheFactory: NamedCacheFactory = TestingNamedCacheFactory()): CordaPersistence {
val persistence = createCordaPersistence(databaseConfig, wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous, schemaService, hikariProperties, cacheFactory) val persistence = createCordaPersistence(databaseConfig, wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous, schemaService, hikariProperties, cacheFactory, null)
persistence.startHikariPool(hikariProperties, databaseConfig, internalSchemas) persistence.startHikariPool(hikariProperties, databaseConfig, internalSchemas)
return persistence return persistence
} }
/**
* Convenience method for creating a fake attachment containing a file with some content.
*/
fun fakeAttachment(filePath: String, content: String): ByteArray {
val bs = ByteArrayOutputStream()
JarOutputStream(bs).use { js ->
js.putNextEntry(ZipEntry(filePath))
js.writer().apply { append(content); flush() }
js.closeEntry()
}
return bs.toByteArray()
}

View File

@ -39,7 +39,7 @@ class MockCordappProvider(
allFlows = emptyList(), allFlows = emptyList(),
jarHash = SecureHash.allOnesHash) jarHash = SecureHash.allOnesHash)
if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) && it.second == contractHash }) { if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) && it.second == contractHash }) {
cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments, contractHash, signers))) cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), fakeAttachmentCached(contractClassName), attachments, contractHash, signers)))
} }
return cordappRegistry.findLast { contractClassName in it.first.contractClassNames }?.second!! return cordappRegistry.findLast { contractClassName in it.first.contractClassNames }?.second!!
} }
@ -57,4 +57,9 @@ class MockCordappProvider(
attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream(), contractHash, signers) attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream(), contractHash, signers)
} }
} }
private val attachmentsCache = mutableMapOf<String, ByteArray>()
private fun fakeAttachmentCached(contractClass: String): ByteArray = attachmentsCache.computeIfAbsent(contractClass) {
fakeAttachment(contractClass, contractClass)
}
} }