mirror of
https://github.com/corda/corda.git
synced 2025-01-02 11:16:44 +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! :-)" }
|
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
|
* 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
|
* 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
|
* keypairs. Note that a sighash is not the same thing as a *transaction id*, which is the hash of the entire
|
||||||
* i.e. the outermost serialised form with everything included.
|
* 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
|
* 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.
|
* 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
|
* 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.
|
* 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)
|
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.
|
* 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
|
* 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
|
package core.serialization
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding
|
import com.google.common.io.BaseEncoding
|
||||||
|
import core.SecureHash
|
||||||
|
import core.sha256
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,7 +37,9 @@ open class OpaqueBytes(val bits: ByteArray) {
|
|||||||
val size: Int get() = bits.size
|
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)
|
fun ByteArray.opaque(): OpaqueBytes = OpaqueBytes(this)
|
||||||
inline fun <reified T : Any> SerializedBytes<T>.deserialize(): T = bits.deserialize()
|
inline fun <reified T : Any> SerializedBytes<T>.deserialize(): T = bits.deserialize()
|
||||||
|
@ -9,12 +9,21 @@
|
|||||||
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 core.SignedWireTransaction
|
||||||
import de.javakaffee.kryoserializers.ArraysAsListSerializer
|
import de.javakaffee.kryoserializers.ArraysAsListSerializer
|
||||||
import org.objenesis.strategy.StdInstantiatorStrategy
|
import org.objenesis.strategy.StdInstantiatorStrategy
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.util.*
|
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
|
||||||
@ -54,7 +63,70 @@ fun <T : Any> T.serialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): SerializedBytes
|
|||||||
Output(stream).use {
|
Output(stream).use {
|
||||||
kryo.writeObject(it, this)
|
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 {
|
fun createKryo(): Kryo {
|
||||||
@ -66,5 +138,16 @@ fun createKryo(): Kryo {
|
|||||||
instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy())
|
instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy())
|
||||||
|
|
||||||
register(Arrays.asList( "" ).javaClass, ArraysAsListSerializer());
|
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