CORDA-2109: Fix a bug that prevents consecutive multiparty contract upgrades

The contract upgrade handler assumes that the state to be upgraded is
created by a WireTransaction. This breaks the upgrade process if it was
in fact issued by a ContractUpgradeWireTransactions or a NotaryChangeWireTransaction.
This commit is contained in:
Andrius Dagys 2018-10-16 16:33:04 +01:00
parent 68d736dd81
commit 715c38766d
4 changed files with 93 additions and 51 deletions

View File

@ -3,15 +3,12 @@ package net.corda.core.flows
import com.natpryce.hamkrest.*
import com.natpryce.hamkrest.assertion.assert
import net.corda.core.contracts.*
import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.testing.internal.matchers.flow.willThrow
import net.corda.core.flows.mixins.WithContracts
import net.corda.core.flows.mixins.WithFinality
import net.corda.core.identity.AbstractParty
import net.corda.core.internal.Emoji
import net.corda.core.transactions.ContractUpgradeLedgerTransaction
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.USD
@ -20,9 +17,12 @@ import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueFlow
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyContractV2
import net.corda.testing.contracts.DummyContractV3
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.singleIdentity
import net.corda.testing.internal.matchers.flow.willReturn
import net.corda.testing.internal.matchers.flow.willThrow
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.TestStartedNode
import net.corda.testing.node.internal.cordappsForPackages
@ -57,54 +57,67 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality {
aliceNode.finalise(stx, bob)
val atx = aliceNode.getValidatedTransaction(stx)
val btx = bobNode.getValidatedTransaction(stx)
val aliceTx = aliceNode.getValidatedTransaction(stx)
val bobTx = bobNode.getValidatedTransaction(stx)
// The request is expected to be rejected because party B hasn't authorised the upgrade yet.
assert.that(
aliceNode.initiateDummyContractUpgrade(atx),
aliceNode.initiateContractUpgrade(aliceTx, DummyContractV2::class),
willThrow<UnexpectedFlowEndException>())
// Party B authorise the contract state upgrade, and immediately deauthorise the same.
assert.that(bobNode.authoriseDummyContractUpgrade(btx), willReturn())
assert.that(bobNode.deauthoriseContractUpgrade(btx), willReturn())
// Party B authorises the contract state upgrade, and immediately de-authorises the same.
assert.that(bobNode.authoriseContractUpgrade(bobTx, DummyContractV2::class), willReturn())
assert.that(bobNode.deauthoriseContractUpgrade(bobTx), willReturn())
// The request is expected to be rejected because party B has subsequently deauthorised a previously authorised upgrade.
// The request is expected to be rejected because party B has subsequently de-authorised a previously authorised upgrade.
assert.that(
aliceNode.initiateDummyContractUpgrade(atx),
aliceNode.initiateContractUpgrade(aliceTx, DummyContractV2::class),
willThrow<UnexpectedFlowEndException>())
// Party B authorise the contract state upgrade
assert.that(bobNode.authoriseDummyContractUpgrade(btx), willReturn())
// Party B authorises the contract state upgrade.
assert.that(bobNode.authoriseContractUpgrade(bobTx, DummyContractV2::class), willReturn())
// Party A initiates contract upgrade flow, expected to succeed this time.
assert.that(
aliceNode.initiateDummyContractUpgrade(atx),
aliceNode.initiateContractUpgrade(aliceTx, DummyContractV2::class),
willReturn(
aliceNode.hasDummyContractUpgradeTransaction()
and bobNode.hasDummyContractUpgradeTransaction()))
aliceNode.hasContractUpgradeTransaction<DummyContract.State, DummyContractV2.State>()
and bobNode.hasContractUpgradeTransaction<DummyContract.State, DummyContractV2.State>()))
val upgradedState = aliceNode.getStateFromVault(DummyContractV2.State::class)
// We now test that the upgraded state can be upgraded further, to V3.
// Party B authorises the contract state upgrade.
assert.that(bobNode.authoriseContractUpgrade(upgradedState, DummyContractV3::class), willReturn())
// Party A initiates contract upgrade flow which is expected to succeed.
assert.that(
aliceNode.initiateContractUpgrade(upgradedState, DummyContractV3::class),
willReturn(
aliceNode.hasContractUpgradeTransaction<DummyContractV2.State, DummyContractV3.State>()
and bobNode.hasContractUpgradeTransaction<DummyContractV2.State, DummyContractV3.State>()))
}
private fun TestStartedNode.issueCash(amount: Amount<Currency> = Amount(1000, USD)) =
services.startFlow(CashIssueFlow(amount, OpaqueBytes.of(1), notary))
.andRunNetwork()
.resultFuture.getOrThrow()
services.startFlow(CashIssueFlow(amount, OpaqueBytes.of(1), notary))
.andRunNetwork()
.resultFuture.getOrThrow()
private fun TestStartedNode.getBaseStateFromVault() = getStateFromVault(ContractState::class)
private fun TestStartedNode.getCashStateFromVault() = getStateFromVault(CashV2.State::class)
private fun hasIssuedAmount(expected: Amount<Issued<Currency>>) =
hasContractState(has(CashV2.State::amount, equalTo(expected)))
hasContractState(has(CashV2.State::amount, equalTo(expected)))
private fun belongsTo(vararg recipients: AbstractParty) =
hasContractState(has(CashV2.State::owners, equalTo(recipients.toList())))
hasContractState(has(CashV2.State::owners, equalTo(recipients.toList())))
private fun <T : ContractState> hasContractState(expectation: Matcher<T>) =
has<StateAndRef<T>, T>(
"contract state",
{ it.state.data },
expectation)
has<StateAndRef<T>, T>(
"contract state",
{ it.state.data },
expectation)
@Test
fun `upgrade Cash to v2`() {
@ -123,14 +136,14 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality {
val upgradedState = aliceNode.getCashStateFromVault()
assert.that(upgradedState,
hasIssuedAmount(Amount(1000000, USD) `issued by` (alice.ref(1)))
and belongsTo(anonymisedRecipient))
and belongsTo(anonymisedRecipient))
// Make sure the upgraded state can be spent
val movedState = upgradedState.state.data.copy(amount = upgradedState.state.data.amount.times(2))
val spendUpgradedTx = aliceNode.signInitialTransaction {
addInputState(upgradedState)
addOutputState(
upgradedState.state.copy(data = movedState)
upgradedState.state.copy(data = movedState)
)
addCommand(CashV2.Move(), alice.owningKey)
}
@ -161,35 +174,24 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality {
override fun verify(tx: LedgerTransaction) {}
}
//region Operations
private fun TestStartedNode.initiateDummyContractUpgrade(tx: SignedTransaction) =
initiateContractUpgrade(tx, DummyContractV2::class)
private fun TestStartedNode.authoriseDummyContractUpgrade(tx: SignedTransaction) =
authoriseContractUpgrade(tx, DummyContractV2::class)
//endregion
//region Matchers
private fun TestStartedNode.hasDummyContractUpgradeTransaction() =
hasContractUpgradeTransaction<DummyContract.State, DummyContractV2.State>()
private inline fun <reified FROM : Any, reified TO: Any> TestStartedNode.hasContractUpgradeTransaction() =
has<StateAndRef<ContractState>, ContractUpgradeLedgerTransaction>(
"a contract upgrade transaction",
{ getContractUpgradeTransaction(it) },
isUpgrade<FROM, TO>())
private inline fun <reified FROM : Any, reified TO : Any> TestStartedNode.hasContractUpgradeTransaction() =
has<StateAndRef<ContractState>, ContractUpgradeLedgerTransaction>(
"a contract upgrade transaction",
{ getContractUpgradeTransaction(it) },
isUpgrade<FROM, TO>())
private fun TestStartedNode.getContractUpgradeTransaction(state: StateAndRef<ContractState>) =
services.validatedTransactions.getTransaction(state.ref.txhash)!!
.resolveContractUpgradeTransaction(services)
services.validatedTransactions.getTransaction(state.ref.txhash)!!
.resolveContractUpgradeTransaction(services)
private inline fun <reified FROM : Any, reified TO : Any> isUpgrade() =
isUpgradeFrom<FROM>() and isUpgradeTo<TO>()
private inline fun <reified T: Any> isUpgradeFrom() =
private inline fun <reified T : Any> isUpgradeFrom() =
has<ContractUpgradeLedgerTransaction, Any>("input data", { it.inputs.single().state.data }, isA<T>(anything))
private inline fun <reified T: Any> isUpgradeTo() =
private inline fun <reified T : Any> isUpgradeTo() =
has<ContractUpgradeLedgerTransaction, Any>("output data", { it.outputs.single().data }, isA<T>(anything))
//endregion
}

