Compare commits

..

19 Commits

Author SHA1 Message Date
Pratik Mankawde
c6224c48ed Merge branch 'pratik/std-coro/add-coroutine-primitives' into pratik/std-coro/migrate-entry-points
Bring in develop via the upstream branch. Two classes of collision needed
manual resolution:

Namespace case refactor (develop #7933) renamed RPC -> rpc and
xrpl::RPC::Tuning -> xrpl::rpc::tuning. Conflicts were resolved by taking
develop's naming together with this stack's coroutine API (postCoroTask,
CoroTask<void>, no Context::coro):
  - src/test/app/Path_test.cpp
  - src/test/app/PathMPT_test.cpp
  - src/test/jtx/impl/TestHelpers.cpp
  - src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp (no textual
    conflict; stale spellings carried forward on stack-authored lines)

New old-API caller: develop added postCoro and Context::coro uses in
src/test/app/PayChan_test.cpp, which this stack does not otherwise touch,
so it merged without conflict but would not compile once Context::coro is
removed here. Both call sites are migrated to postCoroTask and the
.coro = {} designated initialisers dropped. The kFeeHeavyBurdenRpc
assertions that motivated develop's change are preserved unchanged.
2026-08-19 17:47:30 +01:00
Pratik Mankawde
5fb1429e3a Merge remote-tracking branch 'origin/develop' into pratik/std-coro/add-coroutine-primitives
# Conflicts:
#	.cspell.config.yaml
2026-08-19 17:41:56 +01:00
Pratik Mankawde
61041155d2 Merge branch 'pratik/std-coro/add-coroutine-primitives' into pratik/std-coro/migrate-entry-points 2026-07-27 13:48:08 +01:00
Pratik Mankawde
5ea72b45f1 Merge remote-tracking branch 'origin/develop' into pratik/std-coro/add-coroutine-primitives 2026-07-27 13:47:54 +01:00
Pratik Mankawde
70813b336a Suppress clang-tidy coroutine-capture warnings in pathfinding tests
The Path_test and TestHelpers coroutine lambdas capture locals by
reference, but the caller blocks on Gate::waitFor() until the coroutine
signals completion, so the captures cannot dangle. Add NOLINTNEXTLINE
for cppcoreguidelines-avoid-capturing-lambda-coroutines with a comment
explaining the lifetime guarantee.
2026-07-27 12:33:49 +01:00
Pratik Mankawde
072e11ecd1 Address clang-tidy findings in entry-point migration
- Suppress cppcoreguidelines-avoid-capturing-lambda-coroutines with
  NOLINT where lifetime is guaranteed: GRPCServer (thisShared keeps
  CallData alive), ServerHandler RPC/WS clients (captures by value,
  handler outlives JobQueue jobs), and PathMPT_test (test blocks on
  Gate::waitFor until the coroutine completes).
- Change ServerHandler::processRequest to take Output const& to fix
  cppcoreguidelines-rvalue-reference-param-not-moved.
- Replace std::lock_guard with std::scoped_lock in RipplePathFind.
- Remove redundant includes and default member initializer in
  Context.h, GRPCServer.h, RipplePathFind.cpp.
2026-07-27 12:06:25 +01:00
Pratik Mankawde
62839b1531 Merge branch 'pratik/std-coro/add-coroutine-primitives' into pratik/std-coro/migrate-entry-points 2026-07-27 11:30:34 +01:00
Pratik Mankawde
9bddc54722 Fix clang-tidy violations in coroutine primitives
- NOLINT the compiler-mandated coroutine protocol names (promise_type,
  await_ready, ...) that conflict with readability-identifier-naming and
  readability-convert-member-functions-to-static
- Add [[nodiscard]] to handle(), done(), await_ready()
- Replace std::lock_guard with std::scoped_lock const
- Pass CoroTaskRunner name by value and std::move it; default-init
  runCount_ in-class
- Drop unused <coroutine> include from JobQueue.h; include
  instrumentation.h directly in JobQueueAwaiter.h
- CoroTask_test: kN constant naming, const locals, file-level NOLINT for
  cppcoreguidelines-avoid-capturing-lambda-coroutines (lifetimes are
  gated and joined)
2026-07-27 11:30:16 +01:00
Pratik Mankawde
8c7a27d721 Name the ripple_path_find wait timeout and fire completion on exception
- Replace the magic 30s in doRipplePathFind with
  RPC::Tuning::kPathfindCompletionTimeout.
- In PathRequestManager::updateAll, invoke updateComplete() via a
  ScopeExit guard on the one-shot (hasCompletion) path so the blocked
  RPC handler is released immediately even if doUpdate throws, instead
  of waiting out the full timeout.
2026-07-25 15:05:08 +01:00
Pratik Mankawde
9266c23ef6 Bound concurrent blocking ripple_path_find calls to prevent worker starvation
The no-ledger branch of doRipplePathFind parks its JobQueue worker on a
condition_variable for up to 30s, waiting for a completion that is fired
from a JtUpdatePf job -- which itself needs a free worker to run. With
unbounded JtClientRpc concurrency, enough simultaneous ripple_path_find
calls could park every worker, stalling the entire JobQueue until the
30s timeouts expired. The old Boost.Coroutine implementation suspended
and released the worker, so it did not have this failure mode.

Reuse the RPC::LegacyPathFind guard (already applied on the
ledger-specified branch) to admit at most kMaxPathfindsInProgress
non-admin blocking requests, returning rpcTOO_BUSY beyond that. The
guard stays in scope across the wait.
2026-07-25 12:39:04 +01:00
Pratik Mankawde
5e9ce16de6 Merge branch 'pratik/std-coro/add-coroutine-primitives' into pratik/std-coro/migrate-entry-points 2026-07-25 12:37:14 +01:00
Pratik Mankawde
8d4ea00453 Strengthen CoroTask_test assertions and timeout handling
- testExceptionPropagation now co_awaits an inner CoroTask<void> that
  throws and asserts the rethrown message, covering the
  CoroTask<void>::await_resume rethrow path; the old version could not
  distinguish a throw from a normal return.
- testJobQueueAwaiter now actually awaits the JobQueueAwaiter struct
  (single use per coroutine, per the GCC-12 note) and asserts the full
  ordered step sequence instead of only the terminal value.
- testValueException asserts the caught exception's message.
- All waitFor() timeouts early-return on failure instead of falling
  through to join()/state reads (null-deref and TSAN-race hazards on
  timeout, plus the nSuspend_ assert in ~Env).
- Remove dead shared_ptr locals in testCorrectOrder/testMultipleYields.
- Add missing includes (<array>, <stdexcept>, <string>, <vector>,
  LocalValue.h, CoroTask.h).
2026-07-25 12:36:30 +01:00
Pratik Mankawde
bdcf094171 Guard resume() against completed coroutines and log unhandled exceptions
- resume() now skips the handle resume when the task is null or done
  (duplicate external post() after completion), matching the old
  Coro::resume() 'if (coro_)' guard instead of invoking UB in release
  builds. The runCount_ bookkeeping still runs to balance post().
- Exceptions escaping a top-level coroutine body were captured by
  unhandled_exception() and destroyed unobserved with the frame; they
  are now logged at error level before the frame is released.
- Document that join() may return via the finished_ disjunct while the
  final resume() is still completing its bookkeeping.
2026-07-25 12:33:51 +01:00
Pratik Mankawde
a42e8174d9 Fix check-rename CI failure
- Remove BoostToStdCoroutineSwitchPlan.md and BoostToStdCoroutineTaskList.md
  working documents from the repo root; the rename script rewrites their
  'rippled' references and the job fails on the resulting dirty tree.
- Reword two CoroTask.h comments to use 'xrpld'.
- Drop cspell words (cppcoro, gantt, Pratik, Mankawde) that existed only
  for the removed documents.
2026-07-25 12:30:38 +01:00
Pratik Mankawde
4c18dd867c Merge branch 'pratik/std-coro/add-coroutine-primitives' into pratik/std-coro/migrate-entry-points
Forward-merge the updated coroutine primitives (which themselves carry the
latest develop) into the entry-point migration.

Conflict resolutions:
- .cspell.config.yaml: keep develop's dotfile name, merge word lists.
- Context.h: drop the coro member; infoSub gets a default initializer.
- Application.cpp, ServerHandler.cpp, TestHelpers.cpp, Path_test.cpp,
  PathMPT_test.cpp: adopt develop's designated-initializer JsonContext and
  renamed identifiers (JtClient, kApiVersionIfUnspecified, kMaxSrcCur, Gate).
- GRPCServer: process(coro) becomes processRequest(), posted with
  postCoroTask.
- RipplePathFind.cpp: follow develop's move to handlers/orderbook/ and
  PathRequestManager; the completion state shared with the path-finding
  continuation is now heap-allocated so it outlives an early return.
- AMMTest.cpp: develop moved find_paths_request into TestHelpers.cpp, so the
  local copy is dropped and the migration applied there instead.
2026-07-24 20:52:28 +01:00
Pratik Mankawde
fb6ece56f2 Merge remote-tracking branch 'origin/develop' into pratik/std-coro/add-coroutine-primitives
Resolves the develop rename of cspell.config.yaml and the JobQueue.h
conflicts, and aligns the new coroutine primitives with develop naming
(CreateT, mutex_/mutexRun_, JtClient, forceMultiThread,
beast::unit_test::Suite).

Also addresses two review findings:

- postCoroTask now holds a jobCounter_ reservation for the whole
  function. JobQueue::stop() joins jobCounter_ before asserting
  nSuspend_ == 0, so the reservation closes the window between the
  ++nSuspend_ and the balancing post()/expectEarlyExit(), and doubles
  as the shutdown check.

- YieldPostAwaiter::await_suspend returns a coroutine_handle<>
  (symmetric transfer) instead of resuming inline. This keeps a
  yield loop against a stopping JobQueue from growing the stack
  without bound and avoids touching a frame that resume() may have
  already destroyed.
2026-07-24 20:39:41 +01:00
Pratik Mankawde
0e815aa1ac feat: Migrate production entry points from Boost.Coroutine to C++20 coroutines
Migrate all production coroutine entry points from Boost.Coroutine
to C++20 std::coroutine using the CoroTask/CoroTaskRunner primitives:

- RipplePathFind: Replace Coro suspend/resume with co_await pattern,
  add cv timeout for graceful shutdown.
- ServerHandler: Replace Coro-based processRequest with CoroTask,
  simplify coroutine lifecycle management.
- GRPCServer: Replace Coro with CoroTask for streaming RPC handlers.
- Remove Coro usage from Context.h aggregate initialization.
- Add exception handling in coroutine bodies to prevent unhandled
  exceptions from escaping the coroutine frame.
2026-03-25 15:48:10 +00:00
Pratik Mankawde
21149a81e3 feat: Add C++20 coroutine primitives: CoroTask, CoroTaskRunner, JobQueueAwaiter
Add C++20 std::coroutine based task primitives for the JobQueue:

- CoroTask<T>: A coroutine return type with RAII ownership semantics
  and symmetric transfer for efficient resumption.
- CoroTaskRunner: Manages coroutine lifecycle on the JobQueue with
  suspend/resume tracking, LocalValue preservation, and graceful
  shutdown support.
- JobQueueAwaiter: External awaiter combining yield+post atomically.
- yieldAndPost(): Inline awaiter workaround for GCC-12 codegen bug
  where external awaiters at multiple co_await points corrupt the
  coroutine state machine resume index.
- CoroTask_test: Comprehensive test suite covering task lifecycle,
  suspend/resume, shutdown, and value-returning coroutines.
- BoostToStdCoroutineSwitchPlan.md: Migration plan documentation.
2026-03-25 15:46:44 +00:00
Pratik Mankawde
b78202a99a docs: Add Boost to C++20 coroutine migration plan
Comprehensive migration plan documenting the switch from
Boost.Coroutine2 to C++20 standard coroutines in rippled, including
research analysis, implementation phases, risk assessment, and
testing strategy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 15:44:19 +00:00
250 changed files with 2806 additions and 34937 deletions

View File

@@ -7,7 +7,6 @@ ignorePaths:
- cmake/**
- LICENSE.md
- .clang-tidy
- src/test/app/wasm_fixtures/*.c
- nix/check-tools/*.txt # generated, and full of Nix store hashes
language: en
allowCompoundWords: true # TODO (#6334)
@@ -69,7 +68,6 @@ words:
- Btrfs
- Buildx
- canonicality
- cdylib
- canonicalised
- cctools
- changespq
@@ -107,7 +105,6 @@ words:
- deleteme
- demultiplexer
- deserializaton
- desugars
- desync
- desynced
- determ
@@ -134,7 +131,6 @@ words:
- gcov
- gcovr
- ghead
- gmock
- Gnutella
- godexsoft
- gpgcheck
@@ -144,9 +140,7 @@ words:
- hwaddress
- hwrap
- ifndef
- impls
- inequation
- initialiser
- insuf
- insuff
- invasively
@@ -256,13 +250,14 @@ words:
- pyparsing
- qalloc
- qbsprofile
- qself
- queuable
- Raphson
- rcflags
- replayer
- repodata
- repomd
- repost
- reposts
- rerandomize
- rerandomization
- rerandomized
@@ -310,11 +305,11 @@ words:
- sponsees
- SRPMS
- sslws
- stackful
- statsd
- STATSDCOLLECTOR
- stissue
- stnum
- stnumber
- stobj
- stobject
- stpath
@@ -355,7 +350,6 @@ words:
- unflatten
- unfund
- unimpair
- unmetered
- unroutable
- unscalable
- unserviced
@@ -376,8 +370,6 @@ words:
- vfalco
- vinnie
- wasmi
- wasmparser
- Werror
- wextra
- wptr
- writeme
@@ -386,7 +378,6 @@ words:
- xbridge
- xchain
- xcrun
- xfloat
- ximinez
- XMACRO
- xored

View File

@@ -1,38 +0,0 @@
name: Use cargo artifacts cache
description: >
Cache the cargo build artifacts with rust-cache. Never caches ~/.cargo/bin:
when saving the cache, rust-cache deletes all binaries that were already
present there, which on persistent self-hosted runners wipes the tools
installed by prepare-runner. Harmless on ephemeral runners, but kept
consistent everywhere.
inputs:
workspaces:
description: "Workspaces to cache, as 'workspace -> target' lines."
required: false
default: crates
key:
description: "Additional part of the cache key."
required: false
default: ""
cache-directories:
description: "Additional non-workspace directories to cache."
required: false
default: ""
save-if:
description: "Condition for saving the cache after the job."
required: false
default: "true"
runs:
using: composite
steps:
- name: Use cargo artifacts cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
cache-bin: "false"
cache-directories: ${{ inputs.cache-directories }}
key: ${{ inputs.key }}
save-if: ${{ inputs.save-if }}
workspaces: ${{ inputs.workspaces }}

View File

@@ -4,7 +4,6 @@ updates:
directories:
- /
- .github/actions/build-deps/
- .github/actions/cargo-cache/
- .github/actions/release-info/
- .github/actions/set-compiler-env/
- .github/actions/setup-conan/

View File

@@ -12,6 +12,7 @@ _BASE_CMAKE_ARGS = [
"-Dwerr=ON",
"-Dxrpld=ON",
"-Dwextra=ON",
"-Drust=ON",
]
# Maps sanitizer names (as used in cmake) to short config-name suffixes.

View File

@@ -58,7 +58,7 @@ jobs:
base_image: debian:bookworm
- name: rhel
base_image: registry.access.redhat.com/ubi9/ubi:latest
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@9e7e4e80af9e684c116b38369add8eea64451f32
with:
image_name: xrpld/nix-${{ matrix.distro.name }}
dockerfile: nix/docker/Dockerfile

View File

@@ -39,7 +39,7 @@ jobs:
# AlmaLinux rather than UBI9, which does not ship rpm-sign.
- name: rhel
base_image: almalinux:9
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@9e7e4e80af9e684c116b38369add8eea64451f32
with:
image_name: xrpld/packaging-${{ matrix.distro.name }}
dockerfile: package/Dockerfile

View File

@@ -30,7 +30,7 @@ jobs:
permissions:
contents: read
packages: write
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@9e7e4e80af9e684c116b38369add8eea64451f32
with:
image_name: xrpld/pre-commit
dockerfile: bin/pre-commit/Dockerfile

View File

@@ -79,7 +79,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c
with:
enable_ccache: false

View File

@@ -14,7 +14,7 @@ on:
jobs:
# Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks.
run-hooks:
uses: XRPLF/actions/.github/workflows/pre-commit.yml@f1952595d212e86169935135efc66294b4574131
uses: XRPLF/actions/.github/workflows/pre-commit.yml@3ba08d6ddf114092891d48491fc2e26c3ba15552
with:
runs_on: ubuntu-latest
container: '{ "image": "ghcr.io/xrplf/xrpld/pre-commit:sha-f56b79f" }'

View File

@@ -47,7 +47,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c
with:
enable_ccache: false

View File

@@ -129,7 +129,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c
with:
enable_ccache: ${{ inputs.ccache_enabled }}
@@ -163,7 +163,7 @@ jobs:
compiler: ${{ inputs.compiler }}
- name: Use cargo artifacts cache
uses: ./.github/actions/cargo-cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
cache-directories: ${{ env.BUILD_DIR }}/corrosion
key: ${{ inputs.config_name }}
@@ -373,10 +373,7 @@ jobs:
- name: Run Rust tests
if: ${{ !inputs.build_only }}
working-directory: crates
# `xrpl-wasm-vm-ffi` is left out on Windows: its tests link as an executable, and
# MSVC - unlike the Unix linkers - will not dead-strip the never-called cxx wrappers
# whose C++ shims only the CMake build defines. The other runners cover these tests.
run: cargo nextest run --workspace --all-features --locked --no-tests=warn ${{ runner.os == 'Windows' && '--exclude xrpl-wasm-vm-ffi' || '' }}
run: cargo nextest run --workspace --all-features --locked --no-tests=warn
# Smoke-run every benchmark module with a single repetition to confirm the
# benchmarks still build and execute. This is a correctness check, not a

View File

@@ -27,7 +27,7 @@ jobs:
determine-files:
permissions:
contents: read
uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@70145243b905dc3e040a61d39c00e178cfb96f71
uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@d041ac9f1fa9f07a4ba335eb4c1c82233fb3fef6
run-clang-tidy:
name: Run clang tidy
@@ -43,7 +43,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c
with:
enable_ccache: false
@@ -60,7 +60,7 @@ jobs:
compiler: ${{ env.COMPILER }}
- name: Use cargo artifacts cache
uses: ./.github/actions/cargo-cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
cache-directories: ${{ env.BUILD_DIR }}/corrosion
save-if: ${{ github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/release') }}
@@ -87,6 +87,7 @@ jobs:
-Dwerr=ON \
-Dxrpld=ON \
-Dverify_headers=ON \
-Drust=ON \
..
- name: Build clang-tidy prerequisites

View File

@@ -33,7 +33,9 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Use cargo artifacts cache
uses: ./.github/actions/cargo-cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: crates
- name: Run clippy
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
@@ -46,7 +48,9 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Use cargo artifacts cache
uses: ./.github/actions/cargo-cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: crates
- name: Generate coverage report
run: cargo llvm-cov nextest --workspace --all-features --locked --no-tests=warn --lcov --output-path lcov.info
@@ -72,7 +76,9 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Use cargo artifacts cache
uses: ./.github/actions/cargo-cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: crates
- name: Build documentation
env:

View File

@@ -68,7 +68,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
uses: XRPLF/actions/prepare-runner@c00c22ada3bd6bcda48fcb0d62fbbab49fec8a0f
with:
enable_ccache: false

View File

@@ -1,6 +1,6 @@
| :warning: **WARNING** :warning: |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| These instructions assume you have a C++ development environment ready with Git, Python, Conan, CMake, Rust, and a C++ compiler. For help setting one up on Linux, macOS, or Windows, [see this guide](./docs/build/environment.md).<br><br>These instructions also assume a basic familiarity with Conan and CMake. If you are unfamiliar with Conan, you can read our [crash course](./docs/build/conan.md) or the official [Getting Started][conan-getting-started] walkthrough. |
| :warning: **WARNING** :warning: |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| These instructions assume you have a C++ development environment ready with Git, Python, Conan, CMake, and a C++ compiler. For help setting one up on Linux, macOS, or Windows, [see this guide](./docs/build/environment.md).<br><br>These instructions also assume a basic familiarity with Conan and CMake. If you are unfamiliar with Conan, you can read our [crash course](./docs/build/conan.md) or the official [Getting Started][conan-getting-started] walkthrough. |
## Minimum Requirements
@@ -304,6 +304,7 @@ See [Sanitizers docs](./docs/build/sanitizers.md) for more details.
| ---------------- | ------------- | ----------------------------------------------------------------------------- |
| `assert` | OFF | Force enabling assertions. |
| `coverage` | OFF | Prepare the coverage report. |
| `rust` | OFF | Build the Rust crates and the C++ code that depends on them. |
| `tests` | OFF | Build tests. |
| `unity` | OFF | Configure a unity build. |
| `verify_headers` | ON | Make the `verify-headers` target available to compile each header on its own. |
@@ -318,15 +319,23 @@ builds may be faster for incremental builds, and can be helpful for detecting
### Rust crates
The build compiles the Rust workspace in `crates/` and generates the cxxbridge
bindings the C++ side includes, so it needs a Rust toolchain (`cargo`, `rustc`)
at the channel pinned in [`rust-toolchain.toml`](./rust-toolchain.toml). The
[Nix development shell](./docs/build/nix.md) provides one; otherwise install it
as described in [Rust](./docs/build/environment.md#rust).
The Rust crates in `crates/` are only part of the build when `rust` is ON. With
`-Drust=OFF` (the default) the `crates` directory is not added to the build, no
cxxbridge bindings are generated, and the C++ tests that exercise the Rust
interop are not compiled — so no Rust toolchain is needed. CI builds always pass
`-Drust=ON`.
With `-Drust=ON` you need one extra dependency: a Rust toolchain (`cargo`,
`rustc`) matching the channel pinned in
[`rust-toolchain.toml`](./rust-toolchain.toml), which compiles the crates and
generates the cxxbridge bindings. It is provided by the
[Nix development shell](./docs/build/nix.md), so `-Drust=ON` works there without
any extra setup; otherwise install it as described in
[Rust](./docs/build/environment.md#rust).
The crates also have their own Rust unit tests. Those are run with `cargo` and
need only the Rust toolchain, independently of CMake (CI runs them with
`cargo nextest`):
need only the Rust toolchain, independently of CMake and of the `rust` option
(CI runs them with `cargo nextest`):
```bash
cargo test --manifest-path crates/Cargo.toml --workspace

View File

@@ -160,8 +160,11 @@ endif()
add_custom_target(tidy_prerequisites)
add_subdirectory(crates)
if(rust)
add_subdirectory(crates)
endif()
include(XrplCore)
include(XrplProtocolAutogen)
include(XrplInstall)
include(XrplValidatorKeys)

View File

@@ -321,7 +321,7 @@ See the [environment setup guide](./docs/build/environment.md#clang-tidy) for ho
### Running clang-tidy locally
Before running clang-tidy, you must generate the files it depends on (protobuf headers and the cxxbridge headers from the Rust crates). Configure the project as described in [`BUILD.md`](./BUILD.md), then build the `tidy_prerequisites` target, which generates all of them:
Before running clang-tidy, you must generate the files it depends on (protobuf headers, and, when the project is configured with `-Drust=ON`, the cxxbridge headers from the Rust crates). Configure the project as described in [`BUILD.md`](./BUILD.md), then build the `tidy_prerequisites` target, which generates all of them:
```bash
cmake --build build --target tidy_prerequisites

View File

@@ -120,10 +120,7 @@ if(MSVC)
_SILENCE_ALL_CXX17_DEPRECATION_WARNINGS
$<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CONFIG:Debug>>:_CRTDBG_MAP_ALLOC>
)
target_link_libraries(
common
INTERFACE -errorreport:none -machine:X64 -ignore:4099
)
target_link_libraries(common INTERFACE -errorreport:none -machine:X64)
else()
target_compile_options(
common

View File

@@ -207,11 +207,7 @@ target_link_libraries(
)
add_module(xrpl tx)
target_link_libraries(
xrpl.libxrpl.tx
PUBLIC xrpl.libxrpl.ledger xrpl_wasm_vm_ffi_cxxbridge
)
add_dependencies(xrpl.libxrpl.tx xrpl_crates)
target_link_libraries(xrpl.libxrpl.tx PUBLIC xrpl.libxrpl.ledger)
add_module(xrpl consensus)
target_link_libraries(

View File

@@ -32,6 +32,11 @@ endif()
option(benchmark "Build benchmarks" ON)
# When OFF, the crates directory is not added to the build at all: no Rust
# toolchain is required, no cxxbridge bindings are generated, and the C++ tests
# that consume those bindings are left out of the build tree.
option(rust "Build the Rust crates and the C++ code that depends on them" OFF)
# Enabled by default so every header is compiled on its own as the main file of
# its own compile_commands.json entry - this is what lets clang-tidy (and clangd
# and IDEs) analyse a header's own includes directly. The per-header objects are

View File

@@ -152,12 +152,8 @@ class Xrpl(ConanFile):
"CMakeLists.txt",
"cfg/*",
"cmake/*",
"crates/*",
"crates/.cargo/*",
"!crates/target/*",
"external/*",
"include/*",
"rust-toolchain.toml",
"src/*",
)

View File

@@ -101,11 +101,4 @@ function(add_xrpl_crate name)
add_dependencies(xrpl_crates ${name}_cxxbridge)
endfunction()
add_xrpl_crate(xrpl_wasm_vm_ffi CRATE xrpl_wasm_vm_ffi FILES lib.rs)
add_xrpl_crate(xrpl_wasm_testkit CRATE xrpl_wasm_testkit FILES lib.rs)
target_include_directories(
xrpl_wasm_vm_ffi_cxxbridge
PRIVATE ${CMAKE_SOURCE_DIR}/include
)
add_xrpl_crate(rs_hello_world CRATE rs_hello_world FILES lib.rs)

234
crates/Cargo.lock generated
View File

@@ -8,18 +8,6 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cc"
version = "1.2.61"
@@ -69,24 +57,24 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.199"
version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824894a4a85dca76d4c95c2b9098c036f5a29f627b30c12780774f6654e60974"
checksum = "6fe442a792c7c736eea18b32a7f8a3b63cf8aafabda6760042dc2fdeda456291"
dependencies = [
"cc",
"cxx-build",
"cxxbridge-cmd",
"cxxbridge-flags",
"cxxbridge-macro",
"foldhash 0.2.0",
"foldhash",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.199"
version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1ae0b651ea5b0000b19513aef5a03f194d7e3486f2d9258b658da8677fe9036"
checksum = "e3184a94384c663718698311a78a51ac00c484c10b4eeac06fb0a068c5f64fa2"
dependencies = [
"cc",
"codespan-reporting",
@@ -99,9 +87,9 @@ dependencies = [
[[package]]
name = "cxxbridge-cmd"
version = "1.0.199"
version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb05f91d3fb8435d9bab6ac5ce6ac1868be774325fb7fb2a91be39393b21388e"
checksum = "0148d8fd1199329ddf1d157a5e134e51ceff37c6a7ddd38615c399d81cb05d8d"
dependencies = [
"clap",
"codespan-reporting",
@@ -113,15 +101,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.199"
version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf293202e0e3e98495785745389e8d0755b217e66f19194a5c695c25e03282ef"
checksum = "52850339faed2eaadd24e286dc1d8268cc6f8a7bd9524d713adc9099566b4c89"
[[package]]
name = "cxxbridge-macro"
version = "1.0.199"
version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca001d746947c7249ed9d332a10f7a59daedbafeb0ec68c5c18a7db7a93f6ccc"
checksum = "2c77c856545d886c9bd5215409ebb63b925e262135248b50c79e5a5f194ee47c"
dependencies = [
"indexmap",
"proc-macro2",
@@ -141,27 +129,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash 0.1.5",
]
[[package]]
name = "hashbrown"
version = "0.17.0"
@@ -175,21 +148,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown 0.17.0",
"hashbrown",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "link-cplusplus"
version = "1.0.12"
@@ -199,12 +160,6 @@ dependencies = [
"cc",
]
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -223,18 +178,19 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rs-hello_world"
version = "0.1.0"
dependencies = [
"cxx",
]
[[package]]
name = "scratch"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2"
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.228"
@@ -271,22 +227,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "spin"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
[[package]]
name = "string-interner"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0"
dependencies = [
"hashbrown 0.15.5",
"serde",
]
[[package]]
name = "strsim"
version = "0.11.1"
@@ -336,99 +276,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "wasm-encoder"
version = "0.254.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09480d646178e5fdd12bb06e812d0af9a3a191dbc9cd697fdc86687beade7393"
dependencies = [
"leb128fmt",
"wasmparser 0.254.0",
]
[[package]]
name = "wasmi"
version = "2.0.0-beta.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab57cbb8db5ee46c6667b642544d7664adfbc0ea6a1ab219c92d734b795f36b1"
dependencies = [
"spin",
"wasmi_collections",
"wasmi_core",
"wasmi_ir",
"wasmparser 0.228.0",
]
[[package]]
name = "wasmi_collections"
version = "2.0.0-beta.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ea3ee266456966465c55a1f440e33116caf2b05a4fc30da36cb0c9813059d5"
dependencies = [
"string-interner",
]
[[package]]
name = "wasmi_core"
version = "2.0.0-beta.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f8285efe48a9e1afbcdfcc19cd807b3eb20129b7e199c7a99efd30ba192926b"
dependencies = [
"libm",
]
[[package]]
name = "wasmi_ir"
version = "2.0.0-beta.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6227be1aebba39b4815ab6a312d0528590f0db2473621ec9285606410889b0a6"
dependencies = [
"wasmi_core",
]
[[package]]
name = "wasmparser"
version = "0.228.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3"
dependencies = [
"bitflags",
"indexmap",
]
[[package]]
name = "wasmparser"
version = "0.254.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27"
dependencies = [
"bitflags",
"indexmap",
"semver",
]
[[package]]
name = "wast"
version = "254.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ed4dfc8f6b9fc38b231065e2cdfbf7359af5ab945990abf09658dcc63c3e32"
dependencies = [
"bumpalo",
"leb128fmt",
"memchr",
"unicode-width",
"wasm-encoder",
]
[[package]]
name = "wat"
version = "1.254.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7127f7f9b8f127c879991cecd35f494e4628bae1b0874c681414d8d8831e952c"
dependencies = [
"wast",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
@@ -452,46 +299,3 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "xrpl-host-functions"
version = "0.1.0"
dependencies = [
"xrpl-host-functions-macros",
]
[[package]]
name = "xrpl-host-functions-macros"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
"xrpl-host-functions",
]
[[package]]
name = "xrpl-wasm-testkit"
version = "0.1.0"
dependencies = [
"cxx",
"wat",
]
[[package]]
name = "xrpl-wasm-vm"
version = "0.1.0"
dependencies = [
"wasmi",
"wat",
"xrpl-host-functions",
]
[[package]]
name = "xrpl-wasm-vm-ffi"
version = "0.1.0"
dependencies = [
"cxx",
"xrpl-host-functions",
"xrpl-wasm-vm",
]

View File

@@ -1,15 +1,9 @@
[workspace]
members = [
"xrpl-wasm-vm-ffi",
"xrpl-wasm-vm",
"xrpl-wasm-testkit",
"xrpl-host-functions",
"xrpl-host-functions-macros",
]
members = ["hello_world"]
resolver = "3"
[workspace.dependencies]
cxx = { version = "1.0.199", features = ["c++20"] }
cxx = { version = "1.0.198", features = ["c++20"] }
[workspace.package]
edition = "2024"

View File

@@ -1,11 +1,10 @@
[package]
name = "xrpl-wasm-testkit"
name = "rs-hello_world"
version = "0.1.0"
edition.workspace = true
[lib]
crate-type = ["staticlib", "rlib"]
crate-type = ["staticlib"]
[dependencies]
cxx.workspace = true
wat = "1"

View File

@@ -0,0 +1,10 @@
#[cxx::bridge(namespace = "rs::hello_world")]
mod ffi {
extern "Rust" {
fn hello_world() -> String;
}
}
pub fn hello_world() -> String {
"hello_world".to_string()
}

View File

@@ -1,18 +0,0 @@
[package]
name = "xrpl-host-functions-macros"
version = "0.1.0"
edition.workspace = true
[lib]
proc-macro = true
[dependencies]
syn = { version = "3", features = ["full"] }
quote = "1"
proc-macro2 = "1"
# The doctest declares host functions returning `HostResult`, which the facade
# crate hand-writes. Cargo allows this cycle because dev-dependencies are outside
# the library build graph.
[dev-dependencies]
xrpl-host-functions.path = "../xrpl-host-functions"

View File

@@ -1,12 +0,0 @@
/// Folds accumulated diagnostics into the single error a macro can return.
///
/// `syn::Error` is itself a collection: `combine` appends, and
/// `into_compile_error` emits one `compile_error!` per recorded span. Folding
/// instead of returning the first error means every mistake in a
/// `host_functions!` block surfaces in one build rather than one per rebuild.
pub(crate) fn combine(errors: Vec<syn::Error>) -> Option<syn::Error> {
errors.into_iter().reduce(|mut first, next| {
first.combine(next);
first
})
}

View File

@@ -1,405 +0,0 @@
mod errors;
mod parsed_host_function;
use std::collections::HashSet;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
TraitItemFn,
parse::{Parse, ParseStream},
parse2,
};
use parsed_host_function::ParsedHostFunction;
/// Declares the wasm host ABI once, and generates everything that follows from it.
///
/// The input is a block of `fn` declarations, each carrying the gas cost the host
/// charges before the call and the name the guest imports it under. Doc comments
/// are kept and appear on the generated items.
///
/// This crate is an implementation detail of `xrpl-host-functions`, which
/// hand-writes the types the declarations refer to and holds the one declaration
/// block.
///
/// # What it generates
///
/// Three items, in the scope the block is written in:
///
/// - `pub trait HostFunctions`: one method per declaration, emitted verbatim —
/// receiver, parameters, return type and doc comment exactly as written. An
/// execution environment implements it; the rest of the expansion does not
/// mention it.
/// - `pub enum HostFunctionSpec`: one variant per declaration, named by
/// PascalCasing the function name (`get_ledger_sqn` becomes `GetLedgerSqn`) and
/// carrying that declaration's doc comment. Its `const fn wasm_name` and
/// `const fn gas` are the ABI metadata, and `ALL` is every variant in
/// declaration order — what a wasm engine iterates to build its import table.
/// - `struct HostFnSpec`: private, one row of that metadata table. It exists only
/// so `wasm_name` and `gas` read from a single `match` over the declarations,
/// and never appears in a signature a caller can name.
///
/// The expansion introduces no other name and reaches for none: the only paths in
/// it are `Self::Variant` and whatever the declarations themselves spell. So the
/// block compiles wherever the types it names — `HostResult` above — resolve.
///
/// ```
/// use xrpl_host_functions::HostResult;
/// use xrpl_host_functions_macros::host_functions;
///
/// host_functions! {
/// /// The sequence number of the ledger being built, as 4 little-endian bytes.
/// #[gas = 60]
/// #[wasm_name = "ldgr_index"]
/// fn get_ledger_sqn(&self, out: &mut [u8]) -> HostResult<usize>;
///
/// /// Writes `msg` to the trace log.
/// #[gas = 500]
/// #[wasm_name = "trace_num"]
/// fn trace_num(&self, msg: &str, number: i64) -> HostResult<()>;
/// }
///
/// // The trait's methods are the declarations, down to the `&self` receiver the
/// // VM calls the host through.
/// fn ledger_sqn(host: &dyn HostFunctions, out: &mut [u8]) -> HostResult<usize> {
/// host.get_ledger_sqn(out)
/// }
///
/// // The metadata is a `const` table, so gas and import names are available at
/// // compile time rather than looked up at run time.
/// const TRACE_GAS: u64 = HostFunctionSpec::TraceNum.gas();
/// assert_eq!(TRACE_GAS, 500);
///
/// assert_eq!(HostFunctionSpec::GetLedgerSqn.wasm_name(), "ldgr_index");
/// assert_eq!(
/// HostFunctionSpec::ALL,
/// &[HostFunctionSpec::GetLedgerSqn, HostFunctionSpec::TraceNum],
/// );
/// ```
///
/// A declaration must be a plain `fn` taking `&self` and returning
/// `HostResult<T>`, with no body and no generics: it maps to exactly one wasm
/// import signature. Two declarations may not share a `wasm_name`, nor collapse to
/// the same PascalCase variant.
#[proc_macro]
pub fn host_functions(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
expand(input.into())
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
fn expand(input: TokenStream) -> syn::Result<TokenStream> {
let HostFunctionsInput { functions } = parse2(input)?;
let mut parsed = Vec::with_capacity(functions.len());
let mut errors = Vec::new();
for function in functions {
match ParsedHostFunction::parse(function) {
Ok(function) => parsed.push(function),
Err(error) => errors.push(error),
}
}
if let Some(error) = errors::combine(errors) {
return Err(error);
}
if let Some(error) = errors::combine(collisions(&parsed)) {
return Err(error);
}
Ok(generate(&parsed))
}
/// Names two declarations may not share, because the generated code would then
/// fail to compile at a span the caller cannot see.
fn collisions(functions: &[ParsedHostFunction]) -> Vec<syn::Error> {
let mut errors = Vec::new();
let mut variants = HashSet::new();
let mut wasm_names = HashSet::new();
for function in functions {
if !variants.insert(function.variant.to_string()) {
errors.push(syn::Error::new_spanned(
&function.variant,
format!(
"another host function already becomes the `{}` variant",
function.variant
),
));
}
if !wasm_names.insert(function.wasm_name.value()) {
errors.push(syn::Error::new_spanned(
&function.wasm_name,
format!(
"another host function is already imported as `{}`",
function.wasm_name.value()
),
));
}
}
errors
}
fn generate(functions: &[ParsedHostFunction]) -> TokenStream {
let trait_methods = functions.iter().map(ParsedHostFunction::trait_method);
let variants = functions
.iter()
.map(ParsedHostFunction::variant_declaration);
let spec_arms = functions.iter().map(ParsedHostFunction::spec_arm);
let all = functions.iter().map(|function| &function.variant);
quote! {
/// The host side of the wasm ABI: one method per function a guest may
/// import.
///
/// Implement it once per execution environment — the ledger host, a test
/// double, a benchmark fake — and a guest module cannot tell them apart.
/// Each method is one declaration from the `host_functions!` block, as
/// written; its `&self` receiver is not part of the ABI the guest sees,
/// so a host that must mutate does so behind interior mutability.
///
/// # The output contract
///
/// A method handed an `out` buffer **writes into it only when the whole
/// value fits, and returns the value's true length whether it fitted or
/// not.**
///
/// The length is the value's, not the number of bytes written, because it
/// is how a guest that asked with too small a buffer learns the size to
/// ask for next time. The engine turns a length past the buffer into
/// `BufferTooSmall`, and one past the field cap into `DataFieldTooLarge`,
/// so a host needs to know neither.
///
/// Writing nothing unless the value fits is the half only a host can hold
/// up. An engine can bound how many bytes are *writable* — and does, by
/// handing over a region clamped to the field cap — but it cannot take
/// back what a method already put there. A host that wrote a truncated
/// prefix and then reported the larger length would leave those bytes in
/// guest memory behind a refusal the guest is told to ignore.
pub trait HostFunctions {
#(#trait_methods)*
}
/// One row of the ABI table: what [`HostFunctionSpec::wasm_name`] and
/// [`HostFunctionSpec::gas`] read from.
///
/// Private, and the only reason it exists is to keep both of them fed
/// from a single `match` over the declarations.
struct HostFnSpec {
name: &'static str,
gas: u64,
}
/// Identifies one host function, and is the compile-time source of its
/// ABI metadata.
///
/// One variant per `host_functions!` declaration, named by converting the
/// function name to PascalCase. [`Self::ALL`] is the whole ABI, which is
/// what a wasm engine iterates to build its import table.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostFunctionSpec {
#(#variants,)*
}
impl HostFunctionSpec {
/// Every host function, in the order declared.
///
/// This is the complete import surface a guest may link against: a
/// function absent here cannot be called, and one present here must
/// be registered for a module that imports it to instantiate.
pub const ALL: &'static [Self] = &[#(Self::#all,)*];
/// This function's row of the ABI table.
const fn spec(self) -> HostFnSpec {
match self {
#(#spec_arms,)*
}
}
/// The name a guest imports this function under.
///
/// A guest's import name must match this exactly, or the module
/// fails to instantiate. Usable in `const` context, so import lists
/// can be built at compile time.
pub const fn wasm_name(self) -> &'static str {
self.spec().name
}
/// Gas charged before the call runs, independent of its arguments.
///
/// Consensus-relevant: two nodes that disagree on this value
/// disagree on transaction outcomes. Usable in `const` context, so
/// gas tables can be built at compile time.
pub const fn gas(self) -> u64 {
self.spec().gas
}
}
}
}
struct HostFunctionsInput {
functions: Vec<TraitItemFn>,
}
impl Parse for HostFunctionsInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut functions = Vec::new();
while !input.is_empty() {
functions.push(input.parse()?);
}
Ok(HostFunctionsInput { functions })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_an_empty_block() {
expand(quote! {}).unwrap();
}
#[test]
fn reports_mistakes_from_every_function() {
let error = expand(quote! {
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
#[gas = 2000]
fn sha512_half(&self, data: &[u8]) -> HostResult<[u8; 32]>;
})
.expect_err("expected parsing to fail");
let messages: Vec<_> = error.into_iter().map(|error| error.to_string()).collect();
assert_eq!(messages.len(), 2, "{messages:?}");
assert!(messages[0].contains("missing `#[gas"), "{messages:?}");
assert!(messages[1].contains("missing `#[wasm_name"), "{messages:?}");
}
#[test]
fn propagates_syntax_errors() {
let error = expand(quote! { fn missing_semicolon() }).expect_err("expected a syntax error");
assert!(!error.to_string().is_empty());
}
/// The messages of every diagnostic recorded by one failed `expand`.
fn messages(input: TokenStream) -> Vec<String> {
let Err(error) = expand(input) else {
panic!("expected expansion to fail");
};
error.into_iter().map(|error| error.to_string()).collect()
}
#[test]
fn generates_the_trait_the_enum_and_the_table() {
let generated = expand(quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
#[gas = 500]
#[wasm_name = "trace_num"]
fn trace_num(&self, msg: &str, number: i64) -> HostResult<()>;
})
.unwrap()
.to_string();
for expected in [
"pub trait HostFunctions",
"fn get_ledger_sqn (& self) -> HostResult < [u8 ; 4] > ;",
"fn trace_num (& self , msg : & str , number : i64) -> HostResult < () > ;",
"pub enum HostFunctionSpec { GetLedgerSqn , TraceNum , }",
"pub const ALL : & 'static [Self] = & [Self :: GetLedgerSqn , Self :: TraceNum ,]",
// The table's row type is generated too, and stays private.
"struct HostFnSpec { name : & 'static str , gas : u64 , }",
"const fn spec (self) -> HostFnSpec",
"Self :: GetLedgerSqn => HostFnSpec { name : \"ldgr_index\" , gas : 60u64 }",
"pub const fn wasm_name (self) -> & 'static str",
"pub const fn gas (self) -> u64",
] {
assert!(generated.contains(expected), "missing {expected:?}");
}
}
/// The expansion stands alone: every name in it is either generated here or
/// written in the declarations, so it cannot depend on the crate it lands in.
#[test]
fn names_no_crate_of_its_own() {
let generated = expand(quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
})
.unwrap()
.to_string();
assert!(!generated.contains("xrpl_host_functions"), "{generated}");
// `Self::Variant` is the only path the expansion may build: anything else
// would reach out of the generated code. Doc comments spell paths without
// spaces (`Self::ALL`), so they do not match.
for (index, _) in generated.match_indices(" :: ") {
assert!(
generated[..index].ends_with("Self"),
"path out of the expansion at {index}: {generated}"
);
}
}
/// `spec` is an implementation detail of the two accessors, so it must not
/// become part of the ABI crate's public surface.
#[test]
fn keeps_the_table_row_private() {
let generated = expand(quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
})
.unwrap()
.to_string();
assert!(!generated.contains("pub struct HostFnSpec"), "{generated}");
assert!(!generated.contains("pub const fn spec"), "{generated}");
}
#[test]
fn rejects_two_functions_that_share_a_wasm_name() {
let messages = messages(quote! {
#[gas = 60]
#[wasm_name = "trace"]
fn trace(&self, msg: &str) -> HostResult<()>;
#[gas = 70]
#[wasm_name = "trace"]
fn trace_num(&self, msg: &str, number: i64) -> HostResult<()>;
});
assert_eq!(messages.len(), 1, "{messages:?}");
assert!(
messages[0].contains("already imported as `trace`"),
"{messages:?}"
);
}
/// Names that differ only in underscores collapse to one enum variant.
#[test]
fn rejects_two_functions_that_share_a_variant() {
let messages = messages(quote! {
#[gas = 60]
#[wasm_name = "a"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
#[gas = 70]
#[wasm_name = "b"]
fn get_ledger__sqn(&self) -> HostResult<[u8; 4]>;
});
assert_eq!(messages.len(), 1, "{messages:?}");
assert!(
messages[0].contains("`GetLedgerSqn` variant"),
"{messages:?}"
);
}
}

View File

@@ -1,859 +0,0 @@
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote};
use syn::{
Attribute, Ident, LitInt, LitStr, PathArguments, ReceiverKind, ReturnType, Safety, Signature,
TraitItemFn, Type, TypePath, parse::Parse,
};
use crate::errors;
/// `#[gas = N]`: the base gas charged before the call runs.
const GAS: &str = "gas";
/// `#[wasm_name = "..."]`: the name the guest imports the function under.
const WASM_NAME: &str = "wasm_name";
/// `///` desugars to `#[doc = "..."]` before macro expansion.
const DOC: &str = "doc";
/// The alias every declaration returns its success type through.
const HOST_RESULT: &str = "HostResult";
/// One entry of a `host_functions!` block: its ABI metadata and its signature.
pub(crate) struct ParsedHostFunction {
pub(crate) gas: u64,
/// Kept as the literal the user wrote, so diagnostics and the generated
/// string both carry that span.
pub(crate) wasm_name: LitStr,
/// Doc comments, in source order, to re-emit on the generated items.
pub(crate) docs: Vec<Attribute>,
/// The enum variant this declaration becomes, spanned at the function name.
pub(crate) variant: Ident,
pub(crate) signature: Signature,
}
impl ParsedHostFunction {
/// `#[doc …] fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;`
pub(crate) fn trait_method(&self) -> TokenStream {
let docs = &self.docs;
// The declaration is already a trait method: emitted verbatim, so what
// the block reads like is what the trait is.
let signature = &self.signature;
quote! {
#(#docs)*
#signature;
}
}
/// `#[doc …] GetLedgerSqn`
pub(crate) fn variant_declaration(&self) -> TokenStream {
let docs = &self.docs;
let variant = &self.variant;
quote! {
#(#docs)*
#variant
}
}
/// `Self::GetLedgerSqn => HostFnSpec { name: "ldgr_index", gas: 60u64 }`
pub(crate) fn spec_arm(&self) -> TokenStream {
let Self {
gas,
wasm_name,
variant,
..
} = self;
quote! {
Self::#variant => HostFnSpec { name: #wasm_name, gas: #gas }
}
}
pub(crate) fn parse(function: TraitItemFn) -> syn::Result<Self> {
let mut gas = None;
let mut wasm_name = None;
let mut docs = Vec::new();
let mut errors = Vec::new();
// Tracked separately from `gas`/`wasm_name` so a malformed attribute is
// not also reported as a missing one.
let mut saw_gas = false;
let mut saw_wasm_name = false;
for attr in function.attrs {
if attr.path().is_ident(GAS) {
saw_gas = true;
if let Err(error) = int_value(&attr).and_then(|v| set_once(&mut gas, v, &attr)) {
errors.push(error);
}
} else if attr.path().is_ident(WASM_NAME) {
saw_wasm_name = true;
if let Err(error) = value::<LitStr>(&attr, "a string literal")
.and_then(|v| set_once(&mut wasm_name, v, &attr))
{
errors.push(error);
}
} else if attr.path().is_ident(DOC) {
docs.push(attr);
} else {
errors.push(syn::Error::new_spanned(
&attr,
format!("unexpected attribute `{}`", path_name(&attr)),
));
}
}
if !saw_gas {
errors.push(syn::Error::new_spanned(
&function.sig.ident,
format!("missing `#[{GAS} = ...]` attribute"),
));
}
if !saw_wasm_name {
errors.push(syn::Error::new_spanned(
&function.sig.ident,
format!("missing `#[{WASM_NAME} = \"...\"]` attribute"),
));
}
if let Some(body) = &function.default {
errors.push(syn::Error::new_spanned(
body,
"a host function is implemented by the host, so it must not have a body",
));
}
if !function.sig.generics.params.is_empty() || function.sig.generics.where_clause.is_some()
{
errors.push(syn::Error::new_spanned(
&function.sig.ident,
"a host function must not be generic: it maps to one wasm import signature",
));
}
errors.extend(check_receiver(&function.sig).err());
errors.extend(check_return_type(&function.sig).err());
if let Some(name) = &wasm_name {
errors.extend(check_wasm_name(name).err());
}
reject_modifiers(&function.sig, &mut errors);
// A name whose PascalCase form is not a legal variant is reported here
// rather than emitted, which would either panic or fail downstream.
let variant = match variant_ident(&function.sig.ident) {
Ok(variant) => Some(variant),
Err(error) => {
errors.push(error);
None
}
};
if let Some(error) = errors::combine(errors) {
return Err(error);
}
let (Some(gas), Some(wasm_name), Some(variant)) = (gas, wasm_name, variant) else {
unreachable!("every absent field is reported above");
};
Ok(Self {
gas,
wasm_name,
docs,
variant,
signature: function.sig,
})
}
}
/// Every declaration carries a receiver, and it is always `&self`.
///
/// `&self` is the only receiver that can work: the VM reaches the host through a
/// shared `&dyn HostFunctions` stored in the wasmi `Store`, and a host that needs
/// to mutate does so behind interior mutability. The receiver is not part of the
/// wasm ABI — the guest passes no `self` — so it is uniform across the block.
fn check_receiver(signature: &Signature) -> syn::Result<()> {
let Some(receiver) = signature.receiver() else {
return Err(syn::Error::new_spanned(
&signature.ident,
format!(
"a host function must declare its receiver: `fn {}(&self, ...)`",
signature.ident
),
));
};
// `&self` and nothing else: not `&mut self`, not `self`/`mut self`, not a
// typed `self: Box<Self>`, and not a spelled-out lifetime.
if !matches!(receiver.kind, ReceiverKind::Reference(_, None, None)) {
return Err(syn::Error::new_spanned(
receiver,
"a host function's receiver must be exactly `&self`: the VM calls the host \
through a shared `&dyn HostFunctions`",
));
}
Ok(())
}
/// Every declaration returns `HostResult<T>`, including the ones that yield
/// nothing (`HostResult<()>`).
///
/// One shape for every function is what lets a single dispatch adapter lower them
/// all: lift the arguments out of guest memory, call the host, then turn `Ok(T)`
/// into the wire's non-negative `i32` and `Err(e)` into a negative code or a trap.
/// A function returning a bare `T` would need its own arm.
fn check_return_type(signature: &Signature) -> syn::Result<()> {
const SHAPE: &str = "a host function must return `HostResult<T>` — \
`HostResult<()>` if it yields nothing";
let ReturnType::Type(_, returned) = &signature.output else {
return Err(syn::Error::new_spanned(&signature.ident, SHAPE));
};
let Type::Path(TypePath {
qself: None, path, ..
}) = &**returned
else {
return Err(syn::Error::new_spanned(returned, SHAPE));
};
// The last segment only, so `HostResult<T>` may be written qualified.
let Some(last) = path.segments.last() else {
return Err(syn::Error::new_spanned(returned, SHAPE));
};
if last.ident != HOST_RESULT {
return Err(syn::Error::new_spanned(returned, SHAPE));
}
// `HostResult` without its success type is `HostResult` the alias, which names
// no type; rustc's own message for that is unhelpfully far from the cause.
let PathArguments::AngleBracketed(arguments) = &last.arguments else {
return Err(syn::Error::new_spanned(
returned,
format!("`{HOST_RESULT}` needs its success type: `{HOST_RESULT}<T>`"),
));
};
if arguments.args.len() != 1 {
return Err(syn::Error::new_spanned(
arguments,
format!("`{HOST_RESULT}` takes exactly one type: `{HOST_RESULT}<T>`"),
));
}
Ok(())
}
/// `const`, `async`, `unsafe`/`safe` and `extern "…"` have no meaning in the
/// wasm ABI, and would otherwise pass silently into the generated trait.
fn reject_modifiers(signature: &Signature, errors: &mut Vec<syn::Error>) {
const PLAIN: &str =
"a host function must be a plain `fn`: this modifier is not part of the wasm ABI";
if let Some(constness) = &signature.constness {
errors.push(syn::Error::new_spanned(constness, PLAIN));
}
if let Some(asyncness) = &signature.asyncness {
errors.push(syn::Error::new_spanned(asyncness, PLAIN));
}
match &signature.safety {
Safety::Default => {}
Safety::Safe(token) => errors.push(syn::Error::new_spanned(token, PLAIN)),
Safety::Unsafe(token) => errors.push(syn::Error::new_spanned(token, PLAIN)),
}
if let Some(abi) = &signature.abi {
errors.push(syn::Error::new_spanned(abi, PLAIN));
}
}
/// The wasm import name reaches the engine's import table verbatim, so it is
/// held to what an import name can sanely be rather than to any string.
fn check_wasm_name(name: &LitStr) -> syn::Result<()> {
let value = name.value();
if value.is_empty() {
return Err(syn::Error::new_spanned(
name,
"the wasm name must not be empty",
));
}
if let Some(character) = value
.chars()
.find(|c| !c.is_ascii_alphanumeric() && *c != '_')
{
return Err(syn::Error::new_spanned(
name,
format!(
"a wasm name may only contain `A-Za-z0-9_`, but this one contains {character:?}"
),
));
}
Ok(())
}
/// The enum variant a declaration becomes: `get_ledger_sqn` -> `GetLedgerSqn`.
///
/// The result carries `ident`'s span, so anything the compiler says about the
/// variant points at the declaration that produced it.
fn variant_ident(ident: &Ident) -> syn::Result<Ident> {
// `to_string` spells raw identifiers `r#type`; the `r#` is not part of the name.
let name = ident.to_string();
let name = name.strip_prefix("r#").unwrap_or(&name);
let mut pascal = String::with_capacity(name.len());
let mut capitalize = true;
for character in name.chars() {
if character == '_' {
capitalize = true;
} else if capitalize {
pascal.extend(character.to_uppercase());
capitalize = false;
} else {
pascal.push(character);
}
}
// A name of nothing but underscores leaves `pascal` empty; the original is
// already a legal identifier, so keep it.
if pascal.is_empty() {
return Ok(ident.clone());
}
// `Ident::new` panics on a leading digit (`_2fa` -> `2fa`) and silently
// accepts keyword spellings (`self_` -> `Self`), which then fails to parse
// where the variant is emitted. Parsing rejects both, without panicking.
if let Err(error) = syn::parse_str::<Ident>(&pascal) {
return Err(syn::Error::new_spanned(
ident,
format!(
"this name becomes the enum variant `{pascal}`, which is not a valid \
variant name ({error}); rename the host function"
),
));
}
Ok(format_ident!("{pascal}", span = ident.span()))
}
/// Records `value`, or reports that the attribute appeared more than once.
fn set_once<T>(slot: &mut Option<T>, value: T, attr: &Attribute) -> syn::Result<()> {
if slot.replace(value).is_some() {
return Err(syn::Error::new_spanned(
attr,
format!("duplicate `{}` attribute", path_name(attr)),
));
}
Ok(())
}
/// The value of `#[name = <value>]`, parsed as `T`.
///
/// `expected` completes "`gas` expects …": syn's own message for the wrong kind
/// of literal names neither the attribute nor what it wanted.
fn value<T: Parse>(attr: &Attribute, expected: &str) -> syn::Result<T> {
let expr = &attr.meta.require_name_value()?.value;
syn::parse2(expr.to_token_stream()).map_err(|_| {
syn::Error::new_spanned(expr, format!("`{}` expects {expected}", path_name(attr)))
})
}
fn int_value(attr: &Attribute) -> syn::Result<u64> {
let int: LitInt = value(attr, "an integer literal")?;
// `LitInt` keeps the sign in its digits, so `base10_parse::<u64>` would
// report a negative value as "invalid digit found in string".
if int.base10_digits().starts_with('-') {
return Err(syn::Error::new_spanned(
int,
format!("`{}` must not be negative", path_name(attr)),
));
}
int.base10_parse()
}
/// The attribute's path as written, for diagnostics: `gas`, or `foo::bar`.
fn path_name(attr: &Attribute) -> String {
attr.path()
.segments
.iter()
.map(|segment| segment.ident.to_string())
.collect::<Vec<_>>()
.join("::")
}
#[cfg(test)]
mod tests {
use super::*;
use syn::{Expr, ExprLit, Lit, parse_quote};
/// The message of every diagnostic recorded by one failed `parse`.
///
/// `expect_err` is unavailable here: it needs `T: Debug`, and syn only
/// implements `Debug` for its AST types under the `extra-traits` feature.
fn messages(function: TraitItemFn) -> Vec<String> {
let Err(error) = ParsedHostFunction::parse(function) else {
panic!("expected parsing to fail");
};
error.into_iter().map(|error| error.to_string()).collect()
}
fn doc_text(attr: &Attribute) -> String {
match &attr.meta.require_name_value().unwrap().value {
Expr::Lit(ExprLit {
lit: Lit::Str(text),
..
}) => text.value(),
_ => panic!("doc attribute is not a string literal"),
}
}
#[test]
fn reads_gas_and_wasm_name() {
let parsed = ParsedHostFunction::parse(parse_quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
})
.unwrap();
assert_eq!(parsed.gas, 60);
assert_eq!(parsed.wasm_name.value(), "ldgr_index");
assert_eq!(parsed.signature.ident.to_string(), "get_ledger_sqn");
assert_eq!(parsed.variant.to_string(), "GetLedgerSqn");
assert!(parsed.docs.is_empty());
}
#[test]
fn derives_variant_names_from_function_names() {
for (function, variant) in [
("get_ledger_sqn", "GetLedgerSqn"),
("sha512_half", "Sha512Half"),
("trace", "Trace"),
("get_current_ledger_obj_field", "GetCurrentLedgerObjField"),
("r#type", "Type"),
("trace2", "Trace2"),
// Pathological, but must not panic: no letters to capitalize.
("__", "__"),
] {
let ident = format_ident!("{function}");
assert_eq!(
variant_ident(&ident).map(|v| v.to_string()).ok(),
Some(variant.to_owned()),
"{function}"
);
}
}
/// `_2fa` would PascalCase to `2fa`; building that `Ident` panics, and a
/// panic in a proc macro is reported with no useful span at all.
#[test]
fn rejects_a_name_that_becomes_a_leading_digit() {
let messages = messages(parse_quote! {
#[gas = 60]
#[wasm_name = "two_factor"]
fn _2fa(&self) -> HostResult<()>;
});
assert_eq!(messages.len(), 1, "{messages:?}");
assert!(
messages[0].contains("becomes the enum variant `2fa`"),
"{messages:?}"
);
}
/// `self_` PascalCases to `Self`, which `Ident::new` accepts and rustc then
/// rejects where the variant is emitted. `r#Self` is not a legal escape.
#[test]
fn rejects_a_name_that_becomes_a_keyword() {
for function in ["self_", "_self"] {
let ident = format_ident!("{function}");
let Err(error) = variant_ident(&ident) else {
panic!("expected `{function}` to be rejected");
};
assert!(
error.to_string().contains("variant `Self`"),
"{}",
error.to_string()
);
}
}
#[test]
fn rejects_negative_gas() {
let messages = messages(parse_quote! {
#[gas = -5]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
});
assert_eq!(messages.len(), 1, "{messages:?}");
assert_eq!(messages[0], "`gas` must not be negative");
}
#[test]
fn rejects_unusable_wasm_names() {
let empty = messages(parse_quote! {
#[gas = 60]
#[wasm_name = ""]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
});
assert_eq!(empty.len(), 1, "{empty:?}");
assert_eq!(empty[0], "the wasm name must not be empty");
let spaced = messages(parse_quote! {
#[gas = 60]
#[wasm_name = "ldgr index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
});
assert_eq!(spaced.len(), 1, "{spaced:?}");
assert!(spaced[0].contains("may only contain"), "{spaced:?}");
}
#[test]
fn rejects_signature_modifiers() {
for declaration in [
quote! { unsafe fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>; },
quote! { async fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>; },
quote! { const fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>; },
quote! { extern "C" fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>; },
] {
let function: TraitItemFn = syn::parse2(quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
#declaration
})
.unwrap();
let messages = messages(function);
assert_eq!(messages.len(), 1, "{messages:?}");
assert!(messages[0].contains("must be a plain `fn`"), "{messages:?}");
}
}
#[test]
fn trait_method_keeps_the_declared_receiver_and_ends_in_a_semicolon() {
let parsed = ParsedHostFunction::parse(parse_quote! {
/// Hashes `data`.
#[gas = 2000]
#[wasm_name = "sha512_half"]
fn sha512_half(&self, data: &[u8]) -> HostResult<[u8; 32]>;
})
.unwrap();
// `///` reaches the macro as `#[doc = r"..."]`: rustc's lexer spells doc
// comments as raw string literals.
let method = parsed.trait_method().to_string();
assert!(
method.starts_with("# [doc = r\" Hashes `data`.\"]"),
"{method}"
);
assert!(
method
.contains("fn sha512_half (& self , data : & [u8]) -> HostResult < [u8 ; 32] > ;"),
"{method}"
);
}
#[test]
fn spec_arm_carries_the_name_and_the_gas() {
let parsed = ParsedHostFunction::parse(parse_quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
})
.unwrap();
assert_eq!(
parsed.spec_arm().to_string(),
"Self :: GetLedgerSqn => HostFnSpec { name : \"ldgr_index\" , gas : 60u64 }"
);
}
#[test]
fn keeps_doc_comments_in_source_order() {
let parsed = ParsedHostFunction::parse(parse_quote! {
/// First line.
///
/// Third line.
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
})
.unwrap();
let docs: Vec<_> = parsed.docs.iter().map(doc_text).collect();
assert_eq!(docs, vec![" First line.", "", " Third line."]);
}
#[test]
fn preserves_parameters_and_return_type() {
let traced = ParsedHostFunction::parse(parse_quote! {
#[gas = 500]
#[wasm_name = "trace"]
fn trace(&self, msg: &str, data: &[u8], as_hex: bool) -> HostResult<()>;
})
.unwrap();
// The receiver is `inputs[0]`; the three wasm parameters follow it.
assert_eq!(traced.signature.inputs.len(), 4);
assert_eq!(
traced.signature.output.to_token_stream().to_string(),
"-> HostResult < () >"
);
let hashed = ParsedHostFunction::parse(parse_quote! {
#[gas = 2000]
#[wasm_name = "sha512_half"]
fn sha512_half(&self, data: &[u8]) -> HostResult<[u8; HASH_LEN]>;
})
.unwrap();
assert_eq!(
hashed.signature.output.to_token_stream().to_string(),
"-> HostResult < [u8 ; HASH_LEN] >"
);
}
#[test]
fn reports_both_missing_attributes_at_once() {
let messages = messages(parse_quote! {
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
});
assert_eq!(messages.len(), 2);
assert!(messages[0].contains("missing `#[gas"), "{messages:?}");
assert!(messages[1].contains("missing `#[wasm_name"), "{messages:?}");
}
#[test]
fn names_the_unexpected_attribute() {
let messages = messages(parse_quote! {
#[gas = 60]
#[wsam_name = "typo"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
});
// The typo'd attribute, plus the `wasm_name` it failed to be.
assert_eq!(messages.len(), 2);
assert!(
messages.iter().any(|m| m.contains("`wsam_name`")),
"{messages:?}"
);
}
#[test]
fn rejects_wrong_literal_types() {
let gas = messages(parse_quote! {
#[gas = "60"]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
});
assert_eq!(gas.len(), 1, "{gas:?}");
assert!(
gas[0].contains("`gas` expects an integer literal"),
"{gas:?}"
);
let name = messages(parse_quote! {
#[gas = 60]
#[wasm_name = 7]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
});
assert_eq!(name.len(), 1, "{name:?}");
assert!(
name[0].contains("`wasm_name` expects a string literal"),
"{name:?}"
);
}
#[test]
fn rejects_gas_that_does_not_fit_in_u64() {
let messages = messages(parse_quote! {
#[gas = 99999999999999999999999]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
});
assert_eq!(messages.len(), 1, "{messages:?}");
assert!(messages[0].contains("number too large"), "{messages:?}");
}
#[test]
fn rejects_attribute_shapes_other_than_name_value() {
let bare = messages(parse_quote! {
#[gas]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
});
assert_eq!(bare.len(), 1, "{bare:?}");
assert!(bare[0].contains("gas = ..."), "{bare:?}");
let list = messages(parse_quote! {
#[gas(60)]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
});
assert_eq!(list.len(), 1, "{list:?}");
}
#[test]
fn rejects_duplicate_attributes() {
let messages = messages(parse_quote! {
#[gas = 60]
#[gas = 70]
#[wasm_name = "ldgr_index"]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
});
assert_eq!(messages.len(), 2, "{messages:?}");
assert!(messages[0].contains("duplicate `gas`"), "{messages:?}");
assert!(
messages[1].contains("duplicate `wasm_name`"),
"{messages:?}"
);
}
/// A malformed attribute must not also be reported as an absent one.
#[test]
fn does_not_report_a_malformed_attribute_as_missing() {
let messages = messages(parse_quote! {
#[gas = "60"]
#[wasm_name = 7]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]>;
});
assert_eq!(messages.len(), 2, "{messages:?}");
assert!(
!messages.iter().any(|m| m.contains("missing")),
"{messages:?}"
);
}
#[test]
fn rejects_a_body() {
let messages = messages(parse_quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]> { Ok([0; 4]) }
});
assert_eq!(messages.len(), 1, "{messages:?}");
assert!(messages[0].contains("must not have a body"), "{messages:?}");
}
#[test]
fn rejects_generics() {
let parameter = messages(parse_quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn<T>(&self) -> HostResult<T>;
});
assert_eq!(parameter.len(), 1, "{parameter:?}");
assert!(
parameter[0].contains("must not be generic"),
"{parameter:?}"
);
let clause = messages(parse_quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult<[u8; 4]> where Self: Sized;
});
assert_eq!(clause.len(), 1, "{clause:?}");
}
#[test]
fn requires_a_receiver() {
let messages = messages(parse_quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn() -> HostResult<[u8; 4]>;
});
assert_eq!(messages.len(), 1, "{messages:?}");
assert!(
messages[0].contains("must declare its receiver: `fn get_ledger_sqn(&self, ...)`"),
"{messages:?}"
);
}
/// Anything but `&self` would need a host the VM cannot hand out: it holds
/// one shared `&dyn HostFunctions` for the whole run.
#[test]
fn rejects_receivers_other_than_shared_self() {
for receiver in [
quote! { &mut self },
quote! { self },
quote! { mut self },
quote! { self: Box<Self> },
quote! { &'a self },
] {
let function: TraitItemFn = syn::parse2(quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(#receiver) -> HostResult<[u8; 4]>;
})
.unwrap_or_else(|_| panic!("`{receiver}` should parse"));
let messages = messages(function);
assert_eq!(messages.len(), 1, "`{receiver}`: {messages:?}");
assert!(
messages[0].contains("must be exactly `&self`"),
"`{receiver}`: {messages:?}"
);
}
}
/// A bare `T` return would need its own lowering arm, so the uniform shape is
/// required rather than inferred.
#[test]
fn rejects_returns_that_are_not_host_result() {
for output in [
quote! {},
quote! { -> () },
quote! { -> [u8; 4] },
quote! { -> i32 },
quote! { -> Result<[u8; 4], HostError> },
quote! { -> impl Iterator<Item = u8> },
] {
let function: TraitItemFn = syn::parse2(quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) #output;
})
.unwrap_or_else(|_| panic!("`{output}` should parse"));
let messages = messages(function);
assert_eq!(messages.len(), 1, "`{output}`: {messages:?}");
assert!(
messages[0].contains("must return `HostResult<T>`"),
"`{output}`: {messages:?}"
);
}
}
/// `HostResult` may be written qualified, since the trait method keeps whatever
/// path resolves where the block is written.
#[test]
fn accepts_a_qualified_host_result() {
let parsed = ParsedHostFunction::parse(parse_quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> xrpl_host_functions::HostResult<[u8; 4]>;
})
.unwrap();
assert!(
parsed
.trait_method()
.to_string()
.contains("xrpl_host_functions :: HostResult < [u8 ; 4] >"),
"{}",
parsed.trait_method()
);
}
/// `HostResult` with no success type names no type at all; rustc's own error
/// for that lands on the generated trait, far from the declaration.
#[test]
fn rejects_host_result_without_a_success_type() {
let messages = messages(parse_quote! {
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self) -> HostResult;
});
assert_eq!(messages.len(), 1, "{messages:?}");
assert!(
messages[0].contains("needs its success type"),
"{messages:?}"
);
}
}

View File

@@ -1,7 +0,0 @@
[package]
name = "xrpl-host-functions"
version = "0.1.0"
edition.workspace = true
[dependencies]
xrpl-host-functions-macros.path = "../xrpl-host-functions-macros"

View File

@@ -1,503 +0,0 @@
//! The wasm host ABI: the one place it is declared.
//!
//! `host_functions!` turns the declaration block at the bottom of this file into the
//! [`HostFunctions`] trait a host implements and the [`HostFunctionSpec`] table a
//! wasm engine registers from.
//!
//! The split: hand-written here is the vocabulary the declarations are written in —
//! [`HostError`], [`TraceDataType`], [`HostResult`], [`HASH_LEN`] — and everything
//! derived from the declarations is generated. The expansion names nothing this file
//! does not, so the two sides meet only in the block below.
//!
//! So this file is lists — error codes, trace data types, functions. The `macro_rules!`
//! that expand the first two into enums live in `macros.rs`.
#![no_std]
#[macro_use]
mod macros;
// Not re-exported: the ABI is declared once, here, and this is the only call site.
use xrpl_host_functions_macros::host_functions;
host_errors! {
Unimplemented = -1,
FieldNotFound = -2,
BufferTooSmall = -3,
NoArray = -4,
NotLeafField = -5,
LocatorMalformed = -6,
SlotOutRange = -7,
SlotsFull = -8,
EmptySlot = -9,
LedgerObjNotFound = -10,
OutOfTransferLimit = -11,
DataFieldTooLarge = -12,
PointerOutOfBounds = -13,
NoMemExported = -14,
InvalidParams = -15,
InvalidAccount = -16,
InvalidField = -17,
IndexOutOfBounds = -18,
FloatInputMalformed = -19,
FloatComputationError = -20,
/// Internal fatal error.
/// User code will never see this error but keep it reserved to not rely on the value.
InternalFatal = -2147483648,
}
/// Convenience alias for the trait's fallible returns.
pub type HostResult<T> = Result<T, HostError>;
/// A `sha512Half` digest: the first 32 bytes of a SHA-512, as XRPL uses it.
pub const HASH_LEN: usize = 32;
trace_data_types! {
/// 8 little-endian bytes, rendered as a signed decimal.
Int64 = 1,
/// 8 little-endian bytes, rendered as an unsigned decimal.
Uint64 = 2,
/// A serialized XRPL float: 12 bytes, mantissa then exponent.
Xfloat = 3,
/// A 20-byte account ID, rendered as base58.
Account = 4,
/// A serialized `STAmount`.
Amount = 5,
/// Raw bytes, hex-encoded.
AsHex = 6,
/// Bytes rendered verbatim as text.
AsText = 7,
}
host_functions! {
/// The sequence number of the ledger being built, as 4 little-endian bytes.
#[gas = 60]
#[wasm_name = "ldgr_index"]
fn get_ledger_sqn(&self, out: &mut [u8]) -> HostResult<usize>;
/// The close time of the parent (last-closed) ledger, as 4 little-endian bytes.
#[gas = 60]
#[wasm_name = "parent_ldgr_time"]
fn get_parent_ledger_time(&self, out: &mut [u8]) -> HostResult<usize>;
/// The hash of the parent (last-closed) ledger, as 32 bytes.
#[gas = 60]
#[wasm_name = "parent_ldgr_hash"]
fn get_parent_ledger_hash(&self, out: &mut [u8]) -> HostResult<usize>;
/// The base fee of the ledger being built, in drops, as 4 little-endian bytes.
#[gas = 60]
#[wasm_name = "base_fee"]
fn get_base_fee(&self, out: &mut [u8]) -> HostResult<usize>;
/// Whether an amendment is enabled. The input is either its 32-byte id or its name;
/// the answer is `1` if enabled and `0` if not.
#[gas = 100]
#[wasm_name = "amendment_enabled"]
fn is_amendment_enabled(&self, amendment: &[u8]) -> HostResult<i32>;
/// Load the ledger object with the given 32-byte id into a cache slot, so later
/// calls can read its fields. `cache_idx` selects the slot (1-based); `0` asks the
/// host to assign a free one. Answers the slot used.
#[gas = 5000]
#[wasm_name = "cache_le"]
fn cache_ledger_obj(&self, obj_id: &[u8], cache_idx: i32) -> HostResult<i32>;
/// The serialized bytes of one field of the transaction being executed, selected
/// by its `SField` code.
#[gas = 70]
#[wasm_name = "tx_field"]
fn get_tx_field(&self, field: i32, out: &mut [u8]) -> HostResult<usize>;
/// The serialized bytes of one field of the current (escrow) ledger object.
#[gas = 70]
#[wasm_name = "home_le_field"]
fn get_current_ledger_obj_field(&self, field: i32, out: &mut [u8]) -> HostResult<usize>;
/// The serialized bytes of one field of a previously cached ledger object,
/// selected by its cache slot and the field's `SField` code.
#[gas = 70]
#[wasm_name = "le_field"]
fn get_ledger_obj_field(&self, cache_idx: i32, field: i32, out: &mut [u8]) -> HostResult<usize>;
/// The serialized bytes of a nested field of the transaction, reached by a
/// `locator`: a path of little-endian `i32` steps (so its byte length is a non-zero
/// multiple of 4).
#[gas = 110]
#[wasm_name = "tx_inner"]
fn get_tx_nested_field(&self, locator: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The serialized bytes of a nested field of the current (escrow) ledger object,
/// reached by a `locator`, as with [`HostFunctions::get_tx_nested_field`].
#[gas = 110]
#[wasm_name = "home_le_inner"]
fn get_current_ledger_obj_nested_field(
&self,
locator: &[u8],
out: &mut [u8],
) -> HostResult<usize>;
/// The serialized bytes of a nested field of a previously cached ledger object,
/// selected by its cache slot and reached by a `locator`.
#[gas = 110]
#[wasm_name = "le_inner"]
fn get_ledger_obj_nested_field(
&self,
cache_idx: i32,
locator: &[u8],
out: &mut [u8],
) -> HostResult<usize>;
/// The number of elements in an array field of the transaction, selected by its
/// `SField` code. Answers the count directly; `NoArray` if the field is not an array.
#[gas = 40]
#[wasm_name = "tx_arr_len"]
fn get_tx_array_len(&self, field: i32) -> HostResult<i32>;
/// The number of elements in an array field of the current (escrow) ledger
/// object, as with [`HostFunctions::get_tx_array_len`].
#[gas = 40]
#[wasm_name = "home_le_arr_len"]
fn get_current_ledger_obj_array_len(&self, field: i32) -> HostResult<i32>;
/// The number of elements in an array field of a previously cached ledger object,
/// selected by its cache slot and `SField` code.
#[gas = 40]
#[wasm_name = "le_arr_len"]
fn get_ledger_obj_array_len(&self, cache_idx: i32, field: i32) -> HostResult<i32>;
/// The number of elements in a nested array field of the transaction, reached by a
/// `locator`.
#[gas = 70]
#[wasm_name = "tx_inner_arr_len"]
fn get_tx_nested_array_len(&self, locator: &[u8]) -> HostResult<i32>;
/// The number of elements in a nested array field of the current (escrow) ledger
/// object, reached by a `locator`, as with [`HostFunctions::get_tx_nested_array_len`].
#[gas = 70]
#[wasm_name = "home_le_inner_arr_len"]
fn get_current_ledger_obj_nested_array_len(&self, locator: &[u8]) -> HostResult<i32>;
/// The number of elements in a nested array field of a previously cached ledger
/// object, selected by its cache slot and reached by a `locator`.
#[gas = 70]
#[wasm_name = "le_inner_arr_len"]
fn get_ledger_obj_nested_array_len(&self, cache_idx: i32, locator: &[u8]) -> HostResult<i32>;
/// Verify `signature` over `message` under `pubkey`. Answers `1` if the signature
/// is valid, `0` if not, or a negative error.
#[gas = 300]
#[wasm_name = "check_sig"]
fn check_signature(
&self,
message: &[u8],
signature: &[u8],
pubkey: &[u8],
) -> HostResult<i32>;
/// The 32-byte ledger key (keylet) of an account's `AccountRoot`, computed from a
/// 20-byte account id.
#[gas = 350]
#[wasm_name = "accountroot_id"]
fn account_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of an AMM, computed from its two assets. Each asset is a byte
/// slice whose length selects its kind (24 = MPT, 20 = XRP, 40 = issued currency +
/// issuer).
#[gas = 450]
#[wasm_name = "amm_id"]
fn amm_keylet(&self, asset1: &[u8], asset2: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of a `Check`, computed from a 20-byte account id and its
/// sequence number. `seq` is the guest's `u32` carried as its `i32` bit pattern.
#[gas = 350]
#[wasm_name = "check_id"]
fn check_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of a `Credential`, computed from the 20-byte subject and
/// issuer account ids and a credential-type byte string.
#[gas = 350]
#[wasm_name = "credential_id"]
fn credential_keylet(
&self,
subject: &[u8],
issuer: &[u8],
credential_type: &[u8],
out: &mut [u8],
) -> HostResult<usize>;
/// The 32-byte keylet of a `Delegate` object, computed from the 20-byte account and
/// the account it authorizes.
#[gas = 350]
#[wasm_name = "delegate_id"]
fn delegate_keylet(
&self,
account: &[u8],
authorize: &[u8],
out: &mut [u8],
) -> HostResult<usize>;
/// The 32-byte keylet of a `DepositPreauth`, computed from the 20-byte account and
/// the account it authorizes to deposit.
#[gas = 350]
#[wasm_name = "deposit_preauth_id"]
fn deposit_preauth_keylet(
&self,
account: &[u8],
authorize: &[u8],
out: &mut [u8],
) -> HostResult<usize>;
/// The 32-byte keylet of an account's `DID`, computed from its 20-byte account id.
#[gas = 350]
#[wasm_name = "did_id"]
fn did_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of an `Escrow`, computed from the 20-byte owner account and
/// its sequence number. `seq` is the guest's `u32` carried as its `i32` bit
/// pattern.
#[gas = 350]
#[wasm_name = "escrow_id"]
fn escrow_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of a `RippleState` (trust line), computed from two 20-byte
/// account ids and a 20-byte currency.
#[gas = 400]
#[wasm_name = "trustline_id"]
fn trust_line_keylet(
&self,
account1: &[u8],
account2: &[u8],
currency: &[u8],
out: &mut [u8],
) -> HostResult<usize>;
/// The 32-byte keylet of an `MPTokenIssuance`, computed from the 20-byte issuer
/// account and its sequence number. `seq` is the guest's `u32` carried as its `i32`
/// bit pattern.
#[gas = 350]
#[wasm_name = "mpt_issuance_id"]
fn mptoken_issuance_keylet(
&self,
issuer: &[u8],
seq: i32,
out: &mut [u8],
) -> HostResult<usize>;
/// The 32-byte keylet of an `MPToken`, computed from a 24-byte MPT issuance id and
/// the 20-byte holder account.
#[gas = 500]
#[wasm_name = "mptoken_id"]
fn mptoken_keylet(&self, mptid: &[u8], holder: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of an `NFTokenOffer`, computed from the 20-byte owner account
/// and its sequence number. `seq` is the guest's `u32` carried as its `i32` bit
/// pattern.
#[gas = 350]
#[wasm_name = "nft_offer_id"]
fn nftoken_offer_keylet(
&self,
account: &[u8],
seq: i32,
out: &mut [u8],
) -> HostResult<usize>;
/// The 32-byte keylet of an `Offer`, computed from the 20-byte owner account and
/// its sequence number. `seq` is the guest's `u32` carried as its `i32` bit
/// pattern.
#[gas = 350]
#[wasm_name = "offer_id"]
fn offer_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of an `Oracle`, computed from the 20-byte owner account and
/// its document id. `doc_id` is the guest's `u32` carried as its `i32` bit pattern.
#[gas = 350]
#[wasm_name = "oracle_id"]
fn oracle_keylet(&self, account: &[u8], doc_id: i32, out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of a `PayChannel`, computed from the 20-byte source account,
/// the 20-byte destination account, and the channel's sequence number. `seq` is the
/// guest's `u32` carried as its `i32` bit pattern.
#[gas = 350]
#[wasm_name = "paychan_id"]
fn paychannel_keylet(
&self,
account: &[u8],
destination: &[u8],
seq: i32,
out: &mut [u8],
) -> HostResult<usize>;
/// The 32-byte keylet of a `PermissionedDomain`, computed from the 20-byte owner
/// account and its sequence number. `seq` is the guest's `u32` carried as its `i32`
/// bit pattern.
#[gas = 350]
#[wasm_name = "permissioned_domain_id"]
fn permissioned_domain_keylet(
&self,
account: &[u8],
seq: i32,
out: &mut [u8],
) -> HostResult<usize>;
/// The 32-byte keylet of a `SignerList`, computed from its 20-byte owner account.
#[gas = 350]
#[wasm_name = "signers_id"]
fn signer_list_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of a `Ticket`, computed from the 20-byte owner account and
/// its ticket sequence number. `seq` is the guest's `u32` carried as its `i32` bit
/// pattern.
#[gas = 350]
#[wasm_name = "ticket_id"]
fn ticket_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of a `Vault`, computed from the 20-byte owner account and its
/// sequence number. `seq` is the guest's `u32` carried as its `i32` bit pattern.
#[gas = 350]
#[wasm_name = "vault_id"]
fn vault_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
/// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512.
#[gas = 2000]
#[wasm_name = "sha512_half"]
fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// Writes `msg` to the trace log, followed by `data` rendered as `data_type` says.
///
/// The one declaration whose wasm function has **no result**: this node's own log
/// is its only effect, so a guest is told nothing. An `Err` from a host therefore
/// reaches it in no form, and only the host-fatal ones do anything at all.
///
/// It is also the one declaration that is **not** the wasm parameter order.
/// `data_type` is the third wasm parameter, between the two regions, because that
/// is where the guest stdlib declares it; `register.rs` takes the arguments in wasm
/// order and calls this in declaration order.
#[gas = 30]
#[wasm_name = "trace"]
fn trace(&self, msg: &str, data: &[u8], data_type: TraceDataType) -> HostResult<()>;
/// Stores `data` as the current object's data field, replacing whatever was there,
/// and returns the number of bytes stored; `DataFieldTooLarge` if it exceeds the
/// host's limit.
#[gas = 1000]
#[wasm_name = "set_data"]
fn update_data(&self, data: &[u8]) -> HostResult<i32>;
/// The URI of the `NFToken` with id `nft_id` (32 bytes) held by the 20-byte
/// `account`.
#[gas = 5000]
#[wasm_name = "nft_uri"]
fn get_nft(&self, account: &[u8], nft_id: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The 20-byte issuer account encoded in the `NFToken` id `nft_id` (32 bytes).
#[gas = 70]
#[wasm_name = "nft_issuer"]
fn get_nft_issuer(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The taxon encoded in the `NFToken` id `nft_id` (32 bytes), as four little-endian
/// bytes.
#[gas = 60]
#[wasm_name = "nft_taxon"]
fn get_nft_taxon(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The flags encoded in the `NFToken` id `nft_id` (32 bytes).
#[gas = 60]
#[wasm_name = "nft_flags"]
fn get_nft_flags(&self, nft_id: &[u8]) -> HostResult<i32>;
/// The transfer fee encoded in the `NFToken` id `nft_id` (32 bytes).
#[gas = 60]
#[wasm_name = "nft_xfer_fee"]
fn get_nft_transfer_fee(&self, nft_id: &[u8]) -> HostResult<i32>;
/// The sequence number encoded in the `NFToken` id `nft_id` (32 bytes), as four
/// little-endian bytes.
#[gas = 60]
#[wasm_name = "nft_serial"]
fn get_nft_sequence(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult<usize>;
// A "float" here is an XRPL `Number` in its serialized form: a byte blob the guest
// holds opaquely and hands back to these functions. Inputs and outputs that are
// floats are byte regions; `mode` is the rounding mode, a scalar the guest chooses.
/// A float built from the signed integer `x` under rounding `mode`.
#[gas = 100]
#[wasm_name = "float_from_int"]
fn float_from_int(&self, x: i64, mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// A float built from the unsigned integer in the 8-byte region `x` under rounding
/// `mode`.
#[gas = 130]
#[wasm_name = "float_from_uint"]
fn float_from_uint(&self, x: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// A float built from the serialized `STAmount` in `amount` under rounding `mode`.
#[gas = 150]
#[wasm_name = "float_from_stamount"]
fn float_from_stamount(&self, amount: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// A float built from the serialized `STNumber` in `number` under rounding `mode`.
#[gas = 150]
#[wasm_name = "float_from_stnumber"]
fn float_from_stnumber(&self, number: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// The float `x` rounded to a signed integer under rounding `mode`, as eight
/// little-endian bytes.
#[gas = 130]
#[wasm_name = "float_to_int"]
fn float_to_int(&self, x: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// The float `x` split into its mantissa (eight little-endian bytes) and its exponent
/// (four little-endian bytes), each written to its own output region.
#[gas = 130]
#[wasm_name = "float_to_mant_exp"]
fn float_to_mant_exp(
&self,
x: &[u8],
mantissa_out: &mut [u8],
exponent_out: &mut [u8],
) -> HostResult<usize>;
/// A float built from `mantissa` and `exponent` under rounding `mode`.
#[gas = 100]
#[wasm_name = "float_from_mant_exp"]
fn float_from_mant_exp(
&self,
mantissa: i64,
exponent: i32,
mode: i32,
out: &mut [u8],
) -> HostResult<usize>;
/// Compares floats `x` and `y`, returning a negative, zero, or positive scalar as
/// `x` is less than, equal to, or greater than `y`.
#[gas = 80]
#[wasm_name = "float_cmp"]
fn float_compare(&self, x: &[u8], y: &[u8]) -> HostResult<i32>;
/// The float sum `x + y` under rounding `mode`.
#[gas = 160]
#[wasm_name = "float_add"]
fn float_add(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// The float difference `x - y` under rounding `mode`.
#[gas = 160]
#[wasm_name = "float_sub"]
fn float_subtract(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// The float product `x * y` under rounding `mode`.
#[gas = 300]
#[wasm_name = "float_mult"]
fn float_multiply(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// The float quotient `x / y` under rounding `mode`.
#[gas = 300]
#[wasm_name = "float_div"]
fn float_divide(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// The float `x` raised to the power `n` under rounding `mode`.
#[gas = 5500]
#[wasm_name = "float_pow"]
fn float_power(&self, x: &[u8], n: i32, mode: i32, out: &mut [u8]) -> HostResult<usize>;
}

View File

@@ -1,102 +0,0 @@
//! The `macro_rules!` behind the two hand-listed enums, [`crate::HostError`] and
//! [`crate::TraceDataType`].
//!
//! Each takes one list of `Variant = code,` and expands the enum together with the
//! `ALL`/`code`/`from_code` set that must not fall behind it. The lists themselves stay
//! in `lib.rs`, beside the `host_functions!` block.
/// Declares [`crate::HostError`] from one list: the variants, `HostError::ALL` and
/// `HostError::from_code`'s table all expand from the codes given.
///
/// One list is what makes `ALL` complete. Rust cannot enumerate an enum's
/// variants — an exhaustive `match` forces an arm per variant but gives nothing to
/// iterate — so a hand-written `ALL` beside a hand-written enum could only be kept
/// in step by review, and `ALL`'s whole purpose is to be the set a test can trust.
/// A code added to the list gains its `ALL` entry and its `from_code` arm by
/// construction. `HostFunctionSpec::ALL` is complete the same way, from the
/// `host_functions!` block.
macro_rules! host_errors {
($($(#[$doc:meta])* $variant:ident = $code:literal,)+) => {
/// Error codes a host function may return.
///
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum HostError {
$($(#[$doc])* $variant = $code,)+
}
impl HostError {
/// Every error a host function may return, in code order.
///
/// The complete set, and complete by construction: a wasm engine's
/// split between the codes it hands the guest and the conditions it
/// traps on is a decision per variant, so the test that checks the
/// split iterates this and a code added to the ABI cannot slip past it.
pub const ALL: &'static [HostError] = &[$(HostError::$variant,)+];
/// The negative wire value a failed call returns. Every code but
/// `InternalFatal` is one a guest reads off that value.
#[inline]
pub const fn code(self) -> i32 {
self as i32
}
/// Reconstruct a `HostError` from its wire code.
///
/// A code this ABI does not define is `InternalFatal`: an answer the
/// caller cannot act on is the call not having been served, and that is
/// the variant which says so. Positive values are not errors at all and go
/// the same way, since this is reached only once a negative return has
/// been read as a failure.
pub const fn from_code(code: i32) -> HostError {
match code {
$($code => HostError::$variant,)+
_ => HostError::InternalFatal,
}
}
}
};
}
/// Declares [`crate::TraceDataType`] from one list, so `TraceDataType::ALL`,
/// `TraceDataType::code` and `TraceDataType::from_code` cannot fall behind the
/// variants — the reason `host_errors!` above is written this way.
macro_rules! trace_data_types {
($($(#[$doc:meta])* $variant:ident = $code:literal,)+) => {
/// How [`HostFunctions::trace`] is to read its data buffer.
///
/// The discriminants are wire values shared with the guest stdlib: append only,
/// never renumber. They start at 1, so a zeroed argument names no type rather
/// than the first one.
///
/// This is the declaration a guest and a host both compile against. The host
/// side needs a second one — `cxx` cannot be a dependency here, since this
/// crate also links into the guest — so `xrpl-wasm-vm-ffi` declares a shared
/// enum for C++ and converts, exhaustively, from this.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum TraceDataType {
$($(#[$doc])* $variant = $code,)+
}
impl TraceDataType {
/// Every data type a guest may name, in code order.
pub const ALL: &'static [TraceDataType] = &[$(TraceDataType::$variant,)+];
/// The wire value a guest passes to name this type.
#[inline]
pub const fn code(self) -> i32 {
self as i32
}
/// The type `code` names, or `None`: the engine drops a call it cannot
/// read rather than guessing at a rendering the guest did not ask for.
pub const fn from_code(code: i32) -> Option<TraceDataType> {
match code {
$($code => Some(TraceDataType::$variant),)+
_ => None,
}
}
}
};
}

View File

@@ -1,34 +0,0 @@
//! `host_functions!` must work outside the crate that declares the ABI: the only
//! names its expansion needs are the ones the declarations themselves spell.
use xrpl_host_functions::HostResult;
use xrpl_host_functions_macros::host_functions;
host_functions! {
/// Answers with the number it was given.
#[gas = 7]
#[wasm_name = "ping"]
fn ping(&self, number: i32) -> HostResult<i32>;
}
struct Host;
impl HostFunctions for Host {
fn ping(&self, number: i32) -> HostResult<i32> {
Ok(number)
}
}
#[test]
fn the_generated_table_stands_on_its_own() {
assert_eq!(HostFunctionSpec::ALL.len(), 1);
assert_eq!(HostFunctionSpec::Ping.wasm_name(), "ping");
assert_eq!(HostFunctionSpec::Ping.gas(), 7);
}
/// The generated trait is implementable from another crate, which is the point of
/// declaring the ABI in a library at all.
#[test]
fn the_generated_trait_is_implementable_here() {
assert_eq!(Host.ping(3), Ok(3));
}

View File

@@ -1,996 +0,0 @@
//! Exercises the API that `host_functions!` generates, not the macro itself:
//! the `HostFunctions` trait is implementable and callable both directly and
//! through `&dyn`, and the generated `HostFunctionSpec` and `TraceDataType`
//! tables agree with the declarations in `src/lib.rs`. The macro's own parsing
//! and diagnostics are covered by the unit tests in `xrpl-host-functions-macros`.
use std::cell::RefCell;
use std::collections::HashSet;
use xrpl_host_functions::{
HASH_LEN, HostError, HostFunctionSpec, HostFunctions, HostResult, TraceDataType,
};
/// Records what it was asked to do; enough to prove the trait is usable.
///
/// Every method takes `&self`, so a host that records anything keeps it behind
/// interior mutability.
#[derive(Default)]
struct FakeHost {
traced: RefCell<Vec<String>>,
}
/// The contract every byte-producing host function follows: write only if the
/// value fits, and report its true length either way, so the engine can turn a
/// value that doesn't fit into `BufferTooSmall` without the host knowing the
/// guest's buffer size.
fn put(out: &mut [u8], value: &[u8]) -> HostResult<usize> {
if let Some(dst) = out.get_mut(..value.len()) {
dst.copy_from_slice(value);
}
Ok(value.len())
}
impl HostFunctions for FakeHost {
fn get_ledger_sqn(&self, out: &mut [u8]) -> HostResult<usize> {
put(out, &7u32.to_le_bytes())
}
fn get_parent_ledger_time(&self, out: &mut [u8]) -> HostResult<usize> {
put(out, &9u32.to_le_bytes())
}
fn get_parent_ledger_hash(&self, out: &mut [u8]) -> HostResult<usize> {
put(out, &[0xab; HASH_LEN])
}
fn get_base_fee(&self, out: &mut [u8]) -> HostResult<usize> {
put(out, &10u32.to_le_bytes())
}
/// Returns a flag rather than bytes, and reads its input: enabled unless empty.
fn is_amendment_enabled(&self, amendment: &[u8]) -> HostResult<i32> {
Ok(i32::from(!amendment.is_empty()))
}
/// Returns a slot: the requested one, or slot 1 when asked to pick.
fn cache_ledger_obj(&self, _obj_id: &[u8], cache_idx: i32) -> HostResult<i32> {
Ok(if cache_idx == 0 { 1 } else { cache_idx })
}
/// A field getter over the transaction; fails on a negative selector.
fn get_tx_field(&self, field: i32, out: &mut [u8]) -> HostResult<usize> {
if field < 0 {
return Err(HostError::FieldNotFound);
}
put(out, &[field as u8])
}
/// Fails on a field it doesn't know, so the error channel is exercised too.
fn get_current_ledger_obj_field(&self, field: i32, out: &mut [u8]) -> HostResult<usize> {
if field < 0 {
return Err(HostError::FieldNotFound);
}
put(out, &[field as u8])
}
/// A field getter over a cached object, keyed by slot and selector.
fn get_ledger_obj_field(
&self,
cache_idx: i32,
field: i32,
out: &mut [u8],
) -> HostResult<usize> {
if cache_idx <= 0 || field < 0 {
return Err(HostError::FieldNotFound);
}
put(out, &[cache_idx as u8, field as u8])
}
/// A nested-field getter over the transaction, keyed by the locator bytes.
fn get_tx_nested_field(&self, locator: &[u8], out: &mut [u8]) -> HostResult<usize> {
if locator.is_empty() {
return Err(HostError::LocatorMalformed);
}
put(out, &[locator[0], locator.len() as u8])
}
/// The same, over the current ledger object.
fn get_current_ledger_obj_nested_field(
&self,
locator: &[u8],
out: &mut [u8],
) -> HostResult<usize> {
if locator.is_empty() {
return Err(HostError::LocatorMalformed);
}
put(out, &[locator.len() as u8, locator[0]])
}
/// The same, over a cached object keyed by slot.
fn get_ledger_obj_nested_field(
&self,
cache_idx: i32,
locator: &[u8],
out: &mut [u8],
) -> HostResult<usize> {
if cache_idx <= 0 || locator.is_empty() {
return Err(HostError::LocatorMalformed);
}
put(out, &[cache_idx as u8, locator[0]])
}
/// A scalar-in, scalar-out count; `NoArray` on a negative selector.
fn get_tx_array_len(&self, field: i32) -> HostResult<i32> {
if field < 0 {
return Err(HostError::NoArray);
}
Ok(field)
}
/// The same, over the current ledger object.
fn get_current_ledger_obj_array_len(&self, field: i32) -> HostResult<i32> {
if field < 0 {
return Err(HostError::NoArray);
}
Ok(field + 1)
}
/// The same, over a cached object keyed by slot.
fn get_ledger_obj_array_len(&self, cache_idx: i32, field: i32) -> HostResult<i32> {
if cache_idx <= 0 || field < 0 {
return Err(HostError::NoArray);
}
Ok(cache_idx + field)
}
/// A nested array-length getter, keyed by the locator bytes.
fn get_tx_nested_array_len(&self, locator: &[u8]) -> HostResult<i32> {
if locator.is_empty() {
return Err(HostError::LocatorMalformed);
}
Ok(locator.len() as i32)
}
/// The same, over the current ledger object.
fn get_current_ledger_obj_nested_array_len(&self, locator: &[u8]) -> HostResult<i32> {
if locator.is_empty() {
return Err(HostError::LocatorMalformed);
}
Ok(locator.len() as i32 + 1)
}
/// The same, over a cached object keyed by slot.
fn get_ledger_obj_nested_array_len(&self, cache_idx: i32, locator: &[u8]) -> HostResult<i32> {
if cache_idx <= 0 || locator.is_empty() {
return Err(HostError::LocatorMalformed);
}
Ok(cache_idx + locator.len() as i32)
}
/// Reads three regions and returns a verdict: valid unless the signature is empty.
fn check_signature(
&self,
_message: &[u8],
signature: &[u8],
_pubkey: &[u8],
) -> HostResult<i32> {
Ok(i32::from(!signature.is_empty()))
}
/// A keylet getter: reads an account, writes a 32-byte keylet; `InvalidAccount`
/// on an empty account.
fn account_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult<usize> {
if account.is_empty() {
return Err(HostError::InvalidAccount);
}
put(out, &[account[0]; HASH_LEN])
}
/// A two-asset keylet getter; `InvalidParams` if the two assets are equal.
fn amm_keylet(&self, asset1: &[u8], asset2: &[u8], out: &mut [u8]) -> HostResult<usize> {
if asset1 == asset2 {
return Err(HostError::InvalidParams);
}
put(out, &[asset1.len() as u8; HASH_LEN])
}
/// A keylet from an account and a sequence; `InvalidAccount` on an empty account.
fn check_keylet(&self, account: &[u8], _seq: i32, out: &mut [u8]) -> HostResult<usize> {
if account.is_empty() {
return Err(HostError::InvalidAccount);
}
put(out, &[account[0]; HASH_LEN])
}
/// A keylet from subject, issuer, and credential type; `InvalidAccount` if either
/// account is empty, `InvalidParams` if the type is empty.
fn credential_keylet(
&self,
subject: &[u8],
issuer: &[u8],
credential_type: &[u8],
out: &mut [u8],
) -> HostResult<usize> {
if subject.is_empty() || issuer.is_empty() {
return Err(HostError::InvalidAccount);
}
if credential_type.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &[subject[0]; HASH_LEN])
}
/// A keylet from two accounts; `InvalidAccount` if either is empty, `InvalidParams`
/// if they are equal.
fn delegate_keylet(
&self,
account: &[u8],
authorize: &[u8],
out: &mut [u8],
) -> HostResult<usize> {
if account.is_empty() || authorize.is_empty() {
return Err(HostError::InvalidAccount);
}
if account == authorize {
return Err(HostError::InvalidParams);
}
put(out, &[account[0]; HASH_LEN])
}
/// The same two-account shape, for a `DepositPreauth`.
fn deposit_preauth_keylet(
&self,
account: &[u8],
authorize: &[u8],
out: &mut [u8],
) -> HostResult<usize> {
if account.is_empty() || authorize.is_empty() {
return Err(HostError::InvalidAccount);
}
if account == authorize {
return Err(HostError::InvalidParams);
}
put(out, &[authorize[0]; HASH_LEN])
}
/// A single-account keylet, for a `DID`.
fn did_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult<usize> {
if account.is_empty() {
return Err(HostError::InvalidAccount);
}
put(out, &[account[0]; HASH_LEN])
}
/// The account-and-sequence shape, for an `Escrow`.
fn escrow_keylet(&self, account: &[u8], _seq: i32, out: &mut [u8]) -> HostResult<usize> {
if account.is_empty() {
return Err(HostError::InvalidAccount);
}
put(out, &[account[0]; HASH_LEN])
}
/// A keylet from two accounts and a currency; `InvalidAccount` if either account
/// is empty, `InvalidParams` if they are equal or the currency is empty.
fn trust_line_keylet(
&self,
account1: &[u8],
account2: &[u8],
currency: &[u8],
out: &mut [u8],
) -> HostResult<usize> {
if account1.is_empty() || account2.is_empty() {
return Err(HostError::InvalidAccount);
}
if account1 == account2 || currency.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &[account1[0]; HASH_LEN])
}
/// The issuer-and-sequence shape, for an `MPTokenIssuance`.
fn mptoken_issuance_keylet(
&self,
issuer: &[u8],
_seq: i32,
out: &mut [u8],
) -> HostResult<usize> {
if issuer.is_empty() {
return Err(HostError::InvalidAccount);
}
put(out, &[issuer[0]; HASH_LEN])
}
/// A keylet from an MPT id and a holder; `InvalidParams` if the id is empty,
/// `InvalidAccount` if the holder is empty.
fn mptoken_keylet(&self, mptid: &[u8], holder: &[u8], out: &mut [u8]) -> HostResult<usize> {
if mptid.is_empty() {
return Err(HostError::InvalidParams);
}
if holder.is_empty() {
return Err(HostError::InvalidAccount);
}
put(out, &[mptid[0]; HASH_LEN])
}
/// The account-and-sequence shape, for an `NFTokenOffer`.
fn nftoken_offer_keylet(&self, account: &[u8], _seq: i32, out: &mut [u8]) -> HostResult<usize> {
if account.is_empty() {
return Err(HostError::InvalidAccount);
}
put(out, &[account[0]; HASH_LEN])
}
/// The same account-and-sequence shape, for an `Offer`.
fn offer_keylet(&self, account: &[u8], _seq: i32, out: &mut [u8]) -> HostResult<usize> {
if account.is_empty() {
return Err(HostError::InvalidAccount);
}
put(out, &[account[0]; HASH_LEN])
}
/// The same account-and-scalar shape, for an `Oracle` keyed by document id.
fn oracle_keylet(&self, account: &[u8], _doc_id: i32, out: &mut [u8]) -> HostResult<usize> {
if account.is_empty() {
return Err(HostError::InvalidAccount);
}
put(out, &[account[0]; HASH_LEN])
}
/// A two-account-and-sequence shape, for a `PayChannel`; `InvalidAccount` if
/// either account is empty.
fn paychannel_keylet(
&self,
account: &[u8],
destination: &[u8],
_seq: i32,
out: &mut [u8],
) -> HostResult<usize> {
if account.is_empty() || destination.is_empty() {
return Err(HostError::InvalidAccount);
}
put(out, &[account[0]; HASH_LEN])
}
/// The same account-and-sequence shape, for a `PermissionedDomain`.
fn permissioned_domain_keylet(
&self,
account: &[u8],
_seq: i32,
out: &mut [u8],
) -> HostResult<usize> {
if account.is_empty() {
return Err(HostError::InvalidAccount);
}
put(out, &[account[0]; HASH_LEN])
}
/// The account-only shape, for a `SignerList`.
fn signer_list_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult<usize> {
if account.is_empty() {
return Err(HostError::InvalidAccount);
}
put(out, &[account[0]; HASH_LEN])
}
/// The same account-and-sequence shape, for a `Ticket`.
fn ticket_keylet(&self, account: &[u8], _seq: i32, out: &mut [u8]) -> HostResult<usize> {
if account.is_empty() {
return Err(HostError::InvalidAccount);
}
put(out, &[account[0]; HASH_LEN])
}
/// The same account-and-sequence shape, for a `Vault`.
fn vault_keylet(&self, account: &[u8], _seq: i32, out: &mut [u8]) -> HostResult<usize> {
if account.is_empty() {
return Err(HostError::InvalidAccount);
}
put(out, &[account[0]; HASH_LEN])
}
fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult<usize> {
let mut digest = [0; HASH_LEN];
digest[0] = data.len() as u8;
put(out, &digest)
}
fn trace(&self, msg: &str, data: &[u8], data_type: TraceDataType) -> HostResult<()> {
self.traced
.borrow_mut()
.push(format!("{msg}/{data_type:?}/{}", data.len()));
Ok(())
}
/// Reads a data blob and returns the count of bytes stored.
fn update_data(&self, data: &[u8]) -> HostResult<i32> {
Ok(data.len() as i32)
}
/// Reads an account and an nft id, writes a byte value; `InvalidParams` if either
/// is empty.
fn get_nft(&self, account: &[u8], nft_id: &[u8], out: &mut [u8]) -> HostResult<usize> {
if account.is_empty() || nft_id.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &[account[0]; HASH_LEN])
}
/// Reads an nft id, writes a byte value; `InvalidParams` on an empty id.
fn get_nft_issuer(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult<usize> {
if nft_id.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &[nft_id[0]; HASH_LEN])
}
/// The same, for the taxon.
fn get_nft_taxon(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult<usize> {
if nft_id.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &nft_id[0].to_le_bytes())
}
/// Reads an nft id and returns a scalar; `InvalidParams` on an empty id.
fn get_nft_flags(&self, nft_id: &[u8]) -> HostResult<i32> {
if nft_id.is_empty() {
return Err(HostError::InvalidParams);
}
Ok(i32::from(nft_id[0]))
}
/// The same, for the transfer fee.
fn get_nft_transfer_fee(&self, nft_id: &[u8]) -> HostResult<i32> {
if nft_id.is_empty() {
return Err(HostError::InvalidParams);
}
Ok(i32::from(nft_id[0]))
}
/// The same byte-output shape, for the sequence number.
fn get_nft_sequence(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult<usize> {
if nft_id.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &nft_id[0].to_le_bytes())
}
/// A scalar-in float: writes the low byte of `x` as a stand-in float.
fn float_from_int(&self, x: i64, _mode: i32, out: &mut [u8]) -> HostResult<usize> {
put(out, &[x as u8])
}
/// A byte-in float; `InvalidParams` on an empty region.
fn float_from_uint(&self, x: &[u8], _mode: i32, out: &mut [u8]) -> HostResult<usize> {
if x.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &[x[0]])
}
/// The same, for a serialized amount.
fn float_from_stamount(&self, amount: &[u8], _mode: i32, out: &mut [u8]) -> HostResult<usize> {
if amount.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &[amount[0]])
}
/// The same, for a serialized number.
fn float_from_stnumber(&self, number: &[u8], _mode: i32, out: &mut [u8]) -> HostResult<usize> {
if number.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &[number[0]])
}
/// A float rounded to an integer, written as bytes.
fn float_to_int(&self, x: &[u8], _mode: i32, out: &mut [u8]) -> HostResult<usize> {
if x.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &[x[0]])
}
/// Writes a mantissa (its first byte) and an exponent (its first byte) to two
/// regions, returning their combined length.
fn float_to_mant_exp(
&self,
x: &[u8],
mantissa_out: &mut [u8],
exponent_out: &mut [u8],
) -> HostResult<usize> {
if x.is_empty() {
return Err(HostError::InvalidParams);
}
let m = put(mantissa_out, &[x[0]])?;
let e = put(exponent_out, &[x[0]])?;
Ok(m + e)
}
/// A two-scalar-in float.
fn float_from_mant_exp(
&self,
mantissa: i64,
_exponent: i32,
_mode: i32,
out: &mut [u8],
) -> HostResult<usize> {
put(out, &[mantissa as u8])
}
/// Reads two floats and returns a scalar; `InvalidParams` if either is empty.
fn float_compare(&self, x: &[u8], y: &[u8]) -> HostResult<i32> {
if x.is_empty() || y.is_empty() {
return Err(HostError::InvalidParams);
}
Ok(i32::from(x[0]) - i32::from(y[0]))
}
/// A binary float operator; `InvalidParams` if either operand is empty.
fn float_add(&self, x: &[u8], y: &[u8], _mode: i32, out: &mut [u8]) -> HostResult<usize> {
if x.is_empty() || y.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &[x[0]])
}
/// The same shape, for subtraction.
fn float_subtract(&self, x: &[u8], y: &[u8], _mode: i32, out: &mut [u8]) -> HostResult<usize> {
if x.is_empty() || y.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &[x[0]])
}
/// The same shape, for multiplication.
fn float_multiply(&self, x: &[u8], y: &[u8], _mode: i32, out: &mut [u8]) -> HostResult<usize> {
if x.is_empty() || y.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &[x[0]])
}
/// The same shape, for division.
fn float_divide(&self, x: &[u8], y: &[u8], _mode: i32, out: &mut [u8]) -> HostResult<usize> {
if x.is_empty() || y.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &[x[0]])
}
/// The same shape, for exponentiation.
fn float_power(&self, x: &[u8], _n: i32, _mode: i32, out: &mut [u8]) -> HostResult<usize> {
if x.is_empty() {
return Err(HostError::InvalidParams);
}
put(out, &[x[0]])
}
}
#[test]
fn the_trait_is_implementable() {
let host = FakeHost::default();
let mut out = [0u8; HASH_LEN];
assert_eq!(host.get_ledger_sqn(&mut out), Ok(4));
assert_eq!(out[..4], [7, 0, 0, 0]);
assert_eq!(host.get_parent_ledger_time(&mut out), Ok(4));
assert_eq!(out[..4], [9, 0, 0, 0]);
assert_eq!(host.get_parent_ledger_hash(&mut out), Ok(HASH_LEN));
assert_eq!(out[0], 0xab);
assert_eq!(host.get_base_fee(&mut out), Ok(4));
assert_eq!(out[..4], [10, 0, 0, 0]);
assert_eq!(host.is_amendment_enabled(&[1; 32]), Ok(1));
assert_eq!(host.is_amendment_enabled(&[]), Ok(0));
assert_eq!(host.cache_ledger_obj(&[1; 32], 0), Ok(1));
assert_eq!(host.cache_ledger_obj(&[1; 32], 5), Ok(5));
assert_eq!(host.get_tx_field(5, &mut out), Ok(1));
assert_eq!(out[0], 5);
assert_eq!(host.get_current_ledger_obj_field(3, &mut out), Ok(1));
assert_eq!(out[0], 3);
assert_eq!(host.get_ledger_obj_field(2, 4, &mut out), Ok(2));
assert_eq!(out[..2], [2, 4]);
assert_eq!(host.get_tx_nested_field(&[9, 0, 0, 0], &mut out), Ok(2));
assert_eq!(out[..2], [9, 4]);
assert_eq!(
host.get_current_ledger_obj_nested_field(&[9, 0, 0, 0], &mut out),
Ok(2)
);
assert_eq!(out[..2], [4, 9]);
assert_eq!(
host.get_ledger_obj_nested_field(3, &[9, 0, 0, 0], &mut out),
Ok(2)
);
assert_eq!(out[..2], [3, 9]);
assert_eq!(host.get_tx_array_len(3), Ok(3));
assert_eq!(host.get_tx_array_len(-1), Err(HostError::NoArray));
assert_eq!(host.get_current_ledger_obj_array_len(3), Ok(4));
assert_eq!(
host.get_current_ledger_obj_array_len(-1),
Err(HostError::NoArray)
);
assert_eq!(host.get_ledger_obj_array_len(2, 3), Ok(5));
assert_eq!(host.get_ledger_obj_array_len(0, 3), Err(HostError::NoArray));
assert_eq!(host.get_tx_nested_array_len(&[9, 0, 0, 0]), Ok(4));
assert_eq!(
host.get_tx_nested_array_len(&[]),
Err(HostError::LocatorMalformed)
);
assert_eq!(
host.get_current_ledger_obj_nested_array_len(&[9, 0, 0, 0]),
Ok(5)
);
assert_eq!(
host.get_current_ledger_obj_nested_array_len(&[]),
Err(HostError::LocatorMalformed)
);
assert_eq!(
host.get_ledger_obj_nested_array_len(2, &[9, 0, 0, 0]),
Ok(6)
);
assert_eq!(
host.get_ledger_obj_nested_array_len(0, &[9, 0, 0, 0]),
Err(HostError::LocatorMalformed)
);
assert_eq!(host.check_signature(b"msg", b"sig", b"pk"), Ok(1));
assert_eq!(host.check_signature(b"msg", b"", b"pk"), Ok(0));
assert_eq!(host.account_keylet(&[7; 20], &mut out), Ok(HASH_LEN));
assert_eq!(out[0], 7);
assert_eq!(
host.account_keylet(&[], &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(host.amm_keylet(&[1; 20], &[2; 40], &mut out), Ok(HASH_LEN));
assert_eq!(out[0], 20);
assert_eq!(
host.amm_keylet(&[1; 20], &[1; 20], &mut out),
Err(HostError::InvalidParams)
);
assert_eq!(host.check_keylet(&[7; 20], 5, &mut out), Ok(HASH_LEN));
assert_eq!(out[0], 7);
assert_eq!(
host.check_keylet(&[], 5, &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(
host.credential_keylet(&[7; 20], &[8; 20], b"cred", &mut out),
Ok(HASH_LEN)
);
assert_eq!(out[0], 7);
assert_eq!(
host.credential_keylet(&[], &[8; 20], b"cred", &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(
host.delegate_keylet(&[7; 20], &[8; 20], &mut out),
Ok(HASH_LEN)
);
assert_eq!(out[0], 7);
assert_eq!(
host.delegate_keylet(&[], &[8; 20], &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(
host.deposit_preauth_keylet(&[7; 20], &[8; 20], &mut out),
Ok(HASH_LEN)
);
assert_eq!(out[0], 8);
assert_eq!(
host.deposit_preauth_keylet(&[7; 20], &[7; 20], &mut out),
Err(HostError::InvalidParams)
);
assert_eq!(host.did_keylet(&[7; 20], &mut out), Ok(HASH_LEN));
assert_eq!(out[0], 7);
assert_eq!(
host.did_keylet(&[], &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(host.escrow_keylet(&[7; 20], 5, &mut out), Ok(HASH_LEN));
assert_eq!(out[0], 7);
assert_eq!(
host.escrow_keylet(&[], 5, &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(
host.trust_line_keylet(&[7; 20], &[8; 20], &[1; 20], &mut out),
Ok(HASH_LEN)
);
assert_eq!(out[0], 7);
assert_eq!(
host.trust_line_keylet(&[7; 20], &[7; 20], &[1; 20], &mut out),
Err(HostError::InvalidParams)
);
assert_eq!(
host.mptoken_issuance_keylet(&[7; 20], 5, &mut out),
Ok(HASH_LEN)
);
assert_eq!(out[0], 7);
assert_eq!(
host.mptoken_issuance_keylet(&[], 5, &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(
host.mptoken_keylet(&[9; 24], &[8; 20], &mut out),
Ok(HASH_LEN)
);
assert_eq!(out[0], 9);
assert_eq!(
host.mptoken_keylet(&[], &[8; 20], &mut out),
Err(HostError::InvalidParams)
);
assert_eq!(
host.nftoken_offer_keylet(&[7; 20], 5, &mut out),
Ok(HASH_LEN)
);
assert_eq!(out[0], 7);
assert_eq!(
host.nftoken_offer_keylet(&[], 5, &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(host.offer_keylet(&[7; 20], 5, &mut out), Ok(HASH_LEN));
assert_eq!(out[0], 7);
assert_eq!(
host.offer_keylet(&[], 5, &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(host.oracle_keylet(&[7; 20], 5, &mut out), Ok(HASH_LEN));
assert_eq!(out[0], 7);
assert_eq!(
host.oracle_keylet(&[], 5, &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(
host.paychannel_keylet(&[7; 20], &[8; 20], 5, &mut out),
Ok(HASH_LEN)
);
assert_eq!(out[0], 7);
assert_eq!(
host.paychannel_keylet(&[7; 20], &[], 5, &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(
host.permissioned_domain_keylet(&[7; 20], 5, &mut out),
Ok(HASH_LEN)
);
assert_eq!(out[0], 7);
assert_eq!(
host.permissioned_domain_keylet(&[], 5, &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(host.signer_list_keylet(&[7; 20], &mut out), Ok(HASH_LEN));
assert_eq!(out[0], 7);
assert_eq!(
host.signer_list_keylet(&[], &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(host.ticket_keylet(&[7; 20], 5, &mut out), Ok(HASH_LEN));
assert_eq!(out[0], 7);
assert_eq!(
host.ticket_keylet(&[], 5, &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(host.vault_keylet(&[7; 20], 5, &mut out), Ok(HASH_LEN));
assert_eq!(out[0], 7);
assert_eq!(
host.vault_keylet(&[], 5, &mut out),
Err(HostError::InvalidAccount)
);
assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN));
assert_eq!(out[0], 3);
assert_eq!(host.trace("hello", b"xy", TraceDataType::AsHex), Ok(()));
assert_eq!(host.update_data(b"abcd"), Ok(4));
assert_eq!(host.get_nft(&[7; 20], &[9; 32], &mut out), Ok(HASH_LEN));
assert_eq!(out[0], 7);
assert_eq!(
host.get_nft(&[], &[9; 32], &mut out),
Err(HostError::InvalidParams)
);
assert_eq!(host.get_nft_issuer(&[9; 32], &mut out), Ok(HASH_LEN));
assert_eq!(out[0], 9);
assert_eq!(
host.get_nft_issuer(&[], &mut out),
Err(HostError::InvalidParams)
);
assert_eq!(host.get_nft_taxon(&[9; 32], &mut out), Ok(1));
assert_eq!(host.get_nft_flags(&[9; 32]), Ok(9));
assert_eq!(host.get_nft_flags(&[]), Err(HostError::InvalidParams));
assert_eq!(host.get_nft_transfer_fee(&[9; 32]), Ok(9));
assert_eq!(host.get_nft_sequence(&[9; 32], &mut out), Ok(1));
assert_eq!(host.float_from_int(5, 0, &mut out), Ok(1));
assert_eq!(host.float_from_uint(&[3; 8], 0, &mut out), Ok(1));
assert_eq!(host.float_from_stamount(&[3; 8], 0, &mut out), Ok(1));
assert_eq!(host.float_from_stnumber(&[3; 8], 0, &mut out), Ok(1));
assert_eq!(host.float_to_int(&[3; 8], 0, &mut out), Ok(1));
let mut mant = [0u8; 8];
let mut exp = [0u8; 4];
assert_eq!(host.float_to_mant_exp(&[3; 8], &mut mant, &mut exp), Ok(2));
assert_eq!(host.float_from_mant_exp(5, 0, 0, &mut out), Ok(1));
assert_eq!(host.float_compare(&[9; 8], &[4; 8]), Ok(5));
assert_eq!(
host.float_compare(&[], &[4; 8]),
Err(HostError::InvalidParams)
);
assert_eq!(host.float_add(&[3; 8], &[4; 8], 0, &mut out), Ok(1));
assert_eq!(host.float_subtract(&[3; 8], &[4; 8], 0, &mut out), Ok(1));
assert_eq!(host.float_multiply(&[3; 8], &[4; 8], 0, &mut out), Ok(1));
assert_eq!(host.float_divide(&[3; 8], &[4; 8], 0, &mut out), Ok(1));
assert_eq!(host.float_power(&[3; 8], 2, 0, &mut out), Ok(1));
assert_eq!(*host.traced.borrow(), ["hello/AsHex/2"]);
}
/// The error channel every declaration carries: an `Err` the VM turns into the
/// wire's negative return code.
#[test]
fn a_failing_call_reports_its_error_code() {
let host = FakeHost::default();
let mut out = [0u8; 8];
assert_eq!(
host.get_current_ledger_obj_field(-1, &mut out),
Err(HostError::FieldNotFound)
);
assert_eq!(HostError::FieldNotFound.code(), -2);
}
/// A host reports the value's true length even when it cannot write it, which is
/// what lets the engine answer `BufferTooSmall` on the guest's behalf.
#[test]
fn a_short_buffer_still_reports_the_true_length() {
let host = FakeHost::default();
let mut out = [0u8; 2];
assert_eq!(host.get_ledger_sqn(&mut out), Ok(4));
assert_eq!(
out,
[0, 0],
"nothing is written when the value does not fit"
);
}
/// The VM reaches the host as one shared trait object held in the wasmi `Store`,
/// which is what the `&self` receivers are for.
#[test]
fn the_trait_is_callable_through_a_shared_trait_object() {
let fake = FakeHost::default();
let host: &dyn HostFunctions = &fake;
let mut out = [0u8; 4];
assert_eq!(host.get_ledger_sqn(&mut out), Ok(4));
assert_eq!(
host.trace("count", &1i64.to_le_bytes(), TraceDataType::Int64),
Ok(())
);
assert_eq!(*fake.traced.borrow(), ["count/Int64/8"]);
}
/// The whole table, written out: the one place the ABI's wire names and gas costs
/// appear as literals, and a deliberate change-detector, since both are consensus
/// input. Everything else reads `HostFunctionSpec::gas()` instead.
///
/// `ALL` is in declaration order, so comparing the whole vec pins the order and the
/// membership too.
#[test]
fn the_spec_table_matches_the_declarations() {
let table: Vec<(&str, u64)> = HostFunctionSpec::ALL
.iter()
.map(|function| (function.wasm_name(), function.gas()))
.collect();
assert_eq!(
table,
[
("ldgr_index", 60),
("parent_ldgr_time", 60),
("parent_ldgr_hash", 60),
("base_fee", 60),
("amendment_enabled", 100),
("cache_le", 5000),
("tx_field", 70),
("home_le_field", 70),
("le_field", 70),
("tx_inner", 110),
("home_le_inner", 110),
("le_inner", 110),
("tx_arr_len", 40),
("home_le_arr_len", 40),
("le_arr_len", 40),
("tx_inner_arr_len", 70),
("home_le_inner_arr_len", 70),
("le_inner_arr_len", 70),
("check_sig", 300),
("accountroot_id", 350),
("amm_id", 450),
("check_id", 350),
("credential_id", 350),
("delegate_id", 350),
("deposit_preauth_id", 350),
("did_id", 350),
("escrow_id", 350),
("trustline_id", 400),
("mpt_issuance_id", 350),
("mptoken_id", 500),
("nft_offer_id", 350),
("offer_id", 350),
("oracle_id", 350),
("paychan_id", 350),
("permissioned_domain_id", 350),
("signers_id", 350),
("ticket_id", 350),
("vault_id", 350),
("sha512_half", 2000),
("trace", 30),
("set_data", 1000),
("nft_uri", 5000),
("nft_issuer", 70),
("nft_taxon", 60),
("nft_flags", 60),
("nft_xfer_fee", 60),
("nft_serial", 60),
("float_from_int", 100),
("float_from_uint", 130),
("float_from_stamount", 150),
("float_from_stnumber", 150),
("float_to_int", 130),
("float_to_mant_exp", 130),
("float_from_mant_exp", 100),
("float_cmp", 80),
("float_add", 160),
("float_sub", 160),
("float_mult", 300),
("float_div", 300),
("float_pow", 5500),
]
);
}
/// The other half of the wire vocabulary, and the same change-detector argument: the
/// codes are what a guest passes, so they are pinned as literals here. `ALL` is in code
/// order, so the round trip pins the discriminants and not just the membership.
#[test]
fn every_trace_data_type_survives_the_wire() {
let codes: Vec<i32> = TraceDataType::ALL.iter().map(|t| t.code()).collect();
assert_eq!(codes, [1, 2, 3, 4, 5, 6, 7]);
for &data_type in TraceDataType::ALL {
assert_eq!(TraceDataType::from_code(data_type.code()), Some(data_type));
}
}
/// A code no declaration names is refused rather than read as a neighbouring type.
/// Zero is the one worth naming: it is what a guest sends by omission.
#[test]
fn an_unnamed_trace_data_type_code_is_refused() {
for code in [0, -1, 8, i32::MAX, i32::MIN] {
assert_eq!(TraceDataType::from_code(code), None, "code {code}");
}
}
/// `ALL` is what a wasm engine iterates to register imports, so no two declarations
/// may collapse to the same wire name. The table above pins membership and order;
/// this adds only uniqueness, and restates nothing.
#[test]
fn every_variant_appears_in_all_exactly_once() {
let names: HashSet<&str> = HostFunctionSpec::ALL
.iter()
.map(|function| function.wasm_name())
.collect();
assert_eq!(names.len(), HostFunctionSpec::ALL.len());
}
/// Both accessors are `const`, so an engine can build its import and gas tables at
/// compile time rather than on every invocation. The assertions sit in `const`
/// blocks so they are checked while compiling, which is the claim; the values
/// themselves are pinned above.
#[test]
fn the_table_is_usable_in_const_context() {
const NAME: &str = HostFunctionSpec::Trace.wasm_name();
const GAS: u64 = HostFunctionSpec::Trace.gas();
const { assert!(!NAME.is_empty()) };
const { assert!(GAS > 0) };
}

View File

@@ -1,102 +0,0 @@
//! Exercises what `host_errors!` generates: the wire codes, the set
//! [`HostError::ALL`] names, and the round trip between them.
//!
//! The codes are consensus input — they are what a guest reads off a failed host
//! call — so they are pinned here as literals and derived everywhere else.
use xrpl_host_functions::HostError;
/// The whole set, written out in the order `ALL` gives it: the one place the wire
/// codes appear as literals, and a deliberate change-detector, since a code that
/// moves changes what every deployed guest is told.
#[test]
fn the_error_table_matches_the_declarations() {
let table: Vec<(HostError, i32)> = HostError::ALL
.iter()
.map(|&error| (error, error.code()))
.collect();
assert_eq!(
table,
[
(HostError::Unimplemented, -1),
(HostError::FieldNotFound, -2),
(HostError::BufferTooSmall, -3),
(HostError::NoArray, -4),
(HostError::NotLeafField, -5),
(HostError::LocatorMalformed, -6),
(HostError::SlotOutRange, -7),
(HostError::SlotsFull, -8),
(HostError::EmptySlot, -9),
(HostError::LedgerObjNotFound, -10),
(HostError::OutOfTransferLimit, -11),
(HostError::DataFieldTooLarge, -12),
(HostError::PointerOutOfBounds, -13),
(HostError::NoMemExported, -14),
(HostError::InvalidParams, -15),
(HostError::InvalidAccount, -16),
(HostError::InvalidField, -17),
(HostError::IndexOutOfBounds, -18),
(HostError::FloatInputMalformed, -19),
(HostError::FloatComputationError, -20),
(HostError::InternalFatal, i32::MIN),
]
);
}
/// The guest-facing set is `-1 ..= -20` and nothing else: those entries are xrpld's
/// `HostFunctionError`, and each is a code some contract may read.
///
/// `InternalFatal` is the one deliberate exception, exempted by name rather than by
/// widening the range: a condition with no number a contract can act on needs no number
/// in the range a contract reads, and holding it at `i32::MIN` is what keeps it from
/// ever colliding with a code appended to xrpld's list.
#[test]
fn every_code_but_the_sentinel_is_in_the_shared_range() {
let shared: Vec<HostError> = HostError::ALL
.iter()
.copied()
.filter(|&error| error != HostError::InternalFatal)
.collect();
let outside: Vec<HostError> = shared
.iter()
.copied()
.filter(|error| !(-20..=-1).contains(&error.code()))
.collect();
assert!(outside.is_empty(), "outside -1..=-20: {outside:?}");
assert_eq!(shared.len(), 20);
assert_eq!(HostError::InternalFatal.code(), i32::MIN);
assert_eq!(HostError::ALL.len(), 21);
}
/// Every code a guest can be handed comes back as the error that produced it, so a
/// caller reading a negative return value recovers the condition and not a
/// neighbouring one. The table above pins the numbers; this adds only the round
/// trip.
#[test]
fn every_wire_code_round_trips_back_to_its_error() {
for &error in HostError::ALL {
assert_eq!(HostError::from_code(error.code()), error, "{error:?}");
}
}
/// A code from outside the set is `InternalFatal`: a host answering something this ABI
/// does not define has not served the call, whatever it meant by it, and success is not
/// an error at all.
///
/// `-21` is the code xrpld would append next, so it is the one that decides whether a
/// list this crate has not caught up with reaches a guest or stops the run. `i32::MIN +
/// 1` is next to the sentinel and unassigned, which is what makes the sentinel a value
/// rather than a range.
#[test]
fn a_code_outside_the_set_is_internal_fatal() {
for code in [-21, i32::MIN + 1, 0, 1, i32::MAX] {
assert_eq!(
HostError::from_code(code),
HostError::InternalFatal,
"{code}"
);
}
}

View File

@@ -1,49 +0,0 @@
//! Assembles WebAssembly text for the C++ test suite. **Test-only.**
//!
//! A crate of its own rather than an entry on `xrpl-wasm-vm-ffi`, and the separation is the
//! point. The engine pins `wasmi = { default-features = false }` precisely so a text
//! assembler cannot reach the consensus path — wasmi's `wat` feature is on by default and
//! makes `Module::new` accept text as readily as binary, which would make a transaction's
//! validity a build flag. Putting `compile_wat` on the production bridge would link `wat`
//! into xrpld even if nothing called it.
//!
//! Linked only into `xrpl_tests`, never into `libxrpl` or `xrpld`, so "no assembler in the
//! shipped node" is a property of the link graph rather than a flag someone can flip.
#![deny(rustdoc::broken_intra_doc_links)]
#[cxx::bridge(namespace = "rs::wasm_testkit")]
mod ffi {
extern "Rust" {
/// Assemble `wat` to a wasm module.
///
/// Throws `rust::Error` on invalid input, which is what a test wants: a typo in a
/// fixture should fail the test that holds it, at the line that holds it.
fn compile_wat(wat: &str) -> Result<Vec<u8>>;
}
}
fn compile_wat(wat: &str) -> Result<Vec<u8>, wat::Error> {
wat::parse_str(wat)
}
#[cfg(test)]
mod tests {
use super::compile_wat;
#[test]
fn a_module_assembles_to_something_beginning_with_the_wasm_magic() {
let wasm = compile_wat("(module)").expect("assembles");
assert_eq!(&wasm[..4], b"\0asm");
}
#[test]
fn a_typo_is_an_error_rather_than_a_module() {
let error = compile_wat("(module (func (export").expect_err("must not assemble");
assert!(
!error.to_string().is_empty(),
"the error has to say something"
);
}
}

View File

@@ -1,12 +0,0 @@
[package]
name = "xrpl-wasm-vm-ffi"
version = "0.1.0"
edition.workspace = true
[lib]
crate-type = ["staticlib", "rlib"]
[dependencies]
cxx.workspace = true
xrpl-host-functions = { path = "../xrpl-host-functions" }
xrpl-wasm-vm = { path = "../xrpl-wasm-vm" }

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
[package]
name = "xrpl-wasm-vm"
version = "0.1.0"
edition.workspace = true
[dependencies]
wasmi = { version = "2.0.0-beta.10", default-features = false, features = ["std", "validate", "portable-dispatch"] }
xrpl-host-functions = { path = "../xrpl-host-functions" }
[dev-dependencies]
wat = "1"

View File

@@ -1,844 +0,0 @@
use crate::region::Region;
use crate::vm::{MAX_FIELD_BYTES, VmState};
use core::ops::Range;
use wasmi::{Caller, Memory};
use xrpl_host_functions::{HostError, HostFunctionSpec, HostFunctions, HostResult};
/// A condition that stops the run. It is a property of the run rather than an answer
/// to a call, so it reaches no guest and carries no wire code — which is why it is
/// not a [`HostError`]: no host can report one and no contract can read one.
///
/// The three are the outcomes a host call can end a run with, and
/// `From<Fault> for RunError` in `vm.rs` is where each gets its name.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Fault {
/// This call's charge would take the meter below zero. The guest exhausting the
/// meter with its own instructions reaches [`crate::vm::RunError::OutOfGas`] by
/// wasmi's `OutOfFuel` trap instead, never through here.
OutOfGas,
/// The call could not be served: either the host said so, or this engine's own
/// fuel meter did not answer.
Internal,
/// There is no linear memory to work in — the module exports none, or the call
/// came from a start section, which runs before there is an instance.
NoMemory,
}
/// How a host call fails: with a code the guest reads off the return value, or with a
/// [`Fault`] that stops the run.
///
/// **The variant picks the channel.** [`to_wire`] reads it rather than asking a
/// predicate, so the two cannot disagree, and a [`FatalHostError`] cannot be built
/// around something a guest was supposed to see.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CallError {
Code(HostError),
Fatal(Fault),
}
/// A host call's result inside the engine: [`HostResult`] plus the faults only the
/// engine can raise.
pub(crate) type CallResult<T> = Result<T, CallError>;
/// Which channel a host's answer takes, decided once, here.
///
/// Three codes stop the run instead of reaching the contract that asked. Each says the
/// call was not served at all — the host could not do it, it has not been wired, or
/// there is nowhere to put the answer — and a contract has no business interpreting
/// any of them, so it is told nothing and the run ends. Every other code is the
/// contract's to read.
impl From<HostError> for CallError {
fn from(error: HostError) -> CallError {
match error {
HostError::InternalFatal => CallError::Fatal(Fault::Internal),
HostError::Unimplemented => CallError::Fatal(Fault::Internal),
HostError::NoMemExported => CallError::Fatal(Fault::NoMemory),
code => CallError::Code(code),
}
}
}
/// The payload a trap carries so [`crate::vm::run`] can name the outcome without
/// parsing a message. Holds a [`Fault`], so by construction no guest-visible code can
/// leave through this channel.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct FatalHostError(pub(crate) Fault);
impl wasmi::errors::HostError for FatalHostError {}
impl core::fmt::Display for FatalHostError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "host call refused: {:?}", self.0)
}
}
/// Charge the call's gas, run its body, put the result on the wire. The one path
/// every registered closure takes, so gas cannot be forgotten.
pub(crate) fn charged(
caller: &mut Caller<'_, VmState<'_>>,
op: HostFunctionSpec,
body: impl FnOnce(&mut Caller<'_, VmState<'_>>) -> CallResult<i32>,
) -> Result<i32, wasmi::Error> {
to_wire(charge(caller, op.gas()).and_then(|()| body(caller)))
}
/// [`charged`] for a call the guest gets no answer from: its wasm function has no
/// result, so a soft error has nowhere to go and is dropped. The gas is charged first
/// and charged whatever happens after, so the cost is all such a call leaves behind.
///
/// Only `trace` takes this path.
pub(crate) fn charged_unreported(
caller: &mut Caller<'_, VmState<'_>>,
op: HostFunctionSpec,
body: impl FnOnce(&mut Caller<'_, VmState<'_>>) -> CallResult<()>,
) -> Result<(), wasmi::Error> {
dropped(charge(caller, op.gas()).and_then(|()| body(caller)))
}
/// [`to_wire`] for a call with no result: there is no return value to encode a code
/// in, so it is dropped. A [`Fault`] still stops the run — that is a property of the
/// run, not an answer to the call.
fn dropped(result: CallResult<()>) -> Result<(), wasmi::Error> {
match result {
Err(CallError::Fatal(fault)) => Err(wasmi::Error::host(FatalHostError(fault))),
_ => Ok(()),
}
}
fn to_wire(result: CallResult<i32>) -> Result<i32, wasmi::Error> {
match result {
Ok(value) => Ok(value),
Err(CallError::Code(error)) => Ok(error.code()),
Err(CallError::Fatal(fault)) => Err(wasmi::Error::host(FatalHostError(fault))),
}
}
/// Deduct `cost` fuel; [`Fault::OutOfGas`] if it would go negative.
///
/// A meter that will not answer is this crate's own defect, not the contract's, so it
/// is [`Fault::Internal`] rather than a number a guest could act on.
fn charge<T>(caller: &mut Caller<'_, T>, cost: u64) -> CallResult<()> {
let remaining = caller
.get_fuel()
.map_err(|_| CallError::Fatal(Fault::Internal))?;
match remaining.checked_sub(cost) {
Some(left) => caller
.set_fuel(left)
.map_err(|_| CallError::Fatal(Fault::Internal)),
None => {
let _ = caller.set_fuel(0);
Err(CallError::Fatal(Fault::OutOfGas))
}
}
}
fn charge_transfer(state: &VmState<'_>, n: usize) -> Result<(), HostError> {
let n = n as u64;
let remaining = state.transfer_budget.get();
match remaining.checked_sub(n) {
Some(left) => {
state.transfer_budget.set(left);
Ok(())
}
None => Err(HostError::OutOfTransferLimit),
}
}
fn memory(caller: &Caller<'_, VmState<'_>>) -> CallResult<Memory> {
caller
.data()
.memory
.ok_or(CallError::Fatal(Fault::NoMemory))
}
/// [`Region::read`] of the guest's memory, for a call that reads and writes nothing
/// back (`trace`).
pub(crate) fn read_borrowed<'a>(
caller: &'a Caller<'_, VmState<'_>>,
input: Region,
) -> CallResult<&'a [u8]> {
let mem = memory(caller)?;
Ok(input.read(mem.data(caller))?)
}
/// Decode a guest `u32` argument — a keylet's sequence number or document id — from
/// its four little-endian bytes, carried on to the host as its `i32` bit pattern.
///
/// The ABI transports these as a 4-byte region rather than a wasm scalar (the guest
/// SDK passes `seq.to_le_bytes()`), so the region must be exactly four bytes;
/// `InvalidParams` otherwise.
pub(crate) fn read_u32_arg(bytes: &[u8]) -> HostResult<i32> {
let arr: [u8; 4] = bytes.try_into().map_err(|_| HostError::InvalidParams)?;
Ok(i32::from_le_bytes(arr))
}
/// Service a call whose answer is bytes, written straight into the guest's output
/// region.
///
/// **`fill` returns the value's true length, not what it wrote**: a host holding 64
/// bytes and offered room for 4 writes nothing and answers `64`, which is how the
/// guest learns the size to ask for. So `n` is bounded by neither the region, the
/// cap, nor the budget, and all three checks below are reachable.
pub(crate) fn write_into(
caller: &mut Caller<'_, VmState<'_>>,
out: Region,
fill: impl FnOnce(&dyn HostFunctions, &mut [u8]) -> HostResult<usize>,
) -> CallResult<i32> {
let range = out.range()?;
let cap = range.len();
let mem = memory(caller)?;
let host: &dyn HostFunctions = caller.data().host;
let budget = usize::try_from(caller.data().transfer_budget.get()).unwrap_or(usize::MAX);
let buf = mem
.data_mut(&mut *caller)
.get_mut(range)
.ok_or(HostError::PointerOutOfBounds)?;
let buf = &mut buf[..cap.min(MAX_FIELD_BYTES).min(budget)];
let n = fill(host, buf)?;
if n > MAX_FIELD_BYTES {
return Err(HostError::DataFieldTooLarge.into());
}
if n > cap {
return Err(HostError::BufferTooSmall.into());
}
charge_transfer(caller.data(), n)?;
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
reason = "`n > MAX_FIELD_BYTES` returned above, and the cap is far inside i32"
)]
let n = n as i32;
Ok(n)
}
/// Service a call that reads guest memory and writes bytes back to it: the host
/// fills the run's output buffer, which is copied to the guest once every rule has
/// passed.
///
/// `call` gets the guest's whole memory, so it can borrow any number of input
/// regions with [`Region::read`] — which a `&mut` view of that memory would forbid.
/// That is why the answer goes through a buffer instead of straight into the guest
/// as [`write_into`]'s does.
///
/// **The host is never told the guest's capacity**: it is offered the whole buffer
/// and reports the value's true length, so the fit is decided here, with nothing yet
/// in guest memory. A refused value therefore reaches it in no part.
///
/// The output is judged after the inputs, so a call with both bad reports the
/// input's verdict. `NoMemExported` precedes both: there is no memory to validate a
/// region against.
pub(crate) fn write_buffered(
caller: &mut Caller<'_, VmState<'_>>,
out: Region,
call: impl FnOnce(&dyn HostFunctions, &[u8], &mut [u8]) -> HostResult<usize>,
) -> CallResult<i32> {
let mem = memory(caller)?;
// One borrow split in two: the guest's bytes for the inputs, the store data for
// the output buffer. Taking them together is what keeps the inputs borrowed
// rather than copied out.
let (data, state) = mem.data_and_store_mut(&mut *caller);
let host: &dyn HostFunctions = state.host;
let n = call(host, data, &mut state.out_buffer[..])?;
// `out` is checked here rather than before the call: the inputs are judged
// first, so a call with both malformed reports the input's verdict.
let range = out.range()?;
let cap = range.len();
if n > MAX_FIELD_BYTES {
return Err(HostError::DataFieldTooLarge.into());
}
let buf = data.get_mut(range).ok_or(HostError::PointerOutOfBounds)?;
if n > cap {
return Err(HostError::BufferTooSmall.into());
}
charge_transfer(state, n)?;
buf[..n].copy_from_slice(&state.out_buffer[..n]);
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
reason = "`n > MAX_FIELD_BYTES` returned above, and the cap is far inside i32"
)]
let n = n as i32;
Ok(n)
}
/// The mantissa and exponent widths `float_to_mant_exp` writes: an `i64` and an `i32`.
/// Fixed by the ABI, not the guest, so the split is a constant rather than a reported
/// length.
const MANTISSA_BYTES: usize = 8;
const EXPONENT_BYTES: usize = 4;
fn check_fits(data: &[u8], range: &Range<usize>, width: usize) -> HostResult<()> {
let region = data
.get(range.clone())
.ok_or(HostError::PointerOutOfBounds)?;
if region.len() < width {
return Err(HostError::BufferTooSmall);
}
Ok(())
}
/// Service `float_to_mant_exp`, the one call that writes two output regions: the host
/// fills the run's output buffer with the mantissa followed by the exponent, and each
/// is copied to its own guest region once every rule has passed.
///
/// Like [`write_buffered`], the host reads its input from the guest's memory and writes
/// to a scratch buffer, so the input stays borrowed rather than copied. The two output
/// regions are judged after the input, and the mantissa's region before the exponent's,
/// so the first fault reported is the leftmost.
///
/// The two widths are the ABI's rather than the guest's, so the length the host reports
/// is checked against their sum for equality rather than as a bound, and ahead of the
/// output regions: a wrong total means there is no answer to place, whatever the guest
/// declared. That is a fatal error and not a status, since the guest asked for nothing
/// wrong.
pub(crate) fn write_mant_exp(
caller: &mut Caller<'_, VmState<'_>>,
mantissa_out: Region,
exponent_out: Region,
call: impl FnOnce(&dyn HostFunctions, &[u8], &mut [u8], &mut [u8]) -> HostResult<usize>,
) -> CallResult<i32> {
let mem = memory(caller)?;
let (data, state) = mem.data_and_store_mut(&mut *caller);
let host: &dyn HostFunctions = state.host;
// The scratch buffer is split at the fixed mantissa width: the host fills the first
// eight bytes with the mantissa and the next four with the exponent.
let (mant_buf, exp_buf) = state.out_buffer.split_at_mut(MANTISSA_BYTES);
let mant_buf = &mut mant_buf[..MANTISSA_BYTES];
let exp_buf = &mut exp_buf[..EXPONENT_BYTES];
let total = call(host, data, mant_buf, exp_buf)?;
// Both buffers are fixed-width and were offered whole, so the only length the host
// can correctly report is their sum. Anything else is the host contradicting the
// ABI: with the widths in doubt, part of what would be copied out is whatever the
// previous call left in the buffer, so none of it is copied.
if total != MANTISSA_BYTES + EXPONENT_BYTES {
return Err(HostError::InternalFatal.into());
}
let mant_range = mantissa_out.range()?;
check_fits(data, &mant_range, MANTISSA_BYTES)?;
let exp_range = exponent_out.range()?;
check_fits(data, &exp_range, EXPONENT_BYTES)?;
charge_transfer(state, MANTISSA_BYTES + EXPONENT_BYTES)?;
let mant_dst = data
.get_mut(mant_range)
.ok_or(HostError::PointerOutOfBounds)?;
mant_dst[..MANTISSA_BYTES].copy_from_slice(&state.out_buffer[..MANTISSA_BYTES]);
let exp_dst = data
.get_mut(exp_range)
.ok_or(HostError::PointerOutOfBounds)?;
exp_dst[..EXPONENT_BYTES]
.copy_from_slice(&state.out_buffer[MANTISSA_BYTES..MANTISSA_BYTES + EXPONENT_BYTES]);
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
reason = "a total other than 12 returned above, and 12 is far inside i32"
)]
let total = total as i32;
Ok(total)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vm::TRANSFER_LIMIT_BYTES;
use std::cell::Cell;
use wasmi::StoreLimitsBuilder;
use xrpl_host_functions::TraceDataType;
/// `charge_transfer` takes the store data, which has to hold a host.
struct UncalledHost;
impl HostFunctions for UncalledHost {
fn get_ledger_sqn(&self, _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn get_parent_ledger_time(&self, _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn get_parent_ledger_hash(&self, _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn get_base_fee(&self, _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn is_amendment_enabled(&self, _amendment: &[u8]) -> HostResult<i32> {
unreachable!("no unit test in this module calls the host")
}
fn cache_ledger_obj(&self, _obj_id: &[u8], _cache_idx: i32) -> HostResult<i32> {
unreachable!("no unit test in this module calls the host")
}
fn get_tx_field(&self, _field: i32, _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn get_current_ledger_obj_field(&self, _field: i32, _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn get_ledger_obj_field(
&self,
_cache_idx: i32,
_field: i32,
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn get_tx_nested_field(&self, _locator: &[u8], _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn get_current_ledger_obj_nested_field(
&self,
_locator: &[u8],
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn get_ledger_obj_nested_field(
&self,
_cache_idx: i32,
_locator: &[u8],
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn get_tx_array_len(&self, _field: i32) -> HostResult<i32> {
unreachable!("no unit test in this module calls the host")
}
fn get_current_ledger_obj_array_len(&self, _field: i32) -> HostResult<i32> {
unreachable!("no unit test in this module calls the host")
}
fn get_ledger_obj_array_len(&self, _cache_idx: i32, _field: i32) -> HostResult<i32> {
unreachable!("no unit test in this module calls the host")
}
fn get_tx_nested_array_len(&self, _locator: &[u8]) -> HostResult<i32> {
unreachable!("no unit test in this module calls the host")
}
fn get_current_ledger_obj_nested_array_len(&self, _locator: &[u8]) -> HostResult<i32> {
unreachable!("no unit test in this module calls the host")
}
fn get_ledger_obj_nested_array_len(
&self,
_cache_idx: i32,
_locator: &[u8],
) -> HostResult<i32> {
unreachable!("no unit test in this module calls the host")
}
fn check_signature(
&self,
_message: &[u8],
_signature: &[u8],
_pubkey: &[u8],
) -> HostResult<i32> {
unreachable!("no unit test in this module calls the host")
}
fn account_keylet(&self, _account: &[u8], _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn amm_keylet(&self, _asset1: &[u8], _asset2: &[u8], _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn check_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn credential_keylet(
&self,
_subject: &[u8],
_issuer: &[u8],
_credential_type: &[u8],
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn delegate_keylet(
&self,
_account: &[u8],
_authorize: &[u8],
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn deposit_preauth_keylet(
&self,
_account: &[u8],
_authorize: &[u8],
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn did_keylet(&self, _account: &[u8], _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn escrow_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn trust_line_keylet(
&self,
_account1: &[u8],
_account2: &[u8],
_currency: &[u8],
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn mptoken_issuance_keylet(
&self,
_issuer: &[u8],
_seq: i32,
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn mptoken_keylet(
&self,
_mptid: &[u8],
_holder: &[u8],
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn nftoken_offer_keylet(
&self,
_account: &[u8],
_seq: i32,
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn offer_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn oracle_keylet(
&self,
_account: &[u8],
_doc_id: i32,
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn paychannel_keylet(
&self,
_account: &[u8],
_destination: &[u8],
_seq: i32,
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn permissioned_domain_keylet(
&self,
_account: &[u8],
_seq: i32,
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn signer_list_keylet(&self, _account: &[u8], _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn ticket_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn vault_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn sha512_half(&self, _data: &[u8], _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn trace(&self, _msg: &str, _data: &[u8], _data_type: TraceDataType) -> HostResult<()> {
unreachable!("no unit test in this module calls the host")
}
fn update_data(&self, _data: &[u8]) -> HostResult<i32> {
unreachable!("no unit test in this module calls the host")
}
fn get_nft(&self, _account: &[u8], _nft_id: &[u8], _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn get_nft_issuer(&self, _nft_id: &[u8], _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn get_nft_taxon(&self, _nft_id: &[u8], _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn get_nft_flags(&self, _nft_id: &[u8]) -> HostResult<i32> {
unreachable!("no unit test in this module calls the host")
}
fn get_nft_transfer_fee(&self, _nft_id: &[u8]) -> HostResult<i32> {
unreachable!("no unit test in this module calls the host")
}
fn get_nft_sequence(&self, _nft_id: &[u8], _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn float_from_int(&self, _x: i64, _mode: i32, _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn float_from_uint(&self, _x: &[u8], _mode: i32, _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn float_from_stamount(
&self,
_amount: &[u8],
_mode: i32,
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn float_from_stnumber(
&self,
_number: &[u8],
_mode: i32,
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn float_to_int(&self, _x: &[u8], _mode: i32, _out: &mut [u8]) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn float_to_mant_exp(
&self,
_x: &[u8],
_mantissa_out: &mut [u8],
_exponent_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn float_from_mant_exp(
&self,
_mantissa: i64,
_exponent: i32,
_mode: i32,
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn float_compare(&self, _x: &[u8], _y: &[u8]) -> HostResult<i32> {
unreachable!("no unit test in this module calls the host")
}
fn float_add(
&self,
_x: &[u8],
_y: &[u8],
_mode: i32,
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn float_subtract(
&self,
_x: &[u8],
_y: &[u8],
_mode: i32,
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn float_multiply(
&self,
_x: &[u8],
_y: &[u8],
_mode: i32,
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn float_divide(
&self,
_x: &[u8],
_y: &[u8],
_mode: i32,
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
fn float_power(
&self,
_x: &[u8],
_n: i32,
_mode: i32,
_out: &mut [u8],
) -> HostResult<usize> {
unreachable!("no unit test in this module calls the host")
}
}
fn state(budget: u64) -> VmState<'static> {
VmState {
host: &UncalledHost,
mem_limits: StoreLimitsBuilder::new().build(),
transfer_budget: Cell::new(budget),
memory: None,
out_buffer: [0u8; MAX_FIELD_BYTES],
}
}
/// `wasmi::Error` is not `PartialEq`, so a test expecting the guest-visible
/// channel says so by going through here.
fn wire(result: CallResult<i32>) -> i32 {
to_wire(result)
.unwrap_or_else(|trap| panic!("expected a guest-visible status, got a trap: {trap}"))
}
#[test]
fn a_success_becomes_the_value_and_an_error_becomes_its_code() {
assert_eq!(wire(Ok(0)), 0);
assert_eq!(wire(Ok(32)), 32);
assert_eq!(wire(Err(HostError::BufferTooSmall.into())), -3);
}
/// The codes a host may answer that a contract must not see, and the fault each
/// becomes. Written out rather than derived from `From<HostError>`, which is what
/// they are asserting.
const STOPS_THE_RUN: [(HostError, Fault); 3] = [
(HostError::InternalFatal, Fault::Internal),
(HostError::Unimplemented, Fault::Internal),
(HostError::NoMemExported, Fault::NoMemory),
];
/// Every fault, so the two tests below are the whole set and not a sample.
/// `From<Fault> for RunError` is what forces a fault added later to be
/// considered; this is what forces it to be tested.
const ALL_FAULTS: [Fault; 3] = [Fault::OutOfGas, Fault::Internal, Fault::NoMemory];
#[test]
fn a_code_that_stops_the_run_converts_to_its_fault() {
for (error, fault) in STOPS_THE_RUN {
assert_eq!(CallError::from(error), CallError::Fatal(fault), "{error:?}");
}
}
/// Over `HostError::ALL`, so it is the whole ABI and not a sample: a code added
/// to the ABI arrives already asserted to reach the guest as itself, and stopping
/// the run on it is then a change someone has to come and make.
///
/// `OutOfTransferLimit` is the row worth reading twice: the one budget a
/// contract can be expected to handle, so it is told no rather than killed.
#[test]
fn every_other_code_reaches_the_guest_as_itself() {
for &error in HostError::ALL {
if STOPS_THE_RUN.iter().any(|&(stops, _)| stops == error) {
continue;
}
assert_eq!(CallError::from(error), CallError::Code(error), "{error:?}");
assert_eq!(wire(Err(error.into())), error.code(), "{error:?}");
}
}
/// The trap carries the fault, so `run` can name the outcome without parsing a
/// message.
#[test]
fn a_fault_becomes_a_trap_carrying_it() {
for fault in ALL_FAULTS {
let trap = to_wire(Err(CallError::Fatal(fault)))
.expect_err("a fault must not reach the guest as a code");
let payload = trap.downcast_ref::<FatalHostError>().unwrap_or_else(|| {
panic!("{fault:?}: expected a FatalHostError payload, got: {trap}")
});
assert_eq!(*payload, FatalHostError(fault));
}
}
/// The result-less path splits the same two channels differently: a fault still
/// stops the run, and every code is dropped, since `trace` has no return value to
/// carry it. Over `HostError::ALL` for the reason above — a code added to the ABI
/// arrives asserted against both paths.
#[test]
fn a_call_with_no_result_drops_a_code_and_traps_on_a_fault() {
assert!(dropped(Ok(())).is_ok());
for &error in HostError::ALL {
if let CallError::Code(code) = CallError::from(error) {
assert!(
dropped(Err(CallError::Code(code))).is_ok(),
"{error:?} has no channel to the guest and must be dropped"
);
}
}
for fault in ALL_FAULTS {
let trap =
dropped(Err(CallError::Fatal(fault))).expect_err("a fault must stop the run");
let payload = trap.downcast_ref::<FatalHostError>().unwrap_or_else(|| {
panic!("{fault:?}: expected a FatalHostError payload, got: {trap}")
});
assert_eq!(*payload, FatalHostError(fault));
}
}
#[test]
fn a_transfer_spends_the_budget() {
let state = state(100);
assert_eq!(charge_transfer(&state, 30), Ok(()));
assert_eq!(state.transfer_budget.get(), 70);
assert_eq!(charge_transfer(&state, 70), Ok(()));
assert_eq!(state.transfer_budget.get(), 0);
}
/// The budget bounds the total, so the transfer that would overrun it is
/// refused whole rather than partially charged.
#[test]
fn a_transfer_past_the_budget_is_refused_and_charges_nothing() {
let state = state(100);
assert_eq!(
charge_transfer(&state, 101),
Err(HostError::OutOfTransferLimit)
);
assert_eq!(
state.transfer_budget.get(),
100,
"a refusal must not charge"
);
assert_eq!(charge_transfer(&state, 100), Ok(()));
assert_eq!(
charge_transfer(&state, 1),
Err(HostError::OutOfTransferLimit)
);
}
#[test]
fn transferring_nothing_costs_nothing() {
let state = state(0);
assert_eq!(charge_transfer(&state, 0), Ok(()));
assert_eq!(state.transfer_budget.get(), 0);
}
/// The field cap holds one call to a small share of the run's budget, so the
/// budget bounds a run rather than a call. An inequality, not the two values:
/// those are pinned in `vm.rs`.
#[test]
fn no_single_value_can_exhaust_the_run_budget() {
assert!(
(MAX_FIELD_BYTES as u64) * 64 <= TRANSFER_LIMIT_BYTES,
"one {MAX_FIELD_BYTES}-byte value against a {TRANSFER_LIMIT_BYTES}-byte budget"
);
}
#[test]
fn read_u32_arg_success() {
let number: u32 = 0x12345678;
let le_array: [u8; 4] = number.to_le_bytes();
assert_eq!(le_array, [0x78, 0x56, 0x34, 0x12]);
let result = read_u32_arg(&le_array);
assert!(result.is_ok());
assert_eq!(result.unwrap(), number.try_into().unwrap());
}
#[test]
fn read_u32_arg_invalid_length() {
let le_array = [0x56, 0x34, 0x12];
let result = read_u32_arg(&le_array);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), HostError::InvalidParams);
}
}

View File

@@ -1,28 +0,0 @@
//! The escrow wasm VM: compile a contract, meter it, and serve its host calls.
//!
//! Every guest access goes through `abi.rs` and reaches linear memory only by
//! wasmi's bounds-checked slice operations; `forbid(unsafe_code)` makes that a
//! property rather than a claim. The cast lints are on for the same reason — on a
//! consensus path a truncating or sign-losing cast changes what a contract is
//! charged or told, so each one is argued for at its site.
#![forbid(unsafe_code)]
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(unreachable_pub)]
#![deny(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_lossless
)]
mod abi;
mod preflight;
mod region;
mod register;
mod vm;
pub use preflight::{CheckError, check};
pub use vm::{
MAX_FIELD_BYTES, MAX_MEMORY_BYTES, MAX_MEMORY_PAGES, MAX_TABLE_ELEMENTS, RunError, RunFailure,
RunOutcome, TRANSFER_LIMIT_BYTES, run,
};

View File

@@ -1,407 +0,0 @@
//! Screening a contract before it reaches the ledger.
//!
//! [`check`] answers whether [`crate::run`] would refuse a module before the
//! guest's first instruction — the three stages a caller maps to a malformed
//! transaction rather than to a failed one. It needs **no host, no store and no
//! gas**: everything it reads is a property of the compiled module. That is what
//! makes it callable from a transaction's preflight, which has no ledger to serve
//! host calls from.
//!
//! Two things it deliberately does not screen. A module exporting **no** linear
//! memory passes: a contract that makes no host call needs none, and one that
//! does is refused at the call and charged for what it burned. A start section
//! passes: it is guest code, and executing it is the one thing a check must not do
//! — a trap in one is charged to the contract like any other trap.
//!
//! Two things it screens that a run can only discover: an exported memory, or an
//! exported table, larger than the engine grants. Both read the same export list, so
//! [`check_exported_resources`] is one pass — see it for what stays invisible, and
//! why the table case leaves much more of it there.
use std::fmt;
use wasmi::{ExternType, FuncType, Module, ValType};
use xrpl_host_functions::HostFunctionSpec;
use crate::register::HOST_MODULE;
use crate::vm::{MAX_MEMORY_PAGES, MAX_TABLE_ELEMENTS, compile};
/// Why a module cannot be run. One variant per stage, since the caller maps the
/// stages separately.
#[derive(Debug)]
pub enum CheckError {
/// `wasm` is not a valid module under this engine's configuration.
Compile(String),
/// An import no engine of this ABI defines: another module namespace, a name
/// that is not a host function, or one imported as something other than a
/// function.
Import(String),
/// No export named `function_name` with signature `() -> i32`.
EntryPoint(String),
/// The module asks for more linear memory than the engine grants.
Memory(String),
/// The module asks for a larger table than the engine grants.
Table(String),
}
impl fmt::Display for CheckError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CheckError::Compile(detail) => write!(f, "compile: {detail}"),
CheckError::Import(detail) => write!(f, "import: {detail}"),
// The detail says which of the entry point's failures this is, since
// "no entry point" would be wrong for an export of the wrong type.
CheckError::EntryPoint(detail) => write!(f, "{detail}"),
CheckError::Memory(detail) => write!(f, "memory: {detail}"),
CheckError::Table(detail) => write!(f, "table: {detail}"),
}
}
}
/// Screen `wasm`: it must compile, import only what the engine serves, export
/// `function_name` as `() -> i32`, and ask for no more memory or table than it may
/// have.
///
/// The stages are ordered by how much of the module each explains. An import fault
/// is reported before a missing entry point because the imports are what the rest of
/// the module is built on; the resource caps come last, being a request rather than a
/// mistake about the ABI.
pub fn check(wasm: &[u8], function_name: &str) -> Result<(), CheckError> {
let module = compile(wasm).map_err(CheckError::Compile)?;
check_imports(&module)?;
check_entry_point(&module, function_name)?;
check_exported_resources(&module)
}
/// Every import must be one the linker defines. The first that is not ends the
/// check, so a module with several faults reports the earliest.
fn check_imports(module: &Module) -> Result<(), CheckError> {
for import in module.imports() {
check_import(import.module(), import.name(), import.ty()).map_err(CheckError::Import)?;
}
Ok(())
}
/// Whether the engine defines this one import.
///
/// The set of names is [`HostFunctionSpec::ALL`], which is also what
/// [`crate::register::register_host_functions`] iterates — so a check and a run
/// cannot disagree about which names exist, and adding a host function extends
/// both at once. The one thing this does not compare is `ty`'s *signature*, which
/// still parts a module from the engine at instantiation; the kind is compared
/// because the engine defines these names as functions and as nothing else.
///
/// The rules are ordered, not merely alternatives: a guest importing `env::malloc`
/// is told about the namespace rather than that `malloc` is not a host function,
/// because the namespace is the one that explains every other import it has too.
fn check_import(module: &str, name: &str, ty: &ExternType) -> Result<(), String> {
if module != HOST_MODULE {
return Err(format!("'{module}::{name}' is not from '{HOST_MODULE}'"));
}
if !HostFunctionSpec::ALL
.iter()
.any(|op| op.wasm_name() == name)
{
return Err(format!("no host function '{name}'"));
}
if !matches!(ty, ExternType::Func(_)) {
return Err(format!("'{HOST_MODULE}::{name}' is not a function"));
}
Ok(())
}
fn check_entry_point(module: &Module, name: &str) -> Result<(), CheckError> {
match module.get_export(name) {
Some(ExternType::Func(ty)) if is_entry_point(&ty) => Ok(()),
found => Err(CheckError::EntryPoint(entry_point_fault(found, name))),
}
}
/// The entry point's type: nothing in, one `i32` out — what [`crate::run`]'s
/// `get_typed_func::<(), i32>` accepts.
fn is_entry_point(ty: &FuncType) -> bool {
ty.params().is_empty() && matches!(ty.results(), [ValType::I32])
}
/// A module may declare no more linear memory, and no larger a table, than the
/// engine grants. One pass over the exports, since both rules read the same list and
/// the export table is the only place either is visible.
///
/// **A memory or table the module keeps to itself is therefore not screened**: it is
/// absent from the exports, and the store's limiter is what refuses it, at
/// instantiation. That gap is wide for tables — Rust exports
/// `__indirect_function_table` only under `--export-table`, so unexported is the
/// normal shape — and narrow for memories, since a contract needs an exported one to
/// make any host call at all.
///
/// A module faulting on both is reported by whichever it declares first. Neither
/// fault explains the other, so there is no precedence to preserve — only the need
/// for every node to reach the same verdict, which export order already gives.
fn check_exported_resources(module: &Module) -> Result<(), CheckError> {
for export in module.exports() {
match export.ty() {
ExternType::Memory(ty) => {
check_initial_pages(ty.minimum()).map_err(CheckError::Memory)?;
}
ExternType::Table(ty) => {
check_initial_elements(ty.minimum()).map_err(CheckError::Table)?;
}
_ => {}
}
}
Ok(())
}
/// Whether the engine will grant a memory of this declared initial size.
///
/// The *minimum* only: a declared maximum past the cap is legal and simply
/// unreachable, which `vm_limits::a_declared_maximum_past_the_cap_is_allowed_but_
/// unreachable` pins on the run side. Refusing it here would turn a runnable
/// contract away.
fn check_initial_pages(pages: u64) -> Result<(), String> {
if pages > u64::from(MAX_MEMORY_PAGES) {
return Err(format!(
"initial memory of {pages} pages is past the {MAX_MEMORY_PAGES}-page cap"
));
}
Ok(())
}
/// Whether the engine will grant a table of this declared initial size.
///
/// The *minimum* is the whole question: `table.grow` belongs to the reference-types
/// proposal, which [`crate::vm`]'s engine turns off, so a table never becomes larger
/// than it was declared and a declared maximum past the cap is simply unreachable.
fn check_initial_elements(elements: u64) -> Result<(), String> {
let cap = u64::try_from(MAX_TABLE_ELEMENTS).expect("the cap is a small constant");
if elements > cap {
return Err(format!(
"initial table of {elements} elements is past the {MAX_TABLE_ELEMENTS}-element cap"
));
}
Ok(())
}
/// How an entry-point lookup failed, in the words both stages use: a check and a
/// run describe the same module the same way, and "no entry point" would send a
/// contract author looking for a function they already have.
pub(crate) fn entry_point_fault(found: Option<ExternType>, name: &str) -> String {
match found {
Some(ExternType::Func(_)) => {
format!("entry point '{name}' has the wrong signature, expected '() -> i32'")
}
Some(_) => format!("export '{name}' is not a function"),
None => format!("no entry point '{name}'"),
}
}
/// The rules, one by one, on inputs built directly rather than parsed out of a
/// module. `tests/preflight.rs` runs real modules through [`check`]; what is here is
/// what a module cannot state precisely — which rule fires, in which order, and in
/// what words the caller logs it.
///
/// `wat` is a dev-dependency, so the one test here that does need a module writes it
/// as text like every other test in the crate. What the library must not gain is a
/// text *entry point* — `check` and `run` take binaries — and a `cfg(test)` caller
/// cannot give it one.
#[cfg(test)]
mod tests {
use super::*;
use wasmi::{GlobalType, MemoryType, Mutability};
/// A host function as a guest declares it. Any function type will do: the
/// signature is not what [`check_import`] compares.
fn a_function() -> ExternType {
ExternType::Func(FuncType::new([ValType::I32], [ValType::I32]))
}
/// A name every one of these tests can use, taken from the ABI rather than
/// spelled, so it stays a real host function as the ABI changes.
fn a_host_function_name() -> &'static str {
HostFunctionSpec::ALL[0].wasm_name()
}
// -----------------------------------------------------------------------
// Imports
// -----------------------------------------------------------------------
/// Every name the ABI declares is served. Derived from `ALL` rather than
/// listed, so a host function added to the ABI is covered the day it lands.
#[test]
fn every_declared_host_function_is_served() {
for op in HostFunctionSpec::ALL {
assert_eq!(
check_import(HOST_MODULE, op.wasm_name(), &a_function()),
Ok(()),
"{}",
op.wasm_name()
);
}
}
#[test]
fn an_import_from_another_namespace_is_refused() {
for namespace in ["env", "host", "host_lib2", ""] {
let refusal = check_import(namespace, a_host_function_name(), &a_function())
.expect_err(namespace);
assert!(
refusal.contains("is not from 'host_lib'"),
"{namespace}: {refusal}"
);
}
}
#[test]
fn an_unknown_name_is_refused() {
let refusal =
check_import(HOST_MODULE, "no_such_function", &a_function()).expect_err("unknown name");
assert_eq!(refusal, "no host function 'no_such_function'");
}
/// The engine defines these names as functions and as nothing else, so a module
/// importing one as a global or a memory does not link either.
#[test]
fn a_host_function_imported_as_anything_else_is_refused() {
for ty in [
ExternType::Global(GlobalType::new(ValType::I32, Mutability::Const)),
ExternType::Memory(MemoryType::new(1, None)),
] {
let name = a_host_function_name();
let refusal = check_import(HOST_MODULE, name, &ty).expect_err("not a function");
assert_eq!(refusal, format!("'host_lib::{name}' is not a function"));
}
}
/// The rules are ordered. An import that breaks two of them is reported by the
/// first, so the message a contract author reads is the one that explains the
/// rest of their imports too.
#[test]
fn the_namespace_is_reported_before_the_name() {
let refusal = check_import("env", "no_such_function", &a_function())
.expect_err("neither the namespace nor the name is served");
assert!(refusal.contains("is not from 'host_lib'"), "{refusal}");
assert!(
!refusal.contains("no host function"),
"the namespace explains it: {refusal}"
);
}
/// Both halves of the type are load-bearing, and neither is checked anywhere
/// a module cannot reach.
#[test]
fn the_entry_point_type_is_nothing_in_and_one_i32_out() {
assert!(is_entry_point(&FuncType::new([], [ValType::I32])));
for wrong in [
FuncType::new([], []),
FuncType::new([], [ValType::I64]),
FuncType::new([ValType::I32], [ValType::I32]),
FuncType::new([], [ValType::I32, ValType::I32]),
] {
assert!(!is_entry_point(&wrong), "{wrong:?}");
}
}
/// Three faults, three descriptions. A run reports these too, with wasmi's own
/// error appended, so a swapped arm would mislead at both stages at once.
#[test]
fn each_entry_point_fault_is_described_as_itself() {
assert_eq!(
entry_point_fault(Some(a_function()), "finish"),
"entry point 'finish' has the wrong signature, expected '() -> i32'"
);
assert_eq!(
entry_point_fault(
Some(ExternType::Global(GlobalType::new(
ValType::I32,
Mutability::Const
))),
"finish"
),
"export 'finish' is not a function"
);
assert_eq!(
entry_point_fault(None, "finish"),
"no entry point 'finish'",
"an absent export must not be reported as a wrong signature"
);
}
/// The cap itself is granted; one page past it is not. The boundary is the whole
/// rule, and it is the same boundary the store's limiter applies at
/// instantiation.
#[test]
fn the_initial_memory_may_reach_the_cap_but_not_pass_it() {
assert_eq!(check_initial_pages(0), Ok(()));
assert_eq!(check_initial_pages(u64::from(MAX_MEMORY_PAGES)), Ok(()));
let past = u64::from(MAX_MEMORY_PAGES) + 1;
let refusal = check_initial_pages(past).expect_err("one page past the cap");
assert_eq!(
refusal,
format!("initial memory of {past} pages is past the {MAX_MEMORY_PAGES}-page cap")
);
}
/// The cap itself is granted; one element past it is not. The boundary is the
/// whole rule, and it is the same boundary the store's limiter applies at
/// instantiation.
#[test]
fn the_initial_table_may_reach_the_cap_but_not_pass_it() {
let cap = u64::try_from(MAX_TABLE_ELEMENTS).expect("fits");
assert_eq!(check_initial_elements(0), Ok(()));
assert_eq!(check_initial_elements(cap), Ok(()));
let past = cap + 1;
let refusal = check_initial_elements(past).expect_err("one element past the cap");
assert_eq!(
refusal,
format!(
"initial table of {past} elements is past the {MAX_TABLE_ELEMENTS}-element cap"
)
);
}
/// The bridge logs this string and the C++ tests match on it, so the stage's
/// prefix is part of the interface rather than a debugging aid.
#[test]
fn a_refusal_names_its_stage() {
assert_eq!(
CheckError::Compile("bad magic".to_string()).to_string(),
"compile: bad magic"
);
assert_eq!(
CheckError::Memory("initial memory of 129 pages".to_string()).to_string(),
"memory: initial memory of 129 pages"
);
assert_eq!(
CheckError::Table("initial table of 1025 elements".to_string()).to_string(),
"table: initial table of 1025 elements"
);
assert_eq!(
CheckError::Import("no host function 'x'".to_string()).to_string(),
"import: no host function 'x'"
);
// The entry point's detail already says which of its three faults it is,
// so a prefix would only repeat it.
assert_eq!(
CheckError::EntryPoint("no entry point 'finish'".to_string()).to_string(),
"no entry point 'finish'"
);
}
#[test]
fn the_stages_run_in_order() {
assert!(
matches!(check(b"not wasm", "finish"), Err(CheckError::Compile(_))),
"nothing is screened until the module compiles"
);
// A module that compiles and imports nothing, so it reaches the entry point.
let empty = wat::parse_str("(module)").expect("assembles");
assert!(
matches!(check(&empty, "finish"), Err(CheckError::EntryPoint(_))),
"a module that compiles and imports nothing reaches the entry point"
);
}
}

View File

@@ -1,50 +0,0 @@
use crate::vm::MAX_FIELD_BYTES;
use core::ops::Range;
use xrpl_host_functions::{HostError, HostResult};
/// A byte region as the guest declared it: the `(ptr, len)` pair off the wire, not
/// yet checked.
///
/// Every byte parameter in this ABI is such a pair, so pairing them once at the wire
/// boundary is what keeps the helpers in `abi.rs` from each taking two loose integers
/// they could be handed in either order.
///
/// It lives in a module of its own so that the fields are out of reach and
/// [`range`](Region::range) is the *only* way to indices — the check cannot be
/// skipped, only deferred. Construction is infallible for that reason: a call whose
/// output region is malformed is then refused in the order its own helper chooses,
/// rather than at the moment the pair happened to be formed.
#[derive(Copy, Clone)]
pub(crate) struct Region {
ptr: i32,
len: i32,
}
impl Region {
pub(crate) fn new(ptr: i32, len: i32) -> Region {
Region { ptr, len }
}
/// `start..end` as indices. The conversion is the negativity check — it fails on
/// exactly the negative values — and the addition guards a 32-bit `usize`, where
/// two `i32`s can sum past the end.
pub(crate) fn range(self) -> HostResult<Range<usize>> {
let (Ok(start), Ok(len)) = (usize::try_from(self.ptr), usize::try_from(self.len)) else {
return Err(HostError::InvalidParams);
};
let end = start
.checked_add(len)
.ok_or(HostError::PointerOutOfBounds)?;
Ok(start..end)
}
/// The region's bytes, refused past the field cap. No copy: the slice aliases
/// `data`.
pub(crate) fn read(self, data: &[u8]) -> HostResult<&[u8]> {
let range = self.range()?;
if range.len() > MAX_FIELD_BYTES {
return Err(HostError::DataFieldTooLarge);
}
data.get(range).ok_or(HostError::PointerOutOfBounds)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,405 +0,0 @@
use std::cell::Cell;
use std::fmt;
use std::sync::LazyLock;
use wasmi::{
Config, Engine, Export, Linker, Memory, Module, Store, StoreLimits, StoreLimitsBuilder,
TrapCode,
};
use xrpl_host_functions::HostFunctions;
use crate::abi::{FatalHostError, Fault};
use crate::preflight::entry_point_fault;
use crate::register::register_host_functions;
/// wasm linear-memory page size, fixed by the wasm spec (64 KiB).
const WASM_PAGE_BYTES: u32 = 64 * 1024;
/// Linear-memory page cap.
pub const MAX_MEMORY_PAGES: u32 = 128;
/// [`MAX_MEMORY_PAGES`] in bytes: 8 MiB.
pub const MAX_MEMORY_BYTES: usize = (MAX_MEMORY_PAGES * WASM_PAGE_BYTES) as usize;
/// Cap on a table's element count.
///
/// A table entry is 8 bytes and wasmi materializes every one of them inside
/// `instantiate_and_start` — before the guest's first instruction, so no gas charge
/// can reach the cost. Without this cap the ceiling is the validator's, `u32::MAX`
/// entries, which a module asks for in five bytes of LEB128 and pays for in ~34 GiB.
pub const MAX_TABLE_ELEMENTS: usize = 1024;
/// Total bytes the host may write into guest memory in one [`run`], separate from
/// gas.
///
/// One direction only. What the guest passes in is not charged: it reaches the host
/// as a borrowed slice of guest memory, capped per value at [`MAX_FIELD_BYTES`] by
/// `Region::read` and in number by gas, and a host that keeps a copy (`update_data`)
/// bounds it on its own side.
pub const TRANSFER_LIMIT_BYTES: u64 = 1 << 20;
/// Size cap on any single value crossing the boundary, in either direction; over
/// it is `DataFieldTooLarge`.
///
/// A protocol limit: `kMaxWasmDataLength` in `include/xrpl/protocol/Protocol.h`.
pub const MAX_FIELD_BYTES: usize = 1024;
/// State threaded through every host call, stored in the wasmi [`Store`].
pub(crate) struct VmState<'h> {
pub(crate) host: &'h dyn HostFunctions,
/// Enforces [`store_limits`] via `Store::limiter`, which needs a `&mut` into it
/// from `&mut VmState` — hence a field rather than a local.
pub(crate) mem_limits: StoreLimits,
/// Remaining transfer budget for this run ([`TRANSFER_LIMIT_BYTES`]).
///
/// A `Cell` because it is decremented from a shared `&Caller`. One thread per
/// invocation touches the store, so the lack of `Sync` costs nothing.
///
/// TODO: the extra charge for an unaligned field copy has nothing to attach to
/// until this ABI gains a `FieldLocator` host function.
pub(crate) transfer_budget: Cell<u64>,
/// The guest's linear memory, resolved once by [`run`] after instantiation so
/// no host call pays for an export lookup.
///
/// Caching the handle is sound because a [`Memory`] is an arena index, not a
/// pointer to the bytes: it survives `memory.grow`, and `data`/`data_mut`
/// re-derive the slice per call.
///
/// The handle is scoped to one store, so this assumes **one module, one
/// instance, one store per `run`**. Module linking or nested execution would
/// have to resolve per instance: a cached handle would serve a call against the
/// wrong instance's memory, which is a wrong answer rather than an error.
pub(crate) memory: Option<Memory>,
/// Where a host writes a value before [`crate::abi::write_buffered`] copies it
/// to the guest. One buffer per run, so no call zero-fills one of its own.
///
/// Inline rather than boxed: the store's data is built once and then only
/// borrowed, so a kilobyte in it costs a move where a `Box` costs an
/// allocation. A local would cost neither, but `forbid(unsafe_code)` means a
/// stack buffer is zero-filled — per call, which is the cost this removes.
pub(crate) out_buffer: [u8; MAX_FIELD_BYTES],
}
/// Outcome of running an escrow contract to completion.
#[derive(Debug)]
pub struct RunOutcome {
/// The value returned by the exported entry point (`finish`): `> 0` means
/// allow the escrow to finish.
pub result: i32,
/// Fuel (gas) consumed by the whole invocation — guest instructions plus
/// the per-call host charges.
pub fuel_used: u64,
}
/// Why a run produced no result. Each variant is one outcome for the caller to
/// map to a TER.
#[derive(Debug)]
pub enum RunError {
/// `wasm` is not a valid module under this engine's configuration.
Compile(String),
/// The module compiled but the engine would not accept it: an import the
/// linker does not define, or an initial memory past the page cap. Not guest
/// code failing — a start section that traps is [`RunError::Trap`].
Instantiate(String),
/// No export named `function_name` with signature `() -> i32`: absent, not a
/// function, or a function of another type — which the detail tells apart.
EntryPoint(String),
/// Gas exhausted — by the guest's own instructions or by a host call's
/// charge. [`RunFailure::fuel_used`] is the whole limit.
OutOfGas,
/// The host could not serve a call.
Internal,
/// A host call had no linear memory to work in: the module exports none, or
/// the call came from a start section, which runs before there is an instance
/// to resolve the memory from.
NoMemory,
/// The guest trapped: `unreachable`, division by zero, an out-of-bounds
/// access, or `memory.grow` past the page cap. Wherever the guest was
/// executing, including a start section during instantiation.
Trap(String),
}
impl fmt::Display for RunError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RunError::Compile(detail) => write!(f, "compile: {detail}"),
RunError::Instantiate(detail) => write!(f, "instantiate: {detail}"),
// The detail says which of the entry point's failures this is, since
// "no entry point" would be wrong for an export of the wrong type.
RunError::EntryPoint(detail) => write!(f, "{detail}"),
RunError::OutOfGas => write!(f, "out of gas"),
RunError::Internal => write!(f, "internal error"),
RunError::NoMemory => write!(f, "no exported memory"),
RunError::Trap(detail) => write!(f, "trap: {detail}"),
}
}
}
/// A failed run, with the gas it still owes: a contract that traps or exhausts
/// its gas is charged for what it burned.
#[derive(Debug)]
pub struct RunFailure {
pub error: RunError,
/// Fuel consumed before the failure. The whole limit when gas ran out; `0`
/// when the module never ran.
pub fuel_used: u64,
}
impl fmt::Display for RunFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (fuel used: {})", self.error, self.fuel_used)
}
}
impl RunFailure {
/// A failure with no fuel accounted: it stopped the run at or before the guest's
/// first instruction, or under a store with no meter to read.
fn owing_nothing(error: RunError) -> RunFailure {
RunFailure {
error,
fuel_used: 0,
}
}
}
/// Fuel spent out of `gas`: the one place a run's cost is measured, so success,
/// trap and refusal all report it the same way.
///
/// `Store::get_fuel` fails only on a store without fuel metering, which
/// [`build_wasm_engine`] rules out and `run`'s `set_fuel` would already have
/// caught — so a failure here is a defect in this crate. It must not become a
/// number: `0` forgives a run its whole cost, `gas` charges an untouched one for
/// everything. [`RunError::Internal`] instead.
fn fuel_used(store: &Store<VmState<'_>>, gas: u64) -> Result<u64, RunError> {
store
.get_fuel()
.map(|remaining| gas.saturating_sub(remaining))
.map_err(|_| RunError::Internal)
}
/// Report `error` with the run's cost attached. A cost that cannot be read replaces
/// the outcome rather than being invented — see [`fuel_used`].
fn failed(store: &Store<VmState<'_>>, gas: u64, error: RunError) -> RunFailure {
match fuel_used(store, gas) {
Ok(fuel_used) => RunFailure { error, fuel_used },
Err(unmetered) => RunFailure::owing_nothing(unmetered),
}
}
/// The outcome a `wasmi::Error` names for itself, if any, rather than leaving it to
/// the stage that raised it.
///
/// Two ways a run halts mid-flight: a host call that could not be served, which
/// carries a [`FatalHostError`] saying which condition it was, and the guest's own
/// instructions exhausting the meter, which wasmi raises as `OutOfFuel`.
///
/// Both can happen anywhere the guest executes — including a start section, which
/// is guest code running during instantiation — so every stage from there on asks
/// this before naming a failure after itself.
fn guest_halted(error: &wasmi::Error) -> Option<RunError> {
if let Some(fatal) = error.downcast_ref::<FatalHostError>() {
return Some(fatal.0.into());
}
(error.as_trap_code() == Some(TrapCode::OutOfFuel)).then_some(RunError::OutOfGas)
}
/// Why instantiation failed, once [`guest_halted`] has ruled out the two conditions
/// that can arise anywhere.
///
/// A start section is guest code, so it can trap on its own — `unreachable`, a
/// division by zero, an out-of-bounds access — and a trap is the guest's fault
/// wherever it happens. Naming that after the *stage* would file it beside the
/// module faults a caller treats as its own defect, and charge nothing for
/// instructions the contract burned. What is left for [`RunError::Instantiate`] is a
/// module the linker or the store would not accept at all.
fn instantiation_failure(error: &wasmi::Error) -> RunError {
match error.as_trap_code() {
Some(_) => RunError::Trap(error.to_string()),
None => RunError::Instantiate(error.to_string()),
}
}
/// The outcome a [`Fault`] is: the one place a stopped call becomes a stopped run.
///
/// Total and one arm each, because a `Fault` is only ever a condition that stops the
/// run — the guest-visible codes cannot reach here, which is what
/// [`crate::abi::CallError`] buys. A fault added later has no arm and does not
/// compile.
impl From<Fault> for RunError {
fn from(fault: Fault) -> RunError {
match fault {
Fault::OutOfGas => RunError::OutOfGas,
Fault::Internal => RunError::Internal,
Fault::NoMemory => RunError::NoMemory,
}
}
}
/// The process-wide wasmi engine, built once on first use.
///
/// The configuration is consensus-fixed and identical for every invocation, and an
/// [`Engine`] is an internally `Arc`ed `Send + Sync` handle, so one shared engine
/// serves concurrent [`run`] calls.
pub(crate) fn wasm_engine() -> &'static Engine {
static ENGINE: LazyLock<Engine> = LazyLock::new(build_wasm_engine);
&ENGINE
}
/// Build the wasmi engine the escrow VM requires: deterministic, minimal
/// features, fuel metering on.
fn build_wasm_engine() -> Engine {
let mut config = Config::default();
config.consume_fuel(true);
config.ignore_custom_sections(true);
config.wasm_mutable_global(false);
config.wasm_multi_value(false);
config.wasm_sign_extension(false);
config.wasm_saturating_float_to_int(false);
config.wasm_bulk_memory(false);
config.wasm_reference_types(false);
config.wasm_tail_call(false);
config.wasm_extended_const(false);
config.floats(false);
config.wasm_multi_memory(false);
config.wasm_custom_page_sizes(false);
// Disabled through the crate feature flag.
// config.wasm_memory64(false);
config.wasm_wide_arithmetic(false);
config.allow_start_fn(false);
Engine::new(&config)
}
/// Every resource ceiling a run is given, in one place.
///
/// The two *size* caps are what a contract can reach today. The three *count* caps
/// are set to 1 although [`build_wasm_engine`] already forces each: turning
/// `wasm_reference_types` on would let a module declare up to
/// `wasmparser::MAX_WASM_TABLES` tables, `wasm_multi_memory` likewise for memories,
/// and both size caps are **per table and per memory, not aggregate** — so a feature
/// flag flipped in isolation would multiply the ceiling by a hundred rather than
/// leave it be. The counts are what keeps those two decisions independent.
///
/// wasmi enforces the counts by asking the limiter before it allocates
/// (`can_create_more_instances`/`_memories`/`_tables`); they default to 10000, so
/// leaving them unset is not the same as their being unreachable.
fn store_limits() -> StoreLimits {
StoreLimitsBuilder::new()
.memory_size(MAX_MEMORY_BYTES)
.table_elements(MAX_TABLE_ELEMENTS)
.instances(1)
.tables(1)
.memories(1)
.trap_on_grow_failure(true)
.build()
}
/// Compile `wasm` for this engine.
///
/// The one path to a [`Module`]: the configuration is what decides whether a
/// contract is valid at all, so [`run`] and [`crate::check`] must not be able to
/// compile against different ones.
pub(crate) fn compile(wasm: &[u8]) -> Result<Module, String> {
Module::new(wasm_engine(), wasm).map_err(|e| e.to_string())
}
/// Run a contract: compile `wasm`, give it `gas` fuel, service its host
/// calls through `host`, and call the exported `function_name`.
pub fn run<'h>(
wasm: &[u8],
gas: u64,
host: &'h dyn HostFunctions,
function_name: &str,
) -> Result<RunOutcome, RunFailure> {
let engine = wasm_engine();
let module =
compile(wasm).map_err(|detail| RunFailure::owing_nothing(RunError::Compile(detail)))?;
let mut store = Store::new(
engine,
VmState {
host,
mem_limits: store_limits(),
transfer_budget: Cell::new(TRANSFER_LIMIT_BYTES),
memory: None,
out_buffer: [0u8; MAX_FIELD_BYTES],
},
);
store
.set_fuel(gas)
.map_err(|_| RunFailure::owing_nothing(RunError::Internal))?;
store.limiter(|state| &mut state.mem_limits);
let mut linker = Linker::<VmState<'h>>::new(engine);
register_host_functions(&mut linker)
.map_err(|_| RunFailure::owing_nothing(RunError::Internal))?;
let instance = match linker.instantiate_and_start(&mut store, &module) {
Ok(instance) => instance,
Err(e) => {
let error = guest_halted(&e).unwrap_or_else(|| instantiation_failure(&e));
return Err(failed(&store, gas, error));
}
};
store.data_mut().memory = instance.exports(&store).find_map(Export::into_memory);
let function = match instance.get_typed_func::<(), i32>(&store, function_name) {
Ok(function) => function,
Err(e) => {
let found = instance
.get_export(&store, function_name)
.map(|export| export.ty(&store));
let error =
RunError::EntryPoint(format!("{}: {e}", entry_point_fault(found, function_name)));
return Err(failed(&store, gas, error));
}
};
let result = match function.call(&mut store, ()) {
Ok(result) => result,
Err(e) => {
let error = guest_halted(&e).unwrap_or_else(|| RunError::Trap(e.to_string()));
return Err(failed(&store, gas, error));
}
};
let fuel_used = fuel_used(&store, gas).map_err(RunFailure::owing_nothing)?;
Ok(RunOutcome { result, fuel_used })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_engine_is_one_engine() {
assert!(Engine::same(wasm_engine(), wasm_engine()));
}
/// One instance, one table, one memory — asserted here rather than through a
/// module, because no module can reach these. `wasm_reference_types(false)` and
/// `wasm_multi_memory(false)` make a module declaring a second table or memory
/// fail *validation*, so a run never gets far enough to consult the limiter.
/// That is exactly why the counts are worth pinning: they are the ceiling that
/// survives one of those flags being turned on, and nothing else would fail if
/// they were silently dropped.
#[test]
fn the_store_grants_one_of_each_thing_a_module_can_own() {
use wasmi::ResourceLimiter;
let limits = store_limits();
assert_eq!(limits.instances(), 1);
assert_eq!(limits.tables(), 1);
assert_eq!(limits.memories(), 1);
}
/// The only place these numbers appear as literals; every other test derives
/// them from the constants.
#[test]
fn the_limits_are_the_protocol_limits() {
assert_eq!(MAX_MEMORY_PAGES, 128, "linear-memory page cap");
assert_eq!(MAX_MEMORY_BYTES, 8 * 1024 * 1024, "page cap in bytes");
assert_eq!(MAX_TABLE_ELEMENTS, 1024, "table-element cap");
assert_eq!(MAX_FIELD_BYTES, 1024, "kMaxWasmDataLength");
assert_eq!(TRANSFER_LIMIT_BYTES, 1 << 20, "kWasmTransferLimit");
}
}

View File

@@ -1,989 +0,0 @@
//! The two budgets a run spends: gas (fuel), and the transfer limit on bytes
//! crossing the boundary. Both are consensus input, so several of these tests
//! assert exact numbers.
mod support;
use support::{
Answer, EMPTY_REGION, FakeHost, ONE_PAGE, PLENTY_OF_GAS, code, import, module, run,
run_with_gas, trace_call,
};
use xrpl_host_functions::{HASH_LEN, HostError, HostFunctionSpec, TraceDataType};
use xrpl_wasm_vm::{MAX_FIELD_BYTES, RunError, TRANSFER_LIMIT_BYTES};
// ---------------------------------------------------------------------------
// Gas
// ---------------------------------------------------------------------------
/// The fuel a module of `body` burns, given gas to spare.
fn fuel_for(body: &str, parts: &[&str], host: &FakeHost) -> u64 {
let wat = module(parts, body);
run(&wat, host).expect("the module should run").fuel_used
}
/// The fuel a module burns doing nothing but returning a constant; every figure
/// below builds on it. wasmi's number, pinned deliberately because wasmi's fuel
/// table is consensus input.
const EMPTY_MODULE_FUEL: u64 = 30;
/// wasmi's own fuel for a host call whose operands are all small constants: 15 per
/// `*.const`, and the `call` itself is free. Our gas sits on top.
///
/// This holds only while every operand is a small constant. wasmi widens a
/// constant's encoding past a threshold, and a wider const costs more, so a call
/// built with a large constant fails here. Every call in [`call_for`] keeps its
/// operands small for that reason.
fn wasmi_call_fuel(small_const_operands: u64) -> u64 {
15 * small_const_operands
}
/// What wasmi charges on top of that for a call to a function with no result —
/// `trace`'s shape, and nothing else in the ABI. Per call, not per module. Measured
/// and pinned like the figures above.
const WASMI_NO_RESULT_FUEL: u64 = 15;
/// wasmi's fuel for one `(drop …)`, which is how a module makes more than one call
/// and keeps only the last result. Pinned like the two above.
const WASMI_DROP_FUEL: u64 = 22;
/// The wasm a test needs in order to call one host function: the `(import …)`
/// declaration, a call with small-constant operands, and how many it pushes.
struct Call {
import: &'static str,
call: &'static str,
operands: u64,
/// Whether the call leaves an `i32` behind. `trace` does not, which is why
/// [`Call::body`] ends every module with a constant instead of the call.
yields: bool,
}
impl Call {
/// `n` calls in a row, leaving one `i32` for the module to return: the last
/// answer where there is one, and a constant where the call has none.
fn body(&self, n: usize) -> String {
if self.yields {
format!(
"{}{}",
format!("(drop {}) ", self.call).repeat(n - 1),
self.call
)
} else {
format!("{}(i32.const 0)", format!("{} ", self.call).repeat(n))
}
}
/// What [`Call::body`] burns beside the calls' own gas and the module's floor:
/// one `drop` between consecutive answers, or wasmi's own surcharge on a call
/// that has none.
fn overhead(&self, n: u64) -> u64 {
if self.yields {
(n - 1) * WASMI_DROP_FUEL
} else {
n * WASMI_NO_RESULT_FUEL
}
}
}
/// The test wasm for each host function. The `match` is exhaustive, so a function
/// added to the ABI fails to compile until it has wasm here, and iterating
/// [`HostFunctionSpec::ALL`] then covers the whole ABI.
fn call_for(op: HostFunctionSpec) -> Call {
let (import, call, operands) = match op {
HostFunctionSpec::GetLedgerSqn => (
import::LDGR_INDEX,
"(call $ldgr_index (i32.const 0) (i32.const 4))",
2,
),
HostFunctionSpec::GetParentLedgerTime => (
import::PARENT_LDGR_TIME,
"(call $parent_ldgr_time (i32.const 0) (i32.const 4))",
2,
),
HostFunctionSpec::GetParentLedgerHash => (
import::PARENT_LDGR_HASH,
"(call $parent_ldgr_hash (i32.const 0) (i32.const 32))",
2,
),
HostFunctionSpec::GetBaseFee => (
import::BASE_FEE,
"(call $base_fee (i32.const 0) (i32.const 4))",
2,
),
HostFunctionSpec::IsAmendmentEnabled => (
import::AMENDMENT_ENABLED,
"(call $amendment_enabled (i32.const 0) (i32.const 32))",
2,
),
HostFunctionSpec::CacheLedgerObj => (
import::CACHE_LE,
"(call $cache_le (i32.const 0) (i32.const 32) (i32.const 0))",
3,
),
HostFunctionSpec::GetTxField => (
import::TX_FIELD,
"(call $tx_field (i32.const 1) (i32.const 0) (i32.const 4))",
3,
),
HostFunctionSpec::GetCurrentLedgerObjField => (
import::HOME_LE_FIELD,
"(call $home_le_field (i32.const 1) (i32.const 0) (i32.const 4))",
3,
),
HostFunctionSpec::GetLedgerObjField => (
import::LE_FIELD,
"(call $le_field (i32.const 1) (i32.const 1) (i32.const 0) (i32.const 4))",
4,
),
HostFunctionSpec::GetTxNestedField => (
import::TX_INNER,
"(call $tx_inner (i32.const 0) (i32.const 4) (i32.const 8) (i32.const 4))",
4,
),
HostFunctionSpec::GetCurrentLedgerObjNestedField => (
import::HOME_LE_INNER,
"(call $home_le_inner (i32.const 0) (i32.const 4) (i32.const 8) (i32.const 4))",
4,
),
HostFunctionSpec::GetLedgerObjNestedField => (
import::LE_INNER,
"(call $le_inner (i32.const 1) (i32.const 0) (i32.const 4) (i32.const 8) (i32.const 4))",
5,
),
HostFunctionSpec::GetTxArrayLen => {
(import::TX_ARR_LEN, "(call $tx_arr_len (i32.const 1))", 1)
}
HostFunctionSpec::GetCurrentLedgerObjArrayLen => (
import::HOME_LE_ARR_LEN,
"(call $home_le_arr_len (i32.const 1))",
1,
),
HostFunctionSpec::GetLedgerObjArrayLen => (
import::LE_ARR_LEN,
"(call $le_arr_len (i32.const 1) (i32.const 1))",
2,
),
HostFunctionSpec::GetTxNestedArrayLen => (
import::TX_INNER_ARR_LEN,
"(call $tx_inner_arr_len (i32.const 0) (i32.const 4))",
2,
),
HostFunctionSpec::GetCurrentLedgerObjNestedArrayLen => (
import::HOME_LE_INNER_ARR_LEN,
"(call $home_le_inner_arr_len (i32.const 0) (i32.const 4))",
2,
),
HostFunctionSpec::GetLedgerObjNestedArrayLen => (
import::LE_INNER_ARR_LEN,
"(call $le_inner_arr_len (i32.const 1) (i32.const 0) (i32.const 4))",
3,
),
HostFunctionSpec::CheckSignature => (
import::CHECK_SIG,
"(call $check_sig (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0))",
6,
),
HostFunctionSpec::AccountKeylet => (
import::ACCOUNTROOT_ID,
"(call $accountroot_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 32))",
4,
),
HostFunctionSpec::AmmKeylet => (
import::AMM_ID,
"(call $amm_id (i32.const 0) (i32.const 20) (i32.const 24) (i32.const 40) (i32.const 0) (i32.const 32))",
6,
),
HostFunctionSpec::CheckKeylet => (
import::CHECK_ID,
"(call $check_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
6,
),
HostFunctionSpec::CredentialKeylet => (
import::CREDENTIAL_ID,
"(call $credential_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 4) (i32.const 44) (i32.const 20))",
8,
),
HostFunctionSpec::DelegateKeylet => (
import::DELEGATE_ID,
"(call $delegate_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 32))",
6,
),
HostFunctionSpec::DepositPreauthKeylet => (
import::DEPOSIT_PREAUTH_ID,
"(call $deposit_preauth_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 32))",
6,
),
HostFunctionSpec::DidKeylet => (
import::DID_ID,
"(call $did_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 32))",
4,
),
HostFunctionSpec::EscrowKeylet => (
import::ESCROW_ID,
"(call $escrow_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
6,
),
HostFunctionSpec::TrustLineKeylet => (
import::TRUSTLINE_ID,
"(call $trustline_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 20) (i32.const 60) (i32.const 32))",
8,
),
HostFunctionSpec::MptokenIssuanceKeylet => (
import::MPT_ISSUANCE_ID,
"(call $mpt_issuance_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
6,
),
HostFunctionSpec::MptokenKeylet => (
import::MPTOKEN_ID,
"(call $mptoken_id (i32.const 0) (i32.const 24) (i32.const 24) (i32.const 20) (i32.const 44) (i32.const 20))",
6,
),
HostFunctionSpec::NftokenOfferKeylet => (
import::NFT_OFFER_ID,
"(call $nft_offer_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
6,
),
HostFunctionSpec::OfferKeylet => (
import::OFFER_ID,
"(call $offer_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
6,
),
HostFunctionSpec::OracleKeylet => (
import::ORACLE_ID,
"(call $oracle_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
6,
),
HostFunctionSpec::PaychannelKeylet => (
import::PAYCHAN_ID,
"(call $paychan_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 40) (i32.const 20))",
8,
),
HostFunctionSpec::PermissionedDomainKeylet => (
import::PERMISSIONED_DOMAIN_ID,
"(call $permissioned_domain_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
6,
),
HostFunctionSpec::SignerListKeylet => (
import::SIGNERS_ID,
"(call $signers_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 32))",
4,
),
HostFunctionSpec::TicketKeylet => (
import::TICKET_ID,
"(call $ticket_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
6,
),
HostFunctionSpec::VaultKeylet => (
import::VAULT_ID,
"(call $vault_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))",
6,
),
HostFunctionSpec::Sha512Half => (
import::SHA512_HALF,
"(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))",
4,
),
HostFunctionSpec::Trace => (
import::TRACE,
"(call $trace (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 0) (i32.const 0))",
5,
),
HostFunctionSpec::UpdateData => (
import::SET_DATA,
"(call $set_data (i32.const 0) (i32.const 8))",
2,
),
HostFunctionSpec::GetNft => (
import::NFT_URI,
"(call $nft_uri (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 32) (i32.const 52) (i32.const 12))",
6,
),
HostFunctionSpec::GetNftIssuer => (
import::NFT_ISSUER,
"(call $nft_issuer (i32.const 0) (i32.const 32) (i32.const 32) (i32.const 20))",
4,
),
HostFunctionSpec::GetNftTaxon => (
import::NFT_TAXON,
"(call $nft_taxon (i32.const 0) (i32.const 32) (i32.const 32) (i32.const 4))",
4,
),
HostFunctionSpec::GetNftFlags => (
import::NFT_FLAGS,
"(call $nft_flags (i32.const 0) (i32.const 32))",
2,
),
HostFunctionSpec::GetNftTransferFee => (
import::NFT_XFER_FEE,
"(call $nft_xfer_fee (i32.const 0) (i32.const 32))",
2,
),
HostFunctionSpec::GetNftSequence => (
import::NFT_SERIAL,
"(call $nft_serial (i32.const 0) (i32.const 32) (i32.const 32) (i32.const 4))",
4,
),
HostFunctionSpec::FloatFromInt => (
import::FLOAT_FROM_INT,
"(call $float_from_int (i64.const 0) (i32.const 0) (i32.const 8) (i32.const 0))",
4,
),
HostFunctionSpec::FloatFromUint => (
import::FLOAT_FROM_UINT,
"(call $float_from_uint (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 0))",
5,
),
HostFunctionSpec::FloatFromStamount => (
import::FLOAT_FROM_STAMOUNT,
"(call $float_from_stamount (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 0))",
5,
),
HostFunctionSpec::FloatFromStnumber => (
import::FLOAT_FROM_STNUMBER,
"(call $float_from_stnumber (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 0))",
5,
),
HostFunctionSpec::FloatToInt => (
import::FLOAT_TO_INT,
"(call $float_to_int (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 0))",
5,
),
HostFunctionSpec::FloatToMantExp => (
import::FLOAT_TO_MANT_EXP,
"(call $float_to_mant_exp (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 4))",
6,
),
HostFunctionSpec::FloatFromMantExp => (
import::FLOAT_FROM_MANT_EXP,
"(call $float_from_mant_exp (i64.const 0) (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 0))",
5,
),
HostFunctionSpec::FloatCompare => (
import::FLOAT_CMP,
"(call $float_cmp (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8))",
4,
),
HostFunctionSpec::FloatAdd => (
import::FLOAT_ADD,
"(call $float_add (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 8) (i32.const 0))",
7,
),
HostFunctionSpec::FloatSubtract => (
import::FLOAT_SUB,
"(call $float_sub (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 8) (i32.const 0))",
7,
),
HostFunctionSpec::FloatMultiply => (
import::FLOAT_MULT,
"(call $float_mult (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 8) (i32.const 0))",
7,
),
HostFunctionSpec::FloatDivide => (
import::FLOAT_DIV,
"(call $float_div (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 8) (i32.const 0))",
7,
),
HostFunctionSpec::FloatPower => (
import::FLOAT_POW,
"(call $float_pow (i32.const 0) (i32.const 8) (i32.const 2) (i32.const 8) (i32.const 8) (i32.const 0))",
6,
),
};
Call {
import,
call,
operands,
yields: !matches!(op, HostFunctionSpec::Trace),
}
}
#[test]
fn an_empty_module_burns_a_fixed_amount_of_fuel() {
let fuel = fuel_for("(i32.const 0)", &[ONE_PAGE], &FakeHost::new());
assert_eq!(fuel, EMPTY_MODULE_FUEL);
}
/// Calling a host function `n` times costs `n` times its gas, to the unit. Every
/// other term is known — the module's floor, wasmi's fuel per call, one `drop` per
/// answered call — so the total is a closed form, with the gas read from the spec
/// table rather than restated. `n = 1` pins the charge, `n > 1` pins that it lands
/// on every call rather than once per run.
#[test]
fn a_host_call_costs_its_gas_every_time_it_is_called() {
let host = FakeHost::new().answering_field(1, Answer::bytes([0xaa]));
for &op in HostFunctionSpec::ALL {
let call = call_for(op);
let per_call = wasmi_call_fuel(call.operands) + op.gas();
for n in 1..=3 {
let body = call.body(n);
let n = n as u64;
assert_eq!(
fuel_for(&body, &[call.import, ONE_PAGE], &host),
EMPTY_MODULE_FUEL + n * per_call + call.overhead(n),
"{n} x {}",
call.call
);
}
}
}
/// The gas charge precedes the call's body, so a failing call costs exactly what a
/// successful one costs. Field 1 is answered and field 7 is not; the two modules
/// are otherwise identical, so their totals are comparable.
#[test]
fn a_failing_host_call_costs_exactly_what_a_successful_one_costs() {
let host = FakeHost::new().answering_field(1, Answer::bytes([0xaa]));
let call = |field: i32| {
module(
&[import::HOME_LE_FIELD, ONE_PAGE],
&format!("(call $home_le_field (i32.const {field}) (i32.const 0) (i32.const 4))"),
)
};
let answered = run(&call(1), &host).expect("the module should run");
let refused = run(&call(7), &host).expect("the module should run");
assert_eq!(answered.result, 1);
assert_eq!(refused.result, code(HostError::FieldNotFound));
assert_eq!(refused.fuel_used, answered.fuel_used);
}
/// `fuel_used` is `gas - remaining`: what the run spent, not what was left or what
/// it was handed. The gas figures are derived from the run's cost, so the boundary
/// — exactly enough, and one short — is among the cases.
#[test]
fn fuel_used_is_what_was_spent_not_what_was_supplied() {
let host = FakeHost::new();
let op = HostFunctionSpec::GetLedgerSqn;
let call = call_for(op);
let wat = module(&[call.import, ONE_PAGE], call.call);
let cost = EMPTY_MODULE_FUEL + wasmi_call_fuel(call.operands) + op.gas();
// Exactly its cost is enough, and no amount above it changes the figure. The
// result is checked too, so the figure belongs to a run that did the work
// rather than to one that was cut short.
for gas in [cost, cost + 1, cost * 100, PLENTY_OF_GAS] {
let outcome = run_with_gas(&wat, gas, &host).expect("should run");
assert_eq!(
outcome.result, 4,
"gas {gas}: the call should have succeeded"
);
assert_eq!(outcome.fuel_used, cost, "gas {gas}");
}
// One fuel short: the run ends at the call it cannot pay for and still owes the
// whole limit, because `charge` spends what is left.
let short = run_with_gas(&wat, cost - 1, &host).expect_err("one fuel short must not complete");
assert!(
matches!(short.error, RunError::OutOfGas),
"expected the run to end out of gas, got: {short}"
);
assert_eq!(short.fuel_used, cost - 1);
}
/// Fuel is metered, so the same module burns the same fuel every time — a
/// property consensus depends on.
#[test]
fn the_same_run_burns_the_same_fuel() {
let call = call_for(HostFunctionSpec::Trace);
let wat = module(&[call.import, ONE_PAGE], &call.body(1));
let first = run(&wat, &FakeHost::new()).expect("should run").fuel_used;
for _ in 0..4 {
assert_eq!(
run(&wat, &FakeHost::new()).expect("should run").fuel_used,
first
);
}
assert!(first > HostFunctionSpec::Trace.gas());
}
/// Too little gas to finish stops the run: the meter refuses the guest's own
/// instructions before it ever reaches the host call.
#[test]
fn a_run_that_cannot_afford_itself_fails() {
let host = FakeHost::new();
let wat = module(
&[import::LDGR_INDEX, ONE_PAGE],
"(call $ldgr_index (i32.const 0) (i32.const 4))",
);
for gas in [0, 1, 10] {
let Err(failure) = run_with_gas(&wat, gas, &host) else {
panic!("gas {gas} should not have completed");
};
assert!(
matches!(failure.error, RunError::OutOfGas),
"gas {gas}: expected the run to end out of gas, got: {failure}"
);
}
}
/// A guest looping forever is stopped by gas rather than running away, and owes
/// the gas it burned doing it.
#[test]
fn an_endless_loop_is_stopped_by_gas() {
const GAS: u64 = 100_000;
let host = FakeHost::new();
let wat = module(&[ONE_PAGE], "(loop $l (br $l)) (i32.const 0)");
let failure = run_with_gas(&wat, GAS, &host).expect_err("an endless loop must not complete");
assert!(
matches!(failure.error, RunError::OutOfGas),
"expected the meter to stop it, got: {failure}"
);
// The cost break down is as follows:
// 1. There is a function entry charge (finish function) which seems to be 63 units of fuel.
// 2. Each iteration costs 2 units of fuel.
// For a GAS amount of 100,000, we will be limited to burning an odd number of fuel.
// So the way the test is written, the most fuel that will be used is 99,999 units.
assert_eq!(
failure.fuel_used,
GAS - 1,
"a runaway guest burns all but the last unit of the limit"
);
}
/// A host call refused its gas stops the run: the guest never gets a chance to
/// ignore the refusal and carry on, and it is charged the whole limit.
///
/// The gas range is every amount that reaches the call and cannot pay for it, so
/// the case is the whole boundary rather than one number. `trace` is the call under
/// it because it is the one that could not report a refusal even if it wanted to:
/// stopping the run is the whole of what the guest sees.
#[test]
fn a_host_call_refused_its_gas_stops_the_run() {
let host = FakeHost::new();
let op = HostFunctionSpec::Trace;
let call = call_for(op);
let wat = module(&[call.import, ONE_PAGE], &call.body(1));
// Measured rather than derived: the whole run's cost, less the call's own gas,
// is the least a guest can be given and still reach the call. Below that the
// meter stops the guest's own instructions instead, which is
// `a_run_that_cannot_afford_itself_fails`'s case, not this one.
let cost = run(&wat, &FakeHost::new())
.expect("the module should run")
.fuel_used;
for gas in cost - op.gas()..cost {
let Err(failure) = run_with_gas(&wat, gas, &host) else {
panic!("gas {gas}: the run completed, so the guest was handed the refusal");
};
assert!(
matches!(failure.error, RunError::OutOfGas),
"gas {gas}: expected the run to end out of gas, got: {failure}"
);
assert_eq!(
failure.fuel_used, gas,
"gas {gas}: a call it cannot afford burns the whole limit"
);
}
assert!(host.traces().is_empty(), "the host body must not have run");
}
// ---------------------------------------------------------------------------
// The transfer limit
// ---------------------------------------------------------------------------
/// A module that repeats `call` while `keep_going` holds, then returns the last
/// status, so a budget can be run to exhaustion inside one invocation.
fn until_refused(imports: &str, call: &str, keep_going: &str) -> String {
module(
&[imports, ONE_PAGE],
&format!(
"(local $r i32)
(loop $l
(local.set $r {call})
(br_if $l {keep_going}))
(local.get $r)"
),
)
}
/// For a call whose success is a positive byte count.
const WHILE_POSITIVE: &str = "(i32.gt_s (local.get $r) (i32.const 0))";
/// Bytes written into guest memory are charged against the run's budget, and the
/// budget is a per-run total: 1 MiB of 1 KiB values exhausts it.
#[test]
fn writes_spend_the_transfer_budget() {
let host = FakeHost::new().answering_field(1, Answer::filler(MAX_FIELD_BYTES));
let wat = until_refused(
import::HOME_LE_FIELD,
&format!("(call $home_le_field (i32.const 1) (i32.const 0) (i32.const {MAX_FIELD_BYTES}))"),
WHILE_POSITIVE,
);
let outcome = run(&wat, &host).expect("the module should run");
assert_eq!(outcome.result, code(HostError::OutOfTransferLimit));
assert_eq!(
host.fields_asked.borrow().len() as u64,
TRANSFER_LIMIT_BYTES / MAX_FIELD_BYTES as u64 + 1,
"one call per 1 KiB of budget, plus the one that was refused"
);
}
/// The budget is per run, not per call: a fresh run starts with a full budget.
#[test]
fn each_run_gets_its_own_budget() {
let wat = until_refused(
import::HOME_LE_FIELD,
&format!("(call $home_le_field (i32.const 1) (i32.const 0) (i32.const {MAX_FIELD_BYTES}))"),
WHILE_POSITIVE,
);
for _ in 0..2 {
let host = FakeHost::new().answering_field(1, Answer::filler(MAX_FIELD_BYTES));
let outcome = run(&wat, &host).expect("the module should run");
assert_eq!(outcome.result, code(HostError::OutOfTransferLimit));
assert_eq!(
host.fields_asked.borrow().len() as u64,
TRANSFER_LIMIT_BYTES / MAX_FIELD_BYTES as u64 + 1
);
}
}
/// A run well inside the budget never sees it.
#[test]
fn a_modest_run_never_meets_the_budget() {
let host = FakeHost::new().answering_field(1, Answer::filler(MAX_FIELD_BYTES));
let wat = module(
&[import::HOME_LE_FIELD, ONE_PAGE],
&format!("(call $home_le_field (i32.const 1) (i32.const 0) (i32.const {MAX_FIELD_BYTES}))"),
);
let outcome = run(&wat, &host).expect("the module should run");
assert_eq!(outcome.result, MAX_FIELD_BYTES as i32);
}
/// A write the budget refuses is a write that did not happen. `float_to_mant_exp` is
/// the case worth pinning: its two regions are charged as one, so a call that cannot
/// pay for both must leave both alone rather than place the mantissa and refuse.
#[test]
fn a_write_the_budget_refuses_reaches_guest_memory_in_no_part() {
let host = FakeHost::new()
.answering_field(1, Answer::filler(MAX_FIELD_BYTES))
.answering_float_mant_exp(vec![1, 2, 3, 4, 5, 6, 7, 8], vec![9, 10, 11, 12]);
// Spend the budget on 1 KiB fields at offset 0, then ask for a mantissa and an
// exponent at offsets well clear of them.
let call = "(call $float_to_mant_exp (i32.const 0) (i32.const 8) (i32.const 2048) (i32.const 8) (i32.const 2064) (i32.const 4))";
let spent = |tail: &str| {
module(
&[import::HOME_LE_FIELD, import::FLOAT_TO_MANT_EXP, ONE_PAGE],
&format!(
"(local $r i32)
(loop $l
(local.set $r (call $home_le_field (i32.const 1) (i32.const 0) (i32.const {MAX_FIELD_BYTES})))
(br_if $l {WHILE_POSITIVE}))
{tail}"
),
)
};
let refused = run(&spent(call), &host).expect("the module should run");
assert_eq!(refused.result, code(HostError::OutOfTransferLimit));
let wat = spent(&format!(
"(drop {call})
(i32.or (i32.load8_u (i32.const 2048)) (i32.load8_u (i32.const 2064)))"
));
let outcome = run(&wat, &host).expect("the module should run");
assert_eq!(outcome.result, 0, "neither region should be written");
}
/// The same rule on the path that writes straight into guest memory: `write_into`
/// hands the host a slice *of the guest's own buffer*, so a value the budget cannot
/// pay for has to be kept out of that slice before the host fills it.
///
/// The probe region is one the spending loop never writes to, so anything found there
/// came from the refused call.
#[test]
fn a_straight_write_the_budget_refuses_reaches_guest_memory_in_no_part() {
/// Clear of the offset the spending loop writes to.
const PROBE: usize = 2048;
/// Every byte of the value, so the fold sees a prefix as readily as the whole.
const MARK: u8 = 0xff;
let host = FakeHost::new().answering_field(1, Answer::bytes(vec![MARK; MAX_FIELD_BYTES]));
let call = format!(
"(call $home_le_field (i32.const 1) (i32.const {PROBE}) (i32.const {MAX_FIELD_BYTES}))"
);
// Every local the tails below use is declared here: wasm wants them all ahead of
// the first instruction.
let spent = |tail: &str| {
module(
&[import::HOME_LE_FIELD, ONE_PAGE],
&format!(
"(local $r i32) (local $i i32) (local $seen i32)
(loop $l
(local.set $r (call $home_le_field (i32.const 1) (i32.const 0) (i32.const {MAX_FIELD_BYTES})))
(br_if $l {WHILE_POSITIVE}))
{tail}"
),
)
};
let refused = run(&spent(&call), &host).expect("the module should run");
assert_eq!(refused.result, code(HostError::OutOfTransferLimit));
// Guest memory starts zero-filled, so or-ing the region together reports whether
// any byte of it was written.
let wat = spent(&format!(
"(drop {call})
(loop $l
(local.set $seen (i32.or (local.get $seen)
(i32.load8_u (i32.add (i32.const {PROBE}) (local.get $i)))))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $l (i32.lt_u (local.get $i) (i32.const {MAX_FIELD_BYTES}))))
(local.get $seen)"
));
let outcome = run(&wat, &host).expect("the module should run");
assert_eq!(outcome.result, 0, "not one byte should have been written");
}
/// What a write may deliver is what is *left* of the budget, to the byte.
///
/// The prologue spends all but `LEFT`, and field 3's host answers with as much as it
/// is offered — so the window `write_into` opened is what it reports and what it
/// leaves in guest memory, and both are read off as `LEFT`. A mark is a 1, so the
/// fold over the probe's whole buffer counts the bytes that reached it.
///
/// `LEFT` is under [`MAX_FIELD_BYTES`] and the buffer is wider than both probes'
/// values, so it is the budget answering and neither the field cap nor the guest's
/// capacity. Field 4 is the byte past it: a host whose value is one larger than what
/// is left, which no window can hold.
#[test]
fn a_write_may_deliver_what_is_left_of_the_budget_and_not_a_byte_more() {
/// Full-cap writes, all the prologue can make without overshooting.
const BULK: u64 = TRANSFER_LIMIT_BYTES / MAX_FIELD_BYTES as u64 - 1;
/// What the prologue leaves unspent.
const LEFT: usize = MAX_FIELD_BYTES / 2;
/// The write that trims what [`BULK`] leaves down to [`LEFT`].
const TRIM: usize = MAX_FIELD_BYTES - LEFT;
/// Clear of the offset the prologue writes to.
const PROBE: usize = 2048;
const BUFFER: usize = MAX_FIELD_BYTES;
/// One per byte written, so the fold below sums to how many there were.
const MARK: u8 = 1;
assert_eq!(
BULK * MAX_FIELD_BYTES as u64 + TRIM as u64 + LEFT as u64,
TRANSFER_LIMIT_BYTES,
"the prologue must spend all but LEFT of the budget"
);
let host = FakeHost::new()
.answering_field(1, Answer::filler(MAX_FIELD_BYTES))
.answering_field(2, Answer::filler(TRIM))
.answering_field(3, Answer::as_much_as_offered(MARK))
.answering_field(4, Answer::claiming(LEFT + 1));
let probe = |field: i32| {
format!(
"(call $home_le_field (i32.const {field}) (i32.const {PROBE}) (i32.const {BUFFER}))"
)
};
// Every local the tails use, declared where wasm wants them.
let after_prologue = |tail: String| {
let wat = module(
&[import::HOME_LE_FIELD, ONE_PAGE],
&format!(
"(local $i i32) (local $marks i32)
(loop $l
(drop (call $home_le_field (i32.const 1) (i32.const 0) (i32.const {MAX_FIELD_BYTES})))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $l (i32.lt_u (local.get $i) (i32.const {BULK}))))
(drop (call $home_le_field (i32.const 2) (i32.const 0) (i32.const {TRIM})))
(local.set $i (i32.const 0))
{tail}"
),
);
run(&wat, &host).expect("the module should run").result
};
assert_eq!(
after_prologue(probe(3)),
LEFT as i32,
"the host should be offered exactly what is left"
);
// Guest memory starts zero-filled, so summing the probe's whole buffer counts the
// marks in it.
assert_eq!(
after_prologue(format!(
"(drop {})
(loop $l
(local.set $marks (i32.add (local.get $marks)
(i32.load8_u (i32.add (i32.const {PROBE}) (local.get $i)))))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $l (i32.lt_u (local.get $i) (i32.const {BUFFER}))))
(local.get $marks)",
probe(3)
)),
LEFT as i32,
"and that many marks, no more, should reach guest memory"
);
assert_eq!(
after_prologue(probe(4)),
code(HostError::OutOfTransferLimit),
"a value one byte past what is left fits no window"
);
}
/// Reads leave the budget alone: `read_borrowed` hands the host a slice *aliasing*
/// guest memory, so there are no copied bytes to charge. What bounds how many reads
/// a run can make is gas, which every host call pays before its body runs.
///
/// The observation is the write at the end, not the reads: the module reads four
/// times the whole budget first, so a rule that charged reads would have nothing
/// left, and the write would answer `OutOfTransferLimit` instead of a byte count.
#[test]
fn reads_do_not_spend_the_transfer_budget() {
/// 1 KiB reads, four times over the budget.
const READS: u64 = 4 * TRANSFER_LIMIT_BYTES / MAX_FIELD_BYTES as u64;
let host = FakeHost::new().answering_field(1, Answer::filler(MAX_FIELD_BYTES));
let read = trace_call(
TraceDataType::AsHex,
EMPTY_REGION,
&format!("(i32.const 0) (i32.const {MAX_FIELD_BYTES})"),
);
let wat = module(
&[import::TRACE, import::HOME_LE_FIELD, ONE_PAGE],
&format!(
"(local $i i32)
(loop $l
{read}
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $l (i32.lt_u (local.get $i) (i32.const {READS}))))
(call $home_le_field (i32.const 1) (i32.const 0) (i32.const {MAX_FIELD_BYTES}))"
),
);
let outcome = run(&wat, &host).expect("the module should run");
assert_eq!(
host.traces().len() as u64,
READS,
"every read should have been served"
);
assert_eq!(
outcome.result, MAX_FIELD_BYTES as i32,
"the write after {READS} reads of {MAX_FIELD_BYTES} bytes should still have its budget"
);
}
/// Only the output half of a read-write call spends the budget. `sha512_half`'s
/// input is a borrowed read like any other, aliasing guest memory rather than
/// crossing the boundary, so a run may hash far more bytes than the budget holds as
/// long as the digests it writes fit inside it.
///
/// The two totals are asserted, so the arithmetic that makes the case is in the
/// test rather than in a comment: the inputs alone would overrun the budget, the
/// digests alone are a small fraction of it.
#[test]
fn only_the_output_half_of_a_read_write_spends_the_budget() {
/// Enough 1 KiB inputs to overrun the budget twice over.
const CALLS: u64 = 2 * TRANSFER_LIMIT_BYTES / MAX_FIELD_BYTES as u64;
assert!(
CALLS * MAX_FIELD_BYTES as u64 > TRANSFER_LIMIT_BYTES,
"the inputs alone must overrun the budget"
);
assert!(
CALLS * HASH_LEN as u64 <= TRANSFER_LIMIT_BYTES / 2,
"the digests alone must stay well inside it"
);
let host = FakeHost::new().answering_digest(Answer::filler(HASH_LEN));
let wat = module(
&[import::SHA512_HALF, ONE_PAGE],
&format!(
"(local $i i32)
(local $r i32)
(loop $l
(local.set $r (call $sha512_half (i32.const 0) (i32.const {MAX_FIELD_BYTES})
(i32.const 0) (i32.const {HASH_LEN})))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $l (i32.lt_u (local.get $i) (i32.const {CALLS}))))
(local.get $r)"
),
);
let outcome = run(&wat, &host).expect("the module should run");
assert_eq!(
host.digested.borrow().len() as u64,
CALLS,
"every call should have been served"
);
assert_eq!(
outcome.result, HASH_LEN as i32,
"only the digests are charged, and they fit"
);
}
// cspell:disable
/// Measures each pinned fuel figure straight from wasmi and asserts the constant
/// still matches. This is what fails first when a wasmi upgrade shifts the fuel
/// table, and it prints every measured number so the constants can be re-derived:
///
/// cargo test -p xrpl-wasm-vm --test budgets probe_fuel -- --exact --nocapture
///
/// The behavioural tests above build totals out of these constants; this one ties
/// each constant back to the one measurement that defines it.
// cspell:enable
#[test]
fn probe_fuel() {
let h = FakeHost::new().answering_field(1, Answer::bytes([0xaa]));
// EMPTY_MODULE_FUEL: a module that only returns a constant.
let empty = fuel_for("(i32.const 0)", &[ONE_PAGE], &h);
eprintln!("EMPTY_MODULE_FUEL = {empty}");
assert_eq!(empty, EMPTY_MODULE_FUEL, "EMPTY_MODULE_FUEL");
// wasmi_call_fuel(operands) = fuel(1 call) - empty - gas, for every op. Asserting
// it against the formula across all operand counts pins both slope and intercept.
eprintln!("--- wasmi_call_fuel by operand count ---");
for &op in HostFunctionSpec::ALL {
let c = call_for(op);
// A no-result op's body ends in a trailing constant, not the call's result,
// so its total carries an extra push; it is pinned in the NO_RESULT section.
if !c.yields {
continue;
}
let one = fuel_for(&c.body(1), &[c.import, ONE_PAGE], &h);
let measured = one - empty - op.gas();
eprintln!(
"operands={:2} wasmi_call_fuel={measured:3} {}",
c.operands, c.call
);
assert_eq!(
measured,
wasmi_call_fuel(c.operands),
"wasmi_call_fuel({}) for {}",
c.operands,
c.call
);
}
// WASMI_DROP_FUEL: a second yielding call adds one call plus one drop.
let g = call_for(HostFunctionSpec::GetLedgerSqn);
let g1 = fuel_for(&g.body(1), &[g.import, ONE_PAGE], &h);
let g2 = fuel_for(&g.body(2), &[g.import, ONE_PAGE], &h);
let drop = (g2 - g1) - (wasmi_call_fuel(g.operands) + HostFunctionSpec::GetLedgerSqn.gas());
eprintln!("WASMI_DROP_FUEL = {drop}");
assert_eq!(drop, WASMI_DROP_FUEL, "WASMI_DROP_FUEL");
// WASMI_NO_RESULT_FUEL: trace is the only no-result op; its module ends in a
// trailing constant instead of the call's result.
let t = call_for(HostFunctionSpec::Trace);
let t1 = fuel_for(&t.body(1), &[t.import, ONE_PAGE], &h);
let no_result = t1 - empty - wasmi_call_fuel(t.operands) - HostFunctionSpec::Trace.gas();
eprintln!("WASMI_NO_RESULT_FUEL = {no_result}");
assert_eq!(no_result, WASMI_NO_RESULT_FUEL, "WASMI_NO_RESULT_FUEL");
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,621 +0,0 @@
//! The bounds, field-cap and buffer-fit rules `abi.rs` enforces on every region
//! crossing the boundary. This is the policy the guest observes, so each rule is
//! pinned to the code it answers with.
mod support;
use support::{
Answer, COMPLETED, EMPTY_REGION, FakeHost, ONE_PAGE, code, failure, import, module, status,
traced,
};
use xrpl_host_functions::{HASH_LEN, HostError, TraceDataType};
use xrpl_wasm_vm::{MAX_FIELD_BYTES, RunError};
/// One page, so anything at or past 65536 is out of bounds.
const PAGE: i64 = 64 * 1024;
/// The per-field size cap, as a wasm operand.
const CAP: i64 = MAX_FIELD_BYTES as i64;
/// One byte over the cap: the smallest value the engine must refuse.
const OVER_CAP: i64 = CAP + 1;
// ---------------------------------------------------------------------------
// Output regions (`write_into`)
// ---------------------------------------------------------------------------
/// The whole output region must be in bounds, not merely its start — the engine
/// checks `[dst, dst + cap)` before the host is allowed to write.
#[test]
fn an_output_region_running_past_memory_is_refused() {
let host = FakeHost::new();
for (dst, cap) in [(PAGE, 4), (PAGE - 3, 4), (PAGE + 1024, 4), (0, PAGE + 1)] {
let wat = module(
&[import::LDGR_INDEX, ONE_PAGE],
&format!("(call $ldgr_index (i32.const {dst}) (i32.const {cap}))"),
);
assert_eq!(
status(&wat, &host),
code(HostError::PointerOutOfBounds),
"dst {dst} cap {cap}"
);
}
}
/// A region ending exactly at the last byte of memory is in bounds.
#[test]
fn an_output_region_ending_at_the_last_byte_is_allowed() {
let host = FakeHost::new();
let wat = module(
&[import::LDGR_INDEX, ONE_PAGE],
&format!("(call $ldgr_index (i32.const {}) (i32.const 4))", PAGE - 4),
);
assert_eq!(status(&wat, &host), 4);
}
/// The wire carries `i32`, so a guest can present a negative pointer or length.
#[test]
fn a_negative_output_pointer_or_length_is_refused() {
let host = FakeHost::new();
for (dst, cap) in [(-1, 4), (0, -1), (-1, -1), (i32::MIN, 4)] {
let wat = module(
&[import::LDGR_INDEX, ONE_PAGE],
&format!("(call $ldgr_index (i32.const {dst}) (i32.const {cap}))"),
);
assert_eq!(
status(&wat, &host),
code(HostError::InvalidParams),
"dst {dst} cap {cap}"
);
}
}
/// The host reports a value's true length whether or not it fitted; a value that
/// did not fit is the guest's error, not the host's.
#[test]
fn a_value_larger_than_the_buffer_is_refused() {
let host = FakeHost::new().answering_field(1, Answer::filler(64));
let wat = module(
&[import::HOME_LE_FIELD, ONE_PAGE],
"(call $home_le_field (i32.const 1) (i32.const 0) (i32.const 63))",
);
assert_eq!(status(&wat, &host), code(HostError::BufferTooSmall));
let wat = module(
&[import::HOME_LE_FIELD, ONE_PAGE],
"(call $home_le_field (i32.const 1) (i32.const 0) (i32.const 64))",
);
assert_eq!(status(&wat, &host), 64, "exactly enough room is enough");
}
/// A zero-length output region is in bounds and simply cannot hold anything.
#[test]
fn a_zero_length_output_region_is_in_bounds_but_too_small() {
let host = FakeHost::new();
let wat = module(
&[import::LDGR_INDEX, ONE_PAGE],
"(call $ldgr_index (i32.const 0) (i32.const 0))",
);
assert_eq!(status(&wat, &host), code(HostError::BufferTooSmall));
}
/// A host that reports more than the per-field cap is refused even when the
/// guest offered room for it: the cap is the engine's rule, not the buffer's.
#[test]
fn a_value_past_the_field_cap_is_refused() {
let host = FakeHost::new()
.answering_field(1, Answer::claiming(OVER_CAP as usize))
.answering_field(2, Answer::claiming(MAX_FIELD_BYTES));
let wat = module(
&[import::HOME_LE_FIELD, ONE_PAGE],
"(call $home_le_field (i32.const 1) (i32.const 0) (i32.const 4096))",
);
assert_eq!(status(&wat, &host), code(HostError::DataFieldTooLarge));
let wat = module(
&[import::HOME_LE_FIELD, ONE_PAGE],
"(call $home_le_field (i32.const 2) (i32.const 0) (i32.const 4096))",
);
assert_eq!(status(&wat, &host), CAP as i32, "the cap itself is allowed");
}
/// A refused over-cap value leaves nothing behind. `write_into` hands the host at
/// most [`MAX_FIELD_BYTES`] of the guest's buffer however much room the guest
/// declared, so a value past the cap does not fit the region it is offered and no
/// prefix of it can reach guest memory either.
///
/// The host answers with a real over-cap value: [`Answer::claiming`] writes
/// nothing whatever the engine does, so it could not tell the two apart. The
/// second module folds the *whole* declared buffer rather than one byte, so the
/// claim is about the region and not about its first byte.
#[test]
fn an_over_cap_value_is_refused_without_reaching_guest_memory() {
/// The buffer the guest declares: well over the cap, so the clamp bites.
const BUFFER: usize = 4096;
let over_cap = vec![0xff; MAX_FIELD_BYTES + 1];
let host = FakeHost::new().answering_field(1, Answer::bytes(over_cap));
let call = format!("(call $home_le_field (i32.const 1) (i32.const 0) (i32.const {BUFFER}))");
// The status the guest sees, from a module that returns it directly.
let refusing = module(&[import::HOME_LE_FIELD, ONE_PAGE], &call);
assert_eq!(
status(&refusing, &host),
code(HostError::DataFieldTooLarge),
"the value is refused"
);
// Every byte of the buffer, or-ed together: guest memory starts zero-filled,
// so any byte the host wrote shows up here.
let reading = module(
&[import::HOME_LE_FIELD, ONE_PAGE],
&format!(
"(local $i i32)
(local $seen i32)
(drop {call})
(loop $l
(local.set $seen (i32.or (local.get $seen) (i32.load8_u (local.get $i))))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $l (i32.lt_u (local.get $i) (i32.const {BUFFER}))))
(local.get $seen)"
),
);
assert_eq!(
status(&reading, &host),
0,
"and not one of its bytes is in the guest's buffer"
);
}
/// The field cap is checked before the buffer-fit rule, so a value that breaks both
/// is reported as over-cap. The guest branches on the code, and the two rules
/// answer different questions, so the order is worth pinning.
#[test]
fn the_field_cap_precedes_the_buffer_fit_check() {
let host = FakeHost::new().answering_field(1, Answer::claiming(MAX_FIELD_BYTES + 1));
// A 63-byte buffer: the value is both over the cap and far too big to fit.
let wat = module(
&[import::HOME_LE_FIELD, ONE_PAGE],
"(call $home_le_field (i32.const 1) (i32.const 0) (i32.const 63))",
);
assert_eq!(status(&wat, &host), code(HostError::DataFieldTooLarge));
}
// ---------------------------------------------------------------------------
// Input regions (`Region::read`, via `sha512_half`)
//
// `sha512_half`'s first pair is an input region like any other, and it is the
// input the guest gets a status back from: `trace`, the other reader, answers
// nothing at all. So the codes are pinned here and the silence below.
// ---------------------------------------------------------------------------
/// An input region is bounds-checked the same way an output region is. Every case
/// here stays within the field cap, which on an input is checked first.
#[test]
fn an_input_region_running_past_memory_is_refused() {
let host = FakeHost::new();
for (ptr, len) in [(PAGE, 1), (PAGE - 3, 4), (PAGE - 1, CAP)] {
let wat = module(
&[import::SHA512_HALF, ONE_PAGE],
&format!(
"(call $sha512_half (i32.const {ptr}) (i32.const {len})
(i32.const 0) (i32.const {HASH_LEN}))"
),
);
assert_eq!(
status(&wat, &host),
code(HostError::PointerOutOfBounds),
"ptr {ptr} len {len}"
);
assert!(host.digested.borrow().is_empty(), "the host is not called");
}
}
#[test]
fn a_negative_input_pointer_or_length_is_refused() {
let host = FakeHost::new();
for (ptr, len) in [(-1, 1), (0, -1), (i32::MIN, 1)] {
let wat = module(
&[import::SHA512_HALF, ONE_PAGE],
&format!(
"(call $sha512_half (i32.const {ptr}) (i32.const {len})
(i32.const 0) (i32.const {HASH_LEN}))"
),
);
assert_eq!(
status(&wat, &host),
code(HostError::InvalidParams),
"ptr {ptr} len {len}"
);
}
}
/// The field cap bounds what the guest may hand *in*, too.
#[test]
fn an_input_past_the_field_cap_is_refused() {
let host = FakeHost::new();
let digest = |len: i64| {
module(
&[import::SHA512_HALF, ONE_PAGE],
&format!(
"(call $sha512_half (i32.const 0) (i32.const {len})
(i32.const 2048) (i32.const {HASH_LEN}))"
),
)
};
assert_eq!(
status(&digest(OVER_CAP), &host),
code(HostError::DataFieldTooLarge)
);
assert!(host.digested.borrow().is_empty());
assert_eq!(
status(&digest(CAP), &host),
HASH_LEN as i32,
"the cap itself is allowed"
);
}
/// The two directions check in opposite orders: an input's length is known before
/// the read, so the cap comes first, while an output's region has to be resolved
/// before the host can produce a value, so bounds come first there.
#[test]
fn the_field_cap_precedes_the_bounds_check_on_an_input() {
let host = FakeHost::new();
let reading = module(
&[import::SHA512_HALF, ONE_PAGE],
&format!(
"(call $sha512_half (i32.const 0) (i32.const {})
(i32.const 0) (i32.const {HASH_LEN}))",
PAGE + 1
),
);
assert_eq!(status(&reading, &host), code(HostError::DataFieldTooLarge));
let writing = module(
&[import::LDGR_INDEX, ONE_PAGE],
&format!("(call $ldgr_index (i32.const 0) (i32.const {}))", PAGE + 1),
);
assert_eq!(status(&writing, &host), code(HostError::PointerOutOfBounds));
}
// ---------------------------------------------------------------------------
// The reader with no result (`read_borrowed`, via `trace`)
// ---------------------------------------------------------------------------
/// `trace` reads two regions and either one being bad refuses the call. The same
/// rule as above, and the guest is told nothing: the refusal is the host not being
/// called, and the run carries on to the constant that follows.
#[test]
fn both_of_traces_regions_are_checked_silently() {
let host = FakeHost::new();
let regions = [
(
format!("(i32.const {PAGE}) (i32.const 1)"),
EMPTY_REGION.to_owned(),
),
(
EMPTY_REGION.to_owned(),
format!("(i32.const {PAGE}) (i32.const 1)"),
),
(
EMPTY_REGION.to_owned(),
format!("(i32.const 0) (i32.const {OVER_CAP})"),
),
(
"(i32.const -1) (i32.const 1)".to_owned(),
EMPTY_REGION.to_owned(),
),
];
for (msg, data) in regions {
let wat = module(
&[import::TRACE, ONE_PAGE],
&traced(TraceDataType::AsHex, &msg, &data),
);
assert_eq!(status(&wat, &host), COMPLETED, "msg {msg} data {data}");
assert!(
host.traces().is_empty(),
"msg {msg} data {data}: the host must not be called"
);
}
}
// ---------------------------------------------------------------------------
// Both at once (`write_buffered`, via `sha512_half`)
// ---------------------------------------------------------------------------
/// A call with an input and an output region decides everything about the input
/// before anything about the output, so a bad input is reported however the output
/// region is wrong — out of bounds, or a pointer that is not one at all.
///
/// The whole output region, params included, is judged after the host has answered.
/// Hoisting any part of that above the call would put the output's verdict first for
/// these cases, and there is no half of it that can be hoisted on a principle the
/// other half shares.
#[test]
fn a_read_write_checks_its_input_before_its_output() {
let host = FakeHost::new();
let digest = |src: i64, src_len: i64, dst: i64| {
module(
&[import::SHA512_HALF, ONE_PAGE],
&format!(
"(call $sha512_half (i32.const {src}) (i32.const {src_len})
(i32.const {dst}) (i32.const {HASH_LEN}))"
),
)
};
let over_cap = digest(0, OVER_CAP, 0);
assert_eq!(status(&over_cap, &host), code(HostError::DataFieldTooLarge));
let out_of_bounds = digest(PAGE, 4, 0);
assert_eq!(
status(&out_of_bounds, &host),
code(HostError::PointerOutOfBounds)
);
// A bad input against each way the output can be wrong: the input's verdict is
// the one reported, and the host is never asked for a value nobody can take.
for dst in [PAGE, -1] {
let both_bad = digest(0, OVER_CAP, dst);
assert_eq!(
status(&both_bad, &host),
code(HostError::DataFieldTooLarge),
"dst {dst}"
);
}
assert!(host.digested.borrow().is_empty(), "the host is not reached");
}
/// The output half of a read-write call obeys the same rules as a plain write.
#[test]
fn a_read_write_output_obeys_the_write_rules() {
let host = FakeHost::new().answering_digest(Answer::filler(32));
let wat = module(
&[import::SHA512_HALF, ONE_PAGE],
"(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 31))",
);
assert_eq!(status(&wat, &host), code(HostError::BufferTooSmall));
let wat = module(
&[import::SHA512_HALF, ONE_PAGE],
&format!(
"(call $sha512_half (i32.const 0) (i32.const 4) (i32.const {PAGE}) (i32.const 32))"
),
);
assert_eq!(status(&wat, &host), code(HostError::PointerOutOfBounds));
}
/// A refused value reaches guest memory in no part, however much of it the host
/// wrote. The host answers with 32 bytes it did write and a length it did not, so
/// the refusal happens with the value sitting in the run's output buffer — and the
/// guest's buffer has to come back untouched.
///
/// Stronger than the contract asks for: a guest must not read its buffer on a
/// negative status. It holds because the buffer is copied to the guest only after
/// the length, the bounds, the fit and the budget have all passed, so there is no
/// window in which a refused value is in guest memory.
#[test]
fn a_refused_value_leaves_nothing_in_guest_memory() {
const MARKER: u8 = 77;
// The two refusals a value can meet after the host has produced it: longer
// than the field cap, and longer than the buffer the guest offered.
let refusals = [
(MAX_FIELD_BYTES + 1, HASH_LEN, HostError::DataFieldTooLarge),
(HASH_LEN, HASH_LEN - 1, HostError::BufferTooSmall),
];
for (claimed, cap, expected) in refusals {
let host =
FakeHost::new().answering_digest(Answer::writing_but_claiming([MARKER; 32], claimed));
let call = format!(
"(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 64) (i32.const {cap}))"
);
let refused = module(&[import::SHA512_HALF, ONE_PAGE], &call);
assert_eq!(
status(&refused, &host),
code(expected),
"claiming {claimed}"
);
// The same call, reporting what is at the output region afterwards.
let inspect = module(
&[import::SHA512_HALF, ONE_PAGE],
&format!("(drop {call}) (i32.load8_u (i32.const 64))"),
);
assert_eq!(
status(&inspect, &host),
0,
"claiming {claimed}: the refused value must not have been written"
);
}
}
/// An input region may overlap the output region: the host is served the input as
/// it stands and its answer lands afterwards, so the two cannot interfere. The
/// marker is any byte distinct from the input's first (`a`), so `finish` returning
/// it proves the write landed.
#[test]
fn an_input_may_overlap_the_output() {
const MARKER: u8 = 99;
let host = FakeHost::new().answering_digest(Answer::bytes([MARKER; HASH_LEN]));
let wat = module(
&[
import::SHA512_HALF,
ONE_PAGE,
r#"(data (i32.const 0) "abcd")"#,
],
&format!(
"(drop (call $sha512_half (i32.const 0) (i32.const 4)
(i32.const 0) (i32.const {HASH_LEN})))
(i32.load8_u (i32.const 0))"
),
);
assert_eq!(
status(&wat, &host),
i32::from(MARKER),
"the output overwrote the input"
);
assert_eq!(
*host.digested.borrow(),
vec![b"abcd".to_vec()],
"the host saw the input as it was"
);
}
// ---------------------------------------------------------------------------
// The memory export itself
// ---------------------------------------------------------------------------
/// A host call with no memory to work in ends the run instead of answering the
/// guest: there is no buffer for a status to describe, and nothing the guest could
/// do about the answer — which is what puts this beside out-of-gas on the fatal
/// channel. What the guest burned getting there is still charged.
fn assert_no_memory(wat: &str, host: &FakeHost) {
let failure = failure(wat, host);
assert!(
matches!(failure.error, RunError::NoMemory),
"expected the run to end for want of a memory export, got: {failure}"
);
assert!(failure.fuel_used > 0, "{failure}");
}
/// Every region is relative to the guest's exported memory, so a module without
/// one cannot make a host call at all.
#[test]
fn a_module_that_exports_no_memory_cannot_call_the_host() {
let host = FakeHost::new();
let wat = module(
&[import::LDGR_INDEX, "(memory 1)"],
"(call $ldgr_index (i32.const 0) (i32.const 4))",
);
assert_no_memory(&wat, &host);
}
/// Having no memory is answered before anything about a call's arguments, so a
/// module without one ends the run even when its arguments would have earned a
/// guest-visible code of their own (here an input over the field cap).
///
/// The order is deliberate: no memory is a fact about the instance, not about this
/// call, and a region cannot be validated against a memory that is not there. It
/// costs the guest nothing — every call such a module makes ends the run anyway.
#[test]
fn no_memory_is_answered_before_a_calls_arguments_are() {
let host = FakeHost::new();
let wat = module(
&[import::SHA512_HALF, "(memory 1)"],
&format!(
"(call $sha512_half (i32.const 0) (i32.const {OVER_CAP})
(i32.const 0) (i32.const {HASH_LEN}))"
),
);
assert_no_memory(&wat, &host);
}
/// The memory's export *name* is not part of the contract: the engine takes the
/// module's memory whatever it is called. Nothing in the wasm spec attaches meaning
/// to `"memory"` — it is a toolchain convention, so the kind decides.
#[test]
fn a_memory_exported_under_any_name_is_the_guests_memory() {
let host = FakeHost::new();
for name in ["mem", "linear", "the memory"] {
let wat = module(
&[
import::LDGR_INDEX,
&format!(r#"(memory (export "{name}") 1)"#),
],
"(drop (call $ldgr_index (i32.const 64) (i32.const 4)))
(i32.load (i32.const 64))",
);
assert_eq!(
status(&wat, &host),
7,
"the host wrote into the memory exported as '{name}'"
);
}
}
/// One memory exported under several names is one memory. The engine resolves the
/// first export of kind memory, and with at most one memory per module every such
/// export is that memory, so the order the exports are walked in cannot change the
/// answer.
#[test]
fn one_memory_exported_under_several_names_is_still_that_memory() {
let host = FakeHost::new();
let wat = module(
&[
import::LDGR_INDEX,
r#"(memory (export "memory") (export "mem") (export "linear") 1)"#,
],
"(drop (call $ldgr_index (i32.const 64) (i32.const 4)))
(i32.load (i32.const 64))",
);
assert_eq!(status(&wat, &host), 7);
}
/// The export has to *be* a memory: a global named `memory` is not one, and it
/// neither serves as the guest's memory nor hides the memory the module really
/// exports. The kind decides, so the conventional name carries no weight on
/// either side.
#[test]
fn an_export_named_memory_that_is_not_a_memory_is_not_the_guests_memory() {
let host = FakeHost::new();
let call = "(call $ldgr_index (i32.const 0) (i32.const 4))";
let wrong_kind = module(
&[
import::LDGR_INDEX,
"(memory 1)",
r#"(global (export "memory") i32 (i32.const 0))"#,
],
call,
);
assert_no_memory(&wrong_kind, &host);
let shadowed = module(
&[
import::LDGR_INDEX,
r#"(memory (export "mem") 1)"#,
r#"(global (export "memory") i32 (i32.const 0))"#,
],
call,
);
assert_eq!(
status(&shadowed, &host),
4,
"the real memory is found past the global that took its name"
);
}
/// Bounds follow the memory the module actually declared, not a fixed page.
#[test]
fn bounds_follow_the_declared_memory_size() {
let host = FakeHost::new();
let wat = module(
&[import::LDGR_INDEX, r#"(memory (export "memory") 2)"#],
&format!("(call $ldgr_index (i32.const {PAGE}) (i32.const 4))"),
);
assert_eq!(status(&wat, &host), 4, "the second page is in bounds");
}

View File

@@ -1,595 +0,0 @@
//! What screening refuses, and that it refuses nothing a run would have served.
//!
//! `check` reaches its verdict from the compiled module alone, so these tests take
//! no host — except the ones that put the same module through `run` to compare the
//! two.
mod support;
use support::{ENTRY, FakeHost, ONE_PAGE, PLENTY_OF_GAS, assemble, import, module};
use xrpl_host_functions::HostFunctionSpec;
use xrpl_wasm_vm::{CheckError, MAX_MEMORY_PAGES, MAX_TABLE_ELEMENTS, RunError};
/// Assert which stage screening refused a module at, because the caller maps the
/// stages separately. The error comes back out for the tests that also read its
/// message.
macro_rules! assert_stage {
($refusal:expr, $stage:pat) => {{
let refusal = $refusal;
assert!(
matches!(refusal, $stage),
concat!("expected a ", stringify!($stage), " refusal, got: {}"),
refusal
);
refusal
}};
}
/// Screens `wat`, which must assemble.
fn check(wat: &str) -> Result<(), CheckError> {
xrpl_wasm_vm::check(&assemble(wat), ENTRY)
}
fn refusal(wat: &str) -> CheckError {
check(wat).expect_err(&format!("expected this module to be refused:\n{wat}"))
}
fn passes(wat: &str) {
if let Err(refusal) = check(wat) {
panic!("expected this module to pass, but: {refusal}\n{wat}");
}
}
// ---------------------------------------------------------------------------
// Compiling
// ---------------------------------------------------------------------------
/// A contract that imports a host function, exports its memory and exports the
/// entry point is what screening is looking for.
#[test]
fn a_runnable_contract_passes() {
passes(&module(
&[import::LDGR_INDEX, ONE_PAGE],
"(call $ldgr_index (i32.const 0) (i32.const 4))",
));
}
/// Bytes that are not a wasm module at all.
#[test]
fn garbage_does_not_pass() {
for bytes in [b"".as_slice(), b"not wasm", &[0x00, 0x61, 0x73, 0x6d]] {
let refusal = xrpl_wasm_vm::check(bytes, ENTRY).expect_err("garbage must not pass");
assert_stage!(refusal, CheckError::Compile(_));
}
}
/// Screening takes wasm binaries, and text is not one — the same rule the VM
/// applies, from the same `wasmi` built without its `wat` feature. Turning that
/// feature on would make this transaction blob valid at both ends.
#[test]
fn a_text_format_module_does_not_pass() {
let text = module(&[ONE_PAGE], "(i32.const 0)");
let refusal =
xrpl_wasm_vm::check(text.as_bytes(), ENTRY).expect_err("text must not pass as a module");
assert_stage!(refusal, CheckError::Compile(_));
// The same module, assembled first, passes: the text is sound and only the
// format was refused.
passes(&text);
}
/// A feature the engine disables is refused here too, because both stages compile
/// against the one engine. `vm_limits.rs` walks every disabled feature; this pins
/// that screening sees the same configuration.
#[test]
fn a_disabled_feature_does_not_pass() {
let refusal = refusal(&module(
&[ONE_PAGE],
"(drop (f64.add (f64.const 1) (f64.const 2))) (i32.const 0)",
));
let refusal = assert_stage!(refusal, CheckError::Compile(_)).to_string();
assert!(refusal.contains("floating-point"), "{refusal}");
}
// ---------------------------------------------------------------------------
// Imports
// ---------------------------------------------------------------------------
/// Every host function the ABI declares, spelled as a guest imports it. The count
/// is asserted against the ABI so a function added to it cannot be left out here.
const ALL_IMPORTS: [&str; 60] = [
import::LDGR_INDEX,
import::PARENT_LDGR_TIME,
import::PARENT_LDGR_HASH,
import::BASE_FEE,
import::AMENDMENT_ENABLED,
import::CACHE_LE,
import::TX_FIELD,
import::HOME_LE_FIELD,
import::LE_FIELD,
import::TX_INNER,
import::HOME_LE_INNER,
import::LE_INNER,
import::TX_ARR_LEN,
import::HOME_LE_ARR_LEN,
import::LE_ARR_LEN,
import::TX_INNER_ARR_LEN,
import::HOME_LE_INNER_ARR_LEN,
import::LE_INNER_ARR_LEN,
import::CHECK_SIG,
import::ACCOUNTROOT_ID,
import::AMM_ID,
import::CHECK_ID,
import::CREDENTIAL_ID,
import::DELEGATE_ID,
import::DEPOSIT_PREAUTH_ID,
import::DID_ID,
import::ESCROW_ID,
import::TRUSTLINE_ID,
import::MPT_ISSUANCE_ID,
import::MPTOKEN_ID,
import::NFT_OFFER_ID,
import::OFFER_ID,
import::ORACLE_ID,
import::PAYCHAN_ID,
import::PERMISSIONED_DOMAIN_ID,
import::SIGNERS_ID,
import::TICKET_ID,
import::VAULT_ID,
import::SHA512_HALF,
import::TRACE,
import::SET_DATA,
import::NFT_URI,
import::NFT_ISSUER,
import::NFT_TAXON,
import::NFT_FLAGS,
import::NFT_XFER_FEE,
import::NFT_SERIAL,
import::FLOAT_FROM_INT,
import::FLOAT_FROM_UINT,
import::FLOAT_FROM_STAMOUNT,
import::FLOAT_FROM_STNUMBER,
import::FLOAT_TO_INT,
import::FLOAT_TO_MANT_EXP,
import::FLOAT_FROM_MANT_EXP,
import::FLOAT_CMP,
import::FLOAT_ADD,
import::FLOAT_SUB,
import::FLOAT_MULT,
import::FLOAT_DIV,
import::FLOAT_POW,
];
#[test]
fn every_declared_host_function_may_be_imported() {
assert_eq!(
ALL_IMPORTS.len(),
HostFunctionSpec::ALL.len(),
"the ABI gained a host function with no import declaration in this test"
);
let mut parts = ALL_IMPORTS.to_vec();
parts.push(ONE_PAGE);
passes(&module(&parts, "(i32.const 0)"));
}
/// A module may import fewer host functions than are registered, but not more.
#[test]
fn an_unknown_host_function_does_not_pass() {
let refusal = refusal(&module(
&[
r#"(import "host_lib" "no_such_function" (func $f (param i32) (result i32)))"#,
ONE_PAGE,
],
"(call $f (i32.const 0))",
));
let refusal = assert_stage!(refusal, CheckError::Import(_)).to_string();
assert!(
refusal.contains("no host function 'no_such_function'"),
"{refusal}"
);
}
/// Host functions live under one module name — `host_lib` — and an import naming
/// another is refused even when the function name is real. `env` is in the list
/// because that is what plain clang emits.
#[test]
fn an_import_from_another_module_does_not_pass() {
for module_name in ["host", "env", ""] {
let refusal = refusal(&module(
&[
&format!(
r#"(import "{module_name}" "ldgr_index" (func $f (param i32 i32) (result i32)))"#
),
ONE_PAGE,
],
"(call $f (i32.const 0) (i32.const 4))",
));
let refusal = assert_stage!(refusal, CheckError::Import(_)).to_string();
assert!(refusal.contains("is not from 'host_lib'"), "{refusal}");
}
}
/// A host function's name imported as something other than a function. The engine
/// defines it as a function and nothing else, so this does not link either.
#[test]
fn a_host_function_imported_as_a_global_does_not_pass() {
let refusal = refusal(&module(
&[
r#"(import "host_lib" "ldgr_index" (global $g i32))"#,
ONE_PAGE,
],
"(global.get $g)",
));
let refusal = assert_stage!(refusal, CheckError::Import(_)).to_string();
assert!(
refusal.contains("'host_lib::ldgr_index' is not a function"),
"{refusal}"
);
}
/// A module faulty at two stages is refused by the earlier one — it imports what no
/// engine serves *and* exports no entry point. The imports are what the rest of the
/// module depends on, so that is the message worth having.
#[test]
fn the_earlier_stage_is_the_one_reported() {
let refusal = refusal(
r#"(module
(import "host_lib" "no_such_function" (func $f (result i32)))
(memory (export "memory") 1)
(func (export "not_the_entry_point") (result i32) (call $f)))"#,
);
assert_stage!(refusal, CheckError::Import(_));
}
/// The signature is the one part of an import screening does not compare, so a
/// module that will not link can still pass. Recorded here because it is the gap
/// this stage leaves, not because it is wanted.
#[test]
fn an_import_with_the_wrong_signature_still_passes() {
let wat = module(
&[
r#"(import "host_lib" "ldgr_index" (func $f (param i64 i64) (result i32)))"#,
ONE_PAGE,
],
"(i32.const 0)",
);
passes(&wat);
let host = FakeHost::new();
let failure = xrpl_wasm_vm::run(&assemble(&wat), PLENTY_OF_GAS, &host, ENTRY)
.expect_err("a mistyped import must not link");
assert!(
matches!(failure.error, RunError::Instantiate(_)),
"{failure}"
);
}
// ---------------------------------------------------------------------------
// The entry point
// ---------------------------------------------------------------------------
#[test]
fn a_missing_entry_point_does_not_pass() {
let refusal = refusal(
r#"(module (memory (export "memory") 1)
(func (export "other") (result i32) (i32.const 0)))"#,
);
let refusal = assert_stage!(refusal, CheckError::EntryPoint(_)).to_string();
assert_eq!(refusal, "no entry point 'finish'");
}
/// The entry point is looked up by the name the caller asks for, as a run looks it
/// up: screening a contract for one entry point says nothing about another.
#[test]
fn the_entry_point_is_the_name_the_caller_gives() {
let wasm = assemble(
r#"(module (memory (export "memory") 1)
(func (export "other") (result i32) (i32.const 0)))"#,
);
assert!(xrpl_wasm_vm::check(&wasm, "other").is_ok());
assert!(xrpl_wasm_vm::check(&wasm, ENTRY).is_err());
}
/// Both halves of the entry point's type are screened: a module returning the
/// wrong thing, or taking anything at all, would fail the run's typed lookup.
#[test]
fn an_entry_point_of_the_wrong_type_does_not_pass() {
for (signature, body) in [
("(result i64)", "(i64.const 0)"),
("(param i32) (result i32)", "(i32.const 0)"),
("", "(nop)"),
] {
let refusal = refusal(&format!(
r#"(module (memory (export "memory") 1)
(func (export "finish") {signature} {body}))"#
));
let refusal = assert_stage!(refusal, CheckError::EntryPoint(_)).to_string();
assert_eq!(
refusal, "entry point 'finish' has the wrong signature, expected '() -> i32'",
"{signature}"
);
}
}
/// An export of the entry point's name that is not a function at all is a third
/// case, and named as such: nothing is missing and no signature is wrong.
#[test]
fn an_entry_point_that_is_not_a_function_does_not_pass() {
let refusal = refusal(
r#"(module (memory (export "memory") 1) (global (export "finish") i32 (i32.const 0)))"#,
);
let refusal = assert_stage!(refusal, CheckError::EntryPoint(_)).to_string();
assert_eq!(refusal, "export 'finish' is not a function");
}
// ---------------------------------------------------------------------------
// Agreement with a run
// ---------------------------------------------------------------------------
/// A module with no linear memory to export passes. A contract that makes no host
/// call needs none, and one that does is refused at the call and charged — a
/// runtime fault, not a malformed module.
#[test]
fn a_module_exporting_no_memory_passes() {
let wat = r#"(module (func (export "finish") (result i32) (i32.const 0)))"#;
passes(wat);
let host = FakeHost::new();
assert_eq!(
xrpl_wasm_vm::run(&assemble(wat), PLENTY_OF_GAS, &host, ENTRY)
.expect("a module that calls no host function needs no memory")
.result,
0
);
}
/// Modules spanning what screening decides, each also put through a run.
fn modules() -> Vec<(&'static str, String)> {
vec![
(
"a runnable contract",
module(&[import::LDGR_INDEX, ONE_PAGE], "(i32.const 0)"),
),
(
"a contract that traps",
module(&[ONE_PAGE], "(unreachable)"),
),
(
"a disabled feature",
module(&[ONE_PAGE], "(i32.extend8_s (i32.const 1))"),
),
(
"an unknown host function",
module(
&[
r#"(import "host_lib" "nope" (func $f (result i32)))"#,
ONE_PAGE,
],
"(call $f)",
),
),
(
"an import from another module",
module(
&[
r#"(import "env" "ldgr_index" (func $f (param i32 i32) (result i32)))"#,
ONE_PAGE,
],
"(i32.const 0)",
),
),
(
"a host function imported as a global",
module(
&[r#"(import "host_lib" "trace" (global $g i32))"#, ONE_PAGE],
"(global.get $g)",
),
),
(
"no entry point",
r#"(module (memory (export "memory") 1)
(func (export "other") (result i32) (i32.const 0)))"#
.to_string(),
),
(
"an entry point of the wrong type",
r#"(module (memory (export "memory") 1)
(func (export "finish") (result i64) (i64.const 0)))"#
.to_string(),
),
]
}
/// Screening refuses a module exactly when a run would refuse it at one of the
/// three stages screening covers — nothing it rejects would have run, and nothing
/// it passes stops before the entry point is called. The exceptions are the ones
/// [`what_static_screening_cannot_see`] lists.
#[test]
fn screening_and_a_run_agree() {
let host = FakeHost::new();
for (label, wat) in modules() {
let wasm = assemble(&wat);
let refused_early = match xrpl_wasm_vm::run(&wasm, PLENTY_OF_GAS, &host, ENTRY) {
Err(failure) => matches!(
failure.error,
RunError::Compile(_) | RunError::Instantiate(_) | RunError::EntryPoint(_)
),
Ok(_) => false,
};
assert_eq!(
xrpl_wasm_vm::check(&wasm, ENTRY).is_err(),
refused_early,
"{label}"
);
}
}
/// A module asking for more memory than the engine grants is refused, so the
/// contract that could never run does not reach the ledger. The cap itself passes.
#[test]
fn an_exported_memory_past_the_cap_does_not_pass() {
let wat = module(
&[&format!(
r#"(memory (export "memory") {})"#,
MAX_MEMORY_PAGES + 1
)],
"(i32.const 0)",
);
let refusal = assert_stage!(refusal(&wat), CheckError::Memory(_)).to_string();
assert!(refusal.contains("past the 128-page cap"), "{refusal}");
passes(&module(
&[&format!(r#"(memory (export "memory") {MAX_MEMORY_PAGES})"#)],
"(i32.const 0)",
));
}
/// A declared *maximum* past the cap is legal and simply unreachable, so screening
/// must not turn it away: `vm_limits` runs this very module to completion.
#[test]
fn a_declared_maximum_past_the_cap_still_passes() {
passes(&module(
&[&format!(
r#"(memory (export "memory") 1 {})"#,
MAX_MEMORY_PAGES + 1
)],
"(i32.const 0)",
));
}
/// A module asking for more table than the engine grants is refused for the same
/// reason a memory is. The cap itself passes.
#[test]
fn an_exported_table_past_the_cap_does_not_pass() {
let wat = module(
&[&format!(
r#"(table (export "t") {} funcref)"#,
MAX_TABLE_ELEMENTS + 1
)],
"(i32.const 0)",
);
let refusal = assert_stage!(refusal(&wat), CheckError::Table(_)).to_string();
assert!(refusal.contains("past the 1024-element cap"), "{refusal}");
passes(&module(
&[&format!(
r#"(table (export "t") {MAX_TABLE_ELEMENTS} funcref)"#
)],
"(i32.const 0)",
));
}
/// Both caps are applied in one pass over the exports, so neither may end the walk
/// early: a passing memory must not hide a failing table declared after it, and a
/// passing table must not hide a failing memory.
#[test]
fn one_pass_screens_both_resources() {
let after_a_passing_memory = refusal(&module(
&[
ONE_PAGE,
&format!(r#"(table (export "t") {} funcref)"#, MAX_TABLE_ELEMENTS + 1),
],
"(i32.const 0)",
));
assert_stage!(after_a_passing_memory, CheckError::Table(_));
let after_a_passing_table = refusal(&module(
&[
r#"(table (export "t") 1 funcref)"#,
&format!(r#"(memory (export "memory") {})"#, MAX_MEMORY_PAGES + 1),
],
"(i32.const 0)",
));
assert_stage!(after_a_passing_table, CheckError::Memory(_));
}
/// As with memory, a declared *maximum* past the cap is unreachable rather than
/// wrong: `vm_limits` runs this very module to completion.
#[test]
fn a_declared_table_maximum_past_the_cap_still_passes() {
passes(&module(
&[&format!(
r#"(table (export "t") 1 {} funcref)"#,
MAX_TABLE_ELEMENTS + 1
)],
"(i32.const 0)",
));
}
/// The gap, listed rather than described. A memory or a table a module keeps to
/// itself is not in its exports, so these are the modules that pass screening and
/// then fail to *instantiate* — which is why a run's refusal at that stage cannot be
/// read as the node's fault.
///
/// The two entries are not equally remote. A contract needs an exported memory to
/// make any host call, so the memory row can do nothing but compute and the SDK does
/// not produce one. A table, though, is *normally* unexported — Rust exports
/// `__indirect_function_table` only under `--export-table` — so the table row is the
/// shape a hostile module actually takes, and the store's limiter is the only thing
/// standing in front of it.
#[test]
fn what_static_screening_cannot_see() {
let host = FakeHost::new();
for (label, declaration) in [
("memory", format!("(memory {})", MAX_MEMORY_PAGES + 1)),
(
"table",
format!("(table {} funcref)", MAX_TABLE_ELEMENTS + 1),
),
] {
let wat = format!(
r#"(module {declaration}
(func (export "finish") (result i32) (i32.const 0)))"#
);
passes(&wat);
let failure = match xrpl_wasm_vm::run(&assemble(&wat), PLENTY_OF_GAS, &host, ENTRY) {
Err(failure) => failure,
Ok(outcome) => panic!(
"the store's limiter must refuse the {label}, but the module returned {}",
outcome.result
),
};
assert!(
matches!(failure.error, RunError::Instantiate(_)),
"{label}: {failure}"
);
}
}
/// A start section runs guest code at instantiation, before the entry point. The
/// engine disallows it, so screening refuses the module outright rather than letting
/// any code run ahead of the entry point.
#[test]
fn a_start_section_is_refused_by_screening() {
let wat = format!(
r#"(module {ONE_PAGE}
(func $init (unreachable))
(start $init)
(func (export "finish") (result i32) (i32.const 0)))"#
);
let refusal = assert_stage!(refusal(&wat), CheckError::Compile(_)).to_string();
assert!(refusal.contains("start"), "{refusal}");
}
#[test]
fn a_memory64_memory_is_refused_by_screening() {
let wat = r#"(module
(memory i64 1)
(func (export "finish") (result i32) (i32.const 0)))"#;
let refusal = assert_stage!(refusal(wat), CheckError::Compile(_)).to_string();
assert!(
refusal.contains("memory64") || refusal.contains("i64"),
"{refusal}"
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,600 +0,0 @@
//! What the engine refuses outright: modules it will not compile, will not
//! instantiate, or cannot find an entry point in — plus the memory and table caps.
//!
//! These are the sandbox's outer wall. Everything here fails the run rather than
//! returning a code to the guest, so each test reads the failure's message.
mod support;
use support::{
FakeHost, ONE_PAGE, PLENTY_OF_GAS, failure, import, module, run, run_entry, run_with_gas,
};
use xrpl_wasm_vm::{MAX_MEMORY_PAGES, MAX_TABLE_ELEMENTS, RunError};
/// Assert which stage a run failed at, because the caller maps the stages to
/// different outcomes. A stage is one `RunError` variant, so the expectation is a
/// pattern; the failure comes back out for the tests that also read its message.
macro_rules! assert_stage {
($failure:expr, $stage:pat) => {{
let failure = $failure;
assert!(
matches!(failure.error, $stage),
concat!("expected a ", stringify!($stage), " failure, got: {}"),
failure
);
failure
}};
}
// ---------------------------------------------------------------------------
// Linear memory
// ---------------------------------------------------------------------------
/// A module declaring more than the cap fails to instantiate — the limit applies
/// to the initial memory, not only to growth.
#[test]
fn an_initial_memory_past_the_cap_is_refused() {
let host = FakeHost::new();
let wat = module(
&[&format!(
r#"(memory (export "memory") {})"#,
MAX_MEMORY_PAGES + 1
)],
"(i32.const 0)",
);
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
}
/// The cap itself is allowed.
#[test]
fn an_initial_memory_at_the_cap_is_allowed() {
let host = FakeHost::new();
let wat = module(
&[&format!(r#"(memory (export "memory") {MAX_MEMORY_PAGES})"#)],
"(i32.const 0)",
);
assert_eq!(run(&wat, &host).expect("should run").result, 0);
}
/// Growth up to the cap succeeds; growth past it traps rather than answering -1 as
/// `memory.grow` otherwise would, because the engine's limiter sets
/// `trap_on_grow_failure(true)`.
#[test]
fn growth_stops_at_the_cap() {
let host = FakeHost::new();
let wat = module(
&[ONE_PAGE],
&format!("(memory.grow (i32.const {}))", MAX_MEMORY_PAGES - 1),
);
assert_eq!(
run(&wat, &host).expect("should run").result,
1,
"growing to exactly the cap answers the previous size"
);
let wat = module(
&[ONE_PAGE],
&format!("(memory.grow (i32.const {MAX_MEMORY_PAGES}))"),
);
assert_stage!(failure(&wat, &host), RunError::Trap(_));
}
/// A module may declare a maximum above the cap: the cap is enforced on the initial
/// memory and on growth, not on the memory type's declared bound.
#[test]
fn a_declared_maximum_past_the_cap_is_allowed_but_unreachable() {
let host = FakeHost::new();
let memory = format!(r#"(memory (export "memory") 1 {})"#, MAX_MEMORY_PAGES + 1);
let wat = module(&[&memory], "(i32.const 0)");
assert_eq!(run(&wat, &host).expect("should run").result, 0);
let wat = module(
&[&memory],
&format!("(memory.grow (i32.const {MAX_MEMORY_PAGES}))"),
);
assert_stage!(failure(&wat, &host), RunError::Trap(_));
}
// ---------------------------------------------------------------------------
// Tables
// ---------------------------------------------------------------------------
/// A table's whole cost is paid at instantiation: wasmi writes all 8 bytes of every
/// element before the guest's first instruction, so a module declaring more than the
/// cap must be refused there rather than charged for it.
#[test]
fn an_initial_table_past_the_cap_is_refused() {
let host = FakeHost::new();
let wat = module(
&[&format!("(table {} funcref)", MAX_TABLE_ELEMENTS + 1)],
"(i32.const 0)",
);
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
}
/// The cap itself is allowed.
#[test]
fn an_initial_table_at_the_cap_is_allowed() {
let host = FakeHost::new();
let wat = module(
&[&format!("(table {MAX_TABLE_ELEMENTS} funcref)")],
"(i32.const 0)",
);
assert_eq!(run(&wat, &host).expect("should run").result, 0);
}
/// The cap binds a table the module keeps to itself, which is the case that matters:
/// a contract has no reason to export its table, so screening never sees the one a
/// hostile module declares.
#[test]
fn the_table_cap_binds_an_unexported_table() {
let host = FakeHost::new();
let wat = module(
&[&format!("(table {} funcref)", u32::from(u16::MAX) * 100)],
"(i32.const 0)",
);
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
}
/// A declared *maximum* past the cap is legal and simply unreachable, mirroring what
/// linear memory allows. Nothing can reach it: `table.grow` is a reference-types
/// instruction and the engine turns that feature off, so a table's declared minimum
/// is also its final size.
#[test]
fn a_declared_table_maximum_past_the_cap_is_allowed_but_unreachable() {
let host = FakeHost::new();
let wat = module(
&[&format!(
"(table 1 {} funcref)",
u64::try_from(MAX_TABLE_ELEMENTS).expect("fits") + 1
)],
"(i32.const 0)",
);
assert_eq!(run(&wat, &host).expect("should run").result, 0);
}
// ---------------------------------------------------------------------------
// Engine configuration
// ---------------------------------------------------------------------------
/// One row per feature `build_wasm_engine` turns off: the smallest module that uses
/// it, and the fragment of wasmi's refusal that names the feature. A row declaring
/// its own memory omits [`ONE_PAGE`], or it is refused for having two memories
/// instead.
fn disabled_features() -> Vec<(&'static str, Vec<&'static str>, &'static str, &'static str)> {
vec![
(
"wasm_multi_value",
vec![
ONE_PAGE,
"(func $two (result i32 i32) (i32.const 1) (i32.const 2))",
],
"(call $two) (drop) (drop) (i32.const 0)",
"multi-value",
),
(
"wasm_sign_extension",
vec![ONE_PAGE],
"(i32.extend8_s (i32.const 1))",
"sign extension",
),
(
"wasm_bulk_memory",
vec![ONE_PAGE],
"(memory.fill (i32.const 0) (i32.const 0) (i32.const 1)) (i32.const 0)",
"bulk memory",
),
(
"wasm_reference_types",
vec![ONE_PAGE, "(table 1 externref)"],
"(i32.const 0)",
"reference types",
),
// The proposal covers mutable globals crossing the module boundary; an
// internal one is core wasm and stays allowed — see the test below.
(
"wasm_mutable_global",
vec![ONE_PAGE, r#"(global (export "g") (mut i32) (i32.const 0))"#],
"(i32.const 0)",
"mutable global",
),
(
"wasm_tail_call",
vec![ONE_PAGE, "(func $f (result i32) (i32.const 0))"],
"(return_call $f)",
"tail call",
),
// Arithmetic in a constant initialiser. wasmi names the operator rather
// than the proposal here.
(
"wasm_extended_const",
vec![
ONE_PAGE,
"(global $g i32 (i32.add (i32.const 1) (i32.const 2)))",
],
"(global.get $g)",
"non-constant operator",
),
(
"wasm_multi_memory",
vec![ONE_PAGE, "(memory 1)"],
"(i32.const 0)",
"multiple memories",
),
(
"wasm_memory64",
vec![r#"(memory (export "memory") i64 1)"#],
"(i32.const 0)",
"memory64",
),
(
"wasm_custom_page_sizes",
vec![r#"(memory (export "memory") 1 (pagesize 1))"#],
"(i32.const 0)",
"custom page sizes",
),
(
"wasm_wide_arithmetic",
vec![ONE_PAGE],
"(drop (i64.add128 (i64.const 1) (i64.const 2) (i64.const 3) (i64.const 4)))
(i32.const 0)",
"wide arithmetic",
),
// Determinism across nodes is the reason floats are off.
(
"floats",
vec![ONE_PAGE],
"(drop (f64.add (f64.const 1) (f64.const 2))) (i32.const 0)",
"floating-point",
),
]
}
/// Every feature the engine disables is refused, and refused for that reason.
///
/// `wasm_custom_page_sizes` and `wasm_wide_arithmetic` are off by default in wasmi
/// 1.1 (`engine/config.rs:72,74`), so their rows guard against wasmi changing that
/// default rather than against this engine's own config.
#[test]
fn every_disabled_feature_is_refused_by_name() {
let host = FakeHost::new();
for (knob, parts, body, expected) in disabled_features() {
let wat = module(&parts, body);
let failure = assert_stage!(failure(&wat, &host), RunError::Compile(_)).to_string();
assert!(
failure.contains(expected),
"{knob}: expected a refusal mentioning {expected:?}, got: {failure}"
);
}
}
/// The three knobs [`every_disabled_feature_is_refused_by_name`] cannot cover. The
/// engine is a process-wide `LazyLock`, so a test observes the one configuration
/// `build_wasm_engine` makes: a knob masked by another, or with no caller-visible
/// effect, has no distinguishing module.
#[test]
fn the_knobs_without_a_module_of_their_own() {
let host = FakeHost::new();
// `wasm_saturating_float_to_int(false)`: every saturating conversion takes a
// float operand, so `floats(false)` refuses it first, as the message shows.
let wat = module(&[ONE_PAGE], "(i32.trunc_sat_f32_s (f32.const 1))");
let refusal = failure(&wat, &host).to_string();
assert!(refusal.contains("floating-point"), "{refusal}");
assert!(!refusal.contains("saturating"), "{refusal}");
// `ignore_custom_sections(true)`: governs whether wasmi retains custom
// sections, not accept/reject, so this pins only that one is harmless.
let wat = module(
&[ONE_PAGE, r#"(@custom "note" "ignored")"#],
"(i32.const 0)",
);
assert_eq!(run(&wat, &host).expect("should run").result, 0);
// `consume_fuel(true)`: with it off, `Store::set_fuel` fails and `run` returns
// before instantiating, so every test in the suite fails.
let wat = module(&[ONE_PAGE], "(i32.const 0)");
assert!(run(&wat, &host).expect("should run").fuel_used > 0);
}
/// A mutable global the module keeps to itself is core wasm, so the disabled
/// proposal does not reach it: a guest can still have mutable state.
#[test]
fn an_internal_mutable_global_is_still_allowed() {
let host = FakeHost::new();
let wat = module(
&[ONE_PAGE, "(global $g (mut i32) (i32.const 0))"],
"(global.set $g (i32.const 7)) (global.get $g)",
);
assert_eq!(run(&wat, &host).expect("should run").result, 7);
}
/// Bytes that are not a wasm module at all.
#[test]
fn garbage_does_not_compile() {
let host = FakeHost::new();
for bytes in [b"".as_slice(), b"not wasm", &[0x00, 0x61, 0x73, 0x6d]] {
let failure = xrpl_wasm_vm::run(bytes, PLENTY_OF_GAS, &host, support::ENTRY)
.expect_err("garbage must not compile");
assert_stage!(failure, RunError::Compile(_));
}
}
/// The VM takes wasm binaries, and text is not one. wasmi's `wat` feature is on by
/// default and would have `Module::new` assemble text too, so the crate builds
/// wasmi without it; turning it back on would make this transaction blob valid.
#[test]
fn the_vm_refuses_a_text_format_module() {
let host = FakeHost::new();
let text = module(&[ONE_PAGE], "(i32.const 0)");
let failure = xrpl_wasm_vm::run(text.as_bytes(), PLENTY_OF_GAS, &host, support::ENTRY)
.expect_err("text must not compile as a module");
assert_stage!(failure, RunError::Compile(_));
// The same module, assembled first, runs: the text is sound and only the
// format was refused.
assert_eq!(run(&text, &host).expect("should run").result, 0);
}
// ---------------------------------------------------------------------------
// Imports
// ---------------------------------------------------------------------------
/// A module may import fewer host functions than are registered, but not more:
/// an import the linker does not define fails instantiation.
#[test]
fn an_unknown_import_fails_instantiation() {
let host = FakeHost::new();
let wat = module(
&[
r#"(import "host_lib" "no_such_function" (func $f (param i32) (result i32)))"#,
ONE_PAGE,
],
"(call $f (i32.const 0))",
);
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
}
/// Host functions are registered under one module name — `host_lib`, the name the
/// guest SDK and this repo's fixtures import from — and a guest naming a different
/// one does not link. `env` is in the list because that is what plain clang emits.
#[test]
fn the_import_module_name_must_match() {
let host = FakeHost::new();
for module_name in ["host", "env", ""] {
let wat = module(
&[
&format!(
r#"(import "{module_name}" "ldgr_index" (func $f (param i32 i32) (result i32)))"#
),
ONE_PAGE,
],
"(call $f (i32.const 0) (i32.const 4))",
);
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
}
}
/// An import spelled with the wrong signature does not link even under the right
/// name, which is what makes the registered signatures load-bearing.
#[test]
fn an_import_with_the_wrong_signature_fails_instantiation() {
let host = FakeHost::new();
for signature in [
"(param i32) (result i32)", // too few parameters
"(param i32 i32 i32) (result i32)", // too many
"(param i64 i64) (result i32)", // wrong parameter types
"(param i32 i32) (result i64)", // wrong result type
"(param i32 i32)", // no result
] {
let wat = module(
&[
&format!(r#"(import "host_lib" "ldgr_index" (func $f {signature}))"#),
ONE_PAGE,
],
"(i32.const 0)",
);
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
}
}
/// A module that imports a host function it never calls still has to link.
#[test]
fn an_unused_import_is_still_linked() {
let host = FakeHost::new();
let wat = module(
&[import::LDGR_INDEX, import::TRACE, ONE_PAGE],
"(i32.const 0)",
);
assert_eq!(run(&wat, &host).expect("should run").result, 0);
}
// ---------------------------------------------------------------------------
// The start section
// ---------------------------------------------------------------------------
/// The engine disallows start sections, so a module carrying one is rejected at
/// compile and never runs. No guest code executes ahead of the entry point, whatever
/// that code would have done — trap, loop, or call the host — so nothing is metered
/// and no fuel is burned. Screening catches the same module up front
/// (`preflight::a_start_section_is_refused_by_screening`); this pins that `run`
/// refuses it the same way rather than instantiating it.
#[test]
fn a_start_section_module_is_rejected_at_compile() {
let host = FakeHost::new();
let wat = format!(
r#"(module {ONE_PAGE}
(func $init (unreachable))
(start $init)
(func (export "finish") (result i32) (i32.const 0)))"#
);
let failure = assert_stage!(
run_with_gas(&wat, PLENTY_OF_GAS, &host)
.expect_err("a module with a start section must not run"),
RunError::Compile(_)
);
assert_eq!(
failure.fuel_used, 0,
"no guest code runs, so nothing is charged: {failure}"
);
}
/// What `RunError::Instantiate` is left to mean: a module the linker or the store
/// would not accept, rather than one whose guest code failed. Its two shapes, so the
/// variant is not left standing for nothing.
#[test]
fn instantiation_failure_is_a_module_the_engine_will_not_accept() {
let host = FakeHost::new();
// The linker defines no such import.
let wat = module(
&[
r#"(import "host_lib" "no_such_function" (func $f (result i32)))"#,
ONE_PAGE,
],
"(call $f)",
);
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
// The store's limiter will not grant the memory, and does not trap to say so.
let wat = module(
&[&format!("(memory {})", MAX_MEMORY_PAGES + 1)],
"(i32.const 0)",
);
assert_stage!(failure(&wat, &host), RunError::Instantiate(_));
}
// ---------------------------------------------------------------------------
// The entry point
// ---------------------------------------------------------------------------
#[test]
fn a_missing_entry_point_fails() {
let host = FakeHost::new();
let wat = r#"(module (memory (export "memory") 1) (func (export "other") (result i32) (i32.const 0)))"#;
let failure = assert_stage!(
run_with_gas(wat, PLENTY_OF_GAS, &host)
.expect_err("a module without the entry point must not run"),
RunError::EntryPoint(_)
);
assert!(
failure.to_string().contains("no entry point 'finish'"),
"{failure}"
);
}
/// The entry point is looked up by the name the caller asks for.
#[test]
fn the_entry_point_is_the_name_the_caller_gives() {
let host = FakeHost::new();
let wat = r#"(module (memory (export "memory") 1) (func (export "other") (result i32) (i32.const 9)))"#;
let outcome = run_entry(wat, &host, "other").expect("should run");
assert_eq!(outcome.result, 9);
}
/// The entry point must take nothing and return an `i32`. A module that exports the
/// name with another signature is told so, rather than being told the export is
/// missing: wasmi answers both cases with one error, and "no entry point" would send
/// a contract author looking for a function they already have.
#[test]
fn an_entry_point_of_the_wrong_type_fails() {
let host = FakeHost::new();
for signature in ["(result i64)", "(param i32) (result i32)", ""] {
let body = if signature.contains("result i64") {
"(i64.const 0)"
} else if signature.is_empty() {
"(nop)"
} else {
"(i32.const 0)"
};
let wat = format!(
r#"(module (memory (export "memory") 1) (func (export "finish") {signature} {body}))"#
);
let failure = assert_stage!(
run_with_gas(&wat, PLENTY_OF_GAS, &host)
.expect_err("a wrongly-typed entry point must not run"),
RunError::EntryPoint(_)
)
.to_string();
assert!(
failure.contains("entry point 'finish' has the wrong signature"),
"{signature}: {failure}"
);
assert!(
!failure.contains("no entry point"),
"a present export must not be reported as absent — {signature}: {failure}"
);
}
}
/// An export of the entry point's name that is not a function at all is a third
/// case, and named as such: nothing is missing and no signature is wrong.
#[test]
fn an_entry_point_that_is_not_a_function_fails() {
let host = FakeHost::new();
let wat =
r#"(module (memory (export "memory") 1) (global (export "finish") i32 (i32.const 0)))"#;
let failure = assert_stage!(
run_with_gas(wat, PLENTY_OF_GAS, &host).expect_err("a non-function export must not run"),
RunError::EntryPoint(_)
)
.to_string();
assert!(
failure.contains("export 'finish' is not a function"),
"{failure}"
);
}
/// A guest that traps fails the run rather than returning a value.
#[test]
fn a_trapping_guest_fails_the_run() {
let host = FakeHost::new();
let wat = module(&[ONE_PAGE], "(unreachable)");
assert_stage!(failure(&wat, &host), RunError::Trap(_));
// An out-of-bounds guest access is a trap too, caught by the engine rather
// than anything the host is asked about.
let wat = module(&[ONE_PAGE], "(i32.load (i32.const 100000))");
assert_stage!(failure(&wat, &host), RunError::Trap(_));
}
#[test]
fn a_memory64_module_is_rejected_at_compile() {
let host = FakeHost::new();
let wat = r#"(module
(memory i64 1)
(func (export "finish") (result i32) (i32.const 0)))"#;
let failure = assert_stage!(
run_with_gas(wat, PLENTY_OF_GAS, &host)
.expect_err("a module using 64-bit memory must not run"),
RunError::Compile(_)
);
assert_eq!(
failure.fuel_used, 0,
"rejected before instantiation, so nothing is charged: {failure}"
);
}

View File

@@ -1,5 +1,5 @@
Our [build instructions][BUILD.md] assume you have a C++ development
environment complete with Git, Python, Conan, CMake, Rust, and a C++ compiler.
environment complete with Git, Python, Conan, CMake, and a C++ compiler.
This document explains how to set one up.
[BUILD.md]: ../../BUILD.md
@@ -36,17 +36,19 @@ compiler building. Treat support for anything outside the table as best-effort.
Besides a compiler, building `xrpld` requires:
| Tool | Minimum version |
| ------------------------------------------- | ------------------------ |
| [Git](https://git-scm.com/downloads) | any recent |
| [Python](https://www.python.org/downloads/) | 3.11 |
| [Conan](https://conan.io/downloads.html) | 2.17 |
| [CMake](https://cmake.org/download/) | 3.16 |
| [Rust](https://rustup.rs) | 1.95 (see [Rust](#rust)) |
| Tool | Minimum version |
| ------------------------------------------- | --------------- |
| [Git](https://git-scm.com/downloads) | any recent |
| [Python](https://www.python.org/downloads/) | 3.11 |
| [Conan](https://conan.io/downloads.html) | 2.17 |
| [CMake](https://cmake.org/download/) | 3.16 |
On Linux and macOS, the [Nix development shell](./nix.md) provides all of them
(see below). On Windows they have to be installed manually.
Building with `-Drust=ON` additionally requires a Rust toolchain, see
[Rust](#rust). A default build does not, so it is not in the table above.
Once they are in place, verify that everything is installed and runnable with:
```bash
@@ -120,14 +122,18 @@ manually:
"x64 Native Tools Command Prompt". CI configures CMake with the
`Visual Studio 18 2026` generator.
- [Git for Windows](https://git-scm.com/download/win)
- Python, Conan, CMake, and Rust, at the versions listed in
- Python, Conan, and CMake, at the versions listed in
[Required tools](#required-tools).
- a [Rust toolchain](https://rustup.rs) — only needed to build with
`-Drust=ON`, see [Rust](#rust)
## Rust
The repository contains a Rust workspace in [`crates/`](../../crates), whose
crates are exposed to C++ through [cxx](https://cxx.rs) bindings and compiled by
the CMake build, so a Rust toolchain is required.
crates are exposed to C++ through [cxx](https://cxx.rs) bindings. It is **not**
part of a default build: the CMake `rust` option is OFF by default, and with it
off no Rust toolchain is needed. It is only required when configuring with
`-Drust=ON` (which is what CI does), see [Options](../../BUILD.md#options).
The toolchain (`cargo`, `rustc`) is pinned to the channel in
[`rust-toolchain.toml`](../../rust-toolchain.toml) at the repository root. If

5
docs/build/nix.md vendored
View File

@@ -128,8 +128,9 @@ Coverage builds (`-Dcoverage=ON`) work in the `gcc` shell (and `gcc-plain` on Li
each ships a `gcov` matching its compiler, since Nix's cc-wrapper does not expose one.
The `clang` shells do not include `llvm-cov`, so use a `gcc` shell for coverage.
The Rust toolchain the build needs is included too: every shell provides the
channel pinned in [`rust-toolchain.toml`](../../rust-toolchain.toml) (see
Builds of the Rust crates (`-Drust=ON`) also work out of the box: every shell
provides the Rust toolchain pinned in
[`rust-toolchain.toml`](../../rust-toolchain.toml) (see
[Rust](./environment.md#rust)), plus the `cargo-audit`, `cargo-llvm-cov` and
`cargo-nextest` plugins.

View File

@@ -65,7 +65,7 @@ wherever it appears in the repository configuration.
4. Add the repository, using the channel you picked in [Release channels](#release-channels):
```bash
echo "deb [signed-by=/etc/apt/keyrings/xrplf.asc] https://packages.xrplf.org/repository/deb-stable any main" | \
echo "deb [signed-by=/etc/apt/keyrings/xrplf.asc] https://packages.xrplf.org/repository/deb-stable focal main" | \
sudo tee /etc/apt/sources.list.d/xrplf.list
```
@@ -98,13 +98,13 @@ wherever it appears in the repository configuration.
enabled=1
baseurl=https://packages.xrplf.org/repository/rpm-stable/
gpgcheck=1
repo_gpgcheck=1
repo_gpgcheck=0
gpgkey=https://packages.xrplf.org/xrplf.asc
REPOFILE
```
`gpgcheck=1` verifies each package against the key above.
`repo_gpgcheck=1` verifies the repository metadata, which the server signs with the same key.
`repo_gpgcheck` is off because the repository metadata is generated by the server and is not signed.
3. Install the `xrpld` package:

View File

@@ -543,21 +543,8 @@ public:
setround(RoundingMode inMode);
/**
* Convert an integer to a RoundingMode, validating that it is in range.
* Returns which mantissa scale is currently in use for normalization.
*
* Returns std::nullopt if the value does not correspond to a valid
* RoundingMode.
*/
static std::optional<RoundingMode>
checkedRoundingMode(int mode) noexcept
{
if (mode < static_cast<int>(RoundingMode::ToNearest) ||
mode > static_cast<int>(RoundingMode::Upward))
return std::nullopt;
return static_cast<RoundingMode>(mode);
}
/**
* If you think you need to call this outside of unit tests, no you don't.
*/
static MantissaRange::MantissaScale

View File

@@ -0,0 +1,713 @@
#pragma once
#include <xrpl/beast/utility/instrumentation.h>
#include <coroutine>
#include <exception>
#include <type_traits>
#include <utility>
#include <variant>
namespace xrpl {
template <typename T = void>
class CoroTask;
/**
* CoroTask<void> -- coroutine return type for void-returning coroutines.
*
* Class / Dependency Diagram
* ==========================
*
* CoroTask<void>
* +-----------------------------------------------+
* | - handle_ : Handle (coroutine_handle<promise>) |
* +-----------------------------------------------+
* | + handle(), done() |
* | + await_ready/suspend/resume (Awaiter iface) |
* +-----------------------------------------------+
* | owns
* v
* promise_type
* +-----------------------------------------------+
* | - exception_ : std::exception_ptr |
* | - continuation_ : std::coroutine_handle<> |
* +-----------------------------------------------+
* | + get_return_object() -> CoroTask |
* | + initial_suspend() -> suspend_always (lazy) |
* | + final_suspend() -> FinalAwaiter |
* | + return_void() |
* | + unhandled_exception() |
* +-----------------------------------------------+
* | returns at final_suspend
* v
* FinalAwaiter
* +-----------------------------------------------+
* | await_suspend(h): |
* | if continuation_ set -> symmetric transfer |
* | else -> noop_coroutine |
* +-----------------------------------------------+
*
* Design Notes
* ------------
* - Lazy start: initial_suspend returns suspend_always, so the coroutine
* body does not execute until the handle is explicitly resumed.
* - Symmetric transfer: await_suspend returns a coroutine_handle instead
* of void/bool, allowing the scheduler to jump directly to the next
* coroutine without growing the call stack.
* - Continuation chaining: when one CoroTask is co_await-ed inside
* another, the caller's handle is stored as continuation_ so
* FinalAwaiter can resume it when this task finishes.
* - Move-only: the handle is exclusively owned; copy is deleted.
*
* Usage Examples
* ==============
*
* 1. Basic void coroutine (the most common case in xrpld):
*
* CoroTask<void> doWork(std::shared_ptr<CoroTaskRunner> runner) {
* // do something
* co_await runner->suspend(); // yield control
* // resumed later via runner->post() or runner->resume()
* co_return;
* }
*
* 2. co_await-ing one CoroTask<void> from another (chaining):
*
* CoroTask<void> inner() {
* // ...
* co_return;
* }
* CoroTask<void> outer() {
* co_await inner(); // continuation_ links outer -> inner
* co_return; // FinalAwaiter resumes outer
* }
*
* 3. Exceptions propagate through co_await:
*
* CoroTask<void> failing() {
* throw std::runtime_error("oops");
* co_return;
* }
* CoroTask<void> caller() {
* try { co_await failing(); }
* catch (std::runtime_error const&) { // caught here }
* }
*
* Caveats / Pitfalls
* ==================
*
* BUG-RISK: Dangling references in coroutine parameters.
* Coroutine parameters are copied into the frame, but references
* are NOT -- they are stored as-is. If the referent goes out of scope
* before the coroutine finishes, you get use-after-free.
*
* // BROKEN -- local dies before coroutine runs:
* CoroTask<void> bad(int& ref) { co_return; }
* void launch() {
* int local = 42;
* auto task = bad(local); // frame stores &local
* } // local destroyed; frame holds dangling ref
*
* // FIX -- pass by value, or ensure lifetime via shared_ptr.
*
* BUG-RISK: GCC 14 corrupts reference captures in coroutine lambdas.
* When a lambda that returns CoroTask captures by reference ([&]),
* GCC 14 may generate a corrupted coroutine frame. Always capture
* by explicit pointer-to-value instead:
*
* // BROKEN on GCC 14:
* jq.postCoroTask(t, n, [&](auto) -> CoroTask<void> { ... });
*
* // FIX -- capture pointers explicitly:
* jq.postCoroTask(t, n, [ptr = &val](auto) -> CoroTask<void> { ... });
*
* BUG-RISK: Resuming a destroyed or completed CoroTask.
* Calling handle().resume() after the coroutine has already run to
* completion (done() == true) is undefined behavior. The CoroTaskRunner
* guards against this with an XRPL_ASSERT, but standalone usage of
* CoroTask must check done() before resuming.
*
* BUG-RISK: Moving a CoroTask that is being awaited.
* If task A is co_await-ed by task B (so A.continuation_ == B), moving
* or destroying A will invalidate the continuation link. Never move
* or reassign a CoroTask while it is mid-execution or being awaited.
*
* LIMITATION: CoroTask is fire-and-forget for the top-level owner.
* There is no built-in notification when the coroutine finishes.
* The caller must use external synchronization (e.g. CoroTaskRunner::join
* or a gate/condition_variable) to know when it is done.
*
* LIMITATION: No cancellation token.
* There is no way to cancel a suspended CoroTask from outside. The
* coroutine body must cooperatively check a flag (e.g. jq_.isStopping())
* after each co_await and co_return early if needed.
*
* LIMITATION: Stackless -- cannot suspend from nested non-coroutine calls.
* If a coroutine calls a regular function that wants to "yield", it
* cannot. Only the immediate coroutine body can use co_await.
* This is acceptable for xrpld because all yield() sites are shallow.
*/
template <>
class CoroTask<void>
{
public:
// The C++ coroutine protocol mandates these names (promise_type,
// initial_suspend, await_ready, ...) and instance-callable awaiter
// methods, which conflict with the project naming/static conventions.
// NOLINTBEGIN(readability-identifier-naming, readability-convert-member-functions-to-static)
struct promise_type;
using Handle = std::coroutine_handle<promise_type>;
/**
* Coroutine promise. Compiler uses this to manage coroutine state.
* Stores the exception (if any) and the continuation handle for
* symmetric transfer back to the awaiting coroutine.
*/
struct promise_type
{
// Captured exception from the coroutine body, rethrown in
// await_resume() when this task is co_await-ed by a caller.
std::exception_ptr exception_;
// Handle to the coroutine that is co_await-ing this task.
// Set by await_suspend(). FinalAwaiter uses it for symmetric
// transfer back to the caller. Null if this is a top-level task.
std::coroutine_handle<> continuation_;
/**
* Create the CoroTask return object.
* Called by the compiler at coroutine creation.
*/
CoroTask
get_return_object()
{
return CoroTask{Handle::from_promise(*this)};
}
/**
* Lazy start. The coroutine body does not execute until the
* handle is explicitly resumed (e.g. by CoroTaskRunner::resume).
*/
std::suspend_always
initial_suspend() noexcept
{
return {};
}
/**
* Awaiter returned by final_suspend(). Uses symmetric transfer:
* if a continuation exists, transfers control directly to it
* (tail-call, no stack growth). Otherwise returns noop_coroutine
* so the coroutine frame stays alive for the owner to destroy.
*/
struct FinalAwaiter
{
/**
* Always false. We need await_suspend to run for
* symmetric transfer.
*/
bool
await_ready() noexcept
{
return false;
}
/**
* Symmetric transfer: returns the continuation handle so
* the compiler emits a tail-call instead of a nested resume.
* If no continuation is set, returns noop_coroutine to
* suspend at final_suspend without destroying the frame.
*
* @param h Handle to this completing coroutine
*
* @return Continuation handle, or noop_coroutine
*/
std::coroutine_handle<>
await_suspend(Handle h) noexcept
{
if (auto cont = h.promise().continuation_)
return cont;
return std::noop_coroutine();
}
void
await_resume() noexcept
{
}
};
/**
* Returns FinalAwaiter for symmetric transfer at coroutine end.
*/
FinalAwaiter
final_suspend() noexcept
{
return {};
}
/**
* Called by the compiler for `co_return;` (void coroutine).
*/
void
return_void()
{
}
/**
* Called by the compiler when an exception escapes the coroutine
* body. Captures it for later rethrowing in await_resume().
*/
void
unhandled_exception()
{
exception_ = std::current_exception();
}
};
// NOLINTEND(readability-identifier-naming, readability-convert-member-functions-to-static)
/**
* Default constructor. Creates an empty (null handle) task.
*/
CoroTask() = default;
/**
* Takes ownership of a compiler-generated coroutine handle.
*
* @param h Coroutine handle to own
*/
explicit CoroTask(Handle h) : handle_(h)
{
}
/**
* Destroys the coroutine frame if this task owns one.
*/
~CoroTask()
{
if (handle_)
handle_.destroy();
}
/**
* Move constructor. Transfers handle ownership, leaves other empty.
*/
CoroTask(CoroTask&& other) noexcept : handle_(std::exchange(other.handle_, {}))
{
}
/**
* Move assignment. Destroys current frame (if any), takes other's.
*/
CoroTask&
operator=(CoroTask&& other) noexcept
{
if (this != &other)
{
if (handle_)
handle_.destroy();
handle_ = std::exchange(other.handle_, {});
}
return *this;
}
CoroTask(CoroTask const&) = delete;
CoroTask&
operator=(CoroTask const&) = delete;
/**
* @return The underlying coroutine_handle
*/
[[nodiscard]] Handle
handle() const
{
return handle_;
}
/**
* @return true if the coroutine has run to completion (or thrown)
*/
[[nodiscard]] bool
done() const
{
return handle_ && handle_.done();
}
// -- Awaiter interface: allows `co_await someCoroTask;` --
/**
* Always false. This task is lazy, so co_await always suspends
* the caller to set up the continuation link.
*/
// NOLINTBEGIN(readability-identifier-naming, readability-convert-member-functions-to-static)
[[nodiscard]] bool
await_ready() const noexcept
{
return false;
}
/**
* Stores the caller's handle as our continuation, then returns
* our handle for symmetric transfer (caller suspends, we resume).
*
* @param caller Handle of the coroutine doing co_await on us
*
* @return Our handle for symmetric transfer
*/
std::coroutine_handle<>
await_suspend(std::coroutine_handle<> caller) noexcept
{
XRPL_ASSERT(handle_, "xrpl::CoroTask<void>::await_suspend : handle is valid");
handle_.promise().continuation_ = caller;
return handle_; // Symmetric transfer
}
/**
* Called in the awaiting coroutine's context after this task
* completes. Rethrows any exception captured by
* unhandled_exception().
*/
void
await_resume()
{
XRPL_ASSERT(handle_, "xrpl::CoroTask<void>::await_resume : handle is valid");
if (auto& ep = handle_.promise().exception_)
std::rethrow_exception(ep);
}
// NOLINTEND(readability-identifier-naming, readability-convert-member-functions-to-static)
private:
// Exclusively-owned coroutine handle. Null after move or default
// construction. Destroyed in the destructor.
Handle handle_;
};
/**
* CoroTask<T> -- coroutine return type for value-returning coroutines.
*
* Class / Dependency Diagram
* ==========================
*
* CoroTask<T>
* +-----------------------------------------------+
* | - handle_ : Handle (coroutine_handle<promise>) |
* +-----------------------------------------------+
* | + handle(), done() |
* | + await_ready/suspend/resume (Awaiter iface) |
* +-----------------------------------------------+
* | owns
* v
* promise_type
* +-----------------------------------------------+
* | - result_ : variant<monostate, T, |
* | exception_ptr> |
* | - continuation_ : std::coroutine_handle<> |
* +-----------------------------------------------+
* | + get_return_object() -> CoroTask |
* | + initial_suspend() -> suspend_always (lazy) |
* | + final_suspend() -> FinalAwaiter |
* | + return_value(T) -> stores in result_[1] |
* | + unhandled_exception -> stores in result_[2] |
* +-----------------------------------------------+
* | returns at final_suspend
* v
* FinalAwaiter (same symmetric-transfer pattern as CoroTask<void>)
*
* Value Extraction
* ----------------
* await_resume() inspects the variant:
* - index 2 (exception_ptr) -> rethrow
* - index 1 (T) -> return value via move
*
* Usage Examples
* ==============
*
* 1. Simple value return:
*
* CoroTask<int> computeAnswer() { co_return 42; }
*
* CoroTask<void> caller() {
* int v = co_await computeAnswer(); // v == 42
* }
*
* 2. Chaining value-returning coroutines:
*
* CoroTask<int> add(int a, int b) { co_return a + b; }
* CoroTask<int> doubleSum(int a, int b) {
* int s = co_await add(a, b);
* co_return s * 2;
* }
*
* 3. Exception propagation from inner to outer:
*
* CoroTask<int> failing() {
* throw std::runtime_error("bad");
* co_return 0; // never reached
* }
* CoroTask<void> caller() {
* try {
* int v = co_await failing(); // throws here
* } catch (std::runtime_error const& e) {
* // e.what() == "bad"
* }
* }
*
* Caveats / Pitfalls (in addition to CoroTask<void> caveats above)
* ================================================================
*
* BUG-RISK: await_resume() moves the value out of the variant.
* Calling co_await on the same CoroTask<T> instance twice is undefined
* behavior -- the second call will see a moved-from T. CoroTask is
* single-shot: one co_return, one co_await.
*
* BUG-RISK: T must be move-constructible.
* return_value(T) takes by value and moves into the variant.
* Types that are not movable cannot be used as T.
*
* LIMITATION: No co_yield support.
* CoroTask<T> only supports a single co_return. It does not implement
* yield_value(), so using co_yield inside a CoroTask<T> coroutine is a
* compile error. For streaming values, a different return type
* (e.g. Generator<T>) would be needed.
*
* LIMITATION: Result is only accessible via co_await.
* There is no .get() or .result() method. The value can only be
* extracted by co_await-ing the CoroTask<T> from inside another
* coroutine. For extracting results in non-coroutine code, pass a
* pointer to the caller and write through it (as the tests do).
*/
template <typename T>
class CoroTask
{
static_assert(
std::is_move_constructible_v<T>,
"CoroTask<T> requires T to be move-constructible");
public:
// The C++ coroutine protocol mandates these names (promise_type,
// initial_suspend, await_ready, ...) and instance-callable awaiter
// methods, which conflict with the project naming/static conventions.
// NOLINTBEGIN(readability-identifier-naming, readability-convert-member-functions-to-static)
struct promise_type;
using Handle = std::coroutine_handle<promise_type>;
/**
* Coroutine promise for value-returning coroutines.
* Stores the result as a variant: monostate (not yet set),
* T (co_return value), or exception_ptr (unhandled exception).
*/
struct promise_type
{
// Tri-state result:
// index 0 (monostate) -- coroutine has not yet completed
// index 1 (T) -- co_return value stored here
// index 2 (exception) -- unhandled exception captured here
std::variant<std::monostate, T, std::exception_ptr> result_;
// Handle to the coroutine co_await-ing this task. Used by
// FinalAwaiter for symmetric transfer. Null for top-level tasks.
std::coroutine_handle<> continuation_;
/**
* Create the CoroTask return object.
* Called by the compiler at coroutine creation.
*/
CoroTask
get_return_object()
{
return CoroTask{Handle::from_promise(*this)};
}
/**
* Lazy start. Coroutine body does not run until explicitly resumed.
*/
std::suspend_always
initial_suspend() noexcept
{
return {};
}
/**
* Symmetric-transfer awaiter at coroutine completion.
* Same pattern as CoroTask<void>::FinalAwaiter.
*/
struct FinalAwaiter
{
bool
await_ready() noexcept
{
return false;
}
/**
* Returns continuation for symmetric transfer, or
* noop_coroutine if this is a top-level task.
*
* @param h Handle to this completing coroutine
*
* @return Continuation handle, or noop_coroutine
*/
std::coroutine_handle<>
await_suspend(Handle h) noexcept
{
if (auto cont = h.promise().continuation_)
return cont;
return std::noop_coroutine();
}
void
await_resume() noexcept
{
}
};
FinalAwaiter
final_suspend() noexcept
{
return {};
}
/**
* Called by the compiler for `co_return value;`.
* Moves the value into result_ at index 1.
*
* @param value The value to store
*/
void
return_value(T value)
{
result_.template emplace<1>(std::move(value));
}
/**
* Captures unhandled exceptions at index 2 of result_.
* Rethrown later in await_resume().
*/
void
unhandled_exception()
{
result_.template emplace<2>(std::current_exception());
}
};
// NOLINTEND(readability-identifier-naming, readability-convert-member-functions-to-static)
/**
* Default constructor. Creates an empty (null handle) task.
*/
CoroTask() = default;
/**
* Takes ownership of a compiler-generated coroutine handle.
*
* @param h Coroutine handle to own
*/
explicit CoroTask(Handle h) : handle_(h)
{
}
/**
* Destroys the coroutine frame if this task owns one.
*/
~CoroTask()
{
if (handle_)
handle_.destroy();
}
/**
* Move constructor. Transfers handle ownership, leaves other empty.
*/
CoroTask(CoroTask&& other) noexcept : handle_(std::exchange(other.handle_, {}))
{
}
/**
* Move assignment. Destroys current frame (if any), takes other's.
*/
CoroTask&
operator=(CoroTask&& other) noexcept
{
if (this != &other)
{
if (handle_)
handle_.destroy();
handle_ = std::exchange(other.handle_, {});
}
return *this;
}
CoroTask(CoroTask const&) = delete;
CoroTask&
operator=(CoroTask const&) = delete;
/**
* @return The underlying coroutine_handle
*/
[[nodiscard]] Handle
handle() const
{
return handle_;
}
/**
* @return true if the coroutine has run to completion (or thrown)
*/
[[nodiscard]] bool
done() const
{
return handle_ && handle_.done();
}
// -- Awaiter interface: allows `T val = co_await someCoroTask;` --
/**
* Always false. co_await always suspends to set up continuation.
*/
// NOLINTBEGIN(readability-identifier-naming, readability-convert-member-functions-to-static)
[[nodiscard]] bool
await_ready() const noexcept
{
return false;
}
/**
* Stores caller as continuation, returns our handle for
* symmetric transfer.
*
* @param caller Handle of the coroutine doing co_await on us
*
* @return Our handle for symmetric transfer
*/
std::coroutine_handle<>
await_suspend(std::coroutine_handle<> caller) noexcept
{
XRPL_ASSERT(handle_, "xrpl::CoroTask<T>::await_suspend : handle is valid");
handle_.promise().continuation_ = caller;
return handle_;
}
/**
* Extracts the result: rethrows if exception, otherwise moves
* the T value out of the variant. Single-shot: calling twice
* on the same task is undefined (moved-from T).
*
* @return The co_return-ed value
*/
T
await_resume()
{
XRPL_ASSERT(handle_, "xrpl::CoroTask<T>::await_resume : handle is valid");
auto& result = handle_.promise().result_;
if (auto* ep = std::get_if<2>(&result))
std::rethrow_exception(*ep);
return std::get<1>(std::move(result));
}
// NOLINTEND(readability-identifier-naming, readability-convert-member-functions-to-static)
private:
// Exclusively-owned coroutine handle. Null after move or default
// construction. Destroyed in the destructor.
Handle handle_;
};
} // namespace xrpl

View File

@@ -0,0 +1,448 @@
#pragma once
/**
* @file CoroTaskRunner.ipp
*
* CoroTaskRunner inline implementation.
*
* This file contains the business logic for managing C++20 coroutines
* on the JobQueue. It is included at the bottom of JobQueue.h.
*
* Data Flow: suspend / post / resume cycle
* =========================================
*
* coroutine body CoroTaskRunner JobQueue
* -------------- -------------- --------
* |
* co_await runner->suspend()
* |
* +--- await_suspend ------> onSuspend()
* | ++nSuspend_ ------------> nSuspend_
* | [coroutine is now suspended]
* |
* . (externally or by yieldAndPost())
* .
* +--- (caller calls) -----> post()
* | ++runCount_
* | addJob(resume) ----------> job enqueued
* | |
* | [worker picks up]
* | |
* +--- <----- resume() <-----------------------------------+
* | --nSuspend_ ------> nSuspend_
* | swap in LocalValues (lvs_)
* | task_.handle().resume()
* | |
* | [coroutine body continues here]
* | |
* | swap out LocalValues
* | --runCount_
* | cv_.notify_all()
* v
*
* Thread Safety
* =============
* - mutex_ : guards task_.handle().resume() so that post()-before-suspend
* races cannot resume the coroutine while it is still running.
* (See the race condition discussion in JobQueue.h)
* - mutexRun_ : guards runCount_ counter; used by join() to wait until
* all in-flight resume operations complete.
* - jq_.mutex_: guards nSuspend_ increments/decrements.
*
* Common Mistakes When Modifying This File
* =========================================
*
* 1. Changing lock ordering.
* resume() acquires locks sequentially (never held simultaneously):
* jq_.mutex_ (released immediately), then mutex_ (held across resume),
* then mutexRun_ (released after decrement). post() acquires only
* mutexRun_. Any new code path must follow the same order.
*
* 2. Removing the shared_from_this() capture in post().
* The lambda passed to addJob captures [this, sp = shared_from_this()].
* If you remove sp, 'this' can be destroyed before the job runs,
* causing use-after-free. The sp capture is load-bearing.
*
* 3. Forgetting to decrement nSuspend_ on a new code path.
* Every ++nSuspend_ must have a matching --nSuspend_. If you add a new
* suspension path (e.g. a new awaiter) and forget to decrement on resume
* or on failure, JobQueue::stop() will hang.
*
* 4. Calling task_.handle().resume() without holding mutex_.
* This allows a race where the coroutine runs on two threads
* simultaneously. Always hold mutex_ around resume().
*
* 5. Swapping LocalValues outside of the mutex_ critical section.
* The swap-in and swap-out of LocalValues must bracket the resume()
* call. If you move the swap-out before the lock_guard(mutex_) is
* released, you break LocalValue isolation for any code that runs
* after the coroutine suspends but before the lock is dropped.
*/
namespace xrpl {
/**
* Construct a CoroTaskRunner. Sets runCount_ to 0; does not
* create the coroutine. Call init() afterwards.
*
* @param jq The JobQueue this coroutine will run on
* @param type Job type for scheduling priority
* @param name Human-readable name for logging
*/
inline JobQueue::CoroTaskRunner::CoroTaskRunner(
CreateT,
JobQueue& jq,
JobType type,
std::string name)
: jq_(jq), type_(type), name_(std::move(name))
{
}
/**
* Initialize with a coroutine-returning callable.
* Stores the callable on the heap (FuncStore) so it outlives the
* coroutine frame. Coroutine frames store a reference to the
* callable's implicit object parameter (the lambda). If the callable
* is a temporary, that reference dangles after the caller returns.
* Keeping the callable alive here ensures the coroutine's captures
* remain valid.
*
* @param f Callable: CoroTask<void>(shared_ptr<CoroTaskRunner>)
*/
template <class F>
void
JobQueue::CoroTaskRunner::init(F&& f)
{
using Fn = std::decay_t<F>;
auto store = std::make_unique<FuncStore<Fn>>(std::forward<F>(f));
task_ = store->func(shared_from_this());
storedFunc_ = std::move(store);
}
/**
* Destructor. Waits for any in-flight resume() to complete, then
* asserts (debug) that the coroutine has finished or
* expectEarlyExit() was called.
*
* The join() call is necessary because with async dispatch the
* coroutine runs on a worker thread. The gate signal (which wakes
* the test thread) can arrive before resume() has set finished_.
* join() synchronizes via mutexRun_, establishing a happens-before
* edge: finished_ = true -> unlock(mutexRun_) in resume() ->
* lock(mutexRun_) in join() -> read finished_.
*/
inline JobQueue::CoroTaskRunner::~CoroTaskRunner()
{
#ifndef NDEBUG
join();
XRPL_ASSERT(finished_, "xrpl::JobQueue::CoroTaskRunner::~CoroTaskRunner : is finished");
#endif
}
/**
* Increment the JobQueue's suspended-coroutine count (nSuspend_).
*/
inline void
JobQueue::CoroTaskRunner::onSuspend()
{
std::scoped_lock const lock(jq_.mutex_);
++jq_.nSuspend_;
}
/**
* Decrement nSuspend_ without resuming.
*/
inline void
JobQueue::CoroTaskRunner::onUndoSuspend()
{
std::scoped_lock const lock(jq_.mutex_);
--jq_.nSuspend_;
}
/**
* Return a SuspendAwaiter whose await_suspend() increments nSuspend_
* before the coroutine actually suspends. The caller must later call
* post() or resume() to continue execution.
*
* @return Awaiter for use with `co_await runner->suspend()`
*/
inline auto
JobQueue::CoroTaskRunner::suspend()
{
/**
* Custom awaiter for suspend(). Always suspends (await_ready
* returns false) and increments nSuspend_ in await_suspend().
*/
// The C++ coroutine protocol mandates these awaiter names and
// instance-callable methods, which conflict with the project
// naming/static conventions.
// NOLINTBEGIN(readability-identifier-naming, readability-convert-member-functions-to-static)
struct SuspendAwaiter
{
CoroTaskRunner& runner_; // The runner that owns this coroutine.
/**
* Always returns false so the coroutine suspends.
*/
[[nodiscard]] bool
await_ready() const noexcept
{
return false;
}
/**
* Called when the coroutine suspends. Increments nSuspend_
* so the JobQueue knows a coroutine is waiting.
*/
void
await_suspend(std::coroutine_handle<>) const
{
runner_.onSuspend();
}
void
await_resume() const noexcept
{
}
};
// NOLINTEND(readability-identifier-naming, readability-convert-member-functions-to-static)
return SuspendAwaiter{*this};
}
/**
* Suspend and immediately repost on the JobQueue. Equivalent to
* `co_await JobQueueAwaiter{runner}` but uses an inline struct
* to work around a GCC-12 codegen bug (see declaration in JobQueue.h).
*
* If the JobQueue is stopping (post fails), the suspend count is
* undone and the coroutine continues immediately via symmetric
* transfer back to its own handle.
*
* @return An inline YieldPostAwaiter
*/
inline auto
JobQueue::CoroTaskRunner::yieldAndPost()
{
// The C++ coroutine protocol mandates these awaiter names and
// instance-callable methods, which conflict with the project
// naming/static conventions.
// NOLINTBEGIN(readability-identifier-naming, readability-convert-member-functions-to-static)
struct YieldPostAwaiter
{
CoroTaskRunner& runner_;
[[nodiscard]] bool
await_ready() const noexcept
{
return false;
}
/**
* Returns a coroutine_handle<> (symmetric transfer) rather than
* void + h.resume(). Two reasons:
*
* 1. h.resume() runs the coroutine nested inside this frame. A
* coroutine that yields in a loop against a stopping JobQueue
* fails post() every iteration, so the stack grows without
* bound. Symmetric transfer is a tail call and does not nest.
*
* 2. After h.resume() returns, the coroutine may have completed
* and destroyed its frame -- the frame this awaiter lives in.
* Returning from await_suspend would then touch freed memory.
*
* A bool return would also avoid nesting, but GCC-12 miscompiles
* bool-returning await_suspend (see JobQueueAwaiter.h).
*
* @return noop_coroutine() to stay suspended (job posted);
* the caller's handle to continue now (JQ stopping)
*/
std::coroutine_handle<>
await_suspend(std::coroutine_handle<> h)
{
runner_.onSuspend();
if (!runner_.post())
{
runner_.onUndoSuspend();
return h;
}
return std::noop_coroutine();
}
void
await_resume() const noexcept
{
}
};
// NOLINTEND(readability-identifier-naming, readability-convert-member-functions-to-static)
return YieldPostAwaiter{*this};
}
/**
* Schedule coroutine resumption as a job on the JobQueue.
* A shared_ptr capture (sp) prevents this CoroTaskRunner from being
* destroyed while the job is queued but not yet executed.
*
* @return false if the JobQueue rejected the job (shutting down)
*/
inline bool
JobQueue::CoroTaskRunner::post()
{
{
std::scoped_lock const lk(mutexRun_);
++runCount_;
}
// sp prevents 'this' from being destroyed while the job is pending
if (jq_.addJob(type_, name_, [this, sp = shared_from_this()]() { resume(); }))
{
return true;
}
// The coroutine will not run. Undo the runCount_ increment.
std::scoped_lock const lk(mutexRun_);
--runCount_;
cv_.notify_all();
return false;
}
/**
* Resume the coroutine on the current thread.
*
* Steps:
* 1. Decrement nSuspend_ (under jq_.mutex_)
* 2. Swap in this coroutine's LocalValues for thread-local isolation
* 3. Resume the coroutine handle (under mutex_)
* 4. Swap out LocalValues, restoring the thread's previous state
* 5. Decrement runCount_ and notify join() waiters
*
* @pre post() must have been called before resume(). Direct calls
* without a prior post() will corrupt runCount_ and break join().
* Note: runCount_ is NOT incremented here — post() already did that.
* This ensures join() stays blocked for the entire post->resume lifetime.
*/
inline void
JobQueue::CoroTaskRunner::resume()
{
{
std::scoped_lock const lock(jq_.mutex_);
--jq_.nSuspend_;
}
auto saved = detail::getLocalValues().release();
detail::getLocalValues().reset(&lvs_);
std::scoped_lock const lock(mutex_);
XRPL_ASSERT(
task_.handle() && !task_.done(),
"xrpl::JobQueue::CoroTaskRunner::resume : task handle is valid and not done");
if (task_.handle() && !task_.done())
{
task_.handle().resume();
}
else
{
// A resume() with no coroutine to run (e.g. a duplicate external
// post() after completion). Resuming a null or finished handle is
// undefined behavior, so skip it -- this matches the old
// Coro::resume() `if (coro_)` guard. The bookkeeping below still
// runs to balance the ++runCount_ done by the post() that
// scheduled this call.
JLOG(jq_.journal_.warn())
<< "CoroTaskRunner::resume called for coroutine '" << name_
<< "' with no runnable coroutine (duplicate post or already completed)";
}
detail::getLocalValues().release();
detail::getLocalValues().reset(saved);
if (task_.done())
{
finished_ = true;
// An exception that escapes a top-level coroutine body is captured
// by promise_type::unhandled_exception() but has no awaiter to
// rethrow it, so it would vanish with the frame. Surface it in the
// log. (The old Boost path propagated it out of resume() instead.)
if (auto const& ep = task_.handle().promise().exception_)
{
try
{
std::rethrow_exception(ep);
}
catch (std::exception const& e)
{
JLOG(jq_.journal_.error())
<< "Unhandled exception in coroutine '" << name_ << "': " << e.what();
}
catch (...)
{
JLOG(jq_.journal_.error())
<< "Unhandled non-standard exception in coroutine '" << name_ << "'";
}
}
// Break the shared_ptr cycle: frame -> shared_ptr<runner> -> this.
// Use std::move (not task_ = {}) so task_.handle_ is null BEFORE the
// frame is destroyed. operator= would destroy the frame while handle_
// still holds the old value -- a re-entrancy hazard on GCC-12 if
// frame destruction triggers runner cleanup.
[[maybe_unused]] auto completed = std::move(task_);
}
std::scoped_lock const lk(mutexRun_);
--runCount_;
cv_.notify_all();
}
/**
* @return true if the coroutine has not yet run to completion
*/
inline bool
JobQueue::CoroTaskRunner::runnable() const
{
// After normal completion, task_ is reset to break the shared_ptr cycle
// (handle_ becomes null). A null handle means the coroutine is done.
return task_.handle() && !task_.done();
}
/**
* Handle early termination when the coroutine never ran (e.g. JobQueue
* is stopping). Decrements nSuspend_ and destroys the coroutine frame
* to break the shared_ptr cycle: frame -> lambda -> runner -> frame.
*/
inline void
JobQueue::CoroTaskRunner::expectEarlyExit()
{
if (!finished_)
{
std::scoped_lock const lock(jq_.mutex_);
--jq_.nSuspend_;
finished_ = true;
}
// Break the shared_ptr cycle: frame -> shared_ptr<runner> -> this.
// The coroutine is at initial_suspend and never ran user code, so
// destroying it is safe. Use std::move (not task_ = {}) so
// task_.handle_ is null before the frame is destroyed.
{
[[maybe_unused]] auto completed = std::move(task_);
}
storedFunc_.reset();
}
/**
* Block until all pending/active resume operations complete.
* Uses cv_ + mutexRun_ to wait until runCount_ reaches 0 or
* finished_ becomes true. The finished_ check handles the case
* where resume() is called directly (without post()), which
* decrements runCount_ below zero. In that scenario runCount_
* never returns to 0, but finished_ becoming true guarantees
* the coroutine is done and no more resumes will occur.
*
* Note: when join() returns via the finished_ disjunct, the final
* resume() call may still be executing its post-completion
* bookkeeping (the --runCount_ / notify after finished_ is set).
* That is safe -- the coroutine body has fully completed and the
* runner is kept alive by the resume job's shared_ptr -- but
* callers must not assume resume() itself has returned.
*/
inline void
JobQueue::CoroTaskRunner::join()
{
std::unique_lock<std::mutex> lk(mutexRun_);
cv_.wait(lk, [this]() { return runCount_ == 0 || finished_; });
}
} // namespace xrpl

View File

@@ -2,6 +2,7 @@
#include <xrpl/basics/LocalValue.h>
#include <xrpl/core/ClosureCounter.h>
#include <xrpl/core/CoroTask.h>
#include <xrpl/core/JobTypeData.h>
#include <xrpl/core/detail/Workers.h>
#include <xrpl/json/json_value.h>
@@ -152,6 +153,420 @@ public:
join();
};
/**
* C++20 coroutine lifecycle manager. Replaces Coro for new code.
*
* Class / Inheritance / Dependency Diagram
* =========================================
*
* std::enable_shared_from_this<CoroTaskRunner>
* ^
* | (public inheritance)
* |
* CoroTaskRunner
* +---------------------------------------------------+
* | - lvs_ : detail::LocalValues |
* | - jq_ : JobQueue& |
* | - type_ : JobType |
* | - name_ : std::string |
* | - runCount_ : int (in-flight resumes) |
* | - mutex_ : std::mutex (coroutine guard) |
* | - mutexRun_ : std::mutex (join guard) |
* | - cv_ : condition_variable |
* | - task_ : CoroTask<void> |
* | - storedFunc_ : unique_ptr<FuncBase> (type-erased)|
* +---------------------------------------------------+
* | + init(F&&) : set up coroutine callable |
* | + onSuspend() : ++jq_.nSuspend_ |
* | + onUndoSuspend() : --jq_.nSuspend_ |
* | + suspend() : returns SuspendAwaiter |
* | + post() : schedule resume on JobQueue |
* | + resume() : resume coroutine on caller |
* | + runnable() : !task_.done() |
* | + expectEarlyExit() : teardown for failed post |
* | + join() : block until not running |
* +---------------------------------------------------+
* | |
* | owns | references
* v v
* CoroTask<void> JobQueue
* (coroutine frame) (thread pool + nSuspend_)
*
* FuncBase / FuncStore<F> (type-erased heap storage
* for the coroutine lambda)
*
* Coroutine Lifecycle (Control Flow)
* ===================================
*
* Caller thread JobQueue worker thread
* ------------- ----------------------
* postCoroTask(f)
* |
* +-- reserve a jobCounter_ slot (reject if JQ shutting down)
* +-- ++nSuspend_ (lazy start counts as suspended)
* +-- make_shared<CoroTaskRunner>
* +-- init(f)
* | +-- store lambda on heap (FuncStore)
* | +-- task_ = f(shared_from_this())
* | [coroutine created, suspended at initial_suspend]
* +-- post()
* | +-- ++runCount_
* | +-- addJob(type_, [resume]{})
* | resume()
* | |
* | +-- --nSuspend_
* | +-- swap in LocalValues
* | +-- task_.handle().resume()
* | | [coroutine body runs]
* | | ...
* | | co_await suspend()
* | | +-- ++nSuspend_
* | | [coroutine suspends]
* | +-- swap out LocalValues
* | +-- --runCount_
* | +-- cv_.notify_all()
* |
* post() <-- called externally or by yieldAndPost()
* +-- ++runCount_
* +-- addJob(type_, [resume]{})
* resume()
* |
* +-- [coroutine body continues]
* +-- co_return
* +-- --runCount_
* +-- cv_.notify_all()
* join()
* +-- cv_.wait([]{runCount_ == 0})
* +-- [done]
*
* Usage Examples
* ==============
*
* 1. Fire-and-forget coroutine (most common pattern):
*
* jq.postCoroTask(JtClient, "MyWork",
* [](auto runner) -> CoroTask<void> {
* doSomeWork();
* co_await runner->suspend(); // yield to other jobs
* doMoreWork();
* co_return;
* });
*
* 2. Manually controlling suspend / resume (external trigger):
*
* auto runner = jq.postCoroTask(JtClient, "ExtTrigger",
* [&result](auto runner) -> CoroTask<void> {
* startAsyncOperation(callback);
* co_await runner->suspend();
* // callback called runner->post() to get here
* result = collectResult();
* co_return;
* });
* // ... later, from the callback:
* runner->post(); // reschedule the coroutine on the JobQueue
*
* 3. Using yieldAndPost() for automatic suspend + repost:
*
* jq.postCoroTask(JtClient, "AutoRepost",
* [](auto runner) -> CoroTask<void> {
* step1();
* co_await runner->yieldAndPost(); // yield + auto-repost
* step2();
* co_await runner->yieldAndPost();
* step3();
* co_return;
* });
*
* 4. Checking shutdown after co_await (cooperative cancellation):
*
* jq.postCoroTask(JtClient, "Cancellable",
* [&jq](auto runner) -> CoroTask<void> {
* while (moreWork()) {
* co_await runner->yieldAndPost();
* if (jq.isStopping())
* co_return; // bail out cleanly
* processNextItem();
* }
* co_return;
* });
*
* Caveats / Pitfalls
* ==================
*
* BUG-RISK: Calling suspend() without a matching post()/resume().
* After co_await runner->suspend(), the coroutine is parked and
* nSuspend_ is incremented. If nothing ever calls post() or
* resume(), the coroutine is leaked and JobQueue::stop() will
* hang forever waiting for nSuspend_ to reach zero.
*
* BUG-RISK: Calling post() on an already-running coroutine.
* post() schedules a resume() job. If the coroutine has not
* actually suspended yet (no co_await executed), the resume job
* will try to call handle().resume() while the coroutine is still
* running on another thread. This is UB. The mutex_ prevents
* data corruption but the logic is wrong — always co_await
* suspend() before calling post(). (The test incorrect_order()
* shows this works only because mutex_ serializes the calls.)
*
* BUG-RISK: Dropping the shared_ptr<CoroTaskRunner> before join().
* The CoroTaskRunner destructor asserts that finished_ is true
* (the coroutine completed). If you let the last shared_ptr die
* while the coroutine is still running or suspended, you get an
* assertion failure in debug and UB in release. Always call
* join() or expectEarlyExit() first.
*
* BUG-RISK: Lambda captures outliving the coroutine frame.
* The lambda passed to postCoroTask is heap-allocated (FuncStore)
* to prevent dangling. But objects captured by pointer still need
* their own lifetime management. If you capture a raw pointer to
* a stack variable, and the stack frame exits before the coroutine
* finishes, the pointer dangles. Use shared_ptr or ensure the
* pointed-to object outlives the coroutine.
*
* BUG-RISK: Forgetting co_return in a void coroutine.
* If the coroutine body falls off the end without co_return,
* the compiler may silently treat it as co_return (per standard),
* but some compilers warn. Always write explicit co_return.
*
* LIMITATION: CoroTaskRunner only supports CoroTask<void>.
* The task_ member is CoroTask<void>. To return values from
* the top-level coroutine, write through a captured pointer
* (as the tests demonstrate), or co_await inner CoroTask<T>
* coroutines that return values.
*
* LIMITATION: One coroutine per CoroTaskRunner.
* init() must be called exactly once. You cannot reuse a
* CoroTaskRunner to run a second coroutine. Create a new one
* via postCoroTask() instead.
*
* LIMITATION: No timeout on join().
* join() blocks indefinitely. If the coroutine is suspended
* and never posted, join() will deadlock. Use timed waits
* on the gate pattern (condition_variable + wait_for) in tests.
*/
class CoroTaskRunner : public std::enable_shared_from_this<CoroTaskRunner>
{
private:
// Per-coroutine thread-local storage. Swapped in before resume()
// and swapped out after, so each coroutine sees its own LocalValue
// state regardless of which worker thread executes it.
detail::LocalValues lvs_;
// Back-reference to the owning JobQueue. Used to post jobs,
// increment/decrement nSuspend_, and acquire jq_.mutex_.
JobQueue& jq_;
// Job type passed to addJob() when posting this coroutine.
JobType type_;
// Human-readable name for this coroutine job (for logging).
std::string name_;
// Number of in-flight resume operations (pending + active).
// Incremented by post(), decremented when resume() finishes.
// Guarded by mutexRun_. join() blocks until this reaches 0.
//
// A counter (not a bool) is needed because post() can be called
// from within the coroutine body (e.g. via yieldAndPost()),
// enqueuing a second resume while the first is still running.
// A bool would be clobbered: R2.post() sets true, then R1's
// cleanup sets false — losing the fact that R2 is still pending.
int runCount_ = 0;
// Serializes all coroutine resume() calls, preventing concurrent
// execution of the coroutine body on multiple threads. Handles the
// race where post() enqueues a resume before the coroutine has
// actually suspended (post-before-suspend pattern).
std::mutex mutex_;
// Guards runCount_. Used with cv_ for join() to wait
// until all pending/active resume operations complete.
std::mutex mutexRun_;
// Notified when runCount_ reaches zero, allowing
// join() waiters to wake up.
std::condition_variable cv_;
// The coroutine handle wrapper. Owns the coroutine frame.
// Set by init(). Reset to empty in resume() upon coroutine
// completion (to break the shared_ptr cycle) or in
// expectEarlyExit() on early termination.
CoroTask<void> task_;
/**
* Type-erased base for heap-stored callables.
* Prevents the coroutine lambda from being destroyed before
* the coroutine frame is done with it.
*
* @see FuncStore
*/
struct FuncBase
{
virtual ~FuncBase() = default;
};
/**
* Concrete type-erased storage for a callable of type F.
* The coroutine frame stores a reference to the lambda's implicit
* object parameter. If the lambda is a temporary, that reference
* dangles after the call returns. FuncStore keeps it alive on
* the heap for the lifetime of the CoroTaskRunner.
*/
template <class F>
struct FuncStore : FuncBase
{
F func; // The stored callable (coroutine lambda).
explicit FuncStore(F&& f) : func(std::move(f))
{
}
};
// Heap-allocated callable storage. Set by init(), ensures the
// lambda outlives the coroutine frame that references it.
std::unique_ptr<FuncBase> storedFunc_;
// True once the coroutine has completed or expectEarlyExit() was
// called. Asserted in the destructor (debug) to catch leaked
// runners. Available in all builds to guard expectEarlyExit()
// against double-decrementing nSuspend_.
bool finished_ = false;
public:
/**
* Tag type for private construction. Prevents external code
* from constructing CoroTaskRunner directly. Use postCoroTask().
*/
struct CreateT
{
explicit CreateT() = default;
};
/**
* Construct a CoroTaskRunner. Private by convention (CreateT tag).
*
* @param jq The JobQueue this coroutine will run on
* @param type Job type for scheduling priority
* @param name Human-readable name for logging
*/
CoroTaskRunner(CreateT, JobQueue&, JobType, std::string);
CoroTaskRunner(CoroTaskRunner const&) = delete;
CoroTaskRunner&
operator=(CoroTaskRunner const&) = delete;
/**
* Destructor. Asserts (debug) that the coroutine has finished
* or expectEarlyExit() was called.
*/
~CoroTaskRunner();
/**
* Initialize with a coroutine-returning callable.
* Must be called exactly once, after the object is managed by
* shared_ptr (because init uses shared_from_this internally).
* This is handled automatically by postCoroTask().
*
* @param f Callable: CoroTask<void>(shared_ptr<CoroTaskRunner>)
*/
template <class F>
void
init(F&& f);
/**
* Increment the JobQueue's suspended-coroutine count (nSuspend_).
* Called when the coroutine is about to suspend. Every call
* must be balanced by a corresponding decrement (via resume()
* or onUndoSuspend()), or JobQueue::stop() will hang.
*/
void
onSuspend();
/**
* Decrement nSuspend_ without resuming.
* Used to undo onSuspend() when a scheduled post() fails
* (e.g. JobQueue is stopping).
*/
void
onUndoSuspend();
/**
* Suspend the coroutine.
* The awaiter's await_suspend() increments nSuspend_ before the
* coroutine actually suspends. The caller must later call post()
* or resume() to continue execution.
*
* @return An awaiter for use with `co_await runner->suspend()`
*/
auto
suspend();
/**
* Suspend the coroutine and immediately repost it on the
* JobQueue. Combines suspend() + post() atomically inside
* await_suspend, so there is no window where an external
* event could race between the two.
*
* Equivalent to JobQueueAwaiter but defined as an inline
* awaiter returned from a member function. This avoids a
* GCC-12 coroutine codegen bug where an external awaiter
* struct (JobQueueAwaiter) used at multiple co_await points
* corrupts the coroutine state machine's resume index,
* causing the coroutine to hang on the third resumption.
*
* @return An awaiter for use with `co_await runner->yieldAndPost()`
*/
auto
yieldAndPost();
/**
* Schedule coroutine resumption as a job on the JobQueue.
* Captures shared_from_this() to prevent this runner from being
* destroyed while the job is queued.
*
* @return true if the job was accepted; false if the JobQueue
* is stopping (caller must handle cleanup)
*/
bool
post();
/**
* Resume the coroutine on the current thread.
* Decrements nSuspend_, swaps in LocalValues, resumes the
* coroutine handle, swaps out LocalValues, and notifies join()
* waiters. Lock ordering (sequential, non-overlapping):
* jq_.mutex_ -> mutex_ -> mutexRun_.
*
* @pre post() must have been called before resume(). Direct
* calls without a prior post() will corrupt runCount_
* and break join().
*/
void
resume();
/**
* @return true if the coroutine has not yet run to completion
*/
bool
runnable() const;
/**
* Handle early termination when the coroutine never ran.
* Decrements nSuspend_ and destroys the coroutine frame to
* break the shared_ptr cycle (frame -> lambda -> runner -> frame).
* Called by postCoroTask() when post() fails.
*/
void
expectEarlyExit();
/**
* Block until all pending/active resume operations complete.
* Uses cv_ + mutexRun_ to wait until runCount_ reaches 0.
* Warning: deadlocks if the coroutine is suspended and never posted.
*/
void
join();
};
using JobFunction = std::function<void()>;
JobQueue(
@@ -197,6 +612,20 @@ public:
std::shared_ptr<Coro>
postCoro(JobType t, std::string const& name, F&& f);
/**
* Creates a C++20 coroutine and adds a job to the queue to run it.
*
* @param t The type of job.
* @param name Name of the job.
* @param f Callable with signature
* CoroTask<void>(std::shared_ptr<CoroTaskRunner>).
*
* @return shared_ptr to posted CoroTaskRunner. nullptr if not successful.
*/
template <class F>
std::shared_ptr<CoroTaskRunner>
postCoroTask(JobType t, std::string const& name, F&& f);
/**
* Jobs waiting at this priority.
*/
@@ -419,7 +848,8 @@ private:
} // namespace xrpl
#include <xrpl/core/Coro.ipp> // IWYU pragma: keep
#include <xrpl/core/Coro.ipp> // IWYU pragma: keep
#include <xrpl/core/CoroTaskRunner.ipp> // IWYU pragma: keep
namespace xrpl {
@@ -442,4 +872,82 @@ JobQueue::postCoro(JobType t, std::string const& name, F&& f)
return coro;
}
// postCoroTask — entry point for launching a C++20 coroutine on the JobQueue.
//
// Control Flow
// ============
//
// postCoroTask(t, name, f)
// |
// +-- 1. Reserve a jobCounter_ slot — reject if JQ shutting down
// |
// +-- 2. ++nSuspend_ (mirrors Boost Coro ctor's implicit yield)
// | The coroutine is "suspended" from the JobQueue's perspective
// | even though it hasn't run yet — this keeps the JQ shutdown
// | logic correct (it waits for nSuspend_ to reach 0).
// |
// +-- 3. Create CoroTaskRunner (shared_ptr, ref-counted)
// |
// +-- 4. runner->init(f)
// | +-- Heap-allocate the lambda (FuncStore) to prevent
// | | dangling captures in the coroutine frame
// | +-- task_ = f(shared_from_this())
// | [coroutine created but NOT started — lazy initial_suspend]
// |
// +-- 5. runner->post()
// | +-- addJob(type_, [resume]{}) → resume on worker thread
// | +-- failure (JQ stopping):
// | +-- runner->expectEarlyExit()
// | | --nSuspend_, destroy coroutine frame
// | +-- return nullptr
// |
// +-- 6. Release the jobCounter_ slot (on return)
//
// Why reserve a jobCounter_ slot?
// ===============================
// JobQueue::stop() joins jobCounter_ before it acquires mutex_ and asserts
// nSuspend_ == 0. Without a reservation, stop() could run to completion in
// the window between the ++nSuspend_ in step 2 and the balancing post() or
// expectEarlyExit() in step 5, tripping that assert. Holding a slot blocks
// stop()'s join() for the whole function, closing the window. wrap() also
// returns nullopt once the counter is joined, so it doubles as the shutdown
// check — a plain stopping_ read cannot do this, because the read and the
// ++nSuspend_ are not a single atomic step.
//
// Why async post() instead of synchronous resume()?
// ==================================================
// The initial dispatch MUST use async post() so the coroutine body runs on
// a JobQueue worker thread, not the caller's thread. resume() swaps the
// caller's thread-local LocalValues with the coroutine's private copy.
// If the coroutine mutates LocalValues (e.g. thread_specific_storage test),
// those mutations bleed back into the caller's thread-local state after the
// swap-out, corrupting subsequent tests that share the same thread pool.
// Async post() avoids this by running the coroutine on a worker thread whose
// LocalValues are managed by the thread pool, not by the caller.
//
template <class F>
std::shared_ptr<JobQueue::CoroTaskRunner>
JobQueue::postCoroTask(JobType t, std::string const& name, F&& f)
{
// Held until this function returns. Null once jobCounter_ is joined,
// which is how a shutting-down JobQueue rejects new coroutines.
auto const shutdownGuard = jobCounter_.wrap([]() {});
if (!shutdownGuard)
return nullptr;
{
std::scoped_lock const lock(mutex_);
++nSuspend_;
}
auto runner = std::make_shared<CoroTaskRunner>(CoroTaskRunner::CreateT{}, *this, t, name);
runner->init(std::forward<F>(f));
if (!runner->post())
{
runner->expectEarlyExit();
runner.reset();
}
return runner;
}
} // namespace xrpl

View File

@@ -0,0 +1,212 @@
#pragma once
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/JobQueue.h>
#include <coroutine>
#include <memory>
namespace xrpl {
/**
* Awaiter that suspends and immediately reschedules on the JobQueue.
* Equivalent to calling yield() followed by post() in the old Coro API.
*
* Usage:
* co_await JobQueueAwaiter{runner};
*
* What it waits for: The coroutine is re-queued as a job and resumes
* when a worker thread picks it up.
*
* Which thread resumes: A JobQueue worker thread.
*
* What await_resume() returns: void.
*
* Dependency Diagram
* ==================
*
* JobQueueAwaiter
* +----------------------------------------------+
* | + runner : shared_ptr<CoroTaskRunner> |
* +----------------------------------------------+
* | + await_ready() -> false (always suspend) |
* | + await_suspend() -> bool (suspend or cancel) |
* | + await_resume() -> void |
* +----------------------------------------------+
* | |
* | uses | uses
* v v
* CoroTaskRunner JobQueue
* .onSuspend() (via runner->post() -> addJob)
* .onUndoSuspend()
* .post()
*
* Control Flow (await_suspend)
* ============================
*
* co_await JobQueueAwaiter{runner}
* |
* +-- await_ready() -> false
* +-- await_suspend(handle)
* |
* +-- runner->onSuspend() // ++nSuspend_
* +-- runner->post() // addJob to JobQueue
* | |
* | +-- success? return noop_coroutine()
* | | // coroutine stays suspended;
* | | // worker thread will call resume()
* | +-- failure? (JQ stopping)
* | +-- runner->onUndoSuspend() // --nSuspend_
* | +-- return handle // symmetric transfer back
* | // coroutine continues immediately
* | // so it can clean up and co_return
*
* DEPRECATED — prefer `co_await runner->yieldAndPost()`
* =====================================================
*
* GCC-12 has a coroutine codegen bug where using this external awaiter
* struct at multiple co_await points in the same coroutine corrupts the
* state machine's resume index. After the second co_await, the third
* resumption enters handle().resume() but never reaches await_resume()
* or any subsequent user code — the coroutine hangs indefinitely.
*
* The fix is `co_await runner->yieldAndPost()`, which defines the
* awaiter as an inline struct inside a CoroTaskRunner member function.
* GCC-12 handles inline awaiters correctly at multiple co_await points.
*
* This struct is retained for single-use scenarios and documentation
* purposes. For any code that may use co_await in a loop or at
* multiple points, always use `runner->yieldAndPost()`.
*
* Usage Examples
* ==============
*
* 1. Yield and auto-repost (preferred — works on all compilers):
*
* CoroTask<void> handler(auto runner) {
* doPartA();
* co_await runner->yieldAndPost(); // yield + repost
* doPartB(); // runs on a worker thread
* co_return;
* }
*
* 2. Multiple yield points in a loop:
*
* CoroTask<void> batchProcessor(auto runner) {
* for (auto& item : items) {
* process(item);
* co_await runner->yieldAndPost(); // let other jobs run
* }
* co_return;
* }
*
* 3. Graceful shutdown — checking after resume:
*
* CoroTask<void> longTask(auto runner, JobQueue& jq) {
* while (hasWork()) {
* co_await runner->yieldAndPost();
* // If JQ is stopping, await_suspend resumes the coroutine
* // immediately without re-queuing. Always check
* // isStopping() to decide whether to proceed:
* if (jq.isStopping())
* co_return;
* doNextChunk();
* }
* co_return;
* }
*
* Caveats / Pitfalls
* ==================
*
* BUG-RISK: Using a stale or null runner.
* The runner shared_ptr must be valid and point to the CoroTaskRunner
* that owns the coroutine currently executing. Passing a runner from
* a different coroutine, or a default-constructed shared_ptr, is UB.
*
* BUG-RISK: Assuming resume happens on the same thread.
* After co_await, the coroutine resumes on whatever worker thread
* picks up the job. Do not rely on thread-local state unless it is
* managed through LocalValue (which CoroTaskRunner automatically
* swaps in/out).
*
* BUG-RISK: Ignoring the shutdown path.
* When the JobQueue is stopping, post() fails and await_suspend()
* resumes the coroutine immediately (symmetric transfer back to h).
* The coroutine body continues on the same thread. If your code
* after co_await assumes it was re-queued and is running on a worker
* thread, that assumption breaks during shutdown. Always handle the
* "JQ is stopping" case, either by checking jq.isStopping() or by
* letting the coroutine fall through to co_return naturally.
*
* DIFFERENCE from runner->suspend() + runner->post():
* Both JobQueueAwaiter and yieldAndPost() combine suspend + post
* in one atomic operation. With the manual suspend()/post() pattern,
* there is a window between the two calls where an external event
* could race. The atomic awaiters remove that window — onSuspend()
* and post() happen within the same await_suspend() call while the
* coroutine is guaranteed to be suspended. Use yieldAndPost() unless
* you need an external party to decide *when* to call post().
*/
struct JobQueueAwaiter
{
// The CoroTaskRunner that owns the currently executing coroutine.
std::shared_ptr<JobQueue::CoroTaskRunner> runner;
/**
* Always returns false so the coroutine suspends.
*/
// The C++ coroutine protocol mandates these awaiter names and
// instance-callable methods, which conflict with the project
// naming/static conventions.
// NOLINTBEGIN(readability-identifier-naming, readability-convert-member-functions-to-static)
[[nodiscard]] bool
await_ready() const noexcept
{
return false;
}
/**
* Increment nSuspend (equivalent to yield()) and schedule resume
* on the JobQueue (equivalent to post()). If the JobQueue is
* stopping, undoes the suspend count and transfers back to the
* coroutine so it can clean up and co_return.
*
* Returns a coroutine_handle<> (symmetric transfer) instead of
* bool to work around a GCC-12 codegen bug where bool-returning
* await_suspend leaves the coroutine in an invalid state —
* neither properly suspended nor resumed — causing a hang.
*
* WARNING: GCC-12 has an additional codegen bug where using this
* external awaiter struct at multiple co_await points in the same
* coroutine corrupts the state machine's resume index, causing the
* coroutine to hang on the third resumption. Prefer
* `co_await runner->yieldAndPost()` which uses an inline awaiter
* that GCC-12 handles correctly.
*
* @return noop_coroutine() to stay suspended (job posted);
* the caller's handle to resume immediately (JQ stopping)
*/
std::coroutine_handle<>
await_suspend(std::coroutine_handle<> h)
{
XRPL_ASSERT(runner, "xrpl::JobQueueAwaiter::await_suspend : runner is valid");
runner->onSuspend();
if (!runner->post())
{
// JobQueue is stopping. Undo the suspend count and
// transfer back to the coroutine so it can clean up
// and co_return.
runner->onUndoSuspend();
return h;
}
return std::noop_coroutine();
}
void
await_resume() const noexcept
{
}
// NOLINTEND(readability-identifier-naming, readability-convert-member-functions-to-static)
};
} // namespace xrpl

View File

@@ -364,16 +364,6 @@ constexpr std::uint32_t kMaxInvestmentPeriod = std::chrono::seconds{std::chrono:
*/
constexpr std::uint8_t kMaxAssetCheckDepth = 5;
/**
* Maximum length of a Data field in Escrow object that can be updated by WASM code.
*/
constexpr std::size_t kMaxWasmDataLength = 1 * 1024; // 1KB
/**
* Maximum amount of data transfer across hostfunction<->wasm border.
*/
constexpr std::size_t kWasmTransferLimit = 1 << 20; // 1MB
/**
* A ledger index.
*/

View File

@@ -129,10 +129,8 @@ enum TEMcodes : TERUnderlyingType {
temARRAY_TOO_LARGE,
temBAD_TRANSFER_FEE,
temINVALID_INNER_BATCH,
temBAD_MPT,
temBAD_CIPHERTEXT,
temBAD_WASM,
};
//------------------------------------------------------------------------------
@@ -372,7 +370,6 @@ enum TECcodes : TERUnderlyingType {
tecNO_DELEGATE_PERMISSION = 198,
tecBAD_PROOF = 199,
tecNO_SPONSOR_PERMISSION = 200,
tecOUT_OF_GAS = 201,
};
//------------------------------------------------------------------------------

View File

@@ -1,419 +0,0 @@
#pragma once
#include <rust/cxx.h>
#include <cstdint>
namespace xrpl {
// `xrpl::HostFunctions` is forward-declared rather than included: this header is
// `include!()`d by the cxxbridge-generated translation unit, whose target gets only the
// project's `include/` directory - not the Boost paths that HostFunc.h -> Slice.h ->
// strHex.h transitively need. A reference member and declarations alone do not require a
// complete type; HostContext.cpp, compiled into libxrpl, includes the real header.
class HostFunctions;
// Defined by the cxx bridge, which emits it into `xrpl_wasm_vm_ffi_cxxbridge/lib.h` from the
// declaration in `crates/xrpl-wasm-vm-ffi` - so the data types and their wire values are
// written once, in Rust, rather than kept in step with a copy here.
//
// Forward-declared for the reason `HostFunctions` above is: that generated header includes
// this one, so naming its definition here would be circular. A scoped enum with a fixed
// underlying type needs no definition to appear in a signature; `HostContext.cpp` includes
// the generated header for the `switch`.
enum class TraceDataType : std::int32_t;
// The host handed to the Rust wasm engine: one method per entry in the wasm host ABI,
// each forwarding to `xrpl::HostFunctions` - the single source of truth for ledger
// access - and lowering its typed `std::expected` result onto the ABI's wire form.
//
// Every method is `noexcept`, and every body catches everything: a C++ exception
// unwinding into the Rust frames that called it would be undefined behaviour, so a caught
// one leaves here as `HostFunctionError::InternalFatal`, which the engine reads as a fatal
// error and reports as `tecINTERNAL`.
//
// Not an owner: it borrows the `HostFunctions` it is built over for the length of one run.
class HostContext
{
// Non-const so a host function that mutates (`cacheLedgerObj`, `updateData`) can be
// reached from the `const` methods below: constness of the reference is not
// constness of the referent.
HostFunctions& hostFunctions_;
public:
HostContext(HostFunctions& hostFunctions);
// A byte-producing call is handed `out` - a slice aliasing either guest linear
// memory or the engine's output buffer - writes the value only if the whole of it
// fits, and returns the value's *true* length, which may exceed `out`. That is how a
// guest learns the size to ask for, and it is why these methods never need to know
// the guest's capacity: the engine owns the buffer-fit, field-cap and transfer-budget
// rules and derives all three from the length returned here.
//
// A negative return is a `HostFunctionError` code.
[[nodiscard]] std::int32_t
getLedgerSqn(rust::Slice<std::uint8_t> out) const noexcept;
[[nodiscard]] std::int32_t
getParentLedgerTime(rust::Slice<std::uint8_t> out) const noexcept;
[[nodiscard]] std::int32_t
getParentLedgerHash(rust::Slice<std::uint8_t> out) const noexcept;
[[nodiscard]] std::int32_t
getBaseFee(rust::Slice<std::uint8_t> out) const noexcept;
// The amendment is either a 32-byte id or a name; a 32-byte input is tried as an
// id first and falls back to a name lookup. Answers 1 or 0, or a negative
// `HostFunctionError` code.
[[nodiscard]] std::int32_t
isAmendmentEnabled(rust::Slice<std::uint8_t const> amendment) const noexcept;
// The object id must be a 32-byte uint256, else `InvalidParams`. `cacheIdx` selects
// the slot (0 = pick a free one). Answers the slot used, or a negative
// `HostFunctionError` code.
[[nodiscard]] std::int32_t
cacheLedgerObj(rust::Slice<std::uint8_t const> objId, std::int32_t cacheIdx) const noexcept;
[[nodiscard]] std::int32_t
getTxField(std::int32_t field, rust::Slice<std::uint8_t> out) const noexcept;
[[nodiscard]] std::int32_t
getCurrentLedgerObjField(std::int32_t field, rust::Slice<std::uint8_t> out) const noexcept;
[[nodiscard]] std::int32_t
getLedgerObjField(std::int32_t cacheIdx, std::int32_t field, rust::Slice<std::uint8_t> out)
const noexcept;
// The locator is a path of little-endian i32 steps, so its byte length must be a
// non-zero multiple of 4, else `LocatorMalformed`.
[[nodiscard]] std::int32_t
getTxNestedField(rust::Slice<std::uint8_t const> locator, rust::Slice<std::uint8_t> out)
const noexcept;
[[nodiscard]] std::int32_t
getCurrentLedgerObjNestedField(
rust::Slice<std::uint8_t const> locator,
rust::Slice<std::uint8_t> out) const noexcept;
[[nodiscard]] std::int32_t
getLedgerObjNestedField(
std::int32_t cacheIdx,
rust::Slice<std::uint8_t const> locator,
rust::Slice<std::uint8_t> out) const noexcept;
// Answers the array's element count directly, or a negative `HostFunctionError`
// code (`NoArray` if the field is not an array).
[[nodiscard]] std::int32_t
getTxArrayLen(std::int32_t field) const noexcept;
[[nodiscard]] std::int32_t
getCurrentLedgerObjArrayLen(std::int32_t field) const noexcept;
[[nodiscard]] std::int32_t
getLedgerObjArrayLen(std::int32_t cacheIdx, std::int32_t field) const noexcept;
[[nodiscard]] std::int32_t
getTxNestedArrayLen(rust::Slice<std::uint8_t const> locator) const noexcept;
[[nodiscard]] std::int32_t
getCurrentLedgerObjNestedArrayLen(rust::Slice<std::uint8_t const> locator) const noexcept;
[[nodiscard]] std::int32_t
getLedgerObjNestedArrayLen(std::int32_t cacheIdx, rust::Slice<std::uint8_t const> locator)
const noexcept;
// Answers 1/0 for a valid/invalid signature, or a negative `HostFunctionError`.
[[nodiscard]] std::int32_t
checkSignature(
rust::Slice<std::uint8_t const> message,
rust::Slice<std::uint8_t const> signature,
rust::Slice<std::uint8_t const> pubkey) const noexcept;
// The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
accountKeylet(rust::Slice<std::uint8_t const> account, rust::Slice<std::uint8_t> out)
const noexcept;
// Each asset is decoded by length (24 = MPT, 20 = XRP, 40 = issue), else
// `InvalidParams`. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
ammKeylet(
rust::Slice<std::uint8_t const> asset1,
rust::Slice<std::uint8_t const> asset2,
rust::Slice<std::uint8_t> out) const noexcept;
// The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
checkKeylet(
rust::Slice<std::uint8_t const> account,
std::int32_t seq,
rust::Slice<std::uint8_t> out) const noexcept;
// Subject and issuer must each be 20 bytes, else `InvalidParams`. Writes the
// 32-byte keylet.
[[nodiscard]] std::int32_t
credentialKeylet(
rust::Slice<std::uint8_t const> subject,
rust::Slice<std::uint8_t const> issuer,
rust::Slice<std::uint8_t const> credentialType,
rust::Slice<std::uint8_t> out) const noexcept;
// Both accounts must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
delegateKeylet(
rust::Slice<std::uint8_t const> account,
rust::Slice<std::uint8_t const> authorize,
rust::Slice<std::uint8_t> out) const noexcept;
// Both accounts must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
depositPreauthKeylet(
rust::Slice<std::uint8_t const> account,
rust::Slice<std::uint8_t const> authorize,
rust::Slice<std::uint8_t> out) const noexcept;
// The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
didKeylet(rust::Slice<std::uint8_t const> account, rust::Slice<std::uint8_t> out)
const noexcept;
// The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
escrowKeylet(
rust::Slice<std::uint8_t const> account,
std::int32_t seq,
rust::Slice<std::uint8_t> out) const noexcept;
// Both accounts and the currency must each be 20 bytes, else `InvalidParams`.
// Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
trustLineKeylet(
rust::Slice<std::uint8_t const> account1,
rust::Slice<std::uint8_t const> account2,
rust::Slice<std::uint8_t const> currency,
rust::Slice<std::uint8_t> out) const noexcept;
// The issuer id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
mptokenIssuanceKeylet(
rust::Slice<std::uint8_t const> issuer,
std::int32_t seq,
rust::Slice<std::uint8_t> out) const noexcept;
// The MPT id must be 24 bytes and the holder 20, else `InvalidParams`. Writes the
// 32-byte keylet.
[[nodiscard]] std::int32_t
mptokenKeylet(
rust::Slice<std::uint8_t const> mptid,
rust::Slice<std::uint8_t const> holder,
rust::Slice<std::uint8_t> out) const noexcept;
// The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
nftokenOfferKeylet(
rust::Slice<std::uint8_t const> account,
std::int32_t seq,
rust::Slice<std::uint8_t> out) const noexcept;
// The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
offerKeylet(
rust::Slice<std::uint8_t const> account,
std::int32_t seq,
rust::Slice<std::uint8_t> out) const noexcept;
// The account id must be 20 bytes, else `InvalidParams`. `docId` carries the
// guest's u32 as its i32 bit pattern. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
oracleKeylet(
rust::Slice<std::uint8_t const> account,
std::int32_t docId,
rust::Slice<std::uint8_t> out) const noexcept;
// Both account ids must be 20 bytes, else `InvalidParams`. `seq` carries the
// guest's u32 as its i32 bit pattern. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
paychannelKeylet(
rust::Slice<std::uint8_t const> account,
rust::Slice<std::uint8_t const> destination,
std::int32_t seq,
rust::Slice<std::uint8_t> out) const noexcept;
// The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
permissionedDomainKeylet(
rust::Slice<std::uint8_t const> account,
std::int32_t seq,
rust::Slice<std::uint8_t> out) const noexcept;
// The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
signerListKeylet(rust::Slice<std::uint8_t const> account, rust::Slice<std::uint8_t> out)
const noexcept;
// The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
ticketKeylet(
rust::Slice<std::uint8_t const> account,
std::int32_t seq,
rust::Slice<std::uint8_t> out) const noexcept;
// The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's
// u32 as its i32 bit pattern. Writes the 32-byte keylet.
[[nodiscard]] std::int32_t
vaultKeylet(
rust::Slice<std::uint8_t const> account,
std::int32_t seq,
rust::Slice<std::uint8_t> out) const noexcept;
[[nodiscard]] std::int32_t
sha512Half(rust::Slice<std::uint8_t const> data, rust::Slice<std::uint8_t> out) const noexcept;
// Renders `data` as `dataType` says, and hands the text to `HostFunctions::trace`, which
// is what puts it in this node's log.
//
// The one call that answers nothing: the guest's wasm function has no result, and this
// node's own log is the only thing a trace touches, so a buffer that does not hold what
// it claims is logged here and dropped rather than reported to a contract.
void
trace(rust::Str msg, rust::Slice<std::uint8_t const> data, TraceDataType dataType)
const noexcept;
// Stores `data` as the current object's data field and returns the number of bytes
// stored, or a negative `HostFunctionError` code.
[[nodiscard]] std::int32_t
updateData(rust::Slice<std::uint8_t const> data) const noexcept;
// The account id must be 20 bytes and the nft id 32 bytes, else `InvalidParams`.
// Writes the token's URI bytes.
[[nodiscard]] std::int32_t
getNFT(
rust::Slice<std::uint8_t const> account,
rust::Slice<std::uint8_t const> nftId,
rust::Slice<std::uint8_t> out) const noexcept;
// The nft id must be 32 bytes, else `InvalidParams`. Writes the 20-byte issuer
// account encoded in the id.
[[nodiscard]] std::int32_t
getNFTIssuer(rust::Slice<std::uint8_t const> nftId, rust::Slice<std::uint8_t> out)
const noexcept;
// The nft id must be 32 bytes, else `InvalidParams`. Writes the taxon as its four
// little-endian bytes.
[[nodiscard]] std::int32_t
getNFTTaxon(rust::Slice<std::uint8_t const> nftId, rust::Slice<std::uint8_t> out)
const noexcept;
// The nft id must be 32 bytes, else `InvalidParams`. Returns the flags, or a
// negative `HostFunctionError` code.
[[nodiscard]] std::int32_t
getNFTFlags(rust::Slice<std::uint8_t const> nftId) const noexcept;
// The nft id must be 32 bytes, else `InvalidParams`. Returns the transfer fee, or a
// negative `HostFunctionError` code.
[[nodiscard]] std::int32_t
getNFTTransferFee(rust::Slice<std::uint8_t const> nftId) const noexcept;
// The nft id must be 32 bytes, else `InvalidParams`. Writes the sequence number as
// its four little-endian bytes.
[[nodiscard]] std::int32_t
getNFTSequence(rust::Slice<std::uint8_t const> nftId, rust::Slice<std::uint8_t> out)
const noexcept;
// Float / number arithmetic. A float is an XRPL `Number` in serialized form;
// `mode` is a rounding mode. Each writes the result float bytes unless noted.
[[nodiscard]] std::int32_t
floatFromInt(std::int64_t x, std::int32_t mode, rust::Slice<std::uint8_t> out) const noexcept;
// The integer region must be eight bytes, else `InvalidParams`.
[[nodiscard]] std::int32_t
floatFromUint(
rust::Slice<std::uint8_t const> x,
std::int32_t mode,
rust::Slice<std::uint8_t> out) const noexcept;
// `amount` must be a serialized `STAmount`, else `InvalidParams`.
[[nodiscard]] std::int32_t
floatFromSTAmount(
rust::Slice<std::uint8_t const> amount,
std::int32_t mode,
rust::Slice<std::uint8_t> out) const noexcept;
// `number` must be a serialized `STNumber`, else `InvalidParams`.
[[nodiscard]] std::int32_t
floatFromSTNumber(
rust::Slice<std::uint8_t const> number,
std::int32_t mode,
rust::Slice<std::uint8_t> out) const noexcept;
// Rounds the float to an integer, written as its eight little-endian bytes.
[[nodiscard]] std::int32_t
floatToInt(rust::Slice<std::uint8_t const> x, std::int32_t mode, rust::Slice<std::uint8_t> out)
const noexcept;
// Writes the mantissa (eight little-endian bytes) and the exponent (four little-
// endian bytes) to two output regions; returns their total size.
[[nodiscard]] std::int32_t
floatToMantExp(
rust::Slice<std::uint8_t const> x,
rust::Slice<std::uint8_t> mantissaOut,
rust::Slice<std::uint8_t> exponentOut) const noexcept;
[[nodiscard]] std::int32_t
floatFromMantExp(
std::int64_t mantissa,
std::int32_t exponent,
std::int32_t mode,
rust::Slice<std::uint8_t> out) const noexcept;
// Returns a negative, zero, or positive scalar as `x` is less than, equal to, or
// greater than `y`, or a negative `HostFunctionError` code on failure.
[[nodiscard]] std::int32_t
floatCompare(rust::Slice<std::uint8_t const> x, rust::Slice<std::uint8_t const> y)
const noexcept;
[[nodiscard]] std::int32_t
floatAdd(
rust::Slice<std::uint8_t const> x,
rust::Slice<std::uint8_t const> y,
std::int32_t mode,
rust::Slice<std::uint8_t> out) const noexcept;
[[nodiscard]] std::int32_t
floatSubtract(
rust::Slice<std::uint8_t const> x,
rust::Slice<std::uint8_t const> y,
std::int32_t mode,
rust::Slice<std::uint8_t> out) const noexcept;
[[nodiscard]] std::int32_t
floatMultiply(
rust::Slice<std::uint8_t const> x,
rust::Slice<std::uint8_t const> y,
std::int32_t mode,
rust::Slice<std::uint8_t> out) const noexcept;
[[nodiscard]] std::int32_t
floatDivide(
rust::Slice<std::uint8_t const> x,
rust::Slice<std::uint8_t const> y,
std::int32_t mode,
rust::Slice<std::uint8_t> out) const noexcept;
[[nodiscard]] std::int32_t
floatPower(
rust::Slice<std::uint8_t const> x,
std::int32_t n,
std::int32_t mode,
rust::Slice<std::uint8_t> out) const noexcept;
};
} // namespace xrpl

View File

@@ -1,464 +0,0 @@
#pragma once
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <cstdint>
#include <expected>
#include <string>
#include <string_view>
namespace xrpl {
namespace wasm_float {
std::string
floatToString(Slice const& data);
std::expected<Bytes, HostFunctionError>
floatFromIntImpl(int64_t x, int32_t mode);
std::expected<Bytes, HostFunctionError>
floatFromUintImpl(uint64_t x, int32_t mode);
std::expected<Bytes, HostFunctionError>
floatFromSTAmountImpl(STAmount const& x, int32_t mode);
std::expected<Bytes, HostFunctionError>
floatFromSTNumberImpl(STNumber const& x, int32_t mode);
std::expected<int64_t, HostFunctionError>
floatToIntImpl(Slice const& x, int32_t mode);
std::expected<FloatPair, HostFunctionError>
floatToMantExpImpl(Slice const& x);
std::expected<Bytes, HostFunctionError>
floatFromMantExpImpl(int64_t mantissa, int32_t exponent, int32_t mode);
std::expected<int32_t, HostFunctionError>
floatCompareImpl(Slice const& x, Slice const& y);
std::expected<Bytes, HostFunctionError>
floatAddImpl(Slice const& x, Slice const& y, int32_t mode);
std::expected<Bytes, HostFunctionError>
floatSubtractImpl(Slice const& x, Slice const& y, int32_t mode);
std::expected<Bytes, HostFunctionError>
floatMultiplyImpl(Slice const& x, Slice const& y, int32_t mode);
std::expected<Bytes, HostFunctionError>
floatDivideImpl(Slice const& x, Slice const& y, int32_t mode);
std::expected<Bytes, HostFunctionError>
floatPowerImpl(Slice const& x, int32_t n, int32_t mode);
} // namespace wasm_float
// Intended to work only through wasm runtime. Don't call them directly, except with unit tests
class HostFunctions
{
protected:
beast::Journal j_;
public:
HostFunctions(beast::Journal j = beast::Journal{beast::Journal::getNullSink()}) : j_(j)
{
}
[[nodiscard]] beast::Journal
getJournal() const
{
return j_;
}
// LCOV_EXCL_START
[[nodiscard]] virtual bool
checkSelf() const
{
return true;
}
[[nodiscard]] [[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
getLedgerSqn() const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
getParentLedgerTime() const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Hash, HostFunctionError>
getParentLedgerHash() const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<uint32_t, HostFunctionError>
getBaseFee() const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
isAmendmentEnabled(uint256 const& amendmentId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
isAmendmentEnabled(std::string_view const& amendmentName) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
virtual std::expected<int32_t, HostFunctionError>
cacheLedgerObj(uint256 const& objId, int32_t cacheIdx)
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getTxField(SField const& fname) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getCurrentLedgerObjField(SField const& fname) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getLedgerObjField(int32_t cacheIdx, SField const& fname) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getTxNestedField(FieldLocator const& locator) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getCurrentLedgerObjNestedField(FieldLocator const& locator) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getLedgerObjNestedField(int32_t cacheIdx, FieldLocator const& locator) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getTxArrayLen(SField const& fname) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getCurrentLedgerObjArrayLen(SField const& fname) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getLedgerObjArrayLen(int32_t cacheIdx, SField const& fname) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getTxNestedArrayLen(FieldLocator const& locator) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getCurrentLedgerObjNestedArrayLen(FieldLocator const& locator) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getLedgerObjNestedArrayLen(int32_t cacheIdx, FieldLocator const& locator) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
virtual std::expected<int32_t, HostFunctionError>
updateData(Slice const& data)
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
checkSignature(Slice const& message, Slice const& signature, Slice const& pubkey) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Hash, HostFunctionError>
computeSha512HalfHash(Slice const& data) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
accountKeylet(AccountID const& account) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
ammKeylet(Asset const& issue1, Asset const& issue2) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
checkKeylet(AccountID const& account, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
credentialKeylet(AccountID const& subject, AccountID const& issuer, Slice const& credentialType)
const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
didKeylet(AccountID const& account) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
delegateKeylet(AccountID const& account, AccountID const& authorize) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
depositPreauthKeylet(AccountID const& account, AccountID const& authorize) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
escrowKeylet(AccountID const& account, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
trustLineKeylet(AccountID const& account1, AccountID const& account2, Currency const& currency)
const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
mptokenIssuanceKeylet(AccountID const& issuer, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
mptokenKeylet(MPTID const& mptid, AccountID const& holder) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
nftokenOfferKeylet(AccountID const& account, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
offerKeylet(AccountID const& account, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
oracleKeylet(AccountID const& account, std::uint32_t docId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
paychannelKeylet(AccountID const& account, AccountID const& destination, std::uint32_t seq)
const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
permissionedDomainKeylet(AccountID const& account, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
signerListKeylet(AccountID const& account) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
ticketKeylet(AccountID const& account, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
vaultKeylet(AccountID const& account, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getNFT(AccountID const& account, uint256 const& nftId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getNFTIssuer(uint256 const& nftId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
getNFTTaxon(uint256 const& nftId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getNFTFlags(uint256 const& nftId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getNFTTransferFee(uint256 const& nftId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
getNFTSequence(uint256 const& nftId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
// A no-op rather than Unimplemented: trace only writes to the local log.
// trace_wrap has already rendered the guest's buffer into `data`.
virtual void
trace(std::string_view const& msg, std::string_view const& data) const
{
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatFromInt(int64_t x, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatFromUint(uint64_t x, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatFromSTAmount(STAmount const& x, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatFromSTNumber(STNumber const& x, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int64_t, HostFunctionError>
floatToInt(Slice const& x, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<FloatPair, HostFunctionError>
floatToMantExp(Slice const& x) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatFromMantExp(int64_t mantissa, int32_t exponent, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
floatCompare(Slice const& x, Slice const& y) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatAdd(Slice const& x, Slice const& y, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatSubtract(Slice const& x, Slice const& y, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatMultiply(Slice const& x, Slice const& y, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatDivide(Slice const& x, Slice const& y, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatPower(Slice const& x, int32_t n, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
virtual ~HostFunctions() = default;
// LCOV_EXCL_STOP
};
} // namespace xrpl

View File

@@ -1,287 +0,0 @@
#pragma once
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/wasm/HostFunc.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <expected>
#include <memory>
#include <optional>
#include <string_view>
namespace xrpl {
// Intended to work only through wasm runtime. Don't call them directly, except with unit tests
class WasmHostFunctionsImpl : public HostFunctions
{
ApplyContext& ctx_;
Keylet leKey_;
mutable std::optional<std::shared_ptr<SLE const>> currentLedgerObj_;
static int constexpr maxCache = 256;
std::array<std::shared_ptr<SLE const>, maxCache> cache_;
std::optional<Bytes> data_;
public:
std::expected<std::shared_ptr<SLE const>, HostFunctionError>
getCurrentLedgerObj() const
{
if (!currentLedgerObj_)
currentLedgerObj_ = ctx_.view().read(leKey_);
if (*currentLedgerObj_)
return *currentLedgerObj_;
return std::unexpected(HostFunctionError::LedgerObjNotFound);
}
std::expected<int32_t, HostFunctionError>
normalizeCacheIndex(int32_t cacheIdx) const
{
--cacheIdx;
if (cacheIdx < 0 || cacheIdx >= maxCache)
return std::unexpected(HostFunctionError::SlotOutRange);
if (!cache_[cacheIdx])
return std::unexpected(HostFunctionError::EmptySlot);
return cacheIdx;
}
template <typename F>
void
log(std::string_view const& msg, F&& dataFn) const
{
#ifdef DEBUG_OUTPUT
auto& j = std::cerr;
#else
if (!getJournal().active(beast::Severity::Trace))
return;
auto j = getJournal().trace();
#endif
j << "WasmTrace[" << toShortString(leKey_.key) << "]: " << msg << " " << dataFn();
#ifdef DEBUG_OUTPUT
j << std::endl;
#endif
}
public:
WasmHostFunctionsImpl(ApplyContext& ct, Keylet const& leKey)
: HostFunctions(ct.journal), ctx_(ct), leKey_(leKey)
{
}
bool
checkSelf() const override
{
return !currentLedgerObj_ && !data_ &&
std::ranges::none_of(cache_, [](auto const& p) { return !!p; });
}
std::optional<Bytes> const&
getData() const
{
return data_;
}
std::expected<std::uint32_t, HostFunctionError>
getLedgerSqn() const override;
std::expected<std::uint32_t, HostFunctionError>
getParentLedgerTime() const override;
std::expected<Hash, HostFunctionError>
getParentLedgerHash() const override;
std::expected<std::uint32_t, HostFunctionError>
getBaseFee() const override;
std::expected<int32_t, HostFunctionError>
isAmendmentEnabled(uint256 const& amendmentId) const override;
std::expected<int32_t, HostFunctionError>
isAmendmentEnabled(std::string_view const& amendmentName) const override;
std::expected<int32_t, HostFunctionError>
cacheLedgerObj(uint256 const& objId, int32_t cacheIdx) override;
std::expected<Bytes, HostFunctionError>
getTxField(SField const& fname) const override;
std::expected<Bytes, HostFunctionError>
getCurrentLedgerObjField(SField const& fname) const override;
std::expected<Bytes, HostFunctionError>
getLedgerObjField(int32_t cacheIdx, SField const& fname) const override;
std::expected<Bytes, HostFunctionError>
getTxNestedField(FieldLocator const& locator) const override;
std::expected<Bytes, HostFunctionError>
getCurrentLedgerObjNestedField(FieldLocator const& locator) const override;
std::expected<Bytes, HostFunctionError>
getLedgerObjNestedField(int32_t cacheIdx, FieldLocator const& locator) const override;
std::expected<int32_t, HostFunctionError>
getTxArrayLen(SField const& fname) const override;
std::expected<int32_t, HostFunctionError>
getCurrentLedgerObjArrayLen(SField const& fname) const override;
std::expected<int32_t, HostFunctionError>
getLedgerObjArrayLen(int32_t cacheIdx, SField const& fname) const override;
std::expected<int32_t, HostFunctionError>
getTxNestedArrayLen(FieldLocator const& locator) const override;
std::expected<int32_t, HostFunctionError>
getCurrentLedgerObjNestedArrayLen(FieldLocator const& locator) const override;
std::expected<int32_t, HostFunctionError>
getLedgerObjNestedArrayLen(int32_t cacheIdx, FieldLocator const& locator) const override;
std::expected<int32_t, HostFunctionError>
updateData(Slice const& data) override;
std::expected<int32_t, HostFunctionError>
checkSignature(Slice const& message, Slice const& signature, Slice const& pubkey)
const override;
std::expected<Hash, HostFunctionError>
computeSha512HalfHash(Slice const& data) const override;
std::expected<Bytes, HostFunctionError>
accountKeylet(AccountID const& account) const override;
std::expected<Bytes, HostFunctionError>
ammKeylet(Asset const& issue1, Asset const& issue2) const override;
std::expected<Bytes, HostFunctionError>
checkKeylet(AccountID const& account, std::uint32_t seq) const override;
std::expected<Bytes, HostFunctionError>
credentialKeylet(AccountID const& subject, AccountID const& issuer, Slice const& credentialType)
const override;
std::expected<Bytes, HostFunctionError>
didKeylet(AccountID const& account) const override;
std::expected<Bytes, HostFunctionError>
delegateKeylet(AccountID const& account, AccountID const& authorize) const override;
std::expected<Bytes, HostFunctionError>
depositPreauthKeylet(AccountID const& account, AccountID const& authorize) const override;
std::expected<Bytes, HostFunctionError>
escrowKeylet(AccountID const& account, std::uint32_t seq) const override;
std::expected<Bytes, HostFunctionError>
trustLineKeylet(AccountID const& account1, AccountID const& account2, Currency const& currency)
const override;
std::expected<Bytes, HostFunctionError>
mptokenIssuanceKeylet(AccountID const& issuer, std::uint32_t seq) const override;
std::expected<Bytes, HostFunctionError>
mptokenKeylet(MPTID const& mptid, AccountID const& holder) const override;
std::expected<Bytes, HostFunctionError>
nftokenOfferKeylet(AccountID const& account, std::uint32_t seq) const override;
std::expected<Bytes, HostFunctionError>
offerKeylet(AccountID const& account, std::uint32_t seq) const override;
std::expected<Bytes, HostFunctionError>
oracleKeylet(AccountID const& account, std::uint32_t docId) const override;
std::expected<Bytes, HostFunctionError>
paychannelKeylet(AccountID const& account, AccountID const& destination, std::uint32_t seq)
const override;
std::expected<Bytes, HostFunctionError>
permissionedDomainKeylet(AccountID const& account, std::uint32_t seq) const override;
std::expected<Bytes, HostFunctionError>
signerListKeylet(AccountID const& account) const override;
std::expected<Bytes, HostFunctionError>
ticketKeylet(AccountID const& account, std::uint32_t seq) const override;
std::expected<Bytes, HostFunctionError>
vaultKeylet(AccountID const& account, std::uint32_t seq) const override;
std::expected<Bytes, HostFunctionError>
getNFT(AccountID const& account, uint256 const& nftId) const override;
std::expected<Bytes, HostFunctionError>
getNFTIssuer(uint256 const& nftId) const override;
std::expected<std::uint32_t, HostFunctionError>
getNFTTaxon(uint256 const& nftId) const override;
std::expected<int32_t, HostFunctionError>
getNFTFlags(uint256 const& nftId) const override;
std::expected<int32_t, HostFunctionError>
getNFTTransferFee(uint256 const& nftId) const override;
std::expected<std::uint32_t, HostFunctionError>
getNFTSequence(uint256 const& nftId) const override;
void
trace(std::string_view const& msg, std::string_view const& data) const override;
std::expected<Bytes, HostFunctionError>
floatFromInt(int64_t x, int32_t mode) const override;
std::expected<Bytes, HostFunctionError>
floatFromUint(uint64_t x, int32_t mode) const override;
std::expected<Bytes, HostFunctionError>
floatFromSTAmount(STAmount const& x, int32_t mode) const override;
std::expected<Bytes, HostFunctionError>
floatFromSTNumber(STNumber const& x, int32_t mode) const override;
std::expected<int64_t, HostFunctionError>
floatToInt(Slice const& x, int32_t mode) const override;
std::expected<FloatPair, HostFunctionError>
floatToMantExp(Slice const& x) const override;
std::expected<Bytes, HostFunctionError>
floatFromMantExp(int64_t mantissa, int32_t exponent, int32_t mode) const override;
std::expected<int32_t, HostFunctionError>
floatCompare(Slice const& x, Slice const& y) const override;
std::expected<Bytes, HostFunctionError>
floatAdd(Slice const& x, Slice const& y, int32_t mode) const override;
std::expected<Bytes, HostFunctionError>
floatSubtract(Slice const& x, Slice const& y, int32_t mode) const override;
std::expected<Bytes, HostFunctionError>
floatMultiply(Slice const& x, Slice const& y, int32_t mode) const override;
std::expected<Bytes, HostFunctionError>
floatDivide(Slice const& x, Slice const& y, int32_t mode) const override;
std::expected<Bytes, HostFunctionError>
floatPower(Slice const& x, int32_t n, int32_t mode) const override;
};
} // namespace xrpl

View File

@@ -1,41 +0,0 @@
# WASM Module for Programmable Escrows
WebAssembly execution for programmable escrows. When an escrow is finished, its contract
runs to decide whether the release conditions are met. Specification:
[XLS-0102: WASM VM](https://xls.xrpl.org/xls/XLS-0102-wasm-vm.html).
The engine itself is Rust (`crates/xrpl-wasm-vm`, over wasmi), reached through a cxx
bridge.
## What is in this directory
- **`WasmVM.h`** — the entry points xrpld calls: `runEscrowWasm` (execute a contract,
returning a result and its gas cost, or a `WasmTER`) and `preflightEscrowWasm` (screen a
module with no host and no execution). Both own their TER maps.
- **`HostFunc.h`** — the `HostFunctions` interface: one virtual per host function, each
defaulting to `Unimplemented`, returning `std::expected<T, HostFunctionError>`.
- **`HostFuncImpl.h`** — `WasmHostFunctionsImpl`, the implementation over an
`ApplyContext&`. Bodies are split across `HostFuncImpl*.cpp` by category.
- **`HostContext.h`** — the bridge's C++ half: an ABI-shaped, `noexcept` view of
`HostFunctions` that the engine calls back into. Nothing may unwind into Rust, so every
method catches everything — through `guarded()`, except `trace`, which answers the guest
nothing and so has its own catch that only logs.
- **`WasmCommon.h`** — the shared vocabulary: `HostFunctionError` (the codes a contract
sees), `Bytes`, `FieldLocator`, `WasmTER`, `adjustWasmEndianess`, which is where the
boundary's byte order is decided, and `guarded()`, the catch that turns a throwing host
body into a code the engine can read.
## Host functions
Grouped by what they reach: ledger information; transaction and ledger-object field access;
keylet construction; cryptography; float arithmetic; NFT queries; tracing.
The wire names and per-call gas costs are declared in `crates/xrpl-host-functions`
one `host_functions!` block that generates the ABI trait and the spec table. That
declaration is the single source of truth; `HostFunc.h` is the C++ side of it.
## Entry point
A module must export `escrow_finish` (`escrowFunctionName`) taking no parameters and
returning `int32_t`: positive means the conditions are met, zero or negative rejects the
finish. Everything the contract needs it asks for through a host call.

View File

@@ -1,187 +0,0 @@
#pragma once
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/TER.h>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <limits>
#include <optional>
#include <source_location>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
namespace xrpl {
using Bytes = std::vector<std::uint8_t>;
using Hash = xrpl::uint256;
using FloatPair = std::pair<int64_t, int32_t>;
enum class HostFunctionError : int32_t {
Unimplemented = -1,
FieldNotFound = -2,
BufferTooSmall = -3,
NoArray = -4,
NotLeafField = -5,
LocatorMalformed = -6,
SlotOutRange = -7,
SlotsFull = -8,
EmptySlot = -9,
LedgerObjNotFound = -10,
OutOfTransferLimit = -11,
DataFieldTooLarge = -12,
PointerOutOfBounds = -13,
NoMemExported = -14,
InvalidParams = -15,
InvalidAccount = -16,
InvalidField = -17,
IndexOutOfBounds = -18,
FloatInputMalformed = -19,
FloatComputationError = -20,
// The call was not served at all, so the engine stops the run and the transaction is
// tecINTERNAL rather than the contract being handed a code to interpret. `guarded`
// answers it for a host body that throws.
//
// The only entry outside the -1 ..= -20 range a contract reads: it needs no number
// there, and INT32_MIN cannot collide with a code appended above. Negative so that a
// reader treating it as an ordinary failure is still right.
InternalFatal = std::numeric_limits<int32_t>::min(),
};
template <typename T>
struct WasmResult
{
T result;
int64_t cost;
};
using EscrowResult = WasmResult<int32_t>;
// Engine error when wasm does not run to completion. `cost` is the gas consumed
// when meaningful (tecOUT_OF_GAS / tecFAILED_PROCESSING; caller writes it to tx
// metadata); std::nullopt for tecINTERNAL and malformed input (no gas reported).
struct WasmTER
{
TER ter;
std::optional<int64_t> cost;
};
class FieldLocator
{
int32_t const* ptr_ = nullptr;
uint32_t size_ = 0;
std::vector<int32_t> buf_;
public:
FieldLocator(std::vector<int32_t>&& buf)
: ptr_(&buf[0]), size_(buf.size()), buf_(std::move(buf))
{
}
FieldLocator(int32_t const* ptr, uint32_t const size) : ptr_(ptr), size_(size)
{
}
FieldLocator(FieldLocator const&) = delete;
FieldLocator&
operator=(FieldLocator const&) = delete;
FieldLocator(FieldLocator&&) = default;
FieldLocator&
operator=(FieldLocator&&) = default;
int32_t
operator[](unsigned i) const
{
if (i >= size_)
Throw<std::runtime_error>("index out of bounds");
return ptr_[i];
}
[[nodiscard]] uint32_t
size() const
{
return size_;
}
[[nodiscard]] int32_t const*
data() const
{
return ptr_;
}
[[nodiscard]] bool
empty() const
{
return size_ == 0;
}
};
template <typename T, size_t Size = sizeof(T)>
constexpr T
adjustWasmEndianessHlp(T x)
{
static_assert(std::is_integral_v<T>, "Only integral types");
if constexpr (Size > 1)
{
using U = std::make_unsigned_t<T>;
U u = static_cast<U>(x);
U const low = (u & 0xFF) << ((Size - 1) << 3);
u = adjustWasmEndianessHlp<U, Size - 1>(u >> 8);
return static_cast<T>(low | u);
}
return x;
}
template <typename T, size_t Size = sizeof(T)>
constexpr T
adjustWasmEndianess(T x)
{
// LCOV_EXCL_START
static_assert(std::is_integral_v<T>, "Only integral types");
if constexpr (std::endian::native == std::endian::big)
{
return adjustWasmEndianessHlp(x);
}
return x;
// LCOV_EXCL_STOP
}
constexpr int32_t
hfErrorToInt(HostFunctionError e)
{
return static_cast<int32_t>(e);
}
template <class Body>
std::invoke_result_t<Body>
guarded(
beast::Journal journal,
std::invoke_result_t<Body> onThrow,
Body&& body,
std::source_location const location = std::source_location::current()) noexcept
{
try
{
return body();
}
catch (std::exception const& e)
{
JLOG(journal.error()) << "wasm: " << location.function_name() << " threw: " << e.what();
}
catch (...)
{
JLOG(journal.error()) << "wasm: " << location.function_name() << " threw";
}
return onThrow;
}
} // namespace xrpl

View File

@@ -1,50 +0,0 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/wasm/HostFunc.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <cstdint>
#include <expected>
#include <string_view>
namespace xrpl {
// The export a programmable escrow's contract is run through.
std::string_view inline constexpr escrowFunctionName = "escrow_finish";
// Run `wasmCode`'s `funcName` export with `gasLimit` gas, servicing its host calls
// through `hfs`.
//
// On success the result is what the contract returned - positive means the escrow may
// finish - together with the gas it consumed. On failure it is the TER to apply and,
// when the number means anything, the gas to write to transaction metadata: a contract
// that traps or exhausts its budget is charged for what it burned, while a `tecINTERNAL`
// reports no cost because the fault is the node's rather than the transaction's.
std::expected<EscrowResult, WasmTER>
runEscrowWasm(
Bytes const& wasmCode,
HostFunctions& hfs,
std::int64_t gasLimit,
std::string_view funcName = escrowFunctionName) noexcept;
// Screen `wasmCode`: whether `runEscrowWasm` would refuse it before the contract's
// first instruction. Compiles the module and reads its imports and exports; runs
// nothing.
//
// Takes no `HostFunctions`, because the verdict comes from the compiled module alone.
// That is what makes this callable from a transactor's `preflight`, which has no view
// to build a host over.
//
// `temBAD_WASM` for every fault in the module - the transaction carries something this
// engine cannot run, so it is refused before it can reach the ledger.
// `telFAILED_PROCESSING` if the engine itself failed: nothing was learned about the
// module, and a defect here is not evidence that the transaction is malformed.
NotTEC
preflightEscrowWasm(
Bytes const& wasmCode,
beast::Journal j,
std::string_view funcName = escrowFunctionName) noexcept;
} // namespace xrpl

View File

@@ -126,15 +126,15 @@ release defaults to 1 and is overridable with `-Dpkg_release=N`.
Packages are published to the XRPLF repositories on Sonatype Nexus at
`https://packages.xrplf.org`. The `release-info` action decides the channel from
the event, and `publish_pkg.sh` maps that channel to its repositories:
the event, and `publish_pkg.sh` maps that channel to a repository pair:
| Event | Version | Channel | DEB repository | RPM upload repository |
| ------------------------ | ----------------- | -------------- | ------------------ | ------------------------- |
| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable-hosted` |
| tag | `X.Y.Z-rcN` | `unstable` | `deb-unstable` | `rpm-unstable-hosted` |
| tag | `X.Y.Z-bN` | `experimental` | `deb-experimental` | `rpm-experimental-hosted` |
| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop-hosted` |
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` |
| Event | Version | Channel | DEB repository | RPM repository |
| ------------------------ | ----------------- | -------------- | ------------------ | ------------------ |
| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable` |
| tag | `X.Y.Z-rcN` | `unstable` | `deb-unstable` | `rpm-unstable` |
| tag | `X.Y.Z-bN` | `experimental` | `deb-experimental` | `rpm-experimental` |
| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop` |
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private` |
Only a tag names a channel — do not extend that to `develop`, where
`BuildInfo.cpp`'s `versionString` moves through `-bN`, `-rcN` and even the final
@@ -155,15 +155,12 @@ Conan remote.
Nexus owns the repository metadata; nothing here indexes anything. Worth knowing:
- Each apt-hosted repository needs a distribution (ours use `any`) and a PGP
signing keypair configured in Nexus, which rejects one created without a
keypair. Nexus signs the apt metadata with it, never the packages.
- Hosted yum repositories cannot be signed by Nexus, so each `rpm-<channel>-hosted`
repository sits behind a `rpm-<channel>` yum group repository whose metadata
Nexus signs. Uploads go to the hosted repository; clients point at the group
and verify the metadata with `repo_gpgcheck=1`. Nexus never signs the RPMs
themselves, so `sign_rpm.sh` signs them before they are uploaded, and clients
verify them with `gpgcheck=1`.
- Each apt-hosted repository needs a distribution and a PGP signing keypair
configured in Nexus, which rejects one created without a keypair. Nexus signs
the apt metadata with it, never the packages.
- Hosted yum repositories cannot be signed by Nexus at all, so `sign_rpm.sh`
signs the RPMs before they are uploaded, and rpm clients verify with
`gpgcheck=1` rather than `repo_gpgcheck=1`.
- yum metadata is rebuilt asynchronously, so a successful publish is not
immediately installable.
- Each job uploads only what it built, and uploads are not transactional, so a

View File

@@ -7,13 +7,10 @@ set -euo pipefail
# Usage: publish_pkg.sh <channel> [package-dir]
#
# channel release channel, selecting the 'deb-<channel>' and
# 'rpm-<channel>-hosted' repositories
# 'rpm-<channel>' repository pair
# package-dir searched recursively for *.deb, *.ddeb and *.rpm ('build' by
# default)
#
# RPMs are uploaded to the hosted repository, but yum clients install from the
# 'rpm-<channel>' group repository in front of it, which serves signed metadata.
#
# NEXUS_USERNAME and NEXUS_PASSWORD are required. NEXUS_URL overrides the target
# instance, and DRY_RUN=1 lists the uploads without performing them.
@@ -27,7 +24,7 @@ if [[ -z "${channel}" ]]; then
fi
deb_repo="deb-${channel}"
rpm_repo="rpm-${channel}-hosted"
rpm_repo="rpm-${channel}"
if [[ -z "${DRY_RUN:-}" ]]; then
: "${NEXUS_USERNAME:?is required}" "${NEXUS_PASSWORD:?is required}"

View File

@@ -1,10 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
# Sign the RPMs built by build_pkg.sh. Nexus signs the yum repository metadata
# (via the 'rpm-<channel>' group repository), but never the packages themselves,
# so they carry their own signature. Clients verify the packages with gpgcheck=1
# and the metadata with repo_gpgcheck=1.
# Sign the RPMs built by build_pkg.sh. Nexus cannot sign hosted yum metadata, so
# the packages carry the signature themselves and rpm clients verify them with
# gpgcheck=1.
#
# Usage: sign_rpm.sh [package-dir]
#
@@ -13,9 +12,8 @@ set -euo pipefail
# PKG_SIGNING_KEY must hold an armoured PGP private key. It has no flag, to keep
# the key out of the process list.
#
# The DEBs are deliberately not signed: embedded DEB signatures exist (debsigs),
# but apt does not verify them by default and trusts the repository metadata,
# which Nexus signs, instead.
# There is no DEB equivalent: apt trusts the repository metadata, which Nexus
# signs, rather than the packages themselves.
pkg_dir="${1:-build}"

View File

@@ -108,7 +108,6 @@ transResults()
MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."),
MAKE_ERROR(tecBAD_PROOF, "Proof cannot be verified"),
MAKE_ERROR(tecNO_SPONSOR_PERMISSION, "Sponsor has not authorized this transaction."),
MAKE_ERROR(tecOUT_OF_GAS, "The WASM code ran out of gas during execution."),
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
@@ -205,7 +204,6 @@ transResults()
MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."),
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."),
MAKE_ERROR(temBAD_WASM, "Malformed: Provided WASM code is invalid."),
MAKE_ERROR(terRETRY, "Retry transaction."),
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),

File diff suppressed because it is too large Load Diff

View File

@@ -1,59 +0,0 @@
#include <xrpl/tx/wasm/HostFuncImpl.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <cstdint>
#include <expected>
#include <string_view>
namespace xrpl {
// =========================================================
// SECTION: WRITE FUNCTION
// =========================================================
std::expected<int32_t, HostFunctionError>
WasmHostFunctionsImpl::updateData(Slice const& data)
{
if (data.size() > kMaxWasmDataLength)
return std::unexpected(HostFunctionError::DataFieldTooLarge);
data_ = Bytes(data.begin(), data.end());
return data_->size();
}
// =========================================================
// SECTION: UTILS
// =========================================================
std::expected<int32_t, HostFunctionError>
WasmHostFunctionsImpl::checkSignature(
Slice const& message,
Slice const& signature,
Slice const& pubkey) const
{
if (!publicKeyType(pubkey))
return std::unexpected(HostFunctionError::InvalidParams);
PublicKey const pk(pubkey);
return verify(pk, message, signature);
}
std::expected<Hash, HostFunctionError>
WasmHostFunctionsImpl::computeSha512HalfHash(Slice const& data) const
{
auto const hash = sha512Half(data);
return hash;
}
void
WasmHostFunctionsImpl::trace(std::string_view const& msg, std::string_view const& data) const
{
log(msg, [&data] { return data; });
}
} // namespace xrpl

View File

@@ -1,498 +0,0 @@
#include <xrpl/basics/Number.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/tx/wasm/HostFunc.h>
#include <xrpl/tx/wasm/HostFuncImpl.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <boost/algorithm/hex.hpp>
#include <cstdint>
#include <expected>
#include <iterator>
#include <optional>
#include <string>
#include <utility>
#ifdef _DEBUG
// #define DEBUG_OUTPUT 1
#endif
namespace xrpl {
namespace wasm_float {
namespace detail {
// Decode a serialized STNumber float payload. Returns nullopt if the data is
// not a well-formed encoding.
std::optional<Number>
floatDecode(Slice const& data)
{
static unsigned constexpr encodedFloatSize = 12;
if (data.size() != encodedFloatSize)
return std::nullopt;
try
{
SerialIter it(data);
return STNumber(it, sfNumber).value();
}
catch (...)
{
return std::nullopt;
}
}
// Build a Number from a raw mantissa/exponent pair. Returns nullopt if the
// value cannot be represented, e.g. the exponent is out of range.
std::optional<Number>
numberFromMantExp(int64_t mantissa, int32_t exponent)
{
try
{
return Number(mantissa, exponent);
}
catch (...)
{
return std::nullopt;
}
}
// Serialize a Number to the STNumber float encoding.
std::expected<Bytes, HostFunctionError>
floatEncode(Number const& n)
{
Serializer msg;
STNumber(sfNumber, n).add(msg);
auto data = msg.getData();
#ifdef DEBUG_OUTPUT
std::cout << "m: " << std::setw(20) << n.mantissa() << ", e: " << std::setw(12) << n.exponent()
<< ", hex: ";
std::cout << std::hex << std::uppercase << std::setfill('0');
for (auto const& c : data)
std::cout << std::setw(2) << (unsigned)c << " ";
std::cout << std::dec << std::setfill(' ') << std::endl;
#endif
return std::expected<Bytes, HostFunctionError>(std::move(data));
}
struct FloatState
{
// Set only when the requested mode is valid; sets the rounding mode on
// construction and restores the previous mode on destruction.
std::optional<NumberRoundModeGuard> guard;
explicit FloatState(int32_t mode)
{
if (auto const rm = Number::checkedRoundingMode(mode))
guard.emplace(*rm);
}
explicit
operator bool() const
{
return guard.has_value();
}
};
} // namespace detail
std::string
floatToString(Slice const& data)
{
// set default mode as we don't expect it will be used here
detail::FloatState const rm(static_cast<int32_t>(Number::RoundingMode::ToNearest));
auto const num = detail::floatDecode(data);
if (!num)
{
std::string hex;
hex.reserve(data.size() * 2);
boost::algorithm::hex(data.begin(), data.end(), std::back_inserter(hex));
return "Invalid data: " + hex;
}
return to_string(*num);
}
std::expected<Bytes, HostFunctionError>
floatFromIntImpl(int64_t x, int32_t mode)
{
try
{
detail::FloatState const rm(mode);
if (!rm)
return std::unexpected(HostFunctionError::FloatInputMalformed);
return detail::floatEncode(Number(x));
}
// LCOV_EXCL_START
catch (...)
{
return std::unexpected(HostFunctionError::FloatComputationError);
}
// LCOV_EXCL_STOP
}
std::expected<Bytes, HostFunctionError>
floatFromUintImpl(uint64_t x, int32_t mode)
{
try
{
detail::FloatState const rm(mode);
if (!rm)
return std::unexpected(HostFunctionError::FloatInputMalformed);
return detail::floatEncode(Number(x, 0, Number::Normalized{}));
}
// LCOV_EXCL_START
catch (...)
{
return std::unexpected(HostFunctionError::FloatComputationError);
}
// LCOV_EXCL_STOP
}
std::expected<Bytes, HostFunctionError>
floatFromSTAmountImpl(STAmount const& x, int32_t mode)
{
try
{
detail::FloatState const rm(mode);
if (!rm)
return std::unexpected(HostFunctionError::FloatInputMalformed);
return detail::floatEncode(static_cast<Number>(x));
}
// LCOV_EXCL_START
catch (...)
{
return std::unexpected(HostFunctionError::FloatComputationError);
}
// LCOV_EXCL_STOP
}
std::expected<Bytes, HostFunctionError>
floatFromSTNumberImpl(STNumber const& x, int32_t mode)
{
try
{
detail::FloatState const rm(mode);
if (!rm)
return std::unexpected(HostFunctionError::FloatInputMalformed);
return detail::floatEncode(x.value());
}
// LCOV_EXCL_START
catch (...)
{
return std::unexpected(HostFunctionError::FloatComputationError);
}
// LCOV_EXCL_STOP
}
std::expected<int64_t, HostFunctionError>
floatToIntImpl(Slice const& x, int32_t mode)
{
try
{
detail::FloatState const rm(mode);
if (!rm)
return std::unexpected(HostFunctionError::FloatInputMalformed);
auto const num = detail::floatDecode(x);
if (!num)
return std::unexpected(HostFunctionError::FloatInputMalformed); // LCOV_EXCL_LINE
return static_cast<int64_t>(*num);
}
// LCOV_EXCL_START
catch (...)
{
return std::unexpected(HostFunctionError::FloatComputationError);
}
// LCOV_EXCL_STOP
}
std::expected<FloatPair, HostFunctionError>
floatToMantExpImpl(Slice const& x)
{
try
{
detail::FloatState const rm(static_cast<int32_t>(Number::RoundingMode::ToNearest));
if (!rm)
return std::unexpected(HostFunctionError::FloatInputMalformed);
auto const num = detail::floatDecode(x);
if (!num)
return std::unexpected(HostFunctionError::FloatInputMalformed); // LCOV_EXCL_LINE
return FloatPair(num->mantissa(), num->exponent());
}
// LCOV_EXCL_START
catch (...)
{
return std::unexpected(HostFunctionError::FloatComputationError);
}
// LCOV_EXCL_STOP
}
std::expected<Bytes, HostFunctionError>
floatFromMantExpImpl(int64_t mantissa, int32_t exponent, int32_t mode)
{
try
{
detail::FloatState const rm(mode);
if (!rm)
return std::unexpected(HostFunctionError::FloatInputMalformed);
auto const num = detail::numberFromMantExp(mantissa, exponent);
if (!num)
return std::unexpected(HostFunctionError::FloatInputMalformed);
return detail::floatEncode(*num);
}
catch (...)
{
return std::unexpected(HostFunctionError::FloatComputationError);
}
}
std::expected<int32_t, HostFunctionError>
floatCompareImpl(Slice const& x, Slice const& y)
{
try
{
// set default mode as we don't expect it will be used here
detail::FloatState const rm(static_cast<int32_t>(Number::RoundingMode::ToNearest));
auto const xx = detail::floatDecode(x);
if (!xx)
return std::unexpected(HostFunctionError::FloatInputMalformed);
auto const yy = detail::floatDecode(y);
if (!yy)
return std::unexpected(HostFunctionError::FloatInputMalformed);
if (*xx < *yy)
return 2;
if (*xx == *yy)
return 0;
return 1;
}
// LCOV_EXCL_START
catch (...)
{
return std::unexpected(HostFunctionError::FloatComputationError);
}
// LCOV_EXCL_STOP
}
std::expected<Bytes, HostFunctionError>
floatAddImpl(Slice const& x, Slice const& y, int32_t mode)
{
try
{
detail::FloatState const rm(mode);
if (!rm)
return std::unexpected(HostFunctionError::FloatInputMalformed);
auto const xx = detail::floatDecode(x);
if (!xx)
return std::unexpected(HostFunctionError::FloatInputMalformed);
auto const yy = detail::floatDecode(y);
if (!yy)
return std::unexpected(HostFunctionError::FloatInputMalformed);
return detail::floatEncode(*xx + *yy);
}
// LCOV_EXCL_START
catch (...)
{
return std::unexpected(HostFunctionError::FloatComputationError);
}
// LCOV_EXCL_STOP
}
std::expected<Bytes, HostFunctionError>
floatSubtractImpl(Slice const& x, Slice const& y, int32_t mode)
{
try
{
detail::FloatState const rm(mode);
if (!rm)
return std::unexpected(HostFunctionError::FloatInputMalformed);
auto const xx = detail::floatDecode(x);
if (!xx)
return std::unexpected(HostFunctionError::FloatInputMalformed);
auto const yy = detail::floatDecode(y);
if (!yy)
return std::unexpected(HostFunctionError::FloatInputMalformed);
return detail::floatEncode(*xx - *yy);
}
// LCOV_EXCL_START
catch (...)
{
return std::unexpected(HostFunctionError::FloatComputationError);
}
// LCOV_EXCL_STOP
}
std::expected<Bytes, HostFunctionError>
floatMultiplyImpl(Slice const& x, Slice const& y, int32_t mode)
{
try
{
detail::FloatState const rm(mode);
if (!rm)
return std::unexpected(HostFunctionError::FloatInputMalformed);
auto const xx = detail::floatDecode(x);
if (!xx)
return std::unexpected(HostFunctionError::FloatInputMalformed);
auto const yy = detail::floatDecode(y);
if (!yy)
return std::unexpected(HostFunctionError::FloatInputMalformed);
return detail::floatEncode(*xx * *yy);
}
// LCOV_EXCL_START
catch (...)
{
return std::unexpected(HostFunctionError::FloatComputationError);
}
// LCOV_EXCL_STOP
}
std::expected<Bytes, HostFunctionError>
floatDivideImpl(Slice const& x, Slice const& y, int32_t mode)
{
try
{
detail::FloatState const rm(mode);
if (!rm)
return std::unexpected(HostFunctionError::FloatInputMalformed);
auto const xx = detail::floatDecode(x);
if (!xx)
return std::unexpected(HostFunctionError::FloatInputMalformed);
auto const yy = detail::floatDecode(y);
if (!yy)
return std::unexpected(HostFunctionError::FloatInputMalformed);
return detail::floatEncode(*xx / *yy);
}
catch (...)
{
return std::unexpected(HostFunctionError::FloatComputationError);
}
}
std::expected<Bytes, HostFunctionError>
floatPowerImpl(Slice const& x, int32_t n, int32_t mode)
{
try
{
if ((n < 0) || (n > Number::kMaxExponent))
return std::unexpected(HostFunctionError::FloatInputMalformed);
detail::FloatState const rm(mode);
if (!rm)
return std::unexpected(HostFunctionError::FloatInputMalformed);
auto const xx = detail::floatDecode(x);
if (!xx)
return std::unexpected(HostFunctionError::FloatInputMalformed);
if (*xx == Number() && (n == 0))
return std::unexpected(HostFunctionError::InvalidParams);
return detail::floatEncode(power(*xx, n, 1));
}
// LCOV_EXCL_START
catch (...)
{
return std::unexpected(HostFunctionError::FloatComputationError);
}
// LCOV_EXCL_STOP
}
} // namespace wasm_float
// =========================================================
// ACTUAL HOST FUNCTIONS
// =========================================================
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::floatFromInt(int64_t x, int32_t mode) const
{
return wasm_float::floatFromIntImpl(x, mode);
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::floatFromUint(uint64_t x, int32_t mode) const
{
return wasm_float::floatFromUintImpl(x, mode);
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::floatFromSTAmount(STAmount const& x, int32_t mode) const
{
return wasm_float::floatFromSTAmountImpl(x, mode);
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::floatFromSTNumber(STNumber const& x, int32_t mode) const
{
return wasm_float::floatFromSTNumberImpl(x, mode);
}
std::expected<int64_t, HostFunctionError>
WasmHostFunctionsImpl::floatToInt(Slice const& x, int32_t mode) const
{
return wasm_float::floatToIntImpl(x, mode);
}
std::expected<FloatPair, HostFunctionError>
WasmHostFunctionsImpl::floatToMantExp(Slice const& x) const
{
return wasm_float::floatToMantExpImpl(x);
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::floatFromMantExp(int64_t mantissa, int32_t exponent, int32_t mode) const
{
return wasm_float::floatFromMantExpImpl(mantissa, exponent, mode);
}
std::expected<int32_t, HostFunctionError>
WasmHostFunctionsImpl::floatCompare(Slice const& x, Slice const& y) const
{
return wasm_float::floatCompareImpl(x, y);
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::floatAdd(Slice const& x, Slice const& y, int32_t mode) const
{
return wasm_float::floatAddImpl(x, y, mode);
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::floatSubtract(Slice const& x, Slice const& y, int32_t mode) const
{
return wasm_float::floatSubtractImpl(x, y, mode);
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::floatMultiply(Slice const& x, Slice const& y, int32_t mode) const
{
return wasm_float::floatMultiplyImpl(x, y, mode);
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::floatDivide(Slice const& x, Slice const& y, int32_t mode) const
{
return wasm_float::floatDivideImpl(x, y, mode);
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::floatPower(Slice const& x, int32_t n, int32_t mode) const
{
return wasm_float::floatPowerImpl(x, n, mode);
}
} // namespace xrpl

View File

@@ -1,400 +0,0 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/STBitString.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/tx/wasm/HostFuncImpl.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <cstdint>
#include <expected>
#include <stdexcept>
#include <utility>
#include <variant>
namespace xrpl {
using FieldValue = std::variant<STBase const*, uint256 const*>;
template <class T>
Bytes
getIntBytes(STBase const* obj)
{
static_assert(std::is_integral_v<T>, "Only integral types");
XRPL_ASSERT(obj, "getIntBytes null pointer");
auto const* num(static_cast<STInteger<T> const*>(obj)); // NOLINT
T const data = adjustWasmEndianess(num->value());
auto const* b = reinterpret_cast<uint8_t const*>(&data);
return Bytes{b, b + sizeof(T)};
}
static std::expected<Bytes, HostFunctionError>
getAnyFieldData(STBase const* obj)
{
if (obj == nullptr)
return std::unexpected(HostFunctionError::FieldNotFound);
auto const stype = obj->getSType();
switch (stype)
{
// LCOV_EXCL_START
case STI_UNKNOWN:
case STI_NOTPRESENT:
return std::unexpected(HostFunctionError::FieldNotFound);
// LCOV_EXCL_STOP
case STI_OBJECT:
case STI_ARRAY:
case STI_VECTOR256:
return std::unexpected(HostFunctionError::NotLeafField);
case STI_ACCOUNT: {
auto const* account(static_cast<STAccount const*>(obj)); // NOLINT
auto const& data = account->value();
return Bytes{data.begin(), data.end()};
}
case STI_ISSUE: {
auto const* issue(static_cast<STIssue const*>(obj)); // NOLINT
Asset const& asset(issue->value());
// XRP and IOU will be processed by serializer
if (asset.holds<MPTIssue>())
{
auto const& mptIssue = asset.get<MPTIssue>();
auto const& mptID = mptIssue.getMptID();
return Bytes{mptID.cbegin(), mptID.cend()};
}
break; // Use serializer
}
case STI_VL: {
auto const* vl(static_cast<STBlob const*>(obj)); // NOLINT
auto const& data = vl->value();
return Bytes{data.begin(), data.end()};
}
case STI_UINT16:
return getIntBytes<std::uint16_t>(obj);
case STI_UINT32:
return getIntBytes<std::uint32_t>(obj);
// LCOV_EXCL_START
case STI_UINT64:
return getIntBytes<std::uint64_t>(obj);
case STI_INT32:
return getIntBytes<std::int32_t>(obj);
case STI_INT64:
return getIntBytes<std::int64_t>(obj);
// LCOV_EXCL_STOP
case STI_UINT256: {
auto const* uint256Obj(static_cast<STUInt256 const*>(obj)); // NOLINT
auto const& data = uint256Obj->value();
return Bytes{data.begin(), data.end()};
}
case STI_AMOUNT:
case STI_NUMBER:
default:
break; // Use serializer
}
Serializer msg;
obj->add(msg);
return msg.getData();
}
static std::expected<Bytes, HostFunctionError>
getAnyFieldData(FieldValue const& variantObj)
{
if (STBase const* const* obj = std::get_if<STBase const*>(&variantObj))
return getAnyFieldData(*obj);
if (uint256 const* const* u = std::get_if<uint256 const*>(&variantObj))
return Bytes((*u)->begin(), (*u)->end());
// Unreachable: the variant only holds the two alternatives above. If not, it is an
// xrpld bug, and `guarded` turns the throw into `InternalFatal`, which stops the run ->
// tecINTERNAL.
Throw<std::runtime_error>("field value variant holds neither alternative"); // LCOV_EXCL_LINE
}
static inline bool
noField(STBase const* field)
{
return (field == nullptr) || (STI_NOTPRESENT == field->getSType()) ||
(STI_UNKNOWN == field->getSType());
}
static std::expected<FieldValue, HostFunctionError>
locateField(STObject const& obj, FieldLocator const& locator)
{
STBase const* field = nullptr;
auto const& knownSFields = SField::getKnownCodeToField();
{
int32_t const sfieldCode = adjustWasmEndianess(locator[0]);
auto const it = knownSFields.find(sfieldCode);
if (it == knownSFields.end())
return std::unexpected(HostFunctionError::InvalidField);
auto const& fname(*it->second);
field = obj.peekAtPField(fname);
if (noField(field))
return std::unexpected(HostFunctionError::FieldNotFound);
}
for (unsigned i = 1; i < locator.size(); ++i)
{
int32_t const sfieldCode = adjustWasmEndianess(locator[i]);
if (STI_ARRAY == field->getSType())
{
auto const* arr = static_cast<STArray const*>(field); // NOLINT
if (sfieldCode < 0 || std::cmp_greater_equal(sfieldCode, arr->size()))
return std::unexpected(HostFunctionError::IndexOutOfBounds);
field = &(arr->operator[](sfieldCode));
}
else if (STI_OBJECT == field->getSType())
{
auto const* o = static_cast<STObject const*>(field); // NOLINT
auto const it = knownSFields.find(sfieldCode);
if (it == knownSFields.end())
return std::unexpected(HostFunctionError::InvalidField);
auto const& fname(*it->second);
field = o->peekAtPField(fname);
}
else if (STI_VECTOR256 == field->getSType())
{
auto const* v = static_cast<STVector256 const*>(field); // NOLINT
if (sfieldCode < 0 || std::cmp_greater_equal(sfieldCode, v->size()))
return std::unexpected(HostFunctionError::IndexOutOfBounds);
return FieldValue(&(v->operator[](sfieldCode)));
}
else // simple field must be the last one
{
return std::unexpected(HostFunctionError::LocatorMalformed);
}
if (noField(field))
return std::unexpected(HostFunctionError::FieldNotFound);
}
return FieldValue(field);
}
static inline std::expected<int32_t, HostFunctionError>
getArrayLen(FieldValue const& variantField)
{
if (STBase const* const* field = std::get_if<STBase const*>(&variantField))
{
if ((*field)->getSType() == STI_VECTOR256)
return static_cast<STVector256 const*>(*field)->size(); // NOLINT
if ((*field)->getSType() == STI_ARRAY)
return static_cast<STArray const*>(*field)->size(); // NOLINT
}
// uint256 is not an array so that variant should still return NO_ARRAY
return std::unexpected(HostFunctionError::NoArray); // LCOV_EXCL_LINE
}
std::expected<int32_t, HostFunctionError>
WasmHostFunctionsImpl::cacheLedgerObj(uint256 const& objId, int32_t cacheIdx)
{
auto const& keylet = keylet::unchecked(objId);
if (cacheIdx < 0 || cacheIdx > maxCache)
return std::unexpected(HostFunctionError::SlotOutRange);
if (cacheIdx == 0)
{
for (cacheIdx = 0; cacheIdx < maxCache; ++cacheIdx)
{
if (!cache_[cacheIdx])
break;
}
}
else
{
cacheIdx--; // convert to 0-based index
}
if (cacheIdx >= maxCache)
return std::unexpected(HostFunctionError::SlotsFull);
cache_[cacheIdx] = ctx_.view().read(keylet);
if (!cache_[cacheIdx])
return std::unexpected(HostFunctionError::LedgerObjNotFound);
return cacheIdx + 1; // return 1-based index
}
// Subsection: top level getters
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::getTxField(SField const& fname) const
{
return getAnyFieldData(ctx_.tx.peekAtPField(fname));
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::getCurrentLedgerObjField(SField const& fname) const
{
auto const sle = getCurrentLedgerObj();
if (!sle.has_value())
return std::unexpected(sle.error());
return getAnyFieldData(sle.value()->peekAtPField(fname));
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::getLedgerObjField(int32_t cacheIdx, SField const& fname) const
{
auto const normalizedIdx = normalizeCacheIndex(cacheIdx);
if (!normalizedIdx.has_value())
return std::unexpected(normalizedIdx.error());
return getAnyFieldData(cache_[normalizedIdx.value()]->peekAtPField(fname));
}
// Subsection: nested getters
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::getTxNestedField(FieldLocator const& locator) const
{
auto const r = locateField(ctx_.tx, locator);
if (!r)
return std::unexpected(r.error());
return getAnyFieldData(r.value());
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::getCurrentLedgerObjNestedField(FieldLocator const& locator) const
{
auto const sle = getCurrentLedgerObj();
if (!sle.has_value())
return std::unexpected(sle.error());
auto const r = locateField(*sle.value(), locator);
if (!r)
return std::unexpected(r.error());
return getAnyFieldData(r.value());
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::getLedgerObjNestedField(int32_t cacheIdx, FieldLocator const& locator) const
{
auto const normalizedIdx = normalizeCacheIndex(cacheIdx);
if (!normalizedIdx.has_value())
return std::unexpected(normalizedIdx.error());
auto const r = locateField(*cache_[normalizedIdx.value()], locator);
if (!r)
return std::unexpected(r.error());
return getAnyFieldData(r.value());
}
// Subsection: array length getters
std::expected<int32_t, HostFunctionError>
WasmHostFunctionsImpl::getTxArrayLen(SField const& fname) const
{
if (fname.fieldType != STI_ARRAY && fname.fieldType != STI_VECTOR256)
return std::unexpected(HostFunctionError::NoArray);
auto const* field = ctx_.tx.peekAtPField(fname);
if (noField(field))
return std::unexpected(HostFunctionError::FieldNotFound);
return getArrayLen(field);
}
std::expected<int32_t, HostFunctionError>
WasmHostFunctionsImpl::getCurrentLedgerObjArrayLen(SField const& fname) const
{
if (fname.fieldType != STI_ARRAY && fname.fieldType != STI_VECTOR256)
return std::unexpected(HostFunctionError::NoArray);
auto const sle = getCurrentLedgerObj();
if (!sle.has_value())
return std::unexpected(sle.error());
auto const* field = sle.value()->peekAtPField(fname);
if (noField(field))
return std::unexpected(HostFunctionError::FieldNotFound);
return getArrayLen(field);
}
std::expected<int32_t, HostFunctionError>
WasmHostFunctionsImpl::getLedgerObjArrayLen(int32_t cacheIdx, SField const& fname) const
{
if (fname.fieldType != STI_ARRAY && fname.fieldType != STI_VECTOR256)
return std::unexpected(HostFunctionError::NoArray);
auto const normalizedIdx = normalizeCacheIndex(cacheIdx);
if (!normalizedIdx.has_value())
return std::unexpected(normalizedIdx.error());
auto const* field = cache_[normalizedIdx.value()]->peekAtPField(fname);
if (noField(field))
return std::unexpected(HostFunctionError::FieldNotFound);
return getArrayLen(field);
}
// Subsection: nested array length getters
std::expected<int32_t, HostFunctionError>
WasmHostFunctionsImpl::getTxNestedArrayLen(FieldLocator const& locator) const
{
auto const r = locateField(ctx_.tx, locator);
if (!r)
return std::unexpected(r.error());
auto const& field = r.value();
return getArrayLen(field);
}
std::expected<int32_t, HostFunctionError>
WasmHostFunctionsImpl::getCurrentLedgerObjNestedArrayLen(FieldLocator const& locator) const
{
auto const sle = getCurrentLedgerObj();
if (!sle.has_value())
return std::unexpected(sle.error());
auto const r = locateField(*sle.value(), locator);
if (!r)
return std::unexpected(r.error());
auto const& field = r.value();
return getArrayLen(field);
}
std::expected<int32_t, HostFunctionError>
WasmHostFunctionsImpl::getLedgerObjNestedArrayLen(int32_t cacheIdx, FieldLocator const& locator)
const
{
auto const normalizedIdx = normalizeCacheIndex(cacheIdx);
if (!normalizedIdx.has_value())
return std::unexpected(normalizedIdx.error());
auto const r = locateField(*cache_[normalizedIdx.value()], locator);
if (!r)
return std::unexpected(r.error());
auto const& field = r.value();
return getArrayLen(field);
}
} // namespace xrpl

View File

@@ -1,223 +0,0 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/tx/wasm/HostFuncImpl.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <cstdint>
#include <expected>
namespace xrpl {
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::accountKeylet(AccountID const& account) const
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::account(account);
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::ammKeylet(Asset const& issue1, Asset const& issue2) const
{
if (issue1 == issue2)
return std::unexpected(HostFunctionError::InvalidParams);
// note: this should be removed with the MPT DEX amendment
if (issue1.holds<MPTIssue>() || issue2.holds<MPTIssue>())
return std::unexpected(HostFunctionError::InvalidParams);
auto const keylet = keylet::amm(issue1, issue2);
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::checkKeylet(AccountID const& account, std::uint32_t seq) const
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::check(account, SeqProxy::rawSequence(seq));
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::credentialKeylet(
AccountID const& subject,
AccountID const& issuer,
Slice const& credentialType) const
{
if (!subject || !issuer)
return std::unexpected(HostFunctionError::InvalidAccount);
if (credentialType.empty() || credentialType.size() > kMaxCredentialTypeLength)
return std::unexpected(HostFunctionError::InvalidParams);
auto const keylet = keylet::credential(subject, issuer, credentialType);
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::didKeylet(AccountID const& account) const
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::did(account);
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::delegateKeylet(AccountID const& account, AccountID const& authorize) const
{
if (!account || !authorize)
return std::unexpected(HostFunctionError::InvalidAccount);
if (account == authorize)
return std::unexpected(HostFunctionError::InvalidParams);
auto const keylet = keylet::delegate(account, authorize);
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::depositPreauthKeylet(AccountID const& account, AccountID const& authorize)
const
{
if (!account || !authorize)
return std::unexpected(HostFunctionError::InvalidAccount);
if (account == authorize)
return std::unexpected(HostFunctionError::InvalidParams);
auto const keylet = keylet::depositPreauth(account, authorize);
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::escrowKeylet(AccountID const& account, std::uint32_t seq) const
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::escrow(account, SeqProxy::rawSequence(seq));
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::trustLineKeylet(
AccountID const& account1,
AccountID const& account2,
Currency const& currency) const
{
if (!account1 || !account2)
return std::unexpected(HostFunctionError::InvalidAccount);
if (account1 == account2)
return std::unexpected(HostFunctionError::InvalidParams);
if (currency.isZero())
return std::unexpected(HostFunctionError::InvalidParams);
auto const keylet = keylet::trustLine(account1, account2, currency);
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::mptokenIssuanceKeylet(AccountID const& issuer, std::uint32_t seq) const
{
if (!issuer)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::mptokenIssuance(makeMptID(seq, issuer));
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::mptokenKeylet(MPTID const& mptid, AccountID const& holder) const
{
if (!mptid)
return std::unexpected(HostFunctionError::InvalidParams);
if (!holder)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::mptoken(mptid, holder);
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::nftokenOfferKeylet(AccountID const& account, std::uint32_t seq) const
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::nftokenOffer(account, SeqProxy::rawSequence(seq));
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::offerKeylet(AccountID const& account, std::uint32_t seq) const
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::offer(account, SeqProxy::rawSequence(seq));
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::oracleKeylet(AccountID const& account, std::uint32_t documentId) const
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::oracle(account, documentId);
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::paychannelKeylet(
AccountID const& account,
AccountID const& destination,
std::uint32_t seq) const
{
if (!account || !destination)
return std::unexpected(HostFunctionError::InvalidAccount);
if (account == destination)
return std::unexpected(HostFunctionError::InvalidParams);
auto const keylet = keylet::payChannel(account, destination, SeqProxy::rawSequence(seq));
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::permissionedDomainKeylet(AccountID const& account, std::uint32_t seq) const
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::permissionedDomain(account, SeqProxy::rawSequence(seq));
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::signerListKeylet(AccountID const& account) const
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::signerList(account);
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::ticketKeylet(AccountID const& account, std::uint32_t seq) const
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::ticket(account, SeqProxy::rawTicket(seq));
return Bytes{keylet.key.begin(), keylet.key.end()};
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::vaultKeylet(AccountID const& account, std::uint32_t seq) const
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::vault(account, SeqProxy::rawSequence(seq));
return Bytes{keylet.key.begin(), keylet.key.end()};
}
} // namespace xrpl

View File

@@ -1,55 +0,0 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/ledger/AmendmentTable.h>
#include <xrpl/tx/wasm/HostFuncImpl.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <cstdint>
#include <expected>
#include <string>
#include <string_view>
namespace xrpl {
// =========================================================
// SECTION: LEDGER HEADER FUNCTIONS
// =========================================================
std::expected<std::uint32_t, HostFunctionError>
WasmHostFunctionsImpl::getLedgerSqn() const
{
return ctx_.view().seq();
}
std::expected<std::uint32_t, HostFunctionError>
WasmHostFunctionsImpl::getParentLedgerTime() const
{
return ctx_.view().parentCloseTime().time_since_epoch().count();
}
std::expected<Hash, HostFunctionError>
WasmHostFunctionsImpl::getParentLedgerHash() const
{
return ctx_.view().header().parentHash;
}
std::expected<std::uint32_t, HostFunctionError>
WasmHostFunctionsImpl::getBaseFee() const
{
return ctx_.view().fees().base.drops();
}
std::expected<int32_t, HostFunctionError>
WasmHostFunctionsImpl::isAmendmentEnabled(uint256 const& amendmentId) const
{
return ctx_.view().rules().enabled(amendmentId);
}
std::expected<int32_t, HostFunctionError>
WasmHostFunctionsImpl::isAmendmentEnabled(std::string_view const& amendmentName) const
{
auto const& table = ctx_.registry.get().getAmendmentTable();
auto const amendment = table.find(std::string(amendmentName));
return ctx_.view().rules().enabled(amendment);
}
} // namespace xrpl

View File

@@ -1,74 +0,0 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/nft.h>
#include <xrpl/tx/wasm/HostFuncImpl.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <cstdint>
#include <expected>
namespace xrpl {
// =========================================================
// SECTION: NFT UTILS
// =========================================================
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::getNFT(AccountID const& account, uint256 const& nftId) const
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
if (!nftId)
return std::unexpected(HostFunctionError::InvalidParams);
auto obj = nft::findToken(ctx_.view(), account, nftId);
if (!obj)
return std::unexpected(HostFunctionError::LedgerObjNotFound);
auto objUri = obj->at(~sfURI);
if (!objUri)
return std::unexpected(HostFunctionError::FieldNotFound);
Slice const s = objUri->value();
return Bytes(s.begin(), s.end());
}
std::expected<Bytes, HostFunctionError>
WasmHostFunctionsImpl::getNFTIssuer(uint256 const& nftId) const
{
auto const issuer = nft::getIssuer(nftId);
if (!issuer)
return std::unexpected(HostFunctionError::InvalidParams);
return Bytes{issuer.begin(), issuer.end()};
}
std::expected<std::uint32_t, HostFunctionError>
WasmHostFunctionsImpl::getNFTTaxon(uint256 const& nftId) const
{
return nft::toUInt32(nft::getTaxon(nftId));
}
std::expected<int32_t, HostFunctionError>
WasmHostFunctionsImpl::getNFTFlags(uint256 const& nftId) const
{
return nft::getFlags(nftId);
}
std::expected<int32_t, HostFunctionError>
WasmHostFunctionsImpl::getNFTTransferFee(uint256 const& nftId) const
{
return nft::getTransferFee(nftId);
}
std::expected<std::uint32_t, HostFunctionError>
WasmHostFunctionsImpl::getNFTSequence(uint256 const& nftId) const
{
return nft::getSequence(nftId);
}
} // namespace xrpl

View File

@@ -1,186 +0,0 @@
#include <xrpl/tx/wasm/WasmVM.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/wasm/HostContext.h>
#include <xrpl/tx/wasm/HostFunc.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <rust/cxx.h>
#include <xrpl_wasm_vm_ffi_cxxbridge/lib.h>
#include <cstdint>
#include <expected>
#include <optional>
#include <stdexcept>
#include <string_view>
namespace xrpl {
namespace {
using RunStatus = rs::wasm_vm::RunStatus;
using CheckStatus = rs::wasm_vm::CheckStatus;
// The engine's outcome as the caller's: a value with its gas cost, or a TER with the gas cost
// to record beside it.
//
// A `tecINTERNAL` reports no cost. It says the fault is the node's, and charging a
// transaction for a node's defect would write that defect into the ledger.
//
// Exhaustive over the status enum, with no `default`: the enum is generated from the
// engine's `RunError`, so an outcome added there fails this switch under -Wswitch -Werror
// rather than quietly picking up a neighbour's TER. The return past the switch is for the
// compilers that will not call an exhaustive switch exhaustive; it sits after the switch,
// not in a `default`, so the coverage check above still holds.
std::expected<EscrowResult, WasmTER>
outcome(rs::wasm_vm::RunResult const& run)
{
auto const cost = static_cast<std::int64_t>(run.gas_used);
switch (run.status)
{
case RunStatus::Ok:
return EscrowResult{.result = run.result, .cost = cost};
// The cost is the whole limit: XLS-0102 halts the guest the instant the meter runs
// out, and the run is charged for all of it.
case RunStatus::OutOfGas:
return std::unexpected{WasmTER{.ter = tecOUT_OF_GAS, .cost = cost}};
// The contract's own fault - it trapped, or it never exported the linear memory
// its host calls need - so it is charged for what it burned reaching that point.
case RunStatus::Trap:
case RunStatus::NoMemory:
// A module that will not instantiate is the contract's fault too. Screening
// cannot see every way this happens - a linear memory the module keeps to itself
// is absent from its exports - so a module can pass preflight and still be
// refused here. It is a deterministic property of the code either way, and one
// this node's own conduct had no part in.
case RunStatus::Instantiate:
return std::unexpected{WasmTER{.ter = tecFAILED_PROCESSING, .cost = cost}};
// A module that will not compile, or does not expose the entry point, should have
// been refused at preflight with `temBAD_WASM`: screening decides both from the
// same bytes and the same engine, so agreeing here is not a matter of degree.
// Reaching apply means the screening did not happen, which is a node-side fault
// rather than the transaction's.
case RunStatus::Compile:
case RunStatus::EntryPoint:
// The host could not serve a call, or it threw and `HostContext` caught it.
case RunStatus::Internal:
// The engine panicked: a defect in the engine, reported rather than fatal to the
// node.
case RunStatus::Panic:
return std::unexpected{WasmTER{.ter = tecINTERNAL, .cost = std::nullopt}};
}
UNREACHABLE("xrpl::outcome : unknown RunStatus");
return std::unexpected{WasmTER{.ter = tecINTERNAL, .cost = std::nullopt}};
}
// A screening verdict as a TER.
//
// `temBAD_WASM` says the transaction carries something this engine cannot run: a
// malformed transaction, refused before it can reach the ledger. A panic inside the
// engine is different in kind - nothing was learned about the module - so the answer is
// node-local rather than a claim about the transaction.
//
// Exhaustive over the status enum, with no `default`, for the same reason `outcome` is.
NotTEC
verdict(CheckStatus status)
{
switch (status)
{
case CheckStatus::Ok:
return tesSUCCESS;
// The module will not compile, imports what no engine of this ABI serves, does
// not export the entry point as `() -> i32`, or asks for more linear memory or
// table than it may have.
case CheckStatus::Compile:
case CheckStatus::Import:
case CheckStatus::EntryPoint:
case CheckStatus::Memory:
case CheckStatus::Table:
return temBAD_WASM;
// The engine panicked: a defect in the engine, reported rather than fatal to
// the node, and not the transaction's fault.
case CheckStatus::Panic:
return telFAILED_PROCESSING;
}
UNREACHABLE("xrpl::verdict : unknown CheckStatus");
return telFAILED_PROCESSING;
}
} // namespace
std::expected<EscrowResult, WasmTER>
runEscrowWasm(
Bytes const& wasmCode,
HostFunctions& hfs,
std::int64_t gasLimit,
std::string_view funcName) noexcept
{
XRPL_ASSERT(
gasLimit > 0,
"::xrpl::runEscrowWasm : gas limit is positive (should be checked in preflight)");
// A run needs a budget to spend. Refused here rather than in the engine because what a
// non-positive limit means is a transaction-validity rule; the engine's own budget is
// therefore an unsigned quantity with no invalid value to represent.
if (gasLimit <= 0)
return std::unexpected{WasmTER{.ter = temBAD_AMOUNT, .cost = std::nullopt}};
auto const nodeSideFault = std::unexpected{WasmTER{.ter = tecINTERNAL, .cost = std::nullopt}};
return guarded(hfs.getJournal(), nodeSideFault, [&]() -> std::expected<EscrowResult, WasmTER> {
// The host caches the current ledger object, the slot table and the
// contract's data for the length of one run, so a reused one would answer a
// later contract out of an earlier contract's state.
XRPL_ASSERT(
hfs.checkSelf(), "::xrpl::runEscrowWasm : host functions not clean before the run");
if (!hfs.checkSelf())
{
throw std::runtime_error("host functions not clean before the run");
}
HostContext const ctx{hfs};
auto const run = rs::wasm_vm::run_escrow(
ctx,
rust::Slice<std::uint8_t const>{wasmCode.data(), wasmCode.size()},
static_cast<std::uint64_t>(gasLimit),
rust::Str{funcName.data(), funcName.size()});
auto const result = outcome(run);
if (!result)
{
JLOG(hfs.getJournal().warn())
<< "wasm: " << std::string_view{run.detail.data(), run.detail.size()}
<< ", ter: " << transToken(result.error().ter);
}
return result;
});
}
NotTEC
preflightEscrowWasm(Bytes const& wasmCode, beast::Journal j, std::string_view funcName) noexcept
{
return guarded(j, NotTEC{telFAILED_PROCESSING}, [&]() {
auto const checked = rs::wasm_vm::check_escrow(
rust::Slice<std::uint8_t const>{wasmCode.data(), wasmCode.size()},
rust::Str{funcName.data(), funcName.size()});
auto const ter = verdict(checked.status);
if (!isTesSuccess(ter))
{
JLOG(j.warn()) << "wasm: "
<< std::string_view{checked.detail.data(), checked.detail.size()}
<< ", ter: " << transToken(ter);
}
return ter;
});
}
} // namespace xrpl

View File

@@ -19,6 +19,7 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/core/CoroTask.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/JobQueue.h>
#include <xrpl/json/json_value.h>
@@ -123,7 +124,6 @@ public:
.ledgerMaster = app.getLedgerMaster(),
.consumer = c,
.role = Role::USER,
.coro = {},
.infoSub = {},
.apiVersion = rpc::kApiVersionIfUnspecified},
{},
@@ -135,22 +135,28 @@ public:
numSrc.reserve(rpc::tuning::kMaxSrcCur);
for (std::uint8_t i = 0; i < rpc::tuning::kMaxSrcCur; ++i)
numSrc.push_back(makeMptID(i, bob));
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = xrpl::test::detail::rpf(alice, bob, usd, numSrc);
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(!result.isMember(jss::error));
// Test more than rpc::tuning::max_src_cur source currencies.
numSrc.push_back(makeMptID(rpc::tuning::kMaxSrcCur, bob));
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = xrpl::test::detail::rpf(alice, bob, usd, numSrc);
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(result.isMember(jss::error));
@@ -162,22 +168,28 @@ public:
auto curm = MPTTester({.env = env, .issuer = alice, .holders = {bob}});
numSrc.push_back(curm.issuanceID());
}
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = xrpl::test::detail::rpf(alice, bob, usd, {});
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(!result.isMember(jss::error));
// Test more than rpc::tuning::max_auto_src_cur source currencies.
auto curm = MPTTester({.env = env, .issuer = alice, .holders = {bob}});
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = xrpl::test::detail::rpf(alice, bob, usd, {});
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(result.isMember(jss::error));

View File

@@ -26,6 +26,7 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/core/CoroTask.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/JobQueue.h>
#include <xrpl/json/json_reader.h>
@@ -163,7 +164,6 @@ public:
.ledgerMaster = app.getLedgerMaster(),
.consumer = c,
.role = Role::USER,
.coro = {},
.infoSub = {},
.apiVersion = rpc::kApiVersionIfUnspecified},
{},
@@ -188,11 +188,14 @@ public:
json::Value result;
Gate g;
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = std::move(params);
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
using namespace std::chrono_literals;
@@ -274,7 +277,6 @@ public:
.ledgerMaster = app.getLedgerMaster(),
.consumer = c,
.role = Role::USER,
.coro = {},
.infoSub = {},
.apiVersion = rpc::kApiVersionIfUnspecified},
{},
@@ -282,21 +284,27 @@ public:
json::Value result;
Gate g;
// Test rpc::tuning::max_src_cur source currencies.
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = rpf(Account("alice"), Account("bob"), rpc::tuning::kMaxSrcCur);
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(!result.isMember(jss::error));
// Test more than rpc::tuning::max_src_cur source currencies.
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = rpf(Account("alice"), Account("bob"), rpc::tuning::kMaxSrcCur + 1);
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(result.isMember(jss::error));
@@ -304,22 +312,28 @@ public:
// Test rpc::tuning::max_auto_src_cur source currencies.
for (auto i = 0; i < (rpc::tuning::kMaxAutoSrcCur - 1); ++i)
env.trust(Account("alice")[std::to_string(i + 100)](100), "bob");
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = rpf(Account("alice"), Account("bob"), 0);
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(!result.isMember(jss::error));
// Test more than rpc::tuning::max_auto_src_cur source currencies.
env.trust(Account("alice")["AUD"](100), "bob");
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
context.params = rpf(Account("alice"), Account("bob"), 0);
context.coro = coro;
rpc::doCommand(context, result);
g.signal();
co_return;
});
BEAST_EXPECT(g.waitFor(5s));
BEAST_EXPECT(result.isMember(jss::error));

View File

@@ -24,7 +24,9 @@
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/CoroTask.h>
#include <xrpl/core/Job.h>
#include <xrpl/core/JobQueue.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
@@ -1638,7 +1640,6 @@ struct PayChan_test : public beast::unit_test::Suite
.ledgerMaster = app.getLedgerMaster(),
.consumer = c,
.role = Role::USER,
.coro = {},
.infoSub = {},
.apiVersion = rpc::kApiVersionIfUnspecified},
{},
@@ -1654,10 +1655,13 @@ struct PayChan_test : public beast::unit_test::Suite
BEAST_EXPECT(context.loadType == resource::kFeeReferenceRpc);
json::Value result;
Gate g;
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
context.coro = coro;
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
result = doChannelVerify(context);
g.signal();
co_return;
});
using namespace std::chrono_literals;
@@ -1705,7 +1709,6 @@ struct PayChan_test : public beast::unit_test::Suite
.ledgerMaster = app.getLedgerMaster(),
.consumer = c,
.role = Role::ADMIN, // channel_authorize requires ADMIN or canSign()
.coro = {},
.infoSub = {},
.apiVersion = rpc::kApiVersionIfUnspecified},
{},
@@ -1720,10 +1723,13 @@ struct PayChan_test : public beast::unit_test::Suite
BEAST_EXPECT(context.loadType == resource::kFeeReferenceRpc);
json::Value result;
Gate g;
app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) {
context.coro = coro;
// Safe capture: the test blocks on g.waitFor() until the coroutine
// completes, so the captured locals outlive the coroutine.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
app.getJobQueue().postCoroTask(JtClient, "RPC-Client", [&](auto) -> CoroTask<void> {
result = doChannelAuthorize(context);
g.signal();
co_return;
});
using namespace std::chrono_literals;

View File

@@ -1,488 +0,0 @@
#pragma once
#include <test/jtx/Env.h>
#include <test/unit_test/SuiteJournal.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/detail/ApplyViewBase.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/tx/wasm/HostFunc.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <cstdint>
#include <expected>
#include <string>
#include <string_view>
namespace xrpl::test {
class TestLedgerDataProvider : public HostFunctions
{
jtx::Env& env_;
public:
TestLedgerDataProvider(jtx::Env& env) : HostFunctions(env.journal), env_(env)
{
}
[[nodiscard]] std::expected<std::uint32_t, HostFunctionError>
getLedgerSqn() const override
{
return env_.current()->seq();
}
};
class TestHostFunctions : public HostFunctions
{
protected:
test::jtx::Env& env_;
AccountID accountID_;
Bytes data_;
public:
TestHostFunctions(test::jtx::Env& env) : HostFunctions(env.journal), env_(env)
{
accountID_ = env.master.id();
std::string t = "10000";
data_ = Bytes{t.begin(), t.end()};
}
[[nodiscard]] std::expected<std::uint32_t, HostFunctionError>
getLedgerSqn() const override
{
return 12345;
}
[[nodiscard]] std::expected<std::uint32_t, HostFunctionError>
getParentLedgerTime() const override
{
return 67890;
}
[[nodiscard]] std::expected<Hash, HostFunctionError>
getParentLedgerHash() const override
{
return env_.current()->header().parentHash;
}
[[nodiscard]] std::expected<std::uint32_t, HostFunctionError>
getBaseFee() const override
{
return 10;
}
[[nodiscard]] std::expected<int32_t, HostFunctionError>
isAmendmentEnabled(uint256 const& amendmentId) const override
{
return 1;
}
[[nodiscard]] std::expected<int32_t, HostFunctionError>
isAmendmentEnabled(std::string_view const& amendmentName) const override
{
return 1;
}
std::expected<int32_t, HostFunctionError>
cacheLedgerObj(uint256 const& objId, int32_t cacheIdx) override
{
return 1;
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
getTxField(SField const& fname) const override
{
if (fname == sfAccount)
return Bytes(accountID_.begin(), accountID_.end());
if (fname == sfFee)
{
int64_t x = 235;
auto const* p = reinterpret_cast<uint8_t const*>(&x);
return Bytes{p, p + sizeof(x)};
}
if (fname == sfSequence)
{
auto const x = getLedgerSqn();
if (!x)
return std::unexpected(x.error());
std::uint32_t const data = x.value();
auto const* b = reinterpret_cast<uint8_t const*>(&data);
auto const* e = reinterpret_cast<uint8_t const*>(&data + 1);
return Bytes{b, e};
}
return Bytes();
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
getCurrentLedgerObjField(SField const& fname) const override
{
auto const& sn = fname.getName();
if (sn == "Destination" || sn == "Account")
return Bytes(accountID_.begin(), accountID_.end());
if (sn == "Data")
return data_;
if (sn == "FinishAfter")
{
auto t = env_.current()->parentCloseTime().time_since_epoch().count();
std::string s = std::to_string(t);
return Bytes{s.begin(), s.end()};
}
// FieldNotFound is a guest-returnable code (the contract handles a negative result);
// Unimplemented now maps to a fatal Fault::Internal (tecINTERNAL) that stops the run.
return std::unexpected(HostFunctionError::FieldNotFound);
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
getLedgerObjField(int32_t, SField const& fname) const override
{
if (fname == sfBalance)
{
int64_t x = 10'000;
auto const* p = reinterpret_cast<uint8_t const*>(&x);
return Bytes{p, p + sizeof(x)};
}
if (fname == sfAccount)
return Bytes(accountID_.begin(), accountID_.end());
return data_;
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
getTxNestedField(FieldLocator const& locator) const override
{
if (locator.size() == 1)
{
int32_t const* l = locator.data();
int32_t const sfield = l[0];
if (sfield == sfAccount.getCode())
return Bytes(accountID_.begin(), accountID_.end());
}
uint8_t const a[] = {0x2b, 0x6a, 0x23, 0x2a, 0xa4, 0xc4, 0xbe, 0x41, 0xbf, 0x49, 0xd2,
0x45, 0x9f, 0xa4, 0xa0, 0x34, 0x7e, 0x1b, 0x54, 0x3a, 0x4c, 0x92,
0xfc, 0xee, 0x08, 0x21, 0xc0, 0x20, 0x1e, 0x2e, 0x9a, 0x00};
return Bytes(&a[0], &a[sizeof(a)]);
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
getCurrentLedgerObjNestedField(FieldLocator const& locator) const override
{
if (locator.size() == 1)
{
int32_t const* l = locator.data();
int32_t const sfield = l[0];
if (sfield == sfAccount.getCode())
return Bytes(accountID_.begin(), accountID_.end());
}
uint8_t const a[] = {0x2b, 0x6a, 0x23, 0x2a, 0xa4, 0xc4, 0xbe, 0x41, 0xbf, 0x49, 0xd2,
0x45, 0x9f, 0xa4, 0xa0, 0x34, 0x7e, 0x1b, 0x54, 0x3a, 0x4c, 0x92,
0xfc, 0xee, 0x08, 0x21, 0xc0, 0x20, 0x1e, 0x2e, 0x9a, 0x00};
return Bytes(&a[0], &a[sizeof(a)]);
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
getLedgerObjNestedField(int32_t cacheIdx, FieldLocator const& locator) const override
{
if (locator.size() == 1)
{
int32_t const* l = locator.data();
int32_t const sfield = l[0];
if (sfield == sfAccount.getCode())
return Bytes(accountID_.begin(), accountID_.end());
}
uint8_t const a[] = {0x2b, 0x6a, 0x23, 0x2a, 0xa4, 0xc4, 0xbe, 0x41, 0xbf, 0x49, 0xd2,
0x45, 0x9f, 0xa4, 0xa0, 0x34, 0x7e, 0x1b, 0x54, 0x3a, 0x4c, 0x92,
0xfc, 0xee, 0x08, 0x21, 0xc0, 0x20, 0x1e, 0x2e, 0x9a, 0x00};
return Bytes(&a[0], &a[sizeof(a)]);
}
[[nodiscard]] std::expected<int32_t, HostFunctionError>
getTxArrayLen(SField const& fname) const override
{
return 32;
}
[[nodiscard]] std::expected<int32_t, HostFunctionError>
getCurrentLedgerObjArrayLen(SField const& fname) const override
{
return 32;
}
[[nodiscard]] std::expected<int32_t, HostFunctionError>
getLedgerObjArrayLen(int32_t cacheIdx, SField const& fname) const override
{
return 32;
}
[[nodiscard]] std::expected<int32_t, HostFunctionError>
getTxNestedArrayLen(FieldLocator const& locator) const override
{
return 32;
}
[[nodiscard]] std::expected<int32_t, HostFunctionError>
getCurrentLedgerObjNestedArrayLen(FieldLocator const& locator) const override
{
return 32;
}
[[nodiscard]] std::expected<int32_t, HostFunctionError>
getLedgerObjNestedArrayLen(int32_t cacheIdx, FieldLocator const& locator) const override
{
return 32;
}
std::expected<int32_t, HostFunctionError>
updateData(Slice const& data) override
{
return data.size();
}
[[nodiscard]] std::expected<int32_t, HostFunctionError>
checkSignature(Slice const& message, Slice const& signature, Slice const& pubkey) const override
{
return 1;
}
[[nodiscard]] std::expected<Hash, HostFunctionError>
computeSha512HalfHash(Slice const& data) const override
{
return env_.current()->header().parentHash;
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
accountKeylet(AccountID const& account) const override
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::account(account);
return Bytes{keylet.key.begin(), keylet.key.end()};
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
ammKeylet(Asset const& issue1, Asset const& issue2) const override
{
if (issue1 == issue2)
return std::unexpected(HostFunctionError::InvalidParams);
if (issue1.holds<MPTIssue>() || issue2.holds<MPTIssue>())
return std::unexpected(HostFunctionError::InvalidParams);
auto const keylet = keylet::amm(issue1, issue2);
return Bytes{keylet.key.begin(), keylet.key.end()};
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
checkKeylet(AccountID const& account, std::uint32_t seq) const override
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::check(account, SeqProxy::rawSequence(seq));
return Bytes{keylet.key.begin(), keylet.key.end()};
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
credentialKeylet(AccountID const& subject, AccountID const& issuer, Slice const& credentialType)
const override
{
if (!subject || !issuer || credentialType.empty() ||
credentialType.size() > kMaxCredentialTypeLength)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::credential(subject, issuer, credentialType);
return Bytes{keylet.key.begin(), keylet.key.end()};
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
escrowKeylet(AccountID const& account, std::uint32_t seq) const override
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::escrow(account, SeqProxy::rawSequence(seq));
return Bytes{keylet.key.begin(), keylet.key.end()};
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
oracleKeylet(AccountID const& account, std::uint32_t documentId) const override
{
if (!account)
return std::unexpected(HostFunctionError::InvalidAccount);
auto const keylet = keylet::oracle(account, documentId);
return Bytes{keylet.key.begin(), keylet.key.end()};
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
getNFT(AccountID const& account, uint256 const& nftId) const override
{
if (!account || !nftId)
return std::unexpected(HostFunctionError::InvalidParams);
std::string s = "https://ripple.com";
return Bytes(s.begin(), s.end());
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
getNFTIssuer(uint256 const& nftId) const override
{
return Bytes(accountID_.begin(), accountID_.end());
}
[[nodiscard]] std::expected<std::uint32_t, HostFunctionError>
getNFTTaxon(uint256 const& nftId) const override
{
return 4;
}
[[nodiscard]] std::expected<int32_t, HostFunctionError>
getNFTFlags(uint256 const& nftId) const override
{
return 8;
}
[[nodiscard]] std::expected<int32_t, HostFunctionError>
getNFTTransferFee(uint256 const& nftId) const override
{
return 10;
}
[[nodiscard]] std::expected<std::uint32_t, HostFunctionError>
getNFTSequence(uint256 const& nftId) const override
{
return 4;
}
template <typename F>
void
log(std::string_view const& msg, F&& dataFn) const
{
#ifdef DEBUG_OUTPUT
auto& j = std::cerr;
#else
if (!getJournal().active(beast::Severity::Trace))
return;
auto j = getJournal().trace();
#endif
j << "WasmTrace: " << msg << " " << dataFn();
#ifdef DEBUG_OUTPUT
j << std::endl;
#endif
}
void
trace(std::string_view const& msg, std::string_view const& data) const override
{
log(msg, [&data] { return data; });
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
floatFromInt(int64_t x, int32_t mode) const override
{
return wasm_float::floatFromIntImpl(x, mode);
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
floatFromUint(uint64_t x, int32_t mode) const override
{
return wasm_float::floatFromUintImpl(x, mode);
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
floatFromSTAmount(STAmount const& x, int32_t mode) const override
{
return wasm_float::floatFromSTAmountImpl(x, mode);
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
floatFromSTNumber(STNumber const& x, int32_t mode) const override
{
return wasm_float::floatFromSTNumberImpl(x, mode);
}
[[nodiscard]] std::expected<int64_t, HostFunctionError>
floatToInt(Slice const& x, int32_t mode) const override
{
return wasm_float::floatToIntImpl(x, mode);
}
[[nodiscard]] std::expected<FloatPair, HostFunctionError>
floatToMantExp(Slice const& x) const override
{
return wasm_float::floatToMantExpImpl(x);
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
floatFromMantExp(int64_t mantissa, int32_t exponent, int32_t mode) const override
{
return wasm_float::floatFromMantExpImpl(mantissa, exponent, mode);
}
[[nodiscard]] std::expected<int32_t, HostFunctionError>
floatCompare(Slice const& x, Slice const& y) const override
{
return wasm_float::floatCompareImpl(x, y);
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
floatAdd(Slice const& x, Slice const& y, int32_t mode) const override
{
return wasm_float::floatAddImpl(x, y, mode);
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
floatSubtract(Slice const& x, Slice const& y, int32_t mode) const override
{
return wasm_float::floatSubtractImpl(x, y, mode);
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
floatMultiply(Slice const& x, Slice const& y, int32_t mode) const override
{
return wasm_float::floatMultiplyImpl(x, y, mode);
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
floatDivide(Slice const& x, Slice const& y, int32_t mode) const override
{
return wasm_float::floatDivideImpl(x, y, mode);
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
floatPower(Slice const& x, int32_t n, int32_t mode) const override
{
return wasm_float::floatPowerImpl(x, n, mode);
}
};
class TestHostFunctionsSink : public TestHostFunctions
{
test::StreamSink sink_;
public:
explicit TestHostFunctionsSink(test::jtx::Env& env)
: TestHostFunctions(env), sink_(beast::Severity::Debug)
{
j_ = beast::Journal(sink_);
}
test::StreamSink&
getSink()
{
return sink_;
}
};
} // namespace xrpl::test

View File

@@ -1,243 +0,0 @@
#include <expected>
#ifdef _DEBUG
// #define DEBUG_OUTPUT 1
#endif
#include <test/app/TestHostFunctions.h>
#include <test/app/wasm_fixtures/fixtures.h>
#include <test/jtx/Env.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/wasm/HostFunc.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <xrpl/tx/wasm/WasmVM.h>
#include <boost/algorithm/hex.hpp>
#include <cstdint>
#include <limits>
#include <source_location>
#include <string>
#include <vector>
namespace xrpl::test {
std::vector<uint8_t>
hexToBytes(std::string const& hex)
{
auto const ws = boost::algorithm::unhex(hex);
return Bytes(ws.begin(), ws.end());
}
struct Wasm_test : public beast::unit_test::Suite
{
void
checkResult(
std::expected<WasmResult<int32_t>, WasmTER> re,
int32_t expectedResult,
int64_t expectedCost,
std::source_location const location = std::source_location::current())
{
auto const lineStr = " (" + std::to_string(location.line()) + ")";
if (BEAST_EXPECTS(re.has_value(), transToken(re.error().ter) + lineStr))
{
BEAST_EXPECTS(re->result == expectedResult, std::to_string(re->result) + lineStr);
BEAST_EXPECTS(re->cost == expectedCost, std::to_string(re->cost) + lineStr);
}
}
void
testBadWasm()
{
testcase("bad wasm test");
using namespace test::jtx;
Env const env{*this};
HostFunctions hfs(env.journal);
{
auto wasm = hexToBytes("00000000");
std::string const funcName("mock_escrow");
auto re = runEscrowWasm(wasm, hfs, 15, funcName);
BEAST_EXPECT(!re);
}
{
auto wasm = hexToBytes("00112233445566778899AA");
std::string const funcName("mock_escrow");
auto const re = preflightEscrowWasm(wasm, env.journal, funcName);
BEAST_EXPECT(!isTesSuccess(re));
}
{
// FinishFunction wrong function name
// pub fn bad() -> bool {
// unsafe { host_lib::getLedgerSqn() >= 5 }
// }
auto const badWasm = hexToBytes(
"0061736d010000000105016000017f02190108686f73745f6c69620c6765"
"744c656467657253716e00000302010005030100100611027f00418080c0"
"000b7f00418080c0000b072b04066d656d6f727902000362616400010a5f"
"5f646174615f656e6403000b5f5f686561705f6261736503010a09010700"
"100041044a0b004d0970726f64756365727302086c616e67756167650104"
"52757374000c70726f6365737365642d6279010572757374631d312e3835"
"2e31202834656231363132353020323032352d30332d31352900490f7461"
"726765745f6665617475726573042b0f6d757461626c652d676c6f62616c"
"732b087369676e2d6578742b0f7265666572656e63652d74797065732b0a"
"6d756c746976616c7565");
auto const re = preflightEscrowWasm(badWasm, env.journal, escrowFunctionName);
BEAST_EXPECT(!isTesSuccess(re));
}
}
void
testEscrowWasmDN()
{
testcase("escrow wasm devnet test");
auto const allHFWasm = hexToBytes(kAllHostFunctionsWasmHex);
using namespace test::jtx;
Env env{*this};
{
TestHostFunctions hfs(env);
auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName);
checkResult(re, 1, 50'207);
}
{
// max<int64_t>() gas
TestHostFunctions hfs(env);
auto re = runEscrowWasm(
allHFWasm, hfs, std::numeric_limits<int64_t>::max(), escrowFunctionName);
checkResult(re, 1, 50'207);
}
{ // fail because trying to access nonexistent field
struct FieldNotFoundHostFunctions : public TestHostFunctions
{
explicit FieldNotFoundHostFunctions(Env& env) : TestHostFunctions(env)
{
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
getTxField(SField const& fname) const override
{
return std::unexpected(HostFunctionError::FieldNotFound);
}
};
FieldNotFoundHostFunctions hfs(env);
auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName);
checkResult(re, -201, 28'901);
}
{ // fail because trying to allocate more than MAX_PAGES memory
struct OversizedFieldHostFunctions : public TestHostFunctions
{
explicit OversizedFieldHostFunctions(Env& env) : TestHostFunctions(env)
{
}
[[nodiscard]] std::expected<Bytes, HostFunctionError>
getTxField(SField const& fname) const override
{
return Bytes((128 + 1) * 64 * 1024, 1);
}
};
OversizedFieldHostFunctions hfs(env);
auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName);
checkResult(re, -201, 28'901);
}
}
void
testCodecovWasm()
{
testcase("Codecov wasm test");
using namespace test::jtx;
Env env{*this};
auto const codecovWasm = hexToBytes(kCodecovTestsWasmHex);
TestHostFunctions hfs(env);
auto const allowance = 124'173;
auto re = runEscrowWasm(codecovWasm, hfs, allowance, escrowFunctionName);
checkResult(re, 1, allowance);
}
void
testSwapBytes()
{
testcase("Wasm swap bytes");
uint64_t const swapDataU64 = 0x123456789abcdeffull;
uint64_t const reverseSwapDataU64 = 0xffdebc9a78563412ull;
int64_t const swapDataI64 = 0x123456789abcdeffll;
int64_t const reverseSwapDataI64 = 0xffdebc9a78563412ll;
uint32_t const swapDataU32 = 0x12789aff;
uint32_t const reverseSwapDataU32 = 0xff9a7812;
int32_t const swapDataI32 = 0x12789aff;
int32_t const reverseSwapDataI32 = 0xff9a7812;
uint16_t const swapDataU16 = 0x12ff;
uint16_t const reverseSwapDataU16 = 0xff12;
int16_t const swapDataI16 = 0x12ff;
int16_t const reverseSwapDataI16 = 0xff12;
uint64_t b1 = swapDataU64;
int64_t b2 = swapDataI64;
b1 = adjustWasmEndianessHlp(b1);
b2 = adjustWasmEndianessHlp(b2);
BEAST_EXPECT(b1 == reverseSwapDataU64);
BEAST_EXPECT(b2 == reverseSwapDataI64);
b1 = adjustWasmEndianessHlp(b1);
b2 = adjustWasmEndianessHlp(b2);
BEAST_EXPECT(b1 == swapDataU64);
BEAST_EXPECT(b2 == swapDataI64);
uint32_t b3 = swapDataU32;
int32_t b4 = swapDataI32;
b3 = adjustWasmEndianessHlp(b3);
b4 = adjustWasmEndianessHlp(b4);
BEAST_EXPECT(b3 == reverseSwapDataU32);
BEAST_EXPECT(b4 == reverseSwapDataI32);
b3 = adjustWasmEndianessHlp(b3);
b4 = adjustWasmEndianessHlp(b4);
BEAST_EXPECT(b3 == swapDataU32);
BEAST_EXPECT(b4 == swapDataI32);
uint16_t b5 = swapDataU16;
int16_t b6 = swapDataI16;
b5 = adjustWasmEndianessHlp(b5);
b6 = adjustWasmEndianessHlp(b6);
BEAST_EXPECT(b5 == reverseSwapDataU16);
BEAST_EXPECT(b6 == reverseSwapDataI16);
b5 = adjustWasmEndianessHlp(b5);
b6 = adjustWasmEndianessHlp(b6);
BEAST_EXPECT(b5 == swapDataU16);
BEAST_EXPECT(b6 == swapDataI16);
}
void
run() override
{
testBadWasm();
testEscrowWasmDN();
testCodecovWasm();
testSwapBytes();
}
};
BEAST_DEFINE_TESTSUITE(Wasm, app, xrpl);
} // namespace xrpl::test

View File

@@ -1,3 +0,0 @@
**/target
**/debug
*.wasm

View File

@@ -1,180 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "all_host_functions"
version = "0.1.0"
dependencies = [
"xrpl-common-stdlib",
"xrpl-escrow-stdlib",
]
[[package]]
name = "block-buffer"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"hybrid-array",
]
[[package]]
name = "bs58"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
dependencies = [
"tinyvec",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"hybrid-array",
]
[[package]]
name = "digest"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
]
[[package]]
name = "hybrid-array"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
dependencies = [
"typenum",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "sha2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tinyvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "typenum"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "xrpl-common-stdlib"
version = "0.8.0"
source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9"
dependencies = [
"xrpl-macros",
]
[[package]]
name = "xrpl-escrow-stdlib"
version = "0.1.0"
source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9"
dependencies = [
"xrpl-common-stdlib",
]
[[package]]
name = "xrpl-macros"
version = "0.1.0"
source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9"
dependencies = [
"bs58",
"proc-macro2",
"quote",
"sha2",
"syn",
]

View File

@@ -1,22 +0,0 @@
[package]
name = "all_host_functions"
version = "0.1.0"
edition = "2024"
# This empty workspace definition keeps this project independent of the parent workspace
[workspace]
[lib]
crate-type = ["cdylib"]
[dependencies]
xrpl-std = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-common-stdlib", branch = "error-and-trace" }
xrpl-escrow = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-escrow-stdlib", branch = "error-and-trace" }
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
opt-level = "z"
lto = true

View File

@@ -1,760 +0,0 @@
#![cfg_attr(target_arch = "wasm32", no_std)]
#[cfg(not(target_arch = "wasm32"))]
extern crate std;
//
// Host Functions Test
// Tests 26 host functions (across 7 categories)
//
// With craft you can run this test with:
// craft test --project host_functions_test --test-case host_functions_test
//
// Amount Format Update:
// - XRP amounts now return as 8-byte serialized rippled objects
// - IOU and MPT amounts return in variable-length serialized format
// - Format details: https://xrpl.org/docs/references/protocol/binary-format#amount-fields
//
// Error Code Ranges:
// -100 to -199: Ledger Header Functions (3 functions)
// -200 to -299: Transaction Data Functions (5 functions)
// -300 to -399: Current Ledger Object Functions (4 functions)
// -400 to -499: Any Ledger Object Functions (5 functions)
// -500 to -599: Keylet Generation Functions (4 functions)
// -600 to -699: Utility Functions (4 functions)
// -700 to -799: Data Update Functions (1 function)
//
use xrpl_escrow::current_tx::escrow_finish::EscrowFinish;
use xrpl_std::current_tx::traits::TransactionCommonFields;
use xrpl_std::host;
use xrpl_std::host::trace::TraceDataType;
use xrpl_std::host::trace::{trace, trace_acct_buf, trace_hex, trace_num};
use xrpl_std::sfield;
#[unsafe(no_mangle)]
pub extern "C" fn escrow_finish() -> i32 {
let _ = trace("=== HOST FUNCTIONS TEST ===");
let _ = trace("Testing 26 host functions");
// Category 1: Ledger Header Data Functions (3 functions)
// Error range: -100 to -199
match test_ledger_header_functions() {
0 => (),
err => return err,
}
// Category 2: Transaction Data Functions (5 functions)
// Error range: -200 to -299
match test_transaction_data_functions() {
0 => (),
err => return err,
}
// Category 3: Current Ledger Object Functions (4 functions)
// Error range: -300 to -399
match test_current_ledger_object_functions() {
0 => (),
err => return err,
}
// Category 4: Any Ledger Object Functions (5 functions)
// Error range: -400 to -499
match test_any_ledger_object_functions() {
0 => (),
err => return err,
}
// Category 5: Keylet Generation Functions (4 functions)
// Error range: -500 to -599
match test_keylet_generation_functions() {
0 => (),
err => return err,
}
// Category 6: Utility Functions (4 functions)
// Error range: -600 to -699
match test_utility_functions() {
0 => (),
err => return err,
}
// Category 7: Data Update Functions (1 function)
// Error range: -700 to -799
match test_data_update_functions() {
0 => (),
err => return err,
}
let _ = trace("SUCCESS: All host function tests passed!");
1 // Success return code for WASM finish function
}
/// Test Category 1: Ledger Header Data Functions (3 functions)
/// - get_ledger_sqn() - Get ledger sequence number
/// - get_parent_ledger_time() - Get parent ledger timestamp
/// - get_parent_ledger_hash() - Get parent ledger hash
fn test_ledger_header_functions() -> i32 {
let _ = trace("--- Category 1: Ledger Header Functions ---");
// Test 1.1: get_ledger_sqn() - should return current ledger sequence number
let mut sqn_buffer = [0u8; 4];
let sqn_result = unsafe { host::ldgr_index(sqn_buffer.as_mut_ptr(), sqn_buffer.len()) };
if sqn_result <= 0 {
let _ = trace_num("ERROR: get_ledger_sqn failed:", sqn_result as i64);
return -101; // Ledger sequence number test failed
}
let ledger_sqn = u32::from_be_bytes(sqn_buffer);
let _ = trace_num("Ledger sequence number:", ledger_sqn as i64);
// Test 1.2: get_parent_ledger_time() - should return parent ledger timestamp
let mut time_buffer = [0u8; 4];
let time_result =
unsafe { host::parent_ldgr_time(time_buffer.as_mut_ptr(), time_buffer.len()) };
if time_result <= 0 {
let _ = trace_num("ERROR: get_parent_ledger_time failed:", time_result as i64);
return -102; // Parent ledger time test failed
}
let parent_ledger_time = u32::from_be_bytes(time_buffer);
let _ = trace_num("Parent ledger time:", parent_ledger_time as i64);
// Test 1.3: get_parent_ledger_hash() - should return parent ledger hash (32 bytes)
let mut hash_buffer = [0u8; 32];
let hash_result =
unsafe { host::parent_ldgr_hash(hash_buffer.as_mut_ptr(), hash_buffer.len()) };
if hash_result != 32 {
let _ = trace_num(
"ERROR: get_parent_ledger_hash wrong length:",
hash_result as i64,
);
return -103; // Parent ledger hash test failed - should be exactly 32 bytes
}
let _ = trace_hex("Parent ledger hash:", &hash_buffer);
let _ = trace("SUCCESS: Ledger header functions");
0
}
/// Test Category 2: Transaction Data Functions (5 functions)
/// Tests all functions for accessing current transaction data
fn test_transaction_data_functions() -> i32 {
let _ = trace("--- Category 2: Transaction Data Functions ---");
// Test 2.1: get_tx_field() - Basic transaction field access
// Test with Account field (required, 20 bytes)
let mut account_buffer = [0u8; 20];
let account_len = unsafe {
host::tx_field(
sfield::Account.into(),
account_buffer.as_mut_ptr(),
account_buffer.len(),
)
};
if account_len != 20 {
let _ = trace_num(
"ERROR: get_tx_field(Account) wrong length:",
account_len as i64,
);
return -201; // Basic transaction field test failed
}
let _ = trace_acct_buf("Transaction Account:", &account_buffer);
// Test with Fee field (XRP amount - 8 bytes in new serialized format)
// New format: XRP amounts are always 8 bytes (positive: value | cPositive flag, negative: just value)
let mut fee_buffer = [0u8; 8];
let fee_len = unsafe {
host::tx_field(
sfield::Fee.into(),
fee_buffer.as_mut_ptr(),
fee_buffer.len(),
)
};
if fee_len != 8 {
let _ = trace_num(
"ERROR: get_tx_field(Fee) wrong length (expected 8 bytes for XRP):",
fee_len as i64,
);
return -202; // Fee field test failed - XRP amounts should be exactly 8 bytes
}
let _ = trace_num("Transaction Fee length:", fee_len as i64);
let _ = trace_hex("Transaction Fee (serialized XRP amount):", &fee_buffer);
// Test with Sequence field (required, 4 bytes uint32)
let mut seq_buffer = [0u8; 4];
let seq_len = unsafe {
host::tx_field(
sfield::Sequence.into(),
seq_buffer.as_mut_ptr(),
seq_buffer.len(),
)
};
if seq_len != 4 {
let _ = trace_num(
"ERROR: get_tx_field(Sequence) wrong length:",
seq_len as i64,
);
return -203; // Sequence field test failed
}
let _ = trace_hex("Transaction Sequence:", &seq_buffer);
// NOTE: get_tx_field2() through get_tx_field6() have been deprecated.
// Use get_tx_field() with appropriate parameters for all transaction field access.
// Test 2.2: get_tx_nested_field() - Nested field access with locator
let locator = [
0x01_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8,
]; // Two int32s in little-endian: [1, 0]
let mut nested_buffer = [0u8; 32];
let nested_result = unsafe {
host::tx_inner(
locator.as_ptr(),
locator.len(),
nested_buffer.as_mut_ptr(),
nested_buffer.len(),
)
};
if nested_result < 0 {
let _ = trace_num(
"INFO: get_tx_nested_field not applicable:",
nested_result as i64,
);
// Expected - locator may not match transaction structure
} else {
let _ = trace_num("Nested field length:", nested_result as i64);
let _ = trace_hex("Nested field:", &nested_buffer[..nested_result as usize]);
}
// Test 2.3: get_tx_array_len() - Get array length
let signers_len = unsafe { host::tx_arr_len(sfield::Signers.into()) };
let _ = trace_num("Signers array length:", signers_len as i64);
let memos_len = unsafe { host::tx_arr_len(sfield::Memos.into()) };
let _ = trace_num("Memos array length:", memos_len as i64);
// Test 2.4: get_tx_nested_array_len() - Get nested array length with locator
let nested_array_len = unsafe { host::tx_inner_arr_len(locator.as_ptr(), locator.len()) };
if nested_array_len < 0 {
let _ = trace_num(
"INFO: get_tx_nested_array_len not applicable:",
nested_array_len as i64,
);
} else {
let _ = trace_num("Nested array length:", nested_array_len as i64);
}
let _ = trace("SUCCESS: Transaction data functions");
0
}
/// Test Category 3: Current Ledger Object Functions (4 functions)
/// Tests functions that access the current ledger object being processed
fn test_current_ledger_object_functions() -> i32 {
let _ = trace("--- Category 3: Current Ledger Object Functions ---");
// Test 3.1: get_current_ledger_obj_field() - Access field from current ledger object
// Test with Balance field (XRP amount - 8 bytes in new serialized format)
let mut balance_buffer = [0u8; 8];
let balance_result = unsafe {
host::home_le_field(
sfield::Balance.into(),
balance_buffer.as_mut_ptr(),
balance_buffer.len(),
)
};
if balance_result <= 0 {
let _ = trace_num(
"INFO: get_current_ledger_obj_field(Balance) failed (may be expected):",
balance_result as i64,
);
// This might fail if current ledger object doesn't have balance field
} else if balance_result == 8 {
let _ = trace_num(
"Current object balance length (XRP amount):",
balance_result as i64,
);
let _ = trace_hex(
"Current object balance (serialized XRP amount):",
&balance_buffer,
);
} else {
let _ = trace_num(
"Current object balance length (non-XRP amount):",
balance_result as i64,
);
let _ = trace_hex(
"Current object balance:",
&balance_buffer[..balance_result as usize],
);
}
// Test with Account field
let mut current_account_buffer = [0u8; 20];
let current_account_result = unsafe {
host::home_le_field(
sfield::Account.into(),
current_account_buffer.as_mut_ptr(),
current_account_buffer.len(),
)
};
if current_account_result <= 0 {
let _ = trace_num(
"INFO: get_current_ledger_obj_field(Account) failed:",
current_account_result as i64,
);
} else {
let _ = trace_acct_buf("Current ledger object account:", &current_account_buffer);
}
// Test 3.2: get_current_ledger_obj_nested_field() - Nested field access
let locator = [
0x01_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8,
]; // Two int32s in little-endian: [1, 0]
let mut current_nested_buffer = [0u8; 32];
let current_nested_result = unsafe {
host::home_le_inner(
locator.as_ptr(),
locator.len(),
current_nested_buffer.as_mut_ptr(),
current_nested_buffer.len(),
)
};
if current_nested_result < 0 {
let _ = trace_num(
"INFO: get_current_ledger_obj_nested_field not applicable:",
current_nested_result as i64,
);
} else {
let _ = trace_num("Current nested field length:", current_nested_result as i64);
let _ = trace_hex(
"Current nested field:",
&current_nested_buffer[..current_nested_result as usize],
);
}
// Test 3.3: get_current_ledger_obj_array_len() - Array length in current object
let current_array_len = unsafe { host::home_le_arr_len(sfield::Signers.into()) };
let _ = trace_num(
"Current object Signers array length:",
current_array_len as i64,
);
// Test 3.4: get_current_ledger_obj_nested_array_len() - Nested array length
let current_nested_array_len =
unsafe { host::home_le_inner_arr_len(locator.as_ptr(), locator.len()) };
if current_nested_array_len < 0 {
let _ = trace_num(
"INFO: get_current_ledger_obj_nested_array_len not applicable:",
current_nested_array_len as i64,
);
} else {
let _ = trace_num(
"Current nested array length:",
current_nested_array_len as i64,
);
}
let _ = trace("SUCCESS: Current ledger object functions");
0
}
/// Test Category 4: Any Ledger Object Functions (5 functions)
/// Tests functions that work with cached ledger objects
fn test_any_ledger_object_functions() -> i32 {
let _ = trace("--- Category 4: Any Ledger Object Functions ---");
// First we need to cache a ledger object to test the other functions
// Get the account from transaction and generate its keylet
let escrow_finish = EscrowFinish;
let account_id = escrow_finish.get_account().unwrap();
// Test 4.1: cache_ledger_obj() - Cache a ledger object
let mut keylet_buffer = [0u8; 32];
let keylet_result = unsafe {
host::accountroot_id(
account_id.0.as_ptr(),
account_id.0.len(),
keylet_buffer.as_mut_ptr(),
keylet_buffer.len(),
)
};
if keylet_result != 32 {
let _ = trace_num(
"ERROR: accountroot_id failed for caching test:",
keylet_result as i64,
);
return -401; // Keylet generation failed for caching test
}
let cache_result = unsafe { host::cache_le(keylet_buffer.as_ptr(), keylet_result as usize, 0) };
if cache_result <= 0 {
let _ = trace_num(
"INFO: cache_ledger_obj failed (expected with test fixtures):",
cache_result as i64,
);
// Test fixtures may not contain the account object - this is expected
// We'll test the interface but expect failures
// Test 4.2-4.5 with invalid slot (should fail gracefully)
let mut test_buffer = [0u8; 32];
// Test get_ledger_obj_field with invalid slot
let field_result = unsafe {
host::le_field(
1,
sfield::Balance.into(),
test_buffer.as_mut_ptr(),
test_buffer.len(),
)
};
if field_result < 0 {
let _ = trace_num(
"INFO: get_ledger_obj_field failed as expected (no cached object):",
field_result as i64,
);
}
// Test get_ledger_obj_nested_field with invalid slot
let locator = [
0x01_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8,
]; // Two int32s in little-endian: [1, 0]
let nested_result = unsafe {
host::le_inner(
1,
locator.as_ptr(),
locator.len(),
test_buffer.as_mut_ptr(),
test_buffer.len(),
)
};
if nested_result < 0 {
let _ = trace_num(
"INFO: get_ledger_obj_nested_field failed as expected:",
nested_result as i64,
);
}
// Test get_ledger_obj_array_len with invalid slot
let array_result = unsafe { host::le_arr_len(1, sfield::Signers.into()) };
if array_result < 0 {
let _ = trace_num(
"INFO: get_ledger_obj_array_len failed as expected:",
array_result as i64,
);
}
// Test get_ledger_obj_nested_array_len with invalid slot
let nested_array_result =
unsafe { host::le_inner_arr_len(1, locator.as_ptr(), locator.len()) };
if nested_array_result < 0 {
let _ = trace_num(
"INFO: get_ledger_obj_nested_array_len failed as expected:",
nested_array_result as i64,
);
}
let _ = trace("SUCCESS: Any ledger object functions (interface tested)");
return 0;
}
// If we successfully cached an object, test the access functions
let slot = cache_result;
let _ = trace_num("Successfully cached object in slot:", slot as i64);
// Test 4.2: get_ledger_obj_field() - Access field from cached object
let mut cached_balance_buffer = [0u8; 8];
let cached_balance_result = unsafe {
host::le_field(
slot,
sfield::Balance.into(),
cached_balance_buffer.as_mut_ptr(),
cached_balance_buffer.len(),
)
};
if cached_balance_result <= 0 {
let _ = trace_num(
"INFO: get_ledger_obj_field(Balance) failed:",
cached_balance_result as i64,
);
} else if cached_balance_result == 8 {
let _ = trace_num(
"Cached object balance length (XRP amount):",
cached_balance_result as i64,
);
let _ = trace_hex(
"Cached object balance (serialized XRP amount):",
&cached_balance_buffer,
);
} else {
let _ = trace_num(
"Cached object balance length (non-XRP amount):",
cached_balance_result as i64,
);
let _ = trace_hex(
"Cached object balance:",
&cached_balance_buffer[..cached_balance_result as usize],
);
}
// Test 4.3: get_ledger_obj_nested_field() - Nested field from cached object
let locator = [
0x01_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8,
]; // Two int32s in little-endian: [1, 0]
let mut cached_nested_buffer = [0u8; 32];
let cached_nested_result = unsafe {
host::le_inner(
slot,
locator.as_ptr(),
locator.len(),
cached_nested_buffer.as_mut_ptr(),
cached_nested_buffer.len(),
)
};
if cached_nested_result < 0 {
let _ = trace_num(
"INFO: get_ledger_obj_nested_field not applicable:",
cached_nested_result as i64,
);
} else {
let _ = trace_num("Cached nested field length:", cached_nested_result as i64);
let _ = trace_hex(
"Cached nested field:",
&cached_nested_buffer[..cached_nested_result as usize],
);
}
// Test 4.4: get_ledger_obj_array_len() - Array length from cached object
let cached_array_len = unsafe { host::le_arr_len(slot, sfield::Signers.into()) };
let _ = trace_num(
"Cached object Signers array length:",
cached_array_len as i64,
);
// Test 4.5: get_ledger_obj_nested_array_len() - Nested array length from cached object
let cached_nested_array_len =
unsafe { host::le_inner_arr_len(slot, locator.as_ptr(), locator.len()) };
if cached_nested_array_len < 0 {
let _ = trace_num(
"INFO: get_ledger_obj_nested_array_len not applicable:",
cached_nested_array_len as i64,
);
} else {
let _ = trace_num(
"Cached nested array length:",
cached_nested_array_len as i64,
);
}
let _ = trace("SUCCESS: Any ledger object functions");
0
}
/// Test Category 5: Keylet Generation Functions (4 functions)
/// Tests keylet generation functions for different ledger entry types
fn test_keylet_generation_functions() -> i32 {
let _ = trace("--- Category 5: Keylet Generation Functions ---");
let escrow_finish = EscrowFinish;
let account_id = escrow_finish.get_account().unwrap();
// Test 5.1: accountroot_id() - Generate keylet for account
let mut accountroot_id_buffer = [0u8; 32];
let accountroot_id_result = unsafe {
host::accountroot_id(
account_id.0.as_ptr(),
account_id.0.len(),
accountroot_id_buffer.as_mut_ptr(),
accountroot_id_buffer.len(),
)
};
if accountroot_id_result != 32 {
let _ = trace_num(
"ERROR: accountroot_id failed:",
accountroot_id_result as i64,
);
return -501; // Account keylet generation failed
}
let _ = trace_hex("Account keylet:", &accountroot_id_buffer);
// Test 5.2: credential_keylet() - Generate keylet for credential
let mut credential_keylet_buffer = [0u8; 32];
let credential_keylet_result = unsafe {
host::credential_id(
account_id.0.as_ptr(), // Subject
account_id.0.len(),
account_id.0.as_ptr(), // Issuer - same account for test
account_id.0.len(),
b"TestType".as_ptr(), // Credential type
9usize, // Length of "TestType"
credential_keylet_buffer.as_mut_ptr(),
credential_keylet_buffer.len(),
)
};
if credential_keylet_result <= 0 {
let _ = trace_num(
"INFO: credential_keylet failed (expected - interface issue):",
credential_keylet_result as i64,
);
// This is expected to fail due to unusual parameter types
} else {
let _ = trace_hex(
"Credential keylet:",
&credential_keylet_buffer[..credential_keylet_result as usize],
);
}
// Test 5.3: escrow_keylet() - Generate keylet for escrow
let mut escrow_keylet_buffer = [0u8; 32];
let sequence_number: i32 = 1000;
let sequence_number_bytes = sequence_number.to_be_bytes();
let escrow_keylet_result = unsafe {
host::escrow_id(
account_id.0.as_ptr(),
account_id.0.len(),
sequence_number_bytes.as_ptr(),
sequence_number_bytes.len(),
escrow_keylet_buffer.as_mut_ptr(),
escrow_keylet_buffer.len(),
)
};
if escrow_keylet_result != 32 {
let _ = trace_num("ERROR: escrow_keylet failed:", escrow_keylet_result as i64);
return -503; // Escrow keylet generation failed
}
let _ = trace_hex("Escrow keylet:", &escrow_keylet_buffer);
// Test 5.4: oracle_keylet() - Generate keylet for oracle
let mut oracle_keylet_buffer = [0u8; 32];
let document_id: i32 = 42;
let document_id_bytes = document_id.to_be_bytes();
let oracle_keylet_result = unsafe {
host::oracle_id(
account_id.0.as_ptr(),
account_id.0.len(),
document_id_bytes.as_ptr(),
document_id_bytes.len(),
oracle_keylet_buffer.as_mut_ptr(),
oracle_keylet_buffer.len(),
)
};
if oracle_keylet_result != 32 {
let _ = trace_num("ERROR: oracle_keylet failed:", oracle_keylet_result as i64);
return -504; // Oracle keylet generation failed
}
let _ = trace_hex("Oracle keylet:", &oracle_keylet_buffer);
let _ = trace("SUCCESS: Keylet generation functions");
0
}
/// Test Category 6: Utility Functions (4 functions)
/// Tests utility functions for hashing, NFT access, and tracing
fn test_utility_functions() -> i32 {
let _ = trace("--- Category 6: Utility Functions ---");
// Test 6.1: compute_sha512_half() - SHA512 hash computation (first 32 bytes)
let test_data = b"Hello, XRPL WASM world!";
let mut hash_output = [0u8; 32];
let hash_result = unsafe {
host::sha512_half(
test_data.as_ptr(),
test_data.len(),
hash_output.as_mut_ptr(),
hash_output.len(),
)
};
if hash_result != 32 {
let _ = trace_num("ERROR: compute_sha512_half failed:", hash_result as i64);
return -601; // SHA512 half computation failed
}
let _ = trace_hex("Input data:", test_data);
let _ = trace_hex("SHA512 half hash:", &hash_output);
// Test 6.2: get_nft() - NFT data retrieval
let escrow_finish = EscrowFinish;
let account_id = escrow_finish.get_account().unwrap();
let nft_id = [0u8; 32]; // Dummy NFT ID for testing
let mut nft_buffer = [0u8; 256];
let nft_result = unsafe {
host::nft_uri(
account_id.0.as_ptr(),
account_id.0.len(),
nft_id.as_ptr(),
nft_id.len(),
nft_buffer.as_mut_ptr(),
nft_buffer.len(),
)
};
if nft_result <= 0 {
let _ = trace_num(
"INFO: get_nft failed (expected - no such NFT):",
nft_result as i64,
);
// This is expected - test account likely doesn't own the dummy NFT
} else {
let _ = trace_num("NFT data length:", nft_result as i64);
let _ = trace_hex("NFT data:", &nft_buffer[..nft_result as usize]);
}
// Test 6.3: trace() - Debug logging with data
let trace_message = b"Test trace message";
let trace_data_payload = b"payload";
unsafe {
host::trace(
trace_message.as_ptr(),
trace_message.len(),
TraceDataType::AsHex as i32,
trace_data_payload.as_ptr(),
trace_data_payload.len(),
)
};
// Test 6.4: trace_num() - Debug logging with number
let test_number = 42i64;
trace_num("Test number trace", test_number);
let _ = trace("SUCCESS: Utility functions");
0
}
/// Test Category 7: Data Update Functions (1 function)
/// Tests the function for modifying the current ledger entry
fn test_data_update_functions() -> i32 {
let _ = trace("--- Category 7: Data Update Functions ---");
// Test 7.1: update_data() - Update current ledger entry data
let update_payload = b"Updated ledger entry data from WASM test";
let update_result = unsafe { host::set_data(update_payload.as_ptr(), update_payload.len()) };
if update_result != update_payload.len() as i32 {
let _ = trace_num("ERROR: update_data failed:", update_result as i64);
return -701; // Data update failed
}
let _ = trace_hex("Successfully updated ledger entry with:", update_payload);
let _ = trace("SUCCESS: Data update functions");
0
}

View File

@@ -1,171 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "all_keylets"
version = "0.0.1"
dependencies = [
"xrpl-wasm-stdlib",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bs58"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
dependencies = [
"tinyvec",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tinyvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "typenum"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "xrpl-macros"
version = "0.1.0"
source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=renames#21c522f34a24b460297ebb6be1822680459bf37e"
dependencies = [
"bs58",
"quote",
"sha2",
"syn",
]
[[package]]
name = "xrpl-wasm-stdlib"
version = "0.8.0"
source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=renames#21c522f34a24b460297ebb6be1822680459bf37e"
dependencies = [
"xrpl-macros",
]

View File

@@ -1,21 +0,0 @@
[package]
edition = "2024"
name = "all_keylets"
version = "0.0.1"
# This empty workspace definition keeps this project independent of the parent workspace
[workspace]
[lib]
crate-type = ["cdylib"]
[profile.release]
lto = true
opt-level = 's'
panic = "abort"
[dependencies]
xrpl-std = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-wasm-stdlib", branch = "renames" }
[profile.dev]
panic = "abort"

View File

@@ -1,176 +0,0 @@
#![cfg_attr(target_arch = "wasm32", no_std)]
#[cfg(not(target_arch = "wasm32"))]
extern crate std;
use crate::host::{Error, Result, Result::Err, Result::Ok};
use xrpl_std::core::keylets;
use xrpl_std::core::ledger_objects::current_escrow::get_current_escrow;
use xrpl_std::core::ledger_objects::current_escrow::CurrentEscrow;
use xrpl_std::core::ledger_objects::ledger_object;
use xrpl_std::core::ledger_objects::traits::CurrentEscrowFields;
use xrpl_std::core::ledger_objects::LedgerObjectFieldGetter;
use xrpl_std::core::types::currency::Currency;
use xrpl_std::core::types::issue::{IouIssue, Issue, XrpIssue};
use xrpl_std::core::types::mpt_id::MptId;
use xrpl_std::host;
use xrpl_std::host::trace::{trace, trace_acct, trace_data, trace_num, DataRepr};
use xrpl_std::sfield;
pub fn object_exists<T: LedgerObjectFieldGetter, const CODE: i32>(
keylet_result: Result<keylets::KeyletBytes>,
keylet_type: &str,
sfield: sfield::SField<T, CODE>,
) -> Result<bool> {
let field = CODE;
match keylet_result {
Ok(keylet) => {
let _ = trace_data(keylet_type, &keylet, DataRepr::AsHex);
let slot = unsafe { host::cache_le(keylet.as_ptr(), keylet.len(), 0) };
if slot <= 0 {
let _ = trace_num("Error: ", slot.into());
return Err(Error::from_code(slot));
}
if field == 0 {
let new_field = sfield::PreviousTxnID;
let _ = trace_num("Getting field: ", new_field.clone().into());
match ledger_object::get_field(slot, new_field) {
Ok(data) => {
let _ = trace_data("Field data: ", &data.0, DataRepr::AsHex);
}
Err(result_code) => {
let _ = trace_num("Error getting field: ", result_code.into());
return Err(result_code);
}
}
} else {
let _ = trace_num("Getting field: ", field.into());
match ledger_object::get_field(slot, sfield) {
Ok(_data) => {
let _ = trace("Field data: retrieved");
}
Err(result_code) => {
let _ = trace_num("Error getting field: ", result_code.into());
return Err(result_code);
}
}
}
Ok(true)
}
Err(error) => {
let _ = trace_num("Error getting keylet: ", error.into());
Err(error)
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn escrow_finish() -> i32 {
let _ = trace("$$$$$ STARTING WASM EXECUTION $$$$$");
let escrow: CurrentEscrow = get_current_escrow();
let account = escrow.get_account().unwrap_or_panic();
let _ = trace_acct("Account:", &account);
let destination = escrow.get_destination().unwrap_or_panic();
let _ = trace_acct("Destination:", &destination);
let mut seq = 5;
macro_rules! check_object_exists {
($keylet:expr, $type:expr, $field:expr) => {
match object_exists($keylet, $type, $field) {
Ok(_exists) => {
// false isn't returned
let _ = trace(concat!(
$type,
" object exists, proceeding with escrow finish."
));
}
Err(error) => {
let _ = trace_num("Current seq value:", seq.try_into().unwrap());
return error.code();
}
}
};
}
let accountroot_id = keylets::accountroot_id(&account);
check_object_exists!(accountroot_id, "Account", sfield::Account);
let currency_code: &[u8; 3] = b"USD";
let currency: Currency = Currency::from(*currency_code);
let trustline_id = keylets::trustline_id(&account, &destination, &currency);
check_object_exists!(trustline_id, "Trustline", sfield::Generic);
seq += 1;
let asset1 = Issue::XRP(XrpIssue {});
let asset2 = Issue::IOU(IouIssue::new(destination, currency));
check_object_exists!(keylets::amm_id(&asset1, &asset2), "AMM", sfield::Account);
let check_id = keylets::check_id(&account, seq);
check_object_exists!(check_id, "Check", sfield::Account);
seq += 1;
let cred_type: &[u8] = b"termsandconditions";
let credential_id = keylets::credential_id(&account, &account, cred_type);
check_object_exists!(credential_id, "Credential", sfield::Subject);
seq += 1;
let delegate_id = keylets::delegate_id(&account, &destination);
check_object_exists!(delegate_id, "Delegate", sfield::Account);
seq += 1;
let deposit_preauth_id = keylets::deposit_preauth_id(&account, &destination);
check_object_exists!(deposit_preauth_id, "DepositPreauth", sfield::Account);
seq += 1;
let did_id = keylets::did_id(&account);
check_object_exists!(did_id, "DID", sfield::Account);
seq += 1;
let escrow_id = keylets::escrow_id(&account, seq);
check_object_exists!(escrow_id, "Escrow", sfield::Account);
seq += 1;
let mpt_issuance_id = keylets::mpt_issuance_id(&account, seq);
let mpt_id = MptId::new(seq.try_into().unwrap(), account);
check_object_exists!(mpt_issuance_id, "MPTIssuance", sfield::Issuer);
seq += 1;
let mptoken_id = keylets::mptoken_id(&mpt_id, &destination);
check_object_exists!(mptoken_id, "MPToken", sfield::Account);
let nft_offer_id = keylets::nft_offer_id(&destination, 6);
check_object_exists!(nft_offer_id, "NFTokenOffer", sfield::Owner);
let offer_id = keylets::offer_id(&account, seq);
check_object_exists!(offer_id, "Offer", sfield::Account);
seq += 1;
let paychan_id = keylets::paychan_id(&account, &destination, seq);
check_object_exists!(paychan_id, "PayChannel", sfield::Account);
seq += 1;
let pd_id = keylets::permissioned_domain_id(&account, seq);
check_object_exists!(pd_id, "PermissionedDomain", sfield::Owner);
seq += 1;
let signers_id = keylets::signers_id(&account);
check_object_exists!(signers_id, "SignerList", sfield::Generic);
seq += 1;
seq += 1; // ticket sequence number is one greater
let ticket_id = keylets::ticket_id(&account, seq);
check_object_exists!(ticket_id, "Ticket", sfield::Account);
seq += 1;
let vault_id = keylets::vault_id(&account, seq);
check_object_exists!(vault_id, "Vault", sfield::Account);
// seq += 1;
1 // All keylets exist, finish the escrow.
}

View File

@@ -1,42 +0,0 @@
#include <stdint.h>
int32_t float_from_uint(uint8_t const *, int32_t, uint8_t *, int32_t, int32_t);
int32_t check_id(uint8_t const *, int32_t, uint8_t const *, int32_t, uint8_t *,
int32_t);
uint8_t e_data1[32 * 1024];
uint8_t e_data2[32 * 1024];
int32_t test1()
{
e_data1[1] = 0xFF;
e_data1[2] = 0xFF;
e_data1[3] = 0xFF;
e_data1[4] = 0xFF;
e_data1[5] = 0xFF;
e_data1[6] = 0xFF;
e_data1[7] = 0xFF;
e_data1[8] = 0xFF;
int32_t result = float_from_uint(&e_data1[1], 8, &e_data1[35], 12, 0);
return result >= 0 ? *((int32_t *)(&e_data1[36])) : result;
}
int32_t test2()
{
// Set up misaligned uint32 (seq) at offset 1
e_data2[1] = 0xFF;
e_data2[2] = 0xFF;
e_data2[3] = 0xFF;
e_data2[4] = 0xFF;
// Set up valid non-zero AccountID (20 bytes) at offset 10
for (int i = 0; i < 20; i++)
e_data2[10 + i] = i + 1;
// Call check_id with misaligned uint32 at &e_data2[1] to hit line 72 in
// HostFuncWrapper.cpp
int32_t result = check_id(&e_data2[10], 20, &e_data2[1], 4, &e_data2[35], 32);
// Return the misaligned value directly to validate it was read correctly (-1
// if all 0xFF)
return result >= 0 ? *((int32_t *)(&e_data2[36])) : result;
}
int32_t test() { return test1() + test2(); }

View File

@@ -1,180 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "block-buffer"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"hybrid-array",
]
[[package]]
name = "bs58"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
dependencies = [
"tinyvec",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "codecov_tests"
version = "0.0.1"
dependencies = [
"xrpl-common-stdlib",
"xrpl-escrow-stdlib",
]
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"hybrid-array",
]
[[package]]
name = "digest"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
]
[[package]]
name = "hybrid-array"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
dependencies = [
"typenum",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "sha2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tinyvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "typenum"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "xrpl-common-stdlib"
version = "0.9.0"
source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git#e88dc32a48fac9d0eb6e7239c8a38693221657f6"
dependencies = [
"xrpl-macros",
]
[[package]]
name = "xrpl-escrow-stdlib"
version = "0.9.0"
source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git#e88dc32a48fac9d0eb6e7239c8a38693221657f6"
dependencies = [
"xrpl-common-stdlib",
]
[[package]]
name = "xrpl-macros"
version = "0.9.0"
source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git#e88dc32a48fac9d0eb6e7239c8a38693221657f6"
dependencies = [
"bs58",
"proc-macro2",
"quote",
"sha2",
"syn",
]

View File

@@ -1,19 +0,0 @@
[package]
edition = "2024"
name = "codecov_tests"
version = "0.0.1"
# This empty workspace definition keeps this project independent of the parent workspace
[workspace]
[lib]
crate-type = ["cdylib"]
[profile.release]
lto = true
opt-level = 's'
panic = "abort"
[dependencies]
xrpl-std = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-common-stdlib" }
xrpl-escrow = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-escrow-stdlib" }

View File

@@ -1,56 +0,0 @@
//TODO add docs after discussing the interface
//Note that Craft currently does not honor the rounding modes
#[allow(unused)]
pub const FLOAT_ROUNDING_MODES_TO_NEAREST: i32 = 0;
#[allow(unused)]
pub const FLOAT_ROUNDING_MODES_TOWARDS_ZERO: i32 = 1;
#[allow(unused)]
pub const FLOAT_ROUNDING_MODES_DOWNWARD: i32 = 2;
#[allow(unused)]
pub const FLOAT_ROUNDING_MODES_UPWARD: i32 = 3;
// pub enum RippledRoundingModes{
// ToNearest = 0,
// TowardsZero = 1,
// DOWNWARD = 2,
// UPWARD = 3
// }
#[allow(unused)]
#[link(wasm_import_module = "host_lib")]
unsafe extern "C" {
pub fn parent_ldgr_hash(out_buff_ptr: i32, out_buff_len: i32) -> i32;
pub fn cache_le(keylet_ptr: i32, keylet_len: i32, cache_num: i32) -> i32;
pub fn tx_inner_arr_len(locator_ptr: i32, locator_len: i32) -> i32;
pub fn accountroot_id(
account_ptr: i32,
account_len: i32,
out_buff_ptr: *mut u8,
out_buff_len: usize,
) -> i32;
pub fn trustline_id(
account1_ptr: *const u8,
account1_len: usize,
account2_ptr: *const u8,
account2_len: usize,
currency_ptr: i32,
currency_len: i32,
out_buff_ptr: *mut u8,
out_buff_len: usize,
) -> i32;
// Same wasm functype as the real binding, so this is not a second import of
// host_lib.trace. Loose i32 pointers exercise the out-of-bounds path.
#[link_name = "trace"]
pub fn trace_loose(
msg_read_ptr: i32,
msg_read_len: i32,
data_type: i32,
data_read_ptr: i32,
data_read_len: i32,
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,287 +0,0 @@
# cspell: disable
import os
import re
import shlex
import subprocess
import sys
import tempfile
import zipfile
from difflib import get_close_matches
OPT = "-Oz"
BASE_PATH = os.path.abspath(os.path.dirname(__file__))
def pascal_case(name):
return "".join(word[:1].upper() + word[1:] for word in re.split(r"[_\W]+", name))
def normalize_name(name):
name = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
return re.sub(r"[^a-z0-9]", "", name.lower())
def fixture_key(name):
name = normalize_name(name).removeprefix("k")
return name.removesuffix("wasmhex").removesuffix("hex")
def declared_fixtures():
h_path = os.path.join(BASE_PATH, "fixtures.h")
with open(h_path, "r", encoding="utf8") as f:
return re.findall(
r"extern std::string const ([A-Za-z_][A-Za-z0-9_]*);", f.read()
)
def find_fixture_name(project_name, suffix):
default = re.sub(r"_([a-z])", lambda m: m.group(1).upper(), project_name) + suffix
k_default = f"k{pascal_case(project_name)}{suffix}"
declarations = declared_fixtures()
normalized = {normalize_name(name): name for name in declarations}
fixture_keys = {fixture_key(name): name for name in declarations}
for name in (default, k_default):
if normalize_name(name) in normalized:
return normalized[normalize_name(name)]
project_key = normalize_name(project_name)
matches = [
name
for key, name in fixture_keys.items()
if key.endswith(project_key)
or key.startswith(project_key)
or project_key.endswith(key)
or project_key.startswith(key)
]
if len(matches) == 1:
return matches[0]
close = get_close_matches(project_key, fixture_keys.keys(), n=1, cutoff=0.82)
if close:
return fixture_keys[close[0]]
return k_default
def fixture_cpp_path(fixture_name):
pattern = rf"extern std::string const {fixture_name} ="
for file_name in os.listdir(BASE_PATH):
if not file_name.endswith(".cpp"):
continue
cpp_path = os.path.join(BASE_PATH, file_name)
with open(cpp_path, "r", encoding="utf8") as f:
if re.search(pattern, f.read()):
return cpp_path
return os.path.join(BASE_PATH, "fixtures.cpp")
def update_fixture(project_name, wasm, suffix="WasmHex"):
fixture_name = find_fixture_name(project_name, suffix)
print(f"Updating fixture: {fixture_name}")
cpp_path = fixture_cpp_path(fixture_name)
h_path = os.path.join(BASE_PATH, "fixtures.h")
with open(cpp_path, "r", encoding="utf8") as f:
cpp_content = f.read()
pattern = rf'extern std::string const {fixture_name} =[ \n]+"[^;]*;'
if re.search(pattern, cpp_content, flags=re.MULTILINE):
updated_cpp_content = re.sub(
pattern,
f'extern std::string const {fixture_name} = "{wasm}";',
cpp_content,
flags=re.MULTILINE,
)
else:
with open(h_path, "r", encoding="utf8") as f:
h_content = f.read()
updated_h_content = (
h_content.rstrip() + f"\n\nextern std::string const {fixture_name};\n"
)
with open(h_path, "w", encoding="utf8") as f:
f.write(updated_h_content)
updated_cpp_content = (
cpp_content.rstrip()
+ f'\n\nextern std::string const {fixture_name} = "{wasm}";\n'
)
with open(cpp_path, "w", encoding="utf8") as f:
f.write(updated_cpp_content)
def read_wasm_hex(path):
with open(path, "rb") as f:
return f.read().hex()
def process_rust(project_name):
project_path = os.path.join(BASE_PATH, project_name)
wasm_location = os.path.join(
project_path, "target", "wasm32v1-none", "release", f"{project_name}.wasm"
)
try:
subprocess.run(
["cargo", "build", "--target", "wasm32v1-none", "--release"],
cwd=project_path,
check=True,
)
subprocess.run(
["wasm-opt", wasm_location, OPT, "-o", wasm_location], check=True
)
print(f"WASM file for {project_name} has been built and optimized.")
except FileNotFoundError as e:
print(f"exec error: {e.filename} is required to build Rust fixtures")
sys.exit(1)
except subprocess.CalledProcessError as e:
print(f"exec error: {e}")
sys.exit(1)
update_fixture(project_name, read_wasm_hex(wasm_location))
def process_c(project_name):
project_path = os.path.join(BASE_PATH, f"{project_name}.c")
wasm_path = os.path.join(BASE_PATH, f"{project_name}.wasm")
cc = os.environ.get("CC")
sysroot = os.environ.get("SYSROOT")
if not cc or not sysroot:
print("exec error: CC and SYSROOT are required to build C fixtures")
sys.exit(1)
build_cmd = [
*shlex.split(cc),
f"--sysroot={sysroot}",
"-O3",
"-ffast-math",
"--target=wasm32",
"-fno-exceptions",
"-fno-threadsafe-statics",
"-fvisibility=default",
"-Wl,--export-all",
"-Wl,--no-entry",
"-Wl,--allow-undefined",
"-DNDEBUG",
"--no-standard-libraries",
"-fno-builtin-memset",
"-o",
wasm_path,
project_path,
]
try:
subprocess.run(build_cmd, check=True)
subprocess.run(["wasm-opt", wasm_path, OPT, "-o", wasm_path], check=True)
print(
f"WASM file for {project_name} has been built with WASI support using clang."
)
except FileNotFoundError as e:
print(f"exec error: {e.filename} is required to build C fixtures")
sys.exit(1)
except subprocess.CalledProcessError as e:
print(f"exec error: {e}")
sys.exit(1)
update_fixture(project_name, read_wasm_hex(wasm_path))
def wat_to_wasm(wat_path, wasm_path):
build_cmd = ["wat2wasm", wat_path, "-o", wasm_path]
try:
subprocess.run(build_cmd, check=True)
print(f"WASM file for {os.path.basename(wat_path)} has been built.")
except FileNotFoundError:
print("exec error: wat2wasm is required to build WAT fixtures")
sys.exit(1)
except subprocess.CalledProcessError as e:
print(f"exec error: {e}")
sys.exit(1)
def process_wat_file(wat_path):
project_name = os.path.splitext(os.path.basename(wat_path))[0]
with open(wat_path, "r", encoding="utf8") as f:
if "(module" not in f.read():
print(f"Skipping WAT fixture without a module: {project_name}")
return
with tempfile.TemporaryDirectory() as tmpdir:
wasm_path = os.path.join(tmpdir, f"{project_name}.wasm")
wat_to_wasm(wat_path, wasm_path)
update_fixture(project_name, read_wasm_hex(wasm_path), "Hex")
def process_wat_zip(zip_path):
project_name = os.path.splitext(os.path.basename(zip_path))[0]
with tempfile.TemporaryDirectory() as tmpdir:
with zipfile.ZipFile(zip_path) as archive:
wat_names = [name for name in archive.namelist() if name.endswith(".wat")]
if len(wat_names) != 1:
print(f"exec error: expected one .wat file in {zip_path}")
sys.exit(1)
archive.extract(wat_names[0], tmpdir)
wasm_path = os.path.join(tmpdir, f"{project_name}.wasm")
wat_to_wasm(os.path.join(tmpdir, wat_names[0]), wasm_path)
update_fixture(project_name, read_wasm_hex(wasm_path), "Hex")
def process_wat(project_name):
candidates = [
os.path.join(BASE_PATH, f"{project_name}.wat"),
os.path.join(BASE_PATH, "wat", f"{project_name}.wat"),
os.path.join(BASE_PATH, "wat", f"{project_name}.zip"),
]
for path in candidates:
if os.path.isfile(path):
if path.endswith(".zip"):
process_wat_zip(path)
else:
process_wat_file(path)
return
print(f"exec error: fixture {project_name} not found")
sys.exit(1)
if __name__ == "__main__":
if len(sys.argv) > 2:
print("Usage: python copyFixtures.py [<project_name>]")
sys.exit(1)
if len(sys.argv) == 2:
project_name = os.path.splitext(os.path.basename(sys.argv[1]))[0]
if os.path.isfile(os.path.join(BASE_PATH, project_name, "Cargo.toml")):
process_rust(project_name)
elif os.path.isfile(os.path.join(BASE_PATH, f"{project_name}.c")):
process_c(project_name)
else:
process_wat(project_name)
print("Fixture has been processed.")
else:
dirs = [
d
for d in os.listdir(BASE_PATH)
if os.path.isfile(os.path.join(BASE_PATH, d, "Cargo.toml"))
]
c_files = [f for f in os.listdir(BASE_PATH) if f.endswith(".c")]
wat_files = [f for f in os.listdir(BASE_PATH) if f.endswith(".wat")]
wat_path = os.path.join(BASE_PATH, "wat")
wat_fixture_files = [
f
for f in (os.listdir(wat_path) if os.path.isdir(wat_path) else [])
if f.endswith((".wat", ".zip"))
]
for d in sorted(dirs):
process_rust(d)
for c in sorted(c_files):
process_c(c[:-2])
for wat in sorted(wat_files):
process_wat_file(os.path.join(BASE_PATH, wat))
for wat_fixture in sorted(wat_fixture_files):
path = os.path.join(wat_path, wat_fixture)
if wat_fixture.endswith(".zip"):
process_wat_zip(path)
else:
process_wat_file(path)
print("All fixtures have been processed.")

View File

@@ -1,648 +0,0 @@
#include <test/app/wasm_fixtures/fixtures.h>
#include <string>
extern std::string const kLedgerSqnWasmHex =
"0061736d01000000010e0360027f7f017f6000006000017f02120103656e760a6c6467725f696e6465780000030302"
"01020503010002063f0a7f01418088040b7f004180080b7f004180080b7f004180080b7f00418088040b7f00418008"
"0b7f00418088040b7f00418080080b7f0041000b7f0041010b07b1010c066d656d6f72790200115f5f7761736d5f63"
"616c6c5f63746f727300010d657363726f775f66696e69736800020c5f5f64736f5f68616e646c6503010a5f5f6461"
"74615f656e6403020b5f5f737461636b5f6c6f7703030c5f5f737461636b5f6869676803040d5f5f676c6f62616c5f"
"6261736503050b5f5f686561705f6261736503060a5f5f686561705f656e6403070d5f5f6d656d6f72795f62617365"
"03080c5f5f7461626c655f6261736503090a3d0202000b3801037f230041106b220024002000410c6a410410002101"
"200028020c2102200041106a2400200141054100200241054f1b20014100481b0b007f0970726f647563657273010c"
"70726f6365737365642d62790105636c616e675f31392e312e352d776173692d73646b202868747470733a2f2f6769"
"746875622e636f6d2f6c6c766d2f6c6c766d2d70726f6a656374206162346235613264623538323935386166316565"
"33303861373930636664623432626432343732302900490f7461726765745f6665617475726573042b0f6d75746162"
"6c652d676c6f62616c732b087369676e2d6578742b0f7265666572656e63652d74797065732b0a6d756c746976616c"
"7565";
extern std::string const kAllHostFunctionsWasmHex =
"0061736d0100000001550c60027f7f017f60037f7f7f017f60047f7f7f7f017f60017f017f60067f7f7f7f7f7f017f"
"60037f7f7f0060057f7f7f7f7f0060087f7f7f7f7f7f7f7f017f60057f7f7f7f7f017f60017f0060027f7f00600001"
"7f02b1041808686f73745f6c69620874785f6669656c64000108686f73745f6c6962057472616365000608686f7374"
"5f6c69620a6c6467725f696e646578000008686f73745f6c696210706172656e745f6c6467725f74696d6500000868"
"6f73745f6c696210706172656e745f6c6467725f68617368000008686f73745f6c69620874785f696e6e6572000208"
"686f73745f6c69620a74785f6172725f6c656e000308686f73745f6c69621074785f696e6e65725f6172725f6c656e"
"000008686f73745f6c69620d686f6d655f6c655f6669656c64000108686f73745f6c69620d686f6d655f6c655f696e"
"6e6572000208686f73745f6c69620f686f6d655f6c655f6172725f6c656e000308686f73745f6c696215686f6d655f"
"6c655f696e6e65725f6172725f6c656e000008686f73745f6c69620863616368655f6c65000108686f73745f6c6962"
"0d63726564656e7469616c5f6964000708686f73745f6c696209657363726f775f6964000408686f73745f6c696209"
"6f7261636c655f6964000408686f73745f6c69620b7368613531325f68616c66000208686f73745f6c6962076e6674"
"5f757269000408686f73745f6c6962087365745f64617461000008686f73745f6c69620a6c655f6172725f6c656e00"
"0008686f73745f6c69620e6163636f756e74726f6f745f6964000208686f73745f6c6962106c655f696e6e65725f61"
"72725f6c656e000108686f73745f6c6962086c655f6669656c64000208686f73745f6c6962086c655f696e6e657200"
"08030b0a090a05050b000101030005030100110619037f01418080c0000b7f0041c698c0000b7f0041d098c0000b07"
"3504066d656d6f727902000d657363726f775f66696e697368001c0a5f5f646174615f656e6403010b5f5f68656170"
"5f6261736503020acf210a9d0101027f230041206b2201240020014200370310200142003703082001410036021820"
"00027f024041818020200141086a41141000220241004e0440200241144b0d01200241144704402000418180808078"
"36020441010c030b20002001280218360011200020012903103700092000200129030837000141000c020b20002002"
"36020441010c010b2000417336020441010b3a0000200141206a24000b5a01017f230041106b2202240020012d0000"
"41014604402002200134020437030841df97c000410b4101200241086a41081001000b200020012800113600102000"
"200129000937000820002001290001370000200241106a24000b1900200241214f0440000b20002002360204200020"
"013602000b1900200241094f0440000b20002002360204200020013602000bdd1e01077f230041c0036b2200240041"
"ea97c000411b4107410141001001418598c0004119410741014100100141b583c000412b4107410141001001200041"
"003602500240024002400240024002400240200041d0006a41041002220141004a044020002000280250220141ff81"
"fc0771410878200141187841ff81fc077172ad3703c00141e083c00041174101200041c0016a220241081001200041"
"003602800120004180016a41041003220141004c0d012000200028028001220141ff81fc0771410878200141187841"
"ff81fc077172ad3703c00141f783c00041134101200241081001200042003703d801200042003703d0012000420037"
"03c801200042003703c0012002412010042201412047044020002001ac3703a00141bd84c000412b4101200041a001"
"6a4108100141997f21030c080b418a84c00041134106200041c0016a220441201001419d84c0004120410741014100"
"100141aa85c000412e4107410141001001200041003602b001200042003703a801200042003703a001418180202000"
"41a0016a22024114100022014114470d0241d885c00041144104200241141001200042003703384188801820004138"
"6a22024108100022014108470d03200042083703c00141ec85c00041174101200441081001418386c0004128410620"
"02410810012000410036027841848008200041f8006a22024104100022014104470d0441ab86c00041154106200241"
"04100120004100360051200041013a005020004100360054200042003703d801200042003703d001200042003703c8"
"01200042003703c0010240200041d0006a4108200441201005220141004e044020002001ad3703800141c086c00041"
"14410120004180016a41081001200041306a20042001101a41d486c000410d41062000280230200028023410010c01"
"0b20002001ac3703800141e186c0004129410120004180016a410810010b20004183803c1006ac37038001418a87c0"
"004115410120004180016a22024108100120004189803c1006ac37038001419f87c000411341012002410810010240"
"200041d0006a41081007220141004e044020002001ad3703800141b287c000411441012002410810010c010b200020"
"01ac3703800141c687c000412d410120004180016a410810010b41f387c0004123410741014100100141e792c00041"
"3341074101410010012000420037033841828018200041386a220141081008220241004c0d05200241084604402000"
"42083703c001419a93c000412b4101200041c0016a4108100141c593c000412f41062001410810010c070b20002002"
"ad3703c00141f493c000412f4101200041c0016a41081001200041286a200041386a2002101b41a394c00041174106"
"2000280228200028022c10010c060b20002001ac3703c001418d85c000411d4101200041c0016a41081001419b7f21"
"030c060b20002001ac3703c00141e884c00041254101200041c0016a41081001419a7f21030c050b20002001ac3703"
"c001418289c000412a4101200041c0016a4108100141b77e21030c040b20002001ac3703c00141c188c00041c10041"
"01200041c0016a4108100141b67e21030c030b20002001ac3703c001419688c000412b4101200041c0016a41081001"
"41b57e21030c020b20002002ac3703c00141ba94c00041c5004101200041c0016a410810010b200041003602b00120"
"0042003703a801200042003703a001024041818020200041a0016a220241141008220141004a044041ff94c000411e"
"41042002411410010c010b20002001ac3703c001419d95c00041334101200041c0016a410810010b20004100360051"
"200041013a005020004100360054200042003703d801200042003703d001200042003703c801200042003703c00102"
"40200041d0006a4108200041c0016a220141201009220241004e044020002002ad3703800141d095c000411c410120"
"004180016a41081001200041206a20012002101a41ec95c000411541062000280220200028022410010c010b200020"
"02ac37038001418196c0004139410120004180016a410810010b20004183803c100aac3703800141ba96c000412441"
"0120004180016a2201410810010240200041d0006a4108100b220241004e044020002002ad3703800141de96c00041"
"1c41012001410810010c010b20002002ac3703800141fa96c000413d410120004180016a410810010b41b797c00041"
"28410741014100100141ac89c000412f4107410141001001200041c0016a2204101820004180016a22012004101920"
"0042003703b801200042003703b001200042003703a801200042003703a001024002400240024002402001200041a0"
"016a2202101d22014120460440200241204100100c220541004a044020002005ad3703c00141db89c0004123410120"
"0441081001200042003703782005200041f8006a22014108101e220241004c0d0220024108460440200042083703c0"
"0141fe89c000412a410120044108100141a88ac000412e41062001410810010c060b20002002ad3703c00141d68ac0"
"00412e4101200041c0016a41081001200041186a200041f8006a2002101b41848bc000411641062000280218200028"
"021c10010c050b20002005ac3703c00141bc8dc000413c4101200041c0016a220141081001200042003703d8012000"
"42003703d001200042003703c801200042003703c001410120014120101e22014100480d020c030b20002001ac3703"
"c001419090c000412e4101200041c0016a4108100141ef7c21030c050b20002002ac3703c001419a8bc000412b4101"
"200041c0016a410810010c020b20002001ac37035041f88dc00041c1004101200041d0006a410810010b2000410036"
"0039200041013a00382000410036003c4101200041386a200041c0016a101f2201410048044020002001ac37035041"
"b98ec00041354101200041d0006a410810010b410110202201410048044020002001ac37035041ee8ec00041324101"
"200041d0006a410810010b4101200041386a10212201410048044020002001ac37035041a08fc00041394101200041"
"d0006a410810010b41d98fc000413741074101410010010c010b20004100360039200041013a00382000410036003c"
"200042003703d801200042003703d001200042003703c801200042003703c00102402005200041386a200041c0016a"
"2201101f220241004e044020002002ad37035041c58bc000411b4101200041d0006a41081001200041106a20012002"
"101a41e08bc000411441062000280210200028021410010c010b20002002ac37035041f48bc00041314101200041d0"
"006a410810010b200020051020ac37035041a58cc00041234101200041d0006a22014108100102402005200041386a"
"1021220241004e044020002002ad37035041c88cc000411b41012001410810010c010b20002002ac37035041e38cc0"
"0041354101200041d0006a410810010b41988dc000412441074101410010010b41be90c000412f4107410141001001"
"200041c0016a22011018200041386a2204200110192000420037036820004200370360200042003703582000420037"
"0350024002402004200041d0006a2202101d2201412046044041ed90c000410f410620024120100120004200370398"
"012000420037039001200042003703880120004200370380010240200441142004411441fc90c00041092000418001"
"6a22014120100d220241004a0440200041086a20012002101a418491c000411241062000280208200028020c10010c"
"010b20002002ac3703c001419691c000413c4101200041c0016a410810010b200042003703b801200042003703b001"
"200042003703a801200042003703a00120004180808cc07e360270200041386a22044114200041f0006a4104200041"
"a0016a22024120100e22014120470d0141d291c000410e4106200241201001200042003703d801200042003703d001"
"200042003703c801200042003703c001200041808080d00236027420044114200041f4006a4104200041c0016a4120"
"100f2201412047044020002001ac370378419292c000411c4101200041f8006a4108100141887c21030c040b41e091"
"c000410e4106200041c0016a22044120100141ee91c00041244107410141001001418080c000412541074101410010"
"0120004200370398012000420037039001200042003703880120004200370380010240024041a580c0004117200041"
"80016a2202412010102201412046044041bc80c000410b410641a580c0004117100141c780c0004111410620024120"
"100120041018200041d0006a220620041019200042003703b801200042003703b001200042003703a8012000420037"
"03a00102404100200422036b410371220220036a220520034d0d0020020440200221010340200341003a0000200341"
"016a2103200141016b22010d000b0b200241016b4107490d000340200341003a0000200341076a41003a0000200341"
"066a41003a0000200341056a41003a0000200341046a41003a0000200341036a41003a0000200341026a41003a0000"
"200341016a41003a0000200341086a22032005470d000b0b200541800220026b2201417c716a220320054b04400340"
"20054100360200200541046a22052003490d000b0b024020032001410371220120036a22024f0d0020012205044003"
"40200341003a0000200341016a2103200541016b22050d000b0b200141016b4107490d000340200341003a00002003"
"41076a41003a0000200341066a41003a0000200341056a41003a0000200341046a41003a0000200341036a41003a00"
"00200341026a41003a0000200341016a41003a0000200341086a22032002470d000b0b20064114200041a0016a4120"
"20044180021011220141004c0d0120002001ad37033841d880c00041104101200041386a4108100120014181024f0d"
"0541e880c000410941062004200110010c020b20002001ac3703c00141e381c00041224101200041c0016a41081001"
"41a77b21030c050b20002001ac37033841f180c000412e4101200041386a410810010b419f81c0004112410641b181"
"c000410710012000422a3703384101210341b881c00041114101200041386a4108100141c981c000411a4107410141"
"001001418582c0004129410741014100100141ae82c000412810122201412847044020002001ac3703c001419b83c0"
"00411a4101200041c0016a4108100141c37a21030c040b41d682c0004127410641ae82c0004128100141fd82c00041"
"1e4107410141001001419e98c000412841074101410010010c030b20002001ac3703c00141ca92c000411d41012000"
"41c0016a41081001418b7c21030c020b20002001ac3703c00141ae92c000411c4101200041c0016a4108100141897c"
"21030c010b000b200041c0036a240020030b0c00200041142001412010140b0e002000418280182001200210160b0e"
"002000200141082002412010170b0a0020004183803c10130b0a0020002001410810150b0bd0180100418080c0000b"
"c6182d2d2d2043617465676f727920363a205574696c6974792046756e6374696f6e73202d2d2d48656c6c6f2c2058"
"52504c205741534d20776f726c6421496e70757420646174613a5348413531322068616c6620686173683a4e465420"
"64617461206c656e6774683a4e465420646174613a494e464f3a206765745f6e6674206661696c6564202865787065"
"63746564202d206e6f2073756368204e4654293a54657374207472616365206d6573736167657061796c6f61645465"
"7374206e756d626572207472616365535543434553533a205574696c6974792066756e6374696f6e734552524f523a"
"20636f6d707574655f7368613531325f68616c66206661696c65643a2d2d2d2043617465676f727920373a20446174"
"61205570646174652046756e6374696f6e73202d2d2d55706461746564206c656467657220656e7472792064617461"
"2066726f6d205741534d20746573745375636365737366756c6c792075706461746564206c656467657220656e7472"
"7920776974683a535543434553533a2044617461207570646174652066756e6374696f6e734552524f523a20757064"
"6174655f64617461206661696c65643a2d2d2d2043617465676f727920313a204c6564676572204865616465722046"
"756e6374696f6e73202d2d2d4c65646765722073657175656e6365206e756d6265723a506172656e74206c65646765"
"722074696d653a506172656e74206c656467657220686173683a535543434553533a204c6564676572206865616465"
"722066756e6374696f6e734552524f523a206765745f706172656e745f6c65646765725f686173682077726f6e6720"
"6c656e6774683a4552524f523a206765745f706172656e745f6c65646765725f74696d65206661696c65643a455252"
"4f523a206765745f6c65646765725f73716e206661696c65643a2d2d2d2043617465676f727920323a205472616e73"
"616374696f6e20446174612046756e6374696f6e73202d2d2d5472616e73616374696f6e204163636f756e743a5472"
"616e73616374696f6e20466565206c656e6774683a5472616e73616374696f6e20466565202873657269616c697a65"
"642058525020616d6f756e74293a5472616e73616374696f6e2053657175656e63653a4e6573746564206669656c64"
"206c656e6774683a4e6573746564206669656c643a494e464f3a206765745f74785f6e65737465645f6669656c6420"
"6e6f74206170706c696361626c653a5369676e657273206172726179206c656e6774683a4d656d6f73206172726179"
"206c656e6774683a4e6573746564206172726179206c656e6774683a494e464f3a206765745f74785f6e6573746564"
"5f61727261795f6c656e206e6f74206170706c696361626c653a535543434553533a205472616e73616374696f6e20"
"646174612066756e6374696f6e734552524f523a206765745f74785f6669656c642853657175656e6365292077726f"
"6e67206c656e6774683a4552524f523a206765745f74785f6669656c6428466565292077726f6e67206c656e677468"
"20286578706563746564203820627974657320666f7220585250293a4552524f523a206765745f74785f6669656c64"
"284163636f756e74292077726f6e67206c656e6774683a2d2d2d2043617465676f727920343a20416e79204c656467"
"6572204f626a6563742046756e6374696f6e73202d2d2d5375636365737366756c6c7920636163686564206f626a65"
"637420696e20736c6f743a436163686564206f626a6563742062616c616e6365206c656e677468202858525020616d"
"6f756e74293a436163686564206f626a6563742062616c616e6365202873657269616c697a65642058525020616d6f"
"756e74293a436163686564206f626a6563742062616c616e6365206c656e67746820286e6f6e2d58525020616d6f75"
"6e74293a436163686564206f626a6563742062616c616e63653a494e464f3a206765745f6c65646765725f6f626a5f"
"6669656c642842616c616e636529206661696c65643a436163686564206e6573746564206669656c64206c656e6774"
"683a436163686564206e6573746564206669656c643a494e464f3a206765745f6c65646765725f6f626a5f6e657374"
"65645f6669656c64206e6f74206170706c696361626c653a436163686564206f626a656374205369676e6572732061"
"72726179206c656e6774683a436163686564206e6573746564206172726179206c656e6774683a494e464f3a206765"
"745f6c65646765725f6f626a5f6e65737465645f61727261795f6c656e206e6f74206170706c696361626c653a5355"
"43434553533a20416e79206c6564676572206f626a6563742066756e6374696f6e73494e464f3a2063616368655f6c"
"65646765725f6f626a206661696c65642028657870656374656420776974682074657374206669787475726573293a"
"494e464f3a206765745f6c65646765725f6f626a5f6669656c64206661696c65642061732065787065637465642028"
"6e6f20636163686564206f626a656374293a494e464f3a206765745f6c65646765725f6f626a5f6e65737465645f66"
"69656c64206661696c65642061732065787065637465643a494e464f3a206765745f6c65646765725f6f626a5f6172"
"7261795f6c656e206661696c65642061732065787065637465643a494e464f3a206765745f6c65646765725f6f626a"
"5f6e65737465645f61727261795f6c656e206661696c65642061732065787065637465643a535543434553533a2041"
"6e79206c6564676572206f626a6563742066756e6374696f6e732028696e7465726661636520746573746564294552"
"524f523a206163636f756e74726f6f745f6964206661696c656420666f722063616368696e6720746573743a2d2d2d"
"2043617465676f727920353a204b65796c65742047656e65726174696f6e2046756e6374696f6e73202d2d2d416363"
"6f756e74206b65796c65743a546573745479706543726564656e7469616c206b65796c65743a494e464f3a20637265"
"64656e7469616c5f6b65796c6574206661696c656420286578706563746564202d20696e7465726661636520697373"
"7565293a457363726f77206b65796c65743a4f7261636c65206b65796c65743a535543434553533a204b65796c6574"
"2067656e65726174696f6e2066756e6374696f6e734552524f523a206f7261636c655f6b65796c6574206661696c65"
"643a4552524f523a20657363726f775f6b65796c6574206661696c65643a4552524f523a206163636f756e74726f6f"
"745f6964206661696c65643a2d2d2d2043617465676f727920333a2043757272656e74204c6564676572204f626a65"
"63742046756e6374696f6e73202d2d2d43757272656e74206f626a6563742062616c616e6365206c656e6774682028"
"58525020616d6f756e74293a43757272656e74206f626a6563742062616c616e6365202873657269616c697a656420"
"58525020616d6f756e74293a43757272656e74206f626a6563742062616c616e6365206c656e67746820286e6f6e2d"
"58525020616d6f756e74293a43757272656e74206f626a6563742062616c616e63653a494e464f3a206765745f6375"
"7272656e745f6c65646765725f6f626a5f6669656c642842616c616e636529206661696c656420286d617920626520"
"6578706563746564293a43757272656e74206c6564676572206f626a656374206163636f756e743a494e464f3a2067"
"65745f63757272656e745f6c65646765725f6f626a5f6669656c64284163636f756e7429206661696c65643a437572"
"72656e74206e6573746564206669656c64206c656e6774683a43757272656e74206e6573746564206669656c643a49"
"4e464f3a206765745f63757272656e745f6c65646765725f6f626a5f6e65737465645f6669656c64206e6f74206170"
"706c696361626c653a43757272656e74206f626a656374205369676e657273206172726179206c656e6774683a4375"
"7272656e74206e6573746564206172726179206c656e6774683a494e464f3a206765745f63757272656e745f6c6564"
"6765725f6f626a5f6e65737465645f61727261795f6c656e206e6f74206170706c696361626c653a53554343455353"
"3a2043757272656e74206c6564676572206f626a6563742066756e6374696f6e736572726f725f636f64653d3d3d3d"
"20484f53542046554e4354494f4e532054455354203d3d3d54657374696e6720323620686f73742066756e6374696f"
"6e73535543434553533a20416c6c20686f73742066756e6374696f6e2074657374732070617373656421004d097072"
"6f64756365727302086c616e6775616765010452757374000c70726f6365737365642d6279010572757374631d312e"
"39352e30202835393830373631366520323032362d30342d313429002c0f7461726765745f6665617475726573022b"
"0f6d757461626c652d676c6f62616c732b087369676e2d657874";
extern std::string const kAllKeyletsWasmHex =
"0061736d0100000001500a60067f7f7f7f7f7f017f60047f7f7f7f017f60087f7f7f7f7f7f7f7f017f60047f7f7f7f"
"0060037f7f7f017f60037f7f7e017f60057f7f7f7f7f017f6000017f60037f7f7f0060067f7f7f7f7f7e00029f0418"
"08686f73745f6c69620974726163655f6e756d000508686f73745f6c6962057472616365000608686f73745f6c6962"
"0863616368655f6c65000408686f73745f6c6962086c655f6669656c64000108686f73745f6c69620d686f6d655f6c"
"655f6669656c64000408686f73745f6c69620a74726163655f61636374000108686f73745f6c69620e6163636f756e"
"74726f6f745f6964000108686f73745f6c69620c74727573746c696e655f6964000208686f73745f6c696206616d6d"
"5f6964000008686f73745f6c696208636865636b5f6964000008686f73745f6c69620d63726564656e7469616c5f69"
"64000208686f73745f6c69620b64656c65676174655f6964000008686f73745f6c6962126465706f7369745f707265"
"617574685f6964000008686f73745f6c6962066469645f6964000108686f73745f6c696209657363726f775f696400"
"0008686f73745f6c69620f6d70745f69737375616e63655f6964000008686f73745f6c69620a6d70746f6b656e5f69"
"64000008686f73745f6c69620c6e66745f6f666665725f6964000008686f73745f6c6962086f666665725f69640000"
"08686f73745f6c69620a7061796368616e5f6964000208686f73745f6c6962167065726d697373696f6e65645f646f"
"6d61696e5f6964000008686f73745f6c69620a7369676e6572735f6964000108686f73745f6c6962097469636b6574"
"5f6964000008686f73745f6c6962087661756c745f6964000003070603030307080905030100110619037f01418080"
"c0000b7f0041c28ac0000b7f0041d08ac0000b073504066d656d6f727902000d657363726f775f66696e697368001b"
"0a5f5f646174615f656e6403010b5f5f686561705f6261736503020ae8370614002000200120022003418280204282"
"8020101d0b140020002001200220034181802042818020101d0bd10302017f017e230041a0016b2204240002402001"
"2d0000410146044041d780c000411620012802042201ac10001a200041013a0000200020013602040c010b20044118"
"6a200141196a290000370300200441106a200141116a290000370300200441086a200141096a290000370300200420"
"012900013703002002200320044120410110011a2004412041001002220141004c044041d080c00041072001ac1000"
"1a200041013a0000200020013602040c010b418b80c000410f4285801410001a20014185801420044180016a412010"
"032201412047044041af80c0004115417f20012001417f4e1b2201ac10001a200041013a0000200020013602040c01"
"0b200441c2006a20044182016a2d00003a0000200441f0006a20044197016a2900002205370300200441286a220120"
"04418f016a290000370300200441306a22022005370300200441386a22032004419f016a2d00003a0000200420042f"
"0080013b014020042004290087013703202004200428008301360043200441df006a20032d00003a0000200441d700"
"6a2002290300370000200441cf006a20012903003700002004200429032037004741c480c000410c200441406b4120"
"410110011a20004180023b01000b200441a0016a24000bd32c02097f027e23004180076b2200240041ed80c0004123"
"41014100410010011a02402000027f02404181802020004190016a220741141004220641144604402000410e6a2000"
"4192016a22032d00003a000020002000290097013703e80120002000419c016a22012900003700ed01200020002f00"
"90013b010c200020002903e8013703d806200020002900ed013700dd06200020002800930136000f200041186a2000"
"2900dd06370000200020002903d806370013419081c00041082000410c6a2204411410051a41838020200741141004"
"22064114470d03200041226a20032d00003a000020002000290097013703e801200020012900003700ed0120002000"
"2f0090013b0120200020002903e8013703d806200020002900ed013700dd0620002000280093013600232000412c6a"
"20002900dd06370000200020002903d806370027419881c000410c200041206a411410051a200041a8016a22034200"
"370300200041a0016a2201420037030020004198016a42003703002000420037039001200441142007412010062204"
"4120460d01024020044100480440200020043602380c010b2000417f3602380b41010c020b0c020b200041cd006a20"
"03290300370000200041c5006a20012903003700002000413d6a20004198016a290300370000200020002903900137"
"003541000b3a003420004190016a200041346a41a481c00041071019024020002d0090014101460440200028029401"
"2106419c8ac0004112420510001a0c010b4100210641ab81c000413541014100410010011a200041e6006a41c4003a"
"0000200041e0006a4100360200200041eb006a41003a0000200041d5a6013b01642000420037035820004100360067"
"200041a8016a22044200370300200041a0016a2203420037030020004198016a220142003703002000420037039001"
"02402000410c6a4114200041206a4114200041d8006a411420004190016a4120100722074120470440024020074100"
"480440200020073602700c010b2000417f3602700b410121060c010b20004185016a2004290300370000200041fd00"
"6a2003290300370000200041f5006a2001290300370000200020002903900137006d0b200020063a006c2000419001"
"6a200041ec006a41e081c0004109101a20002d00900141014604402000280294012106419c8ac0004112420510001a"
"0c010b4100210641e981c000413741014100410010011a200041f8016a200041306a2204280100360200200041f001"
"6a200041286a220329010037030020004184026a200041e0006a290300220a3702002000418c026a200041e8006a28"
"02002201360200200020002901203703e8012000200029035822093702fc01200041e8066a22052001360200200041"
"e0066a2207200a370300200020093703d806200041f4066a2003290100370200200041fc066a200428010036020020"
"0020002901203702ec0620004190026a200041d8066a22034128101c20004194016a200041e8016a41d000101c2000"
"410136029001200041f0066a220142003703002005420037030020074200370300200042003703d806024041ae8ac0"
"004114200041bc016a412820034120100822034120470440024020034100480440200020033602ec010c010b200041"
"7f3602ec010b410121060c010b20004181026a2001290300370000200041f9016a2005290300370000200041f1016a"
"2007290300370000200020002903d8063700e9010b200020063a00e801200041bc026a200041e8016a41a082c00041"
"03101920002d00bc02410146044020002802c0022106419c8ac0004112420610001a0c010b4100210641a382c00041"
"3141014100410010011a200041063602d80620004180026a22044200370300200041f8016a22034200370300200041"
"f0016a22014200370300200042003703e80102402000410c6a4114200041d8066a4104200041e8016a412010092207"
"4120470440024020074100480440200020073602c8020c010b2000417f3602c8020b410121060c010b200041dd026a"
"2004290300370000200041d5026a2003290300370000200041cd026a2001290300370000200020002903e8013700c5"
"020b200020063a00c402200041e8016a200041c4026a41d482c0004105101920002d00e801410146044020002802ec"
"012106419c8ac0004112420610001a0c010b41d982c000413341014100410010011a20004180026a42003703002000"
"41f8016a4200370300200041f0016a4200370300200042003703e801024002402000410c6a2201411420014114418c"
"83c0004112200041e8016a4120100a2201412047044041d780c0004116417f20012001417f4e1b2206ac10001a0c01"
"0b200041da066a20002d00ea013a0000200041f0026a200041f7016a290000220a370300200041f8026a200041ff01"
"6a290000220937030020004180036a20004187026a2d000022013a0000200041e7066a200a370000200041ef066a20"
"09370000200041f7066a20013a0000200020002f01e8013b01d806200020002900ef0122093703e802200020002800"
"eb013600db06200020093700df06419e83c000410a200041d8066a22014120410110011a2001412041001002220641"
"004c044041d080c00041072006ac10001a0c010b418b80c000410f4298802010001a200641988020200041e8016a41"
"14100322014114460d0141af80c0004115417f20012001417f4e1b2206ac10001a0b419c8ac0004112420710001a0c"
"010b419a80c000411541014100410010011a41a883c000413841014100410010011a230041206b2208240020084118"
"6a22074200370300200841106a22044200370300200841086a220342003703002008420037030020004184036a2201"
"027f2000410c6a22064114200041206a2202411420084120100b220541204704400240200541004804402001200536"
"02040c010b2001417f3602040b41010c010b20012008290300370001200141196a2007290300370000200141116a20"
"04290300370000200141096a200329030037000041000b3a0000200841206a2400200041e8016a2205200141e083c0"
"004108101920002d00e80145044041e883c000413641014100410010011a230041206b22082400200841186a220742"
"00370300200841106a22044200370300200841086a2203420037030020084200370300200041a8036a2201027f2006"
"41142002411420084120100c22024120470440024020024100480440200120023602040c010b2001417f3602040b41"
"010c010b20012008290300370001200141196a2007290300370000200141116a2004290300370000200141096a2003"
"29030037000041000b3a0000200841206a240020052001419e84c000410e101920002d00e801410146044020002802"
"ec012106419c8ac0004112420910001a0c020b41ac84c000413c41014100410010011a230041206b22022400200241"
"186a22074200370300200241106a22044200370300200241086a2203420037030020024200370300200041cc036a22"
"01027f2000410c6a411420024120100d22054120470440024020054100480440200120053602040c010b2001417f36"
"02040b41010c010b20012002290300370001200141196a2007290300370000200141116a2004290300370000200141"
"096a200329030037000041000b3a0000200241206a2400200041e8016a200141e884c0004103101920002d00e80141"
"0146044020002802ec012106419c8ac0004112420a10001a0c020b41eb84c000413141014100410010011a23004130"
"6b220224002002410b36020c200241286a22074200370300200241206a22044200370300200241186a220342003703"
"0020024200370310200041f0036a2201027f2000410c6a41142002410c6a4104200241106a4120100e220541204704"
"40024020054100480440200120053602040c010b2001417f3602040b41010c010b2001200229031037000120014119"
"6a2007290300370000200141116a2004290300370000200141096a200329030037000041000b3a0000200241306a24"
"00200041e8016a2001419c85c0004106101920002d00e801410146044020002802ec012106419c8ac0004112420b10"
"001a0c020b41a285c000413441014100410010011a230041306b220224002002410c36020c200241286a2207420037"
"0300200241206a22044200370300200241186a220342003703002002420037031020004194046a2201027f2000410c"
"6a41142002410c6a4104200241106a4120100f22054120470440024020054100480440200120053602040c010b2001"
"417f3602040b41010c010b20012002290310370001200141196a2007290300370000200141116a2004290300370000"
"200141096a200329030037000041000b3a0000200241306a2400200041fc016a2000411c6a280100360200200041f4"
"016a200041146a2901003702002000200029010c3702ec01200041808080e0003602e801200041d8066a2103230041"
"406a22042400024020012d0000410146044041d780c000411620012802042201ac10001a200341013a000020032001"
"3602040c010b200441206a200141196a290000370300200441186a200141116a290000370300200441106a20014109"
"6a2900003703002004200129000137030841d685c000410b200441086a22014120410110011a024002402001412041"
"001002220141004c044041d080c00041072001ac10001a0c010b418b80c000410f4284802010001a20014184802020"
"04412c6a4114100322014114460d0141af80c0004115417f20012001417f4e1b2201ac10001a0b200341013a000020"
"0320013602040c010b419a80c000411541014100410010011a20034180023b01000b200441406b240020002d00d806"
"410146044020002802dc062106419c8ac0004112420c10001a0c020b41e185c000413941014100410010011a230041"
"206b22022400200241186a22074200370300200241106a22044200370300200241086a220342003703002002420037"
"0300200041b8046a2201027f200041e8016a4118200041206a41142002412010102205412047044002402005410048"
"0440200120053602040c010b2001417f3602040b41010c010b20012002290300370001200141196a20072903003700"
"00200141116a2004290300370000200141096a200329030037000041000b3a0000200241206a2400200041d8066a20"
"01419a86c0004107101920002d00d806410146044020002802dc062106419c8ac0004112420d10001a0c020b41a186"
"c000413541014100410010011a230041306b220224002002410636020c200241286a22074200370300200241206a22"
"044200370300200241186a2203420037030020024200370310200041dc046a2201027f200041206a41142002410c6a"
"4104200241106a4120101122054120470440024020054100480440200120053602040c010b2001417f3602040b4101"
"0c010b20012002290310370001200141196a2007290300370000200141116a2004290300370000200141096a200329"
"030037000041000b3a0000200241306a2400200041d8066a200141d686c000410c101820002d00d806410146044020"
"002802dc062106419c8ac0004112420d10001a0c020b41e286c000413a41014100410010011a230041306b22022400"
"2002410d36020c200241286a22074200370300200241206a22044200370300200241186a2203420037030020024200"
"37031020004180056a2201027f2000410c6a41142002410c6a4104200241106a412010122205412047044002402005"
"4100480440200120053602040c010b2001417f3602040b41010c010b20012002290310370001200141196a20072903"
"00370000200141116a2004290300370000200141096a200329030037000041000b3a0000200241306a2400200041d8"
"066a2001419c87c0004105101920002d00d806410146044020002802dc062106419c8ac0004112420d10001a0c020b"
"41a187c000413341014100410010011a230041306b220224002002410e36020c200241286a22074200370300200241"
"206a22044200370300200241186a2203420037030020024200370310200041a4056a2201027f2000410c6a41142000"
"41206a41142002410c6a4104200241106a4120101322054120470440024020054100480440200120053602040c010b"
"2001417f3602040b41010c010b20012002290310370001200141196a2007290300370000200141116a200429030037"
"0000200141096a200329030037000041000b3a0000200241306a2400200041d8066a200141d487c000410a10192000"
"2d00d806410146044020002802dc062106419c8ac0004112420e10001a0c020b41de87c00041384101410041001001"
"1a230041306b220224002002410f36020c200241286a22074200370300200241206a22044200370300200241186a22"
"03420037030020024200370310200041c8056a2201027f2000410c6a41142002410c6a4104200241106a4120101422"
"054120470440024020054100480440200120053602040c010b2001417f3602040b41010c010b200120022903103700"
"01200141196a2007290300370000200141116a2004290300370000200141096a200329030037000041000b3a000020"
"0241306a2400200041d8066a2001419688c0004112101820002d00d806410146044020002802dc062106419c8ac000"
"4112420f10001a0c020b41a888c00041c00041014100410010011a230041206b22022400200241186a220742003703"
"00200241106a22044200370300200241086a2203420037030020024200370300200041ec056a2201027f2000410c6a"
"411420024120101522054120470440024020054100480440200120053602040c010b2001417f3602040b41010c010b"
"20012002290300370001200141196a2007290300370000200141116a2004290300370000200141096a200329030037"
"000041000b3a0000200241206a2400200041d8066a200141e888c000410a101a20002d00d806410146044020002802"
"dc062106419c8ac0004112421010001a0c020b41f288c000413841014100410010011a230041306b22022400200241"
"1236020c200241286a22074200370300200241206a22044200370300200241186a2203420037030020024200370310"
"20004190066a2201027f2000410c6a41142002410c6a4104200241106a412010162205412047044002402005410048"
"0440200120053602040c010b2001417f3602040b41010c010b20012002290310370001200141196a20072903003700"
"00200141116a2004290300370000200141096a200329030037000041000b3a0000200241306a2400200041d8066a20"
"0141aa89c0004106101920002d00d806410146044020002802dc062106419c8ac0004112421210001a0c020b410121"
"0641b089c000413441014100410010011a230041306b220224002002411336020c200241286a220742003703002002"
"41206a22044200370300200241186a2203420037030020024200370310200041b4066a2201027f2000410c6a411420"
"02410c6a4104200241106a4120101722054120470440024020054100480440200120053602040c010b2001417f3602"
"040b41010c010b20012002290310370001200141196a2007290300370000200141116a200429030037000020014109"
"6a200329030037000041000b3a0000200241306a2400200041d8066a200141e489c0004105101920002d00d8064101"
"46044020002802dc062106419c8ac0004112421310001a0c020b41e989c000413341014100410010011a0c010b2000"
"2802ec012106419c8ac0004112420810001a0b20004180076a240020060f0b418080c000410b417f20062006417f4e"
"1bac1000000bfd0401067f200241104f0440024020002000410020006b41037122056a22044f0d0020012103200504"
"40200521060340200020032d00003a0000200341016a2103200041016a2100200641016b22060d000b0b200541016b"
"4107490d000340200020032d00003a0000200041016a200341016a2d00003a0000200041026a200341026a2d00003a"
"0000200041036a200341036a2d00003a0000200041046a200341046a2d00003a0000200041056a200341056a2d0000"
"3a0000200041066a200341066a2d00003a0000200041076a200341076a2d00003a0000200341086a2103200041086a"
"22002004470d000b0b2004200220056b2207417c7122086a21000240200120056a2206410371450440200020044d0d"
"0120062101034020042001280200360200200141046a2101200441046a22042000490d000b0c010b200020044d0d00"
"2006410374220541187121032006417c71220241046a2101410020056b411871210520022802002102034020042002"
"2003762001280200220220057472360200200141046a2101200441046a22042000490d000b0b200741037121022006"
"20086a21010b02402000200020026a22064f0d002002410771220304400340200020012d00003a0000200141016a21"
"01200041016a2100200341016b22030d000b0b200241016b4107490d000340200020012d00003a0000200041016a20"
"0141016a2d00003a0000200041026a200141026a2d00003a0000200041036a200141036a2d00003a0000200041046a"
"200141046a2d00003a0000200041056a200141056a2d00003a0000200041066a200141066a2d00003a000020004107"
"6a200141076a2d00003a0000200141086a2101200041086a22002006470d000b0b0b940201017f230041406a220624"
"00024020012d0000410146044041d780c000411620012802042201ac10001a200041013a0000200020013602040c01"
"0b200641206a200141196a290000370300200641186a200141116a290000370300200641106a200141096a29000037"
"03002006200129000137030820022003200641086a22014120410110011a024002402001412041001002220141004c"
"044041d080c00041072001ac10001a0c010b418b80c000410f200510001a200120042006412c6a4114100322014114"
"460d0141af80c0004115417f20012001417f4e1b2201ac10001a0b200041013a0000200020013602040c010b419a80"
"c000411541014100410010011a20004180023b01000b200641406b24000b0bb80a0100418080c0000bae0a6572726f"
"725f636f64653d47657474696e67206669656c643a204669656c6420646174613a207265747269657665644572726f"
"722067657474696e67206669656c643a204669656c6420646174613a204572726f723a204572726f72206765747469"
"6e67206b65796c65743a202424242424205354415254494e47205741534d20455845435554494f4e20242424242441"
"63636f756e743a44657374696e6174696f6e3a4163636f756e744163636f756e74206f626a65637420657869737473"
"2c2070726f63656564696e67207769746820657363726f772066696e6973682e54727573746c696e6554727573746c"
"696e65206f626a656374206578697374732c2070726f63656564696e67207769746820657363726f772066696e6973"
"682e414d4d414d4d206f626a656374206578697374732c2070726f63656564696e67207769746820657363726f7720"
"66696e6973682e436865636b436865636b206f626a656374206578697374732c2070726f63656564696e6720776974"
"6820657363726f772066696e6973682e7465726d73616e64636f6e646974696f6e7343726564656e7469616c437265"
"64656e7469616c206f626a656374206578697374732c2070726f63656564696e67207769746820657363726f772066"
"696e6973682e44656c656761746544656c6567617465206f626a656374206578697374732c2070726f63656564696e"
"67207769746820657363726f772066696e6973682e4465706f736974507265617574684465706f7369745072656175"
"7468206f626a656374206578697374732c2070726f63656564696e67207769746820657363726f772066696e697368"
"2e444944444944206f626a656374206578697374732c2070726f63656564696e67207769746820657363726f772066"
"696e6973682e457363726f77457363726f77206f626a656374206578697374732c2070726f63656564696e67207769"
"746820657363726f772066696e6973682e4d505449737375616e63654d505449737375616e6365206f626a65637420"
"6578697374732c2070726f63656564696e67207769746820657363726f772066696e6973682e4d50546f6b656e4d50"
"546f6b656e206f626a656374206578697374732c2070726f63656564696e67207769746820657363726f772066696e"
"6973682e4e46546f6b656e4f666665724e46546f6b656e4f66666572206f626a656374206578697374732c2070726f"
"63656564696e67207769746820657363726f772066696e6973682e4f666665724f66666572206f626a656374206578"
"697374732c2070726f63656564696e67207769746820657363726f772066696e6973682e5061794368616e6e656c50"
"61794368616e6e656c206f626a656374206578697374732c2070726f63656564696e67207769746820657363726f77"
"2066696e6973682e5065726d697373696f6e6564446f6d61696e5065726d697373696f6e6564446f6d61696e206f62"
"6a656374206578697374732c2070726f63656564696e67207769746820657363726f772066696e6973682e5369676e"
"65724c6973745369676e65724c697374206f626a656374206578697374732c2070726f63656564696e672077697468"
"20657363726f772066696e6973682e5469636b65745469636b6574206f626a656374206578697374732c2070726f63"
"656564696e67207769746820657363726f772066696e6973682e5661756c745661756c74206f626a65637420657869"
"7374732c2070726f63656564696e67207769746820657363726f772066696e6973682e43757272656e742073657120"
"76616c75653a004d0970726f64756365727302086c616e6775616765010452757374000c70726f6365737365642d62"
"79010572757374631d312e38372e30202831373036376539616320323032352d30352d303929002c0f746172676574"
"5f6665617475726573022b0f6d757461626c652d676c6f62616c732b087369676e2d657874";
extern std::string const kCodecovTestsWasmHex =
"0061736d01000000015c0c60067f7f7f7f7f7f017f60027f7f017f60047f7f7f7f017f60037f7f7f017f60077f7f7f"
"7f7f7f7f017f60087f7f7f7f7f7f7f7f017f60057f7f7f7f7f017f60017f017f60057f7f7f7f7f0060047f7f7f7f00"
"60017f006000017f02d8093608686f73745f6c6962057472616365000808686f73745f6c69620a6c6467725f696e64"
"6578000108686f73745f6c696210706172656e745f6c6467725f74696d65000108686f73745f6c696210706172656e"
"745f6c6467725f68617368000108686f73745f6c696208626173655f666565000108686f73745f6c696211616d656e"
"646d656e745f656e61626c6564000108686f73745f6c69620874785f6669656c64000308686f73745f6c69620e6163"
"636f756e74726f6f745f6964000208686f73745f6c69620863616368655f6c65000308686f73745f6c69620d686f6d"
"655f6c655f6669656c64000308686f73745f6c6962086c655f6669656c64000208686f73745f6c69620874785f696e"
"6e6572000208686f73745f6c69620d686f6d655f6c655f696e6e6572000208686f73745f6c6962086c655f696e6e65"
"72000608686f73745f6c69620a74785f6172725f6c656e000708686f73745f6c69620f686f6d655f6c655f6172725f"
"6c656e000708686f73745f6c69620a6c655f6172725f6c656e000108686f73745f6c69621074785f696e6e65725f61"
"72725f6c656e000108686f73745f6c696215686f6d655f6c655f696e6e65725f6172725f6c656e000108686f73745f"
"6c6962106c655f696e6e65725f6172725f6c656e000308686f73745f6c6962087365745f64617461000108686f7374"
"5f6c69620b7368613531325f68616c66000208686f73745f6c696209636865636b5f736967000008686f73745f6c69"
"62076e66745f757269000008686f73745f6c69620a6e66745f697373756572000208686f73745f6c6962096e66745f"
"7461786f6e000208686f73745f6c6962096e66745f666c616773000108686f73745f6c69620c6e66745f786665725f"
"666565000108686f73745f6c69620a6e66745f73657269616c000208686f73745f6c696208636865636b5f69640000"
"08686f73745f6c69620f666c6f61745f66726f6d5f75696e74000608686f73745f6c69620c74727573746c696e655f"
"6964000508686f73745f6c696206616d6d5f6964000008686f73745f6c69620d63726564656e7469616c5f69640005"
"08686f73745f6c69620a6d70746f6b656e5f6964000008686f73745f6c696209666c6f61745f636d70000208686f73"
"745f6c696209666c6f61745f616464000408686f73745f6c696209666c6f61745f737562000408686f73745f6c6962"
"0a666c6f61745f6d756c74000408686f73745f6c696209666c6f61745f646976000408686f73745f6c696209666c6f"
"61745f706f77000008686f73745f6c696209657363726f775f6964000008686f73745f6c69620f6d70745f69737375"
"616e63655f6964000008686f73745f6c69620c6e66745f6f666665725f6964000008686f73745f6c6962086f666665"
"725f6964000008686f73745f6c6962096f7261636c655f6964000008686f73745f6c69620a7061796368616e5f6964"
"000508686f73745f6c6962167065726d697373696f6e65645f646f6d61696e5f6964000008686f73745f6c69620974"
"69636b65745f6964000008686f73745f6c6962087661756c745f6964000008686f73745f6c69620b64656c65676174"
"655f6964000008686f73745f6c6962126465706f7369745f707265617574685f6964000008686f73745f6c69620664"
"69645f6964000208686f73745f6c69620a7369676e6572735f69640002030403090a0b05030100110619037f014180"
"80c0000b7f0041c698c0000b7f0041d098c0000b073504066d656d6f727902000d657363726f775f66696e69736800"
"380a5f5f646174615f656e6403010b5f5f686561705f6261736503020aa22e037201017f230041106b220424000240"
"02402000200147044020022003410741014100100020004100480d0120042000ad3703080c020b20042000ac370308"
"200220034101200441086a41081000200441106a24000f0b20042000ac3703080b418080c000410b4101200441086a"
"41081000000b2801017f230041106b2201240020012000ac37030841aa91c000410b4101200141086a41081000000b"
"832d02087f017e230041a0026b2200240041b591c0004123410741014100100020004100360260200041e0006a2201"
"41041001410441888ec000410a103620004100360260200141041002410441a683c000411010362000420037037820"
"0042003703702000420037036820004200370360200141201003412041a185c0004110103620004100360260200141"
"041004410441d888c000410810362000428182848890a0c080013703202000428182848890a0c08001370318200042"
"8182848890a0c080013703102000428182848890a0c0800137030841d891c000410e1005410141e691c00041111036"
"200041086a41201005410141e691c00041111036200041003602702000420037036820004200370360024002404181"
"802020014114100622014100480d00200141144b0440417321010c010b20014114460d0141808080807821010b2001"
"1037000b2000200029006c3700fd01200020002900673703f801200020002d00623a002e200020002f01603b012c20"
"00200028006336002f200020002903f801370033200020002900fd0137003820004200370378200042003703702000"
"42003703682000420037036002402000412c6a4114200041e0006a4120100722014120470440200141004e0d012001"
"1037000b200020002d00623a0042200020002f01603b01402000200029006f22083703800220002000280063360043"
"200020002900673700472000200837004f20002000290077370057200020002d007f3a005f200041406b4120410010"
"08410141f791c0004108103620004100360270200042003703682000420037036041818020200041e0006a22024114"
"1009411441a68fc000410d103620004100360270200042003703682000420037036041014181802020024114100a41"
"1441bf8cc0004108103602404100200041e4006a22046b410371220320046a220120044d0d00200304402003210503"
"40200441003a0000200441016a2104200541016b22050d000b0b200341016b4107490d000340200441003a00002004"
"41076a41003a0000200441066a41003a0000200441056a41003a0000200441046a41003a0000200441036a41003a00"
"00200441026a41003a0000200441016a41003a0000200441086a22042001470d000b0b2001413c20036b2203417c71"
"6a220420014b0440034020014100360200200141046a22012004490d000b0b024020042003410371220320046a2205"
"4f0d002003220104400340200441003a0000200441016a2104200141016b22010d000b0b200341016b4107490d0003"
"40200441003a0000200441076a41003a0000200441066a41003a0000200441056a41003a0000200441046a41003a00"
"00200441036a41003a0000200441026a41003a0000200441016a41003a0000200441086a22042005470d000b0b2000"
"41043602a00120004181802036026020004100360288022000420037038002200042003703f80120024104200041f8"
"016a22014114100b4114418388c0004108103620004100360288022000420037038002200042003703f80120022000"
"2802a00120014114100c411441e085c000410d103620004100360288022000420037038002200042003703f8014101"
"200220002802a00120014114100d4114418b86c000410810364189803c100e412041ff91c000410a10364189803c10"
"0f4120418992c000410f103641014189803c10104120419892c000410a1036200220002802a0011011412041a292c0"
"0041101036200220002802a0011012412041b292c000411510364101200220002802a0011013412041c792c0004110"
"10362000412c6a220341141014411441d792c000410810362000420037039002200042003703880220004200370380"
"02200042003703f801200220002802a001200141201015412041db82c000410b103641df92c000410c41eb92c00041"
"0b41f692c000410e10164101418493c00041091036200020002903203703c001200020002903183703b80120002000"
"2903103703b001200020002903083703a801200041003b0188022000420037038002200042003703f8012003411420"
"0041a8016a220541202001411210174112418c84c00041071036200041003602880220004200370380022000420037"
"03f801200541202001411410184114419386c000410a1036200041003602f80120054120200141041019410441c48a"
"c0004109103620054120101a4108418d93c0004109103620054120101b410a419693c000410c1036200041003602f8"
"012005412020014104101c410441818fc000410a103641a293c000410d410420034114100041a293c000410d410541"
"af93c0004108100041a293c000410d410541b793c00041081000417f41041003417141bf93c0004118103620004100"
"3602f8012001417f1003417141e686c00041181036200041003a00fa01200041003b01f801200141031003417d41f8"
"80c000411e1036200041003602f8012001418094ebdc0310034173419790c000411d10364102100e416f41d793c000"
"41191036417f20002802a0011011417141f093c000411810362002417f10114171418894c000411810362002418108"
"1011417441a094c00041191036200041e094ebdc036a220420002802a0011011417341b994c0004118103620004200"
"3703900220004200370388022000420037038002200042003703f801200341142004410820014120101d417341e283"
"c00041141036200042003703900220004200370388022000420037038002200042003703f801200341142003411420"
"014120101d417141eb84c00041161036200042003703900220004200370388022000420037038002200042003703f8"
"0120044108200141204100101e417341ea8ec000411710362000420037039002200042003703880220004200370380"
"02200042003703f801200220002802a001200141204100101e4171418185c00041201036200420002802a001410110"
"08417341d194c00041101036200220002802a00141011008417141e194c00041121036200042003703900220004200"
"370388022000420037038002200042003703f801200420002802a0012001412010074173418b8bc000411610362000"
"42003703900220004200370388022000420037038002200042003703f801200220002802a001200141201007417141"
"b683c00041181036200042003703900220004200370388022000420037038002200042003703f80120034114200341"
"14200420002802a00120014120101f417341c78cc000411d1036200042003703900220004200370388022000420037"
"038002200042003703f8012003411420034114200220002802a00120014120101f417141e489c000411f1036200042"
"003703900220004200370388022000420037038002200042003703f80141b298c0004114200420002802a001200141"
"201020417341a688c00041151036200042003703900220004200370388022000420037038002200042003703f80141"
"b298c0004114200220002802a001200141201020417141c787c000411b103620004200370390022000420037038802"
"2000420037038002200042003703f80141b298c000411441f394c0004114200141201020417141bb85c00041251036"
"200042003703900220004200370388022000420037038002200042003703f801418795c000412841b298c000411420"
"0141201020417141c98bc000412110362000200028013c3602dc01200020002901343702d4012000200029012c3702"
"cc01200041808080083602c801200041003b01f801200041c8016a2207411841b298c0004114200141021020417141"
"b185c000410a10362000422a3703e001200420002802a0014101200041e0016a41081000200041003b01f801410220"
"0141021006416f41af80c00041171036200041003b01f8014102200141021009416f419787c000411c103620004100"
"3b01f8014101410220014102100a416f41e682c000411710364102100e416f41d793c000411910364102100f416f41"
"af95c000411e1036410141021010416f41cd95c0004119103641d891c0004181081005417441e695c000411f103641"
"d891c00041c10010054174418596c000411a1036200041003b01f801200241810820014102100b417441f683c00041"
"161036200041003b01f801200241810820014102100c4174418b88c000411b1036200041003b01f801410120024181"
"0820014102100d4174419384c00041161036200241810810114174419f96c000411e103620024181081012417441bd"
"96c00041231036410120024181081013417441e096c000411e103620024181081014417441fe96c0004116103641a2"
"93c00041810841eb92c000410b41f692c000410e10164174418493c0004109103641a293c000410d41eb92c0004181"
"0841f692c000410e10164174418493c0004109103641a293c000410d41eb92c000410b41f692c00041810810164174"
"418493c00041091036200041003b01f8012002418108200141021015417441fe86c00041191036200041003b01f801"
"41b298c00041810841b298c0004114200141021020417441b58bc00041141036200041003b01f80120034114200341"
"142002418108200141021021417441c082c000411b1036200041003b01f80120074181082003411420014102102241"
"7441da80c000411e103641a293c000410d4107200420002802a0011000200042d487b6f4c7d4b1c0003700ec0141a2"
"93c000410d4103200041ec95ebdc036a22054108100041a293c000410d4105200420002802a0011000200541082000"
"41ec016a2204410810234173419497c0004114103620044108200541081023417341a897c00041141036200041003b"
"01f80120054108200441082001410241001024417341ac82c00041141036200041003b01f801200441082005410820"
"01410241001024417341ce83c00041141036200041003b01f80120054108200441082001410241001025417341a18b"
"c00041141036200041003b01f80120044108200541082001410241001025417341c680c00041141036200041003b01"
"f80120054108200441082001410241001026417341cd8ac00041151036200041003b01f80120044108200541082001"
"410241001026417341c781c00041151036200041003b01f80120054108200441082001410241001027417341b387c0"
"0041141036200041003b01f801200441082005410820014102410010274173419d86c00041141036200041003b01f8"
"0120054108410320014102410010284173419681c00041131036200042003703900220004200370388022000420037"
"038002200042003703f8012003411420034114200141201029417141ea8bc000411b10362000420037039002200042"
"00370388022000420037038002200042003703f801200341142003411420014120102a417141e287c0004121103620"
"0042003703900220004200370388022000420037038002200042003703f801200341142003411420014120102b4171"
"41a981c000411e1036200042003703900220004200370388022000420037038002200042003703f801200341142003"
"411420014120102c417141b48ec000411a103620004200370390022000420037038802200042003703800220004200"
"3703f801200341142003411420014120102d4171418b8fc000411b1036200042003703900220004200370388022000"
"420037038002200042003703f80120034114200341142003411420014120102e417141ce8ec000411c103620004200"
"3703900220004200370388022000420037038002200042003703f801200341142003411420014120102f417141e08d"
"c00041281036200042003703900220004200370388022000420037038002200042003703f801200341142003411420"
"0141201030417141b186c000411b1036200042003703900220004200370388022000420037038002200042003703f8"
"012003411420034114200141201031417141b490c000411a1036200220002802a00141001008417141bc97c000411b"
"1036200041003b01f80120034114200220002802a001200141021017417141cc86c000411a1036200041003b01f801"
"200220002802a001200141021018417141858cc000411d1036200041003b01f801200220002802a001200141021019"
"417141ce90c000411c1036200220002802a001101a417141d797c000411c1036200220002802a001101b417141f397"
"c000411f1036200041003602f801200220002802a00120014104101c417141a28cc000411d1036200041003b01f801"
"200220002802a0012001410210074171418b80c00041241036200041808080083602f401200041003b01f801200220"
"002802a001200041f4016a2205410420014102101d4171419f8dc000411e1036200041003b01f801200220002802a0"
"0122062003411420022006200141021021417141d28fc00041241036200041003b01f80120034114200220002802a0"
"01220620022006200141021021417141dc81c00041241036200041003b01f801200220002802a00120034114200141"
"021032417141838ac00041221036200041003b01f80120034114200220002802a001200141021032417141a984c000"
"41221036200041003b01f801200220002802a00120034114200141021033417141fd82c00041291036200041003b01"
"f80120034114200220002802a001200141021033417141e28ac00041291036200041003b01f801200220002802a001"
"2001410210344171418089c000411c1036200041003b01f801200220002802a00120054104200141021029417141b3"
"8fc000411f1036200041003b01f801200220002802a0012003411441f394c000411420014102101f417141c189c000"
"41231036200041003b01f80120034114200220002802a00141f394c000411420014102101f417141bd8dc000412310"
"36200041003b01f801200220002802a0012005410420014102102a4171419c89c00041251036200041003b01f80120"
"074118200220002802a001200141021022417141cb84c00041201036200041003b01f801200220002802a001200541"
"0420014102102b417141928ec00041221036200041003b01f801200220002802a0012005410420014102102c417141"
"ed85c000411e1036200041003b01f801200220002802a0012005410420014102102d417141a58ac000411f10362000"
"41003b01f801200220002802a001200341142005410420014102102e417141f68fc00041211036200041003b01f801"
"20034114200220002802a0012005410420014102102e417141ea90c00041211036200041003b01f801200220002802"
"a0012005410420014102102f4171418082c000412c1036200041003b01f801200220002802a0012001410210354171"
"41e088c00041201036200041003b01f801200220002802a001200541042001410210304171418b91c000411f103620"
"0041003b01f801200220002802a00120054104200141021031417141818dc000411e1036200041003b01f801200220"
"002802a001419298c0004120200141021017417141e48cc000411d103641a293c000410d4104200220002802a00110"
"0041a2a7abdd03410d4107419298c0004120100041a2a7abdd03410d410320044108100041a2a7abdd03410d410420"
"034114100041a2a7abdd03410d410541b793c00041081000200220002802a001410720024181081000200042013703"
"f8012002418108410120014108100041a293c000418108410320044108100041a293c0004181084104200341141000"
"41a293c000418108410541b793c0004108100041a293c000410d4105200220002802a0011000200041003b019e0220"
"0220002802a001200341142000419e026a41021022417141bb88c000411d103641a293c000410d41e3002002200028"
"02a0011000410141004104200341141000200041a0026a240041010f0b000b0b9d180200418080c0000b8715544553"
"54204641494c45446163636f756e74726f6f745f69645f77726f6e675f73697a655f6163636f756e745f696474785f"
"6669656c645f696e76616c69645f736669656c64666c6f61745f7375625f6f6f625f736c696365326d70746f6b656e"
"5f69645f746f6f5f6269675f736c6963655f6d70746964706172656e745f6c6467725f686173685f6275665f746f6f"
"5f736d616c6c666c6f61745f706f775f6f6f625f736c6963656e66745f6f666665725f69645f77726f6e675f73697a"
"655f75696e743332666c6f61745f6d756c745f6f6f625f736c6963653263726564656e7469616c5f69645f77726f6e"
"675f73697a655f6163636f756e745f6964327065726d697373696f6e65645f646f6d61696e5f69645f77726f6e675f"
"73697a655f6163636f756e745f6964666c6f61745f6164645f6f6f625f736c6963653163726564656e7469616c5f69"
"645f746f6f5f6269675f736c6963657368613531325f68616c666c655f6669656c645f696e76616c69645f73666965"
"6c646465706f7369745f707265617574685f69645f77726f6e675f73697a655f6163636f756e745f69643170617265"
"6e745f6c6467725f74696d656163636f756e74726f6f745f69645f77726f6e675f6c656e666c6f61745f6164645f6f"
"6f625f736c69636532636865636b5f69645f6f6f625f6c656e5f75333274785f696e6e65725f746f6f5f6269675f73"
"6c6963656e66745f7572696c655f696e6e65725f746f6f5f6269675f736c69636564656c65676174655f69645f7772"
"6f6e675f73697a655f6163636f756e745f6964326d70746f6b656e5f69645f77726f6e675f73697a655f6163636f75"
"6e745f6964636865636b5f69645f77726f6e675f6c656e5f753332666c6f61745f66726f6d5f75696e745f77726f6e"
"675f6c656e5f75696e743634706172656e745f6c6467725f68617368616d6d5f69645f6d7074616d6d5f69645f6c65"
"6e5f77726f6e675f6e6f6e5f7872705f63757272656e63795f6c656e686f6d655f6c655f696e6e65726f666665725f"
"69645f77726f6e675f73697a655f6163636f756e745f69646c655f696e6e65726e66745f697373756572666c6f6174"
"5f6469765f6f6f625f736c696365327469636b65745f69645f77726f6e675f73697a655f75696e7433326e66745f75"
"72695f77726f6e675f73697a655f75696e74323536706172656e745f6c6467725f686173685f6e65675f6c656e7368"
"613531325f68616c665f746f6f5f6269675f736c696365686f6d655f6c655f6669656c645f696e76616c69645f7366"
"69656c64666c6f61745f6469765f6f6f625f736c69636531616d6d5f69645f6c656e5f77726f6e675f6c656e5f6173"
"736574326d70745f69737375616e63655f69645f77726f6e675f73697a655f75696e74333274785f696e6e6572686f"
"6d655f6c655f696e6e65725f746f6f5f6269675f736c696365616d6d5f69645f6c656e5f6f6f625f6173736574326d"
"70746f6b656e5f69645f6d707469645f77726f6e675f6c656e677468626173655f6665657369676e6572735f69645f"
"77726f6e675f73697a655f6163636f756e745f69646469645f69645f77726f6e675f73697a655f6163636f756e745f"
"69646d70745f69737375616e63655f69645f77726f6e675f73697a655f6163636f756e745f696474727573746c696e"
"655f69645f77726f6e675f73697a655f6163636f756e745f69643174727573746c696e655f69645f77726f6e675f6c"
"656e5f63757272656e637964656c65676174655f69645f77726f6e675f73697a655f6163636f756e745f6964316f72"
"61636c655f69645f77726f6e675f73697a655f6163636f756e745f69646e66745f7461786f6e666c6f61745f6d756c"
"745f6f6f625f736c696365316465706f7369745f707265617574685f69645f77726f6e675f73697a655f6163636f75"
"6e745f6964326163636f756e74726f6f745f69645f6c656e5f6f6f62666c6f61745f7375625f6f6f625f736c696365"
"31616d6d5f69645f746f6f5f6269675f736c696365616d6d5f69645f6c656e5f77726f6e675f7872705f6375727265"
"6e63795f6c656e657363726f775f69645f77726f6e675f73697a655f75696e7433326e66745f6973737565725f7772"
"6f6e675f73697a655f75696e743235366e66745f73657269616c5f77726f6e675f73697a655f75696e743235366c65"
"5f6669656c6474727573746c696e655f69645f6c656e5f6f6f625f63757272656e63796e66745f7572695f77726f6e"
"675f73697a655f6163636f756e745f69647661756c745f69645f77726f6e675f73697a655f6163636f756e745f6964"
"636865636b5f69645f77726f6e675f73697a655f6163636f756e745f696474727573746c696e655f69645f77726f6e"
"675f73697a655f6163636f756e745f6964327065726d697373696f6e65645f646f6d61696e5f69645f77726f6e675f"
"73697a655f75696e7433326c6467725f696e6465786e66745f6f666665725f69645f77726f6e675f73697a655f6163"
"636f756e745f69646f666665725f69645f77726f6e675f73697a655f75696e7433327061796368616e5f69645f7772"
"6f6e675f73697a655f75696e743332666c6f61745f66726f6d5f75696e745f6c656e5f6f6f626e66745f7365726961"
"6c6f7261636c655f69645f77726f6e675f73697a655f75696e743332686f6d655f6c655f6669656c64657363726f77"
"5f69645f77726f6e675f73697a655f6163636f756e745f696463726564656e7469616c5f69645f77726f6e675f7369"
"7a655f6163636f756e745f6964317061796368616e5f69645f77726f6e675f73697a655f6163636f756e745f696431"
"706172656e745f6c6467725f686173685f6c656e5f746f6f5f6c6f6e677661756c745f69645f77726f6e675f73697a"
"655f75696e7433326e66745f7461786f6e5f77726f6e675f73697a655f75696e743235367061796368616e5f69645f"
"77726f6e675f73697a655f6163636f756e745f6964327469636b65745f69645f77726f6e675f73697a655f6163636f"
"756e745f69646572726f725f636f64653d2424242424205354415254494e47205741534d20455845435554494f4e20"
"2424242424746573745f616d656e646d656e74616d656e646d656e745f656e61626c656463616368655f6c6574785f"
"6172725f6c656e686f6d655f6c655f6172725f6c656e6c655f6172725f6c656e74785f696e6e65725f6172725f6c65"
"6e686f6d655f6c655f696e6e65725f6172725f6c656e6c655f696e6e65725f6172725f6c656e7365745f6461746174"
"657374206d65737361676574657374207075626b657974657374207369676e6174757265636865636b5f7369676e66"
"745f666c6167736e66745f786665725f66656574657374696e67207472616365400000000000005f40000000000000"
"00706172656e745f6c6467725f686173685f6e65675f70747274785f6172725f6c656e5f696e76616c69645f736669"
"656c6474785f696e6e65725f6172725f6c656e5f6e65675f70747274785f696e6e65725f6172725f6c656e5f6e6567"
"5f6c656e74785f696e6e65725f6172725f6c656e5f746f6f5f6c6f6e6774785f696e6e65725f6172725f6c656e5f70"
"74725f6f6f6263616368655f6c655f7074725f6f6f6263616368655f6c655f77726f6e675f6c656e55534430303030"
"303030303030303030303030300041af95c0000b8303686f6d655f6c655f6172725f6c656e5f696e76616c69645f73"
"6669656c646c655f6172725f6c656e5f696e76616c69645f736669656c64616d656e646d656e745f656e61626c6564"
"5f746f6f5f6269675f736c696365616d656e646d656e745f656e61626c65645f746f6f5f6c6f6e6774785f696e6e65"
"725f6172725f6c656e5f746f6f5f6269675f736c696365686f6d655f6c655f696e6e65725f6172725f6c656e5f746f"
"6f5f6269675f736c6963656c655f696e6e65725f6172725f6c656e5f746f6f5f6269675f736c6963657365745f6461"
"74615f746f6f5f6269675f736c696365666c6f61745f636d705f6f6f625f736c69636531666c6f61745f636d705f6f"
"6f625f736c6963653263616368655f6c655f77726f6e675f73697a655f75696e743235366e66745f666c6167735f77"
"726f6e675f73697a655f75696e743235366e66745f786665725f6665655f77726f6e675f73697a655f75696e743235"
"363030303030303030303030303030303030303030303030303030303030303031004d0970726f6475636572730208"
"6c616e6775616765010452757374000c70726f6365737365642d6279010572757374631d312e39352e302028353938"
"30373631366520323032362d30342d313429002c0f7461726765745f6665617475726573022b0f6d757461626c652d"
"676c6f62616c732b087369676e2d657874";
extern std::string const kBadAlignWasmHex =
"0061736d01000000011b046000017f60057f7f7f7f7f017f60067f7f7f7f7f7f017f60000002260203656e760f666c"
"6f61745f66726f6d5f75696e74000103656e7608636865636b5f6964000203050403000000050301000306470b7f00"
"4180080b7f00418088020b7f004180080b7f00418088040b7f00418088040b7f00418088080b7f004180080b7f0041"
"8088080b7f004180800c0b7f0041000b7f0041010b07cc0110066d656d6f72790200115f5f7761736d5f63616c6c5f"
"63746f72730002057465737431000307655f64617461310300057465737432000407655f6461746132030104746573"
"7400050c5f5f64736f5f68616e646c6503020a5f5f646174615f656e6403030b5f5f737461636b5f6c6f7703040c5f"
"5f737461636b5f6869676803050d5f5f676c6f62616c5f6261736503060b5f5f686561705f6261736503070a5f5f68"
"6561705f656e6403080d5f5f6d656d6f72795f6261736503090c5f5f7461626c655f62617365030a0a99020402000b"
"2801017f418108427f370000418108410841a308410c41001000220041a40828020020004100481b0b5f01017f419a"
"88024191a4cca00136010041928802428994ace0d0c1c38710370100418a88024281848ca0d0c0c183083701004181"
"8802417f360000418a8802411441818802410441a3880241201001220041a4880228020020004100481b0b8a010103"
"7f418108427f370000418108410841a308410c410010002100419a88024191a4cca00136010041928802428994ace0"
"d0c1c38710370100418a88024281848ca0d0c0c1830837010041818802417f36000041a4082802002101418a880241"
"1441818802410441a3880241201001220241a4880228020020024100481b2000200120004100481b6a0b007f097072"
"6f647563657273010c70726f6365737365642d62790105636c616e675f31392e312e352d776173692d73646b202868"
"747470733a2f2f6769746875622e636f6d2f6c6c766d2f6c6c766d2d70726f6a656374206162346235613264623538"
"32393538616631656533303861373930636664623432626432343732302900490f7461726765745f66656174757265"
"73042b0f6d757461626c652d676c6f62616c732b087369676e2d6578742b0f7265666572656e63652d74797065732b"
"0a6d756c746976616c7565";

View File

@@ -1,10 +0,0 @@
#pragma once
#include <string>
extern std::string const kLedgerSqnWasmHex;
extern std::string const kAllHostFunctionsWasmHex;
extern std::string const kAllKeyletsWasmHex;
extern std::string const kCodecovTestsWasmHex;
extern std::string const kBadAlignWasmHex;

Some files were not shown because too many files have changed in this diff Show More