Compare commits

...

3 Commits

Author SHA1 Message Date
tequ
132a3a8520 Merge branch 'dev' into wasmtime-engine 2026-07-06 14:04:03 +09:00
tequ
0e8cedc7a9 feat(hook): add Wasmtime engine backend behind featureWasmtimeEngine amendment
Add Wasmtime v44.0.1 as a second WebAssembly execution engine, selectable at runtime via the new featureWasmtimeEngine Amendment. When the Amendment is enabled, hook execution and WASM validation switch from WasmEdge to Wasmtime.
sfHookInstructionCount changes semantics to fuel consumed (initial budget: 10,000,000,000 units); fee calculation is redefined accordingly.

New files:
- external/wasmtime/conanfile.py + conandata.yml — prebuilt C API binary package (macOS arm64, Linux x86_64; SHA256-pinned at v44.0.1)
- cmake/deps/Wasmtime.cmake — find_package(wasmtime REQUIRED)
- src/.../WasmtimeEngine.h/.cpp — IWasmEngine implementation

Build system changes:
- CMakeLists.txt, conanfile.py: with_wasmtime option (default on)
- cmake/RippledCore.cmake: link wasmtime::wasmtime alongside wasmedge::wasmedge

Protocol changes:
- features.macro: XRPL_FEATURE(WasmtimeEngine, Supported::yes, DefaultNo)
- WasmEngine.h: constexpr kWasmtimeInitialFuel = 10'000'000'000ULL (consensus-fixed)
- applyHook.cpp, SetHook.cpp, Change.cpp: engine selected by featureWasmtimeEngine

Implementation notes:
- Consensus-fixed config: consume_fuel=true, wasm_simd=true, relaxed_simd=false, reference_types=true, bulk_memory=true,  multi_value=false, nan_canonicalization=true
- Termination ABI: accept/rollback sets ExecState::terminated flag then returns a wasm_trap_t* to unwind the guest; execute() checks the flag before inspecting callErr to distinguish clean termination from errors
- Memory access workaround (ensureMemoryExported): Wasmtime lacks an index-based memory accessor equivalent to WasmEdge_CallingFrameGetMemoryInstance. The binary is patched before instantiation to inject a "memory" export when the module owns memory without exporting it, enabling wasmtime_caller_export_get() inside host callbacks.
2026-05-08 16:49:10 +09:00
tequ
e07b573753 refactor(hook): isolate WasmEdge behind IWasmEngine abstraction layer
Introduce an engine-agnostic abstraction so that <wasmedge/wasmedge.h> is included only in WasmEdgeEngine.cpp, enabling future execution engines to be plugged in without touching Hook API implementation code.

New files:
- include/xrpl/hook/WasmTypes.h     – GuestMemory, WasmValue, HostFunctionDecl
- src/.../hook/detail/WasmEngine.h  – IWasmEngine pure virtual interface, ExecutionResult, hookHostFunctionDecls()
- src/.../hook/detail/WasmEdgeEngine.h/.cpp – WasmEdge implementation; the sole consumer of wasmedge.h

Key changes:
- Macro.h: replace WasmEdge_* types with engine-agnostic WasmValue/GuestMemory; DEFINE_HOOK_FUNCTION now returns HostCallStatus via a thin wrapper
- applyHook.h: remove wasmedge.h include and HookExecutor class entirely
- applyHook.cpp: call makeWasmEdgeEngine()->execute() at the single call site
- SetHook.cpp, Change.cpp: call makeWasmEdgeEngine()->validate() for WASM validation instead of HookExecutor::validateWasm()

No behavioral change; no Amendment required.
2026-05-08 13:32:17 +09:00
21 changed files with 1566 additions and 309 deletions

View File

@@ -124,7 +124,8 @@ find_package(date REQUIRED)
find_package(xxHash REQUIRED)
find_package(magic_enum REQUIRED)
include(deps/WasmEdge)
include(deps/WasmEdge)
include(deps/Wasmtime)
if(TARGET nudb::core)
set(nudb nudb::core)
elseif(TARGET NuDB::nudb)

View File

@@ -58,6 +58,7 @@ target_link_libraries(xrpl.imports.main
OpenSSL::Crypto
Ripple::boost
wasmedge::wasmedge
wasmtime::wasmtime
Ripple::opts
Ripple::syslibs
absl::random_random

View File

@@ -0,0 +1 @@
find_package(wasmtime REQUIRED)

View File

@@ -22,6 +22,7 @@ class Xrpl(ConanFile):
'unity': [True, False],
'xrpld': [True, False],
'with_wasmedge': [True, False],
'with_wasmtime': [True, False],
'tool_requires_b2': [True, False],
}
@@ -53,6 +54,7 @@ class Xrpl(ConanFile):
'unity': False,
'xrpld': False,
'with_wasmedge': True,
'with_wasmtime': True,
'tool_requires_b2': False,
'date/*:header_only': False,
@@ -121,6 +123,8 @@ class Xrpl(ConanFile):
if self.options.with_wasmedge:
self.requires('wasmedge/0.11.2@xahaud/stable')
if self.options.with_wasmtime:
self.requires('wasmtime/44.0.1@xahaud/stable')
if self.options.jemalloc:
self.requires('jemalloc/5.3.0')
if self.options.rocksdb:

25
external/wasmtime/conandata.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
sources:
"44.0.1":
Macos:
"armv8":
"gcc":
url: "https://github.com/bytecodealliance/wasmtime/releases/download/v44.0.1/wasmtime-v44.0.1-aarch64-macos-c-api.tar.xz"
sha256: "5da6d09fa6340db6d7afc8b73d189eff4973825c9a6fa5e814d2f9b1ddfd8998"
"x86_64":
"gcc":
url: "https://github.com/bytecodealliance/wasmtime/releases/download/v44.0.1/wasmtime-v44.0.1-x86_64-macos-c-api.tar.xz"
sha256: ""
Linux:
"x86_64":
"gcc":
url: "https://github.com/bytecodealliance/wasmtime/releases/download/v44.0.1/wasmtime-v44.0.1-x86_64-linux-c-api.tar.xz"
sha256: "fe9b4bfa87724054cebcc09b83a17d5ded66a465054b77c8de0ebc166ad78d7a"
"armv8":
"gcc":
url: "https://github.com/bytecodealliance/wasmtime/releases/download/v44.0.1/wasmtime-v44.0.1-aarch64-linux-c-api.tar.xz"
sha256: ""
Windows:
"x86_64":
"Visual Studio":
url: "https://github.com/bytecodealliance/wasmtime/releases/download/v44.0.1/wasmtime-v44.0.1-x86_64-windows-c-api.zip"
sha256: ""

87
external/wasmtime/conanfile.py vendored Normal file
View File

@@ -0,0 +1,87 @@
from conan import ConanFile
from conan.tools.files import get, copy
from conan.errors import ConanInvalidConfiguration
import os
required_conan_version = ">=1.53.0"
class WasmtimeConan(ConanFile):
name = "wasmtime"
description = ("Wasmtime is a fast and secure runtime for WebAssembly. "
"It is a standalone wasm-only optimizing runtime for WebAssembly and WASI.")
license = "Apache-2.0"
url = "https://github.com/bytecodealliance/wasmtime"
homepage = "https://github.com/bytecodealliance/wasmtime"
topics = ("webassembly", "wasm", "wasi")
package_type = "static-library"
settings = "os", "arch", "compiler", "build_type"
@property
def _compiler_alias(self):
return {
"Visual Studio": "Visual Studio",
"msvc": "Visual Studio",
}.get(str(self.info.settings.compiler), "gcc")
def configure(self):
self.settings.compiler.rm_safe("libcxx")
self.settings.compiler.rm_safe("cppstd")
def validate(self):
try:
self.conan_data["sources"][self.version][str(self.settings.os)][str(self.settings.arch)][self._compiler_alias]
except KeyError:
raise ConanInvalidConfiguration(
f"Binaries for this combination of version/os/arch/compiler are not available "
f"({self.version}/{self.settings.os}/{self.settings.arch}/{self._compiler_alias})"
)
entry = self.conan_data["sources"][self.version][str(self.settings.os)][str(self.settings.arch)][self._compiler_alias]
if not entry.get("sha256"):
raise ConanInvalidConfiguration(
f"SHA256 not configured for {self.version}/{self.settings.os}/{self.settings.arch}/{self._compiler_alias}"
)
def package_id(self):
# Make binary compatible across compiler versions (since we're downloading prebuilt)
self.info.settings.rm_safe("compiler.version")
# Group compilers by their binary compatibility
compiler_name = str(self.info.settings.compiler)
if compiler_name in ["Visual Studio", "msvc"]:
self.info.settings.compiler = "Visual Studio"
else:
self.info.settings.compiler = "gcc"
def build(self):
# Download the prebuilt tarball
entry = self.conan_data["sources"][self.version][str(self.settings.os)][str(self.settings.arch)][self._compiler_alias]
get(self, url=entry["url"], sha256=entry["sha256"],
destination=self.source_folder, strip_root=True)
def package(self):
copy(self, pattern="*.h", dst=os.path.join(self.package_folder, "include"),
src=os.path.join(self.source_folder, "include"), keep_path=True)
srclibdir = os.path.join(self.source_folder, "lib")
dstlibdir = os.path.join(self.package_folder, "lib")
if self.settings.os == "Windows":
copy(self, pattern="wasmtime.lib", src=srclibdir, dst=dstlibdir, keep_path=False)
copy(self, pattern="wasmtime.dll", src=srclibdir, dst=os.path.join(self.package_folder, "bin"), keep_path=False)
elif self.settings.os == "Macos":
copy(self, pattern="libwasmtime.a", src=srclibdir, dst=dstlibdir, keep_path=False)
copy(self, pattern="libwasmtime.dylib", src=srclibdir, dst=dstlibdir, keep_path=False)
else:
copy(self, pattern="libwasmtime.a", src=srclibdir, dst=dstlibdir, keep_path=False)
copy(self, pattern="libwasmtime.so*", src=srclibdir, dst=dstlibdir, keep_path=False)
copy(self, pattern="LICENSE", src=self.source_folder, dst=os.path.join(self.package_folder, "licenses"), keep_path=False)
def package_info(self):
self.cpp_info.libs = ["wasmtime"]
if self.settings.os == "Windows":
self.cpp_info.system_libs += ["ws2_32", "bcrypt", "advapi32", "userenv", "ntdll", "shell32", "ole32"]
if self.settings.os in ["Linux", "FreeBSD"]:
self.cpp_info.system_libs += ["pthread", "dl", "m"]

