Removing clauses (#1195)

* Removing clauses

* Removing clauses from JavaCommercialPaper

* Addressing review comments
This commit is contained in:
mkit 2017-08-09 15:25:31 +01:00 committed by GitHub
parent 28610868c4
commit 0b33214fea
17 changed files with 614 additions and 994 deletions

View File

@ -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 <R> requireThat(body: Requirements.() -> R) = Requirements.body()
/** Filters the command list by type, party and public key all at once. */
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.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 }.

View File

@ -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<State, Commands, State> {
// 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<LedgerTransaction.InOutGroup<State, State>> groupStates(@NotNull LedgerTransaction tx) {
return tx.groupStates(State.class, State::withoutOwner);
}
}
@SuppressWarnings("unused")
class Move extends Clause<State, Commands, State> {
@NotNull
@Override
public Set<Class<? extends CommandData>> getRequiredCommands() {
return Collections.singleton(Commands.Move.class);
}
@NotNull
@Override
public Set<Commands> verify(@NotNull LedgerTransaction tx,
@NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs,
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
State groupingKey) {
AuthenticatedObject<Commands.Move> 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<State, Commands, State> {
@NotNull
@Override
public Set<Class<? extends CommandData>> getRequiredCommands() {
return Collections.singleton(Commands.Redeem.class);
}
@NotNull
@Override
public Set<Commands> verify(@NotNull LedgerTransaction tx,
@NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs,
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
State groupingKey) {
AuthenticatedObject<Commands.Redeem> 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<Issued<Currency>> 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<State, Commands, State> {
@NotNull
@Override
public Set<Class<? extends CommandData>> getRequiredCommands() {
return Collections.singleton(Commands.Issue.class);
}
@NotNull
@Override
public Set<Commands> verify(@NotNull LedgerTransaction tx,
@NotNull List<? extends State> inputs,
@NotNull List<? extends State> outputs,
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
State groupingKey) {
AuthenticatedObject<Commands.Issue> 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<LedgerTransaction.InOutGroup<State, State>> 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<AuthenticatedObject<CommandData>> commands = tx.getCommands().stream().filter(
it -> {
return it.getValue() instanceof Commands;
}
).collect(Collectors.toList());
final AuthenticatedObject<CommandData> command = Iterables.getOnlyElement(commands);
final TimeWindow timeWindow = tx.getTimeWindow();
for (final LedgerTransaction.InOutGroup<State, State> group : groups) {
final List<State> inputs = group.getInputs();
final List<State> outputs = group.getOutputs();
if (command.getValue() instanceof Commands.Move) {
final AuthenticatedObject<Commands.Move> 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<Commands.Redeem> 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<Issued<Currency>> 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<Commands.Issue> 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

View File

@ -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<Currency>,
val maturityDate: Instant
)
override fun verify(tx: LedgerTransaction) = verifyClause(tx, Clauses.Group(), tx.commands.select<Commands>())
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<AbstractParty>
get() = listOf(owner)
val token: Issued<Terms>
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<Issued<Currency>>): ICommercialPaperState = copy(faceValue = newFaceValue)
override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate)
// DOCSTART VaultIndexedQueryCriteria
/** Object Relational Mapping support. */
override fun supportedSchemas(): Iterable<MappedSchema> = 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<State, Commands, Issued<Terms>>(
AnyOf(
Redeem(),
Move(),
Issue())) {
override fun groupStates(tx: LedgerTransaction): List<LedgerTransaction.InOutGroup<State, Issued<Terms>>>
= tx.groupStates<State, Issued<Terms>> { it.token }
}
class Issue : AbstractIssue<State, Commands, Terms>(
{ map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() },
{ token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
override fun verify(tx: LedgerTransaction,
inputs: List<State>,
outputs: List<State>,
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms>?): Set<Commands> {
val consumedCommands = super.verify(tx, inputs, outputs, commands, groupingKey)
commands.requireSingleCommand<Commands.Issue>()
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<State, Commands, Issued<Terms>>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Move::class.java)
override fun verify(tx: LedgerTransaction,
inputs: List<State>,
outputs: List<State>,
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms>?): Set<Commands> {
val command = commands.requireSingleCommand<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.
}
return setOf(command.value)
}
}
class Redeem : Clause<State, Commands, Issued<Terms>>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Redeem::class.java)
override fun verify(tx: LedgerTransaction,
inputs: List<State>,
outputs: List<State>,
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms>?): Set<Commands> {
// 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<Commands.Redeem>()
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<CommercialPaper.Commands>()
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<Issued<Currency>>, 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<Issued<Currency>>, 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<State>, 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<State>, 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)

View File

@ -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<Issued<Currency>>,
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<Issued<Currency>>): 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<CommercialPaperLegacy.Commands>()
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<Issued<Currency>>, 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<State>, 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<State>, 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))
}
}