View File

@ -51,9 +51,13 @@ interface WithContracts : WithMockNet {
fun <T : UpgradedContract<*, *>> TestStartedNode.authoriseContractUpgrade(
tx: SignedTransaction, toClass: KClass<T>) =
startFlow(
ContractUpgradeFlow.Authorise(tx.tx.outRef<ContractState>(0), toClass.java)
)
authoriseContractUpgrade(tx.tx.outRef(0), toClass)
fun <T : UpgradedContract<*, *>> TestStartedNode.authoriseContractUpgrade(
stateAndRef: StateAndRef<ContractState>, toClass: KClass<T>) =
startFlow(
ContractUpgradeFlow.Authorise(stateAndRef, toClass.java)
)
fun TestStartedNode.deauthoriseContractUpgrade(tx: SignedTransaction) = startFlow(
ContractUpgradeFlow.Deauthorise(tx.tx.outRef<ContractState>(0).ref)

View File

@ -56,7 +56,7 @@ class ContractUpgradeHandler(otherSide: FlowSession) : AbstractStateReplacementF
// verify outputs matches the proposed upgrade.
val ourSTX = serviceHub.validatedTransactions.getTransaction(proposal.stateRef.txhash)
requireNotNull(ourSTX) { "We don't have a copy of the referenced state" }
val oldStateAndRef = ourSTX!!.tx.outRef<ContractState>(proposal.stateRef.index)
val oldStateAndRef = ourSTX!!.resolveBaseTransaction(serviceHub).outRef<ContractState>(proposal.stateRef.index)
val authorisedUpgrade = serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?: throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}")
val proposedTx = stx.coreTransaction as ContractUpgradeWireTransaction
val expectedTx = ContractUpgradeUtils.assembleUpgradeTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt, serviceHub)

View File

@ -0,0 +1,36 @@
package net.corda.testing.contracts
import net.corda.core.contracts.*
import net.corda.core.identity.AbstractParty
import net.corda.core.transactions.LedgerTransaction
// The dummy contract doesn't do anything useful. It exists for testing purposes.
/**
* Dummy contract state for testing of the upgrade process.
*/
class DummyContractV3 : UpgradedContractWithLegacyConstraint<DummyContractV2.State, DummyContractV3.State> {
companion object {
const val PROGRAM_ID: ContractClassName = "net.corda.testing.contracts.DummyContractV3"
}
override val legacyContract: String = DummyContractV2.PROGRAM_ID
override val legacyContractConstraint: AttachmentConstraint = AlwaysAcceptAttachmentConstraint
data class State(val magicNumber: Int = 0, val owners: List<AbstractParty>) : ContractState {
override val participants: List<AbstractParty> = owners
}
interface Commands : CommandData {
class Create : TypeOnlyCommandData(), Commands
class Move : TypeOnlyCommandData(), Commands
}
override fun upgrade(state: DummyContractV2.State): State {
return State(state.magicNumber, state.participants)
}
override fun verify(tx: LedgerTransaction) {
// Other verifications.
}
}