Skip to content

feat!: length-prefix every variable-length field in asset canister evidence - #4545

Merged
lwshang merged 12 commits into
masterfrom
fix/asset-evidence-framing
Sep 23, 2026
Merged

lwshang merged 12 commits into
masterfrom
fix/asset-evidence-framing

Conversation

@lwshang

@lwshang lwshang commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Description

The encoding hashed for the evidence of a proposed batch, and for the state hash, now
length-prefixes every variable-length field — asset keys, content types, content encodings,
header names and values, the declared sha256, and asset content — and hashes the number of
entries in a header map. This is the framing that crates/state-hash uses in
dfinity/certified-assets. Each operation in the
encoding is self-delimiting, which makes the digest an injective function of the batch it is
computed over.

The encoding is now specified under compute_evidence in
docs/design/asset-canister-interface.md, so that
tooling which verifies a proposal can reimplement it, and ic-asset and ic-certified-assets
each pin the same test vector for a fixed batch — the two implementations have to agree for
dfx deploy --compute-evidence to be comparable with what the asset canister computes.

The evidence of a SetAssetContent operation now covers last_chunk whether or not chunk_ids
is empty, matching the content that commit_batch stores for the same operation.

The asset canister reports api_version 3. dfx deploy --by-proposal and
dfx deploy --compute-evidence report an error against an asset canister that reports a lower
version, instead of computing a value that cannot be compared with it.

Upgrading

Evidence and state hash values that this change computes differ from the values earlier versions
compute over the same assets, so the asset canister and dfx have to be upgraded together:

  • recompute and re-verify the evidence of any batch proposed before the upgrade (as before, a
    proposed batch does not survive a canister upgrade);
  • expect a new state hash for unchanged assets.

Crate version bumps (ic-certified-assets, ic-asset, icx-asset) are left to the release.

Unrelated CI fixes carried here

CI was red for two reasons that predate this branch, both external. They are fixed here so this
PR can go green; happy to split them into their own PR if preferred.

  • fix: pin vite-plugin-environment to 1.1.3 — 1.1.4, published 2026-09-09, raises its peer
    dependency from vite >= 2.7 to vite >= 8.0. The frontend templates ask for ^1.1.3 against
    vite ^4.3.9 (^5.4.3 for svelte), so npm install in a new project fails with ERESOLVE,
    node_modules stays empty, and the build then fails with tsc: command not found. This broke
    every job that deploys a frontend project: smoke, ui_test, dfx/new, dfx/info,
    dfx/telemetry, dfx/playground, dfx/frontend, dfx/deploy, and one dfx/assetscanister
    case.
  • ci: download bitcoin core from bitcoincore.org — bitcoin.org now 404s for the
    bitcoin-core binaries, failing the dfx/bitcoin jobs during provisioning. The existing sha256
    check on the tarball is unchanged.

Master's last e2e run was 2026-06-23, before either regression, which is why master appears green.

How Has This Been Tested?

  • cargo test -p ic-certified-assets -p ic-asset — 110 + 30 tests. New cases cover header-map
    partitioning, the key/content-type and key/content-encoding boundaries, a key or content region
    run on into the operations that follow it, chunk-boundary independence, and last_chunk
    alongside chunk_ids. Each new case was checked to fail against the previous encoding, rather
    than pass vacuously.
  • evidence_of_known_batch, pinned independently in both crates, checks that ic-asset and
    ic-certified-assets hash the same batch to the same value.
  • cargo clippy -p dfx -p ic-asset -p ic-certified-assets --all-targets, cargo fmt --check,
    shellcheck e2e/**/*.*sh — all clean.
  • e2e: the asset canister tests no longer pin project-dependent evidence values; they capture the
    value the canister computed and assert dfx deploy --compute-evidence produces the same one.
    Not run locally — relying on CI.
  • src/distributed/assetstorage.wasm.gz was rebuilt with
    ./scripts/update-frontend-canister.sh --release-build. Relying on the
    frontend-canister-up-to-date job to confirm the reproducible build.

Checklist:

  • The title of this PR complies with Conventional Commits.
  • I have edited the CHANGELOG accordingly.
  • I have made corresponding changes to the documentation.

🤖 Generated with Claude Code

lwshang and others added 2 commits September 18, 2026 14:39
…idence

The encoding hashed for the evidence of a proposed batch, and for the state
hash, now length-prefixes every variable-length field -- asset keys, content
types, content encodings, header names and values, the declared sha256, and
asset content -- and hashes the number of entries in a header map. This is the
framing that crates/state-hash uses in dfinity/certified-assets. Every
operation in the encoding is self-delimiting, which makes the digest an
injective function of the batch it is computed over.

