mirror of
https://github.com/corda/corda.git
synced 2025-01-29 15:43:55 +00:00
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:
parent
1f17053263
commit
aecc1de0cf
@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user