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

View File

@ -1,72 +1,146 @@
package net.corda.core.flows package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.identity.Party
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import java.security.PublicKey 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 * 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. * 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 * 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. * use the new updated state for future transactions.
*/ */
@InitiatingFlow object ContractUpgradeFlow {
@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) {
companion object { /**
@JvmStatic * Authorise a contract state upgrade.
fun verify(tx: LedgerTransaction) { * This will store the upgrade authorisation in persistent store, and will be queried by [ContractUpgradeFlow.Acceptor] during contract upgrade process.
// Contract Upgrade transaction should have 1 input, 1 output and 1 command. * Invoking this flow indicates the node is willing to upgrade the [StateAndRef] using the [UpgradedContract] class.
verify( * This method will NOT initiate the upgrade process. To start the upgrade process, see [Initiator].
tx.inputStates.single(), */
tx.outputStates.single(), @StartableByRPC
tx.commandsOfType<UpgradeCommand>().single()) 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() * Deauthorise a contract state upgrade.
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet() * This will remove the upgrade authorisation from persistent store (and prevent any further upgrade)
@Suppress("UNCHECKED_CAST") */
val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract<ContractState, *> @StartableByRPC
requireThat { class Deauthorise(
"The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys) val stateRef: StateRef
"Inputs state reference the legacy contract" using (input.contract.javaClass == upgradedContract.legacyContract) ) : FlowLogic< Void?>() {
"Outputs state reference the upgraded contract" using (output.contract.javaClass == command.upgradedContractClass) @Suspendable
"Output state must be an upgraded version of the input state" using (output == upgradedContract.upgrade(input)) 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( @Suspendable
stateRef: StateAndRef<OldState>, override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>, val baseTx = assembleBareTx(originalState, modification, PrivacySalt())
privacySalt: PrivacySalt val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet()
): TransactionBuilder { // TODO: We need a much faster way of finding our key in the transaction
val contractUpgrade = upgradedContractClass.newInstance() val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
return TransactionBuilder(stateRef.state.notary) val stx = serviceHub.signInitialTransaction(baseTx, myKey)
.withItems( return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
stateRef,
contractUpgrade.upgrade(stateRef.state.data),
Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants.map { it.owningKey }),
privacySalt
)
} }
} }
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx { @StartableByRPC
val baseTx = assembleBareTx(originalState, modification, PrivacySalt()) @InitiatedBy(ContractUpgradeFlow.Initiator::class)
val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet() class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Class<out UpgradedContract<ContractState, *>>>(otherSide) {
// TODO: We need a much faster way of finding our key in the transaction
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single() companion object {
val stx = serviceHub.signInitialTransaction(baseTx, myKey) @JvmStatic
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey) 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, * Generic vault query function which takes a [QueryCriteria] object to define filters,
* optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted),
* and returns a [Vault.PageAndUpdates] object containing * and returns a [DataFeed] object containing
* 1) a snapshot as a [Vault.Page] (described previously in [queryBy]) * 1) a snapshot as a [Vault.Page] (described previously in [CordaRPCOps.vaultQueryBy])
* 2) an [Observable] of [Vault.Update] * 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). * the [QueryCriteria] applies to both snapshot and deltas (streaming updates).
*/ */
// DOCSTART VaultTrackByAPI // DOCSTART VaultTrackByAPI
@ -239,20 +239,6 @@ interface CordaRPCOps : RPCOps {
*/ */
fun uploadAttachment(jar: InputStream): SecureHash 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. * Returns the node's current time.
*/ */
@ -338,7 +324,7 @@ inline fun <T, reified R : FlowLogic<T>> CordaRPCOps.startFlow(
flowConstructor: () -> R flowConstructor: () -> R
): FlowHandle<T> = startFlowDynamic(R::class.java) ): 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") @Suppress("UNUSED_PARAMETER")
flowConstructor: (A) -> R, flowConstructor: (A) -> R,
arg0: A arg0: A

View File

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

View File

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

View File

