-
Notifications
You must be signed in to change notification settings - Fork 87
test(futarchy): add fuzz testing suite #496
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
hyckomatej
wants to merge
2
commits into
metaDAOproject:pileks/mini-instructions-fixes
Choose a base branch
from
accretion-xyz:futarchy-fuzzing
base: pileks/mini-instructions-fixes
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| name: Futarchy fuzz | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| sequences: | ||
| description: Number of independent fuzz sequences per worker | ||
| required: true | ||
| default: "500" | ||
| type: string | ||
| flows: | ||
| description: Number of flows executed in each sequence | ||
| required: true | ||
| default: "500" | ||
| type: string | ||
| seed: | ||
| description: Optional base seed for reproducing a previous run | ||
| required: false | ||
| default: "" | ||
| type: string | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: futarchy-fuzz-${{ github.ref }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| fuzz: | ||
| runs-on: ubuntu-22.04 | ||
| timeout-minutes: 300 | ||
|
|
||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Prepare run artifact | ||
| shell: bash | ||
| env: | ||
| FUZZ_SEQUENCES: ${{ inputs.sequences }} | ||
| FUZZ_FLOWS: ${{ inputs.flows }} | ||
| FUZZ_SEED: ${{ inputs.seed }} | ||
| run: | | ||
| mkdir -p fuzz-artifacts | ||
| { | ||
| echo "commit=${GITHUB_SHA}" | ||
| echo "sequences=${FUZZ_SEQUENCES}" | ||
| echo "flows=${FUZZ_FLOWS}" | ||
| if [ -n "$FUZZ_SEED" ]; then | ||
| echo "seed=${FUZZ_SEED}" | ||
| else | ||
| echo "seed=<generated by Wake.sol; see fuzz.log>" | ||
| fi | ||
| } > fuzz-artifacts/run-manifest.txt | ||
|
|
||
| - name: Validate campaign inputs | ||
| shell: bash | ||
| env: | ||
| FUZZ_SEQUENCES: ${{ inputs.sequences }} | ||
| FUZZ_FLOWS: ${{ inputs.flows }} | ||
| run: | | ||
| if ! [[ "$FUZZ_SEQUENCES" =~ ^[1-9][0-9]*$ ]]; then | ||
| echo "::error::sequences must be a positive integer" | ||
| exit 1 | ||
| fi | ||
| if ! [[ "$FUZZ_FLOWS" =~ ^[1-9][0-9]*$ ]]; then | ||
| echo "::error::flows must be a positive integer" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Install system dependencies | ||
| run: | | ||
| sudo apt-get update | ||
| sudo apt-get install --yes --no-install-recommends \ | ||
| build-essential \ | ||
| libssl-dev \ | ||
| libudev-dev \ | ||
| pkg-config \ | ||
| python3-dev | ||
|
|
||
| - name: Install Node.js, Solana, and Anchor | ||
| uses: metadaoproject/setup-anchor@aeeb5505f3fd3d52a1080d14863c6aa428c9fdf9 # v3.4 | ||
| with: | ||
| node-version: "20.18.0" | ||
| solana-cli-version: "1.17.34" | ||
| anchor-version: "0.29.0" | ||
|
|
||
| - name: Install Rust toolchain | ||
| run: | | ||
| rustup toolchain install 1.89.0 --profile minimal | ||
| rustup default 1.89.0 | ||
|
|
||
| - name: Restore build cache | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: | | ||
| ~/.cargo/registry/index/ | ||
| ~/.cargo/registry/cache/ | ||
| ~/.cargo/git/db/ | ||
| ~/.cache/uv/ | ||
| target/ | ||
| ${{ runner.temp }}/wake-sol/target/ | ||
| key: futarchy-fuzz-build-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}-${{ github.run_id }}-${{ github.run_attempt }} | ||
| restore-keys: | | ||
| futarchy-fuzz-build-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}- | ||
| futarchy-fuzz-build-${{ runner.os }}- | ||
|
|
||
| - name: Build Wake.sol | ||
| run: | | ||
| python3 -m pip install --user "uv==0.8.14" | ||
|
|
||
| wake_dir="${RUNNER_TEMP}/wake-sol" | ||
| git init "$wake_dir" | ||
| git -C "$wake_dir" remote add origin https://github.com/ack3-ai/wake.sol.git | ||
| git -C "$wake_dir" fetch --depth 1 origin 096d5bfed511d0f0a3e27c960468a756a6c6440b | ||
| git -C "$wake_dir" checkout --detach FETCH_HEAD | ||
| test "$(git -C "$wake_dir" rev-parse HEAD)" = \ | ||
| "096d5bfed511d0f0a3e27c960468a756a6c6440b" | ||
|
|
||
| cd "$wake_dir" | ||
| "$HOME/.local/bin/uv" sync --frozen --no-install-project | ||
| "$HOME/.local/bin/uv" pip install "maturin[patchelf]==1.9.4" | ||
| "$HOME/.local/bin/uv" run --no-sync maturin develop --release --locked | ||
| .venv/bin/python -c "import wake_sol" | ||
|
|
||
| - name: Build Futarchy | ||
| run: | | ||
| anchor build -p futarchy -- -- --locked | ||
| test -s target/deploy/futarchy.so | ||
| test -s verifiable-builds/conditional_vault.so | ||
| test -s tests/fixtures/squads_multisig.so | ||
| test -s tests/fixtures/squads-program-config | ||
|
|
||
| - name: Verify generated bindings | ||
| run: | | ||
| wake_python="${RUNNER_TEMP}/wake-sol/.venv/bin/python" | ||
| wake_cli="${RUNNER_TEMP}/wake-sol/.venv/bin/wake-sol" | ||
| "$wake_python" fuzz/futarchy/gen_wake_idl.py | ||
| "$wake_cli" gen \ | ||
| --target-idl target/wake-idl \ | ||
| --out fuzz/futarchy/pytypes \ | ||
| --only FUTARELBfJfQ8RDGhg1wdhddq1odMAJUePHFuBYfUxKq \ | ||
| --check \ | ||
| --strict | ||
| "$wake_python" -c "import fuzz.futarchy.pytypes.futarchy" | ||
|
|
||
| - name: Run fuzz campaign | ||
| shell: bash | ||
| env: | ||
| FUZZ_SEQUENCES: ${{ inputs.sequences }} | ||
| FUZZ_FLOWS: ${{ inputs.flows }} | ||
| FUZZ_SEED: ${{ inputs.seed }} | ||
| run: | | ||
| wake_python="${RUNNER_TEMP}/wake-sol/.venv/bin/python" | ||
| wake_cli="${RUNNER_TEMP}/wake-sol/.venv/bin/wake-sol" | ||
| if [ -n "$FUZZ_SEED" ]; then | ||
| test_args=( | ||
| "$wake_python" -m pytest -q -s | ||
| --seed "$FUZZ_SEED" | ||
| fuzz/futarchy/test_fuzz.py | ||
| ) | ||
| echo "mode=single-process seed replay" >> fuzz-artifacts/run-manifest.txt | ||
| else | ||
| test_args=( | ||
| "$wake_cli" test -P -q | ||
| fuzz/futarchy/test_fuzz.py | ||
| ) | ||
| echo "mode=parallel, one worker per CPU" >> fuzz-artifacts/run-manifest.txt | ||
| fi | ||
|
|
||
| set +e | ||
| "${test_args[@]}" 2>&1 | tee fuzz-artifacts/fuzz.log | ||
| fuzz_status=${PIPESTATUS[0]} | ||
| set -e | ||
|
|
||
| if [ -d .wake-sol/logs ]; then | ||
| mkdir -p fuzz-artifacts/wake-logs | ||
| cp -R .wake-sol/logs/. fuzz-artifacts/wake-logs/ | ||
| fi | ||
| echo "exit_status=${fuzz_status}" >> fuzz-artifacts/run-manifest.txt | ||
| exit "$fuzz_status" | ||
|
|
||
| - name: Upload fuzz results | ||
| if: ${{ always() }} | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: futarchy-fuzz-${{ github.run_id }}-${{ github.run_attempt }} | ||
| path: fuzz-artifacts/ | ||
| if-no-files-found: error | ||
| retention-days: 14 | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| # Futarchy Fuzz Test | ||
|
|
||
| This directory contains the Wake.sol stateful fuzz test for the Futarchy | ||
| program at commit `658c342c2f1ea11917d5fed10539d078e9e1a3b7`. It has 29 | ||
| instruction-level happy paths, 29 unhappy paths, two support flows, and twelve | ||
| global invariants. Each instruction wrapper also checks its | ||
| own postconditions or atomic rollback behavior. | ||
|
|
||
| ## Running the test | ||
|
|
||
| 1. Install [Wake.sol](https://github.com/ack3-ai/wake.sol) in a Python virtual | ||
| environment at commit | ||
| `096d5bfed511d0f0a3e27c960468a756a6c6440b`. | ||
|
|
||
| 2. Activate the environment. | ||
|
|
||
| 3. From this repository's root, build the Futarchy program: | ||
|
|
||
| ```bash | ||
| anchor build -p futarchy | ||
| ``` | ||
|
|
||
| 4. Create Wake's compatible IDL and regenerate the checked-in Python bindings: | ||
|
|
||
| ```bash | ||
| python fuzz/futarchy/gen_wake_idl.py | ||
| wake-sol gen \ | ||
| --target-idl target/wake-idl \ | ||
| --out fuzz/futarchy/pytypes \ | ||
| --only FUTARELBfJfQ8RDGhg1wdhddq1odMAJUePHFuBYfUxKq \ | ||
| --strict | ||
| ``` | ||
|
|
||
| 5. Ensure the built Futarchy artifact and checked-in dependencies are present: | ||
|
|
||
| ```text | ||
| target/deploy/futarchy.so | ||
| verifiable-builds/conditional_vault.so | ||
| tests/fixtures/squads_multisig.so | ||
| tests/fixtures/squads-program-config | ||
| ``` | ||
|
|
||
| 6. Run the default 100-sequence, 500-flow campaign: | ||
|
|
||
| ```bash | ||
| python -m pytest -q -s fuzz/futarchy/test_fuzz.py | ||
| ``` | ||
|
|
||
| Use `FUZZ_SEQUENCES` and `FUZZ_FLOWS` to change the number of sequences and | ||
| flows for local or pipeline runs. | ||
|
|
||
| The test prints a base seed. Reproduce that run with: | ||
|
|
||
| ```bash | ||
| BASE_SEED="<printed-base-seed>" | ||
| python -m pytest -q -s --seed "$BASE_SEED" fuzz/futarchy/test_fuzz.py | ||
| ``` | ||
|
|
||
| Failures and their reproducible crash data are written under | ||
| `.wake-sol/logs/crashes/`. | ||
|
|
||
| ### GitHub Actions | ||
|
|
||
| The `Futarchy fuzz` workflow runs only when manually started from the repository's | ||
| Actions page. Choose **Run workflow** and set the sequence and flow counts. Leave | ||
| the seed empty for a new campaign, or enter a previously printed base seed to | ||
| reproduce a run. | ||
|
|
||
| For a new campaign, the workflow starts one worker per available CPU. The | ||
| default is 500 sequences of 500 flows per worker, and both values can be changed | ||
| when starting the workflow. Providing a seed runs an exact single-process replay | ||
| instead. The workflow has a five-hour timeout and uploads the run log, manifest, | ||
| and any Wake crash data as a | ||
| `futarchy-fuzz-<run>-<attempt>` artifact retained for 14 days. | ||
|
|
||
| ## Flows | ||
|
|
||
| Every instruction name below has a `<name>_happy` flow for a valid transition | ||
| and a `<name>_unhappy` flow for a small expected-failure case. | ||
|
|
||
| | Area | Instruction flows | | ||
| | --- | --- | | ||
| | DAO creation | `initialize_dao` | | ||
| | Proposal creation | `initialize_proposal`, `initialize_large_spend_proposal`, `initialize_mint_tokens_proposal`, `initialize_spending_limit_change_proposal`, `initialize_hostile_takeover_proposal`, `initialize_hostile_liquidate_proposal`, `initialize_buyback_token_proposal` | | ||
| | Proposal lifecycle | `stake_to_proposal`, `unstake_from_proposal`, `sponsor_proposal`, `launch_proposal`, `finalize_proposal` | | ||
| | AMM | `provide_liquidity`, `withdraw_liquidity`, `spot_swap`, `conditional_swap`, `collect_fees` | | ||
| | DAO configuration | `update_dao`, `set_spending_limit`, `sync_spending_limit` | | ||
| | Squads administration | `admin_enqueue_multisig_proposal_approval`, `execute_multisig_proposal_approval`, `admin_enqueue_multisig_proposal_cancellation`, `execute_multisig_proposal_cancellation`, `admin_execute_multisig_proposal` | | ||
| | Proposal administration | `admin_cancel_proposal`, `admin_remove_proposal`, `admin_update_proposal_params` | | ||
|
|
||
| Two additional flows support those instruction flows: | ||
|
|
||
| - `advance_clock` moves the deterministic clock across proposal and TWAP | ||
| boundaries. | ||
| - `execute_passed_proposal_payload` executes approved proposal payloads through | ||
| Squads. | ||
|
|
||
| Happy flows check their instruction's account and token transitions. Unhappy | ||
| flows require the intended error and verify that writable accounts roll back. | ||
|
|
||
| ## Global invariants | ||
|
|
||
| | Invariant | Property checked after every flow | | ||
| | --- | --- | | ||
| | `dao_identity_is_canonical` | The DAO PDA, owner, mints, Squads accounts, and AMM vaults remain canonical. | | ||
| | `dao_configuration_stays_valid` | Mutable DAO configuration remains inside the program's valid bounds. | | ||
| | `underlying_token_supplies_are_conserved` | All base and quote tokens remain accounted for. | | ||
| | `spot_reserves_and_fees_are_fully_backed` | Spot vault balances equal reserves plus protocol fees. | | ||
| | `conditional_reserves_and_fees_are_fully_backed` | In Futarchy state, each outcome's spot-plus-conditional reserves and fees equal underlying vault tokens plus that outcome's vault tokens. | | ||
| | `conditional_token_supplies_are_backed` | Both conditional-token supplies are accounted for and collateral covers unresolved or resolved claims. | | ||
| | `positions_sum_to_total_liquidity` | Canonical AMM positions sum to total liquidity. | | ||
| | `proposal_account_graphs_are_canonical` | Proposal, market, vault, and conditional-mint links remain canonical. | | ||
| | `proposal_stake_custody_is_conserved` | Stake records equal proposal custody balances. | | ||
| | `enqueued_approvals_are_canonical` | Live Squads approval records have the correct PDA and transaction index. | | ||
| | `enqueued_cancellations_are_canonical` | Live Squads cancellation records have the correct PDA and transaction index. | | ||
| | `cancelled_transactions_never_execute` | Cancelled Squads proposals remain cancelled and never execute. | |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Wake.sol fuzz harness packages for workspace programs.""" |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Wake.sol fuzz harness for Futarchy.""" |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cache key includes both
github.run_idandgithub.run_attempt, so every invocation creates a distinct cache even whenCargo.lockis unchanged. GitHub caches are immutable: the restore prefix can reuse an older entry, but the workflow will still save another copy of the Cargo, build, and Wake artifacts. Repeated fuzz runs will consume the cache quota and eventually evict useful entries. A stable, content-based key would allow these build caches to be reused without creating a duplicate for every run.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!