Compare commits

..

2 Commits

Author SHA1 Message Date
Nicholas Dudfield
3754109e4b test(server_definitions): adapt amendment tests to ledger_enabled/cfg_forced split
jtx's Env enables amendments by inserting them into config.features (the
same presets mechanism as the [features] stanza), so:

- testNoParams / testSomeEnabled assert on `ledger_enabled` (the canonical
  on-ledger flag) instead of `enabled`, which is now the effective value
  (ledger_enabled || cfg_forced).
- testConfigForced uses a single-feature bitset so exactly one amendment is
  config-forced, and verifies it reports enabled:true, ledger_enabled:false,
  cfg_forced:true while the rest report cfg_forced:false and
  enabled == ledger_enabled.

ripple.rpc.ServerDefinitions: 6 cases, 3435 tests, 0 failures.
2026-06-26 14:20:03 +07:00
Nicholas Dudfield
9d125f4d19 feat(server_definitions): distinguish config-forced from ledger-enabled amendments
`server_definitions` sourced each amendment's `enabled` straight from the
on-ledger Amendments object, so amendments force-activated via the
`[features]` config stanza (live in the Rules / tx processing, but never
voted on-ledger) were reported as not-enabled. That makes the output
misleading on any node that uses `[features]` (notably testnets).

Split per-amendment activation into its two real sources and make
`enabled` reflect what the server actually applies:

    enabled        = ledger_enabled || cfg_forced
    ledger_enabled = recorded in the on-ledger Amendments object (canonical)
    cfg_forced     = forced via the [features] config stanza (node-local)

Additive JSON fields. Adds jss::ledger_enabled, jss::cfg_forced and a
ServerDefinitions test covering the config-forced case.
2026-06-26 13:55:48 +07:00
24 changed files with 422 additions and 1572 deletions

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,6 @@ class Xrpl(ConanFile):
'unity': [True, False],
'xrpld': [True, False],
'with_wasmedge': [True, False],
'with_wasmtime': [True, False],
'tool_requires_b2': [True, False],
}
@@ -54,7 +53,6 @@ class Xrpl(ConanFile):
'unity': False,
'xrpld': False,
'with_wasmedge': True,
'with_wasmtime': True,
'tool_requires_b2': False,
'date/*:header_only': False,
@@ -123,8 +121,6 @@ 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:

View File

@@ -1,25 +0,0 @@
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: ""

View File

@@ -1,87 +0,0 @@
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,8 +10,6 @@
* were then used.
*/
#include <xrpl/hook/WasmTypes.h>
#define LPAREN (
#define RPAREN )
#define COMMA ,
@@ -70,90 +68,104 @@
#define SEP_uint64_t LPAREN uint64_t COMMA
#define SEP_int64_t LPAREN int64_t COMMA
// 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 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++])
#define VAR_ASSIGN(T, V) T V = CAT(VAL_##T)
// 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_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)
#define RET_ASSIGN(T, return_code) CAT2(RET_, T(return_code))
// 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 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
#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, \
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);
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;
// DEFINE_HOOK_FUNCTION: defines the engine-agnostic wrapper + the impl function
// body
#define DEFINE_HOOK_FUNCTION(R, F, ...) \
hook::HostCallStatus hook_api::WasmFunction##F( \
WasmEdge_Result hook_api::WasmFunction##F( \
void* data_ptr, \
hook::GuestMemory& mem, \
hook::WasmValue const* in, \
size_t /*inLen*/, \
hook::WasmValue* out, \
size_t /*outLen*/) \
const WasmEdge_CallingFrameContext* frameCtx, \
const WasmEdge_Value* in, \
WasmEdge_Value* out) \
{ \
__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, mem __VA_OPT__(COMMA STRIP_TYPES(__VA_ARGS__))); \
*hookCtx, \
*const_cast<WasmEdge_CallingFrameContext*>(frameCtx) \
__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 hook::HostCallStatus::Terminate; \
return WasmEdge_Result_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 hook::HostCallStatus::Success; \
return WasmEdge_Result_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, \
hook::GuestMemory& mem __VA_OPT__(COMMA __VA_ARGS__))
WasmEdge_CallingFrameContext const& frameCtx __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]] unsigned char* memory = mem.base; \
[[maybe_unused]] const uint64_t memory_length = mem.size; \
[[maybe_unused]] auto& api = hookCtx.api(); \
if (!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]] 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) \
return INTERNAL_ERROR;
#define HOOK_TEARDOWN() \
@@ -185,10 +197,11 @@
<< " bytes past end of wasm memory"; \
return OUT_OF_BOUNDS; \
} \
if (!mem.write( \
if (!WasmEdge_ResultOK(WasmEdge_MemoryInstanceSetData( \
memoryCtx, \
reinterpret_cast<const uint8_t*>(host_src_ptr), \
guest_dst_ptr, \
reinterpret_cast<uint8_t const*>(host_src_ptr), \
static_cast<uint64_t>(bytes_to_write))) \
bytes_to_write))) \
return INTERNAL_ERROR; \
bytes_written += bytes_to_write; \
}

