mirror of
https://github.com/corda/corda.git
synced 2025-01-19 03:06:36 +00:00
Minor: split TransactionBuilder into its own file, so Transactions.kt is just the core immutable types.
This commit is contained in:
parent
d9cfb5e1eb
commit
306ff69312
170
core/src/main/kotlin/core/TransactionBuilder.kt
Normal file
170
core/src/main/kotlin/core/TransactionBuilder.kt
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import core.crypto.DigitalSignature
|
||||||
|
import core.crypto.SecureHash
|
||||||
|
import core.crypto.signWithECDSA
|
||||||
|
import core.node.services.TimestamperService
|
||||||
|
import core.node.services.TimestampingError
|
||||||
|
import core.serialization.serialize
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.time.Clock
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from
|
||||||
|
* multiple parties.
|
||||||
|
*/
|
||||||
|
class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf(),
|
||||||
|
private val attachments: MutableList<SecureHash> = arrayListOf(),
|
||||||
|
private val outputs: MutableList<ContractState> = arrayListOf(),
|
||||||
|
private val commands: MutableList<Command> = arrayListOf()) {
|
||||||
|
|
||||||
|
val time: TimestampCommand? get() = commands.mapNotNull { it.data as? TimestampCommand }.singleOrNull()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Places a [TimestampCommand] in this transaction, removing any existing command if there is one.
|
||||||
|
* To get the right signature from the timestamping service, use the [timestamp] method after building is
|
||||||
|
* finished, or run use the [TimestampingProtocol] yourself.
|
||||||
|
*
|
||||||
|
* The window of time in which the final timestamp may lie is defined as [time] +/- [timeTolerance].
|
||||||
|
* If you want a non-symmetrical time window you must add the command via [addCommand] yourself. The tolerance
|
||||||
|
* should be chosen such that your code can finish building the transaction and sending it to the TSA within that
|
||||||
|
* window of time, taking into account factors such as network latency. Transactions being built by a group of
|
||||||
|
* collaborating parties may therefore require a higher time tolerance than a transaction being built by a single
|
||||||
|
* node.
|
||||||
|
*/
|
||||||
|
fun setTime(time: Instant, authenticatedBy: Party, timeTolerance: Duration) {
|
||||||
|
check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" }
|
||||||
|
commands.removeAll { it.data is TimestampCommand }
|
||||||
|
addCommand(TimestampCommand(time, timeTolerance), authenticatedBy.owningKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A more convenient way to add items to this transaction that calls the add* methods for you based on type */
|
||||||
|
fun withItems(vararg items: Any): TransactionBuilder {
|
||||||
|
for (t in items) {
|
||||||
|
when (t) {
|
||||||
|
is StateRef -> inputs.add(t)
|
||||||
|
is ContractState -> outputs.add(t)
|
||||||
|
is Command -> commands.add(t)
|
||||||
|
else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The signatures that have been collected so far - might be incomplete! */
|
||||||
|
private val currentSigs = arrayListOf<DigitalSignature.WithKey>()
|
||||||
|
|
||||||
|
fun signWith(key: KeyPair) {
|
||||||
|
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
|
||||||
|
check(commands.count { it.pubkeys.contains(key.public) } > 0) { "Trying to sign with a key that isn't in any command" }
|
||||||
|
val data = toWireTransaction().serialize()
|
||||||
|
addSignatureUnchecked(key.signWithECDSA(data.bits))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that the given signature matches one of the commands and that it is a correct signature over the tx, then
|
||||||
|
* adds it.
|
||||||
|
*
|
||||||
|
* @throws SignatureException if the signature didn't match the transaction contents
|
||||||
|
* @throws IllegalArgumentException if the signature key doesn't appear in any command.
|
||||||
|
*/
|
||||||
|
fun checkAndAddSignature(sig: DigitalSignature.WithKey) {
|
||||||
|
checkSignature(sig)
|
||||||
|
addSignatureUnchecked(sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that the given signature matches one of the commands and that it is a correct signature over the tx.
|
||||||
|
*
|
||||||
|
* @throws SignatureException if the signature didn't match the transaction contents
|
||||||
|
* @throws IllegalArgumentException if the signature key doesn't appear in any command.
|
||||||
|
*/
|
||||||
|
fun checkSignature(sig: DigitalSignature.WithKey) {
|
||||||
|
require(commands.count { it.pubkeys.contains(sig.by) } > 0) { "Signature key doesn't match any command" }
|
||||||
|
sig.verifyWithECDSA(toWireTransaction().serialized)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds the signature directly to the transaction, without checking it for validity. */
|
||||||
|
fun addSignatureUnchecked(sig: DigitalSignature.WithKey) {
|
||||||
|
currentSigs.add(sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses the given timestamper service to request a signature over the WireTransaction be added. There must always be
|
||||||
|
* at least one such signature, but others may be added as well. You may want to have multiple redundant timestamps
|
||||||
|
* in the following cases:
|
||||||
|
*
|
||||||
|
* - Cross border contracts where local law says that only local timestamping authorities are acceptable.
|
||||||
|
* - Backup in case a TSA's signing key is compromised.
|
||||||
|
*
|
||||||
|
* The signature of the trusted timestamper merely asserts that the time field of this transaction is valid.
|
||||||
|
*/
|
||||||
|
@Suspendable
|
||||||
|
fun timestamp(timestamper: TimestamperService, clock: Clock = Clock.systemUTC()) {
|
||||||
|
val t = time ?: throw IllegalStateException("Timestamping requested but no time was inserted into the transaction")
|
||||||
|
|
||||||
|
// Obviously this is just a hard-coded dummy value for now.
|
||||||
|
val maxExpectedLatency = 5.seconds
|
||||||
|
if (Duration.between(clock.instant(), t.before) > maxExpectedLatency)
|
||||||
|
throw TimestampingError.NotOnTimeException()
|
||||||
|
|
||||||
|
// The timestamper may also throw NotOnTimeException if our clocks are desynchronised or if we are right on the
|
||||||
|
// boundary of t.notAfter and network latency pushes us over the edge. By "synchronised" here we mean relative
|
||||||
|
// to GPS time i.e. the United States Naval Observatory.
|
||||||
|
val sig = timestamper.timestamp(toWireTransaction().serialize())
|
||||||
|
addSignatureUnchecked(sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments),
|
||||||
|
ArrayList(outputs), ArrayList(commands))
|
||||||
|
|
||||||
|
fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction {
|
||||||
|
if (checkSufficientSignatures) {
|
||||||
|
val gotKeys = currentSigs.map { it.by }.toSet()
|
||||||
|
for (command in commands) {
|
||||||
|
if (!gotKeys.containsAll(command.pubkeys))
|
||||||
|
throw IllegalStateException("Missing signatures on the transaction for a ${command.data.javaClass.canonicalName} command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addInputState(ref: StateRef) {
|
||||||
|
check(currentSigs.isEmpty())
|
||||||
|
inputs.add(ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addAttachment(attachment: Attachment) {
|
||||||
|
check(currentSigs.isEmpty())
|
||||||
|
attachments.add(attachment.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addOutputState(state: ContractState) {
|
||||||
|
check(currentSigs.isEmpty())
|
||||||
|
outputs.add(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addCommand(arg: Command) {
|
||||||
|
check(currentSigs.isEmpty())
|
||||||
|
|
||||||
|
// We should probably merge the lists of pubkeys for identical commands here.
|
||||||
|
commands.add(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys)))
|
||||||
|
fun addCommand(data: CommandData, keys: List<PublicKey>) = addCommand(Command(data, keys))
|
||||||
|
|
||||||
|
// Accessors that yield immutable snapshots.
|
||||||
|
fun inputStates(): List<StateRef> = ArrayList(inputs)
|
||||||
|
|
||||||
|
fun outputStates(): List<ContractState> = ArrayList(outputs)
|
||||||
|
fun commands(): List<Command> = ArrayList(commands)
|
||||||
|
fun attachments(): List<SecureHash> = ArrayList(attachments)
|
||||||
|
}
|
@ -1,43 +1,32 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
|
||||||
import com.esotericsoftware.kryo.Kryo
|
import com.esotericsoftware.kryo.Kryo
|
||||||
import core.crypto.DigitalSignature
|
import core.crypto.DigitalSignature
|
||||||
import core.crypto.SecureHash
|
import core.crypto.SecureHash
|
||||||
import core.crypto.signWithECDSA
|
|
||||||
import core.crypto.toStringShort
|
import core.crypto.toStringShort
|
||||||
import core.node.services.TimestamperService
|
|
||||||
import core.node.services.TimestampingError
|
|
||||||
import core.serialization.SerializedBytes
|
import core.serialization.SerializedBytes
|
||||||
import core.serialization.THREAD_LOCAL_KRYO
|
import core.serialization.THREAD_LOCAL_KRYO
|
||||||
import core.serialization.deserialize
|
import core.serialization.deserialize
|
||||||
import core.serialization.serialize
|
import core.serialization.serialize
|
||||||
import core.utilities.Emoji
|
import core.utilities.Emoji
|
||||||
import java.security.KeyPair
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
import java.time.Clock
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Views of a transaction as it progresses through the pipeline, from bytes loaded from disk/network to the object
|
* Views of a transaction as it progresses through the pipeline, from bytes loaded from disk/network to the object
|
||||||
* tree passed into a contract.
|
* tree passed into a contract.
|
||||||
*
|
*
|
||||||
* SignedTransaction wraps a serialized WireTransaction. It contains one or more ECDSA signatures, each one from
|
* SignedTransaction wraps a serialized WireTransaction. It contains one or more signatures, each one for
|
||||||
* a public key that is mentioned inside a transaction command.
|
* a public key that is mentioned inside a transaction command. SignedTransaction is the top level transaction type
|
||||||
|
* and the type most frequently passed around the network and stored. The identity of a transaction is the hash
|
||||||
|
* of a WireTransaction, therefore if you are storing data keyed by WT hash be aware that multiple different SWTs may
|
||||||
|
* map to the same key (and they could be different in important ways!).
|
||||||
*
|
*
|
||||||
* 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 the entire
|
* 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.
|
* 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.
|
|
||||||
* Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from
|
|
||||||
* multiple parties.
|
|
||||||
*
|
|
||||||
* LedgerTransaction is derived from WireTransaction. It is the result of doing some basic key lookups on WireCommand
|
* LedgerTransaction is derived from WireTransaction. It is the result of doing some basic key lookups on WireCommand
|
||||||
* to see if any keys are from a recognised party, thus converting the WireCommand objects into
|
* to see if any keys are from a recognised party, thus converting the WireCommand objects into
|
||||||
* AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->party map. In future it'd make more
|
* AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->party map. In future it'd make more
|
||||||
@ -152,156 +141,6 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
|||||||
operator fun plus(sig: DigitalSignature.WithKey) = withAdditionalSignature(sig)
|
operator fun plus(sig: DigitalSignature.WithKey) = withAdditionalSignature(sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A mutable transaction that's in the process of being built, before all signatures are present. */
|
|
||||||
class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf(),
|
|
||||||
private val attachments: MutableList<SecureHash> = arrayListOf(),
|
|
||||||
private val outputs: MutableList<ContractState> = arrayListOf(),
|
|
||||||
private val commands: MutableList<Command> = arrayListOf()) {
|
|
||||||
|
|
||||||
val time: TimestampCommand? get() = commands.mapNotNull { it.data as? TimestampCommand }.singleOrNull()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Places a [TimestampCommand] in this transaction, removing any existing command if there is one.
|
|
||||||
* To get the right signature from the timestamping service, use the [timestamp] method after building is
|
|
||||||
* finished, or run use the [TimestampingProtocol] yourself.
|
|
||||||
*
|
|
||||||
* The window of time in which the final timestamp may lie is defined as [time] +/- [timeTolerance].
|
|
||||||
* If you want a non-symmetrical time window you must add the command via [addCommand] yourself. The tolerance
|
|
||||||
* should be chosen such that your code can finish building the transaction and sending it to the TSA within that
|
|
||||||
* window of time, taking into account factors such as network latency. Transactions being built by a group of
|
|
||||||
* collaborating parties may therefore require a higher time tolerance than a transaction being built by a single
|
|
||||||
* node.
|
|
||||||
*/
|
|
||||||
fun setTime(time: Instant, authenticatedBy: Party, timeTolerance: Duration) {
|
|
||||||
check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" }
|
|
||||||
commands.removeAll { it.data is TimestampCommand }
|
|
||||||
addCommand(TimestampCommand(time, timeTolerance), authenticatedBy.owningKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A more convenient way to add items to this transaction that calls the add* methods for you based on type */
|
|
||||||
fun withItems(vararg items: Any): TransactionBuilder {
|
|
||||||
for (t in items) {
|
|
||||||
when (t) {
|
|
||||||
is StateRef -> inputs.add(t)
|
|
||||||
is ContractState -> outputs.add(t)
|
|
||||||
is Command -> commands.add(t)
|
|
||||||
else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The signatures that have been collected so far - might be incomplete! */
|
|
||||||
private val currentSigs = arrayListOf<DigitalSignature.WithKey>()
|
|
||||||
|
|
||||||
fun signWith(key: KeyPair) {
|
|
||||||
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
|
|
||||||
check(commands.count { it.pubkeys.contains(key.public) } > 0) { "Trying to sign with a key that isn't in any command" }
|
|
||||||
val data = toWireTransaction().serialize()
|
|
||||||
addSignatureUnchecked(key.signWithECDSA(data.bits))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks that the given signature matches one of the commands and that it is a correct signature over the tx, then
|
|
||||||
* adds it.
|
|
||||||
*
|
|
||||||
* @throws SignatureException if the signature didn't match the transaction contents
|
|
||||||
* @throws IllegalArgumentException if the signature key doesn't appear in any command.
|
|
||||||
*/
|
|
||||||
fun checkAndAddSignature(sig: DigitalSignature.WithKey) {
|
|
||||||
checkSignature(sig)
|
|
||||||
addSignatureUnchecked(sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks that the given signature matches one of the commands and that it is a correct signature over the tx.
|
|
||||||
*
|
|
||||||
* @throws SignatureException if the signature didn't match the transaction contents
|
|
||||||
* @throws IllegalArgumentException if the signature key doesn't appear in any command.
|
|
||||||
*/
|
|
||||||
fun checkSignature(sig: DigitalSignature.WithKey) {
|
|
||||||
require(commands.count { it.pubkeys.contains(sig.by) } > 0) { "Signature key doesn't match any command" }
|
|
||||||
sig.verifyWithECDSA(toWireTransaction().serialized)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Adds the signature directly to the transaction, without checking it for validity. */
|
|
||||||
fun addSignatureUnchecked(sig: DigitalSignature.WithKey) {
|
|
||||||
currentSigs.add(sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uses the given timestamper service to request a signature over the WireTransaction be added. There must always be
|
|
||||||
* at least one such signature, but others may be added as well. You may want to have multiple redundant timestamps
|
|
||||||
* in the following cases:
|
|
||||||
*
|
|
||||||
* - Cross border contracts where local law says that only local timestamping authorities are acceptable.
|
|
||||||
* - Backup in case a TSA's signing key is compromised.
|
|
||||||
*
|
|
||||||
* The signature of the trusted timestamper merely asserts that the time field of this transaction is valid.
|
|
||||||
*/
|
|
||||||
@Suspendable
|
|
||||||
fun timestamp(timestamper: TimestamperService, clock: Clock = Clock.systemUTC()) {
|
|
||||||
val t = time ?: throw IllegalStateException("Timestamping requested but no time was inserted into the transaction")
|
|
||||||
|
|
||||||
// Obviously this is just a hard-coded dummy value for now.
|
|
||||||
val maxExpectedLatency = 5.seconds
|
|
||||||
if (Duration.between(clock.instant(), t.before) > maxExpectedLatency)
|
|
||||||
throw TimestampingError.NotOnTimeException()
|
|
||||||
|
|
||||||
// The timestamper may also throw NotOnTimeException if our clocks are desynchronised or if we are right on the
|
|
||||||
// boundary of t.notAfter and network latency pushes us over the edge. By "synchronised" here we mean relative
|
|
||||||
// to GPS time i.e. the United States Naval Observatory.
|
|
||||||
val sig = timestamper.timestamp(toWireTransaction().serialize())
|
|
||||||
addSignatureUnchecked(sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments),
|
|
||||||
ArrayList(outputs), ArrayList(commands))
|
|
||||||
|
|
||||||
fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction {
|
|
||||||
if (checkSufficientSignatures) {
|
|
||||||
val gotKeys = currentSigs.map { it.by }.toSet()
|
|
||||||
for (command in commands) {
|
|
||||||
if (!gotKeys.containsAll(command.pubkeys))
|
|
||||||
throw IllegalStateException("Missing signatures on the transaction for a ${command.data.javaClass.canonicalName} command")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addInputState(ref: StateRef) {
|
|
||||||
check(currentSigs.isEmpty())
|
|
||||||
inputs.add(ref)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addAttachment(attachment: Attachment) {
|
|
||||||
check(currentSigs.isEmpty())
|
|
||||||
attachments.add(attachment.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addOutputState(state: ContractState) {
|
|
||||||
check(currentSigs.isEmpty())
|
|
||||||
outputs.add(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addCommand(arg: Command) {
|
|
||||||
check(currentSigs.isEmpty())
|
|
||||||
|
|
||||||
// We should probably merge the lists of pubkeys for identical commands here.
|
|
||||||
commands.add(arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys)))
|
|
||||||
fun addCommand(data: CommandData, keys: List<PublicKey>) = addCommand(Command(data, keys))
|
|
||||||
|
|
||||||
// Accessors that yield immutable snapshots.
|
|
||||||
fun inputStates(): List<StateRef> = ArrayList(inputs)
|
|
||||||
|
|
||||||
fun outputStates(): List<ContractState> = ArrayList(outputs)
|
|
||||||
fun commands(): List<Command> = ArrayList(commands)
|
|
||||||
fun attachments(): List<SecureHash> = ArrayList(attachments)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
|
Loading…
Reference in New Issue
Block a user