Daml finance app - Scaling + performance (Operator everywhere)

Hi,

In Daml finance app I see that the operator gets involved as signature in every service. I wonder if that design clashes with some recomendations stated in Canton docs related to scaling the network and performance. Scaling and Performance — Daml SDK 2.7.6 documentation

There are some anti-patterns that need to be avoided for the maximum scaling opportunity. For example, having almost all of the parties on a single participant is an anti-pattern to be avoided since that participant will be a bottleneck. Similarly, the design of the Daml model has a strong impact on the degree to which sharding is possible. For example, having a Daml application that introduces a synchronization party through which all transactions need to be validated introduces a bottleneck so it is also an anti-pattern to avoid.

Plan your topology such that your DAML parties can be partitioned into independent blocks. That means, most of your DAML commands involve parties of a single block only. It is ok if some commands involve parties of several (or all) blocks, as long as this happens only very rarely. In particular, avoid having a single master party that is involved in every command, because that party would become a bottleneck of the system.

Thanks!

Hi @jvelasco.intellecteu,

The operator is used in the Daml Finance App as a root of trust, signing the Role and Service contracts.

Generally, a customer that is looking to enter into a Service contract with a provider either trusts the provider or requires some sort of credentials which signed by a trusted party.

The Role contracts which are signed by the operator act as the credential of the provider.

You are right that this mechanism doesn’t scale very well when every other contract is signed by an operator party. This is the reason why the contracts from the Daml Finance library (e.g. holding s or settlement instructions) are generally not signed by an operator, but just by provider and customer.

In theory there is also not really a need for the Service contracts to be signed by an operator, as each party involved can unilaterally choose to archive the service via the Terminate choice.

1 Like

To add to Matteo’s answer, there are two “operating models” for these service contracts:

(1) The centralized model: in this use case it is a requirement that a central party is in control over which parties can extend which services. This is done through the role contracts as mentioned, and only services with the operator’s signature would be considered valid by the app.

(2) The decentralized model: the usage of a central operator is entirely optional. There is nothing preventing a service provider to put themselves as the operator for a service. It’s up to the customer to accept the service under such conditions (they have to know and trust the service provider to be who they claim to be).

In many cases (1) is a requirement, but you are correct that (2) definitely scales better.

1 Like

Thinking about how to get rid of the operator from the Service’s keeping the requirement of having the operator as the root of trust. What about this approach?

Basically, we remove the operator as a signature of the Service and keep it as an observer in order to be able to terminate the Service. On the other hand, the creation of the Service (where the customer is involved) checks that the custodian has an active custodian role provided by the operator.

fetchByKey @Custodian.Role (operator, provider)

As far as we understand:

  1. The customer would have the same guarantees.
  2. The operator can terminate the Service’s.
  3. The operator loses visibility of operations (nonconsuming choices) on the Service’s.
  4. It scales better because the operator does not need to validate Service’s operations (nonconsuming choices)

We would appreciate a lot your input on this. Thanks!

module Workflow.Custody.Service where

import Daml.Finance.Interface.Account.Account qualified as Account (Controllers, Credit(..), R)
import Daml.Finance.Interface.Account.Factory qualified as Account (F, Create(..))
import Daml.Finance.Interface.Holding.Factory qualified as Holding (F)
import Daml.Finance.Interface.Holding.Base qualified as Holding (I)
import Daml.Finance.Interface.Types.Common.Types
  ( AccountKey(..), Id, InstrumentQuantity, PartiesMap )

import Workflow.Custody.Model qualified as Custody
import Workflow.Role.Custodian qualified as Custodian
import Workflow.Util (fetchAndArchive)

template Service
  with
    operator : Party
    provider : Party
    customer : Party
    accountFactoryCid : ContractId Account.F
    holdingFactoryCid : ContractId Holding.F
  where
    signatory provider, customer
    observer operator

    key (provider, customer) : (Party, Party)
    maintainer key._1

    nonconsuming choice RequestOpenAccount : ContractId Custody.OpenAccountRequest
      with
        id : Id
        description : Text
        controllers : Account.Controllers
        observers : PartiesMap
      controller customer
      do
        create Custody.OpenAccountRequest with ..

    nonconsuming choice OpenAccount : AccountKey
      with
        openAccountRequestCid : ContractId Custody.OpenAccountRequest
      controller provider
      do
        Custody.OpenAccountRequest{id; description; controllers; observers} <-
          fetchAndArchive openAccountRequestCid
        let account = AccountKey with custodian = provider; owner = customer; id
        exercise accountFactoryCid Account.Create with
          account
          description
          controllers
          holdingFactoryCid
          observers

        pure account

    nonconsuming choice RequestDeposit : ContractId Custody.DepositRequest
      with
        quantity : InstrumentQuantity
        account : AccountKey
      controller customer
      do
        create Custody.DepositRequest with ..

    nonconsuming choice Deposit : ContractId Holding.I
      with
        depositRequestCid : ContractId Custody.DepositRequest
      controller provider
      do
        Custody.DepositRequest{quantity; account} <- fetchAndArchive depositRequestCid
        (_, ref) <- fetchByKey @Account.R account
        exercise ref.cid Account.Credit with quantity

    choice Terminate : ()
      with
        actor : Party
      controller actor
      do
        assert $ actor == operator || actor == provider || actor == customer
        pure ()

template Offer
  with
    operator : Party
    provider : Party
    customer : Party
    accountFactoryCid : ContractId Account.F
    holdingFactoryCid : ContractId Holding.F
  where
    signatory provider
    observer customer

    choice Accept : ContractId Service
      controller customer
      do
        fetchByKey @Custodian.Role (operator, provider)
        create Service with operator; provider; customer; accountFactoryCid; holdingFactoryCid

    choice Decline : ()
      controller customer
      do pure ()

    choice Withdraw : ()
      controller provider
      do pure ()

template Request
  with
    customer : Party
    provider : Party
    operator : Party
  where
    signatory customer
    observer provider

    choice Cancel : ()
      controller customer
      do pure ()

    choice Reject : ()
      controller provider
      do pure ()

    choice Approve : ContractId Service
      with
        accountFactoryCid : ContractId Account.F
        holdingFactoryCid : ContractId Holding.F
      controller provider
      do
        fetchByKey @Custodian.Role (operator, provider)
        create Service with operator; provider; customer; accountFactoryCid; holdingFactoryCid

Full POC here: link to github

Hi @jvelasco.intellecteu,

I believe yours is a sensible approach and agree with your points 1 → 4.