mirror of
https://github.com/logos-blockchain/lez-fuzzing.git
synced 2026-06-07 11:39:30 +00:00
218 lines
8.3 KiB
YAML
218 lines
8.3 KiB
YAML
name: Mutation Testing
|
||
|
||
# ── When to run ───────────────────────────────────────────────────────────────
|
||
# Plane A (fuzz_props invariants) runs on every PR that touches harness code.
|
||
# Plane B (LEZ protocol vs corpus) is slow (minutes per mutant × many mutants)
|
||
# so it only runs on a weekly schedule or on manual dispatch.
|
||
on:
|
||
pull_request:
|
||
paths:
|
||
- "fuzz_props/**"
|
||
- "fuzz/fuzz_targets/**"
|
||
- ".github/workflows/mutants.yml"
|
||
schedule:
|
||
- cron: "0 4 * * 1" # 04:00 UTC every Monday
|
||
workflow_dispatch:
|
||
|
||
env:
|
||
RISC0_DEV_MODE: "1"
|
||
CARGO_TERM_COLOR: always
|
||
|
||
jobs:
|
||
# ── Plane A: mutate fuzz_props (invariant harness) ────────────────────────
|
||
# Oracle: cargo test -p fuzz_props --release
|
||
# Fast (~30–120 s total). Blocks PRs if any invariant-check logic is
|
||
# under-tested.
|
||
mutants-harness:
|
||
name: Mutants — fuzz_props invariants
|
||
runs-on: ubuntu-latest
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Checkout logos-execution-zone
|
||
uses: ./.github/actions/checkout-lez
|
||
|
||
- name: Install stable Rust toolchain
|
||
uses: dtolnay/rust-toolchain@stable
|
||
|
||
- name: Install logos-blockchain-circuits
|
||
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
|
||
with:
|
||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: Cache cargo registry
|
||
uses: actions/cache@v4
|
||
with:
|
||
path: |
|
||
~/.cargo/registry
|
||
~/.cargo/git
|
||
target
|
||
key: mutants-harness-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
||
|
||
- name: Install cargo-mutants
|
||
run: cargo install cargo-mutants --locked
|
||
|
||
# workspace.metadata.cargo-mutants in Cargo.toml sets:
|
||
# additional_cargo_args = ["--release"]
|
||
# exclude_globs = ["fuzz/fuzz_targets/**"]
|
||
# timeout_multiplier = 3.0
|
||
- name: Run mutation tests on fuzz_props
|
||
run: |
|
||
cargo mutants \
|
||
--package fuzz_props \
|
||
--in-place \
|
||
--output mutants-harness.out
|
||
|
||
- name: Upload mutants report
|
||
if: always()
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: mutants-harness-report
|
||
path: mutants-harness.out/
|
||
|
||
- name: Write GitHub Step Summary
|
||
if: always()
|
||
run: |
|
||
MISSED=$(wc -l < mutants-harness.out/missed.txt 2>/dev/null | tr -d ' ' || echo 0)
|
||
CAUGHT=$(wc -l < mutants-harness.out/caught.txt 2>/dev/null | tr -d ' ' || echo 0)
|
||
{
|
||
echo "## Mutation Testing — \`fuzz_props\` invariants"
|
||
echo ""
|
||
echo "| Result | Count |"
|
||
echo "|--------|-------|"
|
||
echo "| ✅ Caught | ${CAUGHT} |"
|
||
echo "| ❌ Survived | ${MISSED} |"
|
||
echo ""
|
||
if [ "${MISSED}" -gt 0 ]; then
|
||
echo "### Surviving mutants (invariant-checker gaps)"
|
||
echo '```'
|
||
cat mutants-harness.out/missed.txt 2>/dev/null || true
|
||
echo '```'
|
||
echo ""
|
||
echo "> Each surviving mutant represents a mutation in the invariant-checking"
|
||
echo "> code that \`cargo test -p fuzz_props\` did not detect."
|
||
echo "> Add a property-test that specifically exercises that code path."
|
||
else
|
||
echo "> All mutants caught — invariant-checking logic is fully covered."
|
||
fi
|
||
} >> "$GITHUB_STEP_SUMMARY"
|
||
|
||
- name: Fail if any mutations survived
|
||
run: |
|
||
if [ -s mutants-harness.out/missed.txt ]; then
|
||
echo "ERROR: surviving mutants found in fuzz_props — see artifact and Step Summary"
|
||
cat mutants-harness.out/missed.txt
|
||
exit 1
|
||
fi
|
||
|
||
# ── Plane B: mutate LEZ protocol code, oracle = corpus regression ─────────
|
||
# Each mutant: rebuild nssa/common + replay all 15 fuzz corpora (-runs=0).
|
||
# Surviving mutants = protocol bugs the committed corpus has never caught.
|
||
# Runs on schedule (weekly Monday) or manual workflow_dispatch only.
|
||
mutants-protocol:
|
||
name: Mutants — LEZ protocol vs corpus
|
||
runs-on: ubuntu-latest
|
||
if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Checkout logos-execution-zone
|
||
uses: ./.github/actions/checkout-lez
|
||
|
||
# cargo-fuzz requires nightly.
|
||
- name: Install Rust nightly toolchain
|
||
uses: dtolnay/rust-toolchain@nightly
|
||
with:
|
||
components: llvm-tools-preview
|
||
|
||
- name: Install logos-blockchain-circuits
|
||
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
|
||
with:
|
||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: Cache cargo registry
|
||
uses: actions/cache@v4
|
||
with:
|
||
path: |
|
||
~/.cargo/registry
|
||
~/.cargo/git
|
||
target
|
||
key: mutants-protocol-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
||
|
||
- name: Install cargo-fuzz and cargo-mutants
|
||
run: |
|
||
cargo install cargo-fuzz --locked
|
||
cargo install cargo-mutants --locked
|
||
|
||
- name: Make corpus-regression wrapper executable
|
||
run: chmod +x scripts/mutants-corpus-test.sh
|
||
|
||
# Build all 15 fuzz targets once before the mutation loop so that each
|
||
# mutant only needs to rebuild the mutated crate, not the fuzz harness.
|
||
- name: Pre-build fuzz targets
|
||
run: |
|
||
for target in \
|
||
fuzz_transaction_decoding fuzz_stateless_verification \
|
||
fuzz_state_transition fuzz_block_verification \
|
||
fuzz_encoding_roundtrip fuzz_signature_verification \
|
||
fuzz_replay_prevention fuzz_state_diff_computation \
|
||
fuzz_validate_execute_consistency fuzz_state_serialization \
|
||
fuzz_witness_set_verification fuzz_program_deployment_lifecycle \
|
||
fuzz_apply_state_diff_split_path fuzz_multi_block_state_sequence \
|
||
fuzz_sequencer_vs_replayer; do
|
||
cargo fuzz build "${target}"
|
||
done
|
||
|
||
# cargo-mutants is invoked from the LEZ workspace (sibling directory).
|
||
# FUZZ_REPO is exported so the wrapper script can locate the corpus and
|
||
# fuzz directory without relying on relative paths.
|
||
- name: Run mutation tests against LEZ (nssa + common)
|
||
env:
|
||
FUZZ_REPO: ${{ github.workspace }}
|
||
working-directory: logos-execution-zone
|
||
run: |
|
||
cargo mutants \
|
||
--package nssa \
|
||
--package common \
|
||
--in-place \
|
||
--test-command "${{ github.workspace }}/scripts/mutants-corpus-test.sh" \
|
||
--output "${{ github.workspace }}/mutants-protocol.out" \
|
||
--timeout-multiplier 5.0
|
||
|
||
- name: Upload mutants report
|
||
if: always()
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: mutants-protocol-report
|
||
path: mutants-protocol.out/
|
||
|
||
- name: Write GitHub Step Summary
|
||
if: always()
|
||
run: |
|
||
MISSED=$(wc -l < mutants-protocol.out/missed.txt 2>/dev/null | tr -d ' ' || echo 0)
|
||
CAUGHT=$(wc -l < mutants-protocol.out/caught.txt 2>/dev/null | tr -d ' ' || echo 0)
|
||
{
|
||
echo "## Mutation Testing — LEZ protocol vs committed corpus"
|
||
echo ""
|
||
echo "| Result | Count |"
|
||
echo "|--------|-------|"
|
||
echo "| ✅ Caught by corpus | ${CAUGHT} |"
|
||
echo "| ❌ Survived (corpus gap) | ${MISSED} |"
|
||
echo ""
|
||
if [ "${MISSED}" -gt 0 ]; then
|
||
echo "### Surviving mutants (corpus gaps — protocol bugs not yet reached)"
|
||
echo '```'
|
||
cat mutants-protocol.out/missed.txt 2>/dev/null || true
|
||
echo '```'
|
||
echo ""
|
||
echo "> For each surviving mutant:"
|
||
echo "> 1. Run \`cargo fuzz run <target>\` targeting the mutated function."
|
||
echo "> 2. Save the crashing input to \`corpus/libfuzz/<target>/\`."
|
||
echo "> 3. Commit the corpus entry — the next run will show \`CAUGHT\`."
|
||
else
|
||
echo "> All mutants caught — committed corpus covers all tested mutation points."
|
||
fi
|
||
} >> "$GITHUB_STEP_SUMMARY"
|