Proposal: Rust SDK for Canton Network#407
Conversation
|
SIG labels auto-detected and applied: If this is incorrect, you can ask the reviewers to update the labels. |
|
Champion identified Canton Foundation The committee will verify this champion during review. |
|
|
Thanks for review 1 - On the PQS client: it depends on PQS's supported surface, not its internals. The PostgreSQL schema is an implementation detail that evolves (managed by Flyway) and we never touch it directly. We build on the PQS SQL API - the active/creates/archives/exercises functions plus the documented Daml-LF JSON encoding of the payload - which DA calls the stable, supported interface and the only artifacts readers should use. Our typed predicates are standard JSONB filters over that, and decoding/versioning use the representative package_id/package_version it exposes, which also keeps it correct under Smart Contract Upgrade. It's an optional crate, decoupled from the core client and version-pinned, so if a query pattern ever needs a surface PQS doesn't guarantee, we constrain that pattern rather than drop the feature. 2 - On the dpm component: it's mostly packaging on top of a documented, supported mechanism, not a bet on still-evolving behaviour. The component model is stable from SDK 3.5 onward, components are published as OCI artifacts and opted into via the components field, a local/custom codegen component is an explicitly supported shape, and codegen-java/codegen-js are already first-class. dpm codegen-rust follows that same pattern. As extra insurance the codegen also runs as a standalone binary, so even if the component contract shifts the generator keeps working and we just adjust the wrapper. 3 - On abstraction level: it's a thin typed client plus ergonomics plus opt-in resilience, which is the split Canton itself recommends. Writes and streaming go through the Ledger API (gRPC and JSON), reads go through PQS, and the SDK keeps no in-process Active Contract Set cache or event-sourced state of its own. Concretely that's typed, codegen-aware wrappers over the Ledger API, streaming-first, structured errors, an opt-in retry pipeline that preserves command_id so de-duplication stays correct, and OpenTelemetry. The participant owns authoritative state and PQS exposes it queryably, so the SDK stays a client rather than a framework. |
|
Hey @dmitryk-cf , thanks for the reply. This answers my question. Note to voting team: Given that SDK support for other languages is something that is approved for many languages and i think RUST is missing out on the list so this proposal covers that. Also this proposal covers many concerns raised by core team for other proposals on high level so I'm moving this to ready to vote to let core team look into technicalities |
|
@dmitryk-cf One probably important thing I forgot to mention is on Adoption metrics. I don't see clear path for it there so its good to add adoption metrics and also more weightage for funding on that adoption. |
|
@srikanth-bitdynamics |
|
Nice to see a Rust SDK in the mix 👌 One thing you'll probably hit on codegen: we wrapped Maybe you already had thought about that but... 🤔 which way are you leaning? |
Yeah, I've been thinking about exactly this, and the plan is the same: wrap daml-lf-archive rather than reimplement the decoder. The drift-free property is the whole point, so a native Rust decoder is not part of the proposed scope. For the JVM catch, most of it is absorbed by how the SDK is distributed. The pre-built canton-splice-* crates are plain Rust with no JVM at all, so the common case is just cargo add without running codegen directly. The JVM only comes in when someone generates bindings from their own DAR: inside dpm, it is already part of the expected toolchain; outside dpm, it is a build-time JDK/JVM requirement, never a runtime dependency. So: wrap daml-lf-archive, keep codegen as a build-time step, and make the no-JVM prebuilt crates the default path. Appreciate you flagging it. |
|
Nice, a maintained Rust SDK is exactly what's missing here. Big part of our team at VertexPoint Labs works in Rust, and right now using Canton means going straight to raw gRPC and rewriting the same Ledger API + codegen layer on every service. Would definitely pick this up |
|
Looks good to me. Rust is already widely used by infrastructure teams + funding is heavily tied to actual adoption. A maintained Rust SDK with codegen and token-standard support would also save teams from rebuilding the same Canton integrations over and over |
|
|
||
| ### 3. Architectural Alignment — Extending Daml / Canton into the Rust Ecosystem | ||
|
|
||
| The default approach is to extend what exists rather than introduce parallel infrastructure. This proposal does so at two layers. |
There was a problem hiding this comment.
I love this statement!!!
| - Documented Daml-LF → Rust type mapping; first `canton-splice-*` reference crates; sample app using generated bindings. | ||
| - **Verification:** demonstrate codegen → submit command → observe transaction → query ACS on both gRPC and JSON paths; an SCU version bump regenerates compatible code. | ||
|
|
||
| ### Milestone 3: CIP-56 Token Standard, External Signing, PQS Client, Conformance & Security Review |
There was a problem hiding this comment.
The Token Standard II specification was just ratified. I think you should add it since it will be very important going forward.
There was a problem hiding this comment.
Thanks, added first-class V2 (CIP-0112) support to canton-token (Account model, allocation/executor settlement, V2 transfer-event parsing) alongside V1, and reflected it in milestone 3 and the canton-splice-* crates.
… SDK proposal — Account model, allocation/executor settlement, committed allocations & iterated settlement, and EventLog parsing across abstract, mechanics, M3, and acceptance gates
|
@dmitryk-cf we discussed this in the committee meeting (you're welcome to join these in coordination with @hythloda whenever your grants are being discussed). I'm generally very supportive of a Rust SDK but we'd need to have a few individuals from companies building on Canton to comment here on the PR and:
|
|
Very nice to see a rust-sdk being developed. We do like to use rust especially around core cryptographic functions at DFNS! |
…lient Standard — scope v1 to the core client surface (codegen incl. contract keys, TLS, error classification, command recovery, resilient streams, OTel metrics, signing) and explicitly defer the rest (admin/topology, in-memory ACS, multi-sync management, batching, HA, node-health)
|
Posting a quick update on where the proposal stands after a round of changes. Alignment with the Ledger Client Standard. I went through the Ledger Client Standard doc and reworked the proposal to scope against it directly, so the Rust client lands on the same surface and the same patterns as the other language SDKs. v1 now maps to the core client surface in the Standard, and everything beyond that is written up as an explicit, named roadmap. Filled the v1 gaps. Going through the Standard surfaced a few things the earlier draft was light on, and those are now part of v1: TLS, error classification and retry, command recovery, resilient and resumable streams, OpenTelemetry tracing and metrics, configurable structured logging, and codegen that covers contract keys and the full dependency closure with both JSON and gRPC codecs. Token Standard V2 (CIP-0112). V2 is now a first-class target throughout, alongside CIP-56 (V1). Since it ships as new package versions and the codegen is SCU aware, both are reachable from the same client without a separate code path. I also spelled out the token coverage so it isn't ambiguous: one-step transfers, pre-approvals, allocate and transfer, and instrument inspection. Conformance. The M3 conformance suite is now mapped back to the Standard, so every v1 capability has a matching check, plus a published compatibility matrix. Why Nodejumper. I also updated this section to better cover our background and prior work (Rust and previous contributions). |
@noah-dfns one of the things we're currently struggling with in the Tech & Ops committee of the foundation is finding the right people who (1) have the right technical skills (2) intend to use the work output of the grant, and (3) are willing to commit the time to be design partners for the grant. The problem is that it's easy for us to approve a grant, but once we get to evaluating the milestones' acceptance criteria, we need companies that are motivated to use the work product and assess whether the acceptance criteria have been met to their satisfaction. So the question is - do you think DFNS can commit to:
This is a soft commitment with no enforcement mechanism, but we really need a few potential users to drive the feedback loops alongside the people developing the feature. It's also a good filtering mechanism for the voting committee to know how robust the demand is. |
|
@srikanth-bitdynamics and @daniel-t-p see my previous comment, same question to you. Ideally we have 3-4 design partners |
|
@shaul-da We would be happy to participate. We plan to use the Rust SDK in our application and can provide feedback during development. |
So in this proposed design is the only planned jvm dependency for wrapping the Daml LF decoder, but otherwise rust codegen would be operating off the protobuf? |
Yeah, exactly right on the runtime side. Let me just draw out the LF nuance. There are two separate lanes here. The runtime client (commands, transactions, streams over the Ledger API) is pure Rust off the gRPC protos via tonic/prost, with no JVM anywhere in that path. The only place daml-lf-archive shows up is codegen, and even there it's build-time only. So yes, that LF decoder wrap is the single JVM dependency in the design. On the "otherwise off the protobuf" part: that's spot on for the Ledger API lane, and it's almost true for LF too, since the DALFs inside a DAR are protobuf-encoded as well, you can even decode the wire bytes with prost. The catch is that the wire decode is the easy part. The protobuf definitions don't actually enforce all the rules of the LF spec, and the real value of daml-lf-archive is that it abstracts over the LF version and applies that spec correctly. That semantic layer is what moves with every LF release, so decoding the raw proto ourselves would just sign us up for the exact drift the wrap is there to avoid. It's also the same library that hands us the full dependency closure (a DAR of main plus its dependencies), which codegen needs anyway. So it stays the full daml-lf-archive rather than a hand-rolled parser. |
...
Here is a humble contribution (I am not a Scala specialist at all), this is the JVM part in use on the C# codegen, I believe it might be reusable for your Rust SDK. https://github.com/peacefulstudio/daml-codegen-csharp/tree/main/jvm-helper I am more than happy to get this reviewed and improved, to move to a separate repo, to relocate to the canton foundation GH org if anyone feels that it is more appropriate, etc... |
@shaul-da we would be willing to participate. |
|
@shaul-da We are also happy to review and be a design partner |
tomastauber-da
left a comment
There was a problem hiding this comment.
could/would Admin API be also covered, e.g. for manually constructing topology transactions?
|
|
||
| #### Daml-LF → Rust type mapping | ||
|
|
||
| The mapping is documented in full and covers: `Int64` → `i64`; `Numeric n` → a fixed-precision decimal type (`rust_decimal`/`bigdecimal`) preserving scale; `Text` → `String`; `Bool` → `bool`; `Party`/`ContractId a` → newtypes; `Time`/`Date` → typed wrappers; records → structs; variants → enums; enums → enums; `List a` → `Vec<T>`; `Optional a` → `Option<T>`; `TextMap`/`GenMap` → typed maps. Edge cases (high-precision `Numeric`, nested generics) are specified in the docs so behavior is predictable. |
There was a problem hiding this comment.
Time/Date could potentially use chrono
There was a problem hiding this comment.
Good call. Plan is to back those with chrono: Time to chrono::DateTime and Date to chrono::NaiveDate, with time available behind a feature flag.
| - Package dependencies are generated transitively: `daml-lf-archive` returns the main package plus its dependency closure, so a DAR that references types from other packages produces bindings for the whole graph rather than failing on missing references. | ||
| - Contract keys are generated as typed key types, so consumers can exercise choices by key and prefetch keys for accelerated execution, per the Ledger Client Standard. | ||
| - Generated types carry package id and version plus the PackageMap, so consumers resolve the correct template version under Smart Contract Upgrade. A version bump regenerates the bindings rather than breaking silently. | ||
| - Generated objects expose both JSON and gRPC (proto) codecs, matching the serialization the Ledger API's JSON and gRPC endpoints require. |
There was a problem hiding this comment.
I assume the JSON code would be via serde impls, but maybe worth explicitly mentioning it
There was a problem hiding this comment.
Yep, exactly. JSON is via serde (derived Serialize/Deserialize) matching the Daml-LF JSON encoding, and gRPC via prost. I'll make that explicit in the mapping.
| **Full delivery of this proposal will result in:** | ||
|
|
||
| - An idiomatic, async Rust client for the Canton Ledger API (gRPC and JSON transports), published on crates.io, with TLS (mutual and server-side), error classification (retriable vs non-retriable), retry, command de-duplication and recovery, resilient/resumable streams, and OpenTelemetry tracing and metrics. | ||
| - A DAR → Rust code generator that produces type-safe bindings for templates, choices, interfaces, and contract keys, built on `daml-lf-archive` and aware of Smart Contract Upgrade (SCU). | ||
| - Built-in token-standard support for both CIP-56 (V1) and Token Standard V2 (CIP-0112): holdings, transfer instruction, allocations, choice-context, disclosed contracts, plus the V2 additions (account structures, allocation/executor settlement, committed allocations and iterated settlement, and the V2 `EventLog`/transfer-event parsing). | ||
| - JWT/OIDC authentication with presets for common identity providers. | ||
| - Pre-built `canton-splice-*` crates for the protocol DARs (amulet, wallet, token-standard V1 and V2, validator-lifecycle, dso-governance), refreshed on each Canton/Splice release. | ||
| - Integration with `dpm` as a `dpm codegen-rust` component. | ||
| - A reference application, a conformance test suite verified against the Ledger Client Standard, an independent security review, and documentation. | ||
|
|
||
| **Out of scope for v1 (roadmapped, not excluded):** |
There was a problem hiding this comment.
It'd be worth mentioning the compiler / platform targets that are in scope and how.
For example, would WASM be covered and how? (e.g. just JSON Ledger API or gRPC via grpc-web as well?)
There was a problem hiding this comment.
I've added a targets section to the proposal.
The v1 supported and CI-tested matrix is native Tier-1 Rust: Linux, macOS and Windows on both x86_64 and aarch64, plus x86_64 musl for static container builds. That's where the SDK's audience runs (indexers, validators, oracle relays, settlement services), and there it's the full surface, gRPC over HTTP/2 via tonic plus JSON.
On WASM: the generated bindings are plain Rust (serde/prost) with no native deps, so they compile to wasm32 and are usable from WASM as-is, and that part is in v1. Browser transport is the piece I've put on the roadmap rather than v1. In the browser, unary JSON Ledger API calls work over fetch (reqwest's wasm backend), JSON streaming goes over WebSocket (web-sys), and gRPC needs grpc-web through a proxy (e.g. tonic-web-wasm-client) since native HTTP/2 isn't available there. That path mainly serves the front-end / dApp cohort, which is really the TypeScript SDK's lane, and it pulls in a grpc-web proxy plus a separate browser test matrix. So I've kept it demand-gated rather than claim it for v1 and keep the tested matrix honest.
|
|
||
| #### Key dependencies | ||
|
|
||
| `tonic`/`prost` (gRPC), `tokio`, `reqwest` (JSON transport), `daml-lf-archive` (LF decoding, via a thin JVM helper), `rust_decimal`/`bigdecimal`, `serde`, OpenTelemetry. |
There was a problem hiding this comment.
I assume the PQS client would use sqlx or something like that?
There was a problem hiding this comment.
Yep, sqlx is the plan: async, connection pooling, and safe bind-parameter binding.
There was a problem hiding this comment.
I'll add it to the dependency list
| `canton-codegen` reads Daml-LF via `daml-lf-archive`, walks the package, and emits idiomatic Rust: | ||
|
|
||
| - Templates become structs with typed fields; choices become typed exercise builders. | ||
| - Interfaces and views are generated as traits/types. |
There was a problem hiding this comment.
What would be the use case of having interfaces generated as traits? They are similar concepts, but from the Ledger client perspective in Rust, one won't likely implement or use the Daml interfaces as traits... one will usually just exercise some contract choices via an interface they implement, so it's primarily about input and output data of that invocation ?
There was a problem hiding this comment.
Agreed, interfaces are really data-first: the typed view and the choice arg/result of the invocation, plus a toInterface contract-id conversion on templates that implement them. I'll update the proposal to make that explicit.
This is great, thank you. Reusing it makes a lot of sense and I'm fully on board with the shared setup you described. Happy to help move it along. |
…t, target platforms, chrono Time/Date, serde/prost codecs, sqlx PQS, data-first interfaces
I've split the Admin API: party allocation/management and topology read (party-to-participant mappings, namespace delegations, vetted packages) are in v1. The part you mention — manually constructing/signing/submitting topology transactions — is on the roadmap for now. If there's demand for it, it can be added to v1. I've structured it as a named module specifically so it can move forward on request. It's also additive rather than a separate foundation: topology transactions already surface on the Ledger API update stream (which v1 reads), and external-party topology signing reuses the same hash-signing machinery v1 builds for interactive submission. |
|
Strong proposal, I also look forward to the creation of this SDK as I love coding in Rust. Rust also brings in value in the following ways due to the construction of the language:
Looking forward to the creation of this project. |
|
Hi, Kevin from K2F Labs here. Our team has already built much of the listed items in this proposal internally, and would like to be involved in this proposal and upstreaming the work as a milestone with DA's help. Our Rust SDK toolchain featuring codegen, transcoding JSON/proto, and runtime components has been running in production for >6mo for our self-custodial wallet and DEX. The implementation was intentionally designed similar to the original Java source code for better maintainability and eventual upstreaming. There are some missing features from the proposal such as a PQS client, wallet signing in the client, and dpm integration that would be valuable to have as well. Summary: 11 crates, layered from raw protobuf up to a high-level gRPC client. The dependency flow runs bottom-up: proto → LF model → transcoding → client/api → core.
ledger-proto — Generated gRPC stubs + types for the entire Canton Ledger API v2 and Canton admin surface (prost + tonic).
daml-lf-proto — Generated Rust types from the Daml-LF .proto schema (daml_lf, daml_lf_1, daml_lf_2). The low-level package format that DARs encode.
daml-lf — Daml-LF package model.
daml-transcoder — Descriptor-driven transcoding between JSON ⇄ proto Value.
daml-error — Path-tracking error context.
daml — Runtime library that generated Daml code depends on.
daml-derive — Proc-macro crate. #[derive(Event)] generates EventDecoder + EventFilter impls for user enums whose variants wrap Created / Exercised<Choice> (dispatches decoding by template ID). daml-codegen — Daml-to-Rust code generator (library + main.rs CLI).
ledger-api — JSON-serializable Rust domain types mirroring Ledger API v2 protobuf messages, with bidirectional proto conversion via ProtoCodec. Modules: command, event, transaction, update, state, reassignment, identifier, crypto, interactive. (utoipa-annotated for OpenAPI.) ledger-client — High-level gRPC Client (interceptor-generic) for the Ledger API. Supported features:
ledger-core — Higher-level infrastructure over the client (depends on sherpa-core).
|
Development Fund Proposal Submission
Proposal file:
/proposals/2026-06-Nodejumper-rust-sdk.mdSummary
This proposal requests funding for a production-grade, open-source Rust SDK for Canton Network: an async (tokio) Ledger API client over gRPC and JSON, type-safe code generation from DAR packages, built-in CIP-56 token-standard support, and JWT/OIDC authentication, shipped as a
dpmcomponent and published on crates.io under Apache-2.0.Canton's funded language coverage today is Go and Python (#38), C#/.NET (#46), and the TypeScript dApp SDK (#69). Rust is the one unfilled quadrant, and it is the language of the cohort that sits closest to the network: indexers, validators, oracle relays, and market-making engines. Those teams currently drop to raw gRPC/JSON and re-implement command de-duplication, codegen, and CIP-56 handling per project, or depend on a partial community crate with no codegen, conformance, or maintainer.
In short:
daml-lf-archivefoundation and integrated throughdpm, so it extends the toolchain rather than competing with it.We are a GSF-sponsored Canton validator and will maintain the SDK long-term, which is the part that matters most for a language binding.
What this delivers
canton-ledgerclient (gRPC + JSON) with correct change-ID command de-duplication, streaming, structured errors, and OpenTelemetry;canton-codegen: DAR → typed Rust via the officialdaml-lf-archive, Smart-Contract-Upgrade-aware, shipped asdpm codegen-rust;createdEventBlob);canton-splice-*crates for the protocol DARs (cargo add canton-splice-wallet), refreshed on each Canton/Splice release;Codegen and Smart Contract Upgrade
Code generation reads Daml-LF through the official
daml-lf-archivedecoder, not a bespoke DAR parser, so it tracks Daml/LF additions as they ship. Generated types carry package id and version plus the PackageMap, so consumers resolve the correct template version under Smart Contract Upgrade; a version bump regenerates compatible code rather than breaking silently. Codegen is invoked as adpmcomponent (dpm codegen-rust), not a competing standalone CLI.Checklist
/proposals/Notes for Reviewers
The scope is deliberately narrow: a Rust client SDK and codegen for the Ledger API plus CIP-56, distributed through
dpm. It does not add a smart-contract language, a wallet, an indexer, a DEX, or admin/topology tooling.The main points:
daml-lf-archiveunchanged, ships codegen as adpmcomponent, and requires no protocol, ledger-contract, or CIP-56/CIP-0112 changes.DLC-link/canton-libcrate is a useful low-level partial (some ledger calls, registry/wallet helpers) with no codegen, conformance, or maintenance guarantee. We consolidate that surface into a complete, versioned, maintained SDK and will engage its authors so the ecosystem converges rather than forks.