Skip to main content

Content addressing

Every Motoko module in a Neutron package is named by the SHA-256 of its own final content. This is not a packaging convenience — it is load-bearing for correctness, deduplication, and security.

How a module gets its name

During packaging, for each root (the app entry, every memory schema, every migration edge):

  1. Read the module and strip comments and empty lines.
  2. Resolve its imports. mo:prim and mo:⛔ are ignored; mo:<package>/<path> goes through the Mops package map; relative imports resolve next to the importing file.
  3. Recurse — each imported module is processed the same way first.
  4. Rewrite every import path to the imported module's hash.
  5. Hash the resulting final content. That hex digest is the filename.
  6. Verify the final content still matches that digest, then write dist/mo/<hash>.mo.

The manifest's entry field is that hash string without the .mo extension.

Because imports are rewritten before hashing, a module's hash transitively covers its entire dependency closure. Two modules with the same hash are the same code compiled against the same code, all the way down.

What install-time verification checks

The installer independently re-verifies:

  • every package path under mo/ is exactly mo/<64-hex>.mo;
  • hashing the decompressed bytes equals the filename;
  • paths are relative POSIX-style with no empty, ., or .. segments and no backslashes.

A mismatch stops install preparation. A package cannot ship a module whose name lies about its content.

The shared /mo/ namespace

Installed modules land in a global, flat, content-addressed namespace — /mo/<hash>.mo — not under the app's prefix.

That gives three things:

Deduplication. Two apps depending on the same version of the same collection module store one copy. As an instance accumulates apps, marginal storage cost for shared libraries goes to zero.

A compile input the browser can enumerate. To compile a new actor, the kernel lists existing /mo/ keys through an authorization-protected query, then fetches each body over certified HTTP. Content addressing is what makes it safe to reassemble an app's dependency graph from a shared pool.

Immutability. A module file is never mutated. Uploading it is idempotent, so module upload happens early in an install — before any staging or journalling — and a failed install leaves no half-written module behind. Obsolete modules are collected in the verified commit step.

The comment-stripping caveat

Comments and empty lines are removed before hashing, but remaining whitespace is significant. Reformatting whitespace inside code, renaming a local variable, or changing an import path shape therefore changes the hash. Comment-only lines normally disappear, but inline comment edits can change the whitespace that remains, so comments are not categorically hash-neutral.

Memory schemas exploit this deliberately. A packaged schema carries two hashes:

FieldContentRole
hashComment-stripped schema source, before import rewritingImmutable lineage identity
entryThe executable module, after import rewritingWhat actually compiles

So upgrading a Motoko package used by a historical schema can change its executable entry without changing the schema's identity. The compiler still type-checks the resulting stable signature, so this cannot silently break the data contract.

The corollary: pin every package a schema imports, so rebuilding a historical version stays deterministic.

Interaction with the security policy

Content addressing is what makes the reviewed whitelist tractable. The whitelist maps exact content hashes to "dangerous findings in this specific content are accepted".

It does not trust a package name, an import path, or a version string — all of which an attacker can control. It trusts a byte sequence that has already been read. See Static checks.