…shared-bridge test (#7151)
**Motivation**
`Integration Test Shared Bridge - Normal` fails intermittently with
`Receiver balance did not increase correctly` at
`test/tests/l2/shared_bridge.rs:409`. It hit `main` on Aug 3
(`0cc4572c5`) and Aug 13 (`f2998963d`), and `release/v24.0.0` on Aug 12
(`8f3d2ca05`), always with the same message hash `0x66867250…5be951` —
so the test is not wrong about what it expects, the message genuinely
never arrives. In the failing runs L2B never commits batch 1, its L2→L2
message never becomes pending on the router, and L2A's watcher logs `L2
message not found in pending messages` 153 times with 0 verified.
Three separate defects combine to produce that:
**1. Both sequencers commit from the same L1 account.**
`crates/l2/docker-compose.yaml` gives `ethrex_l2`
`--committer.l1-private-key 0x385c…` and the shared-bridge override gave
`ethrex_l2_b` the same key (likewise the proof-coordinator key). They
post to one L1 on the same 15 s timer, so their commit loops race for
the same nonce and the loser is rejected — first as an underpriced
replacement, then as a too-low nonce.
**2. The loser cannot recover, which is a bug beyond CI.**
`bump_gas_generic_tx` computed the blob fee as `1 + (bump_percentage /
100) * 10` and then divided by ten. For the only bump either call site
passes, `30`, that evaluates to `fee * 1 / 10`: **every retry lowered
`max_fee_per_blob_gas` by 90%**, so a batch commitment could never
re-price itself — on CI or on a real L1 during a fee spike. Blob
replacements also require the fee to *double*
(`DEFAULT_BLOB_PRICE_BUMP_PERCENT`), and that threshold applies to the
fee cap and the tip as well, so bumping those by 30% could not have
replaced a blob transaction either. Separately, the retry loop never
noticed when the nonce had become unreachable, so one `NonceTooLow`
burned all ten attempts in ~3.5 s and reported "max retries reached",
hiding the cause.
**3. The test slept a fixed 180 s and then asserted**, which made the
result depend on whatever else the job was doing. The first failure in
this job's history is the run of #7029, which aligned the `cargo test
--no-run` prebuild's features with the run step so the test binary is no
longer rebuilt inside it. Job step timestamps show the window between
"Start Sequencer L2B" and the assertion collapsing from **2 m 35 s to 3
s**, moving the assertion onto the sequencers' cold start — where batch
1 carries all 192 privileged deposit mints and the two co-nonced
committers collide from their first commit. Worth noting the direction:
the passing Jul 22 run had *more* nonce collisions (72 underpriced + 76
too-low) than any failing run, because they happened before the test
looked.
**Description**
- **L2B gets its own committer and proof-coordinator accounts, and its
deployment authorizes them.** Changing the keys alone is not enough:
`Timelock.commitBatch` is `onlyRole(SEQUENCER)`, and the role is granted
from the deployer's `--committer.l1-address` /
`--proof-sender.l1-address`, which both default to L2A's accounts.
Without the matching addresses every L2B `commitBatch` reverts at gas
estimation as an unauthorized caller — the same end symptom, reached a
different way. Neither option is forwarded to the deployer container by
`docker-compose.yaml`, so they are set on the override's
`contract_deployer`, leaving L2A's deployment on the defaults.
- **`bump_gas_generic_tx` raises every fee dimension** by a bump that
clears the mempool's replacement threshold for the transaction's
category — `BLOB_FEE_BUMP_PERCENT` (100) for blob-carrying transactions,
`FEE_BUMP_PERCENT` (30) otherwise — using saturating arithmetic so an
absurd starting fee cannot overflow.
- **The retry loop stops spending attempts that cannot succeed:** it
abandons the transaction with `UnreachableNonce` once the chain's nonce
has passed it (checked against the chain, not against the rejection
text, which differs between clients), and reports the reason when the
`maximum_allowed_max_fee_per_gas` / `_blob_gas` clamps leave a bid the
mempool already rejected.
- **Every shared-bridge assertion now waits for its own condition**
instead of sharing one fixed sleep, with a timeout more generous than
the 180 s it replaces. The L1 deposits accounting is waited on
separately rather than assumed settled once the L2B credit lands: the
two are updated by different transactions on different chains and
nothing orders them. The forced-inclusion test's expiry and verification
waits are deliberately left as sleeps — they are waiting for something
*not* to happen.
- **Hardening:** a test pins L2B's private keys to the addresses its
deployment authorizes, so drift between the two blocks of the override
file fails there instead of surfacing at runtime as an ABI-undecodable
`AccessControlUnauthorizedAccount` revert and a chain that silently
stops committing. Unit tests cover both replacement thresholds,
monotonicity across ten retries, saturation, and — so they cannot
silently stop discriminating — that the previous blob formula fails
them.
This is deliberately **not** a revert of #7029. That PR removed an
accidental delay which had been masking defects 1 and 2 since the
shared-bridge test landed; restoring the delay would hide them again.
**Also worth knowing, not changed here:** this job is gated
`github.event_name != 'merge_group'`, so the merge queue never runs it
and a PR can land without it ever executing against the merged result.
Combined with the paths filter, it effectively only runs on pushes to
main — which is why a ~3% flake sat unnoticed for weeks.
**Checklist**
- [x] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR
includes breaking changes to the `Store` requiring a re-sync. — n/a, no
`Store` changes
Motivation
Nothing in this repo restores a cargo cache. Every job logs
No cache found, including on main, so every CI leg builds from scratch. TheTest - ubuntu-22.04leg of the last completed L1 run spent 29 minutes on that, and the L2 Dev integration test 44 minutes, of which ~36 was rustc (21 min building ethrex, 15.5 min building the test binary, 6.5 min actually testing).Description
The cache is never readable. An entry saved on a
refs/pull/N/mergeref can only be restored by that same PR;?ref=refs/heads/mainreturnscount=0, so there is nothing on a ref that other branches may read.setup-rustnow saves only on main.Nothing fits in the budget either. Counting matrix legs, 32
setup-rustcalls run on main across six workflows, and rust-cache keys per job, so that is 32 writers at 1.2 to 2.75 GB each. The default budget is 10 GB with LRU eviction (admin-configurable, billed above that);actions/cache/usagecurrently reports 17 entries totalling 31.37 GiB. Either way 32 writers do not coexist, so main would thrash exactly as PR refs do now.setup-rustgainscache-pool, forwarded to rust-cache'sshared-key, which replaces the per-job segment of the key. Four pools, each with exactly one writer, picked as the widest dependency set in it:l1pr-main_l1::testtoolingl1-reorgpr-main_l1::reorg-testsrelease-fastl2pr-main_l2::integration-test-F l2,l2-sqlat releaseprovermain_prover::testEverything else restores only, so a lean job cannot overwrite an entry with a subset and clippy-only target dirs never claim a slot.
reorg-testsgets its own pool because it is the only job on main building the root workspace atrelease-fast, which the dev-profilel1entry cannot serve. That is 6 entries (thel1writer is a 3-platform matrix) instead of 32.The second workspace was never cached. rust-cache's
workspacesdefaults to the repo root, andtoolingis a separate workspace with its own target dir, so the ef_tests, reorgs, loc and hive_report builds were all cold regardless of anything above. That includes a step inside thel1writer's own job, sincepr-main_l1::testrunsmake -C tooling/ef_tests/blockchain test.setup-rustnow lists both workspaces. Entries get bigger for it; the count does not change.Writing is opt-in:
save-cachedefaults to'false'and only the four jobs above pass'true'. It has to be opt-in rather than opt-out, becausescheduleandworkflow_dispatchhave no head ref of their own and fire on the default branch, sogithub.refisrefs/heads/mainandsave-ifevaluates true.daily_loc_reportanddaily_hive_reportwere writing thel1entry nightly on that path, anddeep_reorg,pr_lint_readme,pr_perf_levmandpr_perf_build_block_benchcould. Cache keys are immutable, so the first of those to save wins and the pool owner's richer save is skipped, not merged.The remaining PR-only workflows (
pr_loc,pr_check_l2_genesis,pr_perf_blocks_exec) get a pool name too. They restore only; naming a pool is what makes main's entry readable for them, at no storage cost.The L2 test binary was a near total duplicate of the node build. cargo fingerprints include the resolved feature set, and the two invocations diverged on 18 shared packages, among them sha3, tokio, hyper and idna, which sit low enough in the graph that the second build restarted from libc. Most of it came from
ethrex-test'sl2feature being an empty marker while the crate dev-depends onethrexwith default features, so cmd/ethrex and the whole L2 stack compiled twice in different configurations. Forwardingl2toethrex/l2andethrex/l2-sqlbrings the divergence down to 6 packages and makes cmd/ethrex resolve identically in both builds. What still rebuilds is the three cratesethrex-testturnstesting/test-utilson for, plus their dependents.Three other jobs (
main_prover, and two inpr-main_l2) prebuilt the test binary without--features l2and then ran with it. Harmless while the feature was empty, but now it flips the whole L2 stack config, so they get the flag too.The L2 build was hidden inside the backgrounded
cargo run. A compile error did not fail the job, it let the readiness loop poll for 25 min first, and the test binary was compiled while the node and prover were competing for the same 4 cores. Both binaries are now built in their own steps before the node starts, and the job usesrelease-fastinstead ofrelease. The test waits on 15s batch commits and L1 confirmations rather than on EVM throughput, so a slightly slower binary costs very little here.The deposit test raced the dev pre-funding. Removing the implicit sleep exposed a pre-existing bug. A node started with
--devfunds the rich accounts by depositing half of each L1 balance through the bridge, and the deployer awaits only the L1 receipts; the credits reach L2 through the L1 watcher a batch later. The port-3900 readiness gate opens ~20s before that, a gap the test binary's own compile time used to cover. So the suite read a zero initial L2 balance andtest_depositfailed by exactly the 5e26 wei pre-funding.l2_integration_testnow polls every test account's balance until it is non-zero before asserting any delta, which fixes localmake initruns too.Expect no improvement on this PR itself, since main has to run once to populate the pools.
Not in this PR
Jobs building the root workspace at plain
release(thepr_perf_*benchmarks,deep_reorg) have no writer on main, so they restore~/.cargobut no matching target dir. Same forrevm_comparison, which is excluded from the workspace and has a third target dir. Giving them writers costs entries, and the budget is not measurable from outside the org.aligned-sdkis a non-optional dependency ofethrex-l2and pulls the whole SP1 host stack (~4 min of the critical path) into every-F l2build even with no zkVM backend enabled. Gating it behind a feature needs config and release-plumbing changes.Checklist
STORE_SCHEMA_VERSION(crates/storage/lib.rs) if the PR includes breaking changes to theStorerequiring a re-sync. N/A: CI and feature wiring only.