From cba3aab96eba43d5775ed16911b63ac5fdbfa304 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Tue, 12 Jul 2016 11:49:00 +0100 Subject: [PATCH] Rebuild asset contracts using clauses --- .../com/r3corda/contracts/asset/Cash.kt | 46 +- .../r3corda/contracts/asset/FungibleAsset.kt | 129 +--- .../contracts/asset/FungibleAssetState.kt | 14 - .../com/r3corda/contracts/asset/Obligation.kt | 647 +++++++----------- .../clause/AbstractConserveAmount.kt | 2 +- .../r3corda/contracts/clause/AbstractIssue.kt | 7 + .../com/r3corda/contracts/clause/Net.kt | 97 +++ .../contracts/clause/NoZeroSizedOutputs.kt | 2 +- .../com/r3corda/contracts/asset/CashTests.kt | 10 +- .../contracts/asset/ObligationTests.kt | 159 +++-- .../com/r3corda/core/contracts/Structures.kt | 18 +- .../core/contracts/TransactionVerification.kt | 3 - .../contracts/clauses/GroupClauseVerifier.kt | 4 +- 13 files changed, 534 insertions(+), 604 deletions(-) delete mode 100644 contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAssetState.kt create mode 100644 contracts/src/main/kotlin/com/r3corda/contracts/clause/Net.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 2b034b7fa0..5ee3216674 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt @@ -1,6 +1,10 @@ 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.* import com.r3corda.core.node.services.Wallet import com.r3corda.core.utilities.Emoji @@ -28,7 +32,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 : FungibleAsset() { +class Cash : ClauseVerifier() { /** * TODO: * 1) hash should be of the contents, not the URI @@ -40,6 +44,34 @@ class Cash : FungibleAsset() { * 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 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( + NoZeroSizedOutputs(), + Issue(), + ConserveAmount()) + + override fun extractGroups(tx: TransactionForContract): List>> + = tx.groupStates> { it.issuanceDef } + } + + class Issue : AbstractIssue({ sumCash() }, { token -> sumCashOrZero(token) }) { + override val requiredCommands: Set> + get() = setOf(Commands.Issue::class.java) + } + + class ConserveAmount : AbstractConserveAmount() + } /** A state representing a cash claim against some party */ data class State( @@ -47,12 +79,10 @@ class Cash : FungibleAsset() { /** There must be a MoveCommand signed by this key to claim the amount */ override val owner: PublicKey - ) : FungibleAsset.State { + ) : FungibleAsset { constructor(deposit: PartyAndReference, amount: Amount, owner: PublicKey) - : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) + : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) - override val productAmount: Amount - get() = Amount(amount.quantity, amount.token.product) override val deposit: PartyAndReference get() = amount.token.issuer override val exitKeys: Collection @@ -63,8 +93,8 @@ class Cash : FungibleAsset() { override val participants: List get() = listOf(owner) - override fun move(newAmount: Amount, newOwner: PublicKey): FungibleAsset.State - = copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner) + override fun move(newAmount: Amount>, newOwner: PublicKey): FungibleAsset + = copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner) override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})" @@ -72,7 +102,7 @@ class Cash : FungibleAsset() { } // Just for grouping - interface Commands : CommandData { + interface Commands : FungibleAsset.Commands { /** * A command stating that money has been moved, optionally to fulfil another contract. * 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 851aecf93b..9c17a0f6f9 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt @@ -1,41 +1,38 @@ package com.r3corda.contracts.asset import com.r3corda.core.contracts.* -import com.r3corda.core.crypto.Party import java.security.PublicKey import java.util.* -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Cash-like -// - class InsufficientBalanceException(val amountMissing: Amount) : Exception() /** - * Superclass for contracts representing assets which are fungible, countable and issued by a specific party. States - * contain assets which are equivalent (such as cash of the same currency), so records of their existence can - * be merged or split as needed where the issuer is the same. For instance, dollars issued by the Fed are fungible and - * countable (in cents), barrels of West Texas crude are fungible and countable (oil from two small containers - * can be poured into one large container), shares of the same class in a specific company are fungible and - * countable, and so on. + * Interface for contract states representing assets which are fungible, countable and issued by a + * specific party. States contain assets which are equivalent (such as cash of the same currency), + * so records of their existence can be merged or split as needed where the issuer is the same. For + * instance, dollars issued by the Fed are fungible and countable (in cents), barrels of West Texas + * crude are fungible and countable (oil from two small containers can be poured into one large + * container), shares of the same class in a specific company are fungible and countable, and so on. * - * See [Cash] for an example subclass that implements currency. + * See [Cash] for an example contract that implements currency using state objects that implement + * this interface. * * @param T a type that represents the asset in question. This should describe the basic type of the asset * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.). */ -abstract class FungibleAsset : Contract { - /** A state representing a cash claim against some party */ - interface State : FungibleAssetState> { - /** 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 - } +interface FungibleAsset : OwnableState { + /** + * Where the underlying asset backing this ledger entry can be found. The reference + * is only intended for use by the issuer, and is not intended to be meaningful to others. + */ + val deposit: PartyAndReference + val issuanceDef: Issued + 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 + fun move(newAmount: Amount>, newOwner: PublicKey): FungibleAsset // Just for grouping interface Commands : CommandData { @@ -53,92 +50,14 @@ abstract class FungibleAsset : Contract { */ interface Exit : Commands { val amount: Amount> } } - - /** This is the function EVERYONE runs */ - override fun verify(tx: TransactionForContract) { - // Each group is a set of input/output states with distinct issuance definitions. These assets are not fungible - // and must be kept separated for bookkeeping purposes. - val groups = tx.groupStates() { it: FungibleAsset.State -> it.issuanceDef } - - for ((inputs, outputs, token) in groups) { - // Either inputs or outputs could be empty. - val deposit = token.issuer - val issuer = deposit.party - - requireThat { - "there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L } - } - - val issueCommand = tx.commands.select().firstOrNull() - if (issueCommand != null) { - verifyIssueCommand(inputs, outputs, tx, issueCommand, token, issuer) - } else { - val inputAmount = inputs.sumFungibleOrNull() ?: throw IllegalArgumentException("there is at least one asset input for this group") - val outputAmount = 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 exitCommand = tx.commands.select>(party = issuer).singleOrNull() - val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, token) - - requireThat { - "there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L } - "for deposit ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by - (inputAmount == outputAmount + amountExitingLedger) - } - - verifyMoveCommand(inputs, tx) - } - } - } - - private fun verifyIssueCommand(inputs: List>, - outputs: List>, - tx: TransactionForContract, - issueCommand: AuthenticatedObject, - token: Issued, - issuer: Party) { - // 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 ensures that all outputs have the same deposit reference and token. - val inputAmount = inputs.sumFungibleOrZero(token) - val outputAmount = outputs.sumFungible() - val assetCommands = tx.commands.select() - requireThat { - "the issue command has a nonce" by (issueCommand.value.nonce != 0L) - "output states are issued by a command signer" by (issuer in issueCommand.signingParties) - "output values sum to more than the inputs" by (outputAmount > inputAmount) - "there is only a single issue command" by (assetCommands.count() == 1) - } - } } // Small DSL extensions. -/** - * Sums the asset states in the list belonging to a single owner, throwing an exception - * if there are none, or if any of the asset states cannot be added together (i.e. are - * different tokens). - */ -fun Iterable.sumFungibleBy(owner: PublicKey) = filterIsInstance>().filter { it.owner == owner }.map { it.amount }.sumOrThrow() - -/** - * Sums the asset states in the list, throwing an exception if there are none, or if any of the asset - * states cannot be added together (i.e. are different tokens). - */ -fun Iterable.sumFungible() = filterIsInstance>().map { it.amount }.sumOrThrow() - /** Sums the asset states in the list, returning null if there are none. */ -fun Iterable.sumFungibleOrNull() = filterIsInstance>().map { it.amount }.sumOrNull() +fun Iterable.sumFungibleOrNull() = filterIsInstance>().map { it.amount }.sumOrNull() /** Sums the asset states in the list, returning zero of the given token if there are none. */ -fun Iterable.sumFungibleOrZero(token: Issued) = filterIsInstance>().map { it.amount }.sumOrZero(token) +fun Iterable.sumFungibleOrZero(token: Issued) = filterIsInstance>().map { it.amount }.sumOrZero(token) + diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAssetState.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAssetState.kt deleted file mode 100644 index 452ed85af5..0000000000 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAssetState.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.r3corda.contracts.asset - -import com.r3corda.core.contracts.Amount -import com.r3corda.core.contracts.OwnableState -import java.security.PublicKey - -/** - * Common elements of cash contract states. - */ -interface FungibleAssetState : OwnableState { - val issuanceDef: I - val productAmount: Amount - fun move(newAmount: Amount, newOwner: PublicKey): FungibleAssetState -} \ No newline at end of file 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 da45a0bb2a..b6e0fca243 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt @@ -1,8 +1,9 @@ package com.r3corda.contracts.asset import com.google.common.annotations.VisibleForTesting -import com.r3corda.contracts.asset.Obligation.Lifecycle.NORMAL +import com.r3corda.contracts.clause.* 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 @@ -29,7 +30,7 @@ val OBLIGATION_PROGRAM_ID = Obligation() * * @param P the product the obligation is for payment of. */ -class Obligation

: Contract { +class Obligation

: ClauseVerifier() { /** * TODO: @@ -42,6 +43,199 @@ class Obligation

: Contract { * 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

()), + Clauses.Group

()) + + interface Clauses { + /** + * 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 fun extractGroups(tx: TransactionForContract): List, Issued>>> + = tx.groupStates, Issued>> { it.issuanceDef } + } + + /** + * Generic issuance clause + */ + class Issue

: AbstractIssue, Terms

>({ -> sumObligations() }, { token: Issued> -> sumObligationsOrZero(token) }) { + override val requiredCommands: Set> + get() = setOf(Obligation.Commands.Issue::class.java) + } + + /** + * Generic move/exit clause for fungible assets + */ + class ConserveAmount

: AbstractConserveAmount, Terms

>() + + /** + * Clause for supporting netting of obligations. + */ + class Net

: NetClause

() + + /** + * 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 fun verify(tx: TransactionForContract, + inputs: List>, + outputs: List>, + commands: Collection>, + token: Issued>): Set { + val command = commands.requireSingleCommand() + Obligation

().verifySetLifecycleCommand(inputs, outputs, tx, command) + return setOf(command.value) + } + } + + /** + * Obligation-specific clause for settling an outstanding obligation by witnessing + * 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 fun verify(tx: TransactionForContract, + inputs: List>, + outputs: List>, + commands: Collection>, + token: Issued>): Set { + val command = commands.requireSingleCommand>() + val obligor = token.issuer.party + val template = token.product + val inputAmount: Amount>> = inputs.sumObligationsOrNull

() ?: throw IllegalArgumentException("there is at least one obligation input for this group") + val outputAmount: Amount>> = outputs.sumObligationsOrZero(token) + + // Sum up all asset state objects that are moving and fulfil our requirements + + // The fungible asset contract verification handles ensuring there's inputs enough to cover the output states, + // we only care about counting how much is output in this transaction. We then calculate the difference in + // settlement amounts between the transaction inputs and outputs, and the two must match. No elimination is + // done of amounts paid in by each beneficiary, as it's presumed the beneficiaries have enough sense to do that + // themselves. Therefore if someone actually signed the following transaction (using cash just for an example): + // + // Inputs: + // £1m cash owned by B + // £1m owed from A to B + // Outputs: + // £1m cash owned by B + // Commands: + // Settle (signed by A) + // Move (signed by B) + // + // That would pass this check. Ensuring they do not is best addressed in the transaction generation stage. + val assetStates = tx.outputs.filterIsInstance>() + val acceptableAssetStates = assetStates + // TODO: This filter is nonsense, because it just checks there is an asset contract loaded, we need to + // verify the asset contract is the asset contract we expect. + // Something like: + // attachments.mustHaveOneOf(key.acceptableAssetContract) + .filter { it.contract.legalContractReference in template.acceptableContracts } + // Restrict the states to those of the correct issuance definition (this normally + // covers issued product and obligor, but is opaque to us) + .filter { it.issuanceDef in template.acceptableIssuedProducts } + // Catch that there's nothing useful here, so we can dump out a useful error + requireThat { + "there are fungible asset state outputs" by (assetStates.size > 0) + "there are defined acceptable fungible asset states" by (acceptableAssetStates.size > 0) + } + + val amountReceivedByOwner = acceptableAssetStates.groupBy { it.owner } + // Note we really do want to search all commands, because we want move commands of other contracts, not just + // this one. + val moveCommands = tx.commands.select() + var totalPenniesSettled = 0L + val requiredSigners = inputs.map { it.deposit.party.owningKey }.toSet() + + for ((beneficiary, obligations) in inputs.groupBy { it.owner }) { + val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull

() + if (settled != null) { + val debt = obligations.sumObligationsOrZero(token) + require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" } + totalPenniesSettled += settled.quantity + } + } + + // Insist that we can be the only contract consuming inputs, to ensure no other contract can think it's being + // settled as well + requireThat { + "all move commands relate to this contract" by (moveCommands.map { it.value.contractHash } + .all { it == null || it == Obligation

().legalContractReference }) + // Settle commands exclude all other commands, so we don't need to check for contracts moving at the same + // time. + "amounts paid must match recipients to settle" by inputs.map { it.owner }.containsAll(amountReceivedByOwner.keys) + "signatures are present from all obligors" by command.signers.containsAll(requiredSigners) + "there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L } + "at obligor ${obligor.name} the obligations after settlement balance" by + (inputAmount == outputAmount + Amount(totalPenniesSettled, token)) + } + return setOf(command.value) + } + } + + /** + * Obligation-specific clause for verifying that all states are in + * normal lifecycle. In a group clause set, this must be run after + * any lifecycle change clause, which is the only clause that involve + * 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 fun verify(tx: TransactionForContract, commands: Collection>): Set + = verify( + tx.inputs.filterIsInstance>(), + tx.outputs.filterIsInstance>() + ) + + override fun verify(tx: TransactionForContract, + inputs: List>, + outputs: List>, + commands: Collection>, + token: Issued>): Set + = verify(inputs, outputs) + + fun verify(inputs: List>, + outputs: List>): Set { + requireThat { + "all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL } + "all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL } + } + return emptySet() + } + } + } /** * Represents where in its lifecycle a contract state is, which in turn controls the commands that can be applied @@ -58,42 +252,12 @@ class Obligation

: Contract { DEFAULTED } - /** - * Common interface for the state subsets used when determining nettability of two or more states. Exposes the - * underlying issued thing. - */ - interface NetState

{ - val template: StateTemplate

- } - - /** - * Subset of state, containing the elements which must match for two obligation transactions to be nettable. - * If two obligation state objects produce equal bilateral net states, they are considered safe to net directly. - * Bilateral states are used in close-out netting. - */ - data class BilateralNetState

( - val partyKeys: Set, - override val template: StateTemplate

- ) : NetState

- - /** - * Subset of state, containing the elements which must match for two or more obligation transactions to be candidates - * for netting (this does not include the checks to enforce that everyone's amounts received are the same at the end, - * which is handled under the verify() function). - * In comparison to [BilateralNetState], this doesn't include the parties' keys, as ensuring balances match on - * input and output is handled elsewhere. - * Used in cases where all parties (or their proxies) are signing, such as central clearing. - */ - data class MultilateralNetState

( - override val template: StateTemplate

- ) : NetState

- /** * Subset of state, containing the elements specified when issuing a new settlement contract. * * @param P the product the obligation is for payment of. */ - data class StateTemplate

( + data class Terms

( /** The hash of the asset contract we're willing to accept in payment for this debt. */ val acceptableContracts: NonEmptySet, /** The parties whose assets we are willing to accept in payment for this debt. */ @@ -107,21 +271,9 @@ class Obligation

: Contract { get() = acceptableIssuedProducts.map { it.product }.toSet().single() } - /** - * Subset of state, containing the elements specified when issuing a new settlement contract. - * TODO: This needs to be something common to contracts that we can be obliged to pay, and moved - * out into core accordingly. - * - * @param P the product the obligation is for payment of. - */ - data class IssuanceDefinition

( - val obligor: Party, - val template: StateTemplate

- ) - /** * A state representing the obligation of one party (obligor) to deliver a specified number of - * units of an underlying asset (described as issuanceDef.acceptableIssuedProducts) to the beneficiary + * units of an underlying asset (described as token.acceptableIssuedProducts) to the beneficiary * no later than the specified time. * * @param P the product the obligation is for payment of. @@ -130,32 +282,28 @@ class Obligation

: Contract { var lifecycle: Lifecycle = Lifecycle.NORMAL, /** Where the debt originates from (obligor) */ val obligor: Party, - val template: StateTemplate

, + val template: Terms

, val quantity: Long, /** The public key of the entity the contract pays to */ val beneficiary: PublicKey - ) : FungibleAssetState>, BilateralNettableState> { - val amount: Amount

- get() = Amount(quantity, template.product) - val aggregateState: IssuanceDefinition

- get() = issuanceDef - override val productAmount: Amount

- get() = amount + ) : FungibleAsset>, NettableState, MultilateralNetState

> { + override val amount: Amount>> + get() = Amount(quantity, issuanceDef) override val contract = OBLIGATION_PROGRAM_ID - val acceptableContracts: NonEmptySet - get() = template.acceptableContracts - val acceptableIssuanceDefinitions: NonEmptySet<*> - get() = template.acceptableIssuedProducts + override val deposit: PartyAndReference + get() = amount.token.issuer + override val exitKeys: Collection + get() = setOf(owner) val dueBefore: Instant get() = template.dueBefore - override val issuanceDef: IssuanceDefinition

- get() = IssuanceDefinition(obligor, template) + override val issuanceDef: Issued> + get() = Issued(obligor.ref(0), template) override val participants: List get() = listOf(obligor.owningKey, beneficiary) override val owner: PublicKey get() = beneficiary - override fun move(newAmount: Amount

, newOwner: PublicKey): State

+ override fun move(newAmount: Amount>>, newOwner: PublicKey): State

= copy(quantity = newAmount.quantity, beneficiary = newOwner) override fun toString() = when (lifecycle) { @@ -168,7 +316,7 @@ class Obligation

: Contract { check(lifecycle == Lifecycle.NORMAL) return BilateralNetState(setOf(obligor.owningKey, beneficiary), template) } - val multilateralNetState: MultilateralNetState

+ override val multilateralNetState: MultilateralNetState

get() { check(lifecycle == Lifecycle.NORMAL) return MultilateralNetState(template) @@ -188,21 +336,16 @@ class Obligation

: Contract { } } - override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(issuanceDef), copy(beneficiary = newOwner)) - } - - /** Interface for commands that apply to states grouped by issuance definition */ - interface IssuanceCommands

: CommandData { - val aggregateState: IssuanceDefinition

+ override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(beneficiary = newOwner)) } // Just for grouping - interface Commands : CommandData { + interface Commands : FungibleAsset.Commands { /** * Net two or more obligation states together in a close-out netting style. Limited to bilateral netting * as only the beneficiary (not the obligor) needs to sign. */ - data class Net(val type: NetType) : Commands + data class Net(val type: NetType) : Obligation.Commands /** * A command stating that a debt has been moved, optionally to fulfil another contract. @@ -211,30 +354,26 @@ class Obligation

: Contract { * should take the moved states into account when considering whether it is valid. Typically this will be * null. */ - data class Move

(override val aggregateState: IssuanceDefinition

, - override val contractHash: SecureHash? = null) : Commands, IssuanceCommands

, MoveCommand + data class Move(override val contractHash: SecureHash? = null) : Commands, FungibleAsset.Commands.Move /** * Allows new obligation 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 aggregateState: IssuanceDefinition

, - val nonce: Long = random63BitValue()) : Commands, IssuanceCommands

+ data class Issue(override val nonce: Long = random63BitValue()) : FungibleAsset.Commands.Issue, Commands /** * A command stating that the obligor is settling some or all of the amount owed by transferring a suitable * state object to the beneficiary. If this reduces the balance to zero, the state object is destroyed. * @see [MoveCommand]. */ - data class Settle

(override val aggregateState: IssuanceDefinition

, - val amount: Amount

) : Commands, IssuanceCommands

+ data class Settle

(val amount: Amount>>) : Commands /** * A command stating that the beneficiary is moving the contract into the defaulted state as it has not been settled * by the due date, or resetting a defaulted contract back to the issued state. */ - data class SetLifecycle

(override val aggregateState: IssuanceDefinition

, - val lifecycle: Lifecycle) : Commands, IssuanceCommands

{ + data class SetLifecycle(val lifecycle: Lifecycle) : Commands { val inverse: Lifecycle get() = when (lifecycle) { Lifecycle.NORMAL -> Lifecycle.DEFAULTED @@ -246,140 +385,20 @@ class Obligation

: Contract { * A command stating that the debt is being released by the beneficiary. Normally would indicate * either settlement outside of the ledger, or that the obligor is unable to pay. */ - data class Exit

(override val aggregateState: IssuanceDefinition

, - val amount: Amount

) : Commands, IssuanceCommands

+ data class Exit

(override val amount: Amount>>) : Commands, FungibleAsset.Commands.Exit> } - /** This is the function EVERYONE runs */ - override fun verify(tx: TransactionForContract) { - val commands = tx.commands.select() - - // Net commands are special, and cross issuance definitions, so handle them first - val netCommands = commands.select() - if (netCommands.isNotEmpty()) { - val netCommand = netCommands.single() - val groups = when (netCommand.value.type) { - NetType.CLOSE_OUT -> tx.groupStates { it: State

-> it.bilateralNetState } - NetType.PAYMENT -> tx.groupStates { it: State

-> it.multilateralNetState } - } - for ((inputs, outputs, key) in groups) { - verifyNetCommand(inputs, outputs, netCommand, key) - } - } else { - val commandGroups = tx.groupCommands, IssuanceDefinition

> { it.value.aggregateState } - // Each group is a set of input/output states with distinct issuance definitions. These types - // of settlement are not fungible and must be kept separated for bookkeeping purposes. - val groups = tx.groupStates() { it: State

-> it.aggregateState } - - for ((inputs, outputs, key) in groups) { - // Either inputs or outputs could be empty. - val obligor = key.obligor - - requireThat { - "there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L } - } - - verifyCommandGroup(tx, commandGroups[key] ?: emptyList(), inputs, outputs, obligor, key) - } - } - } - - private fun verifyCommandGroup(tx: TransactionForContract, - commands: List>>, - inputs: List>, - outputs: List>, - obligor: Party, - key: IssuanceDefinition

) { - // We've already pre-grouped by product amongst other fields, and verified above that every state specifies - // at least one acceptable issuance definition, so we can just use the first issuance definition to - // determine product - val issued = key.template.acceptableIssuedProducts.first() - - // Issue, default, net and settle commands are all single commands (there's only ever one of them, and - // they exclude all other commands). - val issueCommand = commands.select>().firstOrNull() - val setLifecycleCommand = commands.select>().firstOrNull() - val settleCommand = commands.select>().firstOrNull() - - if (commands.size != 1) { - // Only commands can be move/exit - require(commands.map { it.value }.all { it is Commands.Move || it is Commands.Exit }) - { "only move/exit commands can be present along with other obligation commands" } - } - - // Issue, default and net commands are special, and do not follow normal input/output summing rules, so - // deal with them first - if (setLifecycleCommand != null) { - verifySetLifecycleCommand(inputs, outputs, tx, setLifecycleCommand) - } else { - // Only the default command processes inputs/outputs that are not in the normal state - // TODO: Need to be able to exit defaulted amounts - requireThat { - "all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL } - "all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL } - } - if (issueCommand != null) { - verifyIssueCommand(inputs, outputs, issueCommand, issued, obligor) - } else if (settleCommand != null) { - // Perhaps through an abundance of caution, settlement is enforced as its own command. - // This could perhaps be merged into verifyBalanceChange() later, however doing so introduces a lot - // of scope for making it more opaque what's going on in a transaction and whether it's as expected - // by all parties. - verifySettleCommand(inputs, outputs, tx, settleCommand, issued, obligor, key) - } else { - verifyBalanceChange(inputs, outputs, commands, issued.product, obligor) - } - } - } - - /** - * Verify simple lifecycle changes for settlement contracts, handling exit and move commands. - * - * @param commands a list of commands filtered to those matching issuance definition for the provided inputs and - * outputs. - */ - private fun verifyBalanceChange(inputs: List>, - outputs: List>, - commands: List>>, - product: P, - obligor: Party) { - // Sum up how much settlement owed there is in the inputs, and the difference in outputs. The difference should - // be matched by exit commands representing the extracted amount. - - val inputAmount = inputs.sumObligationsOrNull

() ?: throw IllegalArgumentException("there is at least one obligation input for this group") - val outputAmount = outputs.sumObligationsOrZero(product) - - val exitCommands = commands.select>() - val requiredExitSignatures = HashSet() - val amountExitingLedger: Amount

= if (exitCommands.isNotEmpty()) { - require(exitCommands.size == 1) { "There can only be one exit command" } - val exitCommand = exitCommands.single() - // If we want to remove debt from the ledger, that must be signed for by the beneficiary. For now we require exit - // commands to be signed by all input beneficiarys, unlocking the full input amount, rather than trying to detangle - // exactly who exited what. - requiredExitSignatures.addAll(inputs.map { it.beneficiary }) - exitCommand.value.amount - } else { - Amount(0, product) - } - - requireThat { - "there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L } - "at obligor ${obligor.name} the amounts balance" by - (inputAmount == outputAmount + amountExitingLedger) - } - - verifyMoveCommand>(inputs, commands) - } + override fun extractCommands(tx: TransactionForContract): List> + = tx.commands.select() /** * A default command mutates inputs and produces identical outputs, except that the lifecycle changes. */ @VisibleForTesting - protected fun verifySetLifecycleCommand(inputs: List>, - outputs: List>, + protected fun verifySetLifecycleCommand(inputs: List>>, + outputs: List>>, tx: TransactionForContract, - setLifecycleCommand: AuthenticatedObject>) { + setLifecycleCommand: AuthenticatedObject) { // Default must not change anything except lifecycle, so number of inputs and outputs must match // exactly. require(inputs.size == outputs.size) { "Number of inputs and outputs must match" } @@ -391,162 +410,27 @@ class Obligation

: Contract { // 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()) { - val actualOutput = outputs[stateIdx] - val deadline = input.dueBefore - val timestamp: TimestampCommand? = tx.timestamp - val expectedOutput: State

= input.copy(lifecycle = expectedOutputLifecycle) + if (input is State

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

= input.copy(lifecycle = expectedOutputLifecycle) - requireThat { - "there is a timestamp from the authority" by (timestamp != null) - "the due date has passed" by (timestamp!!.after?.isAfter(deadline) ?: false) - "input state lifecycle is correct" by (input.lifecycle == expectedInputLifecycle) - "output state corresponds exactly to input state, with lifecycle changed" by (expectedOutput == actualOutput) + requireThat { + "there is a timestamp from the authority" by (timestamp != null) + "the due date has passed" by (timestamp!!.after?.isAfter(deadline) ?: false) + "input state lifecycle is correct" by (input.lifecycle == expectedInputLifecycle) + "output state corresponds exactly to input state, with lifecycle changed" by (expectedOutput == actualOutput) + } } } - val owningPubKeys = inputs.map { it.beneficiary }.toSet() + val owningPubKeys = inputs.filter { it is State

}.map { (it as State

).beneficiary }.toSet() val keysThatSigned = setLifecycleCommand.signers.toSet() requireThat { "the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys) } } - @VisibleForTesting - protected fun verifyIssueCommand(inputs: List>, - outputs: List>, - issueCommand: AuthenticatedObject>, - issued: Issued

, - obligor: Party) { - // If we have an issue command, perform special processing: the group is must have no inputs, - // and that signatures are present for all obligors. - - val inputAmount: Amount

= inputs.sumObligationsOrZero(issued.product) - val outputAmount: Amount

= outputs.sumObligations

() - requireThat { - "the issue command has a nonce" by (issueCommand.value.nonce != 0L) - "output states are issued by a command signer" by (obligor in issueCommand.signingParties) - "output values sum to more than the inputs" by (outputAmount > inputAmount) - "valid settlement issuance definition is not this issuance definition" by inputs.none { it.issuanceDef in it.acceptableIssuanceDefinitions } - } - } - - /** - * Verify a netting command. This handles both close-out and payment netting. - */ - @VisibleForTesting - protected fun verifyNetCommand(inputs: Iterable>, - outputs: Iterable>, - command: AuthenticatedObject, - netState: NetState

) { - // TODO: Can we merge this with the checks for aggregated commands? - requireThat { - "all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL } - "all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL } - } - - val template = netState.template - val product = template.product - // Create two maps of balances from obligors to beneficiaries, one for input states, the other for output states. - val inputBalances = extractAmountsDue(product, inputs) - val outputBalances = extractAmountsDue(product, outputs) - - // Sum the columns of the matrices. This will yield the net amount payable to/from each party to/from all other participants. - // The two summaries must match, reflecting that the amounts owed match on both input and output. - requireThat { - "all input states use the same template" by (inputs.all { it.template == template }) - "all output states use the same template" by (outputs.all { it.template == template }) - "amounts owed on input and output must match" by (sumAmountsDue(inputBalances) == sumAmountsDue(outputBalances)) - } - - // TODO: Handle proxies nominated by parties, i.e. a central clearing service - val involvedParties = inputs.map { it.beneficiary }.union(inputs.map { it.obligor.owningKey }).toSet() - when (command.value.type) { - // For close-out netting, allow any involved party to sign - NetType.CLOSE_OUT -> require(command.signers.intersect(involvedParties).isNotEmpty()) { "any involved party has signed" } - // Require signatures from all parties (this constraint can be changed for other contracts, and is used as a - // placeholder while exact requirements are established), or fail the transaction. - NetType.PAYMENT -> require(command.signers.containsAll(involvedParties)) { "all involved parties have signed" } - } - } - - /** - * Verify settlement of state objects. - */ - private fun verifySettleCommand(inputs: List>, - outputs: List>, - tx: TransactionForContract, - command: AuthenticatedObject>, - issued: Issued

, - obligor: Party, - key: IssuanceDefinition

) { - val template = key.template - val inputAmount: Amount

= inputs.sumObligationsOrNull

() ?: throw IllegalArgumentException("there is at least one obligation input for this group") - val outputAmount: Amount

= outputs.sumObligationsOrZero(issued.product) - - // Sum up all asset state objects that are moving and fulfil our requirements - - // The fungible asset contract verification handles ensuring there's inputs enough to cover the output states, - // we only care about counting how much is output in this transaction. We then calculate the difference in - // settlement amounts between the transaction inputs and outputs, and the two must match. No elimination is - // done of amounts paid in by each beneficiary, as it's presumed the beneficiaries have enough sense to do that - // themselves. Therefore if someone actually signed the following transaction (using cash just for an example): - // - // Inputs: - // £1m cash owned by B - // £1m owed from A to B - // Outputs: - // £1m cash owned by B - // Commands: - // Settle (signed by A) - // Move (signed by B) - // - // That would pass this check. Ensuring they do not is best addressed in the transaction generation stage. - val assetStates = tx.outputs.filterIsInstance>() - val acceptableAssetStates = assetStates - // TODO: This filter is nonsense, because it just checks there is an asset contract loaded, we need to - // verify the asset contract is the asset contract we expect. - // Something like: - // attachments.mustHaveOneOf(key.acceptableAssetContract) - .filter { it.contract.legalContractReference in template.acceptableContracts } - // Restrict the states to those of the correct issuance definition (this normally - // covers issued product and obligor, but is opaque to us) - .filter { it.issuanceDef in template.acceptableIssuedProducts } - // Catch that there's nothing useful here, so we can dump out a useful error - requireThat { - "there are fungible asset state outputs" by (assetStates.size > 0) - "there are defined acceptable fungible asset states" by (acceptableAssetStates.size > 0) - } - - val amountReceivedByOwner = acceptableAssetStates.groupBy { it.owner } - // Note we really do want to search all commands, because we want move commands of other contracts, not just - // this one. - val moveCommands = tx.commands.select() - var totalPenniesSettled = 0L - val requiredSigners = inputs.map { it.obligor.owningKey }.toSet() - - for ((beneficiary, obligations) in inputs.groupBy { it.beneficiary }) { - val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull

() - if (settled != null) { - val debt = obligations.sumObligationsOrZero(issued) - require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" } - totalPenniesSettled += settled.quantity - } - } - - // Insist that we can be the only contract consuming inputs, to ensure no other contract can think it's being - // settled as well - requireThat { - "all move commands relate to this contract" by (moveCommands.map { it.value.contractHash } - .all { it == null || it == legalContractReference }) - "contract does not try to consume itself" by (moveCommands.map { it.value }.filterIsInstance>() - .none { it.aggregateState == key }) - "amounts paid must match recipients to settle" by inputs.map { it.beneficiary }.containsAll(amountReceivedByOwner.keys) - "signatures are present from all obligors" by command.signers.containsAll(requiredSigners) - "there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L } - "at obligor ${obligor.name} the obligations after settlement balance" by - (inputAmount == outputAmount + Amount(totalPenniesSettled, issued.product)) - } - } - /** * Generate a transaction performing close-out netting of two or more states. * @@ -577,19 +461,18 @@ class Obligation

: Contract { */ fun generateIssue(tx: TransactionBuilder, obligor: Party, - issuanceDef: StateTemplate

, + issuanceDef: Terms

, pennies: Long, beneficiary: PublicKey, notary: Party) { check(tx.inputStates().isEmpty()) check(tx.outputStates().map { it.data }.sumObligationsOrNull

() == null) - val aggregateState = IssuanceDefinition(obligor, issuanceDef) tx.addOutputState(State(Lifecycle.NORMAL, obligor, issuanceDef, pennies, beneficiary), notary) - tx.addCommand(Commands.Issue(aggregateState), obligor.owningKey) + tx.addCommand(Commands.Issue(), obligor.owningKey) } fun generatePaymentNetting(tx: TransactionBuilder, - issued: Issued

, + issued: Issued>, notary: Party, vararg states: State

) { requireThat { @@ -647,7 +530,7 @@ class Obligation

: Contract { tx.addOutputState(outState, notary) partiesUsed.add(stateAndRef.state.data.beneficiary) } - tx.addCommand(Commands.SetLifecycle(aggregateState, lifecycle), partiesUsed.distinct()) + tx.addCommand(Commands.SetLifecycle(lifecycle), partiesUsed.distinct()) } tx.setTime(issuanceDef.dueBefore, notary, issuanceDef.timeTolerance) } @@ -662,7 +545,7 @@ class Obligation

: Contract { */ fun generateSettle(tx: TransactionBuilder, statesAndRefs: Iterable>>, - assetStatesAndRefs: Iterable>>, + assetStatesAndRefs: Iterable>>, moveCommand: MoveCommand, notary: Party) { val states = statesAndRefs.map { it.state } @@ -682,28 +565,31 @@ class Obligation

: Contract { // on each side together val issuanceDef = getIssuanceDefinitionOrThrow(statesAndRefs.map { it.state.data }) - val template = issuanceDef.template - val obligationTotal: Amount

= states.map { it.data }.sumObligations

() + val template: Terms

= issuanceDef.product + val obligationTotal: Amount

= Amount(states.map { it.data }.sumObligations

().quantity, template.product) var obligationRemaining: Amount

= obligationTotal val assetSigners = HashSet() statesAndRefs.forEach { tx.addInputState(it) } // Move the assets to the new beneficiary - assetStatesAndRefs.forEach { + assetStatesAndRefs.forEach { ref -> if (obligationRemaining.quantity > 0L) { - val assetState = it.state - tx.addInputState(it) - if (obligationRemaining >= assetState.data.productAmount) { - tx.addOutputState(assetState.data.move(assetState.data.productAmount, obligationOwner), notary) - obligationRemaining -= assetState.data.productAmount + tx.addInputState(ref) + + val assetState = ref.state.data + val amount: Amount

= Amount(assetState.amount.quantity, assetState.amount.token.product) + if (obligationRemaining >= amount) { + tx.addOutputState(assetState.move(assetState.amount, obligationOwner), notary) + obligationRemaining -= amount } else { + val change = Amount(obligationRemaining.quantity, assetState.amount.token) // Split the state in two, sending the change back to the previous beneficiary - tx.addOutputState(assetState.data.move(obligationRemaining, obligationOwner), notary) - tx.addOutputState(assetState.data.move(assetState.data.productAmount - obligationRemaining, assetState.data.owner), notary) + tx.addOutputState(assetState.move(change, obligationOwner), notary) + tx.addOutputState(assetState.move(assetState.amount - change, assetState.owner), notary) obligationRemaining -= Amount(0L, obligationRemaining.token) } - assetSigners.add(assetState.data.owner) + assetSigners.add(assetState.owner) } } @@ -716,15 +602,15 @@ class Obligation

: Contract { // Add the asset move command and obligation settle tx.addCommand(moveCommand, assetSigners.toList()) - tx.addCommand(Commands.Settle(issuanceDef, obligationTotal - obligationRemaining), obligationOwner) + tx.addCommand(Commands.Settle(Amount((obligationTotal - obligationRemaining).quantity, issuanceDef)), obligationIssuer.owningKey) } /** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */ - private fun getIssuanceDefinitionOrThrow(states: Iterable>): IssuanceDefinition

= + private fun getIssuanceDefinitionOrThrow(states: Iterable>): Issued> = 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>): StateTemplate

= + private fun getTemplateOrThrow(states: Iterable>): Terms

= states.map { it.template }.distinct().single() } @@ -734,13 +620,13 @@ class Obligation

: Contract { * * @return a map of obligor/beneficiary pairs to the balance due. */ -fun

extractAmountsDue(product: P, states: Iterable>): Map, Amount

> { - val balances = HashMap, Amount

>() +fun

extractAmountsDue(product: Obligation.Terms

, states: Iterable>): Map, Amount>> { + val balances = HashMap, Amount>>() states.forEach { state -> val key = Pair(state.obligor.owningKey, state.beneficiary) val balance = balances[key] ?: Amount(0L, product) - balances[key] = balance + state.productAmount + balances[key] = balance + Amount(state.amount.quantity, state.amount.token.product) } return balances @@ -804,19 +690,18 @@ fun

sumAmountsDue(balances: Map, Amount

>): Map } /** Sums the obligation states in the list, throwing an exception if there are none. All state objects in the list are presumed to be nettable. */ -fun

Iterable.sumObligations(): Amount

+fun

Iterable.sumObligations(): Amount>> = filterIsInstance>().map { it.amount }.sumOrThrow() /** Sums the obligation states in the list, returning null if there are none. */ -fun

Iterable.sumObligationsOrNull(): Amount

? +fun

Iterable.sumObligationsOrNull(): Amount>>? = filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrNull() /** Sums the obligation states in the list, returning zero of the given product if there are none. */ -fun

Iterable.sumObligationsOrZero(product: P): Amount

- = filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(product) +fun

Iterable.sumObligationsOrZero(issuanceDef: Issued>): Amount>> + = filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(issuanceDef) infix fun Obligation.State.at(dueBefore: Instant) = copy(template = template.copy(dueBefore = dueBefore)) -infix fun Obligation.IssuanceDefinition.at(dueBefore: Instant) = copy(template = template.copy(dueBefore = dueBefore)) infix fun Obligation.State.between(parties: Pair) = copy(obligor = parties.first, beneficiary = parties.second) infix fun Obligation.State.`owned by`(owner: PublicKey) = copy(beneficiary = owner) infix fun Obligation.State.`issued by`(party: Party) = copy(obligor = party) @@ -824,7 +709,7 @@ infix fun Obligation.State.`issued by`(party: Party) = copy(obligor = par fun Obligation.State.ownedBy(owner: PublicKey) = copy(beneficiary = owner) fun Obligation.State.issuedBy(party: Party) = copy(obligor = party) -val Issued.OBLIGATION_DEF: Obligation.StateTemplate - get() = Obligation.StateTemplate(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME) +val Issued.OBLIGATION_DEF: Obligation.Terms + get() = Obligation.Terms(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME) val Amount>.OBLIGATION: Obligation.State get() = Obligation.State(Obligation.Lifecycle.NORMAL, MINI_CORP, token.OBLIGATION_DEF, quantity, NullPublicKey) 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 3b4cf6ee06..82b17a1119 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt @@ -13,7 +13,7 @@ import java.security.PublicKey * 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> { +abstract class AbstractConserveAmount, T: Any> : GroupClause> { override val ifMatched: MatchBehaviour get() = MatchBehaviour.END override val ifNotMatched: MatchBehaviour diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractIssue.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractIssue.kt index ee52d72149..8fd7c0e801 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractIssue.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractIssue.kt @@ -6,6 +6,13 @@ import com.r3corda.core.contracts.clauses.MatchBehaviour /** * Standard issue clause for contracts that issue fungible assets. + * + * @param S the type of contract state which is being issued. + * @param T the token underlying the issued state. + * @param sum function to convert a list of states into an amount of the token. Must error if there are no states in + * the list. + * @param sumOrZero function to convert a list of states into an amount of the token, and returns zero if there are + * no states in the list. Takes in an instance of the token definition for constructing the zero amount if needed. */ abstract class AbstractIssue( val sum: List.() -> Amount>, diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/Net.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/Net.kt new file mode 100644 index 0000000000..dee60e570c --- /dev/null +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/Net.kt @@ -0,0 +1,97 @@ +package com.r3corda.contracts.clause + +import com.google.common.annotations.VisibleForTesting +import com.r3corda.contracts.asset.Obligation +import com.r3corda.contracts.asset.extractAmountsDue +import com.r3corda.contracts.asset.sumAmountsDue +import com.r3corda.core.contracts.* +import com.r3corda.core.contracts.clauses.MatchBehaviour +import com.r3corda.core.contracts.clauses.SingleClause +import java.security.PublicKey + +/** + * Common interface for the state subsets used when determining nettability of two or more states. Exposes the + * underlying issued thing. + */ +interface NetState

{ + val template: Obligation.Terms

+} + +/** + * Subset of state, containing the elements which must match for two obligation transactions to be nettable. + * If two obligation state objects produce equal bilateral net states, they are considered safe to net directly. + * Bilateral states are used in close-out netting. + */ +data class BilateralNetState

( + val partyKeys: Set, + override val template: Obligation.Terms

+) : NetState

+ +/** + * Subset of state, containing the elements which must match for two or more obligation transactions to be candidates + * for netting (this does not include the checks to enforce that everyone's amounts received are the same at the end, + * which is handled under the verify() function). + * In comparison to [BilateralNetState], this doesn't include the parties' keys, as ensuring balances match on + * input and output is handled elsewhere. + * Used in cases where all parties (or their proxies) are signing, such as central clearing. + */ +data class MultilateralNetState

( + override val template: Obligation.Terms

+) : NetState

+ +/** + * Clause for netting contract states. Currently only supports obligation contract. + */ +// TODO: Make this usable for any nettable contract states +open class NetClause

: SingleClause { + override val ifNotMatched: MatchBehaviour + get() = MatchBehaviour.CONTINUE + override val ifMatched: MatchBehaviour + get() = MatchBehaviour.END + override val requiredCommands: Set> + get() = setOf(Obligation.Commands.Net::class.java) + + override fun verify(tx: TransactionForContract, commands: Collection>): Set { + val command = commands.requireSingleCommand() + val groups = when (command.value.type) { + NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State

-> it.bilateralNetState } + NetType.PAYMENT -> tx.groupStates { it: Obligation.State

-> it.multilateralNetState } + } + for ((inputs, outputs, key) in groups) { + verifyNetCommand(inputs, outputs, command, key) + } + return setOf(command.value) + } + + /** + * Verify a netting command. This handles both close-out and payment netting. + */ + @VisibleForTesting + public fun verifyNetCommand(inputs: List>, + outputs: List>, + command: AuthenticatedObject, + netState: NetState

) { + val template = netState.template + // Create two maps of balances from obligors to beneficiaries, one for input states, the other for output states. + val inputBalances = extractAmountsDue(template, inputs) + val outputBalances = extractAmountsDue(template, outputs) + + // Sum the columns of the matrices. This will yield the net amount payable to/from each party to/from all other participants. + // The two summaries must match, reflecting that the amounts owed match on both input and output. + requireThat { + "all input states use the same template" by (inputs.all { it.template == template }) + "all output states use the same template" by (outputs.all { it.template == template }) + "amounts owed on input and output must match" by (sumAmountsDue(inputBalances) == sumAmountsDue(outputBalances)) + } + + // TODO: Handle proxies nominated by parties, i.e. a central clearing service + val involvedParties = inputs.map { it.beneficiary }.union(inputs.map { it.obligor.owningKey }).toSet() + when (command.value.type) { + // For close-out netting, allow any involved party to sign + NetType.CLOSE_OUT -> require(command.signers.intersect(involvedParties).isNotEmpty()) { "any involved party has signed" } + // Require signatures from all parties (this constraint can be changed for other contracts, and is used as a + // placeholder while exact requirements are established), or fail the transaction. + NetType.PAYMENT -> require(command.signers.containsAll(involvedParties)) { "all involved parties have signed" } + } + } +} \ No newline at end of file diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt index 483e0736ab..e6b8db4159 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt @@ -9,7 +9,7 @@ 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> { +open class NoZeroSizedOutputs, T: Any> : GroupClause> { override val ifMatched: MatchBehaviour get() = MatchBehaviour.CONTINUE override val ifNotMatched: MatchBehaviour diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt index f3bcd9e8e4..9a4d3728fc 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt @@ -150,15 +150,15 @@ class CashTests { command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() } tweak { command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() } - this `fails with` "there is only a single issue command" + this `fails with` "List has more than one element." } tweak { command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } - this `fails with` "there is only a single issue command" + this `fails with` "All commands must be matched at end of execution." } tweak { command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) } - this `fails with` "there is only a single issue command" + this `fails with` "All commands must be matched at end of execution." } this.verifies() } @@ -238,7 +238,7 @@ class CashTests { input { inState } output { outState.copy(amount = inState.amount / 2).editDepositRef(0) } output { outState.copy(amount = inState.amount / 2).editDepositRef(1) } - this `fails with` "for deposit [01] at issuer MegaCorp the amounts balance" + this `fails with` "for reference [01] at issuer MegaCorp the amounts balance" } // Can't mix currencies. transaction { @@ -271,7 +271,7 @@ class CashTests { input { inState } input { inState.editDepositRef(3) } output { outState.copy(amount = inState.amount * 2).editDepositRef(3) } - this `fails with` "for deposit [01]" + this `fails with` "for reference [01]" } } 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 b69754004e..33db57d779 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt @@ -3,12 +3,13 @@ package com.r3corda.contracts.asset import com.r3corda.contracts.asset.Obligation.Lifecycle import com.r3corda.core.contracts.* import com.r3corda.core.crypto.SecureHash +import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.testing.* import com.r3corda.core.utilities.nonEmptySetOf import org.junit.Test import java.security.PublicKey import java.time.Duration -import java.time.Instant +import java.time.temporal.ChronoUnit import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -16,15 +17,15 @@ import kotlin.test.assertNotEquals import kotlin.test.assertTrue class ObligationTests { - val defaultIssuer = MEGA_CORP.ref(1) - val defaultUsd = USD `issued by` defaultIssuer + val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) + val defaultIssuer = MEGA_CORP.ref(defaultRef) val oneMillionDollars = 1000000.DOLLARS `issued by` defaultIssuer val trustedCashContract = nonEmptySetOf(SecureHash.Companion.randomSHA256() as SecureHash) - val megaIssuedDollars = nonEmptySetOf(Issued(defaultIssuer, USD)) - val megaIssuedPounds = nonEmptySetOf(Issued(defaultIssuer, GBP)) - val fivePm = Instant.parse("2016-01-01T17:00:00.00Z") - val sixPm = Instant.parse("2016-01-01T18:00:00.00Z") - val megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm) + val megaIssuedDollars = nonEmptySetOf(Issued(defaultIssuer, USD)) + val megaIssuedPounds = nonEmptySetOf(Issued(defaultIssuer, GBP)) + val fivePm = TEST_TX_TIME.truncatedTo(ChronoUnit.DAYS).plus(17, ChronoUnit.HOURS) + val sixPm = fivePm.plus(1, ChronoUnit.HOURS) + val megaCorpDollarSettlement = Obligation.Terms(trustedCashContract, megaIssuedDollars, fivePm) val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuedProducts = megaIssuedPounds) val inState = Obligation.State( lifecycle = Lifecycle.NORMAL, @@ -58,24 +59,24 @@ class ObligationTests { } tweak { output { outState } - // No command commanduments - this `fails with` "required com.r3corda.contracts.asset.Obligation.Commands.Move command" + // No command arguments + this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command" } tweak { output { outState } - command(DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) } + command(DUMMY_PUBKEY_2) { Obligation.Commands.Move() } this `fails with` "the owning keys are the same as the signing keys" } tweak { output { outState } output { outState `issued by` MINI_CORP } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) } - this `fails with` "at least one obligation input" + command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } + this `fails with` "at least one asset input" } // Simple reallocation works. tweak { output { outState } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) } + command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } this.verifies() } } @@ -87,16 +88,16 @@ class ObligationTests { transaction { input { DummyState() } output { outState } - command(MINI_CORP_PUBKEY) { Obligation.Commands.Move(outState.issuanceDef) } + command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() } - this `fails with` "there is at least one obligation input" + this `fails with` "there is at least one asset input" } // Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised // institution is allowed to issue as much cash as they want. transaction { output { outState } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Issue(outState.issuanceDef) } + command(DUMMY_PUBKEY_1) { Obligation.Commands.Issue() } this `fails with` "output states are issued by a command signer" } transaction { @@ -109,10 +110,10 @@ class ObligationTests { ) } tweak { - command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement), 0) } + command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(0) } this `fails with` "has a nonce" } - command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement)) } + command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue() } this.verifies() } @@ -128,7 +129,7 @@ class ObligationTests { template = megaCorpDollarSettlement ) assertEquals(ptx.outputStates()[0].data, expected) - assertTrue(ptx.commands()[0].value is Obligation.Commands.Issue<*>) + assertTrue(ptx.commands()[0].value is Obligation.Commands.Issue) assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0]) // We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer. @@ -138,13 +139,13 @@ class ObligationTests { // Move fails: not allowed to summon money. tweak { - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) } - this `fails with` "at obligor MegaCorp the amounts balance" + command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } + this `fails with` "for reference [00] at issuer MegaCorp the amounts balance" } // Issue works. tweak { - command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) } + command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() } this.verifies() } } @@ -153,7 +154,7 @@ class ObligationTests { transaction { input { inState } output { inState.copy(quantity = inState.amount.quantity / 2) } - command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) } + command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() } this `fails with` "output values sum to more than the inputs" } @@ -161,30 +162,26 @@ class ObligationTests { transaction { input { inState } output { inState } - command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) } - this `fails with` "output values sum to more than the inputs" + command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() } + this `fails with` "" } // Can't have any other commands if we have an issue command (because the issue command overrules them) transaction { input { inState } output { inState.copy(quantity = inState.amount.quantity * 2) } - command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) } + command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() } tweak { - command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) } - this `fails with` "only move/exit commands can be present along with other obligation commands" + command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() } + this `fails with` "List has more than one element." } tweak { - command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move(inState.issuanceDef) } - this `fails with` "only move/exit commands can be present along with other obligation commands" + command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move() } + this `fails with` "All commands must be matched at end of execution." } tweak { - command(MEGA_CORP_PUBKEY) { Obligation.Commands.SetLifecycle(inState.issuanceDef, Lifecycle.DEFAULTED) } - this `fails with` "only move/exit commands can be present along with other obligation commands" - } - tweak { - command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, inState.amount / 2) } - this `fails with` "only move/exit commands can be present along with other obligation commands" + command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.amount / 2) } + this `fails with` "All commands must be matched at end of execution." } this.verifies() } @@ -245,7 +242,7 @@ class ObligationTests { val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE_PUBKEY) val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { - Obligation().generatePaymentNetting(this, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) + Obligation().generatePaymentNetting(this, obligationAliceToBob.issuanceDef, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) signWith(ALICE_KEY) signWith(BOB_KEY) signWith(DUMMY_NOTARY_KEY) @@ -259,7 +256,7 @@ class ObligationTests { val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE_PUBKEY) val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { - Obligation().generatePaymentNetting(this, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) + Obligation().generatePaymentNetting(this, obligationAliceToBob.issuanceDef, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) signWith(ALICE_KEY) signWith(BOB_KEY) }.toSignedTransaction().tx @@ -453,7 +450,7 @@ class ObligationTests { input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY } - command(ALICE_PUBKEY) { Obligation.Commands.Settle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) } + command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.issuanceDef)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } this.verifies() } @@ -467,7 +464,7 @@ class ObligationTests { input(500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY) output("Alice's $5,000,000 obligation to Bob") { halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) } output("Bob's $500,000") { 500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY } - command(ALICE_PUBKEY) { Obligation.Commands.Settle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) } + command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.issuanceDef)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } this.verifies() } @@ -480,7 +477,7 @@ class ObligationTests { input(defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob input(1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY) output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY } - command(ALICE_PUBKEY) { Obligation.Commands.Settle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) } + command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.issuanceDef)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } this `fails with` "all inputs are in the normal state" } @@ -495,7 +492,7 @@ class ObligationTests { 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) } - command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Lifecycle.DEFAULTED) } + command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) } this `fails with` "there is a timestamp from the authority" } } @@ -506,7 +503,7 @@ class ObligationTests { transaction("Settlement") { input(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) `at` futureTestTime) output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) `at` futureTestTime).copy(lifecycle = Lifecycle.DEFAULTED) } - command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF) `at` futureTestTime, Lifecycle.DEFAULTED) } + command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) } timestamp(TEST_TX_TIME) this `fails with` "the due date has passed" } @@ -516,7 +513,7 @@ class ObligationTests { transaction("Settlement") { input(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) `at` pastTestTime) output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) `at` pastTestTime).copy(lifecycle = Lifecycle.DEFAULTED) } - command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF) `at` pastTestTime, Lifecycle.DEFAULTED) } + command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) } timestamp(TEST_TX_TIME) this.verifies() } @@ -528,7 +525,7 @@ class ObligationTests { fun testMergeSplit() { // Splitting value works. transaction { - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) } + command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } tweak { input { inState } repeat(4) { output { inState.copy(quantity = inState.quantity / 4) } } @@ -572,7 +569,7 @@ class ObligationTests { transaction { input { inState } output { outState `issued by` MINI_CORP } - this `fails with` "at obligor MegaCorp the amounts balance" + this `fails with` "for reference [00] at issuer MegaCorp the amounts balance" } // Can't mix currencies. transaction { @@ -598,55 +595,54 @@ class ObligationTests { input { inState } input { inState `issued by` MINI_CORP } output { outState } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) } - this `fails with` "at obligor MiniCorp the amounts balance" + command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } + this `fails with` "for reference [00] at issuer MiniCorp the amounts balance" } } @Test - fun exitLedger() { + fun `exit single product obligation`() { // Single input/output straightforward case. transaction { input { inState } output { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) } tweak { - command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 100.DOLLARS) } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) } + command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(100.DOLLARS.quantity, inState.issuanceDef)) } + command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } tweak { - command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 200.DOLLARS) } - this `fails with` "required com.r3corda.contracts.asset.Obligation.Commands.Move command" + command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.issuanceDef)) } + this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command" tweak { - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) } + command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } this.verifies() } } } - // Multi-issuer case. + + } + @Test + fun `exit multiple product obligations`() { + // Multi-product case. transaction { - input { inState } - input { inState `issued by` MINI_CORP } + input { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedPounds)) } + input { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedDollars)) } - output { inState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) `issued by` MINI_CORP } - output { inState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) } + output { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedPounds), quantity = inState.quantity - 200.POUNDS.quantity) } + output { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedDollars), quantity = inState.quantity - 200.DOLLARS.quantity) } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) } + command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } - this `fails with` "at obligor MegaCorp the amounts balance" + this `fails with` "for reference [00] at issuer MegaCorp the amounts balance" - command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 200.DOLLARS) } - tweak { - command(MINI_CORP_PUBKEY) { Obligation.Commands.Exit((inState `issued by` MINI_CORP).issuanceDef, 0.DOLLARS) } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) } - this `fails with` "at obligor MiniCorp the amounts balance" - } - command(MINI_CORP_PUBKEY) { Obligation.Commands.Exit((inState `issued by` MINI_CORP).issuanceDef, 200.DOLLARS) } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) } + command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.issuanceDef.copy(product = megaCorpDollarSettlement))) } + this `fails with` "for reference [00] at issuer MegaCorp the amounts balance" + + command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.POUNDS.quantity, inState.issuanceDef.copy(product = megaCorpPoundSettlement))) } this.verifies() } } @@ -661,20 +657,19 @@ class ObligationTests { // Can't merge them together. tweak { output { inState.copy(beneficiary = DUMMY_PUBKEY_2, quantity = 200000L) } - this `fails with` "at obligor MegaCorp the amounts balance" + this `fails with` "for reference [00] at issuer MegaCorp the amounts balance" } // Missing MiniCorp deposit tweak { output { inState.copy(beneficiary = DUMMY_PUBKEY_2) } output { inState.copy(beneficiary = DUMMY_PUBKEY_2) } - this `fails with` "at obligor MegaCorp the amounts balance" + this `fails with` "for reference [00] at issuer MegaCorp the amounts balance" } // This works. output { inState.copy(beneficiary = DUMMY_PUBKEY_2) } output { inState.copy(beneficiary = DUMMY_PUBKEY_2) `issued by` MINI_CORP } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) } + command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } this.verifies() } } @@ -688,8 +683,7 @@ class ObligationTests { input { pounds } output { inState `owned by` DUMMY_PUBKEY_2 } output { pounds `owned by` DUMMY_PUBKEY_1 } - command(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) } - command(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Obligation.Commands.Move(pounds.issuanceDef) } + command(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Obligation.Commands.Move() } this.verifies() } @@ -754,7 +748,7 @@ class ObligationTests { @Test fun `adding two settlement contracts nets them`() { - val megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm) + val megaCorpDollarSettlement = Obligation.Terms(trustedCashContract, megaIssuedDollars, fivePm) val fiveKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement, 5000.DOLLARS.quantity, MINI_CORP_PUBKEY) val oneKDollarsFromMiniToMega = Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpDollarSettlement, @@ -780,11 +774,12 @@ class ObligationTests { @Test fun `extracting amounts due between parties from a list of states`() { - val megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm) + val megaCorpDollarSettlement = Obligation.Terms(trustedCashContract, megaIssuedDollars, fivePm) val fiveKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement, 5000.DOLLARS.quantity, MINI_CORP_PUBKEY) - val expected = mapOf(Pair(Pair(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), fiveKDollarsFromMegaToMini.amount)) - val actual = extractAmountsDue(USD, listOf(fiveKDollarsFromMegaToMini)) + val amount = fiveKDollarsFromMegaToMini.amount + val expected = mapOf(Pair(Pair(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), Amount(amount.quantity, amount.token.product))) + val actual = extractAmountsDue(megaCorpDollarSettlement, listOf(fiveKDollarsFromMegaToMini)) assertEquals(expected, actual) } @@ -810,7 +805,7 @@ class ObligationTests { val expected = mapOf( Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP)) ) - val actual = netAmountsDue(balanced) + val actual = netAmountsDue(balanced) assertEquals(expected, actual) } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt index 9bef3bcd89..313d57fc49 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -23,7 +23,7 @@ interface NamedByHash { /** * Interface for state objects that support being netted with other state objects. */ -interface BilateralNettableState> { +interface BilateralNettableState> { /** * Returns an object used to determine if two states can be subject to close-out netting. If two states return * equal objects, they can be close out netted together. @@ -34,9 +34,23 @@ interface BilateralNettableState> { * Perform bilateral netting of this state with another state. The two states must be compatible (as in * bilateralNetState objects are equal). */ - fun net(other: T): T + fun net(other: N): N } +/** + * Interface for state objects that support being netted with other state objects. + */ +interface MultilateralNettableState { + /** + * Returns an object used to determine if two states can be subject to close-out netting. If two states return + * equal objects, they can be close out netted together. + */ + val multilateralNetState: T +} + +interface NettableState, T: Any>: BilateralNettableState, + MultilateralNettableState + /** * A contract state (or just "state") contains opaque data used by a contract program. It can be thought of as a disk * file that the program can use to persist data across transactions. States are immutable: once created they are never diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt index 3ae3f9ed59..9844c78c82 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionVerification.kt @@ -96,9 +96,6 @@ data class TransactionForContract(val inputs: List, @Deprecated("This property was renamed to outputs", ReplaceWith("outputs")) val outStates: List get() = outputs - inline fun groupCommands(keySelector: (AuthenticatedObject) -> K): Map>> - = commands.select().groupBy(keySelector) - /** * Given a type and a function that returns a grouping key, associates inputs and outputs together so that they * can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/GroupClauseVerifier.kt b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/GroupClauseVerifier.kt index 2cdaf61f2b..6bbe1ff480 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/clauses/GroupClauseVerifier.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/clauses/GroupClauseVerifier.kt @@ -23,10 +23,10 @@ interface GroupClause : Clause, GroupVerify abstract class GroupClauseVerifier : SingleClause { abstract val clauses: List> - override val requiredCommands: Set> + override val requiredCommands: Set> get() = emptySet() - abstract fun extractGroups(tx: TransactionForContract): List> + abstract fun extractGroups(tx: TransactionForContract): List> override fun verify(tx: TransactionForContract, commands: Collection>): Set { val groups = extractGroups(tx)