mirror of
https://github.com/corda/corda.git
synced 2024-12-21 05:53:23 +00:00
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:
parent
3c23c4f53d
commit
31fbf5e1eb
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user