Skip to content
Merged
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
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,9 +477,9 @@ CD3TE3IAHM737P236XZL2OYU275ZKD6MN7YH7PYYAXYIGEH55OPEWYJC
### Benchmark regression guard

The `scripts/check_benchmark_regression.sh` script compares instruction counts
against stored baselines. CI runs it as a warning-only step, but a large
regression in `invoice_liquidity` will be flagged during review. Run it
locally after performance-sensitive changes:
against stored baselines for `invoice_liquidity` and `iln_governance`. CI
runs it as a warning-only step, but a large regression will be flagged during
review. Run it locally after performance-sensitive changes:

```bash
bash scripts/check_benchmark_regression.sh
Expand Down
7 changes: 7 additions & 0 deletions contracts/iln_governance/benchmarks/baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"benchmarks": {
"create_proposal": { "cpu": 220773, "mem": 33354 },
"cast_vote": { "cpu": 252321, "mem": 41057 },
"delegate_votes": { "cpu": 182897, "mem": 28102 }
}
}
6 changes: 6 additions & 0 deletions contracts/iln_governance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
//! disable mechanism.

#![no_std]

#[cfg(test)]
extern crate std;

use soroban_sdk::{
contract, contracterror, contractimpl, contracttype, token::Client as TokenClient, vec,
Address, BytesN, Env, IntoVal, Symbol, Vec,
Expand Down Expand Up @@ -1172,3 +1176,5 @@ impl GovContract {

#[cfg(test)]
mod test;
#[cfg(test)]
mod tests_benchmarks;
162 changes: 162 additions & 0 deletions contracts/iln_governance/src/tests_benchmarks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#![cfg(test)]

//! Execution cost benchmarks for the governance contract (Issue #522).
//! Emits machine-readable `BENCHMARK` lines for CI regression checks,
//! mirroring the pattern established in `invoice_liquidity/tests_benchmarks.rs`.

use super::*;
use soroban_sdk::{
contract, contractimpl,
testutils::{Address as _, Ledger},
token::StellarAssetClient,
Address, BytesN, Env,
};

#[contract]
struct MockIlnBench;

#[contractimpl]
impl MockIlnBench {
pub fn update_fee_rate(_env: Env, _rate: u32) {}
pub fn add_token(_env: Env, _token: Address) {}
pub fn remove_token(_env: Env, _token: Address) {}
pub fn update_max_discount(_env: Env, _rate: u32) {}
}

struct BaseBenchEnv {
env: Env,
contract: GovContractClient<'static>,
proposer: Address,
voter: Address,
}

fn setup_benchmark_env() -> BaseBenchEnv {
let env = Env::default();
env.mock_all_auths();
env.budget().reset_unlimited();

let mut ledger = env.ledger().get();
ledger.timestamp = 1_700_000_000;
env.ledger().set(ledger);

let token_admin = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(token_admin);
let token_addr = token_id.address();
let token_admin_client = StellarAssetClient::new(&env, &token_addr);

let proposer = Address::generate(&env);
let voter = Address::generate(&env);
token_admin_client.mint(&proposer, &1_000_000);
token_admin_client.mint(&voter, &1_000_000);

let iln_contract = env.register_contract(None, MockIlnBench);
let admin = Address::generate(&env);

let contract_id = env.register_contract(None, GovContract);
let contract = GovContractClient::new(&env, &contract_id);
contract.initialize(&iln_contract, &token_addr, &admin);

BaseBenchEnv {
env,
contract,
proposer,
voter,
}
}

fn emit_benchmark(name: &str, cpu: u64, mem: u64) {
std::println!("BENCHMARK {name} cpu={cpu} mem={mem}");
}

fn measure<F: FnOnce()>(env: &Env, name: &str, action: F) -> (u64, u64) {
env.budget().reset_unlimited();
action();
let cpu = env.budget().cpu_instruction_cost();
let mem = env.budget().memory_bytes_cost();
emit_benchmark(name, cpu, mem);
(cpu, mem)
}

#[test]
fn benchmark_create_proposal() {
let bench = setup_benchmark_env();
let hash = BytesN::from_array(&bench.env, &[7u8; 32]);

measure(&bench.env, "create_proposal", || {
bench.contract.create_proposal(
&bench.proposer,
&ProposalAction::UpdateFeeRate(500),
&hash,
&500,
);
});
}

#[test]
fn benchmark_cast_vote() {
let bench = setup_benchmark_env();
let hash = BytesN::from_array(&bench.env, &[7u8; 32]);
let id = bench.contract.create_proposal(
&bench.proposer,
&ProposalAction::UpdateFeeRate(500),
&hash,
&500,
);

measure(&bench.env, "cast_vote", || {
bench.contract.cast_vote(&bench.voter, &id, &true);
});
}

#[test]
fn benchmark_delegate_votes() {
let bench = setup_benchmark_env();

measure(&bench.env, "delegate_votes", || {
bench.contract.delegate_votes(&bench.voter, &bench.proposer);
});
}

#[test]
fn benchmark_all_functions_summary() {
// Uses "_summary"-suffixed BENCHMARK names so these lines never collide
// with the isolated per-function benchmarks above when CI's regression
// script parses combined test output (tests run concurrently, so line
// order between this test and the isolated ones is not guaranteed).
let mut results = std::vec::Vec::new();

let bench = setup_benchmark_env();
let hash = BytesN::from_array(&bench.env, &[7u8; 32]);

results.push(measure(&bench.env, "create_proposal_summary", || {
bench.contract.create_proposal(
&bench.proposer,
&ProposalAction::UpdateFeeRate(500),
&hash,
&500,
);
}));

let id = bench.contract.create_proposal(
&bench.proposer,
&ProposalAction::UpdateFeeRate(500),
&hash,
&500,
);
results.push(measure(&bench.env, "cast_vote_summary", || {
bench.contract.cast_vote(&bench.voter, &id, &true);
}));
results.push(measure(&bench.env, "delegate_votes_summary", || {
bench.contract.delegate_votes(&bench.voter, &bench.proposer);
}));

std::println!("\n| Function | CPU Instructions | Memory (bytes) |");
std::println!("| --------------- | ----------------- | -------------- |");
for (name, (cpu, mem)) in [
("create_proposal", results[0]),
("cast_vote", results[1]),
("delegate_votes", results[2]),
] {
std::println!("| {name:<15} | {cpu:>17} | {mem:>14} |");
}
}
33 changes: 33 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,39 @@ services:
volumes:
- stellar-e2e-data:/opt/stellar

# Opt-in: builds and deploys insurance_pool against the e2e Stellar node
# for tests that exercise it. See docker-compose.yml for the local-dev
# equivalent and scripts/deploy-insurance-pool.sh for configuration.
#
# Usage: docker compose -f docker-compose.test.yml --profile insurance up insurance-pool-deploy
insurance-pool-deploy:
image: rust:1-slim
container_name: iln-insurance-pool-deploy-e2e
profiles: ["insurance"]
depends_on:
stellar:
condition: service_healthy
working_dir: /workspace
volumes:
- .:/workspace
- insurance-pool-cargo-cache-e2e:/usr/local/cargo/registry
environment:
- INSURANCE_POOL_ADMIN=${INSURANCE_POOL_ADMIN:-}
- INSURANCE_POOL_COVERAGE=${INSURANCE_POOL_COVERAGE:-1000000000}
command: >
bash -c "
set -e &&
rustup target add wasm32v1-none &&
command -v stellar >/dev/null 2>&1 || cargo install --locked stellar-cli &&
stellar network add --global local --rpc-url http://stellar:8000 --network-passphrase 'Standalone Network ; February 2021' --override || true &&
stellar keys generate --global alice || true &&
stellar account fund alice --network local || true &&
bash scripts/deploy-insurance-pool.sh local alice
"
restart: "no"

volumes:
stellar-e2e-data:
driver: local
insurance-pool-cargo-cache-e2e:
driver: local
46 changes: 46 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
# Usage: docker compose up -d stellar
# Check health: docker compose logs -f stellar | grep "ready"
# Stop: docker compose down
#
# Insurance pool testing (opt-in "insurance" profile — builds and deploys
# the insurance_pool contract against the local Stellar node):
# docker compose --profile insurance up insurance-pool-deploy

version: '3.8'

Expand Down Expand Up @@ -59,6 +63,9 @@ services:
- PORT=3000
- HORIZON_URL=http://stellar:8000
- DB_PATH=/app/data/indexer.db
# Contract ID from `insurance-pool-deploy` (see the "insurance" profile
# below) or scripts/deploy-insurance-pool.sh. Empty until deployed.
- INSURANCE_POOL_CONTRACT_ID=${INSURANCE_POOL_CONTRACT_ID:-}
ports:
- "3000:3000"
volumes:
Expand All @@ -83,6 +90,9 @@ services:
environment:
- PORT=3001
- DATABASE_URL=postgres://notifications:notifications@notifications-db:5432/notifications
# Contract ID from `insurance-pool-deploy` (see the "insurance" profile
# below) or scripts/deploy-insurance-pool.sh. Empty until deployed.
- INSURANCE_POOL_CONTRACT_ID=${INSURANCE_POOL_CONTRACT_ID:-}
ports:
- "3001:3001"
healthcheck:
Expand All @@ -93,13 +103,49 @@ services:
start_period: 10s
restart: unless-stopped

# Builds and deploys the insurance_pool contract to the local Stellar node,
# then calls initialize(). Opt-in only — it is not part of the default
# `docker compose up` since it installs the Stellar CLI via `cargo install`
# on first run, which can take several minutes.
#
# Usage: docker compose --profile insurance up insurance-pool-deploy
insurance-pool-deploy:
image: rust:1-slim
container_name: iln-insurance-pool-deploy
profiles: ["insurance"]
depends_on:
stellar:
condition: service_healthy
working_dir: /workspace
volumes:
- .:/workspace
- insurance-pool-cargo-cache:/usr/local/cargo/registry
environment:
# Passed through to insurance_pool's initialize(admin, coverage).
# See scripts/deploy-insurance-pool.sh for defaults.
- INSURANCE_POOL_ADMIN=${INSURANCE_POOL_ADMIN:-}
- INSURANCE_POOL_COVERAGE=${INSURANCE_POOL_COVERAGE:-1000000000}
command: >
bash -c "
set -e &&
rustup target add wasm32v1-none &&
command -v stellar >/dev/null 2>&1 || cargo install --locked stellar-cli &&
stellar network add --global local --rpc-url http://stellar:8000 --network-passphrase 'Standalone Network ; February 2021' --override || true &&
stellar keys generate --global alice || true &&
stellar account fund alice --network local || true &&
bash scripts/deploy-insurance-pool.sh local alice
"
restart: "no"

volumes:
stellar-data:
driver: local
indexer-data:
driver: local
notifications-db-data:
driver: local
insurance-pool-cargo-cache:
driver: local

networks:
default:
Expand Down
23 changes: 17 additions & 6 deletions docs/benchmarks.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
# Smart Contract Benchmarks

*Date:* 2026-05-30
*Date:* 2026-05-30 (invoice_liquidity), 2026-07-25 (iln_governance)

These values are baseline metrics for core contract execution: CPU instructions and memory bytes consumed via Soroban's cost meter (`env.cost_estimate()`). CI compares each run against `contracts/invoice_liquidity/benchmarks/baseline.json` and emits a **warning** (not a failure) when either metric regresses by more than 10%.
These values are baseline metrics for core contract execution: CPU instructions and memory bytes consumed via Soroban's cost meter (`env.budget()`). CI compares each run against the contract's `benchmarks/baseline.json` and emits a **warning** (not a failure) when either metric regresses by more than 10%.

## Baseline Execution Results

### `contracts/invoice_liquidity`

| Function | CPU Instructions | Memory (bytes) |
| -------------- | ---------------- | -------------- |
| submit_invoice | 859421 | 26485 |
| fund_invoice | 1041920 | 38190 |
| mark_paid | 948123 | 35480 |

### `contracts/iln_governance`

| Function | CPU Instructions | Memory (bytes) |
| ---------------- | ---------------- | -------------- |
| create_proposal | 220773 | 33354 |
| cast_vote | 252321 | 41057 |
| delegate_votes | 182897 | 28102 |

## Re-Running Locally

```bash
cd contracts/invoice_liquidity
cd contracts/invoice_liquidity # or contracts/iln_governance
cargo test --target x86_64-unknown-linux-gnu benchmark -- --nocapture
```

Expand All @@ -28,15 +38,16 @@ BENCHMARK submit_invoice cpu=859421 mem=26485
## CI Regression Check

```bash
bash scripts/check_benchmark_regression.sh
bash scripts/check_benchmark_regression.sh # checks every contract listed below
bash scripts/check_benchmark_regression.sh contracts/iln_governance:contracts/iln_governance/benchmarks/baseline.json # checks one
```

The script always exits 0. Regressions above the threshold are reported as `::warning::` annotations in GitHub Actions.
The script always exits 0. Regressions above the threshold are reported as `::warning::` annotations in GitHub Actions. It checks `contracts/invoice_liquidity` and `contracts/iln_governance` by default.

## Updating Baselines

After an intentional optimisation or contract change:

1. Run the benchmark suite locally with `--nocapture`.
2. Update `contracts/invoice_liquidity/benchmarks/baseline.json`.
2. Update `contracts/<contract>/benchmarks/baseline.json`.
3. Update the table in this document.
Loading
Loading