Skip to content

perf(contracts): optimize contract gas usage and establish benchmarks#31

Open
mftee wants to merge 1 commit into
Exquisitech:mainfrom
mftee:perf/23-gas-benchmarks
Open

perf(contracts): optimize contract gas usage and establish benchmarks#31
mftee wants to merge 1 commit into
Exquisitech:mainfrom
mftee:perf/23-gas-benchmarks

Conversation

@mftee

@mftee mftee commented Jul 18, 2026

Copy link
Copy Markdown

Summary

  • counter: COUNTER moved from persistent to temporary storage. Temporary entries are cheaper to read/write (no long-term rent accounting). Safe here because the counter carries no financial value — worst case on TTL lapse is a silent reset to 0.
  • betting: removed MatchStats.bettors: Vec<Address> — it was appended to and the entire MatchStats record rewritten on every single place_bet call, but the field was never read anywhere (not even by claim_payout). This made place_bet's storage cost grow linearly with the number of bettors already on a match. Removing it restores constant per-call cost.
  • betting: get_bet/Bet intentionally kept on persistent storage, with the rationale documented in the module doc comment — Bet holds real transferred funds, a claimed flag, and a payout; moving it to temporary storage risks unrecoverable fund loss if the TTL lapses before a bettor claims/refunds. The issue asked to move get_count/get_bet to temporary storage "where safe" — for get_bet specifically, it isn't.
  • Added gas_bench test modules to counter and betting that measure cpu_instruction_cost/memory_bytes_cost per function via env.budget() and assert against configurable thresholds (see docs/gas-benchmarks.md).
  • Added a Gas budget benchmarks step to .github/workflows/contracts.yml running these two gas_bench suites so a regression fails CI.

A blocking pre-existing issue I found and partially fixed

None of counter, betting, rewards, or player-nft's Cargo.toml declared soroban-sdk's testutils feature in [dev-dependencies] (oracle and vault already had the correct pattern). That meant Env::mock_all_auths, Address::generate, env.register_contract, etc. were unavailable, and none of these four crates' #[cfg(test)] modules had ever actually compiled — invisible until now because contracts.yml only ran cargo check, never cargo test. I fixed the Cargo.toml gap in all four (copying the working pattern from oracle/vault), since my new gas_bench tests need cargo test to actually work.

This surfaced further pre-existing, unrelated bugs in betting's existing mod test that were also never caught for the same reason (several call sites treat an already-auto-unwrapped client method return value as a Result and call .unwrap() on it again; one helper function is missing a lifetime annotation). I fixed two of these opportunistically while getting my own code to compile (the upgrade/try_upgrade call in test_upgrade_requires_two_governance_approvals_and_preserves_state, and the missing lifetime on the initialize test helper), but did not do a full audit of the rest of mod test — that's a pre-existing problem spanning the whole file and out of scope for this gas-optimization issue.

Practical consequence: I could not get a clean local cargo test --release run to completion — beyond the remaining pre-existing test bugs, this Windows dev environment also hit unrelated toolchain issues (missing dlltool.exe for the -gnu target, incomplete MSVC "C++ build tools" for the -msvc target) that block linking locally regardless. The Gas budget benchmarks CI job on this PR is the first environment where this can actually be verified end-to-end, and it may currently fail on the remaining pre-existing bugs in betting's mod test rather than on anything in this diff. I'd rather flag that clearly than claim a green run I couldn't produce. Happy to keep iterating from the actual CI output, or to split the pre-existing-bug cleanup into its own PR if that's preferred.

docs/gas-benchmarks.md

Records the budget thresholds enforced by gas_bench and the rationale for each storage decision above. The "Actual" measured values are marked pending the first real CI run for the reason above, rather than backfilled with numbers I couldn't actually produce locally.

Closes #23

- counter: move COUNTER from persistent to temporary storage (cheaper
  reads/writes, safe here since the counter carries no financial value)
- betting: remove MatchStats.bettors, an unbounded Vec<Address> that was
  appended to and rewritten in full on every place_bet call despite never
  being read anywhere — this made place_bet's storage cost grow linearly
  with the number of bettors on a match
- betting: get_bet/Bet intentionally kept on persistent storage and
  documented why (holds real funds; temporary storage risks fund loss on
  TTL expiry)
- add gas_bench test modules to counter and betting measuring
  cpu_instruction_cost/memory_bytes_cost per function via env.budget(),
  asserting against configurable thresholds
- wire a "Gas budget benchmarks" step into contracts.yml CI so a
  regression fails the build
- fix counter/betting/rewards/player-nft Cargo.toml: none declared
  soroban-sdk's `testutils` feature in [dev-dependencies] (oracle and
  vault already did), so none of their #[cfg(test)] modules had ever
  actually compiled — cargo test was never run in CI, only cargo check
- docs/gas-benchmarks.md: target/actual table and rationale for the
  above storage decisions

Closes Exquisitech#23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Title: perf(contracts): optimize contract gas usage and establish benchmarks`

1 participant