Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions evals/solana-cu-minimalism/trigger-eval.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{"query": "I just added a new instruction to my anchor program that transfers from a vault to multiple recipients in a loop, can you review it for compute unit usage?", "should_trigger": true},
{"query": "this anchor escrow program keeps hitting ComputeBudgetExceeded on devnet, here's the handler in programs/escrow/src/lib.rs, what's eating the CU", "should_trigger": true},
{"query": "can you write the deposit instruction for my pinocchio vault program, accounts are vault_pda, depositor, system_program", "should_trigger": true},
{"query": "just ran /profile-cu and the close_pool instruction is 45k CU, way more than I expected, can you take a look", "should_trigger": true},
{"query": "hey can u help cut down cu on my anchor ix it keeps failing on cu limit lol", "should_trigger": true},
{"query": "I'm about to implement a new instruction in my Anchor program called settle_auction that iterates over up to 20 bidders and refunds losers via CPI to the token program, can you write it?", "should_trigger": true},
{"query": "optimize my program, it's the staking vault from earlier in this chat", "should_trigger": true},
{"query": "can you make this account struct zero-copy instead of a regular Account, it's getting big", "should_trigger": true},
{"query": "can you optimize this React component's re-renders, it's part of our Solana dapp's frontend", "should_trigger": false},
{"query": "what's the gas cost of an Ethereum CALL opcode vs a Solana CPI, just curious how they compare conceptually", "should_trigger": false},
{"query": "write a script to deploy my anchor program to devnet", "should_trigger": false},
{"query": "explain what compute units are on Solana, I'm new to this", "should_trigger": false},
{"query": "rust vec push performance, is it amortized O(1)? this isn't for a Solana project", "should_trigger": false},
{"query": "convert this @solana/web3.js call to @solana/kit in my Next.js app", "should_trigger": false},
{"query": "audit my anchor program for security vulnerabilities, not performance, just security", "should_trigger": false},
{"query": "what does cu mean, my friend texted me 'cu later' and idk what it means", "should_trigger": false}
]
96 changes: 96 additions & 0 deletions solana-cu-minimalism/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
name: solana-cu-minimalism
description: Applies a compute-unit (CU) minimalism checklist when writing or modifying Solana on-chain program code (Anchor or Pinocchio, Rust), and when reviewing existing instruction handlers specifically for compute-unit efficiency. Use whenever the user asks to implement a new Solana instruction handler, account struct, or CPI call; whenever they mention "compute units", "CU budget", "optimize my program", "reduce CU usage", "CU profiling", "ComputeBudgetExceeded", or run commands like /profile-cu or /benchmark; and proactively any time you are about to write a new on-chain instruction handler in a Rust Solana program, even if the user didn't ask for optimization explicitly. Do NOT use for frontend, off-chain client code, non-Solana Rust, deployment scripts, conceptual/comparative questions about CPIs or compute costs that aren't about actual program code, or security-only audits that don't mention performance/CU (use the kit's security skills for those).
license: MIT
compatibility: Requires file access to a Rust Solana program (Anchor or Pinocchio). Standalone-usable, but designed to pair with solana-ai-kit's /profile-cu and /benchmark commands (for measuring the actual before/after CU delta) and its solana-qa-engineer agent (for verifying nothing load-bearing was cut).
metadata:
version: 0.1.0
category: solana-dev
tags: [solana, anchor, pinocchio, compute-units, performance, rust]
---

# Solana CU Minimalism

## Why this matters

Code that compiles and passes tests can still burn far more compute units than it needs to. The instincts that make code "look careful" in most languages — re-validating things that are already validated, reaching for `Vec`/`String` by default, making one cross-program call per loop iteration, logging liberally — are exactly the habits that are expensive on Solana's compute budget. This isn't a style preference; on Solana it's the difference between an instruction that lands and one that fails with `ComputeBudgetExceeded`.

This skill is a checklist to run while writing or reviewing an instruction handler — not a separate audit pass bolted on afterward.

## Critical: this is about removing redundant cost, never about removing safety

Before anything else: account-ownership checks, signer checks, arithmetic overflow checks, and PDA/seed validation are never on the table for removal. If a rung below looks like it would cut one of those to save CU, skip that rung. The goal is removing *redundant or wasteful* cost, not removing *correctness*.

## The ladder

Work down this list before considering a handler done. Stop at the first rung that doesn't apply — don't force every rung onto every handler.

1. **Trust your declarative constraints.** If you're using Anchor's `#[account(...)]` constraints (`has_one`, `constraint =`, `owner =`, etc.), don't also hand-write the same check again in the handler body. The constraint already compiles to that check; a duplicate costs CU twice for zero added safety.
2. **Deserialize each account once.** Read an account's data into a struct or reference a single time per instruction. Re-borrowing or re-deserializing the same account later in the same handler is wasted CU for data you already have in hand.
3. **Prefer fixed-size / zero-copy over heap allocation in hot paths.** `Vec<T>` and `String` involve heap allocation and dynamic Borsh (de)serialization. Where the size is known and bounded, a fixed-size array or a `#[account(zero_copy)]` struct avoids that cost — and avoids exhausting the default heap entirely on data-heavy instructions.
4. **Minimize the number of CPIs, not just their size.** Every cross-program invocation carries fixed overhead on top of whatever the called program does. If a loop is calling another program once per iteration, look for a batched form of that call before accepting "one CPI per item" as final.
5. **Keep logging out of hot paths.** `msg!`/`sol_log` calls are not free. They're fine for one-time errors or debug builds, but they should never sit inside a loop, and should usually be stripped from the path that runs on every successful call once the handler is verified working.
6. **Push pure computation off-chain where trust allows it.** If something can be computed off-chain and verified on-chain cheaply (e.g., a Merkle proof, a sort done client-side and merely checked), do that instead of recomputing it on-chain. Only compute on-chain what must be trustlessly recomputed there.
7. **If none of the above apply, stop.** A handler that's already minimal doesn't need invented changes to look like work was done. Say so plainly.

## Workflow

1. Read the surrounding instruction and account context before changing anything — the ladder runs *after* you understand the data flow, not instead of it.
2. Write or modify the handler normally, then walk it down the ladder above.
3. If the host environment has a CU profiling command available (e.g. solana-ai-kit's `/profile-cu` or `/benchmark`), run it before and after your change and report the actual CU delta — don't just claim a saving, measure it.
4. If no profiling command is available, say explicitly which rungs you applied and why, so the user (or `solana-qa-engineer`, if present) can verify independently rather than taking the claim on faith.
5. Never trade a correctness or security check for a CU saving. If you're unsure whether something is redundant or load-bearing, read `references/compute-budget-basics.md` before deciding — and when still unsure, leave it in and say why.

## Examples

**Example 1 — redundant manual check on top of an Anchor constraint**

Before:
```rust
#[account(has_one = authority)]
pub vault: Account<'info, Vault>,
...
// inside the handler body
if ctx.accounts.vault.authority != ctx.accounts.authority.key() {
return Err(ErrorCode::Unauthorized.into());
}
```
After:
```rust
#[account(has_one = authority)]
pub vault: Account<'info, Vault>,
// the has_one constraint already enforces this — no handler-body check needed
```
Why: `has_one` already performs this exact comparison during account validation, before the handler body runs. The manual check duplicates it for no added safety.

**Example 2 — heap-allocated list where the size is bounded and known**

Before:
```rust
#[account]
pub struct Pool {
pub participants: Vec<Pubkey>, // grows unbounded, Borsh-serialized each write
}
```
After:
```rust
#[account(zero_copy)]
pub struct Pool {
pub participants: [Pubkey; MAX_PARTICIPANTS], // fixed size, no heap, no realloc
pub participant_count: u32,
}
```
Why: if the pool has a known maximum size, a fixed array avoids heap allocation, avoids the cost of re-serializing a growing `Vec` on every write, and avoids needing account reallocation logic entirely.

## Troubleshooting

**"There's no Solana program in context."** Don't apply this skill speculatively to non-Solana Rust or to frontend code — say it doesn't apply rather than guessing.

**"The handler already follows the ladder."** Say so. Don't manufacture changes to appear productive; a clean bill of health is a valid and useful answer.

**"Applying a rung would require cutting a validation check."** Don't. Stop at that rung, leave the check in, and tell the user which check you preserved and why it's load-bearing rather than redundant.

## References

- `references/cpi-and-account-patterns.md` — deeper patterns for batching CPIs and structuring zero-copy accounts
- `references/compute-budget-basics.md` — how to tell redundant validation from load-bearing validation, and where to verify current compute-budget limits (these change between Solana versions, so this skill points you to check rather than hard-coding numbers that may be stale)
23 changes: 23 additions & 0 deletions solana-cu-minimalism/references/compute-budget-basics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Compute budget basics — and the redundant-vs-load-bearing line

## Telling redundant from load-bearing

A check is **redundant** (safe to remove) only if something else in the same transaction, enforced *before* the handler body runs, already guarantees the same thing. The clearest case is an Anchor account constraint (`has_one`, `constraint =`, `owner =`, `signer`) duplicated by a manual `if` in the handler body — the constraint runs during account validation, before your code executes, so the manual copy is provably redundant.

A check is **load-bearing** (never remove) if:
- It's the *only* place that property is enforced (e.g., a manual check because the constraint system can't express this particular invariant)
- It guards against integer overflow/underflow, even if "it probably won't happen in practice"
- It validates a PDA's seeds/bump, a signer, or account ownership, and isn't already covered by an Anchor constraint
- You're not sure — in which case, treat it as load-bearing until you can confirm otherwise

When in doubt, leave it in and say explicitly: "I left this check in because I couldn't confirm it's covered elsewhere — worth a second look from whoever owns this program's security review."

## Compute budget limits — verify against current docs, don't trust hardcoded numbers

Solana's default and maximum compute-unit limits per transaction, and the default heap size, have changed across Solana versions and are the kind of fact that goes stale fast. Rather than hardcoding specific numbers here that might be wrong by the time you read this:

- If solana-ai-kit's `ext/solana-dev` skill (Solana Foundation's official dev skill) is available, check there first — it's kept current with the Solana Foundation's own docs.
- Otherwise, check the current Solana docs directly (search for "Solana compute budget" or "ComputeBudgetProgram") before stating a specific CU limit or heap size as fact to the user.
- If you need to request a higher compute-unit limit or additional heap frame for a transaction, that's done via `ComputeBudgetProgram` instructions (`set_compute_unit_limit`, `request_heap_frame`) added to the transaction — but confirm current syntax against the docs rather than assuming it's unchanged.

This skill's job is to keep instruction-level CU usage minimal in the first place, not to be the source of truth for the network's current budget parameters.
27 changes: 27 additions & 0 deletions solana-cu-minimalism/references/cpi-and-account-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# CPI and account patterns

Loaded when the ladder's rungs 3 or 4 need more detail than fits in SKILL.md.

## Batching CPIs

If a handler calls another program once per item in a loop (e.g., transferring to N recipients, closing N accounts), check the called program's interface for a batched instruction before accepting "one CPI per item":

- Many SPL-style programs expose both a single-item and a batched form. Prefer the batched form if it exists.
- If no batched form exists and you control the called program too, consider whether the operation belongs inside this program instead of behind a CPI at all — collapsing two programs' worth of fixed overhead into one.
- If neither is possible, that's the load-bearing cost of calling an external program N times — accept it and move on rather than working around it with something less safe (e.g., skipping per-item validation to "save time").

## Zero-copy account design

`#[account(zero_copy)]` accounts are read in place rather than deserialized into a fresh struct, which avoids both the heap allocation and the copy. Tradeoffs to know before reaching for it:

- Zero-copy structs must be `#[repr(C)]` and have a fixed, predictable layout — no `Vec`, `String`, `Option<T>` with variable layout, or other dynamically-sized fields inside them.
- They're most worth it for accounts that are large, read/written frequently, or both. For a small, rarely-touched account, the complexity of zero-copy may not be worth it over a normal `Account<'info, T>` — this is a judgment call, not a rule to apply everywhere.
- Mutating a zero-copy account requires `load_mut()` and respecting Rust's borrow rules directly (no implicit clone-on-read) — read the surrounding code's existing zero-copy usage (if any) before introducing a new pattern that doesn't match it.

## Fixed-size arrays vs. Vec

When replacing a `Vec<T>` with a fixed-size array:

- Pick the bound from the actual product requirement (e.g., "this pool supports up to 50 participants"), not an arbitrary round number — and add a comment explaining where the bound came from, so a future reader doesn't assume it's load-bearing for a different reason.
- You'll usually need a separate `count: u32` (or similar) field to track how many of the array's slots are actually in use, since the array itself doesn't shrink/grow.
- If the bound genuinely can't be known ahead of time (truly unbounded growth), a fixed array is the wrong tool — don't force it. Say so and keep the `Vec`, noting the CU/heap tradeoff explicitly instead of silently leaving it unaddressed.