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
|
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.
|
||||||
*
|
*
|
||||||
|
@ -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
|
|
||||||
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.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fullySigned
|
subFlow(signTransactionFlow)
|
||||||
|
|
||||||
|
val txHash = receive<SecureHash>(otherParty).unwrap { it }
|
||||||
|
|
||||||
|
return waitForLedgerCommit(txHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable abstract fun checkProposal(stx: SignedTransaction)
|
||||||
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
:maxdepth: 2
|
||||||
:caption: Component library
|
:caption: Component library
|
||||||
|
|
||||||
|
flow-library
|
||||||
contract-catalogue
|
contract-catalogue
|
||||||
contract-irs
|
contract-irs
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user