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.
This commit is contained in:
Roger Willis 2017-05-11 14:37:53 -04:00 committed by GitHub
parent fc50860dae
commit 6d1462f8eb
8 changed files with 574 additions and 147 deletions

View File

@ -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<SignedTransaction>() {
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<PublicKey>): List<Party> = 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<DigitalSignature.WithKey>(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<SignedTransaction>() {
* @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<SignedTransaction>() {
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<SignedTransaction>(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}"
}
}
}

View File

@ -13,7 +13,7 @@ import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker 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 * 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. * vault. Additionally they will be distributed to the parties reflected in the participants list of the states.
* *

View File

@ -2,18 +2,19 @@ package net.corda.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.DealState 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.flows.FlowLogic
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.ServiceType
import net.corda.core.seconds import net.corda.core.seconds
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import java.security.KeyPair 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: 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 { object TwoPartyDealFlow {
@ -34,27 +35,14 @@ object TwoPartyDealFlow {
@CordaSerializable @CordaSerializable
data class Handshake<out T>(val payload: T, val publicKey: PublicKey) data class Handshake<out T>(val payload: T, val publicKey: PublicKey)
@CordaSerializable
class SignaturesFromPrimary(val sellerSig: DigitalSignature.WithKey, val notarySigs: List<DigitalSignature.WithKey>)
/** /**
* Abstracted bilateral deal flow participant that initiates communication/handshake. * 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<SignedTransaction>() { abstract class Primary(override val progressTracker: ProgressTracker = Primary.tracker()) : FlowLogic<SignedTransaction>() {
companion object { companion object {
object AWAITING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal") object SENDING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal.")
object VERIFYING : ProgressTracker.Step("Verifying proposed transaction") fun tracker() = ProgressTracker(SENDING_PROPOSAL)
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)
} }
abstract val payload: Any abstract val payload: Any
@ -62,123 +50,42 @@ object TwoPartyDealFlow {
abstract val otherParty: Party abstract val otherParty: Party
abstract val myKeyPair: KeyPair abstract val myKeyPair: KeyPair
@Suspendable @Suspendable override fun call(): SignedTransaction {
fun getPartialTransaction(): UntrustworthyData<SignedTransaction> { progressTracker.currentStep = SENDING_PROPOSAL
progressTracker.currentStep = AWAITING_PROPOSAL
// Make the first message we'll send to kick off the flow. // Make the first message we'll send to kick off the flow.
val hello = Handshake(payload, myKeyPair.public) val hello = Handshake(payload, serviceHub.myInfo.legalIdentity.owningKey)
val maybeSTX = sendAndReceive<SignedTransaction>(otherParty, hello) // Wait for the FinalityFlow to finish on the other side and return the tx when it's available.
send(otherParty, hello)
return maybeSTX val signTransactionFlow = object : SignTransactionFlow(otherParty) {
override fun checkTransaction(stx: SignedTransaction) = checkProposal(stx)
} }
@Suspendable subFlow(signTransactionFlow)
fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction {
progressTracker.currentStep = VERIFYING
untrustedPartialTX.unwrap { stx -> val txHash = receive<SecureHash>(otherParty).unwrap { it }
progressTracker.nextStep()
// Check that the tx proposed by the buyer is valid. return waitForLedgerCommit(txHash)
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 @Suspendable abstract fun checkProposal(stx: SignedTransaction)
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.
}
return fullySigned
}
@Suspendable
private fun getNotarySignatures(stx: SignedTransaction): List<DigitalSignature.WithKey> {
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<DigitalSignature.WithKey>): 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
}
}
/** /**
* Abstracted bilateral deal flow participant that is recipient of initial communication. * 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<U>(override val progressTracker: ProgressTracker = Secondary.tracker()) : FlowLogic<SignedTransaction>() { abstract class Secondary<U>(override val progressTracker: ProgressTracker = Secondary.tracker()) : FlowLogic<SignedTransaction>() {
companion object { companion object {
object RECEIVING : ProgressTracker.Step("Waiting for deal info") object RECEIVING : ProgressTracker.Step("Waiting for deal info.")
object VERIFYING : ProgressTracker.Step("Verifying deal info") object VERIFYING : ProgressTracker.Step("Verifying deal info.")
object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal") object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal.")
object SWAPPING_SIGNATURES : ProgressTracker.Step("Swapping signatures with the other party") object COLLECTING_SIGNATURES : ProgressTracker.Step("Collecting signatures from other parties.")
object RECORDING : ProgressTracker.Step("Recording completed transaction") 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 abstract val otherParty: Party
@ -188,23 +95,35 @@ object TwoPartyDealFlow {
val handshake = receiveAndValidateHandshake() val handshake = receiveAndValidateHandshake()
progressTracker.currentStep = SIGNING progressTracker.currentStep = SIGNING
val (ptx, additionalSigningPubKeys) = assembleSharedTX(handshake) val (utx, additionalSigningPubKeys) = assembleSharedTX(handshake)
val stx = signWithOurKeys(additionalSigningPubKeys, ptx) 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 ... " } 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 progressTracker.currentStep = RECORDING
serviceHub.recordTransactions(fullySigned) val ftx = subFlow(FinalityFlow(stx, setOf(otherParty, serviceHub.myInfo.legalIdentity))).single()
logger.trace { "Deal transaction stored" } logger.trace { "Recorded transaction." }
return fullySigned
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 @Suspendable
@ -217,16 +136,6 @@ object TwoPartyDealFlow {
return handshake.unwrap { validateHandshake(it) } 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<SignaturesFromPrimary>(otherParty, stx).unwrap { it }
}
private fun signWithOurKeys(signingPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction { private fun signWithOurKeys(signingPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
// Now sign the transaction with whatever keys we need to move the cash. // Now sign the transaction with whatever keys we need to move the cash.
for (publicKey in signingPubKeys.expandedCompositeKeys) { for (publicKey in signingPubKeys.expandedCompositeKeys) {
@ -244,7 +153,6 @@ object TwoPartyDealFlow {
@CordaSerializable @CordaSerializable
data class AutoOffer(val notary: Party, val dealBeingOffered: DealState) data class AutoOffer(val notary: Party, val dealBeingOffered: DealState)
/** /**
* One side of the flow for inserting a pre-agreed deal. * One side of the flow for inserting a pre-agreed deal.
*/ */
@ -255,6 +163,10 @@ object TwoPartyDealFlow {
override val notaryNode: NodeInfo get() = override val notaryNode: NodeInfo get() =
serviceHub.networkMapCache.notaryNodes.filter { it.notaryIdentity == payload.notary }.single() 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)) return Pair(ptx, arrayListOf(deal.parties.single { it == serviceHub.myInfo.legalIdentity as AbstractParty }.owningKey))
} }
} }
} }

