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
130 changes: 130 additions & 0 deletions contracts/teachlink/src/arbitration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,133 @@ impl ArbitrationManager {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::TeachLinkBridge;
use soroban_sdk::testutils::Address as _;

fn make_profile(env: &Env, address: Address, reputation_score: u32) -> ArbitratorProfile {
let mut specialization = Vec::new(env);
specialization.push_back(String::from_str(env, "General"));

let mut dispute_types_handled = Vec::new(env);
dispute_types_handled.push_back(String::from_str(env, "Payment"));

ArbitratorProfile {
address,
name: String::from_str(env, "Arbiter One"),
specialization,
reputation_score,
total_resolved: 0,
dispute_types_handled,
is_active: true,
}
}

fn with_contract<T>(env: &Env, contract_id: &Address, f: impl FnOnce() -> T) -> T {
env.as_contract(contract_id, f)
}

#[test]
fn update_reputation_increases_on_success() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(TeachLinkBridge, ());

let arbitrator = Address::generate(&env);
let profile = make_profile(&env, arbitrator.clone(), 500);

with_contract(&env, &contract_id, || {
ArbitrationManager::register_arbitrator(&env, profile).unwrap();
ArbitrationManager::update_reputation(&env, arbitrator.clone(), true).unwrap();
});

let updated = with_contract(&env, &contract_id, || {
ArbitrationManager::get_arbitrator(&env, arbitrator).unwrap()
});
assert_eq!(updated.reputation_score, 510);
assert_eq!(updated.total_resolved, 1);
}

#[test]
fn update_reputation_decreases_on_failure() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(TeachLinkBridge, ());

let arbitrator = Address::generate(&env);
let profile = make_profile(&env, arbitrator.clone(), 500);

with_contract(&env, &contract_id, || {
ArbitrationManager::register_arbitrator(&env, profile).unwrap();
ArbitrationManager::update_reputation(&env, arbitrator.clone(), false).unwrap();
});

let updated = with_contract(&env, &contract_id, || {
ArbitrationManager::get_arbitrator(&env, arbitrator).unwrap()
});
assert_eq!(updated.reputation_score, 480);
assert_eq!(updated.total_resolved, 1);
}

#[test]
fn update_reputation_respects_upper_boundary() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(TeachLinkBridge, ());

let arbitrator = Address::generate(&env);
let profile = make_profile(&env, arbitrator.clone(), 995);

with_contract(&env, &contract_id, || {
ArbitrationManager::register_arbitrator(&env, profile).unwrap();
ArbitrationManager::update_reputation(&env, arbitrator.clone(), true).unwrap();
});

let updated = with_contract(&env, &contract_id, || {
ArbitrationManager::get_arbitrator(&env, arbitrator).unwrap()
});
assert_eq!(updated.reputation_score, 1000);
assert_eq!(updated.total_resolved, 1);
}

#[test]
fn update_reputation_respects_lower_boundary() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(TeachLinkBridge, ());

let arbitrator = Address::generate(&env);
let profile = make_profile(&env, arbitrator.clone(), 15);

with_contract(&env, &contract_id, || {
ArbitrationManager::register_arbitrator(&env, profile).unwrap();
ArbitrationManager::update_reputation(&env, arbitrator.clone(), false).unwrap();
});

let updated = with_contract(&env, &contract_id, || {
ArbitrationManager::get_arbitrator(&env, arbitrator).unwrap()
});
assert_eq!(updated.reputation_score, 0);
assert_eq!(updated.total_resolved, 1);
}

#[test]
fn update_reputation_is_noop_for_unregistered_arbitrator() {
let env = Env::default();
let contract_id = env.register(TeachLinkBridge, ());
let unregistered = Address::generate(&env);

let result = with_contract(&env, &contract_id, || {
ArbitrationManager::update_reputation(&env, unregistered.clone(), true)
});
assert_eq!(result, Ok(()));

let stored = with_contract(&env, &contract_id, || {
ArbitrationManager::get_arbitrator(&env, unregistered)
});
assert!(stored.is_none());
}
}
78 changes: 78 additions & 0 deletions docs/GAS_OPTIMIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

This document outlines the gas optimization strategies used in the TeachLink Soroban smart contracts and provides guidance for maintaining high performance.

## Scope and Source of Truth

- Gas estimates below are derived from `gas_thresholds.json` and treated as planning budgets.
- Actual runtime usage should be measured with benchmark tests and compared against `gas_baseline.json`.
- CI regression policy currently fails on increases above 10% over baseline and warns above 5%.

