From 6d1462f8eb2dac87c262ed67fb1563f976c68427 Mon Sep 17 00:00:00 2001 From: Roger Willis Date: Thu, 11 May 2017 14:37:53 -0400 Subject: [PATCH] CollectSignaturesFlow (#560) * Initial commit for CollectSignaturesFlow, some tests and associated documentation via a new "Flow Library" section of the docsite. * Refactored the TwoPartyDealFlow to use the CollectSignaturesFlow. * Added the subclassed CollectsigsFlow to the trader demo, whitelisted it and added a flow initiator for the responder. * Minor edits to progress tracker. * Amended as per Rick's comments. * Generalised this flow, so it now works if more than one signatures have been collected, initially. * Minor edits to the IRS demo so it uses the CollectSignaturesFlow. * For debugging purposes... * Adding CollectsigsFlow support to SIMM Demo. * Removing debug logging. * Amended top level comment: transactions can only have one notary. * Added TODOs as checkTransaction logic is absent. * Addressed Mike's review comments. * Minor edit to flow-library docs. * Updated flow based on Mike's review comments. * Added two usage examples and updated the tests. * Made changes to accommodate new CollectSignaturesFlow approach. * Made changes to SIMM demo to accommodate new CollectSignaturesFlow approach. * Added abstract check proposal method to two party deal flow. * Added missing TODOs. * Addressed Sham's comments. * Rebased to M11. --- .../net/corda/flows/CollectSignaturesFlow.kt | 252 ++++++++++++++++++ .../kotlin/net/corda/flows/FinalityFlow.kt | 2 +- .../net/corda/flows/TwoPartyDealFlow.kt | 199 ++++---------- .../core/flows/CollectSignaturesFlowTests.kt | 191 +++++++++++++ docs/source/flow-library.rst | 63 +++++ docs/source/index.rst | 1 + .../net/corda/flows/TwoPartyTradeFlow.kt | 6 +- .../kotlin/net/corda/irs/flows/FixingFlow.kt | 7 +- 8 files changed, 574 insertions(+), 147 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt create mode 100644 core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt create mode 100644 docs/source/flow-library.rst diff --git a/core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt b/core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt new file mode 100644 index 0000000000..531841381a --- /dev/null +++ b/core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt @@ -0,0 +1,252 @@ +package net.corda.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.crypto.* +import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowLogic +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.unwrap +import java.security.PublicKey + +/** + * The [CollectSignaturesFlow] is used to automate the collection of counter-party signatures for a given transaction. + * + * You would typically use this flow after you have built a transaction with the TransactionBuilder and signed it with + * your key pair. If there are additional signatures to collect then they can be collected using this flow. Signatures + * are collected based upon the [WireTransaction.mustSign] property which contains the union of all the PublicKeys + * listed in the transaction's commands as well as a notary's public key, if required. This flow returns a + * [SignedTransaction] which can then be passed to the [FinalityFlow] for notarisation. The other side of this flow is + * the [SignTransactionFlow]. + * + * **WARNING**: This flow ONLY works with [ServiceHub.legalIdentityKey]s and WILL break if used with randomly generated + * keys by the [ServiceHub.keyManagementService]. + * + * **IMPORTANT** This flow NEEDS to be called with the 'shareParentSessions" parameter of [subFlow] set to true. + * + * Usage: + * + * - Call the [CollectSignaturesFlow] flow as a [subFlow] and pass it a [SignedTransaction] which has at least been + * signed by the transaction creator (and possibly an oracle, if required) + * - The flow expects that the calling node has signed the provided transaction, if not the flow will fail + * - The flow will also fail if: + * 1. The provided transaction is invalid + * 2. Any of the required signing parties cannot be found in the [ServiceHub.networkMapCache] of the initiator + * 3. If the wrong key has been used by a counterparty to sign the transaction + * 4. The counterparty rejects the provided transaction + * - The flow will return a [SignedTransaction] with all the counter-party signatures (but not the notary's!) + * - If the provided transaction has already been signed by all counter-parties then this flow simply returns the + * provided transaction without contacting any counter-parties + * - Call the [FinalityFlow] with the return value of this flow + * + * Example - issuing a multi-lateral agreement which requires N signatures: + * + * val builder = TransactionType.General.Builder(notaryRef) + * val issueCommand = Command(Agreement.Commands.Issue(), state.participants) + * + * builder.withItems(state, issueCommand) + * builder.toWireTransaction().toLedgerTransaction(serviceHub).verify() + * + * // Transaction creator signs transaction. + * val ptx = builder.signWith(serviceHub.legalIdentityKey).toSignedTransaction(false) + * + * // Call to CollectSignaturesFlow. + * // The returned signed transaction will have all signatures appended apart from the notary's. + * val stx = subFlow(CollectSignaturesFlow(ptx)) + * + * @param partiallySignedTx Transaction to collect the remaining signatures for + * + */ + +// TODO: AbstractStateReplacementFlow needs updating to use this flow. +// TODO: TwoPartyTradeFlow needs updating to use this flow. +// TODO: Update this flow to handle randomly generated keys when that works is complete. +class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction, + override val progressTracker: ProgressTracker = tracker()): FlowLogic() { + + companion object { + object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.") + object VERIFYING : ProgressTracker.Step("Verifying collected signatures.") + + fun tracker() = ProgressTracker(COLLECTING, VERIFYING) + + // TODO: Make the progress tracker adapt to the number of counter-parties to collect from. + } + + @Suspendable override fun call(): SignedTransaction { + // TODO: Revisit when key management is properly fleshed out. + // This will break if a party uses anything other than their legalIdentityKey. + // Check the signatures which have already been provided and that the transaction is valid. + // Usually just the Initiator and possibly an oracle would have signed at this point. + val myKey = serviceHub.myInfo.legalIdentity.owningKey + val signed = partiallySignedTx.sigs.map { it.by } + val notSigned = partiallySignedTx.tx.mustSign - signed + + // One of the signatures collected so far MUST be from the initiator of this flow. + require(partiallySignedTx.sigs.any { it.by == myKey }) { + "The Initiator of CollectSignaturesFlow must have signed the transaction." + } + + // The signatures must be valid and the transaction must be valid. + partiallySignedTx.verifySignatures(*notSigned.toTypedArray()) + partiallySignedTx.tx.toLedgerTransaction(serviceHub).verify() + + // Determine who still needs to sign. + progressTracker.currentStep = COLLECTING + val notaryKey = partiallySignedTx.tx.notary?.owningKey + // If present, we need to exclude the notary's PublicKey as the notary signature is collected separately with + // the FinalityFlow. + val unsigned = if (notaryKey != null) notSigned - notaryKey else notSigned + + // If the unsigned counter-parties list is empty then we don't need to collect any more signatures here. + if (unsigned.isEmpty()) return partiallySignedTx + + // Collect signatures from all counter-parties and append them to the partially signed transaction. + val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it) } + val stx = partiallySignedTx + counterpartySignatures + + // Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures. + progressTracker.currentStep = VERIFYING + if (notaryKey != null) stx.verifySignatures(notaryKey) else stx.verifySignatures() + + return stx + } + + /** + * Lookup the [Party] object for each [PublicKey] using the [ServiceHub.networkMapCache]. + */ + @Suspendable private fun keysToParties(keys: List): List = keys.map { + // TODO: Revisit when IdentityService supports resolution of a (possibly random) public key to a legal identity key. + val partyNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it) + ?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.") + partyNode.legalIdentity + } + + /** + * Get and check the required signature. + */ + @Suspendable private fun collectSignature(counterparty: Party): DigitalSignature.WithKey { + return sendAndReceive(counterparty, partiallySignedTx).unwrap { + require(counterparty.owningKey.isFulfilledBy(it.by)) { "Not signed by the required Party." } + it + } + } +} + +/** + * The [SignTransactionFlow] should be called in response to the [CollectSignaturesFlow]. It automates the signing of + * a transaction providing the transaction: + * + * 1. Should actually be signed by the [Party] invoking this flow + * 2. Is valid as per the contracts referenced in the transaction + * 3. Has been, at least, signed by the counter-party which created it + * 4. Conforms to custom checking provided in the [checkTransaction] method of the [SignTransactionFlow] + * + * Usage: + * + * - Subclass [SignTransactionFlow] - this can be done inside an existing flow (as shown below) + * - Override the [checkTransaction] method to add some custom verification logic + * - Call the flow via subFlow with "shareParentSessions" set to true + * - The flow returns the fully signed transaction once it has been committed to the ledger + * + * Example - checking and signing a transaction involving a [DummyContract], see CollectSignaturesFlowTests.Kt for + * further examples: + * + * class Responder(val otherParty: Party): FlowLogic() { + * @Suspendable override fun call(): SignedTransaction { + * // [SignTransactionFlow] sub-classed as a singleton object. + * val flow = object : SignTransactionFlow(otherParty) { + * @Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat { + * val tx = stx.tx + * val magicNumberState = tx.outputs.single().data as DummyContract.MultiOwnerState + * "Must be 1337 or greater" using (magicNumberState.magicNumber >= 1337) + * } + * } + * + * // Invoke the subFlow, in response to the counterparty calling [CollectSignaturesFlow]. + * val stx = subFlow(flow, shareParentSessions = true) + * + * return waitForLedgerCommit(stx.id) + * } + * } + * + * @param otherParty The counter-party which is providing you a transaction to sign. + * + */ +abstract class SignTransactionFlow(val otherParty: Party, + override val progressTracker: ProgressTracker = tracker()) : FlowLogic() { + + companion object { + object RECEIVING : ProgressTracker.Step("Receiving transaction proposal for signing.") + object VERIFYING : ProgressTracker.Step("Verifying transaction proposal.") + object SIGNING : ProgressTracker.Step("Signing transaction proposal.") + + fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING) + } + + @Suspendable override fun call(): SignedTransaction { + progressTracker.currentStep = RECEIVING + val checkedProposal = receive(otherParty).unwrap { proposal -> + progressTracker.currentStep = VERIFYING + // Check that the Responder actually needs to sign. + checkMySignatureRequired(proposal) + // Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's. + checkSignatures(proposal) + // Resolve dependencies and verify, pass in the WireTransaction as we don't have all signatures. + subFlow(ResolveTransactionsFlow(proposal.tx, otherParty)) + proposal.tx.toLedgerTransaction(serviceHub).verify() + // Perform some custom verification over the transaction. + checkTransaction(proposal) + // All good. Unwrap the proposal. + proposal + } + + // Sign and send back our signature to the Initiator. + progressTracker.currentStep = SIGNING + val mySignature = serviceHub.legalIdentityKey.sign(checkedProposal.id) + send(otherParty, mySignature) + + // Return the fully signed transaction once it has been committed. + return waitForLedgerCommit(checkedProposal.id) + } + + @Suspendable private fun checkSignatures(stx: SignedTransaction) { + require(stx.sigs.any { it.by == otherParty.owningKey }) { + "The Initiator of CollectSignaturesFlow must have signed the transaction." + } + val signed = stx.sigs.map { it.by } + val allSigners = stx.tx.mustSign + val notSigned = allSigners - signed + stx.verifySignatures(*notSigned.toTypedArray()) + } + + /** + * The [CheckTransaction] method allows the caller of this flow to provide some additional checks over the proposed + * transaction received from the counter-party. For example: + * + * - Ensuring that the transaction you are receiving is the transaction you *EXPECT* to receive. I.e. is has the + * expected type and number of inputs and outputs + * - Checking that the properties of the outputs are as you would expect. Linking into any reference data sources + * might be appropriate here + * - Checking that the transaction is not incorrectly spending (perhaps maliciously) one of your asset states, as + * potentially the transaction creator has access to some of your state references + * + * **WARNING**: If appropriate checks, such as the ones listed above, are not defined then it is likely that your + * node will sign any transaction if it conforms to the contract code in the transaction's referenced contracts. + * + * @param stx a partially signed transaction received from your counter-party. + * @throws FlowException if the proposed transaction fails the checks. + */ + @Suspendable abstract protected fun checkTransaction(stx: SignedTransaction) + + @Suspendable private fun checkMySignatureRequired(stx: SignedTransaction) { + // TODO: Revisit when key management is properly fleshed out. + val myKey = serviceHub.myInfo.legalIdentity.owningKey + require(myKey in stx.tx.mustSign) { + "Party is not a participant for any of the input states of transaction ${stx.id}" + } + } +} diff --git a/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt index 0d1168f42f..4949c08a5a 100644 --- a/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt @@ -13,7 +13,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker /** - * Verifies the given transactions, then sends them to the named notaries. If the notary agrees that the transactions + * Verifies the given transactions, then sends them to the named notary. If the notary agrees that the transactions * are acceptable then they are from that point onwards committed to the ledger, and will be written through to the * vault. Additionally they will be distributed to the parties reflected in the participants list of the states. * diff --git a/core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt b/core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt index 6284955239..f028b36926 100644 --- a/core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt @@ -2,18 +2,19 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.DealState -import net.corda.core.crypto.* +import net.corda.core.contracts.requireThat +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.expandedCompositeKeys import net.corda.core.flows.FlowLogic import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.NodeInfo +import net.corda.core.node.services.ServiceType import net.corda.core.seconds import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.ProgressTracker -import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.trace import net.corda.core.utilities.unwrap import java.security.KeyPair @@ -26,7 +27,7 @@ import java.security.PublicKey * * TODO: Also, the term Deal is used here where we might prefer Agreement. * - * TODO: Consider whether we can merge this with [TwoPartyTradeFlow] + * TODO: Make this flow more generic. * */ object TwoPartyDealFlow { @@ -34,27 +35,14 @@ object TwoPartyDealFlow { @CordaSerializable data class Handshake(val payload: T, val publicKey: PublicKey) - @CordaSerializable - class SignaturesFromPrimary(val sellerSig: DigitalSignature.WithKey, val notarySigs: List) - /** * Abstracted bilateral deal flow participant that initiates communication/handshake. - * - * There's a good chance we can push at least some of this logic down into core flow logic - * and helper methods etc. */ abstract class Primary(override val progressTracker: ProgressTracker = Primary.tracker()) : FlowLogic() { companion object { - object AWAITING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal") - object VERIFYING : ProgressTracker.Step("Verifying proposed transaction") - object SIGNING : ProgressTracker.Step("Signing transaction") - object NOTARY : ProgressTracker.Step("Getting notary signature") - object SENDING_SIGS : ProgressTracker.Step("Sending transaction signatures to other party") - object RECORDING : ProgressTracker.Step("Recording completed transaction") - object COPYING_TO_REGULATOR : ProgressTracker.Step("Copying regulator") - - fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, NOTARY, SENDING_SIGS, RECORDING, COPYING_TO_REGULATOR) + object SENDING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal.") + fun tracker() = ProgressTracker(SENDING_PROPOSAL) } abstract val payload: Any @@ -62,123 +50,42 @@ object TwoPartyDealFlow { abstract val otherParty: Party abstract val myKeyPair: KeyPair - @Suspendable - fun getPartialTransaction(): UntrustworthyData { - progressTracker.currentStep = AWAITING_PROPOSAL - + @Suspendable override fun call(): SignedTransaction { + progressTracker.currentStep = SENDING_PROPOSAL // Make the first message we'll send to kick off the flow. - val hello = Handshake(payload, myKeyPair.public) - val maybeSTX = sendAndReceive(otherParty, hello) + val hello = Handshake(payload, serviceHub.myInfo.legalIdentity.owningKey) + // Wait for the FinalityFlow to finish on the other side and return the tx when it's available. + send(otherParty, hello) - return maybeSTX - } - - @Suspendable - fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData): SignedTransaction { - progressTracker.currentStep = VERIFYING - - untrustedPartialTX.unwrap { stx -> - progressTracker.nextStep() - - // Check that the tx proposed by the buyer is valid. - val wtx: WireTransaction = stx.verifySignatures(myKeyPair.public, notaryNode.notaryIdentity.owningKey) - logger.trace { "Received partially signed transaction: ${stx.id}" } - - checkDependencies(stx) - - // This verifies that the transaction is contract-valid, even though it is missing signatures. - wtx.toLedgerTransaction(serviceHub).verify() - - // There are all sorts of funny games a malicious secondary might play here, we should fix them: - // - // - This tx may attempt to send some assets we aren't intending to sell to the secondary, if - // we're reusing keys! So don't reuse keys! - // - This tx may include output states that impose odd conditions on the movement of the cash, - // once we implement state pairing. - // - // but the goal of this code is not to be fully secure (yet), but rather, just to find good ways to - // express flow state machines on top of the messaging layer. - - return stx - } - } - - @Suspendable - private fun checkDependencies(stx: SignedTransaction) { - // Download and check all the transactions that this transaction depends on, but do not check this - // transaction itself. - val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet() - subFlow(ResolveTransactionsFlow(dependencyTxIDs, otherParty)) - } - - @Suspendable - override fun call(): SignedTransaction { - val stx: SignedTransaction = verifyPartialTransaction(getPartialTransaction()) - - // These two steps could be done in parallel, in theory. Our framework doesn't support that yet though. - val ourSignature = computeOurSignature(stx) - val allPartySignedTx = stx + ourSignature - val notarySignatures = getNotarySignatures(allPartySignedTx) - - val fullySigned = sendSignatures(allPartySignedTx, ourSignature, notarySignatures) - - progressTracker.currentStep = RECORDING - - serviceHub.recordTransactions(fullySigned) - - logger.trace { "Deal stored" } - - progressTracker.currentStep = COPYING_TO_REGULATOR - val regulators = serviceHub.networkMapCache.regulatorNodes - if (regulators.isNotEmpty()) { - // If there are regulators in the network, then we could copy them in on the transaction via a sub-flow - // which would simply send them the transaction. + val signTransactionFlow = object : SignTransactionFlow(otherParty) { + override fun checkTransaction(stx: SignedTransaction) = checkProposal(stx) } - return fullySigned + subFlow(signTransactionFlow) + + val txHash = receive(otherParty).unwrap { it } + + return waitForLedgerCommit(txHash) } - @Suspendable - private fun getNotarySignatures(stx: SignedTransaction): List { - progressTracker.currentStep = NOTARY - return subFlow(NotaryFlow.Client(stx)) - } - - open fun computeOurSignature(partialTX: SignedTransaction): DigitalSignature.WithKey { - progressTracker.currentStep = SIGNING - return myKeyPair.sign(partialTX.id) - } - - @Suspendable - private fun sendSignatures(allPartySignedTx: SignedTransaction, ourSignature: DigitalSignature.WithKey, - notarySignatures: List): SignedTransaction { - progressTracker.currentStep = SENDING_SIGS - val fullySigned = allPartySignedTx + notarySignatures - - logger.trace { "Built finished transaction, sending back to other party!" } - - send(otherParty, SignaturesFromPrimary(ourSignature, notarySignatures)) - return fullySigned - } + @Suspendable abstract fun checkProposal(stx: SignedTransaction) } - /** * Abstracted bilateral deal flow participant that is recipient of initial communication. - * - * There's a good chance we can push at least some of this logic down into core flow logic - * and helper methods etc. */ abstract class Secondary(override val progressTracker: ProgressTracker = Secondary.tracker()) : FlowLogic() { companion object { - object RECEIVING : ProgressTracker.Step("Waiting for deal info") - object VERIFYING : ProgressTracker.Step("Verifying deal info") - object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal") - object SWAPPING_SIGNATURES : ProgressTracker.Step("Swapping signatures with the other party") - object RECORDING : ProgressTracker.Step("Recording completed transaction") + object RECEIVING : ProgressTracker.Step("Waiting for deal info.") + object VERIFYING : ProgressTracker.Step("Verifying deal info.") + object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal.") + object COLLECTING_SIGNATURES : ProgressTracker.Step("Collecting signatures from other parties.") + object RECORDING : ProgressTracker.Step("Recording completed transaction.") + object COPYING_TO_REGULATOR : ProgressTracker.Step("Copying regulator.") + object COPYING_TO_COUNTERPARTY : ProgressTracker.Step("Copying counterparty.") - fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SWAPPING_SIGNATURES, RECORDING) + fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING, COLLECTING_SIGNATURES, RECORDING, COPYING_TO_REGULATOR, COPYING_TO_COUNTERPARTY) } abstract val otherParty: Party @@ -188,23 +95,35 @@ object TwoPartyDealFlow { val handshake = receiveAndValidateHandshake() progressTracker.currentStep = SIGNING - val (ptx, additionalSigningPubKeys) = assembleSharedTX(handshake) - val stx = signWithOurKeys(additionalSigningPubKeys, ptx) + val (utx, additionalSigningPubKeys) = assembleSharedTX(handshake) + val ptx = signWithOurKeys(additionalSigningPubKeys, utx) - val signatures = swapSignaturesWithPrimary(stx) + logger.trace { "Signed proposed transaction." } + + progressTracker.currentStep = COLLECTING_SIGNATURES + val stx = subFlow(CollectSignaturesFlow(ptx)) logger.trace { "Got signatures from other party, verifying ... " } - val fullySigned = stx + signatures.sellerSig + signatures.notarySigs - fullySigned.verifySignatures() - - logger.trace { "Signatures received are valid. Deal transaction complete! :-)" } - progressTracker.currentStep = RECORDING - serviceHub.recordTransactions(fullySigned) + val ftx = subFlow(FinalityFlow(stx, setOf(otherParty, serviceHub.myInfo.legalIdentity))).single() - logger.trace { "Deal transaction stored" } - return fullySigned + logger.trace { "Recorded transaction." } + + progressTracker.currentStep = COPYING_TO_REGULATOR + val regulators = serviceHub.networkMapCache.regulatorNodes + if (regulators.isNotEmpty()) { + // Copy the transaction to every regulator in the network. This is obviously completely bogus, it's + // just for demo purposes. + regulators.forEach { send(it.serviceIdentities(ServiceType.regulator).first(), ftx) } + } + + progressTracker.currentStep = COPYING_TO_COUNTERPARTY + // Send the final transaction hash back to the other party. + // We need this so we don't break the IRS demo and the SIMM Demo. + send(otherParty, ftx.id) + + return ftx } @Suspendable @@ -217,16 +136,6 @@ object TwoPartyDealFlow { return handshake.unwrap { validateHandshake(it) } } - @Suspendable - private fun swapSignaturesWithPrimary(stx: SignedTransaction): SignaturesFromPrimary { - progressTracker.currentStep = SWAPPING_SIGNATURES - logger.trace { "Sending partially signed transaction to other party" } - - // TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx. - - return sendAndReceive(otherParty, stx).unwrap { it } - } - private fun signWithOurKeys(signingPubKeys: List, ptx: TransactionBuilder): SignedTransaction { // Now sign the transaction with whatever keys we need to move the cash. for (publicKey in signingPubKeys.expandedCompositeKeys) { @@ -244,7 +153,6 @@ object TwoPartyDealFlow { @CordaSerializable data class AutoOffer(val notary: Party, val dealBeingOffered: DealState) - /** * One side of the flow for inserting a pre-agreed deal. */ @@ -255,6 +163,10 @@ object TwoPartyDealFlow { override val notaryNode: NodeInfo get() = serviceHub.networkMapCache.notaryNodes.filter { it.notaryIdentity == payload.notary }.single() + + @Suspendable override fun checkProposal(stx: SignedTransaction) = requireThat { + // Add some constraints here. + } } /** @@ -281,5 +193,4 @@ object TwoPartyDealFlow { return Pair(ptx, arrayListOf(deal.parties.single { it == serviceHub.myInfo.legalIdentity as AbstractParty }.owningKey)) } } - } diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt new file mode 100644 index 0000000000..cbfdd7cba0 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -0,0 +1,191 @@ +package net.corda.core.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Command +import net.corda.core.contracts.DummyContract +import net.corda.core.contracts.TransactionType +import net.corda.core.contracts.requireThat +import net.corda.core.getOrThrow +import net.corda.core.identity.Party +import net.corda.core.node.PluginServiceHub +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.unwrap +import net.corda.flows.CollectSignaturesFlow +import net.corda.flows.FinalityFlow +import net.corda.flows.SignTransactionFlow +import net.corda.testing.MINI_CORP_KEY +import net.corda.testing.node.MockNetwork +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.concurrent.ExecutionException +import kotlin.test.assertFailsWith + +class CollectSignaturesFlowTests { + lateinit var mockNet: MockNetwork + lateinit var a: MockNetwork.MockNode + lateinit var b: MockNetwork.MockNode + lateinit var c: MockNetwork.MockNode + lateinit var notary: Party + + @Before + fun setup() { + mockNet = MockNetwork() + val nodes = mockNet.createSomeNodes(3) + a = nodes.partyNodes[0] + b = nodes.partyNodes[1] + c = nodes.partyNodes[2] + notary = nodes.notaryNode.info.notaryIdentity + mockNet.runNetwork() + CollectSigsTestCorDapp.registerFlows(a.services) + CollectSigsTestCorDapp.registerFlows(b.services) + CollectSigsTestCorDapp.registerFlows(c.services) + } + + @After + fun tearDown() { + mockNet.stopNodes() + } + + object CollectSigsTestCorDapp { + // Would normally be called by custom service init in a CorDapp. + fun registerFlows(pluginHub: PluginServiceHub) { + pluginHub.registerFlowInitiator(TestFlow.Initiator::class.java) { TestFlow.Responder(it) } + pluginHub.registerFlowInitiator(TestFlowTwo.Initiator::class.java) { TestFlowTwo.Responder(it) } + } + } + + // With this flow, the initiators sends an "offer" to the responder, who then initiates the collect signatures flow. + // This flow is a more simplifed version of the "TwoPartyTrade" flow and is a useful example of how both the + // "collectSignaturesFlow" and "SignTransactionFlow" can be used in practise. + object TestFlow { + @InitiatingFlow + class Initiator(val state: DummyContract.MultiOwnerState, val otherParty: Party) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + send(otherParty, state) + + val flow = object : SignTransactionFlow(otherParty) { + @Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat { + val tx = stx.tx + "There should only be one output state" using (tx.outputs.size == 1) + "There should only be one output state" using (tx.inputs.isEmpty()) + val magicNumberState = tx.outputs.single().data as DummyContract.MultiOwnerState + "Must be 1337 or greater" using (magicNumberState.magicNumber >= 1337) + } + } + + val stx = subFlow(flow) + val ftx = waitForLedgerCommit(stx.id) + + return ftx + } + } + + class Responder(val otherParty: Party) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + val state = receive(otherParty).unwrap { it } + val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity + + val command = Command(DummyContract.Commands.Create(), state.participants) + val builder = TransactionType.General.Builder(notary = notary).withItems(state, command) + val ptx = builder.signWith(serviceHub.legalIdentityKey).toSignedTransaction(false) + val stx = subFlow(CollectSignaturesFlow(ptx)) + val ftx = subFlow(FinalityFlow(stx)).single() + + return ftx + } + } + } + + // With this flow, the initiator starts the "CollectTransactionFlow". It is then the responders responsibility to + // override "checkTransaction" and add whatever logic their require to verify the SignedTransaction they are + // receiving off the wire. + object TestFlowTwo { + @InitiatingFlow + class Initiator(val state: DummyContract.MultiOwnerState, val otherParty: Party) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity + val command = Command(DummyContract.Commands.Create(), state.participants) + val builder = TransactionType.General.Builder(notary = notary).withItems(state, command) + val ptx = builder.signWith(serviceHub.legalIdentityKey).toSignedTransaction(false) + val stx = subFlow(CollectSignaturesFlow(ptx)) + val ftx = subFlow(FinalityFlow(stx)).single() + + return ftx + } + } + + class Responder(val otherParty: Party) : FlowLogic() { + @Suspendable override fun call(): SignedTransaction { + val flow = object : SignTransactionFlow(otherParty) { + @Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat { + val tx = stx.tx + "There should only be one output state" using (tx.outputs.size == 1) + "There should only be one output state" using (tx.inputs.isEmpty()) + val magicNumberState = tx.outputs.single().data as DummyContract.MultiOwnerState + "Must be 1337 or greater" using (magicNumberState.magicNumber >= 1337) + } + } + + val stx = subFlow(flow) + + return waitForLedgerCommit(stx.id) + } + } + } + + + @Test + fun `successfully collects two signatures`() { + val magicNumber = 1337 + val parties = listOf(a.info.legalIdentity, b.info.legalIdentity, c.info.legalIdentity) + val state = DummyContract.MultiOwnerState(magicNumber, parties.map { it.owningKey }) + val flow = a.services.startFlow(TestFlowTwo.Initiator(state, b.info.legalIdentity)) + mockNet.runNetwork() + val result = flow.resultFuture.getOrThrow() + result.verifySignatures() + println(result.tx) + println(result.sigs) + } + + @Test + fun `no need to collect any signatures`() { + val onePartyDummyContract = DummyContract.generateInitial(1337, notary, a.info.legalIdentity.ref(1)) + val ptx = onePartyDummyContract.signWith(a.services.legalIdentityKey).toSignedTransaction(false) + val flow = a.services.startFlow(CollectSignaturesFlow(ptx)) + mockNet.runNetwork() + val result = flow.resultFuture.getOrThrow() + result.verifySignatures() + println(result.tx) + println(result.sigs) + } + + @Test + fun `fails when not signed by initiator`() { + val onePartyDummyContract = DummyContract.generateInitial(1337, notary, a.info.legalIdentity.ref(1)) + val ptx = onePartyDummyContract.signWith(MINI_CORP_KEY).toSignedTransaction(false) + val flow = a.services.startFlow(CollectSignaturesFlow(ptx)) + mockNet.runNetwork() + assertFailsWith("The Initiator of CollectSignaturesFlow must have signed the transaction.") { + flow.resultFuture.get() + } + } + + @Test + fun `passes with multiple initial signatures`() { + val twoPartyDummyContract = DummyContract.generateInitial(1337, notary, + a.info.legalIdentity.ref(1), + b.info.legalIdentity.ref(2), + b.info.legalIdentity.ref(3)) + val ptx = twoPartyDummyContract.signWith(a.services.legalIdentityKey).signWith(b.services.legalIdentityKey).toSignedTransaction(false) + val flow = a.services.startFlow(CollectSignaturesFlow(ptx)) + mockNet.runNetwork() + val result = flow.resultFuture.getOrThrow() + println(result.tx) + println(result.sigs) + } +} + diff --git a/docs/source/flow-library.rst b/docs/source/flow-library.rst new file mode 100644 index 0000000000..1110dbc813 --- /dev/null +++ b/docs/source/flow-library.rst @@ -0,0 +1,63 @@ +Flow Library +============ + +There are a number of built-in flows supplied with Corda, which cover some core functionality. + +FinalityFlow +------------ + +The ``FinalityFlow`` verifies the given transactions, then sends them to the specified notary. + +If the notary agrees that the transactions are acceptable then they are from that point onwards committed to the ledger, +and will be written through to the vault. Additionally they will be distributed to the parties reflected in the participants +list of the states. + +The transactions will be topologically sorted before commitment to ensure that dependencies are committed before +dependers, so you don't need to do this yourself. + +The transactions are expected to have already been resolved: if their dependencies are not available in local storage or +within the given set, verification will fail. They must have signatures from all necessary parties other than the notary. + +If specified, the extra recipients are sent all the given transactions. The base set of parties to inform of each +transaction are calculated on a per transaction basis from the contract-given set of participants. + +The flow returns the same transactions, in the same order, with the additional signatures. + + +CollectSignaturesFlow +--------------------- + +The ``CollectSignaturesFlow`` is used to automate the collection of signatures from the counter-parties to a transaction. + +You use the ``CollectSignaturesFlow`` by passing it a ``SignedTransaction`` which has at least been signed by yourself. +The flow will handle the resolution of the counter-party identities and request a signature from each counter-party. + +Finally, the flow will verify all the signatures and return a ``SignedTransaction`` with all the collected signatures. + +When using this flow on the responding side you will have to subclass the ``AbstractCollectSignaturesFlowResponder`` and +provide your own implementation of the ``checkTransaction`` method. This is to add additional verification logic on the +responder side. Types of things you will need to check include: + +* Ensuring that the transaction you are receiving is the transaction you *EXPECT* to receive. I.e. is has the expected + type of inputs and outputs +* Checking that the properties of the outputs are as you would expect, this is in the absence of integrating reference + data sources to facilitate this for us +* Checking that the transaction is not incorrectly spending (perhaps maliciously) one of your asset states, as potentially + the transaction creator has access to some of your state references + +Typically after calling the ``CollectSignaturesFlow`` you then called the ``FinalityFlow``. + +ResolveTransactionsFlow +----------------------- + +This ``ResolveTransactionsFlow`` is used to verify the validity of a transaction by recursively checking the validity of +all the dependencies. Once a transaction is checked it's inserted into local storage so it can be relayed and won't be +checked again. + +A couple of constructors are provided that accept a single transaction. When these are used, the dependencies of that +transaction are resolved and then the transaction itself is verified. Again, if successful, the results are inserted +into the database as long as a [SignedTransaction] was provided. If only the ``WireTransaction`` form was provided +then this isn't enough to put into the local database, so only the dependencies are checked and inserted. This way +to use the flow is helpful when resolving and verifying an unfinished transaction. + +The flow returns a list of verified ``LedgerTransaction`` objects, in a depth-first order. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index bdbe58c668..330f54f375 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -118,6 +118,7 @@ Documentation Contents: :maxdepth: 2 :caption: Component library + flow-library contract-catalogue contract-irs diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt index 068703060d..7d5838c888 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt @@ -3,7 +3,9 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.sumCashBy import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.expandedCompositeKeys +import net.corda.core.crypto.sign import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party @@ -36,6 +38,8 @@ import java.util.* * that represents an atomic asset swap. * * Note that it's the *seller* who initiates contact with the buyer, not vice-versa as you might imagine. + * + * TODO: Refactor this using the [CollectSignaturesFlow]. Note. It requires a large docsite update! */ object TwoPartyTradeFlow { // TODO: Common elements in multi-party transaction consensus and signing should be refactored into a superclass of this diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt index bf65a760ad..1e709df46d 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt @@ -3,17 +3,18 @@ package net.corda.irs.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.TransientProperty import net.corda.core.contracts.* -import net.corda.core.identity.Party import net.corda.core.crypto.keys import net.corda.core.crypto.toBase58String import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.SchedulableFlow +import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.node.PluginServiceHub import net.corda.core.node.services.ServiceType import net.corda.core.seconds import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.trace @@ -123,6 +124,10 @@ object FixingFlow { override val notaryNode: NodeInfo get() { return serviceHub.networkMapCache.notaryNodes.single { it.notaryIdentity == dealToFix.state.notary } } + + @Suspendable override fun checkProposal(stx: SignedTransaction) = requireThat { + // Add some constraints here. + } }