All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.corda.flows.TwoPartyTradeFlow.kt Maven / Gradle / Ivy

There is a newer version: 0.12.1
Show newest version
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.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.node.NodeInfo
import net.corda.core.seconds
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.trace
import java.security.KeyPair
import java.util.*

/**
 * This asset trading flow implements a "delivery vs payment" type swap. It has two parties (B and S for buyer
 * and seller) and the following steps:
 *
 * 1. S sends the [StateAndRef] pointing to what they want to sell to B, along with info about the price they require
 *    B to pay. For example this has probably been agreed on an exchange.
 * 2. B sends to S a [SignedTransaction] that includes the state as input, B's cash as input, the state with the new
 *    owner key as output, and any change cash as output. It contains a single signature from B but isn't valid because
 *    it lacks a signature from S authorising movement of the asset.
 * 3. S signs it and hands the now finalised SignedWireTransaction back to B.
 *
 * Assuming no malicious termination, they both end the flow being in posession of a valid, signed transaction
 * 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.
 *
 * To initiate the flow, use either the [runBuyer] or [runSeller] methods, depending on which side of the trade
 * your node is taking. These methods return a future which will complete once the trade is over and a fully signed
 * transaction is available: you can either block your thread waiting for the flow to complete by using
 * [ListenableFuture.get] or more usefully, register a callback that will be invoked when the time comes.
 *
 * To see an example of how to use this class, look at the unit tests.
 */
// TODO: Common elements in multi-party transaction consensus and signing should be refactored into a superclass of this
// and [AbstractStateReplacementFlow].
object TwoPartyTradeFlow {

    class UnacceptablePriceException(givenPrice: Amount) : FlowException("Unacceptable price: $givenPrice")
    class AssetMismatchException(val expectedTypeName: String, val typeName: String) : FlowException() {
        override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName"
    }

    // This object is serialised to the network and is the first flow message the seller sends to the buyer.
    data class SellerTradeInfo(
            val assetForSale: StateAndRef,
            val price: Amount,
            val sellerOwnerKey: CompositeKey
    )

    data class SignaturesFromSeller(val sellerSig: DigitalSignature.WithKey,
                                    val notarySig: DigitalSignature.WithKey)

    open class Seller(val otherParty: Party,
                      val notaryNode: NodeInfo,
                      val assetToSell: StateAndRef,
                      val price: Amount,
                      val myKeyPair: KeyPair,
                      override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic() {

        companion object {
            object AWAITING_PROPOSAL : ProgressTracker.Step("Awaiting transaction proposal")
            object VERIFYING : ProgressTracker.Step("Verifying transaction proposal")
            object SIGNING : ProgressTracker.Step("Signing transaction")
            // DOCSTART 3
            object NOTARY : ProgressTracker.Step("Getting notary signature") {
                override fun childProgressTracker() = FinalityFlow.tracker()
            }
            // DOCEND 3
            object SENDING_SIGS : ProgressTracker.Step("Sending transaction signatures to buyer")

            fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, NOTARY, SENDING_SIGS)
        }

        // DOCSTART 4
        @Suspendable
        override fun call(): SignedTransaction {
            val partialTX: SignedTransaction = receiveAndCheckProposedTransaction()
            val ourSignature: DigitalSignature.WithKey = calculateOurSignature(partialTX)
            val allPartySignedTx: SignedTransaction = partialTX + ourSignature
            val notarySignature: DigitalSignature.WithKey = getNotarySignature(allPartySignedTx)
            val result: SignedTransaction = sendSignatures(allPartySignedTx, ourSignature, notarySignature)
            return result
        }
        // DOCEND 4

        // DOCSTART 6
        @Suspendable
        private fun getNotarySignature(stx: SignedTransaction): DigitalSignature.WithKey {
            progressTracker.currentStep = NOTARY
            return subFlow(NotaryFlow.Client(stx))
        }
        // DOCEND 6

        // DOCSTART 5
        @Suspendable
        private fun receiveAndCheckProposedTransaction(): SignedTransaction {
            progressTracker.currentStep = AWAITING_PROPOSAL

            val myPublicKey = myKeyPair.public.composite
            // Make the first message we'll send to kick off the flow.
            val hello = SellerTradeInfo(assetToSell, price, myPublicKey)

            val maybeSTX = sendAndReceive(otherParty, hello)

            progressTracker.currentStep = VERIFYING

            maybeSTX.unwrap {
                progressTracker.nextStep()

                // Check that the tx proposed by the buyer is valid.
                val wtx: WireTransaction = it.verifySignatures(myPublicKey, notaryNode.notaryIdentity.owningKey)
                logger.trace { "Received partially signed transaction: ${it.id}" }

                // Download and check all the things that this transaction depends on and verify it is contract-valid,
                // even though it is missing signatures.
                subFlow(ResolveTransactionsFlow(wtx, otherParty))

                if (wtx.outputs.map { it.data }.sumCashBy(myPublicKey).withoutIssuer() != price) {
                    throw FlowException("Transaction is not sending us the right amount of cash")
                }

                return it
            }
        }
        // DOCEND 5

        // Following comment moved here so that it doesn't appear in the docsite:
        // There are all sorts of funny games a malicious secondary might play with it sends maybeSTX (in
        // receiveAndCheckProposedTransaction), 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.

        // DOCSTART 7
        open fun calculateOurSignature(partialTX: SignedTransaction): DigitalSignature.WithKey {
            progressTracker.currentStep = SIGNING
            return myKeyPair.signWithECDSA(partialTX.id)
        }

        @Suspendable
        private fun sendSignatures(allPartySignedTx: SignedTransaction,
                                   ourSignature: DigitalSignature.WithKey,
                                   notarySignature: DigitalSignature.WithKey): SignedTransaction {
            progressTracker.currentStep = SENDING_SIGS
            val fullySigned = allPartySignedTx + notarySignature

            logger.trace { "Built finished transaction, sending back to secondary!" }

            send(otherParty, SignaturesFromSeller(ourSignature, notarySignature))
            return fullySigned
        }
        // DOCEND 7
    }

