From 98cf3a05328b60107412ef4ef8d2b04014c095f4 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 21:58:24 -0400 Subject: [PATCH] feat: Hook up set_data host function --- crates/xrpl-host-functions/src/lib.rs | 7 +++++++ .../tests/generated_abi.rs | 7 +++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 ++++++++ crates/xrpl-wasm-vm/src/abi.rs | 3 +++ crates/xrpl-wasm-vm/src/register.rs | 14 ++++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 +++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 19 +++++++++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 18 ++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 5 +++++ src/libxrpl/tx/wasm/HostContext.cpp | 12 ++++++++++++ 11 files changed, 100 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 7f51efd52c..c44b4add21 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -420,4 +420,11 @@ host_functions! { #[gas = 500] #[wasm_name = "trace_num"] fn trace_num(&self, msg: &str, number: i64) -> 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. + #[gas = 1000] + #[wasm_name = "set_data"] + fn update_data(&self, data: &[u8]) -> HostResult; } diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 4bf9c7dddb..db520485c3 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -401,6 +401,11 @@ impl HostFunctions for FakeHost { self.traced.borrow_mut().push(format!("{msg}={number}")); Ok(()) } + + /// Reads a data blob and returns the count of bytes stored. + fn update_data(&self, data: &[u8]) -> HostResult { + Ok(data.len() as i32) + } } #[test] @@ -615,6 +620,7 @@ fn the_trait_is_implementable() { assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); assert_eq!(host.trace_num("count", -1), Ok(())); + assert_eq!(host.update_data(b"abcd"), Ok(4)); assert_eq!(*host.traced.borrow(), ["hello/2/true", "count=-1"]); } @@ -719,6 +725,7 @@ fn the_spec_table_matches_the_declarations() { ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), + ("set_data", 1000), ] ); } diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 5eac97b36b..3e1a766816 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -391,6 +391,10 @@ mod ffi { #[namespace = "xrpl"] #[cxx_name = "traceNum"] fn trace_num(self: &HostContext, msg: &str, number: i64) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "updateData"] + fn update_data(self: &HostContext, data: &[u8]) -> i32; } } @@ -657,6 +661,10 @@ impl HostFunctions for CxxHost<'_> { fn trace_num(&self, msg: &str, number: i64) -> HostResult<()> { reported(self.ctx.trace_num(msg, number)) } + + fn update_data(&self, data: &[u8]) -> HostResult { + scalar(self.ctx.update_data(data)) + } } fn run_escrow( diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 2ecdbeb816..f009278ba5 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -384,6 +384,9 @@ mod tests { fn trace_num(&self, _msg: &str, _number: i64) -> HostResult<()> { unreachable!("no unit test in this module calls the host") } + fn update_data(&self, _data: &[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 aaea45ac46..1919117b20 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -781,6 +781,20 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::UpdateData => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + ptr: i32, + len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::UpdateData, |c| { + let host = c.data().host; + let data = read_borrowed(c, Region::new(ptr, len))?; + host.update_data(data) + }) + }, + ), }?; } Ok(()) diff --git a/crates/xrpl-wasm-vm/tests/budgets.rs b/crates/xrpl-wasm-vm/tests/budgets.rs index a7a38f1e15..5faf35a512 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -254,6 +254,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $trace_num (i32.const 0) (i32.const 0) (i64.const 0))", 3, ), + HostFunctionSpec::UpdateData => ( + import::SET_DATA, + "(call $set_data (i32.const 0) (i32.const 8))", + 2, + ), }; Call { import, diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index d8c2fe1b16..3d9415d87e 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -748,6 +748,25 @@ fn vault_id_reads_the_account_and_seq() { assert_eq!(*host.vault_keylets_asked.borrow(), vec![(account, 5)]); } +/// A call that reads an input region and returns a scalar rather than writing bytes: +/// the data blob reaches the host, and the byte count it reports comes back as the +/// call's status. +#[test] +fn set_data_passes_the_data_through_and_returns_the_count() { + let host = FakeHost::new().answering_update_data(Ok(8)); + + let wat = module( + &[import::SET_DATA, ONE_PAGE], + "(call $set_data (i32.const 64) (i32.const 8))", + ); + assert_eq!(status(&wat, &host), 8, "the byte count the host reported"); + assert_eq!( + *host.update_data_asked.borrow(), + [vec![0u8; 8]], + "the 8-byte region reached the host" + ); +} + /// 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 912b0eed7e..056f8066a6 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; 41] = [ +const ALL_IMPORTS: [&str; 42] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -140,6 +140,7 @@ const ALL_IMPORTS: [&str; 41] = [ import::SHA512_HALF, import::TRACE, import::TRACE_NUM, + import::SET_DATA, ]; #[test] diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 861019940b..09f81e2b35 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -282,6 +282,10 @@ pub struct FakeHost { pub digested: RefCell>>, /// Every `trace`/`trace_num` call, in order. pub traces: RefCell>, + /// What `update_data` answers, whatever data it is given. + pub update_data_answer: HostResult, + /// Every data blob `update_data` was given. + pub update_data_asked: RefCell>>, } impl Default for FakeHost { @@ -370,6 +374,8 @@ impl Default for FakeHost { fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), traces: RefCell::new(Vec::new()), + update_data_answer: Ok(0), + update_data_asked: RefCell::new(Vec::new()), } } } @@ -671,6 +677,11 @@ impl FakeHost { self } + pub fn answering_update_data(mut self, answer: HostResult) -> FakeHost { + self.update_data_answer = answer; + self + } + pub fn traces(&self) -> Vec { self.traces.borrow().clone() } @@ -1069,6 +1080,11 @@ impl HostFunctions for FakeHost { }); Ok(()) } + + fn update_data(&self, data: &[u8]) -> HostResult { + self.update_data_asked.borrow_mut().push(data.to_vec()); + self.update_data_answer + } } // --------------------------------------------------------------------------- @@ -1132,6 +1148,8 @@ pub mod import { r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; pub const TRACE_NUM: &str = r#"(import "host_lib" "trace_num" (func $trace_num (param i32 i32 i64) (result i32)))"#; + pub const SET_DATA: &str = + r#"(import "host_lib" "set_data" (func $set_data (param 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 8b82c83c9a..14e7838bd1 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -273,6 +273,11 @@ public: [[nodiscard]] std::int32_t traceNum(rust::Str msg, std::int64_t number) const noexcept; + + // Stores `data` as the current object's data field and returns the number of bytes + // stored, or a negative `HostFunctionError` code. + [[nodiscard]] std::int32_t + updateData(rust::Slice data) const noexcept; }; } // namespace xrpl diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 5291fed8f2..2aea22d671 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -862,4 +862,16 @@ HostContext::traceNum(rust::Str msg, std::int64_t number) const noexcept }); } +std::int32_t +HostContext::updateData(rust::Slice data) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const stored = hostFunctions_.updateData(Slice{data.data(), data.size()}); + if (!stored) + return hfErrorToInt(stored.error()); + + return *stored; + }); +} + } // namespace xrpl