## ⛽ Understanding Gas in Soroban

Soroban uses a resource-based metering system. Gas consumption is influenced by:
Expand All @@ -25,6 +31,63 @@ Soroban uses a resource-based metering system. Gas consumption is influenced by:
- Avoid O(n) operations over large collections in contract entrypoints.
- Use pagination or indexing patterns for retrieving large datasets.

## 📦 Per-Operation Cost Estimates

The following instruction and memory budgets are maintained as operational guardrails.

### Contract Operations

| Operation | Max Instructions | Max Memory (bytes) | Notes |
|---|---:|---:|---|
| `initialize` | 500,000 | 50,000 | Contract initialization |
| `add_validator` | 200,000 | 10,000 | Add validator to bridge |
| `add_supported_chain` | 200,000 | 10,000 | Register destination chain |
| `set_bridge_fee` | 150,000 | 5,000 | Update bridge fee |
| `read_query` | 100,000 | 5,000 | Read-only getters |

### Bridge and Cache Operations

| Operation | Max Instructions | Max Memory (bytes) | Notes |
|---|---:|---:|---|
| `bridge_out` | 800,000 | 50,000 | Lock tokens for bridge |
| `complete_bridge` | 1,000,000 | 80,000 | Finalize with signatures |
| `cancel_bridge` | 500,000 | 30,000 | Cancel pending bridge |
| `cache_hit` | 200,000 | 20,000 | Read fresh cached summary |
| `cache_miss_compute` | 1,500,000 | 100,000 | Recompute summary |
| `cache_invalidation` | 150,000 | 5,000 | Invalidate cache entry |

### Consensus and Tokenization

| Operation | Max Instructions | Max Memory (bytes) | Notes |
|---|---:|---:|---|
| `register_validator` | 500,000 | 30,000 | BFT validator registration |
| `create_proposal` | 400,000 | 30,000 | Consensus proposal creation |
| `vote_on_proposal` | 300,000 | 20,000 | Proposal voting |
| `mint_content_token` | 600,000 | 50,000 | NFT mint |
| `transfer_content_token` | 500,000 | 30,000 | NFT transfer |
| `update_metadata` | 400,000 | 30,000 | Metadata update |

### Rewards, Messaging, and Supporting Modules

| Operation | Max Instructions | Max Memory (bytes) | Notes |
|---|---:|---:|---|
| `initialize_rewards` | 400,000 | 20,000 | Rewards setup |
| `fund_reward_pool` | 500,000 | 20,000 | Add pool funds |
| `issue_reward` | 400,000 | 20,000 | Issue reward |
| `claim_rewards` | 600,000 | 30,000 | Claim pending rewards |
| `send_packet` | 700,000 | 50,000 | Cross-chain packet send |
| `deliver_packet` | 500,000 | 30,000 | Mark packet delivered |
| `send_notification` | 350,000 | 20,000 | Notification emit |
| `initialize_mobile_profile` | 400,000 | 30,000 | Mobile profile init |
| `create_audit_record` | 400,000 | 20,000 | Audit record write |

### Deployment Size Costs

| Metric | Limit |
|---|---:|
| WASM warning size | 256,000 bytes |
| WASM hard limit | 307,200 bytes |

## 📊 Gas Benchmarking

We maintain a baseline of gas consumption for all major entrypoints in `gas_baseline.json`.
Expand All @@ -38,6 +101,21 @@ To generate a current gas report, run:
### Thresholds
The `gas_thresholds.json` file defines the maximum allowable gas for each operation. Continuous Integration (CI) will fail if any operation exceeds its defined threshold.

### Interpretation Guide

- A benchmark passing threshold does not guarantee optimality; compare against the previous baseline and module complexity.
- Prioritize optimization work for high-frequency operations first (for example, read queries and bridge completion).
- For regressions, inspect both instruction growth and memory growth. Memory spikes often indicate avoidable temporary allocations.

## ✅ Best Practices and Optimization Tips

- Keep storage access local and minimal: read once, mutate in-memory, write once.
- Use compact types for persisted state and avoid unbounded vectors in hot paths.
- Split expensive workflows into staged transactions where correctness allows it.
- Prefer deterministic branching and avoid deep nested conditionals in high-traffic entrypoints.
- Emit events for observability data that does not need on-chain queryability.
- Measure after every meaningful refactor with `scripts/run_gas_benchmarks.py` and update baselines only after review.

## 📝 Best Practices for Developers

- **Pre-calculate**: Perform heavy computations off-chain if the result can be verified easily on-chain.
Expand Down
Loading