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
This commit is contained in:
Patrick Kuo 2017-02-13 15:39:48 +00:00 committed by GitHub
parent 36052cbd63
commit 28e83d1e66
11 changed files with 175 additions and 38 deletions

View File

@ -12,7 +12,7 @@ val DUMMY_V2_PROGRAM_ID = DummyContractV2()
* Dummy contract state for testing of the upgrade process.
*/
class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> {
override val legacyContract = DUMMY_PROGRAM_ID
override val legacyContract = DummyContract::class.java
data class State(val magicNumber: Int = 0, val owners: List<CompositeKey>) : ContractState {
override val contract = DUMMY_V2_PROGRAM_ID

View File

@ -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<UpgradedContract<*, *>>) : CommandData
data class UpgradeCommand(val upgradedContractClass: Class<out UpgradedContract<*, *>>) : CommandData
/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
data class AuthenticatedObject<out T : Any>(
@ -456,7 +456,7 @@ interface Contract {
* @param NewState the upgraded contract state.
*/
interface UpgradedContract<in OldState : ContractState, out NewState : ContractState> : Contract {
val legacyContract: Contract
val legacyContract: Class<out Contract>
/**
* Upgrade contract's state object to a new state object.
*

View File

@ -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<UpgradedContract<*, *>>)
fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class<out UpgradedContract<*, *>>)
/**
* Authorise a contract state upgrade.

View File

@ -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<UpgradedContract<*, *>>?
fun getAuthorisedContractUpgrade(ref: StateRef): Class<out UpgradedContract<*, *>>?
/**
* 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<UpgradedContract<*, *>>)
fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class<out UpgradedContract<*, *>>)
/**
* Authorise a contract state upgrade.

View File

@ -33,7 +33,7 @@ object ContractUpgradeFlow {
val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract<ContractState, *>
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 <OldState : ContractState, NewState : ContractState> assembleBareTx(
stateRef: StateAndRef<OldState>,
upgradedContractClass: Class<UpgradedContract<OldState, NewState>>
upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>
): 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<OldState : ContractState, NewState : ContractState>(
class Instigator<OldState : ContractState, out NewState : ContractState>(
originalState: StateAndRef<OldState>,
newContractClass: Class<UpgradedContract<OldState, NewState>>
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
newContractClass: Class<out UpgradedContract<OldState, NewState>>
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<out UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
override fun assembleTx(): Pair<SignedTransaction, Iterable<CompositeKey>> {
val stx = assembleBareTx(originalState, modification)
@ -61,10 +61,10 @@ object ContractUpgradeFlow {
}
}
class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Class<UpgradedContract<ContractState, *>>>(otherSide) {
class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Class<out UpgradedContract<ContractState, *>>>(otherSide) {
@Suspendable
@Throws(StateReplacementException::class)
override fun verifyProposal(proposal: Proposal<Class<UpgradedContract<ContractState, *>>>) {
override fun verifyProposal(proposal: Proposal<Class<out UpgradedContract<ContractState, *>>>) {
// 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" }

View File

@ -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<DummyContract.State, DummyContractV2.State>(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<ContractState>(0), DUMMY_V2_PROGRAM_ID.javaClass)
b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)
// Party A initiates contract upgrade flow, expected to succeed this time.
val resultFuture = a.services.startFlow(ContractUpgradeFlow.Instigator<DummyContract.State, DummyContractV2.State>(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<ContractUpgradeFlow.Instigator<*, *>>()
)))
val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Instigator(stateAndRef, upgrade) },
atx!!.tx.outRef<DummyContract.State>(0),
DummyContractV2::class.java).returnValue
mockNet.runNetwork()
assertFailsWith(ExecutionException::class) { rejectedFuture.get() }
// Party B authorise the contract state upgrade.
rpcB.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(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<DummyContract.State>(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<Cash.State>(0)
// Starts contract upgrade flow.
a.services.startFlow(ContractUpgradeFlow.Instigator<Cash.State, CashV2.State>(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<Cash.State, CashV2.State> {
override val legacyContract = Cash()
override val legacyContract = Cash::class.java
data class State(override val amount: Amount<Issued<Currency>>, val owners: List<CompositeKey>) : FungibleAsset<Currency> {
override val owner: CompositeKey = owners.first()

View File

@ -1,3 +1,9 @@
.. highlight:: kotlin
.. raw:: html
<script type="text/javascript" src="_static/jquery.js"></script>
<script type="text/javascript" src="_static/codesets.js"></script>
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<UpgradedContract<*, *>>)
/**
* 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<UpgradedContract<*, *>>)
/**
* 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<DummyContract.State, DummyContractV2.State> {
override val legacyContract = DummyContract::class.java
data class State(val magicNumber: Int = 0, val owners: List<CompositeKey>) : ContractState {
override val contract = DUMMY_V2_PROGRAM_ID
override val participants: List<CompositeKey> = 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(<<StateAndRef of the contract state>>, 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) },
<<StateAndRef of the contract state>>,
DummyContractV2::class.java)
.. note:: See ``ContractUpgradeFlowTest.2 parties contract upgrade using RPC`` for more detailed code example.

View File

@ -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

View File

@ -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()
)
}

View File

@ -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<UpgradedContract<*, *>>) = services.vaultService.authoriseContractUpgrade(state, upgradedContractClass)
override fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class<out UpgradedContract<*, *>>) = 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")

View File

@ -338,13 +338,13 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
}
// TODO : Persists this in DB.
private val authorisedUpgrade = mutableMapOf<StateRef, Class<UpgradedContract<*, *>>>()
private val authorisedUpgrade = mutableMapOf<StateRef, Class<out UpgradedContract<*, *>>>()
override fun getAuthorisedContractUpgrade(ref: StateRef) = authorisedUpgrade[ref]
override fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class<UpgradedContract<*, *>>) {
override fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class<out UpgradedContract<*, *>>) {
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)