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