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
2 changes: 1 addition & 1 deletion .github/workflows/check-pr-description.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Write PR body to file
env:
Expand Down
12 changes: 8 additions & 4 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Cache dependencies
uses: actions/cache@v6
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.cargo/bin/
Expand All @@ -22,6 +22,10 @@ jobs:
target/
key: ${{ runner.os }}-cargo-docs-${{ hashFiles('**/Cargo.lock') }}

# docs.sh -> build.sh -> wasm-opt
- name: Ensure wasm-opt
run: ./scripts/wasm-opt.sh --ensure-tool

- name: Build documentation
# the `--enable-index-page` flag creates an index.html file at the root of the docs
# the `-Zunstable-options` flag is needed to use `--enable-index-page`
Expand All @@ -30,7 +34,7 @@ jobs:

- name: Upload documentation files as artifact
id: docs-artifact
uses: actions/upload-pages-artifact@v5
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with:
path: target/doc
retention-days: 10
Expand All @@ -47,4 +51,4 @@ jobs:
steps:
- name: Deploy to Documentation to GitHub Pages
id: docs-artifact
uses: actions/deploy-pages@v5
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5
57 changes: 32 additions & 25 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Cache dependencies
uses: actions/cache/restore@v6
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.cargo/bin/
Expand All @@ -28,20 +28,20 @@ jobs:
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- uses: actions-rust-lang/setup-rust-toolchain@v2
- uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
with:
toolchain: stable
components: clippy, rustfmt
# This runs the pre-commit hooks defined in .pre-commit-config.yaml
# TODO: figure out how to update to use pre-commit.ci
- uses: pre-commit/action@v3.0.1
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1

clippy_linting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Cache dependencies
uses: actions/cache/restore@v6
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.cargo/bin/
Expand All @@ -57,26 +57,26 @@ jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Run formatting check
run: ./scripts/fmt.sh

check_wasm_exports:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Check WASM contract exports
run: ./scripts/check-wasm-exports.sh

check_ledger_objects_generated:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Use Node.js v24
uses: actions/setup-node@v7
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24.x

Expand All @@ -91,7 +91,7 @@ jobs:
runs-on: ubuntu-latest
if: false
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Run host function audit
run: ./scripts/host-function-audit.sh
Expand All @@ -103,28 +103,28 @@ jobs:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
- name: Install cargo-deny
uses: taiki-e/install-action@cargo-deny
uses: taiki-e/install-action@3ce020901a1c1292716d3268bf506c1ae55a84cd # cargo-deny
- name: Check RustSec advisories
run: ./scripts/cargo-deny.sh

validate_ui:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Validate UI
run: ./scripts/validate-ui.sh

build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Cache dependencies
uses: actions/cache@v6
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.cargo/bin/
Expand All @@ -134,13 +134,17 @@ jobs:
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

# Compiles Binaryen on a cache miss; lands in the cached ~/.cargo/bin.
- name: Ensure wasm-opt
run: ./scripts/wasm-opt.sh --ensure-tool

- name: Run build and test
run: ./scripts/build-and-test.sh

run-markdown:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Run code in Markdown files
run: ./scripts/run-markdown.sh
Expand All @@ -149,15 +153,15 @@ jobs:
runs-on: ubuntu-latest
needs: build_and_test
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Use Node.js v24
uses: actions/setup-node@v7
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24.x

- name: Cache cargo dependencies
uses: actions/cache@v6
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.cargo/bin/
Expand All @@ -169,7 +173,7 @@ jobs:

- name: Cache Node dependencies
id: cache-nodemodules
uses: actions/cache@v6
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
env:
cache-name: cache-node-modules
with:
Expand All @@ -184,6 +188,9 @@ jobs:
if: steps.cache-nodemodules.outputs.cache-hit != 'true'
run: npm ci

- name: Ensure wasm-opt
run: ./scripts/wasm-opt.sh --ensure-tool

- name: Start rippled in Docker
run: ./scripts/docker-rippled.sh start

