Skip to content
Merged
58 changes: 58 additions & 0 deletions GAS_BENCHMARKING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
## Gas Cost Benchmarking Procedures

Goal: produce reproducible cost metrics per entrypoint across typical scenarios and catch regressions.

### Tools

- Stellar CLI (`stellar`) with `--cost`
- RPC simulateTransaction (client SDKs)

### Build

```bash
stellar contract build
```

### Local Simulation (recommended)

- Use `stellar contract invoke --cost` (or `tx simulate`) to print execution cost breakdown before submit.
- For each function, craft inputs for small/medium/large cases.

Example (pseudocode; replace ids/args):

```bash
# Simulate vote cost
stellar contract invoke --id $CONTRACT_ID \
--network futurenet --cost -- \
vote --user $USER --market-id market_1 --outcome Yes --stake 1000
```

Capture output (instructions, ledger read/write counts, bytes) into `benchmarks/results/*.csv`.

### RPC Simulation (programmatic)

- Use SDKs to build a tx that invokes the function and call `simulateTransaction`.
- Record `resourceFee`, `cpuInsns`, `readBytes`, `writeBytes`, `readEntries`, `writeEntries`, and events/return sizes.

### Scenarios to Benchmark

- create_market: short vs long question/outcomes
- vote: single voter; 100 voters; 1,000 voters
- claim_winnings: winner vs loser; large market iteration
- resolve_market: with/without oracle result, with disputes
- fetch_oracle_result: Reflector vs Pyth paths
- collect_fees: resolved vs unresolved

### WASM Size Optimization

```bash
stellar contract optimize --wasm target/wasm32v1-none/release/predictify_hybrid.wasm
```

Track optimized size and ensure below network limits.

### Reporting

- Commit CSVs and a short summary per release under `benchmarks/`.
- Update `GAS_COST_ANALYSIS.md` with highlights (e.g., hot paths, bytes drivers).

51 changes: 51 additions & 0 deletions GAS_CASE_STUDIES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## Gas Optimization Case Studies (Predictify Hybrid)

### 1) Voting: Avoid per-iteration storage access

Issue: Repeated `.get()`/`.set()` inside loops increases read/write entries.

Fix: Read market once, update in-memory, write once.

See the `vote` implementation which already batches to a single final write:

```302:308:contracts/predictify-hybrid/src/lib.rs
market.votes.set(user.clone(), outcome);
market.stakes.set(user.clone(), stake);
market.total_staked += stake;
env.storage().persistent().set(&market_id, &market);
```

Further improvement: Pre-validate `outcome` using an in-memory set if outcomes are large to avoid repeated scans.

### 2) Claiming: Scale with participants carefully

Current approach iterates all votes to compute `winning_total`:

```395:404:contracts/predictify-hybrid/src/lib.rs
let mut winning_total = 0;
for (voter, outcome) in market.votes.iter() {
if &outcome == winning_outcome {
winning_total += market.stakes.get(voter.clone()).unwrap_or(0);
}
}
```

Optimizations:

- Maintain `stakes_per_outcome` totals during `vote` to avoid O(n) scan at claim time.
- Consider a compact bitmap/flag for `claimed` to reduce map overhead.

### 3) Market Creation: Bound string sizes

Cost driver: `question` and `outcomes` lengths inflate write-bytes.

Guideline: Enforce caps (e.g., 140/32 chars). Reject overlong inputs to protect fees.

### 4) Oracle Resolution: Validate before calling

Call the cheapest checks first (staleness, feed format) before cross-contract calls. Skip persistence until a valid result is known.

### 5) Events over Storage

Emit events for analytics (e.g., vote tally changes) and only persist aggregates needed for on-chain reads.

131 changes: 131 additions & 0 deletions GAS_COST_ANALYSIS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
## Gas Usage Analysis (Function Catalog)

This document catalogs public entrypoints in `predictify-hybrid` and provides a structure to record gas usage characteristics and measured costs. Use the benchmarking guide to populate the "Measured Cost" columns.

### Method Inventory

- initialize(env, admin)
- create_market(env, admin, question, outcomes, duration_days, oracle_config) -> Symbol
- vote(env, user, market_id, outcome, stake)
- claim_winnings(env, user, market_id)
- get_market(env, market_id) -> Option<Market>
- fetch_oracle_result(env, market_id, oracle_contract) -> Result<String, Error>
- resolve_market(env, market_id) -> Result<(), Error>
- get_resolution_analytics(env) -> Result<ResolutionAnalytics, Error>
- get_market_analytics(env, market_id) -> Result<MarketStats, Error>
- dispute_market(env, user, market_id, stake, reason) -> Result<(), Error>
- vote_on_dispute(env, user, market_id, dispute_id, vote, stake, reason) -> Result<(), Error>
- resolve_dispute(env, admin, market_id) -> Result<DisputeResolution, Error>
- collect_fees(env, admin, market_id) -> Result<i128, Error>
- extend_market(env, admin, market_id, additional_days, reason, fee_amount) -> Result<(), Error>
- Storage optimization helpers (compress/cleanup/migrate/monitor/optimize/...)