The encoding is now specified under `compute_evidence` in
docs/design/asset-canister-interface.md, so that tooling which verifies a
proposal can reimplement it, and `ic-asset` and `ic-certified-assets` each pin
the same test vector for a fixed batch: the two implementations have to agree
for the evidence of a proposed batch to be comparable with what dfx computes
from source.

The evidence of a SetAssetContent operation now covers `last_chunk` whether or
not `chunk_ids` is empty, matching the content that `commit_batch` stores for
the same operation.

The asset canister reports api_version 3. `dfx deploy --by-proposal` and
`dfx deploy --compute-evidence` now report an error against an asset canister
that reports a lower version, instead of computing a value that cannot be
compared with it.

Evidence and state hash values differ from the ones earlier versions compute
over the same assets, so the asset canister and dfx have to be upgraded
together. The e2e assertions no longer pin project-dependent evidence values;
they compare the value dfx computes with the value the canister computed, which
is the property that matters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rebuilt with ./scripts/update-frontend-canister.sh --release-build, so that the
distributed asset canister carries the framed evidence and state hash encoding.
assetstorage.did is unchanged: api_version already has this signature.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The evidence/state-hash encoding change and API-version gate are compatibility- and integrity-critical and should receive final human verification across upgrade and cross-implementation comparability paths.

Pull request overview

This PR updates the asset canister “evidence” and state-hash encoding to be self-delimiting (length-prefixing every variable-length field, plus hashing header-map entry counts) so the digest becomes an injective function of the encoded batch/state, and aligns the framing with crates/state-hash in dfinity/certified-assets. It also bumps the asset canister api_version to 3 and teaches ic-asset to refuse proposal/evidence workflows against older canisters where computed values would not be comparable.

Changes:

  • Switch evidence/state-hash hashing to a length-prefixed, tagged, domain-separated encoding (and update tests + pinned vectors in both implementations).
  • Ensure SetAssetContent evidence covers last_chunk even when chunk_ids is non-empty (matching commit_batch’s assembled content).
  • Gate dfx/ic-asset proposal + evidence computation on asset canister API version ≥ 3; update docs, changelog, and e2e expectations accordingly.
File summaries
File Description
src/canisters/frontend/ic-certified-assets/src/tests.rs Adds targeted tests for boundary/partition injectivity, chunk-boundary independence, last_chunk coverage, and pins a cross-crate test vector.
src/canisters/frontend/ic-certified-assets/src/lib.rs Bumps asset canister api_version() from 2 to 3.
src/canisters/frontend/ic-certified-assets/src/evidence.rs Implements domain-separated, length-prefixed encoding; hashes header-map lengths; length-prefixes content via total content length; includes last_chunk alongside chunk_ids.
src/canisters/frontend/ic-asset/src/sync.rs Rejects --by-proposal preparation when canister API version is too low for comparable evidence.
src/canisters/frontend/ic-asset/src/evidence/mod.rs Implements matching encoding + domain separator, pins a known-batch vector, and rejects evidence computation against canisters with API version < 3.
src/canisters/frontend/ic-asset/src/error/prepare_sync_for_proposal.rs Adds a clear error for “evidence API version too low” in proposal preparation.
src/canisters/frontend/ic-asset/src/error/compute_evidence.rs Adds API-version query failure + “evidence API version too low” errors for compute-evidence.
e2e/tests-dfx/assetscanister.bash Makes e2e evidence assertions robust by extracting evidence from output and comparing computed vs canister-provided evidence dynamically.
docs/design/asset-canister-interface.md Specifies the hashed encoding under compute_evidence and documents API version 3 semantics/compatibility constraints.
CHANGELOG.md Documents the breaking evidence/state-hash change, upgrade requirements, and related behavior changes.
Review details
  • Files reviewed: 10/11 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

lwshang and others added 3 commits September 18, 2026 16:14
vite-plugin-environment 1.1.4, published 2026-09-09, raises its peer
dependency from `vite >= 2.7` to `vite >= 8.0`. The templates ask for
`^1.1.3` and pin vite at ^4.3.9 (^5.4.3 for svelte), so npm resolves the
plugin to 1.1.4 and `npm install` fails with ERESOLVE in a newly created
project. That leaves node_modules empty, and the build step then fails with
`tsc: command not found`.

Ask for exactly 1.1.3 in all four frontend templates. Verified that each of
the four resolves with the pin and that vanilla_js fails without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bitcoin.org no longer serves the bitcoin-core binaries and returns 404, which
fails the bitcoin e2e job before any test runs. bitcoincore.org serves the
same release; the existing sha256 check on the tarball is unchanged and still
verifies what is downloaded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ic-mops 3.0.0, published 2026-08-20, removed the `toolchain init` and
`toolchain reset` subcommands. setup_playground calls `mops toolchain init` to
install the moc version the playground backend builds with, so an unpinned
`npm i -g ic-mops` now installs 3.x and every playground test fails in setup
with "unknown command 'init'".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lwshang
lwshang marked this pull request as ready for review September 21, 2026 08:42
@lwshang
lwshang requested a review from a team as a code owner September 21, 2026 08:42
@zeropath-ai

