The AI invariant-author for Crucible and Trident on Solana / Anchor.
cf-invariants-anchor is a sidecar to the two open invariant-fuzzing harnesses for Solana programs:
- Crucible (Asymmetric Research, MIT, LibAFL + LiteSVM, v0.2.0) — primary emit target.
- Trident (Ackee Blockchain, MIT) — secondary emit target (staged for Phase 1).
We do not rebuild the harness. Crucible and Trident already own
the LiteSVM execution rails and the IDL-driven program-fuzzing
plumbing. The unclaimed quadrant — and the only thing this crate
ships — is the AI-suggested invariant author that sits on top
of them: ingest an Anchor IDL, propose ranked candidate invariants
from a class library (balance conservation in Phase 0; monotonicity,
access-control, oracle-freshness on the roadmap), and emit a
ready-to-run #[fuzz_fixture] + #[invariant_test] source file
for Crucible.
This package is the Anchor sibling of the Cairo-targeted cf-invariants (Starknet / snforge) shipped by the same operator.
Phase 0 — working artifact, pre-1.0.
Source of truth: the CI badge above. GitHub Actions builds the workspace, builds all three reference program pairs via
cargo build-sbf, and runs the Crucible v0.2.0 harness on every (class × clean/planted) cell on every push — asserting 0 violations on each clean variant and ≥1 violation on each planted variant. The captured scorecards from each run are uploaded as a CI artifact and committed intofindings/{vault_ref,counter_ref,admin_ref}_{clean,planted}/scorecard.mdonce green. The.expected.{json,md}siblings remain as the authored reference; the unsuffixed files are the real captures.Pairs currently proven in CI:
vault_ref/vault_ref_planted— classbalance_conservationcounter_ref/counter_ref_planted— classmonotonic_accountingadmin_ref/admin_ref_planted— classaccess_controlNo "verified" claim in this repo is made by hand — if the badge is red, the artifact is not green. See
findings/LIVE_VALIDATION_PENDING.mdfor the per-class validation status.
What is live in this build:
cf-invariants-anchor ingest <idl>— parses Anchor 0.30 / Codama-style IDL JSON into a typed contract surface (program id, instructions, scalar balance-bearing fields per stored account).cf-invariants-anchor suggest <idl>— produces a ranked list of candidate invariants from the heuristic suggester. Three classes ship today:balance_conservation,monotonic_accounting,access_control. TheInvariantClasstrait +ClassRegistrykeep adding a fourth (oracle/freshness) a one-file change.cf-invariants-anchor suggest <idl> --ai— routes throughcf-invariants-anchor-aifor AI-suggested candidates. Every returned candidate carriesInvariantSource::AiSuggested { model, prompt_version, timestamp_utc }; a JSON audit-log entry is written to.cf-invariants-anchor/ai-log/<timestamp>.json(token counts, cost in USD, SHA-256 of response). Default transport isMockTransport(deterministic, no API key, CI-safe);LiveAnthropicTransportactivates when the CLI is built with--features live-aiANDCF_INVARIANTS_ANCHOR_AI_LIVE=1ANDANTHROPIC_API_KEYset.cf-invariants-anchor emit <idl> --target crucible— renders a Crucible-compatible fuzz fixture for the selected candidate. Each class has its own emit shape: balance uses a fixture-side ledger +fuzz_assert_eq!; monotonic uses alast_seen_*snapshot +fuzz_assert_le!; access-control uses an attacker-keypair probe + sticky-flag assertion.--target trident— Phase-1 stub. Emit returns an explanatory placeholder so the CLI surface is reachable without the Trident rendering being wired.- Three reference contract pairs, one per shipping class:
references/vault_ref{,_planted}—balance_conservation. The planted withdraw transfersamountlamports but decrementsvault.amountbyamount-1; the conservation invariant catches the drift in 1 withdraw.references/counter_ref{,_planted}—monotonic_accounting. Adds alifetime_deposited: u64ratchet field; the planted withdraw decrements it on every call, regressing the lifetime counter.references/admin_ref{,_planted}—access_control. Clean withdraw enforcesseeds = [b"vault", depositor.key().as_ref()]ANDhas_one = depositor; the planted variant drops both, so any signer can drain any vault PDA. The emitted attacker-probe fixture trips on the first successful unauthorized withdraw.
- Scorecard renderer with the AI-disclosure banner emitted
whenever
ai_suggestions_included > 0. The default reference run uses the heuristic source so the banner is dormant on those scorecards (the renderer pathway is exercised by tests); the--aiflag fires the banner. - Workspace test suite (36 tests,
cargo test --workspace).
- Not a fork of Crucible or Trident. Both ship as upstream dependencies; cf-invariants-anchor only authors invariants.
- Not a formal-verification tool. Randomized invariant search, not proofs.
- Not a replacement for hand-authored invariants. AI / heuristic
candidates are always labeled
UNVERIFIEDuntil the contract author accepts them.
cf-invariants-anchor is a Cargo workspace of five Rust crates:
crates/
cf-invariants-anchor-cli/ # binary
cf-invariants-anchor-core/ # shared types: ContractSurface, InvariantCandidate, Scorecard
cf-invariants-anchor-idl/ # Anchor IDL parser → ContractSurface
cf-invariants-anchor-suggest/ # ClassRegistry + balance_conservation suggester
cf-invariants-anchor-emit/ # Render to Crucible / Trident-stub source
cf-invariants-anchor-report/ # Scorecard markdown + JSON renderer
references/
vault_ref{,_planted}/ # balance_conservation pair
counter_ref{,_planted}/ # monotonic_accounting pair
admin_ref{,_planted}/ # access_control pair
findings/
vault_ref_{clean,planted}/ # scorecard.expected.md + CI capture
counter_ref_{clean,planted}/ # scorecard.expected.md + CI capture
admin_ref_{clean,planted}/ # scorecard.expected.md + CI capture
docs/
architecture.md # design, emit-target abstraction, Crucible API record
ai-disclosure.md # AI involvement, disclosure path, audit log
scripts/
run_phase0_harness.sh # reproduce-from-clone driver
prompts/
invariant_suggestion_v1.txt # versioned prompt (Phase 1 AI path)
These are the versions CI builds against on every push (see
.github/workflows/ci.yml). All version
pins were empirically verified against the upstream tag's Cargo.toml,
not eyeballed from code shape:
- Rust stable (workspace MSRV:
1.79). anchor-lang1.0.1 — matches Crucible v0.2.0's workspace (asymmetric-research/crucible @ v0.2.0pinsanchor-lang = "1.0.1").- Anza / Solana CLI v2.1.21 for
cargo-build-sbf. - Solana platform-tools v1.52 (Crucible v0.2.0 deps require
edition2024 support; earlier platform-tools ship rustc 1.84 which
cannot build them — passed as
--tools-version v1.52). - Upstream Crucible v0.2.0 built from source in CI
(
cargo install --path crates/crucible-fuzz-cli).
The fuzz Cargo.tomls reference Crucible via path dep at
../../../../../crucible/crates/crucible-fuzzer — CI clones Crucible
to <repo-root>/../crucible so this resolves. For local reproduction
either clone Crucible to that path, or vendor the two crates and edit
the fuzz Cargo.tomls.
See .tool-versions (informational; CI is authoritative).
cargo install --path crates/cf-invariants-anchor-cli# Ingest an Anchor IDL.
cf-invariants-anchor ingest references/vault_ref/idls/vault_ref.json
# Ask the suggester for ranked candidate invariants.
cf-invariants-anchor suggest references/vault_ref/idls/vault_ref.json
# Emit a Crucible #[fuzz_fixture] + #[invariant_test] file.
cf-invariants-anchor emit references/vault_ref/idls/vault_ref.json \
--target crucible \
--out references/vault_ref/fuzz/vault_ref/src/main.rsCI runs exactly this every push; local reproduction is optional. See
scripts/run_phase0_harness.sh for
the canonical driver.
# 1. Build the clean reference program + its planted twin (SBPF).
cargo build-sbf --tools-version v1.52 \
--manifest-path references/vault_ref/programs/vault_ref/Cargo.toml
cargo build-sbf --tools-version v1.52 \
--manifest-path references/vault_ref_planted/programs/vault_ref/Cargo.toml
# 2. Emit the conservation invariant (already pre-emitted in this repo).
cf-invariants-anchor emit references/vault_ref/idls/vault_ref.json \
--target crucible \
--out references/vault_ref/fuzz/vault_ref/src/main.rs
cp references/vault_ref/fuzz/vault_ref/src/main.rs \
references/vault_ref_planted/fuzz/vault_ref/src/main.rs
# 3. Run Crucible against both variants (timeout small enough for CI;
# the planted bug's minimal counterexample is 2 actions and fires fast).
(cd references/vault_ref/fuzz/vault_ref && \
crucible run vault_ref invariant_amount_conservation --release --timeout 30)
(cd references/vault_ref_planted/fuzz/vault_ref && \
crucible run vault_ref invariant_amount_conservation --release --timeout 30)The CI workflow captures the real output of these runs into
findings/vault_ref_{clean,planted}/scorecard.md and uploads them as
the crucible-scorecards artifact. The scorecard.expected.{json,md}
siblings remain as authored reference for diffing.
| Phase | Surface |
|---|---|
| 0 | IDL ingest → ranked balance-conservation candidates → Crucible emit → scorecard renderer. Heuristic suggester only. ✅ shipped. |
| 1 | AI-suggested invariants live (Anthropic Claude Sonnet path: MockTransport default, LiveAnthropicTransport behind --features live-ai + env). Monotonic + access-control classes added to the suggester + emit. ✅ shipped. |
| 2 (this build) | counter_ref (monotonicity) + admin_ref (access control) reference pairs with CI Crucible proof — each new class catches its planted bug under clean=0 / planted≥1. ✅ in-tree; CI-green gated on the next push. |
| 3 | Trident emit target. Oracle-freshness class. CI scorecard-drift early warning. Shrinking. Multi-account-state surface (full account-mutability-set analysis instead of name-heuristic). |
Open an issue on the GitHub repository, or contact team@caliperforge.com.
For sensitive security disclosures touching cf-invariants-anchor itself, contact michael@caliperforge.com directly.
Apache-2.0. See LICENSE.
cf-invariants-anchor is operated by Michael Moffett under the CaliperForge banner. CaliperForge is a sole-operator engineering studio.
Built with AI assistance. Authored and reviewed by Michael Moffett, operator at CaliperForge. Full policy at caliperforge.com/ai-disclosure. See docs/ai-disclosure.md for the in-repo detail on what the AI module does, the audit-log path, and how to disable it.