Submit Externally Signed Transactions - Part 1

This tutorial demonstrates how to submit Daml commands to a Canton ledger using an external private key for transaction authorization. Before proceeding, it is recommended to review the external signing overview to understand the concept of external signing.

The tutorial illustrates the external signing process using two external parties, Alice and Bob, leveraging the Ping Daml Template which is included by default in all participant nodes.

-- Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0

module Canton.Internal.Ping where

template Ping
  with
    id : Text
    initiator : Party
    responder : Party
  where

    signatory initiator
    observer responder

    choice Respond : ()
      controller responder
        do
          return ()

    choice AbortPing : ()
      with
        anyone : Party
      controller anyone
        do
          return ()
  • In Part 1 Alice creates a Ping contract.

  • In Part 2 Bob exercises the Respond choice on the contract and archives it.

Important

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

Tip

If you already have such an instance running or have completed the onboarding tutorial, proceed to the Setup section.

Start Canton

To obtain a Canton artifact refer to the getting started section. First, navigate to the interactive submission example folder located at examples/08-interactive-submission in the Canton release artifact.

Note

All commands in this tutorial are expected to be run from that folder.

From the artifact directory, start Canton using the command:

../../bin/canton -c examples/08-interactive-submission/interactive-submission.conf --bootstrap examples/08-interactive-submission/bootstrap.canton

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.

This tutorial demonstrates external signing with two external parties: Alice and Bob. If you haven’t onboarded an external party yet, refer to the onboarding tutorial.

To proceed, gather the following information:

  • Alice’s Party Id, protocol signing private key, and protocol signing public key fingerprint

  • Bob’s Party Id

  • Synchronizer Id to which the participant is connected

  • gRPC Ledger API endpoint

The Party IDs and key-related information should already be known from the onboarding tutorial. To retrieve the participant and synchronizer IDs, as well as the gRPC Ledger API ports, run the following commands in the Canton console:

@ sequencer1.synchronizer_id.filterString
res1: String = "da::1220a82692abc55c0367abefc4bdbc23df25688230430ddfeef5759845f26d5cc29c"
@ participant1.config.ledgerApi.address
res2: String = "127.0.0.1"
@ participant1.config.ledgerApi.port.unwrap
res3: Int = 30001

For this tutorial, the following values will be used (replace them with actual values):

  • Alice Party Id: alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e

  • Bob Party Id: bob::1220254d06095b407f8c6a378b6fc443a67d3356ab8edfbf1378cb3e44218de32c8a

  • Synchronizer Id: da::12203c0ecb446b35b0efa78e0bda9fd91716855866150a5eb7611a2ed5d418129de3

  • gRPC Ledger API endpoint: localhost:4001

API

This tutorial interacts with the InteractiveSubmissionService, a service available on the gRPC Ledger API of the participant node.

// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

syntax = "proto3";

package com.daml.ledger.api.v2.interactive;

import "com/daml/ledger/api/v2/commands.proto";
import "com/daml/ledger/api/v2/interactive/interactive_submission_common_data.proto";
import "com/daml/ledger/api/v2/interactive/transaction/v1/interactive_submission_data.proto";
import "com/daml/ledger/api/v2/package_reference.proto";
import "com/daml/ledger/api/v2/value.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";

option csharp_namespace = "Com.Daml.Ledger.Api.V2.Interactive";
option java_outer_classname = "InteractiveSubmissionServiceOuterClass";
option java_package = "com.daml.ledger.api.v2.interactive";

// Service allowing interactive construction of command submissions
//
// The prepare and execute endpoints allow to submit commands in 2-steps:
//
// 1. prepare transaction from commands,
// 2. submit the prepared transaction
//
// This gives callers the ability to sign the daml transaction with their own signing keys
service InteractiveSubmissionService {
  // Requires `readAs` scope for the submitting party when LAPI User authorization is enabled
  rpc PrepareSubmission(PrepareSubmissionRequest) returns (PrepareSubmissionResponse);
  rpc ExecuteSubmission(ExecuteSubmissionRequest) returns (ExecuteSubmissionResponse);

  // A preferred package is the highest-versioned package for a provided package-name
  // that is vetted by all the participants hosting the provided parties.
  //
  // Ledger API clients should use this endpoint for constructing command submissions
  // that are compatible with the provided preferred package, by making informed decisions on:
  // - which are the compatible packages that can be used to create contracts
  // - which contract or exercise choice argument version can be used in the command
  // - which choices can be executed on a template or interface of a contract
  //
  // Can be accessed by any Ledger API client with a valid token when Ledger API authorization is enabled.
  //
  // Provided for backwards compatibility, it will be removed in the Canton version 3.4.0
  rpc GetPreferredPackageVersion(GetPreferredPackageVersionRequest) returns (GetPreferredPackageVersionResponse);

  // Compute the preferred packages for the vetting requirements in the request.
  // A preferred package is the highest-versioned package for a provided package-name
  // that is vetted by all the participants hosting the provided parties.
  //
  // Ledger API clients should use this endpoint for constructing command submissions
  // that are compatible with the provided preferred packages, by making informed decisions on:
  // - which are the compatible packages that can be used to create contracts
  // - which contract or exercise choice argument version can be used in the command
  // - which choices can be executed on a template or interface of a contract
  //
  // If the package preferences could not be computed due to no selection satisfying the requirements,
  // a `FAILED_PRECONDITION` error will be returned.
  //
  // Can be accessed by any Ledger API client with a valid token when Ledger API authorization is enabled.
  //
  // Experimental API: this endpoint is not guaranteed to provide backwards compatibility in future releases
  rpc GetPreferredPackages(GetPreferredPackagesRequest) returns (GetPreferredPackagesResponse);
}

