mirror of
https://github.com/corda/corda.git
synced 2025-01-30 08:04:16 +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:
|
// Open issues:
|
||||||
// - Cannot do currency exchanges this way, as the contract insists that there be a single currency involved!
|
// - 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?
|
// - 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.
|
// 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
|
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.
|
* has a unique ID even when there are no inputs.
|
||||||
*/
|
*/
|
||||||
data class Issue(val nonce: Long = SecureRandom.getInstanceStrong().nextLong()) : Command
|
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.
|
||||||
|
* Note that the wallet list is not updated: it's up to you to do that.
|
||||||
/** Generate a transaction that consumes one or more of the given input states to move money to the given pubkey. */
|
*/
|
||||||
@Throws(InsufficientBalanceException::class)
|
@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
|
// Discussion
|
||||||
//
|
//
|
||||||
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
|
// 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
|
// 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
|
// 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
|
// Finally, we add the states to the provided partial transaction.
|
||||||
// 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.
|
|
||||||
|
|
||||||
val currency = amount.currency
|
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)
|
var gatheredAmount = Amount(0, currency)
|
||||||
for (c in coinsOfCurrency) {
|
for (c in coinsOfCurrency) {
|
||||||
if (gatheredAmount >= amount) break
|
if (gatheredAmount >= amount) break
|
||||||
gathered.add(c)
|
gathered.add(c)
|
||||||
gatheredAmount += c.amount
|
gatheredAmount += c.state.amount
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gatheredAmount < amount)
|
if (gatheredAmount < amount)
|
||||||
throw InsufficientBalanceException(amount - gatheredAmount)
|
throw InsufficientBalanceException(amount - gatheredAmount)
|
||||||
|
|
||||||
val change = gatheredAmount - amount
|
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 (deposit, coins) = it
|
||||||
val totalAmount = coins.map { it.amount }.sumOrThrow()
|
val totalAmount = coins.map { it.state.amount }.sumOrThrow()
|
||||||
State(deposit, totalAmount, to)
|
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.
|
// 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
|
// 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.
|
// 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.
|
// Add a change output and adjust the last output downwards.
|
||||||
states.subList(0, states.lastIndex) +
|
states.subList(0, states.lastIndex) +
|
||||||
states.last().let { it.copy(amount = it.amount - change) } +
|
states.last().let { it.copy(amount = it.amount - change) } +
|
||||||
State(gathered.last().deposit, change, changeKey)
|
State(gathered.last().state.deposit, change, changeKey)
|
||||||
} else states
|
} else states
|
||||||
|
|
||||||
// Finally, generate the commands. Pretend to sign here, real signatures aren't done yet.
|
for (state in gathered) tx.addInputState(state.ref)
|
||||||
val commands = keysUsed.map { AuthenticatedObject(listOf(it), emptyList(), Commands.Move) }
|
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?
|
||||||
return TransactionForTest(gathered.toArrayList(), outputs.toArrayList(), commands.toArrayList())
|
tx.addArg(WireCommand(Commands.Move, keysUsed.toList()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding
|
import com.google.common.io.BaseEncoding
|
||||||
|
import core.serialization.SerializeableWithKryo
|
||||||
|
import java.math.BigInteger
|
||||||
import java.security.*
|
import java.security.*
|
||||||
|
|
||||||
// "sealed" here means there can't be any subclasses other than the ones defined here.
|
// "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(bits: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bits))
|
||||||
fun sha256(str: String) = sha256(str.toByteArray())
|
fun sha256(str: String) = sha256(str.toByteArray())
|
||||||
|
|
||||||
|
fun randomSHA256() = sha256(SecureRandom.getInstanceStrong().generateSeed(32))
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract val signatureAlgorithmName: String
|
abstract val signatureAlgorithmName: String
|
||||||
@ -52,6 +56,14 @@ object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
|||||||
override fun toString() = "NULL_KEY"
|
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 */
|
/** Utility to simplify the act of signing a byte array */
|
||||||
fun PrivateKey.signWithECDSA(bits: ByteArray): DigitalSignature {
|
fun PrivateKey.signWithECDSA(bits: ByteArray): DigitalSignature {
|
||||||
val signer = Signature.getInstance("SHA256withECDSA")
|
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!) */
|
/** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */
|
||||||
fun ContractState.hash(): SecureHash = SecureHash.sha256((serialize()))
|
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
|
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. */
|
/** 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 {
|
data class Institution(val name: String, val owningKey: PublicKey) : SerializeableWithKryo {
|
||||||
override fun toString() = name
|
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) {
|
fun signWith(key: KeyPair) {
|
||||||
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
|
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" }
|
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))
|
currentSigs.add(key.private.signWithECDSA(bits, key.public))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toWire() = WireTransaction(inputStates, outputStates, args)
|
fun toWireTransaction() = WireTransaction(inputStates, outputStates, args)
|
||||||
|
|
||||||
fun toSignedTransaction(): SignedWireTransaction {
|
fun toSignedTransaction(): SignedWireTransaction {
|
||||||
val requiredKeys = args.flatMap { it.pubkeys }.toSet()
|
val requiredKeys = args.flatMap { it.pubkeys }.toSet()
|
||||||
val gotKeys = currentSigs.map { it.by }.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" }
|
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) {
|
fun addInputState(ref: ContractStateRef) {
|
||||||
|
@ -3,6 +3,9 @@ import contracts.DummyContract
|
|||||||
import contracts.InsufficientBalanceException
|
import contracts.InsufficientBalanceException
|
||||||
import core.*
|
import core.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import testutils.*
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.util.*
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
@ -253,76 +256,72 @@ class CashTests {
|
|||||||
|
|
||||||
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
|
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
|
||||||
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
|
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(
|
val WALLET = listOf(
|
||||||
Cash.State(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 100.DOLLARS, OUR_PUBKEY_1),
|
makeCash(100.DOLLARS, MEGA_CORP),
|
||||||
Cash.State(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 400.DOLLARS, OUR_PUBKEY_1),
|
makeCash(400.DOLLARS, MEGA_CORP),
|
||||||
Cash.State(InstitutionReference(MINI_CORP, OpaqueBytes.of(1)), 80.DOLLARS, OUR_PUBKEY_1),
|
makeCash(80.DOLLARS, MINI_CORP),
|
||||||
Cash.State(InstitutionReference(MINI_CORP, OpaqueBytes.of(2)), 80.SWISS_FRANCS, OUR_PUBKEY_1)
|
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
|
@Test
|
||||||
fun craftSimpleDirectSpend() {
|
fun craftSimpleDirectSpend() {
|
||||||
assertEquals(
|
val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1)
|
||||||
transaction {
|
assertEquals(WALLET[0].ref, wtx.inputStates[0])
|
||||||
input { WALLET[0] }
|
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1), wtx.outputStates[0])
|
||||||
output { WALLET[0].copy(owner = THEIR_PUBKEY_1) }
|
assertEquals(OUR_PUBKEY_1, wtx.args[0].pubkeys[0])
|
||||||
arg(OUR_PUBKEY_1) { Cash.Commands.Move }
|
|
||||||
},
|
|
||||||
Cash.craftSpend(100.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun craftSimpleSpendWithChange() {
|
fun craftSimpleSpendWithChange() {
|
||||||
assertEquals(
|
val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1)
|
||||||
transaction {
|
assertEquals(WALLET[0].ref, wtx.inputStates[0])
|
||||||
input { WALLET[0] }
|
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS), wtx.outputStates[0])
|
||||||
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS) }
|
assertEquals(WALLET[0].state.copy(amount = 90.DOLLARS), wtx.outputStates[1])
|
||||||
output { WALLET[0].copy(owner = OUR_PUBKEY_1, amount = 90.DOLLARS) }
|
assertEquals(OUR_PUBKEY_1, wtx.args[0].pubkeys[0])
|
||||||
arg(OUR_PUBKEY_1) { Cash.Commands.Move }
|
|
||||||
},
|
|
||||||
Cash.craftSpend(10.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun craftSpendWithTwoInputs() {
|
fun craftSpendWithTwoInputs() {
|
||||||
assertEquals(
|
val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1)
|
||||||
transaction {
|
assertEquals(WALLET[0].ref, wtx.inputStates[0])
|
||||||
input { WALLET[0] }
|
assertEquals(WALLET[1].ref, wtx.inputStates[1])
|
||||||
input { WALLET[1] }
|
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputStates[0])
|
||||||
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS) }
|
assertEquals(OUR_PUBKEY_1, wtx.args[0].pubkeys[0])
|
||||||
arg(OUR_PUBKEY_1) { Cash.Commands.Move }
|
|
||||||
},
|
|
||||||
Cash.craftSpend(500.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun craftSpendMixedDeposits() {
|
fun craftSpendMixedDeposits() {
|
||||||
assertEquals(
|
val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1)
|
||||||
transaction {
|
assertEquals(WALLET[0].ref, wtx.inputStates[0])
|
||||||
input { WALLET[0] }
|
assertEquals(WALLET[1].ref, wtx.inputStates[1])
|
||||||
input { WALLET[1] }
|
assertEquals(WALLET[2].ref, wtx.inputStates[2])
|
||||||
input { WALLET[2] }
|
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputStates[0])
|
||||||
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS) }
|
assertEquals(WALLET[2].state.copy(owner = THEIR_PUBKEY_1), wtx.outputStates[1])
|
||||||
output { WALLET[2].copy(owner = THEIR_PUBKEY_1) }
|
assertEquals(OUR_PUBKEY_1, wtx.args[0].pubkeys[0])
|
||||||
arg(OUR_PUBKEY_1) { Cash.Commands.Move }
|
|
||||||
},
|
|
||||||
Cash.craftSpend(580.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun craftSpendInsufficientBalance() {
|
fun craftSpendInsufficientBalance() {
|
||||||
try {
|
val e: InsufficientBalanceException = assertFailsWith("balance") {
|
||||||
Cash.craftSpend(1000.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
makeSpend(1000.DOLLARS, THEIR_PUBKEY_1)
|
||||||
assert(false)
|
|
||||||
} catch (e: InsufficientBalanceException) {
|
|
||||||
assertEquals(1000 - 580, e.amountMissing.pennies / 100)
|
|
||||||
}
|
}
|
||||||
|
assertEquals(1000 - 580, e.amountMissing.pennies / 100)
|
||||||
|
|
||||||
assertFailsWith(InsufficientBalanceException::class) {
|
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
|
package contracts
|
||||||
|
|
||||||
import core.*
|
import core.DOLLARS
|
||||||
|
import core.InstitutionReference
|
||||||
|
import core.OpaqueBytes
|
||||||
|
import core.days
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import testutils.*
|
||||||
|
|
||||||
// TODO: Finish this off.
|
// TODO: Finish this off.
|
||||||
|
|
||||||
|
@ -4,6 +4,8 @@ import contracts.Cash
|
|||||||
import core.*
|
import core.*
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import testutils.DUMMY_PUBKEY_1
|
||||||
|
import testutils.MINI_CORP
|
||||||
import testutils.TestUtils
|
import testutils.TestUtils
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
|
@ -1,8 +1,154 @@
|
|||||||
package testutils
|
package testutils
|
||||||
|
|
||||||
|
import contracts.*
|
||||||
|
import core.*
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.time.Instant
|
||||||
|
import kotlin.test.fail
|
||||||
|
|
||||||
object TestUtils {
|
object TestUtils {
|
||||||
val keypair = KeyPairGenerator.getInstance("EC").genKeyPair()
|
val keypair = KeyPairGenerator.getInstance("EC").genKeyPair()
|
||||||
val keypair2 = 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