View File

@@ -10,6 +10,8 @@
* were then used.
*/
#include <xrpl/hook/WasmTypes.h>
#define LPAREN (
#define RPAREN )
#define COMMA ,
@@ -68,104 +70,90 @@
#define SEP_uint64_t LPAREN uint64_t COMMA
#define SEP_int64_t LPAREN int64_t COMMA
#define VAL_uint32_t WasmEdge_ValueGetI32(in[_stack++])
#define VAL_int32_t WasmEdge_ValueGetI32(in[_stack++])
#define VAL_uint64_t WasmEdge_ValueGetI64(in[_stack++])
#define VAL_int64_t WasmEdge_ValueGetI64(in[_stack++])
// VAL_* : extract typed value from WasmValue in[] array (engine-agnostic)
#define VAL_uint32_t (uint32_t) in[_stack++].asI32()
#define VAL_int32_t (int32_t) in[_stack++].asI32()
#define VAL_uint64_t (uint64_t) in[_stack++].asI64()
#define VAL_int64_t (int64_t) in[_stack++].asI64()
#define VAR_ASSIGN(T, V) T V = CAT(VAL_##T)
#define RET_uint32_t(return_code) WasmEdge_ValueGenI32(return_code)
#define RET_int32_t(return_code) WasmEdge_ValueGenI32(return_code)
#define RET_uint64_t(return_code) WasmEdge_ValueGenI64(return_code)
#define RET_int64_t(return_code) WasmEdge_ValueGenI64(return_code)
// RET_* : wrap a C return value into a WasmValue
#define RET_uint32_t(return_code) hook::WasmValue::i32((uint32_t)(return_code))
#define RET_int32_t(return_code) hook::WasmValue::i32((uint32_t)(return_code))
#define RET_uint64_t(return_code) hook::WasmValue::i64((uint64_t)(return_code))
#define RET_int64_t(return_code) hook::WasmValue::i64((uint64_t)(return_code))
#define RET_ASSIGN(T, return_code) CAT2(RET_, T(return_code))
#define TYP_uint32_t WasmEdge_ValType_I32
#define TYP_int32_t WasmEdge_ValType_I32
#define TYP_uint64_t WasmEdge_ValType_I64
#define TYP_int64_t WasmEdge_ValType_I64
// TYP_* : map C type to WasmValue::Kind (used by WasmEdgeEngine.cpp registry)
#define TYP_uint32_t hook::WasmValue::Kind::I32
#define TYP_int32_t hook::WasmValue::Kind::I32
#define TYP_uint64_t hook::WasmValue::Kind::I64
#define TYP_int64_t hook::WasmValue::Kind::I64
#define WASM_VAL_TYPE(T, b) CAT2(TYP_, T)
#define UNSIGNED_TYPE(T) std::make_unsigned_t<T>
// DECLARE_HOOK_FUNCTION: forward-declares the impl function and the engine
// wrapper
#define DECLARE_HOOK_FUNCTION(R, F, ...) \
std::variant<UNSIGNED_TYPE(R), hook_api::hook_return_code> F( \
hook::HookContext& hookCtx, \
WasmEdge_CallingFrameContext const& frameCtx __VA_OPT__( \
COMMA __VA_ARGS__)); \
extern WasmEdge_Result WasmFunction##F( \
void* data_ptr, \
const WasmEdge_CallingFrameContext* frameCtx, \
const WasmEdge_Value* in, \
WasmEdge_Value* out); \
extern WasmEdge_ValType WasmFunctionParams##F[]; \
extern WasmEdge_ValType WasmFunctionResult##F[]; \
extern WasmEdge_FunctionTypeContext* WasmFunctionType##F; \
extern WasmEdge_String WasmFunctionName##F;
hook::GuestMemory& mem __VA_OPT__(COMMA __VA_ARGS__)); \
extern hook::HostCallStatus WasmFunction##F( \
void* userData, \
hook::GuestMemory& mem, \
hook::WasmValue const* in, \
size_t inLen, \
hook::WasmValue* out, \
size_t outLen);
// DEFINE_HOOK_FUNCTION: defines the engine-agnostic wrapper + the impl function
// body
#define DEFINE_HOOK_FUNCTION(R, F, ...) \
WasmEdge_Result hook_api::WasmFunction##F( \
hook::HostCallStatus hook_api::WasmFunction##F( \
void* data_ptr, \
const WasmEdge_CallingFrameContext* frameCtx, \
const WasmEdge_Value* in, \
WasmEdge_Value* out) \
hook::GuestMemory& mem, \
hook::WasmValue const* in, \
size_t /*inLen*/, \
hook::WasmValue* out, \
size_t /*outLen*/) \
{ \
__VA_OPT__(int _stack = 0;) \
__VA_OPT__(FOR_VARS(VAR_ASSIGN, 2, __VA_ARGS__);) \
hook::HookContext* hookCtx = \
reinterpret_cast<hook::HookContext*>(data_ptr); \
auto const& return_code = hook_api::F( \
*hookCtx, \
*const_cast<WasmEdge_CallingFrameContext*>(frameCtx) \
__VA_OPT__(COMMA STRIP_TYPES(__VA_ARGS__))); \
*hookCtx, mem __VA_OPT__(COMMA STRIP_TYPES(__VA_ARGS__))); \
if (std::holds_alternative<hook_api::hook_return_code>(return_code) && \
(std::get<hook_api::hook_return_code>(return_code) == \
RC_ROLLBACK || \
std::get<hook_api::hook_return_code>(return_code) == RC_ACCEPT)) \
return WasmEdge_Result_Terminate; \
return hook::HostCallStatus::Terminate; \
out[0] = RET_ASSIGN( \
R, \
std::holds_alternative<UNSIGNED_TYPE(R)>(return_code) \
? std::get<UNSIGNED_TYPE(R)>(return_code) \
: R(std::get<hook_api::hook_return_code>(return_code))); \
return WasmEdge_Result_Success; \
return hook::HostCallStatus::Success; \
}; \
WasmEdge_ValType hook_api::WasmFunctionParams##F[] = { \
__VA_OPT__(FOR_VARS(WASM_VAL_TYPE, 0, __VA_ARGS__))}; \
WasmEdge_ValType hook_api::WasmFunctionResult##F[1] = { \
WASM_VAL_TYPE(R, dummy)}; \
WasmEdge_FunctionTypeContext* hook_api::WasmFunctionType##F = \
WasmEdge_FunctionTypeCreate( \
WasmFunctionParams##F, \
VA_NARGS(NULL __VA_OPT__(, __VA_ARGS__)), \
WasmFunctionResult##F, \
1); \
WasmEdge_String hook_api::WasmFunctionName##F = \
WasmEdge_StringCreateByCString(#F); \
std::variant<UNSIGNED_TYPE(R), hook_api::hook_return_code> hook_api::F( \
hook::HookContext& hookCtx, \
WasmEdge_CallingFrameContext const& frameCtx __VA_OPT__( \
COMMA __VA_ARGS__))
hook::GuestMemory& mem __VA_OPT__(COMMA __VA_ARGS__))
#define HOOK_SETUP() \
using enum hook_api::hook_return_code; \
try \
{ \
[[maybe_unused]] ApplyContext& applyCtx = hookCtx.applyCtx; \
[[maybe_unused]] auto& view = applyCtx.view(); \
[[maybe_unused]] auto j = applyCtx.app.journal("View"); \
[[maybe_unused]] WasmEdge_MemoryInstanceContext* memoryCtx = \
WasmEdge_CallingFrameGetMemoryInstance(&frameCtx, 0); \
[[maybe_unused]] unsigned char* memory = \
WasmEdge_MemoryInstanceGetPointer(memoryCtx, 0, 0); \
[[maybe_unused]] const uint64_t memory_length = \
WasmEdge_MemoryInstanceGetPageSize(memoryCtx) * \
WasmEdge_kPageSize; \
[[maybe_unused]] auto& api = hookCtx.api(); \
if (!memoryCtx || !memory || !memory_length) \
#define HOOK_SETUP() \
using enum hook_api::hook_return_code; \
try \
{ \
[[maybe_unused]] ApplyContext& applyCtx = hookCtx.applyCtx; \
[[maybe_unused]] auto& view = applyCtx.view(); \
[[maybe_unused]] auto j = applyCtx.app.journal("View"); \
[[maybe_unused]] unsigned char* memory = mem.base; \
[[maybe_unused]] const uint64_t memory_length = mem.size; \
[[maybe_unused]] auto& api = hookCtx.api(); \
if (!memory || !memory_length) \
return INTERNAL_ERROR;
#define HOOK_TEARDOWN() \
@@ -197,11 +185,10 @@
<< " bytes past end of wasm memory"; \
return OUT_OF_BOUNDS; \
} \
if (!WasmEdge_ResultOK(WasmEdge_MemoryInstanceSetData( \
memoryCtx, \
reinterpret_cast<const uint8_t*>(host_src_ptr), \
if (!mem.write( \
guest_dst_ptr, \
bytes_to_write))) \
reinterpret_cast<uint8_t const*>(host_src_ptr), \
static_cast<uint64_t>(bytes_to_write))) \
return INTERNAL_ERROR; \
bytes_written += bytes_to_write; \
}

