Observers and read delegation

When trying to describe read delegation through multi-party submissions, I came across the following question. Assume I want to create a ledger where all parties can look up some reference data that is maintained by the operator. For example, a market where participants can exchange items at a fixed price.


First option would be to create a CurrentPrice contract where all market participants are observers and can look up the current price by key:

template CurrentPrice
  with
    itemName: Text
    price : Int
    operator : Party
    observers : [Party] -- huge list containing every party on the ledger
  where
    signatory operator
    observer observers
    key (itemName, operator) : (Text, Party)
    maintainer key._2

    choice UpdatePrice : ContractId CurrentPrice
      with
        newPrice : Int
      controller operator
      do
        create this with price = newPrice

    choice AddObserver : ContractId CurrentPrice
      with
        newObserver : Party
      controller operator
      do
        create this with observers = newObserver :: this.observers

Second option would be to not any extra observers on the CurrentPrice contract and use a different mechanism (the upcoming readAs parties in command submissions) to allow any party to fetch CurrentPrice contracts.


Third option would be to use the subscription/broadcast code from ex-models/Broadcast.daml at b6b95f3797e8a0a563dbf450d091d215a4aba306 · digital-asset/ex-models · GitHub


What would be the advantages and disadvantages of each option? In particular, is there any big disadvantage in having a contract with a huge number of observers?

2 Likes

One disadvantage of the first option is that you have to maintain the list of observers. For example, if a new party appears on the ledger, the list of observers of the CurrentPrice contract needs to be updated. With the second option, the new party will just have readAs authorization and will automatically be able to observe the CurrentPrice contract.
The third option has the disadvantage that it is rather hard to understand why the broadcasting works at all, given it uses some subtle properties of Daml’s privacy enforcing mechanism. Moreover, this mechanism might be changing in the future and break this way of broadcasting.

1 Like

Pretty much what @drsk said. In addition to operational differences, let’s look at the performance characteristics of the three models for two operations:

  1. A new subscriber joins
  2. The price is updated

Let’s say there are n subscribers to start.

Using a list of observers

  1. When the n+1st subscriber joins, there is a transaction updating the list of observers. The transaction will consist of an exercise on a contract with n observers creating a new contract with n+1 observers. That means there are two events, each of size O(n) being distributed to O(n) participants. That means O(n^2) amount of data needs to flow. You also get O(n) amount of data through the Ledger API on every node to emit the events.
  2. The transaction has a single event updating a contract of size O(n) with n observers. So as in 1., you move around O(n^2) amount of data and emit O(n) data through the ledger API.

Even if you could get rid of the quadratic term through clever data distribution methods or optimizing what data actually gets shipped around, you have linear slowdown. You also have contention between data distribution and maintaining the list of observers.

Using the Boardcast Pattern

  1. The n+1st user gets issued a Subscription contract, which is lightweight. Let’s ignore the Broadcast contract since it’s single signatory and could be moved off-ledger.
  2. Updating the data requires each of the n Subscription contracts to be updated, which means we have a transaction of size O(n). Each one is size O(1) and distributed to the broadcaster and one other party so the broadcaster needs to ship O(n) data in total. Other parties receive only O(1).

This is a significant improvement over the list of observers.

Using read_as

  1. Updating the list of subscribers may not require a transaction at all. It only requires the user to be issued a suitable auth token. It may also involve allocating the reader party to a new node, which is O(1).
  2. Updating the data is a transaction updating a contract with two observers. The transaction will potentially distributed to n nodes, but each node only requires O(1).

This is optimal. To ship price information to n parties on n nodes, you need to send a fixed size piece of data to n nodes. And this boils down to a classic multi-cast problem which most of the underlying ledgers solve efficiently.

2 Likes

@bernhard Thinking more about the read_as model. How would a publisher handle a subscription mechanism. Explicitly, if publisher creates some

template Fact
  with
    data : Data
    publisher: Party
    publishedObserver : Party 
 where
  signatory : publisher
  observer : publishedObserver

And suppose there are other contracts (or an off ledger mechanism) whereby Alice and Bob pay for the right to read Facts. In the beginning publisher share’s publisherObserver read token with Alice and Bob. Now this model works if Carl wants to subscribe, too. But what if Bob stops paying the subscription how can publisher limit Bob's ability to read future Facts without using a new publishObserver ?

1 Like

If they control the participant node Bob uses: Stop issuing him a token that allows reading as that party.
If Bob runs his own node: de-allocate the publishedObserver party from that node.

1 Like

Could you point me at the LedgerAPI calls to accomplish that? I haven’t worked with it yet and the docs are not obvious.

1 Like

These are not Ledger API operations.

Token issuance is left to an external IAM system. For example, if you use a service like auth0 to do auth, you need to change the user’s permissions there.

Party management beyond the allocation of new parties is currently ledger specific. The Canton docs show how to allocate a party on two nodes. De-allocation works, to the best of my knowledge, by using IdentityChangeOp.Remove rather than IdentityChangeOp.Add.

1 Like

Thanks @bernhard for the detailed writeup!

1 Like

Why two observers? I was assuming the operator party would be allocated on each participant node, and would be the single observer of all CurrentPrice contracts. Being allocated on each participant means that all CurrentPrice contracts would be shipped to all index databases, where they can be looked up with the use of read_as tokens.

1 Like

I had separate parties for signatory and observer in mind. But you are right, you could do it with just the one.

1 Like