diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt index b052f0f7bf..cc35dcb343 100644 --- a/core/src/main/kotlin/net/corda/core/Utils.kt +++ b/core/src/main/kotlin/net/corda/core/Utils.kt @@ -9,6 +9,7 @@ import com.google.common.io.ByteStreams import com.google.common.util.concurrent.* import kotlinx.support.jdk7.use import net.corda.core.crypto.newSecureRandom +import net.corda.core.serialization.CordaSerializable import org.slf4j.Logger import rx.Observable import rx.Observer @@ -257,6 +258,7 @@ class ThreadBox(val content: T, val lock: ReentrantLock = ReentrantLock() * * We avoid the use of the word transient here to hopefully reduce confusion with the term in relation to (Java) serialization. */ +@CordaSerializable abstract class RetryableException(message: String) : Exception(message) /** @@ -307,6 +309,7 @@ fun extractZipFile(zipFile: Path, toDirectory: Path) { val Throwable.rootCause: Throwable get() = Throwables.getRootCause(this) /** Representation of an operation that may have thrown an error. */ +@CordaSerializable data class ErrorOr private constructor(val value: A?, val error: Throwable?) { // The ErrorOr holds a value iff error == null constructor(value: A) : this(value, null) diff --git a/core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt b/core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt index 9266f366dc..bcdca06360 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.google.common.annotations.VisibleForTesting +import net.corda.core.serialization.CordaSerializable import java.math.BigDecimal import java.math.BigInteger import java.time.DayOfWeek @@ -34,6 +35,7 @@ import java.util.* * * @param T the type of the token, for example [Currency]. */ +@CordaSerializable data class Amount(val quantity: Long, val token: T) : Comparable> { companion object { /** @@ -108,12 +110,14 @@ fun Iterable>.sumOrZero(currency: T) = if (iterator().hasNext()) s //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** A [FixOf] identifies the question side of a fix: what day, tenor and type of fix ("LIBOR", "EURIBOR" etc) */ +@CordaSerializable data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor) /** A [Fix] represents a named interest rate, on a given day, for a given duration. It can be embedded in a tx. */ data class Fix(val of: FixOf, val value: BigDecimal) : CommandData /** Represents a textual expression of e.g. a formula */ +@CordaSerializable @JsonDeserialize(using = ExpressionDeserializer::class) @JsonSerialize(using = ExpressionSerializer::class) data class Expression(val expr: String) @@ -131,6 +135,7 @@ object ExpressionDeserializer : JsonDeserializer() { } /** Placeholder class for the Tenor datatype - which is a standardised duration of time until maturity */ +@CordaSerializable data class Tenor(val name: String) { private val amount: Int private val unit: TimeUnit @@ -166,6 +171,7 @@ data class Tenor(val name: String) { override fun toString(): String = name + @CordaSerializable enum class TimeUnit(val code: String) { Day("D"), Week("W"), Month("M"), Year("Y") } @@ -175,6 +181,7 @@ data class Tenor(val name: String) { * Simple enum for returning accurals adjusted or unadjusted. * We don't actually do anything with this yet though, so it's ignored for now. */ +@CordaSerializable enum class AccrualAdjustment { Adjusted, Unadjusted } @@ -183,6 +190,7 @@ enum class AccrualAdjustment { * This is utilised in the [DateRollConvention] class to determine which way we should initially step when * finding a business day. */ +@CordaSerializable enum class DateRollDirection(val value: Long) { FORWARD(1), BACKWARD(-1) } /** @@ -190,6 +198,7 @@ enum class DateRollDirection(val value: Long) { FORWARD(1), BACKWARD(-1) } * Depending on the accounting requirement, we can move forward until we get to a business day, or backwards. * There are some additional rules which are explained in the individual cases below. */ +@CordaSerializable enum class DateRollConvention { // direction() cannot be a val due to the throw in the Actual instance @@ -235,6 +244,7 @@ enum class DateRollConvention { * Note that the first character cannot be a number (enum naming constraints), so we drop that * in the toString lest some people get confused. */ +@CordaSerializable enum class DayCountBasisDay { // We have to prefix 30 etc with a letter due to enum naming constraints. D30, @@ -246,6 +256,7 @@ enum class DayCountBasisDay { } /** This forms the year part of the "Day Count Basis" used for interest calculation. */ +@CordaSerializable enum class DayCountBasisYear { // Ditto above comment for years. Y360, @@ -257,6 +268,7 @@ enum class DayCountBasisYear { } /** Whether the payment should be made before the due date, or after it. */ +@CordaSerializable enum class PaymentRule { InAdvance, InArrears, } @@ -266,6 +278,7 @@ enum class PaymentRule { * that would divide into (eg annually = 1, semiannual = 2, monthly = 12 etc). */ @Suppress("unused") // TODO: Revisit post-Vega and see if annualCompoundCount is still needed. +@CordaSerializable enum class Frequency(val annualCompoundCount: Int) { Annual(1) { override fun offset(d: LocalDate, n: Long) = d.plusYears(1 * n) @@ -304,7 +317,9 @@ fun LocalDate.isWorkingDay(accordingToCalendar: BusinessCalendar): Boolean = acc * typical feature of financial contracts, in which a business may not want a payment event to fall on a day when * no staff are around to handle problems. */ +@CordaSerializable open class BusinessCalendar private constructor(val holidayDates: List) { + @CordaSerializable class UnknownCalendar(name: String) : Exception("$name not found") companion object { @@ -434,6 +449,7 @@ fun calculateDaysBetween(startDate: LocalDate, * Enum for the types of netting that can be applied to state objects. Exact behaviour * for each type of netting is left to the contract to determine. */ +@CordaSerializable enum class NetType { /** * Close-out netting applies where one party is bankrupt or otherwise defaults (exact terms are contract specific), @@ -461,6 +477,7 @@ enum class NetType { * @param defaultFractionDigits the number of digits normally after the decimal point when referring to quantities of * this commodity. */ +@CordaSerializable data class Commodity(val commodityCode: String, val displayName: String, val defaultFractionDigits: Int = 0) { @@ -487,6 +504,7 @@ data class Commodity(val commodityCode: String, * So that the first time a state is issued this should be given a new UUID. * Subsequent copies and evolutions of a state should just copy the [externalId] and [id] fields unmodified. */ +@CordaSerializable data class UniqueIdentifier(val externalId: String? = null, val id: UUID = UUID.randomUUID()) : Comparable { override fun toString(): String = if (externalId != null) "${externalId}_$id" else id.toString() diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index 83e8bfd802..099381d831 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -8,6 +8,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogicRef import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.node.services.ServiceType +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.serialize import net.corda.core.transactions.TransactionBuilder @@ -62,6 +63,7 @@ interface NettableState, T : Any> : BilateralNetta * notary is responsible for ensuring there is no "double spending" by only signing a transaction if the input states * are all free. */ +@CordaSerializable interface ContractState { /** * An instance of the contract class that will verify this state. @@ -121,6 +123,7 @@ interface ContractState { * A wrapper for [ContractState] containing additional platform-level state information. * This is the definitive state that is stored on the ledger and used in transaction outputs. */ +@CordaSerializable data class TransactionState @JvmOverloads constructor( /** The custom contract state */ val data: T, @@ -169,6 +172,7 @@ interface IssuanceDefinition * * @param P the type of product underlying the definition, for example [Currency]. */ +@CordaSerializable data class Issued(val issuer: PartyAndReference, val product: P) { override fun toString() = "$product issued by $issuer" } @@ -239,6 +243,7 @@ interface LinearState : ContractState { /** * Standard clause to verify the LinearState safety properties. */ + @CordaSerializable class ClauseVerifier() : Clause() { override fun verify(tx: TransactionForContract, inputs: List, @@ -334,11 +339,13 @@ fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bytes) * A stateref is a pointer (reference) to a state, this is an equivalent of an "outpoint" in Bitcoin. It records which * transaction defined the state and where in that transaction it was. */ +@CordaSerializable data class StateRef(val txhash: SecureHash, val index: Int) { override fun toString() = "$txhash($index)" } /** A StateAndRef is simply a (state, ref) pair. For instance, a vault (which holds available assets) contains these. */ +@CordaSerializable data class StateAndRef(val state: TransactionState, val ref: StateRef) /** Filters a list of [StateAndRef] objects according to the type of the states */ @@ -350,12 +357,14 @@ inline fun Iterable>.filt * Reference to something being stored or issued by a party e.g. in a vault or (more likely) on their normal * ledger. The reference is intended to be encrypted so it's meaningless to anyone other than the party. */ +@CordaSerializable data class PartyAndReference(val party: AnonymousParty, val reference: OpaqueBytes) { constructor(party: Party, reference: OpaqueBytes) : this(party.toAnonymous(), reference) override fun toString() = "${party}$reference" } /** Marker interface for classes that represent commands */ +@CordaSerializable interface CommandData /** Commands that inherit from this are intended to have no data items: it's only their presence that matters. */ @@ -365,6 +374,7 @@ abstract class TypeOnlyCommandData : CommandData { } /** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */ +@CordaSerializable data class Command(val value: CommandData, val signers: List) { init { require(signers.isNotEmpty()) @@ -402,6 +412,7 @@ interface NetCommand : CommandData { data class UpgradeCommand(val upgradedContractClass: Class>) : CommandData /** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */ +@CordaSerializable data class AuthenticatedObject( val signers: List, /** If any public keys were recognised, the looked up institutions are available here */ @@ -413,6 +424,7 @@ data class AuthenticatedObject( * If present in a transaction, contains a time that was verified by the uniqueness service. The true time must be * between (after, before). */ +@CordaSerializable data class Timestamp(val after: Instant?, val before: Instant?) { init { if (after == null && before == null) @@ -431,7 +443,10 @@ data class Timestamp(val after: Instant?, val before: Instant?) { * every [LedgerTransaction] they see on the network, for every input and output state. All contracts must accept the * transaction for it to be accepted: failure of any aborts the entire thing. The time is taken from a trusted * timestamp attached to the transaction itself i.e. it is NOT necessarily the current time. + * + * TODO: Contract serialization is likely to change, so the annotation is likely temporary. */ +@CordaSerializable interface Contract { /** * Takes an object that represents a state transition, and ensures the inputs/outputs/commands make sense. diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt index 7cd6ef4d39..e1539781a9 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt @@ -2,10 +2,12 @@ package net.corda.core.contracts import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Party +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder /** Defines transaction build & validation logic for a specific transaction type */ +@CordaSerializable sealed class TransactionType { override fun equals(other: Any?) = other?.javaClass == javaClass override fun hashCode() = javaClass.name.hashCode() diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt index 2c59c872f9..f7e021ba00 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt @@ -4,6 +4,7 @@ import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Party import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.LedgerTransaction import java.util.* @@ -94,6 +95,7 @@ class AttachmentResolutionException(val hash : SecureHash) : FlowException() { override fun toString(): String = "Attachment resolution failure for $hash" } +@CordaSerializable class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception() sealed class TransactionVerificationException(val tx: LedgerTransaction, cause: Throwable?) : FlowException(cause) { @@ -116,6 +118,8 @@ sealed class TransactionVerificationException(val tx: LedgerTransaction, cause: class TransactionMissingEncumbranceException(tx: LedgerTransaction, val missing: Int, val inOut: Direction) : TransactionVerificationException(tx, null) { override val message: String get() = "Missing required encumbrance $missing in $inOut" } + + @CordaSerializable enum class Direction { INPUT, OUTPUT diff --git a/core/src/main/kotlin/net/corda/core/crypto/AbstractParty.kt b/core/src/main/kotlin/net/corda/core/crypto/AbstractParty.kt index 66ed811916..1d50aca4c4 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/AbstractParty.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/AbstractParty.kt @@ -1,6 +1,7 @@ package net.corda.core.crypto import net.corda.core.contracts.PartyAndReference +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes import java.security.PublicKey @@ -8,6 +9,7 @@ import java.security.PublicKey * An [AbstractParty] contains the common elements of [Party] and [AnonymousParty], specifically the owning key of * the party. In most cases [Party] or [AnonymousParty] should be used, depending on use-case. */ +@CordaSerializable abstract class AbstractParty(val owningKey: CompositeKey) { /** A helper constructor that converts the given [PublicKey] in to a [CompositeKey] with a single node */ constructor(owningKey: PublicKey) : this(owningKey.composite) diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt index 8c5500a7ff..a6f352503f 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt @@ -2,6 +2,7 @@ package net.corda.core.crypto import net.corda.core.crypto.CompositeKey.Leaf import net.corda.core.crypto.CompositeKey.Node +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import java.security.PublicKey @@ -19,6 +20,7 @@ import java.security.PublicKey * Using these constructs we can express e.g. 1 of N (OR) or N of N (AND) signature requirements. By nesting we can * create multi-level requirements such as *"either the CEO or 3 of 5 of his assistants need to sign"*. */ +@CordaSerializable sealed class CompositeKey { /** Checks whether [keys] match a sufficient amount of leaf nodes */ abstract fun isFulfilledBy(keys: Iterable): Boolean diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtilities.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtilities.kt index 17d438b75c..3f2d7473aa 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtilities.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtilities.kt @@ -2,6 +2,7 @@ package net.corda.core.crypto +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes import net.i2p.crypto.eddsa.EdDSAEngine import net.i2p.crypto.eddsa.EdDSAPrivateKey @@ -26,6 +27,7 @@ fun newSecureRandom(): SecureRandom { * signature. It isn't used currently, but experience from Bitcoin suggests such a feature is useful, especially when * building partially signed transactions. */ +@CordaSerializable open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) { /** A digital signature that identifies who the public key is owned by. */ open class WithKey(val by: PublicKey, bits: ByteArray) : DigitalSignature(bits) { @@ -37,6 +39,7 @@ open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) { class LegallyIdentifiable(val signer: Party, bits: ByteArray) : WithKey(signer.owningKey.singleKey, bits) } +@CordaSerializable object NullPublicKey : PublicKey, Comparable { override fun getAlgorithm() = "NULL" override fun getEncoded() = byteArrayOf(0) @@ -48,6 +51,7 @@ object NullPublicKey : PublicKey, Comparable { val NullCompositeKey = NullPublicKey.composite // TODO: Clean up this duplication between Null and Dummy public key +@CordaSerializable class DummyPublicKey(val s: String) : PublicKey, Comparable { override fun getAlgorithm() = "DUMMY" override fun getEncoded() = s.toByteArray() @@ -59,6 +63,7 @@ class DummyPublicKey(val s: String) : PublicKey, Comparable { } /** A signature with a key and value of zero. Useful when you want a signature object that you know won't ever be used. */ +@CordaSerializable object NullSignature : DigitalSignature.WithKey(NullPublicKey, ByteArray(32)) /** Utility to simplify the act of signing a byte array */ diff --git a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt index 488d4c52c7..c7193ce8da 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/PartialMerkleTree.kt @@ -1,10 +1,10 @@ package net.corda.core.crypto -import net.corda.core.crypto.MerkleTree import net.corda.core.crypto.SecureHash.Companion.zeroHash +import net.corda.core.serialization.CordaSerializable import java.util.* - +@CordaSerializable class MerkleTreeException(val reason: String) : Exception() { override fun toString() = "Partial Merkle Tree exception. Reason: $reason" } @@ -43,7 +43,7 @@ class MerkleTreeException(val reason: String) : Exception() { * (there can be a difference in obtained leaves ordering - that's why it's a set comparison not hashing leaves into a tree). * If both equalities hold, we can assume that l3 and l5 belong to the transaction with root h15. */ - +@CordaSerializable class PartialMerkleTree(val root: PartialTree) { /** * The structure is a little different than that of Merkle Tree. @@ -52,6 +52,7 @@ class PartialMerkleTree(val root: PartialTree) { * transaction and leaves that just keep hashes needed for calculation. Reason for this approach: during verification * it's easier to extract hashes used as a base for this tree. */ + @CordaSerializable sealed class PartialTree { class IncludedLeaf(val hash: SecureHash) : PartialTree() class Leaf(val hash: SecureHash) : PartialTree() diff --git a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt index b16d17ef89..36c2f22cb6 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -1,6 +1,7 @@ package net.corda.core.crypto import com.google.common.io.BaseEncoding +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes import java.security.MessageDigest @@ -8,6 +9,7 @@ import java.security.MessageDigest * Container for a cryptographically secure hash value. * Provides utilities for generating a cryptographic hash using different algorithms (currently only SHA-256 supported). */ +@CordaSerializable 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) { diff --git a/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt b/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt index bc377fd7c7..b026e94dca 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SignedData.kt @@ -1,5 +1,6 @@ package net.corda.core.crypto +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import java.security.SignatureException @@ -11,6 +12,7 @@ import java.security.SignatureException * @param raw the raw serialized data. * @param sig the (unverified) signature for the data. */ +@CordaSerializable open class SignedData(val raw: SerializedBytes, val sig: DigitalSignature.WithKey) { /** * Return the deserialized data if the signature can be verified. diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowException.kt b/core/src/main/kotlin/net/corda/core/flows/FlowException.kt index 9419049d68..f6ff8de5db 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowException.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowException.kt @@ -1,5 +1,7 @@ package net.corda.core.flows +import net.corda.core.serialization.CordaSerializable + /** * Exception which can be thrown by a [FlowLogic] at any point in its logic to unexpectedly bring it to a permanent end. * The exception will propagate to all counterparty flows and will be thrown on their end the next time they wait on a @@ -9,6 +11,7 @@ package net.corda.core.flows * [FlowException] (or a subclass) can be a valid expected response from a flow, particularly ones which act as a service. * It is recommended a [FlowLogic] document the [FlowException] types it can throw. */ +@CordaSerializable open class FlowException(override val message: String?, override val cause: Throwable?) : Exception() { constructor(message: String?) : this(message, null) constructor(cause: Throwable?) : this(cause?.toString(), cause) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt index 5324d47480..57a37f50f5 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt @@ -2,6 +2,7 @@ package net.corda.core.flows import com.google.common.primitives.Primitives import net.corda.core.crypto.SecureHash +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken import java.lang.reflect.ParameterizedType import java.lang.reflect.Type @@ -186,6 +187,7 @@ class FlowLogicRefFactory(private val flowWhitelist: Map>) : } } +@CordaSerializable class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException("${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg") /** @@ -194,12 +196,14 @@ class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentEx * Only allows a String reference to the FlowLogic class, and only allows restricted argument types as per [FlowLogicRefFactory]. */ // TODO: align this with the existing [FlowRef] in the bank-side API (probably replace some of the API classes) +@CordaSerializable data class FlowLogicRef internal constructor(val flowLogicClassName: String, val appContext: AppContext, val args: Map) /** * This is just some way to track what attachments need to be in the class loader, but may later include some app * properties loaded from the attachments. And perhaps the authenticated user for an API call? */ +@CordaSerializable data class AppContext(val attachments: List) { // TODO: build a real [AttachmentsClassLoader] etc val classLoader: ClassLoader diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowStateMachine.kt b/core/src/main/kotlin/net/corda/core/flows/FlowStateMachine.kt index 974b9e0d45..275c86c276 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowStateMachine.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowStateMachine.kt @@ -5,6 +5,7 @@ import com.google.common.util.concurrent.ListenableFuture import net.corda.core.crypto.Party import net.corda.core.crypto.SecureHash import net.corda.core.node.ServiceHub +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.UntrustworthyData import org.slf4j.Logger @@ -14,6 +15,7 @@ import java.util.* * A unique identifier for a single state machine run, valid across node restarts. Note that a single run always * has at least one flow, but that flow may also invoke sub-flows: they all share the same run id. */ +@CordaSerializable data class StateMachineRunId private constructor(val uuid: UUID) { companion object { fun createRandom(): StateMachineRunId = StateMachineRunId(UUID.randomUUID()) diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index ed9bf00604..fd5ff3629a 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -14,23 +14,26 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.StateMachineTransactionMapping import net.corda.core.node.services.Vault +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import rx.Observable import java.io.InputStream -import java.io.OutputStream import java.time.Instant import java.util.* +@CordaSerializable data class StateMachineInfo( val id: StateMachineRunId, val flowLogicClassName: String, val progressTrackerStepAndUpdates: Pair>? ) +@CordaSerializable sealed class StateMachineUpdate(val id: StateMachineRunId) { class Added(val stateMachineInfo: StateMachineInfo) : StateMachineUpdate(stateMachineInfo.id) { override fun toString() = "Added($id, ${stateMachineInfo.flowLogicClassName})" } + class Removed(id: StateMachineRunId) : StateMachineUpdate(id) { override fun toString() = "Removed($id)" } @@ -212,6 +215,7 @@ inline fun > CordaRPCOps.startFlow * @param progress The stream of progress tracker events. * @param returnValue A [ListenableFuture] of the flow's return value. */ +@CordaSerializable data class FlowHandle( val id: StateMachineRunId, val progress: Observable, diff --git a/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt b/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt index d1499cdc81..2e0224a6ad 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt @@ -5,6 +5,7 @@ import com.google.common.util.concurrent.SettableFuture import net.corda.core.catch import net.corda.core.node.services.DEFAULT_SESSION_ID import net.corda.core.node.services.PartyInfo +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.DeserializeAsKotlinObjectDef import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize @@ -160,6 +161,7 @@ interface MessageHandlerRegistration * @param sessionID identifier for the session the message is part of. For services listening before * a session is established, use [DEFAULT_SESSION_ID]. */ +@CordaSerializable data class TopicSession(val topic: String, val sessionID: Long = DEFAULT_SESSION_ID) { fun isBlank() = topic.isBlank() && sessionID == DEFAULT_SESSION_ID override fun toString(): String = "$topic.$sessionID" @@ -213,4 +215,5 @@ interface AllPossibleRecipients : MessageRecipients * A general Ack message that conveys no content other than it's presence for use when you want an acknowledgement * from a recipient. Using [Unit] can be ambiguous as it is similar to [Void] and so could mean no response. */ +@CordaSerializable object Ack : DeserializeAsKotlinObjectDef diff --git a/core/src/main/kotlin/net/corda/core/node/AttachmentsClassLoader.kt b/core/src/main/kotlin/net/corda/core/node/AttachmentsClassLoader.kt index 15c60f4419..bd76fd8bbc 100644 --- a/core/src/main/kotlin/net/corda/core/node/AttachmentsClassLoader.kt +++ b/core/src/main/kotlin/net/corda/core/node/AttachmentsClassLoader.kt @@ -2,6 +2,7 @@ package net.corda.core.node import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash +import net.corda.core.serialization.CordaSerializable import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.FileNotFoundException @@ -24,6 +25,7 @@ class AttachmentsClassLoader(attachments: List, parent: ClassLoader private val pathsToAttachments = HashMap() private val idsToAttachments = HashMap() + @CordaSerializable class OverlappingAttachments(val path: String) : Exception() { override fun toString() = "Multiple attachments define a file at path $path" } diff --git a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt b/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt index 797e1fe275..66fae2ca0c 100644 --- a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt +++ b/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt @@ -1,7 +1,7 @@ package net.corda.core.node -import com.esotericsoftware.kryo.Kryo import net.corda.core.messaging.CordaRPCOps +import net.corda.core.serialization.SerializationCustomization import java.util.function.Function /** @@ -40,14 +40,12 @@ abstract class CordaPluginRegistry( open val servicePlugins: List> = emptyList() ) { /** - * Optionally register types with [Kryo] for use over RPC, as we lock down the types that can be serialised in this - * particular use case. - * For example, if you add an RPC interface that carries some contract states back and forth, you need to register - * those classes here using the [register] method on Kryo. - * - * TODO: Kryo and likely the requirement to register classes here will go away when we replace the serialization implementation. + * Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized. * + * For example, if you add a new [ContractState] it needs to be whitelisted. You can do that either by + * adding the @CordaSerializable annotation or via this method. + ** * @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future. */ - open fun registerRPCKryoTypes(kryo: Kryo): Boolean = false + open fun customizeSerialization(custom: SerializationCustomization): Boolean = false } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index 468a8244b3..cfab38e427 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -4,16 +4,19 @@ import net.corda.core.crypto.Party import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType +import net.corda.core.serialization.CordaSerializable /** * Information for an advertised service including the service specific identity information. * The identity can be used in flows and is distinct from the Node's legalIdentity */ +@CordaSerializable data class ServiceEntry(val info: ServiceInfo, val identity: Party) /** * Info about a network node that acts on behalf of some form of contract party. */ +@CordaSerializable data class NodeInfo(val address: SingleMessageRecipient, val legalIdentity: Party, var advertisedServices: List = emptyList(), diff --git a/core/src/main/kotlin/net/corda/core/node/PhysicalLocationStructures.kt b/core/src/main/kotlin/net/corda/core/node/PhysicalLocationStructures.kt index 6f595a4fbc..72ffaa25dc 100644 --- a/core/src/main/kotlin/net/corda/core/node/PhysicalLocationStructures.kt +++ b/core/src/main/kotlin/net/corda/core/node/PhysicalLocationStructures.kt @@ -1,8 +1,10 @@ package net.corda.core.node +import net.corda.core.serialization.CordaSerializable import java.util.* /** A latitude/longitude pair. */ +@CordaSerializable data class WorldCoordinate(val latitude: Double, val longitude: Double) { init { require(latitude in -90..90) @@ -39,6 +41,7 @@ data class WorldCoordinate(val latitude: Double, val longitude: Double) { * A labelled [WorldCoordinate], where the label is human meaningful. For example, the name of the nearest city. * Labels should not refer to non-landmarks, for example, they should not contain the names of organisations. */ +@CordaSerializable data class PhysicalLocation(val coordinate: WorldCoordinate, val description: String) /** diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index e972afac12..cc797f6ccc 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -9,6 +9,7 @@ import net.corda.core.messaging.MessagingService import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.randomOrNull +import net.corda.core.serialization.CordaSerializable import rx.Observable /** @@ -19,6 +20,7 @@ import rx.Observable */ interface NetworkMapCache { + @CordaSerializable sealed class MapChange(val node: NodeInfo) { class Added(node: NodeInfo) : MapChange(node) class Removed(node: NodeInfo) : MapChange(node) @@ -142,6 +144,7 @@ interface NetworkMapCache { fun runWithoutMapService() } +@CordaSerializable sealed class NetworkCacheError : Exception() { /** Indicates a failure to deregister, because of a rejected request from the remote node */ class DeregistrationFailed : NetworkCacheError() diff --git a/core/src/main/kotlin/net/corda/core/node/services/ServiceInfo.kt b/core/src/main/kotlin/net/corda/core/node/services/ServiceInfo.kt index ef958ca962..94774a40f2 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/ServiceInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/ServiceInfo.kt @@ -1,5 +1,7 @@ package net.corda.core.node.services +import net.corda.core.serialization.CordaSerializable + /** * A container for additional information for an advertised service. * @@ -7,6 +9,7 @@ package net.corda.core.node.services * @param name the service name, used for differentiating multiple services of the same type. Can also be used as a * grouping identifier for nodes collectively running a distributed service. */ +@CordaSerializable data class ServiceInfo(val type: ServiceType, val name: String? = null) { companion object { fun parse(encoded: String): ServiceInfo { diff --git a/core/src/main/kotlin/net/corda/core/node/services/ServiceType.kt b/core/src/main/kotlin/net/corda/core/node/services/ServiceType.kt index df0aaee18b..dc7e35102b 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/ServiceType.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/ServiceType.kt @@ -1,10 +1,13 @@ package net.corda.core.node.services +import net.corda.core.serialization.CordaSerializable + /** * Identifier for service types a node can expose over the network to other peers. These types are placed into network * map advertisements. Services that are purely local and are not providing functionality to other parts of the network * don't need a declared service type. */ +@CordaSerializable sealed class ServiceType(val id: String) { init { // Enforce: diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index 18451a9b9e..2ec9d28e78 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -3,6 +3,7 @@ package net.corda.core.node.services import com.google.common.util.concurrent.ListenableFuture import net.corda.core.contracts.* import net.corda.core.crypto.* +import net.corda.core.serialization.CordaSerializable import net.corda.core.toFuture import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction @@ -36,6 +37,7 @@ val DEFAULT_SESSION_ID = 0L * Active means they haven't been consumed yet (or we don't know about it). * Relevant means they contain at least one of our pubkeys. */ +@CordaSerializable class Vault(val states: Iterable>) { /** @@ -46,6 +48,7 @@ class Vault(val states: Iterable>) { * If the vault observes multiple transactions simultaneously, where some transactions consume the outputs of some of the * other transactions observed, then the changes are observed "net" of those. */ + @CordaSerializable data class Update(val consumed: Set>, val produced: Set>) { /** Checks whether the update contains a state of the specified type. */ inline fun containsType() = consumed.any { it.state.data is T } || produced.any { it.state.data is T } diff --git a/core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt index ca960282ce..66171bd2ed 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/StateMachineRecordedTransactionMappingStorage.kt @@ -2,8 +2,10 @@ package net.corda.core.node.services import net.corda.core.crypto.SecureHash import net.corda.core.flows.StateMachineRunId +import net.corda.core.serialization.CordaSerializable import rx.Observable +@CordaSerializable data class StateMachineTransactionMapping(val stateMachineRunId: StateMachineRunId, val transactionId: SecureHash) /** diff --git a/core/src/main/kotlin/net/corda/core/node/services/UniquenessProvider.kt b/core/src/main/kotlin/net/corda/core/node/services/UniquenessProvider.kt index 516f9d7e88..f0b512e166 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/UniquenessProvider.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/UniquenessProvider.kt @@ -3,6 +3,7 @@ package net.corda.core.node.services import net.corda.core.contracts.StateRef import net.corda.core.crypto.Party import net.corda.core.crypto.SecureHash +import net.corda.core.serialization.CordaSerializable /** * A service that records input states of the given transaction and provides conflict information @@ -15,6 +16,7 @@ interface UniquenessProvider { fun commit(states: List, txId: SecureHash, callerIdentity: Party) /** Specifies the consuming transaction for every conflicting state */ + @CordaSerializable data class Conflict(val stateHistory: Map) /** @@ -26,7 +28,9 @@ interface UniquenessProvider { * This allows a party to just submit invalid transactions with outputs it was aware of and * find out where exactly they were spent. */ + @CordaSerializable data class ConsumingTx(val id: SecureHash, val inputIndex: Int, val requestingParty: Party) } +@CordaSerializable class UniquenessException(val error: UniquenessProvider.Conflict) : Exception() diff --git a/core/src/main/kotlin/net/corda/core/serialization/ByteArrays.kt b/core/src/main/kotlin/net/corda/core/serialization/ByteArrays.kt index a74cefe93c..e08325ef58 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/ByteArrays.kt @@ -9,6 +9,7 @@ import java.util.* * In an ideal JVM this would be a value type and be completely overhead free. Project Valhalla is adding such * functionality to Java, but it won't arrive for a few years yet! */ +@CordaSerializable open class OpaqueBytes(val bytes: ByteArray) { init { check(bytes.isNotEmpty()) diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt b/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt new file mode 100644 index 0000000000..4b6350ba03 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt @@ -0,0 +1,178 @@ +package net.corda.core.serialization + +import com.esotericsoftware.kryo.* +import com.esotericsoftware.kryo.util.DefaultClassResolver +import com.esotericsoftware.kryo.util.Util +import net.corda.core.node.AttachmentsClassLoader +import net.corda.core.utilities.loggerFor +import java.io.PrintWriter +import java.lang.reflect.Modifier +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardOpenOption +import java.util.* + +fun Kryo.addToWhitelist(type: Class<*>) { + ((classResolver as? CordaClassResolver)?.whitelist as? MutableClassWhitelist)?.add(type) +} + +fun makeStandardClassResolver(): ClassResolver { + return CordaClassResolver(GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist())) +} + +fun makeNoWhitelistClassResolver(): ClassResolver { + return CordaClassResolver(AllWhitelist) +} + +class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver() { + companion object { + private val logger = loggerFor() + } + + /** Returns the registration for the specified class, or null if the class is not registered. */ + override fun getRegistration(type: Class<*>): Registration? { + return super.getRegistration(type) ?: checkClass(type) + } + + private var whitelistEnabled = true + + fun disableWhitelist() { + whitelistEnabled = false + } + + fun enableWhitelist() { + whitelistEnabled = true + } + + private fun checkClass(type: Class<*>): Registration? { + /** If call path has disabled whitelisting (see [CordaKryo.register]), just return without checking. */ + if(!whitelistEnabled) return null + // Allow primitives, abstracts and interfaces + if (type.isPrimitive || type == Any::class.java || Modifier.isAbstract(type.modifiers) || type==String::class.java) return null + // If array, recurse on element type + if (type.isArray) { + return checkClass(type.componentType) + } + if (!type.isEnum && Enum::class.java.isAssignableFrom(type)) { + // Specialised enum entry, so just resolve the parent Enum type since cannot annotate the specialised entry. + return checkClass(type.superclass) + } + // It's safe to have the Class already, since Kryo loads it with initialisation off. + val hasAnnotation = checkForAnnotation(type) + if (!hasAnnotation && !whitelist.hasListed(type)) { + throw KryoException("Class ${Util.className(type)} is not annotated or on the whitelist, so cannot be used in serialization") + } + return null + } + + override fun registerImplicit(type: Class<*>): Registration { + // We have to set reference to true, since the flag influences how String fields are treated and we want it to be consistent. + val references = kryo.references + try { + kryo.references = true + return register(Registration(type, kryo.getDefaultSerializer(type), NAME.toInt())) + } finally { + kryo.references = references + } + } + + // We don't allow the annotation for classes in attachments for now. The class will be on the main classpath if we have the CorDapp installed. + // We also do not allow extension of KryoSerializable for annotated classes, or combination with @DefaultSerializer for custom serialisation. + // TODO: Later we can support annotations on attachment classes and spin up a proxy via bytecode that we know is harmless. + private fun checkForAnnotation(type: Class<*>): Boolean { + return (type.classLoader !is AttachmentsClassLoader) + && !KryoSerializable::class.java.isAssignableFrom(type) + && !type.isAnnotationPresent(DefaultSerializer::class.java) + && (type.isAnnotationPresent(CordaSerializable::class.java) || hasAnnotationOnInterface(type)) + } + + // Recursively check interfaces for our annotation. + private fun hasAnnotationOnInterface(type: Class<*>): Boolean { + return type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasAnnotationOnInterface(it) } + || (type.superclass != null && hasAnnotationOnInterface(type.superclass)) + } +} + +interface ClassWhitelist { + fun hasListed(type: Class<*>): Boolean +} + +interface MutableClassWhitelist : ClassWhitelist { + fun add(entry: Class<*>) +} + +object EmptyWhitelist : ClassWhitelist { + override fun hasListed(type: Class<*>): Boolean = false +} + +class BuiltInExceptionsWhitelist : ClassWhitelist { + override fun hasListed(type: Class<*>): Boolean = Throwable::class.java.isAssignableFrom(type) && type.`package`.name.startsWith("java.") +} + +object AllWhitelist : ClassWhitelist { + override fun hasListed(type: Class<*>): Boolean = true +} + +// TODO: Need some concept of from which class loader +class GlobalTransientClassWhiteList(val delegate: ClassWhitelist) : MutableClassWhitelist, ClassWhitelist by delegate { + companion object { + val whitelist: MutableSet = Collections.synchronizedSet(mutableSetOf()) + } + + override fun hasListed(type: Class<*>): Boolean { + return (type.name in whitelist) || delegate.hasListed(type) + } + + override fun add(entry: Class<*>) { + whitelist += entry.name + } +} + +/** + * This class is not currently used, but can be installed to log a large number of missing entries from the whitelist + * and was used to track down the initial set. + */ +class LoggingWhitelist(val delegate: ClassWhitelist, val global: Boolean = true) : MutableClassWhitelist { + companion object { + val log = loggerFor() + val globallySeen: MutableSet = Collections.synchronizedSet(mutableSetOf()) + val journalWriter: PrintWriter? = openOptionalDynamicWhitelistJournal() + + private fun openOptionalDynamicWhitelistJournal(): PrintWriter? { + val fileName = System.getenv("WHITELIST_FILE") + if (fileName != null && fileName.isNotEmpty()) { + try { + return PrintWriter(Files.newBufferedWriter(Paths.get(fileName), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE), true) + } catch(ioEx: Exception) { + log.error("Could not open/create whitelist journal file for append: $fileName", ioEx) + } + } + return null + } + } + + private val locallySeen: MutableSet = mutableSetOf() + private val alreadySeen: MutableSet get() = if (global) globallySeen else locallySeen + + override fun hasListed(type: Class<*>): Boolean { + if (type.name !in alreadySeen && !delegate.hasListed(type)) { + alreadySeen += type.name + val className = Util.className(type) + log.warn("Dynamically whitelisted class $className") + if (journalWriter != null) { + journalWriter.println(className) + } + } + return true + } + + override fun add(entry: Class<*>) { + if (delegate is MutableClassWhitelist) { + delegate.add(entry) + } else { + throw UnsupportedOperationException("Cannot add to whitelist since delegate whitelist is not mutable.") + } + } +} + diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaSerializable.kt b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializable.kt new file mode 100644 index 0000000000..ff90f2a462 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaSerializable.kt @@ -0,0 +1,19 @@ +package net.corda.core.serialization + +import java.lang.annotation.Inherited + +/** + * This annotation is a marker to indicate that a class is permitted and intended to be serialized as part of Node messaging. + * + * Strictly speaking, it is critical to identifying that a class is intended to be deserialized by the node, to avoid + * a security compromise later when a vulnerability is discovered in the deserialisation of a class that just happens to + * be on the classpath, perhaps from a 3rd party library, as has been witnessed elsewhere. + * + * It also makes it possible for a code reviewer to clearly identify the classes that can be passed on the wire. + * + * TODO: As we approach a long term wire format, this annotation will only be permitted on classes that meet certain criteria. + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Inherited +annotation class CordaSerializable \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt new file mode 100644 index 0000000000..a7157a733d --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt @@ -0,0 +1,74 @@ +package net.corda.core.serialization + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.util.MapReferenceResolver +import de.javakaffee.kryoserializers.ArraysAsListSerializer +import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer +import de.javakaffee.kryoserializers.guava.* +import net.corda.core.crypto.CompositeKey +import net.corda.core.node.CordaPluginRegistry +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction +import net.corda.core.utilities.NonEmptySet +import net.corda.core.utilities.NonEmptySetSerializer +import net.i2p.crypto.eddsa.EdDSAPrivateKey +import net.i2p.crypto.eddsa.EdDSAPublicKey +import org.objenesis.strategy.StdInstantiatorStrategy +import java.io.BufferedInputStream +import java.util.* + +object DefaultKryoCustomizer { + private val pluginRegistries: List by lazy { + // No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors. + val unusedKryo = Kryo(makeStandardClassResolver(), MapReferenceResolver()) + val customization = KryoSerializationCustomization(unusedKryo) + ServiceLoader.load(CordaPluginRegistry::class.java).toList().filter { it.customizeSerialization(customization) } + } + + // TODO: move all register() to addDefaultSerializer() + fun customize(kryo: Kryo): Kryo { + return kryo.apply { + // Allow construction of objects using a JVM backdoor that skips invoking the constructors, if there is no + // no-arg constructor available. + instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy()) + + register(Arrays.asList("").javaClass, ArraysAsListSerializer()) + register(SignedTransaction::class.java, ImmutableClassSerializer(SignedTransaction::class)) + register(WireTransaction::class.java, WireTransactionSerializer) + register(SerializedBytes::class.java, SerializedBytesSerializer) + + UnmodifiableCollectionsSerializer.registerSerializers(this) + ImmutableListSerializer.registerSerializers(this) + ImmutableSetSerializer.registerSerializers(this) + ImmutableSortedSetSerializer.registerSerializers(this) + ImmutableMapSerializer.registerSerializers(this) + ImmutableMultimapSerializer.registerSerializers(this) + + register(BufferedInputStream::class.java, InputStreamSerializer) + register(Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream"), InputStreamSerializer) + + noReferencesWithin() + + register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer) + register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer) + + // Using a custom serializer for compactness + register(CompositeKey.Node::class.java, CompositeKeyNodeSerializer) + register(CompositeKey.Leaf::class.java, CompositeKeyLeafSerializer) + + // Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway. + register(Array::class, read = { kryo, input -> emptyArray() }, write = { kryo, output, obj -> }) + + // This ensures a NonEmptySetSerializer is constructed with an initial value. + register(NonEmptySet::class.java, NonEmptySetSerializer) + + /** This ensures any kotlin objects that implement [DeserializeAsKotlinObjectDef] are read back in as singletons. */ + addDefaultSerializer(DeserializeAsKotlinObjectDef::class.java, KotlinObjectSerializer) + + addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer()) + + val customization = KryoSerializationCustomization(this) + pluginRegistries.forEach { it.customizeSerialization(customization) } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt index 099ca7ffde..310d15f5c4 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt @@ -1,38 +1,25 @@ package net.corda.core.serialization -import co.paralleluniverse.fibers.Fiber -import co.paralleluniverse.io.serialization.kryo.KryoSerializer -import com.esotericsoftware.kryo.Kryo -import com.esotericsoftware.kryo.Kryo.DefaultInstantiatorStrategy -import com.esotericsoftware.kryo.KryoException -import com.esotericsoftware.kryo.Registration -import com.esotericsoftware.kryo.Serializer +import com.esotericsoftware.kryo.* import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.serializers.JavaSerializer import com.esotericsoftware.kryo.serializers.MapSerializer -import de.javakaffee.kryoserializers.ArraysAsListSerializer -import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer -import de.javakaffee.kryoserializers.guava.* +import com.esotericsoftware.kryo.util.MapReferenceResolver import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.node.AttachmentsClassLoader import net.corda.core.node.services.AttachmentStorage -import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.NonEmptySet -import net.corda.core.utilities.NonEmptySetSerializer import net.i2p.crypto.eddsa.EdDSAPrivateKey import net.i2p.crypto.eddsa.EdDSAPublicKey import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec -import org.objenesis.strategy.StdInstantiatorStrategy import java.io.* import java.lang.reflect.InvocationTargetException import java.nio.file.Files import java.nio.file.Path import java.security.PublicKey -import java.time.Instant import java.util.* import javax.annotation.concurrent.ThreadSafe import kotlin.reflect.* @@ -64,16 +51,25 @@ import kotlin.reflect.jvm.javaType * in invalid states, thus violating system invariants. It isn't designed to handle malicious streams and therefore, * isn't usable beyond the prototyping stage. But that's fine: we can revisit serialisation technologies later after * a formal evaluation process. + * + * We now distinguish between internal, storage related Kryo and external, network facing Kryo. We presently use + * some non-whitelisted classes as part of internal storage. + * TODO: eliminate internal, storage related whitelist issues, such as private keys in blob storage. */ // A convenient instance of Kryo pre-configured with some useful things. Used as a default by various functions. -val THREAD_LOCAL_KRYO: ThreadLocal = ThreadLocal.withInitial { createKryo() } +private val THREAD_LOCAL_KRYO: ThreadLocal = ThreadLocal.withInitial { createKryo() } +// Same again, but this has whitelisting turned off for internal storage use only. +private val INTERNAL_THREAD_LOCAL_KRYO: ThreadLocal = ThreadLocal.withInitial { createInternalKryo() } + +fun threadLocalP2PKryo(): Kryo = THREAD_LOCAL_KRYO.get() +fun threadLocalStorageKryo(): Kryo = INTERNAL_THREAD_LOCAL_KRYO.get() /** * A type safe wrapper around a byte array that contains a serialised object. You can call [SerializedBytes.deserialize] * to get the original object back. */ -class SerializedBytes(bytes: ByteArray) : OpaqueBytes(bytes) { +class SerializedBytes(bytes: ByteArray, val internalOnly: Boolean = false) : OpaqueBytes(bytes) { // It's OK to use lazy here because SerializedBytes is configured to use the ImmutableClassSerializer. val hash: SecureHash by lazy { bytes.sha256() } @@ -81,20 +77,20 @@ class SerializedBytes(bytes: ByteArray) : OpaqueBytes(bytes) { } // Some extension functions that make deserialisation convenient and provide auto-casting of the result. -fun ByteArray.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): T { +fun ByteArray.deserialize(kryo: Kryo = threadLocalP2PKryo()): T { @Suppress("UNCHECKED_CAST") return kryo.readClassAndObject(Input(this)) as T } -fun OpaqueBytes.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): T { +fun OpaqueBytes.deserialize(kryo: Kryo = threadLocalP2PKryo()): T { return this.bytes.deserialize(kryo) } // The more specific deserialize version results in the bytes being cached, which is faster. @JvmName("SerializedBytesWireTransaction") -fun SerializedBytes.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): WireTransaction = WireTransaction.deserialize(this, kryo) +fun SerializedBytes.deserialize(kryo: Kryo = threadLocalP2PKryo()): WireTransaction = WireTransaction.deserialize(this, kryo) -fun SerializedBytes.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): T = bytes.deserialize(kryo) +fun SerializedBytes.deserialize(kryo: Kryo = if (internalOnly) threadLocalStorageKryo() else threadLocalP2PKryo()): T = bytes.deserialize(kryo) /** * A serialiser that avoids writing the wrapper class to the byte stream, thus ensuring [SerializedBytes] is a pure @@ -115,12 +111,12 @@ object SerializedBytesSerializer : Serializer>() { * Can be called on any object to convert it to a byte array (wrapped by [SerializedBytes]), regardless of whether * the type is marked as serializable or was designed for it (so be careful!). */ -fun T.serialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): SerializedBytes { +fun T.serialize(kryo: Kryo = threadLocalP2PKryo(), internalOnly: Boolean = false): SerializedBytes { val stream = ByteArrayOutputStream() Output(stream).use { kryo.writeClassAndObject(it, this) } - return SerializedBytes(stream.toByteArray()) + return SerializedBytes(stream.toByteArray(), internalOnly) } /** @@ -261,6 +257,7 @@ fun Input.readBytesWithLength(): ByteArray { } /** Thrown during deserialisation to indicate that an attachment needed to construct the [WireTransaction] is not found */ +@CordaSerializable class MissingAttachmentsException(val ids: List) : Exception() /** A serialisation engine that knows how to deserialise code inside a sandbox */ @@ -389,66 +386,55 @@ object KotlinObjectSerializer : Serializer() { override fun write(kryo: Kryo, output: Output, obj: DeserializeAsKotlinObjectDef) {} } -fun createKryo(k: Kryo = Kryo()): Kryo { - return k.apply { - // Allow any class to be deserialized (this is insecure but for prototyping we don't care) - isRegistrationRequired = false - // Allow construction of objects using a JVM backdoor that skips invoking the constructors, if there is no - // no-arg constructor available. - instantiatorStrategy = DefaultInstantiatorStrategy(StdInstantiatorStrategy()) +// No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors. +fun createInternalKryo(k: Kryo = CordaKryo(makeNoWhitelistClassResolver())): Kryo { + return DefaultKryoCustomizer.customize(k) +} - register(Arrays.asList("").javaClass, ArraysAsListSerializer()) +// No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors. +fun createKryo(k: Kryo = CordaKryo(makeStandardClassResolver())): Kryo { + return DefaultKryoCustomizer.customize(k) +} - // Because we like to stick a Kryo object in a ThreadLocal to speed things up a bit, we can end up trying to - // serialise the Kryo object itself when suspending a fiber. That's dumb, useless AND can cause crashes, so - // we avoid it here. - register(Kryo::class, - read = { kryo, input -> createKryo((Fiber.getFiberSerializer() as KryoSerializer).kryo) }, - write = { kryo, output, obj -> } - ) +/** + * We need to disable whitelist checking during calls from our Kryo code to register a serializer, since it checks + * for existing registrations and then will enter our [CordaClassResolver.getRegistration] method. + */ +open class CordaKryo(classResolver: ClassResolver) : Kryo(classResolver, MapReferenceResolver()) { + override fun register(type: Class<*>?): Registration { + (classResolver as? CordaClassResolver)?.disableWhitelist() + try { + return super.register(type) + } finally { + (classResolver as? CordaClassResolver)?.enableWhitelist() + } + } - register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer) - register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer) - register(Instant::class.java, ReferencesAwareJavaSerializer) + override fun register(type: Class<*>?, id: Int): Registration { + (classResolver as? CordaClassResolver)?.disableWhitelist() + try { + return super.register(type, id) + } finally { + (classResolver as? CordaClassResolver)?.enableWhitelist() + } + } - // Using a custom serializer for compactness - register(CompositeKey.Node::class.java, CompositeKeyNodeSerializer) - register(CompositeKey.Leaf::class.java, CompositeKeyLeafSerializer) + override fun register(type: Class<*>?, serializer: Serializer<*>?): Registration { + (classResolver as? CordaClassResolver)?.disableWhitelist() + try { + return super.register(type, serializer) + } finally { + (classResolver as? CordaClassResolver)?.enableWhitelist() + } + } - // Some classes have to be handled with the ImmutableClassSerializer because they need to have their - // constructors be invoked (typically for lazy members). - register(SignedTransaction::class.java, ImmutableClassSerializer(SignedTransaction::class)) - - // This class has special handling. - register(WireTransaction::class.java, WireTransactionSerializer) - - // This ensures a SerializedBytes wrapper is written out as just a byte array. - register(SerializedBytes::class.java, SerializedBytesSerializer) - - addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer()) - - // This is required to make all the unit tests pass - register(AnonymousParty::class.java) - register(Party::class.java) - - // This ensures a NonEmptySetSerializer is constructed with an initial value. - register(NonEmptySet::class.java, NonEmptySetSerializer) - - register(Array::class, read = { kryo, input -> emptyArray() }, write = { kryo, output, o -> }) - - /** This ensures any kotlin objects that implement [DeserializeAsKotlinObjectDef] are read back in as singletons. */ - addDefaultSerializer(DeserializeAsKotlinObjectDef::class.java, KotlinObjectSerializer) - - addDefaultSerializer(BufferedInputStream::class.java, InputStreamSerializer) - - UnmodifiableCollectionsSerializer.registerSerializers(k) - ImmutableListSerializer.registerSerializers(k) - ImmutableSetSerializer.registerSerializers(k) - ImmutableSortedSetSerializer.registerSerializers(k) - ImmutableMapSerializer.registerSerializers(k) - ImmutableMultimapSerializer.registerSerializers(k) - - noReferencesWithin() + override fun register(registration: Registration?): Registration { + (classResolver as? CordaClassResolver)?.disableWhitelist() + try { + return super.register(registration) + } finally { + (classResolver as? CordaClassResolver)?.enableWhitelist() + } } } diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomization.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomization.kt new file mode 100644 index 0000000000..08d497589e --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationCustomization.kt @@ -0,0 +1,13 @@ +package net.corda.core.serialization + +import com.esotericsoftware.kryo.Kryo + +interface SerializationCustomization { + fun addToWhitelist(type: Class<*>) +} + +class KryoSerializationCustomization(val kryo: Kryo) : SerializationCustomization { + override fun addToWhitelist(type: Class<*>) { + kryo.addToWhitelist(type) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt index 0c7248ba9e..e2e0a16b4d 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationToken.kt @@ -22,6 +22,7 @@ import java.util.* * * This models a similar pattern to the readReplace/writeReplace methods in Java serialization. */ +@CordaSerializable interface SerializeAsToken { fun toToken(context: SerializeAsTokenContext): SerializationToken } @@ -100,6 +101,7 @@ class SerializeAsTokenContext(toBeTokenized: Any, kryo: Kryo = createKryo()) { * A class representing a [SerializationToken] for some object that is not serializable but can be looked up * (when deserialized) via just the class name. */ +@CordaSerializable data class SingletonSerializationToken private constructor(private val className: String) : SerializationToken { constructor(toBeTokenized: SerializeAsToken) : this(toBeTokenized.javaClass.name) diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 4a548c0c46..c4d059b1c9 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -4,6 +4,7 @@ import net.corda.core.contracts.* import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Party import net.corda.core.crypto.SecureHash +import net.corda.core.serialization.CordaSerializable /** * A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations: @@ -16,6 +17,7 @@ import net.corda.core.crypto.SecureHash * * All the above refer to inputs using a (txhash, output index) pair. */ +@CordaSerializable class LedgerTransaction( /** The resolved input states which will be consumed/invalidated by the execution of this transaction. */ override val inputs: List>, diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt index 3d17ce1b85..96609692c0 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -2,6 +2,7 @@ package net.corda.core.transactions import net.corda.core.contracts.* import net.corda.core.crypto.* +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.createKryo import net.corda.core.serialization.extendKryoHash import net.corda.core.serialization.serialize @@ -67,6 +68,7 @@ interface TraversableTransaction { * Class that holds filtered leaves for a partial Merkle transaction. We assume mixed leaf types, notice that every * field from [WireTransaction] can be used in [PartialMerkleTree] calculation. */ +@CordaSerializable class FilteredLeaves( override val inputs: List, override val attachments: List, @@ -98,6 +100,7 @@ class FilteredLeaves( * @param filteredLeaves Leaves included in a filtered transaction. * @param partialMerkleTree Merkle branch needed to verify filteredLeaves. */ +@CordaSerializable class FilteredTransaction private constructor( val rootHash: SecureHash, val filteredLeaves: FilteredLeaves, diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index 4a8d939278..d26ff2b9ad 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -8,6 +8,7 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.crypto.signWithECDSA import net.corda.core.node.ServiceHub +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import java.security.KeyPair import java.security.SignatureException @@ -41,6 +42,7 @@ data class SignedTransaction(val txBits: SerializedBytes, */ override val id: SecureHash get() = tx.id + @CordaSerializable class SignaturesMissingException(val missing: Set, val descriptions: List, override val id: SecureHash) : NamedByHash, SignatureException() { override fun toString(): String { return "Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.joinToString()}" diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index 1897bc070b..0fac9a2fc4 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -9,9 +9,9 @@ import net.corda.core.crypto.SecureHash import net.corda.core.indexOfOrThrow import net.corda.core.node.ServiceHub import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.THREAD_LOCAL_KRYO import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.core.serialization.threadLocalP2PKryo import net.corda.core.utilities.Emoji import java.security.PublicKey @@ -45,7 +45,7 @@ class WireTransaction( override val id: SecureHash by lazy { merkleTree.hash } companion object { - fun deserialize(data: SerializedBytes, kryo: Kryo = THREAD_LOCAL_KRYO.get()): WireTransaction { + fun deserialize(data: SerializedBytes, kryo: Kryo = threadLocalP2PKryo()): WireTransaction { val wtx = data.bytes.deserialize(kryo) wtx.cachedBytes = data return wtx diff --git a/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt b/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt index 39514b8ed3..924042f586 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt @@ -1,6 +1,7 @@ package net.corda.core.utilities import net.corda.core.TransientProperty +import net.corda.core.serialization.CordaSerializable import rx.Observable import rx.Subscription import rx.subjects.BehaviorSubject @@ -32,7 +33,9 @@ import java.util.* * A progress tracker is *not* thread safe. You may move events from the thread making progress to another thread by * using the [Observable] subscribeOn call. */ +@CordaSerializable class ProgressTracker(vararg steps: Step) { + @CordaSerializable sealed class Change { class Position(val tracker: ProgressTracker, val newStep: Step) : Change() { override fun toString() = newStep.label @@ -48,6 +51,7 @@ class ProgressTracker(vararg steps: Step) { } /** The superclass of all step objects. */ + @CordaSerializable open class Step(open val label: String) { open val changes: Observable get() = Observable.empty() open fun childProgressTracker(): ProgressTracker? = null @@ -81,6 +85,7 @@ class ProgressTracker(vararg steps: Step) { // This field won't be serialized. private val _changes by TransientProperty { PublishSubject.create() } + @CordaSerializable private data class Child(val tracker: ProgressTracker, @Transient val subscription: Subscription?) private val childProgressTrackers = HashMap() diff --git a/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt b/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt index 607c16bede..a4412bf88f 100644 --- a/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt @@ -11,6 +11,7 @@ import net.corda.core.crypto.signWithECDSA import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.node.recordTransactions +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.ProgressTracker @@ -29,6 +30,7 @@ abstract class AbstractStateReplacementFlow { * * @param M the type of a class representing proposed modification by the instigator. */ + @CordaSerializable data class Proposal(val stateRef: StateRef, val modification: M, val stx: SignedTransaction) /** diff --git a/core/src/main/kotlin/net/corda/flows/BroadcastTransactionFlow.kt b/core/src/main/kotlin/net/corda/flows/BroadcastTransactionFlow.kt index eec9561040..bc2528bfb3 100644 --- a/core/src/main/kotlin/net/corda/flows/BroadcastTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/BroadcastTransactionFlow.kt @@ -3,6 +3,7 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.Party import net.corda.core.flows.FlowLogic +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction @@ -17,6 +18,7 @@ import net.corda.core.transactions.SignedTransaction */ class BroadcastTransactionFlow(val notarisedTransaction: SignedTransaction, val participants: Set) : FlowLogic() { + @CordaSerializable data class NotifyTxRequest(val tx: SignedTransaction) @Suspendable diff --git a/core/src/main/kotlin/net/corda/flows/FetchDataFlow.kt b/core/src/main/kotlin/net/corda/flows/FetchDataFlow.kt index 1661476aae..5655b14beb 100644 --- a/core/src/main/kotlin/net/corda/flows/FetchDataFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/FetchDataFlow.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.Party import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic +import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.unwrap import net.corda.flows.FetchDataFlow.DownloadedVsRequestedDataMismatch @@ -32,11 +33,18 @@ abstract class FetchDataFlow( protected val requests: Set, protected val otherSide: Party) : FlowLogic>() { + @CordaSerializable class DownloadedVsRequestedDataMismatch(val requested: SecureHash, val got: SecureHash) : IllegalArgumentException() + + @CordaSerializable class DownloadedVsRequestedSizeMismatch(val requested: Int, val got: Int) : IllegalArgumentException() + class HashNotFound(val requested: SecureHash) : FlowException() + @CordaSerializable data class Request(val hashes: List) + + @CordaSerializable data class Result(val fromDisk: List, val downloaded: List) @Suspendable diff --git a/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt index 2db11e00a3..0878dee1b3 100644 --- a/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt @@ -9,6 +9,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.node.services.TimestampChecker import net.corda.core.node.services.UniquenessException import net.corda.core.node.services.UniquenessProvider +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker @@ -161,6 +162,7 @@ class NotaryException(val error: NotaryError) : FlowException() { override fun toString() = "${super.toString()}: Error response from Notary - $error" } +@CordaSerializable sealed class NotaryError { class Conflict(val txId: SecureHash, val conflict: SignedData) : NotaryError() { override fun toString() = "One or more input states for transaction $txId have been used in another transaction" diff --git a/core/src/main/kotlin/net/corda/flows/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/flows/ResolveTransactionsFlow.kt index f15c614155..969f3c18f9 100644 --- a/core/src/main/kotlin/net/corda/flows/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/ResolveTransactionsFlow.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.Party import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.node.recordTransactions +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction @@ -67,6 +68,7 @@ class ResolveTransactionsFlow(private val txHashes: Set, } + @CordaSerializable class ExcessivelyLargeTransactionGraph() : Exception() // Transactions to verify after the dependencies. diff --git a/core/src/main/kotlin/net/corda/flows/ServiceRequestMessage.kt b/core/src/main/kotlin/net/corda/flows/ServiceRequestMessage.kt index b59bb5a227..95db4bfd9b 100644 --- a/core/src/main/kotlin/net/corda/flows/ServiceRequestMessage.kt +++ b/core/src/main/kotlin/net/corda/flows/ServiceRequestMessage.kt @@ -3,10 +3,12 @@ package net.corda.flows import com.google.common.util.concurrent.ListenableFuture import net.corda.core.messaging.* import net.corda.core.node.services.DEFAULT_SESSION_ID +import net.corda.core.serialization.CordaSerializable /** * Abstract superclass for request messages sent to services which expect a reply. */ +@CordaSerializable interface ServiceRequestMessage { val sessionID: Long val replyTo: SingleMessageRecipient diff --git a/core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt b/core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt index 6bf7d0cd99..886b1d1f21 100644 --- a/core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt @@ -10,6 +10,7 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.recordTransactions import net.corda.core.node.services.ServiceType import net.corda.core.seconds +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction @@ -31,17 +32,21 @@ import java.security.KeyPair */ object TwoPartyDealFlow { + @CordaSerializable class DealMismatchException(val expectedDeal: ContractState, val actualDeal: ContractState) : Exception() { override fun toString() = "The submitted deal didn't match the expected: $expectedDeal vs $actualDeal" } + @CordaSerializable class DealRefMismatchException(val expectedDeal: StateRef, val actualDeal: StateRef) : Exception() { override fun toString() = "The submitted deal didn't match the expected: $expectedDeal vs $actualDeal" } // This object is serialised to the network and is the first flow message the seller sends to the buyer. + @CordaSerializable data class Handshake(val payload: T, val publicKey: CompositeKey) + @CordaSerializable class SignaturesFromPrimary(val sellerSig: DigitalSignature.WithKey, val notarySigs: List) /** @@ -263,7 +268,7 @@ object TwoPartyDealFlow { @Suspendable protected abstract fun assembleSharedTX(handshake: Handshake): Pair> } - + @CordaSerializable data class AutoOffer(val notary: Party, val dealBeingOffered: DealState) diff --git a/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt b/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt index 38f6f54a82..f37a1680c1 100644 --- a/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt +++ b/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt @@ -207,6 +207,7 @@ class AttachmentClassLoaderTests { val kryo = createKryo() kryo.classLoader = cl + kryo.addToWhitelist(contract.javaClass) val state2 = bytes.deserialize(kryo) assert(state2.javaClass.classLoader is AttachmentsClassLoader) @@ -214,6 +215,7 @@ class AttachmentClassLoaderTests { } // top level wrapper + @CordaSerializable class Data(val contract: Contract) @Test @@ -222,7 +224,9 @@ class AttachmentClassLoaderTests { assertNotNull(data.contract) - val bytes = data.serialize() + val kryo2 = createKryo() + kryo2.addToWhitelist(data.contract.javaClass) + val bytes = data.serialize(kryo2) val storage = MockAttachmentStorage() @@ -234,6 +238,7 @@ class AttachmentClassLoaderTests { val kryo = createKryo() kryo.classLoader = cl + kryo.addToWhitelist(Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, cl)) val state2 = bytes.deserialize(kryo) assertEquals(cl, state2.contract.javaClass.classLoader) @@ -259,6 +264,9 @@ class AttachmentClassLoaderTests { val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY) val storage = MockAttachmentStorage() val kryo = createKryo() + kryo.addToWhitelist(contract.javaClass) + kryo.addToWhitelist(Class.forName("net.corda.contracts.isolated.AnotherDummyContract\$State", true, child)) + kryo.addToWhitelist(Class.forName("net.corda.contracts.isolated.AnotherDummyContract\$Commands\$Create", true, child)) // todo - think about better way to push attachmentStorage down to serializer kryo.attachmentStorage = storage diff --git a/core/src/test/kotlin/net/corda/core/serialization/CordaClassResolverTests.kt b/core/src/test/kotlin/net/corda/core/serialization/CordaClassResolverTests.kt new file mode 100644 index 0000000000..bd95d319d9 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/serialization/CordaClassResolverTests.kt @@ -0,0 +1,161 @@ +package net.corda.core.serialization + +import com.esotericsoftware.kryo.* +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import com.esotericsoftware.kryo.util.MapReferenceResolver +import net.corda.core.node.AttachmentClassLoaderTests +import net.corda.core.node.AttachmentsClassLoader +import net.corda.core.node.services.AttachmentStorage +import net.corda.testing.node.MockAttachmentStorage +import org.junit.Test + +@CordaSerializable +enum class Foo { + Bar { + override val value = 0 + }, + Stick { + override val value = 1 + }; + + abstract val value: Int +} + +@CordaSerializable +open class Element + +open class SubElement : Element() + +class SubSubElement : SubElement() + +abstract class AbstractClass + +interface Interface + +@CordaSerializable +interface SerializableInterface + +interface SerializableSubInterface : SerializableInterface + +class NotSerializable + +class SerializableViaInterface : SerializableInterface + +open class SerializableViaSubInterface : SerializableSubInterface + +class SerializableViaSuperSubInterface : SerializableViaSubInterface() + + +@CordaSerializable +class CustomSerializable : KryoSerializable { + override fun read(kryo: Kryo?, input: Input?) { + } + + override fun write(kryo: Kryo?, output: Output?) { + } +} + +@CordaSerializable +@DefaultSerializer(DefaultSerializableSerializer::class) +class DefaultSerializable + +class DefaultSerializableSerializer : Serializer() { + override fun write(kryo: Kryo, output: Output, obj: DefaultSerializable) { + } + + override fun read(kryo: Kryo, input: Input, type: Class): DefaultSerializable { + return DefaultSerializable() + } +} + +class CordaClassResolverTests { + @Test + fun `Annotation on enum works for specialised entries`() { + CordaClassResolver(EmptyWhitelist).getRegistration(Foo.Bar::class.java) + } + + @Test + fun `Annotation on array element works`() { + val values = arrayOf(Element()) + CordaClassResolver(EmptyWhitelist).getRegistration(values.javaClass) + } + + @Test + fun `Annotation not needed on abstract class`() { + CordaClassResolver(EmptyWhitelist).getRegistration(AbstractClass::class.java) + } + + @Test + fun `Annotation not needed on interface`() { + CordaClassResolver(EmptyWhitelist).getRegistration(Interface::class.java) + } + + @Test + fun `Calling register method on modified Kryo does not consult the whitelist`() { + val kryo = CordaKryo(CordaClassResolver(EmptyWhitelist)) + kryo.register(NotSerializable::class.java) + } + + @Test(expected = KryoException::class) + fun `Calling register method on unmodified Kryo does consult the whitelist`() { + val kryo = Kryo(CordaClassResolver(EmptyWhitelist), MapReferenceResolver()) + kryo.register(NotSerializable::class.java) + } + + @Test(expected = KryoException::class) + fun `Annotation is needed without whitelisting`() { + CordaClassResolver(EmptyWhitelist).getRegistration(NotSerializable::class.java) + } + + @Test + fun `Annotation is not needed with whitelisting`() { + val resolver = CordaClassResolver(GlobalTransientClassWhiteList(EmptyWhitelist)) + (resolver.whitelist as MutableClassWhitelist).add(NotSerializable::class.java) + resolver.getRegistration(NotSerializable::class.java) + } + + @Test + fun `Annotation not needed on Object`() { + CordaClassResolver(EmptyWhitelist).getRegistration(Object::class.java) + } + + @Test + fun `Annotation not needed on primitive`() { + CordaClassResolver(EmptyWhitelist).getRegistration(Integer.TYPE) + } + + @Test(expected = KryoException::class) + fun `Annotation does not work for custom serializable`() { + CordaClassResolver(EmptyWhitelist).getRegistration(CustomSerializable::class.java) + } + + @Test(expected = KryoException::class) + fun `Annotation does not work in conjunction with Kryo annotation`() { + CordaClassResolver(EmptyWhitelist).getRegistration(DefaultSerializable::class.java) + } + + private fun importJar(storage: AttachmentStorage) = AttachmentClassLoaderTests.ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it) } + + @Test(expected = KryoException::class) + fun `Annotation does not work in conjunction with AttachmentClassLoader annotation`() { + val storage = MockAttachmentStorage() + val attachmentHash = importJar(storage) + val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }) + val attachedClass = Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, classLoader) + CordaClassResolver(EmptyWhitelist).getRegistration(attachedClass) + } + + @Test + fun `Annotation is inherited from interfaces`() { + CordaClassResolver(EmptyWhitelist).getRegistration(SerializableViaInterface::class.java) + CordaClassResolver(EmptyWhitelist).getRegistration(SerializableViaSubInterface::class.java) + } + + @Test + fun `Annotation is inherited from superclass`() { + CordaClassResolver(EmptyWhitelist).getRegistration(SubElement::class.java) + CordaClassResolver(EmptyWhitelist).getRegistration(SubSubElement::class.java) + CordaClassResolver(EmptyWhitelist).getRegistration(SerializableViaSuperSubInterface::class.java) + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt index d0ace823e7..f56670360b 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt @@ -94,9 +94,11 @@ class KryoTests { assertEquals(-1, readRubbishStream.read()) } + @CordaSerializable private data class Person(val name: String, val birthday: Instant?) @Suppress("unused") + @CordaSerializable private class Cyclic(val value: Int) { val thisInstance = this override fun equals(other: Any?): Boolean = (this === other) || (other is Cyclic && this.value == other.value) diff --git a/core/src/test/kotlin/net/corda/core/serialization/SerializationTokenTest.kt b/core/src/test/kotlin/net/corda/core/serialization/SerializationTokenTest.kt index eca0ee8c8d..23ab02a725 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/SerializationTokenTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/SerializationTokenTest.kt @@ -15,7 +15,7 @@ class SerializationTokenTest { @Before fun setup() { - kryo = THREAD_LOCAL_KRYO.get() + kryo = threadLocalStorageKryo() } @After diff --git a/core/src/test/kotlin/net/corda/core/utilities/ProgressTrackerTest.kt b/core/src/test/kotlin/net/corda/core/utilities/ProgressTrackerTest.kt index ebe7d3e872..01fc410c0d 100644 --- a/core/src/test/kotlin/net/corda/core/utilities/ProgressTrackerTest.kt +++ b/core/src/test/kotlin/net/corda/core/utilities/ProgressTrackerTest.kt @@ -4,7 +4,7 @@ import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.KryoSerializable import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output -import net.corda.core.serialization.createKryo +import net.corda.core.serialization.createInternalKryo import net.corda.core.serialization.serialize import org.junit.Before import org.junit.Test @@ -106,7 +106,7 @@ class ProgressTrackerTest { } } - val kryo = createKryo().apply { + val kryo = createInternalKryo().apply { // This is required to make sure Kryo walks through the auto-generated members for the lambda below. fieldSerializerConfig.isIgnoreSyntheticFields = false } diff --git a/docs/source/clientrpc.rst b/docs/source/clientrpc.rst index 3c27d24a92..8b2256f96d 100644 --- a/docs/source/clientrpc.rst +++ b/docs/source/clientrpc.rst @@ -88,17 +88,15 @@ Wire protocol The client RPC wire protocol is not currently documented. To use it you must use the client library provided. This is likely to change in a future release. -Registering classes with RPC Kryo ---------------------------------- +Whitelisting classes with the Corda node +---------------------------------------- -In the present implementation of the node we use Kryo to generate the *on the wire* representation of contracts states -or any other classes that form part of the RPC arguments or response. To avoid the RPC interface being wide open to all -classes on the classpath, Cordapps will currently have to register any classes or custom serialisers they require with Kryo -if they are not one of those registered by default in ``RPCKryo`` via the plugin architecture. See :doc:`creating-a-cordapp`. -This will require some familiarity with Kryo. An example is shown in :doc:`tutorial-clientrpc-api`. +To avoid the RPC interface being wide open to all +classes on the classpath, Cordapps have to whitelist any classes they require with the serialization framework of Corda, +if they are not one of those whitelisted by default in ``DefaultWhitelist``, via either the plugin architecture or simply +with the annotation ``@CordaSerializable``. See :doc:`creating-a-cordapp` or :doc:`serialization`. An example is shown in :doc:`tutorial-clientrpc-api`. -.. warning:: We will be replacing the use of Kryo in RPC with a stable message format and this will mean that this plugin - customisation point will either go away completely or change. +.. warning:: We will be replacing the use of Kryo in the serialization framework and so additional changes here are likely. .. _CordaRPCClient: api/kotlin/corda/net.corda.client/-corda-r-p-c-client/index.html .. _CordaRPCOps: api/kotlin/corda/net.corda.node.services.messaging/-corda-r-p-c-ops/index.html diff --git a/docs/source/corda-plugins.rst b/docs/source/corda-plugins.rst index 33cb79d963..294a472656 100644 --- a/docs/source/corda-plugins.rst +++ b/docs/source/corda-plugins.rst @@ -91,9 +91,10 @@ extensions to be created, or registered at startup. In particular: functions inside the node, for instance to initiate workflows when certain conditions are met. - e. The ``registerRPCKryoTypes`` function allows custom Kryo serialisers - to be registered and whitelisted for the RPC client interface. For - instance new state types passed to flows started via RPC will need - to be explicitly registered. This will be called at various points on - various threads and needs to be stable and thread safe. + e. The ``customizeSerialization`` function allows classes to be whitelisted + for object serialisation, over and above those tagged with the ``@CordaSerializable`` + annotation. In general the annotation should be preferred. For + instance new state types will need to be explicitly registered. This will be called at + various points on various threads and needs to be stable and thread safe. See + :doc:`serialization`. diff --git a/docs/source/creating-a-cordapp.rst b/docs/source/creating-a-cordapp.rst index f66aa317a3..5286aa6187 100644 --- a/docs/source/creating-a-cordapp.rst +++ b/docs/source/creating-a-cordapp.rst @@ -9,14 +9,15 @@ App plugins .. note:: Currently apps are only supported for JVM languages. -To create an app plugin you must you must extend from `CordaPluginRegistry`_. The JavaDoc contains +To create an app plugin you must extend from `CordaPluginRegistry`_. The JavaDoc contains specific details of the implementation, but you can extend the server in the following ways: 1. Required flows: Specify which flows will be whitelisted for use in your RPC calls. 2. Service plugins: Register your services (see below). 3. Web APIs: You may register your own endpoints under /api/ of the bundled web server. 4. Static web endpoints: You may register your own static serving directories for serving web content from the web server. -5. Registering your additional classes used in RPC. +5. Whitelisting your additional contract, state and other classes for object serialization. Any class that forms part + of a persisted state, that is used in messaging between flows or in RPC needs to be whitelisted. Services -------- diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt index b61c849b56..2e923502f1 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt @@ -1,6 +1,5 @@ package net.corda.docs -import com.esotericsoftware.kryo.Kryo import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.Issued @@ -10,7 +9,9 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.services.ServiceInfo +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes +import net.corda.core.serialization.SerializationCustomization import net.corda.core.transactions.SignedTransaction import net.corda.flows.CashExitFlow import net.corda.flows.CashIssueFlow @@ -132,12 +133,17 @@ fun generateTransactions(proxy: CordaRPCOps) { // END 6 // START 7 +// Not annotated, so need to whitelist manually. data class ExampleRPCValue(val foo: String) +// Annotated, so no need to whitelist manually. +@CordaSerializable +data class ExampleRPCValue2(val bar: Int) + class ExampleRPCCordaPluginRegistry : CordaPluginRegistry() { - override fun registerRPCKryoTypes(kryo: Kryo): Boolean { + override fun customizeSerialization(custom: SerializationCustomization): Boolean { // Add classes like this. - kryo.register(ExampleRPCValue::class.java) + custom.addToWhitelist(ExampleRPCValue::class.java) // You should return true, otherwise your plugin will be ignored for registering classes with Kryo. return true } diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt index 7780be034c..4e9d6a1ea3 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt @@ -14,6 +14,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.node.PluginServiceHub import net.corda.core.node.ServiceHub import net.corda.core.node.services.unconsumedStates +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.unwrap import net.corda.flows.FinalityFlow @@ -27,12 +28,14 @@ object FxTransactionDemoTutorial { } } +@CordaSerializable private data class FxRequest(val tradeId: String, val amount: Amount>, val owner: Party, val counterparty: Party, val notary: Party? = null) +@CordaSerializable private data class FxResponse(val inputs: List>, val outputs: List) diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt index 099dc63b72..1bed23f327 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt @@ -7,6 +7,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.node.PluginServiceHub import net.corda.core.node.ServiceHub import net.corda.core.node.services.linearHeadsOfType +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.unwrap import net.corda.flows.FinalityFlow @@ -32,6 +33,7 @@ inline fun ServiceHub.latest(ref: StateRef): StateAndR // DOCEND 1 // Minimal state model of a manual approval process +@CordaSerializable enum class WorkflowState { NEW, APPROVED, diff --git a/docs/source/flow-state-machines.rst b/docs/source/flow-state-machines.rst index 075057ec72..5380dea569 100644 --- a/docs/source/flow-state-machines.rst +++ b/docs/source/flow-state-machines.rst @@ -117,6 +117,7 @@ each side. } // This object is serialised to the network and is the first flow message the seller sends to the buyer. + @CordaSerializable data class SellerTradeInfo( val assetForSale: StateAndRef, val price: Amount, @@ -188,6 +189,15 @@ and try again. .. note:: Java 9 is likely to remove this pre-marking requirement completely. +Whitelisted classes with the Corda node +--------------------------------------- + +For security reasons, we do not want Corda nodes to be able to receive instances of any class on the classpath +via messaging, since this has been exploited in other Java application containers in the past. Instead, we require +that every class contained in messages is whitelisted. Some classes are whitelisted by default (see ``DefaultWhitelist``), +but others outside of that set need to be whitelisted either by using the annotation ``@CordaSerializable`` or via the +plugin framework. See :doc:`serialization`. You can see above that the ``SellerTradeInfo`` has been annotated. + Starting your flow ------------------ diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index d481554b75..7e9763506e 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -59,6 +59,8 @@ R3 The consortium behind Corda SIMM Standard Initial Margin Model. A way of determining a counterparty's margin payment to another counterparty based on a collection of trades such that, in the event of default, the receiving counterparty has limited exposure. +Serialization + Object serialization is the process of converting objects into a stream of bytes and, deserialization, the reverse process. Service Hub A hub in each Corda node that manages the services upon which other components of the node depend. Services may include facilities for identity management, storage management, network map management etc. Signed Transaction @@ -71,3 +73,5 @@ UTXO Unspent Transaction Output. First introduced by the bitcoin model, an unspent transaction is data that has been output from a transaction but not yet used in another transaction. Verify To confirm that the transaction is valid by ensuring the the outputs are correctly derived from the inputs combined with the command of the transaction. +Whitelisting + To indicate that a class is intended to be passed between nodes or between a node and an RPC client, it is added to a whitelist. This prevents the node presenting a large surface area of all classes in all dependencies of the node as containing possible vulnerabilities. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 650b6f450b..a7c9ab1d9a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -70,6 +70,7 @@ Documentation Contents: :maxdepth: 2 :caption: The Corda node + serialization clientrpc messaging persistence diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 87f9ddcc79..993aabdaab 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -12,6 +12,13 @@ Milestone 9 * Split ``CashFlow`` into individual ``CashIssueFlow``, ``CashPaymentFlow`` and ``CashExitFlow`` flows, so that fine grained permissions can be applied. Added ``CashFlowCommand`` for use-cases where cash flow triggers need to be captured in an object that can be passed around. + * ``CordaPluginRegistry`` method ``registerRPCKryoTypes`` is renamed ``customizeSerialization`` and the argument + types now hide the presence of Kryo. + +* Object Serialization: + + * Consolidated Kryo implementations across RPC and P2P messaging with whitelisting of classes via plugins or with + ``@CordaSerializable`` for added node security. Milestone 8 ----------- diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst new file mode 100644 index 0000000000..8e973559f4 --- /dev/null +++ b/docs/source/serialization.rst @@ -0,0 +1,39 @@ +Object Serialization +==================== + +What is serialization (and deserialization)? +-------------------------------------------- + +Object serialization is the process of converting objects into a stream of bytes and, deserialization, the reverse +process of creating objects from a stream of bytes. It takes place every time nodes pass objects to each other as +messages, when objects are sent to or from RPC clients from the node, and when we store transactions in the database. + +Whitelisting +------------ + +In classic Java serialization, any class on the JVM classpath can be deserialized. This has shown to be a source of exploits +and vulnerabilities by exploiting the large set of 3rd party libraries on the classpath as part of the dependencies of +a JVM application and a carefully crafted stream of bytes to be deserialized. In Corda, we prevent just any class from +being deserialized (and pro-actively during serialization) by insisting that each object's class belongs on a whitelist +of allowed classes. + +Classes get onto the whitelist via one of three mechanisms: + +#. Via the ``@CordaSerializable`` annotation. In order to whitelist a class, this annotation can be present on the + class itself, on any of the super classes or on any interface implemented by the class or super classes or any + interface extended by an interface implemented by the class or superclasses. +#. By returning the class as part of a plugin via the method ``customizeSerialization``. It's important to return + true from this method if you override it, otherwise the plugin will be excluded. See :doc:`corda-plugins`. +#. Via the built in Corda whitelist (see the class ``DefaultWhitelist``). Whilst this is not user editable, it does list + common JDK classes that have been whitelisted for your convenience. + +The annotation is the preferred method for whitelisting. An example is shown in :doc:`tutorial-clientrpc-api`. +It's reproduced here as an example of both ways you can do this for a couple of example classes. + +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt + :language: kotlin + :start-after: START 7 + :end-before: END 7 + +.. note:: Several of the core interfaces at the heart of Corda are already annotated and so any classes that implement + them will automatically be whitelisted. This includes `Contract`, `ContractState` and `CommandData`. diff --git a/docs/source/tutorial-clientrpc-api.rst b/docs/source/tutorial-clientrpc-api.rst index a4cdcd85bd..ea8ace3a6c 100644 --- a/docs/source/tutorial-clientrpc-api.rst +++ b/docs/source/tutorial-clientrpc-api.rst @@ -85,11 +85,11 @@ Now let's try to visualise the transaction graph. We will use a graph drawing li If we run the client with ``Visualise`` we should see a simple random graph being drawn as new transactions are being created. -Registering classes from your CorDapp with RPC Kryo ---------------------------------------------------- +Whitelisting classes from your CorDapp with the Corda node +---------------------------------------------------------- -As described in :doc:`clientrpc`, you currently have to register any additional classes you add that are needed in RPC -requests or responses with the `Kryo` instance RPC uses. Here's an example of how you do this for an example class. +As described in :doc:`clientrpc`, you have to whitelist any additional classes you add that are needed in RPC +requests or responses with the Corda node. Here's an example of both ways you can do this for a couple of example classes. .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt :language: kotlin @@ -98,8 +98,7 @@ requests or responses with the `Kryo` instance RPC uses. Here's an example of h See more on plugins in :doc:`creating-a-cordapp`. -.. warning:: We will be replacing the use of Kryo in RPC with a stable message format and this will mean that this plugin - customisation point will either go away completely or change. +.. warning:: We will be replacing the use of Kryo in the serialization framework and so additional changes here are likely. Security -------- diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/Arrangement.kt b/experimental/src/main/kotlin/net/corda/contracts/universal/Arrangement.kt index 0b4713da58..ee155f05f7 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/Arrangement.kt +++ b/experimental/src/main/kotlin/net/corda/contracts/universal/Arrangement.kt @@ -2,10 +2,12 @@ package net.corda.contracts.universal import net.corda.core.contracts.Frequency import net.corda.core.crypto.Party +import net.corda.core.serialization.CordaSerializable import java.math.BigDecimal import java.time.LocalDate import java.util.* +@CordaSerializable interface Arrangement // A base arrangement with no rights and no obligations. Contract cancellation/termination is a transition to ``Zero``. @@ -30,6 +32,7 @@ data class Obligation(val amount: Perceivable, val currency: Currenc // The ``And`` combinator cannot be root in a arrangement. data class And(val arrangements: Set) : Arrangement +@CordaSerializable data class Action(val name: String, val condition: Perceivable, val arrangement: Arrangement) // An action combinator. This declares a list of named action that can be taken by anyone of the actors given that diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/Perceivable.kt b/experimental/src/main/kotlin/net/corda/contracts/universal/Perceivable.kt index a36bda41bb..582586dbe6 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/Perceivable.kt +++ b/experimental/src/main/kotlin/net/corda/contracts/universal/Perceivable.kt @@ -4,14 +4,17 @@ import net.corda.core.contracts.BusinessCalendar import net.corda.core.contracts.Tenor import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Party +import net.corda.core.serialization.CordaSerializable import java.lang.reflect.Type import java.math.BigDecimal import java.time.Instant import java.time.LocalDate import java.util.* +@CordaSerializable interface Perceivable +@CordaSerializable enum class Comparison { LT, LTE, GT, GTE } @@ -127,6 +130,7 @@ infix fun Perceivable.gte(n: BigDecimal) = perceivableComparison(thi infix fun Perceivable.lte(n: Double) = perceivableComparison(this, Comparison.LTE, const(BigDecimal(n))) infix fun Perceivable.gte(n: Double) = perceivableComparison(this, Comparison.GTE, const(BigDecimal(n))) +@CordaSerializable enum class Operation { PLUS, MINUS, TIMES, DIV } diff --git a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java index 286c1a9f12..98b7a01139 100644 --- a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java @@ -1,22 +1,34 @@ package net.corda.contracts; -import com.google.common.collect.*; -import kotlin.*; -import net.corda.contracts.asset.*; +import com.google.common.collect.ImmutableList; +import kotlin.Pair; +import kotlin.Unit; +import net.corda.contracts.asset.CashKt; import net.corda.core.contracts.*; -import net.corda.core.contracts.TransactionForContract.*; -import net.corda.core.contracts.clauses.*; -import net.corda.core.crypto.*; -import net.corda.core.node.services.*; -import net.corda.core.transactions.*; -import org.jetbrains.annotations.*; +import net.corda.core.contracts.TransactionForContract.InOutGroup; +import net.corda.core.contracts.clauses.AnyOf; +import net.corda.core.contracts.clauses.Clause; +import net.corda.core.contracts.clauses.ClauseVerifier; +import net.corda.core.contracts.clauses.GroupClauseVerifier; +import net.corda.core.crypto.CompositeKey; +import net.corda.core.crypto.CryptoUtilities; +import net.corda.core.crypto.Party; +import net.corda.core.crypto.SecureHash; +import net.corda.core.node.services.VaultService; +import net.corda.core.transactions.TransactionBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.time.*; -import java.util.*; -import java.util.stream.*; +import java.time.Instant; +import java.util.Collections; +import java.util.Currency; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; -import static kotlin.collections.CollectionsKt.*; -import static net.corda.core.contracts.ContractsDSL.*; +import static kotlin.collections.CollectionsKt.single; +import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; +import static net.corda.core.contracts.ContractsDSL.requireThat; /** diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt index 420d6c8657..d121e1ae18 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt @@ -47,6 +47,7 @@ class CommercialPaperLegacy : Contract { interface Commands : CommandData { class Move : TypeOnlyCommandData(), Commands + class Redeem : TypeOnlyCommandData(), Commands // We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP. // However, nothing in the platform enforces that uniqueness: it's up to the issuer. diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt index 621c1add24..c522ce3dda 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt @@ -12,6 +12,7 @@ import net.corda.core.crypto.* import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.Emoji import net.corda.schemas.CashSchemaV1 @@ -74,6 +75,7 @@ class Cash : OnLedgerAsset() { override val requiredCommands: Set> = setOf(Commands.Issue::class.java) } + @CordaSerializable class ConserveAmount : AbstractConserveAmount() } diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt b/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt index 7279cf896a..15f1273b2b 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt @@ -11,6 +11,7 @@ import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Party import net.corda.core.crypto.SecureHash import net.corda.core.crypto.newSecureRandom +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder import java.util.* @@ -86,6 +87,7 @@ class CommodityContract : OnLedgerAsset() } @@ -112,6 +114,7 @@ class CommodityContract : OnLedgerAsset : Contract { * to the state. Most states will not leave the [NORMAL] lifecycle. Note that settled (as an end lifecycle) is * represented by absence of the state on transaction output. */ + @CordaSerializable enum class Lifecycle { /** Default lifecycle state for a contract, in which it can be settled normally */ NORMAL, @@ -242,6 +244,7 @@ class Obligation

: Contract { * * @param P the product the obligation is for payment of. */ + @CordaSerializable data class Terms

( /** The hash of the asset contract we're willing to accept in payment for this debt. */ val acceptableContracts: NonEmptySet, @@ -323,6 +326,7 @@ class Obligation

: Contract { } // Just for grouping + @CordaSerializable interface Commands : FungibleAsset.Commands { /** * Net two or more obligation states together in a close-out netting style. Limited to bilateral netting diff --git a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt index e2664241d3..a31930a6e0 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt @@ -2,7 +2,10 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash -import net.corda.core.contracts.* +import net.corda.core.contracts.Amount +import net.corda.core.contracts.InsufficientBalanceException +import net.corda.core.contracts.TransactionType +import net.corda.core.contracts.issuedBy import net.corda.core.crypto.Party import net.corda.core.node.services.Vault import net.corda.core.node.services.unconsumedStates diff --git a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt index dccba79c55..b96d0938f7 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt @@ -3,7 +3,6 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount -import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.TransactionType import net.corda.core.contracts.issuedBy import net.corda.core.crypto.Party diff --git a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt index 671ac53340..eee0c86dbe 100644 --- a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.Party import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.node.PluginServiceHub +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker @@ -20,6 +21,7 @@ import java.util.* * useful for creation of fake assets. */ object IssuerFlow { + @CordaSerializable data class IssuanceRequestState(val amount: Amount, val issueToParty: Party, val issuerPartyRef: OpaqueBytes) /** diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt index c1cfade1cf..5d3117e3c7 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt @@ -8,6 +8,7 @@ import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.node.NodeInfo import net.corda.core.seconds +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction @@ -39,17 +40,20 @@ object TwoPartyTradeFlow { // and [AbstractStateReplacementFlow]. class UnacceptablePriceException(givenPrice: Amount) : FlowException("Unacceptable price: $givenPrice") + class AssetMismatchException(val expectedTypeName: String, val typeName: String) : FlowException() { override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName" } // This object is serialised to the network and is the first flow message the seller sends to the buyer. + @CordaSerializable data class SellerTradeInfo( val assetForSale: StateAndRef, val price: Amount, val sellerOwnerKey: CompositeKey ) + @CordaSerializable data class SignaturesFromSeller(val sellerSig: DigitalSignature.WithKey, val notarySig: DigitalSignature.WithKey) diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index 8c7a259b8f..fdb1667b3e 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -8,6 +8,7 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.messaging.createMessage import net.corda.core.node.services.DEFAULT_SESSION_ID import net.corda.core.node.services.ServiceInfo +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.flows.ServiceRequestMessage @@ -118,6 +119,7 @@ class P2PMessagingTest : NodeBasedTest() { return net.sendRequest(javaClass.name, request, target) } + @CordaSerializable private data class TestRequest(override val sessionID: Long = random63BitValue(), override val replyTo: SingleMessageRecipient) : ServiceRequestMessage } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/serialization/DefaultWhitelist.kt b/node/src/main/kotlin/net/corda/node/serialization/DefaultWhitelist.kt new file mode 100644 index 0000000000..144caedc1f --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/serialization/DefaultWhitelist.kt @@ -0,0 +1,52 @@ +package net.corda.node.serialization + +import com.esotericsoftware.kryo.KryoException +import com.google.common.net.HostAndPort +import net.corda.core.node.CordaPluginRegistry +import net.corda.core.serialization.SerializationCustomization +import org.apache.activemq.artemis.api.core.SimpleString +import rx.Notification +import java.math.BigDecimal +import java.time.LocalDate +import java.time.Period +import java.util.* + +class DefaultWhitelist : CordaPluginRegistry() { + override fun customizeSerialization(custom: SerializationCustomization): Boolean { + custom.apply { + addToWhitelist(Array(0, {}).javaClass) + addToWhitelist(Notification::class.java) + addToWhitelist(Notification.Kind::class.java) + addToWhitelist(ArrayList::class.java) + addToWhitelist(listOf().javaClass) // EmptyList + addToWhitelist(Pair::class.java) + addToWhitelist(ByteArray::class.java) + addToWhitelist(UUID::class.java) + addToWhitelist(LinkedHashSet::class.java) + addToWhitelist(setOf().javaClass) // EmptySet + addToWhitelist(Currency::class.java) + addToWhitelist(listOf(Unit).javaClass) // SingletonList + addToWhitelist(setOf(Unit).javaClass) // SingletonSet + addToWhitelist(mapOf(Unit to Unit).javaClass) // SingletonSet + addToWhitelist(HostAndPort::class.java) + addToWhitelist(SimpleString::class.java) + addToWhitelist(KryoException::class.java) + addToWhitelist(StringBuffer::class.java) + addToWhitelist(Unit::class.java) + addToWhitelist(java.io.ByteArrayInputStream::class.java) + addToWhitelist(java.lang.Class::class.java) + addToWhitelist(java.math.BigDecimal::class.java) + addToWhitelist(java.security.KeyPair::class.java) + addToWhitelist(java.time.Duration::class.java) + addToWhitelist(java.time.Instant::class.java) + addToWhitelist(java.time.LocalDate::class.java) + addToWhitelist(java.util.Collections.singletonMap("A", "B").javaClass) + addToWhitelist(java.util.HashMap::class.java) + addToWhitelist(java.util.LinkedHashMap::class.java) + addToWhitelist(BigDecimal::class.java) + addToWhitelist(LocalDate::class.java) + addToWhitelist(Period::class.java) + } + return true + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingComponent.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingComponent.kt index 7aae91dc7e..146b0de33b 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingComponent.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingComponent.kt @@ -7,6 +7,7 @@ import net.corda.core.messaging.MessageRecipientGroup import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.read +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.node.services.config.SSLConfiguration import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Inbound @@ -65,6 +66,7 @@ abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() { val hostAndPort: HostAndPort } + @CordaSerializable data class NetworkMapAddress(override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisPeerAddress { override val queueName: String get() = NETWORK_MAP_QUEUE } @@ -80,6 +82,7 @@ abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() { * @param queueName The name of the queue this address is associated with. * @param hostAndPort The address of the node. */ + @CordaSerializable data class NodeAddress(override val queueName: String, override val hostAndPort: HostAndPort) : ArtemisPeerAddress { companion object { fun asPeer(peerIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress { diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCStructures.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCStructures.kt index 45f635fa4e..9c97fe58ff 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCStructures.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCStructures.kt @@ -3,50 +3,22 @@ package net.corda.node.services.messaging import com.esotericsoftware.kryo.Kryo -import com.esotericsoftware.kryo.KryoException import com.esotericsoftware.kryo.Registration import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output -import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture -import de.javakaffee.kryoserializers.ArraysAsListSerializer -import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer -import de.javakaffee.kryoserializers.guava.* -import net.corda.contracts.asset.Cash -import net.corda.core.ErrorOr -import net.corda.core.contracts.* -import net.corda.core.crypto.* import net.corda.core.flows.FlowException -import net.corda.core.flows.IllegalFlowLogicException -import net.corda.core.flows.StateMachineRunId -import net.corda.core.messaging.FlowHandle -import net.corda.core.messaging.StateMachineInfo -import net.corda.core.messaging.StateMachineUpdate -import net.corda.core.node.* -import net.corda.core.node.services.* import net.corda.core.serialization.* import net.corda.core.toFuture import net.corda.core.toObservable -import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.WireTransaction -import net.corda.node.internal.AbstractNode import net.corda.node.services.User import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER -import net.corda.node.services.messaging.ArtemisMessagingComponent.NetworkMapAddress -import net.corda.node.services.statemachine.FlowSessionException -import net.i2p.crypto.eddsa.EdDSAPrivateKey -import net.i2p.crypto.eddsa.EdDSAPublicKey -import org.apache.activemq.artemis.api.core.SimpleString import org.apache.commons.fileupload.MultipartStream -import org.objenesis.strategy.StdInstantiatorStrategy import org.slf4j.Logger import org.slf4j.LoggerFactory import rx.Notification import rx.Observable -import java.io.BufferedInputStream -import java.time.Instant -import java.util.* /** Global RPC logger */ val rpcLog: Logger by lazy { LoggerFactory.getLogger("net.corda.rpc") } @@ -95,6 +67,7 @@ fun requirePermission(permission: String) { * Thrown to indicate a fatal error in the RPC system itself, as opposed to an error generated by the invoked * method. */ +@CordaSerializable open class RPCException(msg: String, cause: Throwable?) : RuntimeException(msg, cause) { constructor(msg: String) : this(msg, null) @@ -112,129 +85,20 @@ object ClassSerializer : Serializer>() { } } +@CordaSerializable class PermissionException(msg: String) : RuntimeException(msg) // The Kryo used for the RPC wire protocol. Every type in the wire protocol is listed here explicitly. // This is annoying to write out, but will make it easier to formalise the wire protocol when the time comes, // because we can see everything we're using in one place. -private class RPCKryo(observableSerializer: Serializer>? = null) : Kryo() { - companion object { - private val pluginRegistries: List by lazy { - val unusedKryo = Kryo() - // Sorting required to give a stable ordering, as Kryo allocates integer tokens for each registered class. - ServiceLoader.load(CordaPluginRegistry::class.java).toList().filter { it.registerRPCKryoTypes(unusedKryo) }.sortedBy { it.javaClass.name } - } - } - +private class RPCKryo(observableSerializer: Serializer>? = null) : CordaKryo(makeStandardClassResolver()) { init { - isRegistrationRequired = true - // Allow construction of objects using a JVM backdoor that skips invoking the constructors, if there is no - // no-arg constructor available. - instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy()) + DefaultKryoCustomizer.customize(this) - register(Arrays.asList("").javaClass, ArraysAsListSerializer()) - register(Instant::class.java, ReferencesAwareJavaSerializer) - register(SignedTransaction::class.java, ImmutableClassSerializer(SignedTransaction::class)) - register(WireTransaction::class.java, WireTransactionSerializer) - register(SerializedBytes::class.java, SerializedBytesSerializer) - register(AnonymousParty::class.java) - register(Party::class.java) - register(Array(0,{}).javaClass) + // RPC specific classes register(Class::class.java, ClassSerializer) - - UnmodifiableCollectionsSerializer.registerSerializers(this) - ImmutableListSerializer.registerSerializers(this) - ImmutableSetSerializer.registerSerializers(this) - ImmutableSortedSetSerializer.registerSerializers(this) - ImmutableMapSerializer.registerSerializers(this) - ImmutableMultimapSerializer.registerSerializers(this) - - register(BufferedInputStream::class.java, InputStreamSerializer) - register(Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream"), InputStreamSerializer) register(MultipartStream.ItemInputStream::class.java, InputStreamSerializer) - - noReferencesWithin() - - register(ErrorOr::class.java) register(MarshalledObservation::class.java, ImmutableClassSerializer(MarshalledObservation::class)) - register(Notification::class.java) - register(Notification.Kind::class.java) - - register(ArrayList::class.java) - register(listOf().javaClass) // EmptyList - register(IllegalStateException::class.java) - register(Pair::class.java) - register(StateMachineUpdate.Added::class.java) - register(StateMachineUpdate.Removed::class.java) - register(StateMachineInfo::class.java) - register(DigitalSignature.WithKey::class.java) - register(DigitalSignature.LegallyIdentifiable::class.java) - register(ByteArray::class.java) - register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer) - register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer) - register(CompositeKey.Leaf::class.java) - register(CompositeKey.Node::class.java) - register(Vault::class.java) - register(Vault.Update::class.java) - register(StateMachineRunId::class.java) - register(StateMachineTransactionMapping::class.java) - register(UUID::class.java) - register(UniqueIdentifier::class.java) - register(LinkedHashSet::class.java) - register(LinkedHashMap::class.java) - register(StateAndRef::class.java) - register(setOf().javaClass) // EmptySet - register(StateRef::class.java) - register(SecureHash.SHA256::class.java) - register(TransactionState::class.java) - register(Cash.State::class.java) - register(Amount::class.java) - register(Issued::class.java) - register(PartyAndReference::class.java) - register(OpaqueBytes::class.java) - register(Currency::class.java) - register(Cash::class.java) - register(Cash.Clauses.ConserveAmount::class.java) - register(listOf(Unit).javaClass) // SingletonList - register(setOf(Unit).javaClass) // SingletonSet - register(ServiceEntry::class.java) - register(NodeInfo::class.java) - register(PhysicalLocation::class.java) - register(NetworkMapCache.MapChange.Added::class.java) - register(NetworkMapCache.MapChange.Removed::class.java) - register(NetworkMapCache.MapChange.Modified::class.java) - register(ArtemisMessagingComponent.NodeAddress::class.java) - register(NetworkMapAddress::class.java) - register(ServiceInfo::class.java) - register(ServiceType.getServiceType("ab", "ab").javaClass) - register(ServiceType.parse("ab").javaClass) - register(WorldCoordinate::class.java) - register(HostAndPort::class.java) - register(SimpleString::class.java) - register(ServiceEntry::class.java) - // Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway. - register(Array::class, read = { kryo, input -> emptyArray() }, write = { kryo, output, obj -> }) - register(FlowException::class.java) - register(FlowSessionException::class.java) - register(IllegalFlowLogicException::class.java) - register(RuntimeException::class.java) - register(IllegalArgumentException::class.java) - register(ArrayIndexOutOfBoundsException::class.java) - register(IndexOutOfBoundsException::class.java) - register(NoSuchElementException::class.java) - register(RPCException::class.java) - register(PermissionException::class.java) - register(Throwable::class.java) - register(FlowHandle::class.java) - register(KryoException::class.java) - register(StringBuffer::class.java) - register(Unit::class.java) - for ((_flow, argumentTypes) in AbstractNode.defaultFlowWhiteList) { - for (type in argumentTypes) { - register(type) - } - } - pluginRegistries.forEach { it.registerRPCKryoTypes(this) } } // TODO: workaround to prevent Observable registration conflict when using plugin registered kyro classes diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt index 0f98f0894b..9a128243f6 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt @@ -16,6 +16,7 @@ import net.corda.core.node.services.DEFAULT_SESSION_ID import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.ServiceType import net.corda.core.random63BitValue +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize @@ -73,27 +74,33 @@ interface NetworkMapService { override val replyTo: SingleMessageRecipient, override val sessionID: Long = random63BitValue()) : ServiceRequestMessage + @CordaSerializable data class FetchMapResponse(val nodes: Collection?, val version: Int) class QueryIdentityRequest(val identity: Party, override val replyTo: SingleMessageRecipient, override val sessionID: Long) : ServiceRequestMessage + @CordaSerializable data class QueryIdentityResponse(val node: NodeInfo?) class RegistrationRequest(val wireReg: WireNodeRegistration, override val replyTo: SingleMessageRecipient, override val sessionID: Long = random63BitValue()) : ServiceRequestMessage + @CordaSerializable data class RegistrationResponse(val success: Boolean) class SubscribeRequest(val subscribe: Boolean, override val replyTo: SingleMessageRecipient, override val sessionID: Long = random63BitValue()) : ServiceRequestMessage + @CordaSerializable data class SubscribeResponse(val confirmed: Boolean) + @CordaSerializable data class Update(val wireReg: WireNodeRegistration, val mapVersion: Int, val replyTo: MessageRecipients) + @CordaSerializable data class UpdateAcknowledge(val mapVersion: Int, val replyTo: MessageRecipients) } @@ -331,6 +338,7 @@ abstract class AbstractNetworkMapService */ // TODO: This might alternatively want to have a node and party, with the node being optional, so registering a node // involves providing both node and paerty, and deregistering a node involves a request with party but no node. +@CordaSerializable class NodeRegistration(val node: NodeInfo, val serial: Long, val type: AddOrRemove, var expires: Instant) { /** * Build a node registration in wire format. @@ -348,6 +356,7 @@ class NodeRegistration(val node: NodeInfo, val serial: Long, val type: AddOrRemo /** * A node registration and its signature as a pair. */ +@CordaSerializable class WireNodeRegistration(raw: SerializedBytes, sig: DigitalSignature.WithKey) : SignedData(raw, sig) { @Throws(IllegalArgumentException::class) override fun verifyData(data: NodeRegistration) { @@ -355,6 +364,7 @@ class WireNodeRegistration(raw: SerializedBytes, sig: DigitalS } } +@CordaSerializable sealed class NodeMapError : Exception() { /** Thrown if the signature on the node info does not match the public key for the identity */ @@ -367,5 +377,8 @@ sealed class NodeMapError : Exception() { class UnknownChangeType : NodeMapError() } +@CordaSerializable data class LastAcknowledgeInfo(val mapVersion: Int) + +@CordaSerializable data class NodeRegistrationInfo(val reg: NodeRegistration, val mapVersion: Int) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt index d01605c8b3..bc066032cc 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt @@ -4,6 +4,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.core.serialization.threadLocalStorageKryo import net.corda.node.services.api.Checkpoint import net.corda.node.services.api.CheckpointStorage import net.corda.node.utilities.* @@ -38,7 +39,7 @@ class DBCheckpointStorage : CheckpointStorage { private val checkpointStorage = synchronizedMap(CheckpointMap()) override fun addCheckpoint(checkpoint: Checkpoint) { - checkpointStorage.put(checkpoint.id, checkpoint.serialize()) + checkpointStorage.put(checkpoint.id, checkpoint.serialize(threadLocalStorageKryo(), true)) } override fun removeCheckpoint(checkpoint: Checkpoint) { diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index 31ba488c61..3402537c81 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -9,6 +9,7 @@ import net.corda.core.* import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash import net.corda.core.node.services.AttachmentStorage +import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.loggerFor import net.corda.node.services.api.AcceptsFileUpload import java.io.FilterInputStream @@ -48,6 +49,7 @@ class NodeAttachmentService(val storePath: Path, metrics: MetricRegistry) : Atta require(storePath.isDirectory()) { "$storePath must be a directory" } } + @CordaSerializable class OnDiskHashMismatch(val file: Path, val actual: SecureHash) : Exception() { override fun toString() = "File $file hashed to $actual: corruption in attachment store?" } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt index 0d6c20be38..d904b1de36 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt @@ -2,8 +2,10 @@ package net.corda.node.services.statemachine import net.corda.core.crypto.Party import net.corda.core.flows.FlowException +import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.UntrustworthyData +@CordaSerializable interface SessionMessage data class SessionInit(val initiatorSessionId: Long, val flowName: String, val firstPayload: Any?) : SessionMessage diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index f308007029..b8ca8685e9 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -369,7 +369,15 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, private fun quasarKryo(): Kryo { val serializer = Fiber.getFiberSerializer(false) as KryoSerializer - return createKryo(serializer.kryo) + return createKryo(serializer.kryo).apply { + // Because we like to stick a Kryo object in a ThreadLocal to speed things up a bit, we can end up trying to + // serialise the Kryo object itself when suspending a fiber. That's dumb, useless AND can cause crashes, so + // we avoid it here. This is checkpointing specific. + register(Kryo::class, + read = { kryo, input -> createKryo((Fiber.getFiberSerializer() as KryoSerializer).kryo) }, + write = { kryo, output, obj -> } + ) + } } private fun createFiber(logic: FlowLogic): FlowStateMachineImpl { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/DistributedImmutableBFTMap.kt b/node/src/main/kotlin/net/corda/node/services/transactions/DistributedImmutableBFTMap.kt index 0ed181d330..2ad2235979 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/DistributedImmutableBFTMap.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/DistributedImmutableBFTMap.kt @@ -1,23 +1,26 @@ package net.corda.node.services.transactions -import bftsmart.tom.ServiceProxy import bftsmart.tom.MessageContext +import bftsmart.tom.ServiceProxy import bftsmart.tom.ServiceReplica import bftsmart.tom.server.defaultservices.DefaultRecoverable import bftsmart.tom.server.defaultservices.DefaultReplier +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.node.utilities.JDBCHashMap import net.corda.node.utilities.databaseTransaction import org.jetbrains.exposed.sql.Database -import java.util.LinkedHashMap +import java.util.* +@CordaSerializable enum class RequestType { Get, Put } /** Sent from [BFTSmartClient] to [BFTSmartServer] */ +@CordaSerializable data class Request(val type: RequestType, val data: Any) class BFTSmartClient(id: Int) { diff --git a/node/src/main/kotlin/net/corda/node/utilities/AddOrRemove.kt b/node/src/main/kotlin/net/corda/node/utilities/AddOrRemove.kt index 87bd5b9fd3..155e7853d2 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AddOrRemove.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AddOrRemove.kt @@ -1,8 +1,11 @@ package net.corda.node.utilities +import net.corda.core.serialization.CordaSerializable + /** * Enum for when adding/removing something, for example adding or removing an entry in a directory. */ +@CordaSerializable enum class AddOrRemove { ADD, REMOVE diff --git a/node/src/main/kotlin/net/corda/node/utilities/JDBCHashMap.kt b/node/src/main/kotlin/net/corda/node/utilities/JDBCHashMap.kt index 4021b159f8..e2de392f42 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/JDBCHashMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/JDBCHashMap.kt @@ -3,6 +3,7 @@ package net.corda.node.utilities import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.core.serialization.threadLocalStorageKryo import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace import org.jetbrains.exposed.sql.* @@ -64,11 +65,11 @@ fun bytesToBlob(value: SerializedBytes<*>, finalizables: MutableList<() -> Unit> return blob } -fun serializeToBlob(value: Any, finalizables: MutableList<() -> Unit>): Blob = bytesToBlob(value.serialize(), finalizables) +fun serializeToBlob(value: Any, finalizables: MutableList<() -> Unit>): Blob = bytesToBlob(value.serialize(threadLocalStorageKryo(), true), finalizables) fun bytesFromBlob(blob: Blob): SerializedBytes { try { - return SerializedBytes(blob.getBytes(0, blob.length().toInt())) + return SerializedBytes(blob.getBytes(0, blob.length().toInt()), true) } finally { blob.free() } diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt index d4524be067..e22121b2e2 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt @@ -1,5 +1,6 @@ package net.corda.node.utilities.registration +import net.corda.core.serialization.CordaSerializable import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.security.cert.Certificate @@ -12,4 +13,5 @@ interface NetworkRegistrationService { fun retrieveCertificates(requestId: String): Array? } +@CordaSerializable class CertificateRequestException(message: String) : Exception(message) diff --git a/node/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/node/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry index a76441b592..8b607465bc 100644 --- a/node/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ b/node/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry @@ -1,3 +1,4 @@ # Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry net.corda.node.services.NotaryChange$Plugin net.corda.node.services.persistence.DataVending$Plugin +net.corda.node.serialization.DefaultWhitelist \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/NodeSchedulerServiceTest.kt index e75762dd48..a41e857028 100644 --- a/node/src/test/kotlin/net/corda/node/services/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/NodeSchedulerServiceTest.kt @@ -12,10 +12,10 @@ import net.corda.core.node.recordTransactions import net.corda.core.node.services.VaultService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.statemachine.StateMachineManager +import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.AddOrRemove import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.configureDatabase diff --git a/node/src/test/kotlin/net/corda/node/services/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/ScheduledFlowTests.kt index b3545fccff..e90a5efd51 100644 --- a/node/src/test/kotlin/net/corda/node/services/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/ScheduledFlowTests.kt @@ -10,7 +10,6 @@ import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.linearHeadsOfType import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.flows.FinalityFlow import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.ValidatingNotaryService diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt index b4d0273e68..b2fcb2bfb9 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt @@ -8,6 +8,7 @@ import net.corda.core.crypto.Party import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.node.services.ServiceType +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder import net.corda.irs.flows.FixingFlow import net.corda.irs.utilities.suggestInterestRateAnnouncementTimeWindow @@ -22,6 +23,7 @@ import java.util.* val IRS_PROGRAM_ID = InterestRateSwap() // This is a placeholder for some types that we haven't identified exactly what they are just yet for things still in discussion +@CordaSerializable open class UnknownType() { override fun equals(other: Any?): Boolean { @@ -106,6 +108,7 @@ abstract class RatePaymentEvent(date: LocalDate, * Basic class for the Fixed Rate Payments on the fixed leg - see [RatePaymentEvent]. * Assumes that the rate is valid. */ +@CordaSerializable class FixedRatePaymentEvent(date: LocalDate, accrualStartDate: LocalDate, accrualEndDate: LocalDate, @@ -128,6 +131,7 @@ class FixedRatePaymentEvent(date: LocalDate, * Basic class for the Floating Rate Payments on the floating leg - see [RatePaymentEvent]. * If the rate is null returns a zero payment. // TODO: Is this the desired behaviour? */ +@CordaSerializable class FloatingRatePaymentEvent(date: LocalDate, accrualStartDate: LocalDate, accrualEndDate: LocalDate, @@ -197,6 +201,7 @@ class InterestRateSwap() : Contract { /** * This Common area contains all the information that is not leg specific. */ + @CordaSerializable data class Common( val baseCurrency: Currency, val eligibleCurrency: Currency, @@ -222,6 +227,7 @@ class InterestRateSwap() : Contract { * data that will changed from state to state (Recall that the design insists that everything is immutable, so we actually * copy / update for each transition). */ + @CordaSerializable data class Calculation( val expression: Expression, val floatingLegPaymentSchedule: Map, @@ -304,6 +310,7 @@ class InterestRateSwap() : Contract { dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment) } + @CordaSerializable open class FixedLeg( var fixedRatePayer: AnonymousParty, notional: Amount, @@ -365,6 +372,7 @@ class InterestRateSwap() : Contract { } + @CordaSerializable open class FloatingLeg( var floatingRatePayer: AnonymousParty, notional: Amount, diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt index 2cd489ce71..809cb7c05f 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt @@ -2,6 +2,7 @@ package net.corda.irs.contract import net.corda.core.contracts.Amount import net.corda.core.contracts.Tenor +import net.corda.core.serialization.CordaSerializable import java.math.BigDecimal import java.util.* @@ -11,6 +12,7 @@ import java.util.* /** * A utility class to prevent the various mixups between percentages, decimals, bips etc. */ +@CordaSerializable open class RatioUnit(val value: BigDecimal) { // TODO: Discuss this type override fun equals(other: Any?) = (other as? RatioUnit)?.value == value override fun hashCode() = value.hashCode() @@ -59,6 +61,7 @@ open class Rate(val ratioUnit: RatioUnit? = null) { /** * A very basic subclass to represent a fixed rate. */ +@CordaSerializable class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) { fun isPositive(): Boolean = ratioUnit!!.value > BigDecimal("0.0") @@ -69,6 +72,7 @@ class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) { /** * The parent class of the Floating rate classes. */ +@CordaSerializable open class FloatingRate : Rate(null) /** diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt index 598affee78..daf8ef4fb4 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt @@ -10,6 +10,7 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.PluginServiceHub import net.corda.core.node.services.ServiceType import net.corda.core.seconds +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.trace @@ -121,6 +122,7 @@ object FixingFlow { /** Used to set up the session between [Floater] and [Fixer] */ + @CordaSerializable data class FixingSession(val ref: StateRef, val oracleType: ServiceType) /** diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt index c35d913a6d..01e868e65f 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt @@ -5,8 +5,8 @@ import net.corda.core.contracts.Fix import net.corda.core.contracts.FixOf import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.Party -import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker @@ -42,9 +42,13 @@ open class RatesFixFlow(protected val tx: TransactionBuilder, fun tracker(fixName: String) = ProgressTracker(QUERYING(fixName), WORKING, SIGNING) } + @CordaSerializable class FixOutOfRange(@Suppress("unused") val byAmount: BigDecimal) : Exception("Fix out of range by $byAmount") + @CordaSerializable data class QueryRequest(val queries: List, val deadline: Instant) + + @CordaSerializable data class SignRequest(val ftx: FilteredTransaction) // DOCSTART 2 diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt index 7f0d725f87..c398e93f78 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt @@ -6,6 +6,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.NodeInfo import net.corda.core.node.PluginServiceHub +import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap import net.corda.node.utilities.TestClock @@ -20,6 +21,7 @@ object UpdateBusinessDayFlow { // This is not really a HandshakeMessage but needs to be so that the send uses the default session ID. This will // resolve itself when the flow session stuff is done. + @CordaSerializable data class UpdateBusinessDayMessage(val date: LocalDate) class Plugin : CordaPluginRegistry() { diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt index 9f3bc79a0c..3b7a710076 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt @@ -1,19 +1,16 @@ package net.corda.irs.plugin -import com.esotericsoftware.kryo.Kryo -import net.corda.core.contracts.* +import net.corda.core.contracts.StateRef import net.corda.core.crypto.Party import net.corda.core.node.CordaPluginRegistry import net.corda.irs.api.InterestRateSwapAPI -import net.corda.irs.contract.* +import net.corda.irs.contract.InterestRateSwap import net.corda.irs.flows.AutoOfferFlow import net.corda.irs.flows.ExitServerFlow import net.corda.irs.flows.FixingFlow import net.corda.irs.flows.UpdateBusinessDayFlow -import java.math.BigDecimal import java.time.Duration import java.time.LocalDate -import java.util.* import java.util.function.Function class IRSPlugin : CordaPluginRegistry() { @@ -28,39 +25,4 @@ class IRSPlugin : CordaPluginRegistry() { ExitServerFlow.Broadcast::class.java.name to setOf(kotlin.Int::class.java.name), FixingFlow.FixingRoleDecider::class.java.name to setOf(StateRef::class.java.name, Duration::class.java.name), FixingFlow.Floater::class.java.name to setOf(Party::class.java.name, FixingFlow.FixingSession::class.java.name)) - - override fun registerRPCKryoTypes(kryo: Kryo): Boolean { - kryo.apply { - register(InterestRateSwap::class.java) - register(InterestRateSwap.State::class.java) - register(InterestRateSwap.FixedLeg::class.java) - register(InterestRateSwap.FloatingLeg::class.java) - register(InterestRateSwap.Calculation::class.java) - register(InterestRateSwap.Common::class.java) - register(Expression::class.java) - register(HashMap::class.java) - register(LinkedHashMap::class.java) - register(RatioUnit::class.java) - register(Tenor::class.java) - register(Tenor.TimeUnit::class.java) - register(BusinessCalendar::class.java) - register(Comparable::class.java) - register(ReferenceRate::class.java) - register(UnknownType::class.java) - register(DayCountBasisDay::class.java) - register(DayCountBasisYear::class.java) - register(FixedRate::class.java) - register(PercentageRatioUnit::class.java) - register(BigDecimal::class.java) - register(AccrualAdjustment::class.java) - register(Frequency::class.java) - register(PaymentRule::class.java) - register(DateRollConvention::class.java) - register(LocalDate::class.java) - register(FixingFlow.FixingSession::class.java) - register(FixedRatePaymentEvent::class.java) - register(FloatingRatePaymentEvent::class.java) - } - return true - } } diff --git a/samples/irs-demo/src/main/resources/simulation/trade.json b/samples/irs-demo/src/main/resources/simulation/trade.json index d19905e240..9416828185 100644 --- a/samples/irs-demo/src/main/resources/simulation/trade.json +++ b/samples/irs-demo/src/main/resources/simulation/trade.json @@ -1,6 +1,6 @@ { "fixedLeg": { - "fixedRatePayer": "bzs7kfAFKFTtGhxNHeN7eiqufP9Q3p9hDvSTi8AyoRAwiLK8ZZ", + "fixedRatePayer": "2eFzn8gRQq7nNgypMCjKik4w8i565TM3xBmp85eefhG1c24VSj5", "notional": { "quantity": 2500000000, "token": "USD" @@ -25,7 +25,7 @@ "interestPeriodAdjustment": "Adjusted" }, "floatingLeg": { - "floatingRatePayer": "bzs7kf3Zc6J8mgNyH2ZddNRp3wzQt8MnPMT4zMYWgouHB4Uro5", + "floatingRatePayer": "2eFzn8gJj7xcdBxExC7XEiiX36dw6HfG3MCpjMt2CaejwUnfAxb", "notional": { "quantity": 2500000000, "token": "USD" diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt index ae98720e7c..5663190cff 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt @@ -5,6 +5,7 @@ import net.corda.core.crypto.AnonymousParty import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Party import net.corda.core.flows.FlowLogicRefFactory +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder import net.corda.vega.flows.SimmRevaluation import java.security.PublicKey @@ -23,6 +24,7 @@ data class PortfolioState(val portfolio: List, val valuation: PortfolioValuation? = null, override val linearId: UniqueIdentifier = UniqueIdentifier()) : RevisionedState, SchedulableState, DealState { + @CordaSerializable data class Update(val portfolio: List? = null, val valuation: PortfolioValuation? = null) override val parties: List get() = _parties.toList() diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioValuation.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioValuation.kt index 81aeb61d9d..e376fa3147 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioValuation.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioValuation.kt @@ -2,6 +2,7 @@ package net.corda.vega.contracts import com.opengamma.strata.basics.currency.MultiCurrencyAmount import com.opengamma.strata.market.param.CurrencyParameterSensitivities +import net.corda.core.serialization.CordaSerializable import net.corda.vega.analytics.CordaMarketData import net.corda.vega.analytics.InitialMarginTriple import java.math.BigDecimal @@ -13,6 +14,7 @@ import java.math.BigDecimal * We have to store trade counts in this object because a history is required and * we want to avoid walking the transaction chain. */ +@CordaSerializable data class PortfolioValuation(val trades: Int, val notional: BigDecimal, val marketData: CordaMarketData, diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/SwapData.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/SwapData.kt index 791bb991e3..faf0c50a99 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/SwapData.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/SwapData.kt @@ -11,7 +11,7 @@ import com.opengamma.strata.product.swap.type.FixedIborSwapConvention import com.opengamma.strata.product.swap.type.FixedIborSwapConventions import net.corda.core.crypto.AbstractParty import net.corda.core.crypto.CompositeKey -import net.corda.core.crypto.Party +import net.corda.core.serialization.CordaSerializable import java.math.BigDecimal import java.time.LocalDate @@ -36,6 +36,7 @@ data class FloatingLeg(val _notional: BigDecimal, override val notional: BigDeci /** * Represents a swap between two parties, a buyer and a seller. This class is a builder for OpenGamma SwapTrades. */ +@CordaSerializable data class SwapData( val id: Pair, val buyer: Pair, diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt index 2117d65a8d..5138077b1c 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt @@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.Party import net.corda.core.flows.FlowLogic import net.corda.core.node.PluginServiceHub +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.unwrap import net.corda.flows.TwoPartyDealFlow @@ -12,6 +13,7 @@ import net.corda.vega.contracts.OGTrade import net.corda.vega.contracts.SwapData object IRSTradeFlow { + @CordaSerializable data class OfferMessage(val notary: Party, val dealBeingOffered: IRSState) class Requester(val swap: SwapData, val otherParty: Party) : FlowLogic() { diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt index a32ce86d32..d2e8b387cb 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt @@ -18,6 +18,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.messaging.Ack import net.corda.core.node.PluginServiceHub import net.corda.core.node.services.dealsWith +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.unwrap import net.corda.flows.AbstractStateReplacementFlow.Proposal @@ -40,6 +41,7 @@ object SimmFlow { * Represents a new portfolio offer unless the stateRef field is non-null, at which point it represents a * portfolio update offer. */ + @CordaSerializable data class OfferMessage(val notary: Party, val dealBeingOffered: PortfolioState, val stateRef: StateRef?, diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/services/SimmService.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/services/SimmService.kt index 00376820f3..5a34a0fad7 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/services/SimmService.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/services/SimmService.kt @@ -1,6 +1,5 @@ package net.corda.vega.services -import com.esotericsoftware.kryo.Kryo import com.google.common.collect.Ordering import com.opengamma.strata.basics.currency.Currency import com.opengamma.strata.basics.currency.CurrencyAmount @@ -14,17 +13,15 @@ import com.opengamma.strata.market.param.TenorDateParameterMetadata import net.corda.core.contracts.StateRef import net.corda.core.crypto.Party import net.corda.core.node.CordaPluginRegistry +import net.corda.core.serialization.SerializationCustomization import net.corda.vega.analytics.CordaMarketData import net.corda.vega.analytics.InitialMarginTriple import net.corda.vega.api.PortfolioApi -import net.corda.vega.contracts.* +import net.corda.vega.contracts.SwapData import net.corda.vega.flows.IRSTradeFlow import net.corda.vega.flows.SimmFlow import net.corda.vega.flows.SimmRevaluation -import java.math.BigDecimal import java.time.LocalDate -import java.time.Period -import java.util.* import java.util.function.Function /** @@ -41,32 +38,21 @@ object SimmService { IRSTradeFlow.Requester::class.java.name to setOf(SwapData::class.java.name, Party::class.java.name)) override val staticServeDirs: Map = mapOf("simmvaluationdemo" to javaClass.classLoader.getResource("simmvaluationweb").toExternalForm()) override val servicePlugins = listOf(Function(SimmFlow::Service), Function(IRSTradeFlow::Service)) - override fun registerRPCKryoTypes(kryo: Kryo): Boolean { - kryo.apply { - register(SwapData::class.java) - register(LocalDate::class.java) - register(BigDecimal::class.java) - register(IRSState::class.java) - register(OGTrade::class.java) - register(PortfolioState::class.java) - register(PortfolioSwap::class.java) - register(PortfolioValuation::class.java) - register(MultiCurrencyAmount::class.java) - register(Ordering.natural>().javaClass) - register(CurrencyAmount::class.java) - register(Currency::class.java) - register(InitialMarginTriple::class.java) - register(CordaMarketData::class.java) - register(CurrencyParameterSensitivities::class.java) - register(CurrencyParameterSensitivity::class.java) - register(DoubleArray::class.java) - register(kotlin.DoubleArray::class.java) - register(LinkedHashMap::class.java) - register(CurveName::class.java) - register(TenorDateParameterMetadata::class.java) - register(Tenor::class.java) - register(Period::class.java) - register(Class.forName("java.util.Collections\$SingletonMap")) + override fun customizeSerialization(custom: SerializationCustomization): Boolean { + custom.apply { + // OpenGamma classes. + addToWhitelist(MultiCurrencyAmount::class.java) + addToWhitelist(Ordering.natural>().javaClass) + addToWhitelist(CurrencyAmount::class.java) + addToWhitelist(Currency::class.java) + addToWhitelist(InitialMarginTriple::class.java) + addToWhitelist(CordaMarketData::class.java) + addToWhitelist(CurrencyParameterSensitivities::class.java) + addToWhitelist(CurrencyParameterSensitivity::class.java) + addToWhitelist(DoubleArray::class.java) + addToWhitelist(CurveName::class.java) + addToWhitelist(TenorDateParameterMetadata::class.java) + addToWhitelist(Tenor::class.java) } return true } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt b/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt index 911750a3c4..16a125dad6 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt @@ -9,6 +9,7 @@ import net.corda.core.getOrThrow import net.corda.core.messaging.* import net.corda.core.node.ServiceEntry import net.corda.core.node.services.PartyInfo +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.trace import net.corda.node.services.api.MessagingServiceBuilder @@ -55,6 +56,7 @@ class InMemoryMessagingNetwork( private var counter = 0 // -1 means stopped. private val handleEndpointMap = HashMap() + @CordaSerializable data class MessageTransfer(val sender: PeerHandle, val message: Message, val recipients: MessageRecipients) { override fun toString() = "${message.topicSession} from '$sender' to '$recipients'" } @@ -193,12 +195,14 @@ class InMemoryMessagingNetwork( } } + @CordaSerializable data class PeerHandle(val id: Int, val description: String) : SingleMessageRecipient { override fun toString() = description override fun equals(other: Any?) = other is PeerHandle && other.id == id override fun hashCode() = id.hashCode() } + @CordaSerializable data class ServiceHandle(val service: ServiceEntry) : MessageRecipientGroup { override fun toString() = "Service($service)" } @@ -266,6 +270,14 @@ class InMemoryMessagingNetwork( _sentMessages.onNext(transfer) } + @CordaSerializable + private data class InMemoryMessage(override val topicSession: TopicSession, override val data: ByteArray, override val uniqueMessageId: UUID, override val debugTimestamp: Instant = Instant.now()) : Message { + override fun toString() = "$topicSession#${String(data)}" + } + + @CordaSerializable + private data class InMemoryReceivedMessage(override val topicSession: TopicSession, override val data: ByteArray, override val uniqueMessageId: UUID, override val debugTimestamp: Instant, override val peer: X500Name) : ReceivedMessage + /** * An [InMemoryMessaging] provides a [MessagingService] that isn't backed by any kind of network or disk storage * system, but just uses regular queues on the heap instead. It is intended for unit testing and developer convenience @@ -355,13 +367,7 @@ class InMemoryMessagingNetwork( /** Returns the given (topic & session, data) pair as a newly created message object. */ override fun createMessage(topicSession: TopicSession, data: ByteArray, uuid: UUID): Message { - return object : Message { - override val topicSession: TopicSession get() = topicSession - override val data: ByteArray get() = data - override val debugTimestamp: Instant = Instant.now() - override val uniqueMessageId: UUID = uuid - override fun toString() = "$topicSession#${String(data)}" - } + return InMemoryMessage(topicSession, data, uuid) } /** @@ -443,12 +449,9 @@ class InMemoryMessagingNetwork( return transfer } - private fun MessageTransfer.toReceivedMessage() = object : ReceivedMessage { - override val topicSession: TopicSession get() = message.topicSession - override val data: ByteArray get() = message.data.copyOf() // Kryo messes with the buffer so give each client a unique copy - override val peer: X500Name get() = X509Utilities.getDevX509Name(sender.description) - override val debugTimestamp: Instant get() = message.debugTimestamp - override val uniqueMessageId: UUID get() = message.uniqueMessageId - } + private fun MessageTransfer.toReceivedMessage(): ReceivedMessage = InMemoryReceivedMessage( + message.topicSession, + message.data.copyOf(), // Kryo messes with the buffer so give each client a unique copy + message.uniqueMessageId, message.debugTimestamp, X509Utilities.getDevX509Name(sender.description)) } }