mirror of
https://github.com/corda/corda.git
synced 2025-05-31 14:40:52 +00:00
Major: introduce TransactionGroup for verifying subgraphs.
This commit is contained in:
parent
636279ced9
commit
a4aef06e41
@ -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
|
* 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.
|
* 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. */
|
/** A StateAndRef is simply a (state, ref) pair. For instance, a wallet (which holds available assets) contains these. */
|
||||||
data class StateAndRef<T : ContractState>(val state: T, val ref: ContractStateRef)
|
data class StateAndRef<T : ContractState>(val state: T, val ref: ContractStateRef)
|
||||||
|
58
src/core/TransactionGroup.kt
Normal file
58
src/core/TransactionGroup.kt
Normal file
@ -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<LedgerTransaction>, val nonVerifiedRoots: Set<LedgerTransaction>) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the group and returns the set of resolved transactions.
|
||||||
|
*/
|
||||||
|
fun verify(programMap: Map<SecureHash, Contract>): Set<TransactionForVerification> {
|
||||||
|
// 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<SecureHash, List<LedgerTransaction>> = (transactions + nonVerifiedRoots).groupBy { it.hash }
|
||||||
|
val refToConsumingTXMap = hashMapOf<ContractStateRef, LedgerTransaction>()
|
||||||
|
|
||||||
|
val resolved = HashSet<TransactionForVerification>(transactions.size)
|
||||||
|
for (tx in transactions) {
|
||||||
|
val inputs = ArrayList<ContractState>(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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -170,7 +170,7 @@ data class TimestampedWireTransaction(
|
|||||||
*
|
*
|
||||||
* TODO: When converting LedgerTransaction into TransactionForVerification, make sure to check for duped inputs.
|
* 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. */
|
/** The input states which will be consumed/invalidated by the execution of this transaction. */
|
||||||
val inStateRefs: List<ContractStateRef>,
|
val inStateRefs: List<ContractStateRef>,
|
||||||
/** The states that will be generated by the execution of this transaction. */
|
/** The states that will be generated by the execution of this transaction. */
|
||||||
|
@ -198,6 +198,8 @@ fun createKryo(): Kryo {
|
|||||||
register(Collections.singletonList(null).javaClass)
|
register(Collections.singletonList(null).javaClass)
|
||||||
register(Collections.singletonMap(1, 2).javaClass)
|
register(Collections.singletonMap(1, 2).javaClass)
|
||||||
register(ArrayList::class.java)
|
register(ArrayList::class.java)
|
||||||
|
register(emptyList<Any>().javaClass)
|
||||||
|
register(Arrays.asList(1,3).javaClass)
|
||||||
|
|
||||||
// These JDK classes use a very minimal custom serialization format and are written to defend against malicious
|
// 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
|
// 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<Cash.State>()
|
registerDataClass<Cash.State>()
|
||||||
register(Cash.Commands.Move.javaClass)
|
register(Cash.Commands.Move.javaClass)
|
||||||
registerDataClass<Cash.Commands.Exit>()
|
registerDataClass<Cash.Commands.Exit>()
|
||||||
|
registerDataClass<Cash.Commands.Issue>()
|
||||||
registerDataClass<CommercialPaper.State>()
|
registerDataClass<CommercialPaper.State>()
|
||||||
register(CommercialPaper.Commands.Move.javaClass)
|
register(CommercialPaper.Commands.Move.javaClass)
|
||||||
register(CommercialPaper.Commands.Redeem.javaClass)
|
register(CommercialPaper.Commands.Redeem.javaClass)
|
||||||
|
100
tests/core/TransactionGroupTests.kt
Normal file
100
tests/core/TransactionGroupTests.kt
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import core.*
|
|||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
import kotlin.test.fail
|
import kotlin.test.fail
|
||||||
|
|
||||||
object TestUtils {
|
object TestUtils {
|
||||||
@ -17,6 +18,8 @@ val MEGA_CORP_KEY = DummyPublicKey("mini")
|
|||||||
val MINI_CORP_KEY = DummyPublicKey("mega")
|
val MINI_CORP_KEY = DummyPublicKey("mega")
|
||||||
val DUMMY_PUBKEY_1 = DummyPublicKey("x1")
|
val DUMMY_PUBKEY_1 = DummyPublicKey("x1")
|
||||||
val DUMMY_PUBKEY_2 = DummyPublicKey("x2")
|
val DUMMY_PUBKEY_2 = DummyPublicKey("x2")
|
||||||
|
val ALICE = DummyPublicKey("alice")
|
||||||
|
val BOB = DummyPublicKey("bob")
|
||||||
val MEGA_CORP = Institution("MegaCorp", MEGA_CORP_KEY)
|
val MEGA_CORP = Institution("MegaCorp", MEGA_CORP_KEY)
|
||||||
val MINI_CORP = Institution("MiniCorp", MINI_CORP_KEY)
|
val MINI_CORP = Institution("MiniCorp", MINI_CORP_KEY)
|
||||||
|
|
||||||
@ -38,7 +41,7 @@ val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = 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:
|
// Define a transaction like this:
|
||||||
//
|
//
|
||||||
@ -57,37 +60,41 @@ val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = mapOf(
|
|||||||
//
|
//
|
||||||
// TODO: Make it impossible to forget to test either a failure or an accept for each transaction{} block
|
// 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
|
infix fun Cash.State.`owned by`(owner: PublicKey) = this.copy(owner = owner)
|
||||||
class TransactionForTest() {
|
|
||||||
private val inStates = arrayListOf<ContractState>()
|
|
||||||
|
|
||||||
class LabeledOutput(val label: String?, val state: ContractState) {
|
class LabeledOutput(val label: String?, val state: ContractState) {
|
||||||
override fun toString() = state.toString() + (if (label != null) " ($label)" else "")
|
override fun toString() = state.toString() + (if (label != null) " ($label)" else "")
|
||||||
override fun equals(other: Any?) = other is LabeledOutput && state.equals(other.state)
|
override fun equals(other: Any?) = other is LabeledOutput && state.equals(other.state)
|
||||||
override fun hashCode(): Int = state.hashCode()
|
override fun hashCode(): Int = state.hashCode()
|
||||||
}
|
}
|
||||||
private val outStates = arrayListOf<LabeledOutput>()
|
|
||||||
|
|
||||||
private val commands: MutableList<AuthenticatedObject<Command>> = arrayListOf()
|
infix fun ContractState.label(label: String) = LabeledOutput(label, this)
|
||||||
|
|
||||||
constructor(inStates: List<ContractState>, outStates: List<ContractState>, commands: List<AuthenticatedObject<Command>>) : this() {
|
abstract class AbstractTransactionForTest {
|
||||||
this.inStates.addAll(inStates)
|
protected val outStates = ArrayList<LabeledOutput>()
|
||||||
this.outStates.addAll(outStates.map { LabeledOutput(null, it) })
|
protected val commands = ArrayList<AuthenticatedObject<Command>>()
|
||||||
this.commands.addAll(commands)
|
|
||||||
}
|
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) {
|
fun arg(vararg key: PublicKey, c: () -> Command) {
|
||||||
val keys = listOf(*key)
|
val keys = listOf(*key)
|
||||||
commands.add(AuthenticatedObject(keys, keys.mapNotNull { TEST_KEYS_TO_CORP_MAP[it] }, c()))
|
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)
|
// Corresponds to the args to Contract.verify
|
||||||
// which is uglier?? :)
|
open class TransactionForTest : AbstractTransactionForTest() {
|
||||||
fun fails_requirement(msg: String) = this.`fails requirement`(msg)
|
private val inStates = arrayListOf<ContractState>()
|
||||||
|
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 accepts(time: Instant = TEST_TX_TIME) = run(time)
|
||||||
fun rejects(withMessage: String? = null, time: Instant = TEST_TX_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")
|
if (!r) throw AssertionError("Expected exception but didn't get one")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow customisation of partial transactions.
|
// which is uglier?? :)
|
||||||
fun tweak(body: TransactionForTest.() -> Unit): TransactionForTest {
|
infix fun `fails requirement`(msg: String) = rejects(msg)
|
||||||
val tx = TransactionForTest()
|
fun fails_requirement(msg: String) = this.`fails requirement`(msg)
|
||||||
tx.inStates.addAll(inStates)
|
|
||||||
tx.outStates.addAll(outStates)
|
|
||||||
tx.commands.addAll(commands)
|
|
||||||
tx.body()
|
|
||||||
return tx
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use this to create transactions where the output of this transaction is automatically used as an input of
|
// Use this to create transactions where the output of this transaction is automatically used as an input of
|
||||||
// the next.
|
// the next.
|
||||||
@ -131,6 +132,16 @@ class TransactionForTest() {
|
|||||||
return tx
|
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 {
|
override fun toString(): String {
|
||||||
return """transaction {
|
return """transaction {
|
||||||
inputs: $inStates
|
inputs: $inStates
|
||||||
@ -149,8 +160,67 @@ class TransactionForTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun transaction(body: TransactionForTest.() -> Unit): TransactionForTest {
|
fun transaction(body: TransactionForTest.() -> Unit) = TransactionForTest().apply { body() }
|
||||||
val tx = TransactionForTest()
|
|
||||||
tx.body()
|
class TransactionGroupForTest {
|
||||||
return tx
|
open inner class LedgerTransactionForTest : AbstractTransactionForTest() {
|
||||||
|
private val inputs = ArrayList<ContractStateRef>()
|
||||||
|
|
||||||
|
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<LedgerTransaction>()
|
||||||
|
private val labelToRefs = HashMap<String, ContractStateRef>()
|
||||||
|
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<LedgerTransaction>()
|
||||||
|
|
||||||
|
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() }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user