Getting Started¶
Interested in Canton? This is the right place to start! You don’t need any prerequisite knowledge, and you will learn:
- how to install Canton and get it up and running in a simple test configuration
- the main concepts of Canton
- the main configuration options
- some simple diagnostic commands on Canton
- the basics of Canton identity management
- how to upload and execute new smart contract code
Installation¶
Canton is a JVM application. To run it natively you need Java 11 or higher installed on your system. Alternatively Canton is available as a docker image (see Canton docker instructions).
Otherwise Canton is platform-agnostic, but we recommend you try it under Linux and macOS 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 community edition latest release and extract the archive, or use the enterprise edition if you have access to it.
The extracted archive has the following structure:
.
├── bin
├── daml
├── dars
├── demo
├── deployment
├── drivers (enterprise)
├── examples
├── lib
└── ...
bin
: contains the scripts for running Canton (canton
under Unix-like systems andcanton.bat
under Windows)daml
: contains the source code for some sample smart contractsdars
: contains the compiled and packaged code of the above contractsdemo
: contains everything needed to run the interactive Canton demodeployment
: contains a few example deployments to cloud or dockerexamples
: contains sample configuration and script files for the Canton consolelib
: contains the Java executables (JARs) needed to run 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, a built-in interactive read-evaluate-print loop (REPL). The REPL gives you an out-of-the-box interface to all Canton features. In addition, as it’s built using Ammonite, you also have the full power of Scala if you need to extend it with new scripts. As such, any valid Scala expression can be typed inside the console:
@ Seq(1,2,3).map(_ * 2)
res1: Seq[Int] = List(2, 4, 6)
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 using the configuration file examples/01-simple-topology/simple-topology.conf
.
You will see the banner on your screen
_____ _
/ ____| | |
| | __ _ _ __ | |_ ___ _ __
| | / _` | '_ \| __/ _ \| '_ \
| |___| (_| | | | | || (_) | | | |
\_____\__,_|_| |_|\__\___/|_| |_|
Welcome to Canton!
Type `help` to get started. `exit` to leave.
Type help
to see the available commands in the console:
@ help
Top-level Commands
------------------
exit - Leave the console
help - Help with console commands; type help("<command>") for detailed help for <command>
Generic Node References
-----------------------
domainManagers - All domain manager nodes (.all, .local, .remote)
..
You can also get help for specific Canton objects and commands:
@ 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 this starting configuration.
It is written in the HOCON format as 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 {
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
}
}
// enable ledger_api commands for our getting started guide
features.enable-testing-commands = yes
}
To run the protocol, the participants must connect to one or more synchronization domains (domains for short). To execute a transaction (a change that updates the shared contracts of several parties), all the parties’ participant nodes must be connected to the same domain. In the remainder of this tutorial, you will construct a network topology that will enable the three parties Alice, Bob, and Bank to transact with each other, as shown here:
The participant nodes provide their parties with a Ledger API as a means to access the ledger. The parties can interact with the Ledger API manually using the console, but in practice these parties use applications to handle the interactions 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:
- manage the participant node’s connections to domains
- add or remove parties to be hosted at the participant node
- upload new Daml archives
- configure the operational data of the participant, such as cryptographic keys
- 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 node also exposes an Admin API for administration services. You can use these to manage keys, set domain parameters and enable or disable participant nodes 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, participant and domain nodes communicate with each other through the Public API. The participants do not communicate with each other directly, but are free to connect to as many domains as they desire.
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. So first, let’s connect the participants to the domain.
Connecting The Nodes¶
Using the console we can run commands on each of the configured (participant or domain) nodes. As such, we can check the health of a node using the health.status command:
@ health.status
res5: EnterpriseCantonStatus = Status for Domain 'mydomain':
Domain id: mydomain::1220c655a824717c42a5f15010208089b3f1d3699e71dc8d3c7bea69c529dffb8a99
Uptime: 4.661359s
Ports:
admin: 30037
public: 30036
Connected Participants: None
Sequencer: SequencerHealthStatus(isActive = true)
Status for Participant 'participant1':
Participant id: PAR::participant1::1220affdac91a0fb0835b644274f25e51b2fca40884ef011ee11a2707dbf022bdce5
Uptime: 3.622545s
Ports:
ledger: 30032
admin: 30033
Connected domains: None
Unhealthy domains: None
Active: true
Status for Participant 'participant2':
Participant id: PAR::participant2::12206638e7c64e714f04285d11c61551005cfa9347330f24047e09ba3a8e059b44e1
Uptime: 2.401039s
Ports:
ledger: 30034
admin: 30035
Connected domains: None
Unhealthy domains: None
Active: true
We can do this also individually on each node. As an example, to query the status of participant1
:
@ participant1.health.status
res6: com.digitalasset.canton.health.admin.data.NodeStatus[com.digitalasset.canton.health.admin.data.ParticipantStatus] = Participant id: PAR::participant1::1220affdac91a0fb0835b644274f25e51b2fca40884ef011ee11a2707dbf022bdce5
Uptime: 3.754614s
Ports:
ledger: 30032
admin: 30033
Connected domains: None
Unhealthy domains: None
Active: true
or for the domain:
@ mydomain.health.status
res7: com.digitalasset.canton.health.admin.data.NodeStatus[mydomain.Status] = Domain id: mydomain::1220c655a824717c42a5f15010208089b3f1d3699e71dc8d3c7bea69c529dffb8a99
Uptime: 4.950557s
Ports:
admin: 30037
public: 30036
Connected Participants: None
Sequencer: SequencerHealthStatus(isActive = true)
Recall that the aliases mydomain
, participant1
and participant2
come from the configuration file.
By default, Canton will start and initialize the nodes automatically. This
behavior can be overridden using the --manual-start
command line flag or appropriate configuration settings.
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.
To connect the participants to the domain:
@ participant1.domains.connect_local(mydomain)
@ participant2.domains.connect_local(mydomain)
Now, check the status again:
@ health.status
res10: EnterpriseCantonStatus = Status for Domain 'mydomain':
Domain id: mydomain::1220c655a824717c42a5f15010208089b3f1d3699e71dc8d3c7bea69c529dffb8a99
Uptime: 7.346305s
Ports:
admin: 30037
public: 30036
Connected Participants:
PAR::participant1::1220affdac91...
PAR::participant2::12206638e7c6...
Sequencer: SequencerHealthStatus(isActive = true)
Status for Participant 'participant1':
Participant id: PAR::participant1::1220affdac91a0fb0835b644274f25e51b2fca40884ef011ee11a2707dbf022bdce5
Uptime: 6.298716s
Ports:
..
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)
res11: Duration = 289 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. This will decrease significantly on the next attempt, and decrease again after JVM’s just-in-time compilation kicks in (by default this is after 10000 iterations).
You have just executed your first smart contract transaction over Canton.
Every participant node 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.
In theory, you could use your participant node’s built-in party for all your application’s smart contract interactions, but 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, the identity of each party, participant, or domain is 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
res12: DomainId = mydomain::1220c655a824...
@ participant1.id
res13: ParticipantId = PAR::participant1::1220affdac91...
@ participant2.id
res14: ParticipantId = PAR::participant2::12206638e7c6...
The human-readable strings in these unique identifiers are derived from the local aliases by default, but can be set to any string of your choice. 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 ofk2
andk2
can speak in the name ofk3
, thenk1
can also speak in the name ofk3
).
In Canton, it’s 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).
Creating Parties¶
You will next create two 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::1220affdac91...
@ val bob = participant2.parties.enable("Bob")
bob : PartyId = Bob::12206638e7c6...
This creates the new parties in the participants’ respective namespaces.
It also notifies the domain of the new parties and allows the participants to submit commands on 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")
res17: Seq[ListPartiesResult] = Vector(
ListPartiesResult(
party = Alice::1220affdac91...,
participants = Vector(
ParticipantDomains(
participant = PAR::participant1::1220affdac91...,
domains = Vector(
DomainPermission(domain = mydomain::1220c655a824..., permission = Submission)
)
)
)
)
)
and the same for Bob:
@ mydomain.parties.list("Bob")
res18: Seq[ListPartiesResult] = Vector(
ListPartiesResult(
party = Bob::12206638e7c6...,
participants = Vector(
ParticipantDomains(
participant = PAR::participant2::12206638e7c6...,
domains = Vector(
DomainPermission(domain = mydomain::1220c655a824..., permission = Submission)
)
)
)
)
)
Extracting Identifiers¶
Canton identifiers can be long strings. They are normally truncated for convenience. However, in some cases we do have to extract these identifiers so they can be shared through other channels. As an example, if you have two participants that run in completely different locations, without a shared console, then you can’t ping as we did before:
@ participant1.health.ping(participant2)
..
Instead, extract the participant id of one node:
@ val extractedId = participant2.id.toProtoPrimitive
extractedId : String = "PAR::participant2::12206638e7c64e714f04285d11c61551005cfa9347330f24047e09ba3a8e059b44e1"
This id can then be shared with the other participant, who in turn can parse the id back into an appropriate object:
@ val p2Id = ParticipantId.tryFromProtoPrimitive(extractedId)
p2Id : ParticipantId = PAR::participant2::12206638e7c6...
And subsequently, this id can be used to ping as well:
@ participant1.health.ping(p2Id)
res22: Duration = 271 milliseconds
This also works for party identifiers:
@ val aliceAsStr = alice.toProtoPrimitive
aliceAsStr : String = "Alice::1220affdac91a0fb0835b644274f25e51b2fca40884ef011ee11a2707dbf022bdce5"
@ val aliceParsed = PartyId.tryFromProtoPrimitive(aliceAsStr)
aliceParsed : PartyId = Alice::1220affdac91...
Generally, a Canton identity boils down to a UniqueIdentifier
and the context in which
this identifier is used. This allows you to directly access the identifier serialization:
@ val p2UidString = participant2.id.uid.toProtoPrimitive
p2UidString : String = "participant2::12206638e7c64e714f04285d11c61551005cfa9347330f24047e09ba3a8e059b44e1"
@ val p2FromUid = ParticipantId(UniqueIdentifier.tryFromProtoPrimitive(p2UidString))
p2FromUid : ParticipantId = PAR::participant2::12206638e7c6...
Provisioning Smart Contract Code¶
To create a contract between Alice and Bob, you must first 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
bulk operator:
@ participants.all.dars.upload("dars/CantonExamples.dar")
res27: Map[com.digitalasset.canton.console.ParticipantReference, String] = Map(
Participant 'participant1' -> "12209e52c9e72adfef78822430f5282fd8c469c83a16b5d8443b6a5299a70e58af9f",
Participant 'participant2' -> "12209e52c9e72adfef78822430f5282fd8c469c83a16b5d8443b6a5299a70e58af9f"
)
The bulk operator allows you to run certain commands on a series of nodes. Canton supports the bulk operators on
the generic nodes
:
@ nodes.local
res28: Seq[com.digitalasset.canton.console.LocalInstanceReference] = ArraySeq(Participant 'participant1', Participant 'participant2', Domain 'mydomain')
or on the specific node type:
@ participants.all
res29: Seq[com.digitalasset.canton.console.ParticipantReference] = List(Participant 'participant1', Participant 'participant2')
Allowed suffixes are .local
, .all
or .remote
, where the remote refers to remote nodes,
which we won’t use here.
To validate that the DAR has been uploaded, run:
@ participant1.dars.list()
res30: Seq[com.digitalasset.canton.participant.admin.v0.DarDescription] = Vector(
DarDescription(
hash = "12209e52c9e72adfef78822430f5282fd8c469c83a16b5d8443b6a5299a70e58af9f",
name = "CantonExamples"
),
DarDescription(
hash = "122076b9f43ce75c4dc97cfad8b1aa3daa88df1c818340c51adfe12e4d7761bbfcc0",
name = "AdminWorkflowsWithVacuuming"
)
)
and on the second participant, run:
@ participant2.dars.list()
res31: Seq[com.digitalasset.canton.participant.admin.v0.DarDescription] = Vector(
DarDescription(
hash = "12209e52c9e72adfef78822430f5282fd8c469c83a16b5d8443b6a5299a70e58af9f",
name = "CantonExamples"
),
DarDescription(
hash = "122076b9f43ce75c4dc97cfad8b1aa3daa88df1c818340c51adfe12e4d7761bbfcc0",
name = "AdminWorkflowsWithVacuuming"
)
)
One important observation is that you cannot list the uploaded DARs on the domain mydomain
. You
will simply get an error if you run mydomain.dars.list()
.
This is due the fact that the domain does not know anything about Daml or smart contracts. All the contract code
is only executed by the involved participants on a need to know basis and needs to be explicitly
enabled by them.
Now you are ready to actually start running smart contracts using Canton.
Executing Smart Contracts¶
Let’s start by looking at some smart contract code. In our example, we’ll have three parties, Alice, Bob and the Bank. 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.
First, we need to add the Bank as a party:
@ val bank = participant2.parties.enable("Bank", waitForDomain = DomainChoice.All)
bank : PartyId = Bank::12206638e7c6...
You might have noticed that we’ve added a waitForDomain
argument here. This is necessary to force some
synchronisation between the nodes to ensure that the new party is known within the distributed system before it is used.
Note
Canton alleviates most synchronization issues when interacting with Daml contracts. Nevertheless, Canton is a
concurrent, distributed system. All operations happen asynchronously.
Creating the Bank
party is an operation local to participant2
, and mydomain
becomes aware of the
party with a delay (see Topology 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.
Most Canton console commands have some form of synchronisation to simplify your life and sometimes,
using utils.retry_until_true(...)
is a handy solution.
The corresponding Daml contracts that we are going to use for this example are:
module Iou where
import Daml.Script
data Amount = Amount {value: Decimal; currency: Text} deriving (Eq, Ord, Show)
amountAsText (amount : Amount) : Text = show amount.value <> amount.currency
template Iou
with
payer: Party
owner: Party
amount: Amount
viewers: [Party]
where
ensure (amount.value >= 0.0)
signatory payer
observer owner
observer viewers
choice Call : ContractId GetCash
controller owner
do
create GetCash with payer; owner; amount
choice Transfer : ContractId Iou
with
newOwner: Party
controller owner
do
create this with owner = newOwner; viewers = []
choice Share : ContractId Iou
with
viewer : Party
controller owner
do
create this with viewers = (viewer :: viewers)
module Paint where
import Daml.Script
import Iou
template PaintHouse
with
painter: Party
houseOwner: Party
where
signatory painter, houseOwner
agreement
show painter <> " will paint the house of " <> show houseOwner
template OfferToPaintHouseByPainter
with
houseOwner: Party
painter: Party
bank: Party
amount: Amount
where
signatory painter
observer houseOwner
choice AcceptByOwner : ContractId Iou
with
iouId : ContractId Iou
controller houseOwner
do
iouId2 <- exercise iouId Transfer with newOwner = painter
paint <- create $ PaintHouse with painter; houseOwner
return iouId2
We won’t dive into the details of Daml, as this is explained elsewhere. But one key observation is that the contracts themselves are passive. The contract instances represent the ledger and only encode the rules according to which the ledger state can be changed. Any change requires you to trigger some Daml contract execution by sending 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. The Ledger API uses gRPC.
In theory, we would need to compile the Daml code into a DAR and then upload it to the participant nodes.
We actually did this already by uploading the CantonExamples.dar
, which includes the contracts. Now we can create our first contract using the template Iou.Iou
. The name of the template is not enough to uniquely
identify it. We also need the package id, which is just the sha256 hash of the binary module containing the
respective template.
Find that package by running:
@ val pkgIou = participant1.packages.find("Iou").head
pkgIou : com.digitalasset.canton.participant.admin.v0.PackageDescription = PackageDescription(
packageId = "c02ce18c2109d82aef1a9f468141e1205869646a94661ac3aac13a84ea0aca55",
sourceDescription = "CantonExamples"
)
Using this package-id, we can create the IOU:
@ val createIouCmd = ledger_api_utils.create(pkgIou.packageId,"Iou","Iou",Map("payer" -> bank,"owner" -> alice,"amount" -> Map("value" -> 100.0, "currency" -> "EUR"),"viewers" -> List()))
createIouCmd : com.daml.ledger.api.v1.commands.Command = Command(
command = Create(
value = CreateCommand(
templateId = Some(
value = Identifier(
packageId = "c02ce18c2109d82aef1a9f468141e1205869646a94661ac3aac13a84ea0aca55",
..
and then send that command to the Ledger API:
@ participant2.ledger_api.commands.submit(Seq(bank), Seq(createIouCmd))
res35: com.daml.ledger.api.v1.transaction.TransactionTree = TransactionTree(
transactionId = "12200d82c370531a2078985ca6567f107ac74560175a8c4c5a20fd06c13f66631600",
commandId = "bf8c4e1b-41af-488a-a4cc-72eaf437893d",
workflowId = "",
effectiveAt = Some(
value = Timestamp(
seconds = 1683269585L,
nanos = 157963000,
unknownFields = UnknownFieldSet(fields = Map())
)
),
offset = "000000000000000015",
..
Here, we’ve submitted this command as party Bank on participant2. Interestingly, we can test here the Daml authorization logic. As the signatory of the contract is Bank, we can’t have Alice submitting the contract:
@ participant1.ledger_api.commands.submit(Seq(alice), Seq(createIouCmd))
ERROR com.digitalasset.canton.integration.EnterpriseEnvironmentDefinition$$anon$3 - Request failed for participant1.
GrpcClientError: INVALID_ARGUMENT/DAML_AUTHORIZATION_ERROR(8,c0ab6dd8): Interpretation error: Error: node NodeId(0) (c02ce18c2109d82aef1a9f468141e1205869646a94661ac3aac13a84ea0aca55:Iou:Iou) requires authorizers Bank::12206638e7c64e714f04285d11c61551005cfa9347330f24047e09ba3a8e059b44e1, but only Alice::1220affdac91a0fb0835b644274f25e51b2fca40884ef011ee11a2707dbf022bdce5 were given
Request: SubmitAndWaitTransactionTree(actAs = Alice::1220affdac91..., readAs = Seq(), commandId = '', workflowId = '', submissionId = '', deduplicationPeriod = None(), commands = ...)
CorrelationId: c0ab6dd8-75ad-4a13-83af-3bedd93a2c99
..
And Alice cannot impersonate the Bank by pretending to be it (on her participant):
@ participant1.ledger_api.commands.submit(Seq(bank), Seq(createIouCmd))
ERROR com.digitalasset.canton.integration.EnterpriseEnvironmentDefinition$$anon$3 - Request failed for participant1.
GrpcRequestRefusedByServer: NOT_FOUND/NO_DOMAIN_ON_WHICH_ALL_SUBMITTERS_CAN_SUBMIT(11,b3c93886): This participant can not submit as the given submitter on any connected domain
Request: SubmitAndWaitTransactionTree(actAs = Bank::12206638e7c6..., readAs = Seq(), commandId = '', workflowId = '', submissionId = '', deduplicationPeriod = None(), commands = ...)
CorrelationId: b3c938865a61cd7b5caac25c682ec398
..
Alice can, however, observe the contract on her participant by searching her Active Contract Set (ACS) for it:
@ val aliceIou = participant1.ledger_api.acs.find_generic(alice, _.templateId == "Iou.Iou")
aliceIou : com.digitalasset.canton.admin.api.client.commands.LedgerApiTypeWrappers.WrappedCreatedEvent = WrappedCreatedEvent(
event = CreatedEvent(
eventId = "#12200d82c370531a2078985ca6567f107ac74560175a8c4c5a20fd06c13f66631600:0",
contractId = "0016903d06d27d586c85dde8cbaa566a64fc8ddd8c8de5bb0927726cfcc83bf95dca01122043781234d370eed1ce9d4c0a0b26c83568e1d304dffdde1e7204eb1787e533ea",
..
We can check Alice’s ACS, which will show us all the contracts Alice knows about:
@ participant1.ledger_api.acs.of_party(alice)
res37: Seq[com.digitalasset.canton.admin.api.client.commands.LedgerApiTypeWrappers.WrappedCreatedEvent] = List(
WrappedCreatedEvent(
event = CreatedEvent(
eventId = "#12200d82c370531a2078985ca6567f107ac74560175a8c4c5a20fd06c13f66631600:0",
contractId = "0016903d06d27d586c85dde8cbaa566a64fc8ddd8c8de5bb0927726cfcc83bf95dca01122043781234d370eed1ce9d4c0a0b26c83568e1d304dffdde1e7204eb1787e533ea",
templateId = Some(
value = Identifier(
packageId = "c02ce18c2109d82aef1a9f468141e1205869646a94661ac3aac13a84ea0aca55",
..
As expected, Alice does see exactly the contract that the Bank previously created. The command returns a sequence of
wrapped CreatedEvent’s.
This Ledger API data type represents the event of a contract’s creation. The output is a bit verbose, but the wrapper
provides convenient functions to manipulate the CreatedEvent
s in the Canton console:
@ participant1.ledger_api.acs.of_party(alice).map(x => (x.templateId, x.arguments))
res38: Seq[(String, Map[String, Any])] = List(
(
"Iou.Iou",
HashMap(
"payer" -> "Bank::12206638e7c64e714f04285d11c61551005cfa9347330f24047e09ba3a8e059b44e1",
"viewers" -> List(elements = Vector()),
"owner" -> "Alice::1220affdac91a0fb0835b644274f25e51b2fca40884ef011ee11a2707dbf022bdce5",
"amount.currency" -> "EUR",
"amount.value" -> "100.0000000000"
)
)
)
Going back to our story, Bob now wants to offer to paint Alice’s house in exchange for money. Again, we need to grab the package id, as the Paint contract is in a different module:
@ val pkgPaint = participant1.packages.find("Paint").head
pkgPaint : com.digitalasset.canton.participant.admin.v0.PackageDescription = PackageDescription(
packageId = "c02ce18c2109d82aef1a9f468141e1205869646a94661ac3aac13a84ea0aca55",
sourceDescription = "CantonExamples"
)
Note that the modules are compositional. The Iou
module is not aware of the Paint
module, but the Paint
module is using the Iou
module within its workflow. This is how we can extend any workflow in Daml and build
on top of it. In particular, the Bank does not need to know about the Paint
module at all, but can still participate
in the transaction without any adverse effect. As a result, everybody can extend the system with their own functionality.
Let’s create and submit the offer now:
@ val createOfferCmd = ledger_api_utils.create(pkgPaint.packageId, "Paint", "OfferToPaintHouseByPainter", Map("bank" -> bank, "houseOwner" -> alice, "painter" -> bob, "amount" -> Map("value" -> 100.0, "currency" -> "EUR")))
createOfferCmd : com.daml.ledger.api.v1.commands.Command = Command(
command = Create(
value = CreateCommand(
templateId = Some(
value = Identifier(
packageId = "c02ce18c2109d82aef1a9f468141e1205869646a94661ac3aac13a84ea0aca55",
..
@ participant2.ledger_api.commands.submit_flat(Seq(bob), Seq(createOfferCmd))
res41: com.daml.ledger.api.v1.transaction.Transaction = Transaction(
transactionId = "1220a1accadda1f2af190299b0d9ac151bc831182ef38414a46252e35d9d62206857",
commandId = "9298d03f-4269-44de-9f98-6a0263132fa2",
workflowId = "",
effectiveAt = Some(
value = Timestamp(
..
Alice will observe this offer on her node:
@ val paintOffer = participant1.ledger_api.acs.find_generic(alice, _.templateId == "Paint.OfferToPaintHouseByPainter")
paintOffer : com.digitalasset.canton.admin.api.client.commands.LedgerApiTypeWrappers.WrappedCreatedEvent = WrappedCreatedEvent(
event = CreatedEvent(
eventId = "#1220a1accadda1f2af190299b0d9ac151bc831182ef38414a46252e35d9d62206857:0",
contractId = "004fa5bea7d32f5bbe1c632101289b509296d2bed9d0087ebda253c212a25818cfca011220e50e34fce8d8f5a7d1815ff71fb45700604599cc3ba369f21c22b5561ac2f22a",
templateId = Some(
value = Identifier(
..
Privacy¶
Looking at the ACS of Alice, Bob and the Bank, we note that Bob sees only the paint offer:
@ participant2.ledger_api.acs.of_party(bob).map(x => (x.templateId, x.arguments))
res43: Seq[(String, Map[String, Any])] = List(
(
"Paint.OfferToPaintHouseByPainter",
HashMap(
"painter" -> "Bob::12206638e7c64e714f04285d11c61551005cfa9347330f24047e09ba3a8e059b44e1",
"houseOwner" -> "Alice::1220affdac91a0fb0835b644274f25e51b2fca40884ef011ee11a2707dbf022bdce5",
"bank" -> "Bank::12206638e7c64e714f04285d11c61551005cfa9347330f24047e09ba3a8e059b44e1",
"amount.currency" -> "EUR",
"amount.value" -> "100.0000000000"
)
)
)
while the Bank sees the Iou contract:
@ participant2.ledger_api.acs.of_party(bank).map(x => (x.templateId, x.arguments))
res44: Seq[(String, Map[String, Any])] = List(
(
"Iou.Iou",
HashMap(
"payer" -> "Bank::12206638e7c64e714f04285d11c61551005cfa9347330f24047e09ba3a8e059b44e1",
"viewers" -> List(elements = Vector()),
"owner" -> "Alice::1220affdac91a0fb0835b644274f25e51b2fca40884ef011ee11a2707dbf022bdce5",
"amount.currency" -> "EUR",
"amount.value" -> "100.0000000000"
)
)
)
But Alice sees both on her participant node:
@ participant1.ledger_api.acs.of_party(alice).map(x => (x.templateId, x.arguments))
res45: Seq[(String, Map[String, Any])] = List(
(
"Iou.Iou",
HashMap(
"payer" -> "Bank::12206638e7c64e714f04285d11c61551005cfa9347330f24047e09ba3a8e059b44e1",
"viewers" -> List(elements = Vector()),
"owner" -> "Alice::1220affdac91a0fb0835b644274f25e51b2fca40884ef011ee11a2707dbf022bdce5",
"amount.currency" -> "EUR",
"amount.value" -> "100.0000000000"
)
),
(
"Paint.OfferToPaintHouseByPainter",
HashMap(
"painter" -> "Bob::12206638e7c64e714f04285d11c61551005cfa9347330f24047e09ba3a8e059b44e1",
"houseOwner" -> "Alice::1220affdac91a0fb0835b644274f25e51b2fca40884ef011ee11a2707dbf022bdce5",
"bank" -> "Bank::12206638e7c64e714f04285d11c61551005cfa9347330f24047e09ba3a8e059b44e1",
"amount.currency" -> "EUR",
"amount.value" -> "100.0000000000"
)
)
)
If there were a third participant node, it wouldn’t have even noticed that there was anything happening, let alone have received any contract data. Or if we had deployed the Bank on that third node, that node
would not have been informed about the Paint offer. This privacy feature goes so far in Canton that not even
everybody within a single atomic transaction is aware of each other. This is a property unique to the Canton
synchronization protocol, which we call sub-transaction privacy. The protocol ensures that only eligible
participants will receive any data. Furthermore, while the node running mydomain
does receive this data, the data
is encrypted and mydomain
cannot read it.
We can run such a step with sub-transaction privacy by accepting the offer, which will lead to the transfer of the Bank Iou, without the Bank actually learning about the Paint agreement:
@ import com.digitalasset.canton.protocol.LfContractId
@ val acceptOffer = ledger_api_utils.exercise("AcceptByOwner", Map("iouId" -> LfContractId.assertFromString(aliceIou.event.contractId)),paintOffer.event)
acceptOffer : com.daml.ledger.api.v1.commands.Command = Command(
command = Exercise(
value = ExerciseCommand(
templateId = Some(
value = Identifier(
packageId = "c02ce18c2109d82aef1a9f468141e1205869646a94661ac3aac13a84ea0aca55",
..
@ participant1.ledger_api.commands.submit_flat(Seq(alice), Seq(acceptOffer))
res48: com.daml.ledger.api.v1.transaction.Transaction = Transaction(
transactionId = "12202fc25c855a3239302ce3786b7fd395e991049af0e24ea8f6824753b555f1da41",
commandId = "3eb41834-93c6-45ec-9a33-fa995bd1cf6c",
workflowId = "",
effectiveAt = Some(
value = Timestamp(
..
Note that the conversion to LfContractId
was required to pass in the Iou contract id as the correct type.
Your Development Choices¶
While the ledger_api
functions in the Console can be handy for educational purposes, the Daml SDK provides you with much more
convenient tools to inspect and manipulate the ledger content:
- The browser based Navigator
- The console version Navigator
- Daml script for scripting
- Daml triggers for reactive operations
- Daml REPL for interactive manipulations
- Json API for browser based UIs
- Bindings in a variety of languages to build your own applications
All these tools work against the Ledger API.
Automation using bootstrap scripts¶
You can configure a bootstrap script to avoid having to manually complete routine tasks such as starting nodes or provisioning parties each time Canton is
started. Bootstrap scripts are automatically run after Canton has started and can
contain any valid Canton Console commands. A bootstrap script is passed via the --bootstrap
CLI argument
when starting Canton. By convention, we use a .canton
file ending.
For example, the bootstrap script to connect the participant nodes to the local domain and ping participant1 from participant2 (see Starting and Connecting The Nodes) is:
// start all local instances defined in the configuration file
nodes.local.start()
// Connect participant1 to mydomain using the connect macro.
// The connect macro will inspect the domain configuration to find the correct URL and Port.
// The macro is convenient for local testing, but obviously doesn't work in a distributed setup.
participant1.domains.connect_local(mydomain)
// Connect participant2 to mydomain using just the target URL and a local name we use to refer to this particular
// connection. This is actually everything Canton requires and this second type of connect call can be used
// in order to connect to a remote Canton domain.
//
// The connect call is just a wrapper that invokes the `domains.register`, `domains.get_agreement` and `domains.accept_agreement` calls.
//
// The address can be either HTTP or HTTPS. From a security perspective, we do assume that we either trust TLS to
// initially introduce the domain. If we don't trust TLS for that, we can also optionally include a so called
// EssentialState that establishes the trust of the participant to the domain.
// Whether a domain will let a participant connect or not is at the discretion of the domain and can be configured
// there. While Canton establishes the connection, we perform a handshake, exchanging keys, authorizing the connection
// and verifying version compatibility.
participant2.domains.connect("mydomain", "http://localhost:5018")
// The above connect operation is asynchronous. It is generally at the discretion of the domain
// to decide if a participant can join and when. Therefore, we need to asynchronously wait here
// until the participant observes its activation on the domain. As the domain is configured to be
// permissionless in this example, the approval will be granted immediately.
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.
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:
- Install the Daml SDK to get access to the Daml IDE and other tools, such as the Navigator.
- Run through the Daml SDK getting-started example to learn how to build your own Daml applications on Canton.
- Follow the Daml documentation to learn how to program new contracts, or check out the Daml Examples to find existing ones for your needs.
- Use the Navigator for easy Web-based access and manipulation of your contracts.
If you want to understand more about Canton:
- Read the requirements that Canton was built for to find out more about the properties of Canton.
- Read the architectural overview for more understanding of Canton concepts and internals.
If you want to deploy your own Canton nodes, consult the installation guide.