Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/wasm-size.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: WASM size

on:
pull_request:
push:
branches:
- main

jobs:
contract-size:
name: Check contract WASM sizes
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Rust targets
run: |
rustup update stable
rustup default stable
rustup target add wasm32-unknown-unknown
rustup target add wasm32v1-none

- name: Install Stellar CLI
run: |
curl -L https://github.com/stellar/stellar-cli/releases/download/v27.0.0/stellar-cli-27.0.0-x86_64-unknown-linux-gnu.tar.gz -o /tmp/stellar-cli.tar.gz
mkdir -p /tmp/stellar-cli
tar -xzf /tmp/stellar-cli.tar.gz -C /tmp/stellar-cli
sudo install /tmp/stellar-cli/stellar /usr/local/bin/stellar
stellar --version

- name: Install wasm-opt
run: |
sudo apt-get update
sudo apt-get install -y binaryen
wasm-opt --version

- name: Check WASM sizes
run: bash scripts/measure_sizes.sh --baseline scripts/size_baseline.json --fail-on-increase

- name: Upload size report
if: always()
uses: actions/upload-artifact@v4
with:
name: wasm-size-report
path: |
target/sizes.json
target/wasm32-unknown-unknown/release/optimized/*.wasm
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ include mx/deploy.mk
include mx/upgrade.mk
include mx/tokens.mk

.PHONY: all help clean
.PHONY: all help clean update-size-baseline

all: build test

Expand All @@ -22,6 +22,7 @@ help:
@printf '%s\n' ' make lint'
@printf '%s\n' ' make test'
@printf '%s\n' ' make build'
@printf '%s\n' ' make update-size-baseline'
@printf '%s\n' ' make smoke-prices'
@printf '%s\n' ''
@printf '%s\n' 'Deploy and upgrade:'
Expand All @@ -43,3 +44,7 @@ help:
clean:
cargo clean
rm -rf .deployed

update-size-baseline:
bash scripts/measure_sizes.sh
cp target/sizes.json scripts/size_baseline.json
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,14 +346,29 @@ rustup target add wasm32-unknown-unknown
### 2. Stellar CLI

```bash
# Install from crates.io with wasm optimiser enabled
cargo install --locked stellar-cli --features opt
# Install from crates.io
cargo install --locked stellar-cli

# Verify
stellar --version
```

### 3. Docker (optional — required for local node)
### 3. wasm-opt

`wasm-opt` is required for contract size measurement and CI size checks.

```bash
# macOS
brew install binaryen

# Ubuntu/Debian
sudo apt-get update && sudo apt-get install -y binaryen

# Verify
wasm-opt --version
```

### 4. Docker (optional — required for local node)

[Install Docker Desktop](https://docs.docker.com/get-docker/) if you want to run a fully local Stellar node instead of using testnet.

Expand Down
37 changes: 37 additions & 0 deletions docs/build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Build

## Contract size budget

Soroban contracts are deployed as WASM, and smaller binaries are cheaper to upload, easier to keep within network limits, and less likely to regress when dependencies or generated bindings change. Size tracking keeps those changes visible during review.

Current optimized contract sizes:

<!-- size-table:start -->
| Contract | WASM size (bytes) | Δ from baseline |
| --- | ---: | ---: |
| adl_handler | 15715 | - |
| data_store | 11405 | - |
| deposit_handler | 18273 | - |
| deposit_vault | 3081 | - |
| exchange_router | 22672 | - |
| fee_handler | 9315 | - |
| liquidation_handler | 15471 | - |
| market_factory | 11640 | - |
| market_token | 7212 | - |
| oracle | 13008 | - |
| order_handler | 42897 | - |
| order_vault | 3604 | - |
| reader | 37622 | - |
| referral_storage | 6727 | - |
| role_store | 5393 | - |
| test_faucet | 4202 | - |
| test_token | 6750 | - |
| withdrawal_handler | 14824 | - |
| withdrawal_vault | 2763 | - |
<!-- size-table:end -->

The baseline is stored in `scripts/size_baseline.json` and must be updated manually after intentional size changes by running:

```bash
make update-size-baseline
```
205 changes: 205 additions & 0 deletions scripts/measure_sizes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
printf '%s\n' "Usage: $0 [--baseline <file>] [--fail-on-increase]"
}

baseline=""
fail_on_increase="0"
while [ "$#" -gt 0 ]; do
case "$1" in
--baseline)
if [ "$#" -lt 2 ]; then
usage >&2
exit 2
fi
baseline="$2"
shift 2
;;
--fail-on-increase)
fail_on_increase="1"
shift
;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
exit 2
;;
esac
done

if ! command -v stellar >/dev/null 2>&1; then
printf '%s\n' "error: stellar CLI is required" >&2
exit 1
fi

if ! command -v wasm-opt >/dev/null 2>&1; then
printf '%s\n' "error: wasm-opt is required" >&2
exit 1
fi

json_tool=""
if command -v python3 >/dev/null 2>&1 && python3 -c 'import json' >/dev/null 2>&1; then
json_tool="python3"
elif command -v python >/dev/null 2>&1 && python -c 'import json' >/dev/null 2>&1; then
json_tool="python"
elif command -v node >/dev/null 2>&1 && node -e 'JSON.parse("{}")' >/dev/null 2>&1; then
json_tool="node"
else
printf '%s\n' "error: python3, python, or node is required" >&2
exit 1
fi

if [ -n "$baseline" ] && [ ! -f "$baseline" ]; then
printf 'error: baseline file not found: %s\n' "$baseline" >&2
exit 1
fi

release_dir="target/wasm32-unknown-unknown/release"
mkdir -p "$release_dir"
rm -f "$release_dir"/*.wasm

if stellar contract build --help | grep -q -- '--release'; then
stellar contract build --release --out-dir "$release_dir"
else
stellar contract build --profile release --out-dir "$release_dir"
fi

optimized_dir="$release_dir/optimized"
mkdir -p "$optimized_dir" target

shopt -s nullglob
wasm_files=("$release_dir"/*.wasm)
if [ "${#wasm_files[@]}" -eq 0 ]; then
printf 'error: no WASM files found in %s\n' "$release_dir" >&2
exit 1
fi

rm -f "$optimized_dir"/*.wasm
for wasm in "${wasm_files[@]}"; do
wasm-opt -O3 "$wasm" -o "$optimized_dir/$(basename "$wasm")"
done

optimized_files=("$optimized_dir"/*.wasm)
if [ "$json_tool" = "node" ]; then
node - "$baseline" "$fail_on_increase" "target/sizes.json" "${optimized_files[@]}" <<'JS'
const fs = require("fs");
const path = require("path");

const baselinePath = process.argv[2];
const failOnIncrease = process.argv[3] === "1";
const outputPath = process.argv[4];
const wasmPaths = process.argv.slice(5);

let baseline = null;
if (baselinePath) {
baseline = JSON.parse(fs.readFileSync(baselinePath, "utf8"));
}

const sizes = {};
for (const wasmPath of wasmPaths) {
sizes[path.basename(wasmPath, ".wasm")] = fs.statSync(wasmPath).size;
}

const sortedSizes = Object.fromEntries(
Object.entries(sizes).sort(([left], [right]) => left.localeCompare(right))
);
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, `${JSON.stringify(sortedSizes, null, 2)}\n`);

function deltaText(contract, size) {
if (baseline === null) {
return "-";
}
const oldSize = baseline[contract];
if (oldSize === undefined) {
return "n/a";
}
const diff = size - Number(oldSize);
const pct = Number(oldSize) === 0 ? "n/a" : `${((diff / Number(oldSize)) * 100).toFixed(2).replace(/^(?!-)/, "+")}%`;
return `${diff >= 0 ? "+" : ""}${diff} (${pct})`;
}

console.log("| Contract | WASM size (bytes) | Δ from baseline |");
console.log("| --- | ---: | ---: |");
const increases = [];
for (const [contract, size] of Object.entries(sortedSizes)) {
console.log(`| ${contract} | ${size} | ${deltaText(contract, size)} |`);
if (baseline !== null && baseline[contract] !== undefined) {
const diff = size - Number(baseline[contract]);
if (diff > 0) {
increases.push(`${contract}: +${diff} bytes`);
}
}
}

if (failOnIncrease && increases.length > 0) {
console.error("");
console.error("WASM size increased against baseline:");
for (const increase of increases) {
console.error(`- ${increase}`);
}
process.exit(1);
}
JS
else
"$json_tool" - "$baseline" "$fail_on_increase" "target/sizes.json" "${optimized_files[@]}" <<'PY'
import json
import os
import sys

baseline_path = sys.argv[1]
fail_on_increase = sys.argv[2] == "1"
output_path = sys.argv[3]
wasm_paths = sys.argv[4:]

baseline = None
if baseline_path:
with open(baseline_path, "r", encoding="utf-8") as fh:
baseline = json.load(fh)

sizes = {
os.path.splitext(os.path.basename(path))[0]: os.path.getsize(path)
for path in wasm_paths
}

os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, "w", encoding="utf-8") as fh:
json.dump(dict(sorted(sizes.items())), fh, indent=2)
fh.write("\n")

def delta_text(contract, size):
if baseline is None:
return "-"
old_size = baseline.get(contract)
if old_size is None:
return "n/a"
diff = size - int(old_size)
if old_size == 0:
pct = "n/a"
else:
pct = f"{(diff / int(old_size)) * 100:+.2f}%"
return f"{diff:+d} ({pct})"

print("| Contract | WASM size (bytes) | Δ from baseline |")
print("| --- | ---: | ---: |")
increases = []
for contract, size in sorted(sizes.items()):
print(f"| {contract} | {size} | {delta_text(contract, size)} |")
if baseline is not None and contract in baseline:
diff = size - int(baseline[contract])
if diff > 0:
increases.append(f"{contract}: +{diff} bytes")

if fail_on_increase and increases:
print("", file=sys.stderr)
print("WASM size increased against baseline:", file=sys.stderr)
for increase in increases:
print(f"- {increase}", file=sys.stderr)
sys.exit(1)
PY
fi
21 changes: 21 additions & 0 deletions scripts/size_baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"adl_handler": 15715,
"data_store": 11405,
"deposit_handler": 18273,
"deposit_vault": 3081,
"exchange_router": 22672,
"fee_handler": 9315,
"liquidation_handler": 15471,
"market_factory": 11640,
"market_token": 7212,
"oracle": 13008,
"order_handler": 42897,
"order_vault": 3604,
"reader": 37622,
"referral_storage": 6727,
"role_store": 5393,
"test_faucet": 4202,
"test_token": 6750,
"withdrawal_handler": 14824,
"withdrawal_vault": 2763
}