Require that a cash Issue command is the only command

Require that a cash Issue command is the only cash command in a transaction.
Although no problems are anticipated with other commands being present, they
could theoretically confuse other verification tools.
This commit is contained in:
Ross Nicoll 2016-05-05 17:30:00 +01:00
parent 20c6be193a
commit 252eb141a7
2 changed files with 44 additions and 20 deletions

View File

@ -93,32 +93,35 @@ class Cash : Contract {
"there are no zero sized outputs" by outputs.none { it.amount.pennies == 0L } "there are no zero sized outputs" by outputs.none { it.amount.pennies == 0L }
} }
if (verifyIssueCommands(inputs, outputs, tx, currency, issuer)) continue 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(currency)
val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one cash input for this group") // If we want to remove cash from the ledger, that must be signed for by the issuer.
val outputAmount = outputs.sumCashOrZero(currency) // 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 = issuer).singleOrNull()
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, currency)
// If we want to remove cash from the ledger, that must be signed for by the issuer. requireThat {
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero. "there are no zero sized inputs" by inputs.none { it.amount.pennies == 0L }
val exitCommand = tx.commands.select<Commands.Exit>(party = issuer).singleOrNull() "for deposit ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, currency) (inputAmount == outputAmount + amountExitingLedger)
}
requireThat { verifyMoveCommands<Commands.Move>(inputs, tx)
"there are no zero sized inputs" by inputs.none { it.amount.pennies == 0L }
"for deposit ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by
(inputAmount == outputAmount + amountExitingLedger)
} }
verifyMoveCommands<Commands.Move>(inputs, tx)
} }
} }
private fun verifyIssueCommands(inputs: List<State>, outputs: List<State>, tx: TransactionForVerification, currency: Currency, issuer: Party): Boolean { private fun verifyIssueCommand(inputs: List<State>,
val issueCommand = tx.commands.select<Commands.Issue>().singleOrNull() outputs: List<State>,
if (issueCommand == null || outputs.isEmpty()) { tx: TransactionForVerification,
return false issueCommand: AuthenticatedObject<Commands.Issue>,
} currency: Currency,
issuer: Party) {
// If we have an issue command, perform special processing: the group is allowed to have no inputs, // 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. // and the output states must have a deposit reference owned by the signer.
// //
@ -132,12 +135,13 @@ class Cash : Contract {
// The grouping ensures that all outputs have the same deposit reference and currency. // The grouping ensures that all outputs have the same deposit reference and currency.
val inputAmount = inputs.sumCashOrZero(currency) val inputAmount = inputs.sumCashOrZero(currency)
val outputAmount = outputs.sumCash() val outputAmount = outputs.sumCash()
val cashCommands = tx.commands.select<Cash.Commands>()
requireThat { requireThat {
"the issue command has a nonce" by (issueCommand.value.nonce != 0L) "the issue command has a nonce" by (issueCommand.value.nonce != 0L)
"output deposits are owned by a command signer" by (issuer in issueCommand.signingParties) "output deposits are owned by a command signer" by (issuer in issueCommand.signingParties)
"output values sum to more than the inputs" by (outputAmount > inputAmount) "output values sum to more than the inputs" by (outputAmount > inputAmount)
"there is only a single issue command" by (cashCommands.count() == 1)
} }
return true
} }
/** /**

View File

@ -143,6 +143,26 @@ class CashTests {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() } arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
this `fails requirement` "output values sum to more than the inputs" this `fails requirement` "output values sum to more than the inputs"
} }
// Can't have any other commands if we have an issue command (because the issue command overrules them)
transaction {
input { inState }
output { inState.copy(amount = inState.amount * 2) }
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
tweak {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
this `fails requirement` "there is only a single issue command"
}
tweak {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
this `fails requirement` "there is only a single issue command"
}
tweak {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
this `fails requirement` "there is only a single issue command"
}
this.accepts()
}
} }
@Test @Test