There's quite a bit to unwrap in this question:
- Why is there no
lookup
?
- Why are there no try-catch blocks?
- Are failing transactions something to be avoided?
- How do I write ledger clients without replicating the contract logic?
lookupByKey
and the proposed lookup
functions allow submitters of transactions to make non-existence assertions about contracts. A None
result from lookupByKey
asserts that a particular key doesn't exist, a None
from lookup
would assert that a particular ContractId
doesn't exist. The big difference is that the contract key of a contract includes the key maintainers, the parties which validate lookups. Thus is is clear who has to authorise a lookupByKey
, and who validates the negative result. With lookup
, there are no parties associated to a ContractId
so there are no sensible authorisation or validation rules.
A weaker version would be fetchIfNotArchived : (Template t) => ContractId t -> Update (Optional t)
, which fails if the id is unknown and returns None
if the contract id is archived. DAML transactions are deterministic on current ledger state, which is currently the set of all active contracts. A function such as fetchIfNotArchived
would introduce a distinction between inexistent contracts and archived contracts in the DAML ledger model, and thus the ledger state would change from all active contracts to the entire ledger from the beginning. DAML ledgers could no longer be truncated and grow linearly over time, which is not desirable.
With try-catch blocks we run into very similar issues. Suppose I had some sort of try-catch. I could write this function:
lookup : (Template t) => ContractId t -> Update (Optional t)
lookup cid = do
try do
c <- fetch cid
return (Some c)
catch
return None
As explained above, there is nobody that could reasonably authorise or validate that the submitter followed the right path. So such a try-catch would have to be "unchecked", meaning the submitter can freely choose whether to go down the try
or the catch
path. You can already mock that up by passing the existence in:
maybeLookup : (Template t) => ContractId t -> Bool -> Update (Optional t)
maybeLookup cid cidIsKnown = if cidIsKnown
then do
c <- fetch cid
return (Some c)
else return None
This requires you to pass in cidIsKnown
from the outside world, which is annoying. One could imagine "unchecked queries" to help here. E.g. one could introduce a function uncheckedContractActive : (Template t) => ContractId t -> Update Bool
, which checks whether the contract is active on the submitter end, and then merely includes the Bool
in the transaction. It would be explicit that it's the submitter's free choice whether to run the transaction as if the cid exists, or the opposite.
Unchecked queries have so far not made it into DAML for the simple reason that they muddy the picture of what is shared, guaranteed, and validated logic, and what is the individual client's concern. In the current design, that split is exactly between DAML contracts and ledger clients.
Failing transactions are not actually that expensive as long as they fail on the submitter node during interpretation. The important thing is to design contract models and ledger clients in such a way that fetch
calls only ever fail because of race conditions, and there is not so much contention on contracts that transactions fail a lot. You can try
- Designing each automation process such that it keeps track of a "pending set", the set of contract IDs that it expects to be archived by commands it has in flight. This eliminates contention of each automation process with itself
- If you have two pieces of automation for the same party contending the same contract consistently, split the contract in two, or merge the automation in one
- If you have two pieces of automation for two parties contending the same contract, restructure the choices to make workflows more synchronous. Ie control in DAML who's turn it is
- If you have pieces of automation that sporadically do large batch operations which lead to contention with other automation, introduce some sort of "guard". E.g. write a
Lock
contract to ledger that indicates to the non-batch automation that it should pause, or protect choices with an Operation
contract which gets revoked when the batch processing starts. The latter has stronger guarantees, but adds overhead as it adds a fetch
to every choice.
Once you have reduced contention to an acceptable level, failing fetch
calls can be handled using retry logic in the ledger client.