Merged in mike-tx-types-refactoring-september (pull request #332)

Refactor common fields from WireTransaction/LedgerTransaction out into BaseTransaction. Put some field invariants into the constructors.
This commit is contained in:
Mike Hearn 2016-09-06 18:03:39 +02:00
commit 5a28b29a7e
5 changed files with 141 additions and 55 deletions

View File

@ -0,0 +1,53 @@
package com.r3corda.core.transactions
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import java.security.PublicKey
import java.util.*
/**
* An abstract class defining fields shared by all transaction types in the system.
*/
abstract class BaseTransaction(
/** The inputs of this transaction. Note that in BaseTransaction subclasses the type of this list may change! */
open val inputs: List<*>,
/** Ordered list of states defined by this transaction, along with the associated notaries. */
val outputs: List<TransactionState<ContractState>>,
/**
* If present, the notary for this transaction. If absent then the transaction is not notarised at all.
* This is intended for issuance/genesis transactions that don't consume any other states and thus can't
* double spend anything.
*/
val notary: Party?,
/**
* Keys that are required to have signed the wrapping [SignedTransaction], ordered to match the list of
* signatures. There is nothing that forces the list to be the _correct_ list of signers for this
* transaction until the transaction is verified by using [LedgerTransaction.verify]. It includes the
* notary key, if the notary field is set.
*/
val signers: List<PublicKey>,
/**
* Pointer to a class that defines the behaviour of this transaction: either normal, or "notary changing".
*/
val type: TransactionType,
/**
* If specified, a time window in which this transaction may have been notarised. Contracts can check this
* time window to find out when a transaction is deemed to have occurred, from the ledger's perspective.
*/
val timestamp: Timestamp?
) : NamedByHash {
fun checkInvariants() {
if (notary == null) check(inputs.isEmpty()) { "The notary must be specified explicitly for any transaction that has inputs." }
if (timestamp != null) check(notary != null) { "If a timestamp is provided, there must be a notary." }
}
override fun equals(other: Any?) =
other is BaseTransaction &&
notary == other.notary &&
signers == other.signers &&
type == other.type &&
timestamp == other.timestamp
override fun hashCode() = Objects.hash(notary, signers, type, timestamp)
}

View File

@ -16,24 +16,23 @@ import java.security.PublicKey
*
* All the above refer to inputs using a (txhash, output index) pair.
*/
data class LedgerTransaction(
/** The input states which will be consumed/invalidated by the execution of this transaction. */
val inputs: List<StateAndRef<*>>,
/** The states that will be generated by the execution of this transaction. */
val outputs: List<TransactionState<*>>,
class LedgerTransaction(
/** The resolved input states which will be consumed/invalidated by the execution of this transaction. */
override val inputs: List<StateAndRef<*>>,
outputs: List<TransactionState<ContractState>>,
/** Arbitrary data passed to the program of each input state. */
val commands: List<AuthenticatedObject<CommandData>>,
/** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */
val attachments: List<Attachment>,
/** The hash of the original serialised WireTransaction. */
override val id: SecureHash,
/** The notary for this party, may be null for transactions with no notary. */
val notary: Party?,
/** The notary key and the command keys together: a signed transaction must provide signatures for all of these. */
val signers: List<PublicKey>,
val timestamp: Timestamp?,
val type: TransactionType
) : NamedByHash {
notary: Party?,
signers: List<PublicKey>,
timestamp: Timestamp?,
type: TransactionType
) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp) {
init { checkInvariants() }
@Suppress("UNCHECKED_CAST")
fun <T : ContractState> outRef(index: Int) = StateAndRef(outputs[index] as TransactionState<T>, StateRef(id, index))
@ -54,4 +53,32 @@ data class LedgerTransaction(
* @throws TransactionVerificationException if anything goes wrong.
*/
fun verify() = type.verify(this)
// TODO: When we upgrade to Kotlin 1.1 we can make this a data class again and have the compiler generate these.
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
if (!super.equals(other)) return false
other as LedgerTransaction
if (inputs != other.inputs) return false
if (outputs != other.outputs) return false
if (commands != other.commands) return false
if (attachments != other.attachments) return false
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + inputs.hashCode()
result = 31 * result + outputs.hashCode()
result = 31 * result + commands.hashCode()
result = 31 * result + attachments.hashCode()
result = 31 * result + id.hashCode()
return result
}
}

View File

@ -20,42 +20,20 @@ import java.security.PublicKey
* the identity of the transaction, that is, it's possible for two [SignedTransaction]s with different sets of
* signatures to have the same identity hash.
*/
data class WireTransaction(
class WireTransaction(
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
val inputs: List<StateRef>,
override val inputs: List<StateRef>,
/** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */
val attachments: List<SecureHash>,
/** Ordered list of states defined by this transaction, along with the associated notaries. */
val outputs: List<TransactionState<ContractState>>,
outputs: List<TransactionState<ContractState>>,
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
val commands: List<Command>,
/**
* If present, the notary for this transaction. If absent then the transaction is not notarised at all.
* This is intended for issuance/genesis transactions that don't consume any other states and thus can't
* double spend anything.
*
* TODO: Ensure the invariant 'notary == null -> inputs.isEmpty' is enforced!
*/
val notary: Party?,
/**
* Keys that are required to have signed the wrapping [SignedTransaction], ordered to match the list of
* signatures. There is nothing that forces the list to be the _correct_ list of signers for this
* transaction until the transaction is verified by using [LedgerTransaction.verify]. It includes the
* notary key, if the notary field is set.
*/
val signers: List<PublicKey>,
/**
* Pointer to a class that defines the behaviour of this transaction: either normal, or "notary changing".
*/
val type: TransactionType,
/**
* If specified, a time window in which this transaction may have been notarised. Contracts can check this
* time window to find out when a transaction is deemed to have occurred, from the ledger's perspective.
*
* TODO: Ensure the invariant 'timestamp != null -> notary != null' is enforced!
*/
val timestamp: Timestamp?
) : NamedByHash {
notary: Party?,
signers: List<PublicKey>,
type: TransactionType,
timestamp: Timestamp?
) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp) {
init { checkInvariants() }
// Cache the serialised form of the transaction and its hash to give us fast access to it.
@Volatile @Transient private var cachedBits: SerializedBytes<WireTransaction>? = null
@ -80,16 +58,6 @@ data class WireTransaction(
/** Returns a [StateAndRef] for the requested output state, or throws [IllegalArgumentException] if not found. */
fun <T : ContractState> outRef(state: ContractState): StateAndRef<T> = outRef(outputs.map { it.data }.indexOfOrThrow(state))
override fun toString(): String {
val buf = StringBuilder()
buf.appendln("Transaction $id:")
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output")
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")
for (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment")
return buf.toString()
}
/**
* Looks up identities and attachments from storage to generate a [LedgerTransaction]. A transaction is expected to
* have been fully resolved using the resolution protocol by this point.
@ -111,4 +79,40 @@ data class WireTransaction(
val resolvedInputs = inputs.map { StateAndRef(services.loadState(it), it) }
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, signers, timestamp, type)
}
override fun toString(): String {
val buf = StringBuilder()
buf.appendln("Transaction $id:")
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output")
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")
for (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment")
return buf.toString()
}
// TODO: When Kotlin 1.1 comes out we can make this class a data class again, and have these be autogenerated.
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
if (!super.equals(other)) return false
other as WireTransaction
if (inputs != other.inputs) return false
if (attachments != other.attachments) return false
if (outputs != other.outputs) return false
if (commands != other.commands) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + inputs.hashCode()
result = 31 * result + attachments.hashCode()
result = 31 * result + outputs.hashCode()
result = 31 * result + commands.hashCode()
return result
}
}

View File

@ -5,7 +5,9 @@ import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.transactions.LedgerTransaction
import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.core.utilities.DUMMY_NOTARY_KEY
import com.r3corda.testing.*
import com.r3corda.testing.ALICE
import com.r3corda.testing.ALICE_PUBKEY
import com.r3corda.testing.BOB
import org.junit.Test
import kotlin.test.assertFailsWith

View File

@ -74,7 +74,7 @@ class TransactionDSL<out T : TransactionDSLInterpreter>(val interpreter: T) : Tr
* @param state The state to be added.
*/
fun input(state: ContractState) {
val transaction = ledgerInterpreter._unverifiedTransaction(null, TransactionBuilder()) {
val transaction = ledgerInterpreter._unverifiedTransaction(null, TransactionBuilder(notary = DUMMY_NOTARY)) {
output { state }
}
input(transaction.outRef<ContractState>(0).ref)