Skip to main content

Externally Signed Topology Transactions

Canton’s Topology formalizes a shared state on a synchronizer and provides a secure, distributed mechanism for modifying this state. This tutorial demonstrates how to build, sign, and submit topology transactions. It is particularly useful for cases where the signature is provided by a key held externally to the network, such as in the case of external party onboarding, or initialization of the root namespace of a participant. This tutorial goes through the steps of importing a root namespace delegation, but can be generalized to any topology mapping. A root namespace delegation is essentially equivalent to a X509v3 CA root certificate in Canton and creates an associated namespace.
This tutorial is for demo purposes. The code snippets should not be used directly in a production environment.

Prerequisites

For simplicity, this tutorial assumes a minimal Canton setup consisting of one participant node connected to one synchronizer (which includes both a sequencer node and a mediator node).

Start Canton

To obtain a Canton artifact refer to the getting started section. From the artifact directory, start Canton using the command:
Once the “Welcome to Canton” message appears, you are ready to proceed.

Setup

Navigate to the interactive submission example folder located at examples/08-interactive-submission in the Canton release artifact.
The code examples in this tutorial are extracted from scripts located in that folder.
To proceed, gather the following information by running the commands below in the Canton console:
  • Admin API endpoint
  • Synchronizer ID
In the rest of the tutorial we use the following values, but make sure to replace them with your own:
  • Admin API endpoint: localhost:4002
  • Synchronizer ID: da::12207a94aca813c822c6ae10a1b5478c2ba1077447b468cc66dbd255f60f8fa333e1

API

This tutorial interacts with the TopologyManagerWriteService, a gRPC service available on the Admin API of the participant node. It assumes that the Admin API is not authenticated via client certificates.

Python

It is recommended to use a dedicated python environment to avoid conflicting dependencies. Considering using venv.
Then run the setup script to generate the necessary python files to interact with Canton’s gRPC interface:
Finally, the following imports will be needed:

Shell

For a terminal-based approach, install the following tools: The tutorial uses a buf proto image to (de)serialize proto messages.
The following functions will be used throughout the tutorial:

Error Handling

When encountering RPC errors, it may be necessary to perform additional deserialization to get actionable information on the cause of the error. Example of an RPC error:
The type field specifies the protobuf type in which the error is encoded. In this case, it is a google.rpc.ErrorInfo message. The following utility code can be used to deal with errors and extract useful information out of them. Bash
Python

1. Signing Keys

First, generate an external signing key pair to use in the rest of this tutorial. Bash
Python

2. Hash

Hashing is required at several steps to compute a hash over a sequence of bytes. The process uses an underlying algorithm, with specific prefixes added to both the input bytes and the final hash:
  1. A hash purpose (a 4-byte integer) is prefixed to the byte sequence. Hash purpose values are defined directly in the Canton codebase.
  2. The resulting data is hashed using the underlying algorithm.
  3. The final multihash is prefixed again with two bytes, following the multi-codec specification:
    • The identifier for the hash algorithm used.
    • The length of the hash.
For most practical usages, SHA-256 can be used as the underlying algorithm, and is used in this tutorial as well.
Bash
Python

3. Fingerprint

Canton uses fingerprints to efficiently identify and reference signing keys. A fingerprint is a hash of the public key. Using the hashing algorithm described previously, compute the fingerprint of the public key. For fingerprints, the hash purpose value is 12. Bash
Python
The scripts in this tutorial can provide a quick way to verify third party implementations of the hashing and signing logic. For instance, the following script outputs the valid fingerprint of a signing public key passed in a base64 format:

4. Namespace Delegation Mapping

There is a number of different mappings available, each modeling a part of the topology state.
This tutorial illustrates the process of importing a root namespace delegation, represented by the NamespaceDelegation mapping, but the same procedure can be applied for any topology mapping. The Namespace Delegation mapping requires three values:
  1. namespace: Root key’s fingerprint
  2. target_key: Public key expected to be used by delegation. Root namespace delegations are self-signed.
    • The format (DER) and specification (EC256) of the key must match those of the key generated in step 1.
  3. is_root_delegation: true for root namespace delegations
Bash
Python

5. Topology Transaction

The topology state is scoped to a synchronizer. Each synchronizer supports a specific version of the Canton protocol, called Protocol Version (more details on the versioning page). When integrating with the topology API, it is important to select which synchronizer the topology changes should target. This is especially relevant when interacting with the topology API of a participant node, which may be connected to multiple synchronizers at any given time. Once a synchronizer is selected, its ProtocolVersion can be retrieved via the SequencerConnectService#GetSynchronizerParameters RPC of the sequencer API.
The Canton console on a sequencer node of the target synchronizer also provides a simple way to get this value:
Each Protocol Version has a corresponding Protobuf Version for protobuf messages involved in the Canton protocol. That includes the TopologyTransaction message. The protobuf version becomes relevant in the Version Wrapper section.
The versioning of protobuf messages is relatively stable and is not expected to change often. The rest of the tutorial assumes the protobuf version used is 30.
Topology transactions consist of three parts:

Topology Mapping

See the Namespace Delegation Mapping section

Serial

The serial is a monotonically increasing number, starting from 1. Each transaction creating, replacing, or deleting a unique topology mapping must specify a serial incrementing the serial of the previous accepted transaction for that mapping by 1. Uniqueness is defined differently for each mapping. Refer to the protobuf definition of the mapping for details. This mechanism ensures that concurrent topology transactions updating the same mapping do not accidentally overwrite each other. To obtain the serial of an existing transaction, use the TopologyManagerReadService to list relevant mappings and obtain their current serial.
In this tutorial, it is assumed that the NamespaceDelegation created is new, in particular there is no pre-existing root namespace delegation with the key created in step 1. The serial is therefore set to 1.

Operation

There are two operations possible:
  • ADD_REPLACE: Adds a new mapping or replaces an existing one.
  • REMOVE: Remove an existing mapping
Bash
Python

6. Version Wrapper

In order to guarantee backwards compatibility while supporting changes to the protobuf messages involved in the protocol, Canton wraps serialized messages with a wrapper that includes the protobuf version tied to the message.
  • data: serialized protobuf topology transaction
  • version: protobuf version of the topology transaction message
Wrap the serialized transaction in an UntypedVersionedMessage, and serialize the result: Bash
Python

7. Transaction Hash

The next step is to compute the hash of the transaction. It is computed from the serialized protobuf of the versioned transaction. Simply reuse the hashing function defined earlier in the tutorial. This time, the hash purpose value is 11. Bash
Python
To facilitate steps 5 to 7, the topology API offers a GenerateTransactions RPC to generate the serialized versioned transaction and its hash. When using the GenerateTransactions API, it is strongly recommended to deserialize the returned transaction, validate its content and re-compute its hash, to prevent any accidental misuse or adversarial behavior of the participant generating the transaction.

8. Signature

The hash is now ready to be signed. For root namespace transactions, there is only one key involved, and it therefore needs only one signature. Other topology mappings may require additional signatures, either because the mappings themselves contain additional public keys (e.g OwnerToKeyMapping), or because the authorization rules of the mapping require signatures from several entities (e.g PartyToParticipant). All transactions, however, require a signature either from the root namespace key of the namespace the transaction is targeting or from a delegated key of that namespace registered via a (non-root) NamespaceDelegation. The authorization rules vary by mapping and are out of the scope of this tutorial, but can be found on their protobuf definition. Sign the hash with the private key: Bash
Python

9. Submit the transaction

Submit the transaction and its signature. This is done via the AddTransactions RPC of the TopologyManagerWriteService: Bash
If everything goes well, Transaction submitted successfully should be displayed. Python

Proposal

The SignedTopologyTransaction message contains a boolean proposal field. When set to true, it allows submitting topology transactions without attaching all the signatures required for the transaction to be fully authorized. This is especially useful in cases where signatures from multiple entities of the network are necessary, that would be tedious and difficult to gather offline.

10. Observe the transaction

The last step of the tutorial is to observe the NamespaceDelegation on the topology state of the synchronizer. Note that the submission is asynchronous, which means it may take some time before the submission is accepted. Bash
Python
This concludes the tutorial. The transaction is now active on the topology state of the synchronizer. The code used in this tutorial is available in the examples/08-interactive-submission folder and can be run with Bash
Python