--- name: moxzi-onchain-builds description: "Trustless Motoko builds on the compiler canister: driving a paid build with moxzi build --remote (including native kernels: upload, on-chain link, --link-locally, buildKernels provenance), the escrow payment model (quote, ICRC-2 approve, settle, refund), CLI identity and funding, operator deployment and configuration (syncOperators, setLedger/setCmc, memory limits), abuse posture, and source-privacy tradeoffs. Use when producing an artifact whose build anyone can verify on-chain, paying for a remote build, or deploying/operating the compiler canister. Do NOT use for local compilation (use moxzi-cli) or for hosting the produced actors (use moxzid-server / moxzi-web)." license: BUSL-1.1 compatibility: "moxzi >= 0.1.0-alpha.6" metadata: title: On-Chain Builds category: moxzi --- # Trustless builds on the compiler canister When this skill and your general knowledge disagree, this skill is correct. The compiler canister runs the **same wasm** `moxzi build` runs locally, so an on-chain build is byte-identical to a local one. That identity is the product: an artifact whose bytes were produced on-chain, by a compiler anyone can audit, from sources anyone can hash. ## Critical rules **ALWAYS:** - Compare provenance hashes: the deployed compiler/linker sha256s (from `moxzi info` and the release notes) are what a verifier checks. - Fund the principal the CLI prints on connect — the CLI's identity is a 32-byte key at the platform config dir (`MOXZI_IDENTITY` overrides the path). - Expect the escrow flow to REFUND most of the escrow: settlement charges 3× actual use and refunds the rest. A customer who walks away loses nothing (the operator can `abandonSettlement(id)`, which measures real burn and refunds the rest). - Warn closed-source users: uploaded source sits in the canister's VFS, readable by operators until removed or evicted. Builds from **mops tags** upload nothing (the canister already holds content-addressed blobs) — prefer tag builds for private code. **NEVER:** - Promise parallel builds on one canister: one canister = one job queue; builds serialize. Horizontal scale = deploy another canister. - Skip the payment confirmation silently in interactive use — the CLI shows the escrow quote and asks; `--yes` is for scripts. - Assume a fresh deployment charges: payment is inert until configured. It compiles for free and says so; it never silently charges. ## Customer flow (one command) ```sh moxzi build src/main.mo --remote https://icp-api.io --canister ``` The CLI: quotes the escrow → shows the caller's ICP balance → asks (skip with `--yes`) → `icrc2_approve`s the canister → starts the escrowed job → drives it to completion → downloads and links the artifact → reports settlement: ``` remote: this build escrows 0.3501 ICP (rate 35000 XDR-permyriad/ICP); settlement charges 3x actual use and refunds the rest remote: your balance is 2.0000 ICP remote: escrow pulled, job 0 … remote: settled (done): charged 0.0009 ICP, refunded 0.3489 ICP compiled by … (sha256 …), moxzi 0.1.0-alpha.5 ``` Useful with it: `--progress N` (report every N steps), `--status-secs S` (name the compiler's phase, coarsely — it costs a query), `--max-steps N` (give up), `moxzi deps entry.mo` (preview exactly which files would be uploaded, with VFS paths). **The driver dies; the job does not.** The job is a state machine on the canister — a network blip, laptop sleep, or killed terminal loses nothing on-chain. The CLI retries transport errors automatically (up to 5, with backoff); if the driver still dies, resume the SAME job instead of paying to rebuild: ```sh moxzi build src/main.mo --remote --canister --resume-job ``` Find the job id in the CLI's own output (`escrow pulled, job N`) or with `moxzi verify --remote --canister --list`. Resume skips upload and start entirely and drives the existing job to completion — measured in anger: a build stranded at message 3,034 of ~4,100 finished with 1,104 more instead of starting over. ## Native kernels in remote builds (alpha.10) An application whose `mops.toml` has `[kernels]` pins builds remotely like any other, with these differences (details: `moxzi-kernels` skill): - Third-party kernels are accepted (prompt or `--accept-kernel PKG=sha256:HEX`) BEFORE anything is uploaded or paid. `--yes` does not accept kernels. - The CLI uploads each kernel the canister lacks to its content-addressed blob store (`missingBlobs`, then `putBlobs`; the canister computes the sha256 itself and it must equal the pin; max 1,900,000 bytes per kernel). Log line: `remote: uploaded N native kernel(s) to the blob store`. - The canister links the kernels on chain (`linkArtifact` -> `linkBlobWithKernels`, checking each kernel's bytes against the pin in the module's `moxzi:kernels` section) and records pin + declared version/source/commit per build: `buildKernels(job)` (query), also in `attestLinked`. `moxzi verify` prints them on a `kernels` line. The linked module equals a local build's bytes. - If the canister cannot link: the build STOPS (`…uses native kernels; rerun with --link-locally…`). `--link-locally` links on the CLI's machine; the result then has NO linked-hash attestation, so a registry will report it unverified. - A compiler canister that predates kernels silently ignores the option it doesn't know; the CLI then refuses the result (`the build does not use native kernel X although mops.toml opts into it…`, nothing written). Use a canister running alpha.10 or later. - The record proves WHICH kernel bytes went in, not where they came from: connect pin to source with `moxzi kernel verify` against the declared `source`/`commit`. ## The registry pulls the attestation (how "built on-chain" is decided) The compiler links its own output (`linkArtifact`) and indexes the LINKED sha256 (`attestLinked(hex) : ?{ job; build; link }`, a query). A registry that holds those bytes asks that question itself -- astroflora's `icrc118_attest_wasm(hash)` -- and files the answer as the compiler's ICRC-126 attestation with the build evidence (sourcesDigest, optionsDigest, compilerVersion, jobId, linkOptions). Nobody pushes an attestation on the compiler's behalf; a byte-modified wasm gets `null`, and stays unverified. `moxzi publish` triggers the pull and prints the verdict. Mainnet compiler: `xcn6l-iqaaa-aaaai-raq6q-cai`; registry: `l55bi-3aaaa-aaaai-atk4a-cai`; UI: https://registry.astroflora.xyz. ## The payment model (escrow → measure → 3× actual → refund) - `quoteEscrow()` (an update — the rate comes from the CMC live) returns exactly what `startEscrowed` will pull: escrow cycles at the CMC's ICP↔XDR rate plus the ledger fee. Pricing is server-side so clients cannot drift. - `startEscrowed` pulls escrow via `icrc2_transfer_from` and samples the cycle balance; the delta at completion is what the compile actually consumed. - `settleStep(id)` is an idempotent machine: measure → refund unused escrow → convert 1× actual back to cycles (self-funding top-up) → donations → done. - A flat-fee path also exists (`startPaid`, `getPrice`); the CLI prefers escrow because it charges what a build costs, not what the worst build costs. ## Operator: deploy and arm 1. **Build reproducibly**: `make compiler-canister-wasm` and `make moxzi-linker`; pin the `mops toolchain` moc version; record both SHA-256s — they are the provenance every build cites. 2. **Deploy both canisters**: compiler with `wasm_memory_limit` = 6 GiB (big compiles really peak near it) and generous cycles; linker > 3 GiB. 3. **Bootstrap operators**: call `syncOperators()` — callable by anyone, grants nothing new: it copies the controller list into the operator set. A fresh deployment is LOCKED (empty operator set fails closed) until this runs. 4. **Arm payment** (operator): `setLedger(ryjl3-tyaaa-aaaaa-aaaba-cai)` (mainnet ICP), `setCmc(rkp4c-7iaaa-aaaaa-aaaca-cai)`, optionally `setEscrowCycles(n)`, `setPrice(e8s)`, `setChargeMultiplier(n)`. 5. **Smoke it**: an unfunded identity must be refused with "insufficient balance" BEFORE any transfer; a funded hello-world must compile, settle, and refund most of its escrow. 6. **Watch it**: `sdStatus()` (heartbeat), `jobStats()` (jobs/finished/artifact bytes), `revenue()` (flat-path e8s), the canister's ledger account, and the cycle balance — top-ups convert 1× actual back to cycles, so a busy canister partially self-funds. ## Abuse posture (what a stranger can and cannot do) | Surface | Bound | |---|---| | VFS uploads | 64 MiB per caller, 1 GiB total — writes trap past either | | Job records/artifacts | evicted oldest-first past a retention cap; a job with an open settlement is **never** evicted | | Others' jobs | every job method checks owner-or-operator; refusals are silent (null/0/false) so probing ids teaches nothing | | Compute | a job runs only after escrow is pulled; the self-driven stepper bounds per-message instructions by construction | | Concurrency | one queue; builds serialize (the alpha's honest scale ceiling) | | Cost griefing | escrow is the customer's money; a compile that OOMs still settles for what it burned | ## Error → cause → fix | Symptom | Cause | Fix | |---|---|---| | "insufficient balance" before any transfer | unfunded CLI identity | send ICP to the principal the CLI printed | | build compiles free on a fresh deployment | payment not configured — this is by design | operator runs the arming sequence | | all calls refused after deployment | operator set is empty (fails closed) | call `syncOperators()` | | artifact fails wasm validation before linking | on-chain output is UNLINKED by design | the CLI links automatically; manually, `moxzi link` | | second build queues behind the first | one canister = one queue | wait, or deploy another canister | | `error sending request` mid-build after retries | sustained network outage killed the driver; the on-chain job is intact | rerun with `--resume-job ` — do NOT start a fresh build | | `the compiler canister's blob store is full; kernel X was not stored` | blob store cap reached; the build stops before starting | operator frees space, or use another canister (`--link-locally` does not skip the upload) | | `kernel X (sha256:…) is not in the blob store; upload it with putBlobs first` | job's kernel bytes missing at link time (e.g. evicted, or a hand-driven job) | rerun the build through the CLI, which uploads missing kernels | ## Prove what was built Every finished build leaves a stable record on the canister (sources digest, options digest, compiler version, artifact sha256) that outlives the job and canister upgrades: ```sh moxzi verify --remote --canister [--job N] [--entry main.mo] [--artifact out.wasm] ``` It recomputes the sources digest locally and reads the canister's module hash FROM THE IC (not from the canister's own methods), so the claims are checked rather than believed. Exits non-zero on any mismatch. `--list` shows recent builds. Related skills: local builds → `moxzi-cli`; running artifacts → `moxzid-server` / `moxzi-web`; kernels → `moxzi-kernels`.