--- name: optimize-contract description: Optimize Scalus/Cardano smart contracts for execution budget (CPU steps and memory). Analyzes @Compile annotated validators for performance issues — expensive patterns, unnecessary allocations, redundant traversals, missed short-circuits. Provides concrete Scalus rewrites with budget impact estimates. Use when reviewing on-chain code performance or when /optimize-contract is invoked. Requires explicit path argument. --- # Smart Contract Optimization Review Analyze Scalus/Cardano smart contracts for execution budget optimization opportunities. ## Prerequisites — Before You Optimize 1. **Establish a baseline.** Run the validator's tests with budget assertions (`assertBudgetEquals` or `assertBudgetWithin`) on representative inputs — simple case, worst case, and typical case. 2. **Identify actual hot paths.** Don't guess — measure. Use `EvalTestDsl` budget assertions to find which code paths dominate the budget. 3. **Optimize surgically.** Change one thing at a time and re-measure. Small, targeted changes are safer than sweeping rewrites. 4. **Re-benchmark after every significant change.** Budget is the only ground truth. ## Target Code Identification Find on-chain code by searching for: 1. Objects/classes with `@Compile` annotation 2. Objects extending `Validator`, `DataParameterizedValidator`, or `ParameterizedValidator` 3. Objects compiled with `PlutusV3.compile()`, `PlutusV2.compile()`, or `PlutusV1.compile()` Search patterns: ``` grep -rn "@Compile" --include="*.scala" grep -rn "extends Validator" --include="*.scala" grep -rn "extends DataParameterizedValidator" --include="*.scala" ``` ## Workflow 1. **Discovery**: Find all `@Compile` annotated code in specified path 2. **Profile**: Identify existing budget tests; note current memory/steps 3. **Analysis**: Check each validator against the optimization checklist below 4. **Prioritize**: Rank findings by estimated budget impact (high/medium/low) 5. **Rewrite**: Propose concrete code changes with before/after 6. **Verify**: Run `sbtn quick` or specific test to confirm budget improvement 7. **Report**: Generate structured report with budget deltas ## Optimization Checklist For detailed patterns with Scalus code examples, see `references/patterns.md`. ### High Impact — Data Structures & Traversals | ID | Pattern | Problem | Fix | |----|---------|---------|-----| | O001 | Multiple list traversals | Separate `filter` then `map` then `length` | Combine into single `foldLeft` | | O002 | `foldRight` on lists | Not tail-recursive, builds thunks | Use `foldLeft` with `reverse` if order matters | | O003 | `list.flatten` | O(n*m) via nested `foldRight` + `++` | Accumulate with `foldLeft` and prepend | | O004 | `list.distinct` | O(n^2) — `foldLeft` with `exists` | Use `SortedMap` or deduplicate at source | | O005 | `list :+ elem` (append) | O(n) per append | Use `elem +: list` (prepend) and reverse once | | O006 | Reconstructing `Value` | Full `Value` maintains invariants expensively | Use `SortedMap` or `PairList` directly when possible | | O007 | `AssocMap` anywhere | `get` scans to hit-or-end; `union` is O(n*m) (`get` per left key, `exists` per right key) | Use `SortedMap`: `get` stops early via `Ord`, `union` is one linear merge | | O008 | `list.map(f).filter(p)` | Two traversals, intermediate list | Single `foldLeft` combining map + filter | | O009 | `list.length == 0` / `map.size == 0` | O(n) traversal; `SortedMap.size` is O(n) although `inline` | Use `isEmpty` on `List`, `SortedMap`, `AssocMap`: one `nullList`, O(1) | | O010 | `AssocMap.fromList` on large input | O(n^2) dedup | Pre-sort and use `SortedMap.fromStrictlyAscendingList` | ### High Impact — Short-Circuiting & Ordering | ID | Pattern | Problem | Fix | |----|---------|---------|-----| | O011 | Expensive checks before cheap ones | Wasted budget on failing txs | Put cheapest/most-likely-to-fail checks first | | O012 | Late `require` for invalid input | Work done before validation | Fail fast — validate inputs at the top | | O013 | Linear condition chains | Average n/2 evaluations for n conditions | Structure as binary decision tree | | O014 | No short-circuit in `&&`/`||` | Evaluating both sides always | Scalus `&&`/`||` DO short-circuit — ensure expensive side is second | | O015 | `list.exists` after construction | Building list just to search it | Inline the search into the fold that builds the data | ### Medium Impact — Data Representation | ID | Pattern | Problem | Fix | |----|---------|---------|-----| | O016 | `===` on a `BigInt`/`ByteString` behind a type variable | Lowers to `equalsData`, not `equalsInteger`/`equalsByteString`: 1 761 779 vs 832 313 cpu, 2.1x (measured) | Make the key type concrete at the comparison site (a `BigInt`-keyed or `ByteString`-keyed helper, not a generic `K`) | | O017 | Constructing tuples/records to return | Allocation + destructuring overhead | Use continuation-passing or accumulator parameters | | O018 | `List[(A, B)]` map operations | ~12 builtins per element | Use `PairList` — ~4 builtins per element via `fstPair`/`sndPair` | | O019 | Pattern matching for Data access | Constructs intermediate Scala objects | Use `Data` builtins directly when structure is known | | O020 | Hand-written `equalsData(a.toData, b.toData)` or `a.toData == b.toData` | Buys nothing: for every Data-backed type `===` already lowers to `equalsData`; both spellings pin to 901 mem / 1 653 665 cpu on a `Value` (measured) | Derive `Eq` and write `a === b`; never compare `TxInfo`-scale structures whole | ### Medium Impact — Computation | ID | Pattern | Problem | Fix | |----|---------|---------|-----| | O021 | `pow(2, n)` | Generic exponentiation loop | Use `exp2(n)` — single builtin via byte shift | | O022 | Manual log2 via division loop | O(log n) divisions | Use `log2(n)` — single builtin via `integerToByteString` | | O023 | Recomputing same expression | Duplicated subexpressions | Use `let` bindings; V3 optimizer has CSE but don't rely on it | | O024 | `generateErrorTraces = true` in prod | Trace strings bloat script and budget | Set `generateErrorTraces = false` for production builds | | O025 | Complex pure computations on-chain | Expensive on-chain work | Move computation off-chain, pass result as redeemer, verify on-chain | ### Medium Impact – Stdlib idioms (measured where a number is given) | ID | Pattern | Problem | Fix | |----|---------|---------|-----| | O031 | `out.datum.inlineOrFail[T](msg) === expected` | Decodes, then compares field-wise: 461 lovelace | `out.hasInlineDatum(expected)`: one `equalsData` on the wrapped datum, 286 lovelace. Use `inlineOrFail` only to read fields | | O032 | `xs.exists(_ === x)` | `exists` is `find(p).isDefined`: allocates an `Option` for a `Boolean`; a fixed per-call tax of 326 483 cpu (miss) / 564 996 cpu (hit) on V3 | `xs.contains(x)`: an intrinsic, no `Option`, no `Eq` closure. For a non-equality predicate use `forall` or a hand fold, not `exists` | | O033 | `xs.filter(p).length` | 2 traversals plus k `mkCons`; `filter` is a non-tail `foldRight`; no pass fuses them | `xs.count(p)`: one tail-recursive `foldLeft`, no allocation | | O034 | `filter(p).length === BigInt(1)` then `.head`, or `count(p) === BigInt(1)` | 2 passes (or 1 pass plus a second scan for the element); the guard and the lookup are separate | `xs.findUniqueOrFail(p, msg)`: one pass, returns the element, fails on 0 or 2+. Against `count(p) === BigInt(1)` on inputs: fee 3 175 vs 3 307 (3 inputs), 6 289 vs 6 804 (10 inputs) | ### Low Impact — Micro-Optimizations | ID | Pattern | Problem | Fix | |----|---------|---------|-----| | O026 | Small recursive helpers | Call overhead per recursion | Unroll first 1-2 iterations for common small cases | | O027 | Non-tail-recursive numeric loops | Stack growth | Rewrite with accumulator parameter | | O028 | Redundant `FromData`/`ToData` conversions | Serialization round-trips | Keep data in `Data` form between operations | | O029 | `list.reverse.foldLeft` | Extra O(n) reverse pass | Use `foldRight` if list is small, or build in correct order | | O030 | Building closures in inner loops | Allocation per iteration | Lift closure outside loop if captures don't change | ## Key Scalus Optimization Principles ### 1. Don't Compute, Verify The most impactful optimization: move work off-chain. Instead of computing a result on-chain, have the off-chain code compute it and pass it as a redeemer field. The validator only checks correctness. ```scala // Expensive: compute on-chain val sqrtResult = radicand.sqRoot // Cheap: verify pre-computed result val sqrtResult = redeemer.sqrtValue require(sqrtResult * sqrtResult <= radicand) require((sqrtResult + 1) * (sqrtResult + 1) > radicand) ``` ### 2. Fail Fast Put cheapest and most-likely-to-fail validations first. Every `require` that fails early saves the budget of all subsequent code. ```scala // Good: cheap check first require(isSignedBy(txInfo, admin), "not admin") require(expensiveValueCheck(txInfo), "value mismatch") // Bad: expensive check first require(expensiveValueCheck(txInfo), "value mismatch") require(isSignedBy(txInfo, admin), "not admin") ``` ### 3. Traverse Once Never traverse a list twice when once will do. Combine filter + map + count into a single fold. ```scala // Bad: three traversals val filtered = items.filter(_.isValid) val mapped = filtered.map(_.amount) val total = mapped.foldLeft(BigInt(0))(_ + _) // Good: single traversal val total = items.foldLeft(BigInt(0)) { (acc, item) => if item.isValid then acc + item.amount else acc } ``` ### 4. Use PairList for Map Operations `PairList` uses raw UPLC pair builtins (~4 ops/element) vs `List[(A, B)]` (~12 ops/element). ```scala // Expensive map.toList.map { case (k, v) => (k, f(v)) } // Cheap — 3x fewer builtins map.toPairList.mapValues(f) ``` ### 5. Write `===`, and Keep Key Types Concrete For every Data-backed type `a === b` already lowers to one `equalsData` builtin. Hand-written `equalsData(a.toData, b.toData)` or `a.toData == b.toData` produces identical UPLC (measured: both pin to 901 mem / 1 653 665 cpu on a `Value`). The cost that is real: `===` on a `BigInt` or `ByteString` behind a type variable emits `equalsData` instead of `equalsInteger` / `equalsByteString`, 1 761 779 vs 832 313 cpu (2.1x, measured). ```scala // Same UPLC, worse to read: do not write this require(equalsData(toData(outputDatum), toData(inputDatum))) // Write this (derive Eq on the datum type) require(outputDatum === inputDatum) // Generic key: equalsData on every step, 2.1x slower def lookup[K: Eq](key: K, entries: List[(K, BigInt)]): Option[BigInt] = entries.find(_._1 === key).map(_._2) // Concrete key: equalsInteger def lookup(key: BigInt, entries: List[(BigInt, BigInt)]): Option[BigInt] = entries.find(_._1 === key).map(_._2) ``` For a continuing datum, compare without decoding: `out.hasInlineDatum(expected)` costs 286 lovelace against 461 for `out.datum.inlineOrFail[T](msg) === expected` (measured, O031). ### 6. Leverage Ledger Invariants The ledger guarantees: inputs are sorted by `TxOutRef`, values are ordered by policy ID, outputs never contain negative quantities, minted values exclude ADA. Align your algorithms with these invariants instead of re-validating them. ### 7. Build Caches for Repeated Lookups If you check membership in the same set multiple times, build a decision closure once. ```scala // Bad: O(n) per check require(signatories.exists(_ === admin1)) require(signatories.exists(_ === admin2)) // Better: single traversal, check both val (hasAdmin1, hasAdmin2) = signatories.foldLeft((false, false)) { case ((a1, a2), sig) => (a1 || sig === admin1, a2 || sig === admin2) } require(hasAdmin1 && hasAdmin2) ``` ### 8. Use Cheap Builtins for Math `log2` and `exp2` use `integerToByteString`/`shiftByteString` — much cheaper than iterative computation. ```scala // Cheap val bits = x.log2 val powerOf2 = n.exp2 // Expensive val bits = manualLog2Loop(x) val powerOf2 = pow(BigInt(2), n) ``` ## Compiler Options That Affect Budget ```scala // V3 lowering, no error traces, UPLC optimizer on private given Options = Options.release ``` ## Measuring Budget Use `EvalTestDsl` for precise budget measurement: ```scala import scalus.testing.dsl.EvalTestDsl.* // Exact budget match — catches regressions AND improvements eval(compiled) .onVM(PlutusV3.makePlutusV3VM()) .expectSuccess() .assertBudgetEquals(memory = 129528, steps = 37_067868) // Upper bound — for tests where budget fluctuates slightly between builds eval(compiled) .onVM(PlutusV3.makePlutusV3VM()) .expectSuccess() .assertBudgetWithin(memory = 140000, steps = 40_000000) ``` **Format convention:** Always use named parameters and `_` at million boundary: `ExUnits(memory = 129528, steps = 37_067868)` ## Output Format Use clickable `file_path:line_number` format for all code locations. ### Finding Format ``` ### [IMPACT] ID: Optimization Name **Location:** `full/path/to/File.scala:LINE` **Estimated savings:** ~X% memory, ~Y% steps (or: high/medium/low) **Current code** (`full/path/to/File.scala:LINE-LINE`): ```scala // actual code from file ``` **Optimized code:** ```scala // proposed optimization ``` **Rationale:** Why this is faster and what budget cost it avoids. --- ``` ### Summary Table ``` ## Summary | ID | Impact | Location | Pattern | Est. Savings | |----|--------|----------|---------|-------------| | O-01 | High | `path/File.scala:123` | Multiple traversals → single fold | ~30% steps | | O-02 | Medium | `path/File.scala:87` | inlineOrFail === x → hasInlineDatum(x) | 286 vs 461 lovelace | | O-03 | Low | `path/File.scala:200` | Unroll small recursion | ~2% steps | **Current budget:** ExUnits(memory = X, steps = Y) **Estimated budget after optimization:** ExUnits(memory = X', steps = Y') ``` ## Interactive Workflow For each finding: 1. Display issue with location and proposed optimization 2. Prompt: "Apply optimization? [y/n/s/d]" - y: Apply change, re-run budget test - n: Skip, log as "declined" - s: Skip without logging - d: Show detailed budget breakdown 3. After all findings: run `sbtn quick` to verify correctness 4. Generate summary report with actual budget deltas (before/after) ## Reference For detailed optimization patterns with Scalus code examples, see: - `references/patterns.md` — Full pattern catalog with before/after code and budget estimates