Profiles

Serve a profile from a canister

Put the profile in the canister's Wasm, where any client can read it with the interface in one certified request.

bash
ic-wasm canister.wasm -o canister.wasm metadata candid:service -f tokens.did -v public
ic-wasm canister.wasm -o canister.wasm metadata candid:profile -f tokens.profile.json -v public
ic-wasm canister.wasm metadata
text
icp:public candid:service
icp:public candid:profile

That is the whole change: no code in the canister, two custom sections in its built Wasm module. The first holds the interface, the second the profile, and both are public. Install or upgrade the canister with this module, and anyone who knows its id can read both in one certified request. The files are the example of Write a profile.

Add the profile to the module#

ic-wasm is DFINITY's Wasm tool for canisters; install it with cargo install ic-wasm --version 0.11.1 --locked. The commands above were run with that version, on a module that held nothing else. metadata <name> -f <file> writes the file's bytes into the custom section icp:<visibility> <name>; metadata with no name lists the sections; metadata candid:profile prints one back. ic-wasm canister.wasm info reported the profile section as 1543 bytes, the size of tokens.profile.json.

Pass -v public

ic-wasm's visibility defaults to private (its metadata --help says so). A private section is readable by the canister's controllers only, so a consumer would find nothing. fact Observed on PocketIC 16.0.0, recorded as the answer to OQ-1 in Open questions: a private section is refused to a caller that is not a controller.

Add candid:service too if your build has not, since the profile binds to it. The section is served from the next install or upgrade. A profile served this way changes with the module only: editing it changes the module hash and needs an upgrade.

Spec §8.1 also names candid-profile-kit embed canister.wasm --profile my.profile.json for this step. That tool is not in this repository and is not published, so this page uses ic-wasm.

Where the profile goes#

The profile document, as UTF-8 JSON without a byte order mark, is the content of the public custom section icp:public candid:profile. @meantjs/profile exports the names as SERVED_SECTION ("icp:public candid:profile") and SERVED_METADATA ("candid:profile").

  • fact The content of a custom section icp:public <name> is at /canister/<canister_id>/metadata/<name> in the state tree, and "otherwise, this path does not exist". The specification recommends icp:public candid:service for the canister's Candid interface; the profile is its neighbour. Source: IC interface specification, canister information.
  • fact Anyone may request that path for a public section, also when the canister has no such section, and the reply is a certificate of the subnet's state, unlike a query's reply, which one replica signs. Source: IC interface specification, HTTPS interface, read state.
  • fact A canister may export at most 16 custom sections named icp:… and 1 MiB of exported custom sections. Source: resource limits. The 1 MiB is for all of them together, candid:service included, so a served profile is at most 1 MiB (SERVED_MAX_BYTES, 1048576).

What may be served#

A profile of kind: "canister" and origin: "author". Its binds.canister_id may be left out, so that one module can be installed as many canisters; when it is given, it has to be the id of the canister that serves it. Bytes that are not such a document are EE_SERVED, and the profile is not applied; the rest of the service resolves without it.

The served bytesThe EE_SERVED message
More than 1 MiBthe served profile is … bytes, over the 1048576 a canister can serve
Not UTF-8the served profile is not UTF-8
A byte order mark firstthe served profile starts with a byte order mark
Not JSON, or not one JSON objectthe served profile is not JSON: …, … is not a JSON object
A packa canister serves a profile of kind "canister", not "pack"
Another origina profile its canister serves is the author's: origin "author", not …
Bound to another canisterthe served profile is bound to canister "be2us-…-qaabq-cai", but bkyz2-…-qaaaq-cai served it

How a consumer reads it#

  1. One read_state for both paths#

    Read /canister/<id>/metadata/candid:service and /canister/<id>/metadata/candid:profile in one read_state request, and verify the certificate. One certificate gives both from the same state, so the interface and the profile cannot come from two versions of the module.

  2. Absent is not unknown#

    A path the certificate shows absent means the canister serves nothing there, and no profile is not an error. A path the certificate neither holds nor shows absent is an error: the spec says the consumer "MUST NOT read it as absent".

  3. Bind it to the served interface#

    Bind the profile as any other, against the interface the same canister serves as candid:service: the same interface_id applies it, a different one makes it rebound or stale (Profiles, layers and binding). A canister that serves no candid:service gives nothing to bind against, and the consumer binds against the interface it has from elsewhere.

  4. Apply it in the author layer#

    A served profile is the author's. It merges above any third-party profile and any pack, so a pack tag it overrides is reported with WW_OVERRIDDEN.

