Skip to content
340 changes: 339 additions & 1 deletion app/contract/contracts/quickex/src/bench_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,20 @@
extern crate std;

use crate::{
escrow_id,
storage::{
compact_escrow_storage_footprint_bytes, legacy_escrow_storage_footprint_bytes, put_escrow,
DataKey, PRIVACY_ENABLED_KEY,
},
EscrowEntry, EscrowStatus, QuickexContract, QuickexContractClient,
};
use soroban_sdk::{
testutils::Address as _, token, xdr::ToXdr, Address, Bytes, BytesN, Env, Symbol, Vec,
testutils::{Address as _, Ledger},
token,
xdr::ToXdr,
Address, Bytes, BytesN, Env, Symbol, Vec,
};
use std::{format, string::String, vec::Vec as StdVec};

// ---------------------------------------------------------------------------
// Shared helpers
Expand Down Expand Up @@ -99,6 +104,177 @@ fn print_budget(env: &Env, label: &str) {
std::println!("[bench] {label:<35} cpu={cpu:<12} mem={mem}");
}

#[derive(Clone, Copy)]
struct CoreBenchResult {
operation: &'static str,
cpu_instructions: u64,
memory_bytes: u64,
storage_fee_bytes: u64,
max_cpu_instructions: u64,
max_memory_bytes: u64,
max_storage_fee_bytes: u64,
}

impl CoreBenchResult {
fn assert_within_threshold(self) {
assert!(
self.cpu_instructions <= self.max_cpu_instructions,
"{} CPU instruction regression: actual={} max={}",
self.operation,
self.cpu_instructions,
self.max_cpu_instructions
);
assert!(
self.memory_bytes <= self.max_memory_bytes,
"{} memory regression: actual={} max={}",
self.operation,
self.memory_bytes,
self.max_memory_bytes
);
assert!(
self.storage_fee_bytes <= self.max_storage_fee_bytes,
"{} storage fee regression: actual={} max={}",
self.operation,
self.storage_fee_bytes,
self.max_storage_fee_bytes
);
}
}

fn storage_bytes_for_pair<K: ToXdr + Clone, V: ToXdr + Clone>(
env: &Env,
key: &K,
value: &V,
) -> u64 {
key.clone().to_xdr(env).len() as u64 + value.clone().to_xdr(env).len() as u64
}

fn escrow_storage_fee_bytes(env: &Env, commitment: &BytesN<32>, entry: &EscrowEntry) -> u64 {
let commitment_bytes: Bytes = commitment.clone().into();
storage_bytes_for_pair(env, &DataKey::Escrow(commitment_bytes), entry)
}

fn escrow_id_storage_fee_bytes(env: &Env, escrow_id: &BytesN<32>, commitment: &BytesN<32>) -> u64 {
storage_bytes_for_pair(env, &DataKey::EscrowIdMap(escrow_id.clone()), commitment)
}

fn measured_budget(env: &Env) -> (u64, u64) {
(
env.cost_estimate().budget().cpu_instruction_cost(),
env.cost_estimate().budget().memory_bytes_cost(),
)
}

fn bench_core_op<F>(
env: &Env,
operation: &'static str,
storage_fee_bytes: u64,
max_cpu_instructions: u64,
max_memory_bytes: u64,
max_storage_fee_bytes: u64,
run: F,
) -> CoreBenchResult
where
F: FnOnce(),
{
env.cost_estimate().budget().reset_default();
run();
let (cpu_instructions, memory_bytes) = measured_budget(env);
let result = CoreBenchResult {
operation,
cpu_instructions,
memory_bytes,
storage_fee_bytes,
max_cpu_instructions,
max_memory_bytes,
max_storage_fee_bytes,
};
std::println!(
"[bench-core] {:<8} cpu={} mem={} storage_fee_bytes={}",
operation,
cpu_instructions,
memory_bytes,
storage_fee_bytes
);
result
}

fn write_core_bench_artifacts(results: &[CoreBenchResult]) {
let artifact_dir = match std::env::var("QUICKEX_BENCH_ARTIFACT_DIR") {
Ok(path) => path,
Err(_) => return,
};

std::fs::create_dir_all(&artifact_dir).expect("create benchmark artifact directory");

let mut json = String::from("{\n \"suite\": \"quickex-core-flow-costs\",\n \"results\": [\n");
for (idx, result) in results.iter().enumerate() {
let comma = if idx + 1 == results.len() { "" } else { "," };
json.push_str(&format!(
" {{ \"operation\": \"{}\", \"cpu_instructions\": {}, \"memory_bytes\": {}, \"storage_fee_bytes\": {}, \"thresholds\": {{ \"cpu_instructions\": {}, \"memory_bytes\": {}, \"storage_fee_bytes\": {} }} }}{}\n",
result.operation,
result.cpu_instructions,
result.memory_bytes,
result.storage_fee_bytes,
result.max_cpu_instructions,
result.max_memory_bytes,
result.max_storage_fee_bytes,
comma
));
}
json.push_str(" ]\n}\n");

let mut markdown = String::from(
"# QuickEx Core Flow Cost Benchmarks\n\n| Operation | CPU instructions | Memory bytes | Storage fee bytes | CPU max | Memory max | Storage max |\n| --- | ---: | ---: | ---: | ---: | ---: | ---: |\n",
);
for result in results {
markdown.push_str(&format!(
"| {} | {} | {} | {} | {} | {} | {} |\n",
result.operation,
result.cpu_instructions,
result.memory_bytes,
result.storage_fee_bytes,
result.max_cpu_instructions,
result.max_memory_bytes,
result.max_storage_fee_bytes
));
}

std::fs::write(
format!("{artifact_dir}/quickex-core-benchmarks.json"),
json.as_bytes(),
)
.expect("write benchmark json artifact");
std::fs::write(
format!("{artifact_dir}/quickex-core-benchmarks.md"),
markdown.as_bytes(),
)
.expect("write benchmark markdown artifact");
}

fn expected_escrow_entry(
env: &Env,
token: &Address,
owner: &Address,
amount: i128,
status: EscrowStatus,
expires_at: u64,
arbiter: Option<Address>,
) -> EscrowEntry {
EscrowEntry {
token: token.clone(),
amount_due: amount,
amount_paid: amount,
owner: owner.clone(),
status,
created_at: env.ledger().timestamp(),
expires_at,
arbiter,
arbiters: Vec::new(env),
arbiter_threshold: 0,
}
}

fn legacy_privacy_storage_key(env: &Env, owner: &Address) -> (Symbol, Address) {
(Symbol::new(env, PRIVACY_ENABLED_KEY), owner.clone())
}
Expand All @@ -114,6 +290,168 @@ fn print_storage_delta(label: &str, legacy_bytes: usize, compact_bytes: usize) {
// Hot-path benchmarks
// ---------------------------------------------------------------------------

/// Benchmark: core lifecycle costs for create, fulfill, refund, and dispute.
/// Fails when a cost crosses its checked-in regression threshold.
#[test]
fn bench_core_lifecycle_costs() {
let mut results: StdVec<CoreBenchResult> = StdVec::new();

{
let (env, client) = setup();
let token = create_test_token(&env);
let owner = Address::generate(&env);
let salt = Bytes::from_slice(&env, b"bench_core_create");
let amount: i128 = 1_000_000;
let timeout_secs = 600u64;
let arbiter = Some(Address::generate(&env));
token::StellarAssetClient::new(&env, &token).mint(&owner, &amount);
let commitment = make_commitment(&env, &owner, amount, &salt);
let escrow_id = escrow_id::derive_escrow_id(
&env,
&token,
amount,
&owner,
&salt,
timeout_secs,
&arbiter,
)
.expect("derive escrow id");
let entry = expected_escrow_entry(
&env,
&token,
&owner,
amount,
EscrowStatus::Pending,
env.ledger().timestamp() + timeout_secs,
arbiter.clone(),
);
let storage_fee_bytes = escrow_storage_fee_bytes(&env, &commitment, &entry)
+ escrow_id_storage_fee_bytes(&env, &escrow_id, &commitment);

results.push(bench_core_op(
&env,
"create",
storage_fee_bytes,
600_000,
150_000,
1_000,
|| {
client.deposit(&token, &amount, &owner, &salt, &timeout_secs, &arbiter);
},
));
}

{
let (env, client) = setup();
let token = create_test_token(&env);
let owner = Address::generate(&env);
let salt = Bytes::from_slice(&env, b"bench_core_fulfill");
let amount: i128 = 1_000_000;
let commitment = make_commitment(&env, &owner, amount, &salt);
seed_escrow(
&env,
&client.address,
&token,
&owner,
amount,
commitment.clone(),
);
token::StellarAssetClient::new(&env, &token).mint(&client.address, &amount);
let entry =
expected_escrow_entry(&env, &token, &owner, amount, EscrowStatus::Spent, 0, None);

results.push(bench_core_op(
&env,
"fulfill",
escrow_storage_fee_bytes(&env, &commitment, &entry),
500_000,
100_000,
1_000,
|| {
client.withdraw(&token, &amount, &commitment, &owner, &salt);
},
));
}

{
let (env, client) = setup();
let token = create_test_token(&env);
let owner = Address::generate(&env);
let salt = Bytes::from_slice(&env, b"bench_core_refund");
let amount: i128 = 1_000_000;
let timeout_secs = 10u64;
token::StellarAssetClient::new(&env, &token).mint(&owner, &amount);
let commitment = client.deposit(&token, &amount, &owner, &salt, &timeout_secs, &None);
env.ledger()
.set_timestamp(env.ledger().timestamp() + timeout_secs);
let entry = expected_escrow_entry(
&env,
&token,
&owner,
amount,
EscrowStatus::Refunded,
env.ledger().timestamp(),
None,
);

results.push(bench_core_op(
&env,
"refund",
escrow_storage_fee_bytes(&env, &commitment, &entry),
500_000,
100_000,
1_000,
|| {
client.refund(&commitment, &owner);
},
));
}

{
let (env, client) = setup();
let token = create_test_token(&env);
let owner = Address::generate(&env);
let arbiter = Address::generate(&env);
let salt = Bytes::from_slice(&env, b"bench_core_dispute");
let amount: i128 = 1_000_000;
token::StellarAssetClient::new(&env, &token).mint(&owner, &amount);
let commitment = client.deposit(
&token,
&amount,
&owner,
&salt,
&600u64,
&Some(arbiter.clone()),
);
let entry = expected_escrow_entry(
&env,
&token,
&owner,
amount,
EscrowStatus::Disputed,
env.ledger().timestamp() + 600,
Some(arbiter),
);

results.push(bench_core_op(
&env,
"dispute",
escrow_storage_fee_bytes(&env, &commitment, &entry),
500_000,
100_000,
1_000,
|| {
client.dispute(&commitment);
},
));
}

write_core_bench_artifacts(&results);
for result in results {
result.assert_within_threshold();
}
}

/// Benchmark: create_amount_commitment
/// Deepest hot path — called inside every deposit and withdraw.
#[test]
Expand Down
Loading