@ -6,6 +6,9 @@ from the previous milestone release.
UNRELEASED 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 * 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. 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 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. 3. Time passes.
4. The developer of contract X discovers a bug in the contract code, and releases a new version, contract Y. 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.
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.
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. 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. 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. 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. 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 .. container:: codeset
.. sourcecode:: kotlin .. 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 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. 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 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 ``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. The transaction will be notarised and persisted once every participant verified and signed the upgrade proposal.
Examples 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``. 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`` 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. 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 rpcClient : CordaRPCClient = << Bank A's Corda RPC Client >>
val rpcA = rpcClient.proxy() 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. 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. 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>>, <<StateAndRef of the contract state>>,
DummyContractV2::class.java) 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.concurrent.CordaFuture
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.core.flows.ContractUpgradeFlow.Acceptor
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.* 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.CordaRPCOps
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.messaging.SingleMessageRecipient 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.*
import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.* import net.corda.core.utilities.*
import net.corda.node.services.ContractUpgradeHandler
import net.corda.node.services.NotaryChangeHandler import net.corda.node.services.NotaryChangeHandler
import net.corda.node.services.NotifyTransactionHandler import net.corda.node.services.NotifyTransactionHandler
import net.corda.node.services.TransactionKeyHandler 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.keys.PersistentKeyManagementService
import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.MessagingService
import net.corda.node.services.messaging.sendRequest 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
import net.corda.node.services.network.NetworkMapService.RegistrationRequest import net.corda.node.services.network.NetworkMapService.RegistrationRequest
import net.corda.node.services.network.NetworkMapService.RegistrationResponse import net.corda.node.services.network.NetworkMapService.RegistrationResponse
import net.corda.node.services.network.NodeRegistration 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.network.PersistentNetworkMapService
import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.persistence.DBCheckpointStorage
import net.corda.node.services.persistence.DBTransactionMappingStorage import net.corda.node.services.persistence.DBTransactionMappingStorage
@ -378,7 +381,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
.filter { it.isUserInvokable() } + .filter { it.isUserInvokable() } +
// Add any core flows here // Add any core flows here
listOf( listOf(
ContractUpgradeFlow::class.java) ContractUpgradeFlow.Initiator::class.java)
} }
/** /**
@ -399,7 +402,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
private fun installCoreFlows() { private fun installCoreFlows() {
installCoreFlow(BroadcastTransactionFlow::class, ::NotifyTransactionHandler) installCoreFlow(BroadcastTransactionFlow::class, ::NotifyTransactionHandler)
installCoreFlow(NotaryChangeFlow::class, ::NotaryChangeHandler) installCoreFlow(NotaryChangeFlow::class, ::NotaryChangeHandler)
installCoreFlow(ContractUpgradeFlow::class, ::ContractUpgradeHandler) installCoreFlow(ContractUpgradeFlow.Initiator::class, ::Acceptor)
installCoreFlow(TransactionKeyFlow::class, ::TransactionKeyHandler) 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 currentNodeTime(): Instant = Instant.now(services.clock)
override fun waitUntilNetworkReady(): CordaFuture<Void?> { 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>() { class TransactionKeyHandler(val otherSide: Party, val revocationEnabled: Boolean) : FlowLogic<Unit>() {
constructor(otherSide: Party) : this(otherSide, false) constructor(otherSide: Party) : this(otherSide, false)
companion object { 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.BFTNonValidatingNotaryService
import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.node.services.transactions.RaftUniquenessProvider import net.corda.node.services.transactions.RaftUniquenessProvider
import net.corda.node.services.upgrade.ContractUpgradeServiceImpl
import net.corda.node.services.vault.VaultSchemaV1 import net.corda.node.services.vault.VaultSchemaV1
/** /**
@ -54,7 +55,8 @@ class NodeSchemaService(customSchemas: Set<MappedSchema> = emptySet()) : SchemaS
RaftUniquenessProvider.RaftState::class.java, RaftUniquenessProvider.RaftState::class.java,
BFTNonValidatingNotaryService.PersistedCommittedState::class.java, BFTNonValidatingNotaryService.PersistedCommittedState::class.java,
PersistentIdentityService.PersistentIdentity::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 // Required schemas are those used by internal Corda services

View File

@ -1,26 +1,51 @@
package net.corda.node.services.upgrade package net.corda.node.services.upgrade
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.contracts.UpgradedContract import net.corda.core.contracts.UpgradedContract
import net.corda.core.node.services.ContractUpgradeService 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 { class ContractUpgradeServiceImpl : ContractUpgradeService {
// TODO : Persist this in DB. @Entity
private val authorisedUpgrade = mutableMapOf<StateRef, Class<out UpgradedContract<*, *>>>() @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<*, *>>) { private companion object {
val upgrade = upgradedContractClass.newInstance() fun createContractUpgradesMap(): PersistentMap<String, String, DBContractUpgrade, String> {
if (upgrade.legacyContract != stateAndRef.state.data.contract.javaClass) { return PersistentMap(
throw IllegalArgumentException("The contract state cannot be upgraded using provided UpgradedContract.") 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<*>) { private val authorisedUpgrade = createContractUpgradesMap()
authorisedUpgrade.remove(stateAndRef.ref)
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) { 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. // Other verifications.
} }
// DOCEND 1 // 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 keyManagementService: KeyManagementService by lazy { MockKeyManagementService(identityService, *keys) }
override val vaultService: VaultService get() = throw UnsupportedOperationException() 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 vaultQueryService: VaultQueryService get() = throw UnsupportedOperationException()
override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException() override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException()
override val clock: Clock get() = Clock.systemUTC() override val clock: Clock get() = Clock.systemUTC()