From 27859c1f6f88c477c94f3d8bbc2a90b37cc50d88 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Fri, 21 Aug 2026 18:10:24 +0100 Subject: [PATCH] Improve docs --- crates/xrpl-host-functions-macros/src/lib.rs | 3 +- crates/xrpl-host-functions/src/lib.rs | 133 +++++++----------- crates/xrpl-wasm-testkit/src/lib.rs | 4 +- crates/xrpl-wasm-vm-ffi/src/lib.rs | 26 ++-- crates/xrpl-wasm-vm/src/abi.rs | 2 +- crates/xrpl-wasm-vm/src/preflight.rs | 7 + crates/xrpl-wasm-vm/tests/preflight.rs | 8 +- crates/xrpl-wasm-vm/tests/vm_limits.rs | 10 +- include/xrpl/tx/wasm/HostContext.h | 9 +- include/xrpl/tx/wasm/README.md | 7 +- include/xrpl/tx/wasm/WasmCommon.h | 6 +- src/libxrpl/tx/wasm/HostContext.cpp | 9 +- src/tests/libxrpl/tx/wasm/MockHostFunctions.h | 8 +- .../libxrpl/tx/wasm/host_calls/Trace.cpp | 6 +- 14 files changed, 110 insertions(+), 128 deletions(-) diff --git a/crates/xrpl-host-functions-macros/src/lib.rs b/crates/xrpl-host-functions-macros/src/lib.rs index 3eb7a88ba5..a8eb4ca7b6 100644 --- a/crates/xrpl-host-functions-macros/src/lib.rs +++ b/crates/xrpl-host-functions-macros/src/lib.rs @@ -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)* } diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 225501a170..9cb5a1d8b0 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -90,16 +90,15 @@ host_functions! { #[wasm_name = "base_fee"] fn get_base_fee(&self, out: &mut [u8]) -> HostResult; - /// 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; /// 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; @@ -122,8 +121,8 @@ host_functions! { fn get_ledger_obj_field(&self, cache_idx: i32, field: i32, out: &mut [u8]) -> HostResult; /// 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; @@ -150,8 +149,7 @@ host_functions! { ) -> HostResult; /// 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; @@ -169,7 +167,7 @@ host_functions! { fn get_ledger_obj_array_len(&self, cache_idx: i32, field: i32) -> HostResult; /// 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; @@ -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; - /// 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; /// 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; - /// 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; /// 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; /// 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; - /// 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; /// 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; /// 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; /// 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; /// 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; /// 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; /// 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; /// 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; /// 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; /// 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; /// 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; - /// 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; /// 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; @@ -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; /// 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; /// 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; - /// 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; - /// 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; - /// 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; - /// 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; @@ -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; /// 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; /// 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; /// 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; - /// 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; - /// 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; - /// 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; /// 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; - /// 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; - /// 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; - /// 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; - /// 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; - /// 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; - /// 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; diff --git a/crates/xrpl-wasm-testkit/src/lib.rs b/crates/xrpl-wasm-testkit/src/lib.rs index 58d3db1885..f503294c59 100644 --- a/crates/xrpl-wasm-testkit/src/lib.rs +++ b/crates/xrpl-wasm-testkit/src/lib.rs @@ -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. diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index d9f63eeb71..0b2b965472 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -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 diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 541e18350d..da7108d2c4 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -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 { let arr: [u8; 4] = bytes.try_into().map_err(|_| HostError::InvalidParams)?; Ok(i32::from_le_bytes(arr)) diff --git a/crates/xrpl-wasm-vm/src/preflight.rs b/crates/xrpl-wasm-vm/src/preflight.rs index 8a2a89fd20..3c4dd53317 100644 --- a/crates/xrpl-wasm-vm/src/preflight.rs +++ b/crates/xrpl-wasm-vm/src/preflight.rs @@ -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. diff --git a/crates/xrpl-wasm-vm/tests/preflight.rs b/crates/xrpl-wasm-vm/tests/preflight.rs index 36d4683998..4746237916 100644 --- a/crates/xrpl-wasm-vm/tests/preflight.rs +++ b/crates/xrpl-wasm-vm/tests/preflight.rs @@ -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(); diff --git a/crates/xrpl-wasm-vm/tests/vm_limits.rs b/crates/xrpl-wasm-vm/tests/vm_limits.rs index a4e8eba35d..065204efef 100644 --- a/crates/xrpl-wasm-vm/tests/vm_limits.rs +++ b/crates/xrpl-wasm-vm/tests/vm_limits.rs @@ -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(); diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index e6150391eb..3ce625181c 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -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 diff --git a/include/xrpl/tx/wasm/README.md b/include/xrpl/tx/wasm/README.md index 7be22a6feb..6b94abc873 100644 --- a/include/xrpl/tx/wasm/README.md +++ b/include/xrpl/tx/wasm/README.md @@ -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 diff --git a/include/xrpl/tx/wasm/WasmCommon.h b/include/xrpl/tx/wasm/WasmCommon.h index 1410936911..421dd84b29 100644 --- a/include/xrpl/tx/wasm/WasmCommon.h +++ b/include/xrpl/tx/wasm/WasmCommon.h @@ -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::min(), }; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 07e6e0bf12..a67860604a 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -18,7 +18,7 @@ #include #include -// For `TraceDataType`, which the bridge declares and this header defines. +// For `TraceDataType`: declared in the cxx bridge, defined in the header it generates. #include #include @@ -76,8 +76,7 @@ answerScalar(rust::Slice 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 parseAsset(rust::Slice bytes) { @@ -111,7 +110,7 @@ parseAsset(rust::Slice 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 parseUint64(rust::Slice bytes) { @@ -126,7 +125,7 @@ parseUint64(rust::Slice 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 std::expected parseST(rust::Slice bytes) diff --git a/src/tests/libxrpl/tx/wasm/MockHostFunctions.h b/src/tests/libxrpl/tx/wasm/MockHostFunctions.h index d75291cc9a..438053fc06 100644 --- a/src/tests/libxrpl/tx/wasm/MockHostFunctions.h +++ b/src/tests/libxrpl/tx/wasm/MockHostFunctions.h @@ -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) diff --git a/src/tests/libxrpl/tx/wasm/host_calls/Trace.cpp b/src/tests/libxrpl/tx/wasm/host_calls/Trace.cpp index 349785e1fa..e7dd854d22 100644 --- a/src/tests/libxrpl/tx/wasm/host_calls/Trace.cpp +++ b/src/tests/libxrpl/tx/wasm/host_calls/Trace.cpp @@ -10,7 +10,7 @@ #include #include #include -// For `TraceDataType`, which the bridge declares and this header defines. +// For `TraceDataType`: declared in the cxx bridge, defined in the header it generates. #include #include @@ -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.