diff --git a/src/core/Structures.kt b/src/core/Structures.kt index 5f10cefb8a..859486382d 100644 --- a/src/core/Structures.kt +++ b/src/core/Structures.kt @@ -25,7 +25,7 @@ fun ContractState.hash(): SecureHash = SecureHash.sha256((serialize())) * A stateref is a pointer to a state, this is an equivalent of an "outpoint" in Bitcoin. It records which transaction * defined the state and where in that transaction it was. */ -data class ContractStateRef(val txhash: SecureHash.SHA256, val index: Int) : SerializeableWithKryo +data class ContractStateRef(val txhash: SecureHash, val index: Int) : SerializeableWithKryo /** A StateAndRef is simply a (state, ref) pair. For instance, a wallet (which holds available assets) contains these. */ data class StateAndRef(val state: T, val ref: ContractStateRef) diff --git a/src/core/TransactionGroup.kt b/src/core/TransactionGroup.kt new file mode 100644 index 0000000000..05f04cd927 --- /dev/null +++ b/src/core/TransactionGroup.kt @@ -0,0 +1,58 @@ +package core + +import java.util.* + +class TransactionResolutionException(val hash: SecureHash) : Exception() +class TransactionConflictException(val conflictRef: ContractStateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception() + +/** + * A TransactionGroup defines a directed acyclic graph of transactions that can be resolved with each other and then + * verified. Successful verification does not imply the non-existence of other conflicting transactions: simply that + * this subgraph does not contain conflicts and is accepted by the involved contracts. + * + * The inputs of the provided transactions must be resolvable either within the [transactions] set, or from the + * [nonVerifiedRoots] set. Transactions in the non-verified set are ignored other than for looking up input states. + */ +class TransactionGroup(val transactions: Set, val nonVerifiedRoots: Set) { + + /** + * Verifies the group and returns the set of resolved transactions. + */ + fun verify(programMap: Map): Set { + // Check that every input can be resolved to an output. + // Check that no output is referenced by more than one input. + // Cycles should be impossible due to the use of hashes as pointers. + check(transactions.intersect(nonVerifiedRoots).isEmpty()) + + val hashToTXMap: Map> = (transactions + nonVerifiedRoots).groupBy { it.hash } + val refToConsumingTXMap = hashMapOf() + + val resolved = HashSet(transactions.size) + for (tx in transactions) { + val inputs = ArrayList(tx.inStateRefs.size) + for (ref in tx.inStateRefs) { + val conflict = refToConsumingTXMap[ref] + if (conflict != null) + throw TransactionConflictException(ref, tx, conflict) + refToConsumingTXMap[ref] = tx + + // Look up the connecting transaction. + val ltx = hashToTXMap[ref.txhash]?.single() ?: throw TransactionResolutionException(ref.txhash) + // Look up the output in that transaction by index. + inputs.add(ltx.outStates[ref.index]) + } + resolved.add(TransactionForVerification(inputs, tx.outStates, tx.commands, tx.time, tx.hash)) + } + + for (tx in resolved) { + try { + tx.verify(programMap) + } catch(e: Exception) { + println(tx) + throw e + } + } + return resolved + } + +} \ No newline at end of file diff --git a/src/core/Transactions.kt b/src/core/Transactions.kt index bd37462c2b..f02b0eaf53 100644 --- a/src/core/Transactions.kt +++ b/src/core/Transactions.kt @@ -170,7 +170,7 @@ data class TimestampedWireTransaction( * * TODO: When converting LedgerTransaction into TransactionForVerification, make sure to check for duped inputs. */ -class LedgerTransaction( +data class LedgerTransaction( /** The input states which will be consumed/invalidated by the execution of this transaction. */ val inStateRefs: List, /** The states that will be generated by the execution of this transaction. */ diff --git a/src/core/serialization/Kryo.kt b/src/core/serialization/Kryo.kt index 4d56bb4da5..055f56c869 100644 --- a/src/core/serialization/Kryo.kt +++ b/src/core/serialization/Kryo.kt @@ -198,6 +198,8 @@ fun createKryo(): Kryo { register(Collections.singletonList(null).javaClass) register(Collections.singletonMap(1, 2).javaClass) register(ArrayList::class.java) + register(emptyList().javaClass) + register(Arrays.asList(1,3).javaClass) // These JDK classes use a very minimal custom serialization format and are written to defend against malicious // streams, so we can just kick it over to java serialization. We get ECPublicKeyImpl/ECPrivteKeyImpl via an @@ -223,6 +225,7 @@ fun createKryo(): Kryo { registerDataClass() register(Cash.Commands.Move.javaClass) registerDataClass() + registerDataClass() registerDataClass() register(CommercialPaper.Commands.Move.javaClass) register(CommercialPaper.Commands.Redeem.javaClass) diff --git a/tests/core/TransactionGroupTests.kt b/tests/core/TransactionGroupTests.kt new file mode 100644 index 0000000000..d2d737ad02 --- /dev/null +++ b/tests/core/TransactionGroupTests.kt @@ -0,0 +1,100 @@ +package core + +import contracts.Cash +import core.testutils.* +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals + +class TransactionGroupTests { + val A_THOUSAND_POUNDS = Cash.State(InstitutionReference(MINI_CORP, OpaqueBytes.of(1, 2, 3)), 1000.POUNDS, MINI_CORP_KEY) + + @Test + fun success() { + transactionGroup { + roots { + transaction(A_THOUSAND_POUNDS label "£1000") + } + + transaction { + input("£1000") + output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE } + arg(MINI_CORP_KEY) { Cash.Commands.Move } + } + + transaction { + input("alice's £1000") + arg(ALICE) { Cash.Commands.Move } + arg(MINI_CORP_KEY) { Cash.Commands.Exit(1000.POUNDS) } + } + + verify() + } + } + + @Test + fun conflict() { + transactionGroup { + val t = transaction { + output("cash") { A_THOUSAND_POUNDS } + arg(MINI_CORP_KEY) { Cash.Commands.Issue() } + } + + val conflict1 = transaction { + input("cash") + val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB + output { HALF } + output { HALF } + arg(MINI_CORP_KEY) { Cash.Commands.Move } + } + + verify() + + // Alice tries to double spend back to herself. + val conflict2 = transaction { + input("cash") + val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE + output { HALF } + output { HALF } + arg(MINI_CORP_KEY) { Cash.Commands.Move } + } + + assertNotEquals(conflict1, conflict2) + + val e = assertFailsWith(TransactionConflictException::class) { + verify() + } + assertEquals(ContractStateRef(t.hash, 0), e.conflictRef) + assertEquals(setOf(conflict1, conflict2), setOf(e.tx1, e.tx2)) + } + } + + @Test + fun disconnected() { + // Check that if we have a transaction in the group that doesn't connect to anything else, it's rejected. + val tg = transactionGroup { + transaction { + output("cash") { A_THOUSAND_POUNDS } + arg(MINI_CORP_KEY) { Cash.Commands.Issue() } + } + + transaction { + input("cash") + output { A_THOUSAND_POUNDS `owned by` BOB } + } + } + + // We have to do this manually without the DSL because transactionGroup { } won't let us create a tx that + // points nowhere. + val ref = ContractStateRef(SecureHash.randomSHA256(), 0) + tg.txns.add(LedgerTransaction( + listOf(ref), listOf(A_THOUSAND_POUNDS), listOf(AuthenticatedObject(listOf(BOB), emptyList(), Cash.Commands.Move)), TEST_TX_TIME, SecureHash.randomSHA256()) + ) + + val e = assertFailsWith(TransactionResolutionException::class) { + tg.verify() + } + assertEquals(e.hash, ref.txhash) + } +} \ No newline at end of file diff --git a/tests/core/testutils/TestUtils.kt b/tests/core/testutils/TestUtils.kt index 73396f004b..6b03829d47 100644 --- a/tests/core/testutils/TestUtils.kt +++ b/tests/core/testutils/TestUtils.kt @@ -5,6 +5,7 @@ import core.* import java.security.KeyPairGenerator import java.security.PublicKey import java.time.Instant +import java.util.* import kotlin.test.fail object TestUtils { @@ -17,6 +18,8 @@ val MEGA_CORP_KEY = DummyPublicKey("mini") val MINI_CORP_KEY = DummyPublicKey("mega") val DUMMY_PUBKEY_1 = DummyPublicKey("x1") val DUMMY_PUBKEY_2 = DummyPublicKey("x2") +val ALICE = DummyPublicKey("alice") +val BOB = DummyPublicKey("bob") val MEGA_CORP = Institution("MegaCorp", MEGA_CORP_KEY) val MINI_CORP = Institution("MiniCorp", MINI_CORP_KEY) @@ -38,7 +41,7 @@ val TEST_PROGRAM_MAP: Map = mapOf( //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // -// DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes. +// Defines a simple DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes. // // Define a transaction like this: // @@ -57,37 +60,41 @@ val TEST_PROGRAM_MAP: Map = mapOf( // // TODO: Make it impossible to forget to test either a failure or an accept for each transaction{} block -// Corresponds to the args to Contract.verify -class TransactionForTest() { - private val inStates = arrayListOf() +infix fun Cash.State.`owned by`(owner: PublicKey) = this.copy(owner = owner) - class LabeledOutput(val label: String?, val state: ContractState) { - override fun toString() = state.toString() + (if (label != null) " ($label)" else "") - override fun equals(other: Any?) = other is LabeledOutput && state.equals(other.state) - override fun hashCode(): Int = state.hashCode() - } - private val outStates = arrayListOf() +class LabeledOutput(val label: String?, val state: ContractState) { + override fun toString() = state.toString() + (if (label != null) " ($label)" else "") + override fun equals(other: Any?) = other is LabeledOutput && state.equals(other.state) + override fun hashCode(): Int = state.hashCode() +} - private val commands: MutableList> = arrayListOf() +infix fun ContractState.label(label: String) = LabeledOutput(label, this) - constructor(inStates: List, outStates: List, commands: List>) : this() { - this.inStates.addAll(inStates) - this.outStates.addAll(outStates.map { LabeledOutput(null, it) }) - this.commands.addAll(commands) - } +abstract class AbstractTransactionForTest { + protected val outStates = ArrayList() + protected val commands = ArrayList>() + + open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, s()).apply { outStates.add(this) } - fun input(s: () -> ContractState) = inStates.add(s()) - fun output(label: String? = null, s: () -> ContractState) = outStates.add(LabeledOutput(label, s())) fun arg(vararg key: PublicKey, c: () -> Command) { val keys = listOf(*key) commands.add(AuthenticatedObject(keys, keys.mapNotNull { TEST_KEYS_TO_CORP_MAP[it] }, c())) } - private fun run(time: Instant) = TransactionForVerification(inStates, outStates.map { it.state }, commands, time, SecureHash.randomSHA256()).verify(TEST_PROGRAM_MAP) + // Forbid patterns like: transaction { ... transaction { ... } } + @Deprecated("Cannot nest transactions, use tweak", level = DeprecationLevel.ERROR) + fun transaction(body: TransactionForTest.() -> Unit) {} +} - infix fun `fails requirement`(msg: String) = rejects(msg) - // which is uglier?? :) - fun fails_requirement(msg: String) = this.`fails requirement`(msg) +// Corresponds to the args to Contract.verify +open class TransactionForTest : AbstractTransactionForTest() { + private val inStates = arrayListOf() + fun input(s: () -> ContractState) = inStates.add(s()) + + protected fun run(time: Instant) { + val tx = TransactionForVerification(inStates, outStates.map { it.state }, commands, time, SecureHash.randomSHA256()) + tx.verify(TEST_PROGRAM_MAP) + } fun accepts(time: Instant = TEST_TX_TIME) = run(time) fun rejects(withMessage: String? = null, time: Instant = TEST_TX_TIME) { @@ -105,15 +112,9 @@ class TransactionForTest() { if (!r) throw AssertionError("Expected exception but didn't get one") } - // Allow customisation of partial transactions. - fun tweak(body: TransactionForTest.() -> Unit): TransactionForTest { - val tx = TransactionForTest() - tx.inStates.addAll(inStates) - tx.outStates.addAll(outStates) - tx.commands.addAll(commands) - tx.body() - return tx - } + // which is uglier?? :) + infix fun `fails requirement`(msg: String) = rejects(msg) + fun fails_requirement(msg: String) = this.`fails requirement`(msg) // Use this to create transactions where the output of this transaction is automatically used as an input of // the next. @@ -131,6 +132,16 @@ class TransactionForTest() { return tx } + // Allow customisation of partial transactions. + fun tweak(body: TransactionForTest.() -> Unit): TransactionForTest { + val tx = TransactionForTest() + tx.inStates.addAll(inStates) + tx.outStates.addAll(outStates) + tx.commands.addAll(commands) + tx.body() + return tx + } + override fun toString(): String { return """transaction { inputs: $inStates @@ -149,8 +160,67 @@ class TransactionForTest() { } } -fun transaction(body: TransactionForTest.() -> Unit): TransactionForTest { - val tx = TransactionForTest() - tx.body() - return tx +fun transaction(body: TransactionForTest.() -> Unit) = TransactionForTest().apply { body() } + +class TransactionGroupForTest { + open inner class LedgerTransactionForTest : AbstractTransactionForTest() { + private val inputs = ArrayList() + + fun input(label: String) { + inputs.add(labelToRefs[label] ?: throw IllegalArgumentException("Unknown label \"$label\"")) + } + + fun toLedgerTransaction(time: Instant): LedgerTransaction { + val wireCmds = commands.map { WireCommand(it.value, it.signers) } + return WireTransaction(inputs, outStates.map { it.state }, wireCmds).toLedgerTransaction(time, TEST_KEYS_TO_CORP_MAP) + } + } + + private inner class InternalLedgerTransactionForTest : LedgerTransactionForTest() { + fun finaliseAndInsertLabels(time: Instant): LedgerTransaction { + val ltx = toLedgerTransaction(time) + for ((index, state) in outStates.withIndex()) { + if (state.label != null) + labelToRefs[state.label] = ContractStateRef(ltx.hash, index) + } + return ltx + } + } + + private val rootTxns = ArrayList() + private val labelToRefs = HashMap() + inner class Roots { + fun transaction(vararg outputStates: LabeledOutput) { + val outs = outputStates.map { it.state } + val wtx = WireTransaction(emptyList(), outs, emptyList()) + val ltx = wtx.toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP) + outputStates.forEachIndexed { index, labeledOutput -> labelToRefs[labeledOutput.label!!] = ContractStateRef(ltx.hash, index) } + rootTxns.add(ltx) + } + + @Deprecated("Does not nest ", level = DeprecationLevel.ERROR) + fun roots(body: Roots.() -> Unit) {} + } + fun roots(body: Roots.() -> Unit) = Roots().apply { body() } + + val txns = ArrayList() + + fun transaction(time: Instant = TEST_TX_TIME, body: LedgerTransactionForTest.() -> Unit): LedgerTransaction { + val forTest = InternalLedgerTransactionForTest() + forTest.body() + val ltx = forTest.finaliseAndInsertLabels(time) + txns.add(ltx) + return ltx + } + + @Deprecated("Does not nest ", level = DeprecationLevel.ERROR) + fun transactionGroup(body: TransactionGroupForTest.() -> Unit) {} + + fun verify() { + toTransactionGroup().verify(TEST_PROGRAM_MAP) + } + + fun toTransactionGroup() = TransactionGroup(txns.map { it }.toSet(), rootTxns.toSet()) } + +fun transactionGroup(body: TransactionGroupForTest.() -> Unit) = TransactionGroupForTest().apply { this.body() }