Expand All @@ -204,9 +211,9 @@ jobs:
env:
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Cache dependencies
uses: actions/cache/restore@v6
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.cargo/bin/
Expand All @@ -218,6 +225,6 @@ jobs:
- name: Install Rust
run: rustup update stable
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
uses: taiki-e/install-action@72f835688cb6c15903b796f704b01e652a2b8835 # cargo-llvm-cov
- name: Generate code coverage
run: ./scripts/coverage.sh
28 changes: 27 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ DEVNET=true ./scripts/run-tests.sh # run against wss://w
./scripts/generate-ledger-objects.sh
```

Other scripts not part of the primary workflow above: `scripts/benchmark-gas.sh`, `scripts/check-wasm-exports.sh`, `scripts/docs.sh` (builds and deploys the GitHub Pages docs/UI site), `scripts/host-function-audit.sh` (see below), `scripts/run-markdown.sh`, `scripts/validate-ui.sh`, `scripts/cargo-deny.sh` (RustSec advisories on the library workspace; advisory-only in CI until the first clean run).
Other scripts not part of the primary workflow above: `scripts/benchmark-gas.sh`, `scripts/check-wasm-exports.sh`, `scripts/docs.sh` (builds and deploys the GitHub Pages docs/UI site), `scripts/host-function-audit.sh` (see below), `scripts/run-markdown.sh`, `scripts/validate-ui.sh`, `scripts/wasm-opt.sh` (called by `build.sh`; see below), `scripts/cargo-deny.sh` (RustSec advisories on the library workspace; advisory-only in CI until the first clean run).

Pre-commit hooks (`.pre-commit-config.yaml`) run `cargo fmt --all` and `cargo clippy --all-targets --all-features -- -Dclippy::all` on staged Rust files, plus prettier with `--no-semi --tab-width 2` for JS/MD/YAML.

Expand Down Expand Up @@ -151,6 +151,32 @@ panic = "abort" # no_std can't unwind; also avoids pulling in a panic handle

The library defines a custom `#[panic_handler]` for `target_arch = "wasm32"` (in `xrpl-common-stdlib/src/lib.rs`) that calls `core::arch::wasm32::unreachable()`. Dev profile uses `panic = "unwind"` so unit tests can run on the host.

`scripts/build.sh` finishes by running `scripts/wasm-opt.sh`, which rewrites the **release** artifacts of both WASM workspaces in place with Binaryen's `wasm-opt` (~30% smaller, which matters because rippled charges gas per instruction and caps contract size). In place, rather than a separate `.opt.wasm`, so `tests/runSingleTest.js`, `ui/embed-wasm.sh`, and the gas benchmark all exercise the bytes that would ship on-ledger. Debug artifacts are left alone.

There is deliberately **no result caching**. Writing in place breaks the hardlink cargo makes from `deps/`, so cargo re-uplifts the unoptimized artifact on every build β€” meaning each `build.sh` run is already exactly one `wasm-opt` pass over pristine output, and a skip-cache keyed on the file would never legitimately hit. An earlier stamp-file version of this was removed: it cost ~0.7s of savings that never materialized, and it silently skipped re-optimization when `WASM_OPT_FLAGS` or the wasm-opt version changed.

The flag set is `-Oz --mvp-features --strip-debug --strip-producers` β€” **pure MVP, deliberately narrower than `wasm32v1-none`.**
Comment thread
sappenin marked this conversation as resolved.
Resolved

The authority for this is `wasm_engine()` in rippled's `crates/xrpl-wasm-vm/src/vm.rs`. Contracts run on **wasmi**, configured for consensus determinism: every post-MVP proposal is explicitly switched off, one line each β€” `wasm_sign_extension(false)`, `wasm_mutable_global(false)`, `wasm_multi_value(false)`, `wasm_saturating_float_to_int(false)`, `wasm_bulk_memory(false)`, `wasm_reference_types(false)`, `wasm_tail_call(false)`, `wasm_extended_const(false)`, `wasm_multi_memory(false)`, `wasm_wide_arithmetic(false)`, `wasm_custom_page_sizes(false)`, plus `floats(false)` and `allow_start_fn(false)`. The reason is consensus, not engine capability: the accepted module set has to be fixed so every validator accepts exactly the same bytes, so the posture is deny-by-default and a module using anything beyond MVP is `temINVALID_BYTECODE`. **The target's feature list is not the authority β€” that file is.** Mirror it; don't infer the allowed set from `wasm32v1-none`.