    // DOCSTART 2
    open class Buyer(val otherParty: Party,
                     val notary: Party,
                     val acceptablePrice: Amount,
                     val typeToBuy: Class) : FlowLogic() {

        object RECEIVING : ProgressTracker.Step("Waiting for seller trading info")
        object VERIFYING : ProgressTracker.Step("Verifying seller assets")
        object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal")
        object SWAPPING_SIGNATURES : ProgressTracker.Step("Swapping signatures with the seller")

        override val progressTracker = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SWAPPING_SIGNATURES)

        // DOCSTART 1
        @Suspendable
        override fun call(): SignedTransaction {
            val tradeRequest = receiveAndValidateTradeRequest()

            progressTracker.currentStep = SIGNING
            val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest)
            val stx = signWithOurKeys(cashSigningPubKeys, ptx)

            val signatures = swapSignaturesWithSeller(stx)

            logger.trace { "Got signatures from seller, verifying ... " }

            val fullySigned = stx + signatures.sellerSig + signatures.notarySig
            fullySigned.verifySignatures()

            logger.trace { "Signatures received are valid. Trade complete! :-)" }
            return fullySigned
        }

        @Suspendable
        private fun receiveAndValidateTradeRequest(): SellerTradeInfo {
            progressTracker.currentStep = RECEIVING
            // Wait for a trade request to come in from the other side
            val maybeTradeRequest = receive(otherParty)

            progressTracker.currentStep = VERIFYING
            maybeTradeRequest.unwrap {
                // What is the seller trying to sell us?
                val asset = it.assetForSale.state.data
                val assetTypeName = asset.javaClass.name
                logger.trace { "Got trade request for a $assetTypeName: ${it.assetForSale}" }

                if (it.price > acceptablePrice)
                    throw UnacceptablePriceException(it.price)
                if (!typeToBuy.isInstance(asset))
                    throw AssetMismatchException(typeToBuy.name, assetTypeName)

                // Check the transaction that contains the state which is being resolved.
                // We only have a hash here, so if we don't know it already, we have to ask for it.
                subFlow(ResolveTransactionsFlow(setOf(it.assetForSale.ref.txhash), otherParty))

                return it
            }
        }

        @Suspendable
        private fun swapSignaturesWithSeller(stx: SignedTransaction): SignaturesFromSeller {
            progressTracker.currentStep = SWAPPING_SIGNATURES
            logger.trace { "Sending partially signed transaction to seller" }

            // TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.

            return sendAndReceive(otherParty, stx).unwrap { it }
        }

        private fun signWithOurKeys(cashSigningPubKeys: List, ptx: TransactionBuilder): SignedTransaction {
            // Now sign the transaction with whatever keys we need to move the cash.
            for (publicKey in cashSigningPubKeys.keys) {
                val privateKey = serviceHub.keyManagementService.toPrivate(publicKey)
                ptx.signWith(KeyPair(publicKey, privateKey))
            }

            return ptx.toSignedTransaction(checkSufficientSignatures = false)
        }

        private fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair> {
            val ptx = TransactionType.General.Builder(notary)

            // Add input and output states for the movement of cash, by using the Cash contract to generate the states
            val (tx, cashSigningPubKeys) = serviceHub.vaultService.generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey)

            // Add inputs/outputs/a command for the movement of the asset.
            tx.addInputState(tradeRequest.assetForSale)

            // Just pick some new public key for now. This won't be linked with our identity in any way, which is what
            // we want for privacy reasons: the key is here ONLY to manage and control ownership, it is not intended to
            // reveal who the owner actually is. The key management service is expected to derive a unique key from some
            // initial seed in order to provide privacy protection.
            val freshKey = serviceHub.keyManagementService.freshKey()
            val (command, state) = tradeRequest.assetForSale.state.data.withNewOwner(freshKey.public.composite)
            tx.addOutputState(state, tradeRequest.assetForSale.state.notary)
            tx.addCommand(command, tradeRequest.assetForSale.state.data.owner)

            // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
            // to have one.
            val currentTime = serviceHub.clock.instant()
            tx.setTime(currentTime, 30.seconds)
            return Pair(tx, cashSigningPubKeys)
        }
        // DOCEND 1
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy