From baa39b735b82908b307ace160ce58e8d5c5ffae8 Mon Sep 17 00:00:00 2001 From: "Abdulmalik A." Date: Mon, 27 Apr 2026 10:14:11 +0000 Subject: [PATCH 1/2] test(docs): add arbitration reputation tests and gas budgets Add unit tests for arbitration reputation updates and expand gas optimization documentation with per-operation thresholds and guidance. Closes #323 Closes #316 --- contracts/teachlink/src/arbitration.rs | 100 +++++++++++++++++++++++++ docs/GAS_OPTIMIZATION.md | 78 +++++++++++++++++++ 2 files changed, 178 insertions(+) diff --git a/contracts/teachlink/src/arbitration.rs b/contracts/teachlink/src/arbitration.rs index c810b6ae..10523a72 100644 --- a/contracts/teachlink/src/arbitration.rs +++ b/contracts/teachlink/src/arbitration.rs @@ -121,3 +121,103 @@ impl ArbitrationManager { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + 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, + } + } + + #[test] + fn update_reputation_increases_on_success() { + let env = Env::default(); + env.mock_all_auths(); + + let arbitrator = Address::generate(&env); + let profile = make_profile(&env, arbitrator.clone(), 500); + + ArbitrationManager::register_arbitrator(&env, profile).unwrap(); + ArbitrationManager::update_reputation(&env, arbitrator.clone(), true).unwrap(); + + let updated = 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 arbitrator = Address::generate(&env); + let profile = make_profile(&env, arbitrator.clone(), 500); + + ArbitrationManager::register_arbitrator(&env, profile).unwrap(); + ArbitrationManager::update_reputation(&env, arbitrator.clone(), false).unwrap(); + + let updated = 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 arbitrator = Address::generate(&env); + let profile = make_profile(&env, arbitrator.clone(), 995); + + ArbitrationManager::register_arbitrator(&env, profile).unwrap(); + ArbitrationManager::update_reputation(&env, arbitrator.clone(), true).unwrap(); + + let updated = 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 arbitrator = Address::generate(&env); + let profile = make_profile(&env, arbitrator.clone(), 15); + + ArbitrationManager::register_arbitrator(&env, profile).unwrap(); + ArbitrationManager::update_reputation(&env, arbitrator.clone(), false).unwrap(); + + let updated = 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 unregistered = Address::generate(&env); + + let result = ArbitrationManager::update_reputation(&env, unregistered.clone(), true); + assert_eq!(result, Ok(())); + + let stored = ArbitrationManager::get_arbitrator(&env, unregistered); + assert!(stored.is_none()); + } +} diff --git a/docs/GAS_OPTIMIZATION.md b/docs/GAS_OPTIMIZATION.md index 55b37386..65846410 100644 --- a/docs/GAS_OPTIMIZATION.md +++ b/docs/GAS_OPTIMIZATION.md @@ -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: @@ -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`. @@ -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. From 03eedf43c4830a1900d9a2d93c9c30e664a8450f Mon Sep 17 00:00:00 2001 From: "Abdulmalik A." Date: Mon, 27 Apr 2026 10:48:58 +0000 Subject: [PATCH 2/2] test(arbitration): run reputation tests in contract context Wrap arbitration unit tests in env.as_contract using a registered contract ID to avoid Soroban instance storage panics outside contract execution. --- contracts/teachlink/src/arbitration.rs | 58 +++++++++++++++++++------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/contracts/teachlink/src/arbitration.rs b/contracts/teachlink/src/arbitration.rs index 10523a72..c863d9dc 100644 --- a/contracts/teachlink/src/arbitration.rs +++ b/contracts/teachlink/src/arbitration.rs @@ -125,6 +125,7 @@ impl ArbitrationManager { #[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 { @@ -145,18 +146,27 @@ mod tests { } } + fn with_contract(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); - ArbitrationManager::register_arbitrator(&env, profile).unwrap(); - ArbitrationManager::update_reputation(&env, arbitrator.clone(), true).unwrap(); + with_contract(&env, &contract_id, || { + ArbitrationManager::register_arbitrator(&env, profile).unwrap(); + ArbitrationManager::update_reputation(&env, arbitrator.clone(), true).unwrap(); + }); - let updated = ArbitrationManager::get_arbitrator(&env, arbitrator).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); } @@ -165,14 +175,19 @@ mod tests { 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); - ArbitrationManager::register_arbitrator(&env, profile).unwrap(); - ArbitrationManager::update_reputation(&env, arbitrator.clone(), false).unwrap(); + with_contract(&env, &contract_id, || { + ArbitrationManager::register_arbitrator(&env, profile).unwrap(); + ArbitrationManager::update_reputation(&env, arbitrator.clone(), false).unwrap(); + }); - let updated = ArbitrationManager::get_arbitrator(&env, arbitrator).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); } @@ -181,14 +196,19 @@ mod tests { 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); - ArbitrationManager::register_arbitrator(&env, profile).unwrap(); - ArbitrationManager::update_reputation(&env, arbitrator.clone(), true).unwrap(); + with_contract(&env, &contract_id, || { + ArbitrationManager::register_arbitrator(&env, profile).unwrap(); + ArbitrationManager::update_reputation(&env, arbitrator.clone(), true).unwrap(); + }); - let updated = ArbitrationManager::get_arbitrator(&env, arbitrator).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); } @@ -197,14 +217,19 @@ mod tests { 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); - ArbitrationManager::register_arbitrator(&env, profile).unwrap(); - ArbitrationManager::update_reputation(&env, arbitrator.clone(), false).unwrap(); + with_contract(&env, &contract_id, || { + ArbitrationManager::register_arbitrator(&env, profile).unwrap(); + ArbitrationManager::update_reputation(&env, arbitrator.clone(), false).unwrap(); + }); - let updated = ArbitrationManager::get_arbitrator(&env, arbitrator).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); } @@ -212,12 +237,17 @@ mod tests { #[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 = ArbitrationManager::update_reputation(&env, unregistered.clone(), true); + let result = with_contract(&env, &contract_id, || { + ArbitrationManager::update_reputation(&env, unregistered.clone(), true) + }); assert_eq!(result, Ok(())); - let stored = ArbitrationManager::get_arbitrator(&env, unregistered); + let stored = with_contract(&env, &contract_id, || { + ArbitrationManager::get_arbitrator(&env, unregistered) + }); assert!(stored.is_none()); } }