CORDA-2817 Revert CORDA-2162 but modify Cash move to allow multiple m… (#4971)

* CORDA-2817 Revert CORDA-2162 but modify Cash move to allow multiple move commands and thus multiple generateSpends in the same transaction.

* CORDA-2817 Remove API changes and internalise into Cash.
This commit is contained in:
Rick Parker 2019-04-02 18:23:43 +01:00 committed by GitHub
parent f4d7bc9a18
commit 2685596798
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 59 additions and 38 deletions

View File

@ -7,7 +7,6 @@ import net.corda.core.contracts.*
import net.corda.core.crypto.*
import net.corda.core.identity.Party
import net.corda.core.internal.*
import net.corda.core.internal.cordapp.CordappResolver
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
@ -672,15 +671,8 @@ with @BelongsToContract, or supply an explicit contract parameter to addOutputSt
/** Returns an immutable list of output [TransactionState]s. */
fun outputStates(): List<TransactionState<*>> = ArrayList(outputs)
/** Returns an immutable list of [Command]s, grouping by [CommandData] and joining signers (from v4, v3 and below return all commands with duplicates for different signers). */
fun commands(): List<Command<*>> {
return if (CordappResolver.currentTargetVersion >= CORDA_VERSION_THAT_INTRODUCED_FLATTENED_COMMANDS) {
commands.groupBy { cmd -> cmd.value }
.entries.map { (data, cmds) -> Command(data, cmds.flatMap(Command<*>::signers).toSet().toList()) }
} else {
ArrayList(commands)
}
}
/** Returns an immutable list of [Command]s. */
fun commands(): List<Command<*>> = ArrayList(commands)
/**
* Sign the built transaction and return it. This is an internal function for use by the service hub, please use

View File

@ -10,8 +10,6 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.internal.AbstractAttachment
import net.corda.core.internal.PLATFORM_VERSION
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
import net.corda.core.internal.cordapp.CordappResolver
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.ZoneVersionTooLowException
import net.corda.core.node.services.AttachmentStorage
@ -115,27 +113,6 @@ class TransactionBuilderTest {
assertThat(wtx.references).containsOnly(referenceStateRef)
}
@Test
fun `multiple commands with same data are joined without duplicates in terms of signers`() {
// This behaviour is only activated for platform version 4 onwards.
CordappResolver.withCordapp(targetPlatformVersion = 4) {
val aliceParty = TestIdentity(ALICE_NAME).party
val bobParty = TestIdentity(BOB_NAME).party
val tx = TransactionBuilder(notary)
tx.addCommand(DummyCommandData, notary.owningKey, aliceParty.owningKey)
tx.addCommand(DummyCommandData, aliceParty.owningKey, bobParty.owningKey)
val commands = tx.commands()
assertThat(commands).hasSize(1)
assertThat(commands.single()).satisfies { cmd ->
assertThat(cmd.value).isEqualTo(DummyCommandData)
assertThat(cmd.signers).hasSize(3)
assertThat(cmd.signers).contains(notary.owningKey, bobParty.owningKey, aliceParty.owningKey)
}
}
}
@Test
fun `automatic signature constraint`() {
val aliceParty = TestIdentity(ALICE_NAME).party

View File

@ -14,10 +14,10 @@ import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.QueryableState
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.finance.schemas.CashSchemaV1
import net.corda.finance.contracts.utils.sumCash
import net.corda.finance.contracts.utils.sumCashOrNull
import net.corda.finance.contracts.utils.sumCashOrZero
import net.corda.finance.schemas.CashSchemaV1
import java.security.PublicKey
import java.util.*
@ -164,7 +164,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
(inputAmount == outputAmount + amountExitingLedger)
}
verifyMoveCommand<Commands.Move>(inputs, tx.commands)
verifyFlattenedMoveCommand<Commands.Move>(inputs, tx.commands)
}
}
}
@ -207,3 +207,37 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
val Amount<Currency>.CASH: Cash.State get() = Cash.State(Amount(quantity, Issued(NULL_PARTY.ref(1), token)), NULL_PARTY)
/** An extension property that lets you get a cash state from an issued token, under the [NULL_PARTY] */
val Amount<Issued<Currency>>.STATE: Cash.State get() = Cash.State(this, NULL_PARTY)
/**
* Simple functionality for verifying multiple move commands that differ only by signers. Verifies that each input has a signature from its owning key.
*
* @param T the type of the move command.
*/
@Throws(IllegalArgumentException::class)
internal inline fun <reified T : MoveCommand> verifyFlattenedMoveCommand(inputs: List<OwnableState>,
commands: List<CommandWithParties<CommandData>>)
: MoveCommand {
// Now check the digital signatures on the move command. 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.owningKey }.toSet()
val commands = commands.groupCommands<T>()
// Does not use requireThat to maintain message compatibility with verifyMoveCommand.
if (commands.isEmpty()) {
throw IllegalStateException("Required ${T::class.qualifiedName} command")
}
requireThat {
"move commands can only differ by signing keys" using (commands.size == 1)
}
val keysThatSigned = commands.values.first()
requireThat {
"the owning keys are a subset of the signing keys" using keysThatSigned.containsAll(owningPubKeys)
}
return commands.keys.single()
}
/** Group commands by instances of the given type. */
internal inline fun <reified T : CommandData> Collection<CommandWithParties<CommandData>>.groupCommands() = groupCommands(T::class.java)
/** Group commands by instances of the given type. */
internal fun <C : CommandData> Collection<CommandWithParties<CommandData>>.groupCommands(klass: Class<C>) = select(klass).groupBy { it.value }.map { it.key to it.value.flatMap { it.signers }.toSet() }.toMap()

View File

@ -164,6 +164,22 @@ class CashTests {
}
}
@Test
fun twoMoves() {
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, inState)
input(Cash.PROGRAM_ID, inState.copy(owner = bob.party))
output(Cash.PROGRAM_ID, outState)
command(alice.publicKey, Cash.Commands.Move())
tweak {
output(Cash.PROGRAM_ID, outState)
command(bob.publicKey, Cash.Commands.Move())
this.verifies()
}
}
}
@BelongsToContract(Cash::class)
object DummyState: ContractState {
override val participants: List<AbstractParty> = emptyList()
@ -273,8 +289,8 @@ class CashTests {
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2))
command(megaCorp.publicKey, Cash.Commands.Issue())
tweak {
command(miniCorp.publicKey, Cash.Commands.Issue())
this.verifies()
command(megaCorp.publicKey, Cash.Commands.Issue())
this `fails with` "there is only a single issue command"
}
this.verifies()
}
@ -889,5 +905,7 @@ class CashTests {
assertEquals(megaCorp.party, out(5).amount.token.issuer.party)
assertEquals(megaCorp.party, out(6).amount.token.issuer.party)
assertEquals(megaCorp.party, out(7).amount.token.issuer.party)
assertEquals(2, wtx.commands.size)
}
}

View File

@ -244,7 +244,7 @@ class ObligationTests {
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
tweak {
command(MEGA_CORP_PUBKEY, Obligation.Commands.Issue())
this.verifies()
this `fails with` "there is only a single issue command"
}
this.verifies()
}