From f72e223f3b4c638ca1b85fb7054ba0568fd5d307 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 30 Jun 2016 10:34:06 +0100 Subject: [PATCH 1/2] Add Commodity class --- .../com/r3corda/core/contracts/FinanceTypes.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt b/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt index e00182c232..a437b10cdd 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt @@ -420,3 +420,16 @@ enum class NetType { */ PAYMENT } + +data class Commodity(val symbol: String, + val displayName: String, + val commodityCode: String = symbol, + val defaultFractionDigits: Int = 0) { + companion object { + private val registry = mapOf( + Pair("FCOJ", Commodity("FCOJ", "Frozen concentrated orange juice")) + ) + fun getInstance(symbol: String): Commodity? + = registry[symbol] + } +} \ No newline at end of file From e1d1aed541a90d037df074acc05985eb032997bd Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 30 Jun 2016 11:05:25 +0100 Subject: [PATCH 2/2] Add commodity contract and test obligations can be settled for it --- .../contracts/asset/CommodityContract.kt | 283 ++++++++++++++++++ .../r3corda/contracts/asset/FungibleAsset.kt | 2 +- .../contracts/asset/ObligationTests.kt | 49 ++- .../r3corda/core/contracts/ContractsDSL.kt | 7 +- 4 files changed, 327 insertions(+), 14 deletions(-) create mode 100644 contracts/src/main/kotlin/com/r3corda/contracts/asset/CommodityContract.kt diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/CommodityContract.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/CommodityContract.kt new file mode 100644 index 0000000000..9fb8a7e542 --- /dev/null +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/CommodityContract.kt @@ -0,0 +1,283 @@ +package com.r3corda.contracts.asset + +import com.r3corda.contracts.clause.AbstractConserveAmount +import com.r3corda.contracts.clause.AbstractIssue +import com.r3corda.contracts.clause.NoZeroSizedOutputs +import com.r3corda.core.contracts.* +import com.r3corda.core.contracts.clauses.* +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.* + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Commodity +// + +// Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode. +val COMMODITY_PROGRAM_ID = CommodityContract() + //SecureHash.sha256("commodity") + +/** + * A commodity contract represents an amount of some commodity, tracked on a distributed ledger. The design of this + * contract is intentionally similar to the [Cash] contract, and the same commands (issue, move, exit) apply, the + * differences are in representation of the underlying commodity. Issuer in this context means the party who has the + * commodity, or is otherwise responsible for delivering the commodity on demand, and the deposit reference is use for + * 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() { + /** + * TODO: + * 1) hash should be of the contents, not the URI + * 2) allow the content to be specified at time of instance creation? + * + * Motivation: it's the difference between a state object referencing a programRef, which references a + * legalContractReference and a state object which directly references both. The latter allows the legal wording + * to evolve without requiring code changes. But creates a risk that users create objects governed by a program + * that is inconsistent with the legal contract + */ + override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html") + + /** + * The clauses for this contract are essentially: + * + * 1. Group all commodity input and output states in a transaction by issued commodity, and then for each group: + * a. Check there are no zero sized output states in the group, and throw an error if so. + * b. Check for an issuance command, and do standard issuance checks if so, THEN STOP. Otherwise: + * c. Check for a move command (required) and an optional exit command, and that input and output totals are correctly + * conserved (output = input - exit) + */ + interface Clauses { + /** + * Grouping clause to extract input and output states into matched groups and then run a set of clauses over + * each group. + */ + class Group : GroupClauseVerifier>() { + /** + * 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 + /** + * 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 + // Subclauses to run on each group + override val clauses: List>> + get() = listOf( + NoZeroSizedOutputs(), + Issue(), + ConserveAmount() + ) + + /** + * Group commodity states by issuance definition (issuer and underlying commodity). + */ + override fun extractGroups(tx: TransactionForContract): List>> + = 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) + } + + /** + * Standard clause for conserving the amount from input to output. + */ + class ConserveAmount : AbstractConserveAmount() + } + + /** A state representing a commodity claim against some party */ + data class State( + override val amount: Amount>, + + /** There must be a MoveCommand signed by this key to claim the amount */ + override val owner: PublicKey + ) : FungibleAsset { + 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 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 fun move(newAmount: Amount>, newOwner: PublicKey): FungibleAsset + = copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner) + + override fun toString() = "Commodity($amount at $deposit owned by ${owner.toStringShort()})" + + override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) + } + + // Just for grouping + interface Commands : FungibleAsset.Commands { + /** + * A command stating that money has been moved, optionally to fulfil another contract. + * + * @param contractHash the contract this move is for the attention of. Only that contract's verify function + * should take the moved states into account when considering whether it is valid. Typically this will be + * null. + */ + data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands + + /** + * Allows new commodity states to be issued into existence: the nonce ("number used once") ensures the transaction + * has a unique ID even when there are no inputs. + */ + data class Issue(override val nonce: Long = newSecureRandom().nextLong()) : FungibleAsset.Commands.Issue, Commands + + /** + * A command stating that money has been withdrawn from the shared ledger and is now accounted for + * in some other way. + */ + data class Exit(override val amount: Amount>) : Commands, FungibleAsset.Commands.Exit + } + override val clauses: List + get() = listOf(Clauses.Group()) + override fun extractCommands(tx: TransactionForContract): List> + = tx.commands.select() + + /** + * Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey. + */ + fun generateIssue(tx: TransactionBuilder, tokenDef: Issued, pennies: Long, owner: PublicKey, notary: Party) + = generateIssue(tx, Amount(pennies, tokenDef), owner, notary) + + /** + * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. + */ + fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: PublicKey, notary: Party) { + check(tx.inputStates().isEmpty()) + check(tx.outputStates().map { it.data }.sumFungibleOrNull() == null) + val at = amount.token.issuer + tx.addOutputState(TransactionState(CommodityContract.State(amount, owner), notary)) + tx.addCommand(Commands.Issue(), 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 + } +} + +/** + * 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). + */ +fun Iterable.sumCommodities() = filterIsInstance().map { it.amount }.sumOrThrow() + +/** Sums the cash states in the list, returning null if there are none. */ +fun Iterable.sumCommoditiesOrNull() = filterIsInstance().map { it.amount }.sumOrNull() + +/** Sums the cash states in the list, returning zero of the given currency if there are none. */ +fun Iterable.sumCommoditiesOrZero(currency: Issued) = filterIsInstance().map { it.amount }.sumOrZero>(currency) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt index 9c17a0f6f9..67d7e7e910 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt @@ -4,7 +4,7 @@ import com.r3corda.core.contracts.* import java.security.PublicKey import java.util.* -class InsufficientBalanceException(val amountMissing: Amount) : Exception() +class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception() /** * Interface for contract states representing assets which are fungible, countable and issued by a diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt index 33db57d779..e299006b46 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt @@ -2,6 +2,7 @@ package com.r3corda.contracts.asset import com.r3corda.contracts.asset.Obligation.Lifecycle import com.r3corda.core.contracts.* +import com.r3corda.core.crypto.NullPublicKey import com.r3corda.core.crypto.SecureHash import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.testing.* @@ -36,7 +37,7 @@ class ObligationTests { ) val outState = inState.copy(beneficiary = DUMMY_PUBKEY_2) - private fun obligationTestRoots( + private fun cashObligationTestRoots( group: LedgerDSL ) = group.apply { unverifiedTransaction { @@ -331,7 +332,7 @@ class ObligationTests { fun `close-out netting`() { // Try netting out two obligations ledger { - obligationTestRoots(this) + cashObligationTestRoots(this) transaction("Issuance") { input("Alice's $1,000,000 obligation to Bob") input("Bob's $1,000,000 obligation to Alice") @@ -346,7 +347,7 @@ class ObligationTests { // Try netting out two obligations, with the third uninvolved obligation left // as-is ledger { - obligationTestRoots(this) + cashObligationTestRoots(this) transaction("Issuance") { input("Alice's $1,000,000 obligation to Bob") input("Bob's $1,000,000 obligation to Alice") @@ -361,7 +362,7 @@ class ObligationTests { // Try having outputs mis-match the inputs ledger { - obligationTestRoots(this) + cashObligationTestRoots(this) transaction("Issuance") { input("Alice's $1,000,000 obligation to Bob") input("Bob's $1,000,000 obligation to Alice") @@ -374,7 +375,7 @@ class ObligationTests { // Have the wrong signature on the transaction ledger { - obligationTestRoots(this) + cashObligationTestRoots(this) transaction("Issuance") { input("Alice's $1,000,000 obligation to Bob") input("Bob's $1,000,000 obligation to Alice") @@ -389,7 +390,7 @@ class ObligationTests { fun `payment netting`() { // Try netting out two obligations ledger { - obligationTestRoots(this) + cashObligationTestRoots(this) transaction("Issuance") { input("Alice's $1,000,000 obligation to Bob") input("Bob's $1,000,000 obligation to Alice") @@ -403,7 +404,7 @@ class ObligationTests { // Try netting out two obligations, but only provide one signature. Unlike close-out netting, we need both // signatures for payment netting ledger { - obligationTestRoots(this) + cashObligationTestRoots(this) transaction("Issuance") { input("Alice's $1,000,000 obligation to Bob") input("Bob's $1,000,000 obligation to Alice") @@ -415,7 +416,7 @@ class ObligationTests { // Multilateral netting, A -> B -> C which can net down to A -> C ledger { - obligationTestRoots(this) + cashObligationTestRoots(this) transaction("Issuance") { input("Bob's $1,000,000 obligation to Alice") input("MegaCorp's $1,000,000 obligation to Bob") @@ -429,7 +430,7 @@ class ObligationTests { // Multilateral netting without the key of the receiving party ledger { - obligationTestRoots(this) + cashObligationTestRoots(this) transaction("Issuance") { input("Bob's $1,000,000 obligation to Alice") input("MegaCorp's $1,000,000 obligation to Bob") @@ -442,10 +443,10 @@ class ObligationTests { } @Test - fun `settlement`() { + fun `cash settlement`() { // Try settling an obligation ledger { - obligationTestRoots(this) + cashObligationTestRoots(this) transaction("Settlement") { input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") @@ -484,11 +485,35 @@ class ObligationTests { } } + @Test + fun `commodity settlement`() { + val defaultFcoj = FCOJ `issued by` defaultIssuer + val oneUnitFcoj = Amount(1, defaultFcoj) + val obligationDef = Obligation.Terms(nonEmptySetOf(CommodityContract().legalContractReference), nonEmptySetOf(defaultFcoj), TEST_TX_TIME) + val oneUnitFcojObligation = Obligation.State(Obligation.Lifecycle.NORMAL, ALICE, + obligationDef, oneUnitFcoj.quantity, NullPublicKey) + // Try settling a simple commodity obligation + ledger { + unverifiedTransaction { + output("Alice's 1 FCOJ obligation to Bob", oneUnitFcojObligation between Pair(ALICE, BOB_PUBKEY)) + output("Alice's 1 FCOJ", CommodityContract.State(oneUnitFcoj, ALICE_PUBKEY)) + } + transaction("Settlement") { + input("Alice's 1 FCOJ obligation to Bob") + input("Alice's 1 FCOJ") + output("Bob's 1 FCOJ") { CommodityContract.State(oneUnitFcoj, BOB_PUBKEY) } + command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.issuanceDef)) } + command(ALICE_PUBKEY) { CommodityContract.Commands.Move(Obligation().legalContractReference) } + verifies() + } + } + } + @Test fun `payment default`() { // Try defaulting an obligation without a timestamp ledger { - obligationTestRoots(this) + cashObligationTestRoots(this) transaction("Settlement") { input("Alice's $1,000,000 obligation to Bob") output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)).copy(lifecycle = Lifecycle.DEFAULTED) } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt index 28723054b7..f317961bc6 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt @@ -19,27 +19,32 @@ import java.util.* //// Currencies /////////////////////////////////////////////////////////////////////////////////////////////////////// fun currency(code: String) = Currency.getInstance(code)!! +fun commodity(code: String) = Commodity.getInstance(code)!! @JvmField val USD = currency("USD") @JvmField val GBP = currency("GBP") @JvmField val CHF = currency("CHF") +@JvmField val FCOJ = commodity("FCOJ") fun DOLLARS(amount: Int): Amount = Amount(amount.toLong() * 100, USD) fun DOLLARS(amount: Double): Amount = Amount((amount * 100).toLong(), USD) fun POUNDS(amount: Int): Amount = Amount(amount.toLong() * 100, GBP) fun SWISS_FRANCS(amount: Int): Amount = Amount(amount.toLong() * 100, CHF) +fun FCOJ(amount: Int): Amount = Amount(amount.toLong() * 100, FCOJ) val Int.DOLLARS: Amount get() = DOLLARS(this) val Double.DOLLARS: Amount get() = DOLLARS(this) val Int.POUNDS: Amount get() = POUNDS(this) val Int.SWISS_FRANCS: Amount get() = SWISS_FRANCS(this) +val Int.FCOJ: Amount get() = FCOJ(this) infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) +infix fun Commodity.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) infix fun Amount.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this) +infix fun Commodity.issuedBy(deposit: PartyAndReference) = Issued(deposit, this) infix fun Amount.issuedBy(deposit: PartyAndReference) = Amount(quantity, token.issuedBy(deposit)) - //// Requirements ///////////////////////////////////////////////////////////////////////////////////////////////////// class Requirements {