From 97f32869dfe040a50615ae5cfdce4e9282d2fd23 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Tue, 11 Aug 2026 10:10:41 -0400 Subject: [PATCH] feat: Hook up float host functions --- crates/xrpl-host-functions/src/lib.rs | 100 +++++++ .../tests/generated_abi.rs | 154 ++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 138 +++++++++ crates/xrpl-wasm-vm/src/abi.rs | 156 ++++++++++ crates/xrpl-wasm-vm/src/register.rs | 277 +++++++++++++++++- crates/xrpl-wasm-vm/tests/budgets.rs | 70 +++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 106 +++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 16 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 180 ++++++++++++ include/xrpl/tx/wasm/HostContext.h | 95 ++++++ src/libxrpl/tx/wasm/HostContext.cpp | 268 +++++++++++++++++ 11 files changed, 1558 insertions(+), 2 deletions(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 36fe13b7ca..71f39403e6 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -463,4 +463,104 @@ host_functions! { #[gas = 60] #[wasm_name = "nft_serial"] fn get_nft_sequence(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult; + + // A "float" here is an XRPL `Number` in its serialized form: a byte blob the guest + // 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. + #[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. + #[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. + #[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. + #[gas = 130] + #[wasm_name = "float_to_mant_exp"] + fn float_to_mant_exp( + &self, + x: &[u8], + mantissa_out: &mut [u8], + exponent_out: &mut [u8], + ) -> HostResult; + + /// A float built from `mantissa` and `exponent` under rounding `mode`. Writes the + /// float bytes; no input region. + #[gas = 100] + #[wasm_name = "float_from_mant_exp"] + fn float_from_mant_exp( + &self, + mantissa: i64, + exponent: i32, + mode: i32, + out: &mut [u8], + ) -> 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. + #[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. + #[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. + #[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. + #[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. + #[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. + #[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. + #[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-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 543857139d..fe656bbbc3 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -455,6 +455,126 @@ impl HostFunctions for FakeHost { } put(out, &nft_id[0].to_le_bytes()) } + + /// A scalar-in float: writes the low byte of `x` as a stand-in float. + fn float_from_int(&self, x: i64, _mode: i32, out: &mut [u8]) -> HostResult { + put(out, &[x as u8]) + } + + /// A byte-in float; `InvalidParams` on an empty region. + fn float_from_uint(&self, x: &[u8], _mode: i32, out: &mut [u8]) -> HostResult { + if x.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[x[0]]) + } + + /// The same, for a serialized amount. + fn float_from_stamount(&self, amount: &[u8], _mode: i32, out: &mut [u8]) -> HostResult { + if amount.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[amount[0]]) + } + + /// The same, for a serialized number. + fn float_from_stnumber(&self, number: &[u8], _mode: i32, out: &mut [u8]) -> HostResult { + if number.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[number[0]]) + } + + /// A float rounded to an integer, written as bytes. + fn float_to_int(&self, x: &[u8], _mode: i32, out: &mut [u8]) -> HostResult { + if x.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[x[0]]) + } + + /// Writes a mantissa (its first byte) and an exponent (its first byte) to two + /// regions, returning their combined length. + fn float_to_mant_exp( + &self, + x: &[u8], + mantissa_out: &mut [u8], + exponent_out: &mut [u8], + ) -> HostResult { + if x.is_empty() { + return Err(HostError::InvalidParams); + } + let m = put(mantissa_out, &[x[0]])?; + let e = put(exponent_out, &[x[0]])?; + Ok(m + e) + } + + /// A two-scalar-in float. + fn float_from_mant_exp( + &self, + mantissa: i64, + _exponent: i32, + _mode: i32, + out: &mut [u8], + ) -> HostResult { + put(out, &[mantissa as u8]) + } + + /// Reads two floats and returns a scalar; `InvalidParams` if either is empty. + fn float_compare(&self, x: &[u8], y: &[u8]) -> HostResult { + if x.is_empty() || y.is_empty() { + return Err(HostError::InvalidParams); + } + Ok(i32::from(x[0]) - i32::from(y[0])) + } + + /// A binary float operator; `InvalidParams` if either operand is empty. + fn float_add(&self, x: &[u8], y: &[u8], _mode: i32, out: &mut [u8]) -> HostResult { + if x.is_empty() || y.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[x[0]]) + } + + /// The same shape, for subtraction. + fn float_subtract(&self, x: &[u8], y: &[u8], _mode: i32, out: &mut [u8]) -> HostResult { + if x.is_empty() || y.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[x[0]]) + } + + /// The same shape, for multiplication. + fn float_multiply(&self, x: &[u8], y: &[u8], _mode: i32, out: &mut [u8]) -> HostResult { + if x.is_empty() || y.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[x[0]]) + } + + /// The same shape, for division. + fn float_divide(&self, x: &[u8], y: &[u8], _mode: i32, out: &mut [u8]) -> HostResult { + if x.is_empty() || y.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[x[0]]) + } + + /// A one-float-and-integer operator; `InvalidParams` on an empty operand. + fn float_root(&self, x: &[u8], _n: i32, _mode: i32, out: &mut [u8]) -> HostResult { + if x.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[x[0]]) + } + + /// The same shape, for exponentiation. + fn float_power(&self, x: &[u8], _n: i32, _mode: i32, out: &mut [u8]) -> HostResult { + if x.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[x[0]]) + } } #[test] @@ -687,6 +807,26 @@ fn the_trait_is_implementable() { assert_eq!(host.get_nft_flags(&[]), Err(HostError::InvalidParams)); assert_eq!(host.get_nft_transfer_fee(&[9; 32]), Ok(9)); assert_eq!(host.get_nft_sequence(&[9; 32], &mut out), Ok(1)); + assert_eq!(host.float_from_int(5, 0, &mut out), Ok(1)); + assert_eq!(host.float_from_uint(&[3; 8], 0, &mut out), Ok(1)); + assert_eq!(host.float_from_stamount(&[3; 8], 0, &mut out), Ok(1)); + assert_eq!(host.float_from_stnumber(&[3; 8], 0, &mut out), Ok(1)); + assert_eq!(host.float_to_int(&[3; 8], 0, &mut out), Ok(1)); + let mut mant = [0u8; 8]; + let mut exp = [0u8; 4]; + assert_eq!(host.float_to_mant_exp(&[3; 8], &mut mant, &mut exp), Ok(2)); + assert_eq!(host.float_from_mant_exp(5, 0, 0, &mut out), Ok(1)); + assert_eq!(host.float_compare(&[9; 8], &[4; 8]), Ok(5)); + assert_eq!( + host.float_compare(&[], &[4; 8]), + Err(HostError::InvalidParams) + ); + assert_eq!(host.float_add(&[3; 8], &[4; 8], 0, &mut out), Ok(1)); + assert_eq!(host.float_subtract(&[3; 8], &[4; 8], 0, &mut out), Ok(1)); + assert_eq!(host.float_multiply(&[3; 8], &[4; 8], 0, &mut out), Ok(1)); + assert_eq!(host.float_divide(&[3; 8], &[4; 8], 0, &mut out), Ok(1)); + assert_eq!(host.float_root(&[3; 8], 2, 0, &mut out), Ok(1)); + assert_eq!(host.float_power(&[3; 8], 2, 0, &mut out), Ok(1)); assert_eq!(*host.traced.borrow(), ["hello/2/true", "count=-1"]); } @@ -798,6 +938,20 @@ fn the_spec_table_matches_the_declarations() { ("nft_flags", 60), ("nft_xfer_fee", 60), ("nft_serial", 60), + ("float_from_int", 100), + ("float_from_uint", 130), + ("float_from_stamount", 150), + ("float_from_stnumber", 150), + ("float_to_int", 130), + ("float_to_mant_exp", 130), + ("float_from_mant_exp", 100), + ("float_cmp", 80), + ("float_add", 160), + ("float_sub", 160), + ("float_mult", 300), + ("float_div", 300), + ("float_root", 5500), + ("float_pow", 5500), ] ); } diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 0ed1ad1053..0353a83f67 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -419,6 +419,77 @@ mod ffi { #[namespace = "xrpl"] #[cxx_name = "getNFTSequence"] fn get_nft_sequence(self: &HostContext, nft_id: &[u8], out: &mut [u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "floatFromInt"] + fn float_from_int(self: &HostContext, x: i64, mode: i32, out: &mut [u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "floatFromUint"] + fn float_from_uint(self: &HostContext, x: &[u8], mode: i32, out: &mut [u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "floatFromSTAmount"] + fn float_from_stamount(self: &HostContext, amount: &[u8], mode: i32, out: &mut [u8]) + -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "floatFromSTNumber"] + fn float_from_stnumber(self: &HostContext, number: &[u8], mode: i32, out: &mut [u8]) + -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "floatToInt"] + fn float_to_int(self: &HostContext, x: &[u8], mode: i32, out: &mut [u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "floatToMantExp"] + fn float_to_mant_exp( + self: &HostContext, + x: &[u8], + mantissa_out: &mut [u8], + exponent_out: &mut [u8], + ) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "floatFromMantExp"] + fn float_from_mant_exp( + self: &HostContext, + mantissa: i64, + exponent: i32, + mode: i32, + out: &mut [u8], + ) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "floatCompare"] + fn float_compare(self: &HostContext, x: &[u8], y: &[u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "floatAdd"] + fn float_add(self: &HostContext, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "floatSubtract"] + fn float_subtract(self: &HostContext, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) + -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "floatMultiply"] + fn float_multiply(self: &HostContext, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) + -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "floatDivide"] + fn float_divide(self: &HostContext, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "floatRoot"] + fn float_root(self: &HostContext, x: &[u8], n: i32, mode: i32, out: &mut [u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "floatPower"] + fn float_power(self: &HostContext, x: &[u8], n: i32, mode: i32, out: &mut [u8]) -> i32; } } @@ -713,6 +784,73 @@ impl HostFunctions for CxxHost<'_> { fn get_nft_sequence(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.get_nft_sequence(nft_id, out)) } + + fn float_from_int(&self, x: i64, mode: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.float_from_int(x, mode, out)) + } + + fn float_from_uint(&self, x: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.float_from_uint(x, mode, out)) + } + + fn float_from_stamount(&self, amount: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.float_from_stamount(amount, mode, out)) + } + + fn float_from_stnumber(&self, number: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.float_from_stnumber(number, mode, out)) + } + + fn float_to_int(&self, x: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.float_to_int(x, mode, out)) + } + + fn float_to_mant_exp( + &self, + x: &[u8], + mantissa_out: &mut [u8], + exponent_out: &mut [u8], + ) -> HostResult { + bytes_written(self.ctx.float_to_mant_exp(x, mantissa_out, exponent_out)) + } + + fn float_from_mant_exp( + &self, + mantissa: i64, + exponent: i32, + mode: i32, + out: &mut [u8], + ) -> HostResult { + bytes_written(self.ctx.float_from_mant_exp(mantissa, exponent, mode, out)) + } + + fn float_compare(&self, x: &[u8], y: &[u8]) -> HostResult { + scalar(self.ctx.float_compare(x, y)) + } + + fn float_add(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.float_add(x, y, mode, out)) + } + + fn float_subtract(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.float_subtract(x, y, mode, out)) + } + + fn float_multiply(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.float_multiply(x, y, mode, out)) + } + + fn float_divide(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.float_divide(x, y, mode, out)) + } + + fn float_root(&self, x: &[u8], n: i32, mode: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.float_root(x, n, mode, out)) + } + + fn float_power(&self, x: &[u8], n: i32, mode: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.float_power(x, n, mode, out)) + } } fn run_escrow( diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 85a83d3e32..3dba7f9785 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -174,6 +174,69 @@ pub(crate) fn write_buffered( Ok(n) } +/// The mantissa and exponent widths `float_to_mant_exp` writes: an `i64` and an `i32`. +/// Fixed by the ABI, not the guest, so the split is a constant rather than a reported +/// length. +const MANTISSA_BYTES: usize = 8; +const EXPONENT_BYTES: usize = 4; + +/// Service `float_to_mant_exp`, the one call that writes two output regions: the host +/// fills the run's output buffer with the mantissa followed by the exponent, and each +/// is copied to its own guest region once every rule has passed. +/// +/// Like [`write_buffered`], the host reads its input from the guest's memory and writes +/// to a scratch buffer, so the input stays borrowed rather than copied. The two output +/// regions are judged after the input, and the mantissa's region before the exponent's, +/// so the first fault reported is the leftmost. +pub(crate) fn write_mant_exp( + caller: &mut Caller<'_, VmState<'_>>, + mantissa_out: Region, + exponent_out: Region, + call: impl FnOnce(&dyn HostFunctions, &[u8], &mut [u8], &mut [u8]) -> HostResult, +) -> HostResult { + let mem = memory(caller)?; + let (data, state) = mem.data_and_store_mut(&mut *caller); + let host: &dyn HostFunctions = state.host; + + // The scratch buffer is split at the fixed mantissa width: the host fills the first + // eight bytes with the mantissa and the next four with the exponent. + let (mant_buf, exp_buf) = state.out_buffer.split_at_mut(MANTISSA_BYTES); + let mant_buf = &mut mant_buf[..MANTISSA_BYTES]; + let exp_buf = &mut exp_buf[..EXPONENT_BYTES]; + + let total = call(host, data, mant_buf, exp_buf)?; + + // Copy the mantissa, then the exponent, each only if its whole value fits its + // region — a region too small is `BufferTooSmall`, with nothing written. + let mant_range = mantissa_out.range()?; + let mant_dst = data + .get_mut(mant_range) + .ok_or(HostError::PointerOutOfBounds)?; + if mant_dst.len() < MANTISSA_BYTES { + return Err(HostError::BufferTooSmall); + } + mant_dst[..MANTISSA_BYTES].copy_from_slice(&state.out_buffer[..MANTISSA_BYTES]); + + let exp_range = exponent_out.range()?; + let exp_dst = data + .get_mut(exp_range) + .ok_or(HostError::PointerOutOfBounds)?; + if exp_dst.len() < EXPONENT_BYTES { + return Err(HostError::BufferTooSmall); + } + exp_dst[..EXPONENT_BYTES] + .copy_from_slice(&state.out_buffer[MANTISSA_BYTES..MANTISSA_BYTES + EXPONENT_BYTES]); + + charge_transfer(state, MANTISSA_BYTES + EXPONENT_BYTES)?; + #[expect( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + reason = "the total is 12, far inside i32" + )] + let total = total as i32; + Ok(total) +} + #[cfg(test)] mod tests { use super::*; @@ -405,6 +468,99 @@ mod tests { fn get_nft_sequence(&self, _nft_id: &[u8], _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn float_from_int(&self, _x: i64, _mode: i32, _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn float_from_uint(&self, _x: &[u8], _mode: i32, _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn float_from_stamount( + &self, + _amount: &[u8], + _mode: i32, + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn float_from_stnumber( + &self, + _number: &[u8], + _mode: i32, + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn float_to_int(&self, _x: &[u8], _mode: i32, _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn float_to_mant_exp( + &self, + _x: &[u8], + _mantissa_out: &mut [u8], + _exponent_out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn float_from_mant_exp( + &self, + _mantissa: i64, + _exponent: i32, + _mode: i32, + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn float_compare(&self, _x: &[u8], _y: &[u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn float_add( + &self, + _x: &[u8], + _y: &[u8], + _mode: i32, + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn float_subtract( + &self, + _x: &[u8], + _y: &[u8], + _mode: i32, + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn float_multiply( + &self, + _x: &[u8], + _y: &[u8], + _mode: i32, + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn float_divide( + &self, + _x: &[u8], + _y: &[u8], + _mode: i32, + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn float_root(&self, _x: &[u8], _n: i32, _mode: i32, _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn float_power( + &self, + _x: &[u8], + _n: i32, + _mode: i32, + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } } fn state(budget: u64) -> VmState<'static> { diff --git a/crates/xrpl-wasm-vm/src/register.rs b/crates/xrpl-wasm-vm/src/register.rs index e40a47e7bc..c6397204be 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -1,4 +1,4 @@ -use crate::abi::{charged, read_borrowed, write_buffered, write_into}; +use crate::abi::{charged, read_borrowed, write_buffered, write_into, write_mant_exp}; use crate::region::Region; use crate::vm::VmState; use wasmi::{Caller, Linker}; @@ -898,6 +898,281 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::FloatFromInt => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + x: i64, + out_ptr: i32, + out_len: i32, + mode: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::FloatFromInt, |c| { + let out = Region::new(out_ptr, out_len); + write_into(c, out, |host, out| host.float_from_int(x, mode, out)) + }) + }, + ), + HostFunctionSpec::FloatFromUint => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + in_ptr: i32, + in_len: i32, + out_ptr: i32, + out_len: i32, + mode: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::FloatFromUint, |c| { + let out = Region::new(out_ptr, out_len); + let x = Region::new(in_ptr, in_len); + write_buffered(c, out, |host, data, buf| { + host.float_from_uint(x.read(data)?, mode, buf) + }) + }) + }, + ), + HostFunctionSpec::FloatFromStamount => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + in_ptr: i32, + in_len: i32, + out_ptr: i32, + out_len: i32, + mode: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::FloatFromStamount, |c| { + let out = Region::new(out_ptr, out_len); + let amount = Region::new(in_ptr, in_len); + write_buffered(c, out, |host, data, buf| { + host.float_from_stamount(amount.read(data)?, mode, buf) + }) + }) + }, + ), + HostFunctionSpec::FloatFromStnumber => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + in_ptr: i32, + in_len: i32, + out_ptr: i32, + out_len: i32, + mode: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::FloatFromStnumber, |c| { + let out = Region::new(out_ptr, out_len); + let number = Region::new(in_ptr, in_len); + write_buffered(c, out, |host, data, buf| { + host.float_from_stnumber(number.read(data)?, mode, buf) + }) + }) + }, + ), + HostFunctionSpec::FloatToInt => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + in_ptr: i32, + in_len: i32, + out_ptr: i32, + out_len: i32, + mode: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::FloatToInt, |c| { + let out = Region::new(out_ptr, out_len); + let x = Region::new(in_ptr, in_len); + write_buffered(c, out, |host, data, buf| { + host.float_to_int(x.read(data)?, mode, buf) + }) + }) + }, + ), + HostFunctionSpec::FloatToMantExp => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + in_ptr: i32, + in_len: i32, + mant_ptr: i32, + mant_len: i32, + exp_ptr: i32, + exp_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::FloatToMantExp, |c| { + let mantissa = Region::new(mant_ptr, mant_len); + let exponent = Region::new(exp_ptr, exp_len); + let x = Region::new(in_ptr, in_len); + write_mant_exp(c, mantissa, exponent, |host, data, mant, exp| { + host.float_to_mant_exp(x.read(data)?, mant, exp) + }) + }) + }, + ), + HostFunctionSpec::FloatFromMantExp => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + mantissa: i64, + exponent: i32, + out_ptr: i32, + out_len: i32, + mode: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::FloatFromMantExp, |c| { + let out = Region::new(out_ptr, out_len); + write_into(c, out, |host, out| { + host.float_from_mant_exp(mantissa, exponent, mode, out) + }) + }) + }, + ), + HostFunctionSpec::FloatCompare => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + x_ptr: i32, + x_len: i32, + y_ptr: i32, + y_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::FloatCompare, |c| { + let host = c.data().host; + let x = read_borrowed(c, Region::new(x_ptr, x_len))?; + let y = read_borrowed(c, Region::new(y_ptr, y_len))?; + host.float_compare(x, y) + }) + }, + ), + HostFunctionSpec::FloatAdd => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + x_ptr: i32, + x_len: i32, + y_ptr: i32, + y_len: i32, + out_ptr: i32, + out_len: i32, + mode: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::FloatAdd, |c| { + let out = Region::new(out_ptr, out_len); + let x = Region::new(x_ptr, x_len); + let y = Region::new(y_ptr, y_len); + write_buffered(c, out, |host, data, buf| { + host.float_add(x.read(data)?, y.read(data)?, mode, buf) + }) + }) + }, + ), + HostFunctionSpec::FloatSubtract => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + x_ptr: i32, + x_len: i32, + y_ptr: i32, + y_len: i32, + out_ptr: i32, + out_len: i32, + mode: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::FloatSubtract, |c| { + let out = Region::new(out_ptr, out_len); + let x = Region::new(x_ptr, x_len); + let y = Region::new(y_ptr, y_len); + write_buffered(c, out, |host, data, buf| { + host.float_subtract(x.read(data)?, y.read(data)?, mode, buf) + }) + }) + }, + ), + HostFunctionSpec::FloatMultiply => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + x_ptr: i32, + x_len: i32, + y_ptr: i32, + y_len: i32, + out_ptr: i32, + out_len: i32, + mode: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::FloatMultiply, |c| { + let out = Region::new(out_ptr, out_len); + let x = Region::new(x_ptr, x_len); + let y = Region::new(y_ptr, y_len); + write_buffered(c, out, |host, data, buf| { + host.float_multiply(x.read(data)?, y.read(data)?, mode, buf) + }) + }) + }, + ), + HostFunctionSpec::FloatDivide => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + x_ptr: i32, + x_len: i32, + y_ptr: i32, + y_len: i32, + out_ptr: i32, + out_len: i32, + mode: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::FloatDivide, |c| { + let out = Region::new(out_ptr, out_len); + let x = Region::new(x_ptr, x_len); + let y = Region::new(y_ptr, y_len); + write_buffered(c, out, |host, data, buf| { + host.float_divide(x.read(data)?, y.read(data)?, mode, buf) + }) + }) + }, + ), + HostFunctionSpec::FloatRoot => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + in_ptr: i32, + in_len: i32, + n: i32, + out_ptr: i32, + out_len: i32, + mode: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::FloatRoot, |c| { + let out = Region::new(out_ptr, out_len); + let x = Region::new(in_ptr, in_len); + write_buffered(c, out, |host, data, buf| { + host.float_root(x.read(data)?, n, mode, buf) + }) + }) + }, + ), + HostFunctionSpec::FloatPower => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + in_ptr: i32, + in_len: i32, + n: i32, + out_ptr: i32, + out_len: i32, + mode: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::FloatPower, |c| { + let out = Region::new(out_ptr, out_len); + let x = Region::new(in_ptr, in_len); + write_buffered(c, out, |host, data, buf| { + host.float_power(x.read(data)?, n, mode, buf) + }) + }) + }, + ), }?; } Ok(()) diff --git a/crates/xrpl-wasm-vm/tests/budgets.rs b/crates/xrpl-wasm-vm/tests/budgets.rs index 78908b904f..d20eca02a1 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -289,6 +289,76 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $nft_serial (i32.const 0) (i32.const 32) (i32.const 32) (i32.const 4))", 4, ), + HostFunctionSpec::FloatFromInt => ( + import::FLOAT_FROM_INT, + "(call $float_from_int (i64.const 0) (i32.const 0) (i32.const 8) (i32.const 0))", + 4, + ), + HostFunctionSpec::FloatFromUint => ( + import::FLOAT_FROM_UINT, + "(call $float_from_uint (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 0))", + 5, + ), + HostFunctionSpec::FloatFromStamount => ( + import::FLOAT_FROM_STAMOUNT, + "(call $float_from_stamount (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 0))", + 5, + ), + HostFunctionSpec::FloatFromStnumber => ( + import::FLOAT_FROM_STNUMBER, + "(call $float_from_stnumber (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 0))", + 5, + ), + HostFunctionSpec::FloatToInt => ( + import::FLOAT_TO_INT, + "(call $float_to_int (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 0))", + 5, + ), + HostFunctionSpec::FloatToMantExp => ( + import::FLOAT_TO_MANT_EXP, + "(call $float_to_mant_exp (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 4))", + 6, + ), + HostFunctionSpec::FloatFromMantExp => ( + import::FLOAT_FROM_MANT_EXP, + "(call $float_from_mant_exp (i64.const 0) (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 0))", + 5, + ), + HostFunctionSpec::FloatCompare => ( + import::FLOAT_CMP, + "(call $float_cmp (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8))", + 4, + ), + HostFunctionSpec::FloatAdd => ( + import::FLOAT_ADD, + "(call $float_add (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 8) (i32.const 0))", + 7, + ), + HostFunctionSpec::FloatSubtract => ( + import::FLOAT_SUB, + "(call $float_sub (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 8) (i32.const 0))", + 7, + ), + HostFunctionSpec::FloatMultiply => ( + import::FLOAT_MULT, + "(call $float_mult (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 8) (i32.const 0))", + 7, + ), + HostFunctionSpec::FloatDivide => ( + import::FLOAT_DIV, + "(call $float_div (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 16) (i32.const 8) (i32.const 0))", + 7, + ), + HostFunctionSpec::FloatRoot => ( + import::FLOAT_ROOT, + "(call $float_root (i32.const 0) (i32.const 8) (i32.const 2) (i32.const 8) (i32.const 8) (i32.const 0))", + 6, + ), + HostFunctionSpec::FloatPower => ( + import::FLOAT_POW, + "(call $float_pow (i32.const 0) (i32.const 8) (i32.const 2) (i32.const 8) (i32.const 8) (i32.const 0))", + 6, + ), }; Call { import, diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index a138aea120..5f5be107b8 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -866,6 +866,112 @@ fn nft_serial_reads_the_id_and_writes_four_bytes() { assert_eq!(*host.nft_sequences_asked.borrow(), vec![nft_id]); } +/// A float built from an i64 scalar and no input region: the value and mode reach the +/// host, and the float bytes it answers land where the guest asked. `float_from_int` +/// carries a genuine `i64` parameter, so this pins that the wide scalar survives. +#[test] +fn float_from_int_passes_the_value_and_writes_the_float() { + let host = FakeHost::new().answering_float(support::Answer::filler(8)); + + let wat = module( + &[import::FLOAT_FROM_INT, ONE_PAGE], + "(call $float_from_int (i64.const 42) (i32.const 64) (i32.const 8) (i32.const 3))", + ); + assert_eq!(status(&wat, &host), 8, "the float length"); + assert_eq!(*host.float_from_int_asked.borrow(), vec![(42, 3)]); +} + +/// A float built from an 8-byte input region: the integer bytes and mode reach the +/// host, and the float bytes it answers land where the guest asked. +#[test] +fn float_from_uint_reads_the_input_and_writes_the_float() { + let host = FakeHost::new().answering_float(support::Answer::filler(8)); + + let wat = module( + &[import::FLOAT_FROM_UINT, ONE_PAGE], + "(call $float_from_uint (i32.const 0) (i32.const 8) (i32.const 64) (i32.const 8) (i32.const 1))", + ); + assert_eq!(status(&wat, &host), 8, "the float length"); + assert_eq!( + *host.float_from_uint_asked.borrow(), + vec![(vec![0u8; 8], 1)] + ); +} + +/// The one call that writes two output regions: the mantissa lands in the first, the +/// exponent in the second, and the status is their combined length. +#[test] +fn float_to_mant_exp_writes_both_regions() { + let host = + FakeHost::new().answering_float_mant_exp(vec![1, 2, 3, 4, 5, 6, 7, 8], vec![9, 10, 11, 12]); + + // Mantissa to offset 64, exponent to offset 80; read the first byte of each back. + let wat = module( + &[import::FLOAT_TO_MANT_EXP, ONE_PAGE], + "(call $float_to_mant_exp (i32.const 0) (i32.const 8) (i32.const 64) (i32.const 8) (i32.const 80) (i32.const 4))", + ); + assert_eq!(status(&wat, &host), 12, "the mantissa and exponent lengths"); + assert_eq!(*host.float_to_mant_exp_asked.borrow(), vec![vec![0u8; 8]]); + + let wat = module( + &[import::FLOAT_TO_MANT_EXP, ONE_PAGE], + "(drop (call $float_to_mant_exp (i32.const 0) (i32.const 8) (i32.const 64) (i32.const 8) (i32.const 80) (i32.const 4))) + (i32.load8_u (i32.const 80))", + ); + assert_eq!(status(&wat, &host), 9, "the exponent's first byte"); +} + +/// A comparison that reads two float regions and returns a scalar verdict, no output +/// region involved. +#[test] +fn float_cmp_reads_both_and_returns_the_verdict() { + let host = FakeHost::new().answering_float_compare(Ok(-1)); + + let wat = module( + &[import::FLOAT_CMP, ONE_PAGE], + "(call $float_cmp (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8))", + ); + assert_eq!(status(&wat, &host), -1, "the comparison verdict"); + assert_eq!( + *host.float_compare_asked.borrow(), + vec![(vec![0u8; 8], vec![0u8; 8])] + ); +} + +/// A binary operator that reads two float regions and a mode, and writes the result: +/// both operands and the mode reach the host, tagged by operator. +#[test] +fn float_add_reads_both_operands_and_the_mode() { + let host = FakeHost::new().answering_float(support::Answer::filler(8)); + + let wat = module( + &[import::FLOAT_ADD, ONE_PAGE], + "(call $float_add (i32.const 0) (i32.const 8) (i32.const 8) (i32.const 8) (i32.const 64) (i32.const 8) (i32.const 2))", + ); + assert_eq!(status(&wat, &host), 8, "the result length"); + assert_eq!( + *host.float_binops_asked.borrow(), + vec![("add", vec![0u8; 8], vec![0u8; 8], 2)] + ); +} + +/// A unary operator that reads one float region, an integer, and a mode: all three +/// reach the host, tagged by operator. +#[test] +fn float_root_reads_the_float_the_degree_and_the_mode() { + let host = FakeHost::new().answering_float(support::Answer::filler(8)); + + let wat = module( + &[import::FLOAT_ROOT, ONE_PAGE], + "(call $float_root (i32.const 0) (i32.const 8) (i32.const 3) (i32.const 64) (i32.const 8) (i32.const 1))", + ); + assert_eq!(status(&wat, &host), 8, "the result length"); + assert_eq!( + *host.float_unops_asked.borrow(), + vec![("root", vec![0u8; 8], 3, 1)] + ); +} + /// A leading scalar parameter reaches the host as declared. #[test] fn home_le_field_passes_the_field_selector_through() { diff --git a/crates/xrpl-wasm-vm/tests/preflight.rs b/crates/xrpl-wasm-vm/tests/preflight.rs index a3932e434d..4208e30d9d 100644 --- a/crates/xrpl-wasm-vm/tests/preflight.rs +++ b/crates/xrpl-wasm-vm/tests/preflight.rs @@ -98,7 +98,7 @@ fn a_disabled_feature_does_not_pass() { /// Every host function the ABI declares, spelled as a guest imports it. The count /// is asserted against the ABI so a function added to it cannot be left out here. -const ALL_IMPORTS: [&str; 48] = [ +const ALL_IMPORTS: [&str; 62] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -147,6 +147,20 @@ const ALL_IMPORTS: [&str; 48] = [ import::NFT_FLAGS, import::NFT_XFER_FEE, import::NFT_SERIAL, + import::FLOAT_FROM_INT, + import::FLOAT_FROM_UINT, + import::FLOAT_FROM_STAMOUNT, + import::FLOAT_FROM_STNUMBER, + import::FLOAT_TO_INT, + import::FLOAT_TO_MANT_EXP, + import::FLOAT_FROM_MANT_EXP, + import::FLOAT_CMP, + import::FLOAT_ADD, + import::FLOAT_SUB, + import::FLOAT_MULT, + import::FLOAT_DIV, + import::FLOAT_ROOT, + import::FLOAT_POW, ]; #[test] diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 73de5a4d61..350f3a1f9b 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -312,6 +312,35 @@ pub struct FakeHost { pub nft_sequences: HashMap, Answer>, /// Every nft id `get_nft_sequence` was asked for. pub nft_sequences_asked: RefCell>>, + + /// What every float-producing call writes. + pub float_answer: Answer, + /// Every `(x, mode)` `float_from_int` was asked for. + pub float_from_int_asked: RefCell>, + /// Every `(x, mode)` `float_from_uint` was asked for. + pub float_from_uint_asked: RefCell, i32)>>, + /// Every `(amount, mode)` `float_from_stamount` was asked for. + pub float_from_stamount_asked: RefCell, i32)>>, + /// Every `(number, mode)` `float_from_stnumber` was asked for. + pub float_from_stnumber_asked: RefCell, i32)>>, + /// Every `(x, mode)` `float_to_int` was asked for. + pub float_to_int_asked: RefCell, i32)>>, + /// The mantissa and exponent bytes `float_to_mant_exp` writes to its two regions. + pub float_mant_exp_answer: (Vec, Vec), + /// Every float `float_to_mant_exp` was asked for. + pub float_to_mant_exp_asked: RefCell>>, + /// Every `(mantissa, exponent, mode)` `float_from_mant_exp` was asked for. + pub float_from_mant_exp_asked: RefCell>, + /// What `float_compare` answers, whatever floats it is given. + pub float_compare_answer: HostResult, + /// Every `(x, y)` `float_compare` was asked for. + pub float_compare_asked: RefCell, Vec)>>, + /// Every `(x, y, mode)` the four binary float operators were asked for, tagged by + /// operator name. + pub float_binops_asked: RefCell, Vec, i32)>>, + /// Every `(x, n, mode)` `float_root` and `float_power` were asked for, tagged by + /// operator name. + pub float_unops_asked: RefCell, i32, i32)>>, } impl Default for FakeHost { @@ -414,6 +443,19 @@ impl Default for FakeHost { nft_fee_asked: RefCell::new(Vec::new()), nft_sequences: HashMap::new(), nft_sequences_asked: RefCell::new(Vec::new()), + float_answer: Answer::filler(8), + float_from_int_asked: RefCell::new(Vec::new()), + float_from_uint_asked: RefCell::new(Vec::new()), + float_from_stamount_asked: RefCell::new(Vec::new()), + float_from_stnumber_asked: RefCell::new(Vec::new()), + float_to_int_asked: RefCell::new(Vec::new()), + float_mant_exp_answer: (vec![0u8; 8], vec![0u8; 4]), + float_to_mant_exp_asked: RefCell::new(Vec::new()), + float_from_mant_exp_asked: RefCell::new(Vec::new()), + float_compare_answer: Ok(0), + float_compare_asked: RefCell::new(Vec::new()), + float_binops_asked: RefCell::new(Vec::new()), + float_unops_asked: RefCell::new(Vec::new()), } } } @@ -755,6 +797,21 @@ impl FakeHost { self } + pub fn answering_float(mut self, answer: Answer) -> FakeHost { + self.float_answer = answer; + self + } + + pub fn answering_float_mant_exp(mut self, mantissa: Vec, exponent: Vec) -> FakeHost { + self.float_mant_exp_answer = (mantissa, exponent); + self + } + + pub fn answering_float_compare(mut self, answer: HostResult) -> FakeHost { + self.float_compare_answer = answer; + self + } + pub fn traces(&self) -> Vec { self.traces.borrow().clone() } @@ -1201,6 +1258,114 @@ impl HostFunctions for FakeHost { None => Err(HostError::InvalidParams), } } + + fn float_from_int(&self, x: i64, mode: i32, out: &mut [u8]) -> HostResult { + self.float_from_int_asked.borrow_mut().push((x, mode)); + self.float_answer.fill(out) + } + + fn float_from_uint(&self, x: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + self.float_from_uint_asked + .borrow_mut() + .push((x.to_vec(), mode)); + self.float_answer.fill(out) + } + + fn float_from_stamount(&self, amount: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + self.float_from_stamount_asked + .borrow_mut() + .push((amount.to_vec(), mode)); + self.float_answer.fill(out) + } + + fn float_from_stnumber(&self, number: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + self.float_from_stnumber_asked + .borrow_mut() + .push((number.to_vec(), mode)); + self.float_answer.fill(out) + } + + fn float_to_int(&self, x: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + self.float_to_int_asked + .borrow_mut() + .push((x.to_vec(), mode)); + self.float_answer.fill(out) + } + + fn float_to_mant_exp( + &self, + x: &[u8], + mantissa_out: &mut [u8], + exponent_out: &mut [u8], + ) -> HostResult { + self.float_to_mant_exp_asked.borrow_mut().push(x.to_vec()); + let (mantissa, exponent) = &self.float_mant_exp_answer; + mantissa_out[..mantissa.len()].copy_from_slice(mantissa); + exponent_out[..exponent.len()].copy_from_slice(exponent); + Ok(mantissa.len() + exponent.len()) + } + + fn float_from_mant_exp( + &self, + mantissa: i64, + exponent: i32, + mode: i32, + out: &mut [u8], + ) -> HostResult { + self.float_from_mant_exp_asked + .borrow_mut() + .push((mantissa, exponent, mode)); + self.float_answer.fill(out) + } + + fn float_compare(&self, x: &[u8], y: &[u8]) -> HostResult { + self.float_compare_asked + .borrow_mut() + .push((x.to_vec(), y.to_vec())); + self.float_compare_answer + } + + fn float_add(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + self.float_binops_asked + .borrow_mut() + .push(("add", x.to_vec(), y.to_vec(), mode)); + self.float_answer.fill(out) + } + + fn float_subtract(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + self.float_binops_asked + .borrow_mut() + .push(("sub", x.to_vec(), y.to_vec(), mode)); + self.float_answer.fill(out) + } + + fn float_multiply(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + self.float_binops_asked + .borrow_mut() + .push(("mult", x.to_vec(), y.to_vec(), mode)); + self.float_answer.fill(out) + } + + fn float_divide(&self, x: &[u8], y: &[u8], mode: i32, out: &mut [u8]) -> HostResult { + self.float_binops_asked + .borrow_mut() + .push(("div", x.to_vec(), y.to_vec(), mode)); + self.float_answer.fill(out) + } + + fn float_root(&self, x: &[u8], n: i32, mode: i32, out: &mut [u8]) -> HostResult { + self.float_unops_asked + .borrow_mut() + .push(("root", x.to_vec(), n, mode)); + self.float_answer.fill(out) + } + + fn float_power(&self, x: &[u8], n: i32, mode: i32, out: &mut [u8]) -> HostResult { + self.float_unops_asked + .borrow_mut() + .push(("pow", x.to_vec(), n, mode)); + self.float_answer.fill(out) + } } // --------------------------------------------------------------------------- @@ -1275,6 +1440,21 @@ pub mod import { pub const NFT_XFER_FEE: &str = r#"(import "host_lib" "nft_xfer_fee" (func $nft_xfer_fee (param i32 i32) (result i32)))"#; pub const NFT_SERIAL: &str = r#"(import "host_lib" "nft_serial" (func $nft_serial (param i32 i32 i32 i32) (result i32)))"#; + pub const FLOAT_FROM_INT: &str = r#"(import "host_lib" "float_from_int" (func $float_from_int (param i64 i32 i32 i32) (result i32)))"#; + pub const FLOAT_FROM_UINT: &str = r#"(import "host_lib" "float_from_uint" (func $float_from_uint (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const FLOAT_FROM_STAMOUNT: &str = r#"(import "host_lib" "float_from_stamount" (func $float_from_stamount (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const FLOAT_FROM_STNUMBER: &str = r#"(import "host_lib" "float_from_stnumber" (func $float_from_stnumber (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const FLOAT_TO_INT: &str = r#"(import "host_lib" "float_to_int" (func $float_to_int (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const FLOAT_TO_MANT_EXP: &str = r#"(import "host_lib" "float_to_mant_exp" (func $float_to_mant_exp (param i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const FLOAT_FROM_MANT_EXP: &str = r#"(import "host_lib" "float_from_mant_exp" (func $float_from_mant_exp (param i64 i32 i32 i32 i32) (result i32)))"#; + pub const FLOAT_CMP: &str = + r#"(import "host_lib" "float_cmp" (func $float_cmp (param i32 i32 i32 i32) (result i32)))"#; + pub const FLOAT_ADD: &str = r#"(import "host_lib" "float_add" (func $float_add (param i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const FLOAT_SUB: &str = r#"(import "host_lib" "float_sub" (func $float_sub (param i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const FLOAT_MULT: &str = r#"(import "host_lib" "float_mult" (func $float_mult (param i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const FLOAT_DIV: &str = r#"(import "host_lib" "float_div" (func $float_div (param i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const FLOAT_ROOT: &str = r#"(import "host_lib" "float_root" (func $float_root (param i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const FLOAT_POW: &str = r#"(import "host_lib" "float_pow" (func $float_pow (param i32 i32 i32 i32 i32 i32) (result i32)))"#; } /// One page of linear memory, exported under the name the engine looks for. diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index fa44678ee0..3dcde16280 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -314,6 +314,101 @@ public: [[nodiscard]] std::int32_t getNFTSequence(rust::Slice nftId, rust::Slice out) const noexcept; + + // Float / number arithmetic. A float is an XRPL `Number` in serialized form; + // `mode` is a rounding mode. Each writes the result float bytes unless noted. + + [[nodiscard]] std::int32_t + floatFromInt(std::int64_t x, std::int32_t mode, rust::Slice out) const noexcept; + + // The integer region must be eight bytes, else `InvalidParams`. + [[nodiscard]] std::int32_t + floatFromUint( + rust::Slice x, + std::int32_t mode, + rust::Slice out) const noexcept; + + // `amount` must be a serialized `STAmount`, else `InvalidParams`. + [[nodiscard]] std::int32_t + floatFromSTAmount( + rust::Slice amount, + std::int32_t mode, + rust::Slice out) const noexcept; + + // `number` must be a serialized `STNumber`, else `InvalidParams`. + [[nodiscard]] std::int32_t + floatFromSTNumber( + rust::Slice number, + std::int32_t mode, + rust::Slice out) const noexcept; + + // Rounds the float to an integer, written as its eight little-endian bytes. + [[nodiscard]] std::int32_t + floatToInt(rust::Slice x, std::int32_t mode, rust::Slice out) + const noexcept; + + // Writes the mantissa (eight little-endian bytes) and the exponent (four little- + // endian bytes) to two output regions; returns their total size. + [[nodiscard]] std::int32_t + floatToMantExp( + rust::Slice x, + rust::Slice mantissaOut, + rust::Slice exponentOut) const noexcept; + + [[nodiscard]] std::int32_t + floatFromMantExp( + std::int64_t mantissa, + std::int32_t exponent, + std::int32_t mode, + rust::Slice out) const noexcept; + + // Returns a negative, zero, or positive scalar as `x` is less than, equal to, or + // greater than `y`, or a negative `HostFunctionError` code on failure. + [[nodiscard]] std::int32_t + floatCompare(rust::Slice x, rust::Slice y) + const noexcept; + + [[nodiscard]] std::int32_t + floatAdd( + rust::Slice x, + rust::Slice y, + std::int32_t mode, + rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + floatSubtract( + rust::Slice x, + rust::Slice y, + std::int32_t mode, + rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + floatMultiply( + rust::Slice x, + rust::Slice y, + std::int32_t mode, + rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + floatDivide( + rust::Slice x, + rust::Slice y, + std::int32_t mode, + rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + floatRoot( + rust::Slice x, + std::int32_t n, + std::int32_t mode, + rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + floatPower( + rust::Slice x, + std::int32_t n, + std::int32_t mode, + rust::Slice out) const noexcept; }; } // namespace xrpl diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 3af6aa1071..eb620c5a32 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -6,6 +6,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -15,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -90,6 +94,36 @@ parseAsset(rust::Slice bytes) return std::unexpected(HostFunctionError::InvalidParams); } +// 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. +std::expected +parseUint64(rust::Slice bytes) +{ + if (bytes.size() != sizeof(std::uint64_t)) + return std::unexpected(HostFunctionError::InvalidParams); + + std::uint64_t x = 0; + std::memcpy(&x, bytes.data(), sizeof(x)); + return adjustWasmEndianess(x); +} + +// 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. +template +std::expected +parseST(rust::Slice bytes) +{ + try + { + SerialIter sit{Slice{bytes.data(), bytes.size()}}; + return T{sit, sfGeneric}; + } + catch (std::exception const&) + { + return std::unexpected(HostFunctionError::InvalidParams); + } +} + } // namespace HostContext::HostContext(HostFunctions& hostFunctions) : hostFunctions_(hostFunctions) @@ -971,4 +1005,238 @@ HostContext::getNFTSequence(rust::Slice nftId, rust::Slice out) + const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const value = hostFunctions_.floatFromInt(x, mode); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + +std::int32_t +HostContext::floatFromUint( + rust::Slice x, + std::int32_t mode, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const parsed = parseUint64(x); + if (!parsed) + return hfErrorToInt(parsed.error()); + + auto const value = hostFunctions_.floatFromUint(*parsed, mode); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + +std::int32_t +HostContext::floatFromSTAmount( + rust::Slice amount, + std::int32_t mode, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const parsed = parseST(amount); + if (!parsed) + return hfErrorToInt(parsed.error()); + + auto const value = hostFunctions_.floatFromSTAmount(*parsed, mode); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + +std::int32_t +HostContext::floatFromSTNumber( + rust::Slice number, + std::int32_t mode, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const parsed = parseST(number); + if (!parsed) + return hfErrorToInt(parsed.error()); + + auto const value = hostFunctions_.floatFromSTNumber(*parsed, mode); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + +std::int32_t +HostContext::floatToInt( + rust::Slice x, + std::int32_t mode, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const value = hostFunctions_.floatToInt(Slice{x.data(), x.size()}, mode); + if (!value) + return hfErrorToInt(value.error()); + + return answerScalar(out, *value); + }); +} + +std::int32_t +HostContext::floatToMantExp( + rust::Slice x, + rust::Slice mantissaOut, + rust::Slice exponentOut) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const value = hostFunctions_.floatToMantExp(Slice{x.data(), x.size()}); + if (!value) + return hfErrorToInt(value.error()); + + // The engine copies each region only if the whole value fits, so writing the + // true lengths here and summing them matches its accounting. + auto const r1 = answerScalar(mantissaOut, value->first); + auto const r2 = answerScalar(exponentOut, value->second); + return r1 + r2; + }); +} + +std::int32_t +HostContext::floatFromMantExp( + std::int64_t mantissa, + std::int32_t exponent, + std::int32_t mode, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const value = hostFunctions_.floatFromMantExp(mantissa, exponent, mode); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + +std::int32_t +HostContext::floatCompare(rust::Slice x, rust::Slice y) + const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const value = + hostFunctions_.floatCompare(Slice{x.data(), x.size()}, Slice{y.data(), y.size()}); + if (!value) + return hfErrorToInt(value.error()); + + return *value; + }); +} + +std::int32_t +HostContext::floatAdd( + rust::Slice x, + rust::Slice y, + std::int32_t mode, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const value = + hostFunctions_.floatAdd(Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + +std::int32_t +HostContext::floatSubtract( + rust::Slice x, + rust::Slice y, + std::int32_t mode, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const value = hostFunctions_.floatSubtract( + Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + +std::int32_t +HostContext::floatMultiply( + rust::Slice x, + rust::Slice y, + std::int32_t mode, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const value = hostFunctions_.floatMultiply( + Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + +std::int32_t +HostContext::floatDivide( + rust::Slice x, + rust::Slice y, + std::int32_t mode, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const value = + hostFunctions_.floatDivide(Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + +std::int32_t +HostContext::floatRoot( + rust::Slice x, + std::int32_t n, + std::int32_t mode, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const value = hostFunctions_.floatRoot(Slice{x.data(), x.size()}, n, mode); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + +std::int32_t +HostContext::floatPower( + rust::Slice x, + std::int32_t n, + std::int32_t mode, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const value = hostFunctions_.floatPower(Slice{x.data(), x.size()}, n, mode); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + } // namespace xrpl