Voter Verification Guide
This guide explains how to independently verify that a proposed canister upgrade in the OHSHII-LAUNCHER DAO was built from the claimed source code. NEVER trust proposal text alone - always verify before voting.
Security Warning
Section titled “Security Warning”CRITICAL: A malicious proposer can write anything in the proposal title and description. The ONLY way to know if a proposal is legitimate is to:
- Clone the source repository
- Checkout the exact commit referenced in the proposal
- Review the code changes
- Build with the sandboxed Docker script
- Compare your locally computed hash with the proposal’s hash
If the hashes don’t match, DO NOT vote for the proposal.
A matching hash is only half the review
Section titled “A matching hash is only half the review”A reproducible build proves the WASM is the source the proposal declares. It does not prove that source is safe. A backdoor genuinely present in the declared commit — an extra principal accepted by is_admin(), say — reproduces perfectly: your hash matches, and no artifact-level scan can see it, because it adds no method and no export.
Step 3 above is what catches that, and it is the step people skip because it used to be the awkward one. It no longer is.
Governance records the (repository, commit) pair every canister is currently running, written only after an upgrade’s module hash is confirmed on-chain. That gives the review a second endpoint: the proposal says where the code is going, this says where it is.
icp canister call xnsdw-yaaaa-aaaal-qsqua-cai get_deployed_source '(principal "<target_canister_id>")' -n icThe proposal view links the comparison for you when both sides are on GitHub. Otherwise, compare locally — note that the proposer’s commit normally lives in their own fork, since proposals are made before (or instead of) an upstream merge:
git clone <deployed_repo_url> && cd <repo>git remote add proposed <proposal_repo_url>git fetch proposedgit diff <deployed_commit> proposed/<proposal_commit>If get_deployed_source returns null, the baseline is not recorded — the canister was last upgraded before governance began storing provenance. That is not a clean bill of health: review the full proposed source rather than a delta.
Read the diff before voting. A green hash with an unread diff is not verification.
Embedded (nested) wasms: the backend carries other canisters’ code
Section titled “Embedded (nested) wasms: the backend carries other canisters’ code”The backend module embeds three other wasm modules as data blobs
(include_bytes! in src/backend/src/lib.rs): the SONS governance wasm it
installs on every campaign canister, and DFINITY’s ICRC ledger + index wasms it
installs for every created token. These blobs never appear in the backend’s
export section or candid metadata — reviewing “the backend’s methods” says
nothing about them. Three rules make them verifiable:
- The full reproducible rebuild covers them transitively — and it is the
ONLY check that does. The proposal’s WASM hash is a SHA-256 over every
byte of the module, embedded blobs included, and
docker-build.shrebuilds the SONS blob from source before building the backend. A matching hash therefore proves the embedded SONS wasm is exactly what the declared commit’ssrc/sons_governance/produces. Conversely: any weaker check (scanning the uploaded wasm’s exports, candid metadata, a partial build) proves nothing about the embedded blobs. 1-bis. Chunked install does not change what you verify. The backend installs the SONS blob by uploading it in ≤ 1 MiB chunks into the new canister’s own chunk store and callinginstall_chunked_code, which the platform completes only if the chunks reassemble to the declared hash. The bytes are identical to the embedded gz, so the module hash a created governance canister reports is stillembedded_backend_gzfromDeploy/WASM_HASHES.txt, and every check in this guide reads the same way. 1-ter. Which hash a given campaign governance canister reports. Right after the backend creates it, a campaign governance canister reportsembedded_backend_gz: the SHA-256 ofDeploy/wasm/sons_governance_embedded_in_backend.wasm.gz, which is the exact blob inside the backend (./Deploy/verify-proposal.sh ohshii_launcher_backendprints it and checks that those bytes are inside the backend module). A campaign canister that was later upgraded by one of its own proposals reports that proposal’s WASM Hash instead. The build compresses every module inside the build container, soDeploy/wasm/sons_governance_docker.wasm.gz(the module a SONS upgrade proposal carries) and the embedded blob are the same bytes and have the same hash. - Source review must include
src/sons_governance/(and every crate the backend embeds), not just the backend crate. A hand-editedis_admin()in the SONS source reproduces perfectly and passes every artifact scan — only reading the diff catches it. And never trust a binary committed in the tree: a.wasm.gzblob inside a commit is unreviewable by definition; trust comes from the pipeline rebuilding it from source (SONS) or from a pinned official hash (DFINITY blobs, next point). - The DFINITY blobs are pinned.
scripts/check_pinned_artifacts.py(run automatically bydocker-build.sh, and runnable standalone with just Python 3) verifiesicrc1_wasm/ic-icrc1-ledger.wasm.gzandic-icrc1-index.wasm.gzagainst hashes that were verified byte-identical to DFINITY’s official distribution (theledger-suite-icrc-2025-01-21GitHub release anddownload.dfinity.systemsat the wasm’s owngit_commit_id, respectively — the provenance trail is in the script’sPINStable). Any change to these blobs must arrive with an updated pin pointing at an official release.
As defence-in-depth, scripts/check_no_dev_admin.py (and the on-chain gate in
both governance canisters) also performs a nested scan: it walks the full
module bytes, finds embedded wasm modules (gzipped or raw), and fails if any of
them exports a dev_* method. Like the outer export scan, this catches the
lazy-malicious build — it does not catch deliberately obfuscated blobs or
source-level backdoors, which only rules 1–2 cover.
Prerequisites
Section titled “Prerequisites”Before you begin, ensure you have:
- Git installed
- Docker installed and running (for backend verification)
- dfx installed (for frontend evidence verification) - Install dfx
- icp-cli (optional) — interchangeable with dfx for every ON-CHAIN hash
check in this guide (
npm install -g @icp-sdk/icp-cli). Frontend evidence nuance: the STAGING flow is documented with icp-cli (icp canister call … propose_commit_batch, needsPreparepermission — see Managing an SNS → Stage the asset update), but icp-cli has no equivalent of the permissionless VOTER-side evidence computation (dfx deploy --compute-evidence), and the official docs’ alternative (re-staging from the same source and comparing evidence) requiresPreparepermission on the asset canister. THIS repo therefore ships a dfx-free equivalent:tools/compute-evidence(wraps the sameic_asset::compute_evidencelibrary call dfx uses — read-only, anonymous, no permissions; see the Frontend section below). - Node.js and npm installed (for frontend verification)
- Terminal/command line access
What a backend verification needs depends on the path you take:
| Path | You need | Time and disk (measured) |
|---|---|---|
| Rebuild the modules yourself (the minimal path) | Git, Docker with linux/amd64 support. No Rust toolchain. | About 7 minutes on an arm64 Mac building linux/amd64 under emulation, plus a one-time download of the pinned Rust image (about 3.5 GB unpacked). About 8 GB of Docker disk (image and build cache). |
./Deploy/docker-build.sh and ./Deploy/verify-proposal.sh (the full release path) | Git, Docker, Rust 1.95.0 with the wasm32-unknown-unknown target (the version in rust-toolchain.toml), cargo, Python 3, Node.js, and didc (downloaded automatically when missing). Before the Docker build it runs every release check on your machine, including cargo test --workspace. Some of those checks also read the sources of the companion OhShii Locker repository: clone it next to this repository, into a directory named dev-ohshii-locker, and keep a directory name containing launcher for this one. | 12 to 14 minutes measured with a warm local Cargo cache; a first run also compiles the whole workspace for the local tests, and the local target/ directory needs tens of GB. Plus the Docker disk of the minimal path. |
Quick Verification
Section titled “Quick Verification”For Backend Proposals (UpgradeCanister)
Section titled “For Backend Proposals (UpgradeCanister)”Use the one-click verification script:
# 1. Clone the repository and checkout the proposal's commitgit clone <public repository URL, published with the upgrade proposal> ohshii-launchercd ohshii-launchergit checkout <git_commit_hash_from_proposal>
# 2. Inspect the Dockerfile (IMPORTANT: read it before running)cat Dockerfile
# 3. Build (runs the release checks on your machine, then the Docker build)./Deploy/docker-build.sh
# 4. Run the verification helper./Deploy/verify-proposal.sh <canister_name>The script outputs the raw module SHA-256 hash and the .wasm.gz SHA-256 hash. Compare
the .wasm.gz hash with the proposal’s WASM Hash (--compare <hash> does it for you
and accepts either hash; --module <file> compares the raw module inside a file you
obtained). Without the Rust, Python and Node.js prerequisites, take the
minimal path instead.
It also runs a content check the hash cannot provide: it parses the wasm
export section of the artefact it just built and fails if any local-test
(dev_*) method is compiled in (scripts/check_no_dev_admin.py). Those methods
exist only in the local test environment’s builds and must never reach mainnet;
a hash match alone would not notice them, because a contaminated build compared
against itself still matches. (If python3 is unavailable the scan is skipped
with a visible warning — install it rather than voting on an unscanned build.) This scan does not replace the source review
described above — a backdoor written into the declared source adds no export, so
Step 3 remains the only defence against that class.
Valid canister names: ohshii_launcher_backend, dao_storage, pool_manager, ohshii_governance, sons_governance
For Frontend Proposals (AssetUpgrade)
Section titled “For Frontend Proposals (AssetUpgrade)”The verify-proposal.sh script does NOT work for frontend. Use dfx with --compute-evidence:
# 1. Clone the repository and checkout the proposal's commitgit clone <public repository URL, published with the upgrade proposal> ohshii-launchercd ohshii-launchergit checkout <git_commit_hash_from_proposal>
# 2. Install dependenciesnpm ci
# 3. Compute the evidence hash locally (requires dfx + network access)dfx deploy ohshii_launcher_frontend --network ic --compute-evidenceThis builds the frontend locally, connects to the IC to determine what batch operations would be needed, and computes the Evidence Hash. Compare it with the proposal’s Evidence field. If they match, the proposed assets are built from that source code.
dfx-free alternative (same evidence, no permissions needed): this repo
ships a tiny standalone tool wrapping the very library call dfx uses
(ic_asset::compute_evidence — read-only queries against the live canister,
anonymous caller). Unlike dfx it does NOT run the npm build for you — build
first:
npm cinpm run build:appcargo run --release --manifest-path tools/compute-evidence/Cargo.toml -- <frontend_canister_id> ./distThe printed hash is the evidence — compare it with the proposal’s Evidence field. Remember: evidence is a DELTA against the LIVE target canister’s current state, so always point the tool at the real canister the proposal targets (a test-environment canister yields a different hash by construction).
Concurrency (what if a second frontend proposal is attempted?): the
asset canister hard-serializes frontend upgrades — while a proposed batch
exists, create_batch fails with “Batch N is already proposed. Delete or
execute it to propose another” (verified in the upstream ic-certified-assets
source), so a second, different frontend update cannot even be STAGED until
the pending one is committed or deleted. Proposed batches never expire
(only un-proposed, inactive batches do, after ~5 minutes).
Cleanup is AUTOMATIC: when an AssetUpgrade (or dapp-batch asset step)
proposal ends in any non-executed terminal state — rejected, expired,
veto-upheld, or execution-failed — the governance canister schedules a
delete_batch on the target and its timer retries it with backoff until the
asset canister is unblocked (an executed proposal consumed its batch, so
there is nothing to delete). Batches staged but never attached to a proposal
are discovered and removed the same way (abandoned-session probe). Watch
progress via the public get_pending_asset_cleanups query on
ohshii_governance; a persistent failure surfaces as an
asset_batch_cleanup_gave_up event in the governance event log.
Fallback if the automatic queue gives up: ohshii_governance exposes
guardian_delete_asset_batch(<frontend_id>, <N>), a limited recovery command
for the governance-set guardian that runs the same guarded delete_batch path.
It refuses any batch that a still-live proposal references. Independently, the
standard asset-canister delete_batch method is available to any principal
holding Prepare or Commit permission on that asset canister.
This lock is also what keeps the evidence baseline stable for the whole vote: no other frontend change can land under a pending proposal.
Why this verifies a not-yet-executed proposal: the live state is only
the COMMON BASELINE. The proposer’s staged batch (not committed — the
canister still serves the old frontend) and your locally computed
operations are both diffs from that same unchanged state, so the two
evidence hashes match exactly when — and only when — the bytes the proposer
staged equal the bytes your build of the declared commit produces. If even
one staged file differs from the commit’s output, its content sha256 inside
the operations changes and the evidence no longer matches. (Prerequisite,
same as with dfx: the frontend build must be deterministic — same commit +
npm ci + same Node version.)
Requires: dfx installed (dfx path only), internet access to IC mainnet. See “Frontend (Asset) Canister Upgrades” below for full details.
Rebuild the modules yourself (the minimal path)
Section titled “Rebuild the modules yourself (the minimal path)”This path builds only the modules, from the commit, and compares hashes. It runs no
release checks on your machine, so it needs only Git and Docker with linux/amd64
support (Docker Desktop provides it; on an arm64 Linux host install the emulator once
with docker run --privileged --rm tonistiigi/binfmt --install amd64). No Rust
toolchain is needed.
git clone <public repository URL, published with the upgrade proposal> ohshii-launchercd ohshii-launchergit checkout <git_commit_hash_from_proposal>git status --porcelainThe last command must print nothing. Read Dockerfile before running it: the base image
is pinned by tag AND @sha256 digest and built for linux/amd64, ic-wasm is installed
with --locked, every cargo build uses --locked, and the last builder step compresses
every module with gzip -9 -n inside the container. Then build from the commit into an
empty directory outside the repository:
git -c core.autocrlf=false -c core.eol=lf archive HEAD | docker buildx build --platform linux/amd64 --no-cache --target export --output type=local,dest=../ohshii-rebuild -git archive HEAD sends only the files of the commit, byte for byte (the two -c
options and the repository’s .gitattributes stop git from converting line endings,
which it would otherwise do with core.autocrlf=true, the Git for Windows default), so nothing else on your machine
(an untracked file, a local build directory) can enter the build, and the result does
not depend on your operating system, your gzip, or the directory you cloned into.
../ohshii-rebuild/ then holds, for each canister, the raw module
<canister>_docker.wasm and the compressed module <canister>_docker.wasm.gz, plus
sons_governance_embedded_in_backend.wasm.gz.
Compare the raw module first. It is the module itself, independent of any compressor:
shasum -a 256 ../ohshii-rebuild/<canister>_docker.wasmCompare it with the raw hash the release publishes (the RAW WASM HASHES section of
Deploy/WASM_HASHES.txt), or with the raw module of the proposal’s file:
gunzip -c <the proposal's .wasm.gz> | shasum -a 256.
Then compare the module the proposal installs:
shasum -a 256 ../ohshii-rebuild/<canister>_docker.wasm.gzThis must equal the proposal’s WASM Hash. The .wasm.gz is made inside the build
container, so it is byte for byte the same on every machine.
The SONS module every new campaign receives is
sons_governance_embedded_in_backend.wasm.gz, byte for byte:
shasum -a 256 ../ohshii-rebuild/sons_governance_embedded_in_backend.wasm.gzA campaign governance canister created by the backend reports this hash as its module hash (rule 1-ter above).
After execution, read the installed module hash (public; no identity needed) and
compare it with the .wasm.gz hash above:
dfx canister info <canister_id> --network ic --identity anonymousA MISMATCH at any step means the proposal does not install what this commit builds. Do not vote for it.
This path proves that the bytes come from the commit. It does not review the commit:
read the diff as described in “A matching hash is only half the review”, and run the
export scan (python3 scripts/check_no_dev_admin.py ../ohshii-rebuild, Python 3 only).
Detailed Verification Steps
Section titled “Detailed Verification Steps”Step 1: Clone the Repository
Section titled “Step 1: Clone the Repository”git clone <public repository URL, published with the upgrade proposal> ohshii-launchercd ohshii-launcherStep 2: Checkout the Exact Commit
Section titled “Step 2: Checkout the Exact Commit”The proposal displays a git_commit_hash. Checkout that exact commit:
git checkout <git_commit_hash>Important: If the commit hash doesn’t exist in the repository, the proposal may be fraudulent.
Step 3: Inspect the Dockerfile
Section titled “Step 3: Inspect the Dockerfile”Before running any build, read the Dockerfile to ensure it doesn’t contain malicious commands:
cat DockerfileLook for:
- A base image pinned by tag AND
@sha256digest, with--platform=linux/amd64(rust:1.95.0-bookworm@sha256:…, the Rust version ofrust-toolchain.toml) cargo install ic-wasm --version 0.7.0 --locked, and--lockedon everycargo build- The final
gzip -9 -nstep that compresses every module inside the container - No suspicious
curl,wget, or external downloads - No hardcoded private keys or backdoors
Step 4: Run the Reproducible Build
Section titled “Step 4: Run the Reproducible Build”The Docker build produces deterministic WASM files:
./Deploy/docker-build.shThis script:
- Runs every release check on your machine first (generated declarations,
cargo test --workspace, the Node.js and Python source gates), so it needs the full prerequisites listed above - Builds all 5 canisters in an isolated Docker container, from a
git archiveof the commit (untracked files never enter the build) - Optimizes WASMs with
ic-wasmand compresses each one withgzip -9 -ninside the container - Outputs hashes to
Deploy/WASM_HASHES.txt
Build time: 12 to 14 minutes measured on the same machine with a warm local Cargo cache (about 2.5 minutes of checks on your machine, then the Docker build); a first run also compiles the whole workspace for the local tests. If you only want the hashes, the minimal path skips the checks on your machine.
Step 5: Verify the Hash
Section titled “Step 5: Verify the Hash”Option A: Use the verification helper
./Deploy/verify-proposal.sh <canister_name>Valid canister names:
ohshii_launcher_backenddao_storagepool_managerohshii_governancesons_governance
Option B: Manual verification
# The hash is already in WASM_HASHES.txtcat Deploy/WASM_HASHES.txt | grep <canister_name>
# Or calculate manuallyshasum -a 256 Deploy/wasm/<canister_name>_docker.wasm.gzStep 6: Compare with Proposal
Section titled “Step 6: Compare with Proposal”Open the proposal in the OHSHII-LAUNCHER governance interface. Compare:
| Proposal Type | Compare your hash with… |
|---|---|
| Backend Upgrade | WASM Hash field |
| Frontend (Asset) Upgrade | Evidence field (see the frontend section below) |
MATCH = The proposed WASM was built from the claimed source code. Review the source diff (see “A matching hash is only half the review”) before you vote.
MISMATCH = The proposed WASM is different. DO NOT VOTE for this proposal.
Understanding the Hashes
Section titled “Understanding the Hashes”Raw vs Gzipped WASM
Section titled “Raw vs Gzipped WASM”- Raw
.wasm: Uncompressed WebAssembly module - Gzipped
.wasm.gz: Compressed withgzip -9 -ninside the build container
DAO proposals use .wasm.gz because:
- Smaller size = lower storage costs
- Faster upload to the Internet Computer
Why the .wasm.gz is made inside the container
Section titled “Why the .wasm.gz is made inside the container”-n stops gzip from writing the file name and modification time into the header, so
one gzip program gives the same bytes every time. It does not make different gzip
programs agree: macOS gzip and GNU gzip compress the same module to different bytes
(measured on all five modules). So the build compresses every module inside the build
container, with the gzip of the pinned image, and exports the .wasm.gz; nothing is
compressed on your machine. Two consequences:
- the
.wasm.gzyou rebuild is byte for byte the proposal’s, whatever your operating system; - if you ever compress a module yourself, compare the RAW module
(
gunzip -c file.wasm.gz | shasum -a 256), never your own.wasm.gz.
Hash Types in Proposals
Section titled “Hash Types in Proposals”| Hash Type | Used For | Description | How to Verify |
|---|---|---|---|
| WASM Hash | Backend upgrades | SHA-256 of the .wasm.gz module | ./Deploy/verify-proposal.sh <canister> |
| Deployed module hash | Any installed canister | SHA-256 of the bytes actually installed (.wasm.gz after a governance execution; raw .wasm after a direct pipeline deploy) | dfx canister info <id> --network ic or icp canister status <id> -n ic — or ./scripts/verify-hash.sh ic (checks both encodings) |
| Evidence | Frontend upgrades | Deterministic hash over all batch operations | dfx deploy ohshii_launcher_frontend --network ic --compute-evidence (dfx, or the repo’s dfx-free tools/compute-evidence; icp-cli itself covers only the Prepare-permission staging flow) |
| Content Hash | Frontend upgrades | SHA-256 of individual uploaded files | shasum -a 256 <file> (simpler alternative) |
Backend Canister Upgrades
Section titled “Backend Canister Upgrades”Backend upgrades use install_chunked_code to deploy compiled Rust canisters.
Verification Flow
Section titled “Verification Flow”- Proposer builds WASM with Docker
- Proposer uploads chunks to proposal storage
- Proposal stores
wasm_module_hash(SHA-256 of.wasm.gz) - You verify by rebuilding and comparing hashes
- At execution, backend recalculates hash from stored chunks (TOCTOU protection),
installs them, and on-chain re-verifies that the deployed module hash equals
the proposal’s WASM Hash (
verify_installationin ohshii_governance) - You can independently confirm the same thing with
dfx canister infooricp canister status(see below)
Post-Execution Verification
Section titled “Post-Execution Verification”Which hash to expect — this depends on WHO installed the canister:
| Deploy path | Bytes installed | Deployed module hash equals |
|---|---|---|
| Governance proposal execution (the DAO path this guide is about) | the proposal’s .wasm.gz chunks | the proposal’s WASM Hash (= SHA-256 of Deploy/wasm/<name>_docker.wasm.gz) |
| Direct install by a canister controller, outside a proposal | the raw .wasm | SHA-256 of Deploy/wasm/<name>_docker.wasm |
Read the deployed module hash with either CLI (public information — no
identity or controller rights needed; both print it with a 0x prefix):
dfx canister info <canister_id> --network ic --identity anonymousicp canister status <canister_id> -n icCompute the local hashes to compare against:
shasum -a 256 Deploy/wasm/<canister_name>_docker.wasm.gzshasum -a 256 Deploy/wasm/<canister_name>_docker.wasmOr let the repo do the whole comparison (read-only; it checks BOTH encodings and tells you which deploy path the on-chain hash corresponds to):
./scripts/verify-hash.sh icTiming matters: these on-chain checks are meaningful only AFTER the
proposal has executed, and only if your local build is the proposed commit.
While the vote is still open, the chain still runs the previous version and
a mismatch is expected — during the vote the only valid check is
verify-proposal.sh against the proposal’s WASM Hash.
Frontend (Asset) Canister Upgrades
Section titled “Frontend (Asset) Canister Upgrades”Frontend upgrades use the IC asset canister’s batch upload system (the same used by all SNS DAOs on the Internet Computer). This is a fundamentally different process from backend upgrades.
Important: The verify-proposal.sh script does NOT work for frontend. Use dfx deploy --compute-evidence instead.
Reference: This verification process follows the official DFINITY documentation: SNS Asset Canisters
How Asset Canister Updates Work (Background)
Section titled “How Asset Canister Updates Work (Background)”Unlike backend canisters (where you upload a single .wasm.gz file), frontend assets (HTML, CSS, JS)
are stored inside the asset canister. All asset canisters use the same standard assetstorage.wasm.gz
code provided by dfx — so module_hash is the same for every asset canister. What differs is the
content stored inside.
The update process works like this:
- Proposer calls
prepare_asset_upload(<frontend canister id>, <expected chunks>)onohshii_governancefirst. It collects the proposal fee, gives the caller a 30-minute lease on the target, and grants the callerPrepareon it. It also deletes any proposed batch already on the target that no other live lease or live proposal holds — so a batch staged before this call is deleted by it. Always prepare first, then stage. - With the same identity, the proposer runs
dfx deploy ohshii_launcher_frontend --network ic --by-proposal - dfx builds the frontend, uploads asset chunks to the canister, and locks the batch
- The asset canister computes an evidence hash via
compute_evidence()— a deterministic hash over ALL batch operations (file keys, content types, encodings, SHA-256 of each file) - The proposer creates the
UpgradeAssetCanisterproposal while the lease is live; the proposal stores:batch_id,evidence,git_repo_url,git_commit_hash - Voters verify the evidence by rebuilding locally with
--compute-evidence - If approved, governance calls
commit_proposed_batch()which re-validates the evidence (TOCTOU protection)
What You See in a Frontend Proposal
Section titled “What You See in a Frontend Proposal”When you view an AssetUpgrade proposal in the governance interface:
| Field | Example | What It Means |
|---|---|---|
| Evidence | e3b0c44298fc1c... | Deterministic hash over all batch operations (you CAN verify this!) |
| Batch ID | 42 | Identifies the locked batch on the asset canister |
| Git Repo URL | <public repository URL, published with the upgrade proposal> | Source repository |
| Git Commit | f1e2d3c4b5a6... | Exact commit the frontend was built from |
| Build Instructions | npm ci && npm run build | How to reproduce the build |
Step-by-Step Frontend Verification
Section titled “Step-by-Step Frontend Verification”Step 1: Clone and Checkout
Section titled “Step 1: Clone and Checkout”git clone <public repository URL, published with the upgrade proposal> ohshii-launchercd ohshii-launchergit checkout <git_commit_hash_from_proposal>Step 2: Install Dependencies
Section titled “Step 2: Install Dependencies”npm ci
npm ciuses the exact versions frompackage-lock.json, which maximizes reproducibility.
Step 3: Compute Evidence Locally
Section titled “Step 3: Compute Evidence Locally”dfx deploy ohshii_launcher_frontend --network ic --compute-evidenceWhat this command does:
- Builds the frontend locally (dfx automatically runs
npm run buildfor asset canisters — you do NOT need to run it manually) - Connects to the IC mainnet read-only to fetch the current state of the asset canister (it does NOT deploy or upload anything)
- Determines what batch operations would be needed to go from the current state to the new version
- Computes a deterministic hash over those operations (the evidence)
- Displays the evidence hash and exits
Why --network ic? The evidence depends on the DIFF between the current canister state and the proposed new state. dfx must read the current state from IC mainnet. Without --network ic, dfx would try a local network where the canister doesn’t exist (causing the “no Wasm module” error). This is a read-only operation — nothing is deployed or modified on mainnet.
Example output:
Computed evidence: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855Step 4: Compare Evidence with the Proposal
Section titled “Step 4: Compare Evidence with the Proposal”| Your Computed Evidence | Proposal’s Evidence | Result |
|---|---|---|
e3b0c44298fc1c... | e3b0c44298fc1c... | MATCH - The proposed assets match the source code |
e3b0c44298fc1c... | 7a8b9c0d1e2f... | MISMATCH - Do NOT vote! |
Understanding the Evidence Hash
Section titled “Understanding the Evidence Hash”The evidence hash is NOT a simple file hash. It is a deterministic hash computed by the
asset canister’s compute_evidence() function over the entire set of batch operations:
Evidence = Hash( CreateAsset { key: "/index.html", content_type: "text/html", ... }, SetAssetContent { key: "/index.html", sha256: <file_hash>, chunks: [...] }, SetAssetContent { key: "/assets/index-abc123.js", sha256: <file_hash>, chunks: [...] }, DeleteAsset { key: "/old-file.css" }, ... all operations needed to go from current state → new state)This means:
- Same source code + same current canister state = same evidence (deterministic)
- The evidence covers ALL files, not just one — it is a comprehensive proof
- It also includes deletions of old files and metadata changes
- The proposer cannot influence this computation — it is calculated by the IC
Why --network ic is Required (and Safe)
Section titled “Why --network ic is Required (and Safe)”The --compute-evidence flag connects to the IC mainnet because the evidence depends on the
difference between the current canister state and the proposed state. Two identical builds
would produce different evidence if the canister’s current state is different.
This is a read-only operation. Despite the dfx deploy command name, the --compute-evidence
flag prevents any deployment. It only:
- Reads the current canister state from IC (query call)
- Compares it with your locally built assets
- Computes and displays the evidence hash
Without --network ic, dfx defaults to a local network where the canister doesn’t exist,
causing the error: "canister contains no Wasm module".
You need:
- dfx installed (install guide)
- Internet access to the IC mainnet (read-only)
- Node.js + npm installed (dfx runs
npm run buildautomatically)
Alternative: Content Hash Verification (If You Don’t Have dfx)
Section titled “Alternative: Content Hash Verification (If You Don’t Have dfx)”If you don’t have dfx installed, you can do a simpler verification by building manually and checking individual file hashes:
# 1. Install dependencies and build the frontend manuallynpm cinpm run build
# 2. Hash the specific file mentioned in the proposal's Build Instructionsshasum -a 256 dist/index.htmlCompare this with the Content Hash if shown in the proposal. This verifies individual files but does NOT cover the full batch (deletions, metadata changes, etc.).
Reproducibility
Section titled “Reproducibility”The build is designed to be reproducible — the same commit should produce the same evidence hash:
- Timestamps are derived from the git commit (not
Date.now()), so they are identical for the same commit - Dependencies are locked via
package-lock.jsonand installed withnpm ci - Vite uses
strictRequires: truefor deterministic CommonJS handling
| Factor | Backend (Docker) | Frontend (dfx) |
|---|---|---|
| Build reproducibility | Identical (Docker isolates everything) | Should match if same Node.js major version |
| Verification tool | verify-proposal.sh | dfx deploy --compute-evidence |
Tips if evidence doesn’t match:
- Use
npm cibefore running dfx (notnpm install) - dfx runs
npm run buildautomatically — do NOT run it separately - Use the same Node.js major version as the proposer (check
node --version) - Run
git statusto ensure no local modifications - Run
git log -1 --format=%Hto verify you’re on the exact commit
If evidence doesn’t match:
- Check you’re on the correct git commit
- Check your Node.js version matches the proposer’s
- Review the source code manually — if the code looks legitimate, the mismatch may be due to build tool differences, not malicious intent
- Ask the proposer to specify their Node.js version in the Build Instructions
Common Frontend Verification Issues
Section titled “Common Frontend Verification Issues””Command not found: dfx”
Section titled “”Command not found: dfx””Install dfx: sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)"
”My evidence doesn’t match!”
Section titled “”My evidence doesn’t match!””Possible causes:
- Wrong commit: Verify you checked out the exact commit from the proposal
- Different Node.js version: Use
node --versionand compare with the proposer’s - Modified files: Run
git statusto ensure no local changes - Stale npm cache: Try
rm -rf node_modules && npm ci - Different canister state: If the canister was updated between proposal creation and your verification, evidence will differ (this is rare during voting period)
“The build instructions are suspicious”
Section titled ““The build instructions are suspicious””DO NOT vote if the build instructions contain:
curlorwgetdownloading external files- Commands that modify system files
- Obfuscated or encoded commands
- Anything you don’t understand
Report suspicious proposals to the community.
”I don’t have dfx and can’t install it”
Section titled “”I don’t have dfx and can’t install it””Use the simpler Content Hash method described above, and also:
- Review the source code at the git commit for suspicious changes
- Verify the build instructions are standard (
npm ci && npm run build) - Check that the proposer is a known, trusted community member
Manual Verification (Without Helper Script)
Section titled “Manual Verification (Without Helper Script)”If you prefer to do each step manually:
# 1. Build with Docker./Deploy/docker-build.sh
# 2. The .wasm.gz files are already created in Deploy/wasm/ls -la Deploy/wasm/*.wasm.gz
# 3. Calculate hash manuallyshasum -a 256 Deploy/wasm/ohshii_launcher_backend_docker.wasm.gz
# 4. Never re-create the .wasm.gz on your machine: another gzip program gives# other bytes. To compare modules, compare the raw module instead:gunzip -c Deploy/wasm/ohshii_launcher_backend_docker.wasm.gz | shasum -a 256shasum -a 256 Deploy/wasm/ohshii_launcher_backend_docker.wasmTroubleshooting
Section titled “Troubleshooting”Docker Not Running
Section titled “Docker Not Running”Error: Docker is not running. Please start Docker Desktop.Start Docker Desktop and try again.
Hash Mismatch
Section titled “Hash Mismatch”Possible causes:
- Wrong commit: Double-check you checked out the exact commit from the proposal
- Modified local files: Ensure
git statusshows no uncommitted changes - A
.wasm.gzmade outside the container: a module compressed by another gzip program has other bytes; compare the raw module (gunzip -c … | shasum -a 256) - Malicious proposal: The proposer may have uploaded different code than claimed
WASM File Not Found
Section titled “WASM File Not Found”Error: WASM file not foundRun ./Deploy/docker-build.sh first to generate the WASM files.
Slow Build
Section titled “Slow Build”The Docker build takes about 7 minutes on an arm64 Mac building linux/amd64 under emulation, plus a one-time download of the pinned Rust image. Later builds of an unchanged commit reuse the Docker cache.
Q: Can I verify backend proposals without Docker?
No. Docker ensures the build environment is identical across all machines. Without it, Rust compiler differences could produce different binaries.
Q: Can I verify the Evidence Hash for frontend proposals?
Yes! Run dfx deploy ohshii_launcher_frontend --network ic --compute-evidence. This builds the frontend locally and computes the evidence hash without uploading anything. Compare it with the proposal’s Evidence field. This is the official DFINITY-recommended approach for asset canister verification (same process used by all SNS DAOs on the IC).
Q: Why does --compute-evidence need network access?
The evidence hash depends on the difference between the current canister state and the proposed new state. dfx must read the current asset canister state from the IC to compute what batch operations would be needed, then hash those operations.
Q: What if I disagree with the code changes?
That’s your prerogative as a voter! Verification confirms the WASM matches the code. Whether the code is good is a separate judgment call.
Q: Can the proposer fake the git commit hash?
Technically, they could reference a commit in a forked repository. Always verify the URL is the official public repository (<public repository URL, published with the upgrade proposal>). Always verify the hash of your own local build matches the hash shown in the proposal before voting.
Q: Why do proposals use .wasm.gz instead of raw .wasm?
Smaller file size reduces storage costs and upload time. The IC decompresses it automatically during installation.
Q: What’s TOCTOU protection?
Time-of-Check-Time-of-Use. It prevents tampering between when voters verify and when the code is executed. The backend recalculates hashes at execution time.
Q: How do I know the Dockerfile isn’t malicious?
Read it! It’s a few dozen lines. Look for standard Rust build commands. Report anything suspicious.
Getting Help
Section titled “Getting Help”- GitHub Issues: the issue tracker of the public repository (<public repository URL, published with the upgrade proposal>)
- Community: the official Telegram channel (https://telegram.me/ohshiilabs) and X account (https://x.com/ohshiidao)
- Documentation: see Governance Canister Build for how the per-campaign governance module is built and embedded
Summary Checklist
Section titled “Summary Checklist”Backend Proposals (UpgradeCanister)
Section titled “Backend Proposals (UpgradeCanister)”- Clone the correct repository (
<public repository URL, published with the upgrade proposal>) - Checkout the exact commit hash from the proposal
- Read the Dockerfile before running it
- Run
./Deploy/verify-proposal.sh <canister_name>after./Deploy/docker-build.sh, or take the minimal path - Compare the raw module hash, then your
.wasm.gzhash with the proposal’s WASM Hash - Read the source diff against the deployed commit (
get_deployed_source) - MATCH and a reviewed diff = ready to vote on the merits | MISMATCH = Do not vote
Frontend Proposals (AssetUpgrade)
Section titled “Frontend Proposals (AssetUpgrade)”- Clone the correct repository (
<public repository URL, published with the upgrade proposal>) - Checkout the exact commit hash from the proposal
- Run
npm cito install dependencies - Run
dfx deploy ohshii_launcher_frontend --network ic --compute-evidence - Compare your computed evidence with the proposal’s Evidence field
- MATCH = the staged assets are the commit’s build; review the diff before voting | MISMATCH = Investigate (may be Node.js version difference)
Remember: Trust but verify. Your vote protects the entire community.