corda/docs/source/hello-world-flow.rst
2017-06-16 14:05:52 +01:00

40 KiB

Writing the flow

A flow describes the sequence of steps for agreeing a specific ledger update. By installing new flows on our node, we allow the node to handle new business processes.

We'll have to define two flows to issue an IOUState onto the ledger:

  • One to be run by the node initiating the creation of the IOU
  • One to be run by the node responding to an IOU creation request

Let's start writing our flows. We'll do this by modifying either TemplateFlow.java or TemplateFlow.kt.

FlowLogic

Each flow is implemented as a FlowLogic subclass. You define the steps taken by the flow by overriding FlowLogic.call.

We will define two FlowLogic instances communicating as a pair. The first will be called Initiator, and will be run by the sender of the IOU. The other will be called Acceptor, and will be run by the recipient. We group them together using a class (in Java) or a singleton object (in Kotlin) to show that they are conceptually related.

Overwrite the existing template code with the following:

package com.template

import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker

object IOUFlow {
    @InitiatingFlow
    @StartableByRPC
    class Initiator(val iouValue: Int,
                    val otherParty: Party): FlowLogic<SignedTransaction>() {

        /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */
        override val progressTracker = ProgressTracker()

        /** The flow logic is encapsulated within the call() method. */
        @Suspendable
        override fun call(): SignedTransaction { }
    }

    @InitiatedBy(Initiator::class)
    class Acceptor(val otherParty: Party) : FlowLogic<Unit>() {

        @Suspendable
        override fun call() { }
    }
}
package com.template;

import co.paralleluniverse.fibers.Suspendable;
import net.corda.core.flows.*;
import net.corda.core.identity.Party;
import net.corda.core.transactions.SignedTransaction;
import net.corda.core.utilities.ProgressTracker;

public class IOUFlow {
    @InitiatingFlow
    @StartableByRPC
    public static class Initiator extends FlowLogic<SignedTransaction> {
        private final Integer iouValue;
        private final Party otherParty;

        /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */
        private final ProgressTracker progressTracker = new ProgressTracker();

        public Initiator(Integer iouValue, Party otherParty) {
            this.iouValue = iouValue;
            this.otherParty = otherParty;
        }

        /** The flow logic is encapsulated within the call() method. */
        @Suspendable
        @Override
        public SignedTransaction call() throws FlowException { }
    }

    @InitiatedBy(Initiator.class)
    public static class Acceptor extends FlowLogic<Void> {

        private final Party otherParty;

        public Acceptor(Party otherParty) {
            this.otherParty = otherParty;
        }

        @Suspendable
        @Override
        public Void call() throws FlowException { }
    }
}

We can see that we have two FlowLogic subclasses, each overriding FlowLogic.call. There's a few things to note:

  • FlowLogic.call has a return type that matches the type parameter passed to FlowLogic - this is the return type of running the flow
  • The FlowLogic subclasses can have constructor parameters, which can be used as arguments to FlowLogic.call
  • FlowLogic.call is annotated @Suspendable - this means that the flow will be check-pointed and serialised to disk when it encounters a long-running operation, allowing your node to move on to running other flows. Forgetting this annotation out will lead to some very weird error messages
  • There are also a few more annotations, on the FlowLogic subclasses themselves:
    • @InitiatingFlow means that this flow can be started directly by the node
    • StartableByRPC allows the node owner to start this flow via an RPC call
    • @InitiatedBy(myClass: Class) means that this flow will only start in response to a message sent by another node running the myClass flow

Flow outline

Now that we've defined our FlowLogic subclasses, what are the steps we need to take to issue a new IOU onto the ledger?

On the initiator side, we need to:

  1. Create a valid transaction proposal for the creation of a new IOU
  2. Verify the transaction
  3. Sign the transaction ourselves
  4. Gather the acceptor's signature
  5. Optionally get the transaction notarised, to:
    • Protect against double-spends for transactions with inputs
    • Timestamp transactions that have a TimeWindow
  6. Record the transaction in our vault
  7. Send the transaction to the acceptor so that they can record it too