message PrepareSubmissionRequest {
  // Uniquely identifies the participant user that prepares the transaction.
  // Must be a valid UserIdString (as described in ``value.proto``).
  // Required unless authentication is used with a user token.
  // In that case, the token's user-id will be used for the request's user_id.
  string user_id = 1;

  // Uniquely identifies the command.
  // The triple (user_id, act_as, command_id) constitutes the change ID for the intended ledger change,
  // where act_as is interpreted as a set of party names.
  // The change ID can be used for matching the intended ledger changes with all their completions.
  // Must be a valid LedgerString (as described in ``value.proto``).
  // Required
  string command_id = 2;

  // Individual elements of this atomic command. Must be non-empty.
  // Required
  repeated Command commands = 3;

  // Optional
  MinLedgerTime min_ledger_time = 4;

  // Set of parties on whose behalf the command should be executed, if submitted.
  // If ledger API authorization is enabled, then the authorization metadata must authorize the sender of the request
  // to **read** (not act) on behalf of each of the given parties. This is because this RPC merely prepares a transaction
  // and does not execute it. Therefore read authorization is sufficient even for actAs parties.
  // Note: This may change, and more specific authorization scope may be introduced in the future.
  // Each element must be a valid PartyIdString (as described in ``value.proto``).
  // Required, must be non-empty.
  repeated string act_as = 5;

  // Set of parties on whose behalf (in addition to all parties listed in ``act_as``) contracts can be retrieved.
  // This affects Daml operations such as ``fetch``, ``fetchByKey``, ``lookupByKey``, ``exercise``, and ``exerciseByKey``.
  // Note: A command can only use contracts that are visible to at least
  // one of the parties in ``act_as`` or ``read_as``. This visibility check is independent from the Daml authorization
  // rules for fetch operations.
  // If ledger API authorization is enabled, then the authorization metadata must authorize the sender of the request
  // to read contract data on behalf of each of the given parties.
  // Optional
  repeated string read_as = 6;

  // Additional contracts used to resolve contract & contract key lookups.
  // Optional
  repeated DisclosedContract disclosed_contracts = 7;

  // Must be a valid synchronizer id
  // Required
  string synchronizer_id = 8;

  // The package-id selection preference of the client for resolving
  // package names and interface instances in command submission and interpretation
  repeated string package_id_selection_preference = 9;

  // When true, the response will contain additional details on how the transaction was encoded and hashed
  // This can be useful for troubleshooting of hash mismatches. Should only be used for debugging.
  bool verbose_hashing = 10;

  // Fetches the contract keys into the caches to speed up the command processing.
  // Should only contain contract keys that are expected to be resolved during interpretation of the commands.
  // Keys of disclosed contracts do not need prefetching.
  //
  // Optional
  repeated PrefetchContractKey prefetch_contract_keys = 15;
}

// [docs-entry-start: HashingSchemeVersion]
// The hashing scheme version used when building the hash of the PreparedTransaction
enum HashingSchemeVersion {
  HASHING_SCHEME_VERSION_UNSPECIFIED = 0;
  reserved 1; // Hashing Scheme V1 - unsupported
  HASHING_SCHEME_VERSION_V2 = 2;
}
// [docs-entry-end: HashingSchemeVersion]

message PrepareSubmissionResponse {
  // The interpreted transaction, it represents the ledger changes necessary to execute the commands specified in the request.
  // Clients MUST display the content of the transaction to the user for them to validate before signing the hash if the preparing participant is not trusted.
  PreparedTransaction prepared_transaction = 1;
  // Hash of the transaction, this is what needs to be signed by the party to authorize the transaction.
  // Only provided for convenience, clients MUST recompute the hash from the raw transaction if the preparing participant is not trusted.
  // May be removed in future versions
  bytes prepared_transaction_hash = 2;

  // The hashing scheme version used when building the hash
  HashingSchemeVersion hashing_scheme_version = 3;

  // Optional additional details on how the transaction was encoded and hashed. Only set if verbose_hashing = true in the request
  // Note that there are no guarantees on the stability of the format or content of this field.
  // Its content should NOT be parsed and should only be used for troubleshooting purposes.
  optional string hashing_details = 4;
}

message Signature {
  SignatureFormat format = 1;

  bytes signature = 2;

  // The fingerprint/id of the keypair used to create this signature and needed to verify.
  string signed_by = 3;

  // The signing algorithm specification used to produce this signature
  SigningAlgorithmSpec signing_algorithm_spec = 4;
}

enum SigningAlgorithmSpec {
  SIGNING_ALGORITHM_SPEC_UNSPECIFIED = 0;

