This documents DLT anti-patterns, their drawbacks and more robust ways of achieving the same outcome.
Applications often need to orchestrate calculations at specific times or in a long-running sequence of steps. Examples are:
- Committing assets to a settlement cycle at 10:00 am
- Starting a netting calculation after trade registration has finished
- Triggering the optimization of a portfolio
At first, creating a contract triggering this request might seem convenient:
template OptimizePortfolio with self: Party where signatory self
However, this is a case of using a database [ledger] for interprocess communication. This contract is a computational request from the orchestration unit to a particular program. But the ledger represents the legal rights and obligations associated with a business process: computational requests are a separate concern and shouldn’t be mixed into this. Having them on-ledger has the following drawbacks:
- Code bloat in shared models: introduces more things which need to be agreed upon
- Limited ability to send complicated requests since they first have to be projected into smart contracts
- High latency since intermediate variables have to be committed to the ledger
- Changing the orchestration of a production system has a very high barrier since it may require DAML model upgrades
- Orchestration contracts have no business meaning and contaminate the ledger holding business-oriented legal rights and obligations
Instead, lightweight remote procedure calls (RPC) would be more appropriate. A system designer can consider triggering the application waiting to execute a task with RPC mechanism like:
- An HTTP request
- A general message bus
- A scheduler starting the calculation at a specific time
Notification contracts, which draw a line in the sand and have a real business meaning, don’t fall under this categorization. These are persistent contracts with real meaning to the business process and not an ephemeral computational request as described above.
The DLT domain lends itself to race conditions. How? Multiple parties are concurrently updating shared resources (contracts). Here’s an example that’s vulnerable to race conditions: a DvP where a payer allocates their asset, a receiver has to allocate their cash and then an operator does the final settlement.
template DvP with operator: Party payer: Party receiver: Party assetCid: Optional (ContractId Asset) cashIouCid: Optional (ContractId CashIou) -- controller payer can PayerAllocate: ContractId DvP -- controller receiver can ReceiverAllocate: ContractId DvP -- controller operator can Settle: (ContractId Asset, ContractId CashIou)
If the payer and receiver react to the creation of this contract and try to exercise their respective choices, one will succeed and the other will result in an attempted double-spend. Double-spends create additional work on the system because when an exception is returned, a new command needs to be subsequently generated and reprocessed. In addition, the application developer has to implement careful error handling associated with the failed command submission. It should be everyone’s goal to write double-spend free code as needless exceptions dirty logs and can be a distraction when debugging other problems.
To write your code in a way that avoids race conditions, you should explicitly break up the updating of the state into a workflow of contracts which collect up information from each participant and is deterministic in execution. For the above example, deterministic execution can be achieved by refactoring the DvP into three templates:
DvPRequestcreated by the operator, which only has a choice for the payer to allocate.
DvPwhich is the result of the previous step and only has a choice for the receiver to allocate.
SettlementInstructionwhich is the result of the previous step. It has all the information required for settlement and can be advanced by the operator
Alternatively, if asynchronicity is required, the workflow can be broken up as follows:
- Create a
PayerAllocationcontract to collect up the
- Create a
ReceiverAllocationcontract to collect up the
- Have the
Settlechoice on the
DvPwhich takes the previous two contracts as arguments.
When orchestrating the processing of an obligation, the obligation may go through a set of states. The simplest example is locking an asset where the states are locked versus unlocked. A more complex example is the states of insurance claim:
- Claim Requested
- Cleared Fraud Detection
- Sent for Payment
Initially, it might seem that a convenient way to represent this is with a status variable like below:
data ObligationStatus = ClaimRequested | ClearedFraudDetection | Approved | SentForPayment deriving (Eq, Show) template Obligation with insuranceUnderwriter: Party claimer: Party status : ObligationStatus
Instead, you can break up the obligation into separate contracts for each of the different states.
template ClaimRequest with insuranceUnderwriter: Party claimer: Party template ClaimClearedFraudDetection with insuranceUnderwriter: Party claimer: Party
The drawbacks of maintaining status variables in contracts are:
- It is harder to understand the state of the ledger since you have to inspect contracts
- More complex application code is required since it has to condition on the state the contract
- Within the contract code, having many choices on a contract can make it ambiguous as to how to advance the workflow forward
- The contract code can become complex supporting all the various way to update its internal state
- Information can be leaked to parties who are not involved in the exercising of a choice
- It is harder to update the ledger/models/application if a new state is introduced
- Increased error checking code required to verify the state transitions are correct
- Makes the code harder to reason about
By breaking the contract up and removing the status variable, it eliminates the above drawbacks and makes the system transparent in its state and how to evolve forward.