From 5022f11d9ef2fc9612d11f828259b28c0eefe5cb Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Sun, 8 Nov 2015 13:34:24 +0100 Subject: [PATCH] More moving around and renaming, to have a consistent style between contracts. --- src/contracts/Cash.kt | 86 +++++++++++++++++++----------------- src/contracts/ComedyPaper.kt | 10 ++--- tests/contracts/CashTests.kt | 47 ++++++++++---------- 3 files changed, 74 insertions(+), 69 deletions(-) diff --git a/src/contracts/Cash.kt b/src/contracts/Cash.kt index 152563b147..3a2e94df24 100644 --- a/src/contracts/Cash.kt +++ b/src/contracts/Cash.kt @@ -11,37 +11,8 @@ import java.time.Instant // Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode. val CASH_PROGRAM_ID = SecureHash.sha256("cash") -/** A state representing a claim on the cash reserves of some institution */ -data class CashState( - /** Where the underlying currency backing this ledger entry can be found (propagated) */ - val deposit: InstitutionReference, - - val amount: Amount, - - /** There must be a MoveCommand signed by this key to claim the amount */ - val owner: PublicKey -) : ContractState { - override val programRef = CASH_PROGRAM_ID - override fun toString() = "Cash($amount at $deposit owned by $owner)" -} - -/** A command proving ownership of some input states, the signature covers the output states. */ -class MoveCashCommand : Command { - override fun equals(other: Any?) = other is MoveCashCommand - override fun hashCode() = 0 -} -/** A command stating that money has been withdrawn from the shared ledger and is now accounted for in some other way */ -class ExitCashCommand(val amount: Amount) : Command { - override fun equals(other: Any?) = other is ExitCashCommand && other.amount == amount - override fun hashCode() = amount.hashCode() -} - class InsufficientBalanceException(val amountMissing: Amount) : Exception() -// Small DSL extension. -fun Iterable.sumCashBy(owner: PublicKey) = this.filterIsInstance().filter { it.owner == owner }.map { it.amount }.sum() -fun Iterable.sumCash() = this.filterIsInstance().map { it.amount }.sum() - /** * A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple * input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour @@ -55,10 +26,39 @@ fun Iterable.sumCash() = this.filterIsInstance().map { * At the same time, other contracts that just want money and don't care much who is currently holding it in their * vaults can ignore the issuer/depositRefs and just examine the amount fields. */ -object CashContract : Contract { +object Cash : Contract { + /** A state representing a claim on the cash reserves of some institution */ + data class State( + /** Where the underlying currency backing this ledger entry can be found (propagated) */ + val deposit: InstitutionReference, + + val amount: Amount, + + /** There must be a MoveCommand signed by this key to claim the amount */ + val owner: PublicKey + ) : ContractState { + override val programRef = CASH_PROGRAM_ID + override fun toString() = "Cash($amount at $deposit owned by $owner)" + } + + + sealed class Commands { + /** A command proving ownership of some input states, the signature covers the output states. */ + class Move : Command { + override fun equals(other: Any?) = other is Move + override fun hashCode() = 0 + } + + /** A command stating that money has been withdrawn from the shared ledger and is now accounted for in some other way */ + class Exit(val amount: Amount) : Command { + override fun equals(other: Any?) = other is Exit && other.amount == amount + override fun hashCode() = amount.hashCode() + } + } + /** This is the function EVERYONE runs */ override fun verify(inStates: List, outStates: List, args: List>, time: Instant) { - val cashInputs = inStates.filterIsInstance() + val cashInputs = inStates.filterIsInstance() requireThat { "there is at least one cash input" by cashInputs.isNotEmpty() @@ -69,7 +69,7 @@ object CashContract : Contract { val currency = cashInputs.first().amount.currency // Select all the output states that are cash states. There may be zero if all money is being withdrawn. - val cashOutputs = outStates.filterIsInstance() + val cashOutputs = outStates.filterIsInstance() requireThat { "all outputs use the currency of the inputs" by cashOutputs.all { it.amount.currency == currency } } @@ -84,7 +84,7 @@ object CashContract : Contract { val inputAmount = inputs.map { it.amount }.sum() val outputAmount = outputs.map { it.amount }.sumOrZero(currency) - val issuerCommand = args.select(institution = deposit.institution).singleOrNull() + val issuerCommand = args.select(institution = deposit.institution).singleOrNull() val amountExitingLedger = issuerCommand?.value?.amount ?: Amount(0, inputAmount.currency) requireThat { @@ -98,7 +98,7 @@ object CashContract : Contract { // see a signature from each of those keys. The actual signatures have been verified against the transaction // data by the platform before execution. val owningPubKeys = cashInputs.map { it.owner }.toSortedSet() - val keysThatSigned = args.select().map { it.signer }.toSortedSet() + val keysThatSigned = args.select().map { it.signer }.toSortedSet() requireThat { "the owning keys are the same as the signing keys" by (owningPubKeys == keysThatSigned) } @@ -111,7 +111,7 @@ object CashContract : Contract { /** Generate a transaction that consumes one or more of the given input states to move money to the given pubkey. */ @Throws(InsufficientBalanceException::class) - fun craftSpend(amount: Amount, to: PublicKey, wallet: List): TransactionForTest { + fun craftSpend(amount: Amount, to: PublicKey, wallet: List): TransactionForTest { // Discussion // // This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline. @@ -135,9 +135,9 @@ object CashContract : Contract { // is put into the transaction, which is finally returned. val currency = amount.currency - val coinsOfCurrency = wallet.asSequence().filter { it.amount.currency == currency } + val coinsOfCurrency = wallet.filter { it.amount.currency == currency } - val gathered = arrayListOf() + val gathered = arrayListOf() var gatheredAmount = Amount(0, currency) for (c in coinsOfCurrency) { if (gatheredAmount >= amount) break @@ -154,7 +154,7 @@ object CashContract : Contract { val states = gathered.groupBy { it.deposit }.map { val (deposit, coins) = it val totalAmount = coins.map { it.amount }.sum() - CashState(deposit, totalAmount, to) + State(deposit, totalAmount, to) } val outputs = if (change.pennies > 0) { @@ -165,12 +165,16 @@ object CashContract : Contract { // Add a change output and adjust the last output downwards. states.subList(0, states.lastIndex) + states.last().let { it.copy(amount = it.amount - change) } + - CashState(gathered.last().deposit, change, changeKey) + State(gathered.last().deposit, change, changeKey) } else states // Finally, generate the commands. Pretend to sign here, real signatures aren't done yet. - val commands = keysUsed.map { VerifiedSigned(it, null, MoveCashCommand()) } + val commands = keysUsed.map { VerifiedSigned(it, null, Commands.Move()) } return TransactionForTest(gathered.toArrayList(), outputs.toArrayList(), commands.toArrayList()) } -} \ No newline at end of file +} + +// Small DSL extension. +fun Iterable.sumCashBy(owner: PublicKey) = filterIsInstance().filter { it.owner == owner }.map { it.amount }.sum() +fun Iterable.sumCash() = filterIsInstance().map { it.amount }.sum() diff --git a/src/contracts/ComedyPaper.kt b/src/contracts/ComedyPaper.kt index bf8af7dac9..fa019d5782 100644 --- a/src/contracts/ComedyPaper.kt +++ b/src/contracts/ComedyPaper.kt @@ -21,13 +21,13 @@ import java.time.Instant val CP_PROGRAM_ID = SecureHash.sha256("comedy-paper") -// TODO: Generalise the notion of an owned object into a superclass/supercontract. Consider composition vs inheritance. +// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance. object ComedyPaper : Contract { data class State( - val issuance: InstitutionReference, - val owner: PublicKey, - val faceValue: Amount, - val maturityDate: Instant + val issuance: InstitutionReference, + val owner: PublicKey, + val faceValue: Amount, + val maturityDate: Instant ) : ContractState { override val programRef = CP_PROGRAM_ID diff --git a/tests/contracts/CashTests.kt b/tests/contracts/CashTests.kt index da4bae5c30..5b09f8fec1 100644 --- a/tests/contracts/CashTests.kt +++ b/tests/contracts/CashTests.kt @@ -1,4 +1,5 @@ -import contracts.* +import contracts.Cash +import contracts.InsufficientBalanceException import core.* import org.junit.Test import kotlin.test.assertEquals @@ -9,16 +10,16 @@ import kotlin.test.assertFailsWith // 2. There must be at least one input state (note: not "one of the type the contract wants") class CashTests { - val inState = CashState( + val inState = Cash.State( deposit = InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), amount = 1000.DOLLARS, owner = DUMMY_PUBKEY_1 ) val outState = inState.copy(owner = DUMMY_PUBKEY_2) - val contract = CashContract + val contract = Cash - fun CashState.editInstitution(institution: Institution) = copy(deposit = deposit.copy(institution = institution)) - fun CashState.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref))) + fun Cash.State.editInstitution(institution: Institution) = copy(deposit = deposit.copy(institution = institution)) + fun Cash.State.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref))) @Test fun trivial() { @@ -39,7 +40,7 @@ class CashTests { } transaction { output { outState } - arg(DUMMY_PUBKEY_2) { MoveCashCommand() } + arg(DUMMY_PUBKEY_2) { Cash.Commands.Move() } contract `fails requirement` "the owning keys are the same as the signing keys" } transaction { @@ -50,7 +51,7 @@ class CashTests { // Simple reallocation works. transaction { output { outState } - arg(DUMMY_PUBKEY_1) { MoveCashCommand() } + arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } contract.accepts() } } @@ -60,7 +61,7 @@ class CashTests { fun testMergeSplit() { // Splitting value works. transaction { - arg(DUMMY_PUBKEY_1) { MoveCashCommand() } + arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } transaction { input { inState } for (i in 1..4) output { inState.copy(amount = inState.amount / 4) } @@ -150,16 +151,16 @@ class CashTests { output { outState.copy(amount = inState.amount - 200.DOLLARS) } transaction { - arg(MEGA_CORP_KEY) { ExitCashCommand(100.DOLLARS) } + arg(MEGA_CORP_KEY) { Cash.Commands.Exit(100.DOLLARS) } contract `fails requirement` "the amounts balance" } transaction { - arg(MEGA_CORP_KEY) { ExitCashCommand(200.DOLLARS) } + arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) } contract `fails requirement` "the owning keys are the same as the signing keys" // No move command. transaction { - arg(DUMMY_PUBKEY_1) { MoveCashCommand() } + arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } contract.accepts() } } @@ -172,14 +173,14 @@ class CashTests { output { inState.copy(amount = inState.amount - 200.DOLLARS).editInstitution(MINI_CORP) } output { inState.copy(amount = inState.amount - 200.DOLLARS) } - arg(DUMMY_PUBKEY_1) { MoveCashCommand() } + arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } contract `fails requirement` "at issuer MegaCorp the amounts balance" - arg(MEGA_CORP_KEY) { ExitCashCommand(200.DOLLARS) } + arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) } contract `fails requirement` "at issuer MiniCorp the amounts balance" - arg(MINI_CORP_KEY) { ExitCashCommand(200.DOLLARS) } + arg(MINI_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) } contract.accepts() } } @@ -206,7 +207,7 @@ class CashTests { // This works. output { inState.copy(owner = DUMMY_PUBKEY_2) } output { inState.copy(owner = DUMMY_PUBKEY_2).editInstitution(MINI_CORP) } - arg(DUMMY_PUBKEY_1) { MoveCashCommand() } + arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } contract.accepts() } @@ -223,10 +224,10 @@ class CashTests { val OUR_PUBKEY_1 = DUMMY_PUBKEY_1 val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2 val WALLET = listOf( - CashState(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 100.DOLLARS, OUR_PUBKEY_1), - CashState(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 400.DOLLARS, OUR_PUBKEY_1), - CashState(InstitutionReference(MINI_CORP, OpaqueBytes.of(1)), 80.DOLLARS, OUR_PUBKEY_1), - CashState(InstitutionReference(MINI_CORP, OpaqueBytes.of(2)), 80.SWISS_FRANCS, OUR_PUBKEY_1) + 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) ) @Test @@ -235,7 +236,7 @@ class CashTests { transaction { input { WALLET[0] } output { WALLET[0].copy(owner = THEIR_PUBKEY_1) } - arg(OUR_PUBKEY_1) { MoveCashCommand() } + arg(OUR_PUBKEY_1) { Cash.Commands.Move() } }, contract.craftSpend(100.DOLLARS, THEIR_PUBKEY_1, WALLET) ) @@ -248,7 +249,7 @@ class CashTests { 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) { MoveCashCommand() } + arg(OUR_PUBKEY_1) { Cash.Commands.Move() } }, contract.craftSpend(10.DOLLARS, THEIR_PUBKEY_1, WALLET) ) @@ -261,7 +262,7 @@ class CashTests { input { WALLET[0] } input { WALLET[1] } output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS) } - arg(OUR_PUBKEY_1) { MoveCashCommand() } + arg(OUR_PUBKEY_1) { Cash.Commands.Move() } }, contract.craftSpend(500.DOLLARS, THEIR_PUBKEY_1, WALLET) ) @@ -276,7 +277,7 @@ class CashTests { 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) { MoveCashCommand() } + arg(OUR_PUBKEY_1) { Cash.Commands.Move() } }, contract.craftSpend(580.DOLLARS, THEIR_PUBKEY_1, WALLET) )