  // EdDSA Signature based on Curve25519 with SHA-512
  // http://ed25519.cr.yp.to/
  SIGNING_ALGORITHM_SPEC_ED25519 = 1;

  // Elliptic Curve Digital Signature Algorithm with SHA256
  SIGNING_ALGORITHM_SPEC_EC_DSA_SHA_256 = 2;

  // Elliptic Curve Digital Signature Algorithm with SHA384
  SIGNING_ALGORITHM_SPEC_EC_DSA_SHA_384 = 3;
}

enum SignatureFormat {
  SIGNATURE_FORMAT_UNSPECIFIED = 0;

  // Signature scheme specific signature format
  // Legacy format no longer used, except for migrations
  SIGNATURE_FORMAT_RAW = 1;

  // ASN.1 + DER-encoding of the `r` and `s` integers, as defined in https://datatracker.ietf.org/doc/html/rfc3279#section-2.2.3
  // Used for ECDSA signatures
  SIGNATURE_FORMAT_DER = 2;

  // Concatenation of the integers `r || s` in little-endian form, as defined in https://datatracker.ietf.org/doc/html/rfc8032#section-3.3
  // Note that this is different from the format defined in IEEE P1363, which uses concatenation in big-endian form.
  // Used for EdDSA signatures
  SIGNATURE_FORMAT_CONCAT = 3;

  // Symbolic crypto, must only be used for testing
  SIGNATURE_FORMAT_SYMBOLIC = 10000;
}

// Signatures provided by a single party
message SinglePartySignatures {
  string party = 1; // Submitting party
  repeated Signature signatures = 2; // Signatures
}

// Additional signatures provided by the submitting parties
message PartySignatures {
  // Additional signatures provided by all individual parties
  repeated SinglePartySignatures signatures = 1;
}

message ExecuteSubmissionRequest {
  // the prepared transaction
  // Typically this is the value of the `prepared_transaction` field in `PrepareSubmissionResponse`
  // obtained from calling `prepareSubmission`.
  PreparedTransaction prepared_transaction = 1;

  // The party(ies) signatures that authorize the prepared submission to be executed by this node.
  // Each party can provide one or more signatures..
  // and one or more parties can sign.
  // Note that currently, only single party submissions are supported.
  PartySignatures party_signatures = 2;

  // Specifies the deduplication period for the change ID (See PrepareSubmissionRequest).
  // If omitted, the participant will assume the configured maximum deduplication time.
  oneof deduplication_period {
    // Specifies the length of the deduplication period.
    // It is interpreted relative to the local clock at some point during the submission's processing.
    // Must be non-negative. Must not exceed the maximum deduplication time.
    google.protobuf.Duration deduplication_duration = 3;

    // Specifies the start of the deduplication period by a completion stream offset (exclusive).
    // Must be a valid absolute offset (positive integer).
    int64 deduplication_offset = 4;
  }

  // A unique identifier to distinguish completions for different submissions with the same change ID.
  // Typically a random UUID. Applications are expected to use a different UUID for each retry of a submission
  // with the same change ID.
  // Must be a valid LedgerString (as described in ``value.proto``).
  //
  // Required
  string submission_id = 5;

  // See [PrepareSubmissionRequest.user_id]
  string user_id = 6;

  // The hashing scheme version used when building the hash
  HashingSchemeVersion hashing_scheme_version = 7;

  // If set will influence the chosen ledger effective time but will not result in a submission delay so any override
  // should be scheduled to executed within the window allowed by synchronizer.
  MinLedgerTime min_ledger_time = 8; // Optional
}

message ExecuteSubmissionResponse {}

message MinLedgerTime {
  oneof time {
    // Lower bound for the ledger time assigned to the resulting transaction.
    // The ledger time of a transaction is assigned as part of command interpretation.
    // Important note: for interactive submissions, if the transaction depends on time, it **must** be signed
    // and submitted within a time window around the ledger time assigned to the transaction during the prepare method.
    // The time delta around that ledger time is a configuration of the ledger, usually short, around 1 minute.
    // If however the transaction does not depend on time, the available time window to sign and submit the transaction is bound
    // by the preparation time, which is also assigned in the "prepare" step (this request),
    // but can be configured with a much larger skew, allowing for more time to sign the request (in the order of hours).
    // Must not be set at the same time as min_ledger_time_rel.
    // Optional
    google.protobuf.Timestamp min_ledger_time_abs = 1;

    // Same as min_ledger_time_abs, but specified as a duration, starting from the time this request is received by the server.
    // Must not be set at the same time as min_ledger_time_abs.
    // Optional
    google.protobuf.Duration min_ledger_time_rel = 2;
  }
}

/**
 * Prepared Transaction Message
 */
message PreparedTransaction {
  // Daml Transaction representing the ledger effect if executed. See below
  DamlTransaction transaction = 1;
  // Metadata context necessary to execute the transaction
  Metadata metadata = 2;
}