On the acceptor side, we need to:

  1. Receive the partially-signed transaction from the initiator
  2. Verify its contents and signatures
  3. Append our signature and send it back to the initiator
  4. Wait to receive back the transaction from the initiator
  5. Record the transaction in our vault

Subflows

Although our flow requirements look complex, we can delegate to existing flows to handle many of these tasks. A flow that is invoked within the context of a larger flow to handle a repeatable task is called a subflow.

In our initiator flow, we can automate step 4 by invoking SignTransactionFlow, and we can automate steps 5, 6 and 7 using FinalityFlow. Meanwhile, the entirety of the acceptor's flow can be automated using CollectSignaturesFlow.

All we need to do is write the steps to handle the initiator creating and signing the proposed transaction.

Writing the initiator's flow

Let's work through the steps of the initiator's flow one-by-one.

Building the transaction

We'll approach building the transaction in three steps:

  • Creating a transaction builder
  • Creating the transaction's components
  • Adding the components to the builder

TransactionBuilder

To start building the proposed transaction, we need a TransactionBuilder. This is a mutable transaction class to which we can add inputs, outputs, commands, and any other components the transaction needs.

We create a TransactionBuilder in Initiator.call as follows:

// Additional import.
import net.corda.core.transactions.TransactionBuilder

...

@Suspendable
override fun call(): SignedTransaction {
    // We create a transaction builder
    val txBuilder = TransactionBuilder()
    val notaryIdentity = serviceHub.networkMapCache.getAnyNotary()
    txBuilder.notary = notaryIdentity
}
// Additional import.
import net.corda.core.transactions.TransactionBuilder;

...

@Suspendable
@Override
public SignedTransaction call() throws FlowException {
    // We create a transaction builder
    final TransactionBuilder txBuilder = new TransactionBuilder();
    final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
    txBuilder.setNotary(notary);
}

In the first line, we create a TransactionBuilder. We will also want our transaction to have a notary, in order to prevent double-spends. In the second line, we retrieve the identity of the notary who will be notarising our transaction and add it to the builder.

You can see that the notary's identity is being retrieved from the node's ServiceHub. Whenever we need information within a flow - whether it's about our own node, its contents, or the rest of the network - we use the node's ServiceHub. In particular, ServiceHub.networkMapCache provides information about the other nodes on the network and the services that they offer.

Transaction components

Now that we have our TransactionBuilder, we need to create its components. Remember that we're trying to build the following transaction:

image

scale

15% :align: center

So we'll need the following:

  • The output IOUState
  • A Create command listing both the IOU's sender and recipient as signers

We create these components as follows:

// Additional import.
import net.corda.core.contracts.Command

...

@Suspendable
override fun call(): SignedTransaction {
    // We create a transaction builder
    val txBuilder = TransactionBuilder()
    val notaryIdentity = serviceHub.networkMapCache.getAnyNotary()
    txBuilder.notary = notaryIdentity

    // We create the transaction's components.
    val ourIdentity = serviceHub.myInfo.legalIdentity
    val iou = IOUState(iouValue, ourIdentity, otherParty)
    val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey })
}
// Additional imports.
import com.google.common.collect.ImmutableList;
import net.corda.core.contracts.Command;
import java.security.PublicKey;
import java.util.List;

...

@Suspendable
@Override
public SignedTransaction call() throws FlowException {
    // We create a transaction builder
    final TransactionBuilder txBuilder = new TransactionBuilder();
    final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
    txBuilder.setNotary(notary);

    // We create the transaction's components.
    final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity();
    final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty);
    final List<PublicKey> signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey());
    final Command txCommand = new Command(new IOUContract.Create(), signers);
}

To build the state, we start by retrieving our own identity (again, we get this information from the ServiceHub, via ServiceHub.myInfo). We then build the IOUState, using our identity, the IOUContract, and the IOU value and counterparty from the FlowLogic's constructor parameters.

We also create the command, which pairs the IOUContract.Create command with the public keys of ourselves and the counterparty. If this command is included in the transaction, both ourselves and the counterparty will be required signers.

Adding the components

Finally, we add the items to the transaction using the TransactionBuilder.withItems method:

