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.

3 Likes

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:

  1. Submitter of a command interprets the transaction creating the blinded views for all informees, which includes observers.
  2. Submitter sends the blinded views to recipients via a multi-cast message through the sequencer.
  3. Recipients validate the transaction and (as needed) confirm it.
  4. Mediator aggregates confirmations and sends out a compound confirmation to all informees.
  5. 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:

  1. We make the sequencer responsible that somehow o2 finds out
  2. We make the signatories responsible that somehow o2 finds out
  3. We simply say “a blinded observer cannot submit a transaction with a consuming choice” so that we never encounter this problem.
  4. 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”.

3 Likes

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