// Transaction Metadata
// Refer to the hashing documentation for information on how it should be hashed.
message Metadata {
  message SubmitterInfo {
    repeated string act_as = 1;
    string command_id = 2;
  }

  message GlobalKeyMappingEntry {
    interactive.GlobalKey key = 1;
    optional Value value = 2;
  }

  message InputContract {
    oneof contract {
      // When new versions will be added, they will show here
      interactive.transaction.v1.Create v1 = 1;
    }
    uint64 created_at = 1000;
    reserved 1001; // Used to contain driver_metadata, now contained in event_blob
    bytes event_blob = 1002;
  }

  /* ************************************************** */
  /* ** Metadata information that needs to be signed ** */
  /* ************************************************** */

  // this used to contain the ledger effective time
  reserved 1;

  SubmitterInfo submitter_info = 2;
  string synchronizer_id = 3;
  uint32 mediator_group = 4;
  string transaction_uuid = 5;
  uint64 preparation_time = 6;
  repeated InputContract input_contracts = 7;

  /*
   * Where ledger time constraints are imposed during the execution of the contract they will be populated
   * in the fields below. These are optional because if the transaction does NOT depend on time, these values
   * do not need to be set.
   * The final ledger effective time used will be chosen when the command is submitted through the [execute] RPC.
   * If the ledger effective time is outside of any populated min/max bounds then a different transaction
   * can result, that will cause a confirmation message rejection.
   */
  optional uint64 min_ledger_effective_time = 9;
  optional uint64 max_ledger_effective_time = 10;

  /* ********************************************************** */
  /* ** Metadata information that does NOT need to be signed ** */
  /* ********************************************************** */

  // Contextual information needed to process the transaction but not signed, either because it's already indirectly
  // signed by signing the transaction, or because it doesn't impact the ledger state
  repeated GlobalKeyMappingEntry global_key_mapping = 8;
}

/*
 * Daml Transaction.
 * This represents the effect on the ledger if this transaction is successfully committed.
 */
message DamlTransaction {
  message NodeSeed {
    int32 node_id = 1;
    bytes seed = 2;
  }

  // A transaction may contain nodes with different versions.
  // Each node must be hashed using the hashing algorithm corresponding to its specific version.
  // [docs-entry-start: DamlTransaction.Node]
  message Node {
    string node_id = 1;

    // Versioned node
    oneof versioned_node {
      // Start at 1000 so we can add more fields before if necessary
      // When new versions will be added, they will show here
      interactive.transaction.v1.Node v1 = 1000;
    }
  }
  // [docs-entry-end: DamlTransaction.Node]

  // Transaction version, will be >= max(nodes version)
  string version = 1;
  // Root nodes of the transaction
  repeated string roots = 2;
  // List of nodes in the transaction
  repeated Node nodes = 3;
  // Node seeds are values associated with certain nodes used for generating cryptographic salts
  repeated NodeSeed node_seeds = 4;
}

message GetPreferredPackageVersionRequest {
  // The parties whose participants' vetting state should be considered when resolving the preferred package.
  // Required
  repeated string parties = 1;
  // The package-name for which the preferred package should be resolved.
  // Required
  string package_name = 2;

  // The synchronizer whose vetting state to use for resolving this query.
  // If not specified, the vetting state of all the synchronizers the participant is connected to will be used.
  // Optional
  string synchronizer_id = 3;

  // The timestamp at which the package vetting validity should be computed
  // on the latest topology snapshot as seen by the participant.
  // If not provided, the participant's current clock time is used.
  // Optional
  google.protobuf.Timestamp vetting_valid_at = 4;
}

message GetPreferredPackageVersionResponse {
  // Not populated when no preferred package is found
  // Optional
  PackagePreference package_preference = 1;
}

message PackagePreference {
  // The package reference of the preferred package.
  // Required
  PackageReference package_reference = 1;

  // The synchronizer for which the preferred package was computed.
  // If the synchronizer_id was specified in the request, then it matches the request synchronizer_id.
  // Required
  string synchronizer_id = 2;
}

// Defines a package-name for which the commonly vetted package with the highest version must be found.
message PackageVettingRequirement {
  // The parties whose participants' vetting state should be considered when resolving the preferred package.
  // Required
  repeated string parties = 1;

  // The package-name for which the preferred package should be resolved.
  // Required
  string package_name = 2;
}

message GetPreferredPackagesRequest {
  // The package-name vetting requirements for which the preferred packages should be resolved.
  //
  // Generally it is enough to provide the requirements for the intended command's root package-names.
  // Additional package-name requirements can be provided when additional Daml transaction informees need to use
  // package dependencies of the command's root packages.
  //
  // Required
  repeated PackageVettingRequirement package_vetting_requirements = 1;

  // The synchronizer whose vetting state to use for resolving this query.
  // If not specified, the vetting state of all the synchronizers the participant is connected to will be used.
  // Optional
  string synchronizer_id = 2;

  // The timestamp at which the package vetting validity should be computed
  // on the latest topology snapshot as seen by the participant.
  // If not provided, the participant's current clock time is used.
  // Optional
  google.protobuf.Timestamp vetting_valid_at = 3;
}

message GetPreferredPackagesResponse {
  // The package references of the preferred packages.
  // Must contain one package reference for each requested package-name.
  //
  // If you build command submissions whose content depends on the returned
  // preferred packages, then we recommend submitting the preferred package-ids
  // in the ``package_id_selection_preference`` of the command submission to
  // avoid race conditions with concurrent changes of the on-ledger package vetting state.
  //
  // Required
  repeated PackageReference package_references = 1;

  // The synchronizer for which the package preferences are computed.
  // If the synchronizer_id was specified in the request, then it matches the request synchronizer_id.
  // Required
  string synchronizer_id = 2;
}

Python

It is recommended to use a dedicated python environment to avoid conflicting dependencies. Considering using venv.

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Then run the setup script to generate the necessary python files to interact with Canton’s gRPC interface:

./setup.sh

Shell

For a terminal-based approach, install the following tools:

1. Prepare the transaction

Transform Ledger Command into a Daml Transaction.

Request:

echo '{
  "user_id": "demo_app",
  "command_id": "f2ec4d8f-ccc1-402b-b278-7556fdd2b412",
  "act_as": ["alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e"],
  "synchronizer_id": "da::12203c0ecb446b35b0efa78e0bda9fd91716855866150a5eb7611a2ed5d418129de3",
  "commands": [
    {
      "create": {
        "template_id": {
          "package_id": "#AdminWorkflows",
          "module_name": "Canton.Internal.Ping",
          "entity_name": "Ping"
        },
        "create_arguments": {
          "record_id": null,
          "fields": [
            {
                "label" :"id",
                "value": { "text": "ping_id" }
            },
            {
                "label" :"initiator",
                "value": { "party": "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e" }
            },
            {
                "label" :"responder",
                "value": { "party": "bob::1220254d06095b407f8c6a378b6fc443a67d3356ab8edfbf1378cb3e44218de32c8a" }
            }
          ]
        }
      }
    }
  ]
}' > create_ping_prepare_request.json

cat "create_ping_prepare_request.json" | grpcurl -emit-defaults -plaintext -d @ localhost:4001 com.daml.ledger.api.v2.interactive.InteractiveSubmissionService/PrepareSubmission > create_ping_prepare_response.json

Record the response in create_ping_prepare_response.json to make it easier to submit the transaction afterwards. Now inspect the response with

cat create_ping_prepare_response.json
{
  "prepared_transaction": {
    "transaction": {
      "version": "2.1",
      "roots": [
        "0"
      ],
      "nodes": [
        {
          "node_id": "0",
          "v1": {
            "create": {
              "lf_version": "2.1",
              "contract_id": "004c3409aa2e8f8e22604d58ea6211f667df2bae4abc7984a95d76b3d120b8bd85",
              "package_name": "AdminWorkflows",
              "template_id": {
                "packageId": "9a19e9cc152538d3ad3b99b933ccf881e53b193ee6af17bdd9a65905a6e1f8ab",
                "moduleName": "Canton.Internal.Ping",
                "entityName": "Ping"
              },
              "argument": {
                "record": {
                  "recordId": {
                    "packageId": "9a19e9cc152538d3ad3b99b933ccf881e53b193ee6af17bdd9a65905a6e1f8ab",
                    "moduleName": "Canton.Internal.Ping",
                    "entityName": "Ping"
                  },
                  "fields": [
                    {
                      "label": "id",
                      "value": {
                        "text": "ping_id"
                      }
                    },
                    {
                      "label": "initiator",
                      "value": {
                        "party": "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e"
                      }
                    },
                    {
                      "label": "responder",
                      "value": {
                        "party": "bob::1220254d06095b407f8c6a378b6fc443a67d3356ab8edfbf1378cb3e44218de32c8a"
                      }
                    }
                  ]
                }
              },
              "signatories": [
                "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e"
              ],
              "stakeholders": [
                "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e",
                "bob::1220254d06095b407f8c6a378b6fc443a67d3356ab8edfbf1378cb3e44218de32c8a"
              ]
            }
          }
        }
      ],
      "node_seeds": [
        {
          "seed": "Gv8neKcoUyIvsa5vdfjUxwGQLGuJOUeVO3j26YB4vOQ="
        }
      ]
    },
    "metadata": {
      "submitter_info": {
        "act_as": [
          "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e"
        ],
        "command_id": "f2ec4d8f-ccc1-402b-b278-7556fdd2b412"
      },
      "synchronizer_id": "da::12203c0ecb446b35b0efa78e0bda9fd91716855866150a5eb7611a2ed5d418129de3",
      "transaction_uuid": "0c36b6ea-a5a8-40ee-8708-d47589f34db7",
      "submission_time": "1739897973772660"
    }
  },
  "prepared_transaction_hash": "lafpRryDAe5lA8sBONBv0u2umlGKtnJXnhec/7AN+Ro=",
  "hashing_scheme_version": "HASHING_SCHEME_VERSION_V2"
}

Request

  • user_id: Identifier for the application interacting with the ledger.

  • command_id: Unique, random string identifying this specific command. Each command submission must have a new and unique command_id.

  • act_as: ID of the party issuing the command.

  • synchronizer_id: ID of the synchronizer that processes the transaction upon submission.

  • commands: Ledger commands for submission. In this case, it shows the creation of a Ping contract with Alice as the initiator, Bob as the responder, and a ping_id value. See the command documentation for details.

Response

Transaction