@Suspendable
override fun call(): SignedTransaction {
    // We create a transaction builder
    val txBuilder = TransactionBuilder()
    val notaryIdentity = serviceHub.networkMapCache.getAnyNotary()
    txBuilder.notary = notaryIdentity

    // We create the transaction's components.
    val ourIdentity = serviceHub.myInfo.legalIdentity
    val iou = IOUState(iouValue, ourIdentity, otherParty)
    val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey })

    // Adding the item's to the builder.
    txBuilder.withItems(iou, txCommand)
}
@Suspendable
@Override
public SignedTransaction call() throws FlowException {
    // We create a transaction builder
    final TransactionBuilder txBuilder = new TransactionBuilder();
    final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
    txBuilder.setNotary(notary);

    // We create the transaction's components.
    final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity();
    final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty);
    final List<PublicKey> signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey());
    final Command txCommand = new Command(new IOUContract.Create(), signers);

    // Adding the item's to the builder.
    txBuilder.withItems(iou, txCommand);
}

TransactionBuilder.withItems takes a vararg of:

  • ContractState objects, which are added to the builder as output states
  • StateRef objects (references to the outputs of previous transactions), which are added to the builder as input state references
  • Command objects, which are added to the builder as commands

It will modify the TransactionBuilder in-place to add these components to it.

Verifying the transaction

We've now built our proposed transaction. Before we sign it, we should check that it represents a valid ledger update proposal by verifying the transaction, which will execute each of the transaction's contracts:

@Suspendable
override fun call(): SignedTransaction {
    // We create a transaction builder
    val txBuilder = TransactionBuilder()
    val notaryIdentity = serviceHub.networkMapCache.getAnyNotary()
    txBuilder.notary = notaryIdentity

    // We create the transaction's components.
    val ourIdentity = serviceHub.myInfo.legalIdentity
    val iou = IOUState(iouValue, ourIdentity, otherParty)
    val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey })

    // Adding the item's to the builder.
    txBuilder.withItems(iou, txCommand)

    // Verifying the transaction.
    txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify()
}
@Suspendable
@Override
public SignedTransaction call() throws FlowException {
    // We create a transaction builder
    final TransactionBuilder txBuilder = new TransactionBuilder();
    final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
    txBuilder.setNotary(notary);

    // We create the transaction's components.
    final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity();
    final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty);
    final List<PublicKey> signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey());
    final Command txCommand = new Command(new IOUContract.Create(), signers);

    // Adding the item's to the builder.
    txBuilder.withItems(iou, txCommand);

    // Verifying the transaction.
    txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify();
}

To verify the transaction, we must:

  • Convert the builder into an immutable WireTransaction
  • Convert the WireTransaction into a LedgerTransaction using the ServiceHub. This step resolves the transaction's input state references and attachment references into actual states and attachments (in case their contents are needed to verify the transaction
  • Call LedgerTransaction.verify to test whether the transaction is valid based on the contract of every input and output state in the transaction

If the verification fails, we have built an invalid transaction. Our flow will then end, throwing a TransactionVerificationException.

Signing the transaction

Now that we are satisfied that our transaction proposal is valid, we sign it. Once the transaction is signed, no-one will be able to modify the transaction without invalidating our signature. This effectively makes the transaction immutable.

@Suspendable
override fun call(): SignedTransaction {
    // We create a transaction builder
    val txBuilder = TransactionBuilder()
    val notaryIdentity = serviceHub.networkMapCache.getAnyNotary()
    txBuilder.notary = notaryIdentity

    // We create the transaction's components.
    val ourIdentity = serviceHub.myInfo.legalIdentity
    val iou = IOUState(iouValue, ourIdentity, otherParty)
    val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey })

    // Adding the item's to the builder.
    txBuilder.withItems(iou, txCommand)

    // Verifying the transaction.
    txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify()

    // Signing the transaction.
    val partSignedTx = serviceHub.signInitialTransaction(txBuilder)
}
@Suspendable
@Override
public SignedTransaction call() throws FlowException {
    // We create a transaction builder
    final TransactionBuilder txBuilder = new TransactionBuilder();
    final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
    txBuilder.setNotary(notary);

    // We create the transaction's components.
    final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity();
    final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty);
    final List<PublicKey> signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey());
    final Command txCommand = new Command(new IOUContract.Create(), signers);

    // Adding the item's to the builder.
    txBuilder.withItems(iou, txCommand);

    // Verifying the transaction.
    txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify();

    // Signing the transaction.
    final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder);
}

