CORDA-2109 - Fix a bug that prevents consecutive multiparty contract upgrades (#4131)

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-30 10:14:03 +00:00 committed by Katelyn Baker
parent 13843f1cae
commit 7e4a5413ac
3 changed files with 61 additions and 13 deletions

View File

@ -22,6 +22,7 @@ import net.corda.node.internal.StartedNode
import net.corda.node.services.Permissions.Companion.startFlow import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyContractV2 import net.corda.testing.contracts.DummyContractV2
import net.corda.testing.contracts.DummyContractV3
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.singleIdentity import net.corda.testing.core.singleIdentity
@ -99,17 +100,28 @@ class ContractUpgradeFlowTest {
mockNet.runNetwork() mockNet.runNetwork()
val result = resultFuture.resultFuture.getOrThrow() val result = resultFuture.resultFuture.getOrThrow()
check<DummyContract.State, DummyContractV2.State>(aliceNode, result)
check<DummyContract.State, DummyContractV2.State>(bobNode, result)
fun check(node: StartedNode<MockNode>) { // Check that the updated state can be further upgraded to V3
val upgradeTx = node.database.transaction { // Party B authorise the contract state upgrade
val wtx = node.services.validatedTransactions.getTransaction(result.ref.txhash) bobNode.services.startFlow(ContractUpgradeFlow.Authorise(result, DummyContractV3::class.java)).resultFuture.getOrThrow()
wtx!!.resolveContractUpgradeTransaction(node.services)
} val resultFuture2 = aliceNode.services.startFlow(ContractUpgradeFlow.Initiate(result, DummyContractV3::class.java))
assertTrue(upgradeTx.inputs.single().state.data is DummyContract.State) mockNet.runNetwork()
assertTrue(upgradeTx.outputs.single().data is DummyContractV2.State) val result2 = resultFuture2.resultFuture.getOrThrow()
check<DummyContractV2.State, DummyContractV3.State>(aliceNode, result2)
check<DummyContractV2.State, DummyContractV3.State>(bobNode, result2)
}
private inline fun <reified OLD: ContractState, reified NEW: ContractState> check(node: StartedNode<MockNode>, state: StateAndRef<NEW>) {
val upgradeTx = node.database.transaction {
val wtx = node.services.validatedTransactions.getTransaction(state.ref.txhash)
wtx!!.resolveContractUpgradeTransaction(node.services)
} }
check(aliceNode) assertTrue(upgradeTx.inputs.single().state.data is OLD)
check(bobNode) assertTrue(upgradeTx.outputs.single().data is NEW)
} }
private fun RPCDriverDSL.startProxy(node: StartedNode<MockNode>, user: User): CordaRPCOps { private fun RPCDriverDSL.startProxy(node: StartedNode<MockNode>, user: User): CordaRPCOps {

View File

@ -7,8 +7,8 @@ import net.corda.core.contracts.requireThat
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.ContractUpgradeUtils import net.corda.core.internal.ContractUpgradeUtils
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.node.StatesToRecord import net.corda.core.node.StatesToRecord
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
// TODO: We should have a whitelist of contracts we're willing to accept at all, and reject if the transaction // TODO: We should have a whitelist of contracts we're willing to accept at all, and reject if the transaction
@ -54,9 +54,9 @@ class ContractUpgradeHandler(otherSide: FlowSession) : AbstractStateReplacementF
// verify outputs matches the proposed upgrade. // verify outputs matches the proposed upgrade.
val ourSTX = serviceHub.validatedTransactions.getTransaction(proposal.stateRef.txhash) val ourSTX = serviceHub.validatedTransactions.getTransaction(proposal.stateRef.txhash)
requireNotNull(ourSTX) { "We don't have a copy of the referenced state" } 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) ?: val authorisedUpgrade = serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref)
throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}") ?: throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}")
val proposedTx = stx.coreTransaction as ContractUpgradeWireTransaction val proposedTx = stx.coreTransaction as ContractUpgradeWireTransaction
val expectedTx = ContractUpgradeUtils.assembleUpgradeTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt, serviceHub) val expectedTx = ContractUpgradeUtils.assembleUpgradeTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt, serviceHub)
requireThat { requireThat {

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.
}
}