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:
Patrick Kuo 2017-02-09 17:14:31 +00:00 committed by Chris Rankin
parent ef52014504
commit aff5148c9b
22 changed files with 496 additions and 42 deletions

View File

@ -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(

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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.
*/

View File

@ -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].

View File

@ -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

View 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())
}
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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("")
}
}

View File

@ -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)

View File

@ -0,0 +1,72 @@
Upgrading Contracts
===================
While every care is taken in development of contract code,
inevitably upgrades will be required to fix bugs (in either design or implementation).
Upgrades can involve a substitution of one version of the contract code for another or changing
to a different contract that understands how to migrate the existing state objects. State objects
refer to the contract code (by hash) they are intended for, and even where state objects can be used
with different contract versions, changing this value requires issuing a new state object.
Workflow
--------
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).
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.
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.
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.
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.
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.
11. If signatures are received from all parties, the initiating node assembles the complete signed transaction and sends it to the consensus service.
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.
.. container:: codeset
.. sourcecode:: kotlin
/**
* 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<*>)
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.
The transaction will be notarised and persisted once every participant verified and signed the upgrade proposal.

View File

@ -105,6 +105,7 @@ Documentation Contents:
network-simulator
clauses
merkle-trees
contract-upgrade
.. toctree::
:maxdepth: 2

View File

@ -58,7 +58,7 @@ class RaftNotaryServiceTests : NodeBasedTest() {
private fun issueState(node: AbstractNode, notary: Party, notaryKey: KeyPair): StateAndRef<*> {
return databaseTransaction(node.database) {
val tx = DummyContract.generateInitial(node.info.legalIdentity.ref(0), Random().nextInt(), notary)
val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.legalIdentity.ref(0))
tx.signWith(node.services.legalIdentityKey)
tx.signWith(notaryKey)
val stx = tx.toSignedTransaction()

View File

@ -260,6 +260,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
false
}
startMessagingService(CordaRPCOpsImpl(services, smm, database))
services.registerFlowInitiator(ContractUpgradeFlow.Instigator::class) { ContractUpgradeFlow.Acceptor(it) }
runOnStop += Runnable { net.stop() }
_networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured())
smm.start()

View File

@ -2,6 +2,7 @@ package net.corda.node.internal
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.SecureHash
import net.corda.core.flows.FlowLogic
@ -102,6 +103,8 @@ class CordaRPCOpsImpl(
override fun attachmentExists(id: SecureHash) = services.storageService.attachments.openAttachment(id) != null
override fun uploadAttachment(jar: InputStream) = services.storageService.attachments.importAttachment(jar)
override fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class<UpgradedContract<*, *>>) = services.vaultService.authoriseContractUpgrade(state, upgradedContractClass)
override fun deauthoriseContractUpgrade(state: StateAndRef<*>) = services.vaultService.deauthoriseContractUpgrade(state)
override fun currentNodeTime(): Instant = Instant.now(services.clock)
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
override fun uploadFile(dataType: String, name: String?, file: InputStream): String {

View File

@ -337,14 +337,27 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
return Vault.Update(consumedStates, ourNewStates.toHashSet())
}
private fun isRelevant(state: ContractState, ourKeys: Set<PublicKey>): Boolean {
return if (state is OwnableState) {
state.owner.containsAny(ourKeys)
} else if (state is LinearState) {
// It's potentially of interest to the vault
state.isRelevant(ourKeys)
} else {
false
// TODO : Persists this in DB.
private val authorisedUpgrade = mutableMapOf<StateRef, Class<UpgradedContract<*, *>>>()
override fun getAuthorisedContractUpgrade(ref: StateRef) = authorisedUpgrade[ref]
override fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class<UpgradedContract<*, *>>) {
val upgrade = upgradedContractClass.newInstance()
if (upgrade.legacyContract.javaClass != stateAndRef.state.data.contract.javaClass) {
throw IllegalArgumentException("The contract state cannot be upgraded using provided UpgradedContract.")
}
authorisedUpgrade.put(stateAndRef.ref, upgradedContractClass)
}
}
override fun deauthoriseContractUpgrade(stateAndRef: StateAndRef<*>) {
authorisedUpgrade.remove(stateAndRef.ref)
}
private fun isRelevant(state: ContractState, ourKeys: Set<PublicKey>) = when (state) {
is OwnableState -> state.owner.containsAny(ourKeys)
// It's potentially of interest to the vault
is LinearState -> state.isRelevant(ourKeys)
else -> false
}
}

View File

@ -150,7 +150,7 @@ class NotaryChangeTests {
}
fun issueState(node: AbstractNode, notaryNode: AbstractNode): StateAndRef<*> {
val tx = DummyContract.generateInitial(node.info.legalIdentity.ref(0), Random().nextInt(), notaryNode.info.notaryIdentity)
val tx = DummyContract.generateInitial(Random().nextInt(), notaryNode.info.notaryIdentity, node.info.legalIdentity.ref(0))
val nodeKey = node.services.legalIdentityKey
tx.signWith(nodeKey)
val notaryKeyPair = notaryNode.services.notaryIdentityKey
@ -178,7 +178,7 @@ fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode, notaryNode: A
}
fun issueInvalidState(node: AbstractNode, notary: Party): StateAndRef<*> {
val tx = DummyContract.generateInitial(node.info.legalIdentity.ref(0), Random().nextInt(), notary)
val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.legalIdentity.ref(0))
tx.setTime(Instant.now(), 30.seconds)
val nodeKey = node.services.legalIdentityKey
tx.signWith(nodeKey)

View File

@ -140,7 +140,7 @@ class NotaryServiceTests {
}
fun issueState(node: AbstractNode): StateAndRef<*> {
val tx = DummyContract.generateInitial(node.info.legalIdentity.ref(0), Random().nextInt(), notaryNode.info.notaryIdentity)
val tx = DummyContract.generateInitial(Random().nextInt(), notaryNode.info.notaryIdentity, node.info.legalIdentity.ref(0))
val nodeKey = node.services.legalIdentityKey
tx.signWith(nodeKey)
val notaryKeyPair = notaryNode.services.notaryIdentityKey

View File

@ -86,7 +86,7 @@ class ValidatingNotaryServiceTests {
}
fun issueState(node: AbstractNode): StateAndRef<*> {
val tx = DummyContract.generateInitial(node.info.legalIdentity.ref(0), Random().nextInt(), notaryNode.info.notaryIdentity)
val tx = DummyContract.generateInitial(Random().nextInt(), notaryNode.info.notaryIdentity, node.info.legalIdentity.ref(0))
val nodeKey = node.services.legalIdentityKey
tx.signWith(nodeKey)
val notaryKeyPair = notaryNode.services.notaryIdentityKey

View File

@ -14,7 +14,7 @@ class DummyIssueAndMove(private val notary: Party, private val counterpartyNode:
val random = Random()
val myKeyPair = serviceHub.legalIdentityKey
// Self issue an asset
val issueTx = DummyContract.generateInitial(serviceHub.myInfo.legalIdentity.ref(0), random.nextInt(), notary).apply {
val issueTx = DummyContract.generateInitial(random.nextInt(), notary, serviceHub.myInfo.legalIdentity.ref(0)).apply {
signWith(myKeyPair)
}
serviceHub.recordTransactions(issueTx.toSignedTransaction())

View File

@ -15,7 +15,7 @@ import net.corda.vega.contracts.RevisionedState
*/
object StateRevisionFlow {
class Requester<T>(curStateRef: StateAndRef<RevisionedState<T>>,
updatedData: T) : AbstractStateReplacementFlow.Instigator<RevisionedState<T>, T>(curStateRef, updatedData) {
updatedData: T) : AbstractStateReplacementFlow.Instigator<RevisionedState<T>, RevisionedState<T>, T>(curStateRef, updatedData) {
override fun assembleTx(): Pair<SignedTransaction, List<CompositeKey>> {
val state = originalState.state.data
val tx = state.generateRevision(originalState.state.notary, originalState, modification)