diff --git a/core/src/main/kotlin/com/r3corda/core/transactions/BaseTransaction.kt b/core/src/main/kotlin/com/r3corda/core/transactions/BaseTransaction.kt new file mode 100644 index 0000000000..a4b92337ea --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/transactions/BaseTransaction.kt @@ -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>, + /** + * 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, + /** + * 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) +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/com/r3corda/core/transactions/LedgerTransaction.kt index 7dc7e65b8e..af04aca6de 100644 --- a/core/src/main/kotlin/com/r3corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/com/r3corda/core/transactions/LedgerTransaction.kt @@ -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>, - /** The states that will be generated by the execution of this transaction. */ - val outputs: List>, +class LedgerTransaction( + /** The resolved input states which will be consumed/invalidated by the execution of this transaction. */ + override val inputs: List>, + outputs: List>, /** Arbitrary data passed to the program of each input state. */ val commands: List>, /** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */ val attachments: List, /** 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, - val timestamp: Timestamp?, - val type: TransactionType -) : NamedByHash { + notary: Party?, + signers: List, + timestamp: Timestamp?, + type: TransactionType +) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp) { + init { checkInvariants() } + @Suppress("UNCHECKED_CAST") fun outRef(index: Int) = StateAndRef(outputs[index] as TransactionState, 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 + } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/com/r3corda/core/transactions/WireTransaction.kt index 3ed05f6860..3ce5caadda 100644 --- a/core/src/main/kotlin/com/r3corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/com/r3corda/core/transactions/WireTransaction.kt @@ -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, + override val inputs: List, /** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */ val attachments: List, - /** Ordered list of states defined by this transaction, along with the associated notaries. */ - val outputs: List>, + outputs: List>, /** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */ val commands: List, - /** - * 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, - /** - * 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, + 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? = null @@ -80,16 +58,6 @@ data class WireTransaction( /** Returns a [StateAndRef] for the requested output state, or throws [IllegalArgumentException] if not found. */ fun outRef(state: ContractState): StateAndRef = 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 + } } diff --git a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionTypeTests.kt b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionTypeTests.kt index 40dffcb304..6cf8c4e0ea 100644 --- a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionTypeTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionTypeTests.kt @@ -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 diff --git a/test-utils/src/main/kotlin/com/r3corda/testing/TransactionDSLInterpreter.kt b/test-utils/src/main/kotlin/com/r3corda/testing/TransactionDSLInterpreter.kt index 7a50095b6a..9ba11b7dd8 100644 --- a/test-utils/src/main/kotlin/com/r3corda/testing/TransactionDSLInterpreter.kt +++ b/test-utils/src/main/kotlin/com/r3corda/testing/TransactionDSLInterpreter.kt @@ -74,7 +74,7 @@ class TransactionDSL(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(0).ref)