Getting Started

Interested in Canton? This is the right place to start! You don’t need any prerequisite knowledge, and you will learn:

  1. how to install Canton and get it up and running in a simple test configuration,

  2. the main concepts of Canton,

  3. the main configuration options,

  4. some simple diagnostic commands on Canton,

  5. the basics of Canton identity management, and

  6. how to upload and execute new smart contract code.

Installation

Canton is a JVM application, and to run it natively, you will need Java 11 or higher installed on your system. Alternatively Canton is also available as a docker image (see Canton docker instructions). Otherwise, Canton is platform-agnostic, but we recommend you try it under Linux and OSX if possible as we currently only test those platforms. Under Windows, the Canton console output will be garbled unless you are running Windows 10 and you enable terminal colors (e.g., by running cmd.exe and then executing reg add HKCU\Console /v VirtualTerminalLevel /t REG_DWORD /d 1).

Note

Canton will also start and run under Java 8, but that configuration is unsupported. There are known bugs and Scala/Java incompatibilities in JRE 8 that affect Canton.

To start, download our community edition latest release and extract the archive. You can also use the enterprise edition if you have access to it. The extracted archive has the following structure:

.
├── bin
├── daml
├── dars
├── examples
├── lib
├── demo
└── ...
  • bin: contains the scripts for running Canton (canton under Unix-like systems and canton.bat under Windows)

  • daml: contains the source code of some sample smart contracts

  • dars: contains compiled and packaged code of the above contracts

  • examples: contains sample configuration and script files for the Canton console

  • lib: contains the Java executables (JARs) for running Canton

  • demo: contains everything needed for running the interactive Canton demo

This tutorial assumes you are running a Unix-like shell.

Starting Canton

While Canton supports a daemon mode for production purposes, in this tutorial we will use its console which is a built-in interactive read-evaluate-print loop (REPL). The REPL gives you an out-of-the-box interface to all Canton features. However, as it’s built using Ammonite, you also have the full power of Scala if you need to extend it with new scripts.

Navigate your shell to the directory where you extracted Canton. Then, run

bin/canton --help

to see the command line options that Canton supports. Next, run

bin/canton -c examples/01-simple-topology/simple-topology.conf

