CI: the committed .did files are never checked against the canisters they describe
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 21
- Forks
- 7
- Avg merge
- 10h 30m
- Merged PRs (30d)
- 5
Description
The committed .did files are the source the frontend declarations are generated from, and nothing verifies them against the canisters they describe.
frontend TS declarations
↑ checked interface-sync.yml regenerates and diffs these
committed Rust .did
↑ NOT CHECKED
Rust canister source
A change to a Rust endpoint's signature that nobody hand-mirrors into the .did produces a green run: the declarations still match the .did, and the .did no longer matches the canister.
Correcting my own framing
An earlier version of this issue said the skip at interface-sync.yml:44 deserved an explanation. It has one, at make_did_bindings.sh:12-15, and I should have read it before filing:
The interface-sync CI guard uses this so the check stays reproducible (it depends only on the pinned bindgen version and the committed
.did, not on the candid-extractor / wasm toolchain).
That is a property worth keeping, so this issue does not ask for SKIP_EXTRACT_CANDID to be removed. The guard should stay reproducible. The gap is that nothing else covers what the skip leaves out.
It is also not a config line. candid-extractor is not provisioned — provision-linux.sh:9 installs icp-cli, ic-wasm and ic-mops — so the naive fix adds a CI dependency.
A fix that needs no new dependency
ic_vetkeys_encrypted_maps_canister/src/lib.rs:26 already calls ic_cdk::export_candid!(), which brings __export_service() into the crate. So the comparison can be an ordinary Rust test, running in the job that already compiles the canister:
#[test]
fn committed_did_matches_the_canister() {
assert_eq!(
__export_service(),
include_str!("../ic_vetkeys_encrypted_maps_canister.did"),
);
}
No extractor, no wasm step, no workflow change, and it fails in the same place a signature change is made rather than one repository away.
It is also more reproducible than extraction, which is the property the skip
exists to protect. The test's generator is the candid crate version in
Cargo.lock. make extract-candid calls whatever candid-extractor is on
PATH — unpinned, unlike make_did_bindings.sh, which pins bindgen at 0.3.0.
So this is not a workaround that dodges the toolchain; it is the check
SKIP_EXTRACT_CANDID was waiting for.
The assertion fails on first run, so the PR is three steps. The committed
.did predates doc-comment emission — measured, 17 diff lines, all of them
comments carried from doc comments on the types in ic_vetkeys
(AccessRights, ByteBuf, EncryptedMapData), not on the endpoints:
$ candid-extractor …/ic_vetkeys_encrypted_maps_canister.wasm vs committed .did
DIFFERS -> 17 diff lines
$ didc check, both directions -> equivalent
So: add the test, regenerate both .did files, then regenerate the frontend
declarations in the same PR — they will gain /** */ blocks, so
interface-sync goes red otherwise. That last step is safe: bindgen strips
exactly two slashes, and the Rust extractor emits //, so the comments render
correctly. (It is Motoko's /// that leaves a stray slash — dfinity/icp-js-bindgen#161,
unrelated to this path.)
A second, independent improvement
Separately, the canisters do not carry their own interface:
$ ic-wasm …/ic_vetkeys_encrypted_maps_canister.wasm metadata candid:service
Cannot find metadata candid:service
Emitting candid:service as wasm metadata is what lets explorers and tooling read a canister's interface straight off mainnet. It is worth doing for its own sake rather than as a CI mechanism, and ic-wasm — already provisioned — is what reads it back.
And the same artifact should ship in the package
Absorbed from #443, where this was ask 4. It belongs here rather than in the
architecture issue: it is the same artifact, one consumer further along, and
it depends on nothing that issue decides.
Ship the canonical Candid as a package artifact, so an adopter who owns
some endpoints can prove they still satisfy the stock client — and so the
library can check itself.
Today make_did_bindings.sh regenerates the frontend declarations from the
Rust canisters' .did only, and no Motoko canister commits one, so
nothing compares the two languages' interfaces; agreement is enforced
behaviourally, by the shared suite running against the compiled Motoko wasm.
The composite's current guarantee is the thing being replaced:
Because the mixin is the single source of the endpoint set, the exposed
Candid interface is exactly the one the@icp-sdk/vetkeysfrontend expects,
by construction.
An adopter who re-declares seven endpoints has no way to assert they still add
up to that interface. We generate our canister's Candid and diff it against a
committed copy in CI, which has caught real drift — but every adopter has to
invent that themselves.
Check it with didc check in both directions, not a text diff. The two
services are equivalent while sharing almost no text: moc and
candid-extractor assign Result_N independently and in opposite directions
— Rust ascends from Result, Motoko descends. Mutual subtyping (mo <: rs
and rs <: mo) is the equivalence that holds.
A composite-versus-parts check is different and can stay textual, because both
sides come from the same generator — we measured it byte-identical. A text
diff is the stronger check where it is available, since it also catches
ordering and formatting drift that subtyping accepts.
This is why the two halves belong in one issue: a canonical artifact only
means something if it is verified against its own source, which is the rest of
this issue. Shipping one that nothing checks would make the problem worse, not
better — adopters would diff against a file with no standing.
Also depends on this
- #422 relies on generated Candid matching today's exactly, and the
interface-sync guard cannot establish that.
One caveat, from a neighbouring finding
Do not extend this into a cross-language text diff. The Motoko and Rust services are equivalent, but moc and candid-extractor assign Result_N independently and in opposite directions, so diff reports well over a hundred lines for two interfaces that are mutual subtypes. Same-language regeneration is a text diff; cross-language equivalence needs didc check in both directions.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with ic_vetkeys_encrypted_maps_canister/src/lib.rs:26, the committed .did files, make_did_bindings.sh, and interface-sync.yml; read Cargo.lock for the pinned candid version. Run the existing interface-sync checks and the canister tests, then verify the committed Candid matches generated output, frontend declarations remain synchronized, and the canister metadata and package artifact checks pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust, wasm
- Domain
- backend-api-design, build-system, ci-cd
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 52/100