### Storage Touch Patterns (selected excerpts)

Vote path writes a vote and stake, updates totals, and persists market:

```275:308:contracts/predictify-hybrid/src/lib.rs
// vote(...)
// ...
// Store the vote and stake
market.votes.set(user.clone(), outcome);
market.stakes.set(user.clone(), stake);
market.total_staked += stake;

env.storage().persistent().set(&market_id, &market);
```

Market creation allocates a new `Market` with several empty maps and persists once:

```183:221:contracts/predictify-hybrid/src/lib.rs
// create_market(...)
// Generate ID, compute end_time, then
let market = Market {
// ...
oracle_result: None,
votes: Map::new(&env),
total_staked: 0,
dispute_stakes: Map::new(&env),
stakes: Map::new(&env),
claimed: Map::new(&env),
winning_outcome: None,
fee_collected: false,
state: MarketState::Active,
total_extension_days: 0,
max_extension_days: 30,
extension_history: Vec::new(&env),
};
env.storage().persistent().set(&market_id, &market);
```

Claim path iterates to compute `winning_total` and marks `claimed`:

```395:419:contracts/predictify-hybrid/src/lib.rs
// claim_winnings(...)
// Calculate total winning stakes
let mut winning_total = 0;
for (voter, outcome) in market.votes.iter() {
if &outcome == winning_outcome {
winning_total += market.stakes.get(voter.clone()).unwrap_or(0);
}
}
// Mark as claimed
market.claimed.set(user.clone(), true);
env.storage().persistent().set(&market_id, &market);
```

### Analysis Template

Fill per method after running benchmarks (see GAS_BENCHMARKING.md):

- initialize
- Reads: 0-1 (admin guard if re-init)
- Writes: 1 (Admin key)
- Bytes written (est.): small
- Measured: instructions=…, r-entries=…, w-entries=…, rKB=…, wKB=…

- create_market
- Reads: 1 (admin)
- Writes: 2 (counter, market)
- Bytes drivers: `question`, `outcomes` length
- Risks: long strings blow write-bytes; validate lengths
- Measured: …

- vote
- Reads: 1 (market)
- Writes: 1 (market)
- Map ops: votes.set, stakes.set
- Loop: none
- Measured: …

- claim_winnings
- Reads: 1 (market)
- Writes: 1 (market)
- Loop: iterates `votes` (cost scales with voters)
- Optimization: accumulate and cache totals off-chain; filter losers early
- Measured: …

- fetch_oracle_result
- Reads: 1 (market)
- Cross-contract: yes (oracle)
- Writes: 0 (this method returns result only)
- Measured: …

- resolve_market
- Likely reads+writes market; hybrid algorithm cost scales with votes
- Measured: …

- collect_fees / extend_market / dispute*
- Admin read, market write patterns
- Measured: …

### Length Limits to Enforce (to control write-bytes)

- `question`: recommend <= 140 chars
- `outcomes[i]`: recommend <= 32 chars
- `reason` fields: recommend <= 160 chars

### Recording Results

Record CLI `--cost` outputs and RPC simulation breakdowns in a CSV under `benchmarks/results/` for each function and typical scenarios (small/medium/large markets).

31 changes: 31 additions & 0 deletions GAS_MONITORING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Gas Usage Monitoring and Operations

### Pre-Submit Simulation

- Always simulate and log `--cost` before sending transactions.
- Use RPC `getFeeStats()` to set inclusion fee (p90 recommended under load).

### Metrics to Track

- Distribution of resource fees per function
- Average read/write entries and bytes per function
- Event+return sizes (aim << 8 KB cap)
- Oracle call failure rates and retries

### Alerting

- Spike in write-bytes or write-entries
- Repeated tx failures due to under-estimated event/return size
- Inclusion fee surge vs baseline

### Dashboards

- Per-endpoint cost over time
- Top costly calls and scenarios
- WASM size trend per release

### Operational Playbooks

- If costs climb due to strings: enforce length caps at API layer and/or contract validation.
- If claim/resolve costs spike: batch payouts off-chain via token escrows or staged claims.

88 changes: 88 additions & 0 deletions GAS_OPTIMIZATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
## Gas Optimization Guide (Soroban on Stellar)

This guide explains how to write and maintain gas-efficient Soroban contracts in this repository, with concrete recommendations mapped to `predictify-hybrid` and `hello-world`.

- Audience: Contract developers and reviewers
- Targets: Soroban SDK 22.x; built with wasm32v1-none

### Key Facts (Resource Limits & Fees)

