Cash contract: multi-issuer support

You can now take deposits from multiple different institutions and move/combine/split them appropriately. The issuers are kept separate, you cannot merge 3 different input states from 3 different institutions down to one, but you can merge/split within that specific issuer. Deposit refs are not currently being kept separate, but they should be also (this is coming next).
This commit is contained in:
Mike Hearn
2015-11-03 13:49:28 +01:00
parent ce3d339c62
commit 12f5ddb0aa
3 changed files with 201 additions and 115 deletions

View File

@ -1,11 +1,12 @@
import java.security.PublicKey
import java.util.*
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Cash
// TODO: Implement multi-issuer case.
// TODO: Think about state merging: when does it make sense to merge multiple cash states from the same issuer?
// TODO: Does multi-currency also make sense? Probably?
// TODO: Implement a generate function.
@ -35,47 +36,52 @@ class ExitCashCommand(val amount: Amount) : Command
class CashContract : Contract {
override fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSignedCommand>) {
// Select all input states that are cash states and ensure they are all denominated in the same currency and
// issued by the same issuer.
val inputs = inStates.filterIsInstance<CashState>()
val inputMoney = inputs.sumBy { it.amount.pennies }
val cashInputs = inStates.filterIsInstance<CashState>()
requireThat {
"there is at least one cash input" by inputs.isNotEmpty()
"all inputs use the same currency" by (inputs.groupBy { it.amount.currency }.size == 1)
"all inputs come from the same issuer" by (inputs.groupBy { it.issuingInstitution }.size == 1)
"some money is actually moving" by (inputMoney > 0)
"there is at least one cash input" by cashInputs.isNotEmpty()
"there are no zero sized inputs" by cashInputs.none { it.amount.pennies == 0 }
"all inputs use the same currency" by (cashInputs.groupBy { it.amount.currency }.size == 1)
}
val issuer = inputs.first().issuingInstitution
val currency = inputs.first().amount.currency
val depositReference = inputs.first().depositReference
val currency = cashInputs.first().amount.currency
// Select all the output states that are cash states. There may be zero if all money is being withdrawn.
// If there are any though, check that the currencies and issuers match the inputs.
val outputs = outStates.filterIsInstance<CashState>()
val outputMoney = outputs.sumBy { it.amount.pennies }
val cashOutputs = outStates.filterIsInstance<CashState>()
requireThat {
"all outputs use the currency of the inputs" by outputs.all { it.amount.currency == currency }
"all outputs claim against the issuer of the inputs" by outputs.all { it.issuingInstitution == issuer }
"all outputs use the same deposit reference as the inputs" by outputs.all { it.depositReference == depositReference }
"all outputs use the currency of the inputs" by cashOutputs.all { it.amount.currency == currency }
}
// If we have any commands, find the one that came from the issuer of the original cash deposit and
// check if it's an exit command.
val issuerCommand = args.find { it.signingInstitution == issuer }?.command as? ExitCashCommand
val amountExitingLedger = issuerCommand?.amount?.pennies ?: 0
requireThat("the value exiting the ledger is not more than the input value", amountExitingLedger <= outputMoney)
// Verify the books balance.
requireThat("the amounts balance", inputMoney == outputMoney + amountExitingLedger)
// For each issuer that's represented in the inputs, group the inputs together and verify that the outputs
// balance, taking into account a possible exit command from that issuer.
var outputsLeft = cashOutputs.size
for ((issuer, inputs) in cashInputs.groupBy { it.issuingInstitution }) {
val outputs = cashOutputs.filter { it.issuingInstitution == issuer }
outputsLeft -= outputs.size
val inputAmount = inputs.map { it.amount }.sum()
val outputAmount = outputs.map { it.amount }.sumOrZero(currency)
val issuerCommand = args.filter { it.signingInstitution == issuer }.map { it.command as? ExitCashCommand }.filterNotNull().singleOrNull()
val amountExitingLedger = issuerCommand?.amount ?: Amount(0, inputAmount.currency)
val depositReference = inputs.first().depositReference
requireThat {
"for issuer ${issuer.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger)
// TODO: Introduce a byte array wrapper that makes == do what we expect (Kotlin does not do this for us)
"for issuer ${issuer.name} the deposit references are the same" by outputs.all { Arrays.equals(it.depositReference, depositReference) }
}
}
requireThat { "no output states are unaccounted for" by (outputsLeft == 0) }
// Now check the digital signatures on the move commands. Every input has an owning public key, and we must
// see a signature from each of those keys. The actual signatures have been verified against the transaction
// data by the platform before execution.
val owningPubKeys = inputs.map { it.owner }.toSortedSet()
val owningPubKeys = cashInputs.map { it.owner }.toSortedSet()
val keysThatSigned = args.filter { it.command is MoveCashCommand }.map { it.signer }.toSortedSet()
requireThat("the owning keys are the same as the signing keys", owningPubKeys == keysThatSigned)
requireThat { "the owning keys are the same as the signing keys" by (owningPubKeys == keysThatSigned) }
// Accept.
}

View File

@ -8,14 +8,11 @@ import kotlin.test.fail
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// REQUIREMENTS
fun requireThat(message: String, expression: Boolean) {
if (!expression) throw IllegalArgumentException(message)
}
//
// To understand how requireThat works, read the section "type safe builders" on the Kotlin website:
//
// https://kotlinlang.org/docs/reference/type-safe-builders.html
object Requirements {
infix fun String.by(expr: Boolean) {
if (!expr) throw IllegalArgumentException("Failed requirement: $this")