Skip to content

Commit

Permalink
add visibleByKey (#7146)
Browse files Browse the repository at this point in the history
Adds a Boolean check for the existence of an active contract with a
given key (that is visible with the current set of authorizations), and
explain `lookupByKey` in terms of it to hopefully clarify the relationship
between `lookupByKey` and `fetchByKey`.

Fixes #7143; see that for more information.

CHANGELOG_BEGIN

- [DAML Standard Library] Added a new function `visibleByKey`
  which, given a contract key, returns True if the current context can
  see an active contract with that key.

CHANGELOG_END
  • Loading branch information
garyverhaegen-da authored Aug 24, 2020
1 parent c997f5d commit f54e193
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 29 deletions.
10 changes: 10 additions & 0 deletions compiler/damlc/daml-stdlib-src/DA/Internal/Template/Functions.daml
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,13 @@ fromAnyContractKey (AnyContractKey any rep)

deriving instance Eq Archive
deriving instance Show Archive

-- | True if contract exists, submitter is a stakeholder, and all maintainers
-- authorize. False if contract does not exist and all maintainers authorize.
-- Fails otherwise.
visibleByKey : forall t k. (HasLookupByKey t k) => k -> Update Bool
visibleByKey k = do
m <- lookupByKey @t k
case m of
Some _ -> pure True
None -> pure False
99 changes: 83 additions & 16 deletions docs/source/daml/code-snippets/Keys.daml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,24 @@ template Delegation
do
archive keyedCid

nonconsuming choice UnkeyedFetch
: Keyed
with
cid : ContractId Keyed
delegee : Party
controller delegee
do
fetch cid

nonconsuming choice VisibleKeyed
: Bool
with
key : Party
delegee : Party
controller delegee
do
visibleByKey @Keyed key

nonconsuming choice LookupKeyed
: Optional (ContractId Keyed)
with
Expand Down Expand Up @@ -88,11 +106,11 @@ lookupTest = scenario do
submit sig do exercise divulgercid DivulgeKeyed with ..

-- Now the signatory and observer delegate their choices
sigDelegationCid <- submit sig do
create Delegation with
sigDelegationCid <- submit sig do
create Delegation with
sig
delegees = [obs, divulgee, blind]
obsDelegationCid <- submit obs do
obsDelegationCid <- submit obs do
create Delegation with
sig = obs
delegees = [divulgee, blind]
Expand All @@ -103,6 +121,10 @@ lookupTest = scenario do
submit sig do
(cid, keyed) <- fetchByKey @Keyed sig
assert (keyedCid == cid)
-- Maintainer can see
submit sig do
b <- visibleByKey @Keyed sig
assert b
-- Maintainer can lookup
submit sig do
mcid <- lookupByKey @Keyed sig
Expand All @@ -112,44 +134,83 @@ lookupTest = scenario do
submit obs do
(cid, l) <- fetchByKey @Keyed sig
assert (keyedCid == cid)
-- Stakeholder can't see without authorization
submitMustFail obs do visibleByKey @Keyed sig
-- Stakeholder can see with authorization
submit obs do
b <- exercise sigDelegationCid VisibleKeyed with
delegee = obs
key = sig
assert b
-- 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
submit obs do
mcid <- exercise sigDelegationCid LookupKeyed with
delegee = obs
lookupKey = sig
assert (mcid == Some keyedCid)

-- Divulgee can't fetch
-- Divulgee _can_ fetch the contract directly
submit divulgee do
exercise obsDelegationCid UnkeyedFetch with
delegee = divulgee
cid = keyedCid
-- Divulgee can't fetch through the key
submitMustFail divulgee do fetchByKey @Keyed sig
-- Divulgee can't see
submitMustFail divulgee do visibleByKey @Keyed sig
-- Divulgee can't see with stakeholder authority
submitMustFail divulgee do
exercise obsDelegationCid VisibleKeyed with
delegee = divulgee
key = sig
-- Divulgee can't lookup
submitMustFail divulgee do lookupByKey @Keyed sig
-- Divulgee can't lookup with stakeholder authority
submitMustFail divulgee do
submitMustFail divulgee do
exercise obsDelegationCid LookupKeyed with
delegee = divulgee
lookupKey = sig
-- Divulgee can't do positive lookup with maintainer authority.
-- Divulgee can't do positive lookup with maintainer authority.
submitMustFail divulgee do
b <- exercise sigDelegationCid VisibleKeyed with
delegee = divulgee
key = sig
assert $ not b
-- 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
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
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 see
submitMustFail blind do visibleByKey @Keyed sig
-- Blind party can't see with stakeholder authority
submitMustFail blind do
exercise obsDelegationCid VisibleKeyed with
delegee = blind
key = sig
-- Blind party can't see with maintainer authority
submitMustFail blind do
b <- exercise sigDelegationCid VisibleKeyed with
delegee = blind
key = sig
assert $ not b
-- Blind party can't lookup
submitMustFail blind do lookupByKey @Keyed sig
-- Blind party can't lookup with stakeholder authority
Expand All @@ -166,10 +227,16 @@ lookupTest = scenario do
lookupKey = sig
assert (isNone mcid)
-- Blind party can't fetch with stakeholder authority as lookup is negative
submitMustFail blind do
submitMustFail blind do
exercise obsDelegationCid FetchKeyed with
delegee = blind
lookupKey = sig
-- Blind party can see nonexistence of a contract
submit blind do
b <- exercise obsDelegationCid VisibleKeyed with
delegee = blind
key = obs
assert $ not b
-- Blind can do a negative lookup on a truly nonexistant contract
submit blind do
mcid <- exercise obsDelegationCid LookupKeyed with
Expand All @@ -180,18 +247,18 @@ lookupTest = scenario do

-- Divulgee can archive
submit divulgee do
exercise sigDelegationCid ArchiveKeyed with
exercise sigDelegationCid ArchiveKeyed with
delegee = divulgee
keyedCid
-- Divulgee can create
keyedCid2 <- submit divulgee do
exercise sigDelegationCid CreateKeyed with
keyedCid2 <- submit divulgee do
exercise sigDelegationCid CreateKeyed with
delegee = divulgee
obs

-- Stakeholder can archive
submit obs do
exercise sigDelegationCid ArchiveKeyed with
submit obs do
exercise sigDelegationCid ArchiveKeyed with
delegee = obs
keyedCid = keyedCid2
-- Stakeholder can create
Expand Down
54 changes: 41 additions & 13 deletions docs/source/daml/reference/contract-keys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.. SPDX-License-Identifier: Apache-2.0
.. _contractkeys:

Contract keys
#############

Expand All @@ -27,29 +27,31 @@ It's best to use simple types for your keys like ``Text`` or ``Int``, rather tha
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.
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.
Uniqueness of keys is guaranteed per template. Since multiple templates may use the same key type, some key-related functions 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``.
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 that 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.
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: :ref:`fetchbykey` and :ref:`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.
The primary purpose of contract keys is to provide a stable, and possibly meaningful, identifier that can be used in DAML to fetch contracts. There are two functions to perform such lookups: :ref:`fetchbykey` and :ref:`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.

Limiting key usage to stakeholders also means that keys cannot be used to access a divulged contract, i.e. there can be cases where `fetch` succeeds and `fetchByKey` does not. See the example at the end of this section for details.

.. _fetchbykey:

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.
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).

