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:
- Do the check in two transactions, where it’s root that does the comparison
guess == password - Check against a hash of the password. Ie on
PasswordManagerstorepasswordSha256and make the comparisonsha256 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.
“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.

