Add site link to README footer (2026-06-08) #6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: ci | |
| # cf-invariants-pyth Phase-3.5 CI — invariants + Crucible + scorecards. | |
| # | |
| # The CI run IS the proof: zero local toolchain footprint, every push rebuilds | |
| # the Pyth Solana Receiver program (anchor-lang 0.32.1 -> 1.0.1 port) AND its | |
| # 2 planted twins, then runs the Crucible harness on every clean/planted pair, | |
| # asserting clean=0 violations and planted>=1 per invariant class. Real | |
| # scorecards are captured (ANSI-stripped) under findings/<invariant>/ and | |
| # uploaded as an artifact. If any matrix cell stops being green, the artifact | |
| # is broken — we never publish a manual "verified" claim. | |
| # | |
| # Classes proven by this CI: | |
| # - two_step_governance — pyth_receiver_ref pair, invariant_two_step_governance_atomic | |
| # - reclaim_rent_conservation — pyth_receiver_ref pair, invariant_reclaim_rent_returns_to_write_authority | |
| # | |
| # The 3rd invariant class designed in Phase-1 (`vaa_quorum_gating`) was | |
| # retired in Phase-3 (D-cf-invariants-pyth-phase3-rescope-2026-06-04 = A) | |
| # because driving the planted bug requires a guardian-signing VAA-crafter | |
| # helper outside this harness's port-+-fixture pattern. | |
| # | |
| # Pinned upstream surfaces (matched to cf-invariants-jito's ci.yml): | |
| # - Crucible: v0.2.0 (asymmetric-research/crucible) | |
| # - anchor-lang: 1.0.1 (matches Crucible v0.2.0 workspace) | |
| # - Solana CLI: v2.1.21 (Anza release; recent stable) | |
| # - platform-tools: v1.52 (recent tag with linux-x86_64 asset; the | |
| # bundled default in Agave v2.1.21 is v1.43, | |
| # too old for anchor-lang 1.0.1 deps. | |
| # NB: Agave v2.1.21 ships a buggy | |
| # cargo-build-sbf that panics if | |
| # ~/.cache/solana doesn't exist — we mkdir | |
| # it explicitly before the first invocation.) | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: {} | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| CRUCIBLE_REF: v0.2.0 | |
| SOLANA_VERSION: v2.1.21 | |
| SOLANA_TOOLS_VERSION: v1.52 | |
| # Iteration budget per Crucible run. Small enough to fit a free-tier | |
| # GitHub-hosted runner; large enough for the planted bug to fire | |
| # (minimal counterexamples for these invariants are 1-2 actions). | |
| CRUCIBLE_TIMEOUT: '30' | |
| jobs: | |
| workspace-check: | |
| name: cf-invariants-pyth workspace check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-rust | |
| - name: cargo check --workspace | |
| run: cargo check --workspace --locked || cargo check --workspace | |
| build-sbf: | |
| name: cargo build-sbf pyth-solana-receiver (Phase-3.5 marquee artifact) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-rust | |
| - name: Install Anza / Solana CLI ${{ env.SOLANA_VERSION }} | |
| run: | | |
| sh -c "$(curl -sSfL https://release.anza.xyz/${SOLANA_VERSION}/install)" | |
| echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH | |
| - name: Pre-create platform-tools cache dir | |
| run: mkdir -p "$HOME/.cache/solana" | |
| - name: Verify solana / cargo-build-sbf on PATH | |
| run: | | |
| solana --version | |
| cargo-build-sbf --version | |
| - name: cargo build-sbf pyth-solana-receiver | |
| run: | | |
| cargo build-sbf \ | |
| --tools-version "${SOLANA_TOOLS_VERSION}" \ | |
| --manifest-path programs/pyth-solana-receiver/Cargo.toml | |
| ls -l target/deploy/pyth_solana_receiver.so | |
| - name: Upload built program artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pyth_solana_receiver-so | |
| path: target/deploy/pyth_solana_receiver.so | |
| harness: | |
| name: build + run Crucible harness (2 invariants × clean+planted) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 120 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-rust | |
| - name: Install Anza / Solana CLI ${{ env.SOLANA_VERSION }} | |
| run: | | |
| sh -c "$(curl -sSfL https://release.anza.xyz/${SOLANA_VERSION}/install)" | |
| echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH | |
| - name: Pre-create platform-tools cache dir | |
| run: mkdir -p "$HOME/.cache/solana" | |
| - name: Verify solana / cargo-build-sbf on PATH | |
| run: | | |
| solana --version | |
| cargo-build-sbf --version | |
| - name: Clone Crucible @ ${{ env.CRUCIBLE_REF }} (sibling to this repo) | |
| # Path deps in references/*/fuzz/*/Cargo.toml resolve to | |
| # `../../../../../crucible/...`, i.e. <repo-root>/../crucible. | |
| # We use $GITHUB_WORKSPACE/../crucible to satisfy that. | |
| working-directory: ${{ github.workspace }}/.. | |
| run: | | |
| git clone --depth 1 --branch "${CRUCIBLE_REF}" \ | |
| https://github.com/asymmetric-research/crucible.git crucible | |
| (cd crucible && git rev-parse HEAD) | |
| # Second rust-cache step (Crucible build dir under <repo-root>/../crucible) | |
| # is intentionally inline — the composite action targets the workspace | |
| # cache; this one needs a distinct cache key for the pinned upstream. | |
| - name: Cache cargo (Crucible build) | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: ../crucible -> target | |
| key: crucible-${{ env.CRUCIBLE_REF }} | |
| - name: Build + install Crucible CLI from v0.2.0 source | |
| working-directory: ${{ github.workspace }}/../crucible | |
| run: | | |
| cargo install --path crates/crucible-fuzz-cli --locked || \ | |
| cargo install --path crates/crucible-fuzz-cli | |
| which crucible | |
| crucible --version || crucible --help | head -3 | |
| # ----------------------------------------------------------------- | |
| # Build the clean reference's .so ONCE — the same artifact backs | |
| # both fuzz fixtures under references/pyth_receiver_ref/fuzz/*. | |
| # ----------------------------------------------------------------- | |
| - name: cargo build-sbf pyth_receiver_ref (clean) | |
| run: | | |
| cargo build-sbf \ | |
| --tools-version "${SOLANA_TOOLS_VERSION}" \ | |
| --manifest-path references/pyth_receiver_ref/programs/pyth-solana-receiver/Cargo.toml | |
| ls -l references/pyth_receiver_ref/target/deploy/pyth_solana_receiver.so | |
| # ----------------------------------------------------------------- | |
| # Build the 2 planted twins. | |
| # ----------------------------------------------------------------- | |
| - name: cargo build-sbf pyth_receiver_ref_planted_two_step_governance | |
| run: | | |
| cargo build-sbf \ | |
| --tools-version "${SOLANA_TOOLS_VERSION}" \ | |
| --manifest-path references/pyth_receiver_ref_planted_two_step_governance/programs/pyth-solana-receiver/Cargo.toml | |
| ls -l references/pyth_receiver_ref_planted_two_step_governance/target/deploy/pyth_solana_receiver.so | |
| - name: cargo build-sbf pyth_receiver_ref_planted_reclaim_rent_conservation | |
| run: | | |
| cargo build-sbf \ | |
| --tools-version "${SOLANA_TOOLS_VERSION}" \ | |
| --manifest-path references/pyth_receiver_ref_planted_reclaim_rent_conservation/programs/pyth-solana-receiver/Cargo.toml | |
| ls -l references/pyth_receiver_ref_planted_reclaim_rent_conservation/target/deploy/pyth_solana_receiver.so | |
| # ----------------------------------------------------------------- | |
| # Sync fixture sources clean → planted (the fixture source IS the | |
| # contract; both variants must run the same probe code so the only | |
| # behavioral difference is the planted bug). | |
| # ----------------------------------------------------------------- | |
| - name: Sync fixture sources clean → planted (each invariant) | |
| run: | | |
| for inv in pyth_two_step_governance pyth_reclaim_rent_conservation; do | |
| for planted in pyth_receiver_ref_planted_two_step_governance \ | |
| pyth_receiver_ref_planted_reclaim_rent_conservation; do | |
| cp "references/pyth_receiver_ref/fuzz/${inv}/src/main.rs" \ | |
| "references/${planted}/fuzz/${inv}/src/main.rs" | |
| done | |
| done | |
| # ----------------------------------------------------------------- | |
| # Pair 1 — two_step_governance | |
| # ----------------------------------------------------------------- | |
| - name: Crucible run — two_step_governance CLEAN (expect 0 violations) | |
| id: two_step_governance_clean | |
| working-directory: references/pyth_receiver_ref/fuzz/pyth_two_step_governance | |
| run: | | |
| set -o pipefail | |
| mkdir -p "$GITHUB_WORKSPACE/ci-out" | |
| crucible run pyth_solana_receiver invariant_two_step_governance_atomic \ | |
| --release --timeout "${CRUCIBLE_TIMEOUT}" \ | |
| 2>&1 | tee "$GITHUB_WORKSPACE/ci-out/two_step_governance_clean.out" | |
| if grep -q -E 'FUZZ_FINDING|INVARIANT VIOLATED' "$GITHUB_WORKSPACE/ci-out/two_step_governance_clean.out"; then | |
| echo "::error::two_step_governance clean reported a violation — accept_governance_authority_transfer should atomically clear target" | |
| exit 1 | |
| fi | |
| - name: Crucible run — two_step_governance PLANTED (expect >=1 violation) | |
| id: two_step_governance_planted | |
| working-directory: references/pyth_receiver_ref_planted_two_step_governance/fuzz/pyth_two_step_governance | |
| run: | | |
| set +e | |
| crucible run pyth_solana_receiver invariant_two_step_governance_atomic \ | |
| --release --timeout "${CRUCIBLE_TIMEOUT}" \ | |
| 2>&1 | tee "$GITHUB_WORKSPACE/ci-out/two_step_governance_planted.out" | |
| rc=${PIPESTATUS[0]} | |
| set -e | |
| if grep -q -E 'FUZZ_FINDING|INVARIANT VIOLATED' "$GITHUB_WORKSPACE/ci-out/two_step_governance_planted.out"; then | |
| echo "two_step_governance planted reported a violation (expected)." | |
| exit 0 | |
| elif [ "$rc" -ne 0 ]; then | |
| echo "two_step_governance planted exited non-zero ($rc); treating as detected violation." | |
| exit 0 | |
| else | |
| echo "::error::two_step_governance planted ran clean — the dropped target-clear was not surfaced within ${CRUCIBLE_TIMEOUT}s." | |
| exit 1 | |
| fi | |
| # ----------------------------------------------------------------- | |
| # Pair 2 — reclaim_rent_conservation | |
| # ----------------------------------------------------------------- | |
| - name: Crucible run — reclaim_rent_conservation CLEAN (expect 0 violations) | |
| id: reclaim_rent_conservation_clean | |
| working-directory: references/pyth_receiver_ref/fuzz/pyth_reclaim_rent_conservation | |
| run: | | |
| set -o pipefail | |
| crucible run pyth_solana_receiver invariant_reclaim_rent_returns_to_write_authority \ | |
| --release --timeout "${CRUCIBLE_TIMEOUT}" \ | |
| 2>&1 | tee "$GITHUB_WORKSPACE/ci-out/reclaim_rent_conservation_clean.out" | |
| if grep -q -E 'FUZZ_FINDING|INVARIANT VIOLATED' "$GITHUB_WORKSPACE/ci-out/reclaim_rent_conservation_clean.out"; then | |
| echo "::error::reclaim_rent_conservation clean reported a violation — WrongWriteAuthority constraint should reject attacker" | |
| exit 1 | |
| fi | |
| - name: Crucible run — reclaim_rent_conservation PLANTED (expect >=1 violation) | |
| id: reclaim_rent_conservation_planted | |
| working-directory: references/pyth_receiver_ref_planted_reclaim_rent_conservation/fuzz/pyth_reclaim_rent_conservation | |
| run: | | |
| set +e | |
| crucible run pyth_solana_receiver invariant_reclaim_rent_returns_to_write_authority \ | |
| --release --timeout "${CRUCIBLE_TIMEOUT}" \ | |
| 2>&1 | tee "$GITHUB_WORKSPACE/ci-out/reclaim_rent_conservation_planted.out" | |
| rc=${PIPESTATUS[0]} | |
| set -e | |
| if grep -q -E 'FUZZ_FINDING|INVARIANT VIOLATED' "$GITHUB_WORKSPACE/ci-out/reclaim_rent_conservation_planted.out"; then | |
| echo "reclaim_rent_conservation planted reported a violation (expected)." | |
| exit 0 | |
| elif [ "$rc" -ne 0 ]; then | |
| echo "reclaim_rent_conservation planted exited non-zero ($rc); treating as detected violation." | |
| exit 0 | |
| else | |
| echo "::error::reclaim_rent_conservation planted ran clean — the dropped write_authority constraint was not surfaced within ${CRUCIBLE_TIMEOUT}s." | |
| exit 1 | |
| fi | |
| # ----------------------------------------------------------------- | |
| # Aggregate scorecards from real CI output. Renders findings/*/ | |
| # markdown the same way cf-invariants-jito does — diff-friendly | |
| # ANSI-stripped raw output + summary. | |
| # ----------------------------------------------------------------- | |
| - name: Capture scorecards from real CI output | |
| if: always() | |
| run: | | |
| TC="rustc $(rustc --version | awk '{print $2}'), solana $(solana --version | awk '{print $2}'), platform-tools ${SOLANA_TOOLS_VERSION}, crucible v0.2.0 (anchor-lang 1.0.1)" | |
| STAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| render_scorecard() { | |
| local inv="$1" variant="$2" total="$3" violated="$4" class="$5" | |
| local out_dir="findings/${inv}_${variant}" | |
| local raw="$GITHUB_WORKSPACE/ci-out/${inv}_${variant}.out" | |
| mkdir -p "$out_dir" | |
| { | |
| echo "# cf-invariants-pyth scorecard (CI capture, ${inv} ${variant} variant)" | |
| echo | |
| echo "Captured by \`.github/workflows/ci.yml\` on ${STAMP}." | |
| echo "Toolchain: ${TC}." | |
| echo "Crucible timeout: ${CRUCIBLE_TIMEOUT}s." | |
| echo "Invariant class: \`${class}\`." | |
| echo | |
| echo "## Summary" | |
| echo | |
| echo "- Invariants total: **${total}**" | |
| echo "- Invariants violated: **${violated}**" | |
| echo "- AI-suggested invariants in this run: **0**" | |
| echo "- Crucible version: \`0.2.0\`" | |
| echo | |
| echo "## Raw output" | |
| echo | |
| echo '```' | |
| if [ -f "$raw" ]; then | |
| sed -E 's/\x1b\[[0-9;]*[A-Za-z]//g' "$raw" | |
| else | |
| echo "(no output captured)" | |
| fi | |
| echo '```' | |
| } > "$out_dir/scorecard.md" | |
| } | |
| render_scorecard "two_step_governance" "clean" "1" "0" "two_step_governance" | |
| render_scorecard "two_step_governance" "planted" "1" ">=1" "two_step_governance" | |
| render_scorecard "reclaim_rent_conservation" "clean" "1" "0" "reclaim_rent_conservation" | |
| render_scorecard "reclaim_rent_conservation" "planted" "1" ">=1" "reclaim_rent_conservation" | |
| - name: Upload scorecards + raw output as artifact | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: crucible-scorecards | |
| path: | | |
| ci-out/*.out | |
| findings/two_step_governance_clean/scorecard.md | |
| findings/two_step_governance_planted/scorecard.md | |
| findings/reclaim_rent_conservation_clean/scorecard.md | |
| findings/reclaim_rent_conservation_planted/scorecard.md |