The call to ServiceHub.signInitialTransaction returns a SignedTransaction - an object that pairs the transaction itself with a list of signatures over that transaction.

We can now safely send the builder to our counterparty. If the counterparty tries to modify the transaction, the transaction's hash will change, our digital signature will no longer be valid, and the transaction will not be accepted as a valid ledger update.

Gathering counterparty signatures

The final step in order to create a valid transaction proposal is to collect the counterparty's signature. As discussed, we can automate this process by invoking the built-in CollectSignaturesFlow:

// Additional import.
import net.corda.flows.CollectSignaturesFlow

...

@Suspendable
override fun call(): SignedTransaction {
    // We create a transaction builder
    val txBuilder = TransactionBuilder()
    val notaryIdentity = serviceHub.networkMapCache.getAnyNotary()
    txBuilder.notary = notaryIdentity

    // We create the transaction's components.
    val ourIdentity = serviceHub.myInfo.legalIdentity
    val iou = IOUState(iouValue, ourIdentity, otherParty)
    val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey })

    // Adding the item's to the builder.
    txBuilder.withItems(iou, txCommand)

    // Verifying the transaction.
    txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify()

    // Signing the transaction.
    val partSignedTx = serviceHub.signInitialTransaction(txBuilder)

    // Gathering the signatures.
    val signedTx = subFlow(CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.tracker()))
}
// Additional import.
import net.corda.flows.CollectSignaturesFlow;

...

@Suspendable
@Override
public SignedTransaction call() throws FlowException {
    // We create a transaction builder
    final TransactionBuilder txBuilder = new TransactionBuilder();
    final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
    txBuilder.setNotary(notary);

    // We create the transaction's components.
    final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity();
    final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty);
    final List<PublicKey> signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey());
    final Command txCommand = new Command(new IOUContract.Create(), signers);

    // Adding the item's to the builder.
    txBuilder.withItems(iou, txCommand);

    // Verifying the transaction.
    txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify();

    // Signing the transaction.
    final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder);

    // Gathering the signatures.
    final SignedTransaction signedTx = subFlow(
            new CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.Companion.tracker()));
}

CollectSignaturesFlow gathers signatures from every participant listed on the transaction, and returns a SignedTransaction with all the required signatures.

Finalising the transaction

We now have a valid transaction signed by all the required parties. All that's left to do is to have it notarised and recorded by all the relevant parties. From then on, it will become a permanent part of the ledger. Again, instead of handling this process manually, we'll use a built-in flow called FinalityFlow:

// Additional import.
import net.corda.flows.FinalityFlow

...

@Suspendable
override fun call(): SignedTransaction {
    // We create a transaction builder
    val txBuilder = TransactionBuilder()
    val notaryIdentity = serviceHub.networkMapCache.getAnyNotary()
    txBuilder.notary = notaryIdentity

    // We create the transaction's components.
    val ourIdentity = serviceHub.myInfo.legalIdentity
    val iou = IOUState(iouValue, ourIdentity, otherParty)
    val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey })

    // Adding the item's to the builder.
    txBuilder.withItems(iou, txCommand)

    // Verifying the transaction.
    txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify()

    // Signing the transaction.
    val partSignedTx = serviceHub.signInitialTransaction(txBuilder)

    // Gathering the signatures.
    val signedTx = subFlow(CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.tracker()))

    // Finalising the transaction.
    return subFlow(FinalityFlow(signedTx)).single()
}
// Additional import.
import net.corda.flows.FinalityFlow;

...

