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.
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?
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.
Pretty much what @drsk said. In addition to operational differences, let’s look at the performance characteristics of the three models for two operations:
A new subscriber joins
The price is updated
Let’s say there are n subscribers to start.
Using a list of observers
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.
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
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.
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
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).
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.
@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 ?
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.
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.
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.