Corda flow responder (wrong) assumptions.
This is short article about some assumptions on Corda which Businesses tend to overlook. This is about Corda flow responder flows and how they work. It is intended for people who already know basics of Corda and the terminology.
First of all, let’s remind ourselves what are Corda flows and responder flows. According to https://docs.corda.net/key-concepts-flows.html
- Flows automate the process of agreeing ledger updates
- Communication between nodes only occurs in the context of these flows, and is point-to-point
- Built-in flows are provided to automate common tasks
We will concentrate on the second point. And again citation from the same article.
Nodes communicate by passing messages between flows. Each node has zero or more flow classes that are registered to respond to messages from a single other flow. Suppose Alice is a node on the network and wishes to agree a ledger update with Bob, another network node. To communicate with Bob, Alice must:
Start a flow that Bob is registered to respond to
Send Bob a message within the context of that flow
Bob will start its registered counterparty flow
Now that a connection is established, Alice and Bob can communicate to agree a ledger update by passing a series of messages back and forth, as prescribed by the flow steps.
As Corda is relatively new player in the blockchain world, it has to live in the context of other blockchain established terms and “thinking”. For example, one of such terms is “Contract”, which has different meanings in the Corda world and in the world of Etherum. This is first challenge a Business have to overcome, to understand, that “interesting” part of the Corda network happens on Flow level, where transactions are being prepared and inter-node negotiation happens, and state proposals are built.
Next challenge is to understand that Corda contracts are very important part of your business, as they are ones which will verify your transaction state transition. Reading through Corda documentation, one learns that R3 recommends to separate contracts and states in separate module .jar and these are very special .jars which are treated differently in Corda and network, as any change to this file will cause the business a bit of headache if one is in production already as some sort of contract upgrade operation have to be performed across the entire network. This is curse and blessing of Corda logic and it is intended so, that businesses can be sure, that there are no fishy things happening on the ledger.
But there are also Flows. And here are the shaky ground, which is not shaky if a business plays by the R3 rules, but this tend to be overlooked.
Classical approach to the application and CorDapp building is built a single project with several modules, where one would be contracts-states containing your “core business rules” and that module will be somewhat guarded against tampering. Other modules are more fluid (if they don’t contain any contracts) and can be updated and redeployed without much of the pain. These modules tend to contain Flow implementations. The risk what I think is overlooked by businesses is that application built usually is driven by some central party — bank or insurance company and “central thinking” might kick in. This risk is assumption, that the counter parties of your network will be running YOUR version of “other modules”. Application designers build the project in a such a way as to assume, that counterparty will deploy the flows with corresponding responder flows. So that central party is initiator and responder is your client, which deploys your version of responder flow and everything should work.
This is not true. By the very design, responder flows are for your clients to implement. Application designers could and should not make any assumptions about responder logic as by very design of Corda, these are parts of the network which your counterparty should be allowed to control. All you can control is your initiator flows, and responder flows responding on flows initiated by counterparties. You can’t trust that responder will perform all the checks and balances you intended on your side, so if it’s important, check it yourself in your part of the flow.
Of course, next natural place to put at least some part of your flows is to put them along your contracts. Then the flow logic is expected to be “locked” for your application, and one can’t upgrade and change them without consent from counterparties (or explicitly whitelisting, depending on contract constraints). This might work for initiator flows, but for responder flows this is not the wisest decision as it will complicate application deployment, build and upgrade. It is more likely that your developers made a mistake or missed a business rule in Flow than in Contract. And increasing “moving parts” around your contract .jar means that headache for the business when upgrade is required. What is more important, your responder flows can’t be locked in this way, so that your counterparty can’t override the logic written in the contract.
Here is example of contract-states flow
@StartableByRPC
@InitiatingFlow
class ExperimentalFlow:FlowLogic<Unit>() {
companion object {
object SENDING : ProgressTracker.Step("SendingData")
}
override val progressTracker = ProgressTracker(SENDING)
@Suspendable
override fun call() {
val counterParty = serviceHub.identityService.wellKnownPartyFromX500Name(
CordaX500Name(
organisation = "Client",
locality = "London",
country = "GB")) ?: throw IllegalStateException("Can't find Client party")
val session = initiateFlow(counterParty)
session.sendAndReceive<String>(42).unwrap {
println("Experimental Flow received $it")
}
}
}
@InitiatedBy(ExperimentalFlow::class)
class ExperimentalFlowResponder(val counterParty:FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val receivedValue = counterParty.receive<Int>().unwrap { it }
println("Responder Received $receivedValue")
counterParty.send("Hey I am legal responder, you sent me $receivedValue which makes me happy")
}
}
As a business will expect, that this is the responder flow which will be invoked by Initiator. Notice that ExperimentalFlowResponder is not open class, which means, that no-one can override this flow and leverage Corda platform logic for responder insatllation.
But here is the code in the client module, which does exactly that. And remember, that client module will have installed contract-states module just as initiator module with the legal version of the experimental flow responder.
@InitiatedBy(ExperimentalFlow::class)
abstract class ExperimentalFlowResponder(val counterParty: FlowSession) : FlowLogic<Unit>()
@InitiatedBy(ExperimentalFlow::class)
open class IllegalExperimentalFlowResponder(counterParty: FlowSession) : ExperimentalFlowResponder(counterParty) {
@Suspendable
override fun call() {
val receivedValue = counterParty.receive<Int>().unwrap { it }
println("Responder Received $receivedValue")
counterParty.send("Hey I am ILLEGAL, you sent me $receivedValue and I will try to HACK")
}
}
And here is the console output of running the flows
Mon Jan 14 11:11:15 EET 2019>>> flow start Experimental
Experimental Flow received Hey I am ILLEGAL, you sent me 42 and I will try to HACK
Done
What have we done. Corda platform will always install the responder, which is most “further” from the FlowLogic on type hierarchy, and this is why we defined abstract ExperimentalFlowResponder in the client corDapp to make it “open” so we can extend it by our IllegalExperimentalFlowResponder which now is by one hop further from FlowLogic type hierarchy, and Corda platform will pick this responder instead of one defined in the contracts and states.
Note: for this to work, the client module responder flow package have to match the one in the contract-states module.
And that’s it.
The lesson for business is
Do not assume anything about the content of the counterparty responder flows as you can’t control and should not control, what the counterparty installs.
On the bright note: it is good practice and still it is wise to provide default reference implementation of expected flow logic, as most likely your clients will opt for default “bank” implementation. And if you implement this as open class, thus you will give the client clean way to extend the logic you provide out of box.