View File

@ -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<SignedTransaction>() {
@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<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val state = receive<DummyContract.MultiOwnerState>(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<SignedTransaction>() {
@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<SignedTransaction>() {
@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<ExecutionException>("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)
}
}

View File

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

View File

@ -118,6 +118,7 @@ Documentation Contents:
:maxdepth: 2 :maxdepth: 2
:caption: Component library :caption: Component library
flow-library
contract-catalogue contract-catalogue
contract-irs contract-irs

View File

@ -3,7 +3,9 @@ package net.corda.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.sumCashBy import net.corda.contracts.asset.sumCashBy
import net.corda.core.contracts.* 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.FlowException
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.identity.Party import net.corda.core.identity.Party
@ -36,6 +38,8 @@ import java.util.*
* that represents an atomic asset swap. * 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. * 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 { object TwoPartyTradeFlow {
// TODO: Common elements in multi-party transaction consensus and signing should be refactored into a superclass of this // TODO: Common elements in multi-party transaction consensus and signing should be refactored into a superclass of this

View File

@ -3,17 +3,18 @@ package net.corda.irs.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.TransientProperty import net.corda.core.TransientProperty
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.identity.Party
import net.corda.core.crypto.keys import net.corda.core.crypto.keys
import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toBase58String
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.SchedulableFlow import net.corda.core.flows.SchedulableFlow
import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.PluginServiceHub import net.corda.core.node.PluginServiceHub
import net.corda.core.node.services.ServiceType import net.corda.core.node.services.ServiceType
import net.corda.core.seconds import net.corda.core.seconds
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
@ -123,6 +124,10 @@ object FixingFlow {
override val notaryNode: NodeInfo get() { override val notaryNode: NodeInfo get() {
return serviceHub.networkMapCache.notaryNodes.single { it.notaryIdentity == dealToFix.state.notary } return serviceHub.networkMapCache.notaryNodes.single { it.notaryIdentity == dealToFix.state.notary }
} }
@Suspendable override fun checkProposal(stx: SignedTransaction) = requireThat {
// Add some constraints here.
}
} }