View File

@ -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<Currency, Cash.Commands, Cash.State>() {
*/
// 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<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Cash.Commands>>
= commands.select<Cash.Commands>()
interface Clauses {
class Group : GroupClauseVerifier<State, Commands, Issued<Currency>>(AllOf<State, Commands, Issued<Currency>>(
NoZeroSizedOutputs<State, Commands, Currency>(),
FirstOf<State, Commands, Issued<Currency>>(
Issue(),
ConserveAmount())
)
) {
override fun groupStates(tx: LedgerTransaction): List<LedgerTransaction.InOutGroup<State, Issued<Currency>>>
= tx.groupStates<State, Issued<Currency>> { it.amount.token }
}
class Issue : AbstractIssue<State, Commands, Currency>(
sum = { sumCash() },
sumOrZero = { sumCashOrZero(it) }
) {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
}
@CordaSerializable
class ConserveAmount : AbstractConserveAmount<State, Commands, Currency>()
}
// DOCSTART 1
/** A state representing a cash claim against some party. */
data class State(
@ -120,7 +91,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
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<Currency, Cash.Commands, Cash.State>() {
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
*/
fun generateIssue(tx: TransactionBuilder, amount: Amount<Issued<Currency>>, 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<State>, amount: Amount<Issued<Currency>>, owner: AbstractParty)
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
@ -174,8 +145,73 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
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<Commands.Issue>().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<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
val exitCommand = tx.commands.select<Commands.Exit>(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<Commands.Move>(inputs, tx.commands)
}
}
}
private fun verifyIssueCommand(inputs: List<State>,
outputs: List<State>,
tx: LedgerTransaction,
issueCommand: AuthenticatedObject<Commands.Issue>,
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<Commands.Issue>()
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.

View File

@ -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<Commodity, CommodityContract.Commands, C
*/
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html")
/**
* The clauses for this contract are essentially:
*
* 1. Group all commodity input and output states in a transaction by issued commodity, and then for each group:
* a. Check there are no zero sized output states in the group, and throw an error if so.
* b. Check for an issuance command, and do standard issuance checks if so, THEN STOP. Otherwise:
* c. Check for a move command (required) and an optional exit command, and that input and output totals are correctly
* conserved (output = input - exit)
*/
interface Clauses {
/**
* Grouping clause to extract input and output states into matched groups and then run a set of clauses over
* each group.
*/
class Group : GroupClauseVerifier<State, Commands, Issued<Commodity>>(AnyOf(
NoZeroSizedOutputs<State, Commands, Commodity>(),
Issue(),
ConserveAmount())) {
/**
* Group commodity states by issuance definition (issuer and underlying commodity).
*/
override fun groupStates(tx: LedgerTransaction)
= tx.groupStates<State, Issued<Commodity>> { it.amount.token }
}
/**
* Standard issue clause, specialised to match the commodity issue command.
*/
class Issue : AbstractIssue<State, Commands, Commodity>(
sum = { sumCommodities() },
sumOrZero = { sumCommoditiesOrZero(it) }
) {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
}
/**
* Standard clause for conserving the amount from input to output.
*/
@CordaSerializable
class ConserveAmount : AbstractConserveAmount<State, Commands, Commodity>()
}
/** A state representing a commodity claim against some party */
data class State(
override val amount: Amount<Issued<Commodity>>,
@ -138,8 +90,71 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, C
data class Exit(override val amount: Amount<Issued<Commodity>>) : Commands, FungibleAsset.Commands.Exit<Commodity>
}
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<Commands.Issue>().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<Commands.Exit>(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<Commands.Move>(inputs, tx.commands)
}
}
}
private fun verifyIssueCommand(inputs: List<State>,
outputs: List<State>,
tx: LedgerTransaction,
issueCommand: AuthenticatedObject<Commands.Issue>,
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<CommodityContract.Commands>()
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<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Commands>>
= commands.select<CommodityContract.Commands>()

View File

@ -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<P : Any> {
val template: Obligation.Terms<P>
}
/**
* 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<P : Any>(
val partyKeys: Set<AbstractParty>,
override val template: Obligation.Terms<P>
) : NetState<P>
/**
* 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<P : Any>(
override val template: Obligation.Terms<P>
) : NetState<P>
// 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<Currency>()
@ -55,186 +84,6 @@ class Obligation<P : Any> : 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<P : Any> : GroupClauseVerifier<State<P>, Commands, Issued<Terms<P>>>(
AllOf(
NoZeroSizedOutputs<State<P>, Commands, Terms<P>>(),
FirstOf(
SetLifecycle<P>(),
AllOf(
VerifyLifecycle<State<P>, Commands, Issued<Terms<P>>, P>(),
FirstOf(
Settle<P>(),
Issue(),
ConserveAmount()
)
)
)
)
) {
override fun groupStates(tx: LedgerTransaction): List<LedgerTransaction.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>>
= tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.amount.token }
}
/**
* Generic issuance clause
*/
class Issue<P : Any> : AbstractIssue<State<P>, Commands, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
}
/**
* Generic move/exit clause for fungible assets
*/
class ConserveAmount<P : Any> : AbstractConserveAmount<State<P>, Commands, Terms<P>>()
/**
* Clause for supporting netting of obligations.
*/
class Net<C : CommandData, P : Any> : NetClause<C, P>() {
val lifecycleClause = Clauses.VerifyLifecycle<ContractState, C, Unit, P>()
override fun toString(): String = "Net obligations"
override fun verify(tx: LedgerTransaction, inputs: List<ContractState>, outputs: List<ContractState>, commands: List<AuthenticatedObject<C>>, groupingKey: Unit?): Set<C> {
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<P : Any> : Clause<State<P>, Commands, Issued<Terms<P>>>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.SetLifecycle::class.java)
override fun verify(tx: LedgerTransaction,
inputs: List<State<P>>,
outputs: List<State<P>>,
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms<P>>?): Set<Commands> {
val command = commands.requireSingleCommand<Commands.SetLifecycle>()
Obligation<P>().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<P : Any> : Clause<State<P>, Commands, Issued<Terms<P>>>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Settle::class.java)
override fun verify(tx: LedgerTransaction,
inputs: List<State<P>>,
outputs: List<State<P>>,
commands: List<AuthenticatedObject<Commands>>,
groupingKey: Issued<Terms<P>>?): Set<Commands> {
require(groupingKey != null)
val command = commands.requireSingleCommand<Commands.Settle<P>>()
val obligor = groupingKey!!.issuer.party
val template = groupingKey.product
val inputAmount: Amount<Issued<Terms<P>>> = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
val outputAmount: Amount<Issued<Terms<P>>> = 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<FungibleAsset<*>>()
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<MoveCommand>()
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<P>()
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<P>().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<S : ContractState, C : CommandData, T : Any, P : Any> : Clause<S, C, T>() {
override fun verify(tx: LedgerTransaction,
inputs: List<S>,
outputs: List<S>,
commands: List<AuthenticatedObject<C>>,
groupingKey: T?): Set<C>
= verify(inputs.filterIsInstance<State<P>>(), outputs.filterIsInstance<State<P>>())
private fun verify(inputs: List<State<P>>,
outputs: List<State<P>>): Set<C> {
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<P : Any> : Contract {
data class Exit<P : Any>(override val amount: Amount<Issued<Terms<P>>>) : Commands, FungibleAsset.Commands.Exit<Terms<P>>
}
override fun verify(tx: LedgerTransaction) = verifyClause<Commands>(tx, FirstOf<ContractState, Commands, Unit>(
Clauses.Net<Commands, P>(),
Clauses.Group<P>()
), tx.commands.select<Obligation.Commands>())
override fun verify(tx: LedgerTransaction) {
val netCommand = tx.commands.select<Commands.Net>().firstOrNull()
if (netCommand != null) {
verifyLifecycleCommand(tx.inputStates, tx.outputStates)
verifyNetCommand(tx, netCommand)
} else {
val groups = tx.groupStates { it: Obligation.State<P> -> 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<Commands.SetLifecycle>().firstOrNull()
if (setLifecycleCommand != null) {
verifySetLifecycleCommand(inputs, outputs, tx, setLifecycleCommand)
} else {
verifyLifecycleCommand(inputs, outputs)
val settleCommand = tx.commands.select<Commands.Settle<P>>().firstOrNull()
if (settleCommand != null) {
verifySettleCommand(tx, inputs, outputs, settleCommand, key)
} else {
val issueCommand = tx.commands.select<Commands.Issue>().firstOrNull()
if (issueCommand != null) {
verifyIssueCommand(tx, inputs, outputs, issueCommand, key)
} else {
conserveAmount(tx, inputs, outputs, key)
}
}
}
}
}
}
private fun conserveAmount(tx: LedgerTransaction,
inputs: List<FungibleAsset<Terms<P>>>,
outputs: List<FungibleAsset<Terms<P>>>,
key: Issued<Terms<P>>) {
val issuer = key.issuer
val terms = key.product
val inputAmount = inputs.sumObligationsOrNull<P>() ?: 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<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
val exitCommand = tx.commands.select<Commands.Exit<P>>(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<Commands.Move>(inputs, tx.commands)
}
private fun verifyIssueCommand(tx: LedgerTransaction,
inputs: List<FungibleAsset<Terms<P>>>,
outputs: List<FungibleAsset<Terms<P>>>,
issueCommand: AuthenticatedObject<Commands.Issue>,
key: Issued<Terms<P>>) {
// 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<P>()
val issueCommands = tx.commands.select<Commands.Issue>()
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<FungibleAsset<Terms<P>>>,
outputs: List<FungibleAsset<Terms<P>>>,
command: AuthenticatedObject<Commands.Settle<P>>,
groupingKey: Issued<Terms<P>>) {
val obligor = groupingKey.issuer.party
val template = groupingKey.product
val inputAmount: Amount<Issued<Terms<P>>> = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
val outputAmount: Amount<Issued<Terms<P>>> = 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<FungibleAsset<*>>()
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<MoveCommand>()
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<P>()
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<P>().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<ContractState>, outputs: List<ContractState>) {
val filteredInputs = inputs.filterIsInstance<State<P>>()
val filteredOutputs = outputs.filterIsInstance<State<P>>()
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<NetCommand>) {
val groups = when (command.value.type) {
NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState }
NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> 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<PublicKey> = 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<P : Any> : Contract {
* @param notary the notary for this transaction's outputs.
*/
fun generateCashIssue(tx: TransactionBuilder,
obligor: AbstractParty,
amount: Amount<Issued<Currency>>,
dueBefore: Instant,
beneficiary: AbstractParty,
notary: Party) {
obligor: AbstractParty,
amount: Amount<Issued<Currency>>,
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<P : Any> : 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<Obligation.Terms<P>>,
@ -682,7 +730,7 @@ fun <P : Any> extractAmountsDue(product: Obligation.Terms<P>, states: Iterable<O
/**
* Net off the amounts due between parties.
*/
fun <P: AbstractParty, T : Any> netAmountsDue(balances: Map<Pair<P, P>, Amount<T>>): Map<Pair<P, P>, Amount<T>> {
fun <P : AbstractParty, T : Any> netAmountsDue(balances: Map<Pair<P, P>, Amount<T>>): Map<Pair<P, P>, Amount<T>> {
val nettedBalances = HashMap<Pair<P, P>, Amount<T>>()
balances.forEach { balance ->
@ -709,7 +757,7 @@ fun <P: AbstractParty, T : Any> netAmountsDue(balances: Map<Pair<P, P>, Amount<T
* @param P type of party to operate on.
* @param T token that balances represent
*/
fun <P: AbstractParty, T : Any> sumAmountsDue(balances: Map<Pair<P, P>, Amount<T>>): Map<P, Long> {
fun <P : AbstractParty, T : Any> sumAmountsDue(balances: Map<Pair<P, P>, Amount<T>>): Map<P, Long> {
val sum = HashMap<P, Long>()
// Fill the map with zeroes initially

View File

@ -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<S : FungibleAsset<T>, C : CommandData, T : Any> : Clause<S, C, Issued<T>>() {
private companion object {
val log = loggerFor<AbstractConserveAmount<*, *, *>>()
}
/**
* 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<Issued<T>>,
assetStates: List<StateAndRef<S>>,
deriveState: (TransactionState<S>, Amount<Issued<T>>, AbstractParty) -> TransactionState<S>,
generateMoveCommand: () -> CommandData,
generateExitCommand: (Amount<Issued<T>>) -> CommandData): Set<PublicKey>
= OnLedgerAsset.generateExit(tx, amountIssued, assetStates, deriveState, generateMoveCommand, generateExitCommand)
override fun verify(tx: LedgerTransaction,
inputs: List<S>,
outputs: List<S>,
commands: List<AuthenticatedObject<C>>,
groupingKey: Issued<T>?): Set<C> {
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<Issued<T>> = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for group $groupingKey")
val deposit = groupingKey!!.issuer
val outputAmount: Amount<Issued<T>> = 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<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
val exitCommand = matchedCommands.select<FungibleAsset.Commands.Exit<T>>(parties = null, signers = exitKeys).filter { it.value.amount.token == groupingKey }.singleOrNull()
val amountExitingLedger: Amount<Issued<T>> = 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<FungibleAsset.Commands.Move>(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"
}

View File

@ -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<in S : ContractState, C : CommandData, T : Any>(
val sum: List<S>.() -> Amount<Issued<T>>,
val sumOrZero: List<S>.(token: Issued<T>) -> Amount<Issued<T>>
) : Clause<S, C, Issued<T>>() {
override fun verify(tx: LedgerTransaction,
inputs: List<S>,
outputs: List<S>,
commands: List<AuthenticatedObject<C>>,
groupingKey: Issued<T>?): Set<C> {
require(groupingKey != null)
// TODO: Take in matched commands as a parameter
val issueCommand = commands.requireSingleCommand<IssueCommand>()
// 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)
}
}

View File

@ -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<P : Any> {
val template: Obligation.Terms<P>
}
/**
* 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<P : Any>(
val partyKeys: Set<AbstractParty>,
override val template: Obligation.Terms<P>
) : NetState<P>
/**
* 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<P : Any>(
override val template: Obligation.Terms<P>
) : NetState<P>
/**
* Clause for netting contract states. Currently only supports obligation contract.
*/
// TODO: Make this usable for any nettable contract states
open class NetClause<C : CommandData, P : Any> : Clause<ContractState, C, Unit>() {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Obligation.Commands.Net::class.java)
@Suppress("ConvertLambdaToReference")
override fun verify(tx: LedgerTransaction,
inputs: List<ContractState>,
outputs: List<ContractState>,
commands: List<AuthenticatedObject<C>>,
groupingKey: Unit?): Set<C> {
val matchedCommands: List<AuthenticatedObject<C>> = commands.filter { it.value is NetCommand }
val command = matchedCommands.requireSingleCommand<Obligation.Commands.Net>()
val groups = when (command.value.type) {
NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState }
NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> 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<Obligation.State<P>>,
outputs: List<Obligation.State<P>>,
command: AuthenticatedObject<NetCommand>,
netState: NetState<P>) {
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<PublicKey> = 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" }
}
}
}

View File

@ -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<in S : FungibleAsset<T>, C : CommandData, T : Any> : Clause<S, C, Issued<T>>() {
override fun verify(tx: LedgerTransaction,
inputs: List<S>,
outputs: List<S>,
commands: List<AuthenticatedObject<C>>,
groupingKey: Issued<T>?): Set<C> {
requireThat {
"there are no zero sized outputs" using outputs.none { it.amount.quantity == 0L }
}
return emptySet()
}
override fun toString(): String = "No zero sized outputs"
}

View File

@ -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.

View File

@ -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)

View File

@ -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<Currency, DummyFungibleContract.Commands, DummyFungibleContract.State>() {
@ -31,29 +24,6 @@ class DummyFungibleContract : OnLedgerAsset<Currency, DummyFungibleContract.Comm
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<DummyFungibleContract.Commands>>
= commands.select<DummyFungibleContract.Commands>()
interface Clauses {
class Group : GroupClauseVerifier<State, Commands, Issued<Currency>>(AllOf<State, Commands, Issued<Currency>>(
NoZeroSizedOutputs<State, Commands, Currency>(),
FirstOf<State, Commands, Issued<Currency>>(
Issue(),
ConserveAmount())
)
) {
override fun groupStates(tx: LedgerTransaction): List<LedgerTransaction.InOutGroup<State, Issued<Currency>>>
= tx.groupStates<State, Issued<Currency>> { it.amount.token }
}
class Issue : AbstractIssue<State, Commands, Currency>(
sum = { sumCash() },
sumOrZero = { sumCashOrZero(it) }
) {
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
}
@CordaSerializable
class ConserveAmount : AbstractConserveAmount<State, Commands, Currency>()
}
data class State(
override val amount: Amount<Issued<Currency>>,
@ -129,7 +99,69 @@ class DummyFungibleContract : OnLedgerAsset<Currency, DummyFungibleContract.Comm
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) {
val groups = tx.groupStates { it: 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<Commands.Issue>().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<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
val exitCommand = tx.commands.select<Commands.Exit>(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<Commands.Move>(inputs, tx.commands)
}
}
}
private fun verifyIssueCommand(inputs: List<State>,
outputs: List<State>,
tx: LedgerTransaction,
issueCommand: AuthenticatedObject<Commands.Issue>,
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<Commands.Issue>()
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)
}
}
}

View File

@ -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() }

View File

@ -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() }

View File

@ -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")
}
}