View File

@@ -0,0 +1,114 @@
#ifndef RIPPLE_HOOK_WASMTYPES_H_INCLUDED
#define RIPPLE_HOOK_WASMTYPES_H_INCLUDED
#include <xrpl/basics/base_uint.h>
#include <cstdint>
#include <cstring>
#include <vector>
namespace hook {
struct GuestMemory
{
uint8_t* base;
uint64_t size;
inline bool
inBounds(uint64_t guestPtr, uint64_t len) const
{
if (len > 0 && guestPtr > (UINT64_MAX - len + 1))
return false;
return (guestPtr + len) <= size;
}
inline bool
write(uint64_t guestPtr, void const* src, uint64_t len)
{
if (!inBounds(guestPtr, len))
return false;
std::memcpy(base + guestPtr, src, len);
return true;
}
inline bool
read(uint64_t guestPtr, void* dst, uint64_t len) const
{
if (!inBounds(guestPtr, len))
return false;
std::memcpy(dst, base + guestPtr, len);
return true;
}
};
struct WasmValue
{
enum Kind : uint8_t
{
I32,
I64
} kind;
union
{
uint32_t u32;
uint64_t u64;
};
static inline WasmValue
i32(uint32_t v)
{
WasmValue w;
w.kind = I32;
w.u32 = v;
return w;
}
static inline WasmValue
i64(uint64_t v)
{
WasmValue w;
w.kind = I64;
w.u64 = v;
return w;
}
inline uint32_t
asI32() const
{
return u32;
}
inline uint64_t
asI64() const
{
return u64;
}
};
enum class HostCallStatus
{
Success,
Terminate,
Trap,
};
using HostFunctionFn = HostCallStatus (*)(
void* userData,
GuestMemory& mem,
WasmValue const* in,
size_t inLen,
WasmValue* out,
size_t outLen);
struct HostFunctionDecl
{
char const* name;
HostFunctionFn fn;
std::vector<WasmValue::Kind> params;
WasmValue::Kind result;
ripple::uint256 const* featureGate;
};
} // namespace hook
#endif // RIPPLE_HOOK_WASMTYPES_H_INCLUDED

View File

@@ -34,6 +34,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(WasmtimeEngine, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -15202,8 +15202,9 @@ public:
using namespace test::jtx;
static FeatureBitset const all{supported_amendments()};
static std::array<FeatureBitset, 8> const feats{
static std::array<FeatureBitset, 9> const feats{
all,
all - featureWasmtimeEngine,
all - fixXahauV2,
all - fixXahauV1 - fixXahauV2,
all - fixXahauV1 - fixXahauV2 - fixNSDelete,
@@ -15474,7 +15475,8 @@ SETHOOK_TEST(3, false)
SETHOOK_TEST(4, false)
SETHOOK_TEST(5, false)
SETHOOK_TEST(6, false)
SETHOOK_TEST(7, true)
SETHOOK_TEST(7, false)
SETHOOK_TEST(8, true)
BEAST_DEFINE_TESTSUITE_PRIO(SetHook0, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook1, app, ripple, 2);
@@ -15484,6 +15486,7 @@ BEAST_DEFINE_TESTSUITE_PRIO(SetHook4, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook5, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook6, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook7, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook8, app, ripple, 2);
} // namespace test
} // namespace ripple
#undef M

View File

@@ -97,7 +97,6 @@ struct StubHookContext
std::map<uint32_t, uint32_t> guard_map{};
StubHookResult result = {};
std::optional<ripple::STObject> emitFailure = std::nullopt;
const hook::HookExecutor* module = 0;
};
// Overload that takes external stateMap to avoid dangling reference

View File

@@ -145,8 +145,7 @@ makeStubHookContext(
.executeAgainAsWeak = result.executeAgainAsWeak,
.provisionalMeta = result.provisionalMeta,
},
.emitFailure = stubHookContext.emitFailure,
.module = nullptr};
.emitFailure = stubHookContext.emitFailure};
}
// Original function - WARNING: stateMap reference may become dangling

View File

