Minor: split TransactionBuilder into its own file, so Transactions.kt is just the core immutable types.

This commit is contained in:
Mike Hearn 2016-04-18 15:00:13 +02:00
parent d9cfb5e1eb
commit 306ff69312
2 changed files with 175 additions and 166 deletions

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

View File

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