mirror of
https://github.com/corda/corda.git
synced 2025-01-18 18:56:28 +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
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.signWithECDSA
|
||||
import core.crypto.toStringShort
|
||||
import core.node.services.TimestamperService
|
||||
import core.node.services.TimestampingError
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.THREAD_LOCAL_KRYO
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.Emoji
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
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
|
||||
* tree passed into a contract.
|
||||
*
|
||||
* SignedTransaction wraps a serialized WireTransaction. It contains one or more ECDSA signatures, each one from
|
||||
* a public key that is mentioned inside a transaction command.
|
||||
* SignedTransaction wraps a serialized WireTransaction. It contains one or more signatures, each one for
|
||||
* 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
|
||||
* 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
|
||||
* 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
|
||||
* 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
|
||||
@ -152,156 +141,6 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
||||
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.
|
||||
* 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