Skip to content

ci(l1,l2): key the Rust cache by runner image so pools cannot span glibc versions - #7154

Merged
MegaRedHand merged 2 commits into
mainfrom
fix/ci-rust-cache-runner-image
Aug 19, 2026
Merged

MegaRedHand merged 2 commits into
mainfrom
fix/ci-rust-cache-runner-image

Conversation

@ilitteri

Copy link
Copy Markdown
Collaborator

Motivation

Ethrex Release has been red on main since 2026-08-12, failing in build-ethrex (ubuntu-22.04, l2) while parity-scale-codec reports eighteen unresolved imports of its own re-exports. The dependency is not at fault — the first error in the log is:

error: …/libparity_scale_codec_derive-….so: /lib/x86_64-linux-gnu/libc.so.6:
       version `GLIBC_2.39' not found

The proc-macro .so came out of the Actions cache, built on a runner with a newer glibc than the one restoring it. rustc cannot dlopen it, so extern crate parity_scale_codec_derive fails and every crate::Decode / Encode / Error re-export collapses behind it. The wall of E0432s names the dependency rather than the cache, which is what made this look like a dependency-resolution problem.

This is not caused by any recent PR. The commit immediately before the most recent merge, 06773cee4, fails identically, and tag_release.yaml only triggers on push-to-main and tags — never pull_request — so no PR can catch it.

Description

Swatinem/rust-cache composes its key from runner.os and runner.arch, which are Linux and x64 for both ubuntu-22.04 and ubuntu-24.04. The observed restore was:

Restored from cache key "v0-rust-l2-Linux-x64-73d6c353-e089dd43" full match: false.

Nothing in that key distinguishes the image, so a pool shared by jobs on different images resolves to one entry:

pool writer (save-cache) image readers on another image
l2 pr-main_l2.yaml:integration-test ubuntu-latest → 24.04, glibc 2.39 tag_release.yaml:build-ethrex on 22.04, glibc 2.35
l1 pr-main_l1.yaml:test 22.04, glibc 2.35 lint, daily_hive_report, daily_loc_report, deep_reorg, pr_lint_readme, pr_loc, pr_perf_levm on ubuntu-latest

l2 is the one failing: newer glibc into older runner. l1 has the same flaw pointing the other way, which is forward-compatible and so has not broken — but it would the moment that writer matrix moves to 24.04.

The fix folds ImageOS into the cache key. Image family (ubuntu22 / ubuntu24 / macos15) rather than ImageVersion, which changes with every runner image release and would evict the pools weekly; family is exactly the granularity that separates glibc floors. It is read via $GITHUB_OUTPUT rather than env.ImageOS directly, because the env context is not reliably available to with: on a uses: step inside a composite action.

Fixing it in the shared action rather than at the call site means no pool can ever span images, including pools added later.

Note on rollout

Changing the key invalidates every existing entry once. Pools refill on the next main build; the first run after merge is a cold build.

How to test

The failure only reproduces on main (the workflow has no pull_request trigger), so the real check is that build-ethrex (ubuntu-22.04, l2) goes green on the next push to main. Before then, the key change is visible in any job's log — the restore line should carry the image family:

v0-rust-l2-ubuntu22-Linux-x64-…

Checklist

  • Updated STORE_SCHEMA_VERSION (crates/storage/lib.rs) if the PR includes breaking changes to the Store requiring a re-sync.

…versions

`Ethrex Release` has been failing on main since 2026-08-12 in
`build-ethrex (ubuntu-22.04, l2)`, with `parity-scale-codec` dying on eighteen
unresolved imports of its own re-exports. The dependency is not at fault; the
first error in the log is:

    libparity_scale_codec_derive-*.so: /lib/x86_64-linux-gnu/libc.so.6:
    version `GLIBC_2.39' not found

rust-cache keys on `runner.os` and `runner.arch`, which are `Linux` and `x64`
for both ubuntu-22.04 and ubuntu-24.04, so the restored key was
`v0-rust-l2-Linux-x64-...` for both. The `l2` pool is written by
`pr-main_l2.yaml:integration-test` on `ubuntu-latest`, now ubuntu-24.04 with
glibc 2.39, and read by `build-ethrex` on ubuntu-22.04 with glibc 2.35. The
restored proc-macro `.so` cannot be dlopened there, so `extern crate
parity_scale_codec_derive` fails and every `crate::Decode`/`Encode`/`Error`
re-export collapses. The failure names the dependency rather than the cache,
which is what made it look like a dependency problem.

