From 31fbf5e1eb65a9a28e1519f8cb9fd1362f7e66f0 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 22 Dec 2015 16:41:25 +0000 Subject: [PATCH] Improve the transactions API a small amount with lazily deserialized access to the WireTransaction inside a SignedTransaction, and id/hash fields (again lazily calculated). This required bringing back the ImmutableClassSerializer and registration of classes that need it, to ensure the constructors run. --- .../protocols/TwoPartyTradeProtocol.kt | 2 +- src/main/kotlin/core/Transactions.kt | 97 ++++++++++--------- .../kotlin/core/serialization/ByteArrays.kt | 6 +- src/main/kotlin/core/serialization/Kryo.kt | 85 +++++++++++++++- 4 files changed, 141 insertions(+), 49 deletions(-) diff --git a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt index 3f1858f85a..b4cc3d8511 100644 --- a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt +++ b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt @@ -191,7 +191,7 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) : logger().trace { "Fully signed transaction was valid. Trade complete! :-)" } - return Pair(fullySigned.verify(), ltx) + return Pair(fullySigned.tx, ltx) } } diff --git a/src/main/kotlin/core/Transactions.kt b/src/main/kotlin/core/Transactions.kt index 2b805b7e38..03fd2d2822 100644 --- a/src/main/kotlin/core/Transactions.kt +++ b/src/main/kotlin/core/Transactions.kt @@ -28,8 +28,8 @@ import java.util.* * * WireTransaction is a transaction in a form ready to be serialised/unserialised. A WireTransaction can be hashed * in various ways to calculate a *signature hash* (or sighash), this is the hash that is signed by the various involved - * keypairs. Note that a sighash is not the same thing as a *transaction id*, which is the hash of a SignedWireTransaction - * i.e. the outermost serialised form with everything included. + * keypairs. Note that a sighash is not the same thing as a *transaction id*, which is the hash of the entire + * WireTransaction i.e. the outermost serialised form with everything included. * * A TransactionBuilder is a transaction class that's mutable (unlike the others which are all immutable). It is * intended to be passed around contracts that may edit it by adding new states/commands or modifying the existing set. @@ -60,6 +60,55 @@ data class WireTransaction(val inputStates: List, } } +/** Container for a [WireTransaction] and attached signatures. */ +data class SignedWireTransaction(val txBits: SerializedBytes, val sigs: List) { + init { check(sigs.isNotEmpty()) } + + // Lazily calculated access to the deserialised/hashed transaction data. + @Transient val tx: WireTransaction by lazy { txBits.deserialize() } + + /** A transaction ID is the hash of the [WireTransaction]. Thus adding or removing a signature does not change it. */ + val id: SecureHash get() = txBits.hash + + /** + * 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.bits) + } + + /** + * 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() { + verifySignatures() + // Verify that every command key was in the set that we just verified: there should be no commands that were + // unverified. + val cmdKeys = tx.commands.flatMap { it.pubkeys }.toSet() + val sigKeys = sigs.map { it.by }.toSet() + if (!sigKeys.containsAll(cmdKeys)) + throw SignatureException("Missing signatures on the transaction for: ${cmdKeys - sigKeys}") + } + + /** + * Calls [verify] to check all required signatures are present, and then uses the passed [IdentityService] to call + * [WireTransaction.toLedgerTransaction] to look up well known identities from pubkeys. + */ + fun verifyToLedgerTransaction(identityService: IdentityService): LedgerTransaction { + verify() + return tx.toLedgerTransaction(identityService, id) + } +} + + /** * Thrown if an attempt is made to timestamp a transaction using a trusted timestamper, but the time on the transaction * is too far in the past or future relative to the local clock and thus the timestamper would reject it. @@ -180,50 +229,6 @@ class TransactionBuilder(private val inputStates: MutableList fun commands(): List = ArrayList(commands) } -data class SignedWireTransaction(val txBits: SerializedBytes, val sigs: List) { - 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.bits) - } - - /** - * 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() - // Verify that every command key was in the set that we just verified: there should be no commands that were - // unverified. - val cmdKeys = wtx.commands.flatMap { it.pubkeys }.toSet() - val sigKeys = sigs.map { it.by }.toSet() - if (!sigKeys.containsAll(cmdKeys)) - throw SignatureException("Missing signatures on the transaction for: ${cmdKeys - sigKeys}") - return wtx - } - - /** - * Calls [verify] to check all required signatures are present, and then uses the passed [IdentityService] to call - * [WireTransaction.toLedgerTransaction] to look up well known identities from pubkeys. - */ - fun verifyToLedgerTransaction(identityService: IdentityService): LedgerTransaction { - return verify().toLedgerTransaction(identityService, txBits.bits.sha256()) - } -} - /** * 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 signatures at this point have been lined up diff --git a/src/main/kotlin/core/serialization/ByteArrays.kt b/src/main/kotlin/core/serialization/ByteArrays.kt index f6c3b7a6ea..07724ca8d2 100644 --- a/src/main/kotlin/core/serialization/ByteArrays.kt +++ b/src/main/kotlin/core/serialization/ByteArrays.kt @@ -9,6 +9,8 @@ package core.serialization import com.google.common.io.BaseEncoding +import core.SecureHash +import core.sha256 import java.util.* /** @@ -35,7 +37,9 @@ open class OpaqueBytes(val bits: ByteArray) { val size: Int get() = bits.size } -class SerializedBytes(bits: ByteArray) : OpaqueBytes(bits) +class SerializedBytes(bits: ByteArray) : OpaqueBytes(bits) { + @Transient val hash: SecureHash by lazy { bits.sha256() } +} fun ByteArray.opaque(): OpaqueBytes = OpaqueBytes(this) inline fun SerializedBytes.deserialize(): T = bits.deserialize() diff --git a/src/main/kotlin/core/serialization/Kryo.kt b/src/main/kotlin/core/serialization/Kryo.kt index 9a695d27bb..5b6696100f 100644 --- a/src/main/kotlin/core/serialization/Kryo.kt +++ b/src/main/kotlin/core/serialization/Kryo.kt @@ -9,12 +9,21 @@ package core.serialization 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.Output +import core.SignedWireTransaction import de.javakaffee.kryoserializers.ArraysAsListSerializer import org.objenesis.strategy.StdInstantiatorStrategy import java.io.ByteArrayOutputStream +import java.lang.reflect.InvocationTargetException 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 @@ -54,7 +63,70 @@ fun T.serialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): SerializedBytes Output(stream).use { kryo.writeObject(it, this) } - return SerializedBytes(stream.toByteArray()) + return SerializedBytes(stream.toByteArray()) +} + +class ImmutableClassSerializer(val klass: KClass) : Serializer() { + 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 { + assert(type.kotlin == klass) + 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(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!! } + } } fun createKryo(): Kryo { @@ -66,5 +138,16 @@ fun createKryo(): Kryo { instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy()) register(Arrays.asList( "" ).javaClass, ArraysAsListSerializer()); + + // Some classes have to be handled with the ImmutableClassSerializer because they need to have their + // constructors be invoked (typically for lazy members). + val immutables = listOf( + SignedWireTransaction::class, + SerializedBytes::class + ) + + immutables.forEach { + register(it.java, ImmutableClassSerializer(it)) + } } } \ No newline at end of file