4

With contract keys there are two functions fetchByKey and lookupByKey, the latter of which allows me to handle negative lookups. I don't see a lookup : (Template t) => ContractId t -> Update (Optional t) function that does the same for contract Ids. Nor do I see a mechanism for try-catch that would allow me to handle failing fetch calls.

How do I avoid failing transactions without reimplementing the entire DAML logic client-side?

bame
  • 960
  • 5
  • 7

1 Answers1

6

There's quite a bit to unwrap in this question:

  1. Why is there no lookup?
  2. Why are there no try-catch blocks?
  3. Are failing transactions something to be avoided?
  4. 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.

bame
  • 960
  • 5
  • 7
  • Possibly worth noting that while try-catch does not work for the reasons discussed, there is no particular reason why DAML can't eventually support explicit try-throw-catch. Explicit exceptions do not require the same sort of validation or time-leak problems you have with catching liveness or contention errors. The DAML Ledger model describes a distributed system, and while it has excellent consensus properties due to its deterministic evaluation, there are still limits to what you can know about the state of any distributed system. – Recurse Sep 27 '19 at 09:51