[CORDA-2162]: Cash.generateSpend cannot be used twice to generate two cash moves in the same tx (fix). (#4394)

This commit is contained in:
Michele Sollecito 2018-12-11 14:42:41 +00:00 committed by GitHub
parent 85102fa0e5
commit 6b1dc2ef27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 72 additions and 7 deletions

View File

@ -132,7 +132,7 @@ open class TransactionBuilder @JvmOverloads constructor(
createComponentGroups( createComponentGroups(
inputStates(), inputStates(),
resolvedOutputs, resolvedOutputs,
commands, commands(),
(allContractAttachments + attachments).toSortedSet().toList(), // Sort the attachments to ensure transaction builds are stable. (allContractAttachments + attachments).toSortedSet().toList(), // Sort the attachments to ensure transaction builds are stable.
notary, notary,
window, window,
@ -629,8 +629,8 @@ with @BelongsToContract, or supply an explicit contract parameter to addOutputSt
/** Returns an immutable list of output [TransactionState]s. */ /** Returns an immutable list of output [TransactionState]s. */
fun outputStates(): List<TransactionState<*>> = ArrayList(outputs) fun outputStates(): List<TransactionState<*>> = ArrayList(outputs)
/** Returns an immutable list of [Command]s. */ /** Returns an immutable list of [Command]s, grouping by [CommandData] and joining signers. */
fun commands(): List<Command<*>> = ArrayList(commands) fun commands(): List<Command<*>> = commands.groupBy { cmd -> cmd.value }.entries.map { (data, cmds) -> Command(data, cmds.flatMap(Command<*>::signers).toSet().toList()) }
/** /**
* Sign the built transaction and return it. This is an internal function for use by the service hub, please use * Sign the built transaction and return it. This is an internal function for use by the service hub, please use

View File

@ -122,6 +122,25 @@ class TransactionBuilderTest {
assertThat(wtx.references).containsOnly(referenceStateRef) assertThat(wtx.references).containsOnly(referenceStateRef)
} }
@Test
fun `multiple commands with same data are joined without duplicates in terms of signers`() {
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 @Test
fun `automatic signature constraint`() { fun `automatic signature constraint`() {
val aliceParty = TestIdentity(ALICE_NAME).party val aliceParty = TestIdentity(ALICE_NAME).party

View File

@ -17,6 +17,8 @@ Unreleased
* Fixed a problem with IRS demo not being able to simulate future dates as expected (https://github.com/corda/corda/issues/3851). * Fixed a problem with IRS demo not being able to simulate future dates as expected (https://github.com/corda/corda/issues/3851).
* Fixed a problem that was preventing `Cash.generateSpend` to be used more than once per transaction (https://github.com/corda/corda/issues/4110).
* ``SwapIdentitiesFlow``, from the experimental confidential-identities module, is now an inlined flow. Instead of passing in a ``Party`` with * ``SwapIdentitiesFlow``, from the experimental confidential-identities module, is now an inlined flow. Instead of passing in a ``Party`` with
whom to exchange the anonymous identity, a ``FlowSession`` to that party is required instead. The flow running on the other side must whom to exchange the anonymous identity, a ``FlowSession`` to that party is required instead. The flow running on the other side must
also call ``SwapIdentitiesFlow``. This change was required as the previous API allowed any counterparty to generate anonoymous identities also call ``SwapIdentitiesFlow``. This change was required as the previous API allowed any counterparty to generate anonoymous identities

View File

@ -19,7 +19,6 @@ import net.corda.finance.utils.sumCashOrNull
import net.corda.finance.utils.sumCashOrZero import net.corda.finance.utils.sumCashOrZero
import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.NodeVaultService
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.testing.contracts.DummyState
import net.corda.testing.core.* import net.corda.testing.core.*
import net.corda.testing.dsl.EnforceVerifyOrFail import net.corda.testing.dsl.EnforceVerifyOrFail
import net.corda.testing.dsl.TransactionDSL import net.corda.testing.dsl.TransactionDSL
@ -103,6 +102,10 @@ class CashTests {
vaultFiller.fillWithSomeTestCash(400.DOLLARS, megaCorpServices, 1, megaCorp.ref(1), ourIdentity) vaultFiller.fillWithSomeTestCash(400.DOLLARS, megaCorpServices, 1, megaCorp.ref(1), ourIdentity)
vaultFiller.fillWithSomeTestCash(80.DOLLARS, miniCorpServices, 1, miniCorp.ref(1), ourIdentity) vaultFiller.fillWithSomeTestCash(80.DOLLARS, miniCorpServices, 1, miniCorp.ref(1), ourIdentity)
vaultFiller.fillWithSomeTestCash(80.SWISS_FRANCS, miniCorpServices, 1, miniCorp.ref(1), ourIdentity) vaultFiller.fillWithSomeTestCash(80.SWISS_FRANCS, miniCorpServices, 1, miniCorp.ref(1), ourIdentity)
vaultFiller.fillWithSomeTestCash(100.POUNDS, megaCorpServices, 1, megaCorp.ref(1), ourIdentity)
vaultFiller.fillWithSomeTestCash(400.POUNDS, megaCorpServices, 1, megaCorp.ref(1), ourIdentity)
vaultFiller.fillWithSomeTestCash(80.POUNDS, miniCorpServices, 1, miniCorp.ref(1), ourIdentity)
} }
database.transaction { database.transaction {
vaultStatesUnconsumed = ourServices.vaultService.queryBy<Cash.State>().states vaultStatesUnconsumed = ourServices.vaultService.queryBy<Cash.State>().states
@ -269,8 +272,8 @@ class CashTests {
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2)) output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2))
command(megaCorp.publicKey, Cash.Commands.Issue()) command(megaCorp.publicKey, Cash.Commands.Issue())
tweak { tweak {
command(megaCorp.publicKey, Cash.Commands.Issue()) command(miniCorp.publicKey, Cash.Commands.Issue())
this `fails with` "there is only a single issue command" this.verifies()
} }
this.verifies() this.verifies()
} }
@ -846,4 +849,45 @@ class CashTests {
assertEquals(megaCorp.party, out(2).amount.token.issuer.party) assertEquals(megaCorp.party, out(2).amount.token.issuer.party)
assertEquals(megaCorp.party, out(3).amount.token.issuer.party) assertEquals(megaCorp.party, out(3).amount.token.issuer.party)
} }
@Test
fun generateSpendTwiceWithinATransaction() {
val tx = TransactionBuilder(dummyNotary.party)
database.transaction {
val payments = listOf(
PartyAndAmount(miniCorpAnonymised, 400.DOLLARS),
PartyAndAmount(charlie.party.anonymise(), 150.DOLLARS)
)
Cash.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert())
}
database.transaction {
val payments = listOf(
PartyAndAmount(miniCorpAnonymised, 400.POUNDS),
PartyAndAmount(charlie.party.anonymise(), 150.POUNDS)
)
Cash.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert())
}
val wtx = tx.toWireTransaction(ourServices)
fun out(i: Int) = wtx.getOutput(i) as Cash.State
assertEquals(8, wtx.outputs.size)
assertEquals(80.DOLLARS, out(0).amount.withoutIssuer())
assertEquals(320.DOLLARS, out(1).amount.withoutIssuer())
assertEquals(150.DOLLARS, out(2).amount.withoutIssuer())
assertEquals(30.DOLLARS, out(3).amount.withoutIssuer())
assertEquals(miniCorp.party, out(0).amount.token.issuer.party)
assertEquals(megaCorp.party, out(1).amount.token.issuer.party)
assertEquals(megaCorp.party, out(2).amount.token.issuer.party)
assertEquals(megaCorp.party, out(3).amount.token.issuer.party)
assertEquals(80.POUNDS, out(4).amount.withoutIssuer())
assertEquals(320.POUNDS, out(5).amount.withoutIssuer())
assertEquals(150.POUNDS, out(6).amount.withoutIssuer())
assertEquals(30.POUNDS, out(7).amount.withoutIssuer())
assertEquals(miniCorp.party, out(4).amount.token.issuer.party)
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)
}
} }

View File

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