mirror of
https://github.com/corda/corda.git
synced 2025-01-29 15:43:55 +00:00
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:
parent
68d736dd81
commit
715c38766d
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user