Skip to content

fix(ci): commit-independent toolchain hash; generate-once VK cache - #25111

Merged
fcarreiro merged 1 commit into
nextfrom
cl/fix-vk-cache-generation
Aug 5, 2026
Merged

fcarreiro merged 1 commit into
nextfrom
cl/fix-vk-cache-generation

Conversation

@charlielye

Copy link
Copy Markdown
Contributor

The incident

PR #24306 failed aztec-up/scripts/run_test.sh bridge_and_claim deterministically — the identical BBApiException: Failed to verify the generated proof! ~2.8s into the only ClientIVC prove of the test, across multiple "retries", while the same test was 60/60 green on next. The retries could never help: the failure was frozen inside cached build artifacts (aztec-up-test-image-… and yarn-project-….tar.gz), which are content-addressed by tree hash and therefore reused by every run of the same tree.

Tracing how a wrong artifact could sit under a correct content key led to two independent defects that compose:

Defect 1: the toolchain hash changes on every commit (regression, #25078 / #25057 / #25047)

labs-aztec-toolchain/bootstrap.sh hash computed the toolchain identity as hash_str $(git hash-object <built binaries>). But inject_version (barretenberg/cpp/bootstrap.sh) stamps git rev-parse --short HEAD into bb/bb-avm bytes on any non-release build — so the binary bytes encode the commit, and byte-identical source trees produce a different toolchain hash on every commit.

Observed directly: two CI runs of byte-identical trees (commits 5041b6bb / fafdd723) had toolchain hashes f2e3f37015b9cdfa vs d714a9fc28a62cff, zero overlap between their 84 contract-*.tar.gz cache names, and bb logged ~/.bb/5041b6b/vk_cache vs ~/.bb/fafdd72/vk_cache — the injected commit even keys the runtime VK cache directory.

Since every noir-contracts cache key mixes in AZTEC_TOOLCHAIN_HASH, the practical effect is: every commit of every PR recompiles all ~84 contracts and mass-regenerates their VKs in parallel into a brand-new, empty, shared VK cache. Before this scheme (pre-Aug-3), contract keys were built from source rebuild patterns over bb/noir/transpiler — identical trees reused artifacts.

It is also conceptually circular: the binaries themselves rebuild if and only if their source hash changes (their artifact names are the source hash — both runs above shared barretenberg-clang20-11cccdad4e78d0a2.zst), so deriving downstream rebuild keys from the binary bytes answers "did the toolchain change?" with information that is only available after deciding exactly that.

Fix: the toolchain hash now composes the providers' source content hashes — barretenberg/cpp/bootstrap.sh hash + noir/bootstrap.sh hash — plus the presence of the optional binaries (bb-avm/acvm, whose availability is part of the toolchain's identity but whose content is already covered by the provider hashes). Rebuild decisions at every level of the graph now key on the same inputs. A future labs-repo provisioning mode (build_labs, currently stubbed) should use the released toolchain's version string as its fixed identity.

Defect 2: the shared VK cache is not generate-once (latent since ~June)

get_or_generate_cached_app_vk (barretenberg/cpp/src/barretenberg/api/aztec_process.cpp) did exists() → read_file, and on miss wrote the VK directly to the final path — no locking, no temp-file+rename. The cache directory is shared by all concurrent bb processes (the noir-projects builds run one per contract under GNU parallel), and distinct contracts can carry byte-identical functions: the simulated account contracts stub out signature verification, so simulated_schnorr_account and simulated_ecdsa_account produce the same bytecode hash → the same cache path, wanted simultaneously by two unrelated processes.

The failing run's logs show the race firing three times (same VK hash "Generating…" concurrently from two contracts), plus other processes reading entries moments after a different process generated them. Two simultaneous writers on one path — or a reader consuming a half-written file — silently embeds a truncated VK into a contract artifact. Account-contract VKs are exactly what ClientIVC private-kernel recursion verifies, matching the observed deterministic proof-verification failure; the corrupted artifact then flowed into the yarn-project tarball and the aztec-up test image, both content-keyed, freezing the failure for every subsequent run of that tree.

Fix: each cache entry now takes an exclusive flock on a <entry>.lock sidecar for the duration of check-generate-write — one process generates while the rest block, then read the completed entry (generate-once, which is also the economical behavior: previously colliding processes each did the full ChonkComputeVk). Writes go via temp file + rename, so even a reader without the advisory lock (the Windows fallback, where flock is unavailable) can never observe a partial entry; a lost rename race there is benign and resolved in favor of the completed equivalent entry. The file is already excluded from WASM builds.

Why the fixes fix it

  • Defect 1's fix removes the trigger: identical trees reuse contract artifacts again, so the parallel mass-VK-regen (and its race window) happens only when bb/noir/transpiler genuinely change — and recovers the associated CI compute wasted on every commit since Aug 3.
  • Defect 2's fix removes the vulnerability: when a legitimate mass regeneration does happen (any PR that really changes bb), concurrent generation of shared-bytecode VKs is serialized and atomic, so a corrupt artifact can no longer be produced, and therefore can no longer be frozen into content-addressed caches.

