Exception type for duplicate key errors

Hi @David_Martins, welcome to the forum!

TL;DR: You could make it work in some cases but you can’t catch all errors making this a pretty confusing feature. In addition to that contract key uniqueness is very tricky to implement in distributed settings which is why Canton does not provide uniqueness. So overall, I don’t think it makes sense to implement this.

Now to some code:
As a first attempt, we can try the following

exception DuplicateKey
  where
  message "DuplicateKey"

createWithKey : forall t k. (Template t, TemplateKey t k) => t -> Update (ContractId t)
createWithKey c = do
  r <- lookupByKey @t (key c)
  case r of
    None -> create c
    Some _ -> throw DuplicateKey

Looks easy enough right?

Now let’s use this in a somewhat silly example where we try to find the first key that does not yet exist and create a contract with that key:

template WithKey
  with
    p : Party
    v : Int
  where
    signatory p
    key this : WithKey
    maintainer key.p

template Helper
  with
    p : Party
  where
    signatory p
    choice FirstFree : ContractId WithKey
      controller p
      do firstFree p

firstFree p = go p 0

go p i =
  try createWithKey (WithKey p i)
  catch
    DuplicateKey -> go p (i + 1)

This works great as we can see in a simple example

test = do
  p <- allocateParty "p"
  submit p $ createAndExerciseCmd (Helper p) FirstFree
  submit p $ createAndExerciseCmd (Helper p) FirstFree

This will create contracts with key (p, 0) and (p, 1) exactly as we expect.

Now to make this example even sillier, let’s delegate the creation to another party:

  with
    p : Party
    o : Party
  where
    signatory p
    observer o
    nonconsuming choice FirstFreeDelegate : ContractId WithKey
      controller o
      do firstFree p

Seems to work great in a simple test script

testDelegate = do
  p <- allocateParty "p"
  o <- allocateParty "o"
  cid <- submit p $ createCmd (Delegate p o)
  submit o $ exerciseCmd cid FirstFreeDelegate

However, now let’s add another call to FirstFreeDelegate at the end to create a second contracts. Suddenly, things don’t look so good anymore

Scenario execution failed on commit at Main:67:3:
  Attempt to fetch, lookup or exercise a key associated with a contract not visible to the committer.

Oh no what happened?!

This is a subtlety when it comes to contract keys in Daml: The submitting party (o in this example), must be a stakeholder on the contract for lookupByKey to succeed. However, this is not the case here: p is the only stakeholder.

Now why do we have this seemingly arbitrary restriction?
The property we want to provide for contract keys is that assuming the key lookup is “allowed” (meaning well authorized and the submitter) is a stakeholder it can always succeed.
What happens if we drop the stakeholder restriction? Remember that privacy is a fundamental feature in Daml. If we do not have the stakeholder restriction then we get into a situation where it might be the case that a contract with the given key exists, however the participant you submit to does not (yet) know about this contract so it has no choice but to return a negative lookup. That leads to pretty confusing semantics around contract keys so we instead went for the additional restriction where we can always correctly resolve a key if you are a stakeholder and fail the submission otherwise.

Now to summarize, as seen above, you can implement something that handles duplicate keys in some form but it has some issues and doesn’t work as you might expect it to work. Adding to that the fact that we don’t support contract key uniqueness (and therefore duplicate key errors) in Canton, there are currently no plans to add an exception for this.

4 Likes