Corda - Designing Secure Contracts - corda

Recently I've been looking into the security and vulnerability aspects of contract commands in Corda. A debate arose as to whether some contract command constraints should be strict or whether they should be relaxed in order to allow transaction composition of different inputs, outputs and commands.
The problem is that whilst I can see the benefit of allowing transaction composition, I feel like relaxed contract command constraints actually pose security vulnerabilities, and in my opinion, it would be better to secure against these vulnerabilities at the contract level, in such that the signing participants of the command reach consensus through contract verification as a whole, rather than relying on flow level checks, which could be overlooked by a developer or circumvented by a malicious node.
Example - Bankruptcy Declaration
This example allows nodes on the network to declare bankruptcy. Assume in this case that a bankruptcy declaration state is simply the identity of the node declaring bankruptcy, and a reason.
#BelongsToContract(BankruptcyDeclarationContract::class)
data class BankruptcyDeclarationState(
override val owner: AbstractParty,
val reason: String
) : OwnableState { ... }
Strict Verification
Strict verification requires that, on issuance...
Zero input states must be consumed.
One output state must be created.
Only the owner must sign.
fun verifyIssue(tx: LedgerTransaction, signers: Set<PublicKey>) = requireThat {
"Zero input states must be consumed." using (tx.inputs.isEmpty())
"One output state must be created." using (tx.outputs.size == 1)
val state = tx.outputsOfType<BankruptcyDeclarationState>().single()
"Only the owner must sign." using (state.owner.owningKey == signers.single())
}
Relaxed Verification
Relaxed verification requires that, on issuance...
Zero input states of type BankruptcyDeclarationState must be consumed.
One output state of type BankruptcyDeclarationState must be created.
Only the owner must sign.
fun verifyIssue(tx: LedgerTransaction, signers: Set<PublicKey>) = requireThat {
val inputs = tx.inputsOfType<BankruptcyDeclarationState>()
val outputs = tx.outputsOfType<BankruptcyDeclarationState>()
"Zero input states of type BankruptcyDeclarationState must be consumed." using
(inputs.isEmpty())
"One output state of type BankruptcyDeclarationState must be created." using
(outputs.size == 1)
"Only the owner must sign." using (outputs.single().owner.owningKey == signers.single())
}
Observations
Strict verification ensures that the inputs and outputs are checked globally, rather than checking for specific input and output types, however this has a disadvantage that transaction composition of inputs and outputs is impossible.
Relaxed verification ensures that only inputs and outputs of the required state type are checked, which would allow transaction composition of different input and output types.
The key here is that only the node declaring bankruptcy must sign, which means that issuance of a BankruptcyDeclarationState can only occur from that node. Nobody else should be allowed to declare bankruptcy on behalf of another node on the network.
Identifying The Vulnerability
Suppose we chose to model our contract command constraints to be relaxed, so that we can compose transactions. Also, suppose that we have a contract command for some ObligationState which when issued, requires that:
Zero input states of type ObligationState must be consumed.
One output state of type ObligationState must be created.
The obligor and obligee must sign.
Now that we have two state types and two contract commands, we can compose a transaction that uses both, and identify the vulnerability. Assume here that bob is initiating this transaction.
val transaction = with(TransactionBuilder(notary)) {
addOutputState(ObligationState(alice, bob), ObligationContract.ID)
addCommand(ObligationContract.Issue(), aliceKey, bobKey)
addOutputState(BankruptcyDeclarationState(alice, "..."), BankruptcyDeclarationContract.ID)
addCommand(BankruptcyDeclarationContract.Issue(), aliceKey)
}
Remember that only the owner of a BankruptcyDeclarationState must sign, but the obligor and obligee of an ObligationState must sign, therefore this initiating flow will collect signatures from the required counter-parties. The vulnerability here is that bob initiates this transaction, but includes an output of type BankruptcyDeclarationState which is owned by alice. He shouldn't be allowed to do that because only the owner should be allowed to issue BankruptcyDeclarationState but in this case alice will unwittingly sign because of the requirement to sign for the ObligationState.
There is an argument to be made here that the flows could be written in such a way that alice would check the transaction before signing to ensure that certain states were not included, but I do not feel like this is enough. This requires developers and node administrators to carry out due diligence of flows ensuring their security.
In contrast, strict contract command constraints would prevent these vulnerabilities in what I believe to be a much more secure way - due diligence is therefore only required at a contract level, rather than from every developer writing flows that consume the contracts.
What I am looking for in this respect is some definitive guide to whether contract command constraints should be strict, relaxed, or whether there are other considerations to be made that I've missed. Thanks.

