This commit is contained in:
Sergey Kuznetsov
2026-08-04 16:35:55 +01:00
parent eb946d23af
commit 6c02c45cbe
39 changed files with 162 additions and 7618 deletions

View File

@@ -0,0 +1,126 @@
#pragma once
#include <nudb/detail/stream.hpp>
#include <cstddef>
#include <cstdint>
#include <type_traits>
namespace xrpl::node_store {
// This is a variant of the base128 varint format from
// google protocol buffers:
// https://developers.google.com/protocol-buffers/docs/encoding#varints
// field tag
struct Varint;
// Metafunction to return largest
// possible size of T represented as varint.
// T must be unsigned
template <class T, bool = std::is_unsigned_v<T>>
struct VarintTraits;
template <class T>
struct VarintTraits<T, true>
{
explicit VarintTraits() = default;
static constexpr std::size_t kMax = ((8 * sizeof(T)) + 6) / 7;
};
// Returns: Number of bytes consumed or 0 on error,
// if the buffer was too small or t overflowed.
//
template <class = void>
std::size_t
readVarint(void const* buf, std::size_t buflen, std::size_t& t)
{
if (buflen == 0)
return 0;
t = 0;
auto const* p = reinterpret_cast<std::uint8_t const*>(buf);
std::size_t n = 0;
while (p[n] & 0x80)
{
if (++n >= buflen)
return 0;
}
if (++n > buflen)
return 0;
// Special case for 0
if (n == 1 && *p == 0)
{
t = 0;
return 1;
}
auto const used = n;
while (n > 0)
{
--n;
auto const d = p[n];
auto const t0 = t;
t *= 127;
t += d & 0x7f;
if (t <= t0)
return 0; // overflow
}
return used;
}
template <class T>
std::size_t
sizeVarint(T v)
requires(std::is_unsigned_v<T>)
{
std::size_t n = 0;
do
{
v /= 127;
++n;
} while (v != 0);
return n;
}
template <class = void>
std::size_t
writeVarint(void* p0, std::size_t v)
{
// NOLINTNEXTLINE(misc-const-correctness)
auto* p = reinterpret_cast<std::uint8_t*>(p0);
do
{
std::uint8_t d = v % 127;
v /= 127;
if (v != 0)
d |= 0x80;
*p++ = d;
} while (v != 0);
return p - reinterpret_cast<std::uint8_t*>(p0);
}
// input stream
template <class T>
void
read(nudb::detail::istream& is, std::size_t& u)
requires(std::is_same_v<T, Varint>)
{
auto p0 = is(1);
auto p1 = p0;
while (*p1++ & 0x80)
is(1);
readVarint(p0, p1 - p0, u);
}
// output stream
template <class T>
void
write(nudb::detail::ostream& os, std::size_t t)
requires(std::is_same_v<T, Varint>)
{
writeVarint(os.data(sizeVarint(t)), t);
}
} // namespace xrpl::node_store

View File

@@ -2,7 +2,6 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
@@ -12,9 +11,6 @@
#include <cstdint>
#include <expected>
#include <functional>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
@@ -73,7 +69,6 @@ floatPowerImpl(Slice const& x, int32_t n, int32_t mode);
class HostFunctions
{
protected:
RTOptRef rt_;
beast::Journal j_;
public:
@@ -81,26 +76,6 @@ public:
{
}
void
setRT(WasmRuntimeWrapper& rt)
{
rt_ = rt;
}
void
resetRT()
{
rt_ = std::nullopt;
}
[[nodiscard]] WasmRuntimeWrapper&
getRT() const
{
if (!rt_)
Throw<std::logic_error>("Wasm runtime not set");
return rt_->get();
}
[[nodiscard]] beast::Journal
getJournal() const
{
@@ -518,6 +493,4 @@ public:
// LCOV_EXCL_STOP
};
using HFRef = std::reference_wrapper<HostFunctions>;
} // namespace xrpl

View File