@Suspendable
@Override
public SignedTransaction call() throws FlowException {
    // We create a transaction builder
    final TransactionBuilder txBuilder = new TransactionBuilder();
    final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
    txBuilder.setNotary(notary);

    // We create the transaction's components.
    final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity();
    final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty);
    final List<PublicKey> signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey());
    final Command txCommand = new Command(new IOUContract.Create(), signers);

    // Adding the item's to the builder.
    txBuilder.withItems(iou, txCommand);

    // Verifying the transaction.
    txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify();

    // Signing the transaction.
    final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder);

    // Gathering the signatures.
    final SignedTransaction signedTx = subFlow(
            new CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.Companion.tracker()));

    // Finalising the transaction.
    return subFlow(new FinalityFlow(signedTx)).get(0);
}

FinalityFlow completely automates the process of:

  • Notarising the transaction
  • Recording it in our vault
  • Sending it to the counterparty for them to record as well

FinalityFlow also returns a list of the notarised transactions. We extract the single item from this list and return it.

That completes the initiator side of the flow.

Writing the acceptor's flow

The acceptor's side of the flow is much simpler. We need to:

  1. Receive a signed transaction from the counterparty
  2. Verify the transaction
  3. Sign the transaction
  4. Send the updated transaction back to the counterparty

As we just saw, the process of building and finalising the transaction will be completely handled by the initiator flow.

SignTransactionFlow

We can automate all four steps of the acceptor's flow by invoking SignTransactionFlow. SignTransactionFlow is a flow that is registered by default on every node to respond to messages from CollectSignaturesFlow (which is invoked by the initiator flow).

As SignTransactionFlow is an abstract class, we have to subclass it and override SignTransactionFlow.checkTransaction:

// Additional import.
import net.corda.flows.SignTransactionFlow

...

@InitiatedBy(Initiator::class)
class Acceptor(val otherParty: Party) : FlowLogic<Unit>() {

    @Suspendable
    override fun call() {
        // Stage 1 - Verifying and signing the transaction.
        subFlow(object : SignTransactionFlow(otherParty, tracker()) {
            override fun checkTransaction(stx: SignedTransaction) {
                // Define custom verification logic here.
            }
        })
    }
}
// Additional import.
import net.corda.flows.SignTransactionFlow;

...

@InitiatedBy(Initiator.class)
public static class Acceptor extends FlowLogic<Void> {

    private final Party otherParty;

    public Acceptor(Party otherParty) {
        this.otherParty = otherParty;
    }

    @Suspendable
    @Override
    public Void call() throws FlowException {
        // Stage 1 - Verifying and signing the transaction.

        class signTxFlow extends SignTransactionFlow {
            private signTxFlow(Party otherParty, ProgressTracker progressTracker) {
                super(otherParty, progressTracker);
            }

            @Override
            protected void checkTransaction(SignedTransaction signedTransaction) {
                // Define custom verification logic here.
            }
        }

        subFlow(new signTxFlow(otherParty, SignTransactionFlow.Companion.tracker()));

        return null;
    }
}

SignTransactionFlow already checks the transaction's signatures, and whether the transaction is contractually valid. The purpose of SignTransactionFlow.checkTransaction is to define any additional verification of the transaction that we wish to perform before we sign it. For example, we may want to:

  • Check that the transaction contains an IOUState
  • Check that the IOU's value isn't too high

Well done! You've finished the flows!

Flow tests

As with contracts, deploying nodes to manually test flows is not efficient. Instead, we can use Corda's flow-test DSL to quickly test our flows. The flow-test DSL works by creating a network of lightweight, "mock" node implementations on which we run our flows.

The first thing we need to do is create this mock network. Open either test/kotlin/com/template/flow/FlowTests.kt or test/java/com/template/contract/ContractTests.java, and overwrite the existing code with:

package com.template

import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.getOrThrow
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetwork.MockNode
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class IOUFlowTests {
    lateinit var net: MockNetwork
    lateinit var a: MockNode
    lateinit var b: MockNode
    lateinit var c: MockNode

    @Before
    fun setup() {
        net = MockNetwork()
        val nodes = net.createSomeNodes(2)
        a = nodes.partyNodes[0]
        b = nodes.partyNodes[1]
        b.registerInitiatedFlow(IOUFlow.Acceptor::class.java)
        net.runNetwork()
    }

    @After
    fun tearDown() {
        net.stopNodes()
    }
}
package com.template;

