Major: Integrate serialisation everywhere, implement basic signing/sig checking code.

Currently the serialised forms and the form fed into the contract aren't quite joined up, because on disk/network a transaction is serialised with input references i.e. (txhash, output index) pairs, but the contract wants to see all input states in fully loaded form. To do that, we need some notion of a database of transactions.
This commit is contained in:
Mike Hearn 2015-11-13 23:12:39 +01:00
parent 162b830bcd
commit dad59d116c
12 changed files with 334 additions and 137 deletions

View File

@ -57,10 +57,7 @@ object Cash : Contract {
* A command stating that money has been withdrawn from the shared ledger and is now accounted for
* in some other way.
*/
class Exit(val amount: Amount) : Command {
override fun equals(other: Any?) = other is Exit && other.amount == amount
override fun hashCode() = amount.hashCode()
}
data class Exit(val amount: Amount) : Command
}
/** This is the function EVERYONE runs */
@ -178,7 +175,7 @@ object Cash : Contract {
} else states
// Finally, generate the commands. Pretend to sign here, real signatures aren't done yet.
val commands = keysUsed.map { VerifiedSigned(listOf(it), emptyList(), Commands.Move) }
val commands = keysUsed.map { AuthenticatedObject(listOf(it), emptyList(), Commands.Move) }
return TransactionForTest(gathered.toArrayList(), outputs.toArrayList(), commands.toArrayList())
}

View File