Expand All @@ -62,6 +64,26 @@ Like ``fetch``, ``fetchByKey`` needs to be authorized by at least one stakeholde

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.

.. _visiblebykey:

visibleByKey
============

``boolean <- visibleByKey @ContractType contractKey``

Use ``visibleByKey`` to check whether you can see an active contract for the given key with the current authorizations. If the contract exists and you have permission to see it, returns ``True``, otherwise returns ``False``.

To clarify, ignoring contention:

1. ``visibleByKey`` will return ``True`` if all of these are true: there exists a contract for the given key, the submitter is a stakeholder on that contract, and at the point of call we have the authorization of **all** of the maintainers of the key.
2. ``visibleByKey`` will return ``False`` if all of those are true: there is no contract for the given key, and at the point of call we have authorization from **all** the maintainers of the key.
3. ``visibleByKey`` will abort the transaction at interpretation time if, at the point of call, we are missing the authorization from any one maintainer of the key.
4. ``visibleByKey`` will fail at validation time (after returning ``False`` at interpretation time) if all of these are true: at the point of call, we have the authorization of **all** the maintainers, and a valid contract exists for the given key, but the submitter is not a stakeholder on that contract.

While it may at first seem too restrictive to require **all** maintainers to authorize the call, this is actually required in order to validate negative lookups. In the positive case, when you can see the contract, it's easy for the transaction to mention which contract it found, and therefore for validators to check that this contract does indeed exist, and is active as of the time of executing the transaction.

For the negative case, however, the transaction submitted for execution cannot say _which_ contract it has not found (as, by definition, it has not found it, and it may not even exist). Still, validators have to be able to reproduce the result of not finding the contract, and therefore they need to be able to look for it, which means having the authorization to ask the maintainers about it.

.. _lookupbykey:

lookupByKey
Expand All @@ -71,14 +93,20 @@ lookupByKey

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.
``lookupByKey`` is conceptually equivalent to

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.
.. code-block:: daml
``lookupByKey`` fails and aborts the transaction if:
lookupByKey : forall c k. (HasFetchByKey c k) => k -> Update (Optional (ContractId c))
lookupByKey k = do
visible <- visibleByKey @c k
if visible then do
(contractId, _ignoredContract) <- fetchByKey @c k
return $ Some contractId
else
return None
- 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.
Therefore, ``lookupByKey`` needs all the same authorizations as `visibleByKey`, for the same reasons, and fails in the same cases.

To get the data from the contract once you've confirmed it exists, you'll still need to use ``fetch``.

Expand All @@ -87,7 +115,7 @@ 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``.
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 do a ``fetchByKey`` followed by an ``exercise``.

Example
*******
Expand Down

0 comments on commit f54e193

Please sign in to comment.