import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
import net.corda.core.contracts.ContractState;
import net.corda.core.contracts.TransactionState;
import net.corda.core.contracts.TransactionVerificationException;
import net.corda.core.transactions.SignedTransaction;
import net.corda.testing.node.MockNetwork;
import net.corda.testing.node.MockNetwork.BasketOfNodes;
import net.corda.testing.node.MockNetwork.MockNode;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import java.util.List;

import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.assertEquals;

public class IOUFlowTests {
    private MockNetwork net;
    private MockNode a;
    private MockNode b;

    @Before
    public void setup() {
        net = new MockNetwork();
        BasketOfNodes nodes = net.createSomeNodes(2);
        a = nodes.getPartyNodes().get(0);
        b = nodes.getPartyNodes().get(1);
        b.registerInitiatedFlow(IOUFlow.Acceptor.class);
        net.runNetwork();
    }

    @After
    public void tearDown() {
        net.stopNodes();
    }

    @Rule
    public final ExpectedException exception = ExpectedException.none();
}

This creates an in-memory network with mocked-out components. The network has two nodes, plus network map and notary nodes. We register any responder flows (in our case, IOUFlow.Acceptor) on our nodes as well.

Our first test will be to check that the flow rejects invalid IOUs:

@Test
fun `flow rejects invalid IOUs`() {
    val flow = IOUFlow.Initiator(-1, b.info.legalIdentity)
    val future = a.services.startFlow(flow).resultFuture
    net.runNetwork()

    // The IOUContract specifies that IOUs cannot have negative values.
    assertFailsWith<TransactionVerificationException> {future.getOrThrow()}
}
@Test
public void flowRejectsInvalidIOUs() throws Exception {
    IOUFlow.Initiator flow = new IOUFlow.Initiator(-1, b.info.getLegalIdentity());
    ListenableFuture<SignedTransaction> future = a.getServices().startFlow(flow).getResultFuture();
    net.runNetwork();

    exception.expectCause(instanceOf(TransactionVerificationException.class));
    future.get();
}

This code causes node A to run the IOUFlow.Initiator flow. The call to MockNetwork.runNetwork is required to simulate the running of a real network.

We then assert that because we passed in a negative IOU value to the flow's constructor, the flow should fail with a TransactionVerificationException. In other words, we are asserting that at some point in flow, the transaction is verified (remember that IOUContract forbids negative value IOUs), causing the flow to fail.

Because flows need to be instrumented by a library called Quasar that allows the flows to be checkpointed and serialized to disk, you need to run these tests using the provided Run Flow Tests - Java or Run Flow Tests - Kotlin run-configurations.

Here is the full suite of tests we'll use for the IOUFlow:

@Test
fun `flow rejects invalid IOUs`() {
    val flow = IOUFlow.Initiator(-1, b.info.legalIdentity)
    val future = a.services.startFlow(flow).resultFuture
    net.runNetwork()

    // The IOUContract specifies that IOUs cannot have negative values.
    assertFailsWith<TransactionVerificationException> {future.getOrThrow()}
}

@Test
fun `SignedTransaction returned by the flow is signed by the initiator`() {
    val flow = IOUFlow.Initiator(1, b.info.legalIdentity)
    val future = a.services.startFlow(flow).resultFuture
    net.runNetwork()

    val signedTx = future.getOrThrow()
    signedTx.verifySignatures(b.services.legalIdentityKey)
}

@Test
fun `SignedTransaction returned by the flow is signed by the acceptor`() {
    val flow = IOUFlow.Initiator(1, b.info.legalIdentity)
    val future = a.services.startFlow(flow).resultFuture
    net.runNetwork()

    val signedTx = future.getOrThrow()
    signedTx.verifySignatures(a.services.legalIdentityKey)
}

@Test
fun `flow records a transaction in both parties' vaults`() {
    val flow = IOUFlow.Initiator(1, b.info.legalIdentity)
    val future = a.services.startFlow(flow).resultFuture
    net.runNetwork()
    val signedTx = future.getOrThrow()

    // We check the recorded transaction in both vaults.
    for (node in listOf(a, b)) {
        assertEquals(signedTx, node.storage.validatedTransactions.getTransaction(signedTx.id))
    }
}

