Skip to content

refactor: Wasm vm redesign - #7952

Open
kuznetsss wants to merge 60 commits into
XRPLF:ripple/wasmi-host-functionsfrom
kuznetsss:Wasm-vm-redesign
Open

refactor: Wasm vm redesign#7952
kuznetsss wants to merge 60 commits into
XRPLF:ripple/wasmi-host-functionsfrom
kuznetsss:Wasm-vm-redesign

Conversation

@kuznetsss

@kuznetsss kuznetsss commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

High Level Overview of Change

This PR replaces the wasmi C-API integration with a Rust wasmi wrapper reached over a cxx
bridge.

What's here

crates/ — cargo workspace, built in via corrosion.

  • xrpl-host-functionsthe ABI's single declaration site. host_functions! { … }
    generates the HostFunctions trait and the spec enum (import name + gas per function).
    no_std, no allocator, zero runtime deps, builds for wasm32 — so the guest SDK can
    implement the same trait from the same declaration.
  • xrpl-wasm-vm — the engine: run, and check (compile / imports / entry point /
    declared memory, with no host, store or gas, because a transaction's preflight() has no
    ledger to serve a host call from).
  • xrpl-wasm-vm-ffi — the bridge. Three crossings: run_escrow in, host calls back out,
    check_escrow in.
  • xrpl-wasm-testkit — test-only compile_wat, a separate crate so no assembler can reach
    the shipped node.

C++HostContext.{h,cpp}, an ABI-shaped noexcept view of HostFunctions; and
WasmVM.{h,cpp} with runEscrowWasm / preflightEscrowWasm and both TER maps.

Not in this PR

  • 5 of ~65 host functions are registered (ldgr_index, home_le_field, sha512_half,
    trace, trace_num). The remaining HostFuncImpl*.cpp are untouched and still compile.
  • src/test/app/HostFuncImpl_test.cpp is commented out: its assertions ran through the
    deleted C lowering.

Base branches

The PR is based on 2 branches:

Context of Change

API Impact

  • Public API: New feature (new methods and/or new fields)
  • Public API: Breaking change (in general, breaking changes should only impact the next api_version)
  • libxrpl change (any change that may affect libxrpl or dependents of libxrpl)
  • Peer protocol change (must be backward compatible or bump the peer protocol version)

Comment thread src/libxrpl/tx/wasm/WasmVM.cpp Outdated
// 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});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
return std::unexpected(WasmTER{.ter = temBAD_AMOUNT, .cost = std::nullopt});
return std::unexpected{WasmTER{.ter = temBAD_AMOUNT, .cost = std::nullopt}};

Comment thread src/libxrpl/tx/wasm/WasmVM.cpp Outdated
if (gasLimit <= 0)
return std::unexpected(WasmTER{.ter = temBAD_AMOUNT, .cost = std::nullopt});

auto const nodeSideFault = std::unexpected(WasmTER{.ter = tecINTERNAL, .cost = std::nullopt});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
auto const nodeSideFault = std::unexpected(WasmTER{.ter = tecINTERNAL, .cost = std::nullopt});
auto const nodeSideFault = std::unexpected{WasmTER{.ter = tecINTERNAL, .cost = std::nullopt}};