@ -1,5 +1,6 @@
package core
import core.serialization.SerializeableWithKryo
import java.math.BigDecimal
import java.security.PublicKey
import java.util.*
@ -8,29 +9,16 @@ import kotlin.math.div
/**
* Defines a simple domain specific language for the specificiation of financial contracts. Currently covers:
*
* - Some utilities for working with commands.
* - Code for working with currencies.
* - An Amount type that represents a positive quantity of a specific currency.
* - A simple language extension for specifying requirements in English, along with logic to enforce them.
*
* TODO: Look into replacing Currency and Amount with CurrencyUnit and MonetaryAmount from the javax.money API (JSR 354)
*/
// TODO: Look into replacing Currency and Amount with CurrencyUnit and MonetaryAmount from the javax.money API (JSR 354)
//// Currencies ///////////////////////////////////////////////////////////////////////////////////////////////////////
// region Misc
inline fun <reified T : Command> List<VerifiedSigned<Command>>.select(signer: PublicKey? = null, institution: Institution? = null) =
filter { it.value is T }.
filter { if (signer == null) true else it.signers.contains(signer) }.
filter { if (institution == null) true else it.signingInstitutions.contains(institution) }.
map { VerifiedSigned<T>(it.signers, it.signingInstitutions, it.value as T) }
inline fun <reified T : Command> List<VerifiedSigned<Command>>.requireSingleCommand() = try {
select<T>().single()
} catch (e: NoSuchElementException) {
throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message.
}
// endregion
// region Currencies
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
fun currency(code: String) = Currency.getInstance(code)
val USD = currency("USD")
@ -40,14 +28,8 @@ val CHF = currency("CHF")
val Int.DOLLARS: Amount get() = Amount(this * 100, USD)
val Int.POUNDS: Amount get() = Amount(this * 100, GBP)
val Int.SWISS_FRANCS: Amount get() = Amount(this * 100, CHF)
val Double.DOLLARS: Amount get() = Amount((this * 100).toInt(), USD)
val Double.POUNDS: Amount get() = Amount((this * 100).toInt(), USD)
val Double.SWISS_FRANCS: Amount get() = Amount((this * 100).toInt(), USD)
// endregion
// region Requirements
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
class Requirements {
infix fun String.by(expr: Boolean) {
@ -59,11 +41,7 @@ inline fun requireThat(body: Requirements.() -> Unit) {
R.body()
}
// endregion
// region Amounts
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//// Amounts //////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Amount represents a positive quantity of currency, measured in pennies, which are the smallest representable units.
@ -75,8 +53,10 @@ inline fun requireThat(body: Requirements.() -> Unit) {
*
* It probably makes sense to replace this with convenience extensions over the JSR 354 MonetaryAmount interface, if
* that spec doesn't turn out to be too heavy (it looks fairly complicated).
*
* TODO: Think about how positive-only vs positive-or-negative amounts can be represented in the type system.
*/
data class Amount(val pennies: Int, val currency: Currency) : Comparable<Amount> {
data class Amount(val pennies: Int, val currency: Currency) : Comparable<Amount>, SerializeableWithKryo {
init {
// 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.
@ -113,5 +93,15 @@ fun Iterable<Amount>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThr
fun Iterable<Amount>.sumOrThrow() = reduce { left, right -> left + right }
fun Iterable<Amount>.sumOrZero(currency: Currency) = if (iterator().hasNext()) sumOrThrow() else Amount(0, currency)
// TODO: Think about how positive-only vs positive-or-negative amounts can be represented in the type system.
// endregion
//// Authenticated commands ///////////////////////////////////////////////////////////////////////////////////////////
inline fun <reified T : Command> List<AuthenticatedObject<Command>>.select(signer: PublicKey? = null, institution: Institution? = null) =
filter { it.value is T }.
filter { if (signer == null) true else it.signers.contains(signer) }.
filter { if (institution == null) true else it.signingInstitutions.contains(institution) }.
map { AuthenticatedObject<T>(it.signers, it.signingInstitutions, it.value as T) }
inline fun <reified T : Command> List<AuthenticatedObject<Command>>.requireSingleCommand() = try {
select<T>().single()
} catch (e: NoSuchElementException) {
throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message.
}

View File

@ -1,13 +1,13 @@
package core
import com.google.common.io.BaseEncoding
import java.security.MessageDigest
import java.security.PublicKey
import java.security.*
// "sealed" here means there can't be any subclasses other than the ones defined here.
sealed class SecureHash(bits: ByteArray) : OpaqueBytes(bits) {
class SHA256(bits: ByteArray) : SecureHash(bits) {
init { require(bits.size == 32) }
override val signatureAlgorithmName: String get() = "SHA256withECDSA"
}
// Like static methods in Java, except the 'companion' is a singleton that can have state.
@ -23,17 +23,25 @@ sealed class SecureHash(bits: ByteArray) : OpaqueBytes(bits) {
fun sha256(str: String) = sha256(str.toByteArray())
}
abstract val signatureAlgorithmName: String
// In future, maybe SHA3, truncated hashes etc.
}
/**
* A wrapper around a digital signature. The covering field is a generic tag usable by whatever is interpreting the
* signature.
* signature. It isn't used currently, but experience from Bitcoin suggests such a feature is useful, especially when
* building partially signed transactions.
*/
sealed class DigitalSignature(bits: ByteArray, val covering: Int) : OpaqueBytes(bits) {
/** A digital signature that identifies who the public key is owned by */
open class WithKey(val by: PublicKey, bits: ByteArray, covering: Int) : DigitalSignature(bits, covering)
open class DigitalSignature(bits: ByteArray, val covering: Int = 0) : OpaqueBytes(bits) {
/** A digital signature that identifies who the public key is owned by. */
open class WithKey(val by: PublicKey, bits: ByteArray, covering: Int = 0) : DigitalSignature(bits, covering) {
fun verifyWithECDSA(content: ByteArray) = by.verifyWithECDSA(content, this)
}
class LegallyIdentifiable(val signer: Institution, bits: ByteArray, covering: Int) : WithKey(signer.owningKey, bits, covering)
}
object NullPublicKey : PublicKey, Comparable<PublicKey> {
@ -43,3 +51,21 @@ object NullPublicKey : PublicKey, Comparable<PublicKey> {
override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1
override fun toString() = "NULL_KEY"
}
/** Utility to simplify the act of signing a byte array */
fun PrivateKey.signWithECDSA(bits: ByteArray, publicKey: PublicKey? = null): DigitalSignature {
val signer = Signature.getInstance("SHA256withECDSA")
signer.initSign(this)
signer.update(bits)
val sig = signer.sign()
return if (publicKey == null) DigitalSignature(sig) else DigitalSignature.WithKey(publicKey, sig)
}
/** Utility to simplify the act of verifying a signature */
fun PublicKey.verifyWithECDSA(content: ByteArray, signature: DigitalSignature) {
val verifier = Signature.getInstance("SHA256withECDSA")
verifier.initVerify(this)
verifier.update(content)
if (verifier.verify(signature.bits) == false)
throw SignatureException("Signature did not match")
}

View File

@ -1,5 +1,7 @@
package core
import core.serialization.SerializeableWithKryo
import core.serialization.serialize
import java.security.PublicKey
/**
@ -7,7 +9,7 @@ import java.security.PublicKey
* 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.
*/
interface ContractState {
interface ContractState : SerializeableWithKryo {
/**
* 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
@ -16,34 +18,30 @@ interface ContractState {
val programRef: SecureHash
}
/**
* A stateref is a pointer to a state, this is an equivalent of an "outpoint" in Bitcoin.
*/
class ContractStateRef(private val txhash: SecureHash.SHA256, private val index: Int)
/** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */
fun ContractState.hash(): SecureHash = SecureHash.sha256((serialize()))
class Institution(
val name: String,
val owningKey: PublicKey
) {
/** A stateref is a pointer to a state, this is an equivalent of an "outpoint" in Bitcoin. */
data class ContractStateRef(val txhash: SecureHash.SHA256, val index: Int) : SerializeableWithKryo
/** An Institution is well known (name, pubkey) pair. In a real system this would probably be an X.509 certificate. */
data class Institution(val name: String, val owningKey: PublicKey) : SerializeableWithKryo {
override fun toString() = name
}
/** Marker interface for objects that represent commands */
interface Command
/**
* Reference to something being stored or issued by an institution 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 institution.
*/
data class InstitutionReference(val institution: Institution, val reference: OpaqueBytes) : SerializeableWithKryo {
override fun toString() = "${institution.name}$reference"
}
/** Provided as an input to a contract; converted to a [VerifiedSigned] by the platform before execution. */
data class SignedCommand(
/** Signatures over this object to prove who it came from: this is fetched off the end of the transaction wire format. */
val commandDataSignatures: List<DigitalSignature.WithKey>,
/** Marker interface for classes that represent commands */
interface Command : SerializeableWithKryo
/** Command data, deserialized to an implementation of [Command] */
val serialized: OpaqueBytes,
/** Identifies what command the serialized data contains (hash of bytecode?) */
val classID: SecureHash
)
/** Obtained from a [SignedCommand], deserialised and signature checked */
data class VerifiedSigned<out T : Command>(
/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
data class AuthenticatedObject<out T : Any>(
val signers: List<PublicKey>,
/** If any public keys were recognised, the looked up institutions are available here */
val signingInstitutions: List<Institution>,
@ -68,12 +66,4 @@ interface Contract {
// TODO: This should probably be a hash of a document, rather than a URL to it.
/** Unparsed reference to the natural language contract that this code is supposed to express (usually a URL). */
val legalContractReference: String
}
/**
* Reference to something being stored or issued by an institution 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 institution.
*/
data class InstitutionReference(val institution: Institution, val reference: OpaqueBytes) {
override fun toString() = "${institution.name}$reference"
}

View File

@ -1,12 +1,13 @@
package core
import contracts.*
import core.serialization.SerializeableWithKryo
import java.math.BigInteger
import java.security.PublicKey
import java.time.Instant
import kotlin.test.fail
class DummyPublicKey(private val s: String) : PublicKey, Comparable<PublicKey> {
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey>, SerializeableWithKryo {
override fun getAlgorithm() = "DUMMY"
override fun getEncoded() = s.toByteArray()
override fun getFormat() = "ASN.1"
@ -69,9 +70,9 @@ class TransactionForTest() {
override fun hashCode(): Int = state.hashCode()
}
private val outStates = arrayListOf<LabeledOutput>()
private val args: MutableList<VerifiedSigned<Command>> = arrayListOf()
private val args: MutableList<AuthenticatedObject<Command>> = arrayListOf()
constructor(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSigned<Command>>) : this() {
constructor(inStates: List<ContractState>, outStates: List<ContractState>, args: List<AuthenticatedObject<Command>>) : this() {
this.inStates.addAll(inStates)
this.outStates.addAll(outStates.map { LabeledOutput(null, it) })
this.args.addAll(args)
@ -82,7 +83,7 @@ class TransactionForTest() {
fun arg(vararg key: PublicKey, c: () -> Command) {
val keys = listOf(*key)
// TODO: replace map->filterNotNull once upgraded to next Kotlin
args.add(VerifiedSigned(keys, keys.map { TEST_KEYS_TO_CORP_MAP[it] }.filterNotNull(), c()))
args.add(AuthenticatedObject(keys, keys.map { TEST_KEYS_TO_CORP_MAP[it] }.filterNotNull(), c()))
}
private fun run(time: Instant) = TransactionForVerification(inStates, outStates.map { it.state }, args, time).verify(TEST_PROGRAM_MAP)

View File

@ -1,33 +1,124 @@
package core
import core.serialization.SerializeableWithKryo
import core.serialization.deserialize
import core.serialization.serialize
import java.security.KeyPair
import java.security.PublicKey
import java.security.SignatureException
import java.time.Instant
import java.util.*
// Various views of transactions as they progress through the pipeline:
//
// TimestampedWireTransaction(WireTransaction) -> LedgerTransaction -> TransactionForVerification
// TransactionForTest
/**
* Views of a transaction as it progresses through the pipeline, from bytes loaded from disk/network to the object
* tree passed into a contract.
*
* TimestampedWireTransaction wraps a serialized SignedWireTransaction. The timestamp is a signature from a timestamping
* authority and is what gives the contract a sense of time. This isn't used yet.
*
* SignedWireTransaction wraps a serialized WireTransaction. It contains one or more ECDSA signatures, each one from
* a public key that is mentioned inside a transaction command.
*
* WireTransaction is a transaction in a form ready to be serialised/unserialised/hashed. This is the object from which
* a transaction ID (hash) is calculated. It contains no signatures and no timestamp. That means, sending a transaction
* to a timestamping authority does NOT change its hash (this may be an issue that leads to confusion and should be
* examined more closely).
*
* LedgerTransaction is derived from WireTransaction and TimestampedWireTransaction together. It is the result of
* doing some basic key lookups on WireCommand to see if any keys are from a recognised institution, thus converting
* the WireCommand objects into AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->institution
* map. In future it'd make more sense to use a certificate scheme and so that logic would get more complex.
*
* All the above refer to inputs using a (txhash, output index) pair.
*
* TransactionForVerification is the same as LedgerTransaction but with the input states looked up from a local
* database and replaced with the real objects. TFV is the form that is finally fed into the contracts.
*/
class WireTransaction(
// TODO: This is supposed to be a protocol buffer, FIX SPE message, etc. For prototype it can just be Kryo serialised.
val tx: ByteArray,
/** 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
// We assume Ed25519 signatures for all. Num signatures == array.length / 64 (each sig is 64 bytes in size)
// This array is in the same order as the public keys in the commands array, so signatures can be matched to
// public keys in that manner.
val signatures: ByteArray
)
/** Transaction ready for serialisation, without any signatures attached. */
data class WireTransaction(val inputStates: List<ContractStateRef>,
val outputStates: List<ContractState>,
val args: List<WireCommand>) : SerializeableWithKryo {
fun signWith(keys: List<KeyPair>): SignedWireTransaction {
val keyMap = keys.map { it.public to it.private }.toMap()
val bits = serialize()
val signWith = args.flatMap { it.pubkeys }.toSet()
if (keys.size != signWith.size)
throw IllegalArgumentException("Incorrect number of keys provided: ${keys.size} vs ${signWith.size}")
val sigs = ArrayList<DigitalSignature.WithKey>()
for (key in signWith) {
val priv = keyMap[key] ?: throw IllegalArgumentException("Command without private signing key found")
sigs.add(priv.signWithECDSA(bits, key) as DigitalSignature.WithKey)
}
return SignedWireTransaction(bits, sigs)
}
class TimestampedWireTransaction(
// A serialised WireTransaction
val hash: SecureHash get() = SecureHash.sha256(serialize())
fun toLedgerTransaction(timestamp: Instant, institutionKeyMap: Map<PublicKey, Institution>): LedgerTransaction {
val authenticatedArgs = args.map {
// TODO: Replace map/filterNotNull with mapNotNull on next Kotlin upgrade.
val institutions = it.pubkeys.map { pk -> institutionKeyMap[pk] }.filterNotNull()
AuthenticatedObject(it.pubkeys, institutions, it.command)
}
return LedgerTransaction(inputStates, outputStates, authenticatedArgs, timestamp)
}
}
data class SignedWireTransaction(val txBits: ByteArray, val sigs: List<DigitalSignature.WithKey>) : SerializeableWithKryo {
init {
check(sigs.isNotEmpty())
}
/**
* Verifies the given signatures against the serialized transaction data. Does NOT deserialise or check the contents
* to ensure there are no missing signatures: use verify() to do that. This weaker version can be useful for
* checking a partially signed transaction being prepared by multiple co-operating parties.
*
* @throws SignatureException if the signature is invalid or does not match.
*/
fun verifySignatures() {
for (sig in sigs)
sig.verifyWithECDSA(txBits)
}
/**
* Verify the signatures, deserialise the wire transaction and then check that the set of signatures found matches
* the set of pubkeys in the commands.
*
* @throws SignatureException if the signature is invalid or does not match.
*/
fun verify(): WireTransaction {
verifySignatures()
val wtx = txBits.deserialize<WireTransaction>()
// Verify that every command key was in the set that we just verified: there should be no commands that were
// unverified.
val cmdKeys = wtx.args.flatMap { it.pubkeys }.toSet()
val sigKeys = sigs.map { it.by }.toSet()
if (cmdKeys != sigKeys)
throw SignatureException("Command keys don't match the signatures: $cmdKeys vs $sigKeys")
return wtx
}
}
// Not used yet.
data class TimestampedWireTransaction(
/** A serialised SignedWireTransaction */
val wireTX: ByteArray,
// This is, for example, an RFC 3161 serialised structure (but we probably want something more compact).
/** Signature from a timestamping authority. For instance using RFC 3161 */
val timestamp: ByteArray
)
) : SerializeableWithKryo
/**
* A LedgerTransaction wraps the data needed to calculate one or more successor states from a set of input states.
* It is the first step after extraction from a WireTransaction. The signature part is tricky.
* It is the first step after extraction from a WireTransaction. The signatures at this point have been lined up
* with the commands from the wire, and verified/looked up.
*
* Not used yet.
*/
class LedgerTransaction(
/** The input states which will be consumed/invalidated by the execution of this transaction. */
@ -35,20 +126,18 @@ class LedgerTransaction(
/** The states that will be generated by the execution of this transaction. */
val outputStates: List<ContractState>,
/** Arbitrary data passed to the program of each input state. */
val args: List<SignedCommand>,
val args: List<AuthenticatedObject<Command>>,
/** The moment the transaction was timestamped for */
val time: Instant
// TODO: nLockTime equivalent?
)
/** A transaction in fully resolved form, ready for passing as input to a verification function */
class TransactionForVerification(
val inStates: List<ContractState>,
val outStates: List<ContractState>,
val args: List<VerifiedSigned<Command>>,
val time: Instant
) {
/** A transaction in fully resolved and sig-checked form, ready for passing as input to a verification function. */
class TransactionForVerification(val inStates: List<ContractState>,
val outStates: List<ContractState>,
val args: List<AuthenticatedObject<Command>>,
val time: Instant) {
fun verify(programMap: Map<SecureHash, Contract>) {
// For each input and output state, locate the program to run. Then execute the verification function. If any
// throws an exception, the entire transaction is invalid.
@ -58,4 +147,5 @@ class TransactionForVerification(
program.verify(this)
}
}
}

View File

@ -1,11 +1,12 @@
package core
import com.google.common.io.BaseEncoding
import core.serialization.SerializeableWithKryo
import java.time.Duration
import java.util.*
/** 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) {
open class OpaqueBytes(val bits: ByteArray) : SerializeableWithKryo {
companion object {
fun of(vararg b: Byte) = OpaqueBytes(byteArrayOf(*b))
}
@ -17,7 +18,6 @@ open class OpaqueBytes(val bits: ByteArray) {
}
override fun hashCode() = Arrays.hashCode(bits)
override fun toString() = "[" + BaseEncoding.base16().encode(bits) + "]"
}

View File

@ -6,13 +6,14 @@ import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.serializers.JavaSerializer
import core.Amount
import core.InstitutionReference
import core.OpaqueBytes
import contracts.Cash
import contracts.ComedyPaper
import core.*
import java.io.ByteArrayOutputStream
import java.lang.reflect.InvocationTargetException
import java.security.PublicKey
import java.security.KeyPairGenerator
import java.time.Instant
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty
import kotlin.reflect.jvm.javaType
@ -68,9 +69,10 @@ import kotlin.reflect.primaryConstructor
*
* - 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.
* - The only properties that exist must be arguments to the constructor. This will need to be relaxed to allow
* for pure-code (no backing state) getters, and to support constant fields like legalContractRef.
* - 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
@ -96,6 +98,11 @@ import kotlin.reflect.primaryConstructor
*
*/
/**
* Marker interface for classes to use with [DataClassSerializer]. Note that only constructor defined properties will
* be serialised!
*/
interface SerializeableWithKryo
class DataClassSerializer<T : SerializeableWithKryo>(val klass: KClass<T>) : Serializer<T>() {
@ -104,12 +111,7 @@ class DataClassSerializer<T : SerializeableWithKryo>(val klass: KClass<T>) : Ser
val constructor = klass.primaryConstructor!!
init {
// Verify that this class is safe to serialise.
//
// 1) No properties that aren't in the constructor.
// 2) Objects are immutable (all properties are final)
assert(props.size == constructor.parameters.size)
assert(props.map { it.name }.toSortedSet() == constructor.parameters.map { it.name }.toSortedSet())
// Verify that this class is immutable (all properties are final)
assert(props.none { it is KMutableProperty<*> })
}
@ -126,7 +128,11 @@ class DataClassSerializer<T : SerializeableWithKryo>(val klass: KClass<T>) : Ser
"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 -> kryo.writeClassAndObject(output, kProperty.get(obj))
else -> try {
kryo.writeClassAndObject(output, kProperty.get(obj))
} catch (e: Exception) {
throw IllegalStateException("Failed to serialize ${param.name} in ${klass.qualifiedName}", e)
}
}
}
}
@ -163,10 +169,12 @@ class DataClassSerializer<T : SerializeableWithKryo>(val klass: KClass<T>) : Ser
}
}
inline fun <reified T : SerializeableWithKryo> Kryo.registerDataClass() = register(T::class.java, DataClassSerializer(T::class))
inline fun <reified T : SerializeableWithKryo> ByteArray.deserialize(kryo: Kryo): T = kryo.readObject(Input(this), T::class.java)
val THREAD_LOCAL_KRYO = ThreadLocal.withInitial { createKryo() }
fun SerializeableWithKryo.serialize(kryo: Kryo): ByteArray {
inline fun <reified T : SerializeableWithKryo> Kryo.registerDataClass() = register(T::class.java, DataClassSerializer(T::class))
inline fun <reified T : SerializeableWithKryo> ByteArray.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): T = kryo.readObject(Input(this), T::class.java)
fun SerializeableWithKryo.serialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): ByteArray {
val stream = ByteArrayOutputStream()
Output(stream).use {
kryo.writeObject(it, this)
@ -174,20 +182,52 @@ fun SerializeableWithKryo.serialize(kryo: Kryo): ByteArray {
return stream.toByteArray()
}
fun kryo(): Kryo {
private val UNUSED_EC_KEYPAIR = KeyPairGenerator.getInstance("EC").genKeyPair()
fun createKryo(): Kryo {
return Kryo().apply {
// Require explicit listing of all types that can be deserialised, to defend against classes that aren't
// designed for serialisation being unexpectedly instantiated.
isRegistrationRequired = true
// Allow various array and list types. Sometimes when the type is private/internal we have to give an example
// instead and then get the class from that. These have built in Kryo serializers that are safe to use.
register(ByteArray::class.java)
register(IntArray::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)
// 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.
// 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(PublicKey::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())
// Now register platform types.
registerDataClass<SecureHash.SHA256>()
registerDataClass<Amount>()
registerDataClass<InstitutionReference>()
registerDataClass<Institution>()
registerDataClass<OpaqueBytes>()
registerDataClass<SignedWireTransaction>()
registerDataClass<ContractStateRef>()
registerDataClass<WireTransaction>()
registerDataClass<WireCommand>()
// TODO: This is obviously a short term hack: there needs to be a way to bundle up and register contracts.
registerDataClass<Cash.State>()
register(Cash.Commands.Move.javaClass)
registerDataClass<Cash.Commands.Exit>()
registerDataClass<ComedyPaper.State>()
register(ComedyPaper.Commands.Move.javaClass)
register(ComedyPaper.Commands.Redeem.javaClass)
// And for unit testing ...
registerDataClass<DummyPublicKey>()
}
}

View File

@ -34,7 +34,7 @@ class CashTests {
transaction {
output { outState }
// No command arguments
this `fails requirement` "required move command"
this `fails requirement` "required contracts.Cash.Commands.Move command"
}
transaction {
output { outState }
@ -167,7 +167,7 @@ class CashTests {
transaction {
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
this `fails requirement` "required move command"
this `fails requirement` "required contracts.Cash.Commands.Move command"
transaction {
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move }

View File

@ -8,16 +8,15 @@ import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
data class Person(val name: String, val birthday: Instant?) : SerializeableWithKryo
data class MustBeWhizzy(val s: String) : SerializeableWithKryo {
init {
assert(s.startsWith("whiz")) { "must be whizzy" }
}
}
class KryoTests {
private val kryo: Kryo = kryo().apply {
data class Person(val name: String, val birthday: Instant?) : SerializeableWithKryo
data class MustBeWhizzy(val s: String) : SerializeableWithKryo {
init {
assert(s.startsWith("whiz")) { "must be whizzy" }
}
}
private val kryo: Kryo = createKryo().apply {
registerDataClass<Person>()
registerDataClass<MustBeWhizzy>()
}

View File

@ -0,0 +1,56 @@
package serialization
import contracts.Cash
import core.*
import org.junit.Test
import testutils.TestUtils
import java.security.SignatureException
import kotlin.test.assertFailsWith
class TransactionSerializationTests {
// Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change).
// It refers to a fake TX/state that we don't bother creating here.
val depositRef = InstitutionReference(MINI_CORP, OpaqueBytes.of(1))
val outputState = Cash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1)
val changeState = Cash.State(depositRef, 400.POUNDS, TestUtils.keypair.public)
val fakeStateRef = ContractStateRef(SecureHash.sha256("fake tx id"), 0)
val tx = WireTransaction(
arrayListOf(fakeStateRef),
arrayListOf(outputState, changeState),
arrayListOf(WireCommand(Cash.Commands.Move, arrayListOf(TestUtils.keypair.public)))
)
@Test
fun signWireTX() {
val signedTX: SignedWireTransaction = tx.signWith(listOf(TestUtils.keypair))
// Now check that the signature we just made verifies.
signedTX.verify()
// Corrupt the data and ensure the signature catches the problem.
signedTX.txBits[5] = 0
assertFailsWith(SignatureException::class) {
signedTX.verify()
}
}
@Test
fun wrongKeys() {
// Can't sign without the private key.
assertFailsWith(IllegalArgumentException::class) {
tx.signWith(listOf())
}
// Can't sign with too many keys.
assertFailsWith(IllegalArgumentException::class) {
tx.signWith(listOf(TestUtils.keypair, TestUtils.keypair2))
}
val signedTX = tx.signWith(listOf(TestUtils.keypair))
// If the signature was removed in transit, we don't like it.
assertFailsWith(SignatureException::class) {
signedTX.copy(sigs = emptyList()).verify()
}
}
}

View File

@ -0,0 +1,8 @@
package testutils
import java.security.KeyPairGenerator
object TestUtils {
val keypair = KeyPairGenerator.getInstance("EC").genKeyPair()
val keypair2 = KeyPairGenerator.getInstance("EC").genKeyPair()
}