Skip to content

Commit 0b85d47

Browse files
authored
build: add a 14-day publish-age cooldown to dependency resolution (#607)
## 🗒️ Description / Motivation Ports Commit-Boost/commit-boost-client#492. Cargo's unstable `min-publish-age` (rust-lang/cargo#17009) lets the resolver skip crate versions published less than N days ago, a cooldown against freshly compromised releases. This enables it at 14 days, adds the Makefile path for resolving under it, and makes every build `--locked` so stable cargo cannot silently re-resolve around the policy. The feature is nightly-only; stable 1.97.1 ignores the tables silently. Only the pinned `nightly-2026-06-21` resolver enforces the cooldown, so `--locked` everywhere is what makes an unresolved manifest change fail loudly instead of resolving on stable. ## What Changed | File | Change | |------|--------| | `.cargo/config.toml` | `[unstable] min-publish-age = true`, `[registry] global-min-publish-age = "14 days"` | | `Makefile` | `make update` (resolve under the cooldown), `make update-allow PACKAGE= VERSION=` (escape hatch, bypasses the whole resolution), `make cooldown-check` (advisory). `make lint`/`make test` now `--locked` | | `.github/workflows/ci.yml` | `cargo check`/`clippy` run `--locked`; new `cooldown` job annotates the run with a warning, never fails | | `Dockerfile` | `cargo chef cook` honors `$LOCKED` like the final build; the shadow variant still sets it empty | | `CONTRIBUTING.md`, `CLAUDE.md` | Document the workflow | ## Correctness / Behavior Guarantees - No manifest or lockfile change. Locked builds are byte-for-byte what they were. - A `cargo update --workspace --dry-run` does **not** flag too-new locked crates, so the check has to be the full `cargo update --dry-run -Z min-publish-age`, grepping `Downgrading|is too new`. That run also refreshes git dependencies (leanSig, leanVM, ethrex, rust-libp2p) to their branch heads with no cooldown; git revs have no publish age and must be reviewed by hand. - **Known state today:** a full re-resolution under any window of 8+ days fails with a misleading `rand ^0.10` conflict. Real cause: rand 0.10 needs chacha20 0.10, whose 0.10.0/0.10.1 are yanked and whose only live release (0.10.2) was published 2026-08-27. `make update` errors and the CI job emits a "probe failed" warning until 2026-09-10; `make update-allow` covers an urgent bump before then. The committed lockfile already pins the yanked chacha20 0.10.0, which any future re-resolution will move. ## Tests Added / Run - `make lint` passes with `--locked`. - Stable `cargo check --locked` and `cargo update --workspace --dry-run --locked` with the new tables: no warnings, no changes. - Temporarily pinned `smallvec 1.16.0` (3 days old) and ran the 7-day dry-run: ` Downgrading smallvec v1.16.0 -> v1.15.2 (...)`, confirming the grep pattern. Lockfile restored identical. - Ran the CI step script locally and `make cooldown-check`: both report the chacha20-driven probe failure as a warning with exit 0. ## Related Issues / PRs - Related to Commit-Boost/commit-boost-client#492 ## ✅ Verification Checklist - [x] Ran `make fmt` — clean (no Rust changes) - [x] Ran `make lint` (clippy with `-D warnings`) — clean - [ ] Ran `make test` — not run; no Rust or fixture changes, only the `--locked` flag was added
1 parent db34b43 commit 0b85d47

6 files changed

Lines changed: 96 additions & 7 deletions

File tree

‎.cargo/config.toml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,16 @@ rustflags = [
33
"-Ctarget-cpu=x86-64-v3",
44
"-Ctarget-feature=+avx2,+sse2,+ssse3,+sse4.1,+sse4.2,+bmi1,+lzcnt,+pclmulqdq",
55
]
6+
7+
# Supply-chain cooldown for dependency resolution (unstable min-publish-age,
8+
# tracking issue rust-lang/cargo#17009): crate versions published less than
9+
# 14 days ago are excluded when the resolver runs on a nightly cargo.
10+
# Stable cargo ignores these tables silently, so builds from the committed
11+
# Cargo.lock are unaffected; run `make update` to resolve under the policy.
12+
# When the feature stabilizes, drop the [unstable] table and the nightly
13+
# resolver pin in the Makefile: the policy then binds all resolution.
14+
[unstable]
15+
min-publish-age = true
16+
17+
[registry]
18+
global-min-publish-age = "14 days"

‎.github/workflows/ci.yml‎

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ jobs:
4444
run: cargo fmt --all -- --check
4545

4646
- name: Cargo check
47-
run: cargo check --workspace --all-targets
47+
run: cargo check --locked --workspace --all-targets
4848

4949
- name: Clippy
50-
run: cargo clippy --workspace --all-targets -- -D warnings
50+
run: cargo clippy --locked --workspace --all-targets -- -D warnings
5151

5252
# tooling/event-monitor declares its own [workspace] table, so every step
5353
# above stops at the root workspace members and never reaches it. Its
@@ -80,6 +80,44 @@ jobs:
8080
# harness end-to-end and its JSON output contract in a few seconds.
8181
- name: Benchmark smoke (mock crypto)
8282
run: |
83-
cargo run --profile release-fast --bin ethlambda -- benchmark synthetic --mock-crypto \
83+
cargo run --locked --profile release-fast --bin ethlambda -- benchmark synthetic --mock-crypto \
8484
--num-validators 4 --warmup-slots 4 --iterations 3 --format json \
8585
| jq -e '.schema_version == 1 and (.samples | length == 3)'
86+
87+
# Stable cargo ignores the publish-age cooldown in .cargo/config.toml, so the
88+
# lockfile can pin too-young crates. Fail the build when either lockfile does.
89+
# Infrastructure failures (toolchain download, a resolution that fails for
90+
# reasons unrelated to age, e.g. a yanked upstream crate) only warn: they are
91+
# outside the PR's control and would block every PR.
92+
cooldown:
93+
name: Dependency cooldown
94+
runs-on: ubuntu-latest
95+
steps:
96+
- uses: actions/checkout@v6
97+
98+
- name: Check lockfiles against the publish-age cooldown
99+
run: |
100+
if ! rustup toolchain install nightly-2026-06-21 --profile minimal; then
101+
echo "::warning title=Publish-age cooldown check skipped::toolchain install failed"
102+
exit 0
103+
fi
104+
status=0
105+
for manifest in Cargo.toml tooling/event-monitor/Cargo.toml; do
106+
if ! cargo +nightly-2026-06-21 update --dry-run -Z min-publish-age --manifest-path "$manifest" > cooldown.txt 2>&1; then
107+
# Drop the index/git refresh chatter so the excerpt is the actual error; %0A = newline in annotations
108+
msg=$(grep -v '^ *Updating ' cooldown.txt | head -20 | sed ':a;N;$!ba;s/\n/%0A/g')
109+
echo "::warning title=Publish-age cooldown probe failed for $manifest::$msg"
110+
continue
111+
fi
112+
# A cooldown-driven downgrade is annotated with the too-young version's
113+
# publish date; downgrades for other reasons (MSRV, a tightened
114+
# requirement) carry no such note and are not this check's business.
115+
hits=$(grep -E '^ *Downgrading .*published' cooldown.txt || true)
116+
if [ -n "$hits" ]; then
117+
count=$(echo "$hits" | wc -l | tr -d ' ')
118+
msg=$(echo "$hits" | head -20 | sed ':a;N;$!ba;s/\n/%0A/g')
119+
echo "::error title=$manifest pins $count crate(s) younger than the publish-age cooldown::$msg"
120+
status=1
121+
fi
122+
done
123+
exit $status

‎CLAUDE.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ make test # All tests + forkchoice spec tests
9090
### Common Operations
9191
```bash
9292
rm -rf leanSpec && make leanSpec/fixtures # Download latest released test fixtures
93+
make update UPDATE_ARGS="-p <crate>" # Bump deps under the 14-day publish-age cooldown (nightly resolver)
94+
make cooldown-check # Fail if a lockfile pins crates younger than the cooldown (same as CI)
9395
make docker-build # Build Docker image (DOCKER_TAG=local)
9496
make run-devnet # Run local devnet with lean-quickstart
9597
```

‎CONTRIBUTING.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ All commits must have a verified signature.
9696
- **Comments:** Explain *why*, not *what*. Code should be self-explanatory.
9797
- **Error handling:** Use `Result` and `thiserror`. Avoid `.unwrap()` outside tests.
9898
- **Dependencies:** Adding a new crate requires justification in the PR description.
99+
Resolve version bumps with `make update` (optionally `UPDATE_ARGS="-p <crate>"`): it runs
100+
the resolver under a 14-day publish-age cooldown (`.cargo/config.toml`) that excludes
101+
freshly published crate versions as a supply-chain precaution. A plain `cargo update` or
102+
`cargo add` on stable bypasses the cooldown; `make lint`/`make test` and CI build
103+
`--locked` so an unresolved manifest change fails loudly instead of silently re-resolving.
104+
Git dependencies have no publish age and are not covered.
99105

100106
### Review Process
101107

‎Dockerfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ ENV NO_DEFAULT_FEATURES=$NO_DEFAULT_FEATURES
3838
ARG LOCKED="--locked"
3939
ENV LOCKED=$LOCKED
4040

41-
RUN cargo chef cook --profile $BUILD_PROFILE $NO_DEFAULT_FEATURES --features "$FEATURES" --recipe-path recipe.json
41+
RUN cargo chef cook --profile $BUILD_PROFILE $NO_DEFAULT_FEATURES --features "$FEATURES" $LOCKED --recipe-path recipe.json
4242

4343
# Build application
4444
# Include .git so vergen-git2 can extract version info (branch, commit SHA)

‎Makefile‎

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: help fmt lint bench docker-build shadow-build shadow-docker-build run-devnet test docs docs-deps docs-serve
1+
.PHONY: help fmt lint bench update cooldown-check docker-build shadow-build shadow-docker-build run-devnet test docs docs-deps docs-serve
22

33
help: ## 📚 Show help for each of the Makefile recipes
44
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
@@ -7,12 +7,42 @@ fmt: ## 🎨 Format all code using rustfmt
77
cargo fmt --all
88

99
lint: ## 🔍 Run clippy on all workspace crates
10-
cargo clippy --workspace --all-targets -- -D warnings
10+
cargo clippy --locked --workspace --all-targets -- -D warnings
1111

1212
test: leanSpec/fixtures ## 🧪 Run all tests
1313
# release-fast: release-grade opt-level to avoid stack overflows during
1414
# signature verification/aggregation, without paying for LTO on every rebuild
15-
cargo test --workspace --profile release-fast
15+
cargo test --locked --workspace --profile release-fast
16+
17+
# Used ONLY to resolve dependency updates: min-publish-age (.cargo/config.toml)
18+
# is nightly-only, everything else runs on the stable toolchain pinned in
19+
# rust-toolchain.toml.
20+
RESOLVER_TOOLCHAIN := nightly-2026-06-21
21+
22+
# Versions published less than 14 days ago are excluded from resolution.
23+
# Resolution done on stable (`cargo add`, plain `cargo update`) is NOT covered;
24+
# this target is the intended path for routine updates. Git dependencies have
25+
# no publish age and are refreshed WITHOUT any cooldown: review their lockfile
26+
# rev changes manually.
27+
update: ## 📦 Update dependencies under the publish-age cooldown (UPDATE_ARGS="-p foo")
28+
rustup toolchain install $(RESOLVER_TOOLCHAIN) --profile minimal > /dev/null && \
29+
cargo +$(RESOLVER_TOOLCHAIN) update -Z min-publish-age $(UPDATE_ARGS)
30+
31+
# Stable cargo ignores the cooldown, so a lockfile can pin too-young crates;
32+
# same check as the CI `cooldown` job, without touching the files. A cooldown
33+
# downgrade is annotated with the too-young version's publish date; downgrades
34+
# for other reasons carry no such note and are not flagged.
35+
cooldown-check: ## 🔎 Fail if a lockfile pins crates younger than the publish-age cooldown
36+
@rustup toolchain install $(RESOLVER_TOOLCHAIN) --profile minimal > /dev/null && \
37+
status=0; \
38+
for manifest in Cargo.toml tooling/event-monitor/Cargo.toml; do \
39+
if ! out=$$(cargo +$(RESOLVER_TOOLCHAIN) update --dry-run -Z min-publish-age --manifest-path $$manifest 2>&1); then \
40+
echo "WARNING: publish-age cooldown probe failed for $$manifest:"; echo "$$out" | grep -v "^ *Updating " | head -20; continue; \
41+
fi; \
42+
hits=$$(echo "$$out" | grep -E "^ *Downgrading .*published" || true); \
43+
if [ -n "$$hits" ]; then echo "ERROR: $$manifest pins crates younger than the publish-age cooldown:"; echo "$$hits"; status=1; fi; \
44+
done; \
45+
exit $$status
1646

1747
BENCH_ARGS ?= synthetic --mock-crypto
1848

0 commit comments

Comments
 (0)