Validation

  • bb builds clean with the C++ change; bash -n on the bootstrap change.
  • Both provider hash commands verified to produce stable content hashes; the new toolchain hash is commit-independent by construction (inputs are source content hashes + a presence string).
  • The poisoned artifacts from the incident (aztec-up-test-image-3747b48e27303118.zst) were separately rebuilt from a from-source local build and force-uploaded; this PR prevents recurrence.

The labs-aztec-toolchain hash hashed the bytes of the built binaries, but
inject_version stamps the current commit into bb/bb-avm on non-release builds,
so byte-identical trees produced a different toolchain hash on every commit.
Every noir-contracts cache key mixes that hash in, so every commit recompiled
all ~84 contracts and mass-regenerated their VKs in parallel into a fresh
shared vk_cache. The toolchain hash now composes the providers' source content
hashes (the same inputs that decide whether the binaries rebuild) plus the
presence of the optional binaries.

That mass regeneration exposed a latent race: get_or_generate_cached_app_vk
checked existence then wrote the final path directly, with no locking — and
distinct contracts can carry byte-identical functions (the simulated account
contracts), so concurrent bb processes double-generated the same entry and
could read a partially written VK. One corrupted account-contract VK baked
into the yarn-project artifact and the aztec-up test image caches, making
bridge_and_claim fail deterministically on every retry of PR #24306. VK cache
entries now take a per-entry flock (generate-once: one process generates,
the rest block then read) and are written via temp file + rename.
@nchamo
nchamo self-requested a review August 5, 2026 15:01

@nchamo nchamo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good, great catch!

@charlielye
charlielye added this pull request to the merge queue Aug 5, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 5, 2026
done
hash_str $(git hash-object "${files[@]}")
hash_str \
$("$ROOT"/barretenberg/cpp/bootstrap.sh hash) \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I will come back to this PR when I get back from OOO next week, but this part is wrong. It makes the labs repo depend on the foundation repo.

@fcarreiro
fcarreiro added this pull request to the merge queue Aug 5, 2026
Merged via the queue into next with commit df0891a Aug 5, 2026
22 checks passed
@fcarreiro
fcarreiro deleted the cl/fix-vk-cache-generation branch August 5, 2026 23:02
fcarreiro added a commit that referenced this pull request Aug 7, 2026
## Summary

- Merges `next` into `monorepo-split/labs`. Three conflicts, kept as
separate commits (needs
  `ci-no-squash`): the merge itself, then the labs-side resolutions.
- Keeps the `labs-aztec-toolchain` hash exactly as it is on this branch.
#25111 changed it to
compose `barretenberg/cpp/bootstrap.sh hash` and `noir/bootstrap.sh
hash`, which makes the labs
toolchain depend on foundation source trees that will not exist in the
labs repo - see

#25111 (comment).
Identity there
comes from the pinned binaries instead. The `hash` function is
byte-identical to this branch's.
- Drops `boxes` from the root bootstrap project list, deleted on `next`
by #25118, alongside the
  `aztec-up` omission this branch already carries.
- Bumps the pinned foundation release to `6.0.0-nightly.20260807`,
matching #25131. The merge brings
`next`'s migration to the published `@aztec/cdb`, which does not exist
at the nightly this branch
was pinned to, and `check_pin_drift` requires every pin to track
`BB_VERSION`, so all six sites
move together. The noir submodule and both pinned Noir crates
(`protocol_types`,
  `bb_proof_verification`) are byte-identical across the bump.

Overlaps #25131 deliberately: the merge independently brings most of it
(the generated cdb sources,
the `generate` script and its ignore/eslint entries are gone, and
`cdb_ipc_server.ts` matches), and
both branches now set the same pin value, so only the lockfiles
conflict. #25131 additionally removes
the now-inputless `ipc-codegen`/`cdb_schema.json` cache patterns in
`yarn-project/bootstrap.sh`,
which this PR leaves alone.

## Catch-up merge with the base branch

`monorepo-split/labs` moved on while this was open - #25133 dropped
`l1-contracts` and #25140
deleted `noir-projects/fnd` and `protocol/constants-codegen` - so the
base is merged back in as a
third commit. All four conflicts are lists that both sides shortened, so
each resolution is the
union of the two removals:

