From 0b33214feabbc2df15dba007e5a55285f5209c3b Mon Sep 17 00:00:00 2001 From: mkit Date: Wed, 9 Aug 2017 15:25:31 +0100 Subject: [PATCH] Removing clauses (#1195) * Removing clauses * Removing clauses from JavaCommercialPaper * Addressing review comments --- .../net/corda/core/contracts/ContractsDSL.kt | 3 +- .../corda/contracts/JavaCommercialPaper.java | 211 +++------ .../net/corda/contracts/CommercialPaper.kt | 189 +++----- .../corda/contracts/CommercialPaperLegacy.kt | 136 ------ .../kotlin/net/corda/contracts/asset/Cash.kt | 108 +++-- .../contracts/asset/CommodityContract.kt | 115 +++-- .../net/corda/contracts/asset/Obligation.kt | 436 ++++++++++-------- .../clause/AbstractConserveAmount.kt | 71 --- .../corda/contracts/clause/AbstractIssue.kt | 56 --- .../kotlin/net/corda/contracts/clause/Net.kt | 102 ---- .../contracts/clause/NoZeroSizedOutputs.kt | 24 - .../corda/contracts/asset/CashTestsJava.java | 4 +- .../corda/contracts/CommercialPaperTests.kt | 8 +- .../corda/contracts/DummyFungibleContract.kt | 100 ++-- .../net/corda/contracts/asset/CashTests.kt | 21 +- .../corda/contracts/asset/ObligationTests.kt | 20 +- .../node/messaging/TwoPartyTradeFlowTests.kt | 4 +- 17 files changed, 614 insertions(+), 994 deletions(-) delete mode 100644 finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt delete mode 100644 finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt delete mode 100644 finance/src/main/kotlin/net/corda/contracts/clause/AbstractIssue.kt delete mode 100644 finance/src/main/kotlin/net/corda/contracts/clause/Net.kt delete mode 100644 finance/src/main/kotlin/net/corda/contracts/clause/NoZeroSizedOutputs.kt diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt index 4446c3c04f..86517fcc3a 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt @@ -2,6 +2,7 @@ package net.corda.core.contracts +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import java.math.BigDecimal import java.security.PublicKey @@ -71,7 +72,7 @@ inline fun requireThat(body: Requirements.() -> R) = Requirements.body() /** Filters the command list by type, party and public key all at once. */ inline fun Collection>.select(signer: PublicKey? = null, - party: Party? = null) = + party: AbstractParty? = null) = filter { it.value is T }. filter { if (signer == null) true else signer in it.signers }. filter { if (party == null) true else party in it.signingParties }. diff --git a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java index e51d3889fe..584bd71926 100644 --- a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java @@ -7,10 +7,6 @@ import kotlin.Pair; import kotlin.Unit; import net.corda.contracts.asset.CashKt; import net.corda.core.contracts.*; -import net.corda.core.contracts.clauses.AnyOf; -import net.corda.core.contracts.clauses.Clause; -import net.corda.core.contracts.clauses.ClauseVerifier; -import net.corda.core.contracts.clauses.GroupClauseVerifier; import net.corda.core.crypto.SecureHash; import net.corda.core.crypto.testing.NullPublicKey; import net.corda.core.identity.AbstractParty; @@ -23,10 +19,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.time.Instant; -import java.util.Collections; import java.util.Currency; import java.util.List; -import java.util.Set; import java.util.stream.Collectors; import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; @@ -113,7 +107,8 @@ public class JavaCommercialPaper implements Contract { if (issuance != null ? !issuance.equals(state.issuance) : state.issuance != null) return false; if (owner != null ? !owner.equals(state.owner) : state.owner != null) return false; if (faceValue != null ? !faceValue.equals(state.faceValue) : state.faceValue != null) return false; - if (maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null) return false; + if (maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null) + return false; return true; } @@ -137,138 +132,6 @@ public class JavaCommercialPaper implements Contract { } } - public interface Clauses { - @SuppressWarnings("unused") - class Group extends GroupClauseVerifier { - // This complains because we're passing generic types into a varargs, but it is valid so we suppress the - // warning. - @SuppressWarnings("unchecked") - Group() { - super(new AnyOf<>( - new Clauses.Redeem(), - new Clauses.Move(), - new Clauses.Issue() - )); - } - - @NotNull - @Override - public List> groupStates(@NotNull LedgerTransaction tx) { - return tx.groupStates(State.class, State::withoutOwner); - } - } - - @SuppressWarnings("unused") - class Move extends Clause { - @NotNull - @Override - public Set> getRequiredCommands() { - return Collections.singleton(Commands.Move.class); - } - - @NotNull - @Override - public Set verify(@NotNull LedgerTransaction tx, - @NotNull List inputs, - @NotNull List outputs, - @NotNull List> commands, - State groupingKey) { - AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class); - // There should be only a single input due to aggregation above - State input = Iterables.getOnlyElement(inputs); - - if (!cmd.getSigners().contains(input.getOwner().getOwningKey())) - throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); - - // Check the output CP state is the same as the input state, ignoring the owner field. - if (outputs.size() != 1) { - throw new IllegalStateException("the state is propagated"); - } - // Don't need to check anything else, as if outputs.size == 1 then the output is equal to - // the input ignoring the owner field due to the grouping. - return Collections.singleton(cmd.getValue()); - } - } - - @SuppressWarnings("unused") - class Redeem extends Clause { - @NotNull - @Override - public Set> getRequiredCommands() { - return Collections.singleton(Commands.Redeem.class); - } - - @NotNull - @Override - public Set verify(@NotNull LedgerTransaction tx, - @NotNull List inputs, - @NotNull List outputs, - @NotNull List> commands, - State groupingKey) { - AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class); - - // There should be only a single input due to aggregation above - State input = Iterables.getOnlyElement(inputs); - - if (!cmd.getSigners().contains(input.getOwner().getOwningKey())) - throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); - - TimeWindow timeWindow = tx.getTimeWindow(); - Instant time = null == timeWindow - ? null - : timeWindow.getUntilTime(); - Amount> received = CashKt.sumCashBy(tx.getOutputs().stream().map(TransactionState::getData).collect(Collectors.toList()), input.getOwner()); - - requireThat(require -> { - require.using("must be timestamped", timeWindow != null); - require.using("received amount equals the face value: " - + received + " vs " + input.getFaceValue(), received.equals(input.getFaceValue())); - require.using("the paper must have matured", time != null && !time.isBefore(input.getMaturityDate())); - require.using("the received amount equals the face value", input.getFaceValue().equals(received)); - require.using("the paper must be destroyed", outputs.isEmpty()); - return Unit.INSTANCE; - }); - - return Collections.singleton(cmd.getValue()); - } - } - - @SuppressWarnings("unused") - class Issue extends Clause { - @NotNull - @Override - public Set> getRequiredCommands() { - return Collections.singleton(Commands.Issue.class); - } - - @NotNull - @Override - public Set verify(@NotNull LedgerTransaction tx, - @NotNull List inputs, - @NotNull List outputs, - @NotNull List> commands, - State groupingKey) { - AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class); - State output = Iterables.getOnlyElement(outputs); - TimeWindow timeWindowCommand = tx.getTimeWindow(); - Instant time = null == timeWindowCommand - ? null - : timeWindowCommand.getUntilTime(); - - requireThat(require -> { - require.using("output values sum to more than the inputs", inputs.isEmpty()); - require.using("output values sum to more than the inputs", output.faceValue.getQuantity() > 0); - require.using("must be timestamped", timeWindowCommand != null); - require.using("the maturity date is not in the past", time != null && time.isBefore(output.getMaturityDate())); - require.using("output states are issued by a command signer", cmd.getSigners().contains(output.issuance.getParty().getOwningKey())); - return Unit.INSTANCE; - }); - - return Collections.singleton(cmd.getValue()); - } - } - } - public interface Commands extends CommandData { class Move implements Commands { @Override @@ -303,7 +166,75 @@ public class JavaCommercialPaper implements Contract { @Override public void verify(@NotNull LedgerTransaction tx) throws IllegalArgumentException { - ClauseVerifier.verifyClause(tx, new Clauses.Group(), extractCommands(tx)); + + // Group by everything except owner: any modification to the CP at all is considered changing it fundamentally. + final List> groups = tx.groupStates(State.class, State::withoutOwner); + + // There are two possible things that can be done with this CP. The first is trading it. The second is redeeming + // it for cash on or after the maturity date. + final List> commands = tx.getCommands().stream().filter( + it -> { + return it.getValue() instanceof Commands; + } + ).collect(Collectors.toList()); + final AuthenticatedObject command = Iterables.getOnlyElement(commands); + final TimeWindow timeWindow = tx.getTimeWindow(); + + for (final LedgerTransaction.InOutGroup group : groups) { + final List inputs = group.getInputs(); + final List outputs = group.getOutputs(); + if (command.getValue() instanceof Commands.Move) { + final AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class); + // There should be only a single input due to aggregation above + final State input = Iterables.getOnlyElement(inputs); + + if (!cmd.getSigners().contains(input.getOwner().getOwningKey())) + throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); + + // Check the output CP state is the same as the input state, ignoring the owner field. + if (outputs.size() != 1) { + throw new IllegalStateException("the state is propagated"); + } + } else if (command.getValue() instanceof Commands.Redeem) { + final AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class); + + // There should be only a single input due to aggregation above + final State input = Iterables.getOnlyElement(inputs); + + if (!cmd.getSigners().contains(input.getOwner().getOwningKey())) + throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); + + final Instant time = null == timeWindow + ? null + : timeWindow.getUntilTime(); + final Amount> received = CashKt.sumCashBy(tx.getOutputs().stream().map(TransactionState::getData).collect(Collectors.toList()), input.getOwner()); + + requireThat(require -> { + require.using("must be timestamped", timeWindow != null); + require.using("received amount equals the face value: " + + received + " vs " + input.getFaceValue(), received.equals(input.getFaceValue())); + require.using("the paper must have matured", time != null && !time.isBefore(input.getMaturityDate())); + require.using("the received amount equals the face value", input.getFaceValue().equals(received)); + require.using("the paper must be destroyed", outputs.isEmpty()); + return Unit.INSTANCE; + }); + } else if (command.getValue() instanceof Commands.Issue) { + final AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class); + final State output = Iterables.getOnlyElement(outputs); + final Instant time = null == timeWindow + ? null + : timeWindow.getUntilTime(); + + requireThat(require -> { + require.using("output values sum to more than the inputs", inputs.isEmpty()); + require.using("output values sum to more than the inputs", output.faceValue.getQuantity() > 0); + require.using("must be timestamped", timeWindow != null); + require.using("the maturity date is not in the past", time != null && time.isBefore(output.getMaturityDate())); + require.using("output states are issued by a command signer", cmd.getSigners().contains(output.issuance.getParty().getOwningKey())); + return Unit.INSTANCE; + }); + } + } } @NotNull diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt index eb22a8a58f..c101c912b0 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt @@ -2,14 +2,9 @@ package net.corda.contracts import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.sumCashBy -import net.corda.contracts.clause.AbstractIssue import net.corda.core.contracts.* -import net.corda.core.contracts.clauses.AnyOf -import net.corda.core.contracts.clauses.Clause -import net.corda.core.contracts.clauses.GroupClauseVerifier -import net.corda.core.contracts.clauses.verifyClause import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.random63BitValue +import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party @@ -45,7 +40,6 @@ import java.util.* * which may need to be tracked. That, in turn, requires validation logic (there is a bean validator that knows how * to do this in the Apache BVal project). */ - val CP_PROGRAM_ID = CommercialPaper() // TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance. @@ -53,13 +47,6 @@ class CommercialPaper : Contract { // TODO: should reference the content of the legal agreement, not its URI override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper") - data class Terms( - val asset: Issued, - val maturityDate: Instant - ) - - override fun verify(tx: LedgerTransaction) = verifyClause(tx, Clauses.Group(), tx.commands.select()) - data class State( val issuance: PartyAndReference, override val owner: AbstractParty, @@ -67,13 +54,10 @@ class CommercialPaper : Contract { val maturityDate: Instant ) : OwnableState, QueryableState, ICommercialPaperState { override val contract = CP_PROGRAM_ID - override val participants: List - get() = listOf(owner) - - val token: Issued - get() = Issued(issuance, Terms(faceValue.token, maturityDate)) + override val participants = listOf(owner) override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner)) + fun withoutOwner() = copy(owner = NULL_PARTY) override fun toString() = "${Emoji.newspaper}CommercialPaper(of $faceValue redeemable on $maturityDate by '$issuance', owned by $owner)" // Although kotlin is smart enough not to need these, as we are using the ICommercialPaperState, we need to declare them explicitly for use later, @@ -82,7 +66,6 @@ class CommercialPaper : Contract { override fun withFaceValue(newFaceValue: Amount>): ICommercialPaperState = copy(faceValue = newFaceValue) override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate) - // DOCSTART VaultIndexedQueryCriteria /** Object Relational Mapping support. */ override fun supportedSchemas(): Iterable = listOf(CommercialPaperSchemaV1) /** Additional used schemas would be added here (eg. CommercialPaperV2, ...) */ @@ -100,97 +83,77 @@ class CommercialPaper : Contract { faceValueIssuerParty = this.faceValue.token.issuer.party.owningKey.toBase58String(), faceValueIssuerRef = this.faceValue.token.issuer.reference.bytes ) - /** Additional schema mappings would be added here (eg. CommercialPaperV2, ...) */ + /** Additional schema mappings would be added here (eg. CommercialPaperV2, ...) */ else -> throw IllegalArgumentException("Unrecognised schema $schema") } } - // DOCEND VaultIndexedQueryCriteria - } - - interface Clauses { - class Group : GroupClauseVerifier>( - AnyOf( - Redeem(), - Move(), - Issue())) { - override fun groupStates(tx: LedgerTransaction): List>> - = tx.groupStates> { it.token } - } - - class Issue : AbstractIssue( - { map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() }, - { token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) { - override val requiredCommands: Set> = setOf(Commands.Issue::class.java) - - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Issued?): Set { - val consumedCommands = super.verify(tx, inputs, outputs, commands, groupingKey) - commands.requireSingleCommand() - val timeWindow = tx.timeWindow - val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances must have a time-window") - - require(outputs.all { time < it.maturityDate }) { "maturity date is not in the past" } - - return consumedCommands - } - } - - class Move : Clause>() { - override val requiredCommands: Set> = setOf(Commands.Move::class.java) - - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Issued?): Set { - val command = commands.requireSingleCommand() - val input = inputs.single() - requireThat { - "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) - "the state is propagated" using (outputs.size == 1) - // Don't need to check anything else, as if outputs.size == 1 then the output is equal to - // the input ignoring the owner field due to the grouping. - } - return setOf(command.value) - } - } - - class Redeem : Clause>() { - override val requiredCommands: Set> = setOf(Commands.Redeem::class.java) - - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Issued?): Set { - // TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date) - // before requiring a single command - val command = commands.requireSingleCommand() - val timeWindow = tx.timeWindow - - val input = inputs.single() - val received = tx.outputStates.sumCashBy(input.owner) - val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must have a time-window") - requireThat { - "the paper must have matured" using (time >= input.maturityDate) - "the received amount equals the face value" using (received == input.faceValue) - "the paper must be destroyed" using outputs.isEmpty() - "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) - } - - return setOf(command.value) - } - - } } interface Commands : CommandData { - data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands + class Move : TypeOnlyCommandData(), Commands + class Redeem : TypeOnlyCommandData(), Commands - data class Issue(override val nonce: Long = random63BitValue()) : IssueCommand, Commands + // We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP. + // However, nothing in the platform enforces that uniqueness: it's up to the issuer. + class Issue : TypeOnlyCommandData(), Commands + } + + override fun verify(tx: LedgerTransaction) { + // Group by everything except owner: any modification to the CP at all is considered changing it fundamentally. + val groups = tx.groupStates(State::withoutOwner) + + // There are two possible things that can be done with this CP. The first is trading it. The second is redeeming + // it for cash on or after the maturity date. + val command = tx.commands.requireSingleCommand() + val timeWindow: TimeWindow? = tx.timeWindow + + // Suppress compiler warning as 'key' is an unused variable when destructuring 'groups'. + @Suppress("UNUSED_VARIABLE") + for ((inputs, outputs, key) in groups) { + when (command.value) { + is Commands.Move -> { + val input = inputs.single() + requireThat { + "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) + "the state is propagated" using (outputs.size == 1) + // Don't need to check anything else, as if outputs.size == 1 then the output is equal to + // the input ignoring the owner field due to the grouping. + } + } + + is Commands.Redeem -> { + // Redemption of the paper requires movement of on-ledger cash. + val input = inputs.single() + val received = tx.outputStates.sumCashBy(input.owner) + val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must have a time-window") + requireThat { + "the paper must have matured" using (time >= input.maturityDate) + "the received amount equals the face value" using (received == input.faceValue) + "the paper must be destroyed" using outputs.isEmpty() + "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) + } + } + + is Commands.Issue -> { + val output = outputs.single() + val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances have a time-window") + requireThat { + // Don't allow people to issue commercial paper under other entities identities. + "output states are issued by a command signer" using + (output.issuance.party.owningKey in command.signers) + "output values sum to more than the inputs" using (output.faceValue.quantity > 0) + "the maturity date is not in the past" using (time < output.maturityDate) + // Don't allow an existing CP state to be replaced by this issuance. + // TODO: this has a weird/incorrect assertion string because it doesn't quite match the logic in the clause version. + // TODO: Consider how to handle the case of mistaken issuances, or other need to patch. + "output values sum to more than the inputs" using inputs.isEmpty() + } + } + + // TODO: Think about how to evolve contracts over time with new commands. + else -> throw IllegalArgumentException("Unrecognised command") + } + } } /** @@ -198,9 +161,10 @@ class CommercialPaper : Contract { * an existing transaction because you aren't able to issue multiple pieces of CP in a single transaction * at the moment: this restriction is not fundamental and may be lifted later. */ - fun generateIssue(issuance: PartyAndReference, faceValue: Amount>, maturityDate: Instant, notary: Party): TransactionBuilder { - val state = TransactionState(State(issuance, issuance.party, faceValue, maturityDate), notary) - return TransactionBuilder(notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) + fun generateIssue(issuance: PartyAndReference, faceValue: Amount>, maturityDate: Instant, + notary: Party): TransactionBuilder { + val state = State(issuance, issuance.party, faceValue, maturityDate) + return TransactionBuilder(notary = notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) } /** @@ -208,7 +172,7 @@ class CommercialPaper : Contract { */ fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: AbstractParty) { tx.addInputState(paper) - tx.addOutputState(TransactionState(paper.state.data.copy(owner = newOwner), paper.state.notary)) + tx.addOutputState(paper.state.data.withOwner(newOwner)) tx.addCommand(Commands.Move(), paper.state.data.owner.owningKey) } @@ -223,15 +187,12 @@ class CommercialPaper : Contract { @Suspendable fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, vault: VaultService) { // Add the cash movement using the states in our vault. - val amount = paper.state.data.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) } - vault.generateSpend(tx, amount, paper.state.data.owner) + vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) tx.addInputState(paper) - tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.data.owner.owningKey) + tx.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey) } } infix fun CommercialPaper.State.`owned by`(owner: AbstractParty) = copy(owner = owner) infix fun CommercialPaper.State.`with notary`(notary: Party) = TransactionState(this, notary) -infix fun ICommercialPaperState.`owned by`(newOwner: AbstractParty) = withOwner(newOwner) - - +infix fun ICommercialPaperState.`owned by`(newOwner: AbstractParty) = withOwner(newOwner) \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt deleted file mode 100644 index b7086a6a29..0000000000 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt +++ /dev/null @@ -1,136 +0,0 @@ -package net.corda.contracts - -import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.sumCashBy -import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.testing.NULL_PARTY -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.Party -import net.corda.core.internal.Emoji -import net.corda.core.node.services.VaultService -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.TransactionBuilder -import java.time.Instant -import java.util.* - -/** - * Legacy version of [CommercialPaper] that includes the full verification logic itself, rather than breaking it - * into clauses. This is here just as an example for the contract tutorial. - */ - -val CP_LEGACY_PROGRAM_ID = CommercialPaperLegacy() - -// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance. -class CommercialPaperLegacy : Contract { - // TODO: should reference the content of the legal agreement, not its URI - override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper") - - data class State( - val issuance: PartyAndReference, - override val owner: AbstractParty, - val faceValue: Amount>, - val maturityDate: Instant - ) : OwnableState, ICommercialPaperState { - override val contract = CP_LEGACY_PROGRAM_ID - override val participants = listOf(owner) - - fun withoutOwner() = copy(owner = NULL_PARTY) - override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner)) - override fun toString() = "${Emoji.newspaper}CommercialPaper(of $faceValue redeemable on $maturityDate by '$issuance', owned by $owner)" - - // Although kotlin is smart enough not to need these, as we are using the ICommercialPaperState, we need to declare them explicitly for use later, - override fun withOwner(newOwner: AbstractParty): ICommercialPaperState = copy(owner = newOwner) - - override fun withFaceValue(newFaceValue: Amount>): ICommercialPaperState = copy(faceValue = newFaceValue) - override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate) - } - - interface Commands : CommandData { - class Move : TypeOnlyCommandData(), Commands - - class Redeem : TypeOnlyCommandData(), Commands - // We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP. - // However, nothing in the platform enforces that uniqueness: it's up to the issuer. - class Issue : TypeOnlyCommandData(), Commands - } - - override fun verify(tx: LedgerTransaction) { - // Group by everything except owner: any modification to the CP at all is considered changing it fundamentally. - val groups = tx.groupStates(State::withoutOwner) - - // There are two possible things that can be done with this CP. The first is trading it. The second is redeeming - // it for cash on or after the maturity date. - val command = tx.commands.requireSingleCommand() - val timeWindow: TimeWindow? = tx.timeWindow - - // Suppress compiler warning as 'key' is an unused variable when destructuring 'groups'. - @Suppress("UNUSED_VARIABLE") - for ((inputs, outputs, key) in groups) { - when (command.value) { - is Commands.Move -> { - val input = inputs.single() - requireThat { - "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) - "the state is propagated" using (outputs.size == 1) - // Don't need to check anything else, as if outputs.size == 1 then the output is equal to - // the input ignoring the owner field due to the grouping. - } - } - - is Commands.Redeem -> { - // Redemption of the paper requires movement of on-ledger cash. - val input = inputs.single() - val received = tx.outputStates.sumCashBy(input.owner) - val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must have a time-window") - requireThat { - "the paper must have matured" using (time >= input.maturityDate) - "the received amount equals the face value" using (received == input.faceValue) - "the paper must be destroyed" using outputs.isEmpty() - "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) - } - } - - is Commands.Issue -> { - val output = outputs.single() - val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances have a time-window") - requireThat { - // Don't allow people to issue commercial paper under other entities identities. - "output states are issued by a command signer" using - (output.issuance.party.owningKey in command.signers) - "output values sum to more than the inputs" using (output.faceValue.quantity > 0) - "the maturity date is not in the past" using (time < output.maturityDate) - // Don't allow an existing CP state to be replaced by this issuance. - // TODO: this has a weird/incorrect assertion string because it doesn't quite match the logic in the clause version. - // TODO: Consider how to handle the case of mistaken issuances, or other need to patch. - "output values sum to more than the inputs" using inputs.isEmpty() - } - } - - // TODO: Think about how to evolve contracts over time with new commands. - else -> throw IllegalArgumentException("Unrecognised command") - } - } - } - - fun generateIssue(issuance: PartyAndReference, faceValue: Amount>, maturityDate: Instant, - notary: Party): TransactionBuilder { - val state = State(issuance, issuance.party, faceValue, maturityDate) - return TransactionBuilder(notary = notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) - } - - fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: AbstractParty) { - tx.addInputState(paper) - tx.addOutputState(paper.state.data.withOwner(newOwner)) - tx.addCommand(Command(Commands.Move(), paper.state.data.owner.owningKey)) - } - - @Throws(InsufficientBalanceException::class) - @Suspendable - fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, vault: VaultService) { - // Add the cash movement using the states in our vault. - vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) - tx.addInputState(paper) - tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner.owningKey)) - } -} diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt index 5e51024f9a..23c7431c65 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt @@ -1,13 +1,6 @@ package net.corda.contracts.asset -import net.corda.contracts.clause.AbstractConserveAmount -import net.corda.contracts.clause.AbstractIssue -import net.corda.contracts.clause.NoZeroSizedOutputs import net.corda.core.contracts.* -import net.corda.core.contracts.clauses.AllOf -import net.corda.core.contracts.clauses.FirstOf -import net.corda.core.contracts.clauses.GroupClauseVerifier -import net.corda.core.contracts.clauses.verifyClause import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.newSecureRandom @@ -15,16 +8,16 @@ import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.internal.Emoji import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState -import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.core.internal.Emoji import net.corda.schemas.CashSchemaV1 import org.bouncycastle.asn1.x500.X500Name import java.math.BigInteger +import java.security.PublicKey import java.util.* ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -61,33 +54,11 @@ class Cash : OnLedgerAsset() { */ // DOCSTART 2 override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") + // DOCEND 2 override fun extractCommands(commands: Collection>): List> = commands.select() - interface Clauses { - class Group : GroupClauseVerifier>(AllOf>( - NoZeroSizedOutputs(), - FirstOf>( - Issue(), - ConserveAmount()) - ) - ) { - override fun groupStates(tx: LedgerTransaction): List>> - = tx.groupStates> { it.amount.token } - } - - class Issue : AbstractIssue( - sum = { sumCash() }, - sumOrZero = { sumCashOrZero(it) } - ) { - override val requiredCommands: Set> = setOf(Commands.Issue::class.java) - } - - @CordaSerializable - class ConserveAmount : AbstractConserveAmount() - } - // DOCSTART 1 /** A state representing a cash claim against some party. */ data class State( @@ -120,7 +91,7 @@ class Cash : OnLedgerAsset() { issuerParty = this.amount.token.issuer.party.owningKey.toBase58String(), issuerRef = this.amount.token.issuer.reference.bytes ) - /** Additional schema mappings would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */ + /** Additional schema mappings would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */ else -> throw IllegalArgumentException("Unrecognised schema $schema") } } @@ -165,7 +136,7 @@ class Cash : OnLedgerAsset() { * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */ fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: AbstractParty, notary: Party) - = generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand()) + = generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand()) override fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty) = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) @@ -174,8 +145,73 @@ class Cash : OnLedgerAsset() { override fun generateIssueCommand() = Commands.Issue() override fun generateMoveCommand() = Commands.Move() - override fun verify(tx: LedgerTransaction) - = verifyClause(tx, Clauses.Group(), extractCommands(tx.commands)) + override fun verify(tx: LedgerTransaction) { + // Each group is a set of input/output states with distinct (reference, currency) attributes. These types + // of cash are not fungible and must be kept separated for bookkeeping purposes. + val groups = tx.groupStates { it: Cash.State -> it.amount.token } + + for ((inputs, outputs, key) in groups) { + // Either inputs or outputs could be empty. + val issuer = key.issuer + val currency = key.product + + requireThat { + "there are no zero sized outputs" using (outputs.none { it.amount.quantity == 0L }) + } + + val issueCommand = tx.commands.select().firstOrNull() + if (issueCommand != null) { + verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer) + } else { + val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one cash input for this group") + val outputAmount = outputs.sumCashOrZero(Issued(issuer, currency)) + + // If we want to remove cash 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 == key }.singleOrNull() + val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, Issued(issuer, currency)) + + requireThat { + "there are no zero sized inputs" using inputs.none { it.amount.quantity == 0L } + "for reference ${issuer.reference} at issuer ${issuer.party} the amounts balance: ${inputAmount.quantity} - ${amountExitingLedger.quantity} != ${outputAmount.quantity}" using + (inputAmount == outputAmount + amountExitingLedger) + } + + verifyMoveCommand(inputs, tx.commands) + } + } + } + + private fun verifyIssueCommand(inputs: List, + outputs: List, + tx: LedgerTransaction, + issueCommand: AuthenticatedObject, + currency: Currency, + issuer: PartyAndReference) { + // 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 cash claims of arbitrary + // amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some + // as-yet-unwritten identity service. See ADP-22 for discussion. + + // The grouping ensures that all outputs have the same deposit reference and currency. + val inputAmount = inputs.sumCashOrZero(Issued(issuer, currency)) + val outputAmount = outputs.sumCash() + val cashCommands = tx.commands.select() + requireThat { + "the issue command has a nonce" using (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.party in issueCommand.signingParties) + "output states are issued by a command signer" using (issuer.party.owningKey in issueCommand.signers) + "output values sum to more than the inputs" using (outputAmount > inputAmount) + "there is only a single issue command" using (cashCommands.count() == 1) + } + } } // Small DSL extensions. diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt b/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt index cf36ea7d7b..f9f0a13523 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt @@ -1,13 +1,7 @@ package net.corda.contracts.asset import net.corda.contracts.Commodity -import net.corda.contracts.clause.AbstractConserveAmount -import net.corda.contracts.clause.AbstractIssue -import net.corda.contracts.clause.NoZeroSizedOutputs import net.corda.core.contracts.* -import net.corda.core.contracts.clauses.AnyOf -import net.corda.core.contracts.clauses.GroupClauseVerifier -import net.corda.core.contracts.clauses.verifyClause import net.corda.core.crypto.SecureHash import net.corda.core.crypto.newSecureRandom import net.corda.core.identity.AbstractParty @@ -49,48 +43,6 @@ class CommodityContract : OnLedgerAsset>(AnyOf( - NoZeroSizedOutputs(), - Issue(), - ConserveAmount())) { - /** - * Group commodity states by issuance definition (issuer and underlying commodity). - */ - override fun groupStates(tx: LedgerTransaction) - = tx.groupStates> { it.amount.token } - } - - /** - * Standard issue clause, specialised to match the commodity issue command. - */ - class Issue : AbstractIssue( - sum = { sumCommodities() }, - sumOrZero = { sumCommoditiesOrZero(it) } - ) { - override val requiredCommands: Set> = setOf(Commands.Issue::class.java) - } - - /** - * Standard clause for conserving the amount from input to output. - */ - @CordaSerializable - class ConserveAmount : AbstractConserveAmount() - } - /** A state representing a commodity claim against some party */ data class State( override val amount: Amount>, @@ -138,8 +90,71 @@ class CommodityContract : OnLedgerAsset>) : Commands, FungibleAsset.Commands.Exit } - override fun verify(tx: LedgerTransaction) - = verifyClause(tx, Clauses.Group(), extractCommands(tx.commands)) + override fun verify(tx: LedgerTransaction) { + // Each group is a set of input/output states with distinct (reference, commodity) attributes. These types + // of commodity are not fungible and must be kept separated for bookkeeping purposes. + val groups = tx.groupStates { it: CommodityContract.State -> it.amount.token } + + for ((inputs, outputs, key) in groups) { + // Either inputs or outputs could be empty. + val issuer = key.issuer + val commodity = key.product + val party = issuer.party + + requireThat { + "there are no zero sized outputs" using ( outputs.none { it.amount.quantity == 0L } ) + } + + val issueCommand = tx.commands.select().firstOrNull() + if (issueCommand != null) { + verifyIssueCommand(inputs, outputs, tx, issueCommand, commodity, issuer) + } else { + val inputAmount = inputs.sumCommoditiesOrNull() ?: throw IllegalArgumentException("there is at least one commodity input for this group") + val outputAmount = outputs.sumCommoditiesOrZero(Issued(issuer, commodity)) + + // If we want to remove commodity 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 = party).singleOrNull() + val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, Issued(issuer, commodity)) + + requireThat { + "there are no zero sized inputs" using ( inputs.none { it.amount.quantity == 0L } ) + "for reference ${issuer.reference} at issuer ${party.nameOrNull()} the amounts balance" using + (inputAmount == outputAmount + amountExitingLedger) + } + + verifyMoveCommand(inputs, tx.commands) + } + } + } + + private fun verifyIssueCommand(inputs: List, + outputs: List, + tx: LedgerTransaction, + issueCommand: AuthenticatedObject, + commodity: Commodity, + issuer: PartyAndReference) { + // 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 cash claims of arbitrary + // amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some + // as-yet-unwritten identity service. See ADP-22 for discussion. + + // The grouping ensures that all outputs have the same deposit reference and currency. + val inputAmount = inputs.sumCommoditiesOrZero(Issued(issuer, commodity)) + val outputAmount = outputs.sumCommodities() + val commodityCommands = tx.commands.select() + requireThat { + "the issue command has a nonce" using (issueCommand.value.nonce != 0L) + "output deposits are owned by a command signer" using (issuer.party in issueCommand.signingParties) + "output values sum to more than the inputs" using (outputAmount > inputAmount) + "there is only a single issue command" using (commodityCommands.count() == 1) + } + } override fun extractCommands(commands: Collection>): List> = commands.select() diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt index 218d2dab23..2a37bda579 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt @@ -5,9 +5,7 @@ import net.corda.contracts.NetCommand import net.corda.contracts.NetType import net.corda.contracts.NettableState import net.corda.contracts.asset.Obligation.Lifecycle.NORMAL -import net.corda.contracts.clause.* import net.corda.core.contracts.* -import net.corda.core.contracts.clauses.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.random63BitValue @@ -30,6 +28,37 @@ import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.set +/** + * 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

+ + // Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode. val OBLIGATION_PROGRAM_ID = Obligation() @@ -55,186 +84,6 @@ class Obligation

: Contract { */ override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.example.gov/cash-settlement.html") - interface Clauses { - /** - * Parent clause for clauses that operate on grouped states (those which are fungible). - */ - class Group

: GroupClauseVerifier, Commands, Issued>>( - AllOf( - NoZeroSizedOutputs, Commands, Terms

>(), - FirstOf( - SetLifecycle

(), - AllOf( - VerifyLifecycle, Commands, Issued>, P>(), - FirstOf( - Settle

(), - Issue(), - ConserveAmount() - ) - ) - ) - ) - ) { - override fun groupStates(tx: LedgerTransaction): List, Issued>>> - = tx.groupStates, Issued>> { it.amount.token } - } - - /** - * Generic issuance clause - */ - class Issue

: AbstractIssue, Commands, Terms

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

: AbstractConserveAmount, Commands, Terms

>() - - /** - * Clause for supporting netting of obligations. - */ - class Net : NetClause() { - val lifecycleClause = Clauses.VerifyLifecycle() - override fun toString(): String = "Net obligations" - - override fun verify(tx: LedgerTransaction, inputs: List, outputs: List, commands: List>, groupingKey: Unit?): Set { - lifecycleClause.verify(tx, inputs, outputs, commands, groupingKey) - return super.verify(tx, inputs, outputs, commands, groupingKey) - } - } - - /** - * Obligation-specific clause for changing the lifecycle of one or more states. - */ - class SetLifecycle

: Clause, Commands, Issued>>() { - override val requiredCommands: Set> = setOf(Commands.SetLifecycle::class.java) - - override fun verify(tx: LedgerTransaction, - inputs: List>, - outputs: List>, - commands: List>, - groupingKey: Issued>?): Set { - val command = commands.requireSingleCommand() - Obligation

().verifySetLifecycleCommand(inputs, outputs, tx, command) - return setOf(command.value) - } - - override fun toString(): String = "Set obligation lifecycle" - } - - /** - * Obligation-specific clause for settling an outstanding obligation by witnessing - * change of ownership of other states to fulfil - */ - class Settle

: Clause, Commands, Issued>>() { - override val requiredCommands: Set> = setOf(Commands.Settle::class.java) - override fun verify(tx: LedgerTransaction, - inputs: List>, - outputs: List>, - commands: List>, - groupingKey: Issued>?): Set { - require(groupingKey != null) - val command = commands.requireSingleCommand>() - val obligor = groupingKey!!.issuer.party - val template = groupingKey.product - val inputAmount: Amount>> = inputs.sumObligationsOrNull

() ?: throw IllegalArgumentException("there is at least one obligation input for this group") - val outputAmount: Amount>> = outputs.sumObligationsOrZero(groupingKey) - - // 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.outputsOfType>() - 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.amount.token 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" using (assetStates.isNotEmpty()) - "there are defined acceptable fungible asset states" using (acceptableAssetStates.isNotEmpty()) - } - - 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.amount.token.issuer.party.owningKey }.toSet() - - for ((beneficiary, obligations) in inputs.groupBy { it.owner }) { - val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull

() - if (settled != null) { - val debt = obligations.sumObligationsOrZero(groupingKey) - require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" } - totalPenniesSettled += settled.quantity - } - } - - val totalAmountSettled = Amount(totalPenniesSettled, command.value.amount.token) - requireThat { - // Insist that we can be the only contract consuming inputs, to ensure no other contract can think it's being - // settled as well - "all move commands relate to this contract" using (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" using inputs.map { it.owner }.containsAll(amountReceivedByOwner.keys) - "amount in settle command ${command.value.amount} matches settled total $totalAmountSettled" using (command.value.amount == totalAmountSettled) - "signatures are present from all obligors" using command.signers.containsAll(requiredSigners) - "there are no zero sized inputs" using inputs.none { it.amount.quantity == 0L } - "at obligor $obligor the obligations after settlement balance" using - (inputAmount == outputAmount + Amount(totalPenniesSettled, groupingKey)) - } - 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 : Clause() { - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: T?): Set - = verify(inputs.filterIsInstance>(), outputs.filterIsInstance>()) - - private fun verify(inputs: List>, - outputs: List>): Set { - requireThat { - "all inputs are in the normal state " using inputs.all { it.lifecycle == Lifecycle.NORMAL } - "all outputs are in the normal state " using 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 * to the state. Most states will not leave the [NORMAL] lifecycle. Note that settled (as an end lifecycle) is @@ -386,10 +235,209 @@ class Obligation

: Contract { data class Exit

(override val amount: Amount>>) : Commands, FungibleAsset.Commands.Exit> } - override fun verify(tx: LedgerTransaction) = verifyClause(tx, FirstOf( - Clauses.Net(), - Clauses.Group

() - ), tx.commands.select()) + override fun verify(tx: LedgerTransaction) { + val netCommand = tx.commands.select().firstOrNull() + if (netCommand != null) { + verifyLifecycleCommand(tx.inputStates, tx.outputStates) + verifyNetCommand(tx, netCommand) + } else { + val groups = tx.groupStates { it: Obligation.State

-> it.amount.token } + for ((inputs, outputs, key) in groups) { + requireThat { + "there are no zero sized outputs" using (outputs.none { it.amount.quantity == 0L }) + } + val setLifecycleCommand = tx.commands.select().firstOrNull() + if (setLifecycleCommand != null) { + verifySetLifecycleCommand(inputs, outputs, tx, setLifecycleCommand) + } else { + verifyLifecycleCommand(inputs, outputs) + val settleCommand = tx.commands.select>().firstOrNull() + if (settleCommand != null) { + verifySettleCommand(tx, inputs, outputs, settleCommand, key) + } else { + val issueCommand = tx.commands.select().firstOrNull() + if (issueCommand != null) { + verifyIssueCommand(tx, inputs, outputs, issueCommand, key) + } else { + conserveAmount(tx, inputs, outputs, key) + } + } + } + } + } + } + + private fun conserveAmount(tx: LedgerTransaction, + inputs: List>>, + outputs: List>>, + key: Issued>) { + val issuer = key.issuer + val terms = key.product + val inputAmount = inputs.sumObligationsOrNull

() ?: throw IllegalArgumentException("there is at least one obligation input for this group") + val outputAmount = outputs.sumObligationsOrZero(Issued(issuer, terms)) + + // If we want to remove obligations 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 == key }.singleOrNull() + val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, Issued(issuer, terms)) + + requireThat { + "there are no zero sized inputs" using (inputs.none { it.amount.quantity == 0L }) + "for reference ${issuer.reference} at issuer ${issuer.party.nameOrNull()} the amounts balance" using + (inputAmount == outputAmount + amountExitingLedger) + } + + verifyMoveCommand(inputs, tx.commands) + } + + private fun verifyIssueCommand(tx: LedgerTransaction, + inputs: List>>, + outputs: List>>, + issueCommand: AuthenticatedObject, + key: Issued>) { + // 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 cash claims of arbitrary + // amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some + // as-yet-unwritten identity service. See ADP-22 for discussion. + + // The grouping ensures that all outputs have the same deposit reference and currency. + val issuer = key.issuer + val terms = key.product + val inputAmount = inputs.sumObligationsOrZero(Issued(issuer, terms)) + val outputAmount = outputs.sumObligations

() + val issueCommands = tx.commands.select() + requireThat { + "the issue command has a nonce" using (issueCommand.value.nonce != 0L) + "output states are issued by a command signer" using (issuer.party in issueCommand.signingParties) + "output values sum to more than the inputs" using (outputAmount > inputAmount) + "there is only a single issue command" using (issueCommands.count() == 1) + } + } + + private fun verifySettleCommand(tx: LedgerTransaction, + inputs: List>>, + outputs: List>>, + command: AuthenticatedObject>, + groupingKey: Issued>) { + val obligor = groupingKey.issuer.party + val template = groupingKey.product + val inputAmount: Amount>> = inputs.sumObligationsOrNull

() ?: throw IllegalArgumentException("there is at least one obligation input for this group") + val outputAmount: Amount>> = outputs.sumObligationsOrZero(groupingKey) + + // 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.outputsOfType>() + 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.amount.token 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" using (assetStates.isNotEmpty()) + "there are defined acceptable fungible asset states" using (acceptableAssetStates.isNotEmpty()) + } + + 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.amount.token.issuer.party.owningKey }.toSet() + + for ((beneficiary, obligations) in inputs.groupBy { it.owner }) { + val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull

() + if (settled != null) { + val debt = obligations.sumObligationsOrZero(groupingKey) + require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" } + totalPenniesSettled += settled.quantity + } + } + + val totalAmountSettled = Amount(totalPenniesSettled, command.value.amount.token) + requireThat { + // Insist that we can be the only contract consuming inputs, to ensure no other contract can think it's being + // settled as well + "all move commands relate to this contract" using (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" using inputs.map { it.owner }.containsAll(amountReceivedByOwner.keys) + "amount in settle command ${command.value.amount} matches settled total $totalAmountSettled" using (command.value.amount == totalAmountSettled) + "signatures are present from all obligors" using command.signers.containsAll(requiredSigners) + "there are no zero sized inputs" using inputs.none { it.amount.quantity == 0L } + "at obligor $obligor the obligations after settlement balance" using + (inputAmount == outputAmount + Amount(totalPenniesSettled, groupingKey)) + } + } + + private fun verifyLifecycleCommand(inputs: List, outputs: List) { + val filteredInputs = inputs.filterIsInstance>() + val filteredOutputs = outputs.filterIsInstance>() + requireThat { + "all inputs are in the normal state " using filteredInputs.all { it.lifecycle == Lifecycle.NORMAL } + "all outputs are in the normal state " using filteredOutputs.all { it.lifecycle == Lifecycle.NORMAL } + } + } + + private fun verifyNetCommand(tx: LedgerTransaction, command: AuthenticatedObject) { + 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 ((groupInputs, groupOutputs, key) in groups) { + + val template = key.template + // Create two maps of balances from obligors to beneficiaries, one for input states, the other for output states. + val inputBalances = extractAmountsDue(template, groupInputs) + val outputBalances = extractAmountsDue(template, groupOutputs) + + // 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" using (groupInputs.all { it.template == template }) + "all output states use the same template" using (groupOutputs.all { it.template == template }) + "amounts owed on input and output must match" using (sumAmountsDue(inputBalances) == sumAmountsDue + (outputBalances)) + } + + // TODO: Handle proxies nominated by parties, i.e. a central clearing service + val involvedParties: Set = groupInputs.map { it.beneficiary.owningKey }.union(groupInputs.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" } + } + } + } /** * A default command mutates inputs and produces identical outputs, except that the lifecycle changes. @@ -488,11 +536,11 @@ class Obligation

: Contract { * @param notary the notary for this transaction's outputs. */ fun generateCashIssue(tx: TransactionBuilder, - obligor: AbstractParty, - amount: Amount>, - dueBefore: Instant, - beneficiary: AbstractParty, - notary: Party) { + obligor: AbstractParty, + amount: Amount>, + dueBefore: Instant, + beneficiary: AbstractParty, + notary: Party) { val issuanceDef = Terms(NonEmptySet.of(Cash().legalContractReference), NonEmptySet.of(amount.token), dueBefore) OnLedgerAsset.generateIssue(tx, TransactionState(State(Lifecycle.NORMAL, obligor, issuanceDef, amount.quantity, beneficiary), notary), Commands.Issue()) } @@ -514,7 +562,7 @@ class Obligation

: Contract { pennies: Long, beneficiary: AbstractParty, notary: Party) - = OnLedgerAsset.generateIssue(tx, TransactionState(State(Lifecycle.NORMAL, obligor, issuanceDef, pennies, beneficiary), notary), Commands.Issue()) + = OnLedgerAsset.generateIssue(tx, TransactionState(State(Lifecycle.NORMAL, obligor, issuanceDef, pennies, beneficiary), notary), Commands.Issue()) fun generatePaymentNetting(tx: TransactionBuilder, issued: Issued>, @@ -682,7 +730,7 @@ fun

extractAmountsDue(product: Obligation.Terms

, states: Iterable netAmountsDue(balances: Map, Amount>): Map, Amount> { +fun

netAmountsDue(balances: Map, Amount>): Map, Amount> { val nettedBalances = HashMap, Amount>() balances.forEach { balance -> @@ -709,7 +757,7 @@ fun netAmountsDue(balances: Map, Amount sumAmountsDue(balances: Map, Amount>): Map { +fun

sumAmountsDue(balances: Map, Amount>): Map { val sum = HashMap() // Fill the map with zeroes initially diff --git a/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt b/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt deleted file mode 100644 index 0dd239b484..0000000000 --- a/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt +++ /dev/null @@ -1,71 +0,0 @@ -package net.corda.contracts.clause - -import net.corda.contracts.asset.OnLedgerAsset -import net.corda.core.contracts.* -import net.corda.core.contracts.clauses.Clause -import net.corda.core.identity.AbstractParty -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.loggerFor -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, C : CommandData, T : Any> : Clause>() { - - private companion object { - val log = loggerFor>() - } - - /** - * Generate an transaction exiting fungible assets from the ledger. - * - * @param tx transaction builder to add states and commands to. - * @param amountIssued the amount to be exited, represented as a quantity of issued currency. - * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is - * the responsibility of the caller to check that they do not attempt to exit funds held by others. - * @return the public keys which must sign the transaction for it to be valid. - */ - @Deprecated("This function will be removed in a future milestone", ReplaceWith("OnLedgerAsset.generateExit()")) - @Throws(InsufficientBalanceException::class) - fun generateExit(tx: TransactionBuilder, amountIssued: Amount>, - assetStates: List>, - deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, - generateMoveCommand: () -> CommandData, - generateExitCommand: (Amount>) -> CommandData): Set - = OnLedgerAsset.generateExit(tx, amountIssued, assetStates, deriveState, generateMoveCommand, generateExitCommand) - - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Issued?): Set { - require(groupingKey != null) { "Conserve amount clause can only be used on grouped states" } - val matchedCommands = commands.filter { command -> command.value is FungibleAsset.Commands.Move || command.value is FungibleAsset.Commands.Exit<*> } - val inputAmount: Amount> = inputs.sumFungibleOrNull() ?: throw IllegalArgumentException("there is at least one asset input for group $groupingKey") - val deposit = groupingKey!!.issuer - val outputAmount: Amount> = outputs.sumFungibleOrZero(groupingKey) - - // If we want to remove assets from the ledger, that must be signed for by the issuer and owner. - val exitKeys: Set = inputs.flatMap { it.exitKeys }.toSet() - val exitCommand = matchedCommands.select>(parties = null, signers = exitKeys).filter { it.value.amount.token == groupingKey }.singleOrNull() - val amountExitingLedger: Amount> = exitCommand?.value?.amount ?: Amount(0, groupingKey) - - requireThat { - "there are no zero sized inputs" using inputs.none { it.amount.quantity == 0L } - "for reference ${deposit.reference} at issuer ${deposit.party} the amounts balance: ${inputAmount.quantity} - ${amountExitingLedger.quantity} != ${outputAmount.quantity}" using - (inputAmount == outputAmount + amountExitingLedger) - } - - verifyMoveCommand(inputs, commands) - - // This is safe because we've taken the commands from a collection of C objects at the start - @Suppress("UNCHECKED_CAST") - return matchedCommands.map { it.value }.toSet() - } - - override fun toString(): String = "Conserve amount between inputs and outputs" -} diff --git a/finance/src/main/kotlin/net/corda/contracts/clause/AbstractIssue.kt b/finance/src/main/kotlin/net/corda/contracts/clause/AbstractIssue.kt deleted file mode 100644 index ccf37384b0..0000000000 --- a/finance/src/main/kotlin/net/corda/contracts/clause/AbstractIssue.kt +++ /dev/null @@ -1,56 +0,0 @@ -package net.corda.contracts.clause - -import net.corda.core.contracts.* -import net.corda.core.contracts.clauses.Clause -import net.corda.core.transactions.LedgerTransaction - -/** - * 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>, - val sumOrZero: List.(token: Issued) -> Amount> -) : Clause>() { - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Issued?): Set { - require(groupingKey != null) - // 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 = groupingKey!!.issuer.party - val inputAmount = inputs.sumOrZero(groupingKey) - val outputAmount = outputs.sum() - requireThat { - "the issue command has a nonce" using (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" using (issuer.owningKey in issueCommand.signers) - "output values sum to more than the inputs" using (outputAmount > inputAmount) - } - - // This is safe because we've taken the command from a collection of C objects at the start - @Suppress("UNCHECKED_CAST") - return setOf(issueCommand.value as C) - } -} diff --git a/finance/src/main/kotlin/net/corda/contracts/clause/Net.kt b/finance/src/main/kotlin/net/corda/contracts/clause/Net.kt deleted file mode 100644 index fedf3b6b4f..0000000000 --- a/finance/src/main/kotlin/net/corda/contracts/clause/Net.kt +++ /dev/null @@ -1,102 +0,0 @@ -package net.corda.contracts.clause - -import com.google.common.annotations.VisibleForTesting -import net.corda.contracts.NetCommand -import net.corda.contracts.NetType -import net.corda.contracts.asset.Obligation -import net.corda.contracts.asset.extractAmountsDue -import net.corda.contracts.asset.sumAmountsDue -import net.corda.core.contracts.* -import net.corda.core.contracts.clauses.Clause -import net.corda.core.identity.AbstractParty -import net.corda.core.transactions.LedgerTransaction -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 : Clause() { - override val requiredCommands: Set> = setOf(Obligation.Commands.Net::class.java) - - @Suppress("ConvertLambdaToReference") - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Unit?): Set { - val matchedCommands: List> = commands.filter { it.value is NetCommand } - val command = matchedCommands.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 ((groupInputs, groupOutputs, key) in groups) { - verifyNetCommand(groupInputs, groupOutputs, command, key) - } - return matchedCommands.map { it.value }.toSet() - } - - /** - * Verify a netting command. This handles both close-out and payment netting. - */ - @VisibleForTesting - 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" using (inputs.all { it.template == template }) - "all output states use the same template" using (outputs.all { it.template == template }) - "amounts owed on input and output must match" using (sumAmountsDue(inputBalances) == sumAmountsDue - (outputBalances)) - } - - // TODO: Handle proxies nominated by parties, i.e. a central clearing service - val involvedParties: Set = inputs.map { it.beneficiary.owningKey }.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" } - } - } -} diff --git a/finance/src/main/kotlin/net/corda/contracts/clause/NoZeroSizedOutputs.kt b/finance/src/main/kotlin/net/corda/contracts/clause/NoZeroSizedOutputs.kt deleted file mode 100644 index 9fcafebe3a..0000000000 --- a/finance/src/main/kotlin/net/corda/contracts/clause/NoZeroSizedOutputs.kt +++ /dev/null @@ -1,24 +0,0 @@ -package net.corda.contracts.clause - -import net.corda.core.contracts.* -import net.corda.core.contracts.clauses.Clause -import net.corda.core.transactions.LedgerTransaction - -/** - * Clause for fungible asset contracts, which enforces that no output state should have - * a balance of zero. - */ -open class NoZeroSizedOutputs, C : CommandData, T : Any> : Clause>() { - override fun verify(tx: LedgerTransaction, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Issued?): Set { - requireThat { - "there are no zero sized outputs" using outputs.none { it.amount.quantity == 0L } - } - return emptySet() - } - - override fun toString(): String = "No zero sized outputs" -} diff --git a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java index ed091c6104..6211f53f92 100644 --- a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java +++ b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java @@ -36,7 +36,7 @@ public class CashTestsJava { tx.tweak(tw -> { tw.output(outState); // No command arguments - return tw.failsWith("required net.corda.core.contracts.FungibleAsset.Commands.Move command"); + return tw.failsWith("required net.corda.contracts.asset.Cash.Commands.Move command"); }); tx.tweak(tw -> { tw.output(outState); @@ -49,7 +49,7 @@ public class CashTestsJava { // with different overloads (for some reason). tw.output(CashKt.issuedBy(outState, getMINI_CORP())); tw.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move()); - return tw.failsWith("at least one asset input"); + return tw.failsWith("at least one cash input"); }); // Simple reallocation works. diff --git a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt index eb53ce9305..31edaa68ba 100644 --- a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt @@ -60,16 +60,16 @@ class KotlinCommercialPaperTest : ICommercialPaperTestTemplate { } class KotlinCommercialPaperLegacyTest : ICommercialPaperTestTemplate { - override fun getPaper(): ICommercialPaperState = CommercialPaperLegacy.State( + override fun getPaper(): ICommercialPaperState = CommercialPaper.State( issuance = MEGA_CORP.ref(123), owner = MEGA_CORP, faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), maturityDate = TEST_TX_TIME + 7.days ) - override fun getIssueCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Issue() - override fun getRedeemCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Redeem() - override fun getMoveCommand(): CommandData = CommercialPaperLegacy.Commands.Move() + override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue() + override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem() + override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move() } @RunWith(Parameterized::class) diff --git a/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt b/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt index f8d78a6852..401b8ae324 100644 --- a/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt +++ b/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt @@ -1,28 +1,21 @@ package net.corda.contracts.asset -import net.corda.contracts.clause.AbstractConserveAmount -import net.corda.contracts.clause.AbstractIssue -import net.corda.contracts.clause.NoZeroSizedOutputs import net.corda.core.contracts.* -import net.corda.core.contracts.clauses.AllOf -import net.corda.core.contracts.clauses.FirstOf -import net.corda.core.contracts.clauses.GroupClauseVerifier -import net.corda.core.contracts.clauses.verifyClause import net.corda.core.crypto.SecureHash import net.corda.core.crypto.newSecureRandom import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.internal.Emoji import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState -import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.core.internal.Emoji import net.corda.schemas.SampleCashSchemaV1 import net.corda.schemas.SampleCashSchemaV2 import net.corda.schemas.SampleCashSchemaV3 +import java.security.PublicKey import java.util.* class DummyFungibleContract : OnLedgerAsset() { @@ -31,29 +24,6 @@ class DummyFungibleContract : OnLedgerAsset>): List> = commands.select() - interface Clauses { - class Group : GroupClauseVerifier>(AllOf>( - NoZeroSizedOutputs(), - FirstOf>( - Issue(), - ConserveAmount()) - ) - ) { - override fun groupStates(tx: LedgerTransaction): List>> - = tx.groupStates> { it.amount.token } - } - - class Issue : AbstractIssue( - sum = { sumCash() }, - sumOrZero = { sumCashOrZero(it) } - ) { - override val requiredCommands: Set> = setOf(Commands.Issue::class.java) - } - - @CordaSerializable - class ConserveAmount : AbstractConserveAmount() - } - data class State( override val amount: Amount>, @@ -129,7 +99,69 @@ class DummyFungibleContract : OnLedgerAsset it.amount.token } + + for ((inputs, outputs, key) in groups) { + // Either inputs or outputs could be empty. + val issuer = key.issuer + val currency = key.product + + requireThat { + "there are no zero sized outputs" using (outputs.none { it.amount.quantity == 0L }) + } + + val issueCommand = tx.commands.select().firstOrNull() + if (issueCommand != null) { + verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer) + } else { + val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one input for this group") + val outputAmount = outputs.sumCashOrZero(Issued(issuer, currency)) + + val exitKeys: Set = inputs.flatMap { it.exitKeys }.toSet() + val exitCommand = tx.commands.select(parties = null, signers = exitKeys).filter { it.value.amount.token == key }.singleOrNull() + val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, Issued(issuer, currency)) + + requireThat { + "there are no zero sized inputs" using inputs.none { it.amount.quantity == 0L } + "for reference ${issuer.reference} at issuer ${issuer.party} the amounts balance: ${inputAmount.quantity} - ${amountExitingLedger.quantity} != ${outputAmount.quantity}" using + (inputAmount == outputAmount + amountExitingLedger) + } + + verifyMoveCommand(inputs, tx.commands) + } + } + } + + private fun verifyIssueCommand(inputs: List, + outputs: List, + tx: LedgerTransaction, + issueCommand: AuthenticatedObject, + currency: Currency, + issuer: PartyAndReference) { + // 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 cash claims of arbitrary + // amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some + // as-yet-unwritten identity service. See ADP-22 for discussion. + + // The grouping ensures that all outputs have the same deposit reference and currency. + val inputAmount = inputs.sumCashOrZero(Issued(issuer, currency)) + val outputAmount = outputs.sumCash() + val cashCommands = tx.commands.select() + requireThat { + "the issue command has a nonce" using (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.party in issueCommand.signingParties) + "output states are issued by a command signer" using (issuer.party.owningKey in issueCommand.signers) + "output values sum to more than the inputs" using (outputAmount > inputAmount) + "there is only a single issue command" using (cashCommands.count() == 1) + } + } } diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index a32fa72471..c70d7f3cf8 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -70,7 +70,8 @@ class CashTests : TestDependencyInjectionBase() { // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. vaultService.notifyAll(txs.map { it.tx }) } - override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher) + + override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher) } miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, @@ -100,7 +101,7 @@ class CashTests : TestDependencyInjectionBase() { tweak { output { outState } // No command arguments - this `fails with` "required net.corda.core.contracts.FungibleAsset.Commands.Move command" + this `fails with` "required net.corda.contracts.asset.Cash.Commands.Move command" } tweak { output { outState } @@ -111,7 +112,7 @@ class CashTests : TestDependencyInjectionBase() { output { outState } output { outState `issued by` MINI_CORP } command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } - this `fails with` "at least one asset input" + this `fails with` "at least one cash input" } // Simple reallocation works. tweak { @@ -130,7 +131,7 @@ class CashTests : TestDependencyInjectionBase() { output { outState } command(MINI_CORP_PUBKEY) { Cash.Commands.Move() } - this `fails with` "there is at least one asset input" + this `fails with` "there is at least one cash input for this group" } } @@ -230,15 +231,7 @@ class CashTests : TestDependencyInjectionBase() { command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() } tweak { command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() } - this `fails with` "List has more than one element." - } - tweak { - command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } - this `fails with` "The following commands were not matched at the end of execution" - } - tweak { - command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount.splitEvenly(2).first()) } - this `fails with` "The following commands were not matched at the end of execution" + this `fails with` "there is only a single issue command" } this.verifies() } @@ -372,7 +365,7 @@ class CashTests : TestDependencyInjectionBase() { tweak { command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) } - this `fails with` "required net.corda.core.contracts.FungibleAsset.Commands.Move command" + this `fails with` "required net.corda.contracts.asset.Cash.Commands.Move command" tweak { command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt index 3b20bbed3e..473486588f 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt @@ -14,9 +14,9 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.days import net.corda.core.utilities.hours import net.corda.testing.* -import org.junit.After import net.corda.testing.contracts.DummyState import net.corda.testing.node.MockServices +import org.junit.After import org.junit.Test import java.time.Instant import java.time.temporal.ChronoUnit @@ -77,7 +77,7 @@ class ObligationTests { tweak { output { outState } // No command arguments - this `fails with` "required net.corda.core.contracts.FungibleAsset.Commands.Move command" + this `fails with` "required net.corda.contracts.asset.Obligation.Commands.Move command" } tweak { output { outState } @@ -88,7 +88,7 @@ class ObligationTests { output { outState } output { outState `issued by` MINI_CORP } command(CHARLIE.owningKey) { Obligation.Commands.Move() } - this `fails with` "at least one asset input" + this `fails with` "at least one obligation input" } // Simple reallocation works. tweak { @@ -107,7 +107,7 @@ class ObligationTests { output { outState } command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() } - this `fails with` "there is at least one asset input" + this `fails with` "at least one obligation input" } // Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised @@ -193,15 +193,7 @@ class ObligationTests { command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() } tweak { command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() } - this `fails with` "List has more than one element." - } - tweak { - command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move() } - this `fails with` "The following commands were not matched at the end of execution" - } - tweak { - command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.amount.splitEvenly(2).first()) } - this `fails with` "The following commands were not matched at the end of execution" + this `fails with` "there is only a single issue command" } this.verifies() } @@ -668,7 +660,7 @@ class ObligationTests { tweak { command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token)) } - this `fails with` "required net.corda.core.contracts.FungibleAsset.Commands.Move command" + this `fails with` "required net.corda.contracts.asset.Obligation.Commands.Move command" tweak { command(CHARLIE.owningKey) { Obligation.Commands.Move() } diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 5e089f8c86..3910859d4d 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -490,7 +490,7 @@ class TwoPartyTradeFlowTests { fun `dependency with error on buyer side`() { mockNet = MockNetwork(false) ledger(initialiseSerialization = false) { - runWithError(true, false, "at least one asset input") + runWithError(true, false, "at least one cash input") } } @@ -498,7 +498,7 @@ class TwoPartyTradeFlowTests { fun `dependency with error on seller side`() { mockNet = MockNetwork(false) ledger(initialiseSerialization = false) { - runWithError(false, true, "Issuances must have a time-window") + runWithError(false, true, "Issuances have a time-window") } }