mirror of
https://github.com/corda/corda.git
synced 2025-02-04 10:11:14 +00:00
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:
parent
fc50860dae
commit
6d1462f8eb
252
core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt
Normal file
252
core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
|
@ -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<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.
|
||||
*
|
||||
* 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>() {
|
||||
|
||||
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<SignedTransaction> {
|
||||
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<SignedTransaction>(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>): 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<SecureHash>(otherParty).unwrap { it }
|
||||
|
||||
return waitForLedgerCommit(txHash)
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
@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<U>(override val progressTracker: ProgressTracker = Secondary.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
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<SignaturesFromPrimary>(otherParty, stx).unwrap { it }
|
||||
}
|
||||
|
||||
private fun signWithOurKeys(signingPubKeys: List<PublicKey>, 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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
63
docs/source/flow-library.rst
Normal file
63
docs/source/flow-library.rst
Normal 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.
|
@ -118,6 +118,7 @@ Documentation Contents:
|
||||
:maxdepth: 2
|
||||
:caption: Component library
|
||||
|
||||
flow-library
|
||||
contract-catalogue
|
||||
contract-irs
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user