mirror of
https://github.com/corda/corda.git
synced 2024-12-21 13:57:54 +00:00
Moved the initiated-side of core flows in net.corda.flows to net.corda.node.services. They are not meant to be visible to end-users.
This commit is contained in:
parent
e22ad19fcd
commit
eba753ddfe
@ -1,84 +1,62 @@
|
||||
package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
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
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* 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
|
||||
* This assembles the transaction for contract upgrade and sends out change proposals to all participants
|
||||
* of that state. If participants agree to the proposed change, they each sign the transaction.
|
||||
* Finally, the transaction containing all signatures is sent back to each participant so they can record it and
|
||||
* use the new updated state for future transactions.
|
||||
*/
|
||||
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())
|
||||
}
|
||||
class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState>(
|
||||
originalState: StateAndRef<OldState>,
|
||||
newContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<out UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
|
||||
|
||||
@JvmStatic
|
||||
fun verify(input: ContractState, output: ContractState, commandData: Command) {
|
||||
val command = commandData.value as UpgradeCommand
|
||||
val participants: Set<PublicKey> = input.participants.toSet()
|
||||
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract<ContractState, *>
|
||||
requireThat {
|
||||
"The signing keys include all participant keys" using keysThatSigned.containsAll(participants)
|
||||
"Inputs state reference the legacy contract" using (input.contract.javaClass == upgradedContract.legacyContract)
|
||||
"Outputs state reference the upgraded contract" using (output.contract.javaClass == command.upgradedContractClass)
|
||||
"Output state must be an upgraded version of the input state" using (output == upgradedContract.upgrade(input))
|
||||
companion object {
|
||||
@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())
|
||||
}
|
||||
}
|
||||
|
||||
private fun <OldState : ContractState, NewState : ContractState> assembleBareTx(
|
||||
stateRef: StateAndRef<OldState>,
|
||||
upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||
): TransactionBuilder {
|
||||
val contractUpgrade = upgradedContractClass.newInstance()
|
||||
return TransactionType.General.Builder(stateRef.state.notary)
|
||||
.withItems(stateRef, contractUpgrade.upgrade(stateRef.state.data), Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants))
|
||||
}
|
||||
|
||||
class Instigator<OldState : ContractState, out NewState : ContractState>(
|
||||
originalState: StateAndRef<OldState>,
|
||||
newContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<out UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
|
||||
|
||||
override fun assembleTx(): Pair<SignedTransaction, Iterable<PublicKey>> {
|
||||
val stx = assembleBareTx(originalState, modification)
|
||||
.signWith(serviceHub.legalIdentityKey)
|
||||
.toSignedTransaction(false)
|
||||
return Pair(stx, originalState.state.data.participants)
|
||||
}
|
||||
}
|
||||
|
||||
class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Class<out UpgradedContract<ContractState, *>>>(otherSide) {
|
||||
@Suspendable
|
||||
@Throws(StateReplacementException::class)
|
||||
override fun verifyProposal(proposal: 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 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()
|
||||
@JvmStatic
|
||||
fun verify(input: ContractState, output: ContractState, commandData: Command) {
|
||||
val command = commandData.value as UpgradeCommand
|
||||
val participants: Set<PublicKey> = input.participants.toSet()
|
||||
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract<ContractState, *>
|
||||
requireThat {
|
||||
"The instigator is one of the participants" using oldStateAndRef.state.data.participants.contains(otherSide.owningKey)
|
||||
"The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" using (proposal.modification == authorisedUpgrade)
|
||||
"The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx)
|
||||
"The signing keys include all participant keys" using keysThatSigned.containsAll(participants)
|
||||
"Inputs state reference the legacy contract" using (input.contract.javaClass == upgradedContract.legacyContract)
|
||||
"Outputs state reference the upgraded contract" using (output.contract.javaClass == command.upgradedContractClass)
|
||||
"Output state must be an upgraded version of the input state" using (output == upgradedContract.upgrade(input))
|
||||
}
|
||||
ContractUpgradeFlow.verify(oldStateAndRef.state.data, expectedTx.outRef<ContractState>(0).state.data, expectedTx.commands.single())
|
||||
}
|
||||
|
||||
fun <OldState : ContractState, NewState : ContractState> assembleBareTx(
|
||||
stateRef: StateAndRef<OldState>,
|
||||
upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||
): TransactionBuilder {
|
||||
val contractUpgrade = upgradedContractClass.newInstance()
|
||||
return TransactionType.General.Builder(stateRef.state.notary)
|
||||
.withItems(
|
||||
stateRef,
|
||||
contractUpgrade.upgrade(stateRef.state.data),
|
||||
Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants))
|
||||
}
|
||||
}
|
||||
|
||||
override fun assembleTx(): Pair<SignedTransaction, Iterable<PublicKey>> {
|
||||
val stx = assembleBareTx(originalState, modification)
|
||||
.signWith(serviceHub.legalIdentityKey)
|
||||
.toSignedTransaction(false)
|
||||
return stx to originalState.state.data.participants
|
||||
}
|
||||
}
|
||||
|
@ -5,119 +5,81 @@ import net.corda.core.crypto.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.flows.NotaryChangeFlow.Acceptor
|
||||
import net.corda.flows.NotaryChangeFlow.Instigator
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* A flow to be used for changing a state's Notary. This is required since all input states to a transaction
|
||||
* must point to the same notary.
|
||||
*
|
||||
* The [Instigator] assembles the transaction for notary 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
|
||||
* This assembles the transaction for notary replacement and sends out change proposals to all participants
|
||||
* of that state. If participants agree to the proposed change, they each sign the transaction.
|
||||
* Finally, the transaction containing all signatures is sent back to each participant so they can record it and
|
||||
* use the new updated state for future transactions.
|
||||
*/
|
||||
object NotaryChangeFlow : AbstractStateReplacementFlow() {
|
||||
class NotaryChangeFlow<out T : ContractState>(
|
||||
originalState: StateAndRef<T>,
|
||||
newNotary: Party,
|
||||
progressTracker: ProgressTracker = tracker())
|
||||
: AbstractStateReplacementFlow.Instigator<T, T, Party>(originalState, newNotary, progressTracker) {
|
||||
|
||||
class Instigator<out T : ContractState>(
|
||||
originalState: StateAndRef<T>,
|
||||
newNotary: Party,
|
||||
progressTracker: ProgressTracker = tracker()) : AbstractStateReplacementFlow.Instigator<T, T, Party>(originalState, newNotary, progressTracker) {
|
||||
override fun assembleTx(): Pair<SignedTransaction, Iterable<PublicKey>> {
|
||||
val state = originalState.state
|
||||
val tx = TransactionType.NotaryChange.Builder(originalState.state.notary)
|
||||
|
||||
override fun assembleTx(): Pair<SignedTransaction, Iterable<PublicKey>> {
|
||||
val state = originalState.state
|
||||
val tx = TransactionType.NotaryChange.Builder(originalState.state.notary)
|
||||
val participants: Iterable<PublicKey>
|
||||
|
||||
val participants: Iterable<PublicKey>
|
||||
|
||||
if (state.encumbrance == null) {
|
||||
val modifiedState = TransactionState(state.data, modification)
|
||||
tx.addInputState(originalState)
|
||||
tx.addOutputState(modifiedState)
|
||||
participants = state.data.participants
|
||||
} else {
|
||||
participants = resolveEncumbrances(tx)
|
||||
}
|
||||
|
||||
val myKey = serviceHub.legalIdentityKey
|
||||
tx.signWith(myKey)
|
||||
|
||||
val stx = tx.toSignedTransaction(false)
|
||||
|
||||
return Pair(stx, participants)
|
||||
if (state.encumbrance == null) {
|
||||
val modifiedState = TransactionState(state.data, modification)
|
||||
tx.addInputState(originalState)
|
||||
tx.addOutputState(modifiedState)
|
||||
participants = state.data.participants
|
||||
} else {
|
||||
participants = resolveEncumbrances(tx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the notary change state transitions to the [tx] builder for the [originalState] and its encumbrance
|
||||
* state chain (encumbrance states might be themselves encumbered by other states).
|
||||
*
|
||||
* @return union of all added states' participants
|
||||
*/
|
||||
private fun resolveEncumbrances(tx: TransactionBuilder): Iterable<PublicKey> {
|
||||
val stateRef = originalState.ref
|
||||
val txId = stateRef.txhash
|
||||
val issuingTx = serviceHub.storageService.validatedTransactions.getTransaction(txId)
|
||||
?: throw StateReplacementException("Transaction $txId not found")
|
||||
val outputs = issuingTx.tx.outputs
|
||||
val myKey = serviceHub.legalIdentityKey
|
||||
tx.signWith(myKey)
|
||||
|
||||
val participants = mutableSetOf<PublicKey>()
|
||||
|
||||
var nextStateIndex = stateRef.index
|
||||
var newOutputPosition = tx.outputStates().size
|
||||
while (true) {
|
||||
val nextState = outputs[nextStateIndex]
|
||||
tx.addInputState(StateAndRef(nextState, StateRef(txId, nextStateIndex)))
|
||||
participants.addAll(nextState.data.participants)
|
||||
|
||||
if (nextState.encumbrance == null) {
|
||||
val modifiedState = TransactionState(nextState.data, modification)
|
||||
tx.addOutputState(modifiedState)
|
||||
break
|
||||
} else {
|
||||
val modifiedState = TransactionState(nextState.data, modification, newOutputPosition + 1)
|
||||
tx.addOutputState(modifiedState)
|
||||
nextStateIndex = nextState.encumbrance
|
||||
}
|
||||
|
||||
newOutputPosition++
|
||||
}
|
||||
|
||||
return participants
|
||||
}
|
||||
val stx = tx.toSignedTransaction(false)
|
||||
|
||||
return Pair(stx, participants)
|
||||
}
|
||||
|
||||
class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Party>(otherSide) {
|
||||
/**
|
||||
* Check the notary change proposal.
|
||||
*
|
||||
* For example, if the proposed new notary has the same behaviour (e.g. both are non-validating)
|
||||
* and is also in a geographically convenient location we can just automatically approve the change.
|
||||
* TODO: In more difficult cases this should call for human attention to manually verify and approve the proposal
|
||||
*/
|
||||
override fun verifyProposal(proposal: AbstractStateReplacementFlow.Proposal<Party>): Unit {
|
||||
val state = proposal.stateRef
|
||||
val proposedTx = proposal.stx.tx
|
||||
/**
|
||||
* Adds the notary change state transitions to the [tx] builder for the [originalState] and its encumbrance
|
||||
* state chain (encumbrance states might be themselves encumbered by other states).
|
||||
*
|
||||
* @return union of all added states' participants
|
||||
*/
|
||||
private fun resolveEncumbrances(tx: TransactionBuilder): Iterable<PublicKey> {
|
||||
val stateRef = originalState.ref
|
||||
val txId = stateRef.txhash
|
||||
val issuingTx = serviceHub.storageService.validatedTransactions.getTransaction(txId)
|
||||
?: throw StateReplacementException("Transaction $txId not found")
|
||||
val outputs = issuingTx.tx.outputs
|
||||
|
||||
if (proposedTx.type !is TransactionType.NotaryChange) {
|
||||
throw StateReplacementException("The proposed transaction is not a notary change transaction.")
|
||||
val participants = mutableSetOf<PublicKey>()
|
||||
|
||||
var nextStateIndex = stateRef.index
|
||||
var newOutputPosition = tx.outputStates().size
|
||||
while (true) {
|
||||
val nextState = outputs[nextStateIndex]
|
||||
tx.addInputState(StateAndRef(nextState, StateRef(txId, nextStateIndex)))
|
||||
participants.addAll(nextState.data.participants)
|
||||
|
||||
if (nextState.encumbrance == null) {
|
||||
val modifiedState = TransactionState(nextState.data, modification)
|
||||
tx.addOutputState(modifiedState)
|
||||
break
|
||||
} else {
|
||||
val modifiedState = TransactionState(nextState.data, modification, newOutputPosition + 1)
|
||||
tx.addOutputState(modifiedState)
|
||||
nextStateIndex = nextState.encumbrance
|
||||
}
|
||||
|
||||
val newNotary = proposal.modification
|
||||
val isNotary = serviceHub.networkMapCache.notaryNodes.any { it.notaryIdentity == newNotary }
|
||||
if (!isNotary) {
|
||||
throw StateReplacementException("The proposed node $newNotary does not run a Notary service")
|
||||
}
|
||||
if (state !in proposedTx.inputs) {
|
||||
throw StateReplacementException("The proposed state $state is not in the proposed transaction inputs")
|
||||
}
|
||||
|
||||
// // An example requirement
|
||||
// val blacklist = listOf("Evil Notary")
|
||||
// checkProposal(newNotary.name !in blacklist) {
|
||||
// "The proposed new notary $newNotary is not trusted by the party"
|
||||
// }
|
||||
newOutputPosition++
|
||||
}
|
||||
|
||||
return participants
|
||||
}
|
||||
}
|
||||
|
@ -21,12 +21,12 @@ import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
import java.security.*
|
||||
|
||||
class ContractUpgradeFlowTest {
|
||||
lateinit var mockNet: MockNetwork
|
||||
@ -66,7 +66,7 @@ class ContractUpgradeFlowTest {
|
||||
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(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
||||
val rejectedFuture = a.services.startFlow(ContractUpgradeFlow(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith(ExecutionException::class) { rejectedFuture.get() }
|
||||
|
||||
@ -74,7 +74,7 @@ class ContractUpgradeFlowTest {
|
||||
b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)
|
||||
|
||||
// Party A initiates contract upgrade flow, expected to succeed this time.
|
||||
val resultFuture = a.services.startFlow(ContractUpgradeFlow.Instigator(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
||||
val resultFuture = a.services.startFlow(ContractUpgradeFlow(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
|
||||
val result = resultFuture.get()
|
||||
@ -121,10 +121,10 @@ class ContractUpgradeFlowTest {
|
||||
val rpcB = CordaRPCOpsImpl(b.services, b.smm, b.database)
|
||||
|
||||
CURRENT_RPC_USER.set(User("user", "pwd", permissions = setOf(
|
||||
startFlowPermission<ContractUpgradeFlow.Instigator<*, *>>()
|
||||
startFlowPermission<ContractUpgradeFlow<*, *>>()
|
||||
)))
|
||||
|
||||
val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Instigator(stateAndRef, upgrade) },
|
||||
val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow(stateAndRef, upgrade) },
|
||||
atx!!.tx.outRef<DummyContract.State>(0),
|
||||
DummyContractV2::class.java).returnValue
|
||||
|
||||
@ -135,7 +135,7 @@ class ContractUpgradeFlowTest {
|
||||
rpcB.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)
|
||||
|
||||
// Party A initiates contract upgrade flow, expected to succeed this time.
|
||||
val resultFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Instigator(stateAndRef, upgrade) },
|
||||
val resultFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow(stateAndRef, upgrade) },
|
||||
atx.tx.outRef<DummyContract.State>(0),
|
||||
DummyContractV2::class.java).returnValue
|
||||
|
||||
@ -165,7 +165,7 @@ class ContractUpgradeFlowTest {
|
||||
assertTrue(baseState.state.data is Cash.State, "Contract state is old version.")
|
||||
val stateAndRef = result.getOrThrow().tx.outRef<Cash.State>(0)
|
||||
// Starts contract upgrade flow.
|
||||
a.services.startFlow(ContractUpgradeFlow.Instigator(stateAndRef, CashV2::class.java))
|
||||
a.services.startFlow(ContractUpgradeFlow(stateAndRef, CashV2::class.java))
|
||||
mockNet.runNetwork()
|
||||
// Get contract state from the vault.
|
||||
val firstState = a.database.transaction { a.vault.unconsumedStates<ContractState>().single() }
|
||||
|
@ -13,6 +13,10 @@ UNRELEASED
|
||||
* ``FlowLogic.getCounterpartyMarker`` is no longer used and been deprecated for removal. If you were using this to
|
||||
manage multiple independent message streams with the same party in the same flow then use sub-flows instead.
|
||||
|
||||
* ``ContractUpgradeFlow.Instigator`` has been renamed to just ``ContractUpgradeFlow``.
|
||||
|
||||
* ``NotaryChangeFlow.Instigator`` has been renamed to just ``NotaryChangeFlow``.
|
||||
|
||||
Milestone 11.0
|
||||
--------------
|
||||
|
||||
|
@ -56,9 +56,9 @@ Currently the vault service is used to manage the authorisation records. The adm
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* This will store the upgrade authorisation in the vault, and will be queried by [ContractUpgradeFlow] during contract upgrade process.
|
||||
* Invoking this method indicate the node is willing to upgrade the [state] using the [upgradedContractClass].
|
||||
* This method will NOT initiate the upgrade process. To start the upgrade process, see [ContractUpgradeFlow.Instigator].
|
||||
* This method will NOT initiate the upgrade process.
|
||||
*/
|
||||
fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class<UpgradedContract<*, *>>)
|
||||
/**
|
||||
@ -111,7 +111,7 @@ The upgraded transaction state will be recorded in every participant's node at t
|
||||
|
||||
val rpcClient : CordaRPCClient = << Bank B's Corda RPC Client >>
|
||||
val rpcB = rpcClient.proxy()
|
||||
rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Instigator(stateAndRef, upgrade) },
|
||||
rpcB.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow(stateAndRef, upgrade) },
|
||||
<<StateAndRef of the contract state>>,
|
||||
DummyContractV2::class.java)
|
||||
|
||||
|
@ -84,7 +84,7 @@ To change the notary for an input state, use the ``NotaryChangeFlow``. For examp
|
||||
@Suspendable
|
||||
fun changeNotary(originalState: StateAndRef<ContractState>,
|
||||
newNotary: Party): StateAndRef<ContractState> {
|
||||
val flow = NotaryChangeFlow.Instigator(originalState, newNotary)
|
||||
val flow = NotaryChangeFlow(originalState, newNotary)
|
||||
return subFlow(flow)
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.flows.*
|
||||
import net.corda.node.services.*
|
||||
import net.corda.node.services.api.*
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
@ -92,7 +93,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
CashIssueFlow::class.java to setOf(Amount::class.java, OpaqueBytes::class.java, Party::class.java),
|
||||
CashPaymentFlow::class.java to setOf(Amount::class.java, Party::class.java),
|
||||
FinalityFlow::class.java to emptySet(),
|
||||
ContractUpgradeFlow.Instigator::class.java to emptySet()
|
||||
ContractUpgradeFlow::class.java to emptySet()
|
||||
)
|
||||
}
|
||||
|
||||
@ -270,8 +271,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
installCoreFlow(FetchTransactionsFlow::class) { otherParty, _ -> FetchTransactionsHandler(otherParty) }
|
||||
installCoreFlow(FetchAttachmentsFlow::class) { otherParty, _ -> FetchAttachmentsHandler(otherParty) }
|
||||
installCoreFlow(BroadcastTransactionFlow::class) { otherParty, _ -> NotifyTransactionHandler(otherParty) }
|
||||
installCoreFlow(NotaryChangeFlow.Instigator::class) { otherParty, _ -> NotaryChangeFlow.Acceptor(otherParty) }
|
||||
installCoreFlow(ContractUpgradeFlow.Instigator::class) { otherParty, _ -> ContractUpgradeFlow.Acceptor(otherParty) }
|
||||
installCoreFlow(NotaryChangeFlow::class) { otherParty, _ -> NotaryChangeHandler(otherParty) }
|
||||
installCoreFlow(ContractUpgradeFlow::class) { otherParty, _ -> ContractUpgradeHandler(otherParty) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
124
node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt
Normal file
124
node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt
Normal file
@ -0,0 +1,124 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionType
|
||||
import net.corda.core.contracts.UpgradedContract
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.flows.*
|
||||
|
||||
/**
|
||||
* This class sets up network message handlers for requests from peers for data keyed by hash. It is a piece of simple
|
||||
* glue that sits between the network layer and the database layer.
|
||||
*
|
||||
* Note that in our data model, to be able to name a thing by hash automatically gives the power to request it. There
|
||||
* are no access control lists. If you want to keep some data private, then you must be careful who you give its name
|
||||
* to, and trust that they will not pass the name onwards. If someone suspects some data might exist but does not have
|
||||
* its name, then the 256-bit search space they'd have to cover makes it physically impossible to enumerate, and as
|
||||
* such the hash of a piece of data can be seen as a type of password allowing access to it.
|
||||
*
|
||||
* Additionally, because nodes do not store invalid transactions, requesting such a transaction will always yield null.
|
||||
*/
|
||||
class FetchTransactionsHandler(otherParty: Party) : FetchDataHandler<SignedTransaction>(otherParty) {
|
||||
override fun getData(id: SecureHash): SignedTransaction? {
|
||||
return serviceHub.storageService.validatedTransactions.getTransaction(id)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use Artemis message streaming support here, called "large messages". This avoids the need to buffer.
|
||||
class FetchAttachmentsHandler(otherParty: Party) : FetchDataHandler<ByteArray>(otherParty) {
|
||||
override fun getData(id: SecureHash): ByteArray? {
|
||||
return serviceHub.storageService.attachments.openAttachment(id)?.open()?.readBytes()
|
||||
}
|
||||
}
|
||||
|
||||
abstract class FetchDataHandler<out T>(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
@Throws(FetchDataFlow.HashNotFound::class)
|
||||
override fun call() {
|
||||
val request = receive<FetchDataFlow.Request>(otherParty).unwrap {
|
||||
if (it.hashes.isEmpty()) throw FlowException("Empty hash list")
|
||||
it
|
||||
}
|
||||
val response = request.hashes.map {
|
||||
getData(it) ?: throw FetchDataFlow.HashNotFound(it)
|
||||
}
|
||||
send(otherParty, response)
|
||||
}
|
||||
|
||||
protected abstract fun getData(id: SecureHash): T?
|
||||
}
|
||||
|
||||
// TODO: We should have a whitelist of contracts we're willing to accept at all, and reject if the transaction
|
||||
// includes us in any outside that list. Potentially just if it includes any outside that list at all.
|
||||
// TODO: Do we want to be able to reject specific transactions on more complex rules, for example reject incoming
|
||||
// cash without from unknown parties?
|
||||
class NotifyTransactionHandler(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val request = receive<BroadcastTransactionFlow.NotifyTxRequest>(otherParty).unwrap { it }
|
||||
subFlow(ResolveTransactionsFlow(request.tx, otherParty), shareParentSessions = true)
|
||||
serviceHub.recordTransactions(request.tx)
|
||||
}
|
||||
}
|
||||
|
||||
class NotaryChangeHandler(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Party>(otherSide) {
|
||||
/**
|
||||
* Check the notary change proposal.
|
||||
*
|
||||
* For example, if the proposed new notary has the same behaviour (e.g. both are non-validating)
|
||||
* and is also in a geographically convenient location we can just automatically approve the change.
|
||||
* TODO: In more difficult cases this should call for human attention to manually verify and approve the proposal
|
||||
*/
|
||||
override fun verifyProposal(proposal: AbstractStateReplacementFlow.Proposal<Party>): Unit {
|
||||
val state = proposal.stateRef
|
||||
val proposedTx = proposal.stx.tx
|
||||
|
||||
if (proposedTx.type !is TransactionType.NotaryChange) {
|
||||
throw StateReplacementException("The proposed transaction is not a notary change transaction.")
|
||||
}
|
||||
|
||||
val newNotary = proposal.modification
|
||||
val isNotary = serviceHub.networkMapCache.notaryNodes.any { it.notaryIdentity == newNotary }
|
||||
if (!isNotary) {
|
||||
throw StateReplacementException("The proposed node $newNotary does not run a Notary service")
|
||||
}
|
||||
if (state !in proposedTx.inputs) {
|
||||
throw StateReplacementException("The proposed state $state is not in the proposed transaction inputs")
|
||||
}
|
||||
|
||||
// // An example requirement
|
||||
// val blacklist = listOf("Evil Notary")
|
||||
// checkProposal(newNotary.name !in blacklist) {
|
||||
// "The proposed new notary $newNotary is not trusted by the party"
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
class ContractUpgradeHandler(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Class<out UpgradedContract<ContractState, *>>>(otherSide) {
|
||||
@Suspendable
|
||||
@Throws(StateReplacementException::class)
|
||||
override fun verifyProposal(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 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 = ContractUpgradeFlow.assembleBareTx(oldStateAndRef, proposal.modification).toWireTransaction()
|
||||
requireThat {
|
||||
"The instigator is one of the participants" using (otherSide.owningKey in oldStateAndRef.state.data.participants)
|
||||
"The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" using (proposal.modification == authorisedUpgrade)
|
||||
"The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx)
|
||||
}
|
||||
ContractUpgradeFlow.verify(oldStateAndRef.state.data, expectedTx.outRef<ContractState>(0).state.data, expectedTx.commands.single())
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
package net.corda.node.services.persistence
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.flows.*
|
||||
|
||||
/**
|
||||
* This class sets up network message handlers for requests from peers for data keyed by hash. It is a piece of simple
|
||||
* glue that sits between the network layer and the database layer.
|
||||
*
|
||||
* Note that in our data model, to be able to name a thing by hash automatically gives the power to request it. There
|
||||
* are no access control lists. If you want to keep some data private, then you must be careful who you give its name
|
||||
* to, and trust that they will not pass the name onwards. If someone suspects some data might exist but does not have
|
||||
* its name, then the 256-bit search space they'd have to cover makes it physically impossible to enumerate, and as
|
||||
* such the hash of a piece of data can be seen as a type of password allowing access to it.
|
||||
*
|
||||
* Additionally, because nodes do not store invalid transactions, requesting such a transaction will always yield null.
|
||||
*/
|
||||
class FetchTransactionsHandler(otherParty: Party) : FetchDataHandler<SignedTransaction>(otherParty) {
|
||||
override fun getData(id: SecureHash): SignedTransaction? {
|
||||
return serviceHub.storageService.validatedTransactions.getTransaction(id)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use Artemis message streaming support here, called "large messages". This avoids the need to buffer.
|
||||
class FetchAttachmentsHandler(otherParty: Party) : FetchDataHandler<ByteArray>(otherParty) {
|
||||
override fun getData(id: SecureHash): ByteArray? {
|
||||
return serviceHub.storageService.attachments.openAttachment(id)?.open()?.readBytes()
|
||||
}
|
||||
}
|
||||
|
||||
abstract class FetchDataHandler<out T>(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
@Throws(FetchDataFlow.HashNotFound::class)
|
||||
override fun call() {
|
||||
val request = receive<FetchDataFlow.Request>(otherParty).unwrap {
|
||||
if (it.hashes.isEmpty()) throw FlowException("Empty hash list")
|
||||
it
|
||||
}
|
||||
val response = request.hashes.map {
|
||||
getData(it) ?: throw FetchDataFlow.HashNotFound(it)
|
||||
}
|
||||
send(otherParty, response)
|
||||
}
|
||||
|
||||
protected abstract fun getData(id: SecureHash): T?
|
||||
}
|
||||
|
||||
// TODO: We should have a whitelist of contracts we're willing to accept at all, and reject if the transaction
|
||||
// includes us in any outside that list. Potentially just if it includes any outside that list at all.
|
||||
// TODO: Do we want to be able to reject specific transactions on more complex rules, for example reject incoming
|
||||
// cash without from unknown parties?
|
||||
class NotifyTransactionHandler(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val request = receive<BroadcastTransactionFlow.NotifyTxRequest>(otherParty).unwrap { it }
|
||||
subFlow(ResolveTransactionsFlow(request.tx, otherParty), shareParentSessions = true)
|
||||
serviceHub.recordTransactions(request.tx)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package net.corda.flows
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.Party
|
||||
@ -6,6 +6,8 @@ import net.corda.core.node.services.TimestampChecker
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.flows.NotaryFlow
|
||||
import net.corda.flows.TransactionParts
|
||||
|
||||
class NonValidatingNotaryFlow(otherSide: Party,
|
||||
timestampChecker: TimestampChecker,
|
@ -3,7 +3,6 @@ package net.corda.node.services.transactions
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.services.TimestampChecker
|
||||
import net.corda.flows.NonValidatingNotaryFlow
|
||||
|
||||
/** A non-validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */
|
||||
class RaftNonValidatingNotaryService(val timestampChecker: TimestampChecker,
|
||||
|
@ -3,7 +3,6 @@ package net.corda.node.services.transactions
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.services.TimestampChecker
|
||||
import net.corda.flows.ValidatingNotaryFlow
|
||||
|
||||
/** A validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */
|
||||
class RaftValidatingNotaryService(val timestampChecker: TimestampChecker,
|
||||
|
@ -5,7 +5,6 @@ import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.services.ServiceType
|
||||
import net.corda.core.node.services.TimestampChecker
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.flows.NonValidatingNotaryFlow
|
||||
|
||||
/** A simple Notary service that does not perform transaction validation */
|
||||
class SimpleNotaryService(val timestampChecker: TimestampChecker,
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.corda.flows
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
@ -8,6 +8,7 @@ import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.flows.*
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
@ -5,7 +5,6 @@ import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.services.ServiceType
|
||||
import net.corda.core.node.services.TimestampChecker
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.flows.ValidatingNotaryFlow
|
||||
|
||||
/** A Notary service that validates the transaction chain of the submitted transaction before committing it */
|
||||
class ValidatingNotaryService(val timestampChecker: TimestampChecker,
|
||||
|
@ -2,14 +2,13 @@ package net.corda.node.services
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.X509Utilities
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.flows.NotaryChangeFlow.Instigator
|
||||
import net.corda.flows.NotaryChangeFlow
|
||||
import net.corda.flows.StateReplacementException
|
||||
import net.corda.node.internal.AbstractNode
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
@ -48,7 +47,7 @@ class NotaryChangeTests {
|
||||
fun `should change notary for a state with single participant`() {
|
||||
val state = issueState(clientNodeA, oldNotaryNode)
|
||||
val newNotary = newNotaryNode.info.notaryIdentity
|
||||
val flow = Instigator(state, newNotary)
|
||||
val flow = NotaryChangeFlow(state, newNotary)
|
||||
val future = clientNodeA.services.startFlow(flow)
|
||||
|
||||
net.runNetwork()
|
||||
@ -61,7 +60,7 @@ class NotaryChangeTests {
|
||||
fun `should change notary for a state with multiple participants`() {
|
||||
val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode)
|
||||
val newNotary = newNotaryNode.info.notaryIdentity
|
||||
val flow = Instigator(state, newNotary)
|
||||
val flow = NotaryChangeFlow(state, newNotary)
|
||||
val future = clientNodeA.services.startFlow(flow)
|
||||
|
||||
net.runNetwork()
|
||||
@ -77,7 +76,7 @@ class NotaryChangeTests {
|
||||
fun `should throw when a participant refuses to change Notary`() {
|
||||
val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode)
|
||||
val newEvilNotary = Party(X500Name("CN=Evil Notary,O=Evil R3,OU=corda,L=London,C=UK"), generateKeyPair().public)
|
||||
val flow = Instigator(state, newEvilNotary)
|
||||
val flow = NotaryChangeFlow(state, newEvilNotary)
|
||||
val future = clientNodeA.services.startFlow(flow)
|
||||
|
||||
net.runNetwork()
|
||||
@ -93,7 +92,7 @@ class NotaryChangeTests {
|
||||
|
||||
val state = StateAndRef(issueTx.outputs.first(), StateRef(issueTx.id, 0))
|
||||
val newNotary = newNotaryNode.info.notaryIdentity
|
||||
val flow = Instigator(state, newNotary)
|
||||
val flow = NotaryChangeFlow(state, newNotary)
|
||||
val future = clientNodeA.services.startFlow(flow)
|
||||
net.runNetwork()
|
||||
val newState = future.resultFuture.getOrThrow()
|
||||
|
@ -12,6 +12,7 @@ import net.corda.core.node.services.unconsumedStates
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.flows.BroadcastTransactionFlow.NotifyTxRequest
|
||||
import net.corda.node.services.NotifyTransactionHandler
|
||||
import net.corda.node.utilities.transaction
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.node.MockNetwork
|
||||
|
Loading…
Reference in New Issue
Block a user