mirror of
https://github.com/corda/corda.git
synced 2025-04-07 11:27:01 +00:00
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:
parent
377ca95387
commit
ebc9cacb53
@ -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")
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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?> {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user