Merged in rnicoll-commodity-cleanup (pull request #256)

Clean up contracts
This commit is contained in:
Ross Nicoll 2016-08-03 17:38:07 +01:00
commit 15eef7adab
7 changed files with 352 additions and 378 deletions

View File

@ -6,7 +6,6 @@ import com.r3corda.contracts.asset.sumCashBy
import com.r3corda.contracts.clause.AbstractIssue
import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.*
import com.r3corda.core.crypto.NullPublicKey
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort
@ -50,8 +49,7 @@ class CommercialPaper : ClauseVerifier() {
val maturityDate: Instant
)
override val clauses: List<SingleClause>
get() = listOf(Clauses.Group())
override val clauses = listOf(Clauses.Group())
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
= tx.commands.select<Commands>()
@ -82,32 +80,27 @@ class CommercialPaper : ClauseVerifier() {
interface Clauses {
class Group : GroupClauseVerifier<State, Issued<Terms>>() {
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val clauses: List<GroupClause<State, Issued<Terms>>>
get() = listOf(
Redeem(),
Move(),
Issue())
override val ifNotMatched = MatchBehaviour.ERROR
override val ifMatched = MatchBehaviour.END
override val clauses = listOf(
Redeem(),
Move(),
Issue()
)
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
= tx.groupStates<State, Issued<Terms>> { it.token }
}
abstract class AbstractGroupClause: GroupClause<State, Issued<Terms>> {
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val ifNotMatched = MatchBehaviour.CONTINUE
override val ifMatched = MatchBehaviour.END
}
class Issue : AbstractIssue<State, 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>>
get() = setOf(Commands.Issue::class.java)
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,
@ -127,8 +120,7 @@ class CommercialPaper : ClauseVerifier() {
}
class Move: AbstractGroupClause() {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Move::class.java)
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Move::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,

View File

@ -184,7 +184,7 @@ class FloatingRatePaymentEvent(date: LocalDate,
* This is just a representation of a vanilla Fixed vs Floating (same currency) IRS in the R3 prototype model.
*/
class InterestRateSwap() : ClauseVerifier() {
override val legalContractReference: SecureHash = SecureHash.sha256("is_this_the_text_of_the_contract ? TBD")
override val legalContractReference = SecureHash.sha256("is_this_the_text_of_the_contract ? TBD")
/**
* This Common area contains all the information that is not leg specific.
@ -457,10 +457,8 @@ class InterestRateSwap() : ClauseVerifier() {
* helper functions for the clauses.
*/
abstract class AbstractIRSClause : GroupClause<State, String> {
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val ifMatched = MatchBehaviour.END
override val ifNotMatched = MatchBehaviour.CONTINUE
// These functions may make more sense to use for basket types, but for now let's leave them here
fun checkLegDates(legs: Array<CommonLeg>) {
@ -504,27 +502,20 @@ class InterestRateSwap() : ClauseVerifier() {
}
class Group : GroupClauseVerifier<State, String>() {
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val ifMatched = MatchBehaviour.END
override val ifNotMatched = MatchBehaviour.ERROR
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, String>> {
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, String>>
// Group by Trade ID for in / out states
return tx.groupStates() { state: InterestRateSwap.State -> state.common.tradeID }
}
= tx.groupStates() { state -> state.common.tradeID }
override val clauses: List<GroupClause<State, String>>
get() = listOf(Agree(), Fix(), Pay(), Mature())
override val clauses = listOf(Agree(), Fix(), Pay(), Mature())
}
class Timestamped : SingleClause {
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val requiredCommands: Set<Class<out CommandData>>
get() = emptySet()
override val ifMatched = MatchBehaviour.CONTINUE
override val ifNotMatched = MatchBehaviour.ERROR
override val requiredCommands = emptySet<Class<out CommandData>>()
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
// TODO: This needs to either be the notary used for the inputs, or otherwise
@ -537,8 +528,7 @@ class InterestRateSwap() : ClauseVerifier() {
}
class Agree : AbstractIRSClause() {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Agree::class.java)
override val requiredCommands = setOf(Commands.Agree::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,
@ -574,8 +564,7 @@ class InterestRateSwap() : ClauseVerifier() {
}
class Fix : AbstractIRSClause() {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Refix::class.java)
override val requiredCommands = setOf(Commands.Refix::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,
@ -620,8 +609,7 @@ class InterestRateSwap() : ClauseVerifier() {
}
class Pay : AbstractIRSClause() {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Pay::class.java)
override val requiredCommands = setOf(Commands.Pay::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,
@ -637,8 +625,7 @@ class InterestRateSwap() : ClauseVerifier() {
}
class Mature : AbstractIRSClause() {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Mature::class.java)
override val requiredCommands = setOf(Commands.Mature::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,
@ -748,7 +735,7 @@ class InterestRateSwap() : ClauseVerifier() {
/**
* Just makes printing it out a bit better for those who don't have 80000 column wide monitors.
*/
fun prettyPrint(): String = toString().replace(",", "\n")
fun prettyPrint() = toString().replace(",", "\n")
}

View File

@ -33,7 +33,7 @@ val CASH_PROGRAM_ID = Cash()
* At the same time, other contracts that just want money and don't care much who is currently holding it in their
* vaults can ignore the issuer/depositRefs and just examine the amount fields.
*/
class Cash : ClauseVerifier() {
class Cash : OnLedgerAsset<Currency, Cash.State>() {
/**
* TODO:
* 1) hash should be of the contents, not the URI
@ -45,19 +45,16 @@ class Cash : ClauseVerifier() {
* that is inconsistent with the legal contract.
*/
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html")
override val clauses: List<SingleClause>
get() = listOf(Clauses.Group())
override val conserveClause: AbstractConserveAmount<State, Currency> = Clauses.ConserveAmount()
override val clauses = listOf(Clauses.Group())
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
= tx.commands.select<Cash.Commands>()
interface Clauses {
class Group : GroupClauseVerifier<State, Issued<Currency>>() {
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val clauses: List<GroupClause<State, Issued<Currency>>>
get() = listOf(
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
override val clauses = listOf(
NoZeroSizedOutputs<State, Currency>(),
Issue(),
ConserveAmount())
@ -66,9 +63,11 @@ class Cash : ClauseVerifier() {
= tx.groupStates<State, Issued<Currency>> { it.issuanceDef }
}
class Issue : AbstractIssue<State, Currency>({ sumCash() }, { token -> sumCashOrZero(token) }) {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Issue::class.java)
class Issue : AbstractIssue<State, Currency>(
sum = { sumCash() },
sumOrZero = { sumCashOrZero(it) }
) {
override val requiredCommands = setOf(Commands.Issue::class.java)
}
class ConserveAmount : AbstractConserveAmount<State, Currency>()
@ -84,15 +83,11 @@ class Cash : ClauseVerifier() {
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: PublicKey)
: this(Amount(amount.quantity, Issued<Currency>(deposit, amount.token)), owner)
override val deposit: PartyAndReference
get() = amount.token.issuer
override val exitKeys: Collection<PublicKey>
get() = setOf(deposit.party.owningKey)
override val deposit = amount.token.issuer
override val exitKeys = setOf(deposit.party.owningKey)
override val contract = CASH_PROGRAM_ID
override val issuanceDef: Issued<Currency>
get() = amount.token
override val participants: List<PublicKey>
get() = listOf(owner)
override val issuanceDef = amount.token
override val participants = listOf(owner)
override fun move(newAmount: Amount<Issued<Currency>>, newOwner: PublicKey): FungibleAsset<Currency>
= copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner)
@ -126,75 +121,6 @@ class Cash : ClauseVerifier() {
data class Exit(override val amount: Amount<Issued<Currency>>) : Commands, FungibleAsset.Commands.Exit<Currency>
}
/**
* Generate an transaction exiting cash 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 changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
* necessarily owned by us.
* @param cashStates the cash 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 exit funds held by others.
* @return the public key of the cash issuer, who must sign the transaction for it to be valid.
*/
// TODO: I'm not at all sure we should support exiting funds others hold, but that's a bigger discussion around the
// contract logic, not just this function.
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<Currency>>,
changeKey: PublicKey, cashStates: List<StateAndRef<State>>): PublicKey {
val currency = amountIssued.token.product
val issuer = amountIssued.token.issuer.party
val amount = Amount(amountIssued.quantity, currency)
var acceptableCoins = cashStates.filter { ref -> ref.state.data.amount.token == amountIssued.token }
val notary = acceptableCoins.firstOrNull()?.state?.notary
// TODO: We should be prepared to produce multiple transactions exiting inputs from
// different notaries, or at least group states by notary and take the set with the
// highest total value
acceptableCoins = acceptableCoins.filter { it.state.notary == notary }
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, Amount(amount.quantity, currency))
val takeChangeFrom = gathered.lastOrNull()
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
Amount<Issued<Currency>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
} else {
null
}
val outputs: List<TransactionState<Cash.State>> = if (change != null) {
// Add a change output and adjust the last output downwards.
listOf(TransactionState(State(amountIssued.token.issuer, Amount(change.quantity, currency), changeKey),
notary!!))
} else emptyList()
for (state in gathered) tx.addInputState(state)
for (state in outputs) tx.addOutputState(state)
tx.addCommand(Commands.Exit(amountIssued), amountIssued.token.issuer.party.owningKey)
return amountIssued.token.issuer.party.owningKey
}
/**
* Gather coins from the given list of states, sufficient to match or exceed the given amount.
*
* @param acceptableCoins list of states to use as inputs.
* @param amount the amount to gather states up to.
* @throws InsufficientBalanceException if there isn't enough value in the states to cover the requested amount.
*/
@Throws(InsufficientBalanceException::class)
private fun gatherCoins(acceptableCoins: List<StateAndRef<State>>,
amount: Amount<Currency>): Pair<ArrayList<StateAndRef<State>>, Amount<Currency>> {
val gathered = arrayListOf<StateAndRef<State>>()
var gatheredAmount = Amount(0, amount.token)
for (c in acceptableCoins) {
if (gatheredAmount >= amount) break
gathered.add(c)
gatheredAmount += Amount(c.state.data.amount.quantity, amount.token)
}
if (gatheredAmount < amount)
throw InsufficientBalanceException(amount - gatheredAmount)
return Pair(gathered, gatheredAmount)
}
/**
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.
*/
@ -208,83 +134,15 @@ class Cash : ClauseVerifier() {
check(tx.inputStates().isEmpty())
check(tx.outputStates().map { it.data }.sumCashOrNull() == null)
val at = amount.token.issuer
tx.addOutputState(TransactionState(Cash.State(amount, owner), notary))
tx.addCommand(Cash.Commands.Issue(), at.party.owningKey)
tx.addOutputState(TransactionState(State(amount, owner), notary))
tx.addCommand(generateIssueCommand(), at.party.owningKey)
}
/**
* Generate a transaction that consumes one or more of the given input states to move money to the given pubkey.
* Note that the wallet list is not updated: it's up to you to do that.
*
* @param onlyFromParties if non-null, the wallet will be filtered to only include cash states issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of cash claims they are willing to accept.
*/
@Throws(InsufficientBalanceException::class)
fun generateSpend(tx: TransactionBuilder, amount: Amount<Currency>, to: PublicKey,
cashStates: List<StateAndRef<State>>, onlyFromParties: Set<Party>? = null): List<PublicKey> {
// Discussion
//
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
//
// First we must select a set of cash states (which for convenience we will call 'coins' here, as in bitcoinj).
// The input states can be considered our "wallet", and may consist of coins of different currencies, and from
// different institutions and deposits.
//
// Coin selection is a complex problem all by itself and many different approaches can be used. It is easily
// possible for different actors to use different algorithms and approaches that, for example, compete on
// privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of
// obfuscation and so on.
//
// Having selected coins of the right currency, we must craft output states for the amount we're sending and
// the "change", which goes back to us. The change is required to make the amounts balance. We may need more
// than one change output in order to avoid merging coins from different deposits. The point of this design
// is to ensure that ledger entries are immutable and globally identifiable.
//
// Finally, we add the states to the provided partial transaction.
val currency = amount.token
val acceptableCoins = run {
val ofCurrency = cashStates.filter { it.state.data.amount.token.product == currency }
if (onlyFromParties != null)
ofCurrency.filter { it.state.data.deposit.party in onlyFromParties }
else
ofCurrency
}
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount)
val takeChangeFrom = gathered.firstOrNull()
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
Amount<Issued<Currency>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
} else {
null
}
val keysUsed = gathered.map { it.state.data.owner }.toSet()
val states = gathered.groupBy { it.state.data.deposit }.map {
val coins = it.value
val totalAmount = coins.map { it.state.data.amount }.sumOrThrow()
TransactionState(State(totalAmount, to), coins.first().state.notary)
}
val outputs = if (change != null) {
// Just copy a key across as the change key. In real life of course, this works but leaks private data.
// In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow
// value flows through the transaction graph.
val changeKey = gathered.first().state.data.owner
// Add a change output and adjust the last output downwards.
states.subList(0, states.lastIndex) +
states.last().let { TransactionState(it.data.copy(amount = it.data.amount - change), it.notary) } +
TransactionState(State(change, changeKey), gathered.last().state.notary)
} else states
for (state in gathered) tx.addInputState(state)
for (state in outputs) tx.addOutputState(state)
// What if we already have a move command with the right keys? Filter it out here or in platform code?
val keysList = keysUsed.toList()
tx.addCommand(Commands.Move(), keysList)
return keysList
}
override fun deriveState(txState: TransactionState<State>, amount: Amount<Issued<Currency>>, owner: PublicKey)
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
override fun generateExitCommand(amount: Amount<Issued<Currency>>) = Commands.Exit(amount)
override fun generateIssueCommand() = Commands.Issue()
override fun generateMoveCommand() = Commands.Move()
}
// Small DSL extensions.
@ -298,7 +156,7 @@ fun Iterable<ContractState>.sumCashBy(owner: PublicKey): Amount<Issued<Currency>
/**
* Sums the cash states in the list, throwing an exception if there are none, or if any of the cash
* states cannot be added together (i.e. are different currencies).
* states cannot be added together (i.e. are different currencies or issuers).
*/
fun Iterable<ContractState>.sumCash(): Amount<Issued<Currency>> = filterIsInstance<Cash.State>().map { it.amount }.sumOrThrow()

View File

@ -9,8 +9,6 @@ import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.newSecureRandom
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.node.services.Wallet
import com.r3corda.core.utilities.Emoji
import java.security.PublicKey
import java.util.*
@ -31,7 +29,7 @@ val COMMODITY_PROGRAM_ID = CommodityContract()
* internal accounting by the issuer (it might be, for example, a warehouse and/or location within a warehouse).
*/
// TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc.
class CommodityContract : ClauseVerifier() {
class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
/**
* TODO:
* 1) hash should be of the contents, not the URI
@ -44,6 +42,8 @@ class CommodityContract : ClauseVerifier() {
*/
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html")
override val conserveClause: AbstractConserveAmount<State, Commodity> = Clauses.ConserveAmount()
/**
* The clauses for this contract are essentially:
*
@ -63,35 +63,34 @@ class CommodityContract : ClauseVerifier() {
* The group clause does not depend on any commands being present, so something has gone terribly wrong if
* it doesn't match.
*/
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val ifNotMatched = MatchBehaviour.ERROR
/**
* The group clause is the only top level clause, so end after processing it. If there are any commands left
* after this clause has run, the clause verifier will trigger an error.
*/
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val ifMatched = MatchBehaviour.END
// Subclauses to run on each group
override val clauses: List<GroupClause<State, Issued<Commodity>>>
get() = listOf(
NoZeroSizedOutputs<State, Commodity>(),
Issue(),
ConserveAmount()
)
override val clauses = listOf(
NoZeroSizedOutputs<State, Commodity>(),
Issue(),
ConserveAmount()
)
/**
* Group commodity states by issuance definition (issuer and underlying commodity).
*/
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Commodity>>>
override fun extractGroups(tx: TransactionForContract)
= tx.groupStates<State, Issued<Commodity>> { it.issuanceDef }
}
/**
* Standard issue clause, specialised to match the commodity issue command.
*/
class Issue : AbstractIssue<State, Commodity>({ -> sumCommodities() }, { token: Issued<Commodity> -> sumCommoditiesOrZero(token) }) {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Issue::class.java)
class Issue : AbstractIssue<State, Commodity>(
sum = { sumCommodities() },
sumOrZero = { sumCommoditiesOrZero(it) }
) {
override val requiredCommands = setOf(Commands.Issue::class.java)
}
/**
@ -110,15 +109,11 @@ class CommodityContract : ClauseVerifier() {
constructor(deposit: PartyAndReference, amount: Amount<Commodity>, owner: PublicKey)
: this(Amount(amount.quantity, Issued<Commodity>(deposit, amount.token)), owner)
override val deposit: PartyAndReference
get() = amount.token.issuer
override val deposit = amount.token.issuer
override val contract = COMMODITY_PROGRAM_ID
override val exitKeys: Collection<PublicKey>
get() = Collections.singleton(owner)
override val issuanceDef: Issued<Commodity>
get() = amount.token
override val participants: List<PublicKey>
get() = listOf(owner)
override val exitKeys = Collections.singleton(owner)
override val issuanceDef = amount.token
override val participants = listOf(owner)
override fun move(newAmount: Amount<Issued<Commodity>>, newOwner: PublicKey): FungibleAsset<Commodity>
= copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner)
@ -151,8 +146,7 @@ class CommodityContract : ClauseVerifier() {
*/
data class Exit(override val amount: Amount<Issued<Commodity>>) : Commands, FungibleAsset.Commands.Exit<Commodity>
}
override val clauses: List<SingleClause>
get() = listOf(Clauses.Group())
override val clauses = listOf(Clauses.Group())
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
= tx.commands.select<CommodityContract.Commands>()
@ -167,107 +161,18 @@ class CommodityContract : ClauseVerifier() {
*/
fun generateIssue(tx: TransactionBuilder, amount: Amount<Issued<Commodity>>, owner: PublicKey, notary: Party) {
check(tx.inputStates().isEmpty())
check(tx.outputStates().map { it.data }.sumFungibleOrNull<Commodity>() == null)
check(tx.outputStates().map { it.data }.sumCashOrNull() == null)
val at = amount.token.issuer
tx.addOutputState(TransactionState(CommodityContract.State(amount, owner), notary))
tx.addCommand(Commands.Issue(), at.party.owningKey)
tx.addOutputState(TransactionState(State(amount, owner), notary))
tx.addCommand(generateIssueCommand(), at.party.owningKey)
}
/**
* Generate a transaction that consumes one or more of the given input states to move money to the given pubkey.
* Note that the wallet list is not updated: it's up to you to do that.
*/
@Throws(InsufficientBalanceException::class)
fun generateSpend(tx: TransactionBuilder, amount: Amount<Issued<Commodity>>, to: PublicKey,
commodityStates: List<StateAndRef<State>>): List<PublicKey> =
generateSpend(tx, Amount(amount.quantity, amount.token.product), to, commodityStates,
setOf(amount.token.issuer.party))
/**
* Generate a transaction that consumes one or more of the given input states to move money to the given pubkey.
* Note that the wallet list is not updated: it's up to you to do that.
*
* @param onlyFromParties if non-null, the wallet will be filtered to only include commodity states issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of commodity claims they are willing to accept.
*/
// TODO: These spend functions should be shared with [Cash], possibly through some common superclass
@Throws(InsufficientBalanceException::class)
fun generateSpend(tx: TransactionBuilder, amount: Amount<Commodity>, to: PublicKey,
commodityStates: List<StateAndRef<State>>, onlyFromParties: Set<Party>? = null): List<PublicKey> {
// Discussion
//
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
//
// First we must select a set of commodity states (which for convenience we will call 'coins' here, as in bitcoinj).
// The input states can be considered our "wallet", and may consist of coins of different currencies, and from
// different institutions and deposits.
//
// Coin selection is a complex problem all by itself and many different approaches can be used. It is easily
// possible for different actors to use different algorithms and approaches that, for example, compete on
// privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of
// obfuscation and so on.
//
// Having selected coins of the right currency, we must craft output states for the amount we're sending and
// the "change", which goes back to us. The change is required to make the amounts balance. We may need more
// than one change output in order to avoid merging coins from different deposits. The point of this design
// is to ensure that ledger entries are immutable and globally identifiable.
//
// Finally, we add the states to the provided partial transaction.
val currency = amount.token
val acceptableCoins = run {
val ofCurrency = commodityStates.filter { it.state.data.amount.token.product == currency }
if (onlyFromParties != null)
ofCurrency.filter { it.state.data.deposit.party in onlyFromParties }
else
ofCurrency
}
val gathered = arrayListOf<StateAndRef<State>>()
var gatheredAmount = Amount(0, currency)
var takeChangeFrom: StateAndRef<State>? = null
for (c in acceptableCoins) {
if (gatheredAmount >= amount) break
gathered.add(c)
gatheredAmount += Amount(c.state.data.amount.quantity, currency)
takeChangeFrom = c
}
if (gatheredAmount < amount)
throw InsufficientBalanceException(amount - gatheredAmount)
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
Amount<Issued<Commodity>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
} else {
null
}
val keysUsed = gathered.map { it.state.data.owner }.toSet()
val states = gathered.groupBy { it.state.data.deposit }.map {
val coins = it.value
val totalAmount = coins.map { it.state.data.amount }.sumOrThrow()
TransactionState(State(totalAmount, to), coins.first().state.notary)
}
val outputs = if (change != null) {
// Just copy a key across as the change key. In real life of course, this works but leaks private data.
// In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow
// value flows through the transaction graph.
val changeKey = gathered.first().state.data.owner
// Add a change output and adjust the last output downwards.
states.subList(0, states.lastIndex) +
states.last().let { TransactionState(it.data.copy(amount = it.data.amount - change), it.notary) } +
TransactionState(State(change, changeKey), gathered.last().state.notary)
} else states
for (state in gathered) tx.addInputState(state)
for (state in outputs) tx.addOutputState(state)
// What if we already have a move command with the right keys? Filter it out here or in platform code?
val keysList = keysUsed.toList()
tx.addCommand(Commands.Move(), keysList)
return keysList
}
override fun deriveState(txState: TransactionState<State>, amount: Amount<Issued<Commodity>>, owner: PublicKey)
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
override fun generateExitCommand(amount: Amount<Issued<Commodity>>) = Commands.Exit(amount)
override fun generateIssueCommand() = Commands.Issue()
override fun generateMoveCommand() = Commands.Move()
}
/**

View File

@ -43,8 +43,7 @@ class Obligation<P> : ClauseVerifier() {
* that is inconsistent with the legal contract.
*/
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.example.gov/cash-settlement.html")
override val clauses: List<SingleClause>
get() = listOf(InterceptorClause(Clauses.VerifyLifecycle<P>(), Clauses.Net<P>()),
override val clauses = listOf(InterceptorClause(Clauses.VerifyLifecycle<P>(), Clauses.Net<P>()),
Clauses.Group<P>())
interface Clauses {
@ -52,18 +51,16 @@ class Obligation<P> : ClauseVerifier() {
* Parent clause for clauses that operate on grouped states (those which are fungible).
*/
class Group<P> : GroupClauseVerifier<State<P>, Issued<Terms<P>>>() {
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val clauses: List<GroupClause<State<P>, Issued<Terms<P>>>>
get() = listOf(
NoZeroSizedOutputs<State<P>, Terms<P>>(),
SetLifecycle<P>(),
VerifyLifecycle<P>(),
Settle<P>(),
Issue(),
ConserveAmount())
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
override val clauses = listOf(
NoZeroSizedOutputs<State<P>, Terms<P>>(),
SetLifecycle<P>(),
VerifyLifecycle<P>(),
Settle<P>(),
Issue(),
ConserveAmount()
)
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>>
= tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.issuanceDef }
@ -73,8 +70,7 @@ class Obligation<P> : ClauseVerifier() {
* Generic issuance clause
*/
class Issue<P> : AbstractIssue<State<P>, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Obligation.Commands.Issue::class.java)
override val requiredCommands = setOf(Obligation.Commands.Issue::class.java)
}
/**
@ -91,12 +87,9 @@ class Obligation<P> : ClauseVerifier() {
* Obligation-specific clause for changing the lifecycle of one or more states.
*/
class SetLifecycle<P> : GroupClause<State<P>, Issued<Terms<P>>> {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.SetLifecycle::class.java)
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val requiredCommands = setOf(Commands.SetLifecycle::class.java)
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
override fun verify(tx: TransactionForContract,
inputs: List<State<P>>,
@ -114,12 +107,9 @@ class Obligation<P> : ClauseVerifier() {
* change of ownership of other states to fulfil
*/
class Settle<P> : GroupClause<State<P>, Issued<Terms<P>>> {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Settle::class.java)
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val requiredCommands = setOf(Commands.Settle::class.java)
override val ifMatched: MatchBehaviour = MatchBehaviour.END
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
override fun verify(tx: TransactionForContract,
inputs: List<State<P>>,
@ -208,12 +198,9 @@ class Obligation<P> : ClauseVerifier() {
* non-standard lifecycle states on input/output.
*/
class VerifyLifecycle<P> : SingleClause, GroupClause<State<P>, Issued<Terms<P>>> {
override val requiredCommands: Set<Class<out CommandData>>
get() = emptySet()
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.CONTINUE
override val ifNotMatched: MatchBehaviour
get() = MatchBehaviour.ERROR
override val requiredCommands: Set<Class<out CommandData>> = emptySet()
override val ifMatched: MatchBehaviour = MatchBehaviour.CONTINUE
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
= verify(
@ -407,8 +394,8 @@ class Obligation<P> : ClauseVerifier() {
// If we have an default command, perform special processing: issued contracts can only be defaulted
// after the due date, and default/reset can only be done by the beneficiary
val expectedInputLifecycle: Lifecycle = setLifecycleCommand.value.inverse
val expectedOutputLifecycle: Lifecycle = setLifecycleCommand.value.lifecycle
val expectedInputLifecycle = setLifecycleCommand.value.inverse
val expectedOutputLifecycle = setLifecycleCommand.value.lifecycle
// Check that we're past the deadline for ALL involved inputs, and that the output states correspond 1:1
for ((stateIdx, input) in inputs.withIndex()) {
@ -416,7 +403,7 @@ class Obligation<P> : ClauseVerifier() {
val actualOutput = outputs[stateIdx]
val deadline = input.dueBefore
val timestamp: TimestampCommand? = tx.timestamp
val expectedOutput: State<P> = input.copy(lifecycle = expectedOutputLifecycle)
val expectedOutput = input.copy(lifecycle = expectedOutputLifecycle)
requireThat {
"there is a timestamp from the authority" by (timestamp != null)
@ -458,6 +445,24 @@ class Obligation<P> : ClauseVerifier() {
tx.addCommand(Commands.Net(NetType.PAYMENT), signer)
}
/**
* Generate an transaction exiting an obligation 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 changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
* necessarily owned by us.
* @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 exit funds held by others.
* @return the public key of the assets issuer, who must sign the transaction for it to be valid.
*/
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<Terms<P>>>,
changeKey: PublicKey, assetStates: List<StateAndRef<Obligation.State<P>>>): PublicKey
= Clauses.ConserveAmount<P>().generateExit(tx, amountIssued, changeKey, assetStates,
deriveState = { state, amount, owner -> state.copy(data = state.data.move(amount, owner)) },
generateExitCommand = { amount -> Commands.Exit<P>(amount) }
)
/**
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
*/
@ -515,7 +520,7 @@ class Obligation<P> : ClauseVerifier() {
lifecycle: Lifecycle,
notary: Party) {
val states = statesAndRefs.map { it.state.data }
val issuanceDef = getTemplateOrThrow(states)
val issuanceDef = getTermsOrThrow(states)
val existingLifecycle = when (lifecycle) {
Lifecycle.DEFAULTED -> Lifecycle.NORMAL
Lifecycle.NORMAL -> Lifecycle.DEFAULTED
@ -580,7 +585,7 @@ class Obligation<P> : ClauseVerifier() {
tx.addInputState(ref)
val assetState = ref.state.data
val amount: Amount<P> = Amount(assetState.amount.quantity, assetState.amount.token.product)
val amount = Amount(assetState.amount.quantity, assetState.amount.token.product)
if (obligationRemaining >= amount) {
tx.addOutputState(assetState.move(assetState.amount, obligationOwner), notary)
obligationRemaining -= amount
@ -612,7 +617,7 @@ class Obligation<P> : ClauseVerifier() {
states.map { it.issuanceDef }.distinct().single()
/** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */
private fun getTemplateOrThrow(states: Iterable<State<P>>): Terms<P> =
private fun getTermsOrThrow(states: Iterable<State<P>>) =
states.map { it.template }.distinct().single()
}

View File

@ -0,0 +1,78 @@
package com.r3corda.contracts.asset
import com.r3corda.contracts.clause.AbstractConserveAmount
import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.*
import com.r3corda.core.crypto.*
import java.security.PublicKey
import java.util.*
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Generic contract for assets on a ledger
//
/**
* An asset transaction may split and merge assets represented by a set of (issuer, depositRef) pairs, across multiple
* input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour (a blend of
* issuer+depositRef) and you couldn't merge outputs of two colours together, but you COULD put them in the same
* transaction.
*
* The goal of this design is to ensure that assets can be withdrawn from the ledger easily: if you receive some asset
* via this contract, you always know where to go in order to extract it from the R3 ledger, no matter how many hands
* it has passed through in the intervening time.
*
* At the same time, other contracts that just want assets and don't care much who is currently holding it can ignore
* the issuer/depositRefs and just examine the amount fields.
*/
abstract class OnLedgerAsset<T : Any, S : FungibleAsset<T>> : ClauseVerifier() {
abstract val conserveClause: AbstractConserveAmount<S, T>
/**
* Generate an transaction exiting 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 changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
* necessarily owned by us.
* @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 exit funds held by others.
* @return the public key of the assets issuer, who must sign the transaction for it to be valid.
*/
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
changeKey: PublicKey, assetStates: List<StateAndRef<S>>): PublicKey
= conserveClause.generateExit(tx, amountIssued, changeKey, assetStates,
deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
generateExitCommand = { amount -> generateExitCommand(amount) }
)
/**
* Generate a transaction that consumes one or more of the given input states to move assets to the given pubkey.
* Note that the wallet list is not updated: it's up to you to do that.
*
* @param onlyFromParties if non-null, the wallet will be filtered to only include asset states issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
*/
@Throws(InsufficientBalanceException::class)
fun generateSpend(tx: TransactionBuilder,
amount: Amount<T>,
to: PublicKey,
assetsStates: List<StateAndRef<S>>,
onlyFromParties: Set<Party>? = null): List<PublicKey>
= conserveClause.generateSpend(tx, amount, to, assetsStates, onlyFromParties,
deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
generateMoveCommand = { generateMoveCommand() })
abstract fun generateExitCommand(amount: Amount<Issued<T>>): FungibleAsset.Commands.Exit<T>
abstract fun generateIssueCommand(): FungibleAsset.Commands.Issue
abstract fun generateMoveCommand(): FungibleAsset.Commands.Move
/**
* Derive a new transaction state based on the given example, with amount and owner modified. This allows concrete
* implementations to have fields in their state which we don't know about here, and we simply leave them untouched
* when sending out "change" from spending/exiting.
*/
abstract fun deriveState(txState: TransactionState<S>, amount: Amount<Issued<T>>, owner: PublicKey): TransactionState<S>
}

View File

@ -1,12 +1,15 @@
package com.r3corda.contracts.clause
import com.r3corda.contracts.asset.FungibleAsset
import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.contracts.asset.sumFungibleOrNull
import com.r3corda.contracts.asset.sumFungibleOrZero
import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.GroupClause
import com.r3corda.core.contracts.clauses.MatchBehaviour
import com.r3corda.core.crypto.Party
import java.security.PublicKey
import java.util.*
/**
* Standardised clause for checking input/output balances of fungible assets. Requires that a
@ -21,6 +24,152 @@ abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause
override val requiredCommands: Set<Class<out CommandData>>
get() = emptySet()
/**
* Gather assets from the given list of states, sufficient to match or exceed the given amount.
*
* @param acceptableCoins list of states to use as inputs.
* @param amount the amount to gather states up to.
* @throws InsufficientBalanceException if there isn't enough value in the states to cover the requested amount.
*/
@Throws(InsufficientBalanceException::class)
private fun gatherCoins(acceptableCoins: Collection<StateAndRef<S>>,
amount: Amount<T>): Pair<ArrayList<StateAndRef<S>>, Amount<T>> {
val gathered = arrayListOf<StateAndRef<S>>()
var gatheredAmount = Amount(0, amount.token)
for (c in acceptableCoins) {
if (gatheredAmount >= amount) break
gathered.add(c)
gatheredAmount += Amount(c.state.data.amount.quantity, amount.token)
}
if (gatheredAmount < amount)
throw InsufficientBalanceException(amount - gatheredAmount)
return Pair(gathered, gatheredAmount)
}
/**
* 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 changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
* necessarily owned by us.
* @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 exit funds held by others.
* @return the public key of the assets issuer, who must sign the transaction for it to be valid.
*/
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
changeKey: PublicKey, assetStates: List<StateAndRef<S>>,
deriveState: (TransactionState<S>, Amount<Issued<T>>, PublicKey) -> TransactionState<S>,
generateExitCommand: (Amount<Issued<T>>) -> CommandData): PublicKey {
val currency = amountIssued.token.product
val amount = Amount(amountIssued.quantity, currency)
var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token }
val notary = acceptableCoins.firstOrNull()?.state?.notary
// TODO: We should be prepared to produce multiple transactions exiting inputs from
// different notaries, or at least group states by notary and take the set with the
// highest total value
acceptableCoins = acceptableCoins.filter { it.state.notary == notary }
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, Amount(amount.quantity, currency))
val takeChangeFrom = gathered.lastOrNull()
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
Amount<Issued<T>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
} else {
null
}
val outputs = if (change != null) {
// Add a change output and adjust the last output downwards.
listOf(deriveState(gathered.last().state, change, changeKey))
} else emptyList()
for (state in gathered) tx.addInputState(state)
for (state in outputs) tx.addOutputState(state)
tx.addCommand(generateExitCommand(amountIssued), amountIssued.token.issuer.party.owningKey)
return amountIssued.token.issuer.party.owningKey
}
/**
* Generate a transaction that consumes one or more of the given input states to move assets to the given pubkey.
* Note that the wallet list is not updated: it's up to you to do that.
*
* @param onlyFromParties if non-null, the wallet will be filtered to only include asset states issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
*/
@Throws(InsufficientBalanceException::class)
fun generateSpend(tx: TransactionBuilder,
amount: Amount<T>,
to: PublicKey,
assetsStates: List<StateAndRef<S>>,
onlyFromParties: Set<Party>? = null,
deriveState: (TransactionState<S>, Amount<Issued<T>>, PublicKey) -> TransactionState<S>,
generateMoveCommand: () -> CommandData): List<PublicKey> {
// Discussion
//
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
//
// First we must select a set of asset states (which for convenience we will call 'coins' here, as in bitcoinj).
// The input states can be considered our "wallet", and may consist of different products, and with different
// issuers and deposits.
//
// Coin selection is a complex problem all by itself and many different approaches can be used. It is easily
// possible for different actors to use different algorithms and approaches that, for example, compete on
// privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of
// obfuscation and so on.
//
// Having selected input states of the correct asset, we must craft output states for the amount we're sending and
// the "change", which goes back to us. The change is required to make the amounts balance. We may need more
// than one change output in order to avoid merging assets from different deposits. The point of this design
// is to ensure that ledger entries are immutable and globally identifiable.
//
// Finally, we add the states to the provided partial transaction.
val currency = amount.token
val acceptableCoins = run {
val ofCurrency = assetsStates.filter { it.state.data.amount.token.product == currency }
if (onlyFromParties != null)
ofCurrency.filter { it.state.data.deposit.party in onlyFromParties }
else
ofCurrency
}
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount)
val takeChangeFrom = gathered.firstOrNull()
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
Amount<Issued<T>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
} else {
null
}
val keysUsed = gathered.map { it.state.data.owner }.toSet()
val states = gathered.groupBy { it.state.data.deposit }.map {
val coins = it.value
val totalAmount = coins.map { it.state.data.amount }.sumOrThrow()
deriveState(coins.first().state, totalAmount, to)
}
val outputs = if (change != null) {
// Just copy a key across as the change key. In real life of course, this works but leaks private data.
// In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow
// value flows through the transaction graph.
val changeKey = gathered.first().state.data.owner
// Add a change output and adjust the last output downwards.
states.subList(0, states.lastIndex) +
states.last().let { deriveState(it, it.data.amount - change, it.data.owner) } +
deriveState(gathered.last().state, change, changeKey)
} else states
for (state in gathered) tx.addInputState(state)
for (state in outputs) tx.addOutputState(state)
// What if we already have a move command with the right keys? Filter it out here or in platform code?
val keysList = keysUsed.toList()
tx.addCommand(generateMoveCommand(), keysList)
return keysList
}
override fun verify(tx: TransactionForContract,
inputs: List<S>,
outputs: List<S>,