Feature Request: Blind Fields
I’ve realized that in a couple of our projects now, it would be really nice to have fields on contracts and choices that are only visible to signatories of the contract and not other observers.
This would especially enable (some) observers to be effectively anonymized from one another, by putting a list of observers (say) in a blind field. You’d also pretty shortly want the same for choices, so that you could, say, have a choice with a flexible controller whose Party was in a blind field of the choice so their identity would only be revealed to signatories, and not anyone else who could see the action taking place.
For example, this might be useful to enable anonymous negotiations between clients of a broker, where a broker creates a contract for the negotiating parties to interact with, and they each can see it, and can take actions on it without revealing their identities to each other, only to the broker.
For example, using the new keyword ‘blind’ as analogous to ‘with’:
template Negotiation
with
broker : Party
terms : Terms
blind
clients : Set Party
accepted : Set Party
where
signatory broker
observer clients
choice Counter : ContractId Negotiation
with
newTerms : Terms
blind
actor : Party
controller actor
do
assertMsg "Must be a client of this negotiation" (Set.member actor clients)
create this with
terms = newTerms
accepted = Set.singleton actor
choice Accept : Either (ContractId Negotiation) (ContractId Deal)
blind
actor : Party
controller actor
do
assertMsg "Must be a client of this negotiation" (Set.member actor clients)
let newAccepted = Set.insert actor accepted
if newAccepted == clients
then Right <$> create Deal with
broker
terms
clients
else Left <$> create this with
accepted = Set.insert actor accepted
This is intentionally a bit simplistic, especially with the single signatory and no reporting of status at all to the participants before the end, but hopefully the idea is clear enough. Any of the clients can counter with new terms (resetting the acceptance to just include themselves), or accept, adding themselves to the set of accepted clients, and if that set becomes the entire set of clients, a new Deal is created (and presumably agreeing parties are revealed to one another at that point and can reconfirm to become signatories of something). The broker has insight into everything which is going on, but while each client sees that this Negotiation contract exists, and so can infer that they must belong to its clients set, they can’t actually see the contents of that set directly, or who has accepted the terms so far.
Currently, anyone who can see a contract can compute who all the observers are, and so if taking some choice on a contract is meant to require disclosing information to a party who is meant to be anonymous with respect to any of its observers, this typically involves some off-ledger component, and you may miss out on the contractual guarantee that it actually takes place as it should, as well as atomicity. One of the key selling points of Daml is to be able to nicely handle the logic about visibility and rights like this, and so I think a feature along these lines could help further strengthen that point.
One weakness of this design as proposed can be seen in the above example: it might be nice for it to be possible to collect the delegated authority of the accepting clients to create a Deal in the end on which they’d all be signatories from the outset, and not merely observers, still without revealing them to one another before the end. At the same time, to allow that would involve allowing parties to agree to things and end up as signatories on contracts before they were aware of with whom they were entering into an agreement, which might also be inappropriate in many cases.
Hi Cale,
Thank you for this input, such product input is invaluable. The idea of blinded fields and/or blinded observers is indeed a compelling one, and one we’ve considered in earnest a few times. But there are a number of challenges and tradeoffs that are so gnarly that we have yet to move forward with something like that.
The first and most important challenge is Canton’s execution model:
- Submitter of a command interprets the transaction creating the blinded views for all informees, which includes observers.
- Submitter sends the blinded views to recipients via a multi-cast message through the sequencer.
- Recipients validate the transaction and (as needed) confirm it.
- Mediator aggregates confirmations and sends out a compound confirmation to all informees.
- All recipients commit the transaction.
If the submitter o1 is one blind observer, and the list of informees includes another o2, then o1 has no way of knowing that they have to create a view for o2 so already step 1 breaks down. We fundamentally have three options:
- We make the sequencer responsible that somehow
o2finds out - We make the signatories responsible that somehow
o2finds out - We simply say “a blinded observer cannot submit a transaction with a consuming choice” so that we never encounter this problem.
- Use Explicit Disclosure instead of observers
3 is pretty uncompelling. It wouldn’t work for your use-case.
Sequencer Responsiblity
Enabling the sequencer to do this means o1 would need to be able to create a view for some unknown set of parties blinded_observers, which the sequencer than has to be able to resolve to [o1, o2].
That either means having [o1, o2] as an encrypted payload as part of blinded_observers with a key that the sequencer can use for decryption or registering blinded_observers as an alias for [o1, o2] at the time of creation of the contract.
If we did this dynamically, we’d need to make the sequencer aware of creations and archivals of such contracts so it can keep track of the needed aliases or keys. Essentially the sequencer turns into a participant node that gets a special view on all contracts with blinded observers. This is such a fundamental break with Canton’s design.
Doing this in a manual fashion might be an option. It would mean each client creates some alias for their party that is only known to them and the domain by default. They’d make this known to the broker for inclusion in the Negotiation. This amounts to privacy features on the topology ledger. and a number of topology transactions around each negotiation to allocate and tidy up these aliases.
Signatory Responsibility
Given Canton’s current execution model, this amounts to inserting extra steps to allow the signatory to augment the transaction. We usually call this “interactive submission”.
The set of views the submitter generates in 1. would be incomplete. It would leave a “blinded branch” in the transaction tree that only the signatories can fill in. When the signatories receive a confirmation request for such a transaction, they’d not validate directly, but first augment the original confirmation request with the subtrees for those blinded branches. They, in turn could have blinded sub-branches so step by step the complete confirmation request would be built up until there are no blinded branches left and the transaction can be validated and confirmed. This would solve for all kinds of problems like enabling a non-observer to fetch a contract for which they have correct authorization, but the performance, security and complexity impact are big unknowns at this point so this is a research project for a future generation of Canton.
Using Explicit Disclosure
The last option is to not use observers at all, but to make the Negotiation contract available out of band, meaning the broker makes it available to the clients through a standard web2 API. The choices could be guarded in such a way that the clients don’t need to learn of each other. To submit such a transaction, the clients would have to feed the Negotiation contract back into the commands via explicit disclosure.
type Terms = ()
type NegotiationId = Text
template Deal
with
broker : Party
clients : Set.Set Party
terms : Terms
where
signatory broker
observer clients
template NegotiationClient
with
broker : Party
nid : NegotiationId
client : Party
clientAlias : Text
where
signatory broker
observer client
fetchAndValidateNC : Negotiation -> ContractId NegotiationClient -> Update NegotiationClient
fetchAndValidateNC Negotiation{..} ncCid = do
nc <- fetch ncCid
let clientAlias = nc.clientAlias
assertMsg "Invalid Client Alias" (clientAlias `Set.member` clientAliases)
assertMsg "Mismatching NegotiationClient" (nc == nc with broker; nid)
return nc
template Negotiation
with
broker : Party
nid : NegotiationId
terms : Terms
clientAliases : Set.Set Text
acceptedAliases : Set.Set Text
where
signatory broker
choice Counter : ContractId Negotiation
with
newTerms : Terms
ncCid : ContractId NegotiationClient
client : Party
controller client
do
nc <- fetchAndValidateNC this ncCid
create this with
terms = newTerms
acceptedAliases = Set.singleton nc.clientAlias
choice Accept : ContractId Negotiation
with
ncCid : ContractId NegotiationClient
client : Party
controller client
do
nc <- fetchAndValidateNC this ncCid
create this with
acceptedAliases = Set.insert nc.clientAlias acceptedAliases
choice CreateDeal : ContractId Deal
with
ncCids : [ContractId NegotiationClient]
controller broker
do
assertMsg "Not all clients have accepted" (clientAliases == acceptedAliases)
clients <- forA ncCids (\ncCid -> do
nc <- fetchAndValidateNC this ncCid
return nc.client
)
create Deal with broker; terms; clients = Set.fromList clients
Note that you can no longer have one of the clients submit the creation of the deal as this step requires the translation of the blinded clients into plain clients. Only the broker can do that so the only other solution approach would require “interactive submission”.
One thing to consider is the inherent contradiction in your requirements according to the Daml you listed. The key lines are:
blind
clients : Set Party
and
controller actor -- (Set.member actor clients)
...
if newAccepted == clients
In the field definition you are saying “clients to a negotiation should not be aware of each other’s identities”; while in the choice you are saying “A client choosing to Accept a deal needs to know the identities of the other clients to the negotiation”.
You seem to be approaching this as if there is some trusted “Daml Platform” executor outside of the parties to the contract that can know everything and can be trusted to oversee the evaluation of the smart contract. A key aspect of Daml is that it doesn’t rely on that sort of centralised trust model. The only parties to the smart contract are the parties listed on the smart contract — when a client exercises the Accept choice, it is that client who runs the Daml associated with the choice, the platform then shares that result with the other listed stakeholders on the contract so they can validate, verify, and confirm that the client executed their choice faithfully.
If the clients don’t know of each other, then there must be an intermediary who does. That relationship of what each party knows and does not know, and the various workflows by which the intermediary is informed of the clients choices, and in turn informs the other clients, is the key modelling problem to solve here—and you can expect to find this modelling of the epistemic facets of your problem encoded explicitly across multiple templates and contracts.
I had a go at modelling something along these lines, and I have included what I came up with below—although with the caveat that I have not tested it at all, so there may well be authorisation and visibility issues I missed; but, I hope it is helpful at clarifying what I mean by modelling the epistemic facets of your problem:
module Constellation where
import DA.Set as Set
data Terms = Terms
deriving (Show, Eq)
template Deal
with
broker : Party
terms : Terms
clients : Set Party
where
signatory broker
template NegotiationMSA
with
auditor : Party
broker : Party
where
signatory auditor, broker
choice LaunchNegotiation :
( ContractId BrokeredNegotiation
, ContractId Negotiation
, [ContractId PrivateInvite])
with
terms : Terms
clients : Set Party
broadcast : Party
controller broker
do
(,,)
<$> create BrokeredNegotiation with
broker
auditor
origTerms = terms
clients
<*> create Negotiation with
broker
broadcast
origTerms = terms
currTerms = terms
accepted = Set.empty
<*> mapA (\client -> create PrivateInvite with
broker
broadcast
client
origTerms = terms
currTerms = terms)
(toList clients)
template BrokeredNegotiation
with
broker : Party
auditor : Party
origTerms : Terms
clients : Set Party
where
signatory broker, auditor
postconsuming choice VerifyAndFinalize : ContractId Deal
with
negotiationId : ContractId Negotiation
controller broker
do
exercise negotiationId ConcludeDeal with brokerNegId = self
nonconsuming choice UpdateInvites : [ContractId PrivateInvite]
controller broker
do
(_, neg) <- fetchByKey @Negotiation (broker, origTerms)
mapA
(\client ->
do
(inviteId, invite) <- fetchByKey @PrivateInvite (broker, client, origTerms)
if invite.currTerms /= neg.currTerms
then exercise inviteId UpdateTerms
else pure inviteId
)
(toList clients)
template Negotiation
with
broker : Party
broadcast : Party
origTerms : Terms
currTerms : Terms
accepted : Set Party
where
signatory broker, accepted
observer broadcast
key (broker, origTerms) : (Party, Terms)
maintainer key._1
choice AcceptPublic : ContractId Negotiation
with
actor : Party
inviteId : ContractId PrivateInvite
controller actor
do
invite <- fetch inviteId
assertMsg "Must be associated with the private invite" $
invite.client == actor
assertMsg "Must be a client of this negotiation" $
(invite.broker, invite.currTerms) == (this.broker, this.currTerms)
create this with accepted = Set.insert actor accepted
choice ConcludeDeal : ContractId Deal
with
brokerNegId : ContractId BrokeredNegotiation
controller broker
do
bNeg <- fetch brokerNegId
assertMsg "Must be a matching negotiation" $
(bNeg.broker, bNeg.origTerms) == (this.broker, this.origTerms)
assertMsg "Deal must be accepted by all clients" $
this.accepted == bNeg.clients
create Deal with broker; terms = currTerms; clients = accepted
choice Counter : ContractId Negotiation
with
actor : Party -- blind
privNegId : ContractId PrivateInvite
newTerms : Terms
controller actor
do
privNeg <- fetch privNegId
assertMsg "Must be a client of this negotiation" $
(privNeg.broker, privNeg.origTerms) == (this.broker, this.origTerms)
create this with accepted = Set.singleton actor; currTerms = newTerms
template PrivateInvite
with
broker : Party
client : Party
broadcast : Party
origTerms : Terms
currTerms : Terms
where
signatory broker
key (broker, client, origTerms) : (Party, Party, Terms)
maintainer key._1
observer client
nonconsuming choice AcceptInvitation : ContractId Negotiation
controller client
do
exerciseByKey @Negotiation (broker, origTerms)
AcceptPublic with actor = client; inviteId = self
choice ConsiderCounter : (ContractId PrivateInvite, ContractId Negotiation)
with
newTerms : Terms
controller client
do
(,)
<$> create this with currTerms = newTerms
<*> exerciseByKey @Negotiation (broker, origTerms)
Counter with actor = client; privNegId = self; newTerms
choice UpdateTerms : ContractId PrivateInvite
controller broker
do
(_, neg) <- fetchByKey @Negotiation (broker, origTerms)
create this with currTerms = neg.currTerms