feat(rust): custom_value_endpoints form for export_encrypted_maps_canister! - #424
Conversation
…ister!
Add a second form of the macro for the wrap-and-extend pattern: a canister that
keeps state linked to each encrypted value (e.g. a metadata row per entry) and
must own the value read/write endpoints to keep that state consistent.
export_encrypted_maps_canister!("app", [m0,m1,m2,m3], custom_value_endpoints);
This generates the control-plane endpoints, the stable state, the
#[init]/#[post_upgrade] hooks, and with_encrypted_maps/_mut accessors, but omits
the 7 value read/write endpoints. Adopters write their own value endpoints and
reuse the library's vetKD/crypto/access-control via the accessors.
Coarse (whole value data-plane) rather than a fine-grained exclude list, on
purpose: it is one unmissable decision instead of a list you can under-specify
(the exclude list proposed in the issue itself omitted a value-reader), and any
value endpoint added later is hidden from adopters automatically. See #423.
The full form is unchanged; its generated Candid is identical (endpoints are
factored into inner macros, but export_candid! sorts methods, so the .did and
interface-sync are unaffected). Backend-only: no frontend files or .did change.
- Factor the macro into __export_encrypted_maps_{common,control_plane_endpoints,
value_endpoints}! helpers composed by the two public arms.
- Emit with_encrypted_maps/with_encrypted_maps_mut accessors in both forms.
- Add a reference canister (ic-vetkeys-encrypted-maps-custom-canister) that
dogfoods the new form; wire it into the CI wasm build.
- Document the form, the omitted set, the write=correctness / read=hygiene
rationale, and the accessors in the macro rustdoc.
- Record the change under an Unreleased changelog heading (no version bump yet).
Refs #423.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
34cc483 to
bb57cf0
Compare
- Add a 'Ready-made EncryptedMaps canister' section to the ic-vetkeys crate README (the docs.rs landing page) covering both the full and custom_value_endpoints forms of export_encrypted_maps_canister! — the macro was previously not mentioned there at all. Example fenced as `ignore` since the README is included as crate docs (doctests). - Add a README to the new custom reference canister and cross-link it from the full reference canister's README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an opt-in “control-plane only” form to the Rust ic_vetkeys::export_encrypted_maps_canister! macro to support wrap-and-extend canisters that must own their value endpoints (e.g., to maintain linked side-state such as per-entry metadata), while keeping the existing full-canister form unchanged.
Changes:
- Extend
export_encrypted_maps_canister!with acustom_value_endpointsform and addwith_encrypted_maps/with_encrypted_maps_mutaccessors. - Factor macro expansion into shared/common + control-plane endpoints + value endpoints helper macros.
- Add a reference “custom canister” crate and wire it into the workspace and CI wasm build; update docs/changelog accordingly.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| Cargo.toml | Adds the new custom reference canister crate to the workspace members. |
| Cargo.lock | Records dependency graph updates for the new canister package. |
| backend/rs/ic_vetkeys/src/encrypted_maps/canister.rs | Implements the new macro form, factors endpoint generation, and documents the two forms + accessors. |
| backend/rs/ic_vetkeys/README.md | Adds a “Ready-made EncryptedMaps canister” section referencing the macro and new form. |
| backend/rs/ic_vetkeys/CHANGELOG.md | Notes the additive macro form under “Unreleased”. |
| backend/rs/canisters/ic_vetkeys_encrypted_maps_custom_canister/src/lib.rs | New reference canister exercising the custom_value_endpoints form with custom value endpoints. |
| backend/rs/canisters/ic_vetkeys_encrypted_maps_custom_canister/README.md | Documentation for the new custom reference canister. |
| backend/rs/canisters/ic_vetkeys_encrypted_maps_custom_canister/Cargo.toml | New crate manifest for the custom reference canister. |
| backend/rs/canisters/ic_vetkeys_encrypted_maps_canister/README.md | Cross-links to the new custom canister and macro form for wrap-and-extend use cases. |
| .github/workflows/backend-rust.yml | Builds the new custom canister in CI for wasm32 target. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Rustdoc: the macro now also defines with_encrypted_maps/_mut (and the endpoint fns) as items in the invoking module; correct the 'does not bind any common names' claim to be precise about types vs. these items. - Accessors: make with_encrypted_maps/_mut pub(crate) so they are reachable from submodules of the invoking crate, and replace unwrap() with expect(...) for a clear trap message if ever called before init. No interface/.did change (accessors are helper fns, not endpoints). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 10 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
backend/rs/ic_vetkeys/src/encrypted_maps/canister.rs:15
- The docstring says the
custom_value_endpointsform provides a single accessor, but the macro emits two functions (with_encrypted_mapsandwith_encrypted_maps_mut). Update wording to avoid confusion.
//! hooks and a [`with_encrypted_maps`]/[`with_encrypted_maps_mut`] accessor,
mraszyk
left a comment
There was a problem hiding this comment.
How about not injecting #[init]/#[post_upgrade], but exposing functions that could be invoked from #[init]/#[post_upgrade] so that people could define their own #[init]/#[post_upgrade]?
- Add parameter-semantics comments to the custom reference canister's endpoints. - Trim the duplicated custom_value_endpoints rustdoc example to a minimal invocation snippet and point to the compile-tested reference canister + password_manager_with_metadata for the full pattern. The init/post_upgrade injection question is left open pending more opinions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
On the Exposing setup fns instead would help adopters who need custom init args or post-upgrade migration logic (none in the current use case), at the cost of more lifecycle boilerplate and reintroducing the 'forgot |
Implements the Rust half of #423.
What changes
export_encrypted_maps_canister!gains a second form:The
custom_value_endpointsform generates the 8 control-plane endpoints, the stable state,#[init]/#[post_upgrade], and two accessors — but omits the 7 value read/write endpoints. Both forms now emit:Why
The generators are all-or-nothing today, so the common "wrap-and-extend" pattern — a canister that stores something linked to each value (e.g. a metadata row per entry) and must own the value endpoints to keep the two stores consistent — can't use the macro at all and hand-writes ~200 lines.
custom_value_endpointslets it generate the safe control-plane passthroughs and write only its custom value endpoints, reusing the library's vetKD/crypto/ACL via the accessors.Coarse (whole value data-plane) instead of a fine-grained
exclude: [list], deliberately — it's one unmissable decision rather than a list you can under-specify. (Theexcludelist in #423's own body proved the trap: it listed 6 names and omittedget_all_accessible_encrypted_maps, which returns raw values viaEncryptedMapData.keyvals— a leak.) It's also forward-safe: a value endpoint added to the library later is hidden from adopters automatically, whereas a baked-in exclude list would silently re-expose it. Full rationale and the alternatives considered are in the #423 thread.How it simplifies adoption (
password_manager_with_metadata)Today that example (~250 lines) forgoes the generator and hand-writes 11 endpoints; ~120–130 of those lines are the 8 passthroughs + init/post_upgrade/state setup that the generator already produces. With
custom_value_endpointsthose collapse to a 3-line macro call and only the 3 genuine*_with_metadataendpoints stay hand-written — roughly halving the file, keeping the passthroughs frontend-compatible by construction, and never emitting the raw value mutators (so the value↔metadata invariant can't be bypassed at the boundary).Which endpoints, and the rationale
insert_encrypted_value,remove_encrypted_value,remove_map_values; readsget_encrypted_value,get_encrypted_values_for_map,get_all_accessible_encrypted_values,get_all_accessible_encrypted_maps.set_user_rights/remove_usertouch only the ACL, no value cascade — so they're always safe to emit.Interface / release impact
export_candid!emits methods alphabetically, so the generated.didis identical — verified by diffing the extracted candid (method set identical) and by interface-sync regenerating declarations from the committed.did(untouched). No frontend files or.didchange; no frontend release.Cargo.tomlbump in this PR).Docs
Implementation & verification
__export_encrypted_maps_{common,control_plane_endpoints,value_endpoints}!helpers composed by the two public arms.ic-vetkeys-encrypted-maps-custom-canisterthat dogfoods the new form + accessors; wired into the CI wasm build..dididentical; encrypted_maps reference tests 19/19; doctests pass andcargo doc -D warningsclean;cargo fmt --checkand CI-stylecargo clippy -- -Dwarningsclean; custom canister builds host + wasm.Motoko gets the mirrored layered mixin in a separate PR.