Represents the explicit ledger changes upon successful commitment.

  • version: Version of the transaction. This is also called LF Version.

  • roots: List of root node ids. A Daml transaction is a list of trees. The nodes are flattened in a single list (see below). The root node ids design the root node of each individual tree in the transaction.

Note

A current limitation of externally signed transactions is that they can only contain a single root node, and therefore a single transaction tree.

  • nodes: List of all nodes in the transaction. There are 4 types of nodes: Create, Exercise, Fetch and Rollback. The number, type and content of each node depends on the Daml model and the state of the ledger.

  • node_seeds: List of seeds used by the Canton protocol to generate cryptographically secure salts. They can be ignored.

Metadata

Additional information required for transaction processing.

  • ledger_effective_time: Time picked during the interpretation of the command into a transaction. Set if and only if the daml model makes use of time.

  • submitter_info: Contains the act_as party and command_id

  • synchronizer_id: Synchronizer that will be used for transaction processing

  • transaction_uuid: Unique value generated by the prepare endpoint to uniquely identify this transaction

    • The transaction UUID is randomly selected during the prepare step and is fixed from that point forward. This allows the mediator node to de-duplicate transactions and prevent replays.

  • mediator_group: Group of mediators that will gather confirmation responses for the transaction. Can be ignored.

  • submission_time: The timestamp that the Canton protocol will use as a submission time to perform validations (e.g for de-duplication)

  • disclosed_events: Existing input contracts used in the transaction

  • global_key_mapping: Unused in the current version, can be ignored.

Hash

  • prepared_transaction_hash: Pre-computed transaction hash. For security reasons the hash should be re-computed client-side as mentioned in the Compute transaction hash section.

Note

The prepare API can return additional details on how the Canton node is hashing the transaction to help troubleshoot hash-related errors (for example: pre-computed and re-computed hash mismatch).

To enable it:

  1. Enable verbose hashing on the participant config ledger-api.interactive-submission-service.enable-verbose-hashing = true.

  2. Set the verbose_hashing field in the PrepareSubmissionRequest to true.

Hashing Scheme Version

Version of the hashing scheme:

Protocol Version

Hashing Scheme Version

33

HASHING_SCHEME_VERSION_V2

Note

If the gRPC Ledger API authorization is enabled, the user must have the readAs claim on behalf of Alice to call the prepare endpoint.

2. Validate the transaction

Deserialize and inspect the transaction to verify its correctness before proceeding. The initiator of the transaction must be able to inspect and validate it to ensure it matches their intent before proceeding. See the Trust Model for guidance.

3. Compute the transaction hash

It is strongly recommended that the transaction hash be recomputed from the transaction and metadata to verify correctness. The pre-computed hash provided in the Prepare step is for debugging purposes.

  • The hashing algorithm specification is available here as well as in the release artifact under protobuf/ledger-api/com/daml/ledger/api/v2/interactive/README.md

  • An example implementation in python is available in the release articact under examples/08-interactive-submission/daml_transaction_hashing_v2.py

4. Sign the transaction hash

Using Alice’s protocol signing private key, sign the hash.

Note

Technically what is needed is the ability to sign with Alice’s key, not the key itself. The management of the key can be delegated to a wallet, HSM or crypto custody provider. In this tutorial the key is managed locally and explicitly to demonstrate the signing process. Refer to the onboarding tutorial for details on how to generate a key for this tutorial.

Assuming Alice's private key is stored in a file called alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e-private-key.der, he hash can be signed using openssl.

Note

In this tutorial the hash retrieved from the response of step 1 will be signed, without re-computing it as suggested in step 3. For an example of how to re-compute the hash, see the Python example.

TRANSACTION_HASH=$(cat create_ping_prepare_response.json | jq -r .prepared_transaction_hash)
PREPARED_TRANSACTION=$(cat create_ping_prepare_response.json | jq -r .prepared_transaction)
SIGNATURE=$(echo -n "$TRANSACTION_HASH" | base64 --decode | openssl pkeyutl -rawin -inkey alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e-private-key.der -keyform DER -sign | openssl base64 -e -A)

5. Execute the transaction

Submit the transaction and its signature to the ledger.

grpcurl -emit-defaults -plaintext -d @ localhost:4001 com.daml.ledger.api.v2.interactive.InteractiveSubmissionService/ExecuteSubmission <<EOM
    {
      "prepared_transaction": $PREPARED_TRANSACTION,
      "hashing_scheme_version": "HASHING_SCHEME_VERSION_V2",
      "user_id": "demo_app",
      "submission_id": "51dd5a0e-2ab6-4ca4-aa9d-9333fb603eb0",
      "party_signatures": {
        "signatures": [
          {
            "party": "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e",
            "signatures": [
              {
                "format": "SIGNATURE_FORMAT_RAW",
                "signature": "$SIGNATURE",
                "signing_algorithm_spec": "SIGNING_ALGORITHM_SPEC_EC_DSA_SHA_256",
                "signed_by": "1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e"
              }
            ]
          }
        ]
      }
    }
    EOM

