From a09120d4459a610fb1777a415acbfaf42d72f0e5 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 3 Nov 2015 18:05:21 +0100 Subject: [PATCH] Cash contract: Introduce a DepositPointer abstraction for easier grouping In this current model, you cannot mix up money from different deposits: they must always be kept separate. --- src/Cash.kt | 22 ++++++++++++---------- tests/CashTests.kt | 37 +++++++++++++++++++------------------ 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/Cash.kt b/src/Cash.kt index e4655cd818..a9de894974 100644 --- a/src/Cash.kt +++ b/src/Cash.kt @@ -11,13 +11,16 @@ import java.util.* // 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") +/** + * Reference to some money being stored by an institution e.g. in a vault or (more likely) on their normal ledger. + * The deposit reference is intended to be encrypted so it's meaningless to anyone other than the institution. + */ +data class DepositPointer(val institution: Institution, val reference: OpaqueBytes) + /** A state representing a claim on the cash reserves of some institution */ data class CashState( - /** The institution that has this original cash deposit (propagated) */ - val issuingInstitution: Institution, - - /** Whatever internal ID the bank needs in order to locate that deposit, may be encrypted (propagated) */ - val depositReference: OpaqueBytes, + /** Where the underlying currency backing this ledger entry can be found (propagated) */ + val deposit: DepositPointer, val amount: Amount, @@ -68,19 +71,18 @@ class CashContract : Contract { // For each deposit that's represented in the inputs, group the inputs together and verify that the outputs // balance, taking into account a possible exit command from that issuer. var outputsLeft = cashOutputs.size - for ((pair, inputs) in cashInputs.groupBy { Pair(it.issuingInstitution, it.depositReference) }) { - val (issuer, depositRef) = pair - val outputs = cashOutputs.filter { it.issuingInstitution == issuer && it.depositReference == depositRef } + for ((deposit, inputs) in cashInputs.groupBy { it.deposit }) { + val outputs = cashOutputs.filter { it.deposit == deposit } outputsLeft -= outputs.size val inputAmount = inputs.map { it.amount }.sum() val outputAmount = outputs.map { it.amount }.sumOrZero(currency) - val issuerCommand = args.filter { it.signingInstitution == issuer }.map { it.command as? ExitCashCommand }.filterNotNull().singleOrNull() + val issuerCommand = args.filter { it.signingInstitution == deposit.institution }.map { it.command as? ExitCashCommand }.filterNotNull().singleOrNull() val amountExitingLedger = issuerCommand?.amount ?: Amount(0, inputAmount.currency) requireThat { - "for deposit $depositRef at issuer ${issuer.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger) + "for deposit ${deposit.reference} at issuer ${deposit.institution.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger) } } diff --git a/tests/CashTests.kt b/tests/CashTests.kt index 7f45f0a554..c67a1d2708 100644 --- a/tests/CashTests.kt +++ b/tests/CashTests.kt @@ -8,15 +8,16 @@ import kotlin.test.assertFailsWith class CashTests { val inState = CashState( - issuingInstitution = MEGA_CORP, - depositReference = OpaqueBytes.of(1), + deposit = DepositPointer(MEGA_CORP, OpaqueBytes.of(1)), amount = 1000.DOLLARS, owner = DUMMY_PUBKEY_1 ) val outState = inState.copy(owner = DUMMY_PUBKEY_2) - val contract = CashContract() + fun CashState.editInstitution(institution: Institution) = copy(deposit = deposit.copy(institution = institution)) + fun CashState.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref))) + @Test fun trivial() { transaction { @@ -41,7 +42,7 @@ class CashTests { } transaction { output { outState } - output { outState.copy(issuingInstitution = MINI_CORP) } + output { outState.editInstitution(MINI_CORP) } contract `fails requirement` "no output states are unaccounted for" } // Simple reallocation works. @@ -95,14 +96,14 @@ class CashTests { // Can't change issuer. transaction { input { inState } - output { outState.copy(issuingInstitution = MINI_CORP) } + output { outState.editInstitution(MINI_CORP) } contract `fails requirement` "at issuer MegaCorp the amounts balance" } // Can't change deposit reference when splitting. transaction { input { inState } - output { outState.copy(depositReference = OpaqueBytes.of(0), amount = inState.amount / 2) } - output { outState.copy(depositReference = OpaqueBytes.of(1), amount = inState.amount / 2) } + output { outState.editDepositRef(0).copy(amount = inState.amount / 2) } + output { outState.editDepositRef(1).copy(amount = inState.amount / 2) } contract `fails requirement` "for deposit [01] at issuer MegaCorp the amounts balance" } // Can't mix currencies. @@ -126,15 +127,15 @@ class CashTests { // Can't have superfluous input states from different issuers. transaction { input { inState } - input { inState.copy(issuingInstitution = MINI_CORP) } + input { inState.editInstitution(MINI_CORP) } output { outState } contract `fails requirement` "at issuer MiniCorp the amounts balance" } // Can't combine two different deposits at the same issuer. transaction { input { inState } - input { inState.copy(depositReference = OpaqueBytes.of(3)) } - output { outState.copy(amount = inState.amount * 2, depositReference = OpaqueBytes.of(3)) } + input { inState.editDepositRef(3) } + output { outState.copy(amount = inState.amount * 2).editDepositRef(3) } contract `fails requirement` "for deposit [01]" } } @@ -164,9 +165,9 @@ class CashTests { // Multi-issuer case. transaction { input { inState } - input { inState.copy(issuingInstitution = MINI_CORP) } + input { inState.editInstitution(MINI_CORP) } - output { inState.copy(issuingInstitution = MINI_CORP, amount = inState.amount - 200.DOLLARS) } + output { inState.copy(amount = inState.amount - 200.DOLLARS).editInstitution(MINI_CORP) } output { inState.copy(amount = inState.amount - 200.DOLLARS) } arg(DUMMY_PUBKEY_1) { MoveCashCommand() } @@ -186,7 +187,7 @@ class CashTests { transaction { // Gather 2000 dollars from two different issuers. input { inState } - input { inState.copy(issuingInstitution = MINI_CORP) } + input { inState.editInstitution(MINI_CORP) } // Can't merge them together. transaction { @@ -202,7 +203,7 @@ class CashTests { // This works. output { inState.copy(owner = DUMMY_PUBKEY_2) } - output { inState.copy(issuingInstitution = MINI_CORP, owner = DUMMY_PUBKEY_2) } + output { inState.copy(owner = DUMMY_PUBKEY_2).editInstitution(MINI_CORP) } arg(DUMMY_PUBKEY_1) { MoveCashCommand() } contract.accepts() } @@ -216,10 +217,10 @@ class CashTests { val OUR_PUBKEY_1 = DUMMY_PUBKEY_1 val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2 val WALLET = listOf( - CashState(issuingInstitution = MEGA_CORP, depositReference = OpaqueBytes.of(1), amount = 100.DOLLARS, owner = OUR_PUBKEY_1), - CashState(issuingInstitution = MEGA_CORP, depositReference = OpaqueBytes.of(2), amount = 400.DOLLARS, owner = OUR_PUBKEY_1), - CashState(issuingInstitution = MINI_CORP, depositReference = OpaqueBytes.of(1), amount = 80.DOLLARS, owner = OUR_PUBKEY_1), - CashState(issuingInstitution = MINI_CORP, depositReference = OpaqueBytes.of(2), amount = 80.SWISS_FRANCS, owner = OUR_PUBKEY_1) + CashState(DepositPointer(MEGA_CORP, OpaqueBytes.of(1)), 100.DOLLARS, OUR_PUBKEY_1), + CashState(DepositPointer(MEGA_CORP, OpaqueBytes.of(2)), 400.DOLLARS, OUR_PUBKEY_1), + CashState(DepositPointer(MINI_CORP, OpaqueBytes.of(1)), 80.DOLLARS, OUR_PUBKEY_1), + CashState(DepositPointer(MINI_CORP, OpaqueBytes.of(2)), 80.SWISS_FRANCS, OUR_PUBKEY_1) ) @Test