Comment thread src/libxrpl/tx/wasm/WasmVM.cpp Outdated
HostContext ctx{hfs};
auto const run = rs::wasm_vm::run_escrow(
ctx,
rust::Slice<std::uint8_t const>(wasmCode.data(), wasmCode.size()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
rust::Slice<std::uint8_t const>(wasmCode.data(), wasmCode.size()),
rust::Slice<std::uint8_t const>{wasmCode.data(), wasmCode.size()},

Comment thread src/libxrpl/tx/wasm/WasmVM.cpp Outdated
ctx,
rust::Slice<std::uint8_t const>(wasmCode.data(), wasmCode.size()),
static_cast<std::uint64_t>(gasLimit),
rust::Str(funcName.data(), funcName.size()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
rust::Str(funcName.data(), funcName.size()));
rust::Str{funcName.data(), funcName.size())};

Comment thread src/libxrpl/tx/wasm/WasmVM.cpp Outdated
if (!result)
{
JLOG(hfs.getJournal().warn())
<< "wasm: " << std::string_view(run.detail.data(), run.detail.size())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
<< "wasm: " << std::string_view(run.detail.data(), run.detail.size())
<< "wasm: " << std::string_view{run.detail.data(), run.detail.size()}

Comment thread src/libxrpl/tx/wasm/WasmVM.cpp Outdated
return impl_->getJournal();
return guarded(j, NotTEC{telFAILED_PROCESSING}, [&]() {
auto const checked = rs::wasm_vm::check_escrow(
rust::Slice<std::uint8_t const>(wasmCode.data(), wasmCode.size()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
rust::Slice<std::uint8_t const>(wasmCode.data(), wasmCode.size()),
rust::Slice<std::uint8_t const>{wasmCode.data(), wasmCode.size()},

Comment thread src/libxrpl/tx/wasm/WasmVM.cpp Outdated
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()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
rust::Str(funcName.data(), funcName.size()));
rust::Str{funcName.data(), funcName.size())};

Comment thread src/libxrpl/tx/wasm/WasmVM.cpp Outdated
if (!isTesSuccess(ter))
{
JLOG(j.warn()) << "wasm: "
<< std::string_view(checked.detail.data(), checked.detail.size())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
<< std::string_view(checked.detail.data(), checked.detail.size())
<< std::string_view{checked.detail.data(), checked.detail.size()}

Comment thread src/tests/libxrpl/tx/wasm/HostCalls.cpp Outdated
// ldgr_index — no input, one scalar output
// ---------------------------------------------------------------------------------------

class LedgerSqnCall : public HostCallTest

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of feel like we should split these tests out into their own source files. There will be a decent amount of host functions and this file will get pretty large.

Comment thread src/tests/libxrpl/tx/wasm/MockHostFunctions.h
// an expectation can say *what* the guest asked the host to work on.
MATCHER_P(BytesAre, expected, "")
{
return std::string_view(reinterpret_cast<char const*>(arg.data()), arg.size()) ==

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
return std::string_view(reinterpret_cast<char const*>(arg.data()), arg.size()) ==
return std::string_view{reinterpret_cast<char const*>(arg.data()), arg.size()} ==

MATCHER_P(BytesAre, expected, "")
{
return std::string_view(reinterpret_cast<char const*>(arg.data()), arg.size()) ==
std::string_view(expected);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
std::string_view(expected);
std::string_view{expected};

Comment thread src/tests/libxrpl/tx/wasm/WasmFixture.h Outdated
std::string text_;

public:
CapturingSink() : Sink(beast::Severity::Warning, false)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
CapturingSink() : Sink(beast::Severity::Warning, false)
CapturingSink() : Sink{beast::Severity::Warning, false}

Comment thread src/tests/libxrpl/tx/wasm/WasmFixture.h Outdated

// Keeps what a run logged. The host's default journal is a null sink, which would let a
// swallowed condition pass a test that only checks the TER.
class CapturingSink : public beast::Journal::Sink

@TimothyBanks TimothyBanks Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a type that should already be available within the repo.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a CaptureSink in CaptureLogs.h.

Comment thread src/tests/libxrpl/tx/wasm/WasmFixture.h Outdated
inline Bytes
assembleWat(std::string_view wat)
{
auto const wasm = rs::wasm_testkit::compile_wat(rust::Str(wat.data(), wat.size()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
auto const wasm = rs::wasm_testkit::compile_wat(rust::Str(wat.data(), wat.size()));
auto const wasm = rs::wasm_testkit::compile_wat(rust::Str{wat.data(), wat.size()});

}

static Bytes
assemble(std::string_view wat)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this function provide over just calling assembleWat?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also converts rust::Vec into Bytes

Comment thread src/tests/libxrpl/tx/wasm/WasmVM.cpp Outdated
char const* what;
Bytes code;
std::string_view entryPoint;
} const cases[] = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this is valid C++ code, it seems like a rarely used way to declare a type and use it in a variable.

Comment thread src/tests/libxrpl/tx/wasm/WasmVM.cpp Outdated
// have screened.
TEST_F(WasmVMTest, TrappingStartSectionIsChargedToTheContract)
{
constexpr std::string_view wat = R"wat(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: All of these wat variables could be static as well.

Comment thread src/tests/libxrpl/tx/wasm/WasmVM.cpp Outdated
// asked to run anything.
TEST_F(WasmVMTest, NoGasIsRefusedAsMalformedRatherThanRun)
{
for (std::int64_t const gas : {std::int64_t{0}, std::int64_t{-1}})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
for (std::int64_t const gas : {std::int64_t{0}, std::int64_t{-1}})
for (auto const gas : {std::int64_t{0}, std::int64_t{-1}})

@TimothyBanks TimothyBanks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pending conversations are all something that can be done at a later time. I think this is a great start so no reason to hold it up from us flushing out the rest of the host functions.

SlotsFull = -8,
EmptySlot = -9,
LedgerObjNotFound = -10,
Decoding = -11,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Severity: MEDIUM

The Rust HostError enum assigns Decoding = -11, but the C++ HostFunctionError in WasmCommon.h assigns OutOfTransferLimit = -11. The doc comment here claims the discriminants "mirror" the C++ enum, but they diverge at this code. When a C++ host function returns error code -11 (meaning OutOfTransferLimit), the Rust engine via HostError::from_code(-11) interprets it as Decoding — a different semantic. Since error codes are consensus-relevant (contracts branch on them), this mismatch can cause incorrect guest behavior or divergence between the two ABI definitions.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: Realign the Rust HostError enum discriminants to match the C++ HostFunctionError enum in WasmCommon.h. Specifically:

  1. In crates/xrpl-host-functions/src/lib.rs, change Decoding = -11 to OutOfTransferLimit = -11 (matching C++ line 37), and move Decoding to a new code beyond the shared C++ range (e.g., -21 or another unused value).
  2. Remove or renumber the current OutOfTransferLimit = -23 (line 86) since it will now be at -11.
  3. Also align Internal = -1 with C++'s Unimplemented = -1 — either rename the Rust variant or add Unimplemented as an alias.
  4. Update the C++ enum in WasmCommon.h to include the Rust-only variants (Decoding, NoRuntime, OutOfGas) so both sides have the same complete set.
  5. Update all downstream uses of these variants:
    • crates/xrpl-wasm-vm/src/register.rs lines 90 and 107 (uses HostError::Decoding)
    • crates/xrpl-wasm-vm/src/vm.rs line 232 (matches on HostError::Decoding)
    • crates/xrpl-wasm-vm/src/abi.rs line 63, 288, 298 (uses HostError::OutOfTransferLimit)
    • crates/xrpl-host-functions/tests/host_errors.rs lines 32 and 44 (expected code values)
    • crates/xrpl-wasm-vm/tests/host_calls.rs line 208 and tests/budgets.rs line 311, 331
  6. Update the test comment at src/tests/libxrpl/tx/wasm/WasmVM.cpp:236 that documents the known drift.

Comment on lines +36 to +38
if (size <= out.size())
std::memcpy(out.data(), value, size);
return static_cast<std::int32_t>(size);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: LOW

The answer() helper casts std::size_t to std::int32_t without range check. If a buggy HostFunctions implementation returns a Bytes vector larger than INT32_MAX, this wraps to a negative value, which the Rust engine interprets as a HostError code — causing a misclassified error on a consensus-critical path.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: Add a range check before the static_cast<std::int32_t>(size) to guard against values larger than INT32_MAX. If size exceeds INT32_MAX, return kHostInternal (the existing fatal-error sentinel already defined in this anonymous namespace) instead of allowing the cast to wrap to a negative value. INT32_MAX is available from the already-included <cstdint>.

⚠️ Experimental Feature: This code suggestion is automatically generated. Please review carefully.

Suggested change
if (size <= out.size())
std::memcpy(out.data(), value, size);
return static_cast<std::int32_t>(size);
if (size > static_cast<std::size_t>(INT32_MAX))
return kHostInternal;
if (size <= out.size())
std::memcpy(out.data(), value, size);
return static_cast<std::int32_t>(size);

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

This PR has conflicts, please resolve them in order for the PR to be reviewed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants