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.
This commit is contained in:
Mike Hearn 2015-12-22 16:41:25 +00:00
parent 3c23c4f53d
commit 31fbf5e1eb
4 changed files with 141 additions and 49 deletions

View File

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

View File

@ -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<ContractStateRef>,
}
}
/** Container for a [WireTransaction] and attached signatures. */
data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, val sigs: List<DigitalSignature.WithKey>) {
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<ContractStateRef>
fun commands(): List<Command> = ArrayList(commands)
}
data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, val sigs: List<DigitalSignature.WithKey>) {
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

View File

@ -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<T : Any>(bits: ByteArray) : OpaqueBytes(bits)
class SerializedBytes<T : Any>(bits: ByteArray) : OpaqueBytes(bits) {
@Transient val hash: SecureHash by lazy { bits.sha256() }
}
fun ByteArray.opaque(): OpaqueBytes = OpaqueBytes(this)
inline fun <reified T : Any> SerializedBytes<T>.deserialize(): T = bits.deserialize()

View File

@ -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 : Any> T.serialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): SerializedBytes
Output(stream).use {
kryo.writeObject(it, this)
}
return SerializedBytes<T>(stream.toByteArray())
return SerializedBytes(stream.toByteArray())
}
class ImmutableClassSerializer<T : Any>(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)
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!! }
}
}
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))
}
}
}