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
40 changes: 39 additions & 1 deletion contracts/governance/src/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

#![cfg(test)]

use soroban_sdk::{testutils::Address as _, Address, Env, String};
use soroban_sdk::{testutils::{Address as _, Ledger}, Address, Env, String};
use cosmosvote_token::TokenContractClient;
use crate::{types::Vote, GovernanceContract, GovernanceContractClient};
use cosmosvote_token::TokenContract;

// ---------------------------------------------------------------------------
// Baselines (Target instruction counts)
// See docs/instruction-budgets.md for rationale and Soroban limits.
// ---------------------------------------------------------------------------

const BASELINE_CREATE_PROPOSAL: u64 = 5_000_000;
const BASELINE_CAST_VOTE: u64 = 2_000_000;
const BASELINE_FINALISE: u64 = 3_000_000;

// ---------------------------------------------------------------------------
// Benchmark runner
Expand Down Expand Up @@ -104,3 +106,39 @@ fn bench_cast_vote() {
BASELINE_CAST_VOTE
);
}

#[test]
fn bench_finalise() {
let (env, gov, token, admin, proposer) = setup_env();

let id = gov.create_proposal(
&proposer,
&String::from_str(&env, "Finalise Benchmark"),
&String::from_str(&env, "Measuring instruction count for finalise"),
&1_000i128,
&3600u64,
&None,
&None,
);

let voter = Address::generate(&env);
token.mint(&admin, &voter, &1_000i128);
gov.cast_vote(&voter, &id, &Vote::Yes);

let end_time = gov.get_proposal(&id).end_time;
env.ledger().with_mut(|l| l.timestamp = end_time + 1);

env.as_contract(&gov.address, || {
env.budget().reset_unlimited();
});
let instructions_before = env.budget().cpu_instruction_cost();
gov.finalise(&id);
let instructions = env.budget().cpu_instruction_cost() - instructions_before;

assert!(
instructions <= threshold(BASELINE_FINALISE),
"finalise used {} instructions, exceeds 10% over baseline {}",
instructions,
BASELINE_FINALISE
);
}
81 changes: 81 additions & 0 deletions docs/instruction-budgets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Instruction Count Budgets

Soroban transactions are subject to a **CPU instruction limit** imposed by the Stellar network. Exceeding this limit causes the transaction to fail with an `ExceededLimit` error. This document records the instruction budget baselines for the three core governance flows and explains how they are enforced.

## Soroban Transaction Limit

The Stellar network enforces a per-transaction CPU instruction limit. As of Stellar Protocol 21, the limit is:

| Resource | Limit |
|----------|-------|
| CPU instructions per transaction | 100,000,000 (100M) |

All governance operations are well within this ceiling. The baselines below are conservative targets that leave headroom for future feature additions.

## Baseline Budgets

Baselines are defined as `const u64` in `contracts/governance/src/benchmarks.rs`. Tests fail if the measured instruction count exceeds the baseline by more than **10%**.

| Operation | Baseline (instructions) | Fail threshold (+10%) | Notes |
|-----------|------------------------|----------------------|-------|
| `create_proposal` | 5,000,000 | 5,500,000 | Includes storage writes for proposal + metadata |
| `cast_vote` | 2,000,000 | 2,200,000 | Includes has-voted guard + balance lookup + write |
| `finalise` | 3,000,000 | 3,300,000 | Includes quorum check + state transition + event emit |

## How Benchmarks Work

The benchmark tests in `contracts/governance/src/benchmarks.rs` use Soroban's built-in `env.budget()` API:

```rust
// Reset budget so prior setup calls don't pollute the measurement
env.as_contract(&gov.address, || {
env.budget().reset_unlimited();
});

let before = env.budget().cpu_instruction_cost();
gov.create_proposal(/* ... */);
let used = env.budget().cpu_instruction_cost() - before;

assert!(used <= threshold(BASELINE_CREATE_PROPOSAL));
```

The `threshold()` helper adds a 10% margin to the baseline, so minor fluctuations in Soroban SDK versions don't cause spurious failures.

## Running the Benchmarks

```bash
# Run all benchmark tests
cargo test -p cosmosvote-governance bench_

# Or via Make
make test
```

Sample output:

```
test benchmarks::bench_create_proposal ... ok
test benchmarks::bench_cast_vote ... ok
test benchmarks::bench_finalise ... ok
```

## Updating Baselines

If a deliberate feature addition increases instruction counts beyond the 10% threshold:

1. Measure the new count by running the benchmark and reading the assertion failure message.
2. Update the corresponding `const BASELINE_*` value in `benchmarks.rs`.
3. Update the table in this document.
4. Add a note in `CHANGELOG.md` describing the change and its cause.

> Baseline increases should be intentional and code-reviewed. An unexpected increase is a signal to investigate the change for unnecessary computation.

## Relationship to Soroban Limits

| Operation | Baseline | % of 100M limit |
|-----------|----------|----------------|
| `create_proposal` | 5,000,000 | 5% |
| `cast_vote` | 2,000,000 | 2% |
| `finalise` | 3,000,000 | 3% |

Even in a worst-case scenario where all three operations are composed in a single test, total instruction usage remains under 10% of the network limit.