As you pointed out correctly, the identical contract code is shared among all transacting parties. That is the only agreement among them.
But each party is responsible for his/her action (signing) by developing his/her own secure flows. The fundamental in writing flows is to verify the transaction against the contract code before signing. Who would sign anything digitally or otherwise without reading/checking the contract?
Did I miss anything?

Related

Can I have an account on a node and trade with that account without the state staying on the account node?

I'm working with corda accounts.
In my scenario the account is created on node M and shared with node D.
Node D runs a state creation flow, where the account is a participant.
By modeling the solution, the transaction must be registered on node D, but not on node M.
The problem is that when using the account belonging to node M, session of node M is required. And when I don't execute a ReceiveFinalityFlow on Responder Flow, an UnexpectedFlowEndException exception is generated.
And I need to be able to make a vaultquery through accountId.
The question is, can I have an account on a node and trade with that account without the state staying on the account node?
FinalityFlow will throw an error if you don't provide a FlowSession for each participant (see here), and in your case the account is a participant; so you need to provide a FlowSession for node M.
Since you pass a FlowSession for node M, then there should be a responder flow where node M calls ReceiveFinalityFlow; otherwise your initiator flow will hang because FinalityFlow will execute a send() to send the transaction to M, while M doesn't have a receive() call (which ReceiveFinalityFlow executes).
You can achieve the requirement that you're asking for by calling ReceiveFinalityFlow and set the input parameter statesToRecord to NONE; by default, that parameter is set to ONLY_RELEVANT (see flow definition here). The various types of StatesToRecord are explained here.
Your responder flow must have an if statement, if getOurIdentity() is node M, then call ReceiveFinalityFlow with statesToRecord == NONE (because you don't want M to record the state), if it's node D then call ReceiveFinalityFlow with statesToRecord == RELEVANT (because you want D to record the state).
Please note that just because you wrote a responder a certain way, doesn't guarantee that node M will execute your version; writing a responder flow is usually the responsibility of the other node; their developers can write their own version of the responder in which they call ReceiveFinalityFlow with statesToRecord == RELEVANT (meaning node M will register the resulting state). Read the first This is not true in this article.
Once you implement the above, please write a flow test that checks that node M:
Didn't register the resulting transaction in its transaction storage
Didn't register the resulting state in its vault
The reason I'm asking you to do this is because I noticed in Corda's code the following:
ReceiveFinalityFlow calls ReceiveTransactionFlow here
ReceiveTransactionFlow calls ResolveTransactionFlow here
ResolveTransactionFlow overrides statesToRecord from NONE to RELEVANT here and this statement has me worried; I just want to make sure that when you set statesToRecord to NONE in ReceiveFinalityFlow for node M, it doesn't record the transaction or the state
Let me know how things go.
Also to query by account, read in my article the below 2 sections:
Search for Finally, how do you query the vault by account?
Also read This is very important!

Corda oracles verification

I'm trying to understand how corda oracles work from an example on github. It seems like in every example oracle verification function checks the data in command and data in output state. I don't understand why that should work because we (issuer node) manage that data and put it in command/output state.
// Our contract does not check that the Nth prime is correct. Instead, it checks that the
// information in the command and state match.
override fun verify(tx: LedgerTransaction) = requireThat {
"There are no inputs" using (tx.inputs.isEmpty())
val output = tx.outputsOfType<PrimeState>().single()
val command = tx.commands.requireSingleCommand<Create>().value
"The prime in the output does not match the prime in the command." using
(command.n == output.n && command.nthPrime == output.nthPrime)
}
In this example state gets Nth prime number from oracle but after it's issued the verification function doesn't rerun generateNth prime function to make sure that this number is really the one we needed. I understand that data in this example is deterministic since Nth prime cannot change but what about the case where we have dynamic data like stock values? Shouldn't oracle verification function also send another http request and get current values to check them?
Firstly, note that contracts in Corda are not able to access the outside world in any way (DB reads, HTTP requests, etc.). If they could, transaction validity would be non-deterministic. A transaction that is found to be valid on day n may become invalid on day n+1 (because a database row changed, or a website went down, etc.). This would cause disagreements about whether a given transaction was a valid ledger update.
However, we sometimes need a transaction to include external data for verification (whether a company is bankrupt, whether a natural catastrophe happened, etc.). To do this, we use a trusted oracle that only signs the transaction if a given piece of data is valid.
We could embed the information in the input or output states. However, this would require us to reveal the entire input or output state to the oracle for signing. For privacy reasons, it is therefore preferable to embed the data in a command that only contains the data of interest to the oracle, so that we can filter out all the other parts of the transaction and only present this command to the oracle for signing.
The oracle will usually perform a DB read or make an HTTP request to check the validity of the data before signing.

Doesn't the contract need to enforce that Oracle has signed the Fix command in IRS example?

I observed that normally in a typical contract we have something similar to
"The signers should be the participants for the state" using
(command.signers.toSet() == state.participants.map { it.owningKey }.toSet())
where we enforce if this specific command is used, the required signers are equal to the participants (assuming participants of the deal are always mandatory to sign) so nodes don't maliciously change the flow to sidestep getting signatures from some parties.
In the IRS demo on Oracle (RateFixFlow/NodeInterestRate), the State has the participants and oracle parties involved. There's also a verifyFixCommand that verifies some rules are met whenever a Fix command was used to get the rates from the Oracle.
Why doesn't the command also enforce that participants must have signed - plus the oracle must sign as well to guarantee the Fix rates was validated by the Oracle? Else why have the flow get the oracle's signature at all. What's stopping the initiator from bypassing getting the oracle signature in the first place.
Was there any rationale behind not putting a similar rule from the below in? i.e
"The signers should be the participants and oracle for the state" using
(command.signers.toSet() ==
(state.participants.map { it.owningKey } + state.oracle.owningKey).toSet())
This is an oversight. The contract's verify method should check the required signers.
It would be appreciated if you could create a PR to amend this. The contribution guides are here: https://docs.corda.net/contributing.html.

In a Corda transaction are particular state transitions associated with partcular commands?

The commands webinar states (at around 3:10) that "input and output states are always grouped by type and that a command is required for each group." The narrator seems to imply that if a transaction consists of multiple commands then each command will be associated with a distinct subset of the state transitions proposed in the transaction.
However such a view of things does not seem to be captured in the structure of LedgerTransaction. It consists of completely independent lists of inputs, outputs and commands. There's nothing denoting an association between particular commands and particular input or output states.
In my contract code I can group states by type, e.g.:
override fun verify(tx: LedgerTransaction) {
val fooStates = tx.inputsOfType<FooState>()
val barStates = tx.inputsOfType<BarStates>()
But I'm just grouping by choice - I don't have to, and there's nothing tying these groups to particular commands.
So what is the webinar referring to when it says "a command is required for each group"?
The sense behind signatures being associated with commands would be clear if the relationship between commands and state transitions existed as described in the webinar. However in reality one doesn't sign off on particular transitions on a per command basis as the LedgerTransaction class does not capture such relationships.
In the key concepts section on commands one has a coupon command and a pay command and it makes sense that the set of people who have to sign off on the bond state transition may be different to those who need to sign off on the cash state transition. But in the code there's nothing tying a coupon command to the particular states that the signers associated with the command are agreeing to if they sign.
Is this stated requirement that each group must have an associated command just something that the developer should implement in their contract verify logic without being something that one tries to capture in the structure of transactions?
Good question.
You touched on grouping within the contract, and that's correct it is down to the contract implementation, you just need to extend it further to enforce which parties are required to sign depending on the command in the transaction.
So, your verify function might look like the below simplified version of the contract within the Option CorDapp sample:
override fun verify(tx: LedgerTransaction) {
val command = tx.commands.requireSingleCommand<Commands>()
when (command.value) {
is Commands.Issue -> {
requireThat {
val cashInputs = tx.inputsOfType<Cash.State>()
val cashOutputs = tx.outputsOfType<Cash.State>()
"Cash.State inputs are consumed" using (cashInputs.isNotEmpty())
"Cash.State outputs are created" using (cashOutputs.isNotEmpty())
val option = tx.outputsOfType<OptionState>().single()
"The issue command requires the issuer's signature" using (option.issuer.owningKey in command.signers)
}
}
is Commands.Trade -> {
requireThat {
val cashInputs = tx.inputsOfType<Cash.State>()
val cashOutputs = tx.outputsOfType<Cash.State>()
"Cash.State inputs are consumed" using (cashInputs.isNotEmpty())
"Cash.State outputs are created" using (cashOutputs.isNotEmpty())
val inputOption = tx.inputsOfType<OptionState>().single()
val outputOption = tx.outputsOfType<OptionState>().single()
"The transfer command requires the old owner's signature" using (inputOption.owner.owningKey in command.signers)
"The transfer command requires the new owner's signature" using (outputOption.owner.owningKey in command.signers)
}
}
else -> throw IllegalArgumentException("Unknown command.")
}
}
We first pull the command (or commands) from the transaction and that gives us context.
Based on that we can then pull the states we're interested in from either the inputs or outputs e.g. cash/option and begin to check that our constraints are met.
You can find the full version of the above sample at https://github.com/CaisR3/cordapp-option and the contract code can be found in base\src\main\kotlin\net\corda\option\base\contract\OptionContract.kt

Where to check contract constraints

From release-M13 of Corda, in the CordApp-Tutorial example, there are some constraints checks made within the flow itself (ExampleFlow.Acceptor). My question is, what constraints may I check on the flow, and what constraints in the Contract? Or it's just an organization issue?
This is a great question. I believe you are referring to:
#InitiatedBy(Initiator::class)
class Acceptor(val otherParty: Party) : FlowLogic<SignedTransaction>() {
#Suspendable
override fun call(): SignedTransaction {
val signTransactionFlow = object : SignTransactionFlow(otherParty) {
override fun checkTransaction(stx: SignedTransaction) = requireThat {
val output = stx.tx.outputs.single().data
"This must be an IOU transaction." using (output is IOUState)
val iou = output as IOUState
"The IOU's value can't be too high." using (iou.iou.value < 100)
}
}
return subFlow(signTransactionFlow)
}
}
The CollectSignaturesFlow and its counterpart, the SignTransactionFlow automates the collection of signatures for any type of transaction. This automation is super useful because developers don't have to manually write flows for signature collection anymore! However, developers have to be aware that given any valid transaction - as per the referenced contract code in the transaction - the counter-party will always sign! This is because transactions are validated in isolation, not relative to some expected external values.
Let me provide two examples:
If I have access to one of your unspent cash states from a previous transaction, then perhaps I could create a cash spend transaction (from you to me) and ask you to sign it via the CollectSignaturesFlow. If the transaction is valid then, without any additional checking, you'll sign it which will result in you sending me the cash. Clearly you don't want this!
The contract code can only go part of the way to verifying a transaction. If you want to check that the transaction represents a deal you want to enter into e.g. price < some amount then you'll have to do some additional checking. The contract code cannot opine on what constitutes a commercially viable deal for you. This checking has to be done as part of the SignTransactionFlow by overriding signTransaction
In a production CorDapp, one may wish to defer to human judgement on whether to sign a transaction and enter into a deal. Or alternatively, this process could be automated by reaching out to some external reference data system via HTTP API or MQ to determine if the deal is one that should be entered into.
In the code example above, we added two simple constraints:
One which prevents a borrower from creating an overly large (greater than 100) IOU state
One which ensures the transaction does indeed contain an IOU state and not some other state we are not expecting
Note that these two constraints cannot be placed inside the contract code. Contract code is more appropriate for defining the constraints that govern how an asset or agreement should evolve over time. For example, with regards to an IOU:
Issuances must be signed by lender and borrower
Issuances must be for a value greater than zero
Redemptions must involve a cash payment of the correct currency
The IOU must be redeemed before the expiry date
More examples available here: https://github.com/roger3cev/iou-cordapp-v2/blob/master/src/main/kotlin/net/corda/iou/contract/IOUContract.kt
Remember, Corda is designed for potentially mutually distrusting parties to enter into consensus about shared facts. As such, nodes cannot implicitly trust what they receive over the wire from their counter-parties, therefore we always have to check what we receive is what we expect to receive.
Hope that makes sense!

Resources