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.
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.
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.
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:
The customer would have the same guarantees.
The operator can terminate the Service’s.
The operator loses visibility of operations (nonconsuming choices) on the Service’s.
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