Contracts: make the Cash craftSpend/generate function update a mutable transaction instead of returning a unit test structure.

Tests: move TestUtils into the test package now the cash contract generate function works the right way.

Transactions: various refactorings to support partially signed transactions.
This commit is contained in:
Mike Hearn 2015-11-16 20:36:27 +01:00
parent 1f17053263
commit aecc1de0cf
9 changed files with 243 additions and 234 deletions

View File

@ -11,7 +11,6 @@ import java.util.*
//
// Open issues:
// - Cannot do currency exchanges this way, as the contract insists that there be a single currency involved!
// - Nothing ensures cash states are not created out of thin air.
// - Complex logic to do grouping: can it be generalised out into platform code?
// Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode.
@ -54,7 +53,7 @@ object Cash : Contract {
object Move : Command
/**
* Allows new cash states to be issued into existence: the nonce ("number used onces") ensures the transaction
* Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction
* has a unique ID even when there are no inputs.
*/
data class Issue(val nonce: Long = SecureRandom.getInstanceStrong().nextLong()) : Command
@ -137,12 +136,12 @@ object Cash : Contract {
}
}
// TODO: craftSpend should work more like in bitcoinj, where it takes and modifies a transaction template.
// This would allow multiple contracts to compose properly (e.g. bond trade+cash movement).
/** Generate a transaction that consumes one or more of the given input states to move money to the given pubkey. */
/**
* Generate a transaction that consumes one or more of the given input states to move money to the given pubkey.
* Note that the wallet list is not updated: it's up to you to do that.
*/
@Throws(InsufficientBalanceException::class)
fun craftSpend(amount: Amount, to: PublicKey, wallet: List<Cash.State>): TransactionForTest {
fun craftSpend(tx: PartialTransaction, amount: Amount, to: PublicKey, wallet: List<StateAndRef<Cash.State>>) {
// Discussion
//
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
@ -158,33 +157,31 @@ object Cash : Contract {
//
// Having selected coins of the right currency, we must craft output states for the amount we're sending and
// the "change", which goes back to us. The change is required to make the amounts balance. We may need more
// than one change output in order to avoid merging coins from different deposits.
// than one change output in order to avoid merging coins from different deposits. The point of this design
// is to ensure that ledger entries are immutable and globally identifiable.
//
// Once we've selected our inputs and generated our outputs, we must calculate a signature for each key that
// appears in the input set. Same as with Bitcoin, ideally keys are never reused for privacy reasons, but we
// must handle the case where they are. Once the signatures are generated, a MoveCommand for each key/sig pair
// is put into the transaction, which is finally returned.
// Finally, we add the states to the provided partial transaction.
val currency = amount.currency
val coinsOfCurrency = wallet.filter { it.amount.currency == currency }
val coinsOfCurrency = wallet.filter { it.state.amount.currency == currency }
val gathered = arrayListOf<State>()
val gathered = arrayListOf<StateAndRef<Cash.State>>()
var gatheredAmount = Amount(0, currency)
for (c in coinsOfCurrency) {
if (gatheredAmount >= amount) break
gathered.add(c)
gatheredAmount += c.amount
gatheredAmount += c.state.amount
}
if (gatheredAmount < amount)
throw InsufficientBalanceException(amount - gatheredAmount)
val change = gatheredAmount - amount
val keysUsed = gathered.map { it.owner }.toSet()
val keysUsed = gathered.map { it.state.owner }.toSet()
val states = gathered.groupBy { it.deposit }.map {
val states = gathered.groupBy { it.state.deposit }.map {
val (deposit, coins) = it
val totalAmount = coins.map { it.amount }.sumOrThrow()
val totalAmount = coins.map { it.state.amount }.sumOrThrow()
State(deposit, totalAmount, to)
}
@ -192,17 +189,17 @@ object Cash : Contract {
// Just copy a key across as the change key. In real life of course, this works but leaks private data.
// In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow
// value flows through the transaction graph.
val changeKey = gathered.first().owner
val changeKey = gathered.first().state.owner
// Add a change output and adjust the last output downwards.
states.subList(0, states.lastIndex) +
states.last().let { it.copy(amount = it.amount - change) } +
State(gathered.last().deposit, change, changeKey)
State(gathered.last().state.deposit, change, changeKey)
} else states
// Finally, generate the commands. Pretend to sign here, real signatures aren't done yet.
val commands = keysUsed.map { AuthenticatedObject(listOf(it), emptyList(), Commands.Move) }
return TransactionForTest(gathered.toArrayList(), outputs.toArrayList(), commands.toArrayList())
for (state in gathered) tx.addInputState(state.ref)
for (state in outputs) tx.addOutputState(state)
// What if we already have a move command with the right keys? Filter it out here or in platform code?
tx.addArg(WireCommand(Commands.Move, keysUsed.toList()))
}
}

View File

@ -1,6 +1,8 @@
package core
import com.google.common.io.BaseEncoding
import core.serialization.SerializeableWithKryo
import java.math.BigInteger
import java.security.*
// "sealed" here means there can't be any subclasses other than the ones defined here.
@ -21,6 +23,8 @@ sealed class SecureHash(bits: ByteArray) : OpaqueBytes(bits) {
fun sha256(bits: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bits))
fun sha256(str: String) = sha256(str.toByteArray())
fun randomSHA256() = sha256(SecureRandom.getInstanceStrong().generateSeed(32))
}
abstract val signatureAlgorithmName: String
@ -52,6 +56,14 @@ object NullPublicKey : PublicKey, Comparable<PublicKey> {
override fun toString() = "NULL_KEY"
}
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey>, SerializeableWithKryo {
override fun getAlgorithm() = "DUMMY"
override fun getEncoded() = s.toByteArray()
override fun getFormat() = "ASN.1"
override fun compareTo(other: PublicKey): Int = BigInteger(encoded).compareTo(BigInteger(other.encoded))
override fun toString() = "PUBKEY[$s]"
}
/** Utility to simplify the act of signing a byte array */
fun PrivateKey.signWithECDSA(bits: ByteArray): DigitalSignature {
val signer = Signature.getInstance("SHA256withECDSA")

View File

@ -21,9 +21,15 @@ interface ContractState : SerializeableWithKryo {
/** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */
fun ContractState.hash(): SecureHash = SecureHash.sha256((serialize()))
/** A stateref is a pointer to a state, this is an equivalent of an "outpoint" in Bitcoin. */
/**
* 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
/** 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)
/** An Institution is well known (name, pubkey) pair. In a real system this would probably be an X.509 certificate. */
data class Institution(val name: String, val owningKey: PublicKey) : SerializeableWithKryo {
override fun toString() = name

View File

@ -1,157 +0,0 @@
package core
import contracts.*
import core.serialization.SerializeableWithKryo
import java.math.BigInteger
import java.security.PublicKey
import java.time.Instant
import kotlin.test.fail
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey>, SerializeableWithKryo {
override fun getAlgorithm() = "DUMMY"
override fun getEncoded() = s.toByteArray()
override fun getFormat() = "ASN.1"
override fun compareTo(other: PublicKey): Int = BigInteger(encoded).compareTo(BigInteger(other.encoded))
override fun toString() = "PUBKEY[$s]"
}
// A few dummy values for testing.
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 MEGA_CORP = Institution("MegaCorp", MEGA_CORP_KEY)
val MINI_CORP = Institution("MiniCorp", MINI_CORP_KEY)
val TEST_KEYS_TO_CORP_MAP: Map<PublicKey, Institution> = mapOf(
MEGA_CORP_KEY to MEGA_CORP,
MINI_CORP_KEY to MINI_CORP
)
// A dummy time at which we will be pretending test transactions are created.
val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
// In a real system this would be a persistent map of hash to bytecode and we'd instantiate the object as needed inside
// a sandbox. For now we just instantiate right at the start of the program.
val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = mapOf(
CASH_PROGRAM_ID to Cash,
CP_PROGRAM_ID to CommercialPaper,
DUMMY_PROGRAM_ID to DummyContract
)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
//
// Define a transaction like this:
//
// transaction {
// input { someExpression }
// output { someExpression }
// arg { someExpression }
//
// transaction {
// ... same thing but works with a copy of the parent, can add inputs/outputs/args just within this scope.
// }
//
// contract.accepts() -> should pass
// contract `fails requirement` "some substring of the error message"
// }
//
// 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<ContractState>()
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<LabeledOutput>()
private val args: MutableList<AuthenticatedObject<Command>> = arrayListOf()
constructor(inStates: List<ContractState>, outStates: List<ContractState>, args: List<AuthenticatedObject<Command>>) : this() {
this.inStates.addAll(inStates)
this.outStates.addAll(outStates.map { LabeledOutput(null, it) })
this.args.addAll(args)
}
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)
args.add(AuthenticatedObject(keys, keys.mapNotNull { TEST_KEYS_TO_CORP_MAP[it] }, c()))
}
private fun run(time: Instant) = TransactionForVerification(inStates, outStates.map { it.state }, args, time).verify(TEST_PROGRAM_MAP)
infix fun `fails requirement`(msg: String) = rejects(msg)
// which is uglier?? :)
fun fails_requirement(msg: String) = this.`fails requirement`(msg)
fun accepts(time: Instant = TEST_TX_TIME) = run(time)
fun rejects(withMessage: String? = null, time: Instant = TEST_TX_TIME) {
val r = try {
run(time)
false
} catch (e: Exception) {
val m = e.message
if (m == null)
fail("Threw exception without a message")
else
if (withMessage != null && !m.toLowerCase().contains(withMessage.toLowerCase())) throw AssertionError("Error was actually: $m", e)
true
}
if (!r) throw AssertionError("Expected exception but didn't get one")
}
// Allow customisation of partial transactions.
fun transaction(body: TransactionForTest.() -> Unit): TransactionForTest {
val tx = TransactionForTest()
tx.inStates.addAll(inStates)
tx.outStates.addAll(outStates)
tx.args.addAll(args)
tx.body()
return tx
}
// Use this to create transactions where the output of this transaction is automatically used as an input of
// the next.
fun chain(vararg outputLabels: String, body: TransactionForTest.() -> Unit) {
val states = outStates.mapNotNull {
val l = it.label
if (l != null && outputLabels.contains(l))
it.state
else
null
}
val tx = TransactionForTest()
tx.inStates.addAll(states)
tx.body()
}
override fun toString(): String {
return """transaction {
inputs: $inStates
outputs: $outStates
args: $args
}"""
}
override fun equals(other: Any?) = this === other || (other is TransactionForTest && inStates == other.inStates && outStates == other.outStates && args == other.args)
override fun hashCode(): Int {
var result = inStates.hashCode()
result += 31 * result + outStates.hashCode()
result += 31 * result + args.hashCode()
return result
}
}
fun transaction(body: TransactionForTest.() -> Unit): TransactionForTest {
val tx = TransactionForTest()
tx.body()
return tx
}

View File

@ -81,17 +81,17 @@ class PartialTransaction(private val inputStates: MutableList<ContractStateRef>
fun signWith(key: KeyPair) {
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
check(args.count { it.pubkeys.contains(key.public) } > 0) { "Trying to sign with a key that isn't in any command" }
val bits = toWire().serialize()
val bits = toWireTransaction().serialize()
currentSigs.add(key.private.signWithECDSA(bits, key.public))
}
private fun toWire() = WireTransaction(inputStates, outputStates, args)
fun toWireTransaction() = WireTransaction(inputStates, outputStates, args)
fun toSignedTransaction(): SignedWireTransaction {
val requiredKeys = args.flatMap { it.pubkeys }.toSet()
val gotKeys = currentSigs.map { it.by }.toSet()
check(gotKeys == requiredKeys) { "The set of required signatures isn't equal to the signatures we've got" }
return SignedWireTransaction(toWire().serialize(), ArrayList(currentSigs))
return SignedWireTransaction(toWireTransaction().serialize(), ArrayList(currentSigs))
}
fun addInputState(ref: ContractStateRef) {

View File

@ -3,6 +3,9 @@ import contracts.DummyContract
import contracts.InsufficientBalanceException
import core.*
import org.junit.Test
import testutils.*
import java.security.PublicKey
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@ -253,76 +256,72 @@ class CashTests {
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
fun makeCash(amount: Amount, corp: Institution, depositRef: Byte = 1) =
StateAndRef(
Cash.State(InstitutionReference(corp, OpaqueBytes.of(depositRef)), amount, OUR_PUBKEY_1),
ContractStateRef(SecureHash.randomSHA256(), Random().nextInt(32))
)
val WALLET = listOf(
Cash.State(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 100.DOLLARS, OUR_PUBKEY_1),
Cash.State(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 400.DOLLARS, OUR_PUBKEY_1),
Cash.State(InstitutionReference(MINI_CORP, OpaqueBytes.of(1)), 80.DOLLARS, OUR_PUBKEY_1),
Cash.State(InstitutionReference(MINI_CORP, OpaqueBytes.of(2)), 80.SWISS_FRANCS, OUR_PUBKEY_1)
makeCash(100.DOLLARS, MEGA_CORP),
makeCash(400.DOLLARS, MEGA_CORP),
makeCash(80.DOLLARS, MINI_CORP),
makeCash(80.SWISS_FRANCS, MINI_CORP, 2)
)
fun makeSpend(amount: Amount, dest: PublicKey): WireTransaction {
val tx = PartialTransaction()
Cash.craftSpend(tx, amount, dest, WALLET)
return tx.toWireTransaction()
}
@Test
fun craftSimpleDirectSpend() {
assertEquals(
transaction {
input { WALLET[0] }
output { WALLET[0].copy(owner = THEIR_PUBKEY_1) }
arg(OUR_PUBKEY_1) { Cash.Commands.Move }
},
Cash.craftSpend(100.DOLLARS, THEIR_PUBKEY_1, WALLET)
)
val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1)
assertEquals(WALLET[0].ref, wtx.inputStates[0])
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1), wtx.outputStates[0])
assertEquals(OUR_PUBKEY_1, wtx.args[0].pubkeys[0])
}
@Test
fun craftSimpleSpendWithChange() {
assertEquals(
transaction {
input { WALLET[0] }
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS) }
output { WALLET[0].copy(owner = OUR_PUBKEY_1, amount = 90.DOLLARS) }
arg(OUR_PUBKEY_1) { Cash.Commands.Move }
},
Cash.craftSpend(10.DOLLARS, THEIR_PUBKEY_1, WALLET)
)
val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1)
assertEquals(WALLET[0].ref, wtx.inputStates[0])
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS), wtx.outputStates[0])
assertEquals(WALLET[0].state.copy(amount = 90.DOLLARS), wtx.outputStates[1])
assertEquals(OUR_PUBKEY_1, wtx.args[0].pubkeys[0])
}
@Test
fun craftSpendWithTwoInputs() {
assertEquals(
transaction {
input { WALLET[0] }
input { WALLET[1] }
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS) }
arg(OUR_PUBKEY_1) { Cash.Commands.Move }
},
Cash.craftSpend(500.DOLLARS, THEIR_PUBKEY_1, WALLET)
)
val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1)
assertEquals(WALLET[0].ref, wtx.inputStates[0])
assertEquals(WALLET[1].ref, wtx.inputStates[1])
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputStates[0])
assertEquals(OUR_PUBKEY_1, wtx.args[0].pubkeys[0])
}
@Test
fun craftSpendMixedDeposits() {
assertEquals(
transaction {
input { WALLET[0] }
input { WALLET[1] }
input { WALLET[2] }
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS) }
output { WALLET[2].copy(owner = THEIR_PUBKEY_1) }
arg(OUR_PUBKEY_1) { Cash.Commands.Move }
},
Cash.craftSpend(580.DOLLARS, THEIR_PUBKEY_1, WALLET)
)
val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1)
assertEquals(WALLET[0].ref, wtx.inputStates[0])
assertEquals(WALLET[1].ref, wtx.inputStates[1])
assertEquals(WALLET[2].ref, wtx.inputStates[2])
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputStates[0])
assertEquals(WALLET[2].state.copy(owner = THEIR_PUBKEY_1), wtx.outputStates[1])
assertEquals(OUR_PUBKEY_1, wtx.args[0].pubkeys[0])
}
@Test
fun craftSpendInsufficientBalance() {
try {
Cash.craftSpend(1000.DOLLARS, THEIR_PUBKEY_1, WALLET)
assert(false)
} catch (e: InsufficientBalanceException) {
assertEquals(1000 - 580, e.amountMissing.pennies / 100)
val e: InsufficientBalanceException = assertFailsWith("balance") {
makeSpend(1000.DOLLARS, THEIR_PUBKEY_1)
}
assertEquals(1000 - 580, e.amountMissing.pennies / 100)
assertFailsWith(InsufficientBalanceException::class) {
Cash.craftSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1, WALLET)
makeSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1)
}
}
}

