spec: service <: principal + release candid 0.10.32#748
Conversation
A service reference is represented by the principal of its canister, so every service type is a subtype of `principal` (and a service reference coerces at `principal`). Modelled exactly on `nat <: int`: an unconditional, premise-free widening in the Primitive Types subsections, with a value-preserving coercion. The wire encoding is unchanged — `M`/`R` for `service <actortype>` and `principal` are already byte-identical (`i8(0)` opaque; `i8(1) M(v* : vec nat8)` transparent), so the coercion is the identity on the reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the spec rule `service <actortype> <: principal` in both
subtype matchers (`subtype_collect_` and the `subtype_` probe): a service
type is unconditionally a subtype of `principal`, mirroring `Nat <: Int`.
Tests (test/subtypes.test.did): `service {}` and `service {m:()->()}`
decode where a `principal` reference is expected; `principal` does NOT
decode where a `service {}` is expected (one-directional). Verified the
positive cases fail without the checker change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pal)
`deserialize_principal` hard-asserted `wire_type == Principal`, rejecting a
service reference on the wire even though `service <: principal`. Since it is
dispatched only when the expected type is `principal`, replace that assertion
with `check_subtype()` (as `deserialize_service`/`deserialize_function` already
do): it admits `principal` (reflexive) and `service <actortype>` wire types,
both read as the same `PrincipalBytes`, so a service reference decodes as its
principal.
Tests (test/reference.test.did): an empty and a non-empty service reference
value decode at type `principal`. Verified they fail without this change; the
reverse (a principal at `service {}`) was already rejected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The niv pin (nixos-20.09) no longer evaluates on current nix, and the default `coq` is now Rocq 9.x, which renamed the stdlib namespace (Coq.* -> Stdlib.*) and relocated FunInd — both used by these proofs. Pin nixpkgs to a modern revision and build with coq_8_18 (the newest coq that accepts the proofs unchanged); drop the dead niv/stdenv.lib bits. `nix-build -A theories coq` is green again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the model with leaf types ServiceT/PrincipalT and reference values ServiceV/PrincipalV, the subtyping rule `ServiceT <: PrincipalT` (modelled exactly on `NatT <: IntT` — unconditional, coercion-preserving), and the matching `coerce` cases. Re-establish every downstream proof with the new constructors: coerce_constituent_eq, the coerce_nice_ind induction principle (new hypotheses + serviceHT/principalHT cases), and UpToNull. soundness and transitive_coherence go through unchanged in shape, so the new rule is machine-checked sound. Verified with coq_8_18. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds the Candid subtyping/coercion rule service <actortype> <: principal, aligning the spec, Rust reference implementation, Coq metatheory, and tests so that service references can be used wherever a principal is expected (without changing the wire format).
Changes:
- Specify
service <actortype> <: principaland the corresponding identity coercion inspec/Candid.md. - Update Rust subtype checking and decoding so
principalcan decode wire-typedservice <…>references. - Extend tests (DID subtyping + reference decoding) and update Coq formalization + Nix pinning for Coq 8.18.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/subtypes.test.did | Adds subtype assertions covering service {} <: principal and the non-reverse. |
| test/reference.test.did | Adds decoding assertions that a service reference value can decode as principal. |
| spec/Candid.md | Documents the new subtyping rule and identity coercion for service→principal. |
| rust/candid/src/types/subtype.rs | Implements unconditional Service(_) <: Principal in the subtype checker. |
| rust/candid/src/de.rs | Adjusts principal deserialization to admit service-typed wire values. |
| coq/nix/default.nix | Re-pins nixpkgs to obtain coq_8_18 for proof compatibility. |
| coq/MiniCandid.v | Extends the metatheory with ServiceT/PrincipalT, subtyping, and coercions. |
| CHANGELOG.md | Notes the new non-breaking decoding/subtyping behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com>
Review dismissed by automation script.
Address code-review feedback on the `service <: principal` decode path: - deserialize_principal: match the wire type directly against `principal` (reflexive) or `service <...>` instead of the general check_subtype(). check_subtype also admits `empty` (bottom) <: principal, which would let a payload declare wire type `empty` yet be decoded as a principal from arbitrary trailing bytes. The direct match is tighter and avoids the subtype-check overhead on this common decode path. - reference.test.did: add a negative test asserting `empty` (bottom) is rejected as a wire type for `principal`. Tests: candid_parser reference suite + candid crate suite pass.
|
✅ No security or compliance issues detected. Reviewed everything up to 85d9527. Security Overview
Detected Code Changes
|
Adversarial review — subtyping soundness & wire-formatI ran an independent adversarial pass over this PR focused on IDL subtyping soundness, wire-format equivalence, decoder correctness, and spec ⇄ impl ⇄ Coq consistency (read the spec/checker/decoder/Coq proof, built the Coq proofs, ran both Rust suites). Verdict: sound — no real defects found. 3 minor, non-blocking observations. Checked and OK
Minor observations (all optional, none block merge)
Adversarial review performed with an AI review agent (Claude Code); findings verified against the source. |
Defense-in-depth cases from adversarial review (behavior already verified by reasoning; these lock it in): reference.test.did (decode): - recursive service value (method returns the service) decodes at `principal` - `func`, `record`, `opt` wire types are rejected at `principal` (only `principal`/`service` share the PrincipalBytes encoding) subtypes.test.did (subtype check): - a recursive (µ) service type is <: principal (Var-alias chain resolves to the service head) All pass: candid_parser test_suite 6/6.
- CHANGELOG.md, spec/Candid.md: strip stray trailing CR introduced by the GitHub-UI "Apply suggestions" commit; normalize to LF-only. - de.rs: use the terse "principal" message in the principal-path check!, matching the deserializer's convention (e.g. "nat", "blob") rather than a sentence. (PR description bullet for deserialize_principal updated separately to reflect the direct wire-type match instead of check_subtype(). Spec line 759 prose left as-is: it mirrors the existing `nat <: int` phrasing at line 751.) candid_parser test_suite 6/6.
Review dismissed by automation script.
The CHANGELOG announced Candid 0.10.32 but the crate versions were never bumped, so `cargo publish` would have shipped this change as 0.10.31. - Move the 0.10.32 entry to a new dated section at the top of the CHANGELOG (matching the one-release-per-dated-section convention). - Bump `candid` and `candid_derive` to 0.10.32 (kept in lockstep per the "sync with the version" comments) and the internal dependency pin. - Refresh the workspace and bench lockfiles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`coq/nix/default.nix` was re-pinned to fetch nixpkgs directly and no longer imports `./sources.nix`, leaving the niv machinery (`sources.nix`, `sources.json`) as dead files. Remove them so the `fetchTarball` pin in `default.nix` is the single authoritative source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The re-pinned nixpkgs `fetchTarball` specified a sha256 that did not match the archive's actual unpacked (NAR) hash, so `nix-build -A theories coq` failed on a clean checkout with a hash mismatch. A local build passed only because `fetchTarball` served a TTL-cached copy that bypassed the check. Pin the hash CI computed for nixpkgs@e8273b29fe1390ec8d4603f2477357555291432e. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
service <: principalservice <: principal + release candid 0.10.32
Summary
A service reference is represented by the principal of its canister, so it is natural to use one wherever a
principalis expected. This adds the subtyping ruleservice <actortype> <: principaland its coercion. Long discussed in caffeinelabs/motoko#2264 (rossberg, chenyan-dfinity, and nomeata converged on it being reasonable at the Candid level).This PR also cuts the corresponding
candid0.10.32 release (see Release below).Spec (
spec/Candid.md)Modelled exactly on the existing
nat <: intrule — an unconditional, premise-free widening in the Primitive Types subsections (principalis itself primitive), with a value-preserving coercion:service <actortype> <: principalservice <text> : service <actortype> ~> principal <text> : principalIt is a conservative extension: it only enlarges
<:(no existing judgment is lost), and it is decode-sound because a service reference is already wire-encoded exactly like a principal —M/Rare byte-identical (i8(0)opaque;i8(1) M(v* : vec nat8)transparent) — so the coercion is the identity on the reference. No change to the binary format (T/M/R). The reverse (principal <: service) is intentionally not added.Reference implementation (
rust/candid)types/subtype.rs:(Service(_), Principal)accepted in both matchers, unconditional (like(Nat, Int)).de.rs:deserialize_principalnow matches the wire type directly againstprincipalorservice <…>(instead of assertingwire == principal), so a service reference decodes atprincipal(identicalPrincipalBytes). It deliberately does not use the generalcheck_subtype(), which would also admitempty(bottom)<: principaland let a payload be decoded as a principal from arbitrary trailing bytes.Tests
test/subtypes.test.did:service {}andservice {m:()->()}<: principal;principal</:service {}.test/reference.test.did: a service reference value decodes atprincipal(empty & non-empty); the reverse was already rejected.Both were confirmed to fail without the corresponding checker/decoder change.
Metatheory (
coq/)MiniCandid.v: extended withServiceT/PrincipalT+ServiceV/PrincipalV, the ruleServiceT <: PrincipalT(modelled onNatT <: IntT), and the coercion.soundnessandtransitive_coherencere-verify with the new constructors — the rule is machine-checked sound.coq/nixto a modern nixpkgs that providescoq_8_18(thenixos-20.09niv pin no longer evaluates; the defaultcoqis now Rocq 9.x, which renamedCoq.*→Stdlib.*and movedFunInd). Removed the now-unused niv machinery (sources.{nix,json}) and pinned thefetchTarballNAR hash.nix-build -A theories coqis green in CI.Release (
candid/candid_derive0.10.32)This PR also cuts the
candidrelease that carries the change:candidandcandid_derive0.10.31 → 0.10.32(kept in lockstep per the "sync with the version" comments) and the internalcandid_derivedependency pin; refresh the workspace andrust/benchlockfiles.CHANGELOG.md: new dated section documentingservice <: principalas a non-breaking change.Follow-ups (not in this PR)