"Read" Disclosure via keys

Mod note: As of SDK 1.5.0 Scenarios have been superseded by the more powerful Daml Script. We now recommend using that for all purposes. For more information, and to learn how to use Script please check out @Andreas’ post on our blog.

I was going to post a question about some code that “doesn’t” work, but then I reread the documentation and started to understand more subtleties. Please consider,

daml 1.2
module Dk where
-- Disclosure via keys?

template Request
  with
    user : Party
    admin : Party
  where
    signatory user

    controller admin can
      Approve : ContractId Capabilities
        do
          create Capabilities with ..

template Capabilities
  with
    admin : Party
    user : Party
  where
    signatory admin, user

    key (admin, user) : (Party, Party)
    maintainer key._1

    controller user can
      nonconsuming HasCapabilitiesByLookup : Bool
        with
          anotherUser : Party
        do
          r <- lookupByKey @Capabilities (admin, anotherUser)
          case r of
            None -> do
                      debug "None"
                      return False
            Some r -> do
                      debug $ "Some" <> (show r)
                      return True

test = scenario do
  admin <- getParty "admin"
  alice <- getParty "alice"
  bob <- getParty "bob"

  request <- alice `submit` do create Request with user = alice, ..
  capabilities <- admin `submit` do request `exercise` Approve

  canLookupSelf <- alice `submit` do capabilities `exercise` HasCapabilitiesByLookup with anotherUser = alice
  assert canLookupSelf

  -- No authority from admin, the maintainer.
  alice `submitMustFail` do lookupByKey @Capabilities (admin, alice)

  canLookupBob <- alice `submit` do capabilities `exercise` HasCapabilitiesByLookup with anotherUser = bob
  -- doesn't have capabilities yet or ...
  assert $ not canLookupBob

  bob_request <- bob `submit` do create Request with user = bob, ..
  bob_capabilities <- admin `submit` do bob_request `exercise` Approve

  stillCantLookupBobo <- alice `submit` do capabilities `exercise` HasCapabilitiesByLookup with anotherUser = bob
  -- Can't lookup because alice is not a stakeholder on the Capabilities contract between admin  and bob.
  assert $ not stillCantLookupBobo

  return ()
  1. There is a difference between performing a (1) lookupByKey as an Update within the Scenario and as a (2) part of a Choice on an agreed to contract. When you write this out the distinction becomes clearer. :slight_smile:
  2. The reason why a lookupByKey can return None can be different!

When I wrote this code I had intended that last lookup to actually work. I do want alice to discover that bob has Capabilities. I have two proposals in mind. The first, simpler, is to change the meaning of lookupByKey when the contract that it refers to is the same a self. Perhaps a different name, lookupSelfByKey would be appropriate? In this case bob can see the contract to which he agrees to, and part of that contract is the ability for others to discover that he has this contract.

The second, more radical, idea is to remove the restriction that the “submitting party is a stakeholder of a matching contract”. ie, make keys only restricted by the maintainer authorizing it. If you do want privacy via the key one could already add a password/shared secret as part of the key,

key (maintainerParty, userParty, password) : (Party, Party, Text) -- make it really good.
maintainer key._1
1 Like

As you mention in the comment, the Capabilities contract is visible to its stakeholders. Stakeholders are the union of signatories, observers, and controllers — so keys and maintainers aren’t relevant to determining visibility.

This deterministic mapping of a contract to its disclosure is a critical foundation of DAML’s privacy guarantees. Recall that the DAML Choice is fully evaluated by the participant before it is submitted to the ledger. So for any of your suggestions to work would require that all of the instances of template be visible to all controllers of any instance of that template—which would destroy any semblance of privacy model.

Point 2 is really saying that lookupByKey returns None when no active contract matching the key is visible to the transaction initiator (which may be different from the controller[s] of the relevant sub-transaction). Your point 2 is really saying there are two reasons why no active contract might not be visible (1. it doesn’t exist; or, 2. it isn’t visible).

Point 1 is mistaken. There is no difference between the behaviour of lookupByKey in a Choice vs. a Scenario. In both cases you must have the Maintainer’s authorization to perform a key lookup. The actual difference between Choice and a Scenario is that the scenario emulates the ledger command submission and therefore is restricted to a single authoriser, while a choice inherits the authority of is signatories and its controllers. As alice is not a maintainer, this does mean that alice can only perform a lookupByKey within a choice as it’s the only way she can perform it with the additional authorizations required.

The one question I have for people with a better understanding of the ledger model than I is: Why do we have the maintainer authorization requirement? The reason given in the ledger model is that this prevents alice from probing for the existence of bob's contracts. But, as this example demonstrates that requirement is already satisfied by the need to evaluate key lookups with respect to the visibility of the submitter, rather than the visibility of the validator. At that point I don’t see what value there is in refusing to permit alice's lookup at line 53 — or if there is one, it can’t be the stated one, and I’d love to know what it is?

53   alice `submitMustFail` do lookupByKey @Capabilities (admin, alice)

First, if I’m reading this correctly, this looks like a bug in the scenario runner: transaction #7 should not commit, as it records a false NoSuchKey assertion on the ledger. @bernhard @gerolf

To @Andrae’s question: as i said, I think the above behavior is a bug; the last transaction should get rejected, as the lookup should return None when evaluated from Alice’s perspective, but then should fail when evaluated at the point where the transaction containing this lookup should get included in the ledger.

I don’t see a problem with the rationale given in the ledger model still holds; without the authorization requirement, I can keep probing a key by submitting transactions with NoSuchKey assertions; without the authorization checks, these would get committed until the point where a contract with this key appears, after which they would start to get rejected. With the authorization rule in place, I don’t see another way to obtain this information - let me know if you do.

It doesn’t commit, this behaves as intended as far as I can see.

  Attempt to fetch, lookup or exercise a key associated with a contract not visible to the committer.
  Contract:  #6:1 (Main:Capabilities)
  Key:  _1 = 'admin'; _2 = 'bob'
  Committer: 'alice'
  Stakeholders: 'admin', 'bob'

The trace statement in HasCapabilitiesByLookup here gives None, which is correct. Alice is not a stakeholder on the contract so het lookup is negative. This is rejected during validation.

The scenario runs with

-   stillCantLookupBobo <- alice `submit` do capabilities `exercise` HasCapabilitiesByLookup with anotherUser = bob
-   -- Can't lookup because alice is not a stakeholder on the Capabilities contract between admin  and bob.
-   assert $ not stillCantLookupBobo

+   -- Can't lookup because alice is not a stakeholder on the Capabilities contract between admin  and bob.
+   alice `submitMustFail` do capabilities `exercise` HasCapabilitiesByLookup with anotherUser = bob

@Andrae my suggestion is:

Perhaps lookupSelfByKey implies the wrong thing. I mean

lookupSelfByKey = lookupByKey @CurrentTemplate

so it would be lookupByKey @Capabilities within any Choice that a party can exercise within Capabilities.

It turns out that transaction #7 only committed in a particular buggy 1.3.0 snapshot version that I had running on my machine. So Leonid’s scenario doesn’t actually run on the latest snapshot version.

Andrae’s point still stands: if Alice were to make use of this, she would need to be informed of the creation of every Capabilities contract (otherwise, she couldn’t find the contract with the matching key), or at least, every capabilities contract with the same admin. But you can already do the latter if you really want to, by adding all the users as observers on all the Capabilities contracts. The only downside is the usual one of contracts with many observers, which is that all the contracts must change if the admin wants to add or remove a user.

@oggy I’m arguing for, suggesting, that when alice calls lookupByKey @Capabilities (in the simpler proposal, only within a choice of Capabilities) she uses admin's right to know about other Capabilities to lookup the one for bob. The result will be a Some (ContractId Capabilities).

Yes, this will be changing the current visibility model. But remember bob does not have to sign this contract and keys are optional to begin with. And the point is to explicitly avoid the adding an observer pattern.

The lookupByKey in HasCapabilitiesByLookup is authorized by admin via the signatory delegation, so my understanding of the authorization model is that this transaction will succeed or fail solely based on existence or not of bob's Capabilities instance. As alice can pass any party to the non-consuming choice, this allows alice to probe for the existence of any contract she is not already privy to, unless the lookupByKey is evaluated and validated with respect to alice's visibility rather than the maintainers.

So I think my point is that I don’t think this is a bug, I think the last transaction should succeed. Alice is authorized to perform the check, and None is epistemologically consistent, and that consistency can be validated by the maintainer, so there is no global validation issue arising from this.

That is true in this case, but need not be true in general. Imagine alice had delegated HasCapabilitiesByLookup to bob so the last transaction was

bob `submit` do delegatedCapabilities `exercise` DelegatedHasCapabilitiesByLookup

Thanks to sub-transaction privacy, admin does not know whether it’s alice or someone else submitting the HasCapabilitiesByLookup sub-transaction. Thus validators can’t validate the “submitters have to be stakeholders” restriction.

Indeed, that restriction is not part of the ledger model, it’s a pure participant node concern. Our participants currently apply this rule as contracts that the submitting party is a stakeholder on are the only ones with an easy way to resolve keys uniquely and with a good chance of correctness.

In the future, we may well add better disclosure mechanisms, which also extend the set of contracts which a submitter can look up.