Repository navigation
Add a KeyManager canister macro (Rust) and mixin (Motoko) #422
Description
Motivation
EncryptedMaps has a canister macro (export_encrypted_maps_canister!, Rust) and mixin (EncryptedMapsCanister, Motoko) that generate a complete, correct canister interface from a few lines. The KeyManager has no equivalent — adopters (and our own reference canisters) hand-write the actor/module wiring, which is error-prone.
That risk is not hypothetical. #421 fixed two upgrade-safety bugs that existed purely because the KeyManager reference canisters were hand-written:
- Rust (
ic_vetkeys_manager_canister): no#[post_upgrade]→ after an upgrade the state wrapper stayedNoneand every endpoint trapped. - Motoko (
ic_vetkeys_manager_canister): the state field wastransient→ enhanced orthogonal persistence wiped it on upgrade.
A macro/mixin makes this class of mistake impossible for adopters, the same way it already does for EncryptedMaps.
Scope
- Rust: an
export_key_manager_canister!macro that generates#[init]+#[post_upgrade]and every#[query]/#[update]endpoint, taking theMemoryinstances from the caller (mirroringexport_encrypted_maps_canister!). - Motoko: a
KeyManagerCanistermixin (mo:ic-vetkeys/key_manager/Canister) with the persistedletstate +transientwrapper,include-able into apersistent actor. - Dogfood both in the reference canisters (
backend/{rs,mo}/canisters/ic_vetkeys_manager_canister), replacing the hand-written wiring. - Prove the generated Candid is byte-identical to today's. Not via the
interface-sync guard, which cannot do this — see the correction below.
Order: Motoko first, Rust follows #447
The two halves are gated on different things, and only the Rust one is blocked.
-
The Motoko mixin is soft-gated by Motoko: define EncryptedMapsCanister in terms of includable parts, so an adopter can own one endpoint #443, not blocked by it. It needs no
lifecycle change — a mixin composes into the adopter's ownpersistent actor,
so it never owns initialization. And it can ship in the shape the library
already uses:EncryptedMapsCanisteris a single composite over a single cut
(Canister.mo:70),
so aKeyManagerCanisterbuilt the same way is consistent with what exists
today rather than a regression against it.So if Motoko: define EncryptedMapsCanister in terms of includable parts, so an adopter can own one endpoint #443's boundary question stalls, this half should not wait on it: ship
the mixin in today's shape and re-cut it when the parts land. The cost is
making the cut twice — smaller than leaving adopters hand-writing the wiring
that produced both test: verify state persists across upgrades on both backends (pocket-ic 15) #421 bugs in the first place. -
The Rust macro is blocked by Rust: separate state, lifecycle and endpoints so an adopter can keep their own #[init] #447.
export_encrypted_maps_canister!
already emits#[init]and#[post_upgrade]
(backend/rs/ic_vetkeys/src/encrypted_maps/canister.rs:240and:251), and a
canister may have only one of each — so an adopter cannot combine the macro
with their own hooks, and two canister macros cannot be used together at all.
Shippingexport_key_manager_canister!before Rust: separate state, lifecycle and endpoints so an adopter can keep their own #[init] #447 settles who owns the
lifecycle would add a second instance of that problem rather than fix it.
Notes
- No frontend release required: the generated Candid must match the current
ic_vetkeys_manager_canister.didexactly, so@icp-sdk/vetkeysand its committed declarations are unaffected. - Correction — the interface-sync guard cannot enforce that for the Motoko half.
frontend/ic_vetkeys/make_did_bindings.shregenerates the TS declarations from.didfiles underbackend/rs/canisters/only, and no Motoko canister commits a.didat all. The guard therefore proves frontend ↔ Rust; nothing compares the Motoko canister's interface to anything. Motoko/Rust agreement is enforced behaviourally, by the shared integration suite running against the compiled Motoko wasm (backend/mo/canisters/*/Makefile:13, viaCUSTOM_WASM_PATH). So the Motoko half of this issue needs those behavioural tests, not the interface guard. - That gap is filed as CI: the committed .did files are never checked against the canisters they describe #445, and is an argument for Motoko: define EncryptedMapsCanister in terms of includable parts, so an adopter can own one endpoint #443's ask 4 (ship the canonical Candid as a package artifact): one committed artifact would let the Rust canister, the Motoko canister and adopters all diff against the same thing.
- Backend releases would carry it: a new
ic-vetkeysRust crate + Motoko mops version (as 0.8.0 / 0.6.0 did for EncryptedMaps).
Follow-up to #421. Companion to the original EncryptedMaps macro/mixin work in #402.
Activity
docs: explain why the manager post_upgrade takes no argument