@Test
fun `recorded transaction has no inputs and a single output, the input IOU`() {
    val flow = IOUFlow.Initiator(1, b.info.legalIdentity)
    val future = a.services.startFlow(flow).resultFuture
    net.runNetwork()
    val signedTx = future.getOrThrow()

    // We check the recorded transaction in both vaults.
    for (node in listOf(a, b)) {
        val recordedTx = node.storage.validatedTransactions.getTransaction(signedTx.id)
        val txOutputs = recordedTx!!.tx.outputs
        assert(txOutputs.size == 1)

        val recordedState = txOutputs[0].data as IOUState
        assertEquals(recordedState.value, 1)
        assertEquals(recordedState.sender, a.info.legalIdentity)
        assertEquals(recordedState.recipient, b.info.legalIdentity)
    }
}
@Test
public void flowRejectsInvalidIOUs() throws Exception {
    IOUFlow.Initiator flow = new IOUFlow.Initiator(-1, b.info.getLegalIdentity());
    ListenableFuture<SignedTransaction> future = a.getServices().startFlow(flow).getResultFuture();
    net.runNetwork();

    exception.expectCause(instanceOf(TransactionVerificationException.class));
    future.get();
}

@Test
public void signedTransactionReturnedByTheFlowIsSignedByTheInitiator() throws Exception {
    IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity());
    ListenableFuture<SignedTransaction> future = a.getServices().startFlow(flow).getResultFuture();
    net.runNetwork();

    SignedTransaction signedTx = future.get();
    signedTx.verifySignatures(b.getServices().getLegalIdentityKey());
}

@Test
public void signedTransactionReturnedByTheFlowIsSignedByTheAcceptor() throws Exception {
    IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity());
    ListenableFuture<SignedTransaction> future = a.getServices().startFlow(flow).getResultFuture();
    net.runNetwork();

    SignedTransaction signedTx = future.get();
    signedTx.verifySignatures(a.getServices().getLegalIdentityKey());
}

@Test
public void flowRecordsATransactionInBothPartiesVaults() throws Exception {
    IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity());
    ListenableFuture<SignedTransaction> future = a.getServices().startFlow(flow).getResultFuture();
    net.runNetwork();
    SignedTransaction signedTx = future.get();

    for (MockNode node : ImmutableList.of(a, b)) {
        assertEquals(signedTx, node.storage.getValidatedTransactions().getTransaction(signedTx.getId()));
    }
}

@Test
public void recordedTransactionHasNoInputsAndASingleOutputTheInputIOU() throws Exception {
    IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity());
    ListenableFuture<SignedTransaction> future = a.getServices().startFlow(flow).getResultFuture();
    net.runNetwork();
    SignedTransaction signedTx = future.get();

    for (MockNode node : ImmutableList.of(a, b)) {
        SignedTransaction recordedTx = node.storage.getValidatedTransactions().getTransaction(signedTx.getId());
        List<TransactionState<ContractState>> txOutputs = recordedTx.getTx().getOutputs();
        assert(txOutputs.size() == 1);

        IOUState recordedState = (IOUState) txOutputs.get(0).getData();
        assert(recordedState.getValue() == 1);
        assertEquals(recordedState.getSender(), a.info.getLegalIdentity());
        assertEquals(recordedState.getRecipient(), b.info.getLegalIdentity());
    }
}

Run these tests and make sure they all pass. If they do, its very likely that we have a working CorDapp.

Progress so far

We now have a flow that we can kick off on our node to completely automate the process of issuing an IOU onto the ledger. Under the hood, this flow takes the form of two communicating FlowLogic subclasses.

We now have a complete CorDapp, made up of:

  • The IOUState, representing IOUs on the ledger
  • The IOUContract, controlling the evolution of IOUState objects over time
  • The IOUFlow, which transforms the creation of a new IOU on the ledger into a push-button process

The final step is to spin up some nodes and test our CorDapp.