From a9b2c3163544fdcfd9d98fa5bafcf5bd7ad4f276 Mon Sep 17 00:00:00 2001 From: dicethedev Date: Wed, 20 May 2026 18:42:28 +0100 Subject: [PATCH 1/2] ci: download latest leanSpec fixture release instead of generating fixtures --- .github/workflows/ci.yml | 88 +++++++++++----------------------------- CLAUDE.md | 4 +- Makefile | 18 +++++--- 3 files changed, 38 insertions(+), 72 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0c70608..6a94d65e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,80 +47,38 @@ jobs: steps: - uses: actions/checkout@v6 - # Read the pinned leanSpec commit from the Makefile (single source of truth) - - name: Get leanSpec pinned commit - id: lean-spec - run: echo "commit=$(sed -n 's/^LEAN_SPEC_COMMIT_HASH:= *//p' Makefile)" >> $GITHUB_OUTPUT + - name: Get leanSpec fixtures SHA + id: fixtures-release + run: | + sha=$(curl -L -f https://github.com/leanEthereum/leanSpec/releases/latest/download/fixtures-prod-scheme.tar.gz.sha256 | cut -d' ' -f1) + echo "sha=$sha" >> $GITHUB_OUTPUT - name: Restore test fixtures cache id: cache-fixtures uses: actions/cache/restore@v5 with: path: leanSpec/fixtures - key: leanspec-fixtures-${{ steps.lean-spec.outputs.commit }} - - # All fixture generation steps are skipped when the cache hits - - name: Checkout leanSpec at pinned commit - if: steps.cache-fixtures.outputs.cache-hit != 'true' - uses: actions/checkout@v6 - with: - repository: leanEthereum/leanSpec - ref: ${{ steps.lean-spec.outputs.commit }} - path: leanSpec - - - name: Install uv and Python 3.14 - if: steps.cache-fixtures.outputs.cache-hit != 'true' - uses: astral-sh/setup-uv@v4 - with: - enable-cache: true - cache-dependency-glob: "leanSpec/pyproject.toml" - python-version: "3.14" - - - name: Sync leanSpec dependencies - if: steps.cache-fixtures.outputs.cache-hit != 'true' - working-directory: leanSpec - run: uv sync --no-progress + key: leanspec-fixtures-${{ steps.fixtures-release.outputs.sha }} - - name: Get production keys URL hash + - name: Download leanSpec fixtures release if: steps.cache-fixtures.outputs.cache-hit != 'true' - id: prod-keys-url - working-directory: leanSpec run: | - URL=$(uv run python -c "from consensus_testing.keys import KEY_DOWNLOAD_URLS; print(KEY_DOWNLOAD_URLS['prod'])") - HASH=$(echo -n "$URL" | sha256sum | awk '{print $1}') - echo "hash=$HASH" >> $GITHUB_OUTPUT - - - name: Restore production keys cache - if: steps.cache-fixtures.outputs.cache-hit != 'true' - id: cache-prod-keys - uses: actions/cache/restore@v5 - with: - path: leanSpec/packages/testing/src/consensus_testing/test_keys/prod_scheme - key: prod-keys-${{ steps.prod-keys-url.outputs.hash }} - - - name: Download production keys - if: steps.cache-fixtures.outputs.cache-hit != 'true' && steps.cache-prod-keys.outputs.cache-hit != 'true' - working-directory: leanSpec - run: uv run python -m consensus_testing.keys --download --scheme prod - - # Save production keys even if a later step fails, so a re-run does - # not have to re-download. See: https://github.com/actions/cache/tree/main/save#always-save-cache - # - # `cache-hit == 'false'` (rather than `!= 'true'`) only matches when - # the restore step actually ran and missed: when fixtures were already - # cached, the restore was skipped and `cache-hit` is empty, so save - # is skipped too. - - name: Save production keys cache - if: always() && steps.cache-prod-keys.outputs.cache-hit == 'false' - uses: actions/cache/save@v5 - with: - path: leanSpec/packages/testing/src/consensus_testing/test_keys/prod_scheme - key: ${{ steps.cache-prod-keys.outputs.cache-primary-key }} - - - name: Generate test fixtures - if: steps.cache-fixtures.outputs.cache-hit != 'true' - working-directory: leanSpec - run: uv run fill --fork=Devnet --scheme prod -o fixtures -n auto + tmpdir=$(mktemp -d) + trap 'rm -rf "$tmpdir"' EXIT + fixtures_url="https://github.com/leanEthereum/leanSpec/releases/latest/download/fixtures-prod-scheme.tar.gz" + sha_url="$fixtures_url.sha256" + echo "Downloading fixtures from $fixtures_url" + curl -L -f -o "$tmpdir/fixtures-prod-scheme.tar.gz" "$fixtures_url" + curl -L -f -o "$tmpdir/fixtures-prod-scheme.tar.gz.sha256" "$sha_url" + expected=$(cut -d' ' -f1 "$tmpdir/fixtures-prod-scheme.tar.gz.sha256") + actual=$(sha256sum "$tmpdir/fixtures-prod-scheme.tar.gz" | awk '{print $1}') + if [ "$expected" != "$actual" ]; then + echo "SHA256 mismatch: expected $expected, got $actual" + exit 1 + fi + rm -rf leanSpec/fixtures + mkdir -p leanSpec/fixtures + tar -xzf "$tmpdir/fixtures-prod-scheme.tar.gz" -C leanSpec/fixtures --strip-components=1 # Save fixtures even if a later step fails, so a re-run does not # have to regenerate them. See: https://github.com/actions/cache/tree/main/save#always-save-cache diff --git a/CLAUDE.md b/CLAUDE.md index 942e4c04..9ff66851 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ Not to be confused with Ethereum consensus clients AKA Beacon Chain clients AKA **Main branch:** `main` **Rust version:** 1.92.0 (edition 2024) -**Test fixtures commit:** Check `LEAN_SPEC_COMMIT_HASH` in Makefile +**Test fixtures release:** Download latest production fixtures from leanSpec releases ## Codebase Structure (10 crates) @@ -81,7 +81,7 @@ make test # All tests + forkchoice spec tests ### Common Operations ```bash .claude/skills/test-pr-devnet/scripts/test-branch.sh # Test branch in multi-client devnet -rm -rf leanSpec && make leanSpec/fixtures # Regenerate test fixtures (requires uv) +rm -rf leanSpec && make leanSpec/fixtures # Download latest released test fixtures make docker-build # Build Docker image (DOCKER_TAG=local) make run-devnet # Run local devnet with lean-quickstart ``` diff --git a/Makefile b/Makefile index 74901c8d..6aa7b807 100644 --- a/Makefile +++ b/Makefile @@ -24,15 +24,23 @@ docker-build: ## 🐳 Build the Docker image -t ghcr.io/lambdaclass/ethlambda:$(DOCKER_TAG) . @echo -# 2026-04-29 -LEAN_SPEC_COMMIT_HASH:=18fe71fee49f8865a5c8a4cb8b1787b0cbc9e25b +LEAN_SPEC_FIXTURES_URL ?= https://github.com/leanEthereum/leanSpec/releases/latest/download/fixtures-prod-scheme.tar.gz +LEAN_SPEC_FIXTURES_SHA_URL ?= $(LEAN_SPEC_FIXTURES_URL).sha256 leanSpec: git clone https://github.com/leanEthereum/leanSpec.git --single-branch - cd leanSpec && git checkout $(LEAN_SPEC_COMMIT_HASH) -leanSpec/fixtures: leanSpec - cd leanSpec && uv run fill --fork devnet -n auto --scheme=prod -o fixtures +leanSpec/fixtures: + tmpdir=$$(mktemp -d); \ + trap 'rm -rf "$$tmpdir"' EXIT; \ + curl -L -f -o "$$tmpdir/fixtures-prod-scheme.tar.gz" "$(LEAN_SPEC_FIXTURES_URL)"; \ + curl -L -f -o "$$tmpdir/fixtures-prod-scheme.tar.gz.sha256" "$(LEAN_SPEC_FIXTURES_SHA_URL)"; \ + expected=$$(cut -d' ' -f1 "$$tmpdir/fixtures-prod-scheme.tar.gz.sha256"); \ + actual=$$(sha256sum "$$tmpdir/fixtures-prod-scheme.tar.gz" | awk '{print $$1}'); \ + [ "$$expected" = "$$actual" ]; \ + rm -rf leanSpec/fixtures; \ + mkdir -p leanSpec/fixtures; \ + tar -xzf "$$tmpdir/fixtures-prod-scheme.tar.gz" -C leanSpec/fixtures --strip-components=1 lean-quickstart: git clone https://github.com/blockblaz/lean-quickstart.git --depth 1 --single-branch From 829eb9631cd4d547c516993eee896be7fbc88dd1 Mon Sep 17 00:00:00 2001 From: dicethedev Date: Wed, 20 May 2026 19:13:26 +0100 Subject: [PATCH 2/2] ci: download leanSpec release fixtures; add SHA verification and prevent TOCTOU --- .github/workflows/ci.yml | 14 ++++++++++---- Makefile | 5 ++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a94d65e..2087ccc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,10 +47,16 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Get leanSpec fixtures SHA + - name: Get leanSpec fixtures release info id: fixtures-release run: | - sha=$(curl -L -f https://github.com/leanEthereum/leanSpec/releases/latest/download/fixtures-prod-scheme.tar.gz.sha256 | cut -d' ' -f1) + api_url="https://api.github.com/repos/leanEthereum/leanSpec/releases/latest" + json=$(curl -sL "$api_url") + fixtures_url=$(echo "$json" | python3 -c "import sys,json; j=json.load(sys.stdin); print(next(a.get('browser_download_url') for a in j.get('assets',[]) if a.get('name')=='fixtures-prod-scheme.tar.gz'))") + sha_url=$(echo "$json" | python3 -c "import sys,json; j=json.load(sys.stdin); print(next(a.get('browser_download_url') for a in j.get('assets',[]) if a.get('name')=='fixtures-prod-scheme.tar.gz.sha256'))") + sha=$(curl -sL "$sha_url" | cut -d' ' -f1) + echo "url=$fixtures_url" >> $GITHUB_OUTPUT + echo "sha_url=$sha_url" >> $GITHUB_OUTPUT echo "sha=$sha" >> $GITHUB_OUTPUT - name: Restore test fixtures cache @@ -65,8 +71,8 @@ jobs: run: | tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT - fixtures_url="https://github.com/leanEthereum/leanSpec/releases/latest/download/fixtures-prod-scheme.tar.gz" - sha_url="$fixtures_url.sha256" + fixtures_url="${{ steps.fixtures-release.outputs.url }}" + sha_url="${{ steps.fixtures-release.outputs.sha_url }}" echo "Downloading fixtures from $fixtures_url" curl -L -f -o "$tmpdir/fixtures-prod-scheme.tar.gz" "$fixtures_url" curl -L -f -o "$tmpdir/fixtures-prod-scheme.tar.gz.sha256" "$sha_url" diff --git a/Makefile b/Makefile index 6aa7b807..1a51c971 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,10 @@ leanSpec/fixtures: curl -L -f -o "$$tmpdir/fixtures-prod-scheme.tar.gz.sha256" "$(LEAN_SPEC_FIXTURES_SHA_URL)"; \ expected=$$(cut -d' ' -f1 "$$tmpdir/fixtures-prod-scheme.tar.gz.sha256"); \ actual=$$(sha256sum "$$tmpdir/fixtures-prod-scheme.tar.gz" | awk '{print $$1}'); \ - [ "$$expected" = "$$actual" ]; \ + if [ "$$expected" != "$$actual" ]; then \ + echo "SHA256 mismatch: expected $$expected, got $$actual" >&2; \ + exit 1; \ + fi; \ rm -rf leanSpec/fixtures; \ mkdir -p leanSpec/fixtures; \ tar -xzf "$$tmpdir/fixtures-prod-scheme.tar.gz" -C leanSpec/fixtures --strip-components=1