Canton
The Canton adapter bridges Tenzro Network with Digital Asset's Canton blockchain platform and DAML smart contracts, enabling enterprise-grade interoperability for regulated industries including finance, healthcare, supply chain, and government. Canton provides privacy-preserving synchronized ledgers with DAML 3.x smart contract execution.
Overview
Canton is an enterprise blockchain platform designed for regulated industries requiring privacy, auditability, and interoperability. Unlike public blockchains, Canton uses a "privacy by design" architecture where only authorized parties see transaction details. Canton's synchronization protocol enables atomic cross-domain transactions while maintaining data sovereignty.
The Tenzro Network Canton adapter (CantonAdapter) enables:
- Execution of DAML 3.x smart contracts from Tenzro
- Cross-chain contract state synchronization
- Privacy-preserving enterprise workflows
- Integration with Canton domain participants
- Real-time contract event streaming
- Multi-party workflow coordination
What is DAML?
DAML (Digital Asset Modeling Language) is a smart contract language specifically designed for multi-party workflows on distributed ledgers. Unlike Solidity (designed for public blockchains), DAML is built for enterprise scenarios where:
- Privacy is required — Only contract stakeholders see contract data
- Multiple parties coordinate — Contracts model real-world agreements
- Correctness is critical — DAML provides strong type safety and formal verification
- Portability matters — Same DAML code runs on multiple ledgers
-- Example DAML contract: Asset Transfer
template Asset
with
issuer: Party
owner: Party
amount: Decimal
where
signatory issuer
signatory owner
choice Transfer : ContractId Asset
with
newOwner: Party
controller owner
do
create this with owner = newOwnerArchitecture
The Canton adapter integrates Tenzro with Canton through the Canton Ledger API v2, a gRPC interface for submitting commands and querying contract state:
┌─────────────────────────────────────────┐
│ Tenzro Network │
│ (AI agents, wallets, settlements) │
└───────────────┬─────────────────────────┘
│
┌───────────────▼─────────────────────────┐
│ CantonAdapter (tenzro-bridge) │
│ - gRPC client (tonic) │
│ - Canton Ledger API v2 │
│ - DAML value encoding/decoding │
└───────────────┬─────────────────────────┘
│ gRPC (TLS)
┌───────────────▼─────────────────────────┐
│ Canton Node │
│ - Canton Ledger API │
│ - DAML Interpreter │
│ - Canton Synchronization Protocol │
└───────────────┬─────────────────────────┘
│
┌───────────────▼─────────────────────────┐
│ Canton Domain │
│ - Multiple participants │
│ - Sequencer (ordering) │
│ - Mediator (conflict resolution) │
│ - Topology manager │
└─────────────────────────────────────────┘The adapter provides three main gRPC service clients:
- CommandService — Submit DAML commands (create, exercise, exercise by key)
- StateService — Query active contracts and contract state
- PackageManagementService — Upload DAML packages (.dar files)
DAML Types in Tenzro
The tenzro-types crate defines Rust types corresponding to DAML values:
use tenzro_types::canton::*;
// Contract identifier (template + contract ID)
let contract_id = DamlContractId {
template_id: DamlTemplateId {
package_id: "0123456789abcdef".to_string(),
module_name: "Finance.Asset".to_string(),
entity_name: "AssetTransfer".to_string(),
},
contract_id: "00abc123...".to_string(),
};
// Party identifier (participant on Canton domain)
let party = DamlParty("Alice::participant1".to_string());
// DAML values (algebraic data types)
let value = DamlValue::Record(vec![
("issuer".to_string(), DamlValue::Party(party.clone())),
("amount".to_string(), DamlValue::Numeric("1000.50".to_string())),
("currency".to_string(), DamlValue::Text("USD".to_string())),
]);DAML supports rich algebraic data types:
Unit— Empty value (similar to Rust())Bool,Int64,Numeric,Text— Primitive typesParty— Canton participant identifierContractId— Reference to an active contractList— Homogeneous collectionsRecord— Named fields (like Rust structs)Variant— Tagged unions (like Rust enums)Optional— Nullable values
Configuration
use tenzro_bridge::adapters::CantonConfig;
let config = CantonConfig {
ledger_api_url: "https://canton.example.com:4401".to_string(),
participant_id: "participant1".to_string(),
application_id: "tenzro-bridge".to_string(),
party: DamlParty("TenzroNetwork::participant1".to_string()),
tls_enabled: true,
tls_ca_cert_path: Some("/path/to/ca.crt".to_string()),
};
let adapter = CantonAdapter::new(config).await?;Configuration Parameters:
ledger_api_url— Canton Ledger API gRPC endpointparticipant_id— Canton participant identifierapplication_id— Application identifier for command deduplicationparty— DAML party authorized to submit commandstls_enabled— Enable TLS for gRPC connectiontls_ca_cert_path— Path to CA certificate for TLS verification
Usage Examples
Submitting a DAML Command
Create a new DAML contract from Tenzro:
use tenzro_types::canton::{DamlCommand, DamlValue};
// Create an Asset contract
let create_command = DamlCommand::Create {
template_id: DamlTemplateId {
package_id: "finance-lib-1.0.0".to_string(),
module_name: "Finance.Asset".to_string(),
entity_name: "Asset".to_string(),
},
create_arguments: DamlValue::Record(vec![
("issuer".to_string(), DamlValue::Party(
DamlParty("CentralBank::participant1".to_string())
)),
("owner".to_string(), DamlValue::Party(
DamlParty("Alice::participant1".to_string())
)),
("amount".to_string(), DamlValue::Numeric("1000.00".to_string())),
("currency".to_string(), DamlValue::Text("USD".to_string())),
]),
};
// Submit to Canton
let transaction = adapter.submit_command(create_command).await?;
println!("Contract created: {}", transaction.transaction_id);Exercising a Choice
Exercise a choice on an existing contract (e.g., transfer ownership):
// Exercise the Transfer choice
let exercise_command = DamlCommand::Exercise {
template_id: template_id.clone(),
contract_id: "00abc123...".to_string(),
choice: "Transfer".to_string(),
choice_argument: DamlValue::Record(vec![
("newOwner".to_string(), DamlValue::Party(
DamlParty("Bob::participant1".to_string())
)),
]),
};
// Submit the choice exercise
let transaction = adapter.submit_command(exercise_command).await?;
// Parse created events to get new contract ID
for event in transaction.events {
match event {
DamlEvent::Created { contract_id, .. } => {
println!("New contract created: {}", contract_id);
}
DamlEvent::Archived { contract_id } => {
println!("Old contract archived: {}", contract_id);
}
}
}Querying Active Contracts
// Query all active Asset contracts
let contracts = adapter.get_active_contracts(
&template_id,
None // No filter
).await?;
println!("Found {} active contracts", contracts.len());
for contract in contracts {
// Extract contract arguments
if let DamlValue::Record(fields) = contract.create_arguments {
for (name, value) in fields {
match (name.as_str(), value) {
("owner", DamlValue::Party(party)) => {
println!("Owner: {}", party.0);
}
("amount", DamlValue::Numeric(amount)) => {
println!("Amount: {}", amount);
}
_ => {}
}
}
}
}Uploading a DAML Package
// Read compiled DAML package (.dar file)
let dar_bytes = std::fs::read("/path/to/finance-lib-1.0.0.dar")?;
// Upload to Canton
let package_id = adapter.upload_dar_package(dar_bytes).await?;
println!("Package uploaded with ID: {}", package_id);
// List all packages
let packages = adapter.list_packages().await?;
for pkg in packages {
println!("Package: {} (size: {} bytes)", pkg.package_id, pkg.size);
}Multi-VM Integration
The Canton adapter integrates with Tenzro's multi-VM architecture. The DamlExecutor in tenzro-vm provides a unified interface for DAML execution alongside EVM and SVM:
use tenzro_vm::executors::{DamlExecutor, VmExecutor};
use tenzro_vm::types::VmType;
// Create DAML executor with Canton connection
let executor = DamlExecutor::new(canton_config).await?;
// Execute DAML transaction
let tx = Transaction {
vm_type: VmType::Daml,
from: sender_address,
to: None, // Contract creation
data: encode_daml_command(create_command)?,
gas_limit: 1_000_000,
// ...
};
let result = executor.execute_transaction(&tx, &state_adapter).await?;
match result.status {
ExecutionStatus::Success => {
println!("DAML contract created successfully");
}
ExecutionStatus::Failure(reason) => {
eprintln!("DAML execution failed: {}", reason);
}
}Lazy Connection and Graceful Degradation
The Canton adapter uses lazy connection initialization and graceful degradation. If a Canton node is unavailable, the adapter returns proper errors instead of panicking:
// Adapter initializes without immediate connection
let adapter = CantonAdapter::new(config).await?;
// First API call triggers connection attempt
match adapter.submit_command(command).await {
Ok(transaction) => {
// Canton is available
}
Err(BridgeError::CantonUnavailable) => {
// Canton node is down, degrade gracefully
log::warn!("Canton unavailable, queueing command for retry");
queue_for_retry(command)?;
}
Err(e) => {
// Other error
return Err(e);
}
}This design allows Tenzro nodes to start without requiring Canton connectivity, essential for hybrid deployments where Canton integration is optional.
Use Cases
Enterprise Asset Tokenization
Issue tokenized assets on Canton with regulatory compliance, then bridge to Tenzro for AI-powered trading and settlement:
// Issue security token on Canton
let issuance = DamlCommand::Create {
template_id: security_token_template,
create_arguments: DamlValue::Record(vec![
("issuer", DamlValue::Party(issuer_party)),
("isin", DamlValue::Text("US0378331005".to_string())),
("quantity", DamlValue::Int64(1_000_000)),
("restriction", DamlValue::Text("RegS".to_string())),
]),
};
let tx = adapter.submit_command(issuance).await?;
// Bridge to Tenzro for liquidity and AI trading
bridge_to_tenzro(tx.contract_id).await?;Healthcare Data Exchange
Model healthcare data sharing agreements in DAML with privacy guarantees, coordinate across institutions via Canton, and use Tenzro AI agents for analytics:
// DAML contract for patient data sharing
template DataSharingAgreement
with
patient: Party
provider: Party
researcher: Party
purpose: Text
expiry: Time
where
signatory patient
signatory provider
observer researcher
choice ShareData : ContractId DataRecord
controller provider
do create DataRecord with ...Supply Chain Provenance
Track goods through multi-party supply chains on Canton, trigger Tenzro settlements based on delivery confirmations, use TEE for verification:
// Exercise delivery confirmation on Canton
let delivery = DamlCommand::Exercise {
template_id: shipment_template,
contract_id: shipment_id,
choice: "ConfirmDelivery".to_string(),
choice_argument: DamlValue::Record(vec![
("location", DamlValue::Text("Warehouse B".to_string())),
("timestamp", DamlValue::Timestamp(chrono::Utc::now())),
("signature", DamlValue::Text(tee_signature)),
]),
};
// Trigger automatic settlement on Tenzro
let settlement = await_canton_event(delivery).await?;
settle_on_tenzro(settlement).await?;Canton Synchronization Protocol
Canton's breakthrough innovation is the synchronization protocol enabling atomic cross-domain transactions. When exercising a DAML choice that affects contracts on multiple domains:
- Each domain's sequencer provides independent ordering
- Mediators validate authorization and detect conflicts
- All domains commit atomically or none commit (two-phase commit)
- Privacy is preserved — each domain only sees its portion of the transaction
This allows Tenzro to participate in multi-domain workflows where some parties use Canton for compliance while others use Tenzro for AI and public settlement.
Security Considerations
- TLS required — Always use TLS for Canton Ledger API connections in production
- Party authorization — Verify the configured party has authorization to submit commands
- Command deduplication — Use unique command IDs to prevent duplicate submissions
- Contract validation — Validate contract arguments match expected DAML types
- Event verification — Verify transaction events match expected contract lifecycle
Error Handling
use tenzro_bridge::error::BridgeError;
match adapter.submit_command(command).await {
Ok(tx) => {
println!("Command submitted: {}", tx.transaction_id);
}
Err(BridgeError::CantonUnavailable) => {
eprintln!("Canton node is unavailable");
}
Err(BridgeError::InvalidDamlCommand(reason)) => {
eprintln!("Invalid DAML command: {}", reason);
}
Err(BridgeError::DamlExecutionError(reason)) => {
eprintln!("DAML execution failed: {}", reason);
}
Err(BridgeError::UnauthorizedParty(party)) => {
eprintln!("Party {:?} not authorized", party);
}
Err(e) => {
eprintln!("Canton error: {:?}", e);
}
}Desktop App Integration
The Tenzro desktop app includes Canton integration via Tauri commands:
// List Canton domains (from desktop app)
const domains = await invoke('list_canton_domains');
// List DAML contracts
const contracts = await invoke('list_daml_contracts', {
templateId: {
packageId: 'finance-lib-1.0.0',
moduleName: 'Finance.Asset',
entityName: 'Asset'
}
});
// Submit DAML command
const result = await invoke('submit_daml_command', {
command: {
type: 'create',
templateId: templateId,
arguments: contractArgs
}
});Current Implementation Status
Implementation Status:
The Canton adapter has been rewritten with real tonic gRPC integration. It uses hand-crafted prost messages for Canton Ledger API v2 and implements lazy connection with graceful degradation.
When Canton is unavailable, the adapter returns proper VmError::CantonError instead of fake success data. See resolved issue #22 in the production readiness audit.
Roadmap
Planned improvements for Canton integration:
- Full Canton Ledger API v2 implementation (transaction trees, completions stream)
- Event streaming for real-time contract updates
- Multi-domain synchronization support
- DAML package compilation from Tenzro CLI
- Integration with Tenzro identity for party management
- Testnet deployment with Canton reference network
- Privacy-preserving analytics via ZK proofs on DAML state