Contract Upgrade API improvements + persistence (#1392)

* All Contract Upgrade functionality performed within a corresponding flow.
Removed RPC API calls for contract upgrade authorisation / de-authorisation.
Added persistence using AppendOnlyPersistentMap.

* Changed to using a PersistentMap to ensure entries can be removed (was causing failing de-authorisation tests).
Fixed all warnings.

* Added mandatory @Suspendable annotations to flows.

* Do not instantiate object unless overridden.

* Updated changelog and CordaDocs.

* Persistence simplification: only store upgrade contract class name (not serialized object)

* Remove nullability from contract_class_name DB column.
This commit is contained in:
josecoll 2017-09-05 13:23:19 +01:00 committed by GitHub
parent 377ca95387
commit ebc9cacb53
14 changed files with 252 additions and 160 deletions

View File

@ -138,6 +138,7 @@ abstract class AbstractStateReplacementFlow {
// We use Void? instead of Unit? as that's what you'd use in Java.
abstract class Acceptor<in T>(val otherSide: Party,
override val progressTracker: ProgressTracker = Acceptor.tracker()) : FlowLogic<Void?>() {
constructor(otherSide: Party) : this(otherSide, Acceptor.tracker())
companion object {
object VERIFYING : ProgressTracker.Step("Verifying state replacement proposal")
object APPROVING : ProgressTracker.Step("State replacement approved")

View File

@ -1,72 +1,146 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.*
import net.corda.core.identity.Party
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import java.security.PublicKey
/**
* A flow to be used for upgrading state objects of an old contract to a new contract.
* A flow to be used for authorising and upgrading state objects of an old contract to a new contract.
*
* This assembles the transaction for contract upgrade and sends out change proposals to all participants
* of that state. If participants agree to the proposed change, they each sign the transaction.
* Finally, the transaction containing all signatures is sent back to each participant so they can record it and
* use the new updated state for future transactions.
*/
@InitiatingFlow
@StartableByRPC
class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState>(
originalState: StateAndRef<OldState>,
newContractClass: Class<out UpgradedContract<OldState, NewState>>
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<out UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
object ContractUpgradeFlow {
companion object {
@JvmStatic
fun verify(tx: LedgerTransaction) {
// Contract Upgrade transaction should have 1 input, 1 output and 1 command.
verify(
tx.inputStates.single(),
tx.outputStates.single(),
tx.commandsOfType<UpgradeCommand>().single())
/**
* Authorise a contract state upgrade.
* This will store the upgrade authorisation in persistent store, and will be queried by [ContractUpgradeFlow.Acceptor] during contract upgrade process.
* Invoking this flow indicates the node is willing to upgrade the [StateAndRef] using the [UpgradedContract] class.
* This method will NOT initiate the upgrade process. To start the upgrade process, see [Initiator].
*/
@StartableByRPC
class Authorise(
val stateAndRef: StateAndRef<*>,
private val upgradedContractClass: Class<out UpgradedContract<*, *>>
) : FlowLogic<Void?>() {
@Suspendable
override fun call(): Void? {
val upgrade = upgradedContractClass.newInstance()
if (upgrade.legacyContract != stateAndRef.state.data.contract.javaClass) {
throw FlowException("The contract state cannot be upgraded using provided UpgradedContract.")
}
serviceHub.contractUpgradeService.storeAuthorisedContractUpgrade(stateAndRef.ref, upgradedContractClass)
return null
}
@JvmStatic
fun verify(input: ContractState, output: ContractState, commandData: Command<UpgradeCommand>) {
val command = commandData.value
val participantKeys: Set<PublicKey> = input.participants.map { it.owningKey }.toSet()
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet()
@Suppress("UNCHECKED_CAST")
val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract<ContractState, *>
requireThat {
"The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys)
"Inputs state reference the legacy contract" using (input.contract.javaClass == upgradedContract.legacyContract)
"Outputs state reference the upgraded contract" using (output.contract.javaClass == command.upgradedContractClass)
"Output state must be an upgraded version of the input state" using (output == upgradedContract.upgrade(input))
}
/**
* Deauthorise a contract state upgrade.
* This will remove the upgrade authorisation from persistent store (and prevent any further upgrade)
*/
@StartableByRPC
class Deauthorise(
val stateRef: StateRef
) : FlowLogic< Void?>() {
@Suspendable
override fun call(): Void? {
serviceHub.contractUpgradeService.removeAuthorisedContractUpgrade(stateRef)
return null
}
}
@InitiatingFlow
@StartableByRPC
class Initiator<OldState : ContractState, out NewState : ContractState>(
originalState: StateAndRef<OldState>,
newContractClass: Class<out UpgradedContract<OldState, NewState>>
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<out UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
companion object {
fun <OldState : ContractState, NewState : ContractState> assembleBareTx(
stateRef: StateAndRef<OldState>,
upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>,
privacySalt: PrivacySalt
): TransactionBuilder {
val contractUpgrade = upgradedContractClass.newInstance()
return TransactionBuilder(stateRef.state.notary)
.withItems(
stateRef,
contractUpgrade.upgrade(stateRef.state.data),
Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants.map { it.owningKey }),
privacySalt
)
}
}
fun <OldState : ContractState, NewState : ContractState> assembleBareTx(
stateRef: StateAndRef<OldState>,
upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>,
privacySalt: PrivacySalt
): TransactionBuilder {
val contractUpgrade = upgradedContractClass.newInstance()
return TransactionBuilder(stateRef.state.notary)
.withItems(
stateRef,
contractUpgrade.upgrade(stateRef.state.data),
Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants.map { it.owningKey }),
privacySalt
)
@Suspendable
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
val baseTx = assembleBareTx(originalState, modification, PrivacySalt())
val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet()
// TODO: We need a much faster way of finding our key in the transaction
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
val stx = serviceHub.signInitialTransaction(baseTx, myKey)
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
}
}
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
val baseTx = assembleBareTx(originalState, modification, PrivacySalt())
val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet()
// TODO: We need a much faster way of finding our key in the transaction
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
val stx = serviceHub.signInitialTransaction(baseTx, myKey)
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
@StartableByRPC
@InitiatedBy(ContractUpgradeFlow.Initiator::class)
class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Class<out UpgradedContract<ContractState, *>>>(otherSide) {
companion object {
@JvmStatic
fun verify(tx: LedgerTransaction) {
// Contract Upgrade transaction should have 1 input, 1 output and 1 command.
verify(tx.inputStates.single(),
tx.outputStates.single(),
tx.commandsOfType<UpgradeCommand>().single())
}
@JvmStatic
fun verify(input: ContractState, output: ContractState, commandData: Command<UpgradeCommand>) {
val command = commandData.value
val participantKeys: Set<PublicKey> = input.participants.map { it.owningKey }.toSet()
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet()
@Suppress("UNCHECKED_CAST")
val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract<ContractState, *>
requireThat {
"The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys)
"Inputs state reference the legacy contract" using (input.contract.javaClass == upgradedContract.legacyContract)
"Outputs state reference the upgraded contract" using (output.contract.javaClass == command.upgradedContractClass)
"Output state must be an upgraded version of the input state" using (output == upgradedContract.upgrade(input))
}
}
}
@Suspendable
@Throws(StateReplacementException::class)
override fun verifyProposal(stx: SignedTransaction, proposal: AbstractStateReplacementFlow.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 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 authorisedUpgrade = serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?:
throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}")
val proposedTx = stx.tx
val expectedTx = ContractUpgradeFlow.Initiator.assembleBareTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt).toWireTransaction()
requireThat {
"The instigator is one of the participants" using (otherSide in oldStateAndRef.state.data.participants)
"The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" using (proposal.modification.name == authorisedUpgrade)
"The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx)
}
ContractUpgradeFlow.Acceptor.verify(
oldStateAndRef.state.data,
expectedTx.outRef<ContractState>(0).state.data,
expectedTx.toLedgerTransaction(serviceHub).commandsOfType<UpgradeCommand>().single())
}
}
}

View File

@ -123,11 +123,11 @@ interface CordaRPCOps : RPCOps {
*
* Generic vault query function which takes a [QueryCriteria] object to define filters,
* optional [PageSpecification] and optional [Sort] modification criteria (default unsorted),
* and returns a [Vault.PageAndUpdates] object containing
* 1) a snapshot as a [Vault.Page] (described previously in [queryBy])
* and returns a [DataFeed] object containing
* 1) a snapshot as a [Vault.Page] (described previously in [CordaRPCOps.vaultQueryBy])
* 2) an [Observable] of [Vault.Update]
*
* Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function.
* Notes: the snapshot part of the query adheres to the same behaviour as the [CordaRPCOps.vaultQueryBy] function.
* the [QueryCriteria] applies to both snapshot and deltas (streaming updates).
*/
// DOCSTART VaultTrackByAPI
@ -239,20 +239,6 @@ interface CordaRPCOps : RPCOps {
*/
fun uploadAttachment(jar: InputStream): SecureHash
/**
* 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<out UpgradedContract<*, *>>)
/**
* Authorise a contract state upgrade.
* This will remove the upgrade authorisation from the vault.
*/
fun deauthoriseContractUpgrade(state: StateAndRef<*>)
/**
* Returns the node's current time.
*/
@ -338,7 +324,7 @@ inline fun <T, reified R : FlowLogic<T>> CordaRPCOps.startFlow(
flowConstructor: () -> R
): FlowHandle<T> = startFlowDynamic(R::class.java)
inline fun <T, A, reified R : FlowLogic<T>> CordaRPCOps.startFlow(
inline fun <T , A, reified R : FlowLogic<T>> CordaRPCOps.startFlow(
@Suppress("UNUSED_PARAMETER")
flowConstructor: (A) -> R,
arg0: A

View File

@ -1,8 +1,8 @@
package net.corda.core.node.services
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.UpgradedContract
import net.corda.core.flows.ContractUpgradeFlow
/**
* The [ContractUpgradeService] is responsible for securely upgrading contract state objects according to
@ -12,19 +12,11 @@ import net.corda.core.contracts.UpgradedContract
interface ContractUpgradeService {
/** Get contracts we would be willing to upgrade the suggested contract to. */
fun getAuthorisedContractUpgrade(ref: StateRef): Class<out UpgradedContract<*, *>>?
fun getAuthorisedContractUpgrade(ref: StateRef): String?
/**
* 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(stateAndRef: StateAndRef<*>, upgradedContractClass: Class<out UpgradedContract<*, *>>)
/** Store authorised state ref and associated UpgradeContract class */
fun storeAuthorisedContractUpgrade(ref: StateRef, upgradedContractClass: Class<out UpgradedContract<*, *>>)
/**
* Authorise a contract state upgrade.
* This will remove the upgrade authorisation from the vault.
*/
fun deauthoriseContractUpgrade(stateAndRef: StateAndRef<*>)
/** Remove a previously authorised state ref */
fun removeAuthorisedContractUpgrade(ref: StateRef)
}

View File

@ -84,15 +84,24 @@ class ContractUpgradeFlowTest {
requireNotNull(btx)
// The request is expected to be rejected because party B hasn't authorised the upgrade yet.
val rejectedFuture = a.services.startFlow(ContractUpgradeFlow(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture
val rejectedFuture = a.services.startFlow(ContractUpgradeFlow.Initiator(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture
mockNet.runNetwork()
assertFailsWith(UnexpectedFlowEndException::class) { rejectedFuture.getOrThrow() }
// Party B authorise the contract state upgrade.
b.services.contractUpgradeService.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)
// Party B authorise the contract state upgrade, and immediately deauthorise the same.
b.services.startFlow(ContractUpgradeFlow.Authorise(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)).resultFuture.getOrThrow()
b.services.startFlow(ContractUpgradeFlow.Deauthorise(btx.tx.outRef<ContractState>(0).ref)).resultFuture.getOrThrow()
// The request is expected to be rejected because party B has subsequently deauthorised and a previously authorised upgrade.
val deauthorisedFuture = a.services.startFlow(ContractUpgradeFlow.Initiator(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture
mockNet.runNetwork()
assertFailsWith(UnexpectedFlowEndException::class) { deauthorisedFuture.getOrThrow() }
// Party B authorise the contract state upgrade
b.services.startFlow(ContractUpgradeFlow.Authorise(btx.tx.outRef<ContractState>(0), DummyContractV2::class.java)).resultFuture.getOrThrow()
// Party A initiates contract upgrade flow, expected to succeed this time.
val resultFuture = a.services.startFlow(ContractUpgradeFlow(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture
val resultFuture = a.services.startFlow(ContractUpgradeFlow.Initiator(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture
mockNet.runNetwork()
val result = resultFuture.getOrThrow()
@ -138,7 +147,10 @@ class ContractUpgradeFlowTest {
val user = rpcTestUser.copy(permissions = setOf(
startFlowPermission<FinalityInvoker>(),
startFlowPermission<ContractUpgradeFlow<*, *>>()
startFlowPermission<ContractUpgradeFlow.Initiator<*, *>>(),
startFlowPermission<ContractUpgradeFlow.Acceptor>(),
startFlowPermission<ContractUpgradeFlow.Authorise>(),
startFlowPermission<ContractUpgradeFlow.Deauthorise>()
))
val rpcA = startProxy(a, user)
val rpcB = startProxy(b, user)
@ -151,18 +163,35 @@ class ContractUpgradeFlowTest {
requireNotNull(atx)
requireNotNull(btx)
val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow(stateAndRef, upgrade) },
val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiator(stateAndRef, upgrade) },
atx!!.tx.outRef<DummyContract.State>(0),
DummyContractV2::class.java).returnValue
mockNet.runNetwork()
assertFailsWith(UnexpectedFlowEndException::class) { rejectedFuture.getOrThrow() }
// Party B authorise the contract state upgrade, and immediately deauthorise the same.
rpcB.startFlow( { stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade ) },
btx!!.tx.outRef<ContractState>(0),
DummyContractV2::class.java).returnValue
rpcB.startFlow( { stateRef -> ContractUpgradeFlow.Deauthorise(stateRef) },
btx.tx.outRef<ContractState>(0).ref).returnValue
// The request is expected to be rejected because party B has subsequently deauthorised and a previously authorised upgrade.
val deauthorisedFuture = rpcA.startFlow( {stateAndRef, upgrade -> ContractUpgradeFlow.Initiator(stateAndRef, upgrade) },
atx.tx.outRef<DummyContract.State>(0),
DummyContractV2::class.java).returnValue
mockNet.runNetwork()
assertFailsWith(UnexpectedFlowEndException::class) { deauthorisedFuture.getOrThrow() }
// Party B authorise the contract state upgrade.
rpcB.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)
rpcB.startFlow( { stateAndRef, upgrade -> ContractUpgradeFlow.Authorise(stateAndRef, upgrade ) },
btx.tx.outRef<ContractState>(0),
DummyContractV2::class.java).returnValue
// Party A initiates contract upgrade flow, expected to succeed this time.
val resultFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow(stateAndRef, upgrade) },
val resultFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiator(stateAndRef, upgrade) },
atx.tx.outRef<DummyContract.State>(0),
DummyContractV2::class.java).returnValue
@ -194,7 +223,7 @@ class ContractUpgradeFlowTest {
val baseState = a.database.transaction { a.services.vaultQueryService.queryBy<ContractState>().states.single() }
assertTrue(baseState.state.data is Cash.State, "Contract state is old version.")
// Starts contract upgrade flow.
val upgradeResult = a.services.startFlow(ContractUpgradeFlow(stateAndRef, CashV2::class.java)).resultFuture
val upgradeResult = a.services.startFlow(ContractUpgradeFlow.Initiator(stateAndRef, CashV2::class.java)).resultFuture
mockNet.runNetwork()
upgradeResult.getOrThrow()
// Get contract state from the vault.

View File

@ -6,6 +6,9 @@ from the previous milestone release.
UNRELEASED
----------
* Contract Upgrades: deprecated RPC authorisation / deauthorisation API calls in favour of equivalent flows in ContractUpgradeFlow.
Implemented contract upgrade persistence using JDBC backed persistent map.
* Vault query common attributes (state status and contract state types) are now handled correctly when using composite
criteria specifications. State status is overridable. Contract states types are aggregatable.

View File

@ -21,23 +21,21 @@ Here's the workflow for contract upgrades:
1. Two banks, A and B negotiate a trade, off-platform
2. Banks A and B execute a protocol to construct a state object representing the trade, using contract X, and include it in a transaction (which is then signed and sent to the Uniqueness Service).
2. Banks A and B execute a protocol to construct a state object representing the trade, using contract X, and include it in a transaction (which is then signed and sent to the consensus service).
3. Time passes.
4. The developer of contract X discovers a bug in the contract code, and releases a new version, contract Y.
And notify the users (e.g. via a mailing list or CorDapp store).
At this point of time all nodes should stop issuing states of contract X.
4. The developer of contract X discovers a bug in the contract code, and releases a new version, contract Y. The developer will then notify all existing users (e.g. via a mailing list or CorDapp store) to stop their nodes from issuing further states with contract X.
5. Banks A and B review the new contract via standard change control processes and identify the contract states they agreed to upgrade, they can decide not to upgrade some contract states as they might be needed for other obligation contract.
5. Banks A and B review the new contract via standard change control processes and identify the contract states they agree to upgrade (they may decide not to upgrade some contract states as these might be needed for some other obligation contract).
6. Banks A and B instruct their Corda nodes (via RPC) to be willing to upgrade state objects of contract X, to state objects for contract Y using agreed upgrade path.
7. One of the parties ``Instigator`` initiates an upgrade of state objects referring to contract X, to a new state object referring to contract Y.
7. One of the parties initiates (``Initiator``) an upgrade of state objects referring to contract X, to a new state object referring to contract Y.
8. A proposed transaction ``Proposal``, taking in the old state and outputting the reissued version, is created and signed with the node's private key.
9. The node ``Instigator`` sends the proposed transaction, along with details of the new contract upgrade path it's proposing, to all participants of the state object.
9. The ``Initiator`` node sends the proposed transaction, along with details of the new contract upgrade path its proposing, to all participants of the state object.
10. Each counterparty ``Acceptor`` verifies the proposal, signs or rejects the state reissuance accordingly, and sends a signature or rejection notification back to the initiating node.
@ -48,32 +46,38 @@ Authorising upgrade
-------------------
Each of the participants in the upgrading contract will have to instruct their node that they are willing to upgrade the state object before the upgrade.
Currently the vault service is used to manage the authorisation records. The administrator can use RPC to perform such instructions.
The ``ContractUpgradeFlow`` is used to manage the authorisation records. The administrator can use RPC to trigger either an ``Authorise`` or ``Deauthorise`` flow.
.. container:: codeset
.. sourcecode:: kotlin
/**
* Authorise a contract state upgrade.
* This will store the upgrade authorisation in the vault, and will be queried by [ContractUpgradeFlow] 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.
*/
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 persistent store, and will be queried by [ContractUpgradeFlow.Acceptor] during contract upgrade process.
* Invoking this flow indicates the node is willing to upgrade the [StateAndRef] using the [UpgradedContract] class.
* This method will NOT initiate the upgrade process. To start the upgrade process, see [Initiator].
*/
@StartableByRPC
class Authorise(
val stateAndRef: StateAndRef<*>,
private val upgradedContractClass: Class<out UpgradedContract<*, *>>
) : FlowLogic<Void?>()
/**
* Deauthorise a contract state upgrade.
* This will remove the upgrade authorisation from persistent store (and prevent any further upgrade)
*/
@StartableByRPC
class Deauthorise(
val stateRef: StateRef
) : FlowLogic< Void?>()
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.
After all parties have registered the intention of upgrading the contract state, one of the contract participants can initiate the upgrade process by triggering the ``Initiator`` contract upgrade flow.
The ``Initiator`` will create a new state and sent to each participant for signatures, each of the participants (Acceptor) will verify, sign the proposal and return to the initiator.
The transaction will be notarised and persisted once every participant verified and signed the upgrade proposal.
Examples
@ -81,7 +85,7 @@ 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.
A 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.
@ -99,7 +103,7 @@ Bank A and Bank B decided to upgrade the contract to ``DummyContractV2``
val rpcClient : CordaRPCClient = << Bank A's Corda RPC Client >>
val rpcA = rpcClient.proxy()
rpcA.authoriseContractUpgrade(<<StateAndRef of the contract state>>, DummyContractV2::class.java)
rpcA.startFlow(ContractUpgradeFlow.Authorise(<<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.
@ -115,7 +119,7 @@ The upgraded transaction state will be recorded in every participant's node at t
<<StateAndRef of the contract state>>,
DummyContractV2::class.java)
.. note:: See ``ContractUpgradeFlowTest.2 parties contract upgrade using RPC`` for more detailed code example.
.. note:: See ``ContractUpgradeFlowTest`` for more detailed code examples.

View File

@ -9,6 +9,7 @@ import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
import net.corda.core.concurrent.CordaFuture
import net.corda.core.crypto.*
import net.corda.core.flows.*
import net.corda.core.flows.ContractUpgradeFlow.Acceptor
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.*
@ -18,14 +19,16 @@ import net.corda.core.internal.concurrent.openFuture
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.RPCOps
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.*
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.node.NodeInfo
import net.corda.core.node.PluginServiceHub
import net.corda.core.node.ServiceEntry
import net.corda.core.node.services.*
import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.*
import net.corda.node.services.ContractUpgradeHandler
import net.corda.node.services.NotaryChangeHandler
import net.corda.node.services.NotifyTransactionHandler
import net.corda.node.services.TransactionKeyHandler
@ -38,11 +41,11 @@ import net.corda.node.services.identity.PersistentIdentityService
import net.corda.node.services.keys.PersistentKeyManagementService
import net.corda.node.services.messaging.MessagingService
import net.corda.node.services.messaging.sendRequest
import net.corda.node.services.network.PersistentNetworkMapCache
import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.network.NetworkMapService.RegistrationRequest
import net.corda.node.services.network.NetworkMapService.RegistrationResponse
import net.corda.node.services.network.NodeRegistration
import net.corda.node.services.network.PersistentNetworkMapCache
import net.corda.node.services.network.PersistentNetworkMapService
import net.corda.node.services.persistence.DBCheckpointStorage
import net.corda.node.services.persistence.DBTransactionMappingStorage
@ -378,7 +381,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
.filter { it.isUserInvokable() } +
// Add any core flows here
listOf(
ContractUpgradeFlow::class.java)
ContractUpgradeFlow.Initiator::class.java)
}
/**
@ -399,7 +402,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
private fun installCoreFlows() {
installCoreFlow(BroadcastTransactionFlow::class, ::NotifyTransactionHandler)
installCoreFlow(NotaryChangeFlow::class, ::NotaryChangeHandler)
installCoreFlow(ContractUpgradeFlow::class, ::ContractUpgradeHandler)
installCoreFlow(ContractUpgradeFlow.Initiator::class, ::Acceptor)
installCoreFlow(TransactionKeyFlow::class, ::TransactionKeyHandler)
}

View File

@ -171,8 +171,6 @@ class CordaRPCOpsImpl(
}
}
override fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class<out UpgradedContract<*, *>>) = services.contractUpgradeService.authoriseContractUpgrade(state, upgradedContractClass)
override fun deauthoriseContractUpgrade(state: StateAndRef<*>) = services.contractUpgradeService.deauthoriseContractUpgrade(state)
override fun currentNodeTime(): Instant = Instant.now(services.clock)
override fun waitUntilNetworkReady(): CordaFuture<Void?> {

View File

@ -49,31 +49,6 @@ class NotaryChangeHandler(otherSide: Party) : AbstractStateReplacementFlow.Accep
}
}
class ContractUpgradeHandler(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Class<out UpgradedContract<ContractState, *>>>(otherSide) {
@Suspendable
@Throws(StateReplacementException::class)
override fun verifyProposal(stx: SignedTransaction, proposal: AbstractStateReplacementFlow.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 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 authorisedUpgrade = serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?:
throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}")
val proposedTx = stx.tx
val expectedTx = ContractUpgradeFlow.assembleBareTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt).toWireTransaction()
requireThat {
"The instigator is one of the participants" using (otherSide in oldStateAndRef.state.data.participants)
"The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" using (proposal.modification == authorisedUpgrade)
"The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx)
}
ContractUpgradeFlow.verify(
oldStateAndRef.state.data,
expectedTx.outRef<ContractState>(0).state.data,
expectedTx.toLedgerTransaction(serviceHub).commandsOfType<UpgradeCommand>().single())
}
}
class TransactionKeyHandler(val otherSide: Party, val revocationEnabled: Boolean) : FlowLogic<Unit>() {
constructor(otherSide: Party) : this(otherSide, false)
companion object {

View File

@ -22,6 +22,7 @@ import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.node.services.transactions.BFTNonValidatingNotaryService
import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.services.transactions.RaftUniquenessProvider
import net.corda.node.services.upgrade.ContractUpgradeServiceImpl
import net.corda.node.services.vault.VaultSchemaV1
/**
@ -54,7 +55,8 @@ class NodeSchemaService(customSchemas: Set<MappedSchema> = emptySet()) : SchemaS
RaftUniquenessProvider.RaftState::class.java,
BFTNonValidatingNotaryService.PersistedCommittedState::class.java,
PersistentIdentityService.PersistentIdentity::class.java,
PersistentIdentityService.PersistentIdentityNames::class.java
PersistentIdentityService.PersistentIdentityNames::class.java,
ContractUpgradeServiceImpl.DBContractUpgrade::class.java
))
// Required schemas are those used by internal Corda services

View File

@ -1,26 +1,51 @@
package net.corda.node.services.upgrade
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.UpgradedContract
import net.corda.core.node.services.ContractUpgradeService
import net.corda.node.utilities.NODE_DATABASE_PREFIX
import net.corda.node.utilities.PersistentMap
import javax.persistence.*
class ContractUpgradeServiceImpl : ContractUpgradeService {
// TODO : Persist this in DB.
private val authorisedUpgrade = mutableMapOf<StateRef, Class<out UpgradedContract<*, *>>>()
@Entity
@Table(name = "${NODE_DATABASE_PREFIX}contract_upgrades")
class DBContractUpgrade(
@Id
@Column(name = "state_ref", length = 96)
var stateRef: String = "",
override fun getAuthorisedContractUpgrade(ref: StateRef) = authorisedUpgrade[ref]
/** refers to the UpgradedContract class name*/
@Column(name = "contract_class_name")
var upgradedContractClassName: String = ""
)
override fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class<out UpgradedContract<*, *>>) {
val upgrade = upgradedContractClass.newInstance()
if (upgrade.legacyContract != stateAndRef.state.data.contract.javaClass) {
throw IllegalArgumentException("The contract state cannot be upgraded using provided UpgradedContract.")
private companion object {
fun createContractUpgradesMap(): PersistentMap<String, String, DBContractUpgrade, String> {
return PersistentMap(
toPersistentEntityKey = { it },
fromPersistentEntity = { Pair(it.stateRef, it.upgradedContractClassName) },
toPersistentEntity = { key: String, value: String ->
DBContractUpgrade().apply {
stateRef = key
upgradedContractClassName = value
}
},
persistentEntityClass = DBContractUpgrade::class.java
)
}
authorisedUpgrade.put(stateAndRef.ref, upgradedContractClass)
}
override fun deauthoriseContractUpgrade(stateAndRef: StateAndRef<*>) {
authorisedUpgrade.remove(stateAndRef.ref)
private val authorisedUpgrade = createContractUpgradesMap()
override fun getAuthorisedContractUpgrade(ref: StateRef) = authorisedUpgrade[ref.toString()]
override fun storeAuthorisedContractUpgrade(ref: StateRef, upgradedContractClass: Class<out UpgradedContract<*, *>>) {
authorisedUpgrade.put(ref.toString(), upgradedContractClass.name)
}
override fun removeAuthorisedContractUpgrade(ref: StateRef) {
authorisedUpgrade.remove(ref.toString())
}
}

View File

@ -32,7 +32,7 @@ class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.St
}
override fun verify(tx: LedgerTransaction) {
if (tx.commands.any { it.value is UpgradeCommand }) ContractUpgradeFlow.verify(tx)
if (tx.commands.any { it.value is UpgradeCommand }) ContractUpgradeFlow.Acceptor.verify(tx)
// Other verifications.
}
// DOCEND 1

View File

@ -152,7 +152,7 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub {
override val keyManagementService: KeyManagementService by lazy { MockKeyManagementService(identityService, *keys) }
override val vaultService: VaultService get() = throw UnsupportedOperationException()
override val contractUpgradeService: ContractUpgradeService = ContractUpgradeServiceImpl()
override val contractUpgradeService: ContractUpgradeService get() = throw UnsupportedOperationException()
override val vaultQueryService: VaultQueryService get() = throw UnsupportedOperationException()
override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException()
override val clock: Clock get() = Clock.systemUTC()