mirror of
https://github.com/corda/corda.git
synced 2025-06-12 04:08:26 +00:00
Merge pull request #1575 from corda/tudor-os-merge-19-11
Tudor os merge 19 11
This commit is contained in:
1232
.ci/api-current.txt
1232
.ci/api-current.txt
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@
|
|||||||
package net.corda.client.jackson.internal
|
package net.corda.client.jackson.internal
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.*
|
import com.fasterxml.jackson.annotation.*
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator.Mode.*
|
import com.fasterxml.jackson.annotation.JsonCreator.Mode.DISABLED
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude.Include
|
import com.fasterxml.jackson.annotation.JsonInclude.Include
|
||||||
import com.fasterxml.jackson.core.JsonGenerator
|
import com.fasterxml.jackson.core.JsonGenerator
|
||||||
import com.fasterxml.jackson.core.JsonParseException
|
import com.fasterxml.jackson.core.JsonParseException
|
||||||
@ -38,7 +38,10 @@ import net.corda.core.utilities.NetworkHostAndPort
|
|||||||
import net.corda.core.utilities.parseAsHex
|
import net.corda.core.utilities.parseAsHex
|
||||||
import net.corda.core.utilities.toHexString
|
import net.corda.core.utilities.toHexString
|
||||||
import net.corda.serialization.internal.AllWhitelist
|
import net.corda.serialization.internal.AllWhitelist
|
||||||
import net.corda.serialization.internal.amqp.*
|
import net.corda.serialization.internal.amqp.SerializerFactoryBuilder
|
||||||
|
import net.corda.serialization.internal.amqp.constructorForDeserialization
|
||||||
|
import net.corda.serialization.internal.amqp.hasCordaSerializable
|
||||||
|
import net.corda.serialization.internal.amqp.propertiesForSerialization
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.cert.CertPath
|
import java.security.cert.CertPath
|
||||||
@ -327,11 +330,11 @@ private class PartialTreeJson(val includedLeaf: SecureHash? = null,
|
|||||||
val right: PartialTreeJson? = null) {
|
val right: PartialTreeJson? = null) {
|
||||||
init {
|
init {
|
||||||
if (includedLeaf != null) {
|
if (includedLeaf != null) {
|
||||||
require(leaf == null && left == null && right == null)
|
require(leaf == null && left == null && right == null) { "Invalid JSON structure" }
|
||||||
} else if (leaf != null) {
|
} else if (leaf != null) {
|
||||||
require(left == null && right == null)
|
require(left == null && right == null) { "Invalid JSON structure" }
|
||||||
} else {
|
} else {
|
||||||
require(left != null && right != null)
|
require(left != null && right != null) { "Invalid JSON structure" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,7 +202,7 @@ class NodeMonitorModel : AutoCloseable {
|
|||||||
val _connection = client.start(username, password)
|
val _connection = client.start(username, password)
|
||||||
// Check connection is truly operational before returning it.
|
// Check connection is truly operational before returning it.
|
||||||
val nodeInfo = _connection.proxy.nodeInfo()
|
val nodeInfo = _connection.proxy.nodeInfo()
|
||||||
require(nodeInfo.legalIdentitiesAndCerts.isNotEmpty())
|
require(nodeInfo.legalIdentitiesAndCerts.isNotEmpty()){"No identity certificates found"}
|
||||||
_connection
|
_connection
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
if (shouldRetry) {
|
if (shouldRetry) {
|
||||||
|
@ -126,7 +126,7 @@ interface Validated<TARGET, ERROR> {
|
|||||||
*/
|
*/
|
||||||
class Unsuccessful<TARGET, ERROR>(override val errors: Set<ERROR>) : Result<TARGET, ERROR>(), Validated<TARGET, ERROR> {
|
class Unsuccessful<TARGET, ERROR>(override val errors: Set<ERROR>) : Result<TARGET, ERROR>(), Validated<TARGET, ERROR> {
|
||||||
init {
|
init {
|
||||||
require(errors.isNotEmpty())
|
require(errors.isNotEmpty()) { "No errors encountered during validation" }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun value(exceptionOnErrors: (Set<ERROR>) -> Exception) = throw exceptionOnErrors.invoke(errors)
|
override fun value(exceptionOnErrors: (Set<ERROR>) -> Exception) = throw exceptionOnErrors.invoke(errors)
|
||||||
|
@ -40,14 +40,14 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
|||||||
|
|
||||||
fun getInstance(asn1: ASN1Primitive): PublicKey {
|
fun getInstance(asn1: ASN1Primitive): PublicKey {
|
||||||
val keyInfo = SubjectPublicKeyInfo.getInstance(asn1)
|
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 sequence = ASN1Sequence.getInstance(keyInfo.parsePublicKey())
|
||||||
val threshold = ASN1Integer.getInstance(sequence.getObjectAt(0)).positiveValue.toInt()
|
val threshold = ASN1Integer.getInstance(sequence.getObjectAt(0)).positiveValue.toInt()
|
||||||
val sequenceOfChildren = ASN1Sequence.getInstance(sequence.getObjectAt(1))
|
val sequenceOfChildren = ASN1Sequence.getInstance(sequence.getObjectAt(1))
|
||||||
val builder = Builder()
|
val builder = Builder()
|
||||||
val listOfChildren = sequenceOfChildren.objects.toList()
|
val listOfChildren = sequenceOfChildren.objects.toList()
|
||||||
listOfChildren.forEach { childAsn1 ->
|
listOfChildren.forEach { childAsn1 ->
|
||||||
require(childAsn1 is ASN1Sequence)
|
require(childAsn1 is ASN1Sequence) { "Child key is not in ASN1 format" }
|
||||||
val childSeq = childAsn1 as ASN1Sequence
|
val childSeq = childAsn1 as ASN1Sequence
|
||||||
val key = Crypto.decodePublicKey((childSeq.getObjectAt(0) as DERBitString).bytes)
|
val key = Crypto.decodePublicKey((childSeq.getObjectAt(0) as DERBitString).bytes)
|
||||||
val weight = ASN1Integer.getInstance(childSeq.getObjectAt(1))
|
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).
|
* is invalid (for example it would contain no keys).
|
||||||
*/
|
*/
|
||||||
fun build(threshold: Int? = null): PublicKey {
|
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
|
val n = children.size
|
||||||
return when {
|
return when {
|
||||||
n > 1 -> CompositeKey(threshold ?: children.map { (_, weight) -> weight }.sum(), children)
|
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). */
|
/** 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) {
|
class SHA256(bytes: ByteArray) : SecureHash(bytes) {
|
||||||
init {
|
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)
|
Security.addProvider(it)
|
||||||
}
|
}
|
||||||
val bouncyCastlePQCProvider = BouncyCastlePQCProvider().apply {
|
val bouncyCastlePQCProvider = BouncyCastlePQCProvider().apply {
|
||||||
require(name == "BCPQC") // The constant it comes from is not final.
|
require(name == "BCPQC") { "Invalid PQCProvider name" }
|
||||||
}.also {
|
}.also {
|
||||||
Security.addProvider(it)
|
Security.addProvider(it)
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
@ -5,7 +5,10 @@ package net.corda.core.internal
|
|||||||
import net.corda.core.DeleteForDJVM
|
import net.corda.core.DeleteForDJVM
|
||||||
import net.corda.core.KeepForDJVM
|
import net.corda.core.KeepForDJVM
|
||||||
import net.corda.core.crypto.*
|
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.OpaqueBytes
|
||||||
import net.corda.core.utilities.UntrustworthyData
|
import net.corda.core.utilities.UntrustworthyData
|
||||||
import org.slf4j.Logger
|
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. */
|
/** Returns the index of the given item or throws [IllegalArgumentException] if not found. */
|
||||||
fun <T> List<T>.indexOfOrThrow(item: T): Int {
|
fun <T> List<T>.indexOfOrThrow(item: T): Int {
|
||||||
val i = indexOf(item)
|
val i = indexOf(item)
|
||||||
require(i != -1)
|
require(i != -1){"No such element"}
|
||||||
return i
|
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.
|
* 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)
|
require(numOfExpectedBytes > 0)
|
||||||
val baos = ByteArrayOutputStream()
|
val baos = ByteArrayOutputStream()
|
||||||
ZipOutputStream(baos).use { zos ->
|
ZipOutputStream(baos).use { zos ->
|
||||||
@ -226,7 +230,7 @@ data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHa
|
|||||||
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)
|
||||||
}
|
}
|
||||||
@ -498,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)
|
@ -14,8 +14,8 @@ interface NamedCacheFactory {
|
|||||||
* the name can be used to create a file name or a metric name.
|
* the name can be used to create a file name or a metric name.
|
||||||
*/
|
*/
|
||||||
fun checkCacheName(name: String) {
|
fun checkCacheName(name: String) {
|
||||||
require(!name.isBlank())
|
require(!name.isBlank()){"Name must not be empty or only whitespace"}
|
||||||
require(allowedChars.matches(name))
|
require(allowedChars.matches(name)){"Invalid characters in cache name"}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V>
|
fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.nodeapi.internal
|
package net.corda.core.internal
|
||||||
|
|
||||||
// TODO: Add to Corda node.conf to allow customisation
|
// TODO: Add to Corda node.conf to allow customisation
|
||||||
const val NODE_INFO_DIRECTORY = "additional-node-infos"
|
const val NODE_INFO_DIRECTORY = "additional-node-infos"
|
@ -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>,
|
||||||
@ -42,4 +50,75 @@ fun combinedHash(components: Iterable<SecureHash>): SecureHash {
|
|||||||
stream.write(it.bytes)
|
stream.write(it.bytes)
|
||||||
}
|
}
|
||||||
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]) }
|
||||||
|
}
|
||||||
}
|
}
|
@ -9,7 +9,7 @@ class AddressBindingException(val addresses: Set<NetworkHostAndPort>) : CordaRun
|
|||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
private fun message(addresses: Set<NetworkHostAndPort>): String {
|
private fun message(addresses: Set<NetworkHostAndPort>): String {
|
||||||
require(addresses.isNotEmpty())
|
require(addresses.isNotEmpty()) { "Address list must not be empty" }
|
||||||
return if (addresses.size > 1) {
|
return if (addresses.size > 1) {
|
||||||
"Failed to bind on an address in ${addresses.joinToString(", ", "[", "]")}."
|
"Failed to bind on an address in ${addresses.joinToString(", ", "[", "]")}."
|
||||||
} else {
|
} else {
|
||||||
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -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]
|
* 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.
|
* 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
|
@JvmStatic
|
||||||
@CordaInternal
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun <T : Any> from(obj: T, serializationFactory: SerializationFactory = SerializationFactory.defaultFactory,
|
fun <T : Any> from(obj: T, serializationFactory: SerializationFactory = SerializationFactory.defaultFactory,
|
||||||
context: SerializationContext = serializationFactory.defaultContext): SerializedBytes<T> {
|
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
|
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.
|
||||||
*/
|
*/
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
|
@ -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]) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -307,7 +307,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." }
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
* 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 {
|
fun slice(start: Int = 0, end: Int = size): ByteBuffer {
|
||||||
require(start >= 0)
|
require(start >= 0) { "Starting index must be greater than or equal to 0" }
|
||||||
require(end >= 0)
|
require(end >= 0){"End index must be greater or equal to 0"}
|
||||||
val clampedStart = min(start, size)
|
val clampedStart = min(start, size)
|
||||||
val clampedEnd = min(end, size)
|
val clampedEnd = min(end, size)
|
||||||
return ByteBuffer.wrap(_bytes, offset + clampedStart, max(0, clampedEnd - clampedStart)).asReadOnlyBuffer()
|
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 {
|
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
|
@KeepForDJVM
|
||||||
class OpaqueBytesSubSequence(override val bytes: ByteArray, offset: Int, size: Int) : ByteSequence(bytes, offset, size) {
|
class OpaqueBytesSubSequence(override val bytes: ByteArray, offset: Int, size: Int) : ByteSequence(bytes, offset, size) {
|
||||||
init {
|
init {
|
||||||
require(offset >= 0 && offset < 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)
|
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.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.
|
||||||
@ -163,4 +162,4 @@ fun Logger.warnOnce(warning: String) {
|
|||||||
warnings.add(warning)
|
warnings.add(warning)
|
||||||
this.warn(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.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
|
||||||
|
@ -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.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()
|
||||||
|
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.
@ -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.
|
||||||
|
|
||||||
|
@ -48,8 +48,10 @@ Here are the contents of the ``reference.conf`` file for Corda Enterprise:
|
|||||||
|
|
||||||
Fields
|
Fields
|
||||||
------
|
------
|
||||||
The available config fields are listed below. ``baseDirectory`` is available as a substitution value and contains the
|
|
||||||
absolute path to the node's base directory.
|
.. note:: All fields can be used with placeholders for environment variables. For example: ``${NODE_TRUST_STORE_PASSWORD}`` would be replaced by the contents of environment variable ``NODE_TRUST_STORE_PASSWORD``. See: `Hiding Sensitive Data`_
|
||||||
|
|
||||||
|
The available config fields are listed below.
|
||||||
|
|
||||||
:myLegalName: The legal identity of the node. This acts as a human-readable alias to the node's public key and can be used with
|
:myLegalName: The legal identity of the node. This acts as a human-readable alias to the node's public key and can be used with
|
||||||
the network map to look up the node's info. This is the name that is used in the node's certificates (either when requesting them
|
the network map to look up the node's info. This is the name that is used in the node's certificates (either when requesting them
|
||||||
@ -395,3 +397,47 @@ Together with the above configuration `tlsCertCrlIssuer` option needs to be set
|
|||||||
|
|
||||||
This set-up ensures that the TLS-level certificates are embedded with the CRL distribution point referencing the CRL issued by R3.
|
This set-up ensures that the TLS-level certificates are embedded with the CRL distribution point referencing the CRL issued by R3.
|
||||||
In cases where a proprietary CRL infrastructure is provided those values need to be changed accordingly.
|
In cases where a proprietary CRL infrastructure is provided those values need to be changed accordingly.
|
||||||
|
|
||||||
|
Hiding Sensitive Data
|
||||||
|
---------------------
|
||||||
|
A frequent requirement is that configuration files must not expose passwords to unauthorised readers. By leveraging environment variables, it is possible to hide passwords and other similar fields.
|
||||||
|
|
||||||
|
Take a simple node config that wishes to protect the node cryptographic stores:
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
myLegalName : "O=PasswordProtectedNode,OU=corda,L=London,C=GB"
|
||||||
|
keyStorePassword : ${KEY_PASS}
|
||||||
|
trustStorePassword : ${TRUST_PASS}
|
||||||
|
p2pAddress : "localhost:12345"
|
||||||
|
devMode : false
|
||||||
|
compatibilityZoneURL : "https://cz.corda.net"
|
||||||
|
|
||||||
|
By delegating to a password store, and using `command substitution` it is possible to ensure that sensitive passwords never appear in plain text.
|
||||||
|
The below examples are of loading Corda with the KEY_PASS and TRUST_PASS variables read from a program named ``corporatePasswordStore``.
|
||||||
|
|
||||||
|
|
||||||
|
Bash
|
||||||
|
~~~~
|
||||||
|
|
||||||
|
.. sourcecode:: shell
|
||||||
|
|
||||||
|
KEY_PASS=$(corporatePasswordStore --cordaKeyStorePassword) TRUST_PASS=$(corporatePasswordStore --cordaTrustStorePassword) java -jar corda.jar
|
||||||
|
|
||||||
|
Windows PowerShell
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. sourcecode:: shell
|
||||||
|
|
||||||
|
$env:KEY_PASS=$(corporatePasswordStore --cordaKeyStorePassword); $env:TRUST_PASS=$(corporatePasswordStore --cordaTrustStorePassword); java -jar corda.jar
|
||||||
|
|
||||||
|
|
||||||
|
For launching on Windows without PowerShell, it is not possible to perform command substitution, and so the variables must be specified manually, for example:
|
||||||
|
|
||||||
|
.. sourcecode:: shell
|
||||||
|
|
||||||
|
SET KEY_PASS=mypassword & SET TRUST_PASS=mypassword & java -jar corda.jar
|
||||||
|
|
||||||
|
.. warning:: If this approach is taken, the passwords will appear in the windows command prompt history.
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,9 +46,13 @@ Several ``ext`` variables are used in a CorDapp's ``build.gradle`` file to defin
|
|||||||
|
|
||||||
``corda_gradle_plugins_versions`` are given in the form ``major.minor.patch``. You should use the same ``major`` and
|
``corda_gradle_plugins_versions`` are given in the form ``major.minor.patch``. You should use the same ``major`` and
|
||||||
``minor`` versions as the Corda version you are using, and the latest ``patch`` version. A list of all the available
|
``minor`` versions as the Corda version you are using, and the latest ``patch`` version. A list of all the available
|
||||||
versions can be found here: https://bintray.com/r3/corda/cordapp. If in doubt, you should base yourself on the version numbers used in the ``build.gradle`` file of the `Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_ and the `Java CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_.
|
versions can be found here: https://bintray.com/r3/corda/cordapp. If in doubt, you should base yourself on the version
|
||||||
|
numbers used in the ``build.gradle`` file of the
|
||||||
|
`Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_ and the
|
||||||
|
`Java CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_.
|
||||||
|
|
||||||
For example, to use version 3.0 of Corda, version 3.0.8 of the Corda gradle plugins, version 0.7.9 of Quasar, and version 1.1.60 of Kotlin, you'd write:
|
For example, to use version 3.0 of Corda, version 3.0.8 of the Corda gradle plugins, version 0.7.9 of Quasar, and
|
||||||
|
version 1.1.60 of Kotlin, you'd write:
|
||||||
|
|
||||||
.. sourcecode:: groovy
|
.. sourcecode:: groovy
|
||||||
|
|
||||||
@ -70,19 +74,55 @@ The ``cordformation`` plugin adds two new gradle configurations:
|
|||||||
configurations should be used for any Corda dependency (e.g. ``corda-core``, ``corda-node``) in order to prevent a
|
configurations should be used for any Corda dependency (e.g. ``corda-core``, ``corda-node``) in order to prevent a
|
||||||
dependency from being included twice (once in the CorDapp JAR and once in the Corda JARs).
|
dependency from being included twice (once in the CorDapp JAR and once in the Corda JARs).
|
||||||
|
|
||||||
To build against Corda, you must add the following to your ``build.gradle`` file:
|
Here are some guidelines for Corda dependencies:
|
||||||
|
|
||||||
* ``net.corda:corda:$corda_release_version`` as a ``cordaRuntime`` dependency
|
* When building a CorDapp, you should always include ``net.corda:corda-core:$corda_release_version`` as a
|
||||||
* Each Corda compile dependency (eg ``net.corda:corda-core:$corda_release_version``) as a ``cordaCompile`` dependency
|
``cordaCompile`` dependency, and ``net.corda:corda:$corda_release_version`` as a ``cordaRuntime`` dependency
|
||||||
|
|
||||||
You may also want to add:
|
* When building an RPC client that communicates with a node (e.g. a webserver), you should include
|
||||||
|
``net.corda:corda-rpc:$corda_release_version`` as a ``cordaCompile`` dependency
|
||||||
|
|
||||||
* ``net.corda:corda-test-utils:$corda_release_version`` as a ``testCompile`` dependency, in order to use Corda's test
|
* When you need to use the network bootstrapper to bootstrap a local network (e.g. when using ``Cordformation``), you
|
||||||
|
should include ``net.corda:corda-node-api:$corda_release_version`` as a ``cordaCompile`` dependency
|
||||||
|
|
||||||
|
* To use Corda's test frameworks, add ``net.corda:corda-test-utils:$corda_release_version`` as a ``testCompile``
|
||||||
|
dependency. Never include ``corda-test-utils`` as a ``compile`` or ``cordaCompile`` dependency
|
||||||
|
|
||||||
|
* Any other Corda dependencies you need should be included as ``cordaCompile`` dependencies
|
||||||
|
|
||||||
|
Here is an overview of the various Corda dependencies:
|
||||||
|
|
||||||
|
* ``corda`` - The Corda fat JAR. Do not use as a compile dependency. Required as a ``cordaRuntime`` dependency when
|
||||||
|
using ``Cordformation``
|
||||||
|
* ``corda-confidential-identities`` - A part of the core Corda libraries. Automatically pulled in by other libraries
|
||||||
|
* ``corda-core`` - Usually automatically included by another dependency, contains core Corda utilities, model, and
|
||||||
|
functionality. Include manually if the utilities are useful or you are writing a library for Corda
|
||||||
|
* ``corda-core-deterministic`` - Used by the Corda node for deterministic contracts. Not likely to be used externally
|
||||||
|
* ``corda-djvm`` - Used by the Corda node for deterministic contracts. Not likely to be used externally
|
||||||
|
* ``corda-finance`` - The Corda finance CorDapp. Only include as a ``cordaCompile`` dependency if using as a dependent
|
||||||
|
Cordapp or if you need access to the Corda finance types. Use as a ``cordapp`` dependency if using as a CorDapp
|
||||||
|
dependency (see below)
|
||||||
|
* ``corda-jackson`` - Corda Jackson support. Use if you plan to serialise Corda objects to and/or from JSON
|
||||||
|
* ``corda-jfx`` - JavaFX utilities with some Corda-specific models and utilities. Only use with JavaFX apps
|
||||||
|
* ``corda-mock`` - A small library of useful mocks. Use if the classes are useful to you
|
||||||
|
* ``corda-node`` - The Corda node. Do not depend on. Used only by the Corda fat JAR and indirectly in testing
|
||||||
frameworks
|
frameworks
|
||||||
* ``net.corda:corda-webserver:$corda_release_version`` as a ``cordaRuntime`` dependency, in order to use Corda's
|
* ``corda-node-api`` - The node API. Required to bootstrap a local network
|
||||||
built-in development webserver
|
* ``corda-node-driver`` - Testing utility for programmatically starting nodes from JVM languages. Use for tests
|
||||||
|
* ``corda-notary-bft-smart`` - A Corda notary implementation
|
||||||
.. warning:: Never include ``corda-test-utils`` as a ``compile`` or ``cordaCompile`` dependency.
|
* ``corda-notary-raft`` - A Corda notary implementation
|
||||||
|
* ``corda-rpc`` - The Corda RPC client library. Used when writing an RPC client
|
||||||
|
* ``corda-serialization`` - The Corda core serialization library. Automatically included by other dependencies
|
||||||
|
* ``corda-serialization-deterministic`` - The Corda core serialization library. Automatically included by other
|
||||||
|
dependencies
|
||||||
|
* ``corda-shell`` - Used by the Corda node. Never depend on directly
|
||||||
|
* ``corda-test-common`` - A common test library. Automatically included by other test libraries
|
||||||
|
* ``corda-test-utils`` - Used when writing tests against Corda/Cordapps
|
||||||
|
* ``corda-tools-explorer`` - The Node Explorer tool. Do not depend on
|
||||||
|
* ``corda-tools-network-bootstrapper`` - The Network Builder tool. Useful in build scripts
|
||||||
|
* ``corda-tools-shell-cli`` - The Shell CLI tool. Useful in build scripts
|
||||||
|
* ``corda-webserver-impl`` - The Corda webserver fat JAR. Deprecated. Usually only used by build scripts
|
||||||
|
* ``corda-websever`` - The Corda webserver library. Deprecated. Use a standard webserver library such as Spring instead
|
||||||
|
|
||||||
Dependencies on other CorDapps
|
Dependencies on other CorDapps
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
@ -157,19 +197,19 @@ The example ``cordapp`` plugin with plugin ``signing`` configuration:
|
|||||||
}
|
}
|
||||||
//...
|
//...
|
||||||
|
|
||||||
CorDapp auto-signing allows to use signature constraints for contracts from the CorDapp
|
CorDapp auto-signing allows to use signature constraints for contracts from the CorDapp without need to create a
|
||||||
without need to create a keystore and configure the ``cordapp`` plugin.
|
keystore and configure the ``cordapp`` plugin. For production deployment ensure to sign the CorDapp using your own
|
||||||
For production deployment ensure to sign the CorDapp using your own certificate e.g. by setting system properties to point to an external keystore
|
certificate e.g. by setting system properties to point to an external keystore or by disabling signing in ``cordapp``
|
||||||
or by disabling signing in ``cordapp`` plugin and signing the CordDapp JAR downstream in your build pipeline.
|
plugin and signing the CordDapp JAR downstream in your build pipeline. CorDapp signed by Corda development certificate
|
||||||
CorDapp signed by Corda development certificate is accepted by Corda node only when running in the development mode.
|
is accepted by Corda node only when running in the development mode. In case CordDapp signed by the (default)
|
||||||
In case CordDapp signed by the (default) development key is run on node in the production mode (e.g. for testing),
|
development key is run on node in the production mode (e.g. for testing), the node may be set to accept the development
|
||||||
the node may be set to accept the development key by adding the ``cordappSignerKeyFingerprintBlacklist = []`` property set to empty list
|
key by adding the ``cordappSignerKeyFingerprintBlacklist = []`` property set to empty list (see
|
||||||
(see :ref:`Configuring a node <corda_configuration_file_signer_blacklist>`).
|
:ref:`Configuring a node <corda_configuration_file_signer_blacklist>`).
|
||||||
|
|
||||||
Signing options can be contextually overwritten by the relevant system properties as described above.
|
Signing options can be contextually overwritten by the relevant system properties as described above. This allows the
|
||||||
This allows the single ``build.gradle`` file to be used for a development build (defaulting to the Corda development keystore)
|
single ``build.gradle`` file to be used for a development build (defaulting to the Corda development keystore) and for
|
||||||
and for a production build (using an external keystore).
|
a production build (using an external keystore). The example system properties setup for the build process which
|
||||||
The example system properties setup for the build process which overrides signing options:
|
overrides signing options:
|
||||||
|
|
||||||
.. sourcecode:: shell
|
.. sourcecode:: shell
|
||||||
|
|
||||||
@ -187,8 +227,9 @@ CorDapp signing can be disabled for a build:
|
|||||||
|
|
||||||
./gradlew -Dsigning.enabled=false
|
./gradlew -Dsigning.enabled=false
|
||||||
|
|
||||||
Other system properties can be explicitly assigned to options by calling ``System.getProperty`` in ``cordapp`` plugin configuration.
|
Other system properties can be explicitly assigned to options by calling ``System.getProperty`` in ``cordapp`` plugin
|
||||||
For example the below configuration sets the specific signing algorithm when a system property is available otherwise defaults to an empty string:
|
configuration. For example the below configuration sets the specific signing algorithm when a system property is
|
||||||
|
available otherwise defaults to an empty string:
|
||||||
|
|
||||||
.. sourcecode:: groovy
|
.. sourcecode:: groovy
|
||||||
|
|
||||||
@ -200,7 +241,8 @@ For example the below configuration sets the specific signing algorithm when a s
|
|||||||
}
|
}
|
||||||
//...
|
//...
|
||||||
|
|
||||||
Then the build process can set the value for *custom.sigalg* system property and other system properties recognized by ``cordapp`` plugin:
|
Then the build process can set the value for *custom.sigalg* system property and other system properties recognized by
|
||||||
|
``cordapp`` plugin:
|
||||||
|
|
||||||
.. sourcecode:: shell
|
.. sourcecode:: shell
|
||||||
|
|
||||||
@ -216,8 +258,8 @@ Cordformation plugin can also sign CorDapps JARs, when deploying set of nodes, s
|
|||||||
|
|
||||||
Example
|
Example
|
||||||
^^^^^^^
|
^^^^^^^
|
||||||
Below is a sample of what a CorDapp's Gradle dependencies block might look like. When building your own CorDapp, you should
|
Below is a sample of what a CorDapp's Gradle dependencies block might look like. When building your own CorDapp, you
|
||||||
base yourself on the ``build.gradle`` file of the
|
should base yourself on the ``build.gradle`` file of the
|
||||||
`Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_ or the
|
`Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_ or the
|
||||||
`Java CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_.
|
`Java CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_.
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 133 KiB |
@ -16,7 +16,7 @@ The example CorDapp allows nodes to agree IOUs with each other, as long as they
|
|||||||
|
|
||||||
We will deploy and run the CorDapp on four test nodes:
|
We will deploy and run the CorDapp on four test nodes:
|
||||||
|
|
||||||
* **Notary**, which hosts a validating notary service
|
* **Notary**, which runs a notary service
|
||||||
* **PartyA**
|
* **PartyA**
|
||||||
* **PartyB**
|
* **PartyB**
|
||||||
* **PartyC**
|
* **PartyC**
|
||||||
@ -30,10 +30,9 @@ Start by downloading the example CorDapp from GitHub:
|
|||||||
|
|
||||||
* Set up your machine by following the :doc:`quickstart guide <getting-set-up>`
|
* Set up your machine by following the :doc:`quickstart guide <getting-set-up>`
|
||||||
|
|
||||||
* Clone the example CorDapp from the `cordapp-example repository <https://github.com/corda/cordapp-example>`_ using
|
* Clone the samples repository from using the following command: ``git clone https://github.com/corda/samples``
|
||||||
the following command: ``git clone https://github.com/corda/cordapp-example``
|
|
||||||
|
|
||||||
* Change directories to the freshly cloned repo: ``cd cordapp-example``
|
* Change directories to the ``cordapp-example`` folder: ``cd samples/cordapp-example``
|
||||||
|
|
||||||
Opening the example CorDapp in IntelliJ
|
Opening the example CorDapp in IntelliJ
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
@ -41,7 +40,7 @@ Let's open the example CorDapp in IntelliJ IDEA:
|
|||||||
|
|
||||||
* Open IntelliJ
|
* Open IntelliJ
|
||||||
|
|
||||||
* A splash screen will appear. Click ``open``, select the cloned ``cordapp-example`` folder, and click ``OK``
|
* A splash screen will appear. Click ``open``, navigate to and select the ``cordapp-example`` folder, and click ``OK``
|
||||||
|
|
||||||
* Once the project is open, click ``File``, then ``Project Structure``. Under ``Project SDK:``, set the project SDK by
|
* Once the project is open, click ``File``, then ``Project Structure``. Under ``Project SDK:``, set the project SDK by
|
||||||
clicking ``New...``, clicking ``JDK``, and navigating to ``C:\Program Files\Java\jdk1.8.0_XXX`` on Windows or ``Library/Java/JavaVirtualMachines/jdk1.8.XXX`` on MacOSX (where ``XXX`` is the
|
clicking ``New...``, clicking ``JDK``, and navigating to ``C:\Program Files\Java\jdk1.8.0_XXX`` on Windows or ``Library/Java/JavaVirtualMachines/jdk1.8.XXX`` on MacOSX (where ``XXX`` is the
|
||||||
@ -65,61 +64,59 @@ The example CorDapp has the following structure:
|
|||||||
│ │ └── log4j2.xml
|
│ │ └── log4j2.xml
|
||||||
│ └── test
|
│ └── test
|
||||||
│ └── log4j2.xml
|
│ └── log4j2.xml
|
||||||
├── doc
|
|
||||||
│ └── example_flow.plantuml
|
|
||||||
├── gradle
|
├── gradle
|
||||||
│ └── wrapper
|
│ └── wrapper
|
||||||
│ ├── gradle-wrapper.jar
|
│ ├── gradle-wrapper.jar
|
||||||
│ └── gradle-wrapper.properties
|
│ └── gradle-wrapper.properties
|
||||||
├── lib
|
|
||||||
│ ├── README.txt
|
|
||||||
│ └── quasar.jar
|
|
||||||
├── java-source
|
├── java-source
|
||||||
│ └── ...
|
|
||||||
├── kotlin-source
|
|
||||||
│ ├── build.gradle
|
│ ├── build.gradle
|
||||||
│ └── src
|
│ └── src
|
||||||
|
│ ├── integrationTest
|
||||||
|
│ │ └── java
|
||||||
|
│ │ └── com
|
||||||
|
│ │ └── example
|
||||||
|
│ │ └── DriverBasedTests.java
|
||||||
│ ├── main
|
│ ├── main
|
||||||
│ │ ├── kotlin
|
│ │ ├── java
|
||||||
│ │ │ └── com
|
│ │ │ └── com
|
||||||
│ │ │ └── example
|
│ │ │ └── example
|
||||||
│ │ │ ├── api
|
│ │ │ ├── api
|
||||||
│ │ │ │ └── ExampleApi.kt
|
│ │ │ │ └── ExampleApi.java
|
||||||
│ │ │ ├── client
|
│ │ │ ├── client
|
||||||
│ │ │ │ └── ExampleClientRPC.kt
|
│ │ │ │ └── ExampleClientRPC.java
|
||||||
│ │ │ ├── contract
|
│ │ │ ├── contract
|
||||||
│ │ │ │ └── IOUContract.kt
|
│ │ │ │ └── IOUContract.java
|
||||||
│ │ │ ├── flow
|
│ │ │ ├── flow
|
||||||
│ │ │ │ └── ExampleFlow.kt
|
│ │ │ │ └── ExampleFlow.java
|
||||||
│ │ │ ├── model
|
|
||||||
│ │ │ │ └── IOU.kt
|
|
||||||
│ │ │ ├── plugin
|
│ │ │ ├── plugin
|
||||||
│ │ │ │ └── ExamplePlugin.kt
|
│ │ │ │ └── ExamplePlugin.java
|
||||||
│ │ │ ├── schema
|
│ │ │ ├── schema
|
||||||
│ │ │ │ └── IOUSchema.kt
|
│ │ │ │ ├── IOUSchema.java
|
||||||
|
│ │ │ │ └── IOUSchemaV1.java
|
||||||
│ │ │ └── state
|
│ │ │ └── state
|
||||||
│ │ │ └── IOUState.kt
|
│ │ │ └── IOUState.java
|
||||||
│ │ └── resources
|
│ │ └── resources
|
||||||
│ │ ├── META-INF
|
│ │ ├── META-INF
|
||||||
│ │ │ └── services
|
│ │ │ └── services
|
||||||
│ │ │ └── net.corda.webserver.services.WebServerPluginRegistry
|
│ │ │ └── net.corda.webserver.services.WebServerPluginRegistry
|
||||||
│ │ ├── certificates
|
|
||||||
│ │ │ ├── readme.txt
|
|
||||||
│ │ │ ├── sslkeystore.jks
|
|
||||||
│ │ │ └── truststore.jks
|
|
||||||
│ │ └── exampleWeb
|
│ │ └── exampleWeb
|
||||||
│ │ ├── index.html
|
│ │ ├── index.html
|
||||||
│ │ └── js
|
│ │ └── js
|
||||||
│ │ └── angular-module.js
|
│ │ └── angular-module.js
|
||||||
│ └── test
|
│ └── test
|
||||||
│ └── kotlin
|
│ └── java
|
||||||
│ └── com
|
│ └── com
|
||||||
│ └── example
|
│ └── example
|
||||||
│ ├── Main.kt
|
│ ├── NodeDriver.java
|
||||||
│ ├── contract
|
│ ├── contract
|
||||||
│ │ └── IOUContractTests.kt
|
│ │ └── IOUContractTests.java
|
||||||
│ └── flow
|
│ └── flow
|
||||||
│ └── IOUFlowTests.kt
|
│ └── IOUFlowTests.java
|
||||||
|
├── kotlin-source
|
||||||
|
│ ├── ...
|
||||||
|
├── lib
|
||||||
|
│ ├── README.txt
|
||||||
|
│ └── quasar.jar
|
||||||
├── .gitignore
|
├── .gitignore
|
||||||
├── LICENCE
|
├── LICENCE
|
||||||
├── README.md
|
├── README.md
|
||||||
@ -137,16 +134,15 @@ The key files and directories are as follows:
|
|||||||
* **gradle** contains the gradle wrapper, which allows the use of Gradle without installing it yourself and worrying
|
* **gradle** contains the gradle wrapper, which allows the use of Gradle without installing it yourself and worrying
|
||||||
about which version is required
|
about which version is required
|
||||||
* **lib** contains the Quasar jar which rewrites our CorDapp's flows to be checkpointable
|
* **lib** contains the Quasar jar which rewrites our CorDapp's flows to be checkpointable
|
||||||
* **kotlin-source** contains the source code for the example CorDapp written in Kotlin
|
* **java-source** contains the source code for the example CorDapp written in Java
|
||||||
|
|
||||||
* **kotlin-source/src/main/kotlin** contains the source code for the example CorDapp
|
* **java-source/src/main/java** contains the source code for the example CorDapp
|
||||||
* **kotlin-source/src/main/resources** contains the certificate store, some static web content to be served by the
|
* **java-source/src/main/resources** contains the certificate store, some static web content to be served by the
|
||||||
nodes and the WebServerPluginRegistry file
|
nodes and the WebServerPluginRegistry file
|
||||||
* **kotlin-source/src/test/kotlin** contains unit tests for the contracts and flows, and the driver to run the nodes
|
* **java-source/src/test/java** contains unit tests for the contracts and flows, and the driver to run the nodes
|
||||||
via IntelliJ
|
via IntelliJ
|
||||||
|
|
||||||
* **java-source** contains the same source code, but written in Java. CorDapps can be developed in any language
|
* **kotlin-source** contains the same source code, but written in Kotlin. CorDapps can be developed in either Java and Kotlin
|
||||||
targeting the JVM
|
|
||||||
|
|
||||||
Running the example CorDapp
|
Running the example CorDapp
|
||||||
---------------------------
|
---------------------------
|
||||||
|
@ -32,14 +32,14 @@ data class PrintedTransitionGraph(val stateClassName: String, val printedPUML: S
|
|||||||
* Shorthand for defining transitions directly from the command class
|
* Shorthand for defining transitions directly from the command class
|
||||||
*/
|
*/
|
||||||
fun <S, R> CommandData.txDef(signer: R? = null, from: S?, to: List<S?>):
|
fun <S, R> CommandData.txDef(signer: R? = null, from: S?, to: List<S?>):
|
||||||
TransitionDef<S, R> = TransitionDef(this::class.java, signer, from, to)
|
TransitionDef<S, R> = TransitionDef(this::class.java, signer, from, to)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For a given [stateClass] that tracks a status, it holds all possible transitions in [ts].
|
* For a given [stateClass] that tracks a status, it holds all possible transitions in [ts].
|
||||||
* This can be used for generic [verify] in contract code as well as for visualizing the state transition graph in PUML ([printGraph]).
|
* This can be used for generic [verify] in contract code as well as for visualizing the state transition graph in PUML ([printGraph]).
|
||||||
*/
|
*/
|
||||||
class StatusTransitions<out S, in R, T : StatusTrackingContractState<S, R>>(private val stateClass: KClass<T>,
|
class StatusTransitions<out S, in R, T : StatusTrackingContractState<S, R>>(private val stateClass: KClass<T>,
|
||||||
private vararg val ts: TransitionDef<S, R>) {
|
private vararg val ts: TransitionDef<S, R>) {
|
||||||
|
|
||||||
private val allowedCmds = ts.map { it.cmd }.toSet()
|
private val allowedCmds = ts.map { it.cmd }.toSet()
|
||||||
|
|
||||||
@ -67,11 +67,11 @@ class StatusTransitions<out S, in R, T : StatusTrackingContractState<S, R>>(priv
|
|||||||
// for each combination of in x out which should normally be at most 1...
|
// for each combination of in x out which should normally be at most 1...
|
||||||
inputStates.forEach { inp ->
|
inputStates.forEach { inp ->
|
||||||
outputStates.forEach { outp ->
|
outputStates.forEach { outp ->
|
||||||
require(inp != null || outp != null)
|
require(inp != null || outp != null) { "Input and output states cannot be both left unspecified" }
|
||||||
val options = matchingTransitions(inp?.status, outp?.status, cmd.value)
|
val options = matchingTransitions(inp?.status, outp?.status, cmd.value)
|
||||||
|
|
||||||
val signerGroup = options.groupBy { it.signer }.entries.singleOrNull()
|
val signerGroup = options.groupBy { it.signer }.entries.singleOrNull()
|
||||||
?: throw IllegalStateException("Cannot have different signers in StatusTransitions for the same command.")
|
?: throw IllegalStateException("Cannot have different signers in StatusTransitions for the same command.")
|
||||||
val signer = signerGroup.key
|
val signer = signerGroup.key
|
||||||
if (signer != null) {
|
if (signer != null) {
|
||||||
// which state determines who is the signer? by default the input, unless it's the initial transition
|
// which state determines who is the signer? by default the input, unless it's the initial transition
|
||||||
|
@ -132,7 +132,8 @@ class FlowWorkerServiceHub(override val configuration: NodeConfiguration,
|
|||||||
identityService::wellKnownPartyFromX500Name,
|
identityService::wellKnownPartyFromX500Name,
|
||||||
identityService::wellKnownPartyFromAnonymous,
|
identityService::wellKnownPartyFromAnonymous,
|
||||||
schemaService,
|
schemaService,
|
||||||
cacheFactory
|
cacheFactory,
|
||||||
|
cordappLoader.appClassLoader
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -92,7 +92,8 @@ class RpcWorkerServiceHub(override val configuration: NodeConfiguration,
|
|||||||
identityService::wellKnownPartyFromX500Name,
|
identityService::wellKnownPartyFromX500Name,
|
||||||
identityService::wellKnownPartyFromAnonymous,
|
identityService::wellKnownPartyFromAnonymous,
|
||||||
schemaService,
|
schemaService,
|
||||||
cacheFactory
|
cacheFactory,
|
||||||
|
cordappLoader.appClassLoader
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -203,7 +203,7 @@ class UniversalContract : Contract {
|
|||||||
val rest = extractRemainder(arr, action)
|
val rest = extractRemainder(arr, action)
|
||||||
|
|
||||||
// for now - let's assume not
|
// for now - let's assume not
|
||||||
require(rest is Zero)
|
require(rest is Zero) { "Remainder must be zero" }
|
||||||
|
|
||||||
requireThat {
|
requireThat {
|
||||||
"action must have a time-window" using (tx.timeWindow != null)
|
"action must have a time-window" using (tx.timeWindow != null)
|
||||||
|
@ -321,7 +321,7 @@ open class BusinessCalendar(val holidayDates: List<LocalDate>) {
|
|||||||
* TODO: Make more efficient if necessary
|
* TODO: Make more efficient if necessary
|
||||||
*/
|
*/
|
||||||
fun moveBusinessDays(date: LocalDate, direction: DateRollDirection, i: Int): LocalDate {
|
fun moveBusinessDays(date: LocalDate, direction: DateRollDirection, i: Int): LocalDate {
|
||||||
require(i >= 0)
|
require(i >= 0){"Days to add/subtract must be positive"}
|
||||||
if (i == 0) return date
|
if (i == 0) return date
|
||||||
var retDate = date
|
var retDate = date
|
||||||
var ctr = 0
|
var ctr = 0
|
||||||
|
@ -39,8 +39,8 @@ private fun rowsToAmount(currency: Currency, rows: Vault.Page<FungibleAsset<*>>)
|
|||||||
return if (rows.otherResults.isEmpty()) {
|
return if (rows.otherResults.isEmpty()) {
|
||||||
Amount(0L, currency)
|
Amount(0L, currency)
|
||||||
} else {
|
} else {
|
||||||
require(rows.otherResults.size == 2)
|
require(rows.otherResults.size == 2){"Invalid number of rows returned by query"}
|
||||||
require(rows.otherResults[1] == currency.currencyCode)
|
require(rows.otherResults[1] == currency.currencyCode){"Currency on rows returned by query does not match expected"}
|
||||||
val quantity = rows.otherResults[0] as Long
|
val quantity = rows.otherResults[0] as Long
|
||||||
Amount(quantity, currency)
|
Amount(quantity, currency)
|
||||||
}
|
}
|
||||||
|
@ -298,7 +298,7 @@ abstract class OnLedgerAsset<T : Any, out C : CommandData, S : FungibleAsset<T>>
|
|||||||
issueCommand: CommandData): Set<PublicKey> {
|
issueCommand: CommandData): Set<PublicKey> {
|
||||||
check(tx.inputStates().isEmpty())
|
check(tx.inputStates().isEmpty())
|
||||||
check(tx.outputStates().map { it.data }.filterIsInstance(transactionState.javaClass).isEmpty())
|
check(tx.outputStates().map { it.data }.filterIsInstance(transactionState.javaClass).isEmpty())
|
||||||
require(transactionState.data.amount.quantity > 0)
|
require(transactionState.data.amount.quantity > 0){"Amount to issue must be greater than zero"}
|
||||||
val at = transactionState.data.amount.token.issuer
|
val at = transactionState.data.amount.token.issuer
|
||||||
val commandSigner = at.party.owningKey
|
val commandSigner = at.party.owningKey
|
||||||
tx.addOutputState(transactionState)
|
tx.addOutputState(transactionState)
|
||||||
|
@ -140,8 +140,8 @@ object TwoPartyDealFlow {
|
|||||||
// Verify the transaction identities represent the correct parties
|
// Verify the transaction identities represent the correct parties
|
||||||
val wellKnownOtherParty = serviceHub.identityService.wellKnownPartyFromAnonymous(it.primaryIdentity)
|
val wellKnownOtherParty = serviceHub.identityService.wellKnownPartyFromAnonymous(it.primaryIdentity)
|
||||||
val wellKnownMe = serviceHub.identityService.wellKnownPartyFromAnonymous(it.secondaryIdentity)
|
val wellKnownMe = serviceHub.identityService.wellKnownPartyFromAnonymous(it.secondaryIdentity)
|
||||||
require(wellKnownOtherParty == otherSideSession.counterparty)
|
require(wellKnownOtherParty == otherSideSession.counterparty){"Well known party for handshake identity ${it.primaryIdentity} does not match counterparty"}
|
||||||
require(wellKnownMe == ourIdentity)
|
require(wellKnownMe == ourIdentity){"Well known party for handshake identity ${it.secondaryIdentity} does not match ourIdentity"}
|
||||||
validateHandshake(it)
|
validateHandshake(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -203,7 +203,7 @@ object TwoPartyTradeFlow {
|
|||||||
// The asset must either be owned by the well known identity of the counterparty, or we must be able to
|
// The asset must either be owned by the well known identity of the counterparty, or we must be able to
|
||||||
// prove the owner is a confidential identity of the counterparty.
|
// prove the owner is a confidential identity of the counterparty.
|
||||||
val assetForSaleIdentity = serviceHub.identityService.wellKnownPartyFromAnonymous(asset.owner)
|
val assetForSaleIdentity = serviceHub.identityService.wellKnownPartyFromAnonymous(asset.owner)
|
||||||
require(assetForSaleIdentity == sellerSession.counterparty)
|
require(assetForSaleIdentity == sellerSession.counterparty){"Well known identity lookup returned identity that does not match counterparty"}
|
||||||
|
|
||||||
// Register the identity we're about to send payment to. This shouldn't be the same as the asset owner
|
// Register the identity we're about to send payment to. This shouldn't be the same as the asset owner
|
||||||
// identity, so that anonymity is enforced.
|
// identity, so that anonymity is enforced.
|
||||||
|
@ -9,8 +9,8 @@ data class ScreenCoordinate(val screenX: Double, val screenY: Double)
|
|||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
data class WorldCoordinate(val latitude: Double, val longitude: Double) {
|
data class WorldCoordinate(val latitude: Double, val longitude: Double) {
|
||||||
init {
|
init {
|
||||||
require(latitude in -90..90)
|
require(latitude in -90..90){"Latitude must be between -90 and +90"}
|
||||||
require(longitude in -180..180)
|
require(longitude in -180..180){"Longitude must be between -180 and +180"}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,8 +24,8 @@ data class WorldCoordinate(val latitude: Double, val longitude: Double) {
|
|||||||
@Suppress("unused") // Used from the visualiser GUI.
|
@Suppress("unused") // Used from the visualiser GUI.
|
||||||
fun project(screenWidth: Double, screenHeight: Double, topLatitude: Double, bottomLatitude: Double,
|
fun project(screenWidth: Double, screenHeight: Double, topLatitude: Double, bottomLatitude: Double,
|
||||||
leftLongitude: Double, rightLongitude: Double): ScreenCoordinate {
|
leftLongitude: Double, rightLongitude: Double): ScreenCoordinate {
|
||||||
require(latitude in bottomLatitude..topLatitude)
|
require(latitude in bottomLatitude..topLatitude){"Latitude must be between $bottomLatitude and $topLatitude"}
|
||||||
require(longitude in leftLongitude..rightLongitude)
|
require(longitude in leftLongitude..rightLongitude){"Longitude must be between $leftLongitude and $rightLongitude"}
|
||||||
|
|
||||||
fun deg2rad(deg: Double) = deg * Math.PI / 180.0
|
fun deg2rad(deg: Double) = deg * Math.PI / 180.0
|
||||||
val leftLngRad = deg2rad(leftLongitude)
|
val leftLngRad = deg2rad(leftLongitude)
|
||||||
|
@ -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>
|
||||||
|
@ -47,7 +47,7 @@ object DevIdentityGenerator {
|
|||||||
|
|
||||||
/** Generates a CFT notary identity, where the entire cluster shares a key pair. */
|
/** Generates a CFT notary identity, where the entire cluster shares a key pair. */
|
||||||
fun generateDistributedNotarySingularIdentity(dirs: List<Path>, notaryName: CordaX500Name): Party {
|
fun generateDistributedNotarySingularIdentity(dirs: List<Path>, notaryName: CordaX500Name): Party {
|
||||||
require(dirs.isNotEmpty())
|
require(dirs.isNotEmpty()){"At least one directory to generate identity for must be specified"}
|
||||||
|
|
||||||
log.trace { "Generating singular identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
|
log.trace { "Generating singular identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ object DevIdentityGenerator {
|
|||||||
|
|
||||||
/** Generates a BFT notary identity: individual key pairs for each cluster member, and a shared composite key. */
|
/** Generates a BFT notary identity: individual key pairs for each cluster member, and a shared composite key. */
|
||||||
fun generateDistributedNotaryCompositeIdentity(dirs: List<Path>, notaryName: CordaX500Name, threshold: Int = 1): Party {
|
fun generateDistributedNotaryCompositeIdentity(dirs: List<Path>, notaryName: CordaX500Name, threshold: Int = 1): Party {
|
||||||
require(dirs.isNotEmpty())
|
require(dirs.isNotEmpty()){"At least one directory to generate identity for must be specified"}
|
||||||
|
|
||||||
log.trace { "Generating composite identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
|
log.trace { "Generating composite identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
|
||||||
|
|
||||||
|
@ -254,7 +254,7 @@ object X509Utilities {
|
|||||||
crlIssuer: X500Name? = null): X509Certificate {
|
crlIssuer: X500Name? = null): X509Certificate {
|
||||||
val builder = createPartialCertificate(certificateType, issuer, issuerPublicKey, subject, subjectPublicKey, validityWindow, nameConstraints, crlDistPoint, crlIssuer)
|
val builder = createPartialCertificate(certificateType, issuer, issuerPublicKey, subject, subjectPublicKey, validityWindow, nameConstraints, crlDistPoint, crlIssuer)
|
||||||
return builder.build(issuerSigner).run {
|
return builder.build(issuerSigner).run {
|
||||||
require(isValidOn(Date()))
|
require(isValidOn(Date())){"Certificate is not valid at instant now"}
|
||||||
toJca()
|
toJca()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -292,8 +292,8 @@ object X509Utilities {
|
|||||||
crlDistPoint,
|
crlDistPoint,
|
||||||
crlIssuer)
|
crlIssuer)
|
||||||
return builder.build(signer).run {
|
return builder.build(signer).run {
|
||||||
require(isValidOn(Date()))
|
require(isValidOn(Date())){"Certificate is not valid at instant now"}
|
||||||
require(isSignatureValid(JcaContentVerifierProviderBuilder().build(issuerKeyPair.public)))
|
require(isSignatureValid(JcaContentVerifierProviderBuilder().build(issuerKeyPair.public))){"Invalid signature"}
|
||||||
toJca()
|
toJca()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -431,10 +431,10 @@ internal constructor(private val initSerEnv: Boolean,
|
|||||||
|
|
||||||
private fun NodeInfo.notaryIdentity(): Party {
|
private fun NodeInfo.notaryIdentity(): Party {
|
||||||
return when (legalIdentities.size) {
|
return when (legalIdentities.size) {
|
||||||
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
|
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
|
||||||
1 -> legalIdentities[0]
|
1 -> legalIdentities[0]
|
||||||
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the
|
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the
|
||||||
// cluster and is shared by all the other members. This is the notary identity.
|
// cluster and is shared by all the other members. This is the notary identity.
|
||||||
2 -> legalIdentities[1]
|
2 -> legalIdentities[1]
|
||||||
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
|
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ package net.corda.nodeapi.internal.network
|
|||||||
|
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.*
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
import net.corda.core.internal.NODE_INFO_DIRECTORY
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Scheduler
|
import rx.Scheduler
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
|
@ -64,7 +64,8 @@ class CordaPersistence(
|
|||||||
databaseConfig: DatabaseConfig,
|
databaseConfig: DatabaseConfig,
|
||||||
schemas: Set<MappedSchema>,
|
schemas: Set<MappedSchema>,
|
||||||
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()
|
||||||
@ -73,7 +74,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
@ -104,7 +104,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
|
||||||
@ -130,13 +130,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())
|
||||||
|
@ -11,7 +11,7 @@ import net.corda.core.node.NodeInfo
|
|||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.node.services.config.NotaryConfig
|
import net.corda.node.services.config.NotaryConfig
|
||||||
import net.corda.nodeapi.internal.DEV_ROOT_CA
|
import net.corda.nodeapi.internal.DEV_ROOT_CA
|
||||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
import net.corda.core.internal.NODE_INFO_DIRECTORY
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
import net.corda.nodeapi.internal.config.parseAs
|
import net.corda.nodeapi.internal.config.parseAs
|
||||||
import net.corda.nodeapi.internal.config.toConfig
|
import net.corda.nodeapi.internal.config.toConfig
|
||||||
|
@ -4,7 +4,7 @@ import net.corda.core.internal.div
|
|||||||
import net.corda.core.internal.list
|
import net.corda.core.internal.list
|
||||||
import net.corda.core.internal.write
|
import net.corda.core.internal.write
|
||||||
import net.corda.nodeapi.eventually
|
import net.corda.nodeapi.eventually
|
||||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
import net.corda.core.internal.NODE_INFO_DIRECTORY
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
@ -5,10 +5,11 @@ import net.corda.core.internal.*
|
|||||||
import net.corda.core.internal.concurrent.transpose
|
import net.corda.core.internal.concurrent.transpose
|
||||||
import net.corda.core.messaging.ParametersUpdateInfo
|
import net.corda.core.messaging.ParametersUpdateInfo
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
|
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
|
||||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
|
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
|
||||||
import net.corda.nodeapi.internal.network.SignedNetworkParameters
|
import net.corda.nodeapi.internal.network.SignedNetworkParameters
|
||||||
@ -24,14 +25,13 @@ import net.corda.testing.node.internal.*
|
|||||||
import net.corda.testing.node.internal.network.NetworkMapServer
|
import net.corda.testing.node.internal.network.NetworkMapServer
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
|
import org.hamcrest.CoreMatchers.`is`
|
||||||
import org.junit.*
|
import org.junit.*
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.Parameterized
|
import org.junit.runners.Parameterized
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
import java.nio.file.Files
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@RunWith(Parameterized::class)
|
@RunWith(Parameterized::class)
|
||||||
@ -247,7 +247,9 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP
|
|||||||
// Make sure the nodes aren't getting the node infos from their additional directories
|
// Make sure the nodes aren't getting the node infos from their additional directories
|
||||||
val nodeInfosDir = baseDirectory / NODE_INFO_DIRECTORY
|
val nodeInfosDir = baseDirectory / NODE_INFO_DIRECTORY
|
||||||
if (nodeInfosDir.exists()) {
|
if (nodeInfosDir.exists()) {
|
||||||
assertThat(nodeInfosDir.list()).isEmpty()
|
Assert.assertThat(nodeInfosDir.list().size, `is`(1))
|
||||||
|
Assert.assertThat(Files.readAllBytes(nodeInfosDir.list().single()).deserialize<SignedNodeInfo>().verified().legalIdentities.first(), `is`( this.nodeInfo.legalIdentities.first()))
|
||||||
|
|
||||||
}
|
}
|
||||||
assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes)
|
assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes)
|
||||||
}
|
}
|
||||||
|
@ -77,10 +77,10 @@ class LargeTransactionsTest : IntegrationTest() {
|
|||||||
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"),
|
||||||
|
@ -128,7 +128,6 @@ public class CordaCaplet extends Capsule {
|
|||||||
}
|
}
|
||||||
// Add additional directories of JARs to the classpath (at the end), e.g., for JDBC drivers.
|
// Add additional directories of JARs to the classpath (at the end), e.g., for JDBC drivers.
|
||||||
augmentClasspath(cp, new File(baseDir, "drivers"));
|
augmentClasspath(cp, new File(baseDir, "drivers"));
|
||||||
augmentClasspath(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);
|
||||||
|
@ -75,7 +75,7 @@ open class SharedNodeCmdLineOptions {
|
|||||||
errors.forEach { error ->
|
errors.forEach { error ->
|
||||||
when (error) {
|
when (error) {
|
||||||
is ConfigException.IO -> logger.error(configFileNotFoundMessage(configFile))
|
is ConfigException.IO -> logger.error(configFileNotFoundMessage(configFile))
|
||||||
else -> logger.error(error.message, error)
|
else -> logger.error(error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
identityService::wellKnownPartyFromX500Name,
|
identityService::wellKnownPartyFromX500Name,
|
||||||
identityService::wellKnownPartyFromAnonymous,
|
identityService::wellKnownPartyFromAnonymous,
|
||||||
schemaService,
|
schemaService,
|
||||||
cacheFactory)
|
cacheFactory,
|
||||||
|
this.cordappLoader.appClassLoader)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// TODO Break cyclic dependency
|
// TODO Break cyclic dependency
|
||||||
@ -466,6 +467,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
|
|
||||||
// Write the node-info file even if nothing's changed, just in case the file has been deleted.
|
// Write the node-info file even if nothing's changed, just in case the file has been deleted.
|
||||||
NodeInfoWatcher.saveToFile(configuration.baseDirectory, nodeInfoAndSigned)
|
NodeInfoWatcher.saveToFile(configuration.baseDirectory, nodeInfoAndSigned)
|
||||||
|
NodeInfoWatcher.saveToFile(configuration.baseDirectory / NODE_INFO_DIRECTORY, nodeInfoAndSigned)
|
||||||
|
|
||||||
// Always republish on startup, it's treated by network map server as a heartbeat.
|
// Always republish on startup, it's treated by network map server as a heartbeat.
|
||||||
if (publish && networkMapClient != null) {
|
if (publish && networkMapClient != null) {
|
||||||
@ -774,7 +776,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
|
if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
|
||||||
val isH2Database = isH2Database(props.getProperty("dataSource.url", ""))
|
val isH2Database = isH2Database(props.getProperty("dataSource.url", ""))
|
||||||
val schemas = if (isH2Database) schemaService.internalSchemas() else schemaService.schemaOptions.keys
|
val schemas = if (isH2Database) schemaService.internalSchemas() else schemaService.schemaOptions.keys
|
||||||
database.startHikariPool(props, configuration.database, schemas, metricRegistry)
|
database.startHikariPool(props, configuration.database, schemas, 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)
|
||||||
}
|
}
|
||||||
@ -1093,21 +1095,22 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
|
|||||||
wellKnownPartyFromX500Name: (CordaX500Name) -> Party?,
|
wellKnownPartyFromX500Name: (CordaX500Name) -> Party?,
|
||||||
wellKnownPartyFromAnonymous: (AbstractParty) -> Party?,
|
wellKnownPartyFromAnonymous: (AbstractParty) -> Party?,
|
||||||
schemaService: SchemaService,
|
schemaService: SchemaService,
|
||||||
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
|
||||||
// either Hibernate can be convinced to stop warning, use the descriptor by default, or something else.
|
// either Hibernate can be convinced to stop warning, use the descriptor by default, or something else.
|
||||||
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))
|
||||||
return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, cacheFactory, attributeConverters)
|
return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, 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 jdbcUrl = hikariProperties.getProperty("dataSource.url", "")
|
val jdbcUrl = hikariProperties.getProperty("dataSource.url", "")
|
||||||
val schemaMigration = SchemaMigration(schemas, dataSource, databaseConfig)
|
val schemaMigration = SchemaMigration(schemas, dataSource, databaseConfig, classloader)
|
||||||
schemaMigration.nodeStartup(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L }, isH2Database(jdbcUrl))
|
schemaMigration.nodeStartup(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L }, isH2Database(jdbcUrl))
|
||||||
start(dataSource, jdbcUrl)
|
start(dataSource, jdbcUrl)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
|
@ -39,7 +39,7 @@ private class MultiplexingReactiveArtemisConsumer(private val queueNames: Set<St
|
|||||||
override fun start() {
|
override fun start() {
|
||||||
|
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
require(!startedFlag)
|
require(!startedFlag) { "Must not be started" }
|
||||||
connect()
|
connect()
|
||||||
startedFlag = true
|
startedFlag = true
|
||||||
}
|
}
|
||||||
@ -59,7 +59,7 @@ private class MultiplexingReactiveArtemisConsumer(private val queueNames: Set<St
|
|||||||
override fun connect() {
|
override fun connect() {
|
||||||
|
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
require(!connected)
|
require(!connected) { "Must not be connected" }
|
||||||
queueNames.forEach { queue ->
|
queueNames.forEach { queue ->
|
||||||
createSession().apply {
|
createSession().apply {
|
||||||
start()
|
start()
|
||||||
|
@ -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
|
||||||
|
@ -77,7 +77,9 @@ class ImmutableClassSerializer<T : Any>(val klass: KClass<T>) : Serializer<T>()
|
|||||||
// Verify that this class is immutable (all properties are final).
|
// Verify that this class is immutable (all properties are final).
|
||||||
// We disable this check inside SGX as the reflection blows up.
|
// We disable this check inside SGX as the reflection blows up.
|
||||||
if (!SgxSupport.isInsideEnclave) {
|
if (!SgxSupport.isInsideEnclave) {
|
||||||
require(props.none { it is KMutableProperty<*> })
|
props.forEach {
|
||||||
|
require(it !is KMutableProperty<*>) { "$it mutable property of class: ${klass} is unsupported" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,8 +284,8 @@ data class SecurityConfiguration(val authService: SecurityConfiguration.AuthServ
|
|||||||
val users: List<User>? = null) {
|
val users: List<User>? = null) {
|
||||||
init {
|
init {
|
||||||
when (type) {
|
when (type) {
|
||||||
AuthDataSourceType.INMEMORY -> require(users != null && connection == null)
|
AuthDataSourceType.INMEMORY -> require(users != null && connection == null) { "In-memory authentication must specify a user list, and must not configure a database" }
|
||||||
AuthDataSourceType.DB -> require(users == null && connection != null)
|
AuthDataSourceType.DB -> require(users == null && connection != null) { "Database-backed authentication must not specify a user list, and must configure a database" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -673,7 +673,7 @@ private class P2PMessagingConsumer(
|
|||||||
override fun start() {
|
override fun start() {
|
||||||
|
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
require(!startedFlag)
|
require(!startedFlag){"Must not already be started"}
|
||||||
drainingModeWasChangedEvents.filter { change -> change.switchedOn() }.doOnNext { initialAndExistingConsumer.switchTo(existingOnlyConsumer) }.subscribe()
|
drainingModeWasChangedEvents.filter { change -> change.switchedOn() }.doOnNext { initialAndExistingConsumer.switchTo(existingOnlyConsumer) }.subscribe()
|
||||||
drainingModeWasChangedEvents.filter { change -> change.switchedOff() }.doOnNext { existingOnlyConsumer.switchTo(initialAndExistingConsumer) }.subscribe()
|
drainingModeWasChangedEvents.filter { change -> change.switchedOff() }.doOnNext { existingOnlyConsumer.switchTo(initialAndExistingConsumer) }.subscribe()
|
||||||
subscriptions += existingOnlyConsumer.messages.doOnNext(messages::onNext).subscribe()
|
subscriptions += existingOnlyConsumer.messages.doOnNext(messages::onNext).subscribe()
|
||||||
|
@ -289,7 +289,7 @@ class RPCServer<OPS : RPCOps>(
|
|||||||
private fun bindingRemovalArtemisMessageHandler(artemisMessage: ClientMessage) {
|
private fun bindingRemovalArtemisMessageHandler(artemisMessage: ClientMessage) {
|
||||||
lifeCycle.requireState(State.STARTED)
|
lifeCycle.requireState(State.STARTED)
|
||||||
val notificationType = artemisMessage.getStringProperty(ManagementHelper.HDR_NOTIFICATION_TYPE)
|
val notificationType = artemisMessage.getStringProperty(ManagementHelper.HDR_NOTIFICATION_TYPE)
|
||||||
require(notificationType == CoreNotificationType.BINDING_REMOVED.name)
|
require(notificationType == CoreNotificationType.BINDING_REMOVED.name){"Message contained notification type of $notificationType instead of expected ${CoreNotificationType.BINDING_REMOVED.name}"}
|
||||||
val clientAddress = artemisMessage.getStringProperty(ManagementHelper.HDR_ROUTING_NAME)
|
val clientAddress = artemisMessage.getStringProperty(ManagementHelper.HDR_ROUTING_NAME)
|
||||||
log.warn("Detected RPC client disconnect on address $clientAddress, scheduling for reaping")
|
log.warn("Detected RPC client disconnect on address $clientAddress, scheduling for reaping")
|
||||||
invalidateClient(SimpleString(clientAddress))
|
invalidateClient(SimpleString(clientAddress))
|
||||||
@ -299,7 +299,7 @@ class RPCServer<OPS : RPCOps>(
|
|||||||
private fun bindingAdditionArtemisMessageHandler(artemisMessage: ClientMessage) {
|
private fun bindingAdditionArtemisMessageHandler(artemisMessage: ClientMessage) {
|
||||||
lifeCycle.requireState(State.STARTED)
|
lifeCycle.requireState(State.STARTED)
|
||||||
val notificationType = artemisMessage.getStringProperty(ManagementHelper.HDR_NOTIFICATION_TYPE)
|
val notificationType = artemisMessage.getStringProperty(ManagementHelper.HDR_NOTIFICATION_TYPE)
|
||||||
require(notificationType == CoreNotificationType.BINDING_ADDED.name)
|
require(notificationType == CoreNotificationType.BINDING_ADDED.name){"Message contained notification type of $notificationType instead of expected ${CoreNotificationType.BINDING_ADDED.name}"}
|
||||||
val clientAddress = SimpleString(artemisMessage.getStringProperty(ManagementHelper.HDR_ROUTING_NAME))
|
val clientAddress = SimpleString(artemisMessage.getStringProperty(ManagementHelper.HDR_ROUTING_NAME))
|
||||||
log.debug("RPC client queue created on address $clientAddress")
|
log.debug("RPC client queue created on address $clientAddress")
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import net.corda.core.serialization.serialize
|
|||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.debug
|
import net.corda.core.utilities.debug
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
import net.corda.core.internal.NODE_INFO_DIRECTORY
|
||||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||||
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
@ -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
|
||||||
@ -300,7 +300,7 @@ class NodeAttachmentService(
|
|||||||
private fun import(jar: InputStream, uploader: String?, filename: String?): AttachmentId {
|
private fun import(jar: InputStream, uploader: String?, filename: String?): AttachmentId {
|
||||||
return database.transaction {
|
return database.transaction {
|
||||||
withContractsInJar(jar) { contractClassNames, inputStream ->
|
withContractsInJar(jar) { contractClassNames, inputStream ->
|
||||||
require(inputStream !is JarInputStream)
|
require(inputStream !is JarInputStream){"Input stream must not be a JarInputStream"}
|
||||||
|
|
||||||
// Read the file into RAM and then calculate its hash. The attachment must fit into memory.
|
// Read the file into RAM and then calculate its hash. The attachment must fit into memory.
|
||||||
// TODO: Switch to a two-phase insert so we can handle attachments larger than RAM.
|
// TODO: Switch to a two-phase insert so we can handle attachments larger than RAM.
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package net.corda.node.services.statemachine
|
package net.corda.node.services.statemachine
|
||||||
|
|
||||||
import co.paralleluniverse.strands.concurrent.AbstractQueuedSynchronizer
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import co.paralleluniverse.strands.concurrent.AbstractQueuedSynchronizer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quasar-compatible latch that may be incremented.
|
* Quasar-compatible latch that may be incremented.
|
||||||
@ -56,7 +56,7 @@ class CountUpDownLatch(initialValue: Int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun countDown(number: Int = 1) {
|
fun countDown(number: Int = 1) {
|
||||||
require(number > 0)
|
require(number > 0){"Number to count down by must be greater than 0"}
|
||||||
sync.releaseShared(number)
|
sync.releaseShared(number)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +197,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
|||||||
"Transaction context is missing. This might happen if a suspendable method is not annotated with @Suspendable annotation."
|
"Transaction context is missing. This might happen if a suspendable method is not annotated with @Suspendable annotation."
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
require(contextTransactionOrNull == null)
|
require(contextTransactionOrNull == null){"Transaction is marked as not present, but is not null"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -392,7 +392,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
|||||||
isDbTransactionOpenOnEntry = true,
|
isDbTransactionOpenOnEntry = true,
|
||||||
isDbTransactionOpenOnExit = false
|
isDbTransactionOpenOnExit = false
|
||||||
)
|
)
|
||||||
require(continuation == FlowContinuation.ProcessEvents)
|
require(continuation == FlowContinuation.ProcessEvents){"Expected a continuation of type ${FlowContinuation.ProcessEvents}, found $continuation "}
|
||||||
unpark(SERIALIZER_BLOCKER)
|
unpark(SERIALIZER_BLOCKER)
|
||||||
}
|
}
|
||||||
return uncheckedCast(processEventsUntilFlowIsResumed(
|
return uncheckedCast(processEventsUntilFlowIsResumed(
|
||||||
|
@ -172,7 +172,7 @@ class SingleThreadedStateMachineManager(
|
|||||||
* @param allowedUnsuspendedFiberCount Optional parameter is used in some tests.
|
* @param allowedUnsuspendedFiberCount Optional parameter is used in some tests.
|
||||||
*/
|
*/
|
||||||
override fun stop(allowedUnsuspendedFiberCount: Int) {
|
override fun stop(allowedUnsuspendedFiberCount: Int) {
|
||||||
require(allowedUnsuspendedFiberCount >= 0)
|
require(allowedUnsuspendedFiberCount >= 0){"allowedUnsuspendedFiberCount must be greater than or equal to zero"}
|
||||||
mutex.locked {
|
mutex.locked {
|
||||||
if (stopping) throw IllegalStateException("Already stopping!")
|
if (stopping) throw IllegalStateException("Already stopping!")
|
||||||
stopping = true
|
stopping = true
|
||||||
@ -775,10 +775,10 @@ class SingleThreadedStateMachineManager(
|
|||||||
) {
|
) {
|
||||||
drainFlowEventQueue(flow)
|
drainFlowEventQueue(flow)
|
||||||
// final sanity checks
|
// final sanity checks
|
||||||
require(lastState.pendingDeduplicationHandlers.isEmpty())
|
require(lastState.pendingDeduplicationHandlers.isEmpty()) { "Flow cannot be removed until all pending deduplications have completed" }
|
||||||
require(lastState.isRemoved)
|
require(lastState.isRemoved) { "Flow must be in removable state before removal" }
|
||||||
require(lastState.checkpoint.subFlowStack.size == 1)
|
require(lastState.checkpoint.subFlowStack.size == 1) { "Checkpointed stack must be empty" }
|
||||||
require(flow.fiber.id !in sessionToFlow.values)
|
require(flow.fiber.id !in sessionToFlow.values) { "Flow fibre must not be needed by an existing session" }
|
||||||
flow.resultFuture.set(removalReason.flowReturnValue)
|
flow.resultFuture.set(removalReason.flowReturnValue)
|
||||||
lastState.flowLogic.progressTracker?.currentStep = ProgressTracker.DONE
|
lastState.flowLogic.progressTracker?.currentStep = ProgressTracker.DONE
|
||||||
changesPublisher.onNext(StateMachineManager.Change.Removed(lastState.flowLogic, Try.Success(removalReason.flowReturnValue)))
|
changesPublisher.onNext(StateMachineManager.Change.Removed(lastState.flowLogic, Try.Success(removalReason.flowReturnValue)))
|
||||||
|
@ -2,17 +2,11 @@ package net.corda.node.services.statemachine.interceptors
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.flows.StateMachineRunId
|
import net.corda.core.flows.StateMachineRunId
|
||||||
import net.corda.core.serialization.*
|
import net.corda.core.serialization.SerializedBytes
|
||||||
import net.corda.core.serialization.internal.CheckpointSerializationContext
|
import net.corda.core.serialization.internal.CheckpointSerializationContext
|
||||||
import net.corda.core.serialization.internal.checkpointDeserialize
|
import net.corda.core.serialization.internal.checkpointDeserialize
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.node.services.statemachine.ActionExecutor
|
import net.corda.node.services.statemachine.*
|
||||||
import net.corda.node.services.statemachine.Event
|
|
||||||
import net.corda.node.services.statemachine.FlowFiber
|
|
||||||
import net.corda.node.services.statemachine.FlowState
|
|
||||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
|
||||||
import net.corda.node.services.statemachine.StateMachineState
|
|
||||||
import net.corda.node.services.statemachine.TransitionExecutor
|
|
||||||
import net.corda.node.services.statemachine.transitions.FlowContinuation
|
import net.corda.node.services.statemachine.transitions.FlowContinuation
|
||||||
import net.corda.node.services.statemachine.transitions.TransitionResult
|
import net.corda.node.services.statemachine.transitions.TransitionResult
|
||||||
import java.util.concurrent.LinkedBlockingQueue
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
@ -69,7 +63,7 @@ class FiberDeserializationChecker {
|
|||||||
private var foundUnrestorableFibers: Boolean = false
|
private var foundUnrestorableFibers: Boolean = false
|
||||||
|
|
||||||
fun start(checkpointSerializationContext: CheckpointSerializationContext) {
|
fun start(checkpointSerializationContext: CheckpointSerializationContext) {
|
||||||
require(checkerThread == null)
|
require(checkerThread == null){"Checking thread must not already be started"}
|
||||||
checkerThread = thread(name = "FiberDeserializationChecker") {
|
checkerThread = thread(name = "FiberDeserializationChecker") {
|
||||||
while (true) {
|
while (true) {
|
||||||
val job = jobQueue.take()
|
val job = jobQueue.take()
|
||||||
|
@ -372,7 +372,7 @@ class NodeRegistrationHelper(
|
|||||||
private class FixedPeriodLimitedRetrialStrategy(times: Int, private val period: Duration) : (Duration?) -> Duration? {
|
private class FixedPeriodLimitedRetrialStrategy(times: Int, private val period: Duration) : (Duration?) -> Duration? {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
require(times > 0)
|
require(times > 0){"Retry attempts must be larger than zero"}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var counter = times
|
private var counter = times
|
||||||
|
@ -16,7 +16,7 @@ import net.corda.core.serialization.serialize
|
|||||||
import net.corda.core.utilities.millis
|
import net.corda.core.utilities.millis
|
||||||
import net.corda.node.VersionInfo
|
import net.corda.node.VersionInfo
|
||||||
import net.corda.node.services.api.NetworkMapCacheInternal
|
import net.corda.node.services.api.NetworkMapCacheInternal
|
||||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
import net.corda.core.internal.NODE_INFO_DIRECTORY
|
||||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
|
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
|
||||||
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
||||||
|
@ -6,7 +6,7 @@ import net.corda.core.internal.createDirectories
|
|||||||
import net.corda.core.internal.div
|
import net.corda.core.internal.div
|
||||||
import net.corda.core.internal.size
|
import net.corda.core.internal.size
|
||||||
import net.corda.core.node.services.KeyManagementService
|
import net.corda.core.node.services.KeyManagementService
|
||||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
import net.corda.core.internal.NODE_INFO_DIRECTORY
|
||||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||||
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
||||||
import net.corda.testing.core.ALICE_NAME
|
import net.corda.testing.core.ALICE_NAME
|
||||||
|
@ -31,7 +31,7 @@ class SchemaMigrationTest {
|
|||||||
private fun configureDatabase(hikariProperties: Properties,
|
private fun configureDatabase(hikariProperties: Properties,
|
||||||
databaseConfig: DatabaseConfig,
|
databaseConfig: DatabaseConfig,
|
||||||
schemaService: NodeSchemaService = NodeSchemaService()): CordaPersistence =
|
schemaService: NodeSchemaService = NodeSchemaService()): CordaPersistence =
|
||||||
createCordaPersistence(databaseConfig, { null }, { null }, schemaService, TestingNamedCacheFactory())
|
createCordaPersistence(databaseConfig, { null }, { null }, schemaService, TestingNamedCacheFactory(), null)
|
||||||
.apply { startHikariPool(hikariProperties, databaseConfig, schemaService.schemaOptions.keys) }
|
.apply { startHikariPool(hikariProperties, databaseConfig, schemaService.schemaOptions.keys) }
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -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)
|
||||||
|
@ -94,7 +94,7 @@ private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.
|
|||||||
val id = rpc.uploadAttachment(it)
|
val id = rpc.uploadAttachment(it)
|
||||||
require(hash == id) { "Id was '$id' instead of '$hash'" }
|
require(hash == id) { "Id was '$id' instead of '$hash'" }
|
||||||
}
|
}
|
||||||
require(rpc.attachmentExists(hash))
|
require(rpc.attachmentExists(hash)){"Attachment matching hash: $hash does not exist"}
|
||||||
}
|
}
|
||||||
|
|
||||||
val flowHandle = rpc.startTrackedFlow(::AttachmentDemoFlow, otherSideFuture.get(), notaryFuture.get(), hash)
|
val flowHandle = rpc.startTrackedFlow(::AttachmentDemoFlow, otherSideFuture.get(), notaryFuture.get(), hash)
|
||||||
@ -159,7 +159,7 @@ fun recipient(rpc: CordaRPCOps, webPort: Int) {
|
|||||||
if (wtx.attachments.isNotEmpty()) {
|
if (wtx.attachments.isNotEmpty()) {
|
||||||
if (wtx.outputs.isNotEmpty()) {
|
if (wtx.outputs.isNotEmpty()) {
|
||||||
val state = wtx.outputsOfType<AttachmentContract.State>().single()
|
val state = wtx.outputsOfType<AttachmentContract.State>().single()
|
||||||
require(rpc.attachmentExists(state.hash))
|
require(rpc.attachmentExists(state.hash)) {"attachment matching hash: ${state.hash} does not exist"}
|
||||||
|
|
||||||
// Download the attachment via the Web endpoint.
|
// Download the attachment via the Web endpoint.
|
||||||
val connection = URL("http://localhost:$webPort/attachments/${state.hash}").openConnection() as HttpURLConnection
|
val connection = URL("http://localhost:$webPort/attachments/${state.hash}").openConnection() as HttpURLConnection
|
||||||
@ -207,7 +207,7 @@ class AttachmentContract : Contract {
|
|||||||
override fun verify(tx: LedgerTransaction) {
|
override fun verify(tx: LedgerTransaction) {
|
||||||
val state = tx.outputsOfType<AttachmentContract.State>().single()
|
val state = tx.outputsOfType<AttachmentContract.State>().single()
|
||||||
// we check that at least one has the matching hash, the other will be the contract
|
// we check that at least one has the matching hash, the other will be the contract
|
||||||
require(tx.attachments.any { it.id == state.hash })
|
require(tx.attachments.any { it.id == state.hash }) {"At least one attachment in transaction must match hash ${state.hash}"}
|
||||||
}
|
}
|
||||||
|
|
||||||
object Command : TypeOnlyCommandData()
|
object Command : TypeOnlyCommandData()
|
||||||
|
@ -40,7 +40,7 @@ data class PortfolioState(val portfolio: List<StateRef>,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun generateRevision(notary: Party, oldState: StateAndRef<*>, updatedValue: Update): TransactionBuilder {
|
override fun generateRevision(notary: Party, oldState: StateAndRef<*>, updatedValue: Update): TransactionBuilder {
|
||||||
require(oldState.state.data == this)
|
require(oldState.state.data == this){"Old state data does not match current state data"}
|
||||||
val portfolio = updatedValue.portfolio ?: portfolio
|
val portfolio = updatedValue.portfolio ?: portfolio
|
||||||
val valuation = updatedValue.valuation ?: valuation
|
val valuation = updatedValue.valuation ?: valuation
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -0,0 +1,209 @@
|
|||||||
|
package net.corda.serialization.internal.amqp
|
||||||
|
|
||||||
|
import net.corda.serialization.internal.model.*
|
||||||
|
import java.io.NotSerializableException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interprets AMQP [Schema] information to obtain [RemoteTypeInformation], caching by [TypeDescriptor].
|
||||||
|
*/
|
||||||
|
class AMQPRemoteTypeModel {
|
||||||
|
|
||||||
|
private val cache: MutableMap<TypeDescriptor, RemoteTypeInformation> = DefaultCacheProvider.createCache()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpret a [Schema] to obtain a [Map] of all of the [RemoteTypeInformation] contained therein, indexed by
|
||||||
|
* [TypeDescriptor].
|
||||||
|
*
|
||||||
|
* A [Schema] contains a set of [TypeNotation]s, which we recursively convert into [RemoteTypeInformation],
|
||||||
|
* associating each new piece of [RemoteTypeInformation] with the [TypeDescriptor] attached to it in the schema.
|
||||||
|
*
|
||||||
|
* We start by building a [Map] of [TypeNotation] by [TypeIdentifier], using [AMQPTypeIdentifierParser] to convert
|
||||||
|
* AMQP type names into [TypeIdentifier]s. This is used as a lookup for resolving notations that are referred to by
|
||||||
|
* type name from other notations, e.g. the types of properties.
|
||||||
|
*
|
||||||
|
* We also build a [Map] of [TypeNotation] by [TypeDescriptor], which we then convert into [RemoteTypeInformation]
|
||||||
|
* while merging with the cache.
|
||||||
|
*/
|
||||||
|
fun interpret(serializationSchemas: SerializationSchemas): Map<TypeDescriptor, RemoteTypeInformation> {
|
||||||
|
val (schema, transforms) = serializationSchemas
|
||||||
|
val notationLookup = schema.types.associateBy { it.name.typeIdentifier }
|
||||||
|
val byTypeDescriptor = schema.types.associateBy { it.typeDescriptor }
|
||||||
|
val enumTransformsLookup = transforms.types.asSequence().map { (name, transformSet) ->
|
||||||
|
name.typeIdentifier to interpretTransformSet(transformSet)
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
val interpretationState = InterpretationState(notationLookup, enumTransformsLookup, cache, emptySet())
|
||||||
|
|
||||||
|
return byTypeDescriptor.mapValues { (typeDescriptor, typeNotation) ->
|
||||||
|
cache.getOrPut(typeDescriptor) { interpretationState.run { typeNotation.name.typeIdentifier.interpretIdentifier() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class InterpretationState(val notationLookup: Map<TypeIdentifier, TypeNotation>,
|
||||||
|
val enumTransformsLookup: Map<TypeIdentifier, EnumTransforms>,
|
||||||
|
val cache: MutableMap<TypeDescriptor, RemoteTypeInformation>,
|
||||||
|
val seen: Set<TypeIdentifier>) {
|
||||||
|
|
||||||
|
private inline fun <T> forgetSeen(block: InterpretationState.() -> T): T =
|
||||||
|
withSeen(emptySet(), block)
|
||||||
|
|
||||||
|
private inline fun <T> withSeen(typeIdentifier: TypeIdentifier, block: InterpretationState.() -> T): T =
|
||||||
|
withSeen(seen + typeIdentifier, block)
|
||||||
|
|
||||||
|
private inline fun <T> withSeen(seen: Set<TypeIdentifier>, block: InterpretationState.() -> T): T =
|
||||||
|
copy(seen = seen).run(block)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Follow a [TypeIdentifier] to the [TypeNotation] associated with it in the lookup, and interpret that notation.
|
||||||
|
* If there is no such notation, interpret the [TypeIdentifier] directly into [RemoteTypeInformation].
|
||||||
|
*
|
||||||
|
* If we have visited this [TypeIdentifier] before while traversing the graph of related [TypeNotation]s, then we
|
||||||
|
* know we have hit a cycle and respond accordingly.
|
||||||
|
*/
|
||||||
|
fun TypeIdentifier.interpretIdentifier(): RemoteTypeInformation =
|
||||||
|
if (this in seen) RemoteTypeInformation.Cycle(this) { forgetSeen { interpretIdentifier() } }
|
||||||
|
else withSeen(this) {
|
||||||
|
val identifier = this@interpretIdentifier
|
||||||
|
notationLookup[identifier]?.interpretNotation(identifier) ?: interpretNoNotation()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Either fetch from the cache, or interpret, cache, and return, the [RemoteTypeInformation] corresponding to this
|
||||||
|
* [TypeNotation].
|
||||||
|
*/
|
||||||
|
private fun TypeNotation.interpretNotation(identifier: TypeIdentifier): RemoteTypeInformation =
|
||||||
|
cache.getOrPut(typeDescriptor) {
|
||||||
|
when (this) {
|
||||||
|
is CompositeType -> interpretComposite(identifier)
|
||||||
|
is RestrictedType -> interpretRestricted(identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpret the properties, interfaces and type parameters in this [TypeNotation], and return suitable
|
||||||
|
* [RemoteTypeInformation].
|
||||||
|
*/
|
||||||
|
private fun CompositeType.interpretComposite(identifier: TypeIdentifier): RemoteTypeInformation {
|
||||||
|
val properties = fields.asSequence().map { it.interpret() }.toMap()
|
||||||
|
val typeParameters = identifier.interpretTypeParameters()
|
||||||
|
val interfaceIdentifiers = provides.map { name -> name.typeIdentifier }
|
||||||
|
val isInterface = identifier in interfaceIdentifiers
|
||||||
|
val interfaces = interfaceIdentifiers.mapNotNull { interfaceIdentifier ->
|
||||||
|
if (interfaceIdentifier == identifier) null
|
||||||
|
else interfaceIdentifier.interpretIdentifier()
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (isInterface) RemoteTypeInformation.AnInterface(typeDescriptor, identifier, properties, interfaces, typeParameters)
|
||||||
|
else RemoteTypeInformation.Composable(typeDescriptor, identifier, properties, interfaces, typeParameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type parameters are read off from the [TypeIdentifier] we translated the AMQP type name into.
|
||||||
|
*/
|
||||||
|
private fun TypeIdentifier.interpretTypeParameters(): List<RemoteTypeInformation> = when (this) {
|
||||||
|
is TypeIdentifier.Parameterised -> parameters.map { it.interpretIdentifier() }
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpret a [RestrictedType] into suitable [RemoteTypeInformation].
|
||||||
|
*/
|
||||||
|
private fun RestrictedType.interpretRestricted(identifier: TypeIdentifier): RemoteTypeInformation = when (identifier) {
|
||||||
|
is TypeIdentifier.Parameterised ->
|
||||||
|
RemoteTypeInformation.Parameterised(
|
||||||
|
typeDescriptor,
|
||||||
|
identifier,
|
||||||
|
identifier.interpretTypeParameters())
|
||||||
|
is TypeIdentifier.ArrayOf ->
|
||||||
|
RemoteTypeInformation.AnArray(
|
||||||
|
typeDescriptor,
|
||||||
|
identifier,
|
||||||
|
identifier.componentType.interpretIdentifier())
|
||||||
|
is TypeIdentifier.Unparameterised ->
|
||||||
|
if (choices.isEmpty()) {
|
||||||
|
RemoteTypeInformation.Unparameterised(
|
||||||
|
typeDescriptor,
|
||||||
|
identifier)
|
||||||
|
} else RemoteTypeInformation.AnEnum(
|
||||||
|
typeDescriptor,
|
||||||
|
identifier,
|
||||||
|
choices.map { it.name },
|
||||||
|
enumTransformsLookup[identifier] ?: EnumTransforms.empty)
|
||||||
|
else -> throw NotSerializableException("Cannot interpret restricted type $this")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpret a [Field] into a name/[RemotePropertyInformation] pair.
|
||||||
|
*/
|
||||||
|
private fun Field.interpret(): Pair<String, RemotePropertyInformation> {
|
||||||
|
val identifier = type.typeIdentifier
|
||||||
|
|
||||||
|
// A type of "*" is replaced with the value of the "requires" field
|
||||||
|
val fieldTypeIdentifier = if (identifier == TypeIdentifier.TopType && !requires.isEmpty()) {
|
||||||
|
requires[0].typeIdentifier
|
||||||
|
} else identifier
|
||||||
|
|
||||||
|
// We convert Java Object types to Java primitive types if the field is mandatory.
|
||||||
|
val fieldType = fieldTypeIdentifier.forcePrimitive(mandatory).interpretIdentifier()
|
||||||
|
|
||||||
|
return name to RemotePropertyInformation(
|
||||||
|
fieldType,
|
||||||
|
mandatory)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is no [TypeNotation] in the [Schema] matching a given [TypeIdentifier], we interpret the [TypeIdentifier]
|
||||||
|
* directly.
|
||||||
|
*/
|
||||||
|
private fun TypeIdentifier.interpretNoNotation(): RemoteTypeInformation =
|
||||||
|
when (this) {
|
||||||
|
is TypeIdentifier.TopType -> RemoteTypeInformation.Top
|
||||||
|
is TypeIdentifier.UnknownType -> RemoteTypeInformation.Unknown
|
||||||
|
is TypeIdentifier.ArrayOf ->
|
||||||
|
RemoteTypeInformation.AnArray(
|
||||||
|
name,
|
||||||
|
this,
|
||||||
|
componentType.interpretIdentifier())
|
||||||
|
is TypeIdentifier.Parameterised ->
|
||||||
|
RemoteTypeInformation.Parameterised(
|
||||||
|
name,
|
||||||
|
this,
|
||||||
|
parameters.map { it.interpretIdentifier() })
|
||||||
|
else -> RemoteTypeInformation.Unparameterised(name, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun interpretTransformSet(transformSet: EnumMap<TransformTypes, MutableList<Transform>>): EnumTransforms {
|
||||||
|
val defaultTransforms = transformSet[TransformTypes.EnumDefault]?.toList() ?: emptyList()
|
||||||
|
val defaults = defaultTransforms.associate { transform -> (transform as EnumDefaultSchemaTransform).new to transform.old }
|
||||||
|
val renameTransforms = transformSet[TransformTypes.Rename]?.toList() ?: emptyList()
|
||||||
|
val renames = renameTransforms.associate { transform -> (transform as RenameSchemaTransform).to to transform.from }
|
||||||
|
|
||||||
|
return EnumTransforms(defaults, renames)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val TypeNotation.typeDescriptor: String get() = descriptor.name?.toString() ?:
|
||||||
|
throw NotSerializableException("Type notation has no type descriptor: $this")
|
||||||
|
|
||||||
|
private val String.typeIdentifier get(): TypeIdentifier = AMQPTypeIdentifierParser.parse(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force e.g. [java.lang.Integer] to `int`, if it is the type of a mandatory field.
|
||||||
|
*/
|
||||||
|
private fun TypeIdentifier.forcePrimitive(mandatory: Boolean) =
|
||||||
|
if (mandatory) primitives[this] ?: this
|
||||||
|
else this
|
||||||
|
|
||||||
|
private val primitives = sequenceOf(
|
||||||
|
Boolean::class,
|
||||||
|
Byte::class,
|
||||||
|
Char::class,
|
||||||
|
Int::class,
|
||||||
|
Short::class,
|
||||||
|
Long::class,
|
||||||
|
Float::class,
|
||||||
|
Double::class).associate {
|
||||||
|
TypeIdentifier.forClass(it.javaObjectType) to TypeIdentifier.forClass(it.javaPrimitiveType!!)
|
||||||
|
}
|
@ -172,7 +172,7 @@ abstract class AbstractAMQPSerializationScheme(
|
|||||||
// Not used as a simple direct import to facilitate testing
|
// Not used as a simple direct import to facilitate testing
|
||||||
open val publicKeySerializer: CustomSerializer<*> = net.corda.serialization.internal.amqp.custom.PublicKeySerializer
|
open val publicKeySerializer: CustomSerializer<*> = net.corda.serialization.internal.amqp.custom.PublicKeySerializer
|
||||||
|
|
||||||
private fun getSerializerFactory(context: SerializationContext): SerializerFactory {
|
fun getSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||||
val key = Pair(context.whitelist, context.deserializationClassLoader)
|
val key = Pair(context.whitelist, context.deserializationClassLoader)
|
||||||
// ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already.
|
// ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already.
|
||||||
return serializerFactoriesForContexts[key] ?: serializerFactoriesForContexts.computeIfAbsent(key) {
|
return serializerFactoriesForContexts[key] ?: serializerFactoriesForContexts.computeIfAbsent(key) {
|
||||||
|
@ -0,0 +1,190 @@
|
|||||||
|
package net.corda.serialization.internal.amqp
|
||||||
|
|
||||||
|
import com.google.common.primitives.Primitives
|
||||||
|
import net.corda.serialization.internal.model.TypeIdentifier
|
||||||
|
import org.apache.qpid.proton.amqp.*
|
||||||
|
import java.io.NotSerializableException
|
||||||
|
import java.lang.StringBuilder
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown if the type string parser enters an illegal state.
|
||||||
|
*/
|
||||||
|
class IllegalTypeNameParserStateException(message: String): NotSerializableException(message)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a state machine which knows how to parse AMQP type strings into [TypeIdentifier]s.
|
||||||
|
*/
|
||||||
|
object AMQPTypeIdentifierParser {
|
||||||
|
|
||||||
|
internal const val MAX_TYPE_PARAM_DEPTH = 32
|
||||||
|
private const val MAX_ARRAY_DEPTH = 32
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a string representing a serialized AMQP type, construct a TypeIdentifier for that string.
|
||||||
|
*
|
||||||
|
* @param typeString The AMQP type string to parse
|
||||||
|
* @return A [TypeIdentifier] representing the type represented by the input string.
|
||||||
|
*/
|
||||||
|
fun parse(typeString: String): TypeIdentifier {
|
||||||
|
validate(typeString)
|
||||||
|
return typeString.fold<ParseState>(ParseState.ParsingRawType(null)) { state, c ->
|
||||||
|
state.accept(c)
|
||||||
|
}.getTypeIdentifier()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure our inputs aren't designed to blow things up.
|
||||||
|
private fun validate(typeString: String) {
|
||||||
|
var maxTypeParamDepth = 0
|
||||||
|
var typeParamdepth = 0
|
||||||
|
|
||||||
|
var maxArrayDepth = 0
|
||||||
|
var wasArray = false
|
||||||
|
var arrayDepth = 0
|
||||||
|
|
||||||
|
for (c in typeString) {
|
||||||
|
if (c.isWhitespace() || c.isJavaIdentifierPart() || c.isJavaIdentifierStart() ||
|
||||||
|
c == '.' || c == ',' || c == '?' || c == '*') continue
|
||||||
|
|
||||||
|
when(c) {
|
||||||
|
'<' -> maxTypeParamDepth = Math.max(++typeParamdepth, typeParamdepth)
|
||||||
|
'>' -> typeParamdepth--
|
||||||
|
'[' -> {
|
||||||
|
arrayDepth = if (wasArray) arrayDepth + 2 else 1
|
||||||
|
maxArrayDepth = Math.max(maxArrayDepth,arrayDepth)
|
||||||
|
}
|
||||||
|
']' -> arrayDepth--
|
||||||
|
else -> throw IllegalTypeNameParserStateException("Type name '$typeString' contains illegal character '$c'")
|
||||||
|
}
|
||||||
|
wasArray = c == ']'
|
||||||
|
}
|
||||||
|
if (maxTypeParamDepth >= MAX_TYPE_PARAM_DEPTH)
|
||||||
|
throw IllegalTypeNameParserStateException("Nested depth of type parameters exceeds maximum of $MAX_TYPE_PARAM_DEPTH")
|
||||||
|
|
||||||
|
if (maxArrayDepth >= MAX_ARRAY_DEPTH)
|
||||||
|
throw IllegalTypeNameParserStateException("Nested depth of arrays exceeds maximum of $MAX_ARRAY_DEPTH")
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ParseState {
|
||||||
|
abstract val parent: ParseState.ParsingParameterList?
|
||||||
|
abstract fun accept(c: Char): ParseState
|
||||||
|
abstract fun getTypeIdentifier(): TypeIdentifier
|
||||||
|
|
||||||
|
fun unexpected(c: Char): ParseState = throw IllegalTypeNameParserStateException("Unexpected character: '$c'")
|
||||||
|
fun notInParameterList(c: Char): ParseState =
|
||||||
|
throw IllegalTypeNameParserStateException("'$c' encountered, but not parsing type parameter list")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We are parsing a raw type name, either at the top level or as part of a list of type parameters.
|
||||||
|
*/
|
||||||
|
data class ParsingRawType(override val parent: ParseState.ParsingParameterList?, val buffer: StringBuilder = StringBuilder()) : ParseState() {
|
||||||
|
override fun accept(c: Char) = when (c) {
|
||||||
|
',' ->
|
||||||
|
if (parent == null) notInParameterList(c)
|
||||||
|
else ParsingRawType(parent.addParameter(getTypeIdentifier()))
|
||||||
|
'[' -> ParsingArray(getTypeIdentifier(), parent)
|
||||||
|
']' -> unexpected(c)
|
||||||
|
'<' -> ParsingRawType(ParsingParameterList(getTypeName(), parent))
|
||||||
|
'>' -> parent?.addParameter(getTypeIdentifier())?.accept(c) ?: notInParameterList(c)
|
||||||
|
else -> apply { buffer.append(c) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTypeName(): String {
|
||||||
|
val typeName = buffer.toString().trim()
|
||||||
|
if (typeName.contains(' '))
|
||||||
|
throw IllegalTypeNameParserStateException("Illegal whitespace in type name $typeName")
|
||||||
|
return typeName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTypeIdentifier(): TypeIdentifier {
|
||||||
|
val typeName = getTypeName()
|
||||||
|
return when (typeName) {
|
||||||
|
"*" -> TypeIdentifier.TopType
|
||||||
|
"?" -> TypeIdentifier.UnknownType
|
||||||
|
in simplified -> simplified[typeName]!!
|
||||||
|
else -> TypeIdentifier.Unparameterised(typeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We are parsing a parameter list, and expect either to start a new parameter, add array-ness to the last
|
||||||
|
* parameter we have, or end the list.
|
||||||
|
*/
|
||||||
|
data class ParsingParameterList(val typeName: String, override val parent: ParsingParameterList?, val parameters: List<TypeIdentifier> = emptyList()) : ParseState() {
|
||||||
|
override fun accept(c: Char) = when (c) {
|
||||||
|
' ' -> this
|
||||||
|
',' -> ParsingRawType(this)
|
||||||
|
'[' ->
|
||||||
|
if (parameters.isEmpty()) unexpected(c)
|
||||||
|
else ParsingArray(
|
||||||
|
// Start adding array-ness to the last parameter we have.
|
||||||
|
parameters[parameters.lastIndex],
|
||||||
|
// Take a copy of this state, dropping the last parameter which will be added back on
|
||||||
|
// when array parsing completes.
|
||||||
|
copy(parameters = parameters.subList(0, parameters.lastIndex)))
|
||||||
|
'>' -> parent?.addParameter(getTypeIdentifier()) ?: Complete(getTypeIdentifier())
|
||||||
|
else -> unexpected(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addParameter(parameter: TypeIdentifier) = copy(parameters = parameters + parameter)
|
||||||
|
|
||||||
|
override fun getTypeIdentifier() = TypeIdentifier.Parameterised(typeName, null, parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We are adding array-ness to some type identifier.
|
||||||
|
*/
|
||||||
|
data class ParsingArray(val componentType: TypeIdentifier, override val parent: ParseState.ParsingParameterList?) : ParseState() {
|
||||||
|
override fun accept(c: Char) = when (c) {
|
||||||
|
' ' -> this
|
||||||
|
'p' -> ParsingArray(forcePrimitive(componentType), parent)
|
||||||
|
']' -> parent?.addParameter(getTypeIdentifier()) ?: Complete(getTypeIdentifier())
|
||||||
|
else -> unexpected(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTypeIdentifier() = TypeIdentifier.ArrayOf(componentType)
|
||||||
|
|
||||||
|
private fun forcePrimitive(componentType: TypeIdentifier): TypeIdentifier =
|
||||||
|
TypeIdentifier.forClass(Primitives.unwrap(componentType.getLocalType().asClass()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We have a complete type identifier, and all we can do to it is add array-ness.
|
||||||
|
*/
|
||||||
|
data class Complete(val identifier: TypeIdentifier) : ParseState() {
|
||||||
|
override val parent: ParseState.ParsingParameterList? get() = null
|
||||||
|
override fun accept(c: Char): ParseState = when (c) {
|
||||||
|
' ' -> this
|
||||||
|
'[' -> ParsingArray(identifier, null)
|
||||||
|
else -> unexpected(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTypeIdentifier() = identifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val simplified = mapOf(
|
||||||
|
"string" to String::class,
|
||||||
|
"boolean" to Boolean::class,
|
||||||
|
"byte" to Byte::class,
|
||||||
|
"char" to Char::class,
|
||||||
|
"int" to Int::class,
|
||||||
|
"short" to Short::class,
|
||||||
|
"long" to Long::class,
|
||||||
|
"double" to Double::class,
|
||||||
|
"float" to Float::class,
|
||||||
|
"ubyte" to UnsignedByte::class,
|
||||||
|
"uint" to UnsignedInteger::class,
|
||||||
|
"ushort" to UnsignedShort::class,
|
||||||
|
"ulong" to UnsignedLong::class,
|
||||||
|
"decimal32" to Decimal32::class,
|
||||||
|
"decimal64" to Decimal64::class,
|
||||||
|
"decimal128" to Decimal128::class,
|
||||||
|
"binary" to ByteArray::class,
|
||||||
|
"timestamp" to Date::class,
|
||||||
|
"uuid" to UUID::class,
|
||||||
|
"symbol" to Symbol::class).mapValues { (_, v) ->
|
||||||
|
TypeIdentifier.forClass(v.javaObjectType)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
package net.corda.serialization.internal.amqp
|
||||||
|
|
||||||
|
import net.corda.serialization.internal.model.TypeIdentifier
|
||||||
|
import org.apache.qpid.proton.amqp.*
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object AMQPTypeIdentifiers {
|
||||||
|
fun isPrimitive(type: Type): Boolean = isPrimitive(TypeIdentifier.forGenericType(type))
|
||||||
|
fun isPrimitive(typeIdentifier: TypeIdentifier) = typeIdentifier in primitiveTypeNamesByName
|
||||||
|
|
||||||
|
fun primitiveTypeName(type: Type): String? =
|
||||||
|
primitiveTypeNamesByName[TypeIdentifier.forGenericType(type)]
|
||||||
|
|
||||||
|
private val primitiveTypeNamesByName = sequenceOf(
|
||||||
|
Character::class to "char",
|
||||||
|
Char::class to "char",
|
||||||
|
Boolean::class to "boolean",
|
||||||
|
Byte::class to "byte",
|
||||||
|
UnsignedByte::class to "ubyte",
|
||||||
|
Short::class to "short",
|
||||||
|
UnsignedShort::class to "ushort",
|
||||||
|
Int::class to "int",
|
||||||
|
UnsignedInteger::class to "uint",
|
||||||
|
Long::class to "long",
|
||||||
|
UnsignedLong::class to "ulong",
|
||||||
|
Float::class to "float",
|
||||||
|
Double::class to "double",
|
||||||
|
Decimal32::class to "decimal32",
|
||||||
|
Decimal64::class to "decimal62",
|
||||||
|
Decimal128::class to "decimal128",
|
||||||
|
Date::class to "timestamp",
|
||||||
|
UUID::class to "uuid",
|
||||||
|
ByteArray::class to "binary",
|
||||||
|
String::class to "string",
|
||||||
|
Symbol::class to "symbol")
|
||||||
|
.flatMap { (klass, name) ->
|
||||||
|
val typeIdentifier = TypeIdentifier.forClass(klass.javaObjectType)
|
||||||
|
val primitiveTypeIdentifier = klass.javaPrimitiveType?.let { TypeIdentifier.forClass(it) }
|
||||||
|
if (primitiveTypeIdentifier == null) sequenceOf(typeIdentifier to name)
|
||||||
|
else sequenceOf(typeIdentifier to name, primitiveTypeIdentifier to name)
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
fun nameForType(typeIdentifier: TypeIdentifier): String = when(typeIdentifier) {
|
||||||
|
is TypeIdentifier.Erased -> typeIdentifier.name
|
||||||
|
is TypeIdentifier.Unparameterised -> primitiveTypeNamesByName[typeIdentifier] ?: typeIdentifier.name
|
||||||
|
is TypeIdentifier.UnknownType,
|
||||||
|
is TypeIdentifier.TopType -> "?"
|
||||||
|
is TypeIdentifier.ArrayOf ->
|
||||||
|
if (typeIdentifier == primitiveByteArrayType) "binary"
|
||||||
|
else nameForType(typeIdentifier.componentType) +
|
||||||
|
if (typeIdentifier.componentType is TypeIdentifier.Unparameterised &&
|
||||||
|
typeIdentifier.componentType.isPrimitive) "[p]"
|
||||||
|
else "[]"
|
||||||
|
is TypeIdentifier.Parameterised -> typeIdentifier.name + typeIdentifier.parameters.joinToString(", ", "<", ">") {
|
||||||
|
nameForType(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val primitiveByteArrayType = TypeIdentifier.ArrayOf(TypeIdentifier.forClass(Byte::class.javaPrimitiveType!!))
|
||||||
|
|
||||||
|
fun nameForType(type: Type): String = nameForType(TypeIdentifier.forGenericType(type))
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
package net.corda.serialization.internal.amqp
|
||||||
|
|
||||||
|
import net.corda.core.internal.uncheckedCast
|
||||||
|
import net.corda.core.utilities.contextLogger
|
||||||
|
import net.corda.serialization.internal.model.DefaultCacheProvider
|
||||||
|
import net.corda.serialization.internal.model.TypeIdentifier
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
|
interface CustomSerializerRegistry {
|
||||||
|
/**
|
||||||
|
* Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer
|
||||||
|
* that expects to find getters and a constructor with a parameter for each property.
|
||||||
|
*/
|
||||||
|
fun register(customSerializer: CustomSerializer<out Any>)
|
||||||
|
fun registerExternal(customSerializer: CorDappCustomSerializer)
|
||||||
|
|
||||||
|
fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>?
|
||||||
|
}
|
||||||
|
|
||||||
|
class CachingCustomSerializerRegistry(
|
||||||
|
private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry)
|
||||||
|
: CustomSerializerRegistry {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val logger = contextLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class CustomSerializerIdentifier(val actualTypeIdentifier: TypeIdentifier, val declaredTypeIdentifier: TypeIdentifier)
|
||||||
|
|
||||||
|
private val customSerializersCache: MutableMap<CustomSerializerIdentifier, AMQPSerializer<Any>> = DefaultCacheProvider.createCache()
|
||||||
|
private var customSerializers: List<SerializerFor> = emptyList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer
|
||||||
|
* that expects to find getters and a constructor with a parameter for each property.
|
||||||
|
*/
|
||||||
|
override fun register(customSerializer: CustomSerializer<out Any>) {
|
||||||
|
logger.trace("action=\"Registering custom serializer\", class=\"${customSerializer.type}\"")
|
||||||
|
|
||||||
|
descriptorBasedSerializerRegistry.getOrBuild(customSerializer.typeDescriptor.toString()) {
|
||||||
|
customSerializers += customSerializer
|
||||||
|
for (additional in customSerializer.additionalSerializers) {
|
||||||
|
register(additional)
|
||||||
|
}
|
||||||
|
customSerializer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerExternal(customSerializer: CorDappCustomSerializer) {
|
||||||
|
logger.trace("action=\"Registering external serializer\", class=\"${customSerializer.type}\"")
|
||||||
|
|
||||||
|
descriptorBasedSerializerRegistry.getOrBuild(customSerializer.typeDescriptor.toString()) {
|
||||||
|
customSerializers += customSerializer
|
||||||
|
customSerializer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>? {
|
||||||
|
val typeIdentifier = CustomSerializerIdentifier(
|
||||||
|
TypeIdentifier.forClass(clazz),
|
||||||
|
TypeIdentifier.forGenericType(declaredType))
|
||||||
|
|
||||||
|
return customSerializersCache[typeIdentifier]
|
||||||
|
?: doFindCustomSerializer(clazz, declaredType)?.also { serializer ->
|
||||||
|
customSerializersCache.putIfAbsent(typeIdentifier, serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doFindCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>? {
|
||||||
|
// e.g. Imagine if we provided a Map serializer this way, then it won't work if the declared type is
|
||||||
|
// AbstractMap, only Map. Otherwise it needs to inject additional schema for a RestrictedType source of the
|
||||||
|
// super type. Could be done, but do we need it?
|
||||||
|
for (customSerializer in customSerializers) {
|
||||||
|
if (customSerializer.isSerializerFor(clazz)) {
|
||||||
|
val declaredSuperClass = declaredType.asClass().superclass
|
||||||
|
|
||||||
|
return if (declaredSuperClass == null
|
||||||
|
|| !customSerializer.isSerializerFor(declaredSuperClass)
|
||||||
|
|| !customSerializer.revealSubclassesInSchema
|
||||||
|
) {
|
||||||
|
logger.debug("action=\"Using custom serializer\", class=${clazz.typeName}, " +
|
||||||
|
"declaredType=${declaredType.typeName}")
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
customSerializer as? AMQPSerializer<Any>
|
||||||
|
} else {
|
||||||
|
// Make a subclass serializer for the subclass and return that...
|
||||||
|
CustomSerializer.SubClass(clazz, uncheckedCast(customSerializer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package net.corda.serialization.internal.amqp
|
||||||
|
|
||||||
|
import net.corda.serialization.internal.model.DefaultCacheProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The quickest way to find a serializer, if one has already been generated, is to look it up by type descriptor.
|
||||||
|
*
|
||||||
|
* This registry gets shared around between various participants that might want to use it as a lookup, or register
|
||||||
|
* serialisers that they have created with it.
|
||||||
|
*/
|
||||||
|
interface DescriptorBasedSerializerRegistry {
|
||||||
|
operator fun get(descriptor: String): AMQPSerializer<Any>?
|
||||||
|
operator fun set(descriptor: String, serializer: AMQPSerializer<Any>)
|
||||||
|
fun getOrBuild(descriptor: String, builder: () -> AMQPSerializer<Any>): AMQPSerializer<Any>
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultDescriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry {
|
||||||
|
|
||||||
|
private val registry: MutableMap<String, AMQPSerializer<Any>> = DefaultCacheProvider.createCache()
|
||||||
|
|
||||||
|
override fun get(descriptor: String): AMQPSerializer<Any>? = registry[descriptor]
|
||||||
|
|
||||||
|
override fun set(descriptor: String, serializer: AMQPSerializer<Any>) {
|
||||||
|
registry.putIfAbsent(descriptor, serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOrBuild(descriptor: String, builder: () -> AMQPSerializer<Any>) =
|
||||||
|
get(descriptor) ?: builder().also { newSerializer -> this[descriptor] = newSerializer }
|
||||||
|
}
|
@ -52,6 +52,8 @@ class PublicPropertyReader(private val readMethod: Method) : PropertyReader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isNullable(): Boolean = readMethod.returnsNullable()
|
override fun isNullable(): Boolean = readMethod.returnsNullable()
|
||||||
|
|
||||||
|
val genericReturnType get() = readMethod.genericReturnType
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -118,7 +118,7 @@ interface SerializerFactory {
|
|||||||
Float::class.java to "float",
|
Float::class.java to "float",
|
||||||
Double::class.java to "double",
|
Double::class.java to "double",
|
||||||
Decimal32::class.java to "decimal32",
|
Decimal32::class.java to "decimal32",
|
||||||
Decimal64::class.java to "decimal62",
|
Decimal64::class.java to "decimal64",
|
||||||
Decimal128::class.java to "decimal128",
|
Decimal128::class.java to "decimal128",
|
||||||
Date::class.java to "timestamp",
|
Date::class.java to "timestamp",
|
||||||
UUID::class.java to "uuid",
|
UUID::class.java to "uuid",
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
package net.corda.serialization.internal.amqp
|
||||||
|
|
||||||
|
import com.google.common.primitives.Primitives
|
||||||
|
import net.corda.core.serialization.ClassWhitelist
|
||||||
|
import net.corda.serialization.internal.model.LocalTypeModelConfiguration
|
||||||
|
import org.apache.qpid.proton.amqp.*
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [LocalTypeModelConfiguration] based on a [ClassWhitelist]
|
||||||
|
*/
|
||||||
|
class WhitelistBasedTypeModelConfiguration(
|
||||||
|
private val whitelist: ClassWhitelist,
|
||||||
|
private val customSerializerRegistry: CustomSerializerRegistry)
|
||||||
|
: LocalTypeModelConfiguration {
|
||||||
|
override fun isExcluded(type: Type): Boolean = whitelist.isNotWhitelisted(type.asClass())
|
||||||
|
override fun isOpaque(type: Type): Boolean = Primitives.unwrap(type.asClass()) in opaqueTypes ||
|
||||||
|
customSerializerRegistry.findCustomSerializer(type.asClass(), type) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from SerializerFactory so that we can have equivalent behaviour, for now.
|
||||||
|
private val opaqueTypes = setOf(
|
||||||
|
Character::class.java,
|
||||||
|
Char::class.java,
|
||||||
|
Boolean::class.java,
|
||||||
|
Byte::class.java,
|
||||||
|
UnsignedByte::class.java,
|
||||||
|
Short::class.java,
|
||||||
|
UnsignedShort::class.java,
|
||||||
|
Int::class.java,
|
||||||
|
UnsignedInteger::class.java,
|
||||||
|
Long::class.java,
|
||||||
|
UnsignedLong::class.java,
|
||||||
|
Float::class.java,
|
||||||
|
Double::class.java,
|
||||||
|
Decimal32::class.java,
|
||||||
|
Decimal64::class.java,
|
||||||
|
Decimal128::class.java,
|
||||||
|
Date::class.java,
|
||||||
|
UUID::class.java,
|
||||||
|
ByteArray::class.java,
|
||||||
|
String::class.java,
|
||||||
|
Symbol::class.java
|
||||||
|
)
|
@ -148,7 +148,9 @@ class ClassCarpenterImpl @JvmOverloads constructor (override val whitelist: Clas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
require(schema.name in _loaded)
|
if (schema.name !in _loaded){
|
||||||
|
throw ClassNotFoundException(schema.name)
|
||||||
|
}
|
||||||
|
|
||||||
return _loaded[schema.name]!!
|
return _loaded[schema.name]!!
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,7 @@ abstract class MetaCarpenterBase(val schemas: CarpenterMetaSchema, val cc: Class
|
|||||||
// carpented class existing and remove it from their dependency list, If that
|
// carpented class existing and remove it from their dependency list, If that
|
||||||
// list is now empty we have no impediment to carpenting that class up
|
// list is now empty we have no impediment to carpenting that class up
|
||||||
schemas.dependsOn.remove(newObject.name)?.forEach { dependent ->
|
schemas.dependsOn.remove(newObject.name)?.forEach { dependent ->
|
||||||
|
|
||||||
require(newObject.name in schemas.dependencies[dependent]!!.second)
|
require(newObject.name in schemas.dependencies[dependent]!!.second)
|
||||||
|
|
||||||
schemas.dependencies[dependent]?.second?.remove(newObject.name)
|
schemas.dependencies[dependent]?.second?.remove(newObject.name)
|
||||||
|
@ -72,8 +72,7 @@ open class NonNullableField(field: Class<out Any?>) : ClassField(field) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun nullTest(mv: MethodVisitor, slot: Int) {
|
override fun nullTest(mv: MethodVisitor, slot: Int) {
|
||||||
require(name != unsetName)
|
check(name != unsetName) {"Property this.name cannot be $unsetName"}
|
||||||
|
|
||||||
if (!field.isPrimitive) {
|
if (!field.isPrimitive) {
|
||||||
with(mv) {
|
with(mv) {
|
||||||
visitVarInsn(ALOAD, 0) // load this
|
visitVarInsn(ALOAD, 0) // load this
|
||||||
@ -109,7 +108,7 @@ class NullableField(field: Class<out Any?>) : ClassField(field) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun nullTest(mv: MethodVisitor, slot: Int) {
|
override fun nullTest(mv: MethodVisitor, slot: Int) {
|
||||||
require(name != unsetName)
|
require(name != unsetName){"Property this.name cannot be $unsetName"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,124 @@
|
|||||||
|
package net.corda.serialization.internal.model
|
||||||
|
|
||||||
|
import java.io.NotSerializableException
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Once we have the complete graph of types requiring carpentry to hand, we can use it to sort those types in reverse-
|
||||||
|
* dependency order, i.e. beginning with those types that have no dependencies on other types, then the types that
|
||||||
|
* depended on those types, and so on. This means we can feed types directly to the [RemoteTypeCarpenter], and don't
|
||||||
|
* have to use the [CarpenterMetaSchema].
|
||||||
|
*
|
||||||
|
* @param typesRequiringCarpentry The set of [RemoteTypeInformation] for types that are not reachable by the current
|
||||||
|
* classloader.
|
||||||
|
*/
|
||||||
|
class CarpentryDependencyGraph private constructor(private val typesRequiringCarpentry: Set<RemoteTypeInformation>) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Sort the [typesRequiringCarpentry] into reverse-dependency order, then pass them to the provided
|
||||||
|
* [Type]-builder, collating the results into a [Map] of [Type] by [TypeIdentifier]
|
||||||
|
*/
|
||||||
|
fun buildInReverseDependencyOrder(
|
||||||
|
typesRequiringCarpentry: Set<RemoteTypeInformation>,
|
||||||
|
getOrBuild: (RemoteTypeInformation) -> Type): Map<TypeIdentifier, Type> =
|
||||||
|
CarpentryDependencyGraph(typesRequiringCarpentry).buildInOrder(getOrBuild)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of inbound edges by node.
|
||||||
|
*
|
||||||
|
* A [RemoteTypeInformation] map key is a type that requires other types to have been constructed before it can be
|
||||||
|
* constructed.
|
||||||
|
*
|
||||||
|
* Each [RemoteTypeInformation] in the corresponding [Set] map value is one of the types that the key-type depends on.
|
||||||
|
*
|
||||||
|
* No key ever maps to an empty set: types with no dependencies are not included in this map.
|
||||||
|
*/
|
||||||
|
private val dependencies = mutableMapOf<RemoteTypeInformation, MutableSet<RemoteTypeInformation>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If it is in [typesRequiringCarpentry], then add an edge from [dependee] to this type to the [dependencies] graph.
|
||||||
|
*/
|
||||||
|
private fun RemoteTypeInformation.dependsOn(dependee: RemoteTypeInformation) = dependsOn(listOf(dependee))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an edge from each of these [dependees] that are in [typesRequiringCarpentry] to this type to the
|
||||||
|
* [dependencies] graph.
|
||||||
|
*/
|
||||||
|
private fun RemoteTypeInformation.dependsOn(dependees: Collection<RemoteTypeInformation>) {
|
||||||
|
val dependeesInTypesRequiringCarpentry = dependees.filter { it in typesRequiringCarpentry }
|
||||||
|
if (dependeesInTypesRequiringCarpentry.isEmpty()) return // we don't want to put empty sets into the map.
|
||||||
|
dependencies.compute(this) { _, dependees ->
|
||||||
|
dependees?.apply { addAll(dependeesInTypesRequiringCarpentry) } ?:
|
||||||
|
dependeesInTypesRequiringCarpentry.toMutableSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traverses each of the [typesRequiringCarpentry], building (or obtaining from a cache) the corresponding [Type]
|
||||||
|
* and populating them into a [Map] of [Type] by [TypeIdentifier].
|
||||||
|
*/
|
||||||
|
private fun buildInOrder(getOrBuild: (RemoteTypeInformation) -> Type): Map<TypeIdentifier, Type> {
|
||||||
|
typesRequiringCarpentry.forEach { it.recordDependencies() }
|
||||||
|
|
||||||
|
return topologicalSort(typesRequiringCarpentry).associate { information ->
|
||||||
|
information.typeIdentifier to getOrBuild(information)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record appropriate dependencies for each type of [RemoteTypeInformation]
|
||||||
|
*/
|
||||||
|
private fun RemoteTypeInformation.recordDependencies() = when (this) {
|
||||||
|
is RemoteTypeInformation.Composable -> {
|
||||||
|
dependsOn(typeParameters)
|
||||||
|
dependsOn(interfaces)
|
||||||
|
dependsOn(properties.values.map { it.type })
|
||||||
|
}
|
||||||
|
is RemoteTypeInformation.AnInterface -> {
|
||||||
|
dependsOn(typeParameters)
|
||||||
|
dependsOn(interfaces)
|
||||||
|
dependsOn(properties.values.map { it.type })
|
||||||
|
}
|
||||||
|
is RemoteTypeInformation.AnArray -> dependsOn(componentType)
|
||||||
|
is RemoteTypeInformation.Parameterised -> dependsOn(typeParameters)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Separate out those [types] which have [noDependencies] from those which still have dependencies.
|
||||||
|
*
|
||||||
|
* Remove the types with no dependencies from the graph, identifying which types are left with no inbound dependees
|
||||||
|
* as a result, then return the types with no dependencies concatenated with the [topologicalSort] of the remaining
|
||||||
|
* types, minus the newly-independent types.
|
||||||
|
*/
|
||||||
|
private fun topologicalSort(
|
||||||
|
types: Set<RemoteTypeInformation>,
|
||||||
|
noDependencies: Set<RemoteTypeInformation> = types - dependencies.keys): Sequence<RemoteTypeInformation> {
|
||||||
|
// Types which still have dependencies.
|
||||||
|
val remaining = dependencies.keys.toSet()
|
||||||
|
|
||||||
|
// Remove the types which have no dependencies from the dependencies of the remaining types, and identify
|
||||||
|
// those types which have no dependencies left after we've done this.
|
||||||
|
val newlyIndependent = dependencies.asSequence().mapNotNull { (dependent, dependees) ->
|
||||||
|
dependees.removeAll(noDependencies)
|
||||||
|
if (dependees.isEmpty()) dependent else null
|
||||||
|
}.toSet()
|
||||||
|
|
||||||
|
// If there are still types with dependencies, and we have no dependencies we can remove, then we can't continue.
|
||||||
|
if (newlyIndependent.isEmpty() && dependencies.isNotEmpty()) {
|
||||||
|
throw NotSerializableException(
|
||||||
|
"Cannot build dependencies for " +
|
||||||
|
dependencies.keys.map { it.typeIdentifier.prettyPrint(false) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the types which have no dependencies remaining, maintaining the invariant that no key maps to an
|
||||||
|
// empty set.
|
||||||
|
dependencies.keys.removeAll(newlyIndependent)
|
||||||
|
|
||||||
|
// Return the types that had no dependencies, then recurse to process the remainder.
|
||||||
|
return noDependencies.asSequence() +
|
||||||
|
if (dependencies.isEmpty()) newlyIndependent.asSequence() else topologicalSort(remaining, newlyIndependent)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
package net.corda.serialization.internal.model
|
||||||
|
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the information we have about a property of a type.
|
||||||
|
*/
|
||||||
|
sealed class LocalPropertyInformation(val isCalculated: Boolean) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [LocalTypeInformation] for the type of the property.
|
||||||
|
*/
|
||||||
|
abstract val type: LocalTypeInformation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the property is a primitive type or is flagged as non-nullable, false otherwise.
|
||||||
|
*/
|
||||||
|
abstract val isMandatory: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A property of an interface, for which we have only a getter method.
|
||||||
|
*
|
||||||
|
* @param observedGetter The method which can be used to obtain the value of this property from an instance of its owning type.
|
||||||
|
*/
|
||||||
|
data class ReadOnlyProperty(val observedGetter: Method, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A property for which we have both a getter, and a matching slot in an array of constructor parameters.
|
||||||
|
*
|
||||||
|
* @param observedGetter The method which can be used to obtain the value of this property from an instance of its owning type.
|
||||||
|
* @param constructorSlot The [ConstructorSlot] to which the property corresponds, used to populate an array of
|
||||||
|
* constructor arguments when creating instances of its owning type.
|
||||||
|
*/
|
||||||
|
data class ConstructorPairedProperty(val observedGetter: Method, val constructorSlot: ConstructorSlot, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A property for which we have no getter, but for which there is a backing field a matching slot in an array of
|
||||||
|
* constructor parameters.
|
||||||
|
*
|
||||||
|
* @param observedField The field which can be used to obtain the value of this property from an instance of its owning type.
|
||||||
|
* @param constructorSlot The [ConstructorSlot] to which the property corresponds, used to populate an array of
|
||||||
|
* constructor arguments when creating instances of its owning type.
|
||||||
|
*/
|
||||||
|
data class PrivateConstructorPairedProperty(val observedField: Field, val constructorSlot: ConstructorSlot, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A property for which we have both getter and setter methods (usually belonging to a POJO which is initialised
|
||||||
|
* with the default no-argument constructor and then configured via setters).
|
||||||
|
*
|
||||||
|
* @param observedGetter The method which can be used to obtain the value of this property from an instance of its owning type.
|
||||||
|
* @param observedSetter The method which can be used to set the value of this property on an instance of its owning type.
|
||||||
|
*/
|
||||||
|
data class GetterSetterProperty(val observedGetter: Method, val observedSetter: Method, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A property for which we have only a getter method, which is annotated with [SerializableCalculatedProperty].
|
||||||
|
*/
|
||||||
|
data class CalculatedProperty(val observedGetter: Method, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* References a slot in an array of constructor parameters.
|
||||||
|
*/
|
||||||
|
data class ConstructorSlot(val parameterIndex: Int, val constructorInformation: LocalConstructorInformation) {
|
||||||
|
val parameterInformation get() = constructorInformation.parameters.getOrNull(parameterIndex) ?:
|
||||||
|
throw IllegalStateException("Constructor slot refers to parameter #$parameterIndex " +
|
||||||
|
"of constructor $constructorInformation, " +
|
||||||
|
"but constructor has only ${constructorInformation.parameters.size} parameters")
|
||||||
|
}
|
@ -0,0 +1,374 @@
|
|||||||
|
package net.corda.serialization.internal.model
|
||||||
|
|
||||||
|
import java.lang.reflect.*
|
||||||
|
import kotlin.reflect.KFunction
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
typealias PropertyName = String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [LocalTypeInformation] captured for a [Type] gathers together everything that can be ascertained about the type
|
||||||
|
* through runtime reflection, in the form of a directed acyclic graph (DAG) of types and relationships between types.
|
||||||
|
*
|
||||||
|
* Types can be related in the following ways:
|
||||||
|
*
|
||||||
|
* * Type A is the type of a _property_ of type B.
|
||||||
|
* * Type A is the type of an _interface_ of type B.
|
||||||
|
* * Type A is the type of the _superclass_ of type B.
|
||||||
|
* * Type A is the type of a _type parameter_ of type B.
|
||||||
|
* * Type A is an _array type_, of which type B is the _component type_.
|
||||||
|
*
|
||||||
|
* All of these relationships are represented by references and collections held by the objects representing the nodes
|
||||||
|
* themselves.
|
||||||
|
*
|
||||||
|
* A type is [Composable] if it is isomorphic to a dictionary of its property values, i.e. if we can obtain an instance
|
||||||
|
* of the type from a dictionary containing typed key/value pairs corresponding to its properties, and a dictionary from
|
||||||
|
* an instance of the type, and can round-trip (in both directions) between these representations without losing
|
||||||
|
* information. This is the basis for compositional serialization, i.e. building a serializer for a type out of the
|
||||||
|
* serializers we have for its property types.
|
||||||
|
*
|
||||||
|
* A type is [Atomic] if it cannot be decomposed or recomposed in this fashion (usually because it is the type of a
|
||||||
|
* scalar value of some sort, such as [Int]), and [Opaque] if we have chosen not to investigate its composability,
|
||||||
|
* typically because it is handled by a custom serializer.
|
||||||
|
*
|
||||||
|
* Abstract types are represented by [AnInterface] and [Abstract], the difference between them being that an [Abstract]
|
||||||
|
* type may have a superclass.
|
||||||
|
*
|
||||||
|
* If a concrete type does not have a unique deserialization constructor, it is represented by [NonComposable], meaning
|
||||||
|
* that we know how to take it apart but do not know how to put it back together again.
|
||||||
|
*
|
||||||
|
* An array of any type is represented by [ArrayOf]. Enums are represented by [AnEnum].
|
||||||
|
*
|
||||||
|
* The type of [Any]/[java.lang.Object] is represented by [Top]. Unbounded wildcards, or wildcards whose upper bound is
|
||||||
|
* [Top], are represented by [Unknown]. Bounded wildcards are always resolved to their upper bounds, e.g.
|
||||||
|
* `List<? extends String>` becomes `List<String>`.
|
||||||
|
*
|
||||||
|
* If we encounter a cycle while traversing the DAG, the type on which traversal detected the cycle is represented by
|
||||||
|
* [Cycle], and no further traversal is attempted from that type. Kotlin objects are represented by [Singleton].
|
||||||
|
*/
|
||||||
|
sealed class LocalTypeInformation {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Using the provided [LocalTypeLookup] to record and locate already-visited nodes, traverse the DAG of related
|
||||||
|
* types beginning the with provided [Type] and construct a complete set of [LocalTypeInformation] for that type.
|
||||||
|
*
|
||||||
|
* @param type The [Type] to obtain [LocalTypeInformation] for.
|
||||||
|
* @param lookup The [LocalTypeLookup] to use to find previously-constructed [LocalTypeInformation].
|
||||||
|
*/
|
||||||
|
fun forType(type: Type, lookup: LocalTypeLookup): LocalTypeInformation =
|
||||||
|
LocalTypeInformationBuilder(lookup).build(type, TypeIdentifier.forGenericType(type))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual type which was observed when constructing this type information.
|
||||||
|
*/
|
||||||
|
abstract val observedType: Type
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [TypeIdentifier] for the type represented by this type information, used to cross-reference with
|
||||||
|
* [RemoteTypeInformation].
|
||||||
|
*/
|
||||||
|
abstract val typeIdentifier: TypeIdentifier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain a multi-line, recursively-indented representation of this type information.
|
||||||
|
*
|
||||||
|
* @param simplifyClassNames By default, class names are printed as their "simple" class names, i.e. "String" instead
|
||||||
|
* of "java.lang.String". If this is set to `false`, then the full class name will be printed instead.
|
||||||
|
*/
|
||||||
|
fun prettyPrint(simplifyClassNames: Boolean = true): String =
|
||||||
|
LocalTypeInformationPrettyPrinter(simplifyClassNames).prettyPrint(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [LocalTypeInformation] corresponding to an unbounded wildcard ([TypeIdentifier.UnknownType])
|
||||||
|
*/
|
||||||
|
object Unknown : LocalTypeInformation() {
|
||||||
|
override val observedType get() = TypeIdentifier.UnknownType.getLocalType()
|
||||||
|
override val typeIdentifier get() = TypeIdentifier.UnknownType
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [LocalTypeInformation] corresponding to [java.lang.Object] / [Any] ([TypeIdentifier.TopType])
|
||||||
|
*/
|
||||||
|
object Top : LocalTypeInformation() {
|
||||||
|
override val observedType get() = TypeIdentifier.TopType.getLocalType()
|
||||||
|
override val typeIdentifier get() = TypeIdentifier.TopType
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [LocalTypeInformation] emitted if we hit a cycle while traversing the graph of related types.
|
||||||
|
*/
|
||||||
|
data class Cycle(
|
||||||
|
override val observedType: Type,
|
||||||
|
override val typeIdentifier: TypeIdentifier,
|
||||||
|
private val _follow: () -> LocalTypeInformation) : LocalTypeInformation() {
|
||||||
|
val follow: LocalTypeInformation get() = _follow()
|
||||||
|
|
||||||
|
// Custom equals / hashcode because otherwise the "follow" lambda makes equality harder to reason about.
|
||||||
|
override fun equals(other: Any?): Boolean =
|
||||||
|
other is Cycle &&
|
||||||
|
other.observedType == observedType &&
|
||||||
|
other.typeIdentifier == typeIdentifier
|
||||||
|
|
||||||
|
override fun hashCode(): Int = Objects.hash(observedType, typeIdentifier)
|
||||||
|
|
||||||
|
override fun toString(): String = "Cycle($observedType, $typeIdentifier)"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* May in fact be a more complex class, but is treated as if atomic, i.e. we don't further expand its properties.
|
||||||
|
*/
|
||||||
|
data class Opaque(override val observedType: Class<*>, override val typeIdentifier: TypeIdentifier,
|
||||||
|
private val _expand: () -> LocalTypeInformation) : LocalTypeInformation() {
|
||||||
|
val expand: LocalTypeInformation get() = _expand()
|
||||||
|
|
||||||
|
// Custom equals / hashcode because otherwise the "expand" lambda makes equality harder to reason about.
|
||||||
|
override fun equals(other: Any?): Boolean =
|
||||||
|
other is Cycle &&
|
||||||
|
other.observedType == observedType &&
|
||||||
|
other.typeIdentifier == typeIdentifier
|
||||||
|
|
||||||
|
override fun hashCode(): Int = Objects.hash(observedType, typeIdentifier)
|
||||||
|
|
||||||
|
override fun toString(): String = "Opaque($observedType, $typeIdentifier)"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a scalar type such as [Int].
|
||||||
|
*/
|
||||||
|
data class Atomic(override val observedType: Class<*>, override val typeIdentifier: TypeIdentifier) : LocalTypeInformation()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an array of some other type.
|
||||||
|
*
|
||||||
|
* @param componentType The [LocalTypeInformation] for the component type of the array (e.g. [Int], if the type is [IntArray])
|
||||||
|
*/
|
||||||
|
data class AnArray(override val observedType: Type, override val typeIdentifier: TypeIdentifier, val componentType: LocalTypeInformation) : LocalTypeInformation()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an `enum`
|
||||||
|
*
|
||||||
|
* @param members The string names of the members of the enum.
|
||||||
|
* @param superclass [LocalTypeInformation] for the superclass of the type (as enums can inherit from other types).
|
||||||
|
* @param interfaces [LocalTypeInformation] for each interface implemented by the type.
|
||||||
|
*/
|
||||||
|
data class AnEnum(
|
||||||
|
override val observedType: Class<*>,
|
||||||
|
override val typeIdentifier: TypeIdentifier,
|
||||||
|
val members: List<String>,
|
||||||
|
val interfaces: List<LocalTypeInformation>): LocalTypeInformation()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a type whose underlying class is an interface.
|
||||||
|
*
|
||||||
|
* @param properties [LocalPropertyInformation] for the read-only properties of the interface, i.e. its "getter" methods.
|
||||||
|
* @param interfaces [LocalTypeInformation] for the interfaces extended by this interface.
|
||||||
|
* @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type.
|
||||||
|
*/
|
||||||
|
data class AnInterface(
|
||||||
|
override val observedType: Type,
|
||||||
|
override val typeIdentifier: TypeIdentifier,
|
||||||
|
val properties: Map<PropertyName, LocalPropertyInformation>,
|
||||||
|
val interfaces: List<LocalTypeInformation>,
|
||||||
|
val typeParameters: List<LocalTypeInformation>) : LocalTypeInformation()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a type whose underlying class is abstract.
|
||||||
|
*
|
||||||
|
* @param properties [LocalPropertyInformation] for the read-only properties of the interface, i.e. its "getter" methods.
|
||||||
|
* @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type.
|
||||||
|
* @param interfaces [LocalTypeInformation] for the interfaces extended by this interface.
|
||||||
|
* @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type.
|
||||||
|
*/
|
||||||
|
data class Abstract(
|
||||||
|
override val observedType: Type,
|
||||||
|
override val typeIdentifier: TypeIdentifier,
|
||||||
|
val properties: Map<PropertyName, LocalPropertyInformation>,
|
||||||
|
val superclass: LocalTypeInformation,
|
||||||
|
val interfaces: List<LocalTypeInformation>,
|
||||||
|
val typeParameters: List<LocalTypeInformation>) : LocalTypeInformation()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a type which has only a single instantiation, e.g. a Kotlin `object`.
|
||||||
|
*
|
||||||
|
* @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type.
|
||||||
|
* @param interfaces [LocalTypeInformation] for the interfaces extended by this interface.
|
||||||
|
*/
|
||||||
|
data class Singleton(override val observedType: Type, override val typeIdentifier: TypeIdentifier, val superclass: LocalTypeInformation, val interfaces: List<LocalTypeInformation>) : LocalTypeInformation()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a type whose instances can be reversibly decomposed into dictionaries of typed values.
|
||||||
|
*
|
||||||
|
* @param constructor [LocalConstructorInformation] for the constructor used when building instances of this type
|
||||||
|
* out of dictionaries of typed values.
|
||||||
|
* @param properties [LocalPropertyInformation] for the properties of the interface.
|
||||||
|
* @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type.
|
||||||
|
* @param interfaces [LocalTypeInformation] for the interfaces extended by this interface.
|
||||||
|
* @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type.
|
||||||
|
*/
|
||||||
|
data class Composable(
|
||||||
|
override val observedType: Type,
|
||||||
|
override val typeIdentifier: TypeIdentifier,
|
||||||
|
val constructor: LocalConstructorInformation,
|
||||||
|
val evolverConstructors: List<EvolverConstructorInformation>,
|
||||||
|
val properties: Map<PropertyName, LocalPropertyInformation>,
|
||||||
|
val superclass: LocalTypeInformation,
|
||||||
|
val interfaces: List<LocalTypeInformation>,
|
||||||
|
val typeParameters: List<LocalTypeInformation>) : LocalTypeInformation()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a type whose instances may have observable properties (represented by "getter" methods), but for which
|
||||||
|
* we do not possess a method (such as a unique "deserialization constructor" satisfied by these properties) for
|
||||||
|
* creating a new instance from a dictionary of property values.
|
||||||
|
*
|
||||||
|
* @param constructor [LocalConstructorInformation] for the constructor of this type, if there is one.
|
||||||
|
* @param properties [LocalPropertyInformation] for the properties of the interface.
|
||||||
|
* @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type.
|
||||||
|
* @param interfaces [LocalTypeInformation] for the interfaces extended by this interface.
|
||||||
|
* @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type.
|
||||||
|
*/
|
||||||
|
data class NonComposable(
|
||||||
|
override val observedType: Type,
|
||||||
|
override val typeIdentifier: TypeIdentifier,
|
||||||
|
val constructor: LocalConstructorInformation?,
|
||||||
|
val properties: Map<PropertyName, LocalPropertyInformation>,
|
||||||
|
val superclass: LocalTypeInformation,
|
||||||
|
val interfaces: List<LocalTypeInformation>,
|
||||||
|
val typeParameters: List<LocalTypeInformation>) : LocalTypeInformation()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a type whose underlying class is a collection class such as [List] with a single type parameter.
|
||||||
|
*
|
||||||
|
* @param elementType [LocalTypeInformation] for the resolved type parameter of the type, i.e. the type of its
|
||||||
|
* elements. [Unknown] if the type is erased.
|
||||||
|
*/
|
||||||
|
data class ACollection(override val observedType: Type, override val typeIdentifier: TypeIdentifier, val elementType: LocalTypeInformation) : LocalTypeInformation() {
|
||||||
|
val isErased: Boolean get() = typeIdentifier is TypeIdentifier.Erased
|
||||||
|
|
||||||
|
fun withElementType(parameter: LocalTypeInformation): ACollection = when(typeIdentifier) {
|
||||||
|
is TypeIdentifier.Erased -> {
|
||||||
|
val unerasedType = typeIdentifier.toParameterized(listOf(parameter.typeIdentifier))
|
||||||
|
ACollection(
|
||||||
|
unerasedType.getLocalType(this::class.java.classLoader),
|
||||||
|
unerasedType,
|
||||||
|
parameter)
|
||||||
|
}
|
||||||
|
is TypeIdentifier.Parameterised -> {
|
||||||
|
val reparameterizedType = typeIdentifier.copy(parameters = listOf(parameter.typeIdentifier))
|
||||||
|
ACollection(
|
||||||
|
reparameterizedType.getLocalType(this::class.java.classLoader),
|
||||||
|
reparameterizedType,
|
||||||
|
parameter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> throw IllegalStateException("Cannot parameterise $this")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a type whose underlying class is a map class such as [Map] with two type parameters.
|
||||||
|
*
|
||||||
|
* @param keyType [LocalTypeInformation] for the first resolved type parameter of the type, i.e. the type of its
|
||||||
|
* keys. [Unknown] if the type is erased.
|
||||||
|
* @param valueType [LocalTypeInformation] for the second resolved type parameter of the type, i.e. the type of its
|
||||||
|
* values. [Unknown] if the type is erased.
|
||||||
|
*/
|
||||||
|
data class AMap(override val observedType: Type, override val typeIdentifier: TypeIdentifier,
|
||||||
|
val keyType: LocalTypeInformation, val valueType: LocalTypeInformation) : LocalTypeInformation() {
|
||||||
|
val isErased: Boolean get() = typeIdentifier is TypeIdentifier.Erased
|
||||||
|
|
||||||
|
fun withParameters(keyType: LocalTypeInformation, valueType: LocalTypeInformation): AMap = when(typeIdentifier) {
|
||||||
|
is TypeIdentifier.Erased -> {
|
||||||
|
val unerasedType = typeIdentifier.toParameterized(listOf(keyType.typeIdentifier, valueType.typeIdentifier))
|
||||||
|
AMap(
|
||||||
|
unerasedType.getLocalType(this::class.java.classLoader),
|
||||||
|
unerasedType,
|
||||||
|
keyType, valueType)
|
||||||
|
}
|
||||||
|
is TypeIdentifier.Parameterised -> {
|
||||||
|
val reparameterizedType = typeIdentifier.copy(parameters = listOf(keyType.typeIdentifier, valueType.typeIdentifier))
|
||||||
|
AMap(
|
||||||
|
reparameterizedType.getLocalType(this::class.java.classLoader),
|
||||||
|
reparameterizedType,
|
||||||
|
keyType, valueType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> throw IllegalStateException("Cannot parameterise $this")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents information about a constructor.
|
||||||
|
*/
|
||||||
|
data class LocalConstructorInformation(
|
||||||
|
val observedMethod: KFunction<Any>,
|
||||||
|
val parameters: List<LocalConstructorParameterInformation>) {
|
||||||
|
val hasParameters: Boolean get() = parameters.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents information about a constructor that is specifically to be used for evolution, and is potentially matched
|
||||||
|
* with a different set of properties to the regular constructor.
|
||||||
|
*/
|
||||||
|
data class EvolverConstructorInformation(
|
||||||
|
val constructor: LocalConstructorInformation,
|
||||||
|
val properties: Map<String, LocalPropertyInformation>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents information about a constructor parameter
|
||||||
|
*/
|
||||||
|
data class LocalConstructorParameterInformation(
|
||||||
|
val name: String,
|
||||||
|
val type: LocalTypeInformation,
|
||||||
|
val isMandatory: Boolean)
|
||||||
|
|
||||||
|
private data class LocalTypeInformationPrettyPrinter(private val simplifyClassNames: Boolean, private val indent: Int = 0) {
|
||||||
|
|
||||||
|
fun prettyPrint(typeInformation: LocalTypeInformation): String =
|
||||||
|
with(typeInformation) {
|
||||||
|
when (this) {
|
||||||
|
is LocalTypeInformation.Abstract ->
|
||||||
|
typeIdentifier.prettyPrint() +
|
||||||
|
printInheritsFrom(interfaces, superclass) +
|
||||||
|
indentAnd { printProperties(properties) }
|
||||||
|
is LocalTypeInformation.AnInterface ->
|
||||||
|
typeIdentifier.prettyPrint() + printInheritsFrom(interfaces)
|
||||||
|
is LocalTypeInformation.Composable -> typeIdentifier.prettyPrint() +
|
||||||
|
printConstructor(constructor) +
|
||||||
|
printInheritsFrom(interfaces, superclass) +
|
||||||
|
indentAnd { printProperties(properties) }
|
||||||
|
else -> typeIdentifier.prettyPrint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun printConstructor(constructor: LocalConstructorInformation) =
|
||||||
|
constructor.parameters.joinToString(", ", "(", ")") {
|
||||||
|
it.name +
|
||||||
|
": " + it.type.typeIdentifier.prettyPrint(simplifyClassNames) +
|
||||||
|
(if (!it.isMandatory) "?" else "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun printInheritsFrom(interfaces: List<LocalTypeInformation>, superclass: LocalTypeInformation? = null): String {
|
||||||
|
val parents = if (superclass == null || superclass == LocalTypeInformation.Top) interfaces.asSequence()
|
||||||
|
else sequenceOf(superclass) + interfaces.asSequence()
|
||||||
|
return if (!parents.iterator().hasNext()) ""
|
||||||
|
else parents.joinToString(", ", ": ", "") { it.typeIdentifier.prettyPrint(simplifyClassNames) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun printProperties(properties: Map<String, LocalPropertyInformation>) =
|
||||||
|
properties.entries.asSequence().sortedBy { it.key }.joinToString("\n", "\n", "") {
|
||||||
|
it.prettyPrint()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map.Entry<String, LocalPropertyInformation>.prettyPrint(): String =
|
||||||
|
" ".repeat(indent) + key +
|
||||||
|
(if(!value.isMandatory) " (optional)" else "") +
|
||||||
|
(if (value.isCalculated) " (calculated)" else "") +
|
||||||
|
": " + value.type.prettyPrint(simplifyClassNames)
|
||||||
|
|
||||||
|
private inline fun indentAnd(block: LocalTypeInformationPrettyPrinter.() -> String) =
|
||||||
|
copy(indent = indent + 1).block()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,410 @@
|
|||||||
|
package net.corda.serialization.internal.model
|
||||||
|
|
||||||
|
import net.corda.core.internal.isAbstractClass
|
||||||
|
import net.corda.core.internal.isConcreteClass
|
||||||
|
import net.corda.core.internal.kotlinObjectInstance
|
||||||
|
import net.corda.core.serialization.ConstructorForDeserialization
|
||||||
|
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||||
|
import net.corda.core.utilities.contextLogger
|
||||||
|
import net.corda.serialization.internal.amqp.*
|
||||||
|
import java.io.NotSerializableException
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.lang.reflect.ParameterizedType
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.LinkedHashMap
|
||||||
|
import kotlin.reflect.KFunction
|
||||||
|
import kotlin.reflect.full.findAnnotation
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
import kotlin.reflect.full.primaryConstructor
|
||||||
|
import kotlin.reflect.jvm.internal.KotlinReflectionInternalError
|
||||||
|
import kotlin.reflect.jvm.isAccessible
|
||||||
|
import kotlin.reflect.jvm.javaConstructor
|
||||||
|
import kotlin.reflect.jvm.javaGetter
|
||||||
|
import kotlin.reflect.jvm.javaType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the logic for building instances of [LocalTypeInformation] by reflecting over local [Type]s.
|
||||||
|
*
|
||||||
|
* @param lookup The [LocalTypeLookup] to use to locate and register constructed [LocalTypeInformation].
|
||||||
|
* @param resolutionContext The [Type] to use when attempting to resolve type variables.
|
||||||
|
* @param visited The [Set] of [TypeIdentifier]s already visited while building information for a given [Type]. Note that
|
||||||
|
* this is not a [MutableSet], as we want to be able to backtrack while traversing through the graph of related types, and
|
||||||
|
* will find it useful to revert to earlier states of knowledge about which types have been visited on a given branch.
|
||||||
|
*/
|
||||||
|
internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, val resolutionContext: Type? = null, val visited: Set<TypeIdentifier> = emptySet()) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = contextLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively build [LocalTypeInformation] for the given [Type] and [TypeIdentifier]
|
||||||
|
*/
|
||||||
|
fun build(type: Type, typeIdentifier: TypeIdentifier): LocalTypeInformation =
|
||||||
|
if (typeIdentifier in visited) LocalTypeInformation.Cycle(type, typeIdentifier) {
|
||||||
|
LocalTypeInformationBuilder(lookup, resolutionContext).build(type, typeIdentifier)
|
||||||
|
}
|
||||||
|
else lookup.findOrBuild(type, typeIdentifier) { isOpaque ->
|
||||||
|
copy(visited = visited + typeIdentifier).buildIfNotFound(type, typeIdentifier, isOpaque)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveAndBuild(type: Type): LocalTypeInformation {
|
||||||
|
val resolved = type.resolveAgainstContext()
|
||||||
|
return build(resolved, TypeIdentifier.forGenericType(resolved, resolutionContext
|
||||||
|
?: type))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Type.resolveAgainstContext(): Type =
|
||||||
|
if (resolutionContext == null) this else resolveAgainst(resolutionContext)
|
||||||
|
|
||||||
|
private fun buildIfNotFound(type: Type, typeIdentifier: TypeIdentifier, isOpaque: Boolean): LocalTypeInformation {
|
||||||
|
val rawType = type.asClass()
|
||||||
|
return when (typeIdentifier) {
|
||||||
|
is TypeIdentifier.TopType -> LocalTypeInformation.Top
|
||||||
|
is TypeIdentifier.UnknownType -> LocalTypeInformation.Unknown
|
||||||
|
is TypeIdentifier.Unparameterised,
|
||||||
|
is TypeIdentifier.Erased -> buildForClass(rawType, typeIdentifier, isOpaque)
|
||||||
|
is TypeIdentifier.ArrayOf -> {
|
||||||
|
LocalTypeInformation.AnArray(
|
||||||
|
type,
|
||||||
|
typeIdentifier,
|
||||||
|
resolveAndBuild(type.componentType()))
|
||||||
|
}
|
||||||
|
is TypeIdentifier.Parameterised -> buildForParameterised(rawType, type as ParameterizedType, typeIdentifier, isOpaque)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildForClass(type: Class<*>, typeIdentifier: TypeIdentifier, isOpaque: Boolean): LocalTypeInformation = withContext(type) {
|
||||||
|
when {
|
||||||
|
Collection::class.java.isAssignableFrom(type) &&
|
||||||
|
!EnumSet::class.java.isAssignableFrom(type) -> LocalTypeInformation.ACollection(type, typeIdentifier, LocalTypeInformation.Unknown)
|
||||||
|
Map::class.java.isAssignableFrom(type) -> LocalTypeInformation.AMap(type, typeIdentifier, LocalTypeInformation.Unknown, LocalTypeInformation.Unknown)
|
||||||
|
type.kotlin.javaPrimitiveType != null -> LocalTypeInformation.Atomic(type.kotlin.javaPrimitiveType!!, typeIdentifier)
|
||||||
|
type.isEnum -> LocalTypeInformation.AnEnum(
|
||||||
|
type,
|
||||||
|
typeIdentifier,
|
||||||
|
type.enumConstants.map { it.toString() },
|
||||||
|
buildInterfaceInformation(type))
|
||||||
|
type.kotlinObjectInstance != null -> LocalTypeInformation.Singleton(
|
||||||
|
type,
|
||||||
|
typeIdentifier,
|
||||||
|
buildSuperclassInformation(type),
|
||||||
|
buildInterfaceInformation(type))
|
||||||
|
type.isInterface -> buildInterface(type, typeIdentifier, emptyList())
|
||||||
|
type.isAbstractClass -> buildAbstract(type, typeIdentifier, emptyList())
|
||||||
|
else -> when {
|
||||||
|
isOpaque -> LocalTypeInformation.Opaque(type, typeIdentifier) {
|
||||||
|
buildNonAtomic(type, type, typeIdentifier, emptyList())
|
||||||
|
}
|
||||||
|
else -> buildNonAtomic(type, type, typeIdentifier, emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildForParameterised(
|
||||||
|
rawType: Class<*>,
|
||||||
|
type: ParameterizedType,
|
||||||
|
typeIdentifier: TypeIdentifier.Parameterised,
|
||||||
|
isOpaque: Boolean): LocalTypeInformation = withContext(type) {
|
||||||
|
when {
|
||||||
|
Collection::class.java.isAssignableFrom(rawType) &&
|
||||||
|
!EnumSet::class.java.isAssignableFrom(rawType) ->
|
||||||
|
LocalTypeInformation.ACollection(type, typeIdentifier, buildTypeParameterInformation(type)[0])
|
||||||
|
Map::class.java.isAssignableFrom(rawType) -> {
|
||||||
|
val (keyType, valueType) = buildTypeParameterInformation(type)
|
||||||
|
LocalTypeInformation.AMap(type, typeIdentifier, keyType, valueType)
|
||||||
|
}
|
||||||
|
rawType.isInterface -> buildInterface(type, typeIdentifier, buildTypeParameterInformation(type))
|
||||||
|
rawType.isAbstractClass -> buildAbstract(type, typeIdentifier, buildTypeParameterInformation(type))
|
||||||
|
else -> when {
|
||||||
|
isOpaque -> LocalTypeInformation.Opaque(rawType, typeIdentifier) {
|
||||||
|
buildNonAtomic(rawType, type, typeIdentifier, buildTypeParameterInformation(type))
|
||||||
|
}
|
||||||
|
else -> buildNonAtomic(rawType, type, typeIdentifier, buildTypeParameterInformation(type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildAbstract(type: Type, typeIdentifier: TypeIdentifier,
|
||||||
|
typeParameters: List<LocalTypeInformation>): LocalTypeInformation.Abstract =
|
||||||
|
LocalTypeInformation.Abstract(
|
||||||
|
type,
|
||||||
|
typeIdentifier,
|
||||||
|
buildReadOnlyProperties(type.asClass()),
|
||||||
|
buildSuperclassInformation(type),
|
||||||
|
buildInterfaceInformation(type),
|
||||||
|
typeParameters)
|
||||||
|
|
||||||
|
private fun buildInterface(type: Type, typeIdentifier: TypeIdentifier,
|
||||||
|
typeParameters: List<LocalTypeInformation>): LocalTypeInformation.AnInterface =
|
||||||
|
LocalTypeInformation.AnInterface(
|
||||||
|
type,
|
||||||
|
typeIdentifier,
|
||||||
|
buildReadOnlyProperties(type.asClass()),
|
||||||
|
buildInterfaceInformation(type),
|
||||||
|
typeParameters)
|
||||||
|
|
||||||
|
private inline fun <T> withContext(newContext: Type, block: LocalTypeInformationBuilder.() -> T): T =
|
||||||
|
copy(resolutionContext = newContext).run(block)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a non-atomic type, which is either [Composable] or [NonComposable].
|
||||||
|
*
|
||||||
|
* Composability is a transitive property: a type is [Composable] iff it has a unique deserialization constructor _and_
|
||||||
|
* all of its property types are also [Composable]. If not, the type is [NonComposable], meaning we cannot deserialize
|
||||||
|
* it without a custom serializer (in which case it should normally have been flagged as [Opaque]).
|
||||||
|
*
|
||||||
|
* Rather than throwing an exception if a type is [NonComposable], we capture its type information so that it can
|
||||||
|
* still be used to _serialize_ values, or as the basis for deciding on an evolution strategy.
|
||||||
|
*/
|
||||||
|
private fun buildNonAtomic(rawType: Class<*>, type: Type, typeIdentifier: TypeIdentifier, typeParameterInformation: List<LocalTypeInformation>): LocalTypeInformation {
|
||||||
|
val superclassInformation = buildSuperclassInformation(type)
|
||||||
|
val interfaceInformation = buildInterfaceInformation(type)
|
||||||
|
val observedConstructor = constructorForDeserialization(type)
|
||||||
|
|
||||||
|
if (observedConstructor == null) {
|
||||||
|
logger.warn("No unique deserialisation constructor found for class $rawType, type is marked as non-composable")
|
||||||
|
return LocalTypeInformation.NonComposable(type, typeIdentifier, null, buildReadOnlyProperties(rawType),
|
||||||
|
superclassInformation, interfaceInformation, typeParameterInformation)
|
||||||
|
}
|
||||||
|
|
||||||
|
val constructorInformation = buildConstructorInformation(type, observedConstructor)
|
||||||
|
val properties = buildObjectProperties(rawType, constructorInformation)
|
||||||
|
|
||||||
|
val hasNonComposableProperties = properties.values.any { it.type is LocalTypeInformation.NonComposable }
|
||||||
|
|
||||||
|
if (!propertiesSatisfyConstructor(constructorInformation, properties) || hasNonComposableProperties) {
|
||||||
|
if (hasNonComposableProperties) {
|
||||||
|
logger.warn("Type ${type.typeName} has non-composable properties and has been marked as non-composable")
|
||||||
|
} else {
|
||||||
|
logger.warn("Properties of type ${type.typeName} do not satisfy its constructor, type has been marked as non-composable")
|
||||||
|
}
|
||||||
|
return LocalTypeInformation.NonComposable(type, typeIdentifier, constructorInformation, properties, superclassInformation,
|
||||||
|
interfaceInformation, typeParameterInformation)
|
||||||
|
}
|
||||||
|
|
||||||
|
val evolverConstructors = evolverConstructors(type).map { ctor ->
|
||||||
|
val constructorInformation = buildConstructorInformation(type, ctor)
|
||||||
|
val evolverProperties = buildObjectProperties(rawType, constructorInformation)
|
||||||
|
EvolverConstructorInformation(constructorInformation, evolverProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
return LocalTypeInformation.Composable(type, typeIdentifier, constructorInformation, evolverConstructors, properties,
|
||||||
|
superclassInformation, interfaceInformation, typeParameterInformation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can we supply all of the mandatory constructor parameters using values addressed by readable properties?
|
||||||
|
private fun propertiesSatisfyConstructor(constructorInformation: LocalConstructorInformation, properties: Map<PropertyName, LocalPropertyInformation>): Boolean {
|
||||||
|
if (!constructorInformation.hasParameters) return true
|
||||||
|
|
||||||
|
val indicesAddressedByProperties = properties.values.asSequence().mapNotNull {
|
||||||
|
when (it) {
|
||||||
|
is LocalPropertyInformation.ConstructorPairedProperty -> it.constructorSlot.parameterIndex
|
||||||
|
is LocalPropertyInformation.PrivateConstructorPairedProperty -> it.constructorSlot.parameterIndex
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}.toSet()
|
||||||
|
|
||||||
|
return (0 until constructorInformation.parameters.size).none { index ->
|
||||||
|
constructorInformation.parameters[index].isMandatory && index !in indicesAddressedByProperties
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSuperclassInformation(type: Type): LocalTypeInformation =
|
||||||
|
resolveAndBuild(type.asClass().genericSuperclass)
|
||||||
|
|
||||||
|
private fun buildInterfaceInformation(type: Type) =
|
||||||
|
type.allInterfaces.asSequence().mapNotNull {
|
||||||
|
if (it == type) return@mapNotNull null
|
||||||
|
resolveAndBuild(it)
|
||||||
|
}.toList()
|
||||||
|
|
||||||
|
private val Type.allInterfaces: Set<Type> get() = exploreType(this)
|
||||||
|
|
||||||
|
private fun exploreType(type: Type, interfaces: MutableSet<Type> = LinkedHashSet()): MutableSet<Type> {
|
||||||
|
val clazz = type.asClass()
|
||||||
|
|
||||||
|
if (clazz.isInterface) {
|
||||||
|
// Ignore classes we've already seen, and stop exploring once we reach an excluded type.
|
||||||
|
if (clazz in interfaces || lookup.isExcluded(clazz)) return interfaces
|
||||||
|
else interfaces += type
|
||||||
|
}
|
||||||
|
|
||||||
|
clazz.genericInterfaces.forEach { exploreType(it.resolveAgainstContext(), interfaces) }
|
||||||
|
if (clazz.genericSuperclass != null) exploreType(clazz.genericSuperclass.resolveAgainstContext(), interfaces)
|
||||||
|
|
||||||
|
return interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildReadOnlyProperties(rawType: Class<*>): Map<PropertyName, LocalPropertyInformation> =
|
||||||
|
rawType.propertyDescriptors().asSequence().mapNotNull { (name, descriptor) ->
|
||||||
|
if (descriptor.field == null || descriptor.getter == null) null
|
||||||
|
else {
|
||||||
|
val paramType = (descriptor.getter.genericReturnType).resolveAgainstContext()
|
||||||
|
val paramTypeInformation = build(paramType, TypeIdentifier.forGenericType(paramType, resolutionContext
|
||||||
|
?: rawType))
|
||||||
|
val isMandatory = paramType.asClass().isPrimitive || !descriptor.getter.returnsNullable()
|
||||||
|
name to LocalPropertyInformation.ReadOnlyProperty(descriptor.getter, paramTypeInformation, isMandatory)
|
||||||
|
}
|
||||||
|
}.sortedBy { (name, _) -> name }.toMap(LinkedHashMap())
|
||||||
|
|
||||||
|
private fun buildObjectProperties(rawType: Class<*>, constructorInformation: LocalConstructorInformation): Map<PropertyName, LocalPropertyInformation> =
|
||||||
|
(calculatedProperties(rawType) + nonCalculatedProperties(rawType, constructorInformation))
|
||||||
|
.sortedBy { (name, _) -> name }
|
||||||
|
.toMap(LinkedHashMap())
|
||||||
|
|
||||||
|
private fun nonCalculatedProperties(rawType: Class<*>, constructorInformation: LocalConstructorInformation): Sequence<Pair<String, LocalPropertyInformation>> =
|
||||||
|
if (constructorInformation.hasParameters) getConstructorPairedProperties(constructorInformation, rawType)
|
||||||
|
else getterSetterProperties(rawType)
|
||||||
|
|
||||||
|
private fun getConstructorPairedProperties(constructorInformation: LocalConstructorInformation, rawType: Class<*>): Sequence<Pair<String, LocalPropertyInformation>> {
|
||||||
|
val constructorParameterIndices = constructorInformation.parameters.asSequence().mapIndexed { index, parameter ->
|
||||||
|
parameter.name to index
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
return rawType.propertyDescriptors().asSequence().mapNotNull { (name, descriptor) ->
|
||||||
|
val property = makeConstructorPairedProperty(constructorParameterIndices, name, descriptor, constructorInformation)
|
||||||
|
if (property == null) null else name to property
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeConstructorPairedProperty(constructorParameterIndices: Map<String, Int>,
|
||||||
|
name: String,
|
||||||
|
descriptor: PropertyDescriptor,
|
||||||
|
constructorInformation: LocalConstructorInformation): LocalPropertyInformation? {
|
||||||
|
val constructorIndex = constructorParameterIndices[name] ?:
|
||||||
|
// In some very rare cases we have a constructor parameter matched by a getter with no backing field,
|
||||||
|
// and cannot infer whether the property name should be capitalised or not.
|
||||||
|
constructorParameterIndices[name.decapitalize()] ?: return null
|
||||||
|
|
||||||
|
if (descriptor.getter == null) {
|
||||||
|
if (descriptor.field == null) return null
|
||||||
|
val paramType = descriptor.field.genericType
|
||||||
|
val paramTypeInformation = resolveAndBuild(paramType)
|
||||||
|
|
||||||
|
return LocalPropertyInformation.PrivateConstructorPairedProperty(
|
||||||
|
descriptor.field,
|
||||||
|
ConstructorSlot(constructorIndex, constructorInformation),
|
||||||
|
paramTypeInformation,
|
||||||
|
constructorInformation.parameters[constructorIndex].isMandatory)
|
||||||
|
}
|
||||||
|
|
||||||
|
val paramType = descriptor.getter.genericReturnType
|
||||||
|
val paramTypeInformation = resolveAndBuild(paramType)
|
||||||
|
|
||||||
|
return LocalPropertyInformation.ConstructorPairedProperty(
|
||||||
|
descriptor.getter,
|
||||||
|
ConstructorSlot(constructorIndex, constructorInformation),
|
||||||
|
paramTypeInformation,
|
||||||
|
descriptor.getter.returnType.isPrimitive ||
|
||||||
|
!descriptor.getter.returnsNullable())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getterSetterProperties(rawType: Class<*>): Sequence<Pair<String, LocalPropertyInformation>> =
|
||||||
|
rawType.propertyDescriptors().asSequence().mapNotNull { (name, descriptor) ->
|
||||||
|
if (descriptor.getter == null || descriptor.setter == null || descriptor.field == null) null
|
||||||
|
else {
|
||||||
|
val paramType = descriptor.getter.genericReturnType
|
||||||
|
val paramTypeInformation = resolveAndBuild(paramType)
|
||||||
|
val isMandatory = paramType.asClass().isPrimitive || !descriptor.getter.returnsNullable()
|
||||||
|
|
||||||
|
name to LocalPropertyInformation.GetterSetterProperty(
|
||||||
|
descriptor.getter,
|
||||||
|
descriptor.setter,
|
||||||
|
paramTypeInformation,
|
||||||
|
isMandatory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculatedProperties(rawType: Class<*>): Sequence<Pair<String, LocalPropertyInformation>> =
|
||||||
|
rawType.calculatedPropertyDescriptors().asSequence().map { (name, v) ->
|
||||||
|
val paramType = v.getter!!.genericReturnType
|
||||||
|
val paramTypeInformation = resolveAndBuild(paramType)
|
||||||
|
val isMandatory = paramType.asClass().isPrimitive || !v.getter.returnsNullable()
|
||||||
|
|
||||||
|
name to LocalPropertyInformation.CalculatedProperty(v.getter, paramTypeInformation, isMandatory)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildTypeParameterInformation(type: ParameterizedType): List<LocalTypeInformation> =
|
||||||
|
type.actualTypeArguments.map {
|
||||||
|
resolveAndBuild(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildConstructorInformation(type: Type, observedConstructor: KFunction<Any>): LocalConstructorInformation {
|
||||||
|
if (observedConstructor.javaConstructor?.parameters?.getOrNull(0)?.name == "this$0")
|
||||||
|
throw NotSerializableException("Type '${type.typeName} has synthetic fields and is likely a nested inner class.")
|
||||||
|
|
||||||
|
return LocalConstructorInformation(observedConstructor, observedConstructor.parameters.map {
|
||||||
|
val parameterType = it.type.javaType
|
||||||
|
LocalConstructorParameterInformation(
|
||||||
|
it.name ?: throw IllegalStateException("Unnamed parameter in constructor $observedConstructor"),
|
||||||
|
resolveAndBuild(parameterType),
|
||||||
|
parameterType.asClass().isPrimitive || !it.type.isMarkedNullable)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Method.returnsNullable(): Boolean = try {
|
||||||
|
val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull {
|
||||||
|
it.javaGetter == this
|
||||||
|
}?.returnType?.toString() ?: "?"
|
||||||
|
|
||||||
|
returnTypeString.endsWith('?') || returnTypeString.endsWith('!')
|
||||||
|
} catch (e: KotlinReflectionInternalError) {
|
||||||
|
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue
|
||||||
|
// is: https://youtrack.jetbrains.com/issue/KT-13077
|
||||||
|
// TODO: Revisit this when Kotlin issue is fixed.
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code for finding the unique constructor we will use for deserialization.
|
||||||
|
*
|
||||||
|
* If any constructor is uniquely annotated with [@ConstructorForDeserialization], then that constructor is chosen.
|
||||||
|
* An error is reported if more than one constructor is annotated.
|
||||||
|
*
|
||||||
|
* Otherwise, if there is a Kotlin primary constructor, it selects that, and if not it selects either the unique
|
||||||
|
* constructor or, if there are two and one is the default no-argument constructor, the non-default constructor.
|
||||||
|
*/
|
||||||
|
private fun constructorForDeserialization(type: Type): KFunction<Any>? {
|
||||||
|
val clazz = type.asClass()
|
||||||
|
if (!clazz.isConcreteClass || clazz.isSynthetic) return null
|
||||||
|
|
||||||
|
val kotlinCtors = clazz.kotlin.constructors
|
||||||
|
|
||||||
|
val annotatedCtors = kotlinCtors.filter { it.findAnnotation<ConstructorForDeserialization>() != null }
|
||||||
|
if (annotatedCtors.size > 1) return null
|
||||||
|
if (annotatedCtors.size == 1) return annotatedCtors.first().apply { isAccessible = true }
|
||||||
|
|
||||||
|
val defaultCtor = kotlinCtors.firstOrNull { it.parameters.isEmpty() }
|
||||||
|
val nonDefaultCtors = kotlinCtors.filter { it != defaultCtor }
|
||||||
|
|
||||||
|
val preferredCandidate = clazz.kotlin.primaryConstructor ?:
|
||||||
|
when(nonDefaultCtors.size) {
|
||||||
|
1 -> nonDefaultCtors.first()
|
||||||
|
0 -> defaultCtor
|
||||||
|
else -> null
|
||||||
|
} ?: return null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
preferredCandidate.apply { isAccessible = true }
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun evolverConstructors(type: Type): List<KFunction<Any>> {
|
||||||
|
val clazz = type.asClass()
|
||||||
|
if (!clazz.isConcreteClass || clazz.isSynthetic) return emptyList()
|
||||||
|
|
||||||
|
return clazz.kotlin.constructors.asSequence()
|
||||||
|
.mapNotNull {
|
||||||
|
val version = it.findAnnotation<DeprecatedConstructorForDeserialization>()?.version
|
||||||
|
if (version == null) null else version to it
|
||||||
|
}
|
||||||
|
.sortedBy { (version, ctor) -> version }
|
||||||
|
.map { (version, ctor) -> ctor.apply { isAccessible = true} }
|
||||||
|
.toList()
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user