Skip to content

DX: provide canister-integration helpers (Rust macro, Motoko mixin) and enforce the cross-language interface contract #402

Description

@marc0olo

Context

The vetKeys EncryptedMaps (and KeyManager) feature is delivered as three coordinated artifacts that must expose the same Candid interface:

  • the Rust ic-vetkeys backend library,
  • the Motoko ic-vetkeys backend library,
  • the frontend @icp-sdk/vetkeys SDK (whose DefaultEncryptedMapsClient bundles a fixed idlFactory).

Today that equivalence is maintained by convention, not by construction:

  1. No enforcement that the interfaces match. There is no committed Motoko .did and no CI check that the Rust canister .did, the Motoko canister .did, and the frontend's bundled idlFactory agree. They line up only because of manual discipline (e.g. a // be consistent with the Rust canister comment). Any of the three can drift silently.

  2. Adopters hand-write ~200 lines of canister boilerplate, per language. The reference canisters are thin wrappers that only inject msg_caller, convert ByteBufBlob<32>, and delegate to the library — but every adopter copies that verbatim. This is error-prone and is exactly where an adopter's interface can drift from what the frontend expects. It also reproduces several known footguns (32-byte key limit, ByteBuf wrapping, Candid variant access-rights).

Net effect: adoption is harder than it should be, and "use the library + the frontend and it just works" is a promise rather than a guarantee.

Proposal

Make canister integration a few lines, and make the interface contract hold by construction.

1. Stay-in-sync guard (highest certainty, ship first; independent of the rest)

  • Treat the Rust reference canister's committed .did as the canonical interface (it already is, de facto — the frontend declarations were generated from it).
  • Add CI that asserts equality against the canonical .did:
    • the Motoko reference canister's generated .did,
    • the frontend's bundled idlFactory,
    • (later) the macro/mixin output.
  • Converts "compatible by convention" → "by construction"; catches drift the moment it happens.

2. Rust macro

  • Ship a library macro (e.g. ic_vetkeys::export_encrypted_maps_canister!{ domain_separator, access_rights, memory_ids }) that expands — in the adopter's crate — to the #[init], the state, all #[query]/#[update] endpoints, and export_candid!().
  • The macro owns the ByteBufBlob<32> conversion + the 32-byte validation, removing those footguns for the standard path.
  • Reference canister dogfoods the macro.

3. Motoko mixin

  • Motoko actor mixins allow defining public shared func endpoints in a reusable unit and include-ing them into an actor. Ship a library-provided mixin carrying the state + all endpoints; the adopter writes include EncryptedMapsCanister(keyName, "domain").
  • Reference canister dogfoods the mixin.
  • Open questions to verify before committing (gating):
    • Does a mixin holding the EncryptedMaps stable state survive canister upgrades under orthogonal persistence?
    • Do the async vetKey endpoints work inside a mixin?
    • Minimum moc version with mixin support, and whether icp-cli's Motoko recipe ships it.
  • Fallback if the spike fails: a verified canonical template + codegen, still validated by the sync guard from (1).

Endgame

Once the reference canisters consume the macro/mixin, those become the canonical interface definition, and the sync guard verifies the frontend idlFactory matches → the cross-language contract is guaranteed by construction.

Scope & sequencing

  • Entirely independent of fix(encrypted_maps): harden derived key material caching #401 (that is a frontend security fix; this is backend libraries + CI).
  • The macro/mixin are additive (new ways to build a canister; they don't change the interface), so they need not gate the unreleased 0.5.0. The sync guard should land sooner since it backs the cross-language promise.
  • The Motoko mixin is gated on the verification spike above; until then it stays a design, with the template/codegen fallback in reserve.

Related

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions