From 5bfa636ffba571d855b26aba32903450a163eb29 Mon Sep 17 00:00:00 2001 From: Cardinal Date: Thu, 3 Sep 2026 10:51:30 +0200 Subject: [PATCH 1/5] ci(echidna): add PR-fast and nightly fuzz jobs Catch invariant regressions on every PR with a time-boxed campaign, and keep overnight reproducers when a property fails. --- .github/workflows/echidna.yml | 124 ++++++++++++++++++++++++++++++++ .gitignore | 3 +- README.md | 2 + echidna/README.md | 25 ++++++- echidna/echidna.ci.fast.yaml | 15 ++++ echidna/echidna.ci.nightly.yaml | 14 ++++ scripts/echidna.sh | 30 +++++++- 7 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/echidna.yml create mode 100644 echidna/echidna.ci.fast.yaml create mode 100644 echidna/echidna.ci.nightly.yaml diff --git a/.github/workflows/echidna.yml b/.github/workflows/echidna.yml new file mode 100644 index 00000000..973ae4a6 --- /dev/null +++ b/.github/workflows/echidna.yml @@ -0,0 +1,124 @@ +name: Echidna + +on: + pull_request: + schedule: + # Nightly deep campaign (03:00 UTC). Scheduled workflows only run from the default branch. + - cron: '0 3 * * *' + workflow_dispatch: {} + +permissions: + contents: read + +concurrency: + group: echidna-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + NODEJS_VERSION: '20' + ECHIDNA_IMAGE: ghcr.io/crytic/echidna/echidna:v2.3.1 + +jobs: + echidna-fast: + name: Echidna (fast) / ${{ matrix.contract }} + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + contract: + - EchidnaStakeRegistryHarness + - EchidnaPriceOracleHarness + - EchidnaPostageStampHarness + - EchidnaRedistributionHarness + - EchidnaRedistributionClaimHarness + - EchidnaSystemHarness + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js and Yarn + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODEJS_VERSION }} + cache: yarn + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run Echidna (fast) + env: + ECHIDNA_CONFIG: echidna/echidna.ci.fast.yaml + ECHIDNA_CONTRACT: ${{ matrix.contract }} + ECHIDNA_TIMEOUT: '900' + run: yarn echidna + + - name: Upload reproducers and logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: echidna-fast-${{ matrix.contract }} + path: | + echidna/logs/ + echidna/corpus/by-contract/ + crytic-export/ + if-no-files-found: warn + retention-days: 14 + + echidna-nightly: + name: Echidna (nightly) / ${{ matrix.contract }} + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 180 + strategy: + fail-fast: false + matrix: + contract: + - EchidnaStakeRegistryHarness + - EchidnaPriceOracleHarness + - EchidnaPostageStampHarness + - EchidnaRedistributionHarness + - EchidnaRedistributionClaimHarness + - EchidnaSystemHarness + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js and Yarn + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODEJS_VERSION }} + cache: yarn + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run Echidna (nightly, 3 seeds) + env: + ECHIDNA_CONFIG: echidna/echidna.ci.nightly.yaml + ECHIDNA_CONTRACT: ${{ matrix.contract }} + # Per-seed cap so 3 seeds plus compile fit inside the 180-minute job timeout. + ECHIDNA_TIMEOUT: '3000' + run: | + status=0 + for seed in 1 2 3; do + echo "::group::Echidna seed ${seed}" + ECHIDNA_SEED="${seed}" ECHIDNA_LOG_DIR="echidna/logs/seed-${seed}" yarn echidna || status=1 + echo "::endgroup::" + done + exit "${status}" + + - name: Upload reproducers and logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: echidna-nightly-${{ matrix.contract }} + path: | + echidna/logs/ + echidna/corpus/by-contract/ + crytic-export/ + if-no-files-found: warn + retention-days: 30 diff --git a/.gitignore b/.gitignore index 9062f884..26493b77 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,8 @@ tenderly.log # Echidna fuzzing echidna/corpus/ +echidna/logs/ crytic-export/ # Cursor -.cursor/rules/audit-code.mdc \ No newline at end of file +.cursor/rules/audit-code.mdc diff --git a/README.md b/README.md index a53378e5..ecd4319b 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,8 @@ Harness layout, properties, and troubleshooting are documented in [echidna/READM yarn echidna ``` +CI runs a fast Echidna campaign on pull requests and a deeper multi-seed campaign nightly. See [echidna/README.md](./echidna/README.md#ci). + ## Run ### [Tests](./test) diff --git a/echidna/README.md b/echidna/README.md index 130bb3f0..8d4bd860 100644 --- a/echidna/README.md +++ b/echidna/README.md @@ -125,10 +125,33 @@ yarn echidna # all harnesses; needs Docker | `seqLen` | 320 | `ECHIDNA_SEQ_LEN` | | `maxBlockDelay` | 152 | — | | workers | yaml | `ECHIDNA_WORKERS` | +| seed | random | `ECHIDNA_SEED` | +| timeout (seconds) | none | `ECHIDNA_TIMEOUT` | Single harness: `ECHIDNA_CONTRACT=EchidnaRedistributionHarness yarn echidna` (also: `EchidnaStakeRegistryHarness`, `EchidnaPriceOracleHarness`, `EchidnaPostageStampHarness`, `EchidnaRedistributionClaimHarness`, `EchidnaSystemHarness`). -Config: `echidna/echidna.yaml` (`ECHIDNA_CONFIG` to override). Corpus/coverage: `echidna/corpus/by-contract//` (gitignored). Crytic: `crytic-export/`. +Config: `echidna/echidna.yaml` (`ECHIDNA_CONFIG` to override). Corpus/coverage: `echidna/corpus/by-contract//` (gitignored). Logs: `echidna/logs/` (gitignored). Crytic: `crytic-export/`. + +## CI + +Workflow: [`.github/workflows/echidna.yml`](../.github/workflows/echidna.yml). + +| Job | When | Budget | Timeout | +|-----|------|--------|---------| +| Fast | Every pull request | `echidna/echidna.ci.fast.yaml` (`testLimit` 4000, `seqLen` 80) | 20 minutes per harness (Echidna `--timeout` 900s) | +| Nightly | 03:00 UTC + manual `workflow_dispatch` | `echidna/echidna.ci.nightly.yaml` (`testLimit` 100000, `seqLen` 320), seeds `1 2 3` | 180 minutes per harness | + +Both jobs run one GitHub Actions matrix entry per harness so a failure names the broken invariant surface. On failure the workflow uploads `echidna/logs/`, corpus reproducers under `echidna/corpus/by-contract/`, and `crytic-export/` as artifacts. + +Reproduce a CI counterexample locally: + +```bash +ECHIDNA_CONFIG=echidna/echidna.ci.fast.yaml \ +ECHIDNA_CONTRACT=EchidnaStakeRegistryHarness \ +yarn echidna +``` + +Nightly campaigns only run automatically after this workflow is on the repository default branch (`master`). Until then, trigger **Echidna** via `workflow_dispatch`. ## Extend diff --git a/echidna/echidna.ci.fast.yaml b/echidna/echidna.ci.fast.yaml new file mode 100644 index 00000000..5eddd3a6 --- /dev/null +++ b/echidna/echidna.ci.fast.yaml @@ -0,0 +1,15 @@ +testMode: property + +# PR smoke campaign: reduced budget so the job finishes inside a strict timeout. +seqLen: 80 +testLimit: 4000 +shrinkLimit: 200 + +# Same round-length bound as the default config so commit → reveal remains reachable. +maxTimeDelay: 0 +maxBlockDelay: 152 + +corpusDir: echidna/corpus +coverage: false +workers: 2 +format: text diff --git a/echidna/echidna.ci.nightly.yaml b/echidna/echidna.ci.nightly.yaml new file mode 100644 index 00000000..fa37d354 --- /dev/null +++ b/echidna/echidna.ci.nightly.yaml @@ -0,0 +1,14 @@ +testMode: property + +# Overnight campaign: default-length sequences, higher test budget. +seqLen: 320 +testLimit: 100000 +shrinkLimit: 1000 + +maxTimeDelay: 0 +maxBlockDelay: 152 + +corpusDir: echidna/corpus +coverage: true +workers: 4 +format: text diff --git a/scripts/echidna.sh b/scripts/echidna.sh index 50d454b4..aad8e8db 100755 --- a/scripts/echidna.sh +++ b/scripts/echidna.sh @@ -13,6 +13,7 @@ cd "$ROOT_DIR" IMAGE="${ECHIDNA_IMAGE:-ghcr.io/crytic/echidna/echidna:latest}" CONTRACT="${ECHIDNA_CONTRACT:-}" CONFIG="${ECHIDNA_CONFIG:-echidna/echidna.yaml}" +LOG_DIR="${ECHIDNA_LOG_DIR:-echidna/logs}" # Crytic-compile reads artifacts/build-info when using --hardhat-ignore-compile inside Docker (no Node/npx). # Stale build-info from deleted Solidity sources causes "Unknown file" failures. @@ -40,6 +41,7 @@ fi # echidna/echidna.yaml: testLimit 60000, seqLen 320). Examples: # ECHIDNA_TEST_LIMIT=20000 ECHIDNA_SEQ_LEN=200 yarn echidna # faster smoke # ECHIDNA_WORKERS=8 ECHIDNA_CONTRACT=EchidnaSystemHarness yarn echidna +# ECHIDNA_SEED=1 ECHIDNA_TIMEOUT=600 yarn echidna # reproducible / time-boxed CI run # Use a string (not an array) so `set -u` never trips on empty `${arr[*]}` on older Bash. ECHIDNA_EXTRA_CLI="" if [[ -n "${ECHIDNA_TEST_LIMIT:-}" ]]; then @@ -51,6 +53,17 @@ fi if [[ -n "${ECHIDNA_WORKERS:-}" ]]; then ECHIDNA_EXTRA_CLI+=" --workers ${ECHIDNA_WORKERS}" fi +if [[ -n "${ECHIDNA_SEED:-}" ]]; then + ECHIDNA_EXTRA_CLI+=" --seed ${ECHIDNA_SEED}" +fi +if [[ -n "${ECHIDNA_TIMEOUT:-}" ]]; then + ECHIDNA_EXTRA_CLI+=" --timeout ${ECHIDNA_TIMEOUT}" +fi +if [[ -n "${ECHIDNA_ARGS:-}" ]]; then + ECHIDNA_EXTRA_CLI+=" ${ECHIDNA_ARGS}" +fi + +mkdir -p "${ROOT_DIR}/${LOG_DIR}" for c in "${CONTRACTS_TO_RUN[@]}"; do echo "==> echidna: running contract $c" >&2 @@ -58,10 +71,17 @@ for c in "${CONTRACTS_TO_RUN[@]}"; do # One corpus + coverage tree per harness so saved sequences stay relevant to # that contract (shared corpus mixed unrelated call shapes and diluted learning). CORPUS_DIR="echidna/corpus/by-contract/${c}" + if [[ -n "${ECHIDNA_SEED:-}" ]]; then + CORPUS_DIR="${CORPUS_DIR}/seed-${ECHIDNA_SEED}" + fi mkdir -p "${ROOT_DIR}/${CORPUS_DIR}" + LOG_FILE="${ROOT_DIR}/${LOG_DIR}/${c}.log" + # Drop stale Crytic output inside Docker (same uid as container root). A host # `rm -rf crytic-export` often fails after Docker created the dir as root. + # Capture the fuzzer exit code while still teeing logs for CI artifacts. + set +e docker run --rm \ --entrypoint sh \ -v "$ROOT_DIR":/src \ @@ -69,8 +89,16 @@ for c in "${CONTRACTS_TO_RUN[@]}"; do "$IMAGE" \ -c "rm -rf crytic-export && echidna-test . --contract ${c} --config ${CONFIG} \ --corpus-dir ${CORPUS_DIR} --coverage-dir ${CORPUS_DIR}/coverage${ECHIDNA_EXTRA_CLI} \ - --crytic-args '--hardhat-ignore-compile'" + --crytic-args '--hardhat-ignore-compile'" \ + 2>&1 | tee "${LOG_FILE}" + ec=${PIPESTATUS[0]} + set -e yarn -s ts-node "${ROOT_DIR}/scripts/echidna-coverage-summary.ts" "${c}" \ --coverage-dir "${ROOT_DIR}/${CORPUS_DIR}/coverage" || true + + if [[ "${ec}" -ne 0 ]]; then + echo "==> echidna: ${c} failed (exit ${ec}); log: ${LOG_FILE}" >&2 + exit "${ec}" + fi done From a227ca19a4dca406a90ea75be6a02b1304a45184 Mon Sep 17 00:00:00 2001 From: Cardinal Date: Thu, 3 Sep 2026 10:55:09 +0200 Subject: [PATCH 2/5] ci(echidna): split nightly into its own workflow Keep PR checks to the fast matrix only, so the overnight job does not show up as a skipped status on every PR. --- .github/workflows/echidna-nightly.yml | 74 +++++++++++++++++++++++++++ .github/workflows/echidna.yml | 64 +---------------------- echidna/README.md | 2 +- 3 files changed, 77 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/echidna-nightly.yml diff --git a/.github/workflows/echidna-nightly.yml b/.github/workflows/echidna-nightly.yml new file mode 100644 index 00000000..c59393f9 --- /dev/null +++ b/.github/workflows/echidna-nightly.yml @@ -0,0 +1,74 @@ +name: Echidna nightly + +on: + schedule: + # Deep campaign (03:00 UTC). Scheduled workflows only run from the default branch. + - cron: '0 3 * * *' + workflow_dispatch: {} + +permissions: + contents: read + +concurrency: + group: echidna-nightly-${{ github.ref }} + cancel-in-progress: false + +env: + NODEJS_VERSION: '20' + ECHIDNA_IMAGE: ghcr.io/crytic/echidna/echidna:v2.3.1 + +jobs: + echidna-nightly: + name: Echidna (nightly) / ${{ matrix.contract }} + runs-on: ubuntu-latest + timeout-minutes: 180 + strategy: + fail-fast: false + matrix: + contract: + - EchidnaStakeRegistryHarness + - EchidnaPriceOracleHarness + - EchidnaPostageStampHarness + - EchidnaRedistributionHarness + - EchidnaRedistributionClaimHarness + - EchidnaSystemHarness + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js and Yarn + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODEJS_VERSION }} + cache: yarn + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run Echidna (nightly, 3 seeds) + env: + ECHIDNA_CONFIG: echidna/echidna.ci.nightly.yaml + ECHIDNA_CONTRACT: ${{ matrix.contract }} + # Per-seed cap so 3 seeds plus compile fit inside the 180-minute job timeout. + ECHIDNA_TIMEOUT: '3000' + run: | + status=0 + for seed in 1 2 3; do + echo "::group::Echidna seed ${seed}" + ECHIDNA_SEED="${seed}" ECHIDNA_LOG_DIR="echidna/logs/seed-${seed}" yarn echidna || status=1 + echo "::endgroup::" + done + exit "${status}" + + - name: Upload reproducers and logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: echidna-nightly-${{ matrix.contract }} + path: | + echidna/logs/ + echidna/corpus/by-contract/ + crytic-export/ + if-no-files-found: warn + retention-days: 30 diff --git a/.github/workflows/echidna.yml b/.github/workflows/echidna.yml index 973ae4a6..1782518f 100644 --- a/.github/workflows/echidna.yml +++ b/.github/workflows/echidna.yml @@ -2,17 +2,14 @@ name: Echidna on: pull_request: - schedule: - # Nightly deep campaign (03:00 UTC). Scheduled workflows only run from the default branch. - - cron: '0 3 * * *' workflow_dispatch: {} permissions: contents: read concurrency: - group: echidna-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + group: echidna-fast-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true env: NODEJS_VERSION: '20' @@ -21,7 +18,6 @@ env: jobs: echidna-fast: name: Echidna (fast) / ${{ matrix.contract }} - if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest timeout-minutes: 20 strategy: @@ -66,59 +62,3 @@ jobs: crytic-export/ if-no-files-found: warn retention-days: 14 - - echidna-nightly: - name: Echidna (nightly) / ${{ matrix.contract }} - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - timeout-minutes: 180 - strategy: - fail-fast: false - matrix: - contract: - - EchidnaStakeRegistryHarness - - EchidnaPriceOracleHarness - - EchidnaPostageStampHarness - - EchidnaRedistributionHarness - - EchidnaRedistributionClaimHarness - - EchidnaSystemHarness - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js and Yarn - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODEJS_VERSION }} - cache: yarn - - - name: Install dependencies - run: yarn install --frozen-lockfile - - - name: Run Echidna (nightly, 3 seeds) - env: - ECHIDNA_CONFIG: echidna/echidna.ci.nightly.yaml - ECHIDNA_CONTRACT: ${{ matrix.contract }} - # Per-seed cap so 3 seeds plus compile fit inside the 180-minute job timeout. - ECHIDNA_TIMEOUT: '3000' - run: | - status=0 - for seed in 1 2 3; do - echo "::group::Echidna seed ${seed}" - ECHIDNA_SEED="${seed}" ECHIDNA_LOG_DIR="echidna/logs/seed-${seed}" yarn echidna || status=1 - echo "::endgroup::" - done - exit "${status}" - - - name: Upload reproducers and logs - if: failure() - uses: actions/upload-artifact@v4 - with: - name: echidna-nightly-${{ matrix.contract }} - path: | - echidna/logs/ - echidna/corpus/by-contract/ - crytic-export/ - if-no-files-found: warn - retention-days: 30 diff --git a/echidna/README.md b/echidna/README.md index 8d4bd860..a3da5ae7 100644 --- a/echidna/README.md +++ b/echidna/README.md @@ -134,7 +134,7 @@ Config: `echidna/echidna.yaml` (`ECHIDNA_CONFIG` to override). Corpus/coverage: ## CI -Workflow: [`.github/workflows/echidna.yml`](../.github/workflows/echidna.yml). +Workflows: [`.github/workflows/echidna.yml`](../.github/workflows/echidna.yml) (PRs) and [`.github/workflows/echidna-nightly.yml`](../.github/workflows/echidna-nightly.yml) (schedule). | Job | When | Budget | Timeout | |-----|------|--------|---------| From 3fb54fdf1058009b54b8def68622cfe687a0c317 Mon Sep 17 00:00:00 2001 From: Cardinal Date: Thu, 3 Sep 2026 10:59:40 +0200 Subject: [PATCH 3/5] ci(echidna): drop nightly campaign Contracts rarely deploy outside PRs, so keep a single time-boxed Echidna job on every pull request. --- .github/workflows/echidna-nightly.yml | 74 ------------------- .github/workflows/echidna.yml | 12 +-- README.md | 2 +- echidna/README.md | 13 +--- echidna/echidna.ci.nightly.yaml | 14 ---- .../{echidna.ci.fast.yaml => echidna.ci.yaml} | 2 +- 6 files changed, 11 insertions(+), 106 deletions(-) delete mode 100644 .github/workflows/echidna-nightly.yml delete mode 100644 echidna/echidna.ci.nightly.yaml rename echidna/{echidna.ci.fast.yaml => echidna.ci.yaml} (75%) diff --git a/.github/workflows/echidna-nightly.yml b/.github/workflows/echidna-nightly.yml deleted file mode 100644 index c59393f9..00000000 --- a/.github/workflows/echidna-nightly.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Echidna nightly - -on: - schedule: - # Deep campaign (03:00 UTC). Scheduled workflows only run from the default branch. - - cron: '0 3 * * *' - workflow_dispatch: {} - -permissions: - contents: read - -concurrency: - group: echidna-nightly-${{ github.ref }} - cancel-in-progress: false - -env: - NODEJS_VERSION: '20' - ECHIDNA_IMAGE: ghcr.io/crytic/echidna/echidna:v2.3.1 - -jobs: - echidna-nightly: - name: Echidna (nightly) / ${{ matrix.contract }} - runs-on: ubuntu-latest - timeout-minutes: 180 - strategy: - fail-fast: false - matrix: - contract: - - EchidnaStakeRegistryHarness - - EchidnaPriceOracleHarness - - EchidnaPostageStampHarness - - EchidnaRedistributionHarness - - EchidnaRedistributionClaimHarness - - EchidnaSystemHarness - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js and Yarn - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODEJS_VERSION }} - cache: yarn - - - name: Install dependencies - run: yarn install --frozen-lockfile - - - name: Run Echidna (nightly, 3 seeds) - env: - ECHIDNA_CONFIG: echidna/echidna.ci.nightly.yaml - ECHIDNA_CONTRACT: ${{ matrix.contract }} - # Per-seed cap so 3 seeds plus compile fit inside the 180-minute job timeout. - ECHIDNA_TIMEOUT: '3000' - run: | - status=0 - for seed in 1 2 3; do - echo "::group::Echidna seed ${seed}" - ECHIDNA_SEED="${seed}" ECHIDNA_LOG_DIR="echidna/logs/seed-${seed}" yarn echidna || status=1 - echo "::endgroup::" - done - exit "${status}" - - - name: Upload reproducers and logs - if: failure() - uses: actions/upload-artifact@v4 - with: - name: echidna-nightly-${{ matrix.contract }} - path: | - echidna/logs/ - echidna/corpus/by-contract/ - crytic-export/ - if-no-files-found: warn - retention-days: 30 diff --git a/.github/workflows/echidna.yml b/.github/workflows/echidna.yml index 1782518f..436eaecd 100644 --- a/.github/workflows/echidna.yml +++ b/.github/workflows/echidna.yml @@ -8,7 +8,7 @@ permissions: contents: read concurrency: - group: echidna-fast-${{ github.event.pull_request.number || github.ref }} + group: echidna-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true env: @@ -16,8 +16,8 @@ env: ECHIDNA_IMAGE: ghcr.io/crytic/echidna/echidna:v2.3.1 jobs: - echidna-fast: - name: Echidna (fast) / ${{ matrix.contract }} + echidna: + name: Echidna / ${{ matrix.contract }} runs-on: ubuntu-latest timeout-minutes: 20 strategy: @@ -44,9 +44,9 @@ jobs: - name: Install dependencies run: yarn install --frozen-lockfile - - name: Run Echidna (fast) + - name: Run Echidna env: - ECHIDNA_CONFIG: echidna/echidna.ci.fast.yaml + ECHIDNA_CONFIG: echidna/echidna.ci.yaml ECHIDNA_CONTRACT: ${{ matrix.contract }} ECHIDNA_TIMEOUT: '900' run: yarn echidna @@ -55,7 +55,7 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: echidna-fast-${{ matrix.contract }} + name: echidna-${{ matrix.contract }} path: | echidna/logs/ echidna/corpus/by-contract/ diff --git a/README.md b/README.md index ecd4319b..15e84de3 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Harness layout, properties, and troubleshooting are documented in [echidna/READM yarn echidna ``` -CI runs a fast Echidna campaign on pull requests and a deeper multi-seed campaign nightly. See [echidna/README.md](./echidna/README.md#ci). +CI runs Echidna on every pull request. See [echidna/README.md](./echidna/README.md#ci). ## Run diff --git a/echidna/README.md b/echidna/README.md index a3da5ae7..1f124706 100644 --- a/echidna/README.md +++ b/echidna/README.md @@ -134,25 +134,18 @@ Config: `echidna/echidna.yaml` (`ECHIDNA_CONFIG` to override). Corpus/coverage: ## CI -Workflows: [`.github/workflows/echidna.yml`](../.github/workflows/echidna.yml) (PRs) and [`.github/workflows/echidna-nightly.yml`](../.github/workflows/echidna-nightly.yml) (schedule). +Workflow: [`.github/workflows/echidna.yml`](../.github/workflows/echidna.yml). Runs on every pull request (and manual `workflow_dispatch`). -| Job | When | Budget | Timeout | -|-----|------|--------|---------| -| Fast | Every pull request | `echidna/echidna.ci.fast.yaml` (`testLimit` 4000, `seqLen` 80) | 20 minutes per harness (Echidna `--timeout` 900s) | -| Nightly | 03:00 UTC + manual `workflow_dispatch` | `echidna/echidna.ci.nightly.yaml` (`testLimit` 100000, `seqLen` 320), seeds `1 2 3` | 180 minutes per harness | - -Both jobs run one GitHub Actions matrix entry per harness so a failure names the broken invariant surface. On failure the workflow uploads `echidna/logs/`, corpus reproducers under `echidna/corpus/by-contract/`, and `crytic-export/` as artifacts. +One matrix job per harness, using `echidna/echidna.ci.yaml` (`testLimit` 4000, `seqLen` 80) with a 20-minute job timeout and Echidna `--timeout` 900s. On failure the workflow uploads `echidna/logs/`, corpus reproducers under `echidna/corpus/by-contract/`, and `crytic-export/` as artifacts. Reproduce a CI counterexample locally: ```bash -ECHIDNA_CONFIG=echidna/echidna.ci.fast.yaml \ +ECHIDNA_CONFIG=echidna/echidna.ci.yaml \ ECHIDNA_CONTRACT=EchidnaStakeRegistryHarness \ yarn echidna ``` -Nightly campaigns only run automatically after this workflow is on the repository default branch (`master`). Until then, trigger **Echidna** via `workflow_dispatch`. - ## Extend 1. Add `src/echidna/Echidna*Harness.sol` — auto-discovered by `scripts/echidna.sh`. diff --git a/echidna/echidna.ci.nightly.yaml b/echidna/echidna.ci.nightly.yaml deleted file mode 100644 index fa37d354..00000000 --- a/echidna/echidna.ci.nightly.yaml +++ /dev/null @@ -1,14 +0,0 @@ -testMode: property - -# Overnight campaign: default-length sequences, higher test budget. -seqLen: 320 -testLimit: 100000 -shrinkLimit: 1000 - -maxTimeDelay: 0 -maxBlockDelay: 152 - -corpusDir: echidna/corpus -coverage: true -workers: 4 -format: text diff --git a/echidna/echidna.ci.fast.yaml b/echidna/echidna.ci.yaml similarity index 75% rename from echidna/echidna.ci.fast.yaml rename to echidna/echidna.ci.yaml index 5eddd3a6..0770ad61 100644 --- a/echidna/echidna.ci.fast.yaml +++ b/echidna/echidna.ci.yaml @@ -1,6 +1,6 @@ testMode: property -# PR smoke campaign: reduced budget so the job finishes inside a strict timeout. +# PR campaign: reduced budget so the job finishes inside a strict timeout. seqLen: 80 testLimit: 4000 shrinkLimit: 200 From 59ab56c5d6bd06082483e0d556d63416e0727d74 Mon Sep 17 00:00:00 2001 From: Cardinal Date: Thu, 3 Sep 2026 11:28:05 +0200 Subject: [PATCH 4/5] ci(echidna): run the full 60k/320 campaign on PRs Drop the smoke-test budget so pull requests fuzz with the same campaign as local yarn echidna. --- .github/workflows/echidna.yml | 6 +++--- README.md | 2 +- echidna/README.md | 3 +-- echidna/echidna.ci.yaml | 15 --------------- 4 files changed, 5 insertions(+), 21 deletions(-) delete mode 100644 echidna/echidna.ci.yaml diff --git a/.github/workflows/echidna.yml b/.github/workflows/echidna.yml index 436eaecd..3583d22b 100644 --- a/.github/workflows/echidna.yml +++ b/.github/workflows/echidna.yml @@ -19,7 +19,7 @@ jobs: echidna: name: Echidna / ${{ matrix.contract }} runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 180 strategy: fail-fast: false matrix: @@ -46,9 +46,9 @@ jobs: - name: Run Echidna env: - ECHIDNA_CONFIG: echidna/echidna.ci.yaml ECHIDNA_CONTRACT: ${{ matrix.contract }} - ECHIDNA_TIMEOUT: '900' + # Stop cleanly before the 180-minute job timeout (setup + compile need headroom). + ECHIDNA_TIMEOUT: '10200' run: yarn echidna - name: Upload reproducers and logs diff --git a/README.md b/README.md index 15e84de3..d0be0008 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Harness layout, properties, and troubleshooting are documented in [echidna/READM yarn echidna ``` -CI runs Echidna on every pull request. See [echidna/README.md](./echidna/README.md#ci). +CI runs the full Echidna campaign on every pull request. See [echidna/README.md](./echidna/README.md#ci). ## Run diff --git a/echidna/README.md b/echidna/README.md index 1f124706..1566f828 100644 --- a/echidna/README.md +++ b/echidna/README.md @@ -136,12 +136,11 @@ Config: `echidna/echidna.yaml` (`ECHIDNA_CONFIG` to override). Corpus/coverage: Workflow: [`.github/workflows/echidna.yml`](../.github/workflows/echidna.yml). Runs on every pull request (and manual `workflow_dispatch`). -One matrix job per harness, using `echidna/echidna.ci.yaml` (`testLimit` 4000, `seqLen` 80) with a 20-minute job timeout and Echidna `--timeout` 900s. On failure the workflow uploads `echidna/logs/`, corpus reproducers under `echidna/corpus/by-contract/`, and `crytic-export/` as artifacts. +One matrix job per harness, using the same campaign as local `yarn echidna` (`echidna/echidna.yaml`: `testLimit` 60000, `seqLen` 320). Each job has a 180-minute cap and Echidna `--timeout` 10200s so the fuzzer stops cleanly. On failure the workflow uploads `echidna/logs/`, corpus reproducers under `echidna/corpus/by-contract/`, and `crytic-export/` as artifacts. Reproduce a CI counterexample locally: ```bash -ECHIDNA_CONFIG=echidna/echidna.ci.yaml \ ECHIDNA_CONTRACT=EchidnaStakeRegistryHarness \ yarn echidna ``` diff --git a/echidna/echidna.ci.yaml b/echidna/echidna.ci.yaml deleted file mode 100644 index 0770ad61..00000000 --- a/echidna/echidna.ci.yaml +++ /dev/null @@ -1,15 +0,0 @@ -testMode: property - -# PR campaign: reduced budget so the job finishes inside a strict timeout. -seqLen: 80 -testLimit: 4000 -shrinkLimit: 200 - -# Same round-length bound as the default config so commit → reveal remains reachable. -maxTimeDelay: 0 -maxBlockDelay: 152 - -corpusDir: echidna/corpus -coverage: false -workers: 2 -format: text From 8021a083f6b4f3e0a62db4b5b9154ab0a1aa316e Mon Sep 17 00:00:00 2001 From: Cardinal Date: Thu, 3 Sep 2026 13:38:05 +0200 Subject: [PATCH 5/5] ci(echidna): leave headroom so CI timeouts keep artifacts Drop Echidna --timeout to 9000s under the 180-minute job cap, and document copying Seed from CI logs for local replay. --- .github/workflows/echidna.yml | 5 +++-- echidna/README.md | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/echidna.yml b/.github/workflows/echidna.yml index 3583d22b..ba5d31f6 100644 --- a/.github/workflows/echidna.yml +++ b/.github/workflows/echidna.yml @@ -47,8 +47,9 @@ jobs: - name: Run Echidna env: ECHIDNA_CONTRACT: ${{ matrix.contract }} - # Stop cleanly before the 180-minute job timeout (setup + compile need headroom). - ECHIDNA_TIMEOUT: '10200' + # 9000s leaves ~30 min for checkout, yarn, compile, and image pull + # inside the 180-minute job cap so a timeout still uploads artifacts. + ECHIDNA_TIMEOUT: '9000' run: yarn echidna - name: Upload reproducers and logs diff --git a/echidna/README.md b/echidna/README.md index 1566f828..27ea447c 100644 --- a/echidna/README.md +++ b/echidna/README.md @@ -136,12 +136,13 @@ Config: `echidna/echidna.yaml` (`ECHIDNA_CONFIG` to override). Corpus/coverage: Workflow: [`.github/workflows/echidna.yml`](../.github/workflows/echidna.yml). Runs on every pull request (and manual `workflow_dispatch`). -One matrix job per harness, using the same campaign as local `yarn echidna` (`echidna/echidna.yaml`: `testLimit` 60000, `seqLen` 320). Each job has a 180-minute cap and Echidna `--timeout` 10200s so the fuzzer stops cleanly. On failure the workflow uploads `echidna/logs/`, corpus reproducers under `echidna/corpus/by-contract/`, and `crytic-export/` as artifacts. +One matrix job per harness, using the same campaign as local `yarn echidna` (`echidna/echidna.yaml`: `testLimit` 60000, `seqLen` 320). Each job has a 180-minute cap and Echidna `--timeout` 9000s so setup plus fuzzing fit under the GitHub Actions limit and a timeout still uploads artifacts. On failure the workflow uploads `echidna/logs/`, corpus reproducers under `echidna/corpus/by-contract/`, and `crytic-export/` as artifacts. -Reproduce a CI counterexample locally: +CI does not pin `ECHIDNA_SEED` (a new seed each run). To replay a CI counterexample, copy `Seed: N` from the uploaded log and pass it locally: ```bash ECHIDNA_CONTRACT=EchidnaStakeRegistryHarness \ +ECHIDNA_SEED=N \ yarn echidna ```