From 0c6c2df48301ef786e2c166c1fb38df9ba6ecefe Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 3 Nov 2015 16:09:02 +0100 Subject: [PATCH] Cash contract: don't allow merging of two different origin deposits together. --- src/Cash.kt | 25 +++++++++++++++++-------- tests/CashTests.kt | 22 +++++++++++++++------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/Cash.kt b/src/Cash.kt index 2b3f2f0747..14ec4641ad 100644 --- a/src/Cash.kt +++ b/src/Cash.kt @@ -4,8 +4,19 @@ import java.util.* ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Cash +// +// 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 +// (a blend of issuer+depositRef) and you couldn't merge outputs of two colours together, but you COULD put them in +// the same transaction. +// +// The goal of this design is to ensure that money can be withdrawn from the ledger easily: if you receive some money +// via this contract, you always know where to go in order to extract it from the R3 ledger via a regular wire transfer, +// no matter how many hands it has passed through in the intervening time. +// +// 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. -// TODO: Think about state merging: when does it make sense to merge multiple cash states from the same issuer? // TODO: Does multi-currency also make sense? Probably? // TODO: Implement a generate function. @@ -51,11 +62,12 @@ class CashContract : Contract { "all outputs use the currency of the inputs" by cashOutputs.all { it.amount.currency == currency } } - // For each issuer that's represented in the inputs, group the inputs together and verify that the outputs + // 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 ((issuer, inputs) in cashInputs.groupBy { it.issuingInstitution }) { - val outputs = cashOutputs.filter { it.issuingInstitution == issuer } + 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 } outputsLeft -= outputs.size val inputAmount = inputs.map { it.amount }.sum() @@ -64,11 +76,8 @@ class CashContract : Contract { val issuerCommand = args.filter { it.signingInstitution == issuer }.map { it.command as? ExitCashCommand }.filterNotNull().singleOrNull() val amountExitingLedger = issuerCommand?.amount ?: Amount(0, inputAmount.currency) - val depositReference = inputs.first().depositReference - requireThat { - "for issuer ${issuer.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger) - "for issuer ${issuer.name} the deposit references are the same" by outputs.all { it.depositReference == depositReference } + "for deposit $depositRef at issuer ${issuer.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger) } } diff --git a/tests/CashTests.kt b/tests/CashTests.kt index e64461b09a..65c3e9cabc 100644 --- a/tests/CashTests.kt +++ b/tests/CashTests.kt @@ -94,14 +94,14 @@ class CashTests { transaction { input { inState } output { outState.copy(issuingInstitution = MINI_CORP) } - contract `fails requirement` "for issuer MegaCorp the amounts balance" + 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) } - contract `fails requirement` "the deposit references are the same" + contract `fails requirement` "for deposit [01] at issuer MegaCorp the amounts balance" } // Can't mix currencies. transaction { @@ -121,11 +121,19 @@ class CashTests { output { outState.copy(amount = 1150.DOLLARS) } contract `fails requirement` "all inputs use the same currency" } + // Can't have superfluous input states from different issuers. transaction { input { inState } input { inState.copy(issuingInstitution = MINI_CORP) } output { outState } - contract `fails requirement` "for issuer MiniCorp the amounts balance" + 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)) } + contract `fails requirement` "for deposit [01]" } } @@ -161,10 +169,10 @@ class CashTests { arg(DUMMY_PUBKEY_1) { MoveCashCommand() } - contract `fails requirement` "for issuer MegaCorp the amounts balance" + contract `fails requirement` "at issuer MegaCorp the amounts balance" arg(MEGA_CORP_KEY) { ExitCashCommand(200.DOLLARS) } - contract `fails requirement` "for issuer MiniCorp the amounts balance" + contract `fails requirement` "at issuer MiniCorp the amounts balance" arg(MINI_CORP_KEY) { ExitCashCommand(200.DOLLARS) } contract.accepts() @@ -181,13 +189,13 @@ class CashTests { // Can't merge them together. transaction { output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS) } - contract `fails requirement` "for issuer MegaCorp the amounts balance" + contract `fails requirement` "at issuer MegaCorp the amounts balance" } // Missing MiniCorp deposit transaction { output { inState.copy(owner = DUMMY_PUBKEY_2) } output { inState.copy(owner = DUMMY_PUBKEY_2) } - contract `fails requirement` "for issuer MegaCorp the amounts balance" + contract `fails requirement` "at issuer MegaCorp the amounts balance" } // This works.