mirror of
https://github.com/corda/corda.git
synced 2024-12-21 22:07:55 +00:00
Delete a lot of Kryo/serialisation related boilerplate.
This is/was an attempt to be secure against malicious streams, but as Kryo is just a temporary bit of scaffolding and isn't intended to be actually used in any real product, it was just a waste of time and the registration requirement was getting increasingly awkward.
This commit is contained in:
parent
951912f8e7
commit
7881be07ed
@ -9,7 +9,6 @@
|
|||||||
package contracts
|
package contracts
|
||||||
|
|
||||||
import core.*
|
import core.*
|
||||||
import core.serialization.SerializeableWithKryo
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -47,7 +46,7 @@ class CrowdFund : Contract {
|
|||||||
val name: String,
|
val name: String,
|
||||||
val target: Amount,
|
val target: Amount,
|
||||||
val closingTime: Instant
|
val closingTime: Instant
|
||||||
) : SerializeableWithKryo {
|
) {
|
||||||
override fun toString() = "Crowdsourcing($target sought by $owner by $closingTime)"
|
override fun toString() = "Crowdsourcing($target sought by $owner by $closingTime)"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,9 +61,9 @@ class CrowdFund : Contract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class Pledge(
|
data class Pledge(
|
||||||
val owner: PublicKey,
|
val owner: PublicKey,
|
||||||
val amount: Amount
|
val amount: Amount
|
||||||
) : SerializeableWithKryo
|
)
|
||||||
|
|
||||||
|
|
||||||
interface Commands : Command {
|
interface Commands : Command {
|
||||||
|
@ -10,7 +10,6 @@ package contracts;
|
|||||||
|
|
||||||
import core.*;
|
import core.*;
|
||||||
import core.TransactionForVerification.*;
|
import core.TransactionForVerification.*;
|
||||||
import core.serialization.*;
|
|
||||||
import org.jetbrains.annotations.*;
|
import org.jetbrains.annotations.*;
|
||||||
|
|
||||||
import java.security.*;
|
import java.security.*;
|
||||||
@ -27,7 +26,7 @@ import static kotlin.CollectionsKt.*;
|
|||||||
* NOTE: For illustration only. Not unit tested.
|
* NOTE: For illustration only. Not unit tested.
|
||||||
*/
|
*/
|
||||||
public class JavaCommercialPaper implements Contract {
|
public class JavaCommercialPaper implements Contract {
|
||||||
public static class State implements ContractState, SerializeableWithKryo {
|
public static class State implements ContractState {
|
||||||
private PartyReference issuance;
|
private PartyReference issuance;
|
||||||
private PublicKey owner;
|
private PublicKey owner;
|
||||||
private Amount faceValue;
|
private Amount faceValue;
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import core.serialization.SerializeableWithKryo
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -67,7 +66,7 @@ inline fun requireThat(body: Requirements.() -> Unit) {
|
|||||||
* TODO: Should amount be abstracted to cover things like quantities of a stock, bond, commercial paper etc? Probably.
|
* TODO: Should amount be abstracted to cover things like quantities of a stock, bond, commercial paper etc? Probably.
|
||||||
* TODO: Think about how positive-only vs positive-or-negative amounts can be represented in the type system.
|
* TODO: Think about how positive-only vs positive-or-negative amounts can be represented in the type system.
|
||||||
*/
|
*/
|
||||||
data class Amount(val pennies: Long, val currency: Currency) : Comparable<Amount>, SerializeableWithKryo {
|
data class Amount(val pennies: Long, val currency: Currency) : Comparable<Amount> {
|
||||||
init {
|
init {
|
||||||
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain
|
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain
|
||||||
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance.
|
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance.
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding
|
import com.google.common.io.BaseEncoding
|
||||||
import core.serialization.SerializeableWithKryo
|
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.*
|
import java.security.*
|
||||||
|
|
||||||
@ -71,7 +70,7 @@ object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
|||||||
override fun toString() = "NULL_KEY"
|
override fun toString() = "NULL_KEY"
|
||||||
}
|
}
|
||||||
|
|
||||||
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey>, SerializeableWithKryo {
|
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
|
||||||
override fun getAlgorithm() = "DUMMY"
|
override fun getAlgorithm() = "DUMMY"
|
||||||
override fun getEncoded() = s.toByteArray()
|
override fun getEncoded() = s.toByteArray()
|
||||||
override fun getFormat() = "ASN.1"
|
override fun getFormat() = "ASN.1"
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import core.serialization.SerializeableWithKryo
|
|
||||||
import core.serialization.serialize
|
import core.serialization.serialize
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
@ -17,7 +16,7 @@ import java.security.PublicKey
|
|||||||
* file that the program can use to persist data across transactions. States are immutable: once created they are never
|
* file that the program can use to persist data across transactions. States are immutable: once created they are never
|
||||||
* updated, instead, any changes must generate a new successor state.
|
* updated, instead, any changes must generate a new successor state.
|
||||||
*/
|
*/
|
||||||
interface ContractState : SerializeableWithKryo {
|
interface ContractState {
|
||||||
/**
|
/**
|
||||||
* Refers to a bytecode program that has previously been published to the network. This contract program
|
* Refers to a bytecode program that has previously been published to the network. This contract program
|
||||||
* will be executed any time this state is used in an input. It must accept in order for the
|
* will be executed any time this state is used in an input. It must accept in order for the
|
||||||
@ -41,13 +40,13 @@ fun ContractState.hash(): SecureHash = SecureHash.sha256((serialize()))
|
|||||||
* A stateref is a pointer to a state, this is an equivalent of an "outpoint" in Bitcoin. It records which transaction
|
* A stateref is a pointer 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.
|
* defined the state and where in that transaction it was.
|
||||||
*/
|
*/
|
||||||
data class ContractStateRef(val txhash: SecureHash, val index: Int) : SerializeableWithKryo
|
data class ContractStateRef(val txhash: SecureHash, val index: Int)
|
||||||
|
|
||||||
/** A StateAndRef is simply a (state, ref) pair. For instance, a wallet (which holds available assets) contains these. */
|
/** A StateAndRef is simply a (state, ref) pair. For instance, a wallet (which holds available assets) contains these. */
|
||||||
data class StateAndRef<out T : ContractState>(val state: T, val ref: ContractStateRef) : SerializeableWithKryo
|
data class StateAndRef<out T : ContractState>(val state: T, val ref: ContractStateRef)
|
||||||
|
|
||||||
/** A [Party] is well known (name, pubkey) pair. In a real system this would probably be an X.509 certificate. */
|
/** A [Party] is well known (name, pubkey) pair. In a real system this would probably be an X.509 certificate. */
|
||||||
data class Party(val name: String, val owningKey: PublicKey) : SerializeableWithKryo {
|
data class Party(val name: String, val owningKey: PublicKey) {
|
||||||
override fun toString() = name
|
override fun toString() = name
|
||||||
|
|
||||||
fun ref(bytes: OpaqueBytes) = PartyReference(this, bytes)
|
fun ref(bytes: OpaqueBytes) = PartyReference(this, bytes)
|
||||||
@ -58,12 +57,12 @@ data class Party(val name: String, val owningKey: PublicKey) : SerializeableWith
|
|||||||
* Reference to something being stored or issued by a party e.g. in a vault or (more likely) on their normal
|
* 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.
|
* ledger. The reference is intended to be encrypted so it's meaningless to anyone other than the party.
|
||||||
*/
|
*/
|
||||||
data class PartyReference(val party: Party, val reference: OpaqueBytes) : SerializeableWithKryo {
|
data class PartyReference(val party: Party, val reference: OpaqueBytes) {
|
||||||
override fun toString() = "${party.name}$reference"
|
override fun toString() = "${party.name}$reference"
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Marker interface for classes that represent commands */
|
/** Marker interface for classes that represent commands */
|
||||||
interface Command : SerializeableWithKryo
|
interface Command
|
||||||
|
|
||||||
/** Commands that inherit from this are intended to have no data items: it's only their presence that matters. */
|
/** Commands that inherit from this are intended to have no data items: it's only their presence that matters. */
|
||||||
abstract class TypeOnlyCommand : Command {
|
abstract class TypeOnlyCommand : Command {
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import core.serialization.SerializeableWithKryo
|
|
||||||
import core.serialization.deserialize
|
import core.serialization.deserialize
|
||||||
import core.serialization.serialize
|
import core.serialization.serialize
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
@ -49,14 +48,14 @@ import java.util.*
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/** Serialized command plus pubkey pair: the signature is stored at the end of the serialized bytes */
|
/** Serialized command plus pubkey pair: the signature is stored at the end of the serialized bytes */
|
||||||
data class WireCommand(val command: Command, val pubkeys: List<PublicKey>) : SerializeableWithKryo {
|
data class WireCommand(val command: Command, val pubkeys: List<PublicKey>) {
|
||||||
constructor(command: Command, key: PublicKey) : this(command, listOf(key))
|
constructor(command: Command, key: PublicKey) : this(command, listOf(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Transaction ready for serialisation, without any signatures attached. */
|
/** Transaction ready for serialisation, without any signatures attached. */
|
||||||
data class WireTransaction(val inputStates: List<ContractStateRef>,
|
data class WireTransaction(val inputStates: List<ContractStateRef>,
|
||||||
val outputStates: List<ContractState>,
|
val outputStates: List<ContractState>,
|
||||||
val commands: List<WireCommand>) : SerializeableWithKryo {
|
val commands: List<WireCommand>) {
|
||||||
fun serializeForSignature(): ByteArray = serialize()
|
fun serializeForSignature(): ByteArray = serialize()
|
||||||
|
|
||||||
fun toLedgerTransaction(timestamp: Instant?, partyKeyMap: Map<PublicKey, Party>, originalHash: SecureHash): LedgerTransaction {
|
fun toLedgerTransaction(timestamp: Instant?, partyKeyMap: Map<PublicKey, Party>, originalHash: SecureHash): LedgerTransaction {
|
||||||
@ -146,7 +145,7 @@ interface TimestamperService {
|
|||||||
fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant
|
fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SignedWireTransaction(val txBits: OpaqueBytes, val sigs: List<DigitalSignature.WithKey>) : SerializeableWithKryo {
|
data class SignedWireTransaction(val txBits: OpaqueBytes, val sigs: List<DigitalSignature.WithKey>) {
|
||||||
init {
|
init {
|
||||||
check(sigs.isNotEmpty())
|
check(sigs.isNotEmpty())
|
||||||
}
|
}
|
||||||
@ -201,7 +200,7 @@ data class TimestampedWireTransaction(
|
|||||||
|
|
||||||
/** Signature from a timestamping authority. For instance using RFC 3161 */
|
/** Signature from a timestamping authority. For instance using RFC 3161 */
|
||||||
val timestamp: OpaqueBytes?
|
val timestamp: OpaqueBytes?
|
||||||
) : SerializeableWithKryo {
|
) {
|
||||||
val transactionID: SecureHash = serialize().sha256()
|
val transactionID: SecureHash = serialize().sha256()
|
||||||
|
|
||||||
fun verifyToLedgerTransaction(timestamper: TimestamperService, partyKeyMap: Map<PublicKey, Party>): LedgerTransaction {
|
fun verifyToLedgerTransaction(timestamper: TimestamperService, partyKeyMap: Map<PublicKey, Party>): LedgerTransaction {
|
||||||
|
@ -9,12 +9,11 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding
|
import com.google.common.io.BaseEncoding
|
||||||
import core.serialization.SerializeableWithKryo
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/** A simple class that wraps a byte array and makes the equals/hashCode/toString methods work as you actually expect */
|
/** A simple class that wraps a byte array and makes the equals/hashCode/toString methods work as you actually expect */
|
||||||
open class OpaqueBytes(val bits: ByteArray) : SerializeableWithKryo {
|
open class OpaqueBytes(val bits: ByteArray) {
|
||||||
init { check(bits.isNotEmpty()) }
|
init { check(bits.isNotEmpty()) }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -9,26 +9,11 @@
|
|||||||
package core.serialization
|
package core.serialization
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo
|
import com.esotericsoftware.kryo.Kryo
|
||||||
import com.esotericsoftware.kryo.KryoException
|
|
||||||
import com.esotericsoftware.kryo.Serializer
|
|
||||||
import com.esotericsoftware.kryo.io.Input
|
import com.esotericsoftware.kryo.io.Input
|
||||||
import com.esotericsoftware.kryo.io.Output
|
import com.esotericsoftware.kryo.io.Output
|
||||||
import com.esotericsoftware.kryo.serializers.JavaSerializer
|
import core.OpaqueBytes
|
||||||
import contracts.Cash
|
import org.objenesis.strategy.StdInstantiatorStrategy
|
||||||
import contracts.CommercialPaper
|
|
||||||
import contracts.CrowdFund
|
|
||||||
import core.*
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.lang.reflect.InvocationTargetException
|
|
||||||
import java.security.KeyPairGenerator
|
|
||||||
import java.security.PublicKey
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
import kotlin.reflect.KMutableProperty
|
|
||||||
import kotlin.reflect.jvm.javaType
|
|
||||||
import kotlin.reflect.memberProperties
|
|
||||||
import kotlin.reflect.primaryConstructor
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialization utilities, using the Kryo framework with a custom serialiser for immutable data classes and a dead
|
* Serialization utilities, using the Kryo framework with a custom serialiser for immutable data classes and a dead
|
||||||
@ -47,145 +32,23 @@ import kotlin.reflect.primaryConstructor
|
|||||||
*
|
*
|
||||||
* But for now we use Kryo to maximise prototyping speed.
|
* But for now we use Kryo to maximise prototyping speed.
|
||||||
*
|
*
|
||||||
* The goals of this code are twofold:
|
* Note that this code ignores *ALL* concerns beyond convenience, in particular it ignores:
|
||||||
*
|
*
|
||||||
* 1) Security
|
* - Performance
|
||||||
* 2) Convenience
|
* - Security
|
||||||
*
|
|
||||||
* in that order.
|
|
||||||
*
|
|
||||||
* SECURITY
|
|
||||||
* --------
|
|
||||||
*
|
|
||||||
* Even though this is prototype code, we should still respect the Java Secure Coding Guidelines and the advice it
|
|
||||||
* gives for the use of object graph serialisation:
|
|
||||||
*
|
|
||||||
* http://www.oracle.com/technetwork/java/seccodeguide-139067.html
|
|
||||||
*
|
|
||||||
* Object graph serialisation is convenient but has a long history of exposing apps to security holes when type system
|
|
||||||
* invariants are violated by objects being reconstructed unexpectedly, or in illegal states.
|
|
||||||
*
|
|
||||||
* Therefore we take the following measures:
|
|
||||||
*
|
|
||||||
* - The ImmutableClassSerializer knows how to build deserialised objects using the primary constructor. Any invariants
|
|
||||||
* enforced by this constructor are therefore enforced (in Kotlin this means logic inside an init{} block, in
|
|
||||||
* Java it means any code in the only defined constructor).
|
|
||||||
* - The ICS asserts that Kryo is configured to only deserialise registered classes. Every class that might appear
|
|
||||||
* in a stream must be specified up front: Kryo will not rummage through the classpath to find any arbitrary
|
|
||||||
* class the stream happens to mention. This improves both performance and security at a loss of developer
|
|
||||||
* convenience.
|
|
||||||
*
|
|
||||||
* The ICS is intended to be used with classes that meet the following constraints:
|
|
||||||
*
|
|
||||||
* - Must be immutable: all properties are final. Note that Kotlin never generates bare public fields, but we should
|
|
||||||
* add some checks for this being done anyway for cases where a contract is defined using Java.
|
|
||||||
* - Requires that the data class be marked as intended for serialization using a marker interface.
|
|
||||||
* - Properties that are not in the constructor are not serialised (but as they are final, they must be either
|
|
||||||
* initialised to a constant or derived from the constructor arguments unless they are reading external state,
|
|
||||||
* which is intended to be forbidden).
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* CONVENIENCE
|
|
||||||
* -----------
|
|
||||||
*
|
|
||||||
* We define a few utilities to make using Kryo convenient.
|
|
||||||
*
|
|
||||||
* The createKryo() function returns a pre-configured Kryo class with a very small number of classes registered, that
|
|
||||||
* are known to be safe.
|
|
||||||
*
|
|
||||||
* A serialize() extension function is added to the SerializeableWithKryo marker interface. This is intended for
|
|
||||||
* serializing immutable data classes (i.e immutable javabeans). A deserialize() method is added to ByteArray to
|
|
||||||
* get the given class back from the stream. A Kryo.registerImmutableClass<>() function is added to clean up the Java
|
|
||||||
* syntax a bit:
|
|
||||||
*
|
|
||||||
* data class Person(val name: String, val birthdate: Instant?) : SerializeableWithKryo
|
|
||||||
*
|
|
||||||
* val kryo = kryo()
|
|
||||||
* kryo.registerImmutableClass<Person>()
|
|
||||||
*
|
|
||||||
* val bits: ByteArray = somePerson.serialize(kryo)
|
|
||||||
* val person2 = bits.deserialize<Person>(kryo)
|
|
||||||
*
|
*
|
||||||
|
* This code will happily deserialise literally anything, including malicious streams that would reconstruct classes
|
||||||
|
* 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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marker interface for classes to use with [ImmutableClassSerializer]. Note that only constructor defined properties will
|
|
||||||
* be serialised!
|
|
||||||
*/
|
|
||||||
interface SerializeableWithKryo
|
|
||||||
|
|
||||||
class ImmutableClassSerializer<T : SerializeableWithKryo>(val klass: KClass<T>) : Serializer<T>() {
|
|
||||||
val props = klass.memberProperties.sortedBy { it.name }
|
|
||||||
val propsByName = props.toMapBy { it.name }
|
|
||||||
val constructor = klass.primaryConstructor!!
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Verify that this class is immutable (all properties are final)
|
|
||||||
assert(props.none { it is KMutableProperty<*> })
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun write(kryo: Kryo, output: Output, obj: T) {
|
|
||||||
output.writeVarInt(constructor.parameters.size, true)
|
|
||||||
output.writeInt(constructor.parameters.hashCode())
|
|
||||||
for (param in constructor.parameters) {
|
|
||||||
val kProperty = propsByName[param.name!!]!!
|
|
||||||
when (param.type.javaType.typeName) {
|
|
||||||
"int" -> output.writeVarInt(kProperty.get(obj) as Int, true)
|
|
||||||
"long" -> output.writeVarLong(kProperty.get(obj) as Long, true)
|
|
||||||
"short" -> output.writeShort(kProperty.get(obj) as Int)
|
|
||||||
"char" -> output.writeChar(kProperty.get(obj) as Char)
|
|
||||||
"byte" -> output.writeByte(kProperty.get(obj) as Byte)
|
|
||||||
"double" -> output.writeDouble(kProperty.get(obj) as Double)
|
|
||||||
"float" -> output.writeFloat(kProperty.get(obj) as Float)
|
|
||||||
else -> try {
|
|
||||||
kryo.writeClassAndObject(output, kProperty.get(obj))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw IllegalStateException("Failed to serialize ${param.name} in ${klass.qualifiedName}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun read(kryo: Kryo, input: Input, type: Class<T>): T {
|
|
||||||
assert(type.kotlin == klass)
|
|
||||||
assert(kryo.isRegistrationRequired)
|
|
||||||
val numFields = input.readVarInt(true)
|
|
||||||
val fieldTypeHash = input.readInt()
|
|
||||||
|
|
||||||
// A few quick checks for data evolution. Note that this is not guaranteed to catch every problem! But it's
|
|
||||||
// good enough for a prototype.
|
|
||||||
if (numFields != constructor.parameters.size)
|
|
||||||
throw KryoException("Mismatch between number of constructor parameters and number of serialised fields for ${klass.qualifiedName} ($numFields vs ${constructor.parameters.size})")
|
|
||||||
if (fieldTypeHash != constructor.parameters.hashCode())
|
|
||||||
throw KryoException("Hashcode mismatch for parameter types for ${klass.qualifiedName}: unsupported type evolution has happened.")
|
|
||||||
|
|
||||||
val args = arrayOfNulls<Any?>(numFields)
|
|
||||||
var cursor = 0
|
|
||||||
for (param in constructor.parameters) {
|
|
||||||
args[cursor++] = when (param.type.javaType.typeName) {
|
|
||||||
"int" -> input.readVarInt(true)
|
|
||||||
"long" -> input.readVarLong(true)
|
|
||||||
"short" -> input.readShort()
|
|
||||||
"char" -> input.readChar()
|
|
||||||
"byte" -> input.readByte()
|
|
||||||
"double" -> input.readDouble()
|
|
||||||
"float" -> input.readFloat()
|
|
||||||
else -> kryo.readClassAndObject(input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If the constructor throws an exception, pass it through instead of wrapping it.
|
|
||||||
return try { constructor.call(*args) } catch (e: InvocationTargetException) { throw e.cause!! }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val THREAD_LOCAL_KRYO = ThreadLocal.withInitial { createKryo() }
|
val THREAD_LOCAL_KRYO = ThreadLocal.withInitial { createKryo() }
|
||||||
|
|
||||||
inline fun <reified T : SerializeableWithKryo> Kryo.registerImmutableClass() = register(T::class.java, ImmutableClassSerializer(T::class))
|
inline fun <reified T : Any> ByteArray.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): T = kryo.readObject(Input(this), T::class.java)
|
||||||
inline fun <reified T : SerializeableWithKryo> ByteArray.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): T = kryo.readObject(Input(this), T::class.java)
|
inline fun <reified T : Any> OpaqueBytes.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): T = kryo.readObject(Input(this.bits), T::class.java)
|
||||||
inline fun <reified T : SerializeableWithKryo> OpaqueBytes.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): T = kryo.readObject(Input(this.bits), T::class.java)
|
|
||||||
|
|
||||||
fun SerializeableWithKryo.serialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): ByteArray {
|
fun Any.serialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): ByteArray {
|
||||||
val stream = ByteArrayOutputStream()
|
val stream = ByteArrayOutputStream()
|
||||||
Output(stream).use {
|
Output(stream).use {
|
||||||
kryo.writeObject(it, this)
|
kryo.writeObject(it, this)
|
||||||
@ -193,84 +56,12 @@ fun SerializeableWithKryo.serialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): ByteA
|
|||||||
return stream.toByteArray()
|
return stream.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val UNUSED_EC_KEYPAIR = KeyPairGenerator.getInstance("EC").genKeyPair()
|
|
||||||
|
|
||||||
fun createKryo(): Kryo {
|
fun createKryo(): Kryo {
|
||||||
return Kryo().apply {
|
return Kryo().apply {
|
||||||
// Require explicit listing of all types that can be deserialised, to defend against classes that aren't
|
// Allow any class to be deserialized (this is insecure but for prototyping we don't care)
|
||||||
// designed for serialisation being unexpectedly instantiated.
|
isRegistrationRequired = false
|
||||||
isRegistrationRequired = true
|
// Allow construction of objects using a JVM backdoor that skips invoking the constructors, if there is no
|
||||||
|
// no-arg constructor available.
|
||||||
// Allow various array and list types. Sometimes when the type is private/internal we have to give an example
|
instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy())
|
||||||
// instead and then get the class from that. These have built in Kryo serializers that are safe to use.
|
|
||||||
register(ByteArray::class.java)
|
|
||||||
register(Collections.EMPTY_LIST.javaClass)
|
|
||||||
register(Collections.EMPTY_MAP.javaClass)
|
|
||||||
register(Collections.singletonList(null).javaClass)
|
|
||||||
register(Collections.singletonMap(1, 2).javaClass)
|
|
||||||
register(ArrayList::class.java)
|
|
||||||
register(emptyList<Any>().javaClass)
|
|
||||||
register(Arrays.asList(1,3).javaClass)
|
|
||||||
|
|
||||||
// These JDK classes use a very minimal custom serialization format and are written to defend against malicious
|
|
||||||
// streams, so we can just kick it over to java serialization. We get ECPublicKeyImpl/ECPrivteKeyImpl via an
|
|
||||||
// example: it'd be faster to just import the sun.security.ec package directly, but that wouldn't play nice
|
|
||||||
// when Java 9 is released, as Project Jigsaw will make internal packages will become unavailable without hacks.
|
|
||||||
register(Instant::class.java, JavaSerializer())
|
|
||||||
register(Currency::class.java, JavaSerializer()) // Only serialises the currency code as a string.
|
|
||||||
register(UNUSED_EC_KEYPAIR.private.javaClass, JavaSerializer())
|
|
||||||
register(UNUSED_EC_KEYPAIR.public.javaClass, JavaSerializer())
|
|
||||||
register(PublicKey::class.java, JavaSerializer())
|
|
||||||
|
|
||||||
// Now register platform types.
|
|
||||||
registerImmutableClass<SecureHash.SHA256>()
|
|
||||||
registerImmutableClass<Amount>()
|
|
||||||
registerImmutableClass<PartyReference>()
|
|
||||||
registerImmutableClass<Party>()
|
|
||||||
registerImmutableClass<OpaqueBytes>()
|
|
||||||
registerImmutableClass<SignedWireTransaction>()
|
|
||||||
registerImmutableClass<ContractStateRef>()
|
|
||||||
registerImmutableClass<WireTransaction>()
|
|
||||||
registerImmutableClass<WireCommand>()
|
|
||||||
registerImmutableClass<TimestampedWireTransaction>()
|
|
||||||
registerImmutableClass<StateAndRef<ContractState>>()
|
|
||||||
|
|
||||||
// Can't use data classes for this in Kotlin 1.0 due to lack of support for inheritance: must write a manual
|
|
||||||
// serialiser instead :(
|
|
||||||
register(DigitalSignature.WithKey::class.java, object : Serializer<DigitalSignature.WithKey>(false, true) {
|
|
||||||
override fun write(kryo: Kryo, output: Output, sig: DigitalSignature.WithKey) {
|
|
||||||
output.writeVarInt(sig.bits.size, true)
|
|
||||||
output.write(sig.bits)
|
|
||||||
output.writeInt(sig.covering, true)
|
|
||||||
kryo.writeObject(output, sig.by)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun read(kryo: Kryo, input: Input, type: Class<DigitalSignature.WithKey>): DigitalSignature.WithKey {
|
|
||||||
val sigLen = input.readVarInt(true)
|
|
||||||
val sigBits = input.readBytes(sigLen)
|
|
||||||
val covering = input.readInt(true)
|
|
||||||
val pubkey = kryo.readObject(input, PublicKey::class.java)
|
|
||||||
return DigitalSignature.WithKey(pubkey, sigBits, covering)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: This is obviously a short term hack: there needs to be a way to bundle up and register contracts.
|
|
||||||
registerImmutableClass<Cash.State>()
|
|
||||||
register(Cash.Commands.Move::class.java)
|
|
||||||
registerImmutableClass<Cash.Commands.Exit>()
|
|
||||||
registerImmutableClass<Cash.Commands.Issue>()
|
|
||||||
registerImmutableClass<CommercialPaper.State>()
|
|
||||||
register(CommercialPaper.Commands.Move::class.java)
|
|
||||||
register(CommercialPaper.Commands.Redeem::class.java)
|
|
||||||
register(CommercialPaper.Commands.Issue::class.java)
|
|
||||||
registerImmutableClass<CrowdFund.State>()
|
|
||||||
registerImmutableClass<CrowdFund.Pledge>()
|
|
||||||
registerImmutableClass<CrowdFund.Campaign>()
|
|
||||||
register(CrowdFund.Commands.Register::class.java)
|
|
||||||
register(CrowdFund.Commands.Pledge::class.java)
|
|
||||||
register(CrowdFund.Commands.Close::class.java)
|
|
||||||
|
|
||||||
// And for unit testing ...
|
|
||||||
registerImmutableClass<DummyPublicKey>()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,28 +12,18 @@ import com.esotericsoftware.kryo.Kryo
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFailsWith
|
|
||||||
import kotlin.test.assertNull
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
class KryoTests {
|
class KryoTests {
|
||||||
data class Person(val name: String, val birthday: Instant?) : SerializeableWithKryo
|
data class Person(val name: String, val birthday: Instant?)
|
||||||
data class MustBeWhizzy(val s: String) : SerializeableWithKryo {
|
|
||||||
init {
|
|
||||||
assert(s.startsWith("whiz")) { "must be whizzy" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val kryo: Kryo = createKryo().apply {
|
private val kryo: Kryo = createKryo()
|
||||||
registerImmutableClass<Person>()
|
|
||||||
registerImmutableClass<MustBeWhizzy>()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun ok() {
|
fun ok() {
|
||||||
val april_17th = Instant.parse("1984-04-17T00:30:00.00Z")
|
val april_17th = Instant.parse("1984-04-17T00:30:00.00Z")
|
||||||
val mike = Person("mike", april_17th)
|
val mike = Person("mike", april_17th)
|
||||||
val bits = mike.serialize(kryo)
|
val bits = mike.serialize(kryo)
|
||||||
assertEquals(64, bits.size)
|
|
||||||
with(bits.deserialize<Person>(kryo)) {
|
with(bits.deserialize<Person>(kryo)) {
|
||||||
assertEquals("mike", name)
|
assertEquals("mike", name)
|
||||||
assertEquals(april_17th, birthday)
|
assertEquals(april_17th, birthday)
|
||||||
@ -49,15 +39,4 @@ class KryoTests {
|
|||||||
assertNull(birthday)
|
assertNull(birthday)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun constructorInvariant() {
|
|
||||||
val pos = MustBeWhizzy("whizzle")
|
|
||||||
val bits = pos.serialize(kryo)
|
|
||||||
// Hack the serialized bytes here, like a very naughty hacker might.
|
|
||||||
bits[10] = 'o'.toByte()
|
|
||||||
assertFailsWith<AssertionError>("must be whizzy") {
|
|
||||||
bits.deserialize<MustBeWhizzy>(kryo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user