You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The vetKeys EncryptedMaps (and KeyManager) feature is delivered as three coordinated artifacts that must expose the same Candid interface:
the Rustic-vetkeys backend library,
the Motokoic-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:
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.
Adopters hand-write ~200 lines of canister boilerplate, per language. The reference canisters are thin wrappers that only inject msg_caller, convert ByteBuf↔Blob<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 ByteBuf↔Blob<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.
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.
Context
The vetKeys EncryptedMaps (and KeyManager) feature is delivered as three coordinated artifacts that must expose the same Candid interface:
ic-vetkeysbackend library,ic-vetkeysbackend library,@icp-sdk/vetkeysSDK (whoseDefaultEncryptedMapsClientbundles a fixedidlFactory).Today that equivalence is maintained by convention, not by construction:
No enforcement that the interfaces match. There is no committed Motoko
.didand no CI check that the Rust canister.did, the Motoko canister.did, and the frontend's bundledidlFactoryagree. They line up only because of manual discipline (e.g. a// be consistent with the Rust canistercomment). Any of the three can drift silently.Adopters hand-write ~200 lines of canister boilerplate, per language. The reference canisters are thin wrappers that only inject
msg_caller, convertByteBuf↔Blob<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,ByteBufwrapping, 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)
.didas the canonical interface (it already is, de facto — the frontend declarations were generated from it)..did:.did,idlFactory,2. Rust macro
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, andexport_candid!().ByteBuf↔Blob<32>conversion + the 32-byte validation, removing those footguns for the standard path.3. Motoko mixin
public shared funcendpoints in a reusable unit andinclude-ing them into an actor. Ship a library-provided mixin carrying the state + all endpoints; the adopter writesinclude EncryptedMapsCanister(keyName, "domain").mocversion with mixin support, and whethericp-cli's Motoko recipe ships it.Endgame
Once the reference canisters consume the macro/mixin, those become the canonical interface definition, and the sync guard verifies the frontend
idlFactorymatches → the cross-language contract is guaranteed by construction.Scope & sequencing
0.5.0. The sync guard should land sooner since it backs the cross-language promise.Related
icskillsEncryptedMaps/vetkeys skills will need updating on release (package rename, version, caching-section rewrite).🤖 Generated with Claude Code