zeropath-ai Bot commented Sep 21, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to 6cfb276.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► docs/design/asset-canister-interface.md
    Introduce and describe the hashed encoding domain, encoding versioning, and API version implications for evidence and state hash; add API Version 3 section and related details
Enhancement ► e2e/tests-dfx/assetscanister.bash
    Add functions to extract and render evidence from deployment outputs; update tests to use dynamically captured evidence and verify against computed evidence
Enhancement ► src/canisters/frontend/ic-asset/src/error/compute_evidence.rs
    Add ApiVersionQueryFailed and EvidenceApiVersionTooLow error variants; improve error reporting for evidence API version issues
Enhancement ► src/canisters/frontend/ic-asset/src/error/prepare_sync_for_proposal.rs
    Add EvidenceApiVersionTooLow error variant to handle API version checks during proposal sync preparation
Enhancement ► src/canisters/frontend/ic-asset/src/evidence/mod.rs
    Introduce ENCODING_DOMAIN constant and EVIDENCE_API_VERSION; update compute_evidence to require minimum API version and include domain in hashing; update compute_state_hash to hash domain; other related hashing helpers adjusted
Enhancement ► src/canisters/frontend/ic-certified-assets/src/evidence.rs
    Introduce ENCODING_DOMAIN and descriptive comments for evidence/state hash domain/version; add consistency notes about v2 encoding; align with asset canister encoding expectations
Enhancement ► src/canisters/frontend/ic-certified-assets/src/lib.rs
    Bump api_version() return value from 2 to 3 to reflect API version upgrade
Enhancement ► src/canisters/frontend/ic-certified-assets/src/tests.rs
    Update tests to accommodate API version changes and dynamic evidence handling; introduce mechanisms to verify evidence from known batches and dynamic deployment outputs; expand tests around evidence comparisons and boundary cases
Enhancement ► src/canisters/frontend/ic-asset/src/sync.rs
    Add API version check against EVIDENCE_API_VERSION in prepare_sync_for_proposal
Enhancement ► src/canisters/frontend/ic-asset/src/evidence/mod.rs (additional context from surrounding changes)

@marc0olo marc0olo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The encoding change looks right and the cross-crate vector is the right way to
pin it. One thing needs fixing before merge.

The spec says each operation is a tag "followed by its fields, in the order they
are listed in the operation". The documented CreateAssetArguments lists
enable_aliasing before allow_raw_access, but hash_create_asset hashes
allow_raw_access first. I reimplemented the encoding from the document alone
and checked it against the vector this PR pins:

pinned in both crates : 984350ce...3c778ca6
spec as written       : 1f872096...b736f48d   mismatch
code order            : 984350ce...3c778ca6   match

The pinned batch has enable_aliasing: Some(true) and
allow_raw_access: Some(false), so the two orders diverge on this very vector.
Since verifying tooling reimplementing from the doc is the reason it exists, I'd
swap the two hash_opt_bool calls in both implementations rather than document
an exception — that keeps the field-order rule uniform, and the encoding is
already breaking here so it costs nothing.

Everything else in the spec reproduced first try: tags, big-endian nat64 length
prefixes, the header-map entry count, opt/bool tagging, content-length prefix,
and SetAssetProperties field order. Nice document.

Also verified: both implementations are structurally identical across all nine
operations; the domain separator is applied exactly once on each of the three
entry paths, including a state-hash restart; the empty-batch e2e constant is
sha256("ic-certified-assets v2"); the last_chunk gap is real and correctly
closed; the version gate is on both entry points, and an older dfx against a new
canister fails closed at commit time. 110 + 30 tests pass locally.

Two nits:

  • ENCODING_DOMAIN says v2 while api_version is 3. Two numbers that have to
    move together, currently off by one — worth a comment tying them.
  • The spec calls the encoding "an injective function of the proposed
    commit_batch arguments" and then says chunking doesn't affect the digest.
    Both can't hold; it's injective over the content, not the arguments. Worth
    stating precisely in a document meant for reimplementers.

+1 on splitting the three CI commits out — the encoding change deserves its own
bisect point.

lwshang and others added 7 commits September 22, 2026 14:58
The three external e2e regressions are split out into their own PR so each
gets its own bisect point, leaving this branch to the evidence encoding alone.
Reverted rather than rewritten, so the branch history reviewers have already
seen stays intact.