Sign-extension is the one that has actually bitten us. `-Oz` introduces `i32.extend8_s`/`i64.extend16_s` on its own, and with `--enable-sign-ext` the e2e suite failed on exactly the four contracts where it chose to (`gas_benchmark`, `trace_escrow_account`, `trace_escrow_finish`, `trace_escrow_ledger_object`) and passed on the other twelve. rustc emits none for any current contract, so wasm-opt was the sole source β€” but that also makes it latent for hand-written code: `wasm32v1-none` advertises sign-ext, so a future rustc or a new contract could emit it and fail on-ledger with wasm-opt uninvolved.

`--enable-mutable-globals` was also dropped. Modules do contain one _internal_ mutable global (the shadow stack pointer), which is fine β€” wasmi's `wasm_mutable_global(false)` only rejects _imported/exported_ mutable globals, and the two globals these contracts export (`__data_end`, `__heap_base`) are immutable. So the flag was a no-op: removing it left output byte-identical. It's gone anyway, because with it present wasm-opt would be permitted to export a mutable global and that would be an on-ledger rejection; without it, the same situation is a loud build-time wasm-opt failure. Same reasoning for floats: `float_tests` contains zero `f32`/`f64` instructions because XRPL float math goes through host functions (`OpaqueFloat`), which is what makes `floats(false)` survivable.

The `target_features` custom section (36 bytes/module) is deliberately **not** stripped, even though `--strip-target-features` would save ~576 bytes total. wasm-opt rewrites it to match the real feature set, so post-fix it reads exactly `+mutable-globals` and nothing else β€” a cheap, greppable record of the host-compatibility contract. rippled ignores it (`ignore_custom_sections(true)`).

