mirror of
https://github.com/corda/corda.git
synced 2024-12-30 17:57:02 +00:00
Cleaned up ContractUpgradeFlow API (#1591)
This commit is contained in:
parent
a11577840c
commit
11be5dd417
@ -2,9 +2,8 @@ package net.corda.core.flows
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
|
import net.corda.core.internal.ContractUpgradeUtils
|
||||||
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 java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,11 +16,37 @@ import java.security.PublicKey
|
|||||||
*/
|
*/
|
||||||
object ContractUpgradeFlow {
|
object ContractUpgradeFlow {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun verify(tx: LedgerTransaction) {
|
||||||
|
// Contract Upgrade transaction should have 1 input, 1 output and 1 command.
|
||||||
|
verify(tx.inputs.single().state,
|
||||||
|
tx.outputs.single(),
|
||||||
|
tx.commandsOfType<UpgradeCommand>().single())
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun verify(input: TransactionState<ContractState>, output: TransactionState<ContractState>, commandData: Command<UpgradeCommand>) {
|
||||||
|
val command = commandData.value
|
||||||
|
val participantKeys: Set<PublicKey> = input.data.participants.map { it.owningKey }.toSet()
|
||||||
|
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet()
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val upgradedContract = javaClass.classLoader.loadClass(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 == upgradedContract.legacyContract)
|
||||||
|
"Outputs state reference the upgraded contract" using (output.contract == command.upgradedContractClass)
|
||||||
|
"Output state must be an upgraded version of the input state" using (output.data == upgradedContract.upgrade(input.data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authorise a contract state upgrade.
|
* 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 will store the upgrade authorisation in persistent store, and will be queried by [ContractUpgradeFlow.Acceptor]
|
||||||
* This method will NOT initiate the upgrade process. To start the upgrade process, see [Initiator].
|
* during contract upgrade process. Invoking this flow indicates the node is willing to upgrade the [StateAndRef] using
|
||||||
|
* the [UpgradedContract] class.
|
||||||
|
*
|
||||||
|
* This flow will NOT initiate the upgrade process. To start the upgrade process, see [Initiate].
|
||||||
*/
|
*/
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class Authorise(
|
class Authorise(
|
||||||
@ -45,9 +70,7 @@ object ContractUpgradeFlow {
|
|||||||
* This will remove the upgrade authorisation from persistent store (and prevent any further upgrade)
|
* This will remove the upgrade authorisation from persistent store (and prevent any further upgrade)
|
||||||
*/
|
*/
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class Deauthorise(
|
class Deauthorise(val stateRef: StateRef) : FlowLogic<Void?>() {
|
||||||
val stateRef: StateRef
|
|
||||||
) : FlowLogic< Void?>() {
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): Void? {
|
override fun call(): Void? {
|
||||||
serviceHub.contractUpgradeService.removeAuthorisedContractUpgrade(stateRef)
|
serviceHub.contractUpgradeService.removeAuthorisedContractUpgrade(stateRef)
|
||||||
@ -55,33 +78,19 @@ object ContractUpgradeFlow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This flow begins the contract upgrade process.
|
||||||
|
*/
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class Initiator<OldState : ContractState, out NewState : ContractState>(
|
class Initiate<OldState : ContractState, out NewState : ContractState>(
|
||||||
originalState: StateAndRef<OldState>,
|
originalState: StateAndRef<OldState>,
|
||||||
newContractClass: Class<out UpgradedContract<OldState, NewState>>
|
newContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||||
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<out UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
|
) : 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,
|
|
||||||
StateAndContract(contractUpgrade.upgrade(stateRef.state.data), upgradedContractClass.name),
|
|
||||||
Command(UpgradeCommand(upgradedContractClass.name), stateRef.state.data.participants.map { it.owningKey }),
|
|
||||||
privacySalt
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
|
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
|
||||||
val baseTx = assembleBareTx(originalState, modification, PrivacySalt())
|
val baseTx = ContractUpgradeUtils.assembleBareTx(originalState, modification, PrivacySalt())
|
||||||
val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet()
|
val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet()
|
||||||
// TODO: We need a much faster way of finding our key in the transaction
|
// TODO: We need a much faster way of finding our key in the transaction
|
||||||
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
|
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
|
||||||
@ -89,57 +98,4 @@ object ContractUpgradeFlow {
|
|||||||
return AbstractStateReplacementFlow.UpgradeTx(stx)
|
return AbstractStateReplacementFlow.UpgradeTx(stx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@StartableByRPC
|
|
||||||
@InitiatedBy(ContractUpgradeFlow.Initiator::class)
|
|
||||||
class Acceptor(otherSide: FlowSession) : 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.inputs.single().state,
|
|
||||||
tx.outputs.single(),
|
|
||||||
tx.commandsOfType<UpgradeCommand>().single())
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun verify(input: TransactionState<ContractState>, output: TransactionState<ContractState>, commandData: Command<UpgradeCommand>) {
|
|
||||||
val command = commandData.value
|
|
||||||
val participantKeys: Set<PublicKey> = input.data.participants.map { it.owningKey }.toSet()
|
|
||||||
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet()
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val upgradedContract = javaClass.classLoader.loadClass(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 == upgradedContract.legacyContract)
|
|
||||||
"Outputs state reference the upgraded contract" using (output.contract == command.upgradedContractClass)
|
|
||||||
"Output state must be an upgraded version of the input state" using (output.data == upgradedContract.upgrade(input.data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 (initiatingSession.counterparty 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,
|
|
||||||
expectedTx.outRef<ContractState>(0).state,
|
|
||||||
expectedTx.toLedgerTransaction(serviceHub).commandsOfType<UpgradeCommand>().single())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package net.corda.core.internal
|
||||||
|
|
||||||
|
import net.corda.core.contracts.*
|
||||||
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
|
||||||
|
object ContractUpgradeUtils {
|
||||||
|
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,
|
||||||
|
StateAndContract(contractUpgrade.upgrade(stateRef.state.data), upgradedContractClass.name),
|
||||||
|
Command(UpgradeCommand(upgradedContractClass.name), stateRef.state.data.participants.map { it.owningKey }),
|
||||||
|
privacySalt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -81,7 +81,7 @@ 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.Initiator(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
val rejectedFuture = a.services.startFlow(ContractUpgradeFlow.Initiate(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
assertFailsWith(UnexpectedFlowEndException::class) { rejectedFuture.getOrThrow() }
|
assertFailsWith(UnexpectedFlowEndException::class) { rejectedFuture.getOrThrow() }
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ class ContractUpgradeFlowTest {
|
|||||||
b.services.startFlow(ContractUpgradeFlow.Deauthorise(btx.tx.outRef<ContractState>(0).ref)).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.
|
// 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
|
val deauthorisedFuture = a.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
assertFailsWith(UnexpectedFlowEndException::class) { deauthorisedFuture.getOrThrow() }
|
assertFailsWith(UnexpectedFlowEndException::class) { deauthorisedFuture.getOrThrow() }
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ class ContractUpgradeFlowTest {
|
|||||||
b.services.startFlow(ContractUpgradeFlow.Authorise(btx.tx.outRef<ContractState>(0), DummyContractV2::class.java)).resultFuture.getOrThrow()
|
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.Initiator(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
val resultFuture = a.services.startFlow(ContractUpgradeFlow.Initiate(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
|
|
||||||
val result = resultFuture.getOrThrow()
|
val result = resultFuture.getOrThrow()
|
||||||
@ -144,8 +144,7 @@ class ContractUpgradeFlowTest {
|
|||||||
|
|
||||||
val user = rpcTestUser.copy(permissions = setOf(
|
val user = rpcTestUser.copy(permissions = setOf(
|
||||||
startFlowPermission<FinalityInvoker>(),
|
startFlowPermission<FinalityInvoker>(),
|
||||||
startFlowPermission<ContractUpgradeFlow.Initiator<*, *>>(),
|
startFlowPermission<ContractUpgradeFlow.Initiate<*, *>>(),
|
||||||
startFlowPermission<ContractUpgradeFlow.Acceptor>(),
|
|
||||||
startFlowPermission<ContractUpgradeFlow.Authorise>(),
|
startFlowPermission<ContractUpgradeFlow.Authorise>(),
|
||||||
startFlowPermission<ContractUpgradeFlow.Deauthorise>()
|
startFlowPermission<ContractUpgradeFlow.Deauthorise>()
|
||||||
))
|
))
|
||||||
@ -160,7 +159,7 @@ class ContractUpgradeFlowTest {
|
|||||||
requireNotNull(atx)
|
requireNotNull(atx)
|
||||||
requireNotNull(btx)
|
requireNotNull(btx)
|
||||||
|
|
||||||
val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiator(stateAndRef, upgrade) },
|
val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) },
|
||||||
atx!!.tx.outRef<DummyContract.State>(0),
|
atx!!.tx.outRef<DummyContract.State>(0),
|
||||||
DummyContractV2::class.java).returnValue
|
DummyContractV2::class.java).returnValue
|
||||||
|
|
||||||
@ -175,7 +174,7 @@ class ContractUpgradeFlowTest {
|
|||||||
btx.tx.outRef<ContractState>(0).ref).returnValue
|
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.
|
// 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) },
|
val deauthorisedFuture = rpcA.startFlow( {stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) },
|
||||||
atx.tx.outRef<DummyContract.State>(0),
|
atx.tx.outRef<DummyContract.State>(0),
|
||||||
DummyContractV2::class.java).returnValue
|
DummyContractV2::class.java).returnValue
|
||||||
|
|
||||||
@ -188,7 +187,7 @@ class ContractUpgradeFlowTest {
|
|||||||
DummyContractV2::class.java).returnValue
|
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.Initiator(stateAndRef, upgrade) },
|
val resultFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Initiate(stateAndRef, upgrade) },
|
||||||
atx.tx.outRef<DummyContract.State>(0),
|
atx.tx.outRef<DummyContract.State>(0),
|
||||||
DummyContractV2::class.java).returnValue
|
DummyContractV2::class.java).returnValue
|
||||||
|
|
||||||
@ -222,7 +221,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.Initiator(stateAndRef, CashV2::class.java)).resultFuture
|
val upgradeResult = a.services.startFlow(ContractUpgradeFlow.Initiate(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.
|
||||||
|
@ -207,6 +207,8 @@ UNRELEASED
|
|||||||
* A new ``ComponentGroupEnum`` is added with the following enum items: ``INPUTS_GROUP``, ``OUTPUTS_GROUP``,
|
* A new ``ComponentGroupEnum`` is added with the following enum items: ``INPUTS_GROUP``, ``OUTPUTS_GROUP``,
|
||||||
``COMMANDS_GROUP``, ``ATTACHMENTS_GROUP``, ``NOTARY_GROUP``, ``TIMEWINDOW_GROUP``.
|
``COMMANDS_GROUP``, ``ATTACHMENTS_GROUP``, ``NOTARY_GROUP``, ``TIMEWINDOW_GROUP``.
|
||||||
|
|
||||||
|
* ``ContractUpgradeFlow.Initiator`` has been renamed to ``ContractUpgradeFlow.Initiate``
|
||||||
|
|
||||||
Milestone 14
|
Milestone 14
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import net.corda.confidential.SwapIdentitiesFlow
|
|||||||
import net.corda.confidential.SwapIdentitiesHandler
|
import net.corda.confidential.SwapIdentitiesHandler
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.ContractUpgradeFlow.Acceptor
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.identity.PartyAndCertificate
|
import net.corda.core.identity.PartyAndCertificate
|
||||||
@ -35,6 +34,7 @@ import net.corda.core.utilities.debug
|
|||||||
import net.corda.node.internal.classloading.requireAnnotation
|
import net.corda.node.internal.classloading.requireAnnotation
|
||||||
import net.corda.node.internal.cordapp.CordappLoader
|
import net.corda.node.internal.cordapp.CordappLoader
|
||||||
import net.corda.node.internal.cordapp.CordappProvider
|
import net.corda.node.internal.cordapp.CordappProvider
|
||||||
|
import net.corda.node.services.ContractUpgradeHandler
|
||||||
import net.corda.node.services.FinalityHandler
|
import net.corda.node.services.FinalityHandler
|
||||||
import net.corda.node.services.NotaryChangeHandler
|
import net.corda.node.services.NotaryChangeHandler
|
||||||
import net.corda.node.services.api.*
|
import net.corda.node.services.api.*
|
||||||
@ -372,7 +372,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
private fun installCoreFlows() {
|
private fun installCoreFlows() {
|
||||||
installCoreFlow(FinalityFlow::class, ::FinalityHandler)
|
installCoreFlow(FinalityFlow::class, ::FinalityHandler)
|
||||||
installCoreFlow(NotaryChangeFlow::class, ::NotaryChangeHandler)
|
installCoreFlow(NotaryChangeFlow::class, ::NotaryChangeHandler)
|
||||||
installCoreFlow(ContractUpgradeFlow.Initiator::class, ::Acceptor)
|
installCoreFlow(ContractUpgradeFlow.Initiate::class, ::ContractUpgradeHandler)
|
||||||
installCoreFlow(SwapIdentitiesFlow::class, ::SwapIdentitiesHandler)
|
installCoreFlow(SwapIdentitiesFlow::class, ::SwapIdentitiesHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,8 +54,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List<URL>)
|
|||||||
/**
|
/**
|
||||||
* Creates a dev mode CordappLoader intended to only be used in test environments.
|
* Creates a dev mode CordappLoader intended to only be used in test environments.
|
||||||
*
|
*
|
||||||
* @param scanPackage Resolves the JARs that contain scanPackage and use them as the source for
|
* @param scanPackages list of packages to scan.
|
||||||
* the classpath scanning.
|
|
||||||
*/
|
*/
|
||||||
fun createDevMode(scanPackages: String): CordappLoader {
|
fun createDevMode(scanPackages: String): CordappLoader {
|
||||||
val paths = scanPackages.split(",").flatMap { scanPackage ->
|
val paths = scanPackages.split(",").flatMap { scanPackage ->
|
||||||
@ -123,7 +122,11 @@ class CordappLoader private constructor(private val cordappJarPaths: List<URL>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
val found = scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() }
|
val found = scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() }
|
||||||
val coreFlows = listOf(ContractUpgradeFlow.Initiator::class.java)
|
val coreFlows = listOf(
|
||||||
|
ContractUpgradeFlow.Initiate::class.java,
|
||||||
|
ContractUpgradeFlow.Authorise::class.java,
|
||||||
|
ContractUpgradeFlow.Deauthorise::class.java
|
||||||
|
)
|
||||||
return found + coreFlows
|
return found + coreFlows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
package net.corda.node.services
|
package net.corda.node.services
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.contracts.ContractState
|
||||||
|
import net.corda.core.contracts.UpgradeCommand
|
||||||
|
import net.corda.core.contracts.UpgradedContract
|
||||||
|
import net.corda.core.contracts.requireThat
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.ContractUpgradeUtils
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
|
||||||
// TODO: We should have a whitelist of contracts we're willing to accept at all, and reject if the transaction
|
// TODO: We should have a whitelist of contracts we're willing to accept at all, and reject if the transaction
|
||||||
@ -41,3 +46,27 @@ class NotaryChangeHandler(otherSideSession: FlowSession) : AbstractStateReplacem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ContractUpgradeHandler(otherSide: FlowSession) : AbstractStateReplacementFlow.Acceptor<Class<out UpgradedContract<ContractState, *>>>(otherSide) {
|
||||||
|
@Suspendable
|
||||||
|
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 = ContractUpgradeUtils.assembleBareTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt).toWireTransaction()
|
||||||
|
requireThat {
|
||||||
|
"The instigator is one of the participants" using (initiatingSession.counterparty 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.verify(
|
||||||
|
oldStateAndRef.state,
|
||||||
|
expectedTx.outRef<ContractState>(0).state,
|
||||||
|
expectedTx.toLedgerTransaction(serviceHub).commandsOfType<UpgradeCommand>().single())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -46,7 +46,7 @@ class CordappLoaderTest {
|
|||||||
val actualCordapp = actual.first()
|
val actualCordapp = actual.first()
|
||||||
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf("net.corda.finance.contracts.isolated.AnotherDummyContract"))
|
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf("net.corda.finance.contracts.isolated.AnotherDummyContract"))
|
||||||
assertThat(actualCordapp.initiatedFlows).isEmpty()
|
assertThat(actualCordapp.initiatedFlows).isEmpty()
|
||||||
assertThat(actualCordapp.rpcFlows).isEqualTo(listOf(loader.appClassLoader.loadClass("net.corda.core.flows.ContractUpgradeFlow\$Initiator").asSubclass(FlowLogic::class.java)))
|
assertThat(actualCordapp.rpcFlows).contains(loader.appClassLoader.loadClass("net.corda.core.flows.ContractUpgradeFlow\$Initiate").asSubclass(FlowLogic::class.java))
|
||||||
assertThat(actualCordapp.services).isEmpty()
|
assertThat(actualCordapp.services).isEmpty()
|
||||||
assertThat(actualCordapp.plugins).hasSize(1)
|
assertThat(actualCordapp.plugins).hasSize(1)
|
||||||
assertThat(actualCordapp.plugins.first().javaClass.name).isEqualTo("net.corda.finance.contracts.isolated.DummyPlugin")
|
assertThat(actualCordapp.plugins.first().javaClass.name).isEqualTo("net.corda.finance.contracts.isolated.DummyPlugin")
|
||||||
|
@ -15,7 +15,7 @@ const val DUMMY_V2_PROGRAM_ID: ContractClassName = "net.corda.testing.contracts.
|
|||||||
*/
|
*/
|
||||||
// DOCSTART 1
|
// DOCSTART 1
|
||||||
class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> {
|
class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> {
|
||||||
override val legacyContract = DummyContract::class.java.name
|
override val legacyContract: String = DummyContract::class.java.name
|
||||||
|
|
||||||
data class State(val magicNumber: Int = 0, val owners: List<AbstractParty>) : ContractState {
|
data class State(val magicNumber: Int = 0, val owners: List<AbstractParty>) : ContractState {
|
||||||
override val participants: List<AbstractParty> = owners
|
override val participants: List<AbstractParty> = owners
|
||||||
@ -31,7 +31,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.Acceptor.verify(tx)
|
if (tx.commands.any { it.value is UpgradeCommand }) ContractUpgradeFlow.verify(tx)
|
||||||
// Other verifications.
|
// Other verifications.
|
||||||
}
|
}
|
||||||
// DOCEND 1
|
// DOCEND 1
|
||||||
|
Loading…
Reference in New Issue
Block a user