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 8 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).

To start, download our latest release and extract the archive. The extracted archive has the following structure:

.
├── bin
├── daml
├── dars
├── examples
├── lib
└── VERSION
  • 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 contain sample configuration and script files for the Canton console (a REPL)
  • lib contains the Java executables (JARs) for running Canton

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
exit - Leave the console
help - Helpful console commands
mydomain - Manage domain 'mydomain'; type 'mydomain help' or 'mydomain help("<methodName>")' for more help
participant1 - Manage participant 'participant1'; type 'participant1 help' or 'participant1 help("<methodName>")' for more help
participant2 - Manage participant 'participant2'; type 'participant2 help' or 'participant2 help("<methodName>")' for more help
...

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 handles are participant1 and participant2, and a single synchronization domain, with the local handle 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 {

  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
      }

      server {
        public.port = 5018
        admin.port = 5019
      }
    }
  }

}

A participant node provides access to the 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 means to access the ledger. In the example configuration, participant1 provides this API on port 10011, and participant2 on port 10021. If these, or any of the other configured ports are taken on your machine, adjust the configuration accordingly. 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 smart contract code,
  4. configure the operational data of the participant, such as cryptographic keys, and
  5. run diagnostic commands.

Similarly, a domain also exposes an 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. Furthermore, a domain also exposes public services, to which the domain’s participants connect to.

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:

@ all start

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

@ mydomain start
@ participant1 start
@ participant2 start

Recall that the handles mydomain, participant1 and participant2 come from the configuration file. If you try to run one of the commands, you will get the expected error message that the node is already running. This only starts the nodes, but they do not yet know about each other. To see this, you can run:

@ status
getting status for all 1 domains and 2 participants

Domain id: mydomain::0105a7c88d29e4273eda4c711bf836945773f3d55da738d122b19f2fd2a7494640
Uptime: 2.713357s
Ports:
public: 6000
admin: 6001
Connected Participants:
None

Participant id: participant1::017edcf2026d690c61ed4b079ae29e0f3d935c644f3a6f72467abf93c4dc802a3f
Uptime: 2.184575s
...

or, equivalently:

@ mydomain status
@ participant1 status
@ participant2 status

For the moment, ignore the long hexadecimal strings that follow the node handles; 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.

@ connect(participant1, mydomain)
@ connect(participant2, mydomain)

Now, check the status again.

@ status
getting status for all 1 domains and 2 participants

Domain id: mydomain::01a5eec4ecbcb6abaed01fecf6c83dca49b2d8f23f0d75b0f83c341e24e804bd37
Uptime: 3m 19.055518s
Ports:
public: 6000
admin: 6001
Connected Participants:
participant1::01bb339896287063c8682fc821dcb4712a1da27cbf0783dc6b48963466090b0107
participant2::01e47a0cb60fde3aa1e3ef372ccfca7452cb85326ae58e64bdedb5f4f16ca98d7b

Participant id: participant1::01bb339896287063c8682fc821dcb4712a1da27cbf0783dc6b48963466090b0107
Uptime: 3m 18.419304s
...

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 ping(party_of(participant2))
Ping round trip time: 2662ms

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 kicks 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 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 get_id
Option[String] = Some("mydomain::01a5eec4ecbcb6abaed01fecf6c83dca49b2d8f23f0d75b0f83c341e24e804bd37")
@ participant1 get_id
Option[String] = Some("participant1::0104491f90f907d06789327172843320b27bb2f4c69357fcfded24d2ec68c3c7db")
@ participant2 get_id
Option[String] = Some("participant2::01f5e22b6d8aaa23df2bd9eb79562c10f4599831c3470d30d649159c305afeabce")

The human-readable strings in these unique identifiers are derived from the local handles. 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 transitively (e.g., if k1 is authorized by the namespace key ns, and k2 is authorized by k1, then k2 is also authorized by ns).

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:

@ enable_party(participant1, "Alice")
com.digitalasset.canton.identity.PartyId = PartyId(UniqueIdentifier(Identifier("Alice"), Namespace(Fingerprint("0104491f90f907d06789327172843320b27bb2f4c69357fcfded24d2ec68c3c7db"))))
@ enable_party(participant2, "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 participant holds the secret key of this namespace. You can check that the parties are now known to mydomain by running the following:

@ mydomain list_parties("Alice")
Seq[ListPartiesResponse.Result] = Vector(
Result(
"Alice::0104491f90f907d06789327172843320b27bb2f4c69357fcfded24d2ec68c3c7db",
"mydomain::01a5eec4ecbcb6abaed01fecf6c83dca49b2d8f23f0d75b0f83c341e24e804bd37",
"participant1::0104491f90f907d06789327172843320b27bb2f4c69357fcfded24d2ec68c3c7db",
Submission,
Ordinary
)
)
@ mydomain list_parties("Bob")
Seq[ListPartiesResponse.Result] = Vector(
Result(
"Bob::01f5e22b6d8aaa23df2bd9eb79562c10f4599831c3470d30d649159c305afeabce",
"mydomain::01a5eec4ecbcb6abaed01fecf6c83dca49b2d8f23f0d75b0f83c341e24e804bd37",
"participant2::01f5e22b6d8aaa23df2bd9eb79562c10f4599831c3470d30d649159c305afeabce",
Submission,
Ordinary
)
)

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. As mentioned in the beginning, Canton currently supports only 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 all_participants console macro:

@ all_participants upload_dar "dars/CantonExamples.dar"
DAR hash: d81bf8586b763dece276b485705e9a17c07d7600762d53783072280e4657d6be
DAR hash: d81bf8586b763dece276b485705e9a17c07d7600762d53783072280e4657d6be

To validate that the DAR has been uploaded, run:

@ participant1 list_dars
DAR hash: d81bf8586b763dece276b485705e9a17c07d7600762d53783072280e4657d6be, filename: CantonExamples
DAR hash: 6dbfc45cfb565bdfefe734b21d048ab796d7f79e5323bd5cb0babdd9782dc5e3, filename: PingPong
@ participant2 list_dars

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

Running DAML Scenarios

DAML supports scenarios, a convenient method for testing DAML smart contracts. Scenarios specify interactions that the different parties have with the shared ledger, and the outcomes of those transactions. A toy example scenario is given below:

painterOffersAndAliceAccepts = painterOffersAndHouseOwnerAccepts "Painter" "Alice" "Bank"

painterOffersAndHouseOwnerAccepts painter houseOwner bank =
  scenario do
    painter <- getParty painter
    houseOwner <- getParty houseOwner
    bank <- getParty bank
    let price = Amount {value = 100.0; currency = "USD"}
    iouId <- submit bank do
        create $ Iou with payer = bank; owner = houseOwner; amount = price
    offerId <- submit painter do
        create $ OfferToPaintHouseByPainter with
          painter = painter; houseOwner = houseOwner; bank = bank; amount = price
    painterIouId <- submit houseOwner do
      exercise offerId AcceptByOwner with iouId
    submit painter do 
      exercise painterIouId Call

We will now explain the main intuition behind the scenario. To understand the scenario in full, consult the entire source file, and also the DAML documentation. In the scenario, Alice owns a house that she wants painted. Alice gets a digital bank note (I-Owe-You, IOU) from her bank. The painter offers to paint Alice’s house, in exchange for such an IOU. Alice next 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 the painter. The painter then chooses to call the IOU, which creates a GetCash contract between him and the bank, which is a statement by the bank that allows the painter to do an off-ledger withdrawal of the specified amount.

Canton also supports running DAML scenarios in the console for testing purposes. Canton’s scenario runner assumes that all party identifiers in a scenario are just strings. In the example above, these were “Alice”, “Painter” and “Bank”. Then, the scenario runner searches all known parties at all participants to find a party whose human-readable name corresponds to the party identifier in the scenario, and also find a participant authorized to represent this party. Finally, it routes the appropriate Ledger API command to this participant. Thus, to run the above scenario, you will first need to provision another party, whose human-readable name is “Bank”. Let’s provision it at participant2.

@ enable_party(participant2,"Bank")
res3: com.digitalasset.canton.identity.PartyId = PartyId(
UniqueIdentifier(Identifier("Bank"), Namespace(Fingerprint("01f5e22b6d8aaa23df2bd9eb79562c10f4599831c3470d30d649159c305afeabce"))))

Next, you will load the scenarios from the DAR, and let the above scenario run. As a diagnostic tool, you can also list the available scenarios using scenario list. However, stick with the scenario from above for the moment, which is called Paint:painterOffersAndAliceAccepts, as you might run into problems with unknown parties otherwise.

@ scenario load("dars/CantonExamples.dar")
Seq[com.digitalasset.daml.lf.data.Ref.PackageId] = Vector(
  "19967d9c9220d11f31bba69c9983e01b18a041693653d926b11efed31fe315c5",
  "a5264aaa710fe11a94ac80d71efc1083997ed09f39047888ae1c1d90cc4a549a",
  "7220d9a38b8b7bd5a9b67149c40e242c488084fd499e0d1a0b5b97721c7933ba"
)
@ scenario list
List[String] = List(
"Test:testScenario",
"Divulgence:aliceDivulgesHerIouToBob",
"SafePaint:safePainterOffersAndAliceAcceptsWithCarol",
"SafePaint:safeTwoPainterOffers",
"SafePaint:safePainterOffersAndAliceAccepts",
"Paint:davidCreatesIou",
"Paint:painterOffersAndAliceAccepts",
"Paint:aliceOffersAndPainterAccepts",
"Paint:aliceCreatesIou",
"Paint:paintOfferAliceDavid",
"Paint:painterOffers",
"Swap:aliceAndBobSwapIous"
)
@ scenario run "Paint:painterOffersAndAliceAccepts"
scenario completed successfully

The output does not tell you much about the effects of executing the scenario. However, you can use further diagnostic commands to check these effects. Every Canton participant has a so-called private contract store (or PCS for short), which stores the contracts of all parties that the participant hosts. Canton allows you to inspect a participant’s PCS as follows.

@ participant1 pcs_search(domain="mydomain")

While the output of this command is raw, you can make the result’s display more digestible by adding filters to the search and processing the results using Scala.

For example, you can restrict the search to contract’s whose templates have the string “Iou” in their name, and use Scala to extract just the template names from the results:

@ (participant1 pcs_search(domain="mydomain", filterTemplate="Iou")).map(_._2.contract.template.qualifiedName)
List[com.digitalasset.daml.lf.data.Ref.QualifiedName] = List(QualifiedName(Iou, Iou), QualifiedName(Iou, Iou))
@ (participant2 pcs_search(domain="mydomain", filterTemplate="Iou")).map(_._2.contract.template.qualifiedName)
res10: List[com.digitalasset.daml.lf.data.Ref.QualifiedName] = List(
QualifiedName(Iou, GetCash),
QualifiedName(Iou, Iou),
QualifiedName(Iou, Iou)
)

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. 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. Follow the DAML documentation to learn how to program new contracts, or check out the DAML Marketplace to find existing ones for your needs.
  3. Use the Navigator for easy Web-based access and manipulation of your contracts.
  4. Read the requirements that Canton was built for to find out more about the properties of Canton.
  5. Read the architectural overview for more understanding of Canton concepts and internals.