`-Oz` specifically is a free choice, not a tuned one: because cargo already applies `opt-level = "s"` + LTO, `-Oz`/`-Os`/`-O3` land within ~0.1% of each other on both size and instruction count, and `-Oz` is never worse on instruction count (on `keylet_exists` it is both smaller and leaner: 10,635 B / 3,891 instrs vs `-O3`'s 11,067 B / 4,088). So the usual "`-Oz` trades gas for size" worry does not apply here β€” relevant because the engine meters per-instruction fuel.

The **tool** is mandatory β€” `scripts/build.sh` calls `wasm-opt.sh` outside its release/debug branch, so even a debug build fails if `wasm-opt` can't be obtained. It resolves one from `$WASM_OPT`, then `PATH`, then `$CARGO_HOME/bin`; finding none it runs `cargo install wasm-opt --version <pin> --locked` and fails the build if that fails. Having no release WASM to optimize is a different matter and exits 0 quietly, so a debug-only build isn't broken by the step β€” but a dir or file named explicitly on the command line that's missing or holds no `.wasm` is treated as a typo and fails. The pinned crate version lives in that script and is the single source of truth: `scripts/setup.sh` and both CI workflows call `./scripts/wasm-opt.sh --ensure-tool` to resolve/install up front rather than stalling a build mid-way. CI's `~/.cargo/bin` cache carries the compiled binary between runs.

The `wasm-opt` crate isn't a reimplementation β€” its binary links Binaryen's real `wasm_opt_main`, so it's the upstream CLI with upstream flags, just built from source (needs a C++17 toolchain, a few minutes on first install). It tracks Binaryen **116** while upstream is at 133; output measured within 0.4% of 132 on every example, so the lag costs nothing that matters. It's pinned because a different Binaryen means different bytes, which means different numbers out of `scripts/benchmark-gas.sh`. A native Binaryen already on `PATH` wins over the pin β€” respecting a developer's install beats a multi-minute compile.

A **cargo-managed** `wasm-opt` (the one at `$CARGO_HOME/bin`), by contrast, is version-checked against the pin and reinstalled if it doesn't match, then re-checked so a bad install fails loudly. This matters because CI caches `~/.cargo/bin` under a key derived from `Cargo.lock`, which bumping `WASM_OPT_CRATE_VERSION` in the script does not change β€” without the check, a pin bump would silently keep using the stale cached binary and the pin would be a lie. The expected Binaryen version is derived from the crate version, which is published as `0.<binaryen>.<patch>`; if that convention ever breaks, fix `PINNED_BINARYEN_VERSION` in the script. An explicit `$WASM_OPT` and a system Binaryen are both exempt from this check.

Note that `cargo install` is invoked with `RUSTFLAGS=""`: the repo-wide `-Dwarnings` would otherwise fail the build on any warning in a third-party crate.

Other knobs: `SKIP_WASM_OPT=true` (the one deliberate opt-out), `WASM_OPT_NO_INSTALL=true` (fail rather than auto-install), `WASM_OPT=<path>`, `WASM_OPT_FLAGS="..."`. The script also takes explicit dirs/files as arguments.

## Writing a contract

Minimal template (see `examples/smart-escrows/hello_world/src/lib.rs`):
Expand Down
5 changes: 4 additions & 1 deletion scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ You can also run individual test suites:
### Main Scripts

- **`build-and-test.sh`** - Build and test the native workspace, build WASM examples, and run craft
- **`build.sh`** - Build all examples (accepts `release` argument for release builds)
- **`build.sh`** - Build all examples (accepts `release` argument for release builds), then shrink the release WASM via `wasm-opt.sh`

### Helper Scripts

Expand All @@ -40,6 +40,7 @@ You can also run individual test suites:
- **`docker-rippled.sh`** - Start/stop/check a local rippled node in Docker, pinned to the same image CI uses; also used by CI itself, so this is the only place the container's `docker run`/health-check/logs logic lives (`start`/`stop`/`status`/`logs`)
- **`host-function-audit.sh`** - Audit host functions against XRPLd (requires Node.js)
- **`cargo-deny.sh`** - Check RustSec advisories on the library workspace (requires `cargo-deny`)
- **`wasm-opt.sh`** - Shrink the built release contracts in place with Binaryen's `wasm-opt` (run automatically at the end of `build.sh`; auto-installs a pinned `wasm-opt` via cargo if none is found, and fails the build if it can't; exits quietly when there's no release WASM to optimize). `--ensure-tool` resolves/installs the tool without optimizing anything.
- **`benchmark-gas.sh`** - Measure and compare gas costs of optimized helper functions
- **`generate-sfields.sh`** - Generate type-safe SField constants from rippled source (requires Node.js)

Expand Down Expand Up @@ -177,6 +178,7 @@ Actions workflows, ensuring perfect consistency between local and CI environment
- **Pre-commit not found**: Run `./scripts/setup.sh` to install dependencies
- **Node.js required**: Install Node.js for the host function audit, or skip that script
- **WASM target missing**: The scripts automatically install the `wasm32v1-none` target
- **Build pauses on "installing wasm-opt via cargo"**: expected on a machine with no `wasm-opt`; it compiles Binaryen from source once (a few minutes, needs a C++17 toolchain). Install a native Binaryen instead (`brew install binaryen` / `apt-get install binaryen`) to skip the compile, or set `SKIP_WASM_OPT=true` to build without the optimization step
- **Docker not found / daemon not reachable**: Install/start Docker, or set `NO_DOCKER=true` (use your own local rippled) or `DEVNET=true` (use WASM Devnet)

## Script Dependencies
Expand All @@ -185,6 +187,7 @@ Actions workflows, ensuring perfect consistency between local and CI environment
setup.sh (run first)
β”œβ”€β”€ build-and-test.sh
β”œβ”€β”€ build.sh
└── wasm-opt.sh (required; auto-installs a pinned wasm-opt via cargo)
β”œβ”€β”€ check-wasm-exports.sh
β”œβ”€β”€ clippy.sh
β”œβ”€β”€ run-tests.sh
Expand Down
3 changes: 3 additions & 0 deletions scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,7 @@ else
fi
cd ..

echo "πŸ—œοΈ Optimizing release WASM..."
./scripts/wasm-opt.sh

echo "βœ… Build completed successfully!"
4 changes: 4 additions & 0 deletions scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ rustup default stable
echo "πŸ“¦ Adding wasm32v1-none target..."
rustup target add wasm32v1-none

# Up front, since installing the crate compiles Binaryen from source.
echo "πŸ“¦ Ensuring wasm-opt (Binaryen) is available..."
./scripts/wasm-opt.sh --ensure-tool

# Note: Pre-commit checks are handled by GitHub Actions using pre-commit/action@v3.0.1
# Local pre-commit installation is optional for development convenience
echo "ℹ️ Pre-commit checks are handled by GitHub Actions"
Expand Down
Loading
Loading