Improve docs

This commit is contained in:
Sergey Kuznetsov
2026-08-21 18:10:24 +01:00
parent e4baece501
commit 27859c1f6f
14 changed files with 110 additions and 128 deletions

View File

@@ -176,8 +176,7 @@ fn generate(functions: &[ParsedHostFunction]) -> TokenStream {
/// handing over a region clamped to the field cap — but it cannot take
/// back what a method already put there. A host that wrote a truncated
/// prefix and then reported the larger length would leave those bytes in
/// guest memory behind a refusal the guest is told to ignore. C++'s
/// `setData` is the reference point: it wrote only on a value that fit.
/// guest memory behind a refusal the guest is told to ignore.
pub trait HostFunctions {
#(#trait_methods)*
}

View File

@@ -90,16 +90,15 @@ host_functions! {
#[wasm_name = "base_fee"]
fn get_base_fee(&self, out: &mut [u8]) -> HostResult<usize>;
/// Whether an amendment is enabled. The input is either its 32-byte id or its
/// name; the answer is `1` if enabled and `0` if not. Unlike the getters, this
/// reads an input region and returns the flag directly rather than writing bytes.
/// Whether an amendment is enabled. The input is either its 32-byte id or its name;
/// the answer is `1` if enabled and `0` if not.
#[gas = 100]
#[wasm_name = "amendment_enabled"]
fn is_amendment_enabled(&self, amendment: &[u8]) -> HostResult<i32>;
/// Load the ledger object with the given 32-byte id into a cache slot, so later
/// calls can read its fields. `cache_idx` selects the slot (1-based); `0` asks the
/// host to assign a free one. Returns the slot used, or a negative error.
/// host to assign a free one. Answers the slot used.
#[gas = 5000]
#[wasm_name = "cache_le"]
fn cache_ledger_obj(&self, obj_id: &[u8], cache_idx: i32) -> HostResult<i32>;
@@ -122,8 +121,8 @@ host_functions! {
fn get_ledger_obj_field(&self, cache_idx: i32, field: i32, out: &mut [u8]) -> HostResult<usize>;
/// The serialized bytes of a nested field of the transaction, reached by a
/// `locator`: a path of little-endian `i32` steps (so its byte length is a
/// non-zero multiple of 4). Reads the locator region and writes the field bytes.
/// `locator`: a path of little-endian `i32` steps (so its byte length is a non-zero
/// multiple of 4).
#[gas = 110]
#[wasm_name = "tx_inner"]
fn get_tx_nested_field(&self, locator: &[u8], out: &mut [u8]) -> HostResult<usize>;
@@ -150,8 +149,7 @@ host_functions! {
) -> HostResult<usize>;
/// The number of elements in an array field of the transaction, selected by its
/// `SField` code. Answers the count directly, or a negative error (`NoArray` if
/// the field is not an array). Reads and writes no memory.
/// `SField` code. Answers the count directly; `NoArray` if the field is not an array.
#[gas = 40]
#[wasm_name = "tx_arr_len"]
fn get_tx_array_len(&self, field: i32) -> HostResult<i32>;
@@ -169,7 +167,7 @@ host_functions! {
fn get_ledger_obj_array_len(&self, cache_idx: i32, field: i32) -> HostResult<i32>;
/// The number of elements in a nested array field of the transaction, reached by a
/// `locator`. Reads the locator region and answers the count directly.
/// `locator`.
#[gas = 70]
#[wasm_name = "tx_inner_arr_len"]
fn get_tx_nested_array_len(&self, locator: &[u8]) -> HostResult<i32>;
@@ -186,8 +184,8 @@ host_functions! {
#[wasm_name = "le_inner_arr_len"]
fn get_ledger_obj_nested_array_len(&self, cache_idx: i32, locator: &[u8]) -> HostResult<i32>;
/// Verify `signature` over `message` under `pubkey`. Reads the three regions and
/// answers `1` if the signature is valid, `0` if not, or a negative error.
/// Verify `signature` over `message` under `pubkey`. Answers `1` if the signature
/// is valid, `0` if not, or a negative error.
#[gas = 300]
#[wasm_name = "check_sig"]
fn check_signature(
@@ -198,28 +196,26 @@ host_functions! {
) -> HostResult<i32>;
/// The 32-byte ledger key (keylet) of an account's `AccountRoot`, computed from a
/// 20-byte account id. Reads the account region and writes the keylet.
/// 20-byte account id.
#[gas = 350]
#[wasm_name = "accountroot_id"]
fn account_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of an AMM, computed from its two assets. Each asset is a
/// byte slice whose length selects its kind (24 = MPT, 20 = XRP, 40 = issued
/// currency + issuer). Reads both asset regions and writes the keylet.
/// The 32-byte keylet of an AMM, computed from its two assets. Each asset is a byte
/// slice whose length selects its kind (24 = MPT, 20 = XRP, 40 = issued currency +
/// issuer).
#[gas = 450]
#[wasm_name = "amm_id"]
fn amm_keylet(&self, asset1: &[u8], asset2: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of a `Check`, computed from a 20-byte account id and its
/// sequence number. `seq` is the guest's `u32` carried as its `i32` bit pattern.
/// Reads the account region and writes the keylet.
#[gas = 350]
#[wasm_name = "check_id"]
fn check_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of a `Credential`, computed from the 20-byte subject and
/// issuer account ids and a credential-type byte string. Reads all three regions
/// and writes the keylet.
/// issuer account ids and a credential-type byte string.
#[gas = 350]
#[wasm_name = "credential_id"]
fn credential_keylet(
@@ -230,8 +226,8 @@ host_functions! {
out: &mut [u8],
) -> HostResult<usize>;
/// The 32-byte keylet of a `Delegate` object, computed from the 20-byte account
/// and the account it authorizes. Reads both account regions and writes the keylet.
/// The 32-byte keylet of a `Delegate` object, computed from the 20-byte account and
/// the account it authorizes.
#[gas = 350]
#[wasm_name = "delegate_id"]
fn delegate_keylet(
@@ -242,8 +238,7 @@ host_functions! {
) -> HostResult<usize>;
/// The 32-byte keylet of a `DepositPreauth`, computed from the 20-byte account and
/// the account it authorizes to deposit. Reads both account regions and writes the
/// keylet.
/// the account it authorizes to deposit.
#[gas = 350]
#[wasm_name = "deposit_preauth_id"]
fn deposit_preauth_keylet(
@@ -254,21 +249,19 @@ host_functions! {
) -> HostResult<usize>;
/// The 32-byte keylet of an account's `DID`, computed from its 20-byte account id.
/// Reads the account region and writes the keylet.
#[gas = 350]
#[wasm_name = "did_id"]
fn did_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of an `Escrow`, computed from the 20-byte owner account and
/// its sequence number. `seq` is the guest's `u32` carried as its `i32` bit
/// pattern. Reads the account region and writes the keylet.
/// pattern.
#[gas = 350]
#[wasm_name = "escrow_id"]
fn escrow_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of a `RippleState` (trust line), computed from two 20-byte
/// account ids and a 20-byte currency. Reads all three regions and writes the
/// keylet.
/// account ids and a 20-byte currency.
#[gas = 400]
#[wasm_name = "trustline_id"]
fn trust_line_keylet(
@@ -280,8 +273,8 @@ host_functions! {
) -> HostResult<usize>;
/// The 32-byte keylet of an `MPTokenIssuance`, computed from the 20-byte issuer
/// account and its sequence number. `seq` is the guest's `u32` carried as its
/// `i32` bit pattern. Reads the account region and writes the keylet.
/// account and its sequence number. `seq` is the guest's `u32` carried as its `i32`
/// bit pattern.
#[gas = 350]
#[wasm_name = "mpt_issuance_id"]
fn mptoken_issuance_keylet(
@@ -292,14 +285,14 @@ host_functions! {
) -> HostResult<usize>;
/// The 32-byte keylet of an `MPToken`, computed from a 24-byte MPT issuance id and
/// the 20-byte holder account. Reads both regions and writes the keylet.
/// the 20-byte holder account.
#[gas = 500]
#[wasm_name = "mptoken_id"]
fn mptoken_keylet(&self, mptid: &[u8], holder: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of an `NFTokenOffer`, computed from the 20-byte owner account
/// and its sequence number. `seq` is the guest's `u32` carried as its `i32` bit
/// pattern. Reads the account region and writes the keylet.
/// pattern.
#[gas = 350]
#[wasm_name = "nft_offer_id"]
fn nftoken_offer_keylet(
@@ -311,22 +304,20 @@ host_functions! {
/// The 32-byte keylet of an `Offer`, computed from the 20-byte owner account and
/// its sequence number. `seq` is the guest's `u32` carried as its `i32` bit
/// pattern. Reads the account region and writes the keylet.
/// pattern.
#[gas = 350]
#[wasm_name = "offer_id"]
fn offer_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of an `Oracle`, computed from the 20-byte owner account and
/// its document id. `doc_id` is the guest's `u32` carried as its `i32` bit pattern.
/// Reads the account region and writes the keylet.
#[gas = 350]
#[wasm_name = "oracle_id"]
fn oracle_keylet(&self, account: &[u8], doc_id: i32, out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of a `PayChannel`, computed from the 20-byte source account,
/// the 20-byte destination account, and the channel's sequence number. `seq` is the
/// guest's `u32` carried as its `i32` bit pattern. Reads both account regions and
/// writes the keylet.
/// guest's `u32` carried as its `i32` bit pattern.
#[gas = 350]
#[wasm_name = "paychan_id"]
fn paychannel_keylet(
@@ -339,7 +330,7 @@ host_functions! {
/// The 32-byte keylet of a `PermissionedDomain`, computed from the 20-byte owner
/// account and its sequence number. `seq` is the guest's `u32` carried as its `i32`
/// bit pattern. Reads the account region and writes the keylet.
/// bit pattern.
#[gas = 350]
#[wasm_name = "permissioned_domain_id"]
fn permissioned_domain_keylet(
@@ -350,21 +341,19 @@ host_functions! {
) -> HostResult<usize>;
/// The 32-byte keylet of a `SignerList`, computed from its 20-byte owner account.
/// Reads the account region and writes the keylet.
#[gas = 350]
#[wasm_name = "signers_id"]
fn signer_list_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of a `Ticket`, computed from the 20-byte owner account and its
/// ticket sequence number. `seq` is the guest's `u32` carried as its `i32` bit
/// pattern. Reads the account region and writes the keylet.
/// The 32-byte keylet of a `Ticket`, computed from the 20-byte owner account and
/// its ticket sequence number. `seq` is the guest's `u32` carried as its `i32` bit
/// pattern.
#[gas = 350]
#[wasm_name = "ticket_id"]
fn ticket_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
/// The 32-byte keylet of a `Vault`, computed from the 20-byte owner account and its
/// sequence number. `seq` is the guest's `u32` carried as its `i32` bit pattern.
/// Reads the account region and writes the keylet.
#[gas = 350]
#[wasm_name = "vault_id"]
fn vault_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult<usize>;
@@ -382,51 +371,48 @@ host_functions! {
///
/// It is also the one declaration that is **not** the wasm parameter order.
/// `data_type` is the third wasm parameter, between the two regions, because that
/// is where xrpld's `trace_proto` and the guest stdlib put it; `register.rs` takes
/// the arguments in wasm order and calls this in declaration order.
/// is where the guest stdlib declares it; `register.rs` takes the arguments in wasm
/// order and calls this in declaration order.
#[gas = 30]
#[wasm_name = "trace"]
fn trace(&self, msg: &str, data: &[u8], data_type: TraceDataType) -> HostResult<()>;
/// Stores `data` as the current object's data field, replacing whatever was there,
/// and returns the number of bytes stored. Reads the data region; `DataFieldTooLarge`
/// if it exceeds the host's limit.
/// and returns the number of bytes stored; `DataFieldTooLarge` if it exceeds the
/// host's limit.
#[gas = 1000]
#[wasm_name = "set_data"]
fn update_data(&self, data: &[u8]) -> HostResult<i32>;
/// The URI of the `NFToken` with id `nft_id` (32 bytes) held by the 20-byte
/// `account`. Reads both regions and writes the URI bytes.
/// `account`.
#[gas = 5000]
#[wasm_name = "nft_uri"]
fn get_nft(&self, account: &[u8], nft_id: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The 20-byte issuer account encoded in the `NFToken` id `nft_id` (32 bytes).
/// Reads the id region and writes the issuer bytes.
#[gas = 70]
#[wasm_name = "nft_issuer"]
fn get_nft_issuer(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The taxon encoded in the `NFToken` id `nft_id` (32 bytes). Reads the id region
/// and writes the taxon as its four little-endian bytes.
/// The taxon encoded in the `NFToken` id `nft_id` (32 bytes), as four little-endian
/// bytes.
#[gas = 60]
#[wasm_name = "nft_taxon"]
fn get_nft_taxon(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult<usize>;
/// The flags encoded in the `NFToken` id `nft_id` (32 bytes). Reads the id region
/// and returns the flags as the call's scalar result.
/// The flags encoded in the `NFToken` id `nft_id` (32 bytes).
#[gas = 60]
#[wasm_name = "nft_flags"]
fn get_nft_flags(&self, nft_id: &[u8]) -> HostResult<i32>;
/// The transfer fee encoded in the `NFToken` id `nft_id` (32 bytes). Reads the id
/// region and returns the fee as the call's scalar result.
/// The transfer fee encoded in the `NFToken` id `nft_id` (32 bytes).
#[gas = 60]
#[wasm_name = "nft_xfer_fee"]
fn get_nft_transfer_fee(&self, nft_id: &[u8]) -> HostResult<i32>;
/// The sequence number encoded in the `NFToken` id `nft_id` (32 bytes). Reads the
/// id region and writes the sequence as its four little-endian bytes.
/// The sequence number encoded in the `NFToken` id `nft_id` (32 bytes), as four
/// little-endian bytes.
#[gas = 60]
#[wasm_name = "nft_serial"]
fn get_nft_sequence(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult<usize>;
@@ -435,39 +421,35 @@ host_functions! {
// holds opaquely and hands back to these functions. Inputs and outputs that are
// floats are byte regions; `mode` is the rounding mode, a scalar the guest chooses.
/// A float built from the signed integer `x` under rounding `mode`. Writes the
/// float bytes; no input region.
/// A float built from the signed integer `x` under rounding `mode`.
#[gas = 100]
#[wasm_name = "float_from_int"]
fn float_from_int(&self, x: i64, mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// A float built from the unsigned integer in the 8-byte region `x` under rounding
/// `mode`. Reads the integer region and writes the float bytes.
/// `mode`.
#[gas = 130]
#[wasm_name = "float_from_uint"]
fn float_from_uint(&self, x: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// A float built from the serialized `STAmount` in `amount` under rounding `mode`.
/// Reads the amount region and writes the float bytes.
#[gas = 150]
#[wasm_name = "float_from_stamount"]
fn float_from_stamount(&self, amount: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// A float built from the serialized `STNumber` in `number` under rounding `mode`.
/// Reads the number region and writes the float bytes.
#[gas = 150]
#[wasm_name = "float_from_stnumber"]
fn float_from_stnumber(&self, number: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// The float `x` rounded to a signed integer under rounding `mode`. Reads the float
/// region and writes the integer as its eight little-endian bytes.
/// The float `x` rounded to a signed integer under rounding `mode`, as eight
/// little-endian bytes.
#[gas = 130]
#[wasm_name = "float_to_int"]
fn float_to_int(&self, x: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// The float `x` split into its mantissa and exponent. Reads the float region and
/// writes the mantissa (eight little-endian bytes) and the exponent (four little-
/// endian bytes) to two separate output regions.
/// The float `x` split into its mantissa (eight little-endian bytes) and its exponent
/// (four little-endian bytes), each written to its own output region.
#[gas = 130]
#[wasm_name = "float_to_mant_exp"]
fn float_to_mant_exp(
@@ -477,8 +459,7 @@ host_functions! {
exponent_out: &mut [u8],
) -> HostResult<usize>;
/// A float built from `mantissa` and `exponent` under rounding `mode`. Writes the
/// float bytes; no input region.
/// A float built from `mantissa` and `exponent` under rounding `mode`.
#[gas = 100]
#[wasm_name = "float_from_mant_exp"]
fn float_from_mant_exp(
@@ -490,43 +471,37 @@ host_functions! {
) -> HostResult<usize>;
/// Compares floats `x` and `y`, returning a negative, zero, or positive scalar as
/// `x` is less than, equal to, or greater than `y`. Reads both float regions.
/// `x` is less than, equal to, or greater than `y`.
#[gas = 80]
#[wasm_name = "float_cmp"]
fn float_compare(&self, x: &[u8], y: &[u8]) -> HostResult<i32>;
/// The float sum `x + y` under rounding `mode`. Reads both float regions and writes
/// the result bytes.
/// The float sum `x + y` under rounding `mode`.
#[gas = 160]
#[wasm_name = "float_add"]
fn float_add(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// The float difference `x - y` under rounding `mode`. Reads both float regions and
/// writes the result bytes.
/// The float difference `x - y` under rounding `mode`.
#[gas = 160]
#[wasm_name = "float_sub"]
fn float_subtract(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// The float product `x * y` under rounding `mode`. Reads both float regions and
/// writes the result bytes.
/// The float product `x * y` under rounding `mode`.
#[gas = 300]
#[wasm_name = "float_mult"]
fn float_multiply(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// The float quotient `x / y` under rounding `mode`. Reads both float regions and
/// writes the result bytes.
/// The float quotient `x / y` under rounding `mode`.
#[gas = 300]
#[wasm_name = "float_div"]
fn float_divide(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// The `n`-th root of the float `x` under rounding `mode`. Reads the float region
/// and writes the result bytes.
/// The `n`-th root of the float `x` under rounding `mode`.
#[gas = 5500]
#[wasm_name = "float_root"]
fn float_root(&self, x: &[u8], n: i32, mode: i32, out: &mut [u8]) -> HostResult<usize>;
/// The float `x` raised to the power `n` under rounding `mode`. Reads the float
/// region and writes the result bytes.
/// The float `x` raised to the power `n` under rounding `mode`.
#[gas = 5500]
#[wasm_name = "float_pow"]
fn float_power(&self, x: &[u8], n: i32, mode: i32, out: &mut [u8]) -> HostResult<usize>;

View File

@@ -4,8 +4,8 @@
//! point. The engine pins `wasmi = { default-features = false }` precisely so a text
//! assembler cannot reach the consensus path — wasmi's `wat` feature is on by default and
//! makes `Module::new` accept text as readily as binary, which would make a transaction's
//! validity a build flag (review finding A5). Putting `compile_wat` on the production bridge
//! would link `wat` into xrpld even if nothing called it.
//! validity a build flag. Putting `compile_wat` on the production bridge would link `wat`
//! into xrpld even if nothing called it.
//!
//! Linked only into `xrpl_tests`, never into `libxrpl` or `xrpld`, so "no assembler in the
//! shipped node" is a property of the link graph rather than a flag someone can flip.

View File

@@ -1,23 +1,25 @@
//! The cxx bridge between the escrow wasm engine and xrpld.
//!
//! Three crossings. C++ calls `run_escrow` once per escrow finish; the engine's host
//! calls come back out through the C++ `HostContext`, which `CxxHost` presents to the
//! engine as an ordinary [`HostFunctions`] implementor. The ABI those calls speak is
//! declared once, in `xrpl-host-functions`, so neither side of this file gets to
//! restate a signature.
//! Three crossings:
//!
//! `check_escrow` is the third, and it crosses in one direction only: screening a
//! module needs no host, so nothing comes back out.
//! - **In:** C++ calls `run_escrow`, once per escrow finish.
//! - **Back out:** that run's host calls leave through the C++ `HostContext`, which
//! `CxxHost` presents to the engine as an ordinary [`HostFunctions`] implementor.
//! - **In only:** C++ screens a module with `check_escrow`. Screening needs no host,
//! so nothing comes back out.
//!
//! **Neither direction may unwind into the other**, and the two halves of that are
//! The ABI the host calls speak is declared once, in `xrpl-host-functions`, so neither
//! side of this file gets to restate a signature.
//!
//! **Neither language may unwind into the other**, and the two halves of that are
//! not symmetric:
//!
//! - A **Rust panic** is caught here, by `guarded`. Letting one reach C++ is
//! undefined behaviour; `[profile.release]` turns overflow checks on, so this is a
//! live path and not a formality.
//! - A **C++ exception** is stopped on the C++ side: every `HostContext` method is
//! `noexcept` and reports failure as a negative `HostError` code. That is what
//! makes `guarded` sufficient — see its documentation.
//! `noexcept` and catches its own. That is what makes `guarded` sufficient — see
//! its documentation.
//!
//! Everything hand-written here is private, so the names above are code spans rather
//! than links, and `cargo doc` needs `--document-private-items` to show any of it.
@@ -168,7 +170,7 @@ mod ffi {
/// The C++ side of the ABI: one method per host function, forwarding to
/// `xrpl::HostFunctions`.
///
/// Every method is `noexcept` and answers with a code, so a host call cannot
/// Every method is `noexcept` and catches everything, so a host call cannot
/// unwind into the engine.
///
/// `cxx_name` on each method below is not cosmetic: the declarations keep the
@@ -941,7 +943,7 @@ impl ffi::CheckResult {
/// **Why catching here is enough.** An unwind can only be caught where every frame
/// between the panic and the catch is Rust, and every frame here is: the engine and
/// wasmi are Rust, and a host call cannot start a C++ unwind because each
/// `HostContext` method is `noexcept` and answers with a code. So the only unwind
/// `HostContext` method is `noexcept` and catches everything. So the only unwind
/// that can reach this frame started in Rust, and this stops it.
///
/// [`AssertUnwindSafe`] is sound because nothing survives to be observed in a torn

View File

@@ -166,7 +166,7 @@ pub(crate) fn read_borrowed<'a>(
///
/// The ABI transports these as a 4-byte region rather than a wasm scalar (the guest
/// SDK passes `seq.to_le_bytes()`), so the region must be exactly four bytes;
/// `InvalidParams` otherwise, matching the C-ABI wrapper's `getDataUInt32`.
/// `InvalidParams` otherwise.
pub(crate) fn read_u32_arg(bytes: &[u8]) -> HostResult<i32> {
let arr: [u8; 4] = bytes.try_into().map_err(|_| HostError::InvalidParams)?;
Ok(i32::from_le_bytes(arr))

View File

@@ -126,6 +126,13 @@ fn is_entry_point(ty: &FuncType) -> bool {
/// engine grants. One pass over the exports, since both rules read the same list and
/// the export table is the only place either is visible.
///
/// **A memory or table the module keeps to itself is therefore not screened**: it is
/// absent from the exports, and the store's limiter is what refuses it, at
/// instantiation. That gap is wide for tables — Rust exports
/// `__indirect_function_table` only under `--export-table`, so unexported is the
/// normal shape — and narrow for memories, since a contract needs an exported one to
/// make any host call at all.
///
/// A module faulting on both is reported by whichever it declares first. Neither
/// fault explains the other, so there is no precedence to preserve — only the need
/// for every node to reach the same verdict, which export order already gives.

View File

@@ -566,10 +566,10 @@ fn what_static_screening_cannot_see() {
}
}
/// A start section is guest code, so screening cannot see whether it traps — but it
/// no longer has to. A trap is the guest's fault wherever it happens, so the run
/// charges the contract for what it burned instead of reporting a module the node
/// should have screened.
/// A start section is guest code, so screening cannot see whether it traps — and does
/// not have to. A trap is the guest's fault wherever it happens, so the run charges the
/// contract for what it burned instead of reporting a module the node should have
/// screened.
#[test]
fn a_start_section_screening_cannot_see_is_charged_as_a_trap() {
let host = FakeHost::new();

View File

@@ -1,5 +1,5 @@
//! What the engine refuses outright: modules it will not compile, will not
//! instantiate, or cannot find an entry point in — plus the linear-memory cap.
//! instantiate, or cannot find an entry point in — plus the memory and table caps.
//!
//! These are the sandbox's outer wall. Everything here fails the run rather than
//! returning a code to the guest, so each test reads the failure's message.
@@ -262,7 +262,7 @@ fn disabled_features() -> Vec<(&'static str, Vec<&'static str>, &'static str, &'
///
/// `wasm_custom_page_sizes` and `wasm_wide_arithmetic` are off by default in wasmi
/// 1.1 (`engine/config.rs:72,74`), so their rows guard against wasmi changing that
/// default rather than against our own config.
/// default rather than against this engine's own config.
#[test]
fn every_disabled_feature_is_refused_by_name() {
let host = FakeHost::new();
@@ -279,9 +279,9 @@ fn every_disabled_feature_is_refused_by_name() {
}
/// The three knobs [`every_disabled_feature_is_refused_by_name`] cannot cover. The
/// engine is a process-wide `LazyLock`, so a test observes the one configuration we
/// build: a knob masked by another, or with no caller-visible effect, has no
/// distinguishing module.
/// engine is a process-wide `LazyLock`, so a test observes the one configuration
/// `build_wasm_engine` makes: a knob masked by another, or with no caller-visible
/// effect, has no distinguishing module.
#[test]
fn the_knobs_without_a_module_of_their_own() {
let host = FakeHost::new();

View File

@@ -27,12 +27,11 @@ enum class TraceDataType : std::int32_t;
// access - and lowering its typed `std::expected` result onto the ABI's wire form.
//
// Every method is `noexcept`, and every body catches everything: a C++ exception
// unwinding into the Rust frames that called it would be undefined behaviour, so a
// failure leaves here as -1, which the engine reads as a fatal error and reports as
// `tecINTERNAL`.
// unwinding into the Rust frames that called it would be undefined behaviour, so a caught
// one leaves here as `HostFunctionError::InternalFatal`, which the engine reads as a fatal
// error and reports as `tecINTERNAL`.
//
// Not an owner: it borrows `hf` for the length of one run. Declared `struct` because the
// Rust side only ever sees an opaque pointer.
// Not an owner: it borrows the `HostFunctions` it is built over for the length of one run.
class HostContext
{
// Non-const so a host function that mutates (`cacheLedgerObj`, `updateData`) can be

View File

@@ -18,11 +18,12 @@ bridge.
`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 `guarded()`.
method catches everything — through `guarded()`, except `trace`, which answers the guest
nothing and so has its own catch that only logs.
- **`WasmCommon.h`** — the shared vocabulary: `HostFunctionError` (the codes a contract
sees), `Bytes`, `FieldLocator`, `WasmTER`, `adjustWasmEndianess`, which is where the
boundary's byte order is decided, and `guarded()`, the one catch every crossing of the
bridge's C++ half goes through.
boundary's byte order is decided, and `guarded()`, the catch that turns a throwing host
body into a code the engine can read.
## Host functions

View File

@@ -50,9 +50,9 @@ enum class HostFunctionError : int32_t {
// tecINTERNAL rather than the contract being handed a code to interpret. `guarded`
// answers it for a host body that throws.
//
// Outside the -1 ..= -20 range that a contract reads, and the only entry that is: it
// needs no number in that range, and INT32_MIN cannot collide with a code appended
// above. Negative so that a reader treating it as an ordinary failure is still right.
// The only entry outside the -1 ..= -20 range a contract reads: it needs no number
// there, and INT32_MIN cannot collide with a code appended above. Negative so that a
// reader treating it as an ordinary failure is still right.
InternalFatal = std::numeric_limits<int32_t>::min(),
};

View File

@@ -18,7 +18,7 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <rust/cxx.h>
// For `TraceDataType`, which the bridge declares and this header defines.
// For `TraceDataType`: declared in the cxx bridge, defined in the header it generates.
#include <xrpl_wasm_vm_ffi_cxxbridge/lib.h>
#include <algorithm>
@@ -76,8 +76,7 @@ answerScalar(rust::Slice<std::uint8_t> out, T value)
// Decode an asset from its wire bytes, whose length selects the kind: an MPT id, a
// bare currency (which must be XRP), or a currency followed by an issuer (which must
// not be XRP). Any other length is malformed. This mirrors `getDataAsset` in the
// C-ABI wrapper the wasm engine replaces.
// not be XRP). Any other length is malformed.
std::expected<Asset, HostFunctionError>
parseAsset(rust::Slice<std::uint8_t const> bytes)
{
@@ -111,7 +110,7 @@ parseAsset(rust::Slice<std::uint8_t const> bytes)
}
// Decode a `uint64` from its eight wire bytes, in the wire's byte order. The region
// must be exactly eight bytes, mirroring `getDataUnsigned` in the C-ABI wrapper.
// must be exactly eight bytes, else `InvalidParams`.
std::expected<std::uint64_t, HostFunctionError>
parseUint64(rust::Slice<std::uint8_t const> bytes)
{
@@ -126,7 +125,7 @@ parseUint64(rust::Slice<std::uint8_t const> bytes)
}
// Deserialize an `ST` object from its wire bytes; `InvalidParams` if the bytes are not
// a well-formed one. Mirrors the try/catch around `SerialIter` in the C-ABI wrapper.
// a well-formed one, which `SerialIter` reports by throwing.
template <class T>
std::expected<T, HostFunctionError>
parseST(rust::Slice<std::uint8_t const> bytes)

View File

@@ -16,10 +16,10 @@ namespace xrpl::test {
// A mock of the host the wasm engine calls back into.
//
// Only the methods the ABI currently declares are mocked, and that is deliberate: the ~60
// others keep `HostFunctions`' own `std::unexpected(Unimplemented)`, so a contract reaching
// for something the ABI has not declared yet fails the way production would. Add a
// `MOCK_METHOD` here when the matching entry is added to `host_functions!`.
// Only the methods the tests beside it exercise are mocked, and that is deliberate: the
// rest keep `HostFunctions`' own `std::unexpected(Unimplemented)`, so a contract reaching
// for one fails the way production would. Add a `MOCK_METHOD` here when a test needs to
// say what that host function answers.
struct MockHostFunctions : HostFunctions
{
explicit MockHostFunctions(beast::Journal journal) : HostFunctions(journal)

View File

@@ -10,7 +10,7 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/WasmFixture.h>
// For `TraceDataType`, which the bridge declares and this header defines.
// For `TraceDataType`: declared in the cxx bridge, defined in the header it generates.
#include <xrpl_wasm_vm_ffi_cxxbridge/lib.h>
#include <cstdint>
@@ -45,8 +45,8 @@ serialized(STAmount const& amount)
} // namespace
// trace — a message, a data type, and a buffer holding what that type says. One import for
// what were five, so what a test varies is the type rather than the function.
// trace — a message, a data type, and a buffer holding what that type says. One import
// covers every rendering, so what a test varies is the type rather than the function.
//
// The buffer arrives as bytes and leaves as text: `HostContext` renders it, and the host is
// handed the finished line. So a test says which renderer the type selected.