Fold `ImageOS` into the cache key. Image family rather than `ImageVersion`,
which changes with every runner image release and would evict the pools weekly;
family is exactly the granularity that separates glibc floors. It is read
through `$GITHUB_OUTPUT` because the `env` context is not reliably available to
`with:` on a `uses:` step inside a composite action.

This also closes the same hazard latent in the `l1` pool, whose writer
(`pr-main_l1.yaml:test`) is ubuntu-22.04 while several readers are
ubuntu-latest. That direction happens to be forward-compatible, so it has not
broken yet, but it would the moment the writer matrix moves to 24.04.

The key change invalidates every existing entry once; the pools refill on the
next main build.
@ilitteri
ilitteri requested a review from a team as a code owner August 14, 2026 20:05
@ilitteri ilitteri added L2 Rollup client L1 Ethereum client ci Github actions, build, tests, etc labels Aug 14, 2026
@github-actions

github-actions Bot commented Aug 14, 2026 •

Copy link
Copy Markdown

⚠️ Known Issues — intentionally skipped tests

Source: docs/known_issues.md

rpc-compat log-bearing cases excluded

Where: KNOWN_EXCLUDED_TESTS in .github/scripts/check-hive-results.sh counts out
eight hive rpc-compat cases — the four eth_getLogs cases, eth_getBlockReceipts/get-block-receipts-latest,
and three eth_getTransactionReceipt cases. They are exactly the cases whose recorded
response contains at least one log object; every case with an empty log array still runs.
Note this leaves eth_getLogs with no rpc-compat coverage at all, since all four of its
cases are in the set.

Why: ethrex populates blockTimestamp on log objects, as geth, besu, nethermind, reth
and erigon all do. hive's rpc-compat compares responses byte-exactly (jsondiff.FullMatch;
the lenient checkJSONStructure path applies only to cases upstream marks speconly), and
the corpus is pinned to execution-apis d08382ae (2025-02-10), whose recordings predate the
field — it entered the schema in execution-apis#639 and the fixtures in #846 (2026-07-22).
So the extra key cannot match, and this is a property of the pin rather than of the response.

The pin cannot move, and this is not temporary. The pin sits one commit before
execution-apis#627, which moved the test chain to a pre-merge genesis: the current corpus has
~36 proof-of-work blocks before its terminal total difficulty. ethrex does not support
pre-merge chains and will not, so importing that chain.rlp fails at block 1 —
validate_block_header has no pre-London base-fee path. Every revision carrying
blockTimestamp in its fixtures also carries that chain, so there is no revision that
satisfies both. Nor can the corpus be patched locally: rpc-compat's Dockerfile clones
ethereum/execution-apis by hard-coded URL, so the branch buildarg cannot point at a fork.

Coverage: the field itself is pinned by
block_timestamp_is_on_the_log_and_not_on_the_receipt in
crates/networking/rpc/types/receipt.rs, which asserts it is present on each log and absent
from the receipt level.

Removal: delete the entries if ethrex ever gains pre-merge chain import, or if upstream
marks these cases speconly so they are type-checked instead of compared byte-for-byte.


Stateless EF tests: 45 of 105 fixtures skipped

