Merge pull request #1575 from corda/tudor-os-merge-19-11

Tudor os merge 19 11
This commit is contained in:
Tudor Malene 2018-11-20 12:57:23 +00:00 committed by GitHub
commit a3a9be6594
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
114 changed files with 3827 additions and 2215 deletions

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
package net.corda.client.jackson.internal
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.core.JsonGenerator
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.toHexString
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.security.PublicKey
import java.security.cert.CertPath
@ -327,11 +330,11 @@ private class PartialTreeJson(val includedLeaf: SecureHash? = null,
val right: PartialTreeJson? = null) {
init {
if (includedLeaf != null) {
require(leaf == null && left == null && right == null)
require(leaf == null && left == null && right == null) { "Invalid JSON structure" }
} else if (leaf != null) {
require(left == null && right == null)
require(left == null && right == null) { "Invalid JSON structure" }
} else {
require(left != null && right != null)
require(left != null && right != null) { "Invalid JSON structure" }
}
}
}

View File

@ -202,7 +202,7 @@ class NodeMonitorModel : AutoCloseable {
val _connection = client.start(username, password)
// Check connection is truly operational before returning it.
val nodeInfo = _connection.proxy.nodeInfo()
require(nodeInfo.legalIdentitiesAndCerts.isNotEmpty())
require(nodeInfo.legalIdentitiesAndCerts.isNotEmpty()){"No identity certificates found"}
_connection
} catch (exception: Exception) {
if (shouldRetry) {

View File

@ -126,7 +126,7 @@ interface Validated<TARGET, ERROR> {
*/
class Unsuccessful<TARGET, ERROR>(override val errors: Set<ERROR>) : Result<TARGET, ERROR>(), Validated<TARGET, ERROR> {
init {
require(errors.isNotEmpty())
require(errors.isNotEmpty()) { "No errors encountered during validation" }
}
override fun value(exceptionOnErrors: (Set<ERROR>) -> Exception) = throw exceptionOnErrors.invoke(errors)

View File

@ -40,14 +40,14 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
fun getInstance(asn1: ASN1Primitive): PublicKey {
val keyInfo = SubjectPublicKeyInfo.getInstance(asn1)
require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.COMPOSITE_KEY)
require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.COMPOSITE_KEY) { "Key must be composite" }
val sequence = ASN1Sequence.getInstance(keyInfo.parsePublicKey())
val threshold = ASN1Integer.getInstance(sequence.getObjectAt(0)).positiveValue.toInt()
val sequenceOfChildren = ASN1Sequence.getInstance(sequence.getObjectAt(1))
val builder = Builder()
val listOfChildren = sequenceOfChildren.objects.toList()
listOfChildren.forEach { childAsn1 ->
require(childAsn1 is ASN1Sequence)
require(childAsn1 is ASN1Sequence) { "Child key is not in ASN1 format" }
val childSeq = childAsn1 as ASN1Sequence
val key = Crypto.decodePublicKey((childSeq.getObjectAt(0) as DERBitString).bytes)
val weight = ASN1Integer.getInstance(childSeq.getObjectAt(1))
@ -274,7 +274,7 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
* is invalid (for example it would contain no keys).
*/
fun build(threshold: Int? = null): PublicKey {
require(threshold == null || threshold > 0)
require(threshold == null || threshold > 0) { "Threshold must not be specified or its value must be greater than zero" }
val n = children.size
return when {
n > 1 -> CompositeKey(threshold ?: children.map { (_, weight) -> weight }.sum(), children)

View File

@ -21,7 +21,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
/** SHA-256 is part of the SHA-2 hash function family. Generated hash is fixed size, 256-bits (32-bytes). */
class SHA256(bytes: ByteArray) : SecureHash(bytes) {
init {
require(bytes.size == 32)
require(bytes.size == 32) { "Invalid hash size, must be 32 bytes" }
}
}

View File

@ -45,7 +45,7 @@ val cordaBouncyCastleProvider = BouncyCastleProvider().apply {
Security.addProvider(it)
}
val bouncyCastlePQCProvider = BouncyCastlePQCProvider().apply {
require(name == "BCPQC") // The constant it comes from is not final.
require(name == "BCPQC") { "Invalid PQCProvider name" }
}.also {
Security.addProvider(it)
}

View File

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

View File

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

View File

@ -5,7 +5,10 @@ package net.corda.core.internal
import net.corda.core.DeleteForDJVM
import net.corda.core.KeepForDJVM
import net.corda.core.crypto.*
import net.corda.core.serialization.*
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.UntrustworthyData
import org.slf4j.Logger
@ -109,7 +112,7 @@ fun <T> List<T>.randomOrNull(): T? {
/** Returns the index of the given item or throws [IllegalArgumentException] if not found. */
fun <T> List<T>.indexOfOrThrow(item: T): Int {
val i = indexOf(item)
require(i != -1)
require(i != -1){"No such element"}
return i
}
@ -218,7 +221,8 @@ data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHa
* Note that a slightly bigger than numOfExpectedBytes size is expected.
*/
@DeleteForDJVM
fun createInMemoryTestZip(numOfExpectedBytes: Int, content: Byte): InputStreamAndHash {
fun createInMemoryTestZip(numOfExpectedBytes: Int, content: Byte, entryName: String = "z"): InputStreamAndHash {
require(numOfExpectedBytes > 0){"Expected bytes must be greater than zero"}
require(numOfExpectedBytes > 0)
val baos = ByteArrayOutputStream()
ZipOutputStream(baos).use { zos ->
@ -226,7 +230,7 @@ data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHa
val bytes = ByteArray(arraySize) { content }
val n = (numOfExpectedBytes - 1) / arraySize + 1 // same as Math.ceil(numOfExpectedBytes/arraySize).
zos.setLevel(Deflater.NO_COMPRESSION)
zos.putNextEntry(ZipEntry("z"))
zos.putNextEntry(ZipEntry(entryName))
for (i in 0 until n) {
zos.write(bytes, 0, arraySize)
}
@ -498,3 +502,18 @@ fun <T : Any> SerializedBytes<Any>.checkPayloadIs(type: Class<T>): Untrustworthy
return type.castIfPossible(payloadData)?.let { UntrustworthyData(it) }
?: throw IllegalArgumentException("We were expecting a ${type.name} but we instead got a ${payloadData.javaClass.name} ($payloadData)")
}
/**
* Simple Map structure that can be used as a cache in the DJVM.
*/
fun <K, V> createSimpleCache(maxSize: Int, onEject: (MutableMap.MutableEntry<K, V>) -> Unit = {}): MutableMap<K, V> {
return object : LinkedHashMap<K, V>() {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>?): Boolean {
val eject = size > maxSize
if (eject) onEject(eldest!!)
return eject
}
}
}
fun <K,V> MutableMap<K,V>.toSynchronised(): MutableMap<K,V> = Collections.synchronizedMap(this)

View File

@ -14,8 +14,8 @@ interface NamedCacheFactory {
* the name can be used to create a file name or a metric name.
*/
fun checkCacheName(name: String) {
require(!name.isBlank())
require(allowedChars.matches(name))
require(!name.isBlank()){"Name must not be empty or only whitespace"}
require(allowedChars.matches(name)){"Invalid characters in cache name"}
}
fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V>

View File

@ -1,4 +1,4 @@
package net.corda.nodeapi.internal
package net.corda.core.internal
// TODO: Add to Corda node.conf to allow customisation
const val NODE_INFO_DIRECTORY = "additional-node-infos"

View File

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

View File

@ -9,7 +9,7 @@ class AddressBindingException(val addresses: Set<NetworkHostAndPort>) : CordaRun
private companion object {
private fun message(addresses: Set<NetworkHostAndPort>): String {
require(addresses.isNotEmpty())
require(addresses.isNotEmpty()) { "Address list must not be empty" }
return if (addresses.size > 1) {
"Failed to bind on an address in ${addresses.joinToString(", ", "[", "]")}."
} else {

View File

@ -177,10 +177,10 @@ interface SerializationContext {
fun withClassLoader(classLoader: ClassLoader): SerializationContext
/**
* Helper method to return a new context based on this context with the appropriate class loader constructed from the passed attachment identifiers.
* (Requires the attachment storage to have been enabled).
* Does not do anything.
*/
@Throws(MissingAttachmentsException::class)
@Deprecated("There is no reason to call this. This method does not actually do anything.")
fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): SerializationContext
/**
@ -300,13 +300,8 @@ class SerializedBytes<T : Any>(bytes: ByteArray) : OpaqueBytes(bytes) {
/**
* Serializes the given object and returns a [SerializedBytes] wrapper for it. An alias for [Any.serialize]
* intended to make the calling smoother for Java users.
*
* TODO: Take out the @CordaInternal annotation post-Enterprise GA when we can add API again.
*
* @suppress
*/
@JvmStatic
@CordaInternal
@JvmOverloads
fun <T : Any> from(obj: T, serializationFactory: SerializationFactory = SerializationFactory.defaultFactory,
context: SerializationContext = serializationFactory.defaultContext): SerializedBytes<T> {

View File

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

View File

@ -73,13 +73,6 @@ interface CheckpointSerializationContext {
*/
fun withClassLoader(classLoader: ClassLoader): CheckpointSerializationContext
/**
* 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.
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -68,8 +68,8 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si
* This method cannot be used to get bytes before [offset] or after [offset]+[size], and never makes a new array.
*/
fun slice(start: Int = 0, end: Int = size): ByteBuffer {
require(start >= 0)
require(end >= 0)
require(start >= 0) { "Starting index must be greater than or equal to 0" }
require(end >= 0){"End index must be greater or equal to 0"}
val clampedStart = min(start, size)
val clampedEnd = min(end, size)
return ByteBuffer.wrap(_bytes, offset + clampedStart, max(0, clampedEnd - clampedStart)).asReadOnlyBuffer()
@ -155,7 +155,7 @@ open class OpaqueBytes(bytes: ByteArray) : ByteSequence(bytes, 0, bytes.size) {
}
init {
require(bytes.isNotEmpty())
require(bytes.isNotEmpty()) { "Byte Array must not be empty" }
}
/**
@ -193,7 +193,7 @@ fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(this)
@KeepForDJVM
class OpaqueBytesSubSequence(override val bytes: ByteArray, offset: Int, size: Int) : ByteSequence(bytes, offset, size) {
init {
require(offset >= 0 && offset < bytes.size)
require(size >= 0 && offset + size <= bytes.size)
require(offset >= 0 && offset < bytes.size) { "Offset must be greater than or equal to 0, and less than the size of the backing array" }
require(size >= 0 && offset + size <= bytes.size) { "Sub-sequence size must be greater than or equal to 0, and less than the size of the backing array" }
}
}

View File

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

View File

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

View File

@ -0,0 +1,98 @@
package net.corda.core.transactions
import net.corda.core.contracts.Contract
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.declaredField
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
import net.corda.core.serialization.serialize
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.OpaqueBytes
import net.corda.nodeapi.DummyContractBackdoor
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.fakeAttachment
import net.corda.testing.services.MockAttachmentStorage
import org.apache.commons.io.IOUtils
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import java.io.NotSerializableException
import java.net.URL
import kotlin.test.assertFailsWith
class AttachmentsClassLoaderSerializationTests {
companion object {
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderSerializationTests::class.java.getResource("isolated.jar")
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
val storage = MockAttachmentStorage()
@Test
fun `Can serialize and deserialize with an attachment classloader`() {
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
val isolatedId = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
val att2 = storage.importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
val serialisedState = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! }) { classLoader ->
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader)
val contract = contractClass.newInstance() as Contract
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
val txt = IOUtils.toString(classLoader.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
assertEquals("some data", txt)
val state = (contract as DummyContractBackdoor).generateInitial(MEGA_CORP.ref(1), 1, DUMMY_NOTARY).outputStates().first()
val serialisedState = state.serialize()
val state1 = serialisedState.deserialize()
assertEquals(state, state1)
serialisedState
}
assertFailsWith<NotSerializableException> {
serialisedState.deserialize()
}
}
// These tests are not Attachment specific. Should they be removed?
@Test
fun `test serialization of SecureHash`() {
val secureHash = SecureHash.randomSHA256()
val bytes = secureHash.serialize()
val copiedSecuredHash = bytes.deserialize()
assertEquals(secureHash, copiedSecuredHash)
}
@Test
fun `test serialization of OpaqueBytes`() {
val opaqueBytes = OpaqueBytes("0123456789".toByteArray())
val bytes = opaqueBytes.serialize()
val copiedOpaqueBytes = bytes.deserialize()
assertEquals(opaqueBytes, copiedOpaqueBytes)
}
@Test
fun `test serialization of sub-sequence OpaqueBytes`() {
val bytesSequence = ByteSequence.of("0123456789".toByteArray(), 3, 2)
val bytes = bytesSequence.serialize()
val copiedBytesSequence = bytes.deserialize()
assertEquals(bytesSequence, copiedBytesSequence)
}
}

View File

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

View File

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

View File

@ -7,6 +7,11 @@ release, see :doc:`upgrade-notes`.
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)
to register/unregister a java package namespace with an associated owner in the network parameter packageOwnership whitelist.

View File

@ -48,8 +48,10 @@ Here are the contents of the ``reference.conf`` file for Corda Enterprise:
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
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.
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.

View File

@ -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
``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
@ -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
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
* Each Corda compile dependency (eg ``net.corda:corda-core:$corda_release_version``) as a ``cordaCompile`` dependency
* When building a CorDapp, you should always include ``net.corda:corda-core:$corda_release_version`` as a
``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
* ``net.corda:corda-webserver:$corda_release_version`` as a ``cordaRuntime`` dependency, in order to use Corda's
built-in development webserver
.. warning:: Never include ``corda-test-utils`` as a ``compile`` or ``cordaCompile`` dependency.
* ``corda-node-api`` - The node API. Required to bootstrap a local network
* ``corda-node-driver`` - Testing utility for programmatically starting nodes from JVM languages. Use for tests
* ``corda-notary-bft-smart`` - A Corda notary implementation
* ``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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -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
without need to create a keystore and configure the ``cordapp`` plugin.
For production deployment ensure to sign the CorDapp using your own certificate e.g. by setting system properties to point to an external keystore
or by disabling signing in ``cordapp`` plugin and signing the CordDapp JAR downstream in your build pipeline.
CorDapp signed by Corda development certificate is accepted by Corda node only when running in the development mode.
In case CordDapp signed by the (default) development key is run on node in the production mode (e.g. for testing),
the node may be set to accept the development key by adding the ``cordappSignerKeyFingerprintBlacklist = []`` property set to empty list
(see :ref:`Configuring a node <corda_configuration_file_signer_blacklist>`).
CorDapp auto-signing allows to use signature constraints for contracts from the CorDapp without need to create a
keystore and configure the ``cordapp`` plugin. For production deployment ensure to sign the CorDapp using your own
certificate e.g. by setting system properties to point to an external keystore or by disabling signing in ``cordapp``
plugin and signing the CordDapp JAR downstream in your build pipeline. CorDapp signed by Corda development certificate
is accepted by Corda node only when running in the development mode. In case CordDapp signed by the (default)
development key is run on node in the production mode (e.g. for testing), the node may be set to accept the development
key by adding the ``cordappSignerKeyFingerprintBlacklist = []`` property set to empty list (see
:ref:`Configuring a node <corda_configuration_file_signer_blacklist>`).
Signing options can be contextually overwritten by the relevant system properties as described above.
This allows the single ``build.gradle`` file to be used for a development build (defaulting to the Corda development keystore)
and for a production build (using an external keystore).
The example system properties setup for the build process which overrides signing options:
Signing options can be contextually overwritten by the relevant system properties as described above. This allows the
single ``build.gradle`` file to be used for a development build (defaulting to the Corda development keystore) and for
a production build (using an external keystore). The example system properties setup for the build process which
overrides signing options:
.. sourcecode:: shell
@ -187,8 +227,9 @@ CorDapp signing can be disabled for a build:
./gradlew -Dsigning.enabled=false
Other system properties can be explicitly assigned to options by calling ``System.getProperty`` in ``cordapp`` plugin configuration.
For example the below configuration sets the specific signing algorithm when a system property is available otherwise defaults to an empty string:
Other system properties can be explicitly assigned to options by calling ``System.getProperty`` in ``cordapp`` plugin
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
@ -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
@ -216,8 +258,8 @@ Cordformation plugin can also sign CorDapps JARs, when deploying set of nodes, s
Example
^^^^^^^
Below is a sample of what a CorDapp's Gradle dependencies block might look like. When building your own CorDapp, you should
base yourself on the ``build.gradle`` file of the
Below is a sample of what a CorDapp's Gradle dependencies block might look like. When building your own CorDapp, you
should base yourself on the ``build.gradle`` file of the
`Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_ or the
`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

View File

@ -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:
* **Notary**, which hosts a validating notary service
* **Notary**, which runs a notary service
* **PartyA**
* **PartyB**
* **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>`
* Clone the example CorDapp from the `cordapp-example repository <https://github.com/corda/cordapp-example>`_ using
the following command: ``git clone https://github.com/corda/cordapp-example``
* Clone the samples repository from using the following command: ``git clone https://github.com/corda/samples``
* 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
---------------------------------------
@ -41,7 +40,7 @@ Let's open the example CorDapp in IntelliJ IDEA:
* 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
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
│   └── test
│   └── log4j2.xml
├── doc
│   └── example_flow.plantuml
├── gradle
│   └── wrapper
│   ├── gradle-wrapper.jar
│   └── gradle-wrapper.properties
├── lib
│   ├── README.txt
│   └── quasar.jar
├── java-source
│   └── ...
├── kotlin-source
│   ├── build.gradle
│   └── src
│   ├── integrationTest
│   │   └── java
│   │   └── com
│   │   └── example
│   │   └── DriverBasedTests.java
│   ├── main
│   │   ├── kotlin
│   │   ├── java
│   │   │   └── com
│   │   │   └── example
│   │   │   ├── api
│   │   │   │   └── ExampleApi.kt
│   │   │   │   └── ExampleApi.java
│   │   │   ├── client
│   │   │   │   └── ExampleClientRPC.kt
│   │   │   │   └── ExampleClientRPC.java
│   │   │   ├── contract
│   │   │   │   └── IOUContract.kt
│   │   │   │   └── IOUContract.java
│   │   │   ├── flow
│   │   │   │   └── ExampleFlow.kt
│   │   │   ├── model
│   │   │   │   └── IOU.kt
│   │   │   │   └── ExampleFlow.java
│   │   │   ├── plugin
│   │   │   │   └── ExamplePlugin.kt
│   │   │   │   └── ExamplePlugin.java
│   │   │   ├── schema
│   │   │   │   └── IOUSchema.kt
│   │   │   │   ├── IOUSchema.java
│   │   │   │   └── IOUSchemaV1.java
│   │   │   └── state
│   │   │   └── IOUState.kt
│   │   │   └── IOUState.java
│   │   └── resources
│   │   ├── META-INF
│   │   │   └── services
│   │   │   └── net.corda.webserver.services.WebServerPluginRegistry
│   │   ├── certificates
│   │   │   ├── readme.txt
│   │   │   ├── sslkeystore.jks
│   │   │   └── truststore.jks
│   │   └── exampleWeb
│   │   ├── index.html
│   │   └── js
│   │   └── angular-module.js
│   └── test
│   └── kotlin
│   └── java
│   └── com
│   └── example
│   ├── Main.kt
│   ├── NodeDriver.java
│   ├── contract
│   │   └── IOUContractTests.kt
│   │   └── IOUContractTests.java
│   └── flow
│   └── IOUFlowTests.kt
│   └── IOUFlowTests.java
├── kotlin-source
│   ├── ...
├── lib
│   ├── README.txt
│   └── quasar.jar
├── .gitignore
├── LICENCE
├── 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
about which version is required
* **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
* **kotlin-source/src/main/resources** contains the certificate store, some static web content to be served by the
* **java-source/src/main/java** contains the source code for the example CorDapp
* **java-source/src/main/resources** contains the certificate store, some static web content to be served by the
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
* **java-source** contains the same source code, but written in Java. CorDapps can be developed in any language
targeting the JVM
* **kotlin-source** contains the same source code, but written in Kotlin. CorDapps can be developed in either Java and Kotlin
Running the example CorDapp
---------------------------

View File

@ -32,14 +32,14 @@ data class PrintedTransitionGraph(val stateClassName: String, val printedPUML: S
* Shorthand for defining transitions directly from the command class
*/
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].
* 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>,
private vararg val ts: TransitionDef<S, R>) {
private vararg val ts: TransitionDef<S, R>) {
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...
inputStates.forEach { inp ->
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 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
if (signer != null) {
// which state determines who is the signer? by default the input, unless it's the initial transition

View File

@ -132,7 +132,8 @@ class FlowWorkerServiceHub(override val configuration: NodeConfiguration,
identityService::wellKnownPartyFromX500Name,
identityService::wellKnownPartyFromAnonymous,
schemaService,
cacheFactory
cacheFactory,
cordappLoader.appClassLoader
)
init {

View File

@ -92,7 +92,8 @@ class RpcWorkerServiceHub(override val configuration: NodeConfiguration,
identityService::wellKnownPartyFromX500Name,
identityService::wellKnownPartyFromAnonymous,
schemaService,
cacheFactory
cacheFactory,
cordappLoader.appClassLoader
)
init {

View File

@ -203,7 +203,7 @@ class UniversalContract : Contract {
val rest = extractRemainder(arr, action)
// for now - let's assume not
require(rest is Zero)
require(rest is Zero) { "Remainder must be zero" }
requireThat {
"action must have a time-window" using (tx.timeWindow != null)

View File

@ -321,7 +321,7 @@ open class BusinessCalendar(val holidayDates: List<LocalDate>) {
* TODO: Make more efficient if necessary
*/
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
var retDate = date
var ctr = 0

View File

@ -39,8 +39,8 @@ private fun rowsToAmount(currency: Currency, rows: Vault.Page<FungibleAsset<*>>)
return if (rows.otherResults.isEmpty()) {
Amount(0L, currency)
} else {
require(rows.otherResults.size == 2)
require(rows.otherResults[1] == currency.currencyCode)
require(rows.otherResults.size == 2){"Invalid number of rows returned by query"}
require(rows.otherResults[1] == currency.currencyCode){"Currency on rows returned by query does not match expected"}
val quantity = rows.otherResults[0] as Long
Amount(quantity, currency)
}

View File

@ -298,7 +298,7 @@ abstract class OnLedgerAsset<T : Any, out C : CommandData, S : FungibleAsset<T>>
issueCommand: CommandData): Set<PublicKey> {
check(tx.inputStates().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 commandSigner = at.party.owningKey
tx.addOutputState(transactionState)

View File

@ -140,8 +140,8 @@ object TwoPartyDealFlow {
// Verify the transaction identities represent the correct parties
val wellKnownOtherParty = serviceHub.identityService.wellKnownPartyFromAnonymous(it.primaryIdentity)
val wellKnownMe = serviceHub.identityService.wellKnownPartyFromAnonymous(it.secondaryIdentity)
require(wellKnownOtherParty == otherSideSession.counterparty)
require(wellKnownMe == ourIdentity)
require(wellKnownOtherParty == otherSideSession.counterparty){"Well known party for handshake identity ${it.primaryIdentity} does not match counterparty"}
require(wellKnownMe == ourIdentity){"Well known party for handshake identity ${it.secondaryIdentity} does not match ourIdentity"}
validateHandshake(it)
}
}

View File

@ -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
// prove the owner is a confidential identity of the counterparty.
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
// identity, so that anonymity is enforced.

View File

@ -9,8 +9,8 @@ data class ScreenCoordinate(val screenX: Double, val screenY: Double)
@CordaSerializable
data class WorldCoordinate(val latitude: Double, val longitude: Double) {
init {
require(latitude in -90..90)
require(longitude in -180..180)
require(latitude in -90..90){"Latitude must be between -90 and +90"}
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.
fun project(screenWidth: Double, screenHeight: Double, topLatitude: Double, bottomLatitude: Double,
leftLongitude: Double, rightLongitude: Double): ScreenCoordinate {
require(latitude in bottomLatitude..topLatitude)
require(longitude in leftLongitude..rightLongitude)
require(latitude in bottomLatitude..topLatitude){"Latitude must be between $bottomLatitude and $topLatitude"}
require(longitude in leftLongitude..rightLongitude){"Longitude must be between $leftLongitude and $rightLongitude"}
fun deg2rad(deg: Double) = deg * Math.PI / 180.0
val leftLngRad = deg2rad(leftLongitude)

View File

@ -25,6 +25,7 @@ import net.corda.testing.contracts.DummyState
import net.corda.testing.core.*
import net.corda.testing.dsl.*
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.vault.CommodityState
import net.corda.testing.node.MockServices
@ -565,7 +566,7 @@ class ObligationTests {
@Test
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 oneUnitFcoj = Amount(1, defaultFcoj)
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)
}
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>
get() = Obligation.Terms(NonEmptySet.of(cashContractBytes.sha256() as SecureHash), NonEmptySet.of(this), TEST_TX_TIME)
private val Amount<Issued<Currency>>.OBLIGATION: Obligation.State<Currency>

View File

@ -47,7 +47,7 @@ object DevIdentityGenerator {
/** Generates a CFT notary identity, where the entire cluster shares a key pair. */
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()}" }
@ -63,7 +63,7 @@ object DevIdentityGenerator {
/** 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 {
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()}" }

View File

@ -254,7 +254,7 @@ object X509Utilities {
crlIssuer: X500Name? = null): X509Certificate {
val builder = createPartialCertificate(certificateType, issuer, issuerPublicKey, subject, subjectPublicKey, validityWindow, nameConstraints, crlDistPoint, crlIssuer)
return builder.build(issuerSigner).run {
require(isValidOn(Date()))
require(isValidOn(Date())){"Certificate is not valid at instant now"}
toJca()
}
}
@ -292,8 +292,8 @@ object X509Utilities {
crlDistPoint,
crlIssuer)
return builder.build(signer).run {
require(isValidOn(Date()))
require(isSignatureValid(JcaContentVerifierProviderBuilder().build(issuerKeyPair.public)))
require(isValidOn(Date())){"Certificate is not valid at instant now"}
require(isSignatureValid(JcaContentVerifierProviderBuilder().build(issuerKeyPair.public))){"Invalid signature"}
toJca()
}
}

View File

@ -431,10 +431,10 @@ internal constructor(private val initSerEnv: Boolean,
private fun NodeInfo.notaryIdentity(): Party {
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]
// 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.
// 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.
2 -> legalIdentities[1]
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
}

View File

@ -2,7 +2,7 @@ package net.corda.nodeapi.internal.network
import net.corda.core.internal.*
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.Scheduler
import rx.Subscription

View File

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

View File

@ -32,7 +32,7 @@ class HibernateConfiguration(
private val attributeConverters: Collection<AttributeConverter<*, *>>,
private val jdbcUrl: String,
cacheFactory: NamedCacheFactory,
val cordappClassLoader: ClassLoader? = null
val customClassLoader: ClassLoader? = null
) {
companion object {
private val logger = contextLogger()
@ -104,7 +104,7 @@ class HibernateConfiguration(
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")
// 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)
if (cordappClassLoader != null) {
if (customClassLoader != null) {
config.standardServiceRegistryBuilder.addService(
ClassLoaderService::class.java,
ClassLoaderServiceImpl(cordappClassLoader))
ClassLoaderServiceImpl(customClassLoader))
}
val metadataBuilder = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build())

View File

@ -11,7 +11,7 @@ import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize
import net.corda.node.services.config.NotaryConfig
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.config.parseAs
import net.corda.nodeapi.internal.config.toConfig

View File

@ -4,7 +4,7 @@ import net.corda.core.internal.div
import net.corda.core.internal.list
import net.corda.core.internal.write
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.junit.Before
import org.junit.Rule

View File

@ -5,10 +5,11 @@ import net.corda.core.internal.*
import net.corda.core.internal.concurrent.transpose
import net.corda.core.messaging.ParametersUpdateInfo
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.getOrThrow
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_UPDATE_FILE_NAME
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 org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.hamcrest.CoreMatchers.`is`
import org.junit.*
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.runners.Parameterized
import java.net.URL
import java.nio.file.Files
import java.time.Instant
@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
val nodeInfosDir = baseDirectory / NODE_INFO_DIRECTORY
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)
}

View File

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

View File

@ -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.
augmentClasspath(cp, new File(baseDir, "drivers"));
augmentClasspath(cp, cordappsDir);
try {
List<String> jarDirs = nodeConfig.getStringList("jarDirs");
log(LOG_VERBOSE, "Configured JAR directories = " + jarDirs);

View File

@ -75,7 +75,7 @@ open class SharedNodeCmdLineOptions {
errors.forEach { error ->
when (error) {
is ConfigException.IO -> logger.error(configFileNotFoundMessage(configFile))
else -> logger.error(error.message, error)
else -> logger.error(error.message)
}
}
}

View File

@ -154,7 +154,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
identityService::wellKnownPartyFromX500Name,
identityService::wellKnownPartyFromAnonymous,
schemaService,
cacheFactory)
cacheFactory,
this.cordappLoader.appClassLoader)
init {
// 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.
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.
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.")
val isH2Database = isH2Database(props.getProperty("dataSource.url", ""))
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.
logVendorString(database, log)
}
@ -1093,21 +1095,22 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
wellKnownPartyFromX500Name: (CordaX500Name) -> Party?,
wellKnownPartyFromAnonymous: (AbstractParty) -> Party?,
schemaService: SchemaService,
cacheFactory: NamedCacheFactory): CordaPersistence {
cacheFactory: NamedCacheFactory,
customClassLoader: ClassLoader?): CordaPersistence {
// 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
// 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.
JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(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 {
val dataSource = DataSourceFactory.createDataSource(hikariProperties, metricRegistry = metricRegistry)
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))
start(dataSource, jdbcUrl)
} catch (ex: Exception) {

View File

@ -39,7 +39,7 @@ private class MultiplexingReactiveArtemisConsumer(private val queueNames: Set<St
override fun start() {
synchronized(this) {
require(!startedFlag)
require(!startedFlag) { "Must not be started" }
connect()
startedFlag = true
}
@ -59,7 +59,7 @@ private class MultiplexingReactiveArtemisConsumer(private val queueNames: Set<St
override fun connect() {
synchronized(this) {
require(!connected)
require(!connected) { "Must not be connected" }
queueNames.forEach { queue ->
createSession().apply {
start()

View File

@ -11,7 +11,7 @@ import net.corda.core.internal.writer
import net.corda.core.serialization.internal.CheckpointSerializationContext
import net.corda.core.serialization.ClassWhitelist
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.TransientClassWhiteList
import net.corda.serialization.internal.amqp.hasCordaSerializable

View File

@ -77,7 +77,9 @@ class ImmutableClassSerializer<T : Any>(val klass: KClass<T>) : Serializer<T>()
// Verify that this class is immutable (all properties are final).
// We disable this check inside SGX as the reflection blows up.
if (!SgxSupport.isInsideEnclave) {
require(props.none { it is KMutableProperty<*> })
props.forEach {
require(it !is KMutableProperty<*>) { "$it mutable property of class: ${klass} is unsupported" }
}
}
}

View File

@ -284,8 +284,8 @@ data class SecurityConfiguration(val authService: SecurityConfiguration.AuthServ
val users: List<User>? = null) {
init {
when (type) {
AuthDataSourceType.INMEMORY -> require(users != null && connection == null)
AuthDataSourceType.DB -> 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) { "Database-backed authentication must not specify a user list, and must configure a database" }
}
}

View File

@ -673,7 +673,7 @@ private class P2PMessagingConsumer(
override fun start() {
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.switchedOff() }.doOnNext { existingOnlyConsumer.switchTo(initialAndExistingConsumer) }.subscribe()
subscriptions += existingOnlyConsumer.messages.doOnNext(messages::onNext).subscribe()

View File

@ -289,7 +289,7 @@ class RPCServer<OPS : RPCOps>(
private fun bindingRemovalArtemisMessageHandler(artemisMessage: ClientMessage) {
lifeCycle.requireState(State.STARTED)
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)
log.warn("Detected RPC client disconnect on address $clientAddress, scheduling for reaping")
invalidateClient(SimpleString(clientAddress))
@ -299,7 +299,7 @@ class RPCServer<OPS : RPCOps>(
private fun bindingAdditionArtemisMessageHandler(artemisMessage: ClientMessage) {
lifeCycle.requireState(State.STARTED)
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))
log.debug("RPC client queue created on address $clientAddress")

View File

@ -7,7 +7,7 @@ import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
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.network.NodeInfoFilesCopier
import rx.Observable

View File

@ -229,7 +229,7 @@ class NodeAttachmentService(
val attachmentImpl = AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad).let {
val contracts = attachment.contractClassNames
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())
} else {
it
@ -300,7 +300,7 @@ class NodeAttachmentService(
private fun import(jar: InputStream, uploader: String?, filename: String?): AttachmentId {
return database.transaction {
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.
// TODO: Switch to a two-phase insert so we can handle attachments larger than RAM.

View File

@ -1,7 +1,7 @@
package net.corda.node.services.statemachine
import co.paralleluniverse.strands.concurrent.AbstractQueuedSynchronizer
import co.paralleluniverse.fibers.Suspendable
import co.paralleluniverse.strands.concurrent.AbstractQueuedSynchronizer
/**
* Quasar-compatible latch that may be incremented.
@ -56,7 +56,7 @@ class CountUpDownLatch(initialValue: Int) {
}
fun countDown(number: Int = 1) {
require(number > 0)
require(number > 0){"Number to count down by must be greater than 0"}
sync.releaseShared(number)
}

View File

@ -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."
}
} 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,
isDbTransactionOpenOnExit = false
)
require(continuation == FlowContinuation.ProcessEvents)
require(continuation == FlowContinuation.ProcessEvents){"Expected a continuation of type ${FlowContinuation.ProcessEvents}, found $continuation "}
unpark(SERIALIZER_BLOCKER)
}
return uncheckedCast(processEventsUntilFlowIsResumed(

View File

@ -172,7 +172,7 @@ class SingleThreadedStateMachineManager(
* @param allowedUnsuspendedFiberCount Optional parameter is used in some tests.
*/
override fun stop(allowedUnsuspendedFiberCount: Int) {
require(allowedUnsuspendedFiberCount >= 0)
require(allowedUnsuspendedFiberCount >= 0){"allowedUnsuspendedFiberCount must be greater than or equal to zero"}
mutex.locked {
if (stopping) throw IllegalStateException("Already stopping!")
stopping = true
@ -775,10 +775,10 @@ class SingleThreadedStateMachineManager(
) {
drainFlowEventQueue(flow)
// final sanity checks
require(lastState.pendingDeduplicationHandlers.isEmpty())
require(lastState.isRemoved)
require(lastState.checkpoint.subFlowStack.size == 1)
require(flow.fiber.id !in sessionToFlow.values)
require(lastState.pendingDeduplicationHandlers.isEmpty()) { "Flow cannot be removed until all pending deduplications have completed" }
require(lastState.isRemoved) { "Flow must be in removable state before removal" }
require(lastState.checkpoint.subFlowStack.size == 1) { "Checkpointed stack must be empty" }
require(flow.fiber.id !in sessionToFlow.values) { "Flow fibre must not be needed by an existing session" }
flow.resultFuture.set(removalReason.flowReturnValue)
lastState.flowLogic.progressTracker?.currentStep = ProgressTracker.DONE
changesPublisher.onNext(StateMachineManager.Change.Removed(lastState.flowLogic, Try.Success(removalReason.flowReturnValue)))

View File

@ -2,17 +2,11 @@ package net.corda.node.services.statemachine.interceptors
import co.paralleluniverse.fibers.Suspendable
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.checkpointDeserialize
import net.corda.core.utilities.contextLogger
import net.corda.node.services.statemachine.ActionExecutor
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.*
import net.corda.node.services.statemachine.transitions.FlowContinuation
import net.corda.node.services.statemachine.transitions.TransitionResult
import java.util.concurrent.LinkedBlockingQueue
@ -69,7 +63,7 @@ class FiberDeserializationChecker {
private var foundUnrestorableFibers: Boolean = false
fun start(checkpointSerializationContext: CheckpointSerializationContext) {
require(checkerThread == null)
require(checkerThread == null){"Checking thread must not already be started"}
checkerThread = thread(name = "FiberDeserializationChecker") {
while (true) {
val job = jobQueue.take()

View File

@ -372,7 +372,7 @@ class NodeRegistrationHelper(
private class FixedPeriodLimitedRetrialStrategy(times: Int, private val period: Duration) : (Duration?) -> Duration? {
init {
require(times > 0)
require(times > 0){"Retry attempts must be larger than zero"}
}
private var counter = times

View File

@ -16,7 +16,7 @@ import net.corda.core.serialization.serialize
import net.corda.core.utilities.millis
import net.corda.node.VersionInfo
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.network.NETWORK_PARAMS_UPDATE_FILE_NAME
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier

View File

@ -6,7 +6,7 @@ import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.size
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.network.NodeInfoFilesCopier
import net.corda.testing.core.ALICE_NAME

View File

@ -31,7 +31,7 @@ class SchemaMigrationTest {
private fun configureDatabase(hikariProperties: Properties,
databaseConfig: DatabaseConfig,
schemaService: NodeSchemaService = NodeSchemaService()): CordaPersistence =
createCordaPersistence(databaseConfig, { null }, { null }, schemaService, TestingNamedCacheFactory())
createCordaPersistence(databaseConfig, { null }, { null }, schemaService, TestingNamedCacheFactory(), null)
.apply { startHikariPool(hikariProperties, databaseConfig, schemaService.schemaOptions.keys) }
@Test

View File

@ -54,10 +54,10 @@ class MaxTransactionSizeTests {
@Test
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
val bigFile1 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 0)
val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 1)
val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 2)
val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 3)
val bigFile1 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 0, "a")
val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 1, "b")
val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 2, "c")
val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 3, "d")
val flow = aliceNode.transaction {
val hash1 = aliceNode.importAttachment(bigFile1.inputStream)
val hash2 = aliceNode.importAttachment(bigFile2.inputStream)
@ -77,10 +77,10 @@ class MaxTransactionSizeTests {
@Test
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
val bigFile1 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 0)
val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 1)
val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 2)
val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 3)
val bigFile1 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 0, "a")
val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 1, "b")
val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 2, "c")
val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 3, "c")
val flow = aliceNode.transaction {
val hash1 = aliceNode.importAttachment(bigFile1.inputStream)
val hash2 = aliceNode.importAttachment(bigFile2.inputStream)

View File

@ -94,7 +94,7 @@ private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.
val id = rpc.uploadAttachment(it)
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)
@ -159,7 +159,7 @@ fun recipient(rpc: CordaRPCOps, webPort: Int) {
if (wtx.attachments.isNotEmpty()) {
if (wtx.outputs.isNotEmpty()) {
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.
val connection = URL("http://localhost:$webPort/attachments/${state.hash}").openConnection() as HttpURLConnection
@ -207,7 +207,7 @@ class AttachmentContract : Contract {
override fun verify(tx: LedgerTransaction) {
val state = tx.outputsOfType<AttachmentContract.State>().single()
// 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()

View File

@ -40,7 +40,7 @@ data class PortfolioState(val portfolio: List<StateRef>,
}
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 valuation = updatedValue.valuation ?: valuation

View File

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

View File

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

View File

@ -16,15 +16,6 @@ data class CheckpointSerializationContextImpl @JvmOverloads constructor(
override val objectReferencesEnabled: Boolean,
override val encoding: SerializationEncoding?,
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 {
return copy(properties = properties + (property to value))
}

View File

@ -8,6 +8,7 @@ import net.corda.core.contracts.Attachment
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.copyBytes
import net.corda.core.serialization.*
import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.core.utilities.ByteSequence
import net.corda.serialization.internal.amqp.amqpMagic
import org.slf4j.LoggerFactory
@ -31,20 +32,12 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe
override val useCase: SerializationContext.UseCase,
override val encoding: SerializationEncoding?,
override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist,
override val lenientCarpenterEnabled: Boolean = false,
private val builder: AttachmentsClassLoaderBuilder = AttachmentsClassLoaderBuilder()
) : SerializationContext {
override val lenientCarpenterEnabled: Boolean = false) : SerializationContext {
/**
* {@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 {
properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean == true || return this
val classLoader = builder.build(attachmentHashes, properties, deserializationClassLoader) ?: return this
return withClassLoader(classLoader)
return this
}
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)
}
/*
* 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
open class SerializationFactoryImpl(
// TODO: This is read-mostly. Probably a faster implementation to be found.

View File

@ -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!!)
}

View File

@ -172,7 +172,7 @@ abstract class AbstractAMQPSerializationScheme(
// Not used as a simple direct import to facilitate testing
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)
// ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already.
return serializerFactoriesForContexts[key] ?: serializerFactoriesForContexts.computeIfAbsent(key) {

View File

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

View File

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

View File

@ -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
}
}

View File

@ -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 }
}

View File

@ -52,6 +52,8 @@ class PublicPropertyReader(private val readMethod: Method) : PropertyReader() {
}
override fun isNullable(): Boolean = readMethod.returnsNullable()
val genericReturnType get() = readMethod.genericReturnType
}
/**

View File

@ -118,7 +118,7 @@ interface SerializerFactory {
Float::class.java to "float",
Double::class.java to "double",
Decimal32::class.java to "decimal32",
Decimal64::class.java to "decimal62",
Decimal64::class.java to "decimal64",
Decimal128::class.java to "decimal128",
Date::class.java to "timestamp",
UUID::class.java to "uuid",

View File

@ -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
)

View File

@ -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]!!
}

View File

@ -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
// list is now empty we have no impediment to carpenting that class up
schemas.dependsOn.remove(newObject.name)?.forEach { dependent ->
require(newObject.name in schemas.dependencies[dependent]!!.second)
schemas.dependencies[dependent]?.second?.remove(newObject.name)

View File

@ -72,8 +72,7 @@ open class NonNullableField(field: Class<out Any?>) : ClassField(field) {
}
override fun nullTest(mv: MethodVisitor, slot: Int) {
require(name != unsetName)
check(name != unsetName) {"Property this.name cannot be $unsetName"}
if (!field.isPrimitive) {
with(mv) {
visitVarInsn(ALOAD, 0) // load this
@ -109,7 +108,7 @@ class NullableField(field: Class<out Any?>) : ClassField(field) {
}
override fun nullTest(mv: MethodVisitor, slot: Int) {
require(name != unsetName)
require(name != unsetName){"Property this.name cannot be $unsetName"}
}
}

View File

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

View File

@ -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")
}

View File

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

View File

@ -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