This starts the console, with the command line parameters specifying that Canton should use the configuration file examples/01-simple-topology/simple-topology.conf. Type help to see the available commands in the console.

   _____            _
  / ____|          | |
 | |     __ _ _ __ | |_ ___  _ __
 | |    / _` | '_ \| __/ _ \| '_ \
 | |___| (_| | | | | || (_) | | | |
  \_____\__,_|_| |_|\__\___/|_| |_|

  Welcome to Canton!
  Type `help` to get started. `exit` to leave.

@ help
Top-level Commands
------------------
exit - Leave the console
help - Help with console commands; type help("<command>") for detailed help for <command>

Generic Node References
-----------------------
domains - All domain nodes
nodes - All nodes
participants - All participant nodes
...

You can also get help for specific Canton objects and commands, like so:

@ help("participant1")
participant1
Manage participant 'participant1'; type 'participant1 help' or 'participant1 help("<methodName>")' for more help
@ participant1.help("start")
start
Start the instance

The Example Topology

To understand the basic elements of Canton, let’s briefly look at the configuration that you started the console with. It is written in the HOCON format and it is shown below. It specifies that you wish to run two participant nodes, whose local aliases are participant1 and participant2, and a single synchronization domain, with the local alias mydomain. It also specifies the storage backend that each node should use (in this tutorial we’re using in-memory storage), and the network ports for various services, which we will describe shortly.

canton {
  parameters {
    manual-start = yes
  }
  participants {

    participant1 {
      storage {
        type = memory
      }

      admin-api {
        port = 5012
      }

      ledger-api {
        port = 5011
      }
    }

    participant2 {
      storage {
        type = memory
      }

      admin-api {
        port = 5022
      }

      ledger-api {
        port = 5021
      }
    }
  }

  domains {
    mydomain {
      storage {
        type = memory
      }

      public-api.port = 5018
      admin-api.port = 5019

    }
  }

}

A participant node provides access to the global virtual Canton ledger to one or more Canton users, called parties. Under the hood, the participants synchronize the state of their parties’ contracts by running the Canton synchronization protocol. To run the protocol, the participants must connect to one or more synchronization domains, or just domains for short. In order to execute a transaction (a change that updates the shared contracts of several parties), there must exist a single domain to which all the parties’ participants are connected. In the remainder of this tutorial, you will construct the following network topology, that will enable the three parties Alice, Bob, and Bank to transact with each other.

Basic elements of Canton

The participant nodes provide their parties with a Ledger API as a mean to access the ledger. The parties can interact with the Ledger API manually, but will in practice use applications to handle the interactions for them and display the data in a user-friendly interface.

In addition to the Ledger API, each participant node also exposes an Admin API. The Admin API allows the administrator (that is, you!) to:

  1. manage the participant’s connections to domains,

  2. add or remove parties to be hosted at the participant,

  3. upload new Daml archives,

  4. configure the operational data of the participant, such as cryptographic keys, and

  5. run diagnostic commands.

The domain node exposes a Public API that is used by participant nodes to communicate with the synchronization domain. This must be accessible from where the participant nodes are hosted.

Similar to the participant node, a domain also exposes an Admin API for administration services. You can use these to enable or disable participants within a domain, for example. The console provides access to the Admin APIs of the configured participants and domains.

Note

Canton’s Admin APIs must not be confused with the admin package of the Ledger API. The admin package of the Ledger API provides services for managing parties and packages on any Daml participant. Canton’s Admin APIs allows you to administrate Canton-based nodes. Both, the participant and the domain nodes expose an Admin API with partially overlapping functionality.

Furthermore, participants and domains communicate with each other through the Public API.

As you can see, nothing in the configuration specifies that our participant1 and participant2 should connect to mydomain. Canton connections are not statically configured – they are added dynamically instead. In fact, when you started the console, even the nodes are not started automatically, as you can also use the console to attach to nodes running in daemon mode. So first, let’s get the nodes up and running, and then you will connect the participants to the domain.

Starting and Connecting The Nodes

The console provides a convenient macro to start all configured nodes:

@ nodes.local start

You could have also done this manually, by starting each node separately, like so:

@ mydomain start
@ participant1 start
@ participant2 start

Recall that the aliases mydomain, participant1 and participant2 come from the configuration file. If you try to start the node more than once, the second and further calls will have no effect.

This only starts the nodes, but they do not yet know about each other. To see this, you can run:

@ health.status
Status for Domain 'mydomain':
Domain id: mydomain::01c8c7795ef6c984ef056fca33983d12f769c64cfd9d24cfa0f080df386ddb3810
Uptime: 7.088278s
Ports:
        public: 5018
        admin: 5019
Connected Participants: None
Sequencer: active

Status for Participant 'participant1':
Participant id: PAR::participant1::015456bdb44cfe7ddef9e81b0023347cee9a0ae48247bc547e1a8121836b76fd5c
Uptime: 4.165019s
Ports:
        ledger: 5011
        admin: 5012
Connected Domains: None
Active: true

Status for Participant 'participant2':
Participant id: PAR::participant2::0143a26fac5845f265ee1520a8857a5faabc37d2394d1c37db98acbd94e5af9996
Uptime: 3.590513s
Ports:
        ledger: 5021
        admin: 5022
Connected Domains: None
Active: true

or, equivalently:

@ mydomain.health.status
@ participant1.health.status
@ participant2.health.status

For the moment, ignore the long hexadecimal strings that follow the node aliases; these have to do with Canton’s identities, which we will explain shortly. As you see, the domain doesn’t have any connected participants, and the participants are also not connected to any domains. Proceed to connect the participants to the domain.

@ participant1.domains.connect_local(mydomain)
@ participant2.domains.connect_local(mydomain)

Now, check the status again.

@ health.status
Status for Domain 'mydomain':
Domain id: mydomain::01c8c7795ef6c984ef056fca33983d12f769c64cfd9d24cfa0f080df386ddb3810
Uptime: 3m 59.090539s
Ports:
        public: 5018
        admin: 5019
Connected Participants:
        PAR::participant1::015456bd...
        PAR::participant2::0143a26f...
Sequencer: Some(SequencerHealthStatus(isActive = true))

Status for Participant 'participant1':
Participant id: PAR::participant1::015456bdb44cfe7ddef9e81b0023347cee9a0ae48247bc547e1a8121836b76fd5c
Uptime: 3m 56.149909s
Ports:
        ledger: 5011
        admin: 5012
Connected Domains:
        mydomain::01c8c779...
...

As you can read from the status, both participants are now connected to the domain. You can test the connection with the following diagnostic command, inspired by the ICMP ping:

@ participant1.health.ping(participant2)
concurrent.duration.Duration = 933 milliseconds

If everything is set up correctly, this will report the “roundtrip time” between the Ledger APIs of the two participants. On the first attempt, this time will probably be several seconds, as the JVM is warming up. It’ll decrease significantly on the next attempt, and also once again after JVM’s just-in-time compilation really kicked in (though this is by default only after 10000 iterations!).

In fact, you have just executed your first smart contract transaction over Canton. Every participant node also has an associated built-in party that can take part in smart contract interactions. The ping command uses a particular smart contract that is by default pre-installed on every Canton participant. In fact, the command uses the Admin API to access a pre-installed application, which then issues Ledger API commands operating on this smart contract.

While you could use the built-in party of your participant for all smart contract interactions of your applications, it’s often useful to have more parties than participants. For example, you might want to run a single participant node within a company, with each employee being a separate party. For this, you need to be able to provision parties.

Canton Identities and Provisioning Parties

In Canton, all identities: of parties, participants, and domains, are represented by a unique identifier. A unique identifier consists of two components: a human-readable string, and the fingerprint of a public key. When displayed in Canton, the components are separated by a double colon. You can see the identifiers of the participants and the domains by running the following in the console:

@ mydomain.id
com.digitalasset.canton.DomainId = mydomain::01a5eec4ecbc...
@ participant1.id
ParticipantId = PAR::participant1::0104491f90f9...
@ participant2.id
ParticipantId = PAR::participant12::01f5e22b6d8a...

The human-readable strings in these unique identifiers are derived from the local aliases. The public key, which is called a namespace, is the root of trust for this identifier. This means that in Canton, any action taken in the name of this identity must be either:

  • signed by this namespace key, or

  • signed by a key that is authorized by the namespace key to speak in the name of this identity, either directly or indirectly (e.g., if k1 can speak in the name of k2 and k2 can speak in the name of k3, then k1 can also speak in the name of k3).

In Canton, it’s perfectly possible to have several unique identifiers that share the same namespace - you’ll see examples of that shortly. However, if you look at the identities resulting from your last console commands, you will see that they belong to different namespaces. By default, each Canton node generates a fresh asymmetric key pair (the secret and public keys) for its own namespace when first started. The key is then stored in the storage, and reused later in case the storage is persistent (recall that simple-topology.conf uses memory storage, which is not persistent).

You will next create a couple of parties, Alice and Bob. Alice will be hosted at participant1, and her identity will use the namespace of participant1. Similarly, Bob will use participant2. Canton provides a handy macro for this:

@ val alice = participant1.parties.enable("Alice")
alice: PartyId = Alice::0104491f90f9...
@ val bob = participant2.parties.enable("Bob")

This creates the new parties in the participants’ respective namespaces. Furthermore, it notifies the domain of the new parties, and allows the participants to submit commands on the behalf of those parties. The domain allows this since, e.g., Alice’s unique identifier uses the same namespace as participant1 and participant1 holds the secret key of this namespace. You can check that the parties are now known to mydomain by running the following:

@ mydomain.parties.list("Alice")
Seq[ListPartiesResult] = List(
  ListPartiesResult(
    party = Alice::0104491f90f9...,
    participants = List(
      ParticipantDomains(
        participant = PAR::participant1::0104491f90f9...,
        domains = List(DomainPermission(domain = mydomain::01a5eec4ecbc..., permission = Submission))
      )
    )
  )
)
@ mydomain.parties.list("Bob")
Seq[ListPartiesResult] = List(
  ListPartiesResult(
    party = Bob::01f5e22b6d8a...,
    participants = List(
      ParticipantDomains(
        participant = PAR::participant2::01f5e22b6d8a...,
        domains = List(DomainPermission(domain = mydomain::01a5eec4ecbc..., permission = Submission))
      )
    )
  )
)

Provisioning Smart Contract Code

To create a contract between Alice and Bob, you will first have to provision the contract’s code to both of their hosting participants. Canton supports smart contracts written in Daml. A Daml contract’s code is specified using a Daml contract template; an actual contract is then a template instance. Daml templates are packaged into Daml archives, or DARs for short. For this tutorial, use the pre-packaged dars/CantonExamples.dar file. To provision it to both participant1 and participant2, you can use the participants.all console macro:

@ participants.all.dars.upload("dars/CantonExamples.dar")
Map[com.digitalasset.canton.console.ParticipantReference, String] = Map(
 Participant 'participant1' -> "01b8927bab3774f44cae76a20d1249b3d39e1250a2b994da1779c4e6f5bb9257a2",
 Participant 'participant2' -> "01b8927bab3774f44cae76a20d1249b3d39e1250a2b994da1779c4e6f5bb9257a2"
)

To validate that the DAR has been uploaded, run:

@ participant1.dars.list()
Seq[com.digitalasset.canton.participant.admin.v0.DarDescription] = Vector(
  DarDescription(
    hash = "1220c5a4ac582223dcf2a59d323e474b3411df96f39cfa1304e2739ab7ca97f3b6b8",
    name = "AdminWorkflows"
  ),
  DarDescription(
    hash = "122066789eacce3df0d21da25a5adcb9f6b06244a7cf55328a8e4d460cfb01d868e7",
    name = "CantonExamples"
  )
)
@ participant2.dars.list()

Now you are finally ready to actually start running smart contracts using Canton.

Executing Smart Contracts

To trigger Daml contract execution, you must send the appropriate commands over the Ledger API. The Canton console gives you interactive access to this API, together with some utilities that can be useful for experimentation. Additionally, you will likely want to use the Scala Codegen to get Scala interfaces for manipulating the smart contract commands.

Let’s execute a simple scenario modeled by Daml contracts. In the scenario, Alice and Bob will agree that Bob has to paint her house. In exchange, Bob will get a digital bank note (I-Owe-You, IOU) from Alice, issued by a bank. Canton ships with this scenario pre-packaged in the Scala script examples/scripts/PaintScenario.sc. If this is the correct path to the script, you can import and execute it like so.

@ import $file.examples.scripts.PaintScenario
@ PaintScenario.run(participant1, participant2, alice, bob)

If your path differs, adjust the import; use, e.g., $file.^.scripts.PaintScenario if the path is ../scripts/PaintScenario.sc. The source of the scenario is given below. Canton ships with the generated Scala interfaces for the contracts in CantonExamples.dar, and the script starts by importing some of them (for Iou and Paint Daml modules). It also imports some Canton utilities for working with the Ledger API (DecodeUtil), as well as the type of participant references that it takes as parameters.

import com.digitalasset.canton.examples.{Iou, Paint}
import com.digitalasset.canton.ledger.api.client.DecodeUtil
import com.digitalasset.canton.console.LocalParticipantReference

def run(
    participant1: LocalParticipantReference,
    participant2: LocalParticipantReference,
    alice: PartyId, bob: PartyId) = {

  val bank = participant2.parties.enable("Bank")

  utils.retry_until_true() {
    mydomain.parties.list("Bank").nonEmpty
  }

  val amount = Iou.Amount(100, "USD")
  val iouCreateCmd = Iou.Iou(
    payer = bank.toPrim,
    owner = alice.toPrim,
    amount = amount,
    viewers = List.empty
  ).create.command
  val iouTx = participant2.ledger_api.commands.submit_flat(Seq(bank), Seq(iouCreateCmd))
  val iou = DecodeUtil.decodeAllCreated(Iou.Iou)(iouTx).head

  val offerCreateCmd = Paint.OfferToPaintHouseByPainter(
    houseOwner = alice.toPrim,
    painter = bob.toPrim,
    bank = bank.toPrim,
    amount = amount
  ).create.command

  val offer = DecodeUtil.decodeAllCreated(Paint.OfferToPaintHouseByPainter)(
    participant2.ledger_api.commands.submit_flat(Seq(bob), Seq(offerCreateCmd))
  ).head

  val acceptanceCmd = offer.contractId.exerciseAcceptByOwner(
    actor=alice.toPrim,
    iouId=iou.contractId
  ).command
  val bobIou = DecodeUtil.decodeAllCreatedTree(Iou.Iou)(
    participant1.ledger_api.commands.submit(Seq(alice), Seq(acceptanceCmd))
  ).head

  val callCmd = bobIou.contractId.exerciseCall(bob.toPrim).command
  val called = DecodeUtil.decodeAllCreated(Iou.GetCash)(
    participant2.ledger_api.commands.submit_flat(Seq(bob), Seq(callCmd))
  ).head
}

To fully understand the scenario, consult the Daml sources of these modules, and also the Daml documentation. Here, we will just provide the main intuition behind it. We start by adding a party to represent the bank. The retry_until_true in the line after is used as a synchronization point. It ensures that the domain will see the creation of the Bank party before forwarding any transactions that involve the Bank.

Note

Canton alleviates most synchronization issues when writing Daml applications. Nevertheless, Canton is a concurrent, distributed system. Creating the Bank party is an operation local to participant2, and mydomain becomes aware of the party with a delay (see Identity Transactions for more detail). Processing and network delays also exist for all other operations that affect multiple nodes, though everyone sees the operations on the domain in the same order. When you execute commands interactively, the delays are usually too small to notice. However, if you’re programming Canton scripts or applications that talk to multiple nodes, you might need some form of manual synchronization. The retry_until_true is a simple handy utility for this purpose.

Next, the Bank issues a digital bank note (I-Owe-You, IOU) to Alice (the iou contract). Bob offers to paint Alice’s house, in exchange for such an IOU (the offer contract). Alice then accepts the offer. As a consequence, which is not visible from the scenario, but is specified in the Daml contract, Alice transfers her IOU to Bob (resulting in the bobIou). Bob then chooses to call the IOU, which creates a GetCash contract (called) between him and the bank. This contract is a statement by the bank that allows the painter to do an off-ledger withdrawal of the specified amount.

The output does not tell you much about the effects of executing the scenario. We can examine these effect by looking at the contracts that the participants store. Daml contracts can be active, meaning that they can be used in commands, or archived, meaning that they have been used up previously. We can get the so-called active contract set using the corresponding Ledger API service:

@ participant1.ledger_api.acs.of_all()
Seq[com.digitalasset.canton.admin.api.client.commands.LedgerApiTypeWrappers.WrappedCreatedEvent] = List(
  WrappedCreatedEvent(
    event = CreatedEvent(
      eventId = "#1220941a6e30a4e9d8e55f859f8c3b04e744ebdc30c80a80959fa97d414a8fe743e4:3",
      contractId = "002e4be9f18186df2d1580e06aa44fcb93fe7b38cb1f45e8e9e65e87801f9076f9ca001220251639edd25e6815d3d817d3bc2503ac1d808a64417977bb42dde414996008fe",
      templateId = Some(value = Identifier(packageId = "56027cc29f3c3e421de25d0f60908a81a88d4bb9d175f52b05bac0da5e3db7f7", moduleName = "Paint", entityName = "PaintHouse")),
      contractKey = None,
      createArguments = Some(
        value = Record(
          recordId = Some(value = Identifier(packageId = "56027cc29f3c3e421de25d0f60908a81a88d4bb9d175f52b05bac0da5e3db7f7", moduleName = "Paint", entityName = "PaintHouse")),
...

The command returns a sequence of wrapped CreatedEvent’s. This Ledger API data type represents the event of a contract’s creation. The wrapper provides convenient functions to manipulate the CreatedEvents in the Canton console:

@ participant2.ledger_api.acs.of_all().map(x => (x.templateId, x.arguments))
Seq[(String, Map[String, Any])] = ListBuffer(
  (
    "Iou.GetCash",
    Map(
      "payer" -> "Bank::01f5e22b6d8aaa23df2bd9eb79562c10f4599831c3470d30d649159c305afeabce",
      "owner" -> "Painter::01f5e22b6d8aaa23df2bd9eb79562c10f4599831c3470d30d649159c305afeabce",
      "value" -> "100.0000000000",
      "currency" -> "USD"
    )
  ),
  (
    "Paint.PaintHouse",
    Map(
      "painter" -> "Painter::01f5e22b6d8aaa23df2bd9eb79562c10f4599831c3470d30d649159c305afeabce",
      "houseOwner" -> "Alice::0104491f90f907d06789327172843320b27bb2f4c69357fcfded24d2ec68c3c7db"
    )
  )
)

You can use Scala’s sequence operations to filter and re-arrange the output, such as filtering for a particular template (“Iou.GetCash”):

@ participant2.ledger_api.acs.of_all().filter(_.templateId == "Iou.GetCash").map(_.arguments)
Seq[Map[String, Any]] = ListBuffer(
  Map(
    "payer" -> "Bank::01f5e22b6d8aaa23df2bd9eb79562c10f4599831c3470d30d649159c305afeabce",
    "owner" -> "Painter::0104491f90f907d06789327172843320b27bb2f4c69357fcfded24d2ec68c3c7db",
    "value" -> "100.0000000000",
    "currency" -> "USD"
  )
)

While the functions in the Console can be handy for development purposes, the Daml SDK provides you with much more convenient tools to inspect the ledger content. All these tools work work against the Ledger API. You can either use the browser based Navigator, the console version of it Navigator or you can write your own application, as explained in the Daml SDK tutorial.

Automation using bootstrap scripts

To avoid having to manually complete routine tasks such as starting nodes or provisioning parties each time Canton is started, a bootstrap script can be configured. Bootstrap scripts are automatically run after Canton has started and can contain any valid Canton CLI commands. They are specified with the --bootstrap option pointing to the bootstrap script location when starting Canton. By convention they have a .canton file ending.

For example, the corresponding bootstrap script to start the nodes, connect the participant nodes to the local domain and ping participant1 from participant2 (see Starting and Connecting The Nodes) is:

nodes.local start
participant1.domains.connect_local(mydomain)
participant2.domains.connect_local(mydomain)

utils.retry_until_true() {
    participant2.domains.active("mydomain")
}
participant2.health.ping(participant1)

Note how we again use retry_until_true to add a manual synchronization point, making sure that participant2 is registered, before proceeding to ping participant1.

Privacy

Before we finish this section, there is one key observation we want to share with you. Assuming you previously executed the participants.parties.enable steps exactly as specified in the tutorial, you will notice that participant2 has one instance of the GetCash template in its store, while participant1 has none.

This is because the GetCash contract produced by our scenario involves only the painter and the bank, both of whom are hosted only at participant2. Canton’s synchronization protocol ensures that participant1 never receives any data about this cash contract. Furthermore, while the node running mydomain does receive this data, the data is encrypted and mydomain cannot read it.

What Next?

You are now ready to start using Canton for serious tasks. If you want to develop a Daml application and run it on Canton, we recommend the following resources:

  1. Install the Daml SDK to get access to the Daml IDE and other tools, such as the Navigator.

  2. Run through the Daml SDK getting-started example to learn how to build your own Daml applications on Canton.

  3. Follow the Daml documentation to learn how to program new contracts, or check out the Daml Marketplace to find existing ones for your needs.

  4. Use the Navigator for easy Web-based access and manipulation of your contracts.

If you want to understand more about Canton:

  1. Read the requirements that Canton was built for to find out more about the properties of Canton.

  2. Read the architectural overview for more understanding of Canton concepts and internals.

If you want to deploy your own Canton nodes, consult the examples/03-advanced-configuration directory for a set of great starting points.