View File

@@ -1,114 +0,0 @@
#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,7 +34,6 @@
// 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

@@ -293,6 +293,8 @@ JSS(effective); // out: ValidatorList
// in: UNL
JSS(elapsed_seconds);
JSS(enabled); // out: AmendmentTable
JSS(ledger_enabled); // out: ServerDefinitions (amendment on-ledger)
JSS(cfg_forced); // out: ServerDefinitions ([features] config stanza)
JSS(engine_result); // out: NetworkOPs, TransactionSign, Submit
JSS(engine_result_code); // out: NetworkOPs, TransactionSign, Submit
JSS(engine_result_message); // out: NetworkOPs, TransactionSign, Submit

View File

@@ -15202,9 +15202,8 @@ public:
using namespace test::jtx;
static FeatureBitset const all{supported_amendments()};
static std::array<FeatureBitset, 9> const feats{
static std::array<FeatureBitset, 8> const feats{
all,
all - featureWasmtimeEngine,
all - fixXahauV2,
all - fixXahauV1 - fixXahauV2,
all - fixXahauV1 - fixXahauV2 - fixNSDelete,
@@ -15475,8 +15474,7 @@ SETHOOK_TEST(3, false)
SETHOOK_TEST(4, false)
SETHOOK_TEST(5, false)
SETHOOK_TEST(6, false)
SETHOOK_TEST(7, false)
SETHOOK_TEST(8, true)
SETHOOK_TEST(7, true)
BEAST_DEFINE_TESTSUITE_PRIO(SetHook0, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook1, app, ripple, 2);
@@ -15486,7 +15484,6 @@ 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,6 +97,7 @@ 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,7 +145,8 @@ makeStubHookContext(
.executeAgainAsWeak = result.executeAgainAsWeak,
.provisionalMeta = result.provisionalMeta,
},
.emitFailure = stubHookContext.emitFailure};
.emitFailure = stubHookContext.emitFailure,
.module = nullptr};
}
// Original function - WARNING: stateMap reference may become dangling

View File

