Manage Domains

Permissioned Domains

Important

This feature is only available in Canton Enterprise

Canton as a network is an open virtual shared ledger. Whoever runs a Canton participant node is part of the same virtual shared ledger. However, the network itself is made up of domains that are used by participants to run the Canton protocol and communicate to their peers. Such domains can be open, allowing any participant with access to a sequencer node to enter and participate in the network. But domains can also be permissioned, where the operator of the domain topology managers needs to explicitly add the participant to the allow-list before the participant can register with a domain.

While the Canton architecture is designed to be resilient against malicious participants, there can never be a guarantee that the implementation of said architecture is absolutely secure. Therefore, it makes sense for most networks to impose control on which participant can be part of the network.

The first layer of control is given by securing access to the public api of the sequencers in the network. This can be done using standard network tools such as firewalls and virtual private networks.

The second layer of control is given by setting the appropriate configuration flag of the domain manager (or domain):

canton.domain-managers.domainManager1.topology.open = false

Assuming we have set up a domain with this flag turned off, the config for that particular domain would read:

@ val config = DomainConnectionConfig("mydomain", sequencer1.sequencerConnection)
config : DomainConnectionConfig = DomainConnectionConfig(
  domain = Domain 'mydomain',
  sequencerConnection = GrpcSequencerConnection(
    endpoints = http://127.0.0.1:15001,
    transportSecurity = false,
..

When a participant attempts to join the domain, it will be rejected:

@ participant1.domains.register(config)
ERROR com.digitalasset.canton.integration.EnterpriseEnvironmentDefinition$$anon$3 - Request failed for participant1.
  GrpcRequestRefusedByServer: FAILED_PRECONDITION/PARTICIPANT_IS_NOT_ACTIVE(9,d3047164): The participant is not yet active
  Request: RegisterDomain(DomainConnectionConfig(
  domain = Domain 'mydomain',
  sequencerConnection = GrpcSequencerConnection(endpoints = http://127.0.0.1:15001, transportSecurity = false, customTrustCertificates = None()),
  manualConnect = false,
  domainId ...
  CorrelationId: d3047164b1024f0a39e55d071ab54bc0
  Context: HashMap(participant -> participant1, test -> ManagePermissionedDomainsDocumentationManual, serverResponse -> Domain Domain 'mydomain' has rejected our on-boarding attempt, domain -> mydomain)
  Command ParticipantAdministration$domains$.register invoked from cmd10000006.sc:1

In order to allow the participant to join the domain, we must first actively enable it on the topology manager. We assume now that the operator of the participant extracts its id into a string:

@ val participantAsString = participant1.id.toProtoPrimitive
participantAsString : String = "PAR::participant1::1220535eecf89b29069440f66fd5cef432321f25fea9cbcb173953ab8d768f7d62dc"

and communicates this string to the operator of the domain topology manager:

@ val participantIdFromString = ParticipantId.tryFromProtoPrimitive(participantAsString)
participantIdFromString : ParticipantId = PAR::participant1::1220535eecf8...

This topology manager can now add the participant by enabling it:

@ domainManager1.participants.set_state(participantIdFromString, ParticipantPermission.Submission, TrustLevel.Ordinary)

Note that the participant is not active yet:

@ domainManager1.participants.active(participantIdFromString)
res5: Boolean = false

So far, what we’ve done with setting the state is to issue a “domain trust certificate”, where the domain topology manager declares that it trusts the participant enough to become a participant of the domain. We can inspect this certificate using:

@ domainManager1.topology.participant_domain_states.list(filterStore="Authorized").map(_.item)
res6: Seq[ParticipantState] = Vector(
  ParticipantState(
    From,
    domainManager1::12209d783e53...,
    PAR::participant1::1220535eecf8...,
    Submission,
    Ordinary
  )
)

In order to have the participant become active on the domain, we need to register the signing keys and the “domain trust certificate” of the participant. The certificate is generated by the participant automatically and sent to the domain during the initial handshake.

We can trigger that handshake again by attempting to reconnect to the domain again:

@ participant1.domains.reconnect_all()

Now, we can check that the participant is active:

@ domainManager1.participants.active(participantIdFromString)
res8: Boolean = true

We can also observe that we now have both sides of the domain trust certificate, the From and the To:

@ domainManager1.topology.participant_domain_states.list(filterStore="Authorized").map(_.item)
res9: Seq[ParticipantState] = Vector(
  ParticipantState(
    From,
    domainManager1::12209d783e53...,
    PAR::participant1::1220535eecf8...,
    Submission,
    Ordinary
  ),
  ParticipantState(
    To,
    domainManager1::12209d783e53...,
    PAR::participant1::1220535eecf8...,
    Submission,
    Ordinary
  )
)

Finally, the participant is healthy and can use the domain:

@ participant1.health.ping(participant1)
res10: Duration = 3479 milliseconds

Domain Rules

Every domain has its own rules in terms of what parameters are used by the participants while running the protocol. The participants obtain these parameters before connecting to the domain. They can be configured using the specific parameter section. An example would be:

init.domain-parameters {
  // example setting
  unique-contract-keys = yes
}

The full set of available parameters can be found in the scala reference documentation.

Dynamic domain parameters

In addition to the parameters that are specified in the configuration, some parameters can be changed at runtime (i.e., while the domain is running); these are called dynamic domain parameters.

A participant can get the current parameters on a domain it is connected to using the following command:

participant.topology.domain_parameters_changes.get_latest(mydomain.id)

A domain operator can update some of the parameters as follows:

mydomain.service.update_dynamic_domain_parameters(_.update(
  participantResponseTimeout = 10.seconds
))