From 0d78df33f8bcdfe0a13157f0f97abd440b0bbe8f Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Tue, 12 Jul 2016 10:51:31 +0100 Subject: [PATCH] Add standard clauses --- .../com/r3corda/contracts/asset/Cash.kt | 2 + .../r3corda/contracts/asset/FungibleAsset.kt | 2 + .../clause/AbstractConserveAmount.kt | 49 ++++++++++++++++++ .../r3corda/contracts/clause/AbstractIssue.kt | 51 +++++++++++++++++++ .../contracts/clause/NoZeroSizedOutputs.kt | 30 +++++++++++ 5 files changed, 134 insertions(+) create mode 100644 experimental/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt create mode 100644 experimental/src/main/kotlin/com/r3corda/contracts/clause/AbstractIssue.kt create mode 100644 experimental/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt 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 8753a6b265..2b034b7fa0 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt @@ -55,6 +55,8 @@ class Cash : FungibleAsset() { get() = Amount(amount.quantity, amount.token.product) override val deposit: PartyAndReference get() = amount.token.issuer + override val exitKeys: Collection + get() = setOf(deposit.party.owningKey) override val contract = CASH_PROGRAM_ID override val issuanceDef: Issued get() = amount.token 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 a50d35b14e..851aecf93b 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt @@ -31,6 +31,8 @@ abstract class FungibleAsset : Contract { /** Where the underlying currency backing this ledger entry can be found (propagated) */ val deposit: PartyAndReference val amount: Amount> + /** There must be an ExitCommand signed by these keys to destroy the amount */ + val exitKeys: Collection /** There must be a MoveCommand signed by this key to claim the amount */ override val owner: PublicKey } diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt b/experimental/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt new file mode 100644 index 0000000000..3b4cf6ee06 --- /dev/null +++ b/experimental/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt @@ -0,0 +1,49 @@ +package com.r3corda.contracts.clause + +import com.r3corda.contracts.asset.FungibleAsset +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 java.security.PublicKey + +/** + * Standardised clause for checking input/output balances of fungible assets. Requires that a + * Move command is provided, and errors if absent. Must be the last clause under a grouping clause; + * errors on no-match, ends on match. + */ +abstract class AbstractConserveAmount, T: Any> : GroupClause> { + override val ifMatched: MatchBehaviour + get() = MatchBehaviour.END + override val ifNotMatched: MatchBehaviour + get() = MatchBehaviour.ERROR + override val requiredCommands: Set> + get() = emptySet() + + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: Collection>, + token: Issued): Set { + val inputAmount: Amount> = inputs.sumFungibleOrNull() ?: throw IllegalArgumentException("there is at least one asset input for group ${token}") + val deposit = token.issuer + val outputAmount: Amount> = outputs.sumFungibleOrZero(token) + + // If we want to remove assets from the ledger, that must be signed for by the issuer. + // A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero. + val exitKeys: Set = inputs.flatMap { it.exitKeys }.toSet() + val exitCommand = tx.commands.select>(parties = null, signers = exitKeys).filter {it.value.amount.token == token}.singleOrNull() + val amountExitingLedger: Amount> = exitCommand?.value?.amount ?: Amount(0, token) + + requireThat { + "there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L } + "for reference ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by + (inputAmount == outputAmount + amountExitingLedger) + } + + return listOf(exitCommand?.value, verifyMoveCommand(inputs, tx)) + .filter { it != null } + .requireNoNulls().toSet() + } +} diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/clause/AbstractIssue.kt b/experimental/src/main/kotlin/com/r3corda/contracts/clause/AbstractIssue.kt new file mode 100644 index 0000000000..ee52d72149 --- /dev/null +++ b/experimental/src/main/kotlin/com/r3corda/contracts/clause/AbstractIssue.kt @@ -0,0 +1,51 @@ +package com.r3corda.contracts.clause + +import com.r3corda.core.contracts.* +import com.r3corda.core.contracts.clauses.GroupClause +import com.r3corda.core.contracts.clauses.MatchBehaviour + +/** + * Standard issue clause for contracts that issue fungible assets. + */ +abstract class AbstractIssue( + val sum: List.() -> Amount>, + val sumOrZero: List.(token: Issued) -> Amount> +) : GroupClause> { + override val ifMatched: MatchBehaviour + get() = MatchBehaviour.END + override val ifNotMatched: MatchBehaviour + get() = MatchBehaviour.CONTINUE + + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: Collection>, + token: Issued): Set { + // TODO: Take in matched commands as a parameter + val issueCommand = commands.requireSingleCommand() + + // If we have an issue command, perform special processing: the group is allowed to have no inputs, + // and the output states must have a deposit reference owned by the signer. + // + // Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must + // sum to more than the inputs. An issuance of zero size is not allowed. + // + // Note that this means literally anyone with access to the network can issue asset claims of arbitrary + // amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some + // external mechanism (such as locally defined rules on which parties are trustworthy). + + // The grouping already ensures that all outputs have the same deposit reference and token. + val issuer = token.issuer.party + val inputAmount = inputs.sumOrZero(token) + val outputAmount = outputs.sum() + requireThat { + "the issue command has a nonce" by (issueCommand.value.nonce != 0L) + // TODO: This doesn't work with the trader demo, so use the underlying key instead + // "output states are issued by a command signer" by (issuer in issueCommand.signingParties) + "output states are issued by a command signer" by (issuer.owningKey in issueCommand.signers) + "output values sum to more than the inputs" by (outputAmount > inputAmount) + } + + return setOf(issueCommand.value) + } +} diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt b/experimental/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt new file mode 100644 index 0000000000..483e0736ab --- /dev/null +++ b/experimental/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt @@ -0,0 +1,30 @@ +package com.r3corda.contracts.clause + +import com.r3corda.contracts.asset.FungibleAsset +import com.r3corda.core.contracts.* +import com.r3corda.core.contracts.clauses.GroupClause +import com.r3corda.core.contracts.clauses.MatchBehaviour + +/** + * Clause for fungible asset contracts, which enforces that no output state should have + * a balance of zero. + */ +open class NoZeroSizedOutputs, T: Any> : GroupClause> { + override val ifMatched: MatchBehaviour + get() = MatchBehaviour.CONTINUE + override val ifNotMatched: MatchBehaviour + get() = MatchBehaviour.ERROR + override val requiredCommands: Set> + get() = emptySet() + + override fun verify(tx: TransactionForContract, + inputs: List, + outputs: List, + commands: Collection>, + token: Issued): Set { + requireThat { + "there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L } + } + return emptySet() + } +}