@@ -1,189 +1,42 @@
# WASM Module for Programmable Escrows
This module provides WebAssembly (WASM) execution capabilities for programmable
escrows on the XRP Ledger. When an escrow is finished, the WASM code runs to
determine whether the escrow conditions are met, enabling custom programmable
logic for escrow release conditions.
For the full specification, see
WebAssembly execution for programmable escrows. When an escrow is finished, its contract
runs to decide whether the release conditions are met. Specification:
[XLS-0102: WASM VM](https://xls.xrpl.org/xls/XLS-0102-wasm-vm.html).
## Architecture
The engine itself is Rust (`crates/xrpl-wasm-vm`, over wasmi), reached through a cxx
bridge. The design docs live in [`docs/claude/wasm-vm/`](../../../../docs/claude/wasm-vm/index.md)
— read [`abi.md`](../../../../docs/claude/wasm-vm/abi.md) before adding a host function and
[`bridge.md`](../../../../docs/claude/wasm-vm/bridge.md) before changing anything that
crosses between the two languages.
The module follows a layered architecture:
## What is in this directory
```
┌─────────────────────────────────────────────────────────────┐
WasmEngine (WasmVM.h) │
│ runEscrowWasm(), preflightEscrowWasm() │
Host function registration │
├─────────────────────────────────────────────────────────────┤
WasmiEngine (WasmiVM.h) │
│ Low-level wasmi interpreter integration │
├─────────────────────────────────────────────────────────────┤
HostFuncWrapper │ HostFuncImpl │
│ C-style WASM bridges │ C++ implementations │
├─────────────────────────────────────────────────────────────┤
│ HostFunc (Interface) │
│ Abstract base class for host functions │
└─────────────────────────────────────────────────────────────┘
```
- **`WasmVM.h`** — the entry points xrpld calls: `runEscrowWasm` (execute a contract,
returning a result and its gas cost, or a `WasmTER`) and `preflightEscrowWasm` (screen a
module with no host and no execution). Both own their TER maps.
- **`HostFunc.h`** — the `HostFunctions` interface: one virtual per host function, each
defaulting to `Unimplemented`, returning `std::expected<T, HostFunctionError>`.
- **`HostFuncImpl.h`** — `WasmHostFunctionsImpl`, the implementation over an
`ApplyContext&`. Bodies are split across `HostFuncImpl*.cpp` by category.
- **`HostContext.h`** — the bridge's C++ half: an ABI-shaped, `noexcept` view of
`HostFunctions` that the engine calls back into. Nothing may unwind into Rust, so every
method routes through one `guarded()`.
- **`WasmCommon.h`** — the shared vocabulary: `HostFunctionError` (the codes a contract
sees), `Bytes`, `FieldLocator`, `WasmTER`, and `adjustWasmEndianess`, which is where the
boundary's byte order is decided.
### Key Components
## Host functions
- **`WasmVM.h` / `detail/WasmVM.cpp`** - High-level facade providing:
- `WasmEngine` singleton that wraps the underlying WASM interpreter
- `runEscrowWasm()` - Execute WASM code for escrow finish
- `preflightEscrowWasm()` - Validate WASM code during preflight
- `createWasmImport()` - Register all host functions
Grouped by what they reach: ledger information; transaction and ledger-object field access;
keylet construction; cryptography; float arithmetic; NFT queries; tracing.
- **`WasmiVM.h` / `detail/WasmiVM.cpp`** - Low-level integration with the
[wasmi](https://github.com/wasmi-labs/wasmi) WebAssembly interpreter:
- `WasmiEngine` - Manages WASM modules, instances, and execution
- Memory management and gas metering
- Function invocation and result handling
The wire names and per-call gas costs are declared in `crates/xrpl-host-functions`
one `host_functions!` block that generates the ABI trait and the spec table. That
declaration is the single source of truth; `HostFunc.h` is the C++ side of it.
- **`HostFunc.h`** - Abstract `HostFunctions` base class defining the interface
for all callable host functions. Each method returns
`std::expected<T, HostFunctionError>`.
## Entry point
- **`HostFuncImpl.h` / `detail/HostFuncImpl*.cpp`** - Concrete
`WasmHostFunctionsImpl` class that implements host functions with access to
`ApplyContext` for ledger state queries. Implementation split across files:
- `HostFuncImpl.cpp` - Core utilities (updateData, checkSignature, etc.)
- `HostFuncImplFloat.cpp` - Float/number arithmetic operations
- `HostFuncImplGetter.cpp` - Field access (transaction, ledger objects)
- `HostFuncImplKeylet.cpp` - Keylet construction functions
- `HostFuncImplLedgerHeader.cpp` - Ledger header info access
- `HostFuncImplNFT.cpp` - NFT-related queries
- `HostFuncImplTrace.cpp` - Debugging/tracing functions
- **`HostFuncWrapper.h` / `detail/HostFuncWrapper.cpp`** - C-style wrapper
functions that bridge WASM calls to C++ `HostFunctions` methods. Each host
function has:
- A `_proto` type alias defining the function signature
- A `_wrap` function that extracts parameters and calls the implementation
- **`ParamsHelper.h`** - Utilities for WASM parameter handling:
- `WASM_IMPORT_FUNC` / `WASM_IMPORT_FUNC2` macros for registration
- `wasmParams()` helper for building parameter vectors
- Type conversion between WASM and C++ types
## Host Functions
Host functions allow WASM code to interact with the XRP Ledger. They are
organized into categories:
- **Ledger Information** - Access ledger sequence, timestamps, hashes, fees
- **Transaction & Ledger Object Access** - Read fields from the transaction
and ledger objects (including the current escrow object)
- **Keylet Construction** - Build keylets to look up various ledger object types
- **Cryptography** - Signature verification and hashing
- **Float Arithmetic** - Mathematical operations for amount calculations
- **NFT Operations** - Query NFT properties
- **Tracing/Debugging** - Log messages for debugging
For the complete list of available host functions, their WASM names, and gas
costs, see the [XLS-0102 specification](https://xls.xrpl.org/xls/XLS-0102-wasm-vm.html)
or `detail/WasmVM.cpp` where they are registered via `WASM_IMPORT_FUNC2` macros.
For method signatures, see `HostFunc.h`.
## Gas Model
Each host function has an associated gas cost. The gas cost is specified when
registering the function in `detail/WasmVM.cpp`:
```cpp
WASM_IMPORT_FUNC2(i, getLedgerSqn, "get_ledger_sqn", hfs, 60);
// ^^ gas cost
```
WASM execution is metered, and if the gas limit is exceeded, execution fails.
## Entry Point
The WASM module must export a function with the name defined by
`escrowFunctionName` (currently `"escrow_finish"`). This function:
- Takes no parameters (or parameters passed via host function calls)
- Returns an `int32_t`:
- `1` (or positive): Escrow conditions are met, allow finish
- `0` (or negative): Escrow conditions are not met, reject finish
## Adding a New Host Function
To add a new host function, follow these steps:
### 1. Add to HostFunc.h (Base Class)
Add a virtual method declaration with a default implementation that returns an
error:
```cpp
virtual std::expected<ReturnType, HostFunctionError>
myNewFunction(ParamType1 param1, ParamType2 param2)
{
return std::unexpected(HostFunctionError::INTERNAL);
}
```
### 2. Add to HostFuncImpl.h (Declaration)
Add the method override declaration in `WasmHostFunctionsImpl`:
```cpp
std::expected<ReturnType, HostFunctionError>
myNewFunction(ParamType1 param1, ParamType2 param2) override;
```
### 3. Implement in detail/HostFuncImpl\*.cpp
Add the implementation in the appropriate file:
```cpp
std::expected<ReturnType, HostFunctionError>
WasmHostFunctionsImpl::myNewFunction(ParamType1 param1, ParamType2 param2)
{
// Implementation using ctx (ApplyContext) for ledger access
return result;
}
```
### 4. Add Wrapper to HostFuncWrapper.h
Add the prototype and wrapper declaration:
```cpp
using myNewFunction_proto = int32_t(uint8_t const*, int32_t, ...);
wasm_trap_t*
myNewFunction_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results);
```
### 5. Implement Wrapper in detail/HostFuncWrapper.cpp
Implement the C-style wrapper that bridges WASM to C++:
```cpp
wasm_trap_t*
myNewFunction_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results)
{
// Extract parameters from params
// Call hfs->myNewFunction(...)
// Set results and return
}
```
### 6. Register in WasmVM.cpp
Add the function registration in `setCommonHostFunctions()` or
`createWasmImport()`:
```cpp
WASM_IMPORT_FUNC2(i, myNewFunction, "my_new_function", hfs, 100);
// ^^ WASM name ^^ gas cost
```
> [!IMPORTANT]
> New host functions MUST be amendment-gated in `WasmVM.cpp`.
> Wrap the registration in an amendment check to ensure the function is only
> available after the corresponding amendment is enabled on the network.
A module must export `escrow_finish` (`escrowFunctionName`) taking no parameters and
returning `int32_t`: positive means the conditions are met, zero or negative rejects the
finish. Everything the contract needs it asks for through a host call.

View File

@@ -7,10 +7,8 @@
#include <bit>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <optional>
#include <stdexcept>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
@@ -21,18 +19,6 @@ using Bytes = std::vector<std::uint8_t>;
using Hash = xrpl::uint256;
using FloatPair = std::pair<int64_t, int32_t>;
// Error signals that cross the wasm boundary as trap messages (the C API has no
// trap code). WasmiEngine::call maps them to TER: hfErrInternal -> tecINTERNAL,
// hfErrOutOfGas / wasmi's OutOfFuel -> tecOUT_OF_GAS, anything else ->
// tecFAILED_PROCESSING.
//
// Matched as substrings, not by equality: the C API returns the Rust Debug form
// of the error, e.g. `Error { kind: Message("HfInternal") }` or
// `Error { kind: TrapCode(OutOfFuel) }`.
std::string_view inline constexpr hfErrInternal = "HfInternal";
std::string_view inline constexpr hfErrOutOfGas = "HfOutOfGas";
std::string_view inline constexpr wasmiTrapOutOfFuel = "OutOfFuel";
enum class HostFunctionError : int32_t {
Unimplemented = -1,
FieldNotFound = -2,
@@ -56,19 +42,6 @@ enum class HostFunctionError : int32_t {
FloatComputationError = -20,
};
enum class WasmTypes { WtI32, WtI64 };
struct Wmem
{
std::uint8_t* p = nullptr;
std::size_t s = 0;
Wmem() = default;
Wmem(void* ptr, std::size_t size) : p(reinterpret_cast<std::uint8_t*>(ptr)), s(size)
{
}
};
template <typename T>
struct WasmResult
{
@@ -136,71 +109,6 @@ public:
}
};
class WasmRuntimeWrapper
{
public:
virtual ~WasmRuntimeWrapper() = default;
virtual Wmem
getMem() = 0;
virtual std::int64_t
getGas() = 0;
virtual std::int64_t
setGas(std::int64_t gas) = 0;
virtual std::int64_t
getTransferLimit() = 0;
virtual std::int64_t
setTransferLimit(std::int64_t transferLimit) = 0;
};
using RTOptRef = std::optional<std::reference_wrapper<WasmRuntimeWrapper>>;
struct WasmParam
{
// We are not supporting float/double
WasmTypes type = WasmTypes::WtI32;
union
{
std::int32_t i32;
std::int64_t i64 = 0;
} of;
};
template <class... Types>
inline void
wasmParamsHlp(std::vector<WasmParam>& v, std::int32_t p, Types&&... args)
{
v.push_back({.type = WasmTypes::WtI32, .of = {.i32 = p}});
wasmParamsHlp(v, std::forward<Types>(args)...);
}
template <class... Types>
inline void
wasmParamsHlp(std::vector<WasmParam>& v, std::int64_t p, Types&&... args)
{
v.push_back({.type = WasmTypes::WtI64, .of = {.i64 = p}});
wasmParamsHlp(v, std::forward<Types>(args)...);
}
inline void
wasmParamsHlp(std::vector<WasmParam>& v)
{
}
template <class... Types>
inline std::vector<WasmParam>
wasmParams(Types&&... args)
{
std::vector<WasmParam> v;
v.reserve(sizeof...(args));
wasmParamsHlp(v, std::forward<Types>(args)...);
return v;
}
template <typename T, size_t Size = sizeof(T)>
constexpr T
adjustWasmEndianessHlp(T x)