@@ -186,10 +186,13 @@ public:
bool expectObsolete =
(votes.at(feature[jss::name].asString()) ==
VoteBehavior::Obsolete);
// "enabled" is now the effective value (ledger_voted || forced);
// this default env votes nothing onto the ledger, so assert on the
// canonical on-ledger flag.
BEAST_EXPECTS(
feature.isMember(jss::enabled) &&
!feature[jss::enabled].asBool(),
feature[jss::name].asString() + " enabled");
feature.isMember(jss::ledger_enabled) &&
!feature[jss::ledger_enabled].asBool(),
feature[jss::name].asString() + " ledger_enabled");
BEAST_EXPECTS(
feature.isMember(jss::vetoed) &&
feature[jss::vetoed].isBool() == !expectObsolete &&
@@ -237,10 +240,12 @@ public:
bool expectObsolete =
(votes.at((*it)[jss::name].asString()) ==
VoteBehavior::Obsolete);
// expectEnabled reflects the on-ledger amendment table, so compare
// against ledger_enabled (enabled is now ledger_voted || forced).
BEAST_EXPECTS(
(*it).isMember(jss::enabled) &&
(*it)[jss::enabled].asBool() == expectEnabled,
(*it)[jss::name].asString() + " enabled");
(*it).isMember(jss::ledger_enabled) &&
(*it)[jss::ledger_enabled].asBool() == expectEnabled,
(*it)[jss::name].asString() + " ledger_enabled");
if (expectEnabled)
BEAST_EXPECTS(
!(*it).isMember(jss::vetoed),
@@ -360,12 +365,78 @@ public:
}
}
void
testConfigForced(FeatureBitset features)
{
testcase("Config-forced features ([features] stanza)");
using namespace test::jtx;
// jtx enables amendments by inserting them into config.features (the
// same presets mechanism as the [features] config stanza), so passing
// a single-feature bitset gives us exactly one config-forced amendment
// and votes nothing onto the ledger. server_definitions must then
// report that one as effectively enabled, distinguishing the source:
// enabled = ledger_enabled || cfg_forced
// ledger_enabled = false (never voted onto the ledger)
// cfg_forced = true (forced via config) for the one feature only
auto const forced = featurePriceOracle;
auto const forcedHex = to_string(forced);
Env env{*this, FeatureBitset(forced)};
auto jrr = env.rpc("server_definitions")[jss::result];
if (!BEAST_EXPECT(jrr.isMember(jss::features)))
return;
bool sawForced = false;
for (auto it = jrr[jss::features].begin();
it != jrr[jss::features].end();
++it)
{
auto const& f = *it;
auto const name = f[jss::name].asString();
// every entry now carries the split flags
if (!BEAST_EXPECTS(
f.isMember(jss::enabled) &&
f.isMember(jss::ledger_enabled) &&
f.isMember(jss::cfg_forced),
name + " split flags"))
return;
// nothing is enabled on-ledger in a fresh env
BEAST_EXPECTS(
!f[jss::ledger_enabled].asBool(), name + " ledger_enabled");
if (it.key().asString() == forcedHex)
{
sawForced = true;
BEAST_EXPECTS(
f[jss::cfg_forced].asBool(), name + " cfg_forced");
// ledger_enabled(false) || cfg_forced(true) == true
BEAST_EXPECTS(f[jss::enabled].asBool(), name + " enabled");
}
else
{
BEAST_EXPECTS(
!f[jss::cfg_forced].asBool(), name + " cfg_forced");
// not forced and not on-ledger => not effectively enabled
BEAST_EXPECTS(
f[jss::enabled].asBool() == f[jss::ledger_enabled].asBool(),
name + " enabled==ledger_enabled");
}
}
BEAST_EXPECT(sawForced);
}
void
testServerFeatures(FeatureBitset features)
{
testNoParams(features);
testSomeEnabled(features);
testWithMajorities(features);
testConfigForced(features);
}
void

View File

