From 154aac5a5bc9df2355f3da278baacb0115cc1bc1 Mon Sep 17 00:00:00 2001 From: pchieneye0 Date: Sat, 30 May 2026 14:07:29 +0000 Subject: [PATCH] feat: add Soroban Escrow gas benchmark tool for issue #984 --- benchmarks/README.md | 37 +++++++ benchmarks/soroban-gas-bench.js | 174 ++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 212 insertions(+) create mode 100644 benchmarks/README.md create mode 100755 benchmarks/soroban-gas-bench.js diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 00000000..fa62fdaa --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,37 @@ +# Soroban Gas Benchmark + +This benchmark measures Soroban gas usage for the Escrow contract methods. + +## Purpose + +- Build the `contracts/escrow` Soroban contract. +- Deploy it locally through the Soroban CLI. +- Invoke common contract methods. +- Parse and report gas usage for each method. + +## Usage + +1. Build the Escrow contract: + +```bash +npm run contracts:build +``` + +2. Run the benchmark: + +```bash +npm run bench:soroban-gas +``` + +3. Optional environment variables: + +- `SOROBAN_NETWORK` - Soroban network name, default is `local`. +- `SOROBAN_RPC_URL` - RPC URL to use instead of a named network. +- `SOROBAN_SECRET_KEY` - Secret key used to invoke contract methods. +- `SKIP_BUILD=1` - Skip WASM build if the contract is already compiled. + +## Notes + +- The script requires the Soroban CLI installed and available in `PATH`. +- If the CLI is unavailable, the script will still emit the current WASM size and instructions. +- `soroban` output must include gas metrics for the script to parse them correctly. diff --git a/benchmarks/soroban-gas-bench.js b/benchmarks/soroban-gas-bench.js new file mode 100755 index 00000000..9b9294dd --- /dev/null +++ b/benchmarks/soroban-gas-bench.js @@ -0,0 +1,174 @@ +#!/usr/bin/env node +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const repoRoot = path.resolve(__dirname, '..'); +const wasmPath = path.resolve(repoRoot, 'contracts', 'target', 'wasm32-unknown-unknown', 'release', 'escrow.wasm'); +const methods = ['initialize', 'release', 'refund', 'emergency_refund', 'get_state']; +const networkName = process.env.SOROBAN_NETWORK || 'local'; +const rpcUrl = process.env.SOROBAN_RPC_URL || ''; +const secretKey = process.env.SOROBAN_SECRET_KEY || ''; + +function commandExists(cmd) { + try { + execSync(`command -v ${cmd}`, { stdio: 'ignore' }); + return true; + } catch { + return false; + } +} + +function runCommand(command, args, options = {}) { + const cmd = [command, ...args].join(' '); + return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'inherit'], ...options }); +} + +function buildEscrowWasm() { + if (fs.existsSync(wasmPath)) { + console.log('āœ… Escrow WASM already built:', wasmPath); + return; + } + + if (!commandExists('cargo')) { + throw new Error( + 'Cargo is required to build the Escrow contract, but it was not found in PATH. Install Rust/Cargo or prebuild the contract via scripts/check-wasm.sh.' + ); + } + + console.log('šŸ”Ø Building Escrow contract WASM...'); + runCommand('bash', ['scripts/check-wasm.sh'], { cwd: repoRoot }); + + if (!fs.existsSync(wasmPath)) { + throw new Error(`Expected WASM at ${wasmPath} after build, but it was not found.`); + } + + const sizeKb = (fs.statSync(wasmPath).size / 1024).toFixed(1); + console.log(`āœ… Built escrow.wasm (${sizeKb} KB)`); +} + +function reportWasmSize() { + if (!fs.existsSync(wasmPath)) { + console.log('āš ļø Escrow WASM not found. Run with cargo installed or build manually with scripts/check-wasm.sh.'); + return; + } + + const size = fs.statSync(wasmPath).size; + const sizeKb = (size / 1024).toFixed(1); + console.log(`šŸ“¦ Escrow WASM size: ${size} bytes (${sizeKb} KB)`); +} + +function printHelp() { + console.log('Usage: node benchmarks/soroban-gas-bench.js'); + console.log('Environment variables:'); + console.log(' SOROBAN_NETWORK - soroban network name (default: local)'); + console.log(' SOROBAN_RPC_URL - soroban RPC URL (optional, overrides network)'); + console.log(' SOROBAN_SECRET_KEY - private key for invoking contract methods'); + console.log(' SKIP_BUILD - set to 1 to skip wasm build step'); +} + +function runSorobanCliBenchmark() { + if (!commandExists('soroban')) { + console.log('āš ļø Soroban CLI is not installed. Skipping runtime gas benchmark.'); + console.log(' Install the Soroban CLI and run this script again to collect gas metrics.'); + return; + } + + if (!secretKey) { + console.log('āš ļø Environment variable SOROBAN_SECRET_KEY is required for contract invocation.'); + console.log(' Set SOROBAN_SECRET_KEY to a valid Soroban account secret and rerun the benchmark.'); + return; + } + + console.log(`🌐 Using Soroban network: ${networkName}`); + if (rpcUrl) { + console.log(`šŸ”Œ RPC URL: ${rpcUrl}`); + } + + try { + console.log('šŸš€ Starting Soroban gas benchmark flow...'); + + const deployArgs = ['contract', 'deploy', '--wasm', wasmPath]; + if (rpcUrl) { + deployArgs.push('--rpc-url', rpcUrl); + } else { + deployArgs.push('--network', networkName); + } + + const deployOutput = runCommand('soroban', deployArgs, { cwd: repoRoot }); + const idMatch = deployOutput.match(/(GC[0-9A-Z]{55}|[A-Z0-9]{56})/); + if (!idMatch) { + throw new Error('Failed to parse contract ID from Soroban deploy output.'); + } + const contractId = idMatch[0]; + console.log(`āœ… Deployed Escrow contract id: ${contractId}`); + + const results = {}; + for (const method of methods) { + console.log(`\nā–¶ Measuring gas for method: ${method}`); + const args = method === 'initialize' + ? [ + '--func', 'initialize', + '--args', + 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL7NV', + 'GAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA33', + 'GAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA44', + contractId, + '500000', + '1000', + ] + : ['--func', method]; + + const invokeArgs = [ + 'contract', + 'invoke', + '--id', + contractId, + '--wasm', + wasmPath, + '--secret-key', + secretKey, + ...args, + ]; + if (rpcUrl) { + invokeArgs.push('--rpc-url', rpcUrl); + } else { + invokeArgs.push('--network', networkName); + } + + const output = runCommand('soroban', invokeArgs, { cwd: repoRoot }); + const gasMatch = output.match(/gas(?:Used|Consumed)[:=]\s*(\d+)/i); + results[method] = gasMatch ? Number(gasMatch[1]) : null; + console.log(` ${method}: ${results[method] ?? 'gas data unavailable'}`); + } + + console.log('\nšŸ“Š Soroban Escrow Gas Benchmark Results'); + methods.forEach((method) => { + console.log(` - ${method}: ${results[method] ?? 'unavailable'}`); + }); + } catch (error) { + console.error('āŒ Soroban CLI benchmark failed:', error.message || error); + console.log('Please ensure the Soroban CLI supports `contract deploy` and `contract invoke` for your version.'); + } +} + +function main() { + if (process.argv.includes('--help') || process.argv.includes('-h')) { + printHelp(); + return; + } + + if (process.env.SKIP_BUILD !== '1') { + try { + buildEscrowWasm(); + } catch (error) { + console.error(`Error: ${error.message}`); + process.exit(1); + } + } + + reportWasmSize(); + runSorobanCliBenchmark(); +} + +main(); diff --git a/package.json b/package.json index d5f341d0..4c54b5ee 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "test:load:breakpoint": "k6 run -e SCENARIO=breakpoint tests/load/api.js", "test:load:legacy": "k6 run tests/load/k6/load_test_scenarios.js", "test:bench": "node tests/load/autocannon/benchmark.js", + "bench:soroban-gas": "node benchmarks/soroban-gas-bench.js", "sdk:generate": "echo 'Start dev server first (npm run dev), then run: openapi-generator-cli generate -i http://localhost:3000/docs/openapi.json -c sdk-config.yaml -o sdk'", "sdk:build": "cd sdk && ./gradlew build", "sdk:generate:ts": "openapi-generator-cli generate -i http://localhost:3000/docs/openapi.json -c sdk-config-ts.yaml -o sdk-ts",