Compare commits

..

4 Commits

Author SHA1 Message Date
tequ
132a3a8520 Merge branch 'dev' into wasmtime-engine 2026-07-06 14:04:03 +09:00
Richard Holland
bb244ef772 put release builds into a candidate folder to prevent auto-update scripts running before smoke tests (#761) 2026-06-21 12:12:43 +10: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
30 changed files with 1658 additions and 2028 deletions

View File

@@ -77,11 +77,6 @@ test.ledger > xrpld.app
test.ledger > xrpld.core
test.ledger > xrpld.ledger
test.ledger > xrpl.protocol
test.net > test.toplevel
test.net > xrpl.basics
test.net > xrpld.core
test.net > xrpld.net
test.net > xrpl.json
test.nodestore > test.jtx
test.nodestore > test.toplevel
test.nodestore > test.unit_test

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

@@ -95,8 +95,16 @@ if [[ "$4" == "" ]]; then
echo "Non GH, local building, no Action runner magic"
else
# GH Action, runner
cp /io/release-build/xahaud /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
if [[ "$(git rev-parse --abbrev-ref HEAD)" == "release" ]]; then
echo "building on the release branch... placing it in builds/candidate"
mkdir /data/builds/candidate
cp /io/release-build/xahaud /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
else
echo "building non-release branch, placing it in builds root"
cp /io/release-build/xahaud /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
fi
echo "Published build to: http://build.xahau.tech/"
echo $(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
fi

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,351 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <xrpld/core/Job.h>
#include <xrpld/core/JobQueue.h>
#include <xrpld/net/RPCSub.h>
#include <xrpl/json/json_value.h>
#include <boost/asio.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <atomic>
#include <chrono>
#include <memory>
#include <string>
#include <thread>
namespace ripple {
namespace test {
// Minimal HTTP endpoint that counts received webhook POSTs and replies
// with a configurable status. Responses are EOF-delimited (no
// Content-Length) and the socket is closed right after writing — the
// exact shape that triggered the original handleData EOF-completion
// leak. So these tests exercise RPCSub flow control AND the HTTPClient
// EOF fix end to end: if either regressed, delivery would stall and the
// expected count would never be reached within the timeout.
class MockWebhookEndpoint
{
boost::asio::io_service ios_;
std::unique_ptr<boost::asio::io_service::work> work_;
boost::asio::ip::tcp::acceptor acceptor_;
std::thread thread_;
unsigned short port_;
std::atomic<int> received_{0};
std::atomic<int> status_{200};
std::atomic<int> delayMs_{0};
public:
MockWebhookEndpoint()
: work_(std::make_unique<boost::asio::io_service::work>(ios_))
, acceptor_(
ios_,
boost::asio::ip::tcp::endpoint(
boost::asio::ip::address::from_string("127.0.0.1"),
0))
{
port_ = acceptor_.local_endpoint().port();
accept();
thread_ = std::thread([this] { ios_.run(); });
}
~MockWebhookEndpoint()
{
work_.reset();
boost::system::error_code ec;
acceptor_.close(ec);
ios_.stop();
if (thread_.joinable())
thread_.join();
}
unsigned short
port() const
{
return port_;
}
int
received() const
{
return received_;
}
void
setStatus(int s)
{
status_ = s;
}
// Delay each reply so delivery is deterministically slower than the
// microsecond-fast enqueue loop — keeps the deque full for the
// queue-cap drop test regardless of scheduling.
void
setResponseDelay(int ms)
{
delayMs_ = ms;
}
private:
void
accept()
{
auto sock = std::make_shared<boost::asio::ip::tcp::socket>(ios_);
acceptor_.async_accept(*sock, [this, sock](auto ec) {
if (ec)
return;
handle(sock);
accept();
});
}
void
handle(std::shared_ptr<boost::asio::ip::tcp::socket> sock)
{
auto buf = std::make_shared<boost::asio::streambuf>();
boost::asio::async_read_until(
*sock, *buf, "\r\n\r\n", [this, sock, buf](auto ec, std::size_t) {
if (ec)
return;
++received_;
auto const delay = delayMs_.load();
if (delay > 0)
{
auto timer =
std::make_shared<boost::asio::steady_timer>(ios_);
timer->expires_from_now(std::chrono::milliseconds(delay));
timer->async_wait(
[this, sock, timer](auto) { reply(sock); });
}
else
{
reply(sock);
}
});
}
void
reply(std::shared_ptr<boost::asio::ip::tcp::socket> sock)
{
// EOF-delimited reply: no Content-Length, close after writing.
// This is the realistic failing-webhook shape.
auto resp = std::make_shared<std::string>(
"HTTP/1.0 " + std::to_string(status_.load()) +
" Reply\r\n\r\n{\"result\":{}}");
boost::asio::async_write(
*sock, boost::asio::buffer(*resp), [sock, resp](auto, std::size_t) {
boost::system::error_code ig;
sock->shutdown(boost::asio::ip::tcp::socket::shutdown_both, ig);
sock->close(ig);
});
}
};
//------------------------------------------------------------------------------
class RPCSub_test : public beast::unit_test::suite
{
// Generous ceiling: the instrumented Debug (coverage) build is much
// slower than Release, so timeouts are sized for that, not Release.
template <class Cond>
bool
waitFor(Cond cond, std::chrono::seconds timeout = std::chrono::seconds{30})
{
auto const deadline = std::chrono::steady_clock::now() + timeout;
while (!cond() && std::chrono::steady_clock::now() < deadline)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
return cond();
}
std::shared_ptr<RPCSub>
makeSub(
jtx::Env& env,
MockWebhookEndpoint& ep,
std::size_t maxQueueSize = 16384)
{
return make_RPCSub(
env.app().getOPs(),
env.app().getJobQueue(),
"http://127.0.0.1:" + std::to_string(ep.port()) + "/",
"",
"",
env.app().logs(),
maxQueueSize);
}
// True once no RPCSub sending job is queued or running. sendThread
// captures a raw `this`, so the RPCSub must not be destroyed while a
// job is still in flight — wait on this before letting the sub die.
bool
sendingIdle(jtx::Env& env)
{
return env.app().getJobQueue().getJobCountTotal(jtCLIENT_SUBSCRIBE) ==
0;
}
// Wait for all events to reach the endpoint AND the sending job to
// finish, so the sub can be torn down without racing sendThread.
void
drainAndSettle(jtx::Env& env, MockWebhookEndpoint& ep, int expected)
{
bool const delivered =
waitFor([&] { return ep.received() >= expected; });
bool const idle = waitFor([&] { return sendingIdle(env); });
log << " drainAndSettle: received=" << ep.received() << "/" << expected
<< " idle=" << idle << std::endl;
BEAST_EXPECT(delivered);
BEAST_EXPECT(idle);
}
void
send(std::shared_ptr<RPCSub> const& sub, int n)
{
Json::Value ev(Json::objectValue);
ev["n"] = n;
sub->send(ev, false);
}
void
testDelivery()
{
testcase("Webhook events are delivered");
using namespace jtx;
Env env{*this};
MockWebhookEndpoint ep;
static constexpr int N = 10;
{
auto sub = makeSub(env, ep);
for (int i = 0; i < N; ++i)
send(sub, i);
drainAndSettle(env, ep, N);
}
BEAST_EXPECT(ep.received() == N);
}
void
testErrorsDoNotStall()
{
testcase("Delivery continues when endpoint returns HTTP 500");
// The original bug (xrpld #6341): an endpoint returning errors
// without Content-Length never completed, stalling delivery to
// ALL subscribers. Here every response is a 500 with no
// Content-Length (EOF-delimited) — all N must still arrive.
using namespace jtx;
Env env{*this};
MockWebhookEndpoint ep;
ep.setStatus(500);
static constexpr int N = 10;
{
auto sub = makeSub(env, ep);
for (int i = 0; i < N; ++i)
send(sub, i);
drainAndSettle(env, ep, N);
}
BEAST_EXPECT(ep.received() == N);
}
void
testRestartAfterDrain()
{
testcase("Sending restarts after the queue drains");
// After a batch drains, sendThread clears mSending and returns.
// A later send() must start a fresh sending job; if mSending were
// left set (the #6341 failure mode) the second burst would never
// be delivered.
using namespace jtx;
Env env{*this};
MockWebhookEndpoint ep;
{
auto sub = makeSub(env, ep);
// First burst, then wait for the sending job to fully drain
// and exit (mSending cleared) — deterministically, not via a
// sleep.
for (int i = 0; i < 5; ++i)
send(sub, i);
drainAndSettle(env, ep, 5);
// Second burst must start a fresh sending job.
for (int i = 5; i < 10; ++i)
send(sub, i);
drainAndSettle(env, ep, 10);
}
BEAST_EXPECT(ep.received() == 10);
}
void
testQueueCapDrops()
{
testcase("Events past the queue cap are dropped");
// With a tiny cap, pushing far more events than delivery can keep
// up with forces send() down the drop path: enqueue is microsecond
// -fast while each (delayed) HTTP delivery is a full round-trip, so
// the deque sits at the cap and excess events are dropped. The
// delay makes "delivery slower than enqueue" hold regardless of
// scheduling, so this isn't timing-dependent. We just need some
// delivered (cap works) and some dropped (drop path exercised).
using namespace jtx;
Env env{*this};
MockWebhookEndpoint ep;
ep.setResponseDelay(50);
static constexpr int pushed = 50;
{
auto sub = makeSub(env, ep, /*maxQueueSize*/ 2);
for (int i = 0; i < pushed; ++i)
send(sub, i);
BEAST_EXPECT(waitFor([&] { return sendingIdle(env); }));
}
log << " queue cap: received " << ep.received() << "/" << pushed
<< std::endl;
BEAST_EXPECT(ep.received() > 0);
BEAST_EXPECT(ep.received() < pushed);
}
public:
void
run() override
{
testDelivery();
testErrorsDoNotStall();
testRestartAfterDrain();
testQueueCapDrops();
}
};
BEAST_DEFINE_TESTSUITE(RPCSub, net, ripple);
} // namespace test
} // namespace ripple

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)

View File

@@ -22,6 +22,7 @@
#include <xrpld/core/JobQueue.h>
#include <xrpld/net/InfoSub.h>
#include <boost/asio/io_service.hpp>
namespace ripple {
@@ -38,17 +39,16 @@ protected:
explicit RPCSub(InfoSub::Source& source);
};
// VFALCO Why is the io_service needed?
std::shared_ptr<RPCSub>
make_RPCSub(
InfoSub::Source& source,
boost::asio::io_service& io_service,
JobQueue& jobQueue,
std::string const& strUrl,
std::string const& strUsername,
std::string const& strPassword,
Logs& logs,
// Max events buffered before new ones are dropped. Configurable so
// tests can exercise the drop path without queueing the full default.
std::size_t maxQueueSize = 16384);
Logs& logs);
} // namespace ripple