- Max per-tx: 100M CPU instructions, 40 MB memory
- Ledger access limits: 40 reads, 25 writes; 200 KB read bytes; ~129 KB write bytes
- Fee highlights (stroops):
- 10,000 instructions: 25
- Read 1 ledger entry: 6,250; Write 1 ledger entry: 10,000
- Read 1 KB: 1,786; Write 1 KB: ~11,800
- Events+return value: 10,000 per KB (up to 8 KB total)
- Bandwidth: 1,624/KB, History archival: 16,235/KB

Reference: "Resource Limits & Fees" in Stellar docs.

### Golden Rules

- Prefer computation over storage. Reads/writes dominate costs; batch and cache in-memory.
- Read once, write once. Accumulate updates in memory, then persist once at end.
- Avoid per-iteration storage access inside loops. Pull state once, work in `Vec`/`Map`, write once.
- Keep data narrow. Use `Symbol`, `BytesN`, and compact enums/keys; avoid long `String` values.
- Emit events for audit-only data; store only what must be read on-chain later.
- Minimize cross-contract calls. They expand footprint, auth, and costs; batch where feasible.
- Validate inputs early and fail fast. Guard clauses save CPU and storage.
- Use fixed-size math and checked ops where possible; avoid unnecessary big-int math.
- Favor `Vec`/`Map` keyed by compact enums over wide maps with long keys.
- Keep return values small; event+return budget is capped at 8 KB.

### Patterns for Soroban

- Use `env.storage().persistent()` for durable state; consider `temporary()` for short-lived, re-creatable data.
- For lists, keep per-address collections keyed by an enum data key, not one giant vector of structs.
- Bundle external token/oracle transfers: one total transfer into the contract, then internal distributions.
- Avoid growing WASM linear memory repeatedly (e.g., large heap vec); pre-size or use small batches.

### Contract-Specific Hotspots

- `vote` and staking accrual: Favor in-memory aggregation; avoid repeated map lookups/sets.
- `claim_winnings`: Compute totals in-stream and avoid re-reading maps repeatedly; short-circuit losers early.
- `create_market`: Validate and compute once; store a compact `Market` struct; avoid overlong strings.
- Oracle resolution: Keep payloads compact, validate staleness and confidence before persisting.

### Data Layout Recommendations

- Keys: Use `Symbol`-based keys or small enums for storage keys.
- Strings: Restrict question/outcome lengths; validate length to prevent excess write bytes.
- Maps: Avoid nested maps when a single flat map of compact keys suffices.

### Events vs Storage

- Emit events for analytics/telemetry and off-chain consumption.
- Store only state needed for on-chain reads (e.g., current totals, winner, claims bitmap/flags).

### Build & Profile Tips

- Use `profile.release` with `opt-level = "z"`, `lto = true`, `panic = "abort"` (already configured).
- Run cost simulations with CLI `--cost` and RPC `simulateTransaction` before submitting.
- Keep function return values and emitted events small.

### Safe Math

- Keep `overflow-checks = true` (already set). Prefer `checked_*` for user-driven arithmetic.
- Normalize precision early (e.g., cents) and avoid repeated scaling.

### Storage TTL and Rent

- Prefer temporary storage for short-lived data; extend TTL intentionally for persistent data.
- Avoid frequent size growth of entries; growing entries triggers higher rent top-ups.

### Code Review Checklist (Gas)

- Are storage reads/writes minimized and batched?
- Any loops calling storage or cross-contract functions per iteration?
- Are keys and values compact? Any unbounded strings or vectors?
- Are external calls minimized, batched, and gated by pre-checks?
- Do functions fail early on invalid inputs to save CPU/storage?
- Are events used instead of storage where on-chain reads aren’t required?

### References

- Stellar Docs: Analyzing smart contract cost and efficiency
- Stellar Docs: Resource Limits & Fees

32 changes: 32 additions & 0 deletions GAS_TESTING_GUIDELINES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Gas Optimization Testing Guidelines

Objective: ensure PRs do not introduce significant cost regressions and follow best practices.

### Unit Tests

- Cover all public entrypoints with valid and invalid inputs (fail fast saves gas).
- Include large-market tests (e.g., many voters) to catch algorithmic costs.

### Snapshot-Based Validation

- For stable scenarios, snapshot CLI `--cost` outputs and diff on PRs.
- Store under `test_snapshots/cost/` with scenario descriptions.

### Lints and Review

- Review loops for storage/cross-contract calls per iteration.
- Check for repeated `.get()`/`.set()` rather than single read/single write patterns.
- Ensure strings/bytes sizes are validated.

### PR Checklist (Gas)

- [ ] Storage ops minimized and batched
- [ ] No per-iteration storage writes in loops
- [ ] External calls minimized/batched
- [ ] Return/events payloads small
- [ ] Enforced input length caps

### Optional Static Analysis

- Consider running a Soroban-focused analyzer to detect storage-in-loop and repeated indirect storage access patterns.

Loading
Loading