@@ -17,7 +17,6 @@
#include <queue>
#include <utility>
#include <vector>
#include <wasmedge/wasmedge.h>
namespace hook {
struct HookContext;
@@ -179,8 +178,6 @@ struct HookResult
foreignStateGrantCache; // add found grants here to avoid rechecking
};
class HookExecutor;
struct SlotEntry
{
std::shared_ptr<const ripple::STObject> storage;
@@ -217,7 +214,6 @@ struct HookContext
emitFailure; // if this is a callback from a failed
// emitted txn then this optional becomes
// populated with the SLE
const HookExecutor* module = 0;
// Lazy-initialized HookAPI member
mutable std::unique_ptr<HookAPI> api_;
@@ -272,231 +268,9 @@ gatherHookParameters(
std::map<std::vector<uint8_t>, std::vector<uint8_t>>& parameters,
beast::Journal const& j_);
// RH TODO: call destruct for these on rippled shutdown
#define ADD_HOOK_FUNCTION(F, ctx) \
{ \
WasmEdge_FunctionInstanceContext* hf = \
WasmEdge_FunctionInstanceCreate( \
hook_api::WasmFunctionType##F, \
hook_api::WasmFunction##F, \
(void*)(&ctx), \
0); \
WasmEdge_ModuleInstanceAddFunction( \
importObj, hook_api::WasmFunctionName##F, hf); \
}
#define HR_ACC() hookResult.account << "-" << hookResult.otxnAccount
#define HC_ACC() hookCtx.result.account << "-" << hookCtx.result.otxnAccount
// create these once at boot and keep them
static WasmEdge_String exportName = WasmEdge_StringCreateByCString("env");
static WasmEdge_String tableName = WasmEdge_StringCreateByCString("table");
static auto* tableType = WasmEdge_TableTypeCreate(
WasmEdge_RefType_FuncRef,
{.HasMax = true, .Shared = false, .Min = 10, .Max = 20});
static auto* memType = WasmEdge_MemoryTypeCreate(
{.HasMax = true, .Shared = false, .Min = 1, .Max = 1});
static WasmEdge_String memName = WasmEdge_StringCreateByCString("memory");
static WasmEdge_String cbakFunctionName =
WasmEdge_StringCreateByCString("cbak");
static WasmEdge_String hookFunctionName =
WasmEdge_StringCreateByCString("hook");
// see: lib/system/allocator.cpp
#define WasmEdge_kPageSize 65536ULL
/**
* HookExecutor is effectively a two-part function:
* The first part sets up the Hook Api inside the wasm import, ready for use
* (this is done during object construction.)
* The second part is actually executing webassembly instructions
* this is done during execteWasm function.
* The instance is single use.
*/
class HookExecutor
{
private:
bool spent = false; // a HookExecutor can only be used once
public:
HookContext& hookCtx;
WasmEdge_ModuleInstanceContext* importObj;
class WasmEdgeVM
{
public:
WasmEdge_ConfigureContext* conf = NULL;
WasmEdge_VMContext* ctx = NULL;
WasmEdgeVM()
{
conf = WasmEdge_ConfigureCreate();
if (!conf)
return;
WasmEdge_ConfigureStatisticsSetInstructionCounting(conf, true);
ctx = WasmEdge_VMCreate(conf, NULL);
}
bool
sane()
{
return ctx && conf;
}
~WasmEdgeVM()
{
if (conf)
WasmEdge_ConfigureDelete(conf);
if (ctx)
WasmEdge_VMDelete(ctx);
}
};
// if an error occured return a string prefixed with `prefix` followed by
// the error description
static std::optional<std::string>
getWasmError(std::string prefix, WasmEdge_Result& res)
{
if (WasmEdge_ResultOK(res))
return {};
const char* msg = WasmEdge_ResultGetMessage(res);
return prefix + ": " + (msg ? msg : "unknown error");
}
/**
* Validate that a web assembly blob can be loaded by wasmedge
*/
static std::optional<std::string>
validateWasm(const void* wasm, size_t len)
{
WasmEdgeVM vm;
if (!vm.sane())
return "Could not create WASMEDGE instance";
WasmEdge_Result res = WasmEdge_VMLoadWasmFromBuffer(
vm.ctx, reinterpret_cast<const uint8_t*>(wasm), len);
if (auto err = getWasmError("VMLoadWasmFromBuffer failed", res); err)
return *err;
res = WasmEdge_VMValidate(vm.ctx);
if (auto err = getWasmError("VMValidate failed", res); err)
return *err;
return {};
}
/**
* Execute web assembly byte code against the constructed Hook Context
* Once execution has occured the exector is spent and cannot be used again
* and should be destructed Information about the execution is populated
* into hookCtx
*/
void
executeWasm(
const void* wasm,
size_t len,
bool callback,
uint32_t wasmParam,
beast::Journal const& j)
{
// HookExecutor can only execute once
XRPL_ASSERT(
!spent,
"HookExecutor::executeWasm : HookExecutor can only execute once");
spent = true;
JLOG(j.trace()) << "HookInfo[" << HC_ACC()
<< "]: creating wasm instance";
WasmEdge_LogOff();
WasmEdgeVM vm;
if (!vm.sane())
{
JLOG(j.warn()) << "HookError[" << HC_ACC()
<< "]: Could not create WASMEDGE instance.";
hookCtx.result.exitType = hook_api::ExitType::WASM_ERROR;
return;
}
WasmEdge_Result res =
WasmEdge_VMRegisterModuleFromImport(vm.ctx, this->importObj);
if (auto err = getWasmError("Import phase failed", res); err)
{
hookCtx.result.exitType = hook_api::ExitType::WASM_ERROR;
JLOG(j.trace()) << "HookError[" << HC_ACC() << "]: " << *err;
return;
}
WasmEdge_Value params[1] = {WasmEdge_ValueGenI32((int64_t)wasmParam)};
WasmEdge_Value returns[1];
res = WasmEdge_VMRunWasmFromBuffer(
vm.ctx,
reinterpret_cast<const uint8_t*>(wasm),
len,
callback ? cbakFunctionName : hookFunctionName,
params,
1,
returns,
1);
if (auto err = getWasmError("WASM VM error", res); err)
{
JLOG(j.warn()) << "HookError[" << HC_ACC() << "]: " << *err;
hookCtx.result.exitType = hook_api::ExitType::WASM_ERROR;
return;
}
auto* statsCtx = WasmEdge_VMGetStatisticsContext(vm.ctx);
hookCtx.result.instructionCount =
WasmEdge_StatisticsGetInstrCount(statsCtx);
// RH NOTE: stack unwind will clean up WasmEdgeVM
}
HookExecutor(HookContext& ctx)
: hookCtx(ctx), importObj(WasmEdge_ModuleInstanceCreate(exportName))
{
ctx.module = this;
WasmEdge_LogSetDebugLevel();
#pragma push_macro("HOOK_API_DEFINITION")
#undef HOOK_API_DEFINITION
#define HOOK_WRAP_PARAMS(...) __VA_ARGS__
#define HOOK_API_DEFINITION(RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE, ...) \
ADD_HOOK_FUNCTION(FUNCTION_NAME, ctx);
#include <xrpl/hook/hook_api.macro>
#undef HOOK_API_DEFINITION
#undef HOOK_WRAP_PARAMS
#pragma pop_macro("HOOK_API_DEFINITION")
WasmEdge_TableInstanceContext* hostTable =
WasmEdge_TableInstanceCreate(tableType);
WasmEdge_ModuleInstanceAddTable(importObj, tableName, hostTable);
WasmEdge_MemoryInstanceContext* hostMem =
WasmEdge_MemoryInstanceCreate(memType);
WasmEdge_ModuleInstanceAddMemory(importObj, memName, hostMem);
}
~HookExecutor()
{
WasmEdge_ModuleInstanceDelete(importObj);
};
};
} // namespace hook
#endif

View File

@@ -0,0 +1,336 @@
#include <xrpld/app/hook/detail/WasmEdgeEngine.h>
#include <xrpld/app/hook/applyHook.h>
#include <xrpl/protocol/Feature.h>
#include <memory>
#include <optional>
#include <string>
#include <type_traits>
#include <vector>
#include <wasmedge/wasmedge.h>
using namespace ripple;
namespace hook {
namespace {
// ── Type-level helpers ──────────────────────────────────────────────────────
template <typename T>
constexpr WasmValue::Kind
kindOf()
{
if constexpr (
std::is_same_v<T, uint64_t> || std::is_same_v<T, int64_t>)
return WasmValue::Kind::I64;
return WasmValue::Kind::I32;
}
template <typename... Ts>
std::vector<WasmValue::Kind>
buildKinds()
{
return {kindOf<Ts>()...};
}
// ── WasmEdge VM RAII ────────────────────────────────────────────────────────
struct WasmEdgeVM
{
WasmEdge_ConfigureContext* conf = nullptr;
WasmEdge_VMContext* ctx = nullptr;
WasmEdgeVM()
{
conf = WasmEdge_ConfigureCreate();
if (!conf)
return;
WasmEdge_ConfigureStatisticsSetInstructionCounting(conf, true);
ctx = WasmEdge_VMCreate(conf, nullptr);
}
bool
sane() const
{
return ctx && conf;
}
~WasmEdgeVM()
{
if (ctx)
WasmEdge_VMDelete(ctx);
if (conf)
WasmEdge_ConfigureDelete(conf);
}
};
static std::optional<std::string>
getWasmError(std::string const& prefix, WasmEdge_Result res)
{
if (WasmEdge_ResultOK(res))
return {};
const char* msg = WasmEdge_ResultGetMessage(res);
return prefix + ": " + (msg ? msg : "unknown error");
}
// ── Bridge: single WasmEdge callback → HostFunctionFn ──────────────────────
struct BridgeData
{
HostFunctionDecl const* decl;
HookContext* ctx;
};
static WasmEdge_Result
bridgeFn(
void* data_ptr,
WasmEdge_CallingFrameContext const* frameCtx,
WasmEdge_Value const* in,
WasmEdge_Value* out)
{
auto* bridge = static_cast<BridgeData*>(data_ptr);
auto* memCtx = WasmEdge_CallingFrameGetMemoryInstance(frameCtx, 0);
GuestMemory mem{
WasmEdge_MemoryInstanceGetPointer(memCtx, 0, 0),
WasmEdge_MemoryInstanceGetPageSize(memCtx) * 65536ULL};
size_t const paramCount = bridge->decl->params.size();
std::vector<WasmValue> inVals(paramCount);
for (size_t i = 0; i < paramCount; ++i)
{
if (bridge->decl->params[i] == WasmValue::Kind::I32)
inVals[i] = WasmValue::i32((uint32_t)WasmEdge_ValueGetI32(in[i]));
else
inVals[i] = WasmValue::i64((uint64_t)WasmEdge_ValueGetI64(in[i]));
}
WasmValue outVal;
auto status = bridge->decl->fn(
bridge->ctx, mem, inVals.data(), paramCount, &outVal, 1);
if (status == HostCallStatus::Terminate || status == HostCallStatus::Trap)
return WasmEdge_Result_Terminate;
if (bridge->decl->result == WasmValue::Kind::I32)
out[0] = WasmEdge_ValueGenI32((int32_t)outVal.asI32());
else
out[0] = WasmEdge_ValueGenI64((int64_t)outVal.asI64());
return WasmEdge_Result_Success;
}
// ── WasmEdgeEngineImpl ──────────────────────────────────────────────────────
class WasmEdgeEngineImpl : public IWasmEngine
{
public:
std::optional<std::string>
validate(void const* wasm, size_t len) override
{
WasmEdgeVM vm;
if (!vm.sane())
return "Could not create WASMEDGE instance";
WasmEdge_Result res = WasmEdge_VMLoadWasmFromBuffer(
vm.ctx, reinterpret_cast<uint8_t const*>(wasm), len);
if (auto err = getWasmError("VMLoadWasmFromBuffer failed", res))
return *err;
res = WasmEdge_VMValidate(vm.ctx);
if (auto err = getWasmError("VMValidate failed", res))
return *err;
return {};
}
ExecutionResult
execute(
void const* wasm,
size_t len,
bool isCallback,
uint32_t wasmParam,
HookContext& ctx,
std::vector<HostFunctionDecl> const& imports,
ripple::Rules const& rules,
beast::Journal const& j) override
{
static WasmEdge_String exportName =
WasmEdge_StringCreateByCString("env");
static WasmEdge_String tableName =
WasmEdge_StringCreateByCString("table");
static auto* tableType = WasmEdge_TableTypeCreate(
WasmEdge_RefType_FuncRef,
{.HasMax = true, .Shared = false, .Min = 10, .Max = 20});
static auto* memType = WasmEdge_MemoryTypeCreate(
{.HasMax = true, .Shared = false, .Min = 1, .Max = 1});
static WasmEdge_String memName =
WasmEdge_StringCreateByCString("memory");
static WasmEdge_String cbakFunctionName =
WasmEdge_StringCreateByCString("cbak");
static WasmEdge_String hookFunctionName =
WasmEdge_StringCreateByCString("hook");
// Bridge data must not reallocate during registration
std::vector<BridgeData> bridgeData;
bridgeData.reserve(imports.size());
auto* importObj = WasmEdge_ModuleInstanceCreate(exportName);
for (auto const& decl : imports)
{
// featureGate: if non-null and not zero, check rules
if (decl.featureGate && !(*decl.featureGate).isZero() &&
!rules.enabled(*decl.featureGate))
continue;
bridgeData.push_back({&decl, &ctx});
std::vector<WasmEdge_ValType> paramTypes;
paramTypes.reserve(decl.params.size());
for (auto k : decl.params)
paramTypes.push_back(
k == WasmValue::Kind::I32 ? WasmEdge_ValType_I32
: WasmEdge_ValType_I64);
WasmEdge_ValType resultType =
decl.result == WasmValue::Kind::I32 ? WasmEdge_ValType_I32
: WasmEdge_ValType_I64;
auto* fnType = WasmEdge_FunctionTypeCreate(
paramTypes.data(), paramTypes.size(), &resultType, 1);
auto* fn = WasmEdge_FunctionInstanceCreate(
fnType, bridgeFn, &bridgeData.back(), 0);
WasmEdge_FunctionTypeDelete(fnType);
auto nameStr = WasmEdge_StringCreateByCString(decl.name);
WasmEdge_ModuleInstanceAddFunction(importObj, nameStr, fn);
WasmEdge_StringDelete(nameStr);
}
WasmEdge_ModuleInstanceAddTable(
importObj, tableName, WasmEdge_TableInstanceCreate(tableType));
WasmEdge_ModuleInstanceAddMemory(
importObj, memName, WasmEdge_MemoryInstanceCreate(memType));
JLOG(j.trace()) << "HookInfo[" << ctx.result.account << "-"
<< ctx.result.otxnAccount
<< "]: creating wasm instance";
WasmEdge_LogOff();
WasmEdgeVM vm;
if (!vm.sane())
{
WasmEdge_ModuleInstanceDelete(importObj);
JLOG(j.warn()) << "HookError[" << ctx.result.account << "-"
<< ctx.result.otxnAccount
<< "]: Could not create WASMEDGE instance.";
return {false, 0, "Could not create WASMEDGE instance"};
}
WasmEdge_Result res =
WasmEdge_VMRegisterModuleFromImport(vm.ctx, importObj);
if (auto err = getWasmError("Import phase failed", res))
{
JLOG(j.trace()) << "HookError[" << ctx.result.account << "-"
<< ctx.result.otxnAccount << "]: " << *err;
WasmEdge_ModuleInstanceDelete(importObj);
return {false, 0, *err};
}
WasmEdge_Value params[1] = {
WasmEdge_ValueGenI32((int64_t)wasmParam)};
WasmEdge_Value returns[1];
res = WasmEdge_VMRunWasmFromBuffer(
vm.ctx,
reinterpret_cast<uint8_t const*>(wasm),
len,
isCallback ? cbakFunctionName : hookFunctionName,
params,
1,
returns,
1);
ExecutionResult result;
if (auto err = getWasmError("WASM VM error", res))
{
result.ok = false;
result.instructionCount = 0;
result.error = *err;
}
else
{
result.ok = true;
auto* statsCtx = WasmEdge_VMGetStatisticsContext(vm.ctx);
result.instructionCount =
WasmEdge_StatisticsGetInstrCount(statsCtx);
}
WasmEdge_ModuleInstanceDelete(importObj);
return result;
}
};
} // anonymous namespace
// ── hookHostFunctionDecls registry ─────────────────────────────────────────
// Expand hook_api.macro with custom HOOK_API_DEFINITION to build the list.
// Each entry stores the WasmFunction##F pointer and parameter kinds.
// For featureGate: uint256{} means "always available" (stored as nullptr),
// named features are stored as their address.
#pragma push_macro("HOOK_API_DEFINITION")
#pragma push_macro("HOOK_WRAP_PARAMS")
#undef HOOK_API_DEFINITION
#undef HOOK_WRAP_PARAMS
// Helper to convert feature gate argument: uint256{} → nullptr,
// named feature variable → address of that variable.
// We use an inline helper function template to do this.
namespace {
template <typename T>
static inline uint256 const*
featureGatePtr(T const& gate)
{
// If gate is zero (default uint256{}), return nullptr (no gate).
if (gate.isZero())
return nullptr;
return &gate;
}
} // namespace
#define HOOK_WRAP_PARAMS(...) __VA_ARGS__
#define HOOK_API_DEFINITION(R, F, P, GATE) \
{ \
#F, \
&hook_api::WasmFunction##F, \
buildKinds<HOOK_WRAP_PARAMS P>(), \
kindOf<R>(), \
featureGatePtr(GATE), \
},
std::vector<HostFunctionDecl> const&
hookHostFunctionDecls()
{
static std::vector<HostFunctionDecl> const decls = {
#include <xrpl/hook/hook_api.macro>
};
return decls;
}
#undef HOOK_API_DEFINITION
#undef HOOK_WRAP_PARAMS
#pragma pop_macro("HOOK_WRAP_PARAMS")
#pragma pop_macro("HOOK_API_DEFINITION")
// ── Factory ─────────────────────────────────────────────────────────────────
std::unique_ptr<IWasmEngine>
makeWasmEdgeEngine()
{
return std::make_unique<WasmEdgeEngineImpl>();
}
} // namespace hook

View File

@@ -0,0 +1,14 @@
#ifndef RIPPLE_APP_HOOK_DETAIL_WASMEDGEENGINE_H_INCLUDED
#define RIPPLE_APP_HOOK_DETAIL_WASMEDGEENGINE_H_INCLUDED
#include <xrpld/app/hook/detail/WasmEngine.h>
#include <memory>
namespace hook {
std::unique_ptr<IWasmEngine>
makeWasmEdgeEngine();
} // namespace hook
#endif // RIPPLE_APP_HOOK_DETAIL_WASMEDGEENGINE_H_INCLUDED

View File

@@ -0,0 +1,51 @@
#ifndef RIPPLE_APP_HOOK_DETAIL_WASMENGINE_H_INCLUDED
#define RIPPLE_APP_HOOK_DETAIL_WASMENGINE_H_INCLUDED
#include <xrpl/hook/WasmTypes.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Rules.h>
#include <optional>
#include <string>
#include <vector>
namespace hook {
/// Initial fuel budget per Hook execution (consensus-fixed; changing
/// this value requires a separate Amendment).
constexpr uint64_t kWasmtimeInitialFuel = 10'000'000'000ULL;
struct HookContext;
struct ExecutionResult
{
bool ok;
uint64_t instructionCount;
std::optional<std::string> error;
};
class IWasmEngine
{
public:
virtual ~IWasmEngine() = default;
virtual std::optional<std::string>
validate(void const* wasm, size_t len) = 0;
virtual ExecutionResult
execute(
void const* wasm,
size_t len,
bool isCallback,
uint32_t wasmParam,
HookContext& ctx,
std::vector<HostFunctionDecl> const& imports,
ripple::Rules const& rules,
beast::Journal const& j) = 0;
};
std::vector<HostFunctionDecl> const&
hookHostFunctionDecls();
} // namespace hook
#endif // RIPPLE_APP_HOOK_DETAIL_WASMENGINE_H_INCLUDED

View File

@@ -0,0 +1,819 @@
// WasmtimeEngine.cpp — Wasmtime backend for the IWasmEngine abstraction.
//
// WHY THIS FILE IS LARGER THAN WasmEdgeEngine.cpp
// ─────────────────────────────────────────────────
// WasmEdge provides WasmEdge_CallingFrameGetMemoryInstance(frame, index),
// which retrieves a module's linear memory by index regardless of whether it
// is exported. Wasmtime has no equivalent: the only way to access memory
// from inside a host-function callback is wasmtime_caller_export_get(), which
// only works for *exported* memories.
//
// Hook WASM modules compiled by wasmcc define their own linear memory
// (WebAssembly section 5) but do not export it. Without an export the memory
// pointer is unavailable inside host callbacks, causing every Hook API call
// that touches guest memory to see mem.base == nullptr and return
// INTERNAL_ERROR, ultimately crashing the execution.
//
// The ensureMemoryExported() function (≈200 lines) works around this by
// scanning the binary before instantiation and injecting a "memory" export
// entry into section 7 when the module owns but does not export its memory.
// The patched binary is semantically identical to the original.
//
// A potential alternative — storing the wasmtime_memory_t handle directly in
// BridgeData — requires knowing at setup time which memory the module will
// actually use (its own vs. an import from "env"), which itself requires
// binary inspection. The export-injection approach is therefore the simplest
// correct solution.
#include <xrpld/app/hook/detail/WasmtimeEngine.h>
#include <xrpld/app/hook/detail/WasmEngine.h>
#include <xrpld/app/hook/applyHook.h>
#include <xrpl/protocol/Feature.h>
#include <cstring>
#include <memory>
#include <optional>
#include <string>
#include <type_traits>
#include <vector>
// wasmtime.h must come LAST to avoid int128_t pollution
#include <wasmtime.h>
using namespace ripple;
namespace hook {
namespace {
// ── Type-level helpers ──────────────────────────────────────────────────────
template <typename T>
constexpr WasmValue::Kind
kindOf()
{
if constexpr (
std::is_same_v<T, uint64_t> || std::is_same_v<T, int64_t>)
return WasmValue::Kind::I64;
return WasmValue::Kind::I32;
}
template <typename... Ts>
std::vector<WasmValue::Kind>
buildKinds()
{
return {kindOf<Ts>()...};
}
// ── Error helpers ────────────────────────────────────────────────────────────
static std::optional<std::string>
wasmtimeError(wasmtime_error_t* err)
{
if (!err)
return {};
wasm_byte_vec_t msg;
wasmtime_error_message(err, &msg);
std::string s(msg.data, msg.size);
wasm_byte_vec_delete(&msg);
wasmtime_error_delete(err);
return s;
}
static std::optional<std::string>
wasmtimeTrap(wasm_trap_t* trap)
{
if (!trap)
return {};
wasm_byte_vec_t msg;
wasm_trap_message(trap, &msg);
std::string s(msg.data, msg.size);
wasm_byte_vec_delete(&msg);
wasm_trap_delete(trap);
return s;
}
// ── Process-global engine (thread-safe; created once) ────────────────────────
static wasm_engine_t*
getGlobalEngine()
{
static wasm_engine_t* gEngine = []() -> wasm_engine_t* {
wasm_config_t* cfg = wasm_config_new();
if (!cfg)
return nullptr;
// Consensus-fixed configuration never change without an Amendment
wasmtime_config_consume_fuel_set(cfg, true);
wasmtime_config_wasm_simd_set(cfg, true);
wasmtime_config_wasm_relaxed_simd_set(cfg, false);
wasmtime_config_wasm_reference_types_set(cfg, true);
wasmtime_config_wasm_bulk_memory_set(cfg, true);
wasmtime_config_wasm_multi_value_set(cfg, false);
wasmtime_config_cranelift_nan_canonicalization_set(cfg, true);
// wasm_threads is behind a feature flag; only set if available.
// The compile-time feature guard in config.h controls whether
// wasmtime_config_wasm_threads_set exists.
#ifdef WASMTIME_FEATURE_THREADS
wasmtime_config_wasm_threads_set(cfg, false);
#endif
// wasm_engine_new_with_config takes ownership of cfg
return wasm_engine_new_with_config(cfg);
}();
return gEngine;
}
// ── WASM binary normalisation ─────────────────────────────────────────────────
//
// Hook WASM modules typically have their own memory (WebAssembly Section 5)
// but do NOT export it. Wasmtime's wasmtime_caller_export_get() only works
// for *exported* memories, so without an export the memory pointer is
// unavailable inside host callbacks.
//
// This function inspects the binary and, when the module owns a memory but
// does not already export it as "memory", injects a new export entry for
// memory index 0. The resulting binary is semantically identical to the
// original but now exposes its memory to the host.
//
// Modules that already export "memory" (or that import memory from "env") are
// returned unchanged.
// LEB128 helpers (unsigned, forward-only)
static uint32_t
readUleb128(uint8_t const* p, size_t& pos, size_t limit)
{
uint32_t result = 0;
uint32_t shift = 0;
while (pos < limit)
{
uint8_t b = p[pos++];
result |= (uint32_t)(b & 0x7F) << shift;
if (!(b & 0x80))
break;
shift += 7;
}
return result;
}
// Encode a value as unsigned LEB128 (appends to out)
static void
writeUleb128(std::vector<uint8_t>& out, uint32_t val)
{
do
{
uint8_t b = val & 0x7F;
val >>= 7;
if (val)
b |= 0x80;
out.push_back(b);
} while (val);
}
// Ensure the WASM binary exports its first memory as "memory".
// Returns the original bytes unchanged if no patching is needed,
// otherwise returns a patched copy.
static std::vector<uint8_t>
ensureMemoryExported(void const* wasm, size_t len)
{
uint8_t const* p = reinterpret_cast<uint8_t const*>(wasm);
// Validate magic + version.
// NOTE: the magic bytes are 0x00 0x61 0x73 0x6D 0x01 0x00 0x00 0x00
// Written as a string literal, "\x00asm…" is ambiguous because \x hex
// escapes are greedy: "\x00a" == "\x0a" (newline). Use a byte array.
static constexpr uint8_t kWasmMagic[8] = {
0x00, 0x61, 0x73, 0x6D, // \0asm
0x01, 0x00, 0x00, 0x00 // version 1
};
if (len < 8 || std::memcmp(p, kWasmMagic, 8) != 0)
return {p, p + len}; // not a valid WASM binary, return as-is
bool hasOwnMemory = false;
bool exportsMemory = false;
// offset to memory section (for the insert point) and export section
size_t exportSectionOffset = 0; // byte offset of existing export section
size_t exportSectionPayloadOff = 0; // offset of payload start
size_t exportSectionPayloadLen = 0; // payload length
size_t pos = 8;
while (pos + 1 < len)
{
uint8_t sectionId = p[pos++];
size_t secLenPos = pos;
uint32_t secLen = readUleb128(p, pos, len);
size_t payloadStart = pos;
if (sectionId == 5) // memory section
{
// At least one memory entry means the module owns a memory
size_t tmp = pos;
uint32_t count = readUleb128(p, tmp, len);
if (count > 0)
hasOwnMemory = true;
}
else if (sectionId == 7) // export section
{
exportSectionOffset = secLenPos - 1; // start of section id byte
exportSectionPayloadOff = payloadStart;
exportSectionPayloadLen = secLen;
size_t tmp = pos;
uint32_t count = readUleb128(p, tmp, len);
for (uint32_t i = 0; i < count; ++i)
{
uint32_t nameLen = readUleb128(p, tmp, len);
if (tmp + nameLen > len)
break;
bool isMemoryName = (nameLen == 6 &&
std::memcmp(p + tmp, "memory", 6) == 0);
tmp += nameLen;
if (tmp >= len)
break;
uint8_t kind = p[tmp++]; // export kind byte
readUleb128(p, tmp, len); // export index
if (isMemoryName && kind == 0x02) // kind 2 == memory
{
exportsMemory = true;
break;
}
}
}
pos = payloadStart + secLen;
}
// No patching needed if:
// - module does not own a memory (it may import one; caller_export_get works)
// - module already exports its memory
if (!hasOwnMemory || exportsMemory)
return {p, p + len};
// Build a new memory export entry: \x06 m e m o r y \x02 \x00
// (name_len=6, "memory", kind=2 (memory), index=0)
std::vector<uint8_t> newExportEntry;
writeUleb128(newExportEntry, 6); // name length
for (char c : std::string("memory"))
newExportEntry.push_back((uint8_t)c);
newExportEntry.push_back(0x02); // export kind: memory
writeUleb128(newExportEntry, 0); // memory index 0
if (exportSectionOffset == 0)
{
// No export section exists at all create one from scratch.
// New section: id=7, payload = count(1) + entry
std::vector<uint8_t> newSection;
newSection.push_back(0x07); // export section id
std::vector<uint8_t> payload;
writeUleb128(payload, 1); // 1 export
payload.insert(payload.end(), newExportEntry.begin(), newExportEntry.end());
writeUleb128(newSection, (uint32_t)payload.size());
newSection.insert(newSection.end(), payload.begin(), payload.end());
// Insert the new section before the code section (id=10) or at end.
// For correctness we append after the last known section.
std::vector<uint8_t> result(p, p + len);
result.insert(result.end(), newSection.begin(), newSection.end());
return result;
}
else
{
// Patch the existing export section: increment count, append entry.
// Read existing export count (as a LEB128)
size_t countPos = exportSectionPayloadOff;
uint32_t existingCount = readUleb128(p, countPos, len);
uint32_t newCount = existingCount + 1;
std::vector<uint8_t> newCountBytes;
writeUleb128(newCountBytes, newCount);
// New payload = new count + existing entries (skip old count bytes) + new entry
std::vector<uint8_t> newPayload;
newPayload.insert(newPayload.end(), newCountBytes.begin(), newCountBytes.end());
// Existing entries start at countPos, end at exportSectionPayloadOff + exportSectionPayloadLen
size_t entriesStart = countPos;
size_t entriesEnd = exportSectionPayloadOff + exportSectionPayloadLen;
if (entriesEnd > len)
entriesEnd = len;
newPayload.insert(newPayload.end(), p + entriesStart, p + entriesEnd);
newPayload.insert(newPayload.end(), newExportEntry.begin(), newExportEntry.end());
// Encode new section length as LEB128
std::vector<uint8_t> newSecLen;
writeUleb128(newSecLen, (uint32_t)newPayload.size());
// Reconstruct the full binary:
// bytes before export section + id(0x07) + new_len + new_payload + bytes after
std::vector<uint8_t> result;
result.reserve(len + newExportEntry.size() + 4);
// Part 1: everything before the export section
result.insert(result.end(), p, p + exportSectionOffset);
// Part 2: section id
result.push_back(0x07);
// Part 3: new length
result.insert(result.end(), newSecLen.begin(), newSecLen.end());
// Part 4: new payload
result.insert(result.end(), newPayload.begin(), newPayload.end());
// Part 5: everything after the export section
size_t afterExport = exportSectionPayloadOff + exportSectionPayloadLen;
if (afterExport < len)
result.insert(result.end(), p + afterExport, p + len);
return result;
}
}
// ── Per-execution state shared between the host callback and caller ──────────
struct ExecState
{
bool terminated = false;
};
struct BridgeData
{
HostFunctionDecl const* decl;
HookContext* ctx;
ExecState* state;
};
// ── wasm_functype_t builder helper ───────────────────────────────────────────
static wasm_functype_t*
buildFuncType(
std::vector<WasmValue::Kind> const& params,
WasmValue::Kind result)
{
wasm_valtype_vec_t paramVec;
wasm_valtype_vec_t resultVec;
if (params.empty())
{
wasm_valtype_vec_new_empty(&paramVec);
}
else
{
std::vector<wasm_valtype_t*> ptrs;
ptrs.reserve(params.size());
for (auto k : params)
ptrs.push_back(
wasm_valtype_new(k == WasmValue::Kind::I32 ? WASM_I32 : WASM_I64));
wasm_valtype_vec_new(&paramVec, ptrs.size(), ptrs.data());
}
{
wasm_valtype_t* rs[1] = {
wasm_valtype_new(result == WasmValue::Kind::I32 ? WASM_I32 : WASM_I64)};
wasm_valtype_vec_new(&resultVec, 1, rs);
}
return wasm_functype_new(&paramVec, &resultVec);
}
// ── Bridge callback ──────────────────────────────────────────────────────────
static wasm_trap_t*
bridgeFn(
void* envPtr,
wasmtime_caller_t* caller,
wasmtime_val_t const* args,
size_t nargs,
wasmtime_val_t* results,
size_t nresults)
{
auto* bridge = static_cast<BridgeData*>(envPtr);
// The module's memory is always exported as "memory" after
// ensureMemoryExported() normalises the binary. For modules that import
// memory from "env" (rare) the imported memory is also accessible via
// the same export path once the linker resolves it.
wasmtime_extern_t memExtern;
GuestMemory mem{nullptr, 0};
if (wasmtime_caller_export_get(
caller, "memory", 6 /* strlen("memory") */, &memExtern))
{
if (memExtern.kind == WASMTIME_EXTERN_MEMORY)
{
wasmtime_context_t* ctx = wasmtime_caller_context(caller);
mem.base = wasmtime_memory_data(ctx, &memExtern.of.memory);
mem.size = wasmtime_memory_data_size(ctx, &memExtern.of.memory);
}
}
// Convert incoming wasmtime_val_t args to WasmValue
size_t const paramCount = bridge->decl->params.size();
std::vector<WasmValue> inVals(paramCount);
for (size_t i = 0; i < paramCount && i < nargs; ++i)
{
if (bridge->decl->params[i] == WasmValue::Kind::I32)
inVals[i] = WasmValue::i32((uint32_t)args[i].of.i32);
else
inVals[i] = WasmValue::i64((uint64_t)args[i].of.i64);
}
WasmValue outVal;
auto status =
bridge->decl->fn(bridge->ctx, mem, inVals.data(), paramCount, &outVal, 1);
if (status == HostCallStatus::Terminate || status == HostCallStatus::Trap)
{
// Signal termination to the outer caller
bridge->state->terminated = true;
// Return a trap to unwind the WASM stack
return wasmtime_trap_new("hook terminated", 15);
}
// Write result
if (nresults > 0)
{
if (bridge->decl->result == WasmValue::Kind::I32)
{
results[0].kind = WASMTIME_I32;
results[0].of.i32 = (int32_t)outVal.asI32();
}
else
{
results[0].kind = WASMTIME_I64;
results[0].of.i64 = (int64_t)outVal.asI64();
}
}
return nullptr; // success
}
// ── WasmtimeEngineImpl ───────────────────────────────────────────────────────
class WasmtimeEngineImpl : public IWasmEngine
{
public:
std::optional<std::string>
validate(void const* wasm, size_t len) override
{
wasm_engine_t* engine = getGlobalEngine();
if (!engine)
return "Could not create Wasmtime engine";
// Normalise the binary so validation sees the same form as execution.
auto patched = ensureMemoryExported(wasm, len);
wasmtime_module_t* mod = nullptr;
wasmtime_error_t* err = wasmtime_module_new(
engine,
patched.data(),
patched.size(),
&mod);
if (auto msg = wasmtimeError(err))
return "Wasmtime validate failed: " + *msg;
wasmtime_module_delete(mod);
return {};
}
ExecutionResult
execute(
void const* wasm,
size_t len,
bool isCallback,
uint32_t wasmParam,
HookContext& ctx,
std::vector<HostFunctionDecl> const& imports,
ripple::Rules const& rules,
beast::Journal const& j) override
{
wasm_engine_t* engine = getGlobalEngine();
if (!engine)
return {false, 0, "Could not obtain Wasmtime engine"};
// Ensure the module's memory is exported so that bridgeFn can access it
// via wasmtime_caller_export_get("memory"). Hook modules often define
// their own memory section without a corresponding export entry.
auto patched = ensureMemoryExported(wasm, len);
// ── Compile module ─────────────────────────────────────────────────
wasmtime_module_t* mod = nullptr;
{
wasmtime_error_t* err = wasmtime_module_new(
engine,
patched.data(),
patched.size(),
&mod);
if (auto msg = wasmtimeError(err))
return {false, 0, "Wasmtime compile failed: " + *msg};
}
// ── Create store + context ─────────────────────────────────────────
wasmtime_store_t* store = wasmtime_store_new(engine, nullptr, nullptr);
if (!store)
{
wasmtime_module_delete(mod);
return {false, 0, "Could not create Wasmtime store"};
}
wasmtime_context_t* storeCtx = wasmtime_store_context(store);
// Set initial fuel (consensus-fixed)
{
wasmtime_error_t* err =
wasmtime_context_set_fuel(storeCtx, kWasmtimeInitialFuel);
if (auto msg = wasmtimeError(err))
{
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Could not set fuel: " + *msg};
}
}
// ── Per-execution termination state ───────────────────────────────
ExecState execState;
// ── Build bridge data (must stay stable in memory) ─────────────────
std::vector<BridgeData> bridgeData;
bridgeData.reserve(imports.size());
// ── Create linker ──────────────────────────────────────────────────
wasmtime_linker_t* linker = wasmtime_linker_new(engine);
static const char kEnvModule[] = "env";
static const size_t kEnvModuleLen = 3; // strlen("env")
for (auto const& decl : imports)
{
// Feature gate: skip if this API is behind a disabled Amendment
if (decl.featureGate && !(*decl.featureGate).isZero() &&
!rules.enabled(*decl.featureGate))
continue;
bridgeData.push_back({&decl, &ctx, &execState});
wasm_functype_t* ft = buildFuncType(decl.params, decl.result);
// NOTE: wasmtime_linker_define_func takes a raw `data` pointer
// but does NOT call the finalizer per-call (only when the linker
// is deleted or shadowed). Since bridgeData is alive for the
// duration of the execution, this is safe.
wasmtime_error_t* err = wasmtime_linker_define_func(
linker,
kEnvModule,
kEnvModuleLen,
decl.name,
std::strlen(decl.name),
ft,
bridgeFn,
&bridgeData.back(),
nullptr);
wasm_functype_delete(ft);
if (auto msg = wasmtimeError(err))
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Linker define_func failed: " + *msg};
}
}
// ── Define the "table" import (funcref, min=10, max=20) ────────────
{
wasm_limits_t tableLimits = {10, 20};
wasm_tabletype_t* tt =
wasm_tabletype_new(wasm_valtype_new(WASM_FUNCREF), &tableLimits);
wasmtime_table_t tbl;
wasmtime_val_t initVal;
initVal.kind = WASMTIME_FUNCREF;
wasmtime_funcref_set_null(&initVal.of.funcref);
wasmtime_error_t* err =
wasmtime_table_new(storeCtx, tt, &initVal, &tbl);
wasm_tabletype_delete(tt);
if (auto msg = wasmtimeError(err))
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Table creation failed: " + *msg};
}
wasmtime_extern_t ext;
ext.kind = WASMTIME_EXTERN_TABLE;
ext.of.table = tbl;
err = wasmtime_linker_define(
linker,
storeCtx,
kEnvModule,
kEnvModuleLen,
"table",
5,
&ext);
if (auto msg = wasmtimeError(err))
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Linker define table failed: " + *msg};
}
}
// ── Define the "memory" import (min=1, max=1 pages) ───────────────
// This is provided for modules that import their memory from "env".
// Modules with own memory sections (Section 5) ignore this definition
// and use their own memory instead (which ensureMemoryExported() will
// have made accessible via a "memory" export).
{
wasm_memorytype_t* mt;
{
wasmtime_error_t* err = wasmtime_memorytype_new(
1, // min pages
true, // max_present
1, // max pages
false, // is_64
false, // shared
0, // page_size_log2 (0 = default 64KiB)
&mt);
if (auto msg = wasmtimeError(err))
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Memory type creation failed: " + *msg};
}
}
wasmtime_memory_t mem;
wasmtime_error_t* err = wasmtime_memory_new(storeCtx, mt, &mem);
wasm_memorytype_delete(mt);
if (auto msg = wasmtimeError(err))
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Memory creation failed: " + *msg};
}
wasmtime_extern_t ext;
ext.kind = WASMTIME_EXTERN_MEMORY;
ext.of.memory = mem;
err = wasmtime_linker_define(
linker,
storeCtx,
kEnvModule,
kEnvModuleLen,
"memory",
6,
&ext);
if (auto msg = wasmtimeError(err))
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Linker define memory failed: " + *msg};
}
}
JLOG(j.trace()) << "HookInfo[" << ctx.result.account << "-"
<< ctx.result.otxnAccount
<< "]: creating wasmtime instance";
// ── Instantiate ───────────────────────────────────────────────────
wasmtime_instance_t instance;
{
wasm_trap_t* trap = nullptr;
wasmtime_error_t* err = wasmtime_linker_instantiate(
linker, storeCtx, mod, &instance, &trap);
if (auto msg = wasmtimeError(err))
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Instantiation error: " + *msg};
}
if (trap)
{
auto msg = wasmtimeTrap(trap);
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {
false,
0,
"Instantiation trap: " + (msg ? *msg : "unknown")};
}
}
// ── Look up "hook" or "cbak" export ───────────────────────────────
const char* funcName = isCallback ? "cbak" : "hook";
size_t funcNameLen = isCallback ? 4 : 4;
wasmtime_extern_t funcExtern;
if (!wasmtime_instance_export_get(
storeCtx,
&instance,
funcName,
funcNameLen,
&funcExtern) ||
funcExtern.kind != WASMTIME_EXTERN_FUNC)
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {
false,
0,
std::string("WASM export '") + funcName + "' not found"};
}
// ── Call hook/cbak with wasmParam ──────────────────────────────────
wasmtime_val_t callArgs[1];
callArgs[0].kind = WASMTIME_I32;
callArgs[0].of.i32 = (int32_t)wasmParam;
wasmtime_val_t callResults[1];
wasm_trap_t* callTrap = nullptr;
wasmtime_error_t* callErr = wasmtime_func_call(
storeCtx,
&funcExtern.of.func,
callArgs,
1,
callResults,
1,
&callTrap);
// Capture fuel consumed before cleaning up
uint64_t fuelRemaining = 0;
wasmtime_context_get_fuel(storeCtx, &fuelRemaining);
uint64_t instructionCount = 0;
if (kWasmtimeInitialFuel >= fuelRemaining)
instructionCount = kWasmtimeInitialFuel - fuelRemaining;
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
// ── Interpret result ───────────────────────────────────────────────
//
// When a host callback returns a wasm_trap_t* to signal accept() or
// rollback(), Wasmtime propagates that as a wasmtime_error_t (with the
// trap embedded as the cause) rather than via the wasm_trap_t** output.
// We therefore check execState.terminated FIRST — before inspecting
// callErr or callTrap — so that clean hook terminations (accept/rollback)
// are always reported as ok=true regardless of which output pointer
// Wasmtime chose to use.
if (execState.terminated)
{
// Hook called accept() or rollback(): clean termination.
// Free any error/trap resources Wasmtime may have allocated.
if (callErr)
wasmtime_error_delete(callErr);
if (callTrap)
wasm_trap_delete(callTrap);
return {true, instructionCount, {}};
}
// Programmer error (wrong arg types, mismatched store, etc.)
if (callErr)
{
auto msg = wasmtimeError(callErr);
return {false, instructionCount, "WASM call error: " + (msg ? *msg : "")};
}
if (callTrap)
{
// Check for out-of-fuel trap
wasmtime_trap_code_t code = 0;
if (wasmtime_trap_code(callTrap, &code) &&
code == WASMTIME_TRAP_CODE_OUT_OF_FUEL)
{
wasm_trap_delete(callTrap);
return {false, instructionCount, "WASM out of fuel"};
}
auto msg = wasmtimeTrap(callTrap);
return {
false,
instructionCount,
"WASM trap: " + (msg ? *msg : "unknown trap")};
}
return {true, instructionCount, {}};
}
};
} // anonymous namespace
// ── Factory ──────────────────────────────────────────────────────────────────
std::unique_ptr<IWasmEngine>
makeWasmtimeEngine()
{
return std::make_unique<WasmtimeEngineImpl>();
}
} // namespace hook