View File

@@ -122,20 +122,12 @@ public:
mComplete = complete;
mTimeout = timeout;
// Bind a non-owning `this` (not shared_from_this()) into mBuild.
// mBuild is a member, so capturing a shared_ptr to self here would
// form a reference cycle (this -> mBuild -> shared_ptr<this>) that
// never breaks, leaking the object and its socket FD after the
// request completes. mBuild is only ever invoked from
// handleRequest(), which always runs inside an async handler that
// already holds a shared_from_this(), so the object is guaranteed
// alive whenever mBuild fires — a raw `this` is safe.
request(
bSSL,
deqSites,
std::bind(
&HTTPClientImp::makeGet,
this,
shared_from_this(),
strPath,
std::placeholders::_1,
std::placeholders::_2),
@@ -401,12 +393,8 @@ public:
if (boost::regex_match(strHeader, smMatch, reBody)) // we got some body
mBody = smMatch[1];
bool const hasContentLength =
boost::regex_match(strHeader, smMatch, reSize);
mReceivedContentLength = hasContentLength;
std::size_t const responseSize = [&] {
if (hasContentLength)
if (boost::regex_match(strHeader, smMatch, reSize))
return beast::lexicalCast<std::size_t>(
std::string(smMatch[1]), maxResponseSize_);
return maxResponseSize_;
@@ -457,24 +445,22 @@ public:
JLOG(j_.trace()) << "Read error: " << mShutdown.message();
invokeComplete(mShutdown);
return;
}
// Either the read completed normally or it ended at EOF. EOF is a
// successful completion for EOF-delimited responses, but it is an
// error when the server promised a Content-Length and closed early.
JLOG(j_.trace()) << "Complete.";
mResponse.commit(bytes_transferred);
std::string strBody{
{std::istreambuf_iterator<char>(&mResponse)},
std::istreambuf_iterator<char>()};
auto completeEc = ecResult;
if (completeEc == boost::asio::error::eof && !mReceivedContentLength)
completeEc.clear();
invokeComplete(completeEc, mStatus, mBody + strBody);
else
{
if (mShutdown)
{
JLOG(j_.trace()) << "Complete.";
}
else
{
mResponse.commit(bytes_transferred);
std::string strBody{
{std::istreambuf_iterator<char>(&mResponse)},
std::istreambuf_iterator<char>()};
invokeComplete(ecResult, mStatus, mBody + strBody);
}
}
}
// Call cancel the deadline timer and invoke the completion routine.
@@ -530,7 +516,6 @@ private:
boost::asio::streambuf mHeader;
boost::asio::streambuf mResponse;
std::string mBody;
bool mReceivedContentLength = false;
const unsigned short mPort;
std::size_t const maxResponseSize_;
int mStatus;