- `Makefile`: `.PHONY` loses both `boxes` (deleted on `next` by #25118)
and `l1-contracts`. The
`barretenberg` aggregate keeps this branch's new `bb-cdb` and drops
`bb-sol`. The extra
  `yarn-project` prerequisites become `bb-ts wsdb bb-avm-sim bb-cdb`.
- `scripts/socket-fix.sh`: the `WORKSPACES` default and its doc comment
become
  `"yarn-project docs playground"`.
- `boxes/bootstrap.sh`, `boxes/package.json`: modify/delete. The
deletion wins - the base had
  edited both, but the whole directory is gone on `next`.

Neither the pin bump nor the lockfiles conflicted here: the base never
touched
`yarn-project/package.json` or either lockfile, so
`6.0.0-nightly.20260807` carries over intact and
`check_pin_drift` passes. `make -n yarn-project` and `make -n
barretenberg` both resolve with
`bb-cdb` present and no dangling `bb-sol`, `l1-contracts`,
`constants-codegen`, or
`noir-projects-fnd` references.

A fourth commit drops two comments that the deletions left describing
things that no longer exist:
`ci3/dist_tag` credited a git-branch-push consumer to `l1-contracts`
(nothing pushes branches off
`dist_tag` anymore), and `yarn-project/bootstrap.sh` claimed
`warm_solc_cache` is normally a no-op
because `l1-contracts-solc` has already populated `~/.svm`. No behavior
change.
fcarreiro added a commit that referenced this pull request Aug 10, 2026
…25151)

Unifies `labs-aztec-toolchain/bootstrap.sh` so one file serves the three
contexts that need it: the monorepo today, the standalone labs repo
after the split, and the foundation repo after the split (which deletes
the labs components, consumes them as a submodule, and runs labs e2e
against its own locally built bb/noir — the pre-split flow). Today the
next and monorepo-split/labs lines carry divergent copies of this file
that conflict on every sync merge; this converges them so the only
intended difference is one committed default.

## The mode protocol

A single variable selects the provisioning mode:

- **Foundation mode** (`FND_ROOT` non-empty): symlink the binaries built
inside the checkout at `FND_ROOT` (`barretenberg/cpp`, the `noir`
submodule), and derive the toolchain identity from that tree's source
hashes. This is `build_monorepo` + the #25111 hash, with the root
parameterized.
- **Pinned mode** (`FND_ROOT` empty): download released binaries at the
pinned `BB_VERSION`/`NOIR_VERSION` (the monorepo-split/labs flow:
bbup/noirup, cached acvm source build, `.pin` provenance record,
`check_pin_drift`), and derive the identity from this directory's
committed content.

The committed default on this line is `FND_ROOT=$(git rev-parse
--show-toplevel)` — foundation mode, today's behavior — so a bare
invocation keeps linking the local build. `AZTEC_TOOLCHAIN_FND_ROOT`
overrides either way: export it empty to force pinned mode, or point it
at a foundation checkout root (how the post-split foundation repo will
drive its labs submodule). The labs line will carry the same file with
an empty default (follow-up PR against monorepo-split/labs).

## Hash semantics

- **Foundation mode**: byte-identical inputs to the current
(post-#25111) hash — providers' source hashes plus observed optional
binaries. Verified the value is unchanged on this tree
(`ce12057b4ea229fb` before and after), so **no cache invalidation on
this line**.
- **Pinned mode**: `cache_content_hash "^labs-aztec-toolchain/"` plus
the declaratively expected optionals (bb-avm iff released for this
platform, acvm iff cargo exists). This replaces the labs line's current
byte-hashing of `bin/`, completing the identity/verification separation
#25111 started: the hash is a pure function of the committed tree
(computable on a fresh checkout — today `hash` fails until `build` has
run, which downstream hash compositions trip over), a corrupted `bin/`
can no longer mint a fresh valid-looking cache key (byte verification
stays in the `.pin` record at provision time), a pin bump still moves
the hash before any binary is refreshed, and a dirty toolchain dir
propagates `disabled-cache` instead of laundering it into a
stable-looking key.

## Other behavior notes

- Foundation-mode `build` now starts from an empty `bin/` and writes the
`.pin` record (previously labs-line-only), so switching between modes in
one checkout fully re-provisions, and `noir_version` can report the
exact submodule tag (e.g. `nightly-2026-07-31`) instead of nargo's base
cargo version; it falls back to the binary when no record exists.
- Pinned mode on this line is exercisable via the override but
`check_pin_drift` will legitimately fail while next's
Nargo.toml/docs/yarn-project pins differ from the labs line's — expected
until the split content converges.
- Call-site interface (`hash`, `build`, `noir_version`, `bin/*` paths)
is unchanged.

## Validation

- `bash -n` clean; fnd-mode `hash` byte-identical to the old script on
the same tree (`ce12057b4ea229fb`).
- Pinned-mode `hash` identical with `bin/` present and absent
(`bd71460d78038ae5`), returns `disabled-cache` on a dirty toolchain dir
locally, and fails (exit 1) under `CI=1` instead of hashing an empty
string.
- Foundation `build` run against this checkout's local bb/noir builds:
symlinks all five binaries, writes the pin record, hash stable across
re-provisioning.
- Invalid `AZTEC_TOOLCHAIN_FND_ROOT` fails with a clear message.

## Follow-up

- Apply the same file to `monorepo-split/labs` with an empty committed
`FND_ROOT` default (its toolchain hash value changes once:
contract/yarn-project caches on that line rebuild one time).
- After the split, the foundation repo's labs-e2e driver exports
`AZTEC_TOOLCHAIN_FND_ROOT=$(git rev-parse --show-toplevel)` before
entering the submodule; the committed default divergence disappears.
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