View File

@@ -0,0 +1,14 @@
#ifndef RIPPLE_APP_HOOK_DETAIL_WASMTIMEENGINE_H_INCLUDED
#define RIPPLE_APP_HOOK_DETAIL_WASMTIMEENGINE_H_INCLUDED
#include <xrpld/app/hook/detail/WasmEngine.h>
#include <memory>
namespace hook {
std::unique_ptr<IWasmEngine>
makeWasmtimeEngine();
} // namespace hook
#endif // RIPPLE_APP_HOOK_DETAIL_WASMTIMEENGINE_H_INCLUDED

View File

@@ -19,7 +19,9 @@
#include <string>
#include <utility>
#include <vector>
#include <wasmedge/wasmedge.h>
#include <xrpld/app/hook/detail/WasmEngine.h>
#include <xrpld/app/hook/detail/WasmEdgeEngine.h>
#include <xrpld/app/hook/detail/WasmtimeEngine.h>
using namespace ripple;
@@ -1082,10 +1084,25 @@ hook::apply(
auto const& j = applyCtx.app.journal("View");
HookExecutor executor{hookCtx};
executor.executeWasm(
wasm.data(), (size_t)wasm.size(), isCallback, wasmParam, j);
auto engine = applyCtx.view().rules().enabled(featureWasmtimeEngine)
? makeWasmtimeEngine()
: makeWasmEdgeEngine();
auto const engineResult = engine->execute(
wasm.data(),
(size_t)wasm.size(),
isCallback,
wasmParam,
hookCtx,
hookHostFunctionDecls(),
applyCtx.view().rules(),
j);
hookCtx.result.instructionCount = engineResult.instructionCount;
if (!engineResult.ok)
{
hookCtx.result.exitType = hook_api::ExitType::WASM_ERROR;
JLOG(j.warn()) << "HookError[" << HC_ACC() << "]: "
<< engineResult.error.value_or("unknown");
}
JLOG(j.trace()) << "HookInfo[" << HC_ACC() << "]: "
<< (hookCtx.result.exitType == hook_api::ExitType::ROLLBACK
@@ -1256,7 +1273,7 @@ DEFINE_HOOK_FUNCTION(
{
return state_foreign_set(
hookCtx,
frameCtx,
mem,
read_ptr,
read_len,
kread_ptr,
@@ -1634,7 +1651,7 @@ DEFINE_HOOK_FUNCTION(
{
return state_foreign(
hookCtx,
frameCtx,
mem,
write_ptr,
write_len,
kread_ptr,
@@ -3196,7 +3213,7 @@ DEFINE_HOOK_FUNCTION(
// proxy only no setup or teardown
auto ret = sto_emplace(
hookCtx,
frameCtx,
mem,
write_ptr,
write_len,
read_ptr,

View File

@@ -18,6 +18,8 @@
//==============================================================================
#include <xrpld/app/hook/applyHook.h>
#include <xrpld/app/hook/detail/WasmEdgeEngine.h>
#include <xrpld/app/hook/detail/WasmtimeEngine.h>
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/AmendmentTable.h>
@@ -637,8 +639,11 @@ Change::activateXahauGenesis()
return;
}
auto wasmValidator = ctx_.view().rules().enabled(featureWasmtimeEngine)
? hook::makeWasmtimeEngine()
: hook::makeWasmEdgeEngine();
std::optional<std::string> result2 =
hook::HookExecutor::validateWasm(
wasmValidator->validate(
wasmBytes.data(), (size_t)wasmBytes.size());
if (result2)

View File

@@ -48,7 +48,9 @@
#include <utility>
#include <variant>
#include <vector>
#include <wasmedge/wasmedge.h>
#include <xrpld/app/hook/detail/WasmEngine.h>
#include <xrpld/app/hook/detail/WasmEdgeEngine.h>
#include <xrpld/app/hook/detail/WasmtimeEngine.h>
#define DEBUG_GUARD_CHECK 1
#define HS_ACC() \
@@ -592,8 +594,11 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
<< "]: Trying to wasm instantiate proposed hook "
<< "size = " << hook.size();
auto wasmValidator = ctx.rules.enabled(featureWasmtimeEngine)
? hook::makeWasmtimeEngine()
: hook::makeWasmEdgeEngine();
std::optional<std::string> result2 =
hook::HookExecutor::validateWasm(
wasmValidator->validate(
hook.data(), (size_t)hook.size());
if (result2)