Choices, delegation, and divugence: how to delegate visibility on contracts?

I’m running into an issue in a situation similar to the one described here by @cocreature .

Specifically, I know that choices can be used to delegate authority when creating new contracts, but it looks like they can’t delegate ability to fetch (or exercise a choice on) contracts that are otherwise invisible to the submitter?

More concretely, to add to the example linked above

template PasswordManager
  with
    root : Party
    password: Text
  where
    signatory root
    key root: Party
    maintainer key
  
template Checker
  with
    root : Party
    obs : Party
  where
    signatory root
    observer obs
    nonconsuming choice CheckPassword : bool
      with
        guess: Text
      controller obs
      do 
         password <- fetchByKey @PasswordManager root
         return guess == password

Basically, I don’t want the PasswordManager contract visible to obs since that would reveal the actual text of the password. But I do want obs to be able to run CheckPassword.

This is obviously a contrived example, but it illustrates what I’m trying to accomplish by keeping contract data private but delegating computation on it in predefined ways. It fails though with CONTRACT_KEY_NOT_FOUND

Any ideas about how to go about delegating reading contracts in this way?

Hi @ryan , Daml’s execution model is such that the submitting party of a transaction sees all contracts that have an action on them .

That means specifically that obs cannot submit a transaction that does a password check via equality in a transaction that they submit. That gives you two options:

  1. Do the check in two transactions, where it’s root that does the comparison guess == password
  2. Check against a hash of the password. Ie on PasswordManager store passwordSha256 and make the comparison sha256 guess = passwordManager.passwordSha256

Note though that in either case, anyone that witnesses one of the events where the password is handled in plain-text, will see the password and can extract it.

I see – thanks for explaining.

Just to check I understand “principle 1” and “principle 2” there. In another example:

template Account
  with
    root : Party
    client : Party
  where
    signatory root
    observer client
  
template AccountArchiver
  with
    root : Party
  where
    signatory root
    nonconsuming choice ArchiveMany: ()
      with
        accounts: [ContractId Account]

      controller obs
      do
        forA accounts archive
        return ()

So what I’m wondering here is what exactly is meant by consequences in “Every party that sees an action sees its (transitive) consequences”. Here the actions in questions are the archives on each of the Accounts in ArchiveMany.

Say we call ArchiveMany in a transaction with contracts [cidA, cidB] as arguments. Will the client for the Account referenced by cidA be able to see the archive action/the Account for cidB (i.e., since both archives happen in the same transaction will each Account now be divulged to all of the different clients)?

You get a transaction tree

#1 Exercise ArchiveMany [cidA, cidB]
  | - #2 Exercise Archive cidA
  | - #3 Exercise Archive cidB

The stakeholders of #1 are root as the signatory of Account and controller of ArchiveMany. They see both of the archived contracts.

The client on cidA is only a stakeholder of #2 as that’s a consuming choice. They will see that and only that.

The term consequence is formally defined here.

A more practical way to learn these things interactively is via the IDE script integration. Once you make your code compile, you can get detailed disclosure information:

module Disclosure where

import Daml.Script

template Account
  with
    root : Party
    obs : Party
    client : Party
  where
    signatory root
    observer client, obs
  
template AccountArchiver
  with
    root : Party
    obs : Party
  where
    signatory root
    observer obs

    nonconsuming choice ArchiveMany: ()
      with
        accounts: [ContractId Account]
      controller obs
      do
        forA accounts archive
        return ()

test_disclosure : Script ()
test_disclosure = script do
  [root, obs, alice, bob] <- mapA allocateParty ["root", "obs", "alice", "bob"]
  cidA <- submit root do createCmd Account with client = alice; ..
  cidB <- submit root do createCmd Account with client = bob; ..
  archiver <- submit root do createCmd AccountArchiver with ..
  submit obs do exerciseCmd archiver ArchiveMany with accounts = [cidA, cidB]
  return ()

You can see here that alice and bob do not see each others’ accounts.

1 Like

“O” and “S” stand for “Observer” and “Signatory” in that table. You have to tick “Show detailed” to see that.

To see a more complex interaction, I’ve split out the roles here somewhat:

module Disclosure where

import Daml.Script

template Account
  with
    creator : Party
    archiver : Party
    client : Party
  where
    signatory creator
    observer client, archiver

    choice DelegatedArchive : ()
      controller archiver
      do return ()
  
template AccountCreator
  with
    creatorRoot : Party
    creatorController : Party
  where
    signatory creatorRoot
    observer creatorController

    nonconsuming choice CreateMany: [ContractId Account]
      with
        parties: [Party]
        archiver: Party
      controller creatorController
      do
        forA parties (\client -> create Account with creator = creatorRoot; ..)

template AccountArchiver
  with
    archiverRoot : Party
    archiverController : Party
  where
    signatory archiverRoot
    observer archiverController

    nonconsuming choice ArchiveMany: ()
      with
        accounts: [ContractId Account]
      controller archiverController
      do
        forA accounts (\cid -> exercise cid DelegatedArchive)
        return ()

test_disclosure : Script ()
test_disclosure = script do
  [creatorRoot, creatorController, archiverRoot, archiverController, alice, bob] <- 
    mapA allocateParty ["creatorRoot", "creatorController", "archiverRoot", "archiverController", "alice", "bob"]
  creator <- submit creatorRoot do createCmd AccountCreator with ..
  archiver <- submit archiverRoot do createCmd AccountArchiver with ..
  accounts <- submit creatorController do 
    exerciseCmd creator CreateMany with 
        parties = [alice, bob]
        archiver = archiverController
  submit archiverController do exerciseCmd archiver ArchiveMany with accounts
  return ()

What you now see is this:

creatorController has witnessed (W) the creation of the contract. They are an informee of the exercise of CreateMany since they are the controller. The creates are a consequence of CreateMany. archiverRoot has had the accounts divulged (D) to them since they are a signatory on the AccountArchiver and the exercises of DelegatedArchive are a consequence of that.

You can see a lot of that info in the transaction view in the IDE as well:

 TX 3 1970-01-01T00:00:00Z (Disclosure:60:3)
  #3:0
  │   disclosed to (since): 'archiverController' (3), 'archiverRoot' (3)
  └─> 'archiverController' exercises ArchiveMany on #1:0 (Disclosure:AccountArchiver)
                           with
                             accounts = [#2:1, #2:2]
      children:
      #3:1
      │   disclosed to (since): 'archiverController' (3), 'archiverRoot' (3), 'alice' (3),
                                'creatorRoot' (3)
      └─> 'archiverController' exercises DelegatedArchive on #2:1 (Disclosure:Account)
      
      #3:2
      │   disclosed to (since): 'archiverController' (3), 'archiverRoot' (3), 'bob' (3),
                                'creatorRoot' (3)
      └─> 'archiverController' exercises DelegatedArchive on #2:2 (Disclosure:Account)

You can see on #3:1 that that event is disclosed to archiverRoot. The reason is that it’s a consequence of #3:1, which is disclosed to archiverRoot.

1 Like