In the request, note the presence of:

  • submission_id: Random string uniquely generated for this submission. This differs from the command_id in that a retry of this same prepared transaction would necessitate a new submission_id. The submission_id is used to correlate several submissions of the same command with completion events (See next step for more on completion events).

    • Because submission_id is not part of the signature, a command can be re-submitted with a different submission_id without requiring a new signature.

  • signatures: Object containing the signature of the transaction hash, along with metadata. In particular:

    • signing_algorithm_spec: Will vary depending on the key used during onboarding.

    • signed_by: Fingerprint of the protocol signing public key of Alice. This tutorial assumes the same key was used to create Alice’s namespace and her protocol signing key. This is why the fingerprint of the signing key matches the second part of her Party Id (after ::). For more details check out the onboarding tutorial and the parties documentation.

Note

If the gRPC Ledger API authorization is enabled, the user must have the actAs claim on behalf of Alice to call the execute endpoint.

6. Observe the transaction outcome

Monitor the completion stream for transaction confirmation, then retrieve the contract ID and binary blob representation.

grpcurl -emit-defaults -plaintext -d @ localhost:4001 com.daml.ledger.api.v2.CommandCompletionService/CompletionStream <<EOM
{
  "user_id": "demo_app",
  "parties": ["alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e"]
}
EOM

Wait until observing a completion event:

 {
   "completion": {
     "command_id": "f2ec4d8f-ccc1-402b-b278-7556fdd2b412",
     "status": {
       "code": 0,
       "message": "",
       "details": [

       ]
     },
     "update_id": "122049bb312e4ba2e6f142b2221f58589b75b0ad253685d3fc82f5758686b037efdb",
     "user_id": "demo_app",
     "act_as": [
       "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e"
     ],
     "submission_id": "51dd5a0e-2ab6-4ca4-aa9d-9333fb603eb0",
     "deduplication_offset": "0",
     "trace_context": {
       "traceparent": "00-65bc60e1399cad60cd2bceea6eddf4a7-9730a005a8779537-01"
     },
     "offset": "24",
     "synchronizer_time": {
       "synchronizer_id": "da::12203c0ecb446b35b0efa78e0bda9fd91716855866150a5eb7611a2ed5d418129de3",
       "record_time": "2025-02-19T17:21:50.512523Z"
     }
   }
 }
  • status.code: A value of 0 indicates the command completed successfully

  • offset: Ledger offset for the event

  • update_id: Unique Id for this completion event

  • submission_id: The submission Id chosen in the submission step

Let’s record both the offset and update_id for the next steps.

UPDATE_ID="122049bb312e4ba2e6f142b2221f58589b75b0ad253685d3fc82f5758686b037efdb"
OFFSET="24"

Note

You may need to interrupt the command with Ctrl-C as the completion stream is a gRPC server streaming RPC which waits for updates from the server until interrupted.

Let’s now retrieve the corresponding transaction:

grpcurl -emit-defaults -plaintext -d @ localhost:4001 com.daml.ledger.api.v2.UpdateService/GetTransactionById <<EOM
{
  "update_id": "$UPDATE_ID",
  "requesting_parties": ["alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e"]
}
EOM
{
  "transaction": {
    "update_id": "122049bb312e4ba2e6f142b2221f58589b75b0ad253685d3fc82f5758686b037efdb",
    "command_id": "f2ec4d8f-ccc1-402p-b278-7556fdd2b412",
    "workflow_id": "",
    "effective_at": "2025-02-19T17:21:50.485967Z",
    "events": [
      {
        "created": {
          "offset": "24",
          "node_id": 0,
          "contract_id": "00aa1fb173904244c175e87ecc226ab652ecce76554c7f5700efd21d11484a2877ca101220a19a8de75c840e5063010386a811ca392e848441102749bf522cd394a816750a",
          "template_id": {
            "packageId": "9a19e9cc152538d3ad3b99b933ccf881e53b193ee6af17bdd9a65905a6e1f8ab",
            "moduleName": "Canton.Internal.Ping",
            "entityName": "Ping"
          },
          "contract_key": null,
          "create_arguments": {
            "recordId": {
              "packageId": "9a19e9cc152538d3ad3b99b933ccf881e53b193ee6af17bdd9a65905a6e1f8ab",
              "moduleName": "Canton.Internal.Ping",
              "entityName": "Ping"
            },
            "fields": [
              {
                "label": "id",
                "value": {
                  "text": "ping_id"
                }
              },
              {
                "label": "initiator",
                "value": {
                  "party": "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e"
                }
              },
              {
                "label": "responder",
                "value": {
                  "party": "bob::1220254d06095b407f8c6a378b6fc443a67d3356ab8edfbf1378cb3e44218de32c8a"
                }
              }
            ]
          },
          "created_event_blob": "",
          "interface_views": [

          ],
          "witness_parties": [
            "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e"
          ],
          "signatories": [
            "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e"
          ],
          "observers": [
            "bob::1220254d06095b407f8c6a378b6fc443a67d3356ab8edfbf1378cb3e44218de32c8a"
          ],
          "created_at": "2025-02-19T17:21:50.485967Z",
          "package_name": "AdminWorkflows"
        }
      }
    ],
    "offset": "24",
    "synchronizer_id": "da::12203c0ecb446b35b0efa78e0bda9fd91716855866150a5eb7611a2ed5d418129de3",
    "trace_context": {
      "traceparent": "00-65bc60e1399cad60cd2bceea6eddf4a7-9730a005a8779537-01"
    },
    "record_time": "2025-02-19T17:21:50.512523Z"
  }
}

