From 28e83d1e66a3dc0e749eae8ccf4fe07c66ab6815 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Mon, 13 Feb 2017 15:39:48 +0000 Subject: [PATCH] Example code for contract upgrade using RPC. (#237) * Added missing out modifier to UpgradedContract class * Added ContractUpgradeFlow.Instigator to whitelist in AbstractNode * Added test for contract upgrade using RPC --- .../corda/core/contracts/DummyContractV2.kt | 2 +- .../net/corda/core/contracts/Structures.kt | 4 +- .../net/corda/core/messaging/CordaRPCOps.kt | 2 +- .../net/corda/core/node/services/Services.kt | 4 +- .../net/corda/flows/ContractUpgradeFlow.kt | 16 +-- .../core/flows/ContractUpgradeFlowTest.kt | 72 ++++++++++++- docs/source/contract-upgrade.rst | 100 +++++++++++++++--- docs/source/index.rst | 2 +- .../net/corda/node/internal/AbstractNode.kt | 3 +- .../corda/node/internal/CordaRPCOpsImpl.kt | 2 +- .../node/services/vault/NodeVaultService.kt | 6 +- 11 files changed, 175 insertions(+), 38 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt b/core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt index c4cf32b255..e4dc7eac81 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt @@ -12,7 +12,7 @@ val DUMMY_V2_PROGRAM_ID = DummyContractV2() * Dummy contract state for testing of the upgrade process. */ class DummyContractV2 : UpgradedContract { - override val legacyContract = DUMMY_PROGRAM_ID + override val legacyContract = DummyContract::class.java data class State(val magicNumber: Int = 0, val owners: List) : ContractState { override val contract = DUMMY_V2_PROGRAM_ID diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index 3898132ee9..79aceba0b4 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -398,7 +398,7 @@ interface NetCommand : CommandData { } /** Indicates that this transaction replaces the inputs contract state to another contract state */ -data class UpgradeCommand(val upgradedContractClass: Class>) : CommandData +data class UpgradeCommand(val upgradedContractClass: Class>) : CommandData /** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */ data class AuthenticatedObject( @@ -456,7 +456,7 @@ interface Contract { * @param NewState the upgraded contract state. */ interface UpgradedContract : Contract { - val legacyContract: Contract + val legacyContract: Class /** * Upgrade contract's state object to a new state object. * diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index d72a7a8f1c..6e766d04c1 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -114,7 +114,7 @@ interface CordaRPCOps : RPCOps { * Invoking this method indicate the node is willing to upgrade the [state] using the [upgradedContractClass]. * This method will NOT initiate the upgrade process. To start the upgrade process, see [ContractUpgradeFlow.Instigator]. */ - fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class>) + fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class>) /** * Authorise a contract state upgrade. diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index 7c3ea19bf0..a65e3a5a40 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -165,7 +165,7 @@ interface VaultService { /** Get contracts we would be willing to upgrade the suggested contract to. */ // TODO: We need a better place to put business logic functions - fun getAuthorisedContractUpgrade(ref: StateRef): Class>? + fun getAuthorisedContractUpgrade(ref: StateRef): Class>? /** * Authorise a contract state upgrade. @@ -173,7 +173,7 @@ interface VaultService { * Invoking this method indicate the node is willing to upgrade the [state] using the [upgradedContractClass]. * This method will NOT initiate the upgrade process. To start the upgrade process, see [ContractUpgradeFlow.Instigator]. */ - fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class>) + fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class>) /** * Authorise a contract state upgrade. diff --git a/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt b/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt index 0e945a946a..35689cbba4 100644 --- a/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt @@ -33,7 +33,7 @@ object ContractUpgradeFlow { val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract requireThat { "The signing keys include all participant keys" by keysThatSigned.containsAll(participants) - "Inputs state reference the legacy contract" by (input.contract.javaClass == upgradedContract.legacyContract.javaClass) + "Inputs state reference the legacy contract" by (input.contract.javaClass == upgradedContract.legacyContract) "Outputs state reference the upgraded contract" by (output.contract.javaClass == command.upgradedContractClass) "Output state must be an upgraded version of the input state" by (output == upgradedContract.upgrade(input)) } @@ -41,17 +41,17 @@ object ContractUpgradeFlow { private fun assembleBareTx( stateRef: StateAndRef, - upgradedContractClass: Class> + upgradedContractClass: Class> ): TransactionBuilder { val contractUpgrade = upgradedContractClass.newInstance() return TransactionType.General.Builder(stateRef.state.notary) - .withItems(stateRef, contractUpgrade.upgrade(stateRef.state.data), Command(UpgradeCommand(contractUpgrade.javaClass), stateRef.state.data.participants)) + .withItems(stateRef, contractUpgrade.upgrade(stateRef.state.data), Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants)) } - class Instigator( + class Instigator( originalState: StateAndRef, - newContractClass: Class> - ) : AbstractStateReplacementFlow.Instigator>>(originalState, newContractClass) { + newContractClass: Class> + ) : AbstractStateReplacementFlow.Instigator>>(originalState, newContractClass) { override fun assembleTx(): Pair> { val stx = assembleBareTx(originalState, modification) @@ -61,10 +61,10 @@ object ContractUpgradeFlow { } } - class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor>>(otherSide) { + class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor>>(otherSide) { @Suspendable @Throws(StateReplacementException::class) - override fun verifyProposal(proposal: Proposal>>) { + override fun verifyProposal(proposal: Proposal>>) { // Retrieve signed transaction from our side, we will apply the upgrade logic to the transaction on our side, and verify outputs matches the proposed upgrade. val stx = subFlow(FetchTransactionsFlow(setOf(proposal.stateRef.txhash), otherSide)).fromDisk.singleOrNull() requireNotNull(stx) { "We don't have a copy of the referenced state" } diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 26182caa86..b87e9dc668 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -6,11 +6,16 @@ import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Party import net.corda.core.crypto.SecureHash import net.corda.core.getOrThrow +import net.corda.core.messaging.startFlow import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.Emoji import net.corda.flows.CashIssueFlow import net.corda.flows.ContractUpgradeFlow import net.corda.flows.FinalityFlow +import net.corda.node.internal.CordaRPCOpsImpl +import net.corda.node.services.User +import net.corda.node.services.messaging.CURRENT_RPC_USER +import net.corda.node.services.startFlowPermission import net.corda.node.utilities.databaseTransaction import net.corda.testing.node.MockNetwork import org.junit.After @@ -60,15 +65,15 @@ class ContractUpgradeFlowTest { requireNotNull(btx) // The request is expected to be rejected because party B haven't authorise the upgrade yet. - val rejectedFuture = a.services.startFlow(ContractUpgradeFlow.Instigator(atx!!.tx.outRef(0), DUMMY_V2_PROGRAM_ID.javaClass)).resultFuture + val rejectedFuture = a.services.startFlow(ContractUpgradeFlow.Instigator(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture mockNet.runNetwork() assertFailsWith(ExecutionException::class) { rejectedFuture.get() } // Party B authorise the contract state upgrade. - b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef(0), DUMMY_V2_PROGRAM_ID.javaClass) + b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef(0), DummyContractV2::class.java) // Party A initiates contract upgrade flow, expected to succeed this time. - val resultFuture = a.services.startFlow(ContractUpgradeFlow.Instigator(atx.tx.outRef(0), DUMMY_V2_PROGRAM_ID.javaClass)).resultFuture + val resultFuture = a.services.startFlow(ContractUpgradeFlow.Instigator(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture mockNet.runNetwork() val result = resultFuture.get() @@ -87,6 +92,63 @@ class ContractUpgradeFlowTest { } } + @Test + fun `2 parties contract upgrade using RPC`() { + // Create dummy contract. + val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.legalIdentity.ref(1), b.info.legalIdentity.ref(1)) + val stx = twoPartyDummyContract.signWith(a.services.legalIdentityKey) + .signWith(b.services.legalIdentityKey) + .toSignedTransaction() + + a.services.startFlow(FinalityFlow(stx, setOf(a.info.legalIdentity, b.info.legalIdentity))) + mockNet.runNetwork() + + val atx = databaseTransaction(a.database) { a.services.storageService.validatedTransactions.getTransaction(stx.id) } + val btx = databaseTransaction(b.database) { b.services.storageService.validatedTransactions.getTransaction(stx.id) } + requireNotNull(atx) + requireNotNull(btx) + + // The request is expected to be rejected because party B haven't authorise the upgrade yet. + + val rpcA = CordaRPCOpsImpl(a.services, a.smm, a.database) + val rpcB = CordaRPCOpsImpl(b.services, b.smm, b.database) + + CURRENT_RPC_USER.set(User("user", "pwd", permissions = setOf( + startFlowPermission>() + ))) + + val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Instigator(stateAndRef, upgrade) }, + atx!!.tx.outRef(0), + DummyContractV2::class.java).returnValue + + mockNet.runNetwork() + assertFailsWith(ExecutionException::class) { rejectedFuture.get() } + + // Party B authorise the contract state upgrade. + rpcB.authoriseContractUpgrade(btx!!.tx.outRef(0), DummyContractV2::class.java) + + // Party A initiates contract upgrade flow, expected to succeed this time. + val resultFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Instigator(stateAndRef, upgrade) }, + atx.tx.outRef(0), + DummyContractV2::class.java).returnValue + + mockNet.runNetwork() + val result = resultFuture.get() + // Check results. + listOf(a, b).forEach { + val stx = databaseTransaction(a.database) { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) } + requireNotNull(stx) + + // Verify inputs. + val input = databaseTransaction(a.database) { a.services.storageService.validatedTransactions.getTransaction(stx!!.tx.inputs.single().txhash) } + requireNotNull(input) + assertTrue(input!!.tx.outputs.single().data is DummyContract.State) + + // Verify outputs. + assertTrue(stx!!.tx.outputs.single().data is DummyContractV2.State) + } + } + @Test fun `upgrade Cash to v2`() { // Create some cash. @@ -94,7 +156,7 @@ class ContractUpgradeFlowTest { mockNet.runNetwork() val stateAndRef = result.getOrThrow().tx.outRef(0) // Starts contract upgrade flow. - a.services.startFlow(ContractUpgradeFlow.Instigator(stateAndRef, CashV2().javaClass)) + a.services.startFlow(ContractUpgradeFlow.Instigator(stateAndRef, CashV2::class.java)) mockNet.runNetwork() // Get contract state form the vault. val state = databaseTransaction(a.database) { a.vault.currentVault.states } @@ -104,7 +166,7 @@ class ContractUpgradeFlowTest { } class CashV2 : UpgradedContract { - override val legacyContract = Cash() + override val legacyContract = Cash::class.java data class State(override val amount: Amount>, val owners: List) : FungibleAsset { override val owner: CompositeKey = owners.first() diff --git a/docs/source/contract-upgrade.rst b/docs/source/contract-upgrade.rst index f249ece091..aa34df8915 100644 --- a/docs/source/contract-upgrade.rst +++ b/docs/source/contract-upgrade.rst @@ -1,3 +1,9 @@ +.. highlight:: kotlin +.. raw:: html + + + + Upgrading Contracts =================== @@ -48,19 +54,18 @@ Currently the vault service is used to manage the authorisation records. The adm .. sourcecode:: kotlin - /** - * Authorise a contract state upgrade. - * This will store the upgrade authorisation in the vault, and will be queried by [ContractUpgradeFlow.Acceptor] during contract upgrade process. - * Invoking this method indicate the node is willing to upgrade the [state] using the [upgradedContractClass]. - * This method will NOT initiate the upgrade process. To start the upgrade process, see [ContractUpgradeFlow.Instigator]. - */ - fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class>) - - /** - * Authorise a contract state upgrade. - * This will remove the upgrade authorisation from the vault. - */ - fun deauthoriseContractUpgrade(state: StateAndRef<*>) + /** + * Authorise a contract state upgrade. + * This will store the upgrade authorisation in the vault, and will be queried by [ContractUpgradeFlow.Acceptor] during contract upgrade process. + * Invoking this method indicate the node is willing to upgrade the [state] using the [upgradedContractClass]. + * This method will NOT initiate the upgrade process. To start the upgrade process, see [ContractUpgradeFlow.Instigator]. + */ + fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class>) + /** + * Authorise a contract state upgrade. + * This will remove the upgrade authorisation from the vault. + */ + fun deauthoriseContractUpgrade(state: StateAndRef<*>) @@ -70,3 +75,72 @@ Proposing an upgrade After all parties have registered the intention of upgrading the contract state, one of the contract participant can initiate the upgrade process by running the contract upgrade flow. The Instigator will create a new state and sent to each participant for signatures, each of the participants (Acceptor) will verify and sign the proposal and returns to the instigator. The transaction will be notarised and persisted once every participant verified and signed the upgrade proposal. + +Examples +-------- + +Lets assume Bank A has entered into an agreement with Bank B, and the contract is translated into contract code ``DummyContract`` with state object ``DummyContractState``. + +Few days after the exchange of contracts, the developer of the contract code discovered a bug/misrepresentation in the contract code. +Bank A and Bank B decided to upgrade the contract to ``DummyContractV2`` + +1. Developer will create a new contract extending the ``UpgradedContract`` class, and a new state object ``DummyContractV2.State`` referencing the new contract. + +.. container:: codeset + + .. sourcecode:: kotlin + + class DummyContractV2 : UpgradedContract { + override val legacyContract = DummyContract::class.java + + data class State(val magicNumber: Int = 0, val owners: List) : ContractState { + override val contract = DUMMY_V2_PROGRAM_ID + override val participants: List = owners + } + + interface Commands : CommandData { + class Create : TypeOnlyCommandData(), Commands + class Move : TypeOnlyCommandData(), Commands + } + + override fun upgrade(state: DummyContract.State): DummyContractV2.State { + return DummyContractV2.State(state.magicNumber, state.participants) + } + + override fun verify(tx: TransactionForContract) { + if (tx.commands.any { it.value is UpgradeCommand }) ContractUpgradeFlow.verify(tx) + // Other verifications. + } + + // The "empty contract" + override val legalContractReference: SecureHash = SecureHash.sha256("") + } + +2. Bank A will instruct its node to accept the contract upgrade to ``DummyContractV2`` for the contract state. + +.. container:: codeset + + .. sourcecode:: kotlin + + val rpcClient : CordaRPCClient = << Bank A's Corda RPC Client >> + val rpcA = rpcClient.proxy() + rpcA.authoriseContractUpgrade(<>, DummyContractV2::class.java) + +3. Bank B now initiate the upgrade Flow, this will send a upgrade proposal to all contract participants. +Each of the participants of the contract state will sign and return the contract state upgrade proposal once they have validated and agreed with the upgrade. +The upgraded transaction state will be recorded in every participant's node at the end of the flow. + +.. container:: codeset + + .. sourcecode:: kotlin + + val rpcClient : CordaRPCClient = << Bank B's Corda RPC Client >> + val rpcB = rpcClient.proxy() + rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Instigator(stateAndRef, upgrade) }, + <>, + DummyContractV2::class.java) + +.. note:: See ``ContractUpgradeFlowTest.2 parties contract upgrade using RPC`` for more detailed code example. + + + diff --git a/docs/source/index.rst b/docs/source/index.rst index e9490ad7a3..1aa4def6d5 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -87,6 +87,7 @@ Documentation Contents: tutorial-contract tutorial-contract-clauses tutorial-test-dsl + contract-upgrade tutorial-integration-testing tutorial-clientrpc-api tutorial-building-transactions @@ -105,7 +106,6 @@ Documentation Contents: network-simulator clauses merkle-trees - contract-upgrade .. toctree:: :maxdepth: 2 diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index f353c3996b..a01f53c47e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -84,7 +84,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, CashExitFlow::class.java to setOf(Amount::class.java, PartyAndReference::class.java), CashIssueFlow::class.java to setOf(Amount::class.java, OpaqueBytes::class.java, Party::class.java), CashPaymentFlow::class.java to setOf(Amount::class.java, Party::class.java), - FinalityFlow::class.java to emptySet() + FinalityFlow::class.java to emptySet(), + ContractUpgradeFlow.Instigator::class.java to emptySet() ) } diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index b4bb235f74..0b7dd19fd4 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -103,7 +103,7 @@ class CordaRPCOpsImpl( override fun attachmentExists(id: SecureHash) = services.storageService.attachments.openAttachment(id) != null override fun uploadAttachment(jar: InputStream) = services.storageService.attachments.importAttachment(jar) - override fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class>) = services.vaultService.authoriseContractUpgrade(state, upgradedContractClass) + override fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class>) = services.vaultService.authoriseContractUpgrade(state, upgradedContractClass) override fun deauthoriseContractUpgrade(state: StateAndRef<*>) = services.vaultService.deauthoriseContractUpgrade(state) override fun currentNodeTime(): Instant = Instant.now(services.clock) @Suppress("OverridingDeprecatedMember", "DEPRECATION") diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 32e7da4ecc..6d11a1abbb 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -338,13 +338,13 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT } // TODO : Persists this in DB. - private val authorisedUpgrade = mutableMapOf>>() + private val authorisedUpgrade = mutableMapOf>>() override fun getAuthorisedContractUpgrade(ref: StateRef) = authorisedUpgrade[ref] - override fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class>) { + override fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class>) { val upgrade = upgradedContractClass.newInstance() - if (upgrade.legacyContract.javaClass != stateAndRef.state.data.contract.javaClass) { + if (upgrade.legacyContract != stateAndRef.state.data.contract.javaClass) { throw IllegalArgumentException("The contract state cannot be upgraded using provided UpgradedContract.") } authorisedUpgrade.put(stateAndRef.ref, upgradedContractClass)