View File

@ -1,7 +1,11 @@
package contracts
import core.*
import core.DOLLARS
import core.InstitutionReference
import core.OpaqueBytes
import core.days
import org.junit.Test
import testutils.*
// TODO: Finish this off.

View File

@ -4,6 +4,8 @@ import contracts.Cash
import core.*
import org.junit.Before
import org.junit.Test
import testutils.DUMMY_PUBKEY_1
import testutils.MINI_CORP
import testutils.TestUtils
import java.security.SignatureException
import kotlin.test.assertFailsWith

View File

@ -1,8 +1,154 @@
package testutils
import contracts.*
import core.*
import java.security.KeyPairGenerator
import java.security.PublicKey
import java.time.Instant
import kotlin.test.fail
object TestUtils {
val keypair = KeyPairGenerator.getInstance("EC").genKeyPair()
val keypair2 = KeyPairGenerator.getInstance("EC").genKeyPair()
}
}
// A few dummy values for testing.
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 MEGA_CORP = Institution("MegaCorp", MEGA_CORP_KEY)
val MINI_CORP = Institution("MiniCorp", MINI_CORP_KEY)
val TEST_KEYS_TO_CORP_MAP: Map<PublicKey, Institution> = mapOf(
MEGA_CORP_KEY to MEGA_CORP,
MINI_CORP_KEY to MINI_CORP
)
// A dummy time at which we will be pretending test transactions are created.
val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
// In a real system this would be a persistent map of hash to bytecode and we'd instantiate the object as needed inside
// a sandbox. For now we just instantiate right at the start of the program.
val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = mapOf(
CASH_PROGRAM_ID to Cash,
CP_PROGRAM_ID to CommercialPaper,
DUMMY_PROGRAM_ID to DummyContract
)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
//
// Define a transaction like this:
//
// transaction {
// input { someExpression }
// output { someExpression }
// arg { someExpression }
//
// transaction {
// ... same thing but works with a copy of the parent, can add inputs/outputs/args just within this scope.
// }
//
// contract.accepts() -> should pass
// contract `fails requirement` "some substring of the error message"
// }
//
// 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<ContractState>()
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<LabeledOutput>()
private val args: MutableList<AuthenticatedObject<Command>> = arrayListOf()
constructor(inStates: List<ContractState>, outStates: List<ContractState>, args: List<AuthenticatedObject<Command>>) : this() {
this.inStates.addAll(inStates)
this.outStates.addAll(outStates.map { LabeledOutput(null, it) })
this.args.addAll(args)
}
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)
args.add(AuthenticatedObject(keys, keys.mapNotNull { TEST_KEYS_TO_CORP_MAP[it] }, c()))
}
private fun run(time: Instant) = TransactionForVerification(inStates, outStates.map { it.state }, args, time).verify(TEST_PROGRAM_MAP)
infix fun `fails requirement`(msg: String) = rejects(msg)
// which is uglier?? :)
fun fails_requirement(msg: String) = this.`fails requirement`(msg)
fun accepts(time: Instant = TEST_TX_TIME) = run(time)
fun rejects(withMessage: String? = null, time: Instant = TEST_TX_TIME) {
val r = try {
run(time)
false
} catch (e: Exception) {
val m = e.message
if (m == null)
fail("Threw exception without a message")
else
if (withMessage != null && !m.toLowerCase().contains(withMessage.toLowerCase())) throw AssertionError("Error was actually: $m", e)
true
}
if (!r) throw AssertionError("Expected exception but didn't get one")
}
// Allow customisation of partial transactions.
fun transaction(body: TransactionForTest.() -> Unit): TransactionForTest {
val tx = TransactionForTest()
tx.inStates.addAll(inStates)
tx.outStates.addAll(outStates)
tx.args.addAll(args)
tx.body()
return tx
}
// Use this to create transactions where the output of this transaction is automatically used as an input of
// the next.
fun chain(vararg outputLabels: String, body: TransactionForTest.() -> Unit) {
val states = outStates.mapNotNull {
val l = it.label
if (l != null && outputLabels.contains(l))
it.state
else
null
}
val tx = TransactionForTest()
tx.inStates.addAll(states)
tx.body()
}
override fun toString(): String {
return """transaction {
inputs: $inStates
outputs: $outStates
args: $args
}"""
}
override fun equals(other: Any?) = this === other || (other is TransactionForTest && inStates == other.inStates && outStates == other.outStates && args == other.args)
override fun hashCode(): Int {
var result = inStates.hashCode()
result += 31 * result + outStates.hashCode()
result += 31 * result + args.hashCode()
return result
}
}
fun transaction(body: TransactionForTest.() -> Unit): TransactionForTest {
val tx = TransactionForTest()
tx.body()
return tx
}