diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index ecc42f57d2..91c1fb1f78 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -6,7 +6,6 @@ import com.r3corda.contracts.asset.sumCashBy import com.r3corda.contracts.clause.AbstractIssue import com.r3corda.core.contracts.* import com.r3corda.core.contracts.clauses.* -import com.r3corda.core.crypto.NullPublicKey import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.toStringShort @@ -50,8 +49,7 @@ class CommercialPaper : ClauseVerifier() { val maturityDate: Instant ) - override val clauses: List - get() = listOf(Clauses.Group()) + override val clauses = listOf(Clauses.Group()) override fun extractCommands(tx: TransactionForContract): List> = tx.commands.select() @@ -82,32 +80,27 @@ class CommercialPaper : ClauseVerifier() { interface Clauses { class Group : GroupClauseVerifier>() { - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.ERROR - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.END - override val clauses: List>> - get() = listOf( - Redeem(), - Move(), - Issue()) + override val ifNotMatched = MatchBehaviour.ERROR + override val ifMatched = MatchBehaviour.END + override val clauses = listOf( + Redeem(), + Move(), + Issue() + ) override fun extractGroups(tx: TransactionForContract): List>> = tx.groupStates> { it.token } } abstract class AbstractGroupClause: GroupClause> { - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.CONTINUE - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.END + override val ifNotMatched = MatchBehaviour.CONTINUE + override val ifMatched = MatchBehaviour.END } class Issue : AbstractIssue( { map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() }, { token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) { - override val requiredCommands: Set> - get() = setOf(Commands.Issue::class.java) + override val requiredCommands: Set> = setOf(Commands.Issue::class.java) override fun verify(tx: TransactionForContract, inputs: List, @@ -127,8 +120,7 @@ class CommercialPaper : ClauseVerifier() { } class Move: AbstractGroupClause() { - override val requiredCommands: Set> - get() = setOf(Commands.Move::class.java) + override val requiredCommands: Set> = setOf(Commands.Move::class.java) override fun verify(tx: TransactionForContract, inputs: List, diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt index 4d2ee4982e..65b4ff6fea 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt @@ -184,7 +184,7 @@ class FloatingRatePaymentEvent(date: LocalDate, * This is just a representation of a vanilla Fixed vs Floating (same currency) IRS in the R3 prototype model. */ class InterestRateSwap() : ClauseVerifier() { - override val legalContractReference: SecureHash = SecureHash.sha256("is_this_the_text_of_the_contract ? TBD") + override val legalContractReference = SecureHash.sha256("is_this_the_text_of_the_contract ? TBD") /** * This Common area contains all the information that is not leg specific. @@ -457,10 +457,8 @@ class InterestRateSwap() : ClauseVerifier() { * helper functions for the clauses. */ abstract class AbstractIRSClause : GroupClause { - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.END - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.CONTINUE + override val ifMatched = MatchBehaviour.END + override val ifNotMatched = MatchBehaviour.CONTINUE // These functions may make more sense to use for basket types, but for now let's leave them here fun checkLegDates(legs: Array) { @@ -504,27 +502,20 @@ class InterestRateSwap() : ClauseVerifier() { } class Group : GroupClauseVerifier() { - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.END - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.ERROR + override val ifMatched = MatchBehaviour.END + override val ifNotMatched = MatchBehaviour.ERROR - override fun extractGroups(tx: TransactionForContract): List> { + override fun extractGroups(tx: TransactionForContract): List> // Group by Trade ID for in / out states - return tx.groupStates() { state: InterestRateSwap.State -> state.common.tradeID } - } + = tx.groupStates() { state -> state.common.tradeID } - override val clauses: List> - get() = listOf(Agree(), Fix(), Pay(), Mature()) + override val clauses = listOf(Agree(), Fix(), Pay(), Mature()) } class Timestamped : SingleClause { - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.CONTINUE - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.ERROR - override val requiredCommands: Set> - get() = emptySet() + override val ifMatched = MatchBehaviour.CONTINUE + override val ifNotMatched = MatchBehaviour.ERROR + override val requiredCommands = emptySet>() override fun verify(tx: TransactionForContract, commands: Collection>): Set { // TODO: This needs to either be the notary used for the inputs, or otherwise @@ -537,8 +528,7 @@ class InterestRateSwap() : ClauseVerifier() { } class Agree : AbstractIRSClause() { - override val requiredCommands: Set> - get() = setOf(Commands.Agree::class.java) + override val requiredCommands = setOf(Commands.Agree::class.java) override fun verify(tx: TransactionForContract, inputs: List, @@ -574,8 +564,7 @@ class InterestRateSwap() : ClauseVerifier() { } class Fix : AbstractIRSClause() { - override val requiredCommands: Set> - get() = setOf(Commands.Refix::class.java) + override val requiredCommands = setOf(Commands.Refix::class.java) override fun verify(tx: TransactionForContract, inputs: List, @@ -620,8 +609,7 @@ class InterestRateSwap() : ClauseVerifier() { } class Pay : AbstractIRSClause() { - override val requiredCommands: Set> - get() = setOf(Commands.Pay::class.java) + override val requiredCommands = setOf(Commands.Pay::class.java) override fun verify(tx: TransactionForContract, inputs: List, @@ -637,8 +625,7 @@ class InterestRateSwap() : ClauseVerifier() { } class Mature : AbstractIRSClause() { - override val requiredCommands: Set> - get() = setOf(Commands.Mature::class.java) + override val requiredCommands = setOf(Commands.Mature::class.java) override fun verify(tx: TransactionForContract, inputs: List, @@ -748,7 +735,7 @@ class InterestRateSwap() : ClauseVerifier() { /** * Just makes printing it out a bit better for those who don't have 80000 column wide monitors. */ - fun prettyPrint(): String = toString().replace(",", "\n") + fun prettyPrint() = toString().replace(",", "\n") } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt index a34cd9f158..5758cc47db 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt @@ -33,7 +33,7 @@ val CASH_PROGRAM_ID = Cash() * 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. */ -class Cash : ClauseVerifier() { +class Cash : OnLedgerAsset() { /** * TODO: * 1) hash should be of the contents, not the URI @@ -45,19 +45,16 @@ class Cash : ClauseVerifier() { * that is inconsistent with the legal contract. */ override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") - override val clauses: List - get() = listOf(Clauses.Group()) + override val conserveClause: AbstractConserveAmount = Clauses.ConserveAmount() + override val clauses = listOf(Clauses.Group()) override fun extractCommands(tx: TransactionForContract): List> = tx.commands.select() interface Clauses { class Group : GroupClauseVerifier>() { - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.END - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.ERROR - override val clauses: List>> - get() = listOf( + override val ifMatched: MatchBehaviour = MatchBehaviour.END + override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR + override val clauses = listOf( NoZeroSizedOutputs(), Issue(), ConserveAmount()) @@ -66,9 +63,11 @@ class Cash : ClauseVerifier() { = tx.groupStates> { it.issuanceDef } } - class Issue : AbstractIssue({ sumCash() }, { token -> sumCashOrZero(token) }) { - override val requiredCommands: Set> - get() = setOf(Commands.Issue::class.java) + class Issue : AbstractIssue( + sum = { sumCash() }, + sumOrZero = { sumCashOrZero(it) } + ) { + override val requiredCommands = setOf(Commands.Issue::class.java) } class ConserveAmount : AbstractConserveAmount() @@ -84,15 +83,11 @@ class Cash : ClauseVerifier() { constructor(deposit: PartyAndReference, amount: Amount, owner: PublicKey) : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) - override val deposit: PartyAndReference - get() = amount.token.issuer - override val exitKeys: Collection - get() = setOf(deposit.party.owningKey) + override val deposit = amount.token.issuer + override val exitKeys = setOf(deposit.party.owningKey) override val contract = CASH_PROGRAM_ID - override val issuanceDef: Issued - get() = amount.token - override val participants: List - get() = listOf(owner) + override val issuanceDef = amount.token + override val participants = listOf(owner) override fun move(newAmount: Amount>, newOwner: PublicKey): FungibleAsset = copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner) @@ -126,75 +121,6 @@ class Cash : ClauseVerifier() { data class Exit(override val amount: Amount>) : Commands, FungibleAsset.Commands.Exit } - /** - * Generate an transaction exiting cash from the ledger. - * - * @param tx transaction builder to add states and commands to. - * @param amountIssued the amount to be exited, represented as a quantity of issued currency. - * @param changeKey the key to send any change to. This needs to be explicitly stated as the input states are not - * necessarily owned by us. - * @param cashStates the cash states to take funds from. No checks are done about ownership of these states, it is - * the responsibility of the caller to check that they do not exit funds held by others. - * @return the public key of the cash issuer, who must sign the transaction for it to be valid. - */ - // TODO: I'm not at all sure we should support exiting funds others hold, but that's a bigger discussion around the - // contract logic, not just this function. - fun generateExit(tx: TransactionBuilder, amountIssued: Amount>, - changeKey: PublicKey, cashStates: List>): PublicKey { - val currency = amountIssued.token.product - val issuer = amountIssued.token.issuer.party - val amount = Amount(amountIssued.quantity, currency) - var acceptableCoins = cashStates.filter { ref -> ref.state.data.amount.token == amountIssued.token } - val notary = acceptableCoins.firstOrNull()?.state?.notary - // TODO: We should be prepared to produce multiple transactions exiting inputs from - // different notaries, or at least group states by notary and take the set with the - // highest total value - acceptableCoins = acceptableCoins.filter { it.state.notary == notary } - - val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, Amount(amount.quantity, currency)) - val takeChangeFrom = gathered.lastOrNull() - val change = if (takeChangeFrom != null && gatheredAmount > amount) { - Amount>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef) - } else { - null - } - - val outputs: List> = if (change != null) { - // Add a change output and adjust the last output downwards. - listOf(TransactionState(State(amountIssued.token.issuer, Amount(change.quantity, currency), changeKey), - notary!!)) - } else emptyList() - - for (state in gathered) tx.addInputState(state) - for (state in outputs) tx.addOutputState(state) - tx.addCommand(Commands.Exit(amountIssued), amountIssued.token.issuer.party.owningKey) - return amountIssued.token.issuer.party.owningKey - } - - /** - * Gather coins from the given list of states, sufficient to match or exceed the given amount. - * - * @param acceptableCoins list of states to use as inputs. - * @param amount the amount to gather states up to. - * @throws InsufficientBalanceException if there isn't enough value in the states to cover the requested amount. - */ - @Throws(InsufficientBalanceException::class) - private fun gatherCoins(acceptableCoins: List>, - amount: Amount): Pair>, Amount> { - val gathered = arrayListOf>() - var gatheredAmount = Amount(0, amount.token) - for (c in acceptableCoins) { - if (gatheredAmount >= amount) break - gathered.add(c) - gatheredAmount += Amount(c.state.data.amount.quantity, amount.token) - } - - if (gatheredAmount < amount) - throw InsufficientBalanceException(amount - gatheredAmount) - - return Pair(gathered, gatheredAmount) - } - /** * Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey. */ @@ -208,83 +134,15 @@ class Cash : ClauseVerifier() { check(tx.inputStates().isEmpty()) check(tx.outputStates().map { it.data }.sumCashOrNull() == null) val at = amount.token.issuer - tx.addOutputState(TransactionState(Cash.State(amount, owner), notary)) - tx.addCommand(Cash.Commands.Issue(), at.party.owningKey) + tx.addOutputState(TransactionState(State(amount, owner), notary)) + tx.addCommand(generateIssueCommand(), at.party.owningKey) } - /** - * 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. - * - * @param onlyFromParties if non-null, the wallet will be filtered to only include cash states issued by the set - * of given parties. This can be useful if the party you're trying to pay has expectations - * about which type of cash claims they are willing to accept. - */ - @Throws(InsufficientBalanceException::class) - fun generateSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey, - cashStates: List>, onlyFromParties: Set? = null): List { - // Discussion - // - // This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline. - // - // First we must select a set of cash states (which for convenience we will call 'coins' here, as in bitcoinj). - // The input states can be considered our "wallet", and may consist of coins of different currencies, and from - // different institutions and deposits. - // - // Coin selection is a complex problem all by itself and many different approaches can be used. It is easily - // possible for different actors to use different algorithms and approaches that, for example, compete on - // privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of - // obfuscation and so on. - // - // 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. The point of this design - // is to ensure that ledger entries are immutable and globally identifiable. - // - // Finally, we add the states to the provided partial transaction. - - val currency = amount.token - val acceptableCoins = run { - val ofCurrency = cashStates.filter { it.state.data.amount.token.product == currency } - if (onlyFromParties != null) - ofCurrency.filter { it.state.data.deposit.party in onlyFromParties } - else - ofCurrency - } - - val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount) - val takeChangeFrom = gathered.firstOrNull() - val change = if (takeChangeFrom != null && gatheredAmount > amount) { - Amount>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef) - } else { - null - } - val keysUsed = gathered.map { it.state.data.owner }.toSet() - - val states = gathered.groupBy { it.state.data.deposit }.map { - val coins = it.value - val totalAmount = coins.map { it.state.data.amount }.sumOrThrow() - TransactionState(State(totalAmount, to), coins.first().state.notary) - } - - val outputs = if (change != null) { - // 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().state.data.owner - // Add a change output and adjust the last output downwards. - states.subList(0, states.lastIndex) + - states.last().let { TransactionState(it.data.copy(amount = it.data.amount - change), it.notary) } + - TransactionState(State(change, changeKey), gathered.last().state.notary) - } else states - - for (state in gathered) tx.addInputState(state) - 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? - val keysList = keysUsed.toList() - tx.addCommand(Commands.Move(), keysList) - return keysList - } + override fun deriveState(txState: TransactionState, amount: Amount>, owner: PublicKey) + = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) + override fun generateExitCommand(amount: Amount>) = Commands.Exit(amount) + override fun generateIssueCommand() = Commands.Issue() + override fun generateMoveCommand() = Commands.Move() } // Small DSL extensions. @@ -298,7 +156,7 @@ fun Iterable.sumCashBy(owner: PublicKey): Amount /** * Sums the cash states in the list, throwing an exception if there are none, or if any of the cash - * states cannot be added together (i.e. are different currencies). + * states cannot be added together (i.e. are different currencies or issuers). */ fun Iterable.sumCash(): Amount> = filterIsInstance().map { it.amount }.sumOrThrow() diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/CommodityContract.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/CommodityContract.kt index 9fb8a7e542..632eb20b9f 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/CommodityContract.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/CommodityContract.kt @@ -9,8 +9,6 @@ import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.newSecureRandom import com.r3corda.core.crypto.toStringShort -import com.r3corda.core.node.services.Wallet -import com.r3corda.core.utilities.Emoji import java.security.PublicKey import java.util.* @@ -31,7 +29,7 @@ val COMMODITY_PROGRAM_ID = CommodityContract() * internal accounting by the issuer (it might be, for example, a warehouse and/or location within a warehouse). */ // TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc. -class CommodityContract : ClauseVerifier() { +class CommodityContract : OnLedgerAsset() { /** * TODO: * 1) hash should be of the contents, not the URI @@ -44,6 +42,8 @@ class CommodityContract : ClauseVerifier() { */ override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html") + override val conserveClause: AbstractConserveAmount = Clauses.ConserveAmount() + /** * The clauses for this contract are essentially: * @@ -63,35 +63,34 @@ class CommodityContract : ClauseVerifier() { * The group clause does not depend on any commands being present, so something has gone terribly wrong if * it doesn't match. */ - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.ERROR + override val ifNotMatched = MatchBehaviour.ERROR /** * The group clause is the only top level clause, so end after processing it. If there are any commands left * after this clause has run, the clause verifier will trigger an error. */ - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.END + override val ifMatched = MatchBehaviour.END // Subclauses to run on each group - override val clauses: List>> - get() = listOf( - NoZeroSizedOutputs(), - Issue(), - ConserveAmount() - ) + override val clauses = listOf( + NoZeroSizedOutputs(), + Issue(), + ConserveAmount() + ) /** * Group commodity states by issuance definition (issuer and underlying commodity). */ - override fun extractGroups(tx: TransactionForContract): List>> + override fun extractGroups(tx: TransactionForContract) = tx.groupStates> { it.issuanceDef } } /** * Standard issue clause, specialised to match the commodity issue command. */ - class Issue : AbstractIssue({ -> sumCommodities() }, { token: Issued -> sumCommoditiesOrZero(token) }) { - override val requiredCommands: Set> - get() = setOf(Commands.Issue::class.java) + class Issue : AbstractIssue( + sum = { sumCommodities() }, + sumOrZero = { sumCommoditiesOrZero(it) } + ) { + override val requiredCommands = setOf(Commands.Issue::class.java) } /** @@ -110,15 +109,11 @@ class CommodityContract : ClauseVerifier() { constructor(deposit: PartyAndReference, amount: Amount, owner: PublicKey) : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) - override val deposit: PartyAndReference - get() = amount.token.issuer + override val deposit = amount.token.issuer override val contract = COMMODITY_PROGRAM_ID - override val exitKeys: Collection - get() = Collections.singleton(owner) - override val issuanceDef: Issued - get() = amount.token - override val participants: List - get() = listOf(owner) + override val exitKeys = Collections.singleton(owner) + override val issuanceDef = amount.token + override val participants = listOf(owner) override fun move(newAmount: Amount>, newOwner: PublicKey): FungibleAsset = copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner) @@ -151,8 +146,7 @@ class CommodityContract : ClauseVerifier() { */ data class Exit(override val amount: Amount>) : Commands, FungibleAsset.Commands.Exit } - override val clauses: List - get() = listOf(Clauses.Group()) + override val clauses = listOf(Clauses.Group()) override fun extractCommands(tx: TransactionForContract): List> = tx.commands.select() @@ -167,107 +161,18 @@ class CommodityContract : ClauseVerifier() { */ fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: PublicKey, notary: Party) { check(tx.inputStates().isEmpty()) - check(tx.outputStates().map { it.data }.sumFungibleOrNull() == null) + check(tx.outputStates().map { it.data }.sumCashOrNull() == null) val at = amount.token.issuer - tx.addOutputState(TransactionState(CommodityContract.State(amount, owner), notary)) - tx.addCommand(Commands.Issue(), at.party.owningKey) + tx.addOutputState(TransactionState(State(amount, owner), notary)) + tx.addCommand(generateIssueCommand(), at.party.owningKey) } - /** - * 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 generateSpend(tx: TransactionBuilder, amount: Amount>, to: PublicKey, - commodityStates: List>): List = - generateSpend(tx, Amount(amount.quantity, amount.token.product), to, commodityStates, - setOf(amount.token.issuer.party)) - /** - * 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. - * - * @param onlyFromParties if non-null, the wallet will be filtered to only include commodity states issued by the set - * of given parties. This can be useful if the party you're trying to pay has expectations - * about which type of commodity claims they are willing to accept. - */ - // TODO: These spend functions should be shared with [Cash], possibly through some common superclass - @Throws(InsufficientBalanceException::class) - fun generateSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey, - commodityStates: List>, onlyFromParties: Set? = null): List { - // Discussion - // - // This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline. - // - // First we must select a set of commodity states (which for convenience we will call 'coins' here, as in bitcoinj). - // The input states can be considered our "wallet", and may consist of coins of different currencies, and from - // different institutions and deposits. - // - // Coin selection is a complex problem all by itself and many different approaches can be used. It is easily - // possible for different actors to use different algorithms and approaches that, for example, compete on - // privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of - // obfuscation and so on. - // - // 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. The point of this design - // is to ensure that ledger entries are immutable and globally identifiable. - // - // Finally, we add the states to the provided partial transaction. - - val currency = amount.token - val acceptableCoins = run { - val ofCurrency = commodityStates.filter { it.state.data.amount.token.product == currency } - if (onlyFromParties != null) - ofCurrency.filter { it.state.data.deposit.party in onlyFromParties } - else - ofCurrency - } - - val gathered = arrayListOf>() - var gatheredAmount = Amount(0, currency) - var takeChangeFrom: StateAndRef? = null - for (c in acceptableCoins) { - if (gatheredAmount >= amount) break - gathered.add(c) - gatheredAmount += Amount(c.state.data.amount.quantity, currency) - takeChangeFrom = c - } - - if (gatheredAmount < amount) - throw InsufficientBalanceException(amount - gatheredAmount) - - val change = if (takeChangeFrom != null && gatheredAmount > amount) { - Amount>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef) - } else { - null - } - val keysUsed = gathered.map { it.state.data.owner }.toSet() - - val states = gathered.groupBy { it.state.data.deposit }.map { - val coins = it.value - val totalAmount = coins.map { it.state.data.amount }.sumOrThrow() - TransactionState(State(totalAmount, to), coins.first().state.notary) - } - - val outputs = if (change != null) { - // 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().state.data.owner - // Add a change output and adjust the last output downwards. - states.subList(0, states.lastIndex) + - states.last().let { TransactionState(it.data.copy(amount = it.data.amount - change), it.notary) } + - TransactionState(State(change, changeKey), gathered.last().state.notary) - } else states - - for (state in gathered) tx.addInputState(state) - 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? - val keysList = keysUsed.toList() - tx.addCommand(Commands.Move(), keysList) - return keysList - } + override fun deriveState(txState: TransactionState, amount: Amount>, owner: PublicKey) + = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) + override fun generateExitCommand(amount: Amount>) = Commands.Exit(amount) + override fun generateIssueCommand() = Commands.Issue() + override fun generateMoveCommand() = Commands.Move() } /** diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt index 3756c27a20..28a8d6d312 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt @@ -43,8 +43,7 @@ class Obligation

: ClauseVerifier() { * that is inconsistent with the legal contract. */ override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.example.gov/cash-settlement.html") - override val clauses: List - get() = listOf(InterceptorClause(Clauses.VerifyLifecycle

(), Clauses.Net

()), + override val clauses = listOf(InterceptorClause(Clauses.VerifyLifecycle

(), Clauses.Net

()), Clauses.Group

()) interface Clauses { @@ -52,18 +51,16 @@ class Obligation

: ClauseVerifier() { * Parent clause for clauses that operate on grouped states (those which are fungible). */ class Group

: GroupClauseVerifier, Issued>>() { - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.END - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.ERROR - override val clauses: List, Issued>>> - get() = listOf( - NoZeroSizedOutputs, Terms

>(), - SetLifecycle

(), - VerifyLifecycle

(), - Settle

(), - Issue(), - ConserveAmount()) + override val ifMatched: MatchBehaviour = MatchBehaviour.END + override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR + override val clauses = listOf( + NoZeroSizedOutputs, Terms

>(), + SetLifecycle

(), + VerifyLifecycle

(), + Settle

(), + Issue(), + ConserveAmount() + ) override fun extractGroups(tx: TransactionForContract): List, Issued>>> = tx.groupStates, Issued>> { it.issuanceDef } @@ -73,8 +70,7 @@ class Obligation

: ClauseVerifier() { * Generic issuance clause */ class Issue

: AbstractIssue, Terms

>({ -> sumObligations() }, { token: Issued> -> sumObligationsOrZero(token) }) { - override val requiredCommands: Set> - get() = setOf(Obligation.Commands.Issue::class.java) + override val requiredCommands = setOf(Obligation.Commands.Issue::class.java) } /** @@ -91,12 +87,9 @@ class Obligation

: ClauseVerifier() { * Obligation-specific clause for changing the lifecycle of one or more states. */ class SetLifecycle

: GroupClause, Issued>> { - override val requiredCommands: Set> - get() = setOf(Commands.SetLifecycle::class.java) - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.END - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.CONTINUE + override val requiredCommands = setOf(Commands.SetLifecycle::class.java) + override val ifMatched: MatchBehaviour = MatchBehaviour.END + override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE override fun verify(tx: TransactionForContract, inputs: List>, @@ -114,12 +107,9 @@ class Obligation

: ClauseVerifier() { * change of ownership of other states to fulfil */ class Settle

: GroupClause, Issued>> { - override val requiredCommands: Set> - get() = setOf(Commands.Settle::class.java) - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.END - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.CONTINUE + override val requiredCommands = setOf(Commands.Settle::class.java) + override val ifMatched: MatchBehaviour = MatchBehaviour.END + override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE override fun verify(tx: TransactionForContract, inputs: List>, @@ -208,12 +198,9 @@ class Obligation

: ClauseVerifier() { * non-standard lifecycle states on input/output. */ class VerifyLifecycle

: SingleClause, GroupClause, Issued>> { - override val requiredCommands: Set> - get() = emptySet() - override val ifMatched: MatchBehaviour - get() = MatchBehaviour.CONTINUE - override val ifNotMatched: MatchBehaviour - get() = MatchBehaviour.ERROR + override val requiredCommands: Set> = emptySet() + override val ifMatched: MatchBehaviour = MatchBehaviour.CONTINUE + override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR override fun verify(tx: TransactionForContract, commands: Collection>): Set = verify( @@ -407,8 +394,8 @@ class Obligation

: ClauseVerifier() { // If we have an default command, perform special processing: issued contracts can only be defaulted // after the due date, and default/reset can only be done by the beneficiary - val expectedInputLifecycle: Lifecycle = setLifecycleCommand.value.inverse - val expectedOutputLifecycle: Lifecycle = setLifecycleCommand.value.lifecycle + val expectedInputLifecycle = setLifecycleCommand.value.inverse + val expectedOutputLifecycle = setLifecycleCommand.value.lifecycle // Check that we're past the deadline for ALL involved inputs, and that the output states correspond 1:1 for ((stateIdx, input) in inputs.withIndex()) { @@ -416,7 +403,7 @@ class Obligation

: ClauseVerifier() { val actualOutput = outputs[stateIdx] val deadline = input.dueBefore val timestamp: TimestampCommand? = tx.timestamp - val expectedOutput: State

= input.copy(lifecycle = expectedOutputLifecycle) + val expectedOutput = input.copy(lifecycle = expectedOutputLifecycle) requireThat { "there is a timestamp from the authority" by (timestamp != null) @@ -458,6 +445,24 @@ class Obligation

: ClauseVerifier() { tx.addCommand(Commands.Net(NetType.PAYMENT), signer) } + /** + * Generate an transaction exiting an obligation from the ledger. + * + * @param tx transaction builder to add states and commands to. + * @param amountIssued the amount to be exited, represented as a quantity of issued currency. + * @param changeKey the key to send any change to. This needs to be explicitly stated as the input states are not + * necessarily owned by us. + * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is + * the responsibility of the caller to check that they do not exit funds held by others. + * @return the public key of the assets issuer, who must sign the transaction for it to be valid. + */ + fun generateExit(tx: TransactionBuilder, amountIssued: Amount>>, + changeKey: PublicKey, assetStates: List>>): PublicKey + = Clauses.ConserveAmount

().generateExit(tx, amountIssued, changeKey, assetStates, + deriveState = { state, amount, owner -> state.copy(data = state.data.move(amount, owner)) }, + generateExitCommand = { amount -> Commands.Exit

(amount) } + ) + /** * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */ @@ -515,7 +520,7 @@ class Obligation

: ClauseVerifier() { lifecycle: Lifecycle, notary: Party) { val states = statesAndRefs.map { it.state.data } - val issuanceDef = getTemplateOrThrow(states) + val issuanceDef = getTermsOrThrow(states) val existingLifecycle = when (lifecycle) { Lifecycle.DEFAULTED -> Lifecycle.NORMAL Lifecycle.NORMAL -> Lifecycle.DEFAULTED @@ -580,7 +585,7 @@ class Obligation

: ClauseVerifier() { tx.addInputState(ref) val assetState = ref.state.data - val amount: Amount

= Amount(assetState.amount.quantity, assetState.amount.token.product) + val amount = Amount(assetState.amount.quantity, assetState.amount.token.product) if (obligationRemaining >= amount) { tx.addOutputState(assetState.move(assetState.amount, obligationOwner), notary) obligationRemaining -= amount @@ -612,7 +617,7 @@ class Obligation

: ClauseVerifier() { states.map { it.issuanceDef }.distinct().single() /** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */ - private fun getTemplateOrThrow(states: Iterable>): Terms

= + private fun getTermsOrThrow(states: Iterable>) = states.map { it.template }.distinct().single() } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt new file mode 100644 index 0000000000..6e34b01ba4 --- /dev/null +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt @@ -0,0 +1,78 @@ +package com.r3corda.contracts.asset + +import com.r3corda.contracts.clause.AbstractConserveAmount +import com.r3corda.core.contracts.* +import com.r3corda.core.contracts.clauses.* +import com.r3corda.core.crypto.* +import java.security.PublicKey +import java.util.* + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Generic contract for assets on a ledger +// + +/** + * An asset transaction may split and merge assets 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 assets can be withdrawn from the ledger easily: if you receive some asset + * via this contract, you always know where to go in order to extract it from the R3 ledger, no matter how many hands + * it has passed through in the intervening time. + * + * At the same time, other contracts that just want assets and don't care much who is currently holding it can ignore + * the issuer/depositRefs and just examine the amount fields. + */ +abstract class OnLedgerAsset> : ClauseVerifier() { + abstract val conserveClause: AbstractConserveAmount + + /** + * Generate an transaction exiting assets from the ledger. + * + * @param tx transaction builder to add states and commands to. + * @param amountIssued the amount to be exited, represented as a quantity of issued currency. + * @param changeKey the key to send any change to. This needs to be explicitly stated as the input states are not + * necessarily owned by us. + * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is + * the responsibility of the caller to check that they do not exit funds held by others. + * @return the public key of the assets issuer, who must sign the transaction for it to be valid. + */ + fun generateExit(tx: TransactionBuilder, amountIssued: Amount>, + changeKey: PublicKey, assetStates: List>): PublicKey + = conserveClause.generateExit(tx, amountIssued, changeKey, assetStates, + deriveState = { state, amount, owner -> deriveState(state, amount, owner) }, + generateExitCommand = { amount -> generateExitCommand(amount) } + ) + + + /** + * Generate a transaction that consumes one or more of the given input states to move assets to the given pubkey. + * Note that the wallet list is not updated: it's up to you to do that. + * + * @param onlyFromParties if non-null, the wallet will be filtered to only include asset states issued by the set + * of given parties. This can be useful if the party you're trying to pay has expectations + * about which type of asset claims they are willing to accept. + */ + @Throws(InsufficientBalanceException::class) + fun generateSpend(tx: TransactionBuilder, + amount: Amount, + to: PublicKey, + assetsStates: List>, + onlyFromParties: Set? = null): List + = conserveClause.generateSpend(tx, amount, to, assetsStates, onlyFromParties, + deriveState = { state, amount, owner -> deriveState(state, amount, owner) }, + generateMoveCommand = { generateMoveCommand() }) + + abstract fun generateExitCommand(amount: Amount>): FungibleAsset.Commands.Exit + abstract fun generateIssueCommand(): FungibleAsset.Commands.Issue + abstract fun generateMoveCommand(): FungibleAsset.Commands.Move + + /** + * Derive a new transaction state based on the given example, with amount and owner modified. This allows concrete + * implementations to have fields in their state which we don't know about here, and we simply leave them untouched + * when sending out "change" from spending/exiting. + */ + abstract fun deriveState(txState: TransactionState, amount: Amount>, owner: PublicKey): TransactionState +} diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt index 82b17a1119..04d553bb88 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt @@ -1,12 +1,15 @@ package com.r3corda.contracts.clause import com.r3corda.contracts.asset.FungibleAsset +import com.r3corda.contracts.asset.InsufficientBalanceException import com.r3corda.contracts.asset.sumFungibleOrNull import com.r3corda.contracts.asset.sumFungibleOrZero import com.r3corda.core.contracts.* import com.r3corda.core.contracts.clauses.GroupClause import com.r3corda.core.contracts.clauses.MatchBehaviour +import com.r3corda.core.crypto.Party import java.security.PublicKey +import java.util.* /** * Standardised clause for checking input/output balances of fungible assets. Requires that a @@ -21,6 +24,152 @@ abstract class AbstractConserveAmount, T: Any> : GroupClause override val requiredCommands: Set> get() = emptySet() + /** + * Gather assets from the given list of states, sufficient to match or exceed the given amount. + * + * @param acceptableCoins list of states to use as inputs. + * @param amount the amount to gather states up to. + * @throws InsufficientBalanceException if there isn't enough value in the states to cover the requested amount. + */ + @Throws(InsufficientBalanceException::class) + private fun gatherCoins(acceptableCoins: Collection>, + amount: Amount): Pair>, Amount> { + val gathered = arrayListOf>() + var gatheredAmount = Amount(0, amount.token) + for (c in acceptableCoins) { + if (gatheredAmount >= amount) break + gathered.add(c) + gatheredAmount += Amount(c.state.data.amount.quantity, amount.token) + } + + if (gatheredAmount < amount) + throw InsufficientBalanceException(amount - gatheredAmount) + + return Pair(gathered, gatheredAmount) + } + + /** + * Generate an transaction exiting fungible assets from the ledger. + * + * @param tx transaction builder to add states and commands to. + * @param amountIssued the amount to be exited, represented as a quantity of issued currency. + * @param changeKey the key to send any change to. This needs to be explicitly stated as the input states are not + * necessarily owned by us. + * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is + * the responsibility of the caller to check that they do not exit funds held by others. + * @return the public key of the assets issuer, who must sign the transaction for it to be valid. + */ + fun generateExit(tx: TransactionBuilder, amountIssued: Amount>, + changeKey: PublicKey, assetStates: List>, + deriveState: (TransactionState, Amount>, PublicKey) -> TransactionState, + generateExitCommand: (Amount>) -> CommandData): PublicKey { + val currency = amountIssued.token.product + val amount = Amount(amountIssued.quantity, currency) + var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token } + val notary = acceptableCoins.firstOrNull()?.state?.notary + // TODO: We should be prepared to produce multiple transactions exiting inputs from + // different notaries, or at least group states by notary and take the set with the + // highest total value + acceptableCoins = acceptableCoins.filter { it.state.notary == notary } + + val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, Amount(amount.quantity, currency)) + val takeChangeFrom = gathered.lastOrNull() + val change = if (takeChangeFrom != null && gatheredAmount > amount) { + Amount>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef) + } else { + null + } + + val outputs = if (change != null) { + // Add a change output and adjust the last output downwards. + listOf(deriveState(gathered.last().state, change, changeKey)) + } else emptyList() + + for (state in gathered) tx.addInputState(state) + for (state in outputs) tx.addOutputState(state) + tx.addCommand(generateExitCommand(amountIssued), amountIssued.token.issuer.party.owningKey) + return amountIssued.token.issuer.party.owningKey + } + + /** + * Generate a transaction that consumes one or more of the given input states to move assets to the given pubkey. + * Note that the wallet list is not updated: it's up to you to do that. + * + * @param onlyFromParties if non-null, the wallet will be filtered to only include asset states issued by the set + * of given parties. This can be useful if the party you're trying to pay has expectations + * about which type of asset claims they are willing to accept. + */ + @Throws(InsufficientBalanceException::class) + fun generateSpend(tx: TransactionBuilder, + amount: Amount, + to: PublicKey, + assetsStates: List>, + onlyFromParties: Set? = null, + deriveState: (TransactionState, Amount>, PublicKey) -> TransactionState, + generateMoveCommand: () -> CommandData): List { + // Discussion + // + // This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline. + // + // First we must select a set of asset states (which for convenience we will call 'coins' here, as in bitcoinj). + // The input states can be considered our "wallet", and may consist of different products, and with different + // issuers and deposits. + // + // Coin selection is a complex problem all by itself and many different approaches can be used. It is easily + // possible for different actors to use different algorithms and approaches that, for example, compete on + // privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of + // obfuscation and so on. + // + // Having selected input states of the correct asset, 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 assets from different deposits. The point of this design + // is to ensure that ledger entries are immutable and globally identifiable. + // + // Finally, we add the states to the provided partial transaction. + + val currency = amount.token + val acceptableCoins = run { + val ofCurrency = assetsStates.filter { it.state.data.amount.token.product == currency } + if (onlyFromParties != null) + ofCurrency.filter { it.state.data.deposit.party in onlyFromParties } + else + ofCurrency + } + + val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount) + val takeChangeFrom = gathered.firstOrNull() + val change = if (takeChangeFrom != null && gatheredAmount > amount) { + Amount>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef) + } else { + null + } + val keysUsed = gathered.map { it.state.data.owner }.toSet() + + val states = gathered.groupBy { it.state.data.deposit }.map { + val coins = it.value + val totalAmount = coins.map { it.state.data.amount }.sumOrThrow() + deriveState(coins.first().state, totalAmount, to) + } + + val outputs = if (change != null) { + // 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().state.data.owner + // Add a change output and adjust the last output downwards. + states.subList(0, states.lastIndex) + + states.last().let { deriveState(it, it.data.amount - change, it.data.owner) } + + deriveState(gathered.last().state, change, changeKey) + } else states + + for (state in gathered) tx.addInputState(state) + 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? + val keysList = keysUsed.toList() + tx.addCommand(generateMoveCommand(), keysList) + return keysList + } + override fun verify(tx: TransactionForContract, inputs: List, outputs: List,