mirror of
https://github.com/corda/corda.git
synced 2025-06-22 09:08:49 +00:00
Merge remote-tracking branch 'open/master' into tudor-os-merge-19-11
# Conflicts: # docs/source/cli-ux-guidelines.rst # node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt # node/src/main/java/CordaCaplet.java # node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt # node/src/main/kotlin/net/corda/node/serialization/kryo/Kryo.kt # testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt # testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt
This commit is contained in:
@ -40,14 +40,14 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
|
||||
fun getInstance(asn1: ASN1Primitive): PublicKey {
|
||||
val keyInfo = SubjectPublicKeyInfo.getInstance(asn1)
|
||||
require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.COMPOSITE_KEY)
|
||||
require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.COMPOSITE_KEY) { "Key must be composite" }
|
||||
val sequence = ASN1Sequence.getInstance(keyInfo.parsePublicKey())
|
||||
val threshold = ASN1Integer.getInstance(sequence.getObjectAt(0)).positiveValue.toInt()
|
||||
val sequenceOfChildren = ASN1Sequence.getInstance(sequence.getObjectAt(1))
|
||||
val builder = Builder()
|
||||
val listOfChildren = sequenceOfChildren.objects.toList()
|
||||
listOfChildren.forEach { childAsn1 ->
|
||||
require(childAsn1 is ASN1Sequence)
|
||||
require(childAsn1 is ASN1Sequence) { "Child key is not in ASN1 format" }
|
||||
val childSeq = childAsn1 as ASN1Sequence
|
||||
val key = Crypto.decodePublicKey((childSeq.getObjectAt(0) as DERBitString).bytes)
|
||||
val weight = ASN1Integer.getInstance(childSeq.getObjectAt(1))
|
||||
@ -274,7 +274,7 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
* is invalid (for example it would contain no keys).
|
||||
*/
|
||||
fun build(threshold: Int? = null): PublicKey {
|
||||
require(threshold == null || threshold > 0)
|
||||
require(threshold == null || threshold > 0) { "Threshold must not be specified or its value must be greater than zero" }
|
||||
val n = children.size
|
||||
return when {
|
||||
n > 1 -> CompositeKey(threshold ?: children.map { (_, weight) -> weight }.sum(), children)
|
||||
|
@ -21,7 +21,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
/** SHA-256 is part of the SHA-2 hash function family. Generated hash is fixed size, 256-bits (32-bytes). */
|
||||
class SHA256(bytes: ByteArray) : SecureHash(bytes) {
|
||||
init {
|
||||
require(bytes.size == 32)
|
||||
require(bytes.size == 32) { "Invalid hash size, must be 32 bytes" }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,7 @@ val cordaBouncyCastleProvider = BouncyCastleProvider().apply {
|
||||
Security.addProvider(it)
|
||||
}
|
||||
val bouncyCastlePQCProvider = BouncyCastlePQCProvider().apply {
|
||||
require(name == "BCPQC") // The constant it comes from is not final.
|
||||
require(name == "BCPQC") { "Invalid PQCProvider name" }
|
||||
}.also {
|
||||
Security.addProvider(it)
|
||||
}
|
||||
|
@ -27,6 +27,11 @@ fun isUploaderTrusted(uploader: String?): Boolean = uploader in TRUSTED_UPLOADER
|
||||
@KeepForDJVM
|
||||
abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
||||
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
|
||||
fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray {
|
||||
return {
|
||||
|
@ -1,6 +1,10 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
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.CordappConfig
|
||||
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.node.ServicesForResolution
|
||||
import net.corda.core.node.ZoneVersionTooLowException
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import org.slf4j.MDC
|
||||
|
||||
// *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]
|
||||
?: 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)
|
@ -5,7 +5,10 @@ package net.corda.core.internal
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import org.slf4j.Logger
|
||||
@ -109,7 +112,7 @@ fun <T> List<T>.randomOrNull(): T? {
|
||||
/** Returns the index of the given item or throws [IllegalArgumentException] if not found. */
|
||||
fun <T> List<T>.indexOfOrThrow(item: T): Int {
|
||||
val i = indexOf(item)
|
||||
require(i != -1)
|
||||
require(i != -1){"No such element"}
|
||||
return i
|
||||
}
|
||||
|
||||
@ -218,7 +221,8 @@ data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHa
|
||||
* Note that a slightly bigger than numOfExpectedBytes size is expected.
|
||||
*/
|
||||
@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)
|
||||
val baos = ByteArrayOutputStream()
|
||||
ZipOutputStream(baos).use { zos ->
|
||||
@ -226,7 +230,7 @@ data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHa
|
||||
val bytes = ByteArray(arraySize) { content }
|
||||
val n = (numOfExpectedBytes - 1) / arraySize + 1 // same as Math.ceil(numOfExpectedBytes/arraySize).
|
||||
zos.setLevel(Deflater.NO_COMPRESSION)
|
||||
zos.putNextEntry(ZipEntry("z"))
|
||||
zos.putNextEntry(ZipEntry(entryName))
|
||||
for (i in 0 until n) {
|
||||
zos.write(bytes, 0, arraySize)
|
||||
}
|
||||
@ -498,3 +502,18 @@ fun <T : Any> SerializedBytes<Any>.checkPayloadIs(type: Class<T>): Untrustworthy
|
||||
return type.castIfPossible(payloadData)?.let { UntrustworthyData(it) }
|
||||
?: 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)
|
@ -14,8 +14,8 @@ interface NamedCacheFactory {
|
||||
* the name can be used to create a file name or a metric name.
|
||||
*/
|
||||
fun checkCacheName(name: String) {
|
||||
require(!name.isBlank())
|
||||
require(allowedChars.matches(name))
|
||||
require(!name.isBlank()){"Name must not be empty or only whitespace"}
|
||||
require(allowedChars.matches(name)){"Invalid characters in cache name"}
|
||||
}
|
||||
|
||||
fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V>
|
||||
|
@ -0,0 +1,4 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
// TODO: Add to Corda node.conf to allow customisation
|
||||
const val NODE_INFO_DIRECTORY = "additional-node-infos"
|
@ -1,15 +1,23 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.componentHash
|
||||
import net.corda.core.crypto.sha256
|
||||
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.transactions.ComponentGroup
|
||||
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
||||
import net.corda.core.transactions.FilteredComponentGroup
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.lazyMapped
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.PublicKey
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/** Constructs a [NotaryChangeWireTransaction]. */
|
||||
class NotaryChangeTransactionBuilder(val inputs: List<StateRef>,
|
||||
@ -42,4 +50,75 @@ fun combinedHash(components: Iterable<SecureHash>): SecureHash {
|
||||
stream.write(it.bytes)
|
||||
}
|
||||
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]) }
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ class AddressBindingException(val addresses: Set<NetworkHostAndPort>) : CordaRun
|
||||
|
||||
private companion object {
|
||||
private fun message(addresses: Set<NetworkHostAndPort>): String {
|
||||
require(addresses.isNotEmpty())
|
||||
require(addresses.isNotEmpty()) { "Address list must not be empty" }
|
||||
return if (addresses.size > 1) {
|
||||
"Failed to bind on an address in ${addresses.joinToString(", ", "[", "]")}."
|
||||
} else {
|
||||
|
@ -177,10 +177,10 @@ interface 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.
|
||||
* (Requires the attachment storage to have been enabled).
|
||||
* Does not do anything.
|
||||
*/
|
||||
@Throws(MissingAttachmentsException::class)
|
||||
@Deprecated("There is no reason to call this. This method does not actually do anything.")
|
||||
fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): SerializationContext
|
||||
|
||||
/**
|
||||
@ -300,13 +300,8 @@ class SerializedBytes<T : Any>(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
/**
|
||||
* Serializes the given object and returns a [SerializedBytes] wrapper for it. An alias for [Any.serialize]
|
||||
* intended to make the calling smoother for Java users.
|
||||
*
|
||||
* TODO: Take out the @CordaInternal annotation post-Enterprise GA when we can add API again.
|
||||
*
|
||||
* @suppress
|
||||
*/
|
||||
@JvmStatic
|
||||
@CordaInternal
|
||||
@JvmOverloads
|
||||
fun <T : Any> from(obj: T, serializationFactory: SerializationFactory = SerializationFactory.defaultFactory,
|
||||
context: SerializationContext = serializationFactory.defaultContext): SerializedBytes<T> {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -73,13 +73,6 @@ interface 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.
|
||||
*/
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
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.node.NetworkParameters
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||
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.WireTransaction.Companion.resolveStateRefBinaryComponent
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import java.security.PublicKey
|
||||
@ -35,6 +38,32 @@ data class ContractUpgradeWireTransaction(
|
||||
/** Required for hiding components in [ContractUpgradeFilteredTransaction]. */
|
||||
val privacySalt: PrivacySalt = PrivacySalt()
|
||||
) : 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 notary: Party by lazy { serializedComponents[NOTARY.ordinal].deserialize<Party>() }
|
||||
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. */
|
||||
fun buildFilteredTransaction(): ContractUpgradeFilteredTransaction {
|
||||
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
|
||||
* transaction is verified during construction.
|
||||
*/
|
||||
override val outputs: List<TransactionState<ContractState>> = inputs.map { (state) ->
|
||||
// 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
|
||||
)
|
||||
}
|
||||
override val outputs: List<TransactionState<ContractState>> = inputs.map { calculateUpgradedState(it.state, upgradedContract, upgradedContractAttachment) }
|
||||
|
||||
/** The required signers are the set of all input states' participants. */
|
||||
override val requiredSigningKeys: Set<PublicKey>
|
||||
|
@ -1,22 +1,21 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AttachmentWithContext
|
||||
import net.corda.core.internal.castIfPossible
|
||||
import net.corda.core.internal.checkMinimumPlatformVersion
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.node.NetworkParameters
|
||||
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.warnOnce
|
||||
import java.util.*
|
||||
import java.util.function.Predicate
|
||||
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:
|
||||
@ -34,7 +33,7 @@ import net.corda.core.utilities.warnOnce
|
||||
// DOCSTART 1
|
||||
@KeepForDJVM
|
||||
@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. */
|
||||
override val inputs: List<StateAndRef<ContractState>>,
|
||||
override val outputs: List<TransactionState<ContractState>>,
|
||||
@ -47,9 +46,38 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
override val notary: Party?,
|
||||
val timeWindow: TimeWindow?,
|
||||
val privacySalt: PrivacySalt,
|
||||
private val networkParameters: NetworkParameters? = null,
|
||||
override val references: List<StateAndRef<ContractState>> = emptyList()
|
||||
private val networkParameters: NetworkParameters?,
|
||||
override val references: List<StateAndRef<ContractState>>,
|
||||
val componentGroups: List<ComponentGroup>?,
|
||||
val resolvedInputBytes: List<SerializedStateAndRef>?,
|
||||
val resolvedReferenceBytes: List<SerializedStateAndRef>?
|
||||
) : 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
|
||||
init {
|
||||
checkBaseInvariants()
|
||||
@ -58,19 +86,25 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
checkEncumbrancesValid()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
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)
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
private val logger = loggerFor<LedgerTransaction>()
|
||||
|
||||
private fun stateToContractClass(state: TransactionState<ContractState>): Try<Class<out Contract>> {
|
||||
return contractClassFor(state.contract, state.data::class.java.classLoader)
|
||||
}
|
||||
@CordaInternal
|
||||
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 }
|
||||
@ -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.
|
||||
|
||||
* 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.
|
||||
*/
|
||||
@ -95,12 +135,17 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
fun verify() {
|
||||
val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = getUniqueContractAttachmentsByContract()
|
||||
|
||||
// TODO - verify for version downgrade
|
||||
validatePackageOwnership(contractAttachmentsByContract)
|
||||
validateStatesAgainstContract()
|
||||
verifyConstraintsValidity(contractAttachmentsByContract)
|
||||
verifyConstraints(contractAttachmentsByContract)
|
||||
verifyContracts()
|
||||
AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments) { transactionClassLoader ->
|
||||
|
||||
val internalTx = createInternalLedgerTransaction()
|
||||
|
||||
// TODO - verify for version downgrade
|
||||
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.
|
||||
*/
|
||||
private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract)
|
||||
private fun validateStatesAgainstContract(internalTx: LedgerTransaction) = internalTx.allStates.forEach { validateStateAgainstContract(it) }
|
||||
|
||||
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
|
||||
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 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.
|
||||
for (state in allStates) {
|
||||
for (state in internalTx.allStates) {
|
||||
checkConstraintValidity(state)
|
||||
}
|
||||
|
||||
// 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.
|
||||
val inputContractGroups = inputs.groupBy { it.state.contract }
|
||||
val outputContractGroups = outputs.groupBy { it.contract }
|
||||
val inputContractGroups = internalTx.inputs.groupBy { it.state.contract }
|
||||
val outputContractGroups = internalTx.outputs.groupBy { it.contract }
|
||||
|
||||
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.
|
||||
val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet()
|
||||
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
|
||||
outputConstraints?.forEach { outputConstraint ->
|
||||
inputConstraints?.forEach { inputConstraint ->
|
||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachmentsByContract[contractClassName]!! ))) {
|
||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachmentsByContract[contractClassName]!!))) {
|
||||
throw TransactionVerificationException.ConstraintPropagationRejection(id, contractClassName, inputConstraint, outputConstraint)
|
||||
}
|
||||
}
|
||||
@ -186,8 +231,8 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
*
|
||||
* @throws TransactionVerificationException if the constraints fail to verify
|
||||
*/
|
||||
private fun verifyConstraints(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||
for (state in allStates) {
|
||||
private fun verifyConstraints(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||
for (state in internalTx.allStates) {
|
||||
val contractAttachment = contractAttachmentsByContract[state.contract]
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
|
||||
@ -226,38 +271,64 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
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.
|
||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||
*/
|
||||
private fun verifyContracts() = inputAndOutputStates.forEach { ts ->
|
||||
val contractClass = getContractClass(ts)
|
||||
val contract = createContractInstance(contractClass)
|
||||
private fun verifyContracts(internalTx: LedgerTransaction) {
|
||||
val contractClasses = (internalTx.inputs.map { it.state } + internalTx.outputs).toSet()
|
||||
.map { it.contract to contractClassFor(it.contract, it.data.javaClass.classLoader) }
|
||||
|
||||
try {
|
||||
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 =
|
||||
val contractInstances = contractClasses.map { (contractClassName, contractClass) ->
|
||||
try {
|
||||
contractClass.newInstance()
|
||||
} 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
|
||||
@ -286,7 +357,8 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
// b) the number of outputs can contain the encumbrance
|
||||
// c) the bi-directionality (full cycle) property is satisfied
|
||||
// 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()) {
|
||||
checkBidirectionalOutputEncumbrances(statesAndEncumbrance)
|
||||
checkNotariesOutputEncumbrance(statesAndEncumbrance)
|
||||
|
@ -6,14 +6,14 @@ import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.ComponentGroupEnum.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.LazyMappedList
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.internal.deserialiseCommands
|
||||
import net.corda.core.internal.deserialiseComponentGroup
|
||||
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.lazyMapped
|
||||
import java.security.PublicKey
|
||||
import java.util.function.Predicate
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
/** 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). */
|
||||
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). */
|
||||
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. */
|
||||
val commands: List<Command<*>> = deserialiseCommands()
|
||||
val commands: List<Command<*>> = deserialiseCommands(componentGroups)
|
||||
|
||||
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." }
|
||||
notaries.firstOrNull()
|
||||
}
|
||||
|
||||
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." }
|
||||
timeWindows.firstOrNull()
|
||||
}
|
||||
@ -66,65 +66,6 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
|
||||
timeWindow?.let { result += listOf(it) }
|
||||
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]) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.KeepForDJVM
|
||||
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.ServicesForResolution
|
||||
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.transactions.NotaryChangeWireTransaction.Component.*
|
||||
@ -75,6 +77,20 @@ data class NotaryChangeWireTransaction(
|
||||
@DeleteForDJVM
|
||||
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 {
|
||||
INPUTS, NOTARY, NEW_NOTARY
|
||||
}
|
||||
|
@ -307,7 +307,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
// 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.
|
||||
require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." }
|
||||
|
@ -7,11 +7,15 @@ import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.ComponentGroupEnum.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.SerializedStateAndRef
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
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.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.lazyMapped
|
||||
@ -99,7 +103,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
return toLedgerTransactionInternal(
|
||||
resolveIdentity = { services.identityService.partyFromKey(it) },
|
||||
resolveAttachment = { services.attachments.openAttachment(it) },
|
||||
resolveStateRef = { services.loadState(it) },
|
||||
resolveStateRefComponent = { resolveStateRefBinaryComponent(it, services) },
|
||||
networkParameters = services.networkParameters
|
||||
)
|
||||
}
|
||||
@ -119,13 +123,14 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
resolveStateRef: (StateRef) -> TransactionState<*>?,
|
||||
@Suppress("UNUSED_PARAMETER") resolveContractAttachment: (TransactionState<ContractState>) -> AttachmentId?
|
||||
): 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(
|
||||
resolveIdentity: (PublicKey) -> Party?,
|
||||
resolveAttachment: (SecureHash) -> Attachment?,
|
||||
resolveStateRef: (StateRef) -> TransactionState<*>?,
|
||||
resolveStateRefComponent: (StateRef) -> SerializedBytes<TransactionState<ContractState>>?,
|
||||
networkParameters: NetworkParameters?
|
||||
): LedgerTransaction {
|
||||
// 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) }
|
||||
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, _ ->
|
||||
resolveStateRef(ref)?.let { StateAndRef(it, ref) } ?: throw TransactionResolutionException(ref.txhash)
|
||||
val resolvedInputs = resolvedInputBytes.lazyMapped { (serialized, ref), _ ->
|
||||
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, _ ->
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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.
|
||||
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.references.serialize().size)
|
||||
minus(ltx.inputs.serialize().size)
|
||||
minus(ltx.resolvedInputBytes!!.sumBy { it.serializedState.size })
|
||||
minus(ltx.resolvedReferenceBytes!!.sumBy { it.serializedState.size })
|
||||
|
||||
// For Commands and outputs we can use the component groups as they are already serialized.
|
||||
minus(componentGroupSize(COMMANDS_GROUP))
|
||||
@ -253,6 +275,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_MAX_TX_SIZE = 10485760
|
||||
|
||||
/**
|
||||
* Creating list of [ComponentGroup] used in one of the constructors of [WireTransaction] required
|
||||
* 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)))
|
||||
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
|
||||
|
@ -68,8 +68,8 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si
|
||||
* This method cannot be used to get bytes before [offset] or after [offset]+[size], and never makes a new array.
|
||||
*/
|
||||
fun slice(start: Int = 0, end: Int = size): ByteBuffer {
|
||||
require(start >= 0)
|
||||
require(end >= 0)
|
||||
require(start >= 0) { "Starting index must be greater than or equal to 0" }
|
||||
require(end >= 0){"End index must be greater or equal to 0"}
|
||||
val clampedStart = min(start, size)
|
||||
val clampedEnd = min(end, size)
|
||||
return ByteBuffer.wrap(_bytes, offset + clampedStart, max(0, clampedEnd - clampedStart)).asReadOnlyBuffer()
|
||||
@ -155,7 +155,7 @@ open class OpaqueBytes(bytes: ByteArray) : ByteSequence(bytes, 0, bytes.size) {
|
||||
}
|
||||
|
||||
init {
|
||||
require(bytes.isNotEmpty())
|
||||
require(bytes.isNotEmpty()) { "Byte Array must not be empty" }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -193,7 +193,7 @@ fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(this)
|
||||
@KeepForDJVM
|
||||
class OpaqueBytesSubSequence(override val bytes: ByteArray, offset: Int, size: Int) : ByteSequence(bytes, offset, size) {
|
||||
init {
|
||||
require(offset >= 0 && offset < bytes.size)
|
||||
require(size >= 0 && offset + size <= bytes.size)
|
||||
require(offset >= 0 && offset < bytes.size) { "Offset must be greater than or equal to 0, and less than the size of the backing array" }
|
||||
require(size >= 0 && offset + size <= bytes.size) { "Sub-sequence size must be greater than or equal to 0, and less than the size of the backing array" }
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.internal.LazyMappedList
|
||||
import net.corda.core.internal.concurrent.get
|
||||
import net.corda.core.internal.createSimpleCache
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
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)
|
||||
|
||||
private const val MAX_SIZE = 100
|
||||
private val warnings = Collections.newSetFromMap(object : LinkedHashMap<String, Boolean>() {
|
||||
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Boolean>?) = size > MAX_SIZE
|
||||
})
|
||||
private val warnings = Collections.newSetFromMap(createSimpleCache<String, Boolean>(MAX_SIZE))
|
||||
|
||||
/**
|
||||
* Utility to help log a warning message only once.
|
||||
@ -163,4 +162,4 @@ fun Logger.warnOnce(warning: String) {
|
||||
warnings.add(warning)
|
||||
this.warn(warning)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,18 +14,13 @@ import net.corda.core.internal.FetchAttachmentsFlow
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.internal.hash
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.makeUnique
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.fakeAttachment
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.InternalMockNodeParameters
|
||||
import net.corda.testing.node.internal.TestStartedNode
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
|
||||
class AttachmentTests : WithMockNet {
|
||||
companion object {
|
||||
@ -46,7 +41,7 @@ class AttachmentTests : WithMockNet {
|
||||
@Test
|
||||
fun `download and store`() {
|
||||
// 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.
|
||||
assert.that(
|
||||
@ -87,7 +82,7 @@ class AttachmentTests : WithMockNet {
|
||||
val badAlice = badAliceNode.info.singleIdentity()
|
||||
|
||||
// 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)
|
||||
|
||||
// Corrupt its store.
|
||||
@ -134,18 +129,6 @@ class AttachmentTests : WithMockNet {
|
||||
}
|
||||
}).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
|
||||
|
||||
//region Operations
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.createWireTransaction
|
||||
import net.corda.testing.internal.fakeAttachment
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@ -118,7 +119,8 @@ class TransactionTests {
|
||||
val commands = emptyList<CommandWithParties<CommandData>>()
|
||||
val attachments = listOf<Attachment>(ContractAttachment(rigorousMock<Attachment>().also {
|
||||
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 timeWindow: TimeWindow? = null
|
||||
val privacySalt = PrivacySalt()
|
||||
|
@ -0,0 +1,17 @@
|
||||
package net.corda.nodeapi
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
|
||||
/**
|
||||
* This interface deliberately mirrors the one in the finance:isolated module.
|
||||
* We will actually link [AnotherDummyContract] against this interface rather
|
||||
* than the one inside isolated.jar, which means we won't need to use reflection
|
||||
* to execute the contract's generateInitial() method.
|
||||
*/
|
||||
interface DummyContractBackdoor {
|
||||
fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder
|
||||
fun inspectState(state: ContractState): Int
|
||||
}
|
BIN
core/src/test/resources/net/corda/core/transactions/isolated.jar
Normal file
BIN
core/src/test/resources/net/corda/core/transactions/isolated.jar
Normal file
Binary file not shown.
Reference in New Issue
Block a user