@@ -17,6 +17,7 @@
#include <queue>
#include <utility>
#include <vector>
#include <wasmedge/wasmedge.h>
namespace hook {
struct HookContext;
@@ -178,6 +179,8 @@ struct HookResult
foreignStateGrantCache; // add found grants here to avoid rechecking
};
class HookExecutor;
struct SlotEntry
{
std::shared_ptr<const ripple::STObject> storage;
@@ -214,6 +217,7 @@ 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_;
@@ -268,9 +272,231 @@ 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

@@ -1,336 +0,0 @@
#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

@@ -1,14 +0,0 @@
#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

@@ -1,51 +0,0 @@
#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

@@ -1,819 +0,0 @@
// 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

@@ -1,14 +0,0 @@
#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,9 +19,7 @@
#include <string>
#include <utility>
#include <vector>
#include <xrpld/app/hook/detail/WasmEngine.h>
#include <xrpld/app/hook/detail/WasmEdgeEngine.h>
#include <xrpld/app/hook/detail/WasmtimeEngine.h>
#include <wasmedge/wasmedge.h>
using namespace ripple;
@@ -1084,25 +1082,10 @@ hook::apply(
auto const& j = applyCtx.app.journal("View");
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");
}
HookExecutor executor{hookCtx};
executor.executeWasm(
wasm.data(), (size_t)wasm.size(), isCallback, wasmParam, j);
JLOG(j.trace()) << "HookInfo[" << HC_ACC() << "]: "
<< (hookCtx.result.exitType == hook_api::ExitType::ROLLBACK
@@ -1273,7 +1256,7 @@ DEFINE_HOOK_FUNCTION(
{
return state_foreign_set(
hookCtx,
mem,
frameCtx,
read_ptr,
read_len,
kread_ptr,
@@ -1651,7 +1634,7 @@ DEFINE_HOOK_FUNCTION(
{
return state_foreign(
hookCtx,
mem,
frameCtx,
write_ptr,
write_len,
kread_ptr,
@@ -3213,7 +3196,7 @@ DEFINE_HOOK_FUNCTION(
// proxy only no setup or teardown
auto ret = sto_emplace(
hookCtx,
mem,
frameCtx,
write_ptr,
write_len,
read_ptr,

View File

@@ -18,8 +18,6 @@
//==============================================================================
#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>
@@ -639,11 +637,8 @@ Change::activateXahauGenesis()
return;
}
auto wasmValidator = ctx_.view().rules().enabled(featureWasmtimeEngine)
? hook::makeWasmtimeEngine()
: hook::makeWasmEdgeEngine();
std::optional<std::string> result2 =
wasmValidator->validate(
hook::HookExecutor::validateWasm(
wasmBytes.data(), (size_t)wasmBytes.size());
if (result2)

View File

@@ -48,9 +48,7 @@
#include <utility>
#include <variant>
#include <vector>
#include <xrpld/app/hook/detail/WasmEngine.h>
#include <xrpld/app/hook/detail/WasmEdgeEngine.h>
#include <xrpld/app/hook/detail/WasmtimeEngine.h>
#include <wasmedge/wasmedge.h>
#define DEBUG_GUARD_CHECK 1
#define HS_ACC() \
@@ -594,11 +592,8 @@ 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 =
wasmValidator->validate(
hook::HookExecutor::validateWasm(
hook.data(), (size_t)hook.size());
if (result2)

View File

@@ -22,6 +22,7 @@
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/AmendmentTable.h>
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/core/Config.h>
#include <xrpld/rpc/detail/TransactionSign.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/json_writer.h>
@@ -545,6 +546,39 @@ doServerDefinitions(RPC::JsonContext& context)
features[to_string(h)][jss::majority] =
t.time_since_epoch().count();
// Amendment activation has two independent sources; surface both so a
// consumer isn't misled by a node that force-enables amendments:
// ledger_enabled : recorded in the on-ledger Amendments object
// (network-canonical; what the table reports as
// "enabled")
// cfg_forced : force-activated via the [features] config stanza
// (node-local; active in the Rules regardless of the
// ledger, casts no votes, never written on-ledger)
// enabled : effective for transaction processing on this
// server, i.e. ledger_enabled || cfg_forced
for (auto const& name : features.getMemberNames())
{
Json::Value& entry = features[name];
bool const ledgerEnabled = entry[jss::enabled].asBool();
entry[jss::ledger_enabled] = ledgerEnabled;
entry[jss::cfg_forced] = false;
// entry[jss::enabled] is left == ledgerEnabled here; only
// cfg_forced amendments below flip it.
}
for (auto const& h : context.app.config().features)
{
Json::Value& entry = features[to_string(h)];
if (!entry.isMember(jss::name))
{
if (auto const fname = featureToName(h); !fname.empty())
entry[jss::name] = fname;
}
if (!entry.isMember(jss::ledger_enabled))
entry[jss::ledger_enabled] = false;
entry[jss::cfg_forced] = true;
entry[jss::enabled] = true; // ledger_enabled || cfg_forced
}
lastFeatures = features;
{
const std::string out = Json::FastWriter().write(features);