I am trying to use trigger to pass information from one DAML contract to another, like this:
template Bar
with
owner: Party
someInfo: Int
state: Bar_State
where
signatory owner
controller owner can
Bar_Accept : ContractId Bar
do
create BarTrigger with info=someInfo
create this with state=BarState_Accepted
Bar_Reject : ContractId Bar
do
create this with state=BarState_Rejected
template BarTrigger
with
owner: Party
info: Int
where
signatory owner
controller owner can
BarTrigger_Consume: ()
do pure()
The state transition of Bar will trigger the execution of DoSomething, which is defined as a choice of some other contract in another file.
triggerRule : Party -> TriggerA () ()
triggerRule _party = do
triggers <- query @BarTrigger
forA_ triggers $ \(cid, c) -> do
if (c.owner == _party)
then do
debug ((show c.owner) <> " do something with " <> "info=" <> (show c.info))
dedupExercise someCid DoSomething with info=c.info
dedupExercise cid BarTrigger_Consume
else pure()
The code works if I only have one trigger generated. If I have multiple triggers (from multiple contracts generated from the template Bar), I get this error:
Trigger is running as alice (context: {triggerDefinition=75c51e68bcce2f98be4ce858be6ad37b26b7bb55277440fa07b61a6295434ec7:Keplaax.Triggers.SellerTrigger:trigger})
[unknown source]: "'alice' do something with info=2"
[unknown source]: "'alice' do something with info=1"
[unknown source]: "'alice' do something with info=1"
Command failed: Inconsistent: DuplicateKey: Contract Key not unique, code: 3 (context: {triggerDefinition=75c51e68bcce2f98be4ce858be6ad37b26b7bb55277440fa07b61a6295434ec7:Keplaax.Triggers.SellerTrigger:trigger})
I guess I am doing something stupid in DoSomething.
I am seeking for advice on how to debug DoSomething with Daml navigator (I guess I have to because I am using trigger?)… I added debug into DoSomething but I am not sure where the trace go.
b1Cid <- submit alice do
create Bar with owner=alice, someInfo=1, state=BarState_Created
b2Cid <- submit alice do
create Bar with owner=alice, someInfo=2, state=BarState_Created
submit alice do exercise b1Cid Bar_Accept
submit alice do exercise b2Cid Bar_Accept
I would get
Trigger is running as alice (context: {triggerDefinition=75c51e68bcce2f98be4ce858be6ad37b26b7bb55277440fa07b61a6295434ec7:Keplaax.Triggers.SellerTrigger:trigger})
[unknown source]: "'alice' do something with info=2"
[unknown source]: "'alice' do something with info=1"
[unknown source]: "'alice' do something with info=1"
Command failed: Inconsistent: DuplicateKey: Contract Key not unique, code: 3 (context: {triggerDefinition=75c51e68bcce2f98be4ce858be6ad37b26b7bb55277440fa07b61a6295434ec7:Keplaax.Triggers.SellerTrigger:trigger})
I would get different result if I do not use the script to exercise Bar_Accept, I would get the expected result like this
(click Bar_Accept from Daml navigator for b1Cid)
[unknown source]: "'alice' do something with info=1"
(click Bar_Accept from Daml navigator for b2Cid)
[unknown source]: "'alice' do something with info=2"
It’s hard to be more precise without the full code, but the error message means the trigger is trying to create a contract with a key that already exists.
DAML enforces key uniqueness: at any point in time, there can only ever be one active contract with a given key. Before you can create another contract with that key, you have to archive the existing one. You may do both in a single transaction, such that other observers never see a state of the ledger where there is no contract for the given key; in that way, contract keys provide a notion of a mutable reference to immutable values.
It is likely there is a deeper concurrency bug in the code that only appears when using triggers simply because of the time it takes to manually click. A simple, immediate fix would be to add a check to your creation code and archive the existing contract if there is one. Here is a small, self-contained example of how to do that:
import Daml.Script
template Asset
with
issuer: Party
message: Text
where
key issuer: Party
maintainer key
signatory issuer
template AssetUpdater
with
c: Party
where
signatory c
preconsuming choice UpdateMessage : ContractId Asset
with
newMsg: Text
controller c
do
alreadyExists <- visibleByKey @Asset c
if alreadyExists then do
(cid, _) <- fetchByKey @Asset c
archive cid
create Asset with issuer = c, message = newMsg
else do
create Asset with issuer = c, message = newMsg
setup : Script ()
setup = script do
alice <- allocatePartyWithHint "Alice" (PartyIdHint "Alice")
_ <- submit alice do
createCmd Asset with
issuer = alice
message = "1"
_ <- submit alice do
createAndExerciseCmd (AssetUpdater alice) (UpdateMessage "2")
pure ()
However, I can’t stress this enough: there is likely something wrong with the design around the use of keys and/or the concurrency semantics of the contracts, so blindly applying the above may just result in a silently broken model instead of a loudly broken one, which is strictly worse.
Hi Gary, thanks a lot of your reply. I fully understand that the problem probably lines in my code. What I am looking for is coding style or best practice that would make bugs easier to locate… after all, bugs are part of life and they are unavoidable.
Your suggestion on adding preconsuming check into the template is an exactly what I am looking for. Now I can add these checks and when the “Duplicate Key” error happens again, I would know where it comes from.
One thing that still puzzle me, is the duplication of the second contract (info=1):
I checked from Daml Navigator that only 2 contracts are created from the template BarTrigger.
I added some more debug to the trigger:
triggerRule _party = do
triggers <- query @BarTrigger
debug $ " === " <> (show triggers)
forA_ triggers $ \(cid, c) -> do
when (c.owner == _party) do
debug $ "Archive the trigger with info=" <> (show c.info))
dedupExercise cid BarTrigger_Consume
I got:
[unknown source]: " === [(<contract-id>,BarTrigger {owner = 'alice', someInfo = 2}),(<contract-id>,BarTrigger {owner = 'alice', someInfo = 1})]"
[unknown source]: "Archive the trigger with info=2"
[unknown source]: "Archive the trigger with info=1"
[unknown source]: " === [(<contract-id>,BarTrigger {owner = 'alice', someInfo = 1})]"
[unknown source]: "Archive the trigger with info=1"
[unknown source]: " === []"
I expect both triggers to be archived after a single iteration of the for loop, but look like only one archive has actually happened.
That would explain why I got DuplicateKey, because in doSomething I am creating a new contract with with the someInfo, and the key of these contracts is (owner, someInfo).
Given the observation that not all contracts are archived in a for loop, I rewrite the trigger:
triggerRule _party = do
triggers <- query @BarTrigger
filteredTriggers = filter (\(_, c) -> c.owner == _party) triggers
unless (null filteredTriggers) do
debug ((show c.owner) <> " do something with " <> "info=" <> (show c.info)
dedupExercise someCid DoSomething with info=c.info
dedupExercise cid BarTrigger_Consume
I am still seeing this debug messages:
[unknown source]: "'alice' do something with info=2"
[unknown source]: "'alice' do something with info=1"
[unknown source]: "'alice' do something with info=1"
But I don’t see the DuplicateKey error any more. I got 2 new contracts created from DoSomething, and this is what I want.