This reverts commit 8ceb0c7 (ci: pin ic-mops to 2.24.0 in the playground e2e
test), ad05f0a (ci: download bitcoin core from bitcoincore.org) and 0d181ea
(fix: pin vite-plugin-environment to 1.1.3 in frontend templates).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
assets.did, CreateAssetArguments and the interface document all list
enable_aliasing before allow_raw_access; hash_create_asset hashed them the
other way round, so a reimplementation that followed the document produced a
different digest for the same batch. Every other operation already hashes its
fields in declaration order, so this was the one exception to the rule the
document states, on top of the content handling it spells out for
SetAssetContent.

Swap the two calls in both implementations and move the pinned cross-crate
vector with them. Reimplementing the encoding from the document alone now
reproduces that vector.

Also, from review:

- State precisely what the encoding is injective over -- the change a batch
  applies, with each SetAssetContent's content taken as one byte string -- and
  say explicitly that it is not injective over the batch arguments, since two
  batches that differ only in how they split content across chunks encode
  identically. The document claimed injectivity over the arguments and then
  noted that chunking does not affect the digest, which cannot both hold.
- Note that the `v2` in the domain separator versions the encoding and is not
  the API version, which is at 3, and that the two move independently.
- Record that the domain separator is hashed as its 22 bytes with no length
  prefix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Picks up the CreateAsset field-order fix. Rebuilt with
./scripts/update-frontend-canister.sh --release-build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pinned vector stopped at CreateAsset, SetAssetContent and DeleteAsset. It
now covers UnsetAssetContent, SetAssetProperties and Clear as well.

SetAssetProperties is the one that most needed it: it is the only operation
whose arguments differ in type between the two crates -- headers arrive in
ic-asset as a Vec<(String, String)> and in the canister as a BTreeMap, with a
hand-written conversion between them -- and assemble_batch_operations emits it
on the by-proposal path. The vector lists those headers out of order on
purpose, so the conversion has to sort them the way the canister does. Its four
opt opt fields also exercise all three shapes: set, explicitly cleared, absent.

Reimplementing the encoding from the interface document reproduces the new
vector, so all three implementations agree.

Also from review:

- Drop the claim that this is the framing crates/state-hash uses in
  dfinity/certified-assets. That crate versions with a u8 rather than a string
  domain, length-prefixes little-endian, sorts headers by (lowercase(name),
  value), folds content in as stored sha256 hashes rather than raw bytes, and
  models served state with no batch-operation concept. The two share the
  technique, not the framing, and the digests are not comparable.
- State the SetAssetContent exception where the field-order rule is given, not
  two paragraphs below it.
- Collapse the content-chunking branch in ic-asset: chunks() yields the whole
  slice when it is shorter than MAX_CHUNK_SIZE and sha256 is streaming, so both
  arms hashed the same bytes.

No shipped canister code changed, so assetstorage.wasm.gz is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment claimed the encoding had been versioned from 1 and the API from 0.
Nothing was ever versioned: no digest anywhere carries a `v1` separator, since
the encoding that preceded this one had no separator at all.

Keep `v2` -- the number counts encodings of this data, and there are two in the
wild: the unframed one that every asset canister installed before API version 3
still computes, and this one -- and say that, rather than inventing a history.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit's comment shifted line numbers in evidence.rs, and the
wasm embeds source locations through panic metadata, so the binary is not
byte-identical even though no code changed. Rebuilt with
./scripts/update-frontend-canister.sh --release-build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@marc0olo marc0olo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The encoding reproduces from the document alone: I reimplemented all six
operations from docs/design/asset-canister-interface.md in a separate language and got
the pinned 5e8a8c1c...c8b28089 first try. The extended vector now machine-checks the
SetAssetProperties conversion on both sides, which was the one thing previously only
traced by hand.

Two things for afterwards, neither blocking.

The version gate is a lower bound only. api_version >= 3 catches a canister too old
to produce a comparable digest, but not a future one on a later encoding — that would
still satisfy >= 3, dfx would report a v2 digest, and the mismatch would surface only
when commit_proposed_batch rejects it, after a reviewer has already compared two numbers
that were never comparable. Since the encoding version and api_version move
independently by design, the fix is for the canister to report the encoding version and
for ic-asset to require equality rather than a floor. Correct as shipped; it bites at the
next encoding change. Worth a follow-up issue.

ic-asset needs a major bump at release. ComputeEvidenceError and
PrepareSyncForProposalError both gain variants here and neither is #[non_exhaustive].
Not worth changing in this PR — none of the crate's 21 error enums is marked, so doing it
for these two alone would be arbitrary and doing it crate-wide is separate scope. Just
needs catching when versions are set.

@lwshang
lwshang merged commit 6de1c89 into master Sep 23, 2026
113 checks passed
@lwshang
lwshang deleted the fix/asset-evidence-framing branch September 23, 2026 12:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants