| name | optimize-validator |
| description | Optimize Aiken validators for lower execution costs and smaller script size. Covers CPU/memory reduction, script size, data structures, and benchmarking. Trigger: "optimize validator", "reduce script size", "lower execution cost", "reduce CPU", "reduce memory", "script too large", "transaction too expensive". |
| allowed-tools | Read Grep Glob |
Optimize Cardano Validator
Guide optimization of Aiken validators for lower execution costs (CPU/memory) and smaller compiled script size. Optimization should only be applied to validators that are already correct and tested.
When to use
- User has a working validator and wants to reduce transaction fees
- Script size exceeds limits or is unnecessarily large
- Execution units (CPU/memory) are higher than expected
- User wants to compare optimization strategies
- Before deploying a reference script (size directly affects storage deposit)
- Transaction is failing due to exceeding execution unit limits
When NOT to use
- User needs to write a new validator (use write-validator)
- User needs a security review (use review-contract)
- The validator has not been tested yet (correctness comes before performance)
- The optimization would remove a security check
Key principles
- Measure before optimizing: Use
aiken build to check script size and aiken bench to measure execution units. Without baseline numbers, you cannot know if changes helped.
- Script size and execution units are independent costs: A larger script is not necessarily more expensive to execute. Inlining functions increases size but can reduce CPU by avoiding closure allocation.
- CPU and memory budgets are separate: A transaction can fail by exceeding either limit independently. Identify which resource is the bottleneck before optimizing.
- The stdlib is well-optimized: Do not rewrite stdlib functions unless profiling shows they are a bottleneck. Custom implementations often end up larger and slower.
- Correctness over performance: Never sacrifice security checks for performance. Optimize how a check is performed, not whether it runs.
Workflow
Step 1: Establish baseline
Read the validator code and gather current metrics.
Suggest the user run:
aiken build
aiken bench
cat plutus.json | jq '.validators[] | {title, size: (.compiledCode | length / 2)}'
Note the following:
- Current script size in bytes (from
plutus.json or build output)
- Execution unit estimates per action (from
aiken bench)
- Which operations the validator performs (list traversals, value comparisons, datum deserialization)
Step 2: Search Bundled Documentation
Search the bundled documentation for relevant content:
${CLAUDE_SKILL_DIR}/../../docs/sources/aiken/ - Aiken language docs
${CLAUDE_SKILL_DIR}/../../docs/sources/aiken-stdlib/ - Aiken standard library docs
${CLAUDE_SKILL_DIR}/../../docs/sources/plutus/ - Plutus docs
Step 3: Identify expensive operations
Search the validator code for known cost centers:
High CPU cost:
- Nested list traversals (O(n*m) comparisons)
- Value comparisons (comparing two
Values traverses both nested maps) -- expensive for multi-asset values
- Large datum deserialization
- Cryptographic hashing (blake2b, sha256)
- On-chain signature verification (extremely expensive -- use
extra_signatories instead)
High memory cost:
- Building large intermediate data structures
- Repeated datum deserialization
- String concatenation for trace messages
- Map/filter chains that create intermediate lists
Script size bloat:
- Trace messages (string literals embedded in compiled output)
- Dead code (unreachable branches, unused functions)
- Redundant type conversions
- Inlined functions that could be shared
Step 4: Apply optimizations
Execution unit optimizations
Order pattern matching by frequency:
// Most common action first -- UPLC checks branches sequentially
when redeemer is {
Swap -> check_swap(...) // 90% of transactions
AddLiquidity -> check_add(...) // 8%
RemoveLiquidity -> check_remove(...) // 1.5%
UpdateParams -> check_update(...) // 0.5%
}
Fail fast -- check cheap conditions before expensive ones:
// GOOD: Cheap signer check (O(small n)) before expensive output scan (O(large n))
list.has(tx.extra_signatories, datum.owner) &&
check_outputs(tx.outputs, expected_value)
Extract common sub-expressions:
// BAD: Resolves own input twice
list.any(tx.outputs, fn(o) { o.address == resolve_own_address(tx, own_ref) }) &&
list.all(tx.outputs, fn(o) {
o.address != resolve_own_address(tx, own_ref) || check_value(o)
})
// GOOD: Compute once, reuse
expect Some(own_input) = transaction.find_input(tx.inputs, own_ref)
let own_addr = own_input.output.address
list.any(tx.outputs, fn(o) { o.address == own_addr }) &&
list.all(tx.outputs, fn(o) { o.address != own_addr || check_value(o) })
Reduce list traversals:
// BAD: Two separate passes over outputs
let has_script_output = list.any(tx.outputs, fn(o) { o.address == script_addr })
let has_payment = list.any(tx.outputs, fn(o) { o.address == seller })
// GOOD: Single pass with combined check
let (has_script, has_pay) =
list.foldl(tx.outputs, (False, False), fn(o, acc) {
let (s, p) = acc
(s || o.address == script_addr, p || o.address == seller)
})
Use expect instead of when for single-variant destructuring:
// LARGER (compiled): when with explicit fail
when datum is {
MyDatum { owner, amount } -> use(owner, amount)
_ -> fail
}
// SMALLER: expect compiles to direct destructure
expect MyDatum { owner, amount } = datum
use(owner, amount)
Script size optimizations
Remove traces for production builds:
aiken build
aiken build --trace-level silent
Extract shared helper functions:
// BAD: Duplicated logic in each branch
when redeemer is {
Claim -> {
expect Some(out) = list.find(tx.outputs, fn(o) { o.address == addr })
assets.lovelace_of(out.value) >= amount && list.has(tx.extra_signatories, owner)
}
Update -> {
expect Some(out) = list.find(tx.outputs, fn(o) { o.address == addr })
assets.lovelace_of(out.value) >= amount && list.has(tx.extra_signatories, owner)
}
}
// GOOD: Shared function
fn check_output_and_signer(tx, addr, amount, signer) {
expect Some(out) = list.find(tx.outputs, fn(o) { o.address == addr })
assets.lovelace_of(out.value) >= amount && list.has(tx.extra_signatories, signer)
}
Remove dead code and unused imports:
Search for functions, types, and imports that are not referenced. Unused code still contributes to script size.
Data structure optimizations
- Use
Pair<a, b> instead of 2-element tuples when possible (smaller UPLC representation)
- For small fixed collections, explicit fields are cheaper than lists
- For lookups, sorted lists with early-exit beat unsorted lists
- Smaller datums mean less deserialization cost -- remove fields that can be computed from other fields
Step 5: Consider reference scripts
For scripts over approximately 4 KB that will be used in multiple transactions:
- Store the script as a reference script in a UTxO
- Reference it by hash in subsequent transactions
- The script is paid for once at creation and amortized over many uses
- Reduces per-transaction size significantly
Step 6: Verify and benchmark
After applying optimizations, the user should verify:
- All existing tests still pass:
aiken check
- Script size delta: Compare compiled sizes before and after
- CPU units delta: Run
aiken bench and compare
- Memory units delta: Run
aiken bench and compare
- No security checks were removed: Review changes for correctness
Provide before/after comparison format:
Before After Delta
cpu: 2,345,678 1,890,123 -19.4%
mem: 234,567 198,432 -15.4%
size: 4,567 3,890 -14.8%
Step 7: Document trade-offs
If any optimization involves a trade-off (e.g., increased size for lower CPU), document:
- What was changed and why
- The measured impact on each metric
- Any correctness considerations
References
references/uplc-cost-model.md -- UPLC cost model basics, operation costs, and budget limits
- Search
${CLAUDE_SKILL_DIR}/../../docs/sources/ for benchmark results and performance requirements
- Aiken documentation on optimization: https://aiken-lang.org
- Use
aiken build --trace-level silent for production builds
- Use
aiken bench for execution unit measurements