diff --git a/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt b/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt index d822a0a653..8f4b0faa8c 100644 --- a/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt @@ -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( + originalState: StateAndRef, + newContractClass: Class> +) : AbstractStateReplacementFlow.Instigator>>(originalState, newContractClass) { - @JvmStatic - fun verify(input: ContractState, output: ContractState, commandData: Command) { - val command = commandData.value as UpgradeCommand - val participants: Set = input.participants.toSet() - val keysThatSigned: Set = commandData.signers.toSet() - @Suppress("UNCHECKED_CAST") - val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract - 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 assembleBareTx( - stateRef: StateAndRef, - upgradedContractClass: Class> - ): 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( - originalState: StateAndRef, - newContractClass: Class> - ) : AbstractStateReplacementFlow.Instigator>>(originalState, newContractClass) { - - override fun assembleTx(): Pair> { - val stx = assembleBareTx(originalState, modification) - .signWith(serviceHub.legalIdentityKey) - .toSignedTransaction(false) - return Pair(stx, originalState.state.data.participants) - } - } - - class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor>>(otherSide) { - @Suspendable - @Throws(StateReplacementException::class) - override fun verifyProposal(proposal: Proposal>>) { - // 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(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 = input.participants.toSet() + val keysThatSigned: Set = commandData.signers.toSet() + @Suppress("UNCHECKED_CAST") + val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract 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(0).state.data, expectedTx.commands.single()) } + + fun assembleBareTx( + stateRef: StateAndRef, + upgradedContractClass: Class> + ): 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> { + val stx = assembleBareTx(originalState, modification) + .signWith(serviceHub.legalIdentityKey) + .toSignedTransaction(false) + return stx to originalState.state.data.participants } } diff --git a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt index c42e0038f8..44ac9942c5 100644 --- a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt @@ -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( + originalState: StateAndRef, + newNotary: Party, + progressTracker: ProgressTracker = tracker()) + : AbstractStateReplacementFlow.Instigator(originalState, newNotary, progressTracker) { - class Instigator( - originalState: StateAndRef, - newNotary: Party, - progressTracker: ProgressTracker = tracker()) : AbstractStateReplacementFlow.Instigator(originalState, newNotary, progressTracker) { + override fun assembleTx(): Pair> { + val state = originalState.state + val tx = TransactionType.NotaryChange.Builder(originalState.state.notary) - override fun assembleTx(): Pair> { - val state = originalState.state - val tx = TransactionType.NotaryChange.Builder(originalState.state.notary) + val participants: Iterable - val participants: Iterable - - 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 { - 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() - - 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(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): 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 { + 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() + + 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 } } diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index be50c01f14..78f18c4346 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -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(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>() + startFlowPermission>() ))) - val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Instigator(stateAndRef, upgrade) }, + val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow(stateAndRef, upgrade) }, atx!!.tx.outRef(0), DummyContractV2::class.java).returnValue @@ -135,7 +135,7 @@ class ContractUpgradeFlowTest { rpcB.authoriseContractUpgrade(btx!!.tx.outRef(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(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(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().single() } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 988bb348c8..1410335844 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -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 -------------- diff --git a/docs/source/contract-upgrade.rst b/docs/source/contract-upgrade.rst index 9b8b9ed9d6..ac3600165a 100644 --- a/docs/source/contract-upgrade.rst +++ b/docs/source/contract-upgrade.rst @@ -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>) /** @@ -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) }, <>, DummyContractV2::class.java) diff --git a/docs/source/key-concepts-consensus-notaries.rst b/docs/source/key-concepts-consensus-notaries.rst index 81eeee4b4b..8b8a1efd0a 100644 --- a/docs/source/key-concepts-consensus-notaries.rst +++ b/docs/source/key-concepts-consensus-notaries.rst @@ -84,7 +84,7 @@ To change the notary for an input state, use the ``NotaryChangeFlow``. For examp @Suspendable fun changeNotary(originalState: StateAndRef, newNotary: Party): StateAndRef { - val flow = NotaryChangeFlow.Instigator(originalState, newNotary) + val flow = NotaryChangeFlow(originalState, newNotary) return subFlow(flow) } diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 6b65d9444c..e07b71b9ac 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -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) } } /** diff --git a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt new file mode 100644 index 0000000000..ee0f303b58 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt @@ -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(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(otherParty) { + override fun getData(id: SecureHash): ByteArray? { + return serviceHub.storageService.attachments.openAttachment(id)?.open()?.readBytes() + } +} + +abstract class FetchDataHandler(val otherParty: Party) : FlowLogic() { + @Suspendable + @Throws(FetchDataFlow.HashNotFound::class) + override fun call() { + val request = receive(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() { + @Suspendable + override fun call() { + val request = receive(otherParty).unwrap { it } + subFlow(ResolveTransactionsFlow(request.tx, otherParty), shareParentSessions = true) + serviceHub.recordTransactions(request.tx) + } +} + +class NotaryChangeHandler(otherSide: Party) : AbstractStateReplacementFlow.Acceptor(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): 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>>(otherSide) { + @Suspendable + @Throws(StateReplacementException::class) + override fun verifyProposal(proposal: AbstractStateReplacementFlow.Proposal>>) { + // 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(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(0).state.data, expectedTx.commands.single()) + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DataVendingService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DataVendingService.kt deleted file mode 100644 index 42fb222fb9..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DataVendingService.kt +++ /dev/null @@ -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(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(otherParty) { - override fun getData(id: SecureHash): ByteArray? { - return serviceHub.storageService.attachments.openAttachment(id)?.open()?.readBytes() - } -} - -abstract class FetchDataHandler(val otherParty: Party) : FlowLogic() { - @Suspendable - @Throws(FetchDataFlow.HashNotFound::class) - override fun call() { - val request = receive(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() { - @Suspendable - override fun call() { - val request = receive(otherParty).unwrap { it } - subFlow(ResolveTransactionsFlow(request.tx, otherParty), shareParentSessions = true) - serviceHub.recordTransactions(request.tx) - } -} diff --git a/core/src/main/kotlin/net/corda/flows/NonValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt similarity index 92% rename from core/src/main/kotlin/net/corda/flows/NonValidatingNotaryFlow.kt rename to node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt index e9a9f17c2b..bbf98c28fb 100644 --- a/core/src/main/kotlin/net/corda/flows/NonValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt @@ -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, diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt index e61eafc079..8e56c79185 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt @@ -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, diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt index bb52ee9d5b..d1e7225e60 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt @@ -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, diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt index 722a1fed2c..54c30ab1be 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt @@ -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, diff --git a/core/src/main/kotlin/net/corda/flows/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt similarity index 97% rename from core/src/main/kotlin/net/corda/flows/ValidatingNotaryFlow.kt rename to node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index 0628cfcb45..14bfad2a18 100644 --- a/core/src/main/kotlin/net/corda/flows/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -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 /** diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt index 2b1000983f..38193ba708 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt @@ -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, diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index fc467c311b..173f188ef2 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -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() diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt index e87e35d961..f6b4463431 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt @@ -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