View File

@@ -1585,10 +1585,6 @@ struct RPCCallImp
// callbackFuncP.
// Receive reply
if (ecResult)
Throw<std::runtime_error>(
"RPC transport error: " + ecResult.message());
if (strData.empty())
Throw<std::runtime_error>(
"no response from server. Please "
@@ -1752,7 +1748,6 @@ rpcClient(
}
{
//@@start blocking-request
boost::asio::io_service isService;
RPCCall::fromNetwork(
isService,
@@ -1776,7 +1771,6 @@ rpcClient(
headers);
isService.run(); // This blocks until there are no more
// outstanding async calls.
//@@end blocking-request
}
if (jvOutput.isMember("result"))
{
@@ -1887,21 +1881,15 @@ fromNetwork(
// Send request
// Number of bytes to try to receive if no Content-Length header is
// received. Webhook event deliveries ("event") ignore the response
// body, so a missing Content-Length must not pre-allocate the full
// 256MB RPC reply budget per in-flight delivery (maxInFlight can be
// 32 -> 8GB). Cap those small; genuine RPC replies (CLI) keep the
// large budget.
auto const RPC_REPLY_MAX_BYTES =
(strMethod == "event") ? megabytes(1) : megabytes(256);
// Number of bytes to try to receive if no
// Content-Length header received
constexpr auto RPC_REPLY_MAX_BYTES = megabytes(256);
using namespace std::chrono_literals;
// auto constexpr RPC_NOTIFY = 10min; // Wietse: lolwut 10 minutes for one
// HTTP call?
auto constexpr RPC_NOTIFY = 30s;
//@@start async-request
HTTPClient::request(
bSSL,
io_service,
@@ -1926,7 +1914,6 @@ fromNetwork(
std::placeholders::_3,
j),
j);
//@@end async-request
}
} // namespace RPCCall

View File

@@ -24,30 +24,29 @@
#include <xrpl/basics/contract.h>
#include <xrpl/json/to_string.h>
#include <deque>
#include <memory>
namespace ripple {
// Subscription object for JSON-RPC
class RPCSubImp : public RPCSub, public std::enable_shared_from_this<RPCSubImp>
class RPCSubImp : public RPCSub
{
public:
RPCSubImp(
InfoSub::Source& source,
boost::asio::io_service& io_service,
JobQueue& jobQueue,
std::string const& strUrl,
std::string const& strUsername,
std::string const& strPassword,
Logs& logs,
std::size_t maxQueueSize)
Logs& logs)
: RPCSub(source)
, m_io_service(io_service)
, m_jobQueue(jobQueue)
, mUrl(strUrl)
, mSSL(false)
, mUsername(strUsername)
, mPassword(strPassword)
, mSending(false)
, maxQueueSize_(maxQueueSize)
, j_(logs.journal("RPCSub"))
, logs_(logs)
{
@@ -79,26 +78,14 @@ public:
{
std::lock_guard sl(mLock);
if (mDeque.size() >= maxQueueSize_)
{
// Always advance mSeq so consumers can detect the gap, but
// rate-limit the log: a hopelessly behind endpoint drops on
// every send() and would otherwise flood the log. Warn on
// the first drop of a run and then once per dropLogInterval.
if (mDropped++ % dropLogInterval == 0)
{
JLOG(j_.warn())
<< "RPCCall::fromNetwork drop: queue full ("
<< mDeque.size() << "), seq=" << mSeq
<< ", endpoint=" << mIp << ", dropped=" << mDropped;
}
++mSeq;
return;
}
// Endpoint caught up enough to accept again; reset so the next
// overflow burst logs its first drop immediately.
mDropped = 0;
// Wietse: we're not going to limit this, this is admin-port only, scale
// accordingly Dropping events just like this results in inconsistent
// data on the receiving end if (mDeque.size() >= eventQueueMax)
// {
// // Drop the previous event.
// JLOG(j_.warn()) << "RPCCall::fromNetwork drop";
// mDeque.pop_back();
// }
auto jm = broadcast ? j_.debug() : j_.info();
JLOG(jm) << "RPCCall::fromNetwork push: " << jvObj;
@@ -110,7 +97,10 @@ public:
// Start a sending thread.
JLOG(j_.info()) << "RPCCall::fromNetwork start";
startSendingJob();
mSending = m_jobQueue.addJob(
jtCLIENT_SUBSCRIBE, "RPCSub::sendThread", [this]() {
sendThread();
});
}
}
@@ -131,66 +121,48 @@ public:
}
private:
// Maximum concurrent HTTP deliveries per batch. Bounds file
// descriptor usage while still allowing parallel delivery to
// capable endpoints. With a 1024 FD process limit shared across
// peers, clients, and the node store, 32 per subscriber is a
// meaningful but survivable chunk even with multiple subscribers.
static constexpr int maxInFlight = 32;
// Log one drop warning per this many drops while the queue stays
// full, to avoid flooding the log on a persistently behind endpoint.
static constexpr std::size_t dropLogInterval = 1000;
// Schedule a sending job. Must be called under mLock. The job holds a
// weak_ptr and re-locks it on entry, so the RPCSub is kept alive for
// the duration of the batch even if it is unsubscribed (and would
// otherwise be destroyed) concurrently — sendThread dereferences this
// only via that strong ref. mDeque events are delivered until the sub
// is gone, after which weak.lock() fails and the job is a no-op.
void
startSendingJob()
{
std::weak_ptr<RPCSubImp> weak = weak_from_this();
mSending = m_jobQueue.addJob(
jtCLIENT_SUBSCRIBE, "RPCSub::sendThread", [weak]() {
if (auto self = weak.lock())
self->sendThread();
});
}
// XXX Could probably create a bunch of send jobs in a single get of the
// lock.
void
sendThread()
{
// Process exactly ONE batch per job, then re-queue if more events
// remain, rather than draining the whole backlog in a single job.
// A local io_service's .run() blocks this worker thread for the
// batch (up to the per-request timeout), so re-queueing between
// batches keeps one slow/hung subscriber from monopolising a
// job-queue worker and starving consensus/ledger/RPC work.
//
// mSending must be cleared under the lock on every non-requeue
// exit path; if it ever stays set without a job in flight, send()
// sees mSending == true and never restarts us, stalling the queue
// forever — the original bug (xrpld issue #6341).
boost::asio::io_service io_service;
int dispatched = 0;
Json::Value jvEvent;
bool bSend;
try
do
{
{
// Obtain the lock to manipulate the queue and change sending.
std::lock_guard sl(mLock);
while (!mDeque.empty() && dispatched < maxInFlight)
if (mDeque.empty())
{
mSending = false;
bSend = false;
}
else
{
auto const [seq, env] = mDeque.front();
mDeque.pop_front();
Json::Value jvEvent = env;
jvEvent = env;
jvEvent["seq"] = seq;
bSend = true;
}
}
// Send outside of the lock.
if (bSend)
{
// XXX Might not need this in a try.
try
{
JLOG(j_.info()) << "RPCCall::fromNetwork: " << mIp;
RPCCall::fromNetwork(
io_service,
m_io_service,
mIp,
mPort,
mUsername,
@@ -201,51 +173,21 @@ private:
mSSL,
true,
logs_);
++dispatched;
}
catch (const std::exception& e)
{
JLOG(j_.info())
<< "RPCCall::fromNetwork exception: " << e.what();
}
}
// dispatched is always > 0 here (send() only starts a job
// after enqueuing, and the re-queue below only fires with a
// non-empty deque), but guard anyway so an empty batch can't
// log/spin — it falls straight through to clear mSending.
if (dispatched > 0)
{
JLOG(j_.info()) << "RPCCall::fromNetwork: " << mIp
<< " dispatching " << dispatched << " events";
io_service.run();
}
}
catch (std::exception const& e)
{
// Bail rather than re-queue: a persistently failing endpoint
// would otherwise spin the job queue. mSending is reset so the
// next send() restarts delivery.
JLOG(j_.warn()) << "RPCSub::sendThread exception: " << e.what();
std::lock_guard sl(mLock);
mSending = false;
return;
}
catch (...)
{
JLOG(j_.warn()) << "RPCSub::sendThread unknown exception";
std::lock_guard sl(mLock);
mSending = false;
return;
}
// Batch complete: re-queue for the next one (mSending stays set)
// or clear mSending if the queue drained — both under the lock to
// avoid a lost-wakeup race with send().
std::lock_guard sl(mLock);
if (mDeque.empty())
mSending = false;
else
startSendingJob();
} while (bSend);
}
private:
// Wietse: we're not going to limit this, this is admin-port only, scale
// accordingly enum { eventQueueMax = 32 };
boost::asio::io_service& m_io_service;
JobQueue& m_jobQueue;
std::string mUrl;
@@ -258,15 +200,8 @@ private:
int mSeq; // Next id to allocate.
std::size_t mDropped = 0; // Consecutive drops while queue is full.
bool mSending; // Sending threead is active.
// Maximum queued events before dropping. The default (16384) is a
// ~10-minute buffer at 100+ events/ledger; a hopelessly behind
// endpoint trips it and consumers detect the gap via the seq field.
std::size_t const maxQueueSize_;
std::deque<std::pair<int, Json::Value>> mDeque;
beast::Journal const j_;
@@ -282,21 +217,21 @@ RPCSub::RPCSub(InfoSub::Source& source) : InfoSub(source, Consumer())
std::shared_ptr<RPCSub>
make_RPCSub(
InfoSub::Source& source,
boost::asio::io_service& io_service,
JobQueue& jobQueue,
std::string const& strUrl,
std::string const& strUsername,
std::string const& strPassword,
Logs& logs,
std::size_t maxQueueSize)
Logs& logs)
{
return std::make_shared<RPCSubImp>(
std::ref(source),
std::ref(io_service),
std::ref(jobQueue),
strUrl,
strUsername,
strPassword,
logs,
maxQueueSize);
logs);
}
} // namespace ripple

View File

@@ -76,6 +76,7 @@ doSubscribe(RPC::JsonContext& context)
{
auto rspSub = make_RPCSub(
context.app.getOPs(),
context.app.getIOService(),
context.app.getJobQueue(),
strUrl,
strUsername,