The events list includes a created object, representing the newly created contract. Extract the corresponding contract_id for reference. To finalize this step, retrieve the binary blob representation of the created contract. This serialized form will be required when executing a choice on the contract in Part 2.

grpcurl -emit-defaults -plaintext -d @ localhost:4001 com.daml.ledger.api.v2.StateService/GetActiveContracts <<EOM
{
  "filter": {
    "filters_by_party": {
      "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e": {
        "cumulative": [
          {
            "wildcard_filter": {
               "include_created_event_blob": true
            }
          }
        ]
      }
    }
  },
  "active_at_offset": "$OFFSET",
  "verbose": true
}
EOM
{
  "workflow_id": "",
  "active_contract": {
    "created_event": {
      "offset": "24",
      "node_id": 0,
      "contract_id": "00aa1fb173904244c175e87ecc226ab652ecce76554c7f5700efd21d11484a2877ca101220a19a8de75c840e5063010386a811ca392e848441102749bf522cd394a816750a",
      "template_id": {
        "packageId": "9a19e9cc152538d3ad3b99b933ccf881e53b193ee6af17bdd9a65905a6e1f8ab",
        "moduleName": "Canton.Internal.Ping",
        "entityName": "Ping"
      },
      "contract_key": null,
      "create_arguments": {
        "recordId": {
          "packageId": "9a19e9cc152538d3ad3b99b933ccf881e53b193ee6af17bdd9a65905a6e1f8ab",
          "moduleName": "Canton.Internal.Ping",
          "entityName": "Ping"
        },
        "fields": [
          {
            "label": "id",
            "value": {
              "text": "ping_id"
            }
          },
          {
            "label": "initiator",
            "value": {
              "party": "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e"
            }
          },
          {
            "label": "responder",
            "value": {
              "party": "bob::1220254d06095b407f8c6a378b6fc443a67d3356ab8edfbf1378cb3e44218de32c8a"
            }
          }
        ]
      },
      "created_event_blob": "CgMyLjESuQQKRQBjTUl2dyqzat8236jWAVQ7N5fKufP9tC25XfYKc8tcGMoQEiCC572aoJidPFPbnuK4QKF9wAuxGP1l7xumvhhNwM7khRIOQWRtaW5Xb3JrZmxvd3MaYApAOWExOWU5Y2MxNTI1MzhkM2FkM2I5OWI5MzNjY2Y4ODFlNTNiMTkzZWU2YWYxN2JkZDlhNjU5MDVhNmUxZjhhYhIGQ2FudG9uEghJbnRlcm5hbBIEUGluZxoEUGluZyKwAWqtAQoLCglCB3BpbmdfaWQKTwpNOkthbGljZTo6MTIyMGQ0NjZhNWQ5NmEzNTA5NzM2YzgyMWUyNWZlODFmYzhhNzNmMjI2ZDkyZTU3ZTk0YTY1MTcwZTU4YjA3ZmMwOGUKTQpLOklib2I6OjEyMjAyNTRkMDYwOTViNDA3ZjhjNmEzNzhiNmZjNDQzYTY3ZDMzNTZhYjhlZGZiZjEzNzhjYjNlNDQyMThkZTMyYzhhKkthbGljZTo6MTIyMGQ0NjZhNWQ5NmEzNTA5NzM2YzgyMWUyNWZlODFmYzhhNzNmMjI2ZDkyZTU3ZTk0YTY1MTcwZTU4YjA3ZmMwOGUySWJvYjo6MTIyMDI1NGQwNjA5NWI0MDdmOGM2YTM3OGI2ZmM0NDNhNjdkMzM1NmFiOGVkZmJmMTM3OGNiM2U0NDIxOGRlMzJjOGE544B3nIAuBgBCKgomCiQIARIgZrl+7TfHbM1LcYFnlh0pNxS091G09Le5mhD5PUCvwmkQHg==",
      "interface_views": [

      ],
      "witness_parties": [
        "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e"
      ],
      "signatories": [
        "alice::1220d466a5d96a3509736c821e25fe81fc8a73f226d92e57e94a65170e58b07fc08e"
      ],
      "observers": [
        "bob::1220254d06095b407f8c6a378b6fc443a67d3356ab8edfbf1378cb3e44218de32c8a"
      ],
      "created_at": "2025-02-19T15:42:56.032995Z",
      "package_name": "AdminWorkflows"
    },
    "synchronizer_id": "da::12203c0ecb446b35b0efa78e0bda9fd91716855866150a5eb7611a2ed5d418129de3",
    "reassignment_counter": "0"
  }
}

The request above might return multiple contracts if additional ones were created after the offset. The relevant contract should be identified by matching its contract_id. The created_event_blob contains a serialized version of the contract, which can be used in subsequent transactions to exercise choices on it.

This concludes Part 1 of the tutorial. In Part 2, Bob exercises the Respond choice to archive the contract.