Contract keys¶
Contract keys are an optional addition to templates. They let you specify a way of uniquely identifying contract instances, using the parameters to the template - similar to a primary key for a database.
You can use contract keys to stably refer to a contract, even through iterations of instances of it.
Here’s an example of setting up a contract key for a bank account, to act as a bank account ID:
type AccountKey = (Party, Text)
template Account with
bank : Party
number : Text
owner : Party
balance : Decimal
observers : [Party]
where
signatory [bank, owner]
observer observers
key (bank, number) : AccountKey
maintainer key._1
What can be a contract key¶
The key can be an arbitrary serializable expression that does not contain contract IDs. However, it must include every party that you want to use as a maintainer
(see Specifying maintainers below).
It’s best to use simple types for your keys like Text
or Int
, rather than a list or more complex type.
Specifying maintainers¶
If you specify a contract key for a template, you must also specify a maintainer
or maintainers, in a similar way to specifying signatories or observers. The maintainers “own” the key in the same way the signatories “own” a contract. Just like signatories of contracts prevent double spends or use of false contract data, maintainers of keys prevent double allocation or incorrect lookups. Since the key is part of the contract, the maintainers must be signatories of the contract. However, maintainers are computed from the key
instead of the template arguments. In the example above, the bank
is ultimately the maintainer of the key.
Uniqueness of keys is guaranteed per template. Since multiple templates may use the same key type, some key-related function must be annotated using the @ContractType
as shown in the examples below.
When you are writing DAML models, the maintainers matter since they affect authorization – much like signatories and observers. You don’t need to do anything to “maintain” the keys. In the above example, it is guaranteed, there there can only be one Account
with a given number
at a given bank
.
Checking of the keys is done automatically at execution time, by the DAML exeuction engine: if someone tries to create a new contract that duplicates an existing contract key, the execution engine will cause that creation to fail.
Contract Lookups¶
The primary purpose of contract keys is to provide a stable, and possibly meaningful, identifier for contracts that can be used in DAML to fetch contracts. There are two functions to perform such lookups: fetchByKey and lookupByKey. Both types of lookup are performed at interpretation time on the submitting Partipant Node, on a best-effort basis. Currently, that best-effort means lookups only return contracts if the submitting Party is a stakeholder of that contract.
In particular, the above means that if multiple commands are submitted simultaneously, all using contract lookups to find and consume a given contract, there will be contention between these commands, and at most one will succeed.
fetchByKey¶
(fetchedContractId, fetchedContract) <- fetchByKey @ContractType contractKey
Use fetchByKey
to fetch the ID and data of the contract with the specified key. It is an alternative to fetch
and behaves the same in most ways.
It returns a tuple of the ID and the contract object (containing all its data).
Like fetch
, fetchByKey
needs to be authorized by at least one stakeholder.
fetchByKey
fails and aborts the transaction if:
- The submitting Party is not a stakeholder on a contract with the given key, or
- A contract was found, but the
fetchByKey
violates the authorization rule, meaning no stakeholder authorized thefetch
..
This means that if it fails, it doesn’t guarantee that a contract with that key doesn’t exist, just that the submitting Party doesn’t know about it, or there are issues with authorization.
lookupByKey¶
optionalContractId <- lookupByKey @ContractType contractKey
Use lookupByKey
to check whether a contract with the specified key exists. If it does exist, lookupByKey
returns the Some contractId
, where contractId
is the ID of the contract; otherwise, it returns None
.
lookupByKey
needs to be authorized by all maintainers of the contract you are trying to lookup. The reason for that is denial of service protection. Without this restriction, a malicious Party could legitimately request another Party to validate any number of negative contract key lookups without that other Party ever having signed anything.
Unlike fetchByKey
, the transaction does not fail if no contract with the given key is found. Instead, lookupByKey
just returns None
. However, that does not guarantee that no such contract exists. The submitting Party may simply not know about it, in which case the transaction will be rejected during validation.
lookupByKey
fails and aborts the transaction if:
- Authorization from at least one maintainer is missing. This check fails at interpretation time.
- The lookup is incorrect. This can happen either due to contention, or because the submitter didn’t know of the contract. This check fails at validation time.
To get the data from the contract once you’ve confirmed it exists, you’ll still need to use fetch
.
exerciseByKey¶
exerciseByKey @ContractType contractKey
Use exerciseByKey
to exercise a choice on a contract identified by its key
(compared to exercise
, which lets you exercise a contract identified by its ContractId
). To run exerciseByKey
you need authorization from the controllers of the choice and at least one stakeholder. This is equivalent to the authorization needed to fo a fetchByKey
followed by an exercise
.
Example¶
A complete example of possible success and failure scenarios of fetchByKey and lookupByKey is shown below.
-- Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
module Keys where
import DA.Optional
template Keyed
with
sig : Party
obs : Party
where
signatory sig
observer obs
key sig : Party
maintainer key
template Divulger
with
divulgee : Party
sig : Party
where
signatory divulgee
controller sig can
nonconsuming DivulgeKeyed
: Keyed
with
keyedCid : ContractId Keyed
do
fetch keyedCid
template Delegation
with
sig : Party
delegees : [Party]
where
signatory sig
observer delegees
nonconsuming choice CreateKeyed
: ContractId Keyed
with
delegee : Party
obs : Party
controller delegee
do
create Keyed with sig; obs
nonconsuming choice ArchiveKeyed
: ()
with
delegee : Party
keyedCid : ContractId Keyed
controller delegee
do
archive keyedCid
nonconsuming choice LookupKeyed
: Optional (ContractId Keyed)
with
lookupKey : Party
delegee : Party
controller delegee
do
lookupByKey @Keyed lookupKey
nonconsuming choice FetchKeyed
: (ContractId Keyed, Keyed)
with
lookupKey : Party
delegee : Party
controller delegee
do
fetchByKey @Keyed lookupKey
lookupTest = scenario do
-- Put four parties in the four possible relationships with a `Keyed`
sig <- getParty "s" -- Signatory
obs <- getParty "o" -- Observer
divulgee <- getParty "d" -- Divulgee
blind <- getParty "b" -- Blind
keyedCid <- submit sig do create Keyed with ..
divulgercid <- submit divulgee do create Divulger with ..
submit sig do exercise divulgercid DivulgeKeyed with ..
-- Now the signatory and observer delegate their choices
sigDelegationCid <- submit sig do
create Delegation with
sig
delegees = [obs, divulgee, blind]
obsDelegationCid <- submit obs do
create Delegation with
sig = obs
delegees = [divulgee, blind]
-- TESTING LOOKUPS AND FETCHES
-- Maintainer can fetch
submit sig do
(cid, keyed) <- fetchByKey @Keyed sig
assert (keyedCid == cid)
-- Maintainer can lookup
submit sig do
mcid <- lookupByKey @Keyed sig
assert (mcid == Some keyedCid)
-- Stakeholder can fetch
submit obs do
(cid, l) <- fetchByKey @Keyed sig
assert (keyedCid == cid)
-- Stakeholder can't lookup without authorization
submitMustFail obs do lookupByKey @Keyed sig
-- Stakeholder can lookup with authorization
submit obs do
mcid <- exercise sigDelegationCid LookupKeyed with
delegee = obs
lookupKey = sig
assert (mcid == Some keyedCid)
-- Divulgee can't fetch
submitMustFail divulgee do fetchByKey @Keyed sig
-- Divulgee can't lookup
submitMustFail divulgee do lookupByKey @Keyed sig
-- Divulgee can't lookup with stakeholder authority
submitMustFail divulgee do
exercise obsDelegationCid LookupKeyed with
delegee = divulgee
lookupKey = sig
-- Divulgee can't do positive lookup with maintainer authority.
-- Note that the lookup returns `None` so the assertion passes.
-- If the assertion is changed to `isSome`, the assertion fails,
-- which means the error message changes. The reason is that the
-- assertion is checked at interpretation time, before the lookup
-- is checked at validation time.
submitMustFail divulgee do
mcid <- exercise sigDelegationCid LookupKeyed with
delegee = divulgee
lookupKey = sig
assert (isNone mcid)
-- Divulgee can't fetch with stakeholder authority
submitMustFail divulgee do
(cid, keyed) <- exercise obsDelegationCid FetchKeyed with
delegee = divulgee
lookupKey = sig
assert (keyedCid == cid)
-- Blind party can't fetch
submitMustFail blind do fetchByKey @Keyed sig
-- Blind party can't lookup
submitMustFail blind do lookupByKey @Keyed sig
-- Blind party can't lookup with stakeholder authority
submitMustFail blind do
exercise obsDelegationCid LookupKeyed with
delegee = blind
lookupKey = sig
-- Blind party can't lookup with maintainer authority.
-- The lookup initially returns `None`, but is rejected at
-- validation time
submitMustFail blind do
mcid <- exercise sigDelegationCid LookupKeyed with
delegee = blind
lookupKey = sig
assert (isNone mcid)
-- Blind party can't fetch with stakeholder authority as lookup is negative
submitMustFail blind do
exercise obsDelegationCid FetchKeyed with
delegee = blind
lookupKey = sig
-- Blind can do a negative lookup on a truly nonexistant contract
submit blind do
mcid <- exercise obsDelegationCid LookupKeyed with
delegee = blind
lookupKey = obs
assert (isNone mcid)
-- TESTING CREATES AND ARCHIVES
-- Divulgee can archive
submit divulgee do
exercise sigDelegationCid ArchiveKeyed with
delegee = divulgee
keyedCid
-- Divulgee can create
keyedCid2 <- submit divulgee do
exercise sigDelegationCid CreateKeyed with
delegee = divulgee
obs
-- Stakeholder can archive
submit obs do
exercise sigDelegationCid ArchiveKeyed with
delegee = obs
keyedCid = keyedCid2
-- Stakeholder can create
keyedCid3 <- submit obs do
exercise sigDelegationCid CreateKeyed with
delegee = obs
obs
return ()