mirror of
https://github.com/corda/corda.git
synced 2025-06-17 14:48:16 +00:00
Add support for contract upgrades (#165)
* Add support for contract upgrades * Add interface for the upgraded contract to implement, which provides functionality for upgrading legacy states. * Add shared upgrade command and verification code for it. * Add DummyContractV2 to illustrate what an upgraded contract looks like. * Add new functions to vault service to support upgrading state objects. * Add contract upgrade flow
This commit is contained in:
@ -10,7 +10,6 @@ import net.corda.core.transactions.TransactionBuilder
|
||||
val DUMMY_PROGRAM_ID = DummyContract()
|
||||
|
||||
data class DummyContract(override val legalContractReference: SecureHash = SecureHash.sha256("")) : Contract {
|
||||
|
||||
interface State : ContractState {
|
||||
val magicNumber: Int
|
||||
}
|
||||
@ -31,8 +30,7 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur
|
||||
data class MultiOwnerState(override val magicNumber: Int = 0,
|
||||
val owners: List<CompositeKey>) : ContractState, State {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
override val participants: List<CompositeKey>
|
||||
get() = owners
|
||||
override val participants: List<CompositeKey> get() = owners
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
@ -46,14 +44,20 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder {
|
||||
val state = SingleOwnerState(magicNumber, owner.party.owningKey)
|
||||
return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey))
|
||||
fun generateInitial(magicNumber: Int, notary: Party, owner: PartyAndReference, vararg otherOwners: PartyAndReference): TransactionBuilder {
|
||||
val owners = listOf(owner) + otherOwners
|
||||
return if (owners.size == 1) {
|
||||
val state = SingleOwnerState(magicNumber, owners.first().party.owningKey)
|
||||
TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owners.first().party.owningKey))
|
||||
} else {
|
||||
val state = MultiOwnerState(magicNumber, owners.map { it.party.owningKey })
|
||||
TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owners.map { it.party.owningKey }))
|
||||
}
|
||||
}
|
||||
|
||||
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: CompositeKey) = move(listOf(prior), newOwner)
|
||||
fun move(priors: List<StateAndRef<DummyContract.SingleOwnerState>>, newOwner: CompositeKey): TransactionBuilder {
|
||||
require(priors.size > 0)
|
||||
require(priors.isNotEmpty())
|
||||
val priorState = priors[0].state.data
|
||||
val (cmd, state) = priorState.withNewOwner(newOwner)
|
||||
return TransactionType.General.Builder(notary = priors[0].state.notary).withItems(
|
||||
|
@ -0,0 +1,59 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.flows.ContractUpgradeFlow
|
||||
|
||||
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
||||
val DUMMY_V2_PROGRAM_ID = DummyContractV2()
|
||||
|
||||
/**
|
||||
* Dummy contract state for testing of the upgrade process.
|
||||
*/
|
||||
class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> {
|
||||
override val legacyContract = DUMMY_PROGRAM_ID
|
||||
|
||||
data class State(val magicNumber: Int = 0, val owners: List<CompositeKey>) : ContractState {
|
||||
override val contract = DUMMY_V2_PROGRAM_ID
|
||||
override val participants: List<CompositeKey> = owners
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Create : TypeOnlyCommandData(), Commands
|
||||
class Move : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
override fun upgrade(state: DummyContract.State): DummyContractV2.State {
|
||||
return DummyContractV2.State(state.magicNumber, state.participants)
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
if (tx.commands.any { it.value is UpgradeCommand }) ContractUpgradeFlow.verify(tx)
|
||||
// Other verifications.
|
||||
}
|
||||
|
||||
// The "empty contract"
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("")
|
||||
|
||||
/**
|
||||
* Generate an upgrade transaction from [DummyContract].
|
||||
*
|
||||
* Note: This is a convenience helper method used for testing only.
|
||||
*
|
||||
* @return a pair of wire transaction, and a set of those who should sign the transaction for it to be valid.
|
||||
*/
|
||||
fun generateUpgradeFromV1(vararg states: StateAndRef<DummyContract.State>): Pair<WireTransaction, Set<CompositeKey>> {
|
||||
val notary = states.map { it.state.notary }.single()
|
||||
require(states.isNotEmpty())
|
||||
|
||||
val signees = states.flatMap { it.state.data.participants }.toSet()
|
||||
return Pair(TransactionType.General.Builder(notary).apply {
|
||||
states.forEach {
|
||||
addInputState(it)
|
||||
addOutputState(upgrade(it.state.data))
|
||||
addCommand(UpgradeCommand(DUMMY_V2_PROGRAM_ID.javaClass), signees.toList())
|
||||
}
|
||||
}.toWireTransaction(), signees)
|
||||
}
|
||||
}
|
@ -381,7 +381,7 @@ interface IssueCommand : CommandData {
|
||||
val nonce: Long
|
||||
}
|
||||
|
||||
/** A common move command for contracts which can change owner. */
|
||||
/** A common move command for contract states which can change owner. */
|
||||
interface MoveCommand : CommandData {
|
||||
/**
|
||||
* Contract code the moved state(s) are for the attention of, for example to indicate that the states are moved in
|
||||
@ -397,6 +397,9 @@ interface NetCommand : CommandData {
|
||||
val type: NetType
|
||||
}
|
||||
|
||||
/** Indicates that this transaction replaces the inputs contract state to another contract state */
|
||||
data class UpgradeCommand(val upgradedContractClass: Class<UpgradedContract<*, *>>) : CommandData
|
||||
|
||||
/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
|
||||
data class AuthenticatedObject<out T : Any>(
|
||||
val signers: List<CompositeKey>,
|
||||
@ -445,6 +448,24 @@ interface Contract {
|
||||
val legalContractReference: SecureHash
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface which can upgrade state objects issued by a contract to a new state object issued by a different contract.
|
||||
*
|
||||
* @param OldState the old contract state (can be [ContractState] or other common supertype if this supports upgrading
|
||||
* more than one state).
|
||||
* @param NewState the upgraded contract state.
|
||||
*/
|
||||
interface UpgradedContract<in OldState : ContractState, out NewState : ContractState> : Contract {
|
||||
val legacyContract: Contract
|
||||
/**
|
||||
* Upgrade contract's state object to a new state object.
|
||||
*
|
||||
* @throws IllegalArgumentException if the given state object is not one that can be upgraded. This can be either
|
||||
* that the class is incompatible, or that the data inside the state object cannot be upgraded for some reason.
|
||||
*/
|
||||
fun upgrade(state: OldState): NewState
|
||||
}
|
||||
|
||||
/**
|
||||
* An attachment is a ZIP (or an optionally signed JAR) that contains one or more files. Attachments are meant to
|
||||
* contain public static data which can be referenced from transactions and utilised from contracts. Good examples
|
||||
@ -480,5 +501,3 @@ interface Attachment : NamedByHash {
|
||||
throw FileNotFoundException()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -3,6 +3,7 @@ package net.corda.core.messaging
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.UpgradedContract
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
@ -107,6 +108,20 @@ interface CordaRPCOps : RPCOps {
|
||||
@Deprecated("This service will be removed in a future milestone")
|
||||
fun uploadFile(dataType: String, name: String?, file: InputStream): 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(state: StateAndRef<*>, upgradedContractClass: Class<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.
|
||||
*/
|
||||
|
@ -7,7 +7,6 @@ import net.corda.core.toFuture
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import rx.Observable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
@ -143,7 +142,7 @@ interface VaultService {
|
||||
|
||||
fun statesForRefs(refs: List<StateRef>): Map<StateRef, TransactionState<*>?> {
|
||||
val refsToStates = currentVault.states.associateBy { it.ref }
|
||||
return refs.associateBy({ it }, { refsToStates[it]?.state })
|
||||
return refs.associateBy({ it }) { refsToStates[it]?.state }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -164,6 +163,24 @@ interface VaultService {
|
||||
return updates.filter { it.consumed.any { it.ref == ref } }.toFuture()
|
||||
}
|
||||
|
||||
/** Get contracts we would be willing to upgrade the suggested contract to. */
|
||||
// TODO: We need a better place to put business logic functions
|
||||
fun getAuthorisedContractUpgrade(ref: StateRef): Class<UpgradedContract<*, *>>?
|
||||
|
||||
/**
|
||||
* 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<UpgradedContract<*, *>>)
|
||||
|
||||
/**
|
||||
* Authorise a contract state upgrade.
|
||||
* This will remove the upgrade authorisation from the vault.
|
||||
*/
|
||||
fun deauthoriseContractUpgrade(stateAndRef: StateAndRef<*>)
|
||||
|
||||
/**
|
||||
* Add a note to an existing [LedgerTransaction] given by its unique [SecureHash] id
|
||||
* Multiple notes may be attached to the same [LedgerTransaction].
|
||||
|
@ -16,26 +16,35 @@ import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.flows.AbstractStateReplacementFlow.Acceptor
|
||||
import net.corda.flows.AbstractStateReplacementFlow.Instigator
|
||||
|
||||
/**
|
||||
* Abstract flow to be used for replacing one state with another, for example when changing the notary of a state.
|
||||
* Notably this requires a one to one replacement of states, states cannot be split, merged or issued as part of these
|
||||
* flows.
|
||||
*
|
||||
* The [Instigator] assembles the transaction for state replacement and sends out change proposals to all participants
|
||||
* ([Acceptor]) of that state. If participants agree to the proposed change, they each sign the transaction.
|
||||
* Finally, [Instigator] sends the transaction containing all signatures back to each participant so they can record it and
|
||||
* use the new updated state for future transactions.
|
||||
*/
|
||||
abstract class AbstractStateReplacementFlow {
|
||||
data class Proposal<out T>(val stateRef: StateRef, val modification: T, val stx: SignedTransaction)
|
||||
/**
|
||||
* The [Proposal] contains the details of proposed state modification.
|
||||
* This is the message sent by the [Instigator] to all participants([Acceptor]) during the state replacement process.
|
||||
*
|
||||
* @param M the type of a class representing proposed modification by the instigator.
|
||||
*/
|
||||
data class Proposal<out M>(val stateRef: StateRef, val modification: M, val stx: SignedTransaction)
|
||||
|
||||
abstract class Instigator<out S : ContractState, out T>(
|
||||
/**
|
||||
* The [Instigator] assembles the transaction for state replacement and sends out change proposals to all participants
|
||||
* ([Acceptor]) of that state. If participants agree to the proposed change, they each sign the transaction.
|
||||
* Finally, [Instigator] sends the transaction containing all participants' signatures to the notary for signature, and
|
||||
* then back to each participant so they can record it and use the new updated state for future transactions.
|
||||
*
|
||||
* @param S the input contract state type
|
||||
* @param T the output contract state type, this can be different from [S]. For example, in contract upgrade, the output state type can be different from the input state type after the upgrade process.
|
||||
* @param M the type of a class representing proposed modification by the instigator.
|
||||
*/
|
||||
abstract class Instigator<out S : ContractState, out T : ContractState, out M>(
|
||||
val originalState: StateAndRef<S>,
|
||||
val modification: T,
|
||||
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<StateAndRef<S>>() {
|
||||
val modification: M,
|
||||
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<StateAndRef<T>>() {
|
||||
companion object {
|
||||
object SIGNING : ProgressTracker.Step("Requesting signatures from other parties")
|
||||
object NOTARY : ProgressTracker.Step("Requesting notary signature")
|
||||
@ -45,7 +54,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
|
||||
@Suspendable
|
||||
@Throws(StateReplacementException::class)
|
||||
override fun call(): StateAndRef<S> {
|
||||
override fun call(): StateAndRef<T> {
|
||||
val (stx, participants) = assembleTx()
|
||||
|
||||
progressTracker.currentStep = SIGNING
|
||||
|
83
core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt
Normal file
83
core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt
Normal file
@ -0,0 +1,83 @@
|
||||
package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.flows.AbstractStateReplacementFlow.Proposal
|
||||
import net.corda.flows.ContractUpgradeFlow.Acceptor
|
||||
import net.corda.flows.ContractUpgradeFlow.Instigator
|
||||
|
||||
/**
|
||||
* A flow to be used for upgrading state objects of an old contract to a new contract.
|
||||
*
|
||||
* The [Instigator] assembles the transaction for contract upgrade and sends out change proposals to all participants
|
||||
* ([Acceptor]) of that state. If participants agree to the proposed change, they each sign the transaction.
|
||||
* Finally, [Instigator] sends the transaction containing all signatures back to each participant so they can record it and
|
||||
* use the new updated state for future transactions.
|
||||
*/
|
||||
object ContractUpgradeFlow {
|
||||
@JvmStatic
|
||||
fun verify(tx: TransactionForContract) {
|
||||
// Contract Upgrade transaction should have 1 input, 1 output and 1 command.
|
||||
verify(tx.inputs.single(), tx.outputs.single(), tx.commands.map { Command(it.value, it.signers) }.single())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun verify(input: ContractState, output: ContractState, commandData: Command) {
|
||||
val command = commandData.value as UpgradeCommand
|
||||
val participants: Set<CompositeKey> = input.participants.toSet()
|
||||
val keysThatSigned: Set<CompositeKey> = commandData.signers.toSet()
|
||||
val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract<ContractState, *>
|
||||
requireThat {
|
||||
"The signing keys include all participant keys" by keysThatSigned.containsAll(participants)
|
||||
"Inputs state reference the legacy contract" by (input.contract.javaClass == upgradedContract.legacyContract.javaClass)
|
||||
"Outputs state reference the upgraded contract" by (output.contract.javaClass == command.upgradedContractClass)
|
||||
"Output state must be an upgraded version of the input state" by (output == upgradedContract.upgrade(input))
|
||||
}
|
||||
}
|
||||
|
||||
private fun <OldState : ContractState, NewState : ContractState> assembleBareTx(
|
||||
stateRef: StateAndRef<OldState>,
|
||||
upgradedContractClass: Class<UpgradedContract<OldState, NewState>>
|
||||
): TransactionBuilder {
|
||||
val contractUpgrade = upgradedContractClass.newInstance()
|
||||
return TransactionType.General.Builder(stateRef.state.notary)
|
||||
.withItems(stateRef, contractUpgrade.upgrade(stateRef.state.data), Command(UpgradeCommand(contractUpgrade.javaClass), stateRef.state.data.participants))
|
||||
}
|
||||
|
||||
class Instigator<OldState : ContractState, NewState : ContractState>(
|
||||
originalState: StateAndRef<OldState>,
|
||||
newContractClass: Class<UpgradedContract<OldState, NewState>>
|
||||
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
|
||||
|
||||
override fun assembleTx(): Pair<SignedTransaction, Iterable<CompositeKey>> {
|
||||
val stx = assembleBareTx(originalState, modification)
|
||||
.signWith(serviceHub.legalIdentityKey)
|
||||
.toSignedTransaction(false)
|
||||
return Pair(stx, originalState.state.data.participants)
|
||||
}
|
||||
}
|
||||
|
||||
class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Class<UpgradedContract<ContractState, *>>>(otherSide) {
|
||||
@Suspendable
|
||||
@Throws(StateReplacementException::class)
|
||||
override fun verifyProposal(proposal: Proposal<Class<UpgradedContract<ContractState, *>>>) {
|
||||
// Retrieve signed transaction from our side, we will apply the upgrade logic to the transaction on our side, and verify outputs matches the proposed upgrade.
|
||||
val stx = subFlow(FetchTransactionsFlow(setOf(proposal.stateRef.txhash), otherSide)).fromDisk.singleOrNull()
|
||||
requireNotNull(stx) { "We don't have a copy of the referenced state" }
|
||||
val oldStateAndRef = stx!!.tx.outRef<ContractState>(proposal.stateRef.index)
|
||||
val authorisedUpgrade = serviceHub.vaultService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?: throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}")
|
||||
val proposedTx = proposal.stx.tx
|
||||
val expectedTx = assembleBareTx(oldStateAndRef, proposal.modification).toWireTransaction()
|
||||
requireThat {
|
||||
"The instigator is one of the participants" by oldStateAndRef.state.data.participants.contains(otherSide.owningKey)
|
||||
"The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" by (proposal.modification == authorisedUpgrade)
|
||||
"The proposed tx matches the expected tx for this upgrade" by (proposedTx == expectedTx)
|
||||
}
|
||||
ContractUpgradeFlow.verify(oldStateAndRef.state.data, expectedTx.outRef<ContractState>(0).state.data, expectedTx.commands.single())
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ object NotaryChangeFlow : AbstractStateReplacementFlow() {
|
||||
class Instigator<out T : ContractState>(
|
||||
originalState: StateAndRef<T>,
|
||||
newNotary: Party,
|
||||
progressTracker: ProgressTracker = tracker()) : AbstractStateReplacementFlow.Instigator<T, Party>(originalState, newNotary, progressTracker) {
|
||||
progressTracker: ProgressTracker = tracker()) : AbstractStateReplacementFlow.Instigator<T, T, Party>(originalState, newNotary, progressTracker) {
|
||||
|
||||
override fun assembleTx(): Pair<SignedTransaction, Iterable<CompositeKey>> {
|
||||
val state = originalState.state
|
||||
|
@ -0,0 +1,31 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.testing.ALICE_PUBKEY
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Tests for the version 2 dummy contract, to cover ensuring upgrade transactions are built correctly.
|
||||
*/
|
||||
class DummyContractV2Tests {
|
||||
@Test
|
||||
fun `upgrade from v1`() {
|
||||
val contractUpgrade = DummyContractV2()
|
||||
val v1State = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), DUMMY_NOTARY)
|
||||
val v1Ref = StateRef(SecureHash.randomSHA256(), 0)
|
||||
val v1StateAndRef = StateAndRef(v1State, v1Ref)
|
||||
val (tx, signers) = DummyContractV2().generateUpgradeFromV1(v1StateAndRef)
|
||||
|
||||
assertEquals(v1Ref, tx.inputs.single())
|
||||
|
||||
val expectedOutput = TransactionState(contractUpgrade.upgrade(v1State.data), DUMMY_NOTARY)
|
||||
val actualOutput = tx.outputs.single()
|
||||
assertEquals(expectedOutput, actualOutput)
|
||||
|
||||
val actualCommand = tx.commands.map { it.value }.single()
|
||||
assertTrue((actualCommand as UpgradeCommand).upgradedContractClass == DummyContractV2::class.java)
|
||||
}
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.utilities.Emoji
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.flows.ContractUpgradeFlow
|
||||
import net.corda.flows.FinalityFlow
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ContractUpgradeFlowTest {
|
||||
lateinit var mockNet: MockNetwork
|
||||
lateinit var a: MockNetwork.MockNode
|
||||
lateinit var b: MockNetwork.MockNode
|
||||
lateinit var notary: Party
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork()
|
||||
val nodes = mockNet.createSomeNodes()
|
||||
a = nodes.partyNodes[0]
|
||||
b = nodes.partyNodes[1]
|
||||
notary = nodes.notaryNode.info.notaryIdentity
|
||||
mockNet.runNetwork()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `2 parties contract upgrade`() {
|
||||
// Create dummy contract.
|
||||
val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.legalIdentity.ref(1), b.info.legalIdentity.ref(1))
|
||||
val stx = twoPartyDummyContract.signWith(a.services.legalIdentityKey)
|
||||
.signWith(b.services.legalIdentityKey)
|
||||
.toSignedTransaction()
|
||||
|
||||
a.services.startFlow(FinalityFlow(stx, setOf(a.info.legalIdentity, b.info.legalIdentity)))
|
||||
mockNet.runNetwork()
|
||||
|
||||
val atx = databaseTransaction(a.database) { a.services.storageService.validatedTransactions.getTransaction(stx.id) }
|
||||
val btx = databaseTransaction(b.database) { b.services.storageService.validatedTransactions.getTransaction(stx.id) }
|
||||
requireNotNull(atx)
|
||||
requireNotNull(btx)
|
||||
|
||||
// The request is expected to be rejected because party B haven't authorise the upgrade yet.
|
||||
val rejectedFuture = a.services.startFlow(ContractUpgradeFlow.Instigator<DummyContract.State, DummyContractV2.State>(atx!!.tx.outRef(0), DUMMY_V2_PROGRAM_ID.javaClass)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith(ExecutionException::class) { rejectedFuture.get() }
|
||||
|
||||
// Party B authorise the contract state upgrade.
|
||||
b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DUMMY_V2_PROGRAM_ID.javaClass)
|
||||
|
||||
// Party A initiates contract upgrade flow, expected to succeed this time.
|
||||
val resultFuture = a.services.startFlow(ContractUpgradeFlow.Instigator<DummyContract.State, DummyContractV2.State>(atx.tx.outRef(0), DUMMY_V2_PROGRAM_ID.javaClass)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
|
||||
val result = resultFuture.get()
|
||||
|
||||
listOf(a, b).forEach {
|
||||
val stx = databaseTransaction(a.database) { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) }
|
||||
requireNotNull(stx)
|
||||
|
||||
// Verify inputs.
|
||||
val input = databaseTransaction(a.database) { a.services.storageService.validatedTransactions.getTransaction(stx!!.tx.inputs.single().txhash) }
|
||||
requireNotNull(input)
|
||||
assertTrue(input!!.tx.outputs.single().data is DummyContract.State)
|
||||
|
||||
// Verify outputs.
|
||||
assertTrue(stx!!.tx.outputs.single().data is DummyContractV2.State)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `upgrade Cash to v2`() {
|
||||
// Create some cash.
|
||||
val result = a.services.startFlow(CashFlow(CashFlow.Command.IssueCash(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary))).resultFuture
|
||||
mockNet.runNetwork()
|
||||
val stateAndRef = result.getOrThrow().tx.outRef<Cash.State>(0)
|
||||
// Starts contract upgrade flow.
|
||||
a.services.startFlow(ContractUpgradeFlow.Instigator<Cash.State, CashV2.State>(stateAndRef, CashV2().javaClass))
|
||||
mockNet.runNetwork()
|
||||
// Get contract state form the vault.
|
||||
val state = databaseTransaction(a.database) { a.vault.currentVault.states }
|
||||
assertTrue(state.single().state.data is CashV2.State, "Contract state is upgraded to the new version.")
|
||||
assertEquals(Amount(1000000, USD).`issued by`(a.info.legalIdentity.ref(1)), (state.first().state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.")
|
||||
assertEquals(listOf(a.info.legalIdentity.owningKey), (state.first().state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.")
|
||||
}
|
||||
|
||||
class CashV2 : UpgradedContract<Cash.State, CashV2.State> {
|
||||
override val legacyContract = Cash()
|
||||
|
||||
data class State(override val amount: Amount<Issued<Currency>>, val owners: List<CompositeKey>) : FungibleAsset<Currency> {
|
||||
override val owner: CompositeKey = owners.first()
|
||||
override val exitKeys = (owners + amount.token.issuer.party.owningKey).toSet()
|
||||
override val contract = CashV2()
|
||||
override val participants = owners
|
||||
|
||||
override fun move(newAmount: Amount<Issued<Currency>>, newOwner: CompositeKey) = copy(amount = amount.copy(newAmount.quantity, amount.token), owners = listOf(newOwner))
|
||||
override fun toString() = "${Emoji.bagOfCash}New Cash($amount at ${amount.token.issuer} owned by $owner)"
|
||||
override fun withNewOwner(newOwner: CompositeKey) = Pair(Cash.Commands.Move(), copy(owners = listOf(newOwner)))
|
||||
}
|
||||
|
||||
override fun upgrade(state: Cash.State) = CashV2.State(state.amount.times(1000), listOf(state.owner))
|
||||
|
||||
override fun verify(tx: TransactionForContract) {}
|
||||
|
||||
// Dummy Cash contract for testing.
|
||||
override val legalContractReference = SecureHash.sha256("")
|
||||
}
|
||||
}
|
@ -146,7 +146,7 @@ class ResolveTransactionsFlowTest {
|
||||
// DOCSTART 2
|
||||
private fun makeTransactions(signFirstTX: Boolean = true, withAttachment: SecureHash? = null): Pair<SignedTransaction, SignedTransaction> {
|
||||
// Make a chain of custody of dummy states and insert into node A.
|
||||
val dummy1: SignedTransaction = DummyContract.generateInitial(MEGA_CORP.ref(1), 0, notary).let {
|
||||
val dummy1: SignedTransaction = DummyContract.generateInitial(0, notary, MEGA_CORP.ref(1)).let {
|
||||
if (withAttachment != null)
|
||||
it.addAttachment(withAttachment)
|
||||
if (signFirstTX)
|
||||
|
Reference in New Issue
Block a user