Read it with @meantjs/profile#

js
import { createSchemaValidator, readServedProfile, resolveService } from "@meantjs/profile";

/**
 * Both metadata paths come from ONE verified read_state certificate. Pass null for a path the certificate
 * shows absent; a path it neither holds nor shows absent is an error, so do not call this with it.
 * @param {string} canisterId
 * @param {Uint8Array | null} serviceBytes  /canister/<id>/metadata/candid:service
 * @param {Uint8Array | null} profileBytes  /canister/<id>/metadata/candid:profile
 * @param {string} [fallbackDid]            the interface from elsewhere, when no candid:service is served
 */
export async function resolveServed(canisterId, serviceBytes, profileBytes, fallbackDid) {
  const did = serviceBytes !== null ? new TextDecoder().decode(serviceBytes) : fallbackDid;
  if (did === undefined) throw new Error("no interface to bind the profile against");
  const served = profileBytes === null ? null : readServedProfile(profileBytes, { canisterId });
  const validateSchema = await createSchemaValidator();
  const r = await resolveService({
    did,
    validateSchema,
    profiles: served !== null && served.doc !== null ? [{ doc: served.doc, label: "candid:profile" }] : [],
  });
  return { ...r, ok: r.ok && (served === null || served.ok), diagnostics: [...(served?.diagnostics ?? []), ...r.diagnostics] };
}

@meantjs/profile does not fetch anything and has no agent dependency: reading the certificate is your agent's job, and the package checks what came back. readServedProfile(bytes, { canisterId }) returns { ok, doc, diagnostics }, with doc null and one EE_SERVED when the bytes cannot be used. The profile it returns is not checked yet: resolveService checks its schema, its binding and its tags as for any profile. To try the function without a canister, feed it the two files:

js
import { readFileSync } from "node:fs";

const service = new TextEncoder().encode(readFileSync("tokens.did", "utf8"));
const profile = new TextEncoder().encode(readFileSync("tokens.profile.json", "utf8"));
const r = await resolveServed("bkyz2-fmaaa-aaaaa-qaaaq-cai", service, profile);
console.log(r.ok, r.profiles.map((p) => `${p.status} ${p.name} ${p.origin}`), r.diagnostics);
text
true [ 'applied tokens-example author' ] []

With a copy of the profile whose binds.canister_id names another canister, the same call gives false, no profile, and the EE_SERVED of the table above, followed by the two II_UNTAGGED_NUMERIC infos of a service that now has no profile.

canisterId is a string, or null to check a module that is not installed yet, since a module does not know the id it will run as; null skips only the canister_id check, and any other value that is not a string throws. Checking the module before you install it works the same way: ic-wasm canister.wasm metadata candid:profile prints the section, and readServedProfile with canisterId: null followed by resolveService gave the example profile applied, with no diagnostic.

A tagged .did#

The candid:service a canister serves may carry doc annotations (Annotate a .did file). They are an author profile too, read from the same certificate: profileFromDocAttributes(await didToDocAttributes(serviceText)) turns them into one. A consumer that reads both applies both in the author layer, where two different values for one tag at one position are EE_CONFLICT. So a canister can serve its whole profile in the interface's comments and no candid:profile at all, or both, as long as they agree.

Other channels#

A profile read through a query method, or fetched from any host but the canister's certified state, is not certified. A consumer may still apply it in the author layer when it trusts that channel, as for a profile the author ships with their code. A query method is not proposed as a way to serve a profile, because its reply is not certified.

What serving proves#

A served profile is certified, not vouched for. The certificate proves that the canister serves these bytes, so no replica or boundary node can change them on the way. The words are still the author's claim: a canister can serve a profile that calls a drain "claim reward", and the controller that installs the module chooses what it says. A consumer should still show the decoded arguments next to the intent for a transfer, approve, admin or destroy. Security model covers what a profile can and cannot vouch for, and Internet Computer the platform pieces this page relies on.