Where: tooling/ef_tests/blockchain — make test-stateless, which runs the
generated vectors_stateless_3278/ conformance set (execution-specs
3c3b6f4af, i.e. #3248 progressive SSZ + #3278 ChainConfig removal).
make test-levm is unaffected.

Skipped: 45 fixtures that fail in ordinary execution

Listed in EXTRA_SKIPS in tests/all.rs. All fail in add_block_pipeline with
GasUsedMismatch or ReceiptsRootMismatch, before any stateless validation
runs, so they are ordinary block-execution divergences rather than stateless
conformance gaps. 60 fixtures still execute, which is what keeps the witness and
SSZ paths covered and keeps the empty-suite guard in parse_and_execute
meaningful.

Why: the vectors and the client are one devnet apart on gas. 3c3b6f4af
carries the glamsterdam-devnet-7 schedule; this client implements devnet-8.

3c3b6f4af (devnet-7) this client (devnet-8)
COLD_STORAGE_ACCESS 3000 2100
ACCOUNT_WRITE 8000 9000
CALL_VALUE 10300 11300
access-list storage key full cold cost cold − WARM_ACCESS

Five of the 45 failed even while the client was still on devnet-7, so those are
skew against 3c3b6f4af itself; the other 40 appeared with the devnet-8 move.
The two groups are commented separately in EXTRA_SKIPS because they lift at
different times.

Corroborating that these are spec-base skew and not client bugs: ethrex passes
the five original cases against the tests-zkevm@v0.6.2 versions of the same
tests, and the eth-act witness dashboard has ethrex at 22829/22829 on
eels/consume-engine-witness and 19413/19413 on stateless-validator sp1 for
that bundle. Diffing witness_codes_reset_delegation across the two fixture
sets: pre and genesisBlockHeader are byte-identical and it is the same
transaction hash, yet postState, receipts, stateRoot, receiptTrie and
blockAccessListHash all differ — the receipt's cumulativeGasUsed goes
30816 → 24653. Same input, different output.

The regular vectors/ bundle has no eip8025_optional_proofs tests, so
make test-levm never sees any of this.

The pin cannot move today. eip8025_optional_proofs does not exist on the
current forks/amsterdam default branch, and 3c3b6f4af has diverged from it
(124 ahead, 315 behind), so it was never merged there. devnets/glamsterdam/8
is 315 commits behind 3c3b6f4af: it has the devnet-8 gas schedule but not the
#3278 schema, so vectors filled from it would carry the pre-#3278 69-byte
statelessOutputBytes this client cannot parse. No upstream revision has both.

Removal: bump SPEC_SHA in scripts/gen_stateless_vectors.sh once an
upstream revision carries both eip8025_optional_proofs and the devnet-8 gas
schedule, then delete the 40-entry group. Re-check the original five at the same
time; if they still fail against a matching base they are real execution bugs
and must not stay skipped.

Why this file previously claimed an Amsterdam-wide skip

parse_and_execute used to drop every network >= Fork::Amsterdam fixture from
the stateless run. Every test in the tests-zkevm bundle it read is
network: Amsterdam (23,946 of 23,946), so the stateless suite executed nothing
at all while reporting success. parse_and_execute now fails any stateless
fixture file that runs zero tests without a named skip, so a structural skip
cannot silently empty the suite again.


ZisK guest program hash changes with the unsync_cell gate

Where: crates/common/types/block.rs, transaction.rs.

The gate on the single-threaded unsync_cell::OnceCell moved from
all(feature = "eip-8025", target_arch = "riscv64") to
all(feature = "zisk", target_arch = "riscv64") when the eip-8025 feature was removed.

The guest ELFs were previously built --features "<zkvm>-build-elf,ci", which never enabled
eip-8025, so they compiled the atomic once_cell variant. bin/zisk/Cargo.toml does enable
ethrex-common/zisk, so the ZisK guest now compiles the unsafe impl Sync cell instead.
That changes the ELF bytes and therefore the program hash and verification key.

This is intended (the guest is single-threaded, so the unsync cell is sound and cheaper), but it
is a VK change rather than a no-op refactor, and the diffstat presents it as a file rename
(eip8025_cell.rs → unsync_cell.rs). Anyone pinning a ZisK VK across this change must
re-register it. The stateless-validator crate now forwards ethrex-common/zisk from its own
zisk feature so the two ZisK guests do not disagree on the cell type.


Release signing key is an unprotected repository secret

Where: .github/workflows/tag_release.yaml.

MINISIGN_SECRET_KEY is a plain repository secret. There is no environment: on
finalize-release or dry-run-release-assets, and gh api repos/lambdaclass/ethrex/rulesets
shows only branch-targeted rulesets, so the github.ref_type == 'tag' condition is a workflow
check rather than an enforced boundary: anyone who can push a tag can reach the signing key.

This is a repository-settings change, not a code change, so it is recorded here rather than
fixed in the tree. Recommended:

  1. Move MINISIGN_SECRET_KEY / MINISIGN_PASSWORD into a GitHub Environment with required
    reviewers, and add environment: to the two jobs that sign.
  2. Add a ruleset targeting refs/tags/v* restricting who may create release tags.

Until then, the compromise of that key is silent and durable: signatures would still verify
against the committed .github/minisign.pub.

@ethrex-project-sync ethrex-project-sync Bot moved this to In Review in ethrex_l1 Aug 14, 2026
@github-actions

Copy link
Copy Markdown

🤖 Kimi Code Review

This change correctly fixes a glibc version mismatch issue in CI caching. The logic is sound and the implementation follows GitHub Actions best practices.

Key points:

  1. Root cause analysis is accurate: The runner.os/runner.arch collision between Ubuntu 22.04 and 24.04 causes proc-macro .so files compiled against glibc 2.39 (24.04) to fail when restored on 22.04 runners with older glibc. This manifests as cryptic "unresolved import" errors during macro expansion.

  2. Cache key strategy is correct: Using ImageOS (ubuntu22/ubuntu24) as a discriminator separates the cache pools by glibc floor without over-invalidating via ImageVersion. The fallback to unknown for self-hosted runners is appropriate defensive programming.

  3. Implementation details:

    • Line 58-59: Correct use of $GITHUB_OUTPUT for composite action inter-step communication
    • Line 83: The key parameter properly appends to shared-key in Swatinem/rust-cache@v2, creating distinct cache namespaces per image family

Minor note: The extensive comment (lines 40-55) is justified given the subtlety of the failure mode—future maintainers need to understand why this discriminator exists to avoid regressions if they refactor the caching strategy.

No changes required; this is a clean fix.


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

No findings.

The change in action.yml looks correct: it adds the runner image family as an extra rust-cache key dimension, and that matches rust-cache’s documented shared-key plus additive key behavior, so it should isolate ubuntu-22.04 and ubuntu-24.04 artifacts without breaking the existing shared pool strategy. (raw.githubusercontent.com)

Only minor caveat: action.yml still collapses self-hosted runners into unknown. That’s acceptable as a documented limitation here, but if a future pool spans heterogeneous self-hosted Linux images, the same ABI mismatch could reappear.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Review: PR 7154 — Key the Rust cache by runner image family

This is a CI-only change to .github/actions/setup-rust/action.yml. No Rust/EVM/consensus code is touched, so most of the usual review dimensions (gas accounting, RLP, state trie, etc.) don't apply here. Reviewed as an infra/CI change.

Correctness

The root-cause analysis and fix are sound and I verified the key claims against the current repo state:

  • tag_release.yaml:build-ethrex reads the l2 pool on ubuntu-22.04, while pr-main_l2.yaml's writer job (save-cache: 'true') runs on ubuntu-latest (24.04) — confirmed at .github/workflows/pr-main_l2.yaml:359-360 vs .github/workflows/tag_release.yaml:76-88. This matches the described glibc-mismatch failure mode.
  • Swatinem/rust-cache@v2's shared-key/key inputs compose additively rather than one overriding the other, so folding ImageOS into key (action.yml:83) alongside the existing shared-key (cache-pool) correctly narrows the pool without breaking the existing pooling scheme.
  • ImageOS (ubuntu22/ubuntu24/macos15) is a real runner-provided environment variable, distinct from the env context — the comment's claim that env.ImageOS isn't reliably usable directly in a with: on a uses: step inside a composite action is a known GitHub Actions gotcha, and routing it through a run: step + $GITHUB_OUTPUT (action.yml:56-59) is the correct workaround.
  • Ordering is correct: the new "Identify the runner image family" step runs after Install Rust and before Add Rust Cache, so steps.image.outputs.family is available when needed.
  • Choosing image family over ImageVersion is the right granularity — it isolates glibc floors without causing weekly cache churn.

Security

No concerns. ${ImageOS:-unknown} is a plain shell variable expansion inside a run: block, not ${{ }} template interpolation of untrusted input, so there's no script-injection surface here (unlike patterns where PR titles/branch names get interpolated directly into run:).

Minor observations (non-blocking)

  1. Residual risk for self-hosted runners (action.yml:54-55): self-hosted runners that don't set ImageOS fall into a shared unknown bucket. Today only main_prover.yaml's prover pool uses a self-hosted (gpu) runner, and it's the sole consumer of that pool, so there's no active bleed — but if a second job/runner ever joins that pool on a different self-hosted image, the same glibc-mismatch bug could resurface silently under the unknown key. Worth a // TODO or tracking note if self-hosted pools grow, but not a reason to block this PR.
  2. Comment verbosity: the two comment blocks (action.yml:40-55, 61-78) are long relative to the code they annotate. Given they're documenting a genuinely non-obvious cross-job cache-poisoning failure mode (and the postmortem context is valuable for whoever next debugs a mysterious GLIBC_x not found error), I think the verbosity is justified here rather than a real issue.
  3. The unchecked STORE_SCHEMA_VERSION checklist item is correctly left unchecked — this PR doesn't touch Store.

Summary

Well-scoped, correctly targeted fix with solid root-cause analysis backed by verifiable evidence from the workflow files (writer/reader OS mismatch on the l2 pool). No correctness or security issues found; only a latent, currently-inactive edge case around self-hosted runners worth keeping in mind for the future.


Automated review by Claude (Anthropic) · sonnet · custom prompt

`Report PR Line Changes` failed on this branch alone with a runner-internal
"Index was out of range" during post-job cleanup. That job checks out both the
PR head and main in one job and runs this composite action from each, so a step
list that differs between the two revisions desynchronises the runner's
post-step bookkeeping — and this is the PR that changes the step list.

Folding the `ImageOS` read into the existing `rustver` step keeps the action at
three steps, matching main, so the two checkouts agree. The cache key is
unchanged in content; only where the value is computed moves.
@MegaRedHand
MegaRedHand added this pull request to the merge queue Aug 19, 2026
Merged via the queue into main with commit 5effb1b Aug 19, 2026
57 checks passed
@MegaRedHand
MegaRedHand deleted the fix/ci-rust-cache-runner-image branch August 19, 2026 20:27
@github-project-automation github-project-automation Bot moved this from In Review to Done in ethrex_l1 Aug 19, 2026
MysticRyuujin pushed a commit to MysticRyuujin/ethrex that referenced this pull request Aug 24, 2026
…lambdaclass#7166)

**Motivation**

`Ethrex Release` is still red on `main` after lambdaclass#7154, failing in
`build-ethrex (ubuntu-22.04, l2)` with the same error that PR set out to
fix ([run
32302415108](https://github.com/lambdaclass/ethrex/actions/runs/32302415108/job/96227727106),
on `7277c63ee`, which already contains it):

```
error: …/libparity_scale_codec_derive-….so: /lib/x86_64-linux-gnu/libc.so.6:
       version `GLIBC_2.39' not found
```

The diagnosis in lambdaclass#7154 holds: a proc-macro `.so` built on ubuntu-24.04
is restored into an ubuntu-22.04 job, `rustc` cannot dlopen it, and
`parity-scale-codec` collapses into eighteen `E0432`s that name the
dependency rather than the cache. The fix is what did not work.

lambdaclass#7154 passed the image family to `Swatinem/rust-cache` through its `key`
input. `key` is only honoured on the branch that builds a per-job key:

```ts
// rust-cache src/config.ts
const sharedKey = core.getInput("shared-key");
if (sharedKey) {
  key += `-${sharedKey}`;
} else {
  const inputKey = core.getInput("key");
  if (inputKey) key += `-${inputKey}`;
  // …job id…
}
```

Every job that sets `cache-pool` sets `shared-key`, so it takes the
first branch and `key` is dropped. The failing run shows the input
arriving and the key ignoring it:

```
with:
  shared-key: l2
  key: ubuntu22
…
Runner image family: ubuntu22
Restore Key:  v0-rust-l2-Linux-x64-73d6c353
Cache Key:    v0-rust-l2-Linux-x64-73d6c353-18b5747f
Restored from cache key "v0-rust-l2-Linux-x64-73d6c353-18b5747f" full match: true.
```

Same pool, same key, both images. The 24.04 writer
(`pr-main_l2.yaml:integration-test`, `runs-on: ubuntu-latest`) and the
22.04 reader (`tag_release.yaml:build-ethrex`) still share one entry.

**Description**

Fold the image family into the shared key itself: `shared-key:
<pool>-<image family>`, so it lands on the branch rust-cache actually
uses.

| | before | after |
| --- | --- | --- |
| pooled job on 22.04 | `v0-rust-l2-Linux-x64-…` |
`v0-rust-l2-ubuntu22-Linux-x64-…` |
| pooled job on 24.04 | `v0-rust-l2-Linux-x64-…` |
`v0-rust-l2-ubuntu24-Linux-x64-…` |

The shared key is left empty when `cache-pool` is unset, which keeps
those jobs (e.g. `pr_nostd.yaml`) on rust-cache's per-job key. `key: ${{
image_family }}` stays on the step for exactly that case, where it is
honoured. `inputs.cache-pool` reaches the script through `env:` rather
than direct `${{ }}` interpolation.

**Consequences**

`l2` has no 22.04 writer: the pool is written by `integration-test` on
`ubuntu-latest`. So `build-ethrex (ubuntu-22.04, l2)` gets a cold build
rather than a poisoned warm one. That is the correct outcome and the
only one available without a 22.04 writer for that pool; giving the
release job its own writable pool would warm it, but each entry runs
1.2-2.75GB against a 10GB LRU budget, so it is a separate call and left
out here.

As in lambdaclass#7154, changing the key invalidates existing entries once. Pools
refill on the next `main` build.

**How to test**

`tag_release.yaml` has no `pull_request` trigger, so the real check is
that `build-ethrex (ubuntu-22.04, l2)` goes green on the next push to
`main`. Before then, any job's log shows the key now carrying the image
family:

```
v0-rust-l2-ubuntu22-Linux-x64-…
```

**Checklist**

- [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR
includes breaking changes to the `Store` requiring a re-sync.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Github actions, build, tests, etc L1 Ethereum client L2 Rollup client

Projects

Status: Done
Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants