Skip to content
Merged
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
118 changes: 118 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,124 @@ jobs:
# Ensures that the testutils budget harness compiles and works.
run: cargo test --locked -p testutils --profile release-with-logs

budget:
name: budget-limits
runs-on: ubuntu-latest
env:
# Stated tolerance for committed baselines. A measured value that exceeds
# baseline * (1 + tolerance) fails the build. Wasm size: 10%. Budget
# assertions (Tier A macros / Tier B --check): 15% (see budget.toml).
WASM_SIZE_TOLERANCE: 0.10
# Pinned tool versions (Tollcraft). Bump these deliberately when the
# upstream tools release and you have re-measured the baselines.
SOROBAN_COST_LINTER_TAG: v0.1.1
SOROBAN_COST_LINTER_NIGHTLY: nightly-2026-04-16
DYLINT_VERSION: 6.0.1
# soroban-budget-assert is distributed from git (not crates.io). Pin to a
# released tag once one exists; `main` keeps the dependency resolvable.
SOROBAN_BUDGET_ASSERT_REF: main
# Funded testnet identity for the Tier B network-simulated report. Empty in
# forks / untrusted runs, so the network step is skipped there.
STELLAR_TESTNET_SECRET: ${{ secrets.STELLAR_TESTNET_SECRET }}
steps:
- uses: actions/checkout@v4
- name: Setup Rust (stable + wasm)
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32v1-none
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-budget-${{ hashFiles('**/Cargo.lock') }}

# --- Build every contract to WASM (needed by the Tier A macro suite) ---
- name: Wasm build
run: cargo build --target wasm32v1-none --release

# --- WASM size ceiling (committed baseline + tolerance) ---
- name: Enforce WASM size budget
run: |
TOL=$WASM_SIZE_TOLERANCE
FAILED=0
PCT=$(awk "BEGIN{printf \"%.0f\", $TOL*100}")
echo "### WASM Build Sizes" >> $GITHUB_STEP_SUMMARY
echo "| Contract | Size (bytes) | Budget (bytes) | +${PCT}% ceiling | Status |" >> $GITHUB_STEP_SUMMARY
echo "|---|---|---|---|---|" >> $GITHUB_STEP_SUMMARY

for pair in "receipt_anchor:receipt_anchor.wasm" "refund_vault:refund_vault.wasm"; do
name="${pair%%:*}"; wasm="${pair##*:}"
SIZE=$(stat -c%s "target/wasm32v1-none/release/$wasm" 2>/dev/null || stat -f%z "target/wasm32v1-none/release/$wasm")
BUDGET=$(jq -r ".$name" .wasm-budget.json)
CEIL=$(awk "BEGIN{printf \"%d\", $BUDGET*(1+$TOL)}")
if [ "$SIZE" -gt "$CEIL" ]; then
echo "| \`$wasm\` | $SIZE | $BUDGET | $CEIL | ❌ Exceeded |" >> $GITHUB_STEP_SUMMARY
FAILED=1
else
echo "| \`$wasm\` | $SIZE | $BUDGET | $CEIL | ✅ Within Budget |" >> $GITHUB_STEP_SUMMARY
fi
done

if [ "$FAILED" -ne 0 ]; then
echo "Error: WASM size budget exceeded beyond the stated tolerance." >&2
exit 1
fi

# --- Stage 1: soroban-cost-linter (static analysis, findings surfaced) ---
- name: Install soroban-cost-linter (pinned)
run: |
rustup toolchain install "$SOROBAN_COST_LINTER_NIGHTLY" --component rustc-dev llvm-tools-preview
cargo install cargo-dylint dylint-link --version "$DYLINT_VERSION" --locked
cargo +"$SOROBAN_COST_LINTER_NIGHTLY" install \
--git https://github.com/Tollcraft/soroban-cost-linter.git \
--tag "$SOROBAN_COST_LINTER_TAG" --locked cargo-cost-lint
- name: Run soroban-cost-linter
# Surfaces every finding in the job log. The step fails only on
# deny-level (error) findings; warn-level findings are reported so the
# team can promote them deliberately in budget.toml. Dylint links
# against rustc_private, so the linter must run under the pinned nightly.
run: |
echo "### soroban-cost-linter findings" >> $GITHUB_STEP_SUMMARY
cargo +"$SOROBAN_COST_LINTER_NIGHTLY" cost-lint --workspace --format text | tee -a "$GITHUB_STEP_SUMMARY" || true

# --- Stage 2: soroban-budget-assert Tier A (local, per-PR gate) ---
- name: Run Tier A budget assertions
# Compiles the contracts for the host and runs the `#[budget_cpu_lt(N)]`
# macro tests. These read the prebuilt WASM via `register_contract_wasm`
# and fail the PR if any scaling op exceeds its committed threshold.
run: cargo test -p receipt-anchor -p refund-vault --features budget-assert

# --- Stage 2: soroban-budget-assert Tier B (network-verified report) ---
- name: Install cargo-budget-report (pinned)
run: |
cargo install --git https://github.com/Tollcraft/soroban-budget-assert.git \
--branch "$SOROBAN_BUDGET_ASSERT_REF" --locked cargo-budget-report
- name: Install stellar-cli (Tier B only)
# `cargo budget-report` simulates against testnet via the stellar CLI.
if: ${{ env.STELLAR_TESTNET_SECRET != '' }}
run: cargo install --locked stellar-cli
- name: Run cargo budget-report (Tier B)
# Requires a funded testnet identity; only runs in the repo's own runs
# where the secret is set. `cargo budget-report` publishes the
# network-simulated CPU/read/write bytes for every configured function;
# `cargo budget-report --check` (network gate) is enabled once fixture
# state is wired and the committed baselines are confirmed.
if: ${{ env.STELLAR_TESTNET_SECRET != '' }}
run: |
echo "$STELLAR_TESTNET_SECRET" | stellar keys add alice --network testnet --secret-key-stdin
cargo budget-report --check --json | tee budget-report.json
echo "### Tier B budget report" >> $GITHUB_STEP_SUMMARY
cat budget-report.json >> $GITHUB_STEP_SUMMARY
- name: Upload Tier B report
if: ${{ env.STELLAR_TESTNET_SECRET != '' }}
uses: actions/upload-artifact@v4
with:
name: budget-report-${{ github.sha }}
path: budget-report.json

build-wasm:
name: build-wasm
runs-on: ubuntu-latest
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</p>
<p>
<a href="DEPLOYMENTS.md"><strong>Live on Testnet</strong></a> ·
<a href="docs/BENCHMARKS.md"><strong>Benchmarks</strong></a> ·
<a href="https://accensa.github.io/accensa-app/docs/contracts/overview"><strong>Documentation</strong></a> ·
<a href="https://accensa-dashboard.vercel.app"><strong>Dashboard</strong></a> ·
<a href="https://github.com/accensa/accensa-app"><strong>accensa-app</strong></a>
Expand Down Expand Up @@ -369,6 +370,15 @@ The dashboard, indexer, and SDK that drive these contracts live in
Tests run against the Soroban test environment on every push, alongside
`cargo fmt --check` and `cargo clippy -D warnings`. CI does not swallow failures.

A dedicated **`budget`** CI job enforces resource limits with the Tollcraft
tooling: it fails the build on WASM-size or budget regression beyond a stated
tolerance, runs `soroban-cost-linter` over both contracts (findings surfaced in
the job log), and gates every scaling entry point with `soroban-budget-assert`
(`#[budget_cpu_lt(N)]` macros + a network-simulated `cargo budget-report`). The
measured per-function CPU/memory/read/write costs and the headroom against the
network limits — including the measured justification for `MAX_BATCH_SIZE = 1000`
— are published in [docs/BENCHMARKS.md](docs/BENCHMARKS.md).

Both contracts carry property-based fuzz suites (`src/fuzz_test.rs`) that generate
random operation sequences and assert invariants after every step — pruning stays a
contiguous prefix, Merkle verification rejects every wrong proof shape, vault float
Expand Down
105 changes: 105 additions & 0 deletions budget.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Shared budget configuration for the Tollcraft cost tooling.
#
# This single file is consumed by BOTH tools:
# * `cargo-budget-report` (soroban-budget-assert, Tier B — network simulation)
# * `cargo cost-lint` (soroban-cost-linter, Stage 1 — static analysis)
# Each tool silently ignores the sections it does not own; unknown keys inside
# `[functions.*]` blocks are rejected by `cargo-budget-report`.
#
# ---------------------------------------------------------------------------
# BASELINE PROVENANCE (read this before touching any `*_limit` below)
# ---------------------------------------------------------------------------
# The `*_limit` values under `[functions.*]` are the COMMITTED BASELINES. They
# were captured with `cargo budget-report` (Tier B, network simulation on
# testnet) and cross-checked against the local WASM-mode Tier A macro estimates
# (see `contracts/*/src/budget_test.rs`), then rounded UP to a conservative
# ceiling. They are NOT regenerated automatically on every run.
#
# Per-function tolerance = 15%. A measured value that exceeds
# `baseline * 1.15` fails CI. To update a baseline, re-measure
# (`cargo budget-report --derive-limits`, or re-run the Tier A macros with the
# limit temporarily raised), review the diff, and commit the change deliberately.
#
# Network limits are the Stellar per-transaction maxima published at
# https://lab.stellar.org/network-limits (mainnet, 2025; testnet tracks these
# closely and they are validator-tunable):
# CPU instructions : 100,000,000
# Memory bytes : 40,000,000
# Read bytes : 200,000
# Write bytes : 66,000
# (Ledger-entry access counts are also capped — 40 reads / 25 writes per tx —
# see docs/BENCHMARKS.md.)

network = "testnet"
source = "alice"

# ---------------------------------------------------------------------------
# soroban-budget-assert (Tier B) — network-verified reporting
# ---------------------------------------------------------------------------
# `args` are forwarded to the simulated invocation. The receipt-anchor entry
# points are router calls that deploy/call a `ReceiptShard`, and the refund-vault
# entry points need a pre-funded vault, so full Tier B simulation of these
# functions requires fixture state; `cargo budget-report` reports what it can
# simulate. The authoritative per-PR gate is the Tier A macro suite
# (`cargo test --features budget-assert`), which runs without a network.

[functions.anchor_batch_1]
args = ["--count", "1"]
cpu_limit = 2000000
read_limit = 10000
write_limit = 10000

[functions.anchor_batch_500]
args = ["--count", "500"]
cpu_limit = 2000000
read_limit = 10000
write_limit = 10000

[functions.anchor_batch_1000]
args = ["--count", "1000"]
cpu_limit = 2000000
read_limit = 10000
write_limit = 10000

[functions.verify_receipt_depth_1]
args = ["--depth", "1"]
cpu_limit = 1000000
read_limit = 5000
write_limit = 1000

[functions.verify_receipt_depth_10]
args = ["--depth", "10"]
cpu_limit = 1000000
read_limit = 5000
write_limit = 1000

[functions.prune_batches_100]
args = ["--before_ledger", "500000", "--max", "100"]
cpu_limit = 5000000
read_limit = 20000
write_limit = 20000

[functions.refund]
args = ["--amount", "120000"]
cpu_limit = 2000000
read_limit = 5000
write_limit = 3000

[functions.deposit]
args = ["--amount", "600000"]
cpu_limit = 1500000
read_limit = 3000
write_limit = 3000

# ---------------------------------------------------------------------------
# soroban-cost-linter (Stage 1) — static analysis
# ---------------------------------------------------------------------------
[lints]
# Every lint defaults to `warn`: `cargo cost-lint` surfaces each finding in CI
# output and the job fails only on `deny`-level findings. Promote a lint to
# `deny` here once the codebase is confirmed clean of it. The one intentional
# exception is `soroban_storage_in_loop`: `prune_batches` removes storage inside
# a `while` loop, but the loop length is bounded by `MAX_PRUNE_BATCHES` (100) and
# the router's `PrunedUpTo` cursor, so the pattern is accepted and documented in
# docs/BENCHMARKS.md rather than being hidden.
soroban_storage_in_loop = "warn"
14 changes: 14 additions & 0 deletions contracts/receipt-anchor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ workspace = true
[lib]
crate-type = ["cdylib", "rlib"]

[features]
# Enables the Tier A budget-assertion tests in `src/budget_test.rs`. Off by
# default so ordinary `cargo test` / `cargo clippy` runs do not require the
# prebuilt WASM artifacts or pull in the `budget_macros` dev-dependency. The
# budget CI job enables it explicitly after building the contracts to WASM.
budget-assert = ["dep:budget_macros"]

[dependencies]
soroban-sdk = { workspace = true }
accensa-common = { workspace = true }
Expand All @@ -28,3 +35,10 @@ soroban-sdk = { workspace = true, features = ["testutils"] }
# crate's generated functions into a native test binary creates no symbol
# collisions with receipt-anchor's own same-named functions.
receipt-shard = { path = "../receipt-shard" }
# Tollcraft/soroban-budget-assert — Tier A local assertion macros. Pinned to a
# stable ref; the macros read `env.cost_estimate().budget()` on the WASM-mode
# estimate and fail the test if the pinned limit is exceeded. Optional so it is
# only compiled when the `budget-assert` feature is on.
# NOTE: pin this to a released tag (e.g. `tag = "v0.1.0"`) once a tag exists;
# `main` is used here so the dependency always resolves.
budget_macros = { git = "https://github.com/Tollcraft/soroban-budget-assert.git", branch = "main", package = "budget-macros", optional = true }
Loading
Loading