From 3a2cf64a698611eea67176b39958b392bd4ad983 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 16:20:50 -0400 Subject: [PATCH] feat: Hook up cache_le host function --- crates/xrpl-host-functions/src/lib.rs | 7 +++++ .../tests/generated_abi.rs | 8 ++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 26 +++++++++++++------ crates/xrpl-wasm-vm/src/abi.rs | 3 +++ crates/xrpl-wasm-vm/src/register.rs | 15 +++++++++++ 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 | 19 ++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 6 +++++ src/libxrpl/tx/wasm/HostContext.cpp | 16 ++++++++++++ 11 files changed, 118 insertions(+), 9 deletions(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 2fb728e728..6911a44648 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -128,6 +128,13 @@ host_functions! { #[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. + #[gas = 5000] + #[wasm_name = "cache_le"] + fn cache_ledger_obj(&self, obj_id: &[u8], cache_idx: i32) -> HostResult; + /// The serialized bytes of one field of the current (escrow) ledger object. #[gas = 70] #[wasm_name = "home_le_field"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index f3ba86c99f..e12d7dc861 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -48,6 +48,11 @@ impl HostFunctions for FakeHost { Ok(i32::from(!amendment.is_empty())) } + /// Returns a slot: the requested one, or slot 1 when asked to pick. + fn cache_ledger_obj(&self, _obj_id: &[u8], cache_idx: i32) -> HostResult { + Ok(if cache_idx == 0 { 1 } else { cache_idx }) + } + /// Fails on a field it doesn't know, so the error channel is exercised too. fn get_current_ledger_obj_field(&self, field: i32, out: &mut [u8]) -> HostResult { if field < 0 { @@ -90,6 +95,8 @@ fn the_trait_is_implementable() { assert_eq!(out[..4], [10, 0, 0, 0]); assert_eq!(host.is_amendment_enabled(&[1; 32]), Ok(1)); assert_eq!(host.is_amendment_enabled(&[]), Ok(0)); + assert_eq!(host.cache_ledger_obj(&[1; 32], 0), Ok(1)); + assert_eq!(host.cache_ledger_obj(&[1; 32], 5), Ok(5)); assert_eq!(host.get_current_ledger_obj_field(3, &mut out), Ok(1)); assert_eq!(out[0], 3); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); @@ -164,6 +171,7 @@ fn the_spec_table_matches_the_declarations() { ("parent_ldgr_hash", 60), ("base_fee", 60), ("amendment_enabled", 100), + ("cache_le", 5000), ("home_le_field", 70), ("sha512_half", 2000), ("trace", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 6314cbad74..f90d29141b 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -177,6 +177,12 @@ mod ffi { #[cxx_name = "isAmendmentEnabled"] fn is_amendment_enabled(self: &HostContext, amendment: &[u8]) -> i32; + /// Caches the object with `obj_id` in slot `cache_idx` (`0` = pick one) and + /// answers the slot used, or a negative `HostError` code. + #[namespace = "xrpl"] + #[cxx_name = "cacheLedgerObj"] + fn cache_ledger_obj(self: &HostContext, obj_id: &[u8], cache_idx: i32) -> i32; + #[namespace = "xrpl"] #[cxx_name = "getCurrentLedgerObjField"] fn get_current_ledger_obj_field(self: &HostContext, field: i32, out: &mut [u8]) -> i32; @@ -225,9 +231,9 @@ fn reported(n: i32) -> HostResult<()> { Ok(()) } -/// A call whose answer is the scalar the guest reads directly (a flag): a -/// non-negative value is that answer, a negative one its error code. -fn flag(n: i32) -> HostResult { +/// A call whose answer is a scalar the guest reads directly (a flag, a slot index): +/// a non-negative value is that answer, a negative one its error code. +fn scalar(n: i32) -> HostResult { if n < 0 { return Err(HostError::from_code(n)); } @@ -252,7 +258,11 @@ impl HostFunctions for CxxHost<'_> { } fn is_amendment_enabled(&self, amendment: &[u8]) -> HostResult { - flag(self.ctx.is_amendment_enabled(amendment)) + scalar(self.ctx.is_amendment_enabled(amendment)) + } + + fn cache_ledger_obj(&self, obj_id: &[u8], cache_idx: i32) -> HostResult { + scalar(self.ctx.cache_ledger_obj(obj_id, cache_idx)) } fn get_current_ledger_obj_field(&self, field: i32, out: &mut [u8]) -> HostResult { @@ -553,9 +563,9 @@ mod tests { assert_eq!(bytes_written(-3), Err(HostError::BufferTooSmall)); assert_eq!(reported(0), Ok(())); assert_eq!(reported(-14), Err(HostError::NoMemExported)); - assert_eq!(flag(1), Ok(1)); - assert_eq!(flag(0), Ok(0)); - assert_eq!(flag(-2), Err(HostError::FieldNotFound)); + assert_eq!(scalar(1), Ok(1)); + assert_eq!(scalar(0), Ok(0)); + assert_eq!(scalar(-2), Err(HostError::FieldNotFound)); } /// An exception caught on the C++ side arrives as `-1`, which has to reach the @@ -565,7 +575,7 @@ mod tests { fn a_caught_cxx_exception_arrives_as_internal() { assert_eq!(bytes_written(-1), Err(HostError::Internal)); assert_eq!(reported(-1), Err(HostError::Internal)); - assert_eq!(flag(-1), Err(HostError::Internal)); + assert_eq!(scalar(-1), Err(HostError::Internal)); } // ----------------------------------------------------------------------- diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 02f284cfdc..ff16f17454 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -200,6 +200,9 @@ mod tests { fn is_amendment_enabled(&self, _amendment: &[u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn cache_ledger_obj(&self, _obj_id: &[u8], _cache_idx: i32) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn get_current_ledger_obj_field(&self, _field: i32, _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } diff --git a/crates/xrpl-wasm-vm/src/register.rs b/crates/xrpl-wasm-vm/src/register.rs index 7de7b569bb..dceaf0f2c6 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -88,6 +88,21 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::CacheLedgerObj => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + id_ptr: i32, + id_len: i32, + cache_idx: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::CacheLedgerObj, |c| { + let host = c.data().host; + let obj_id = read_borrowed(c, Region::new(id_ptr, id_len))?; + host.cache_ledger_obj(obj_id, cache_idx) + }) + }, + ), HostFunctionSpec::GetCurrentLedgerObjField => linker.func_wrap( HOST_MODULE, op.wasm_name(), diff --git a/crates/xrpl-wasm-vm/tests/budgets.rs b/crates/xrpl-wasm-vm/tests/budgets.rs index 7542748e8b..a8a5f8d79a 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -76,6 +76,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $amendment_enabled (i32.const 0) (i32.const 32))", 2, ), + HostFunctionSpec::CacheLedgerObj => ( + import::CACHE_LE, + "(call $cache_le (i32.const 0) (i32.const 32) (i32.const 0))", + 3, + ), HostFunctionSpec::GetCurrentLedgerObjField => ( import::HOME_LE_FIELD, "(call $home_le_field (i32.const 1) (i32.const 0) (i32.const 4))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 3ebd26b321..fe7700bdcf 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -120,6 +120,25 @@ fn amendment_enabled_reads_the_input_and_returns_the_flag() { assert_eq!(status(&wat, &host), 0, "the disabled flag"); } +/// A call that reads an input region and takes a second scalar arg: both the object +/// id and the requested slot reach the host, and the slot it chose comes back as the +/// status. +#[test] +fn cache_le_passes_the_object_id_and_slot_through() { + let host = FakeHost::new().answering_cache_slot(Ok(4)); + + let wat = module( + &[import::CACHE_LE, ONE_PAGE], + "(call $cache_le (i32.const 64) (i32.const 32) (i32.const 7))", + ); + assert_eq!(status(&wat, &host), 4, "the slot the host chose"); + assert_eq!( + *host.cached.borrow(), + [(vec![0u8; 32], 7)], + "the id region and the requested slot reached the host" + ); +} + /// The output region is wherever the guest points, not a fixed address. #[test] fn the_output_region_is_the_pointer_the_guest_gave() { diff --git a/crates/xrpl-wasm-vm/tests/preflight.rs b/crates/xrpl-wasm-vm/tests/preflight.rs index 01af4598b5..e3ce04b519 100644 --- a/crates/xrpl-wasm-vm/tests/preflight.rs +++ b/crates/xrpl-wasm-vm/tests/preflight.rs @@ -98,12 +98,13 @@ 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; 9] = [ +const ALL_IMPORTS: [&str; 10] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, import::BASE_FEE, import::AMENDMENT_ENABLED, + import::CACHE_LE, import::HOME_LE_FIELD, import::SHA512_HALF, import::TRACE, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index d9d01a5bd1..33a0aa475c 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -113,6 +113,10 @@ pub struct FakeHost { pub amendment_enabled: HostResult, /// Every amendment `is_amendment_enabled` was asked about. pub amendments_asked: RefCell>>, + /// What `cache_ledger_obj` answers: the slot it "used". + pub cache_slot: HostResult, + /// Every (object id, requested slot) `cache_ledger_obj` was asked to cache. + pub cached: RefCell, i32)>>, /// What `get_current_ledger_obj_field` answers, by field selector. An /// unlisted selector answers `FieldNotFound`. pub fields: HashMap, @@ -141,6 +145,9 @@ impl Default for FakeHost { // Enabled by default; the id-or-name dispatch is the host's job, not the ABI's. amendment_enabled: Ok(1), amendments_asked: RefCell::new(Vec::new()), + // Slot 1 by default; slot assignment is the host's job, not the ABI's. + cache_slot: Ok(1), + cached: RefCell::new(Vec::new()), fields: HashMap::new(), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), @@ -180,6 +187,11 @@ impl FakeHost { self } + pub fn answering_cache_slot(mut self, answer: HostResult) -> FakeHost { + self.cache_slot = answer; + self + } + pub fn answering_field(mut self, field: i32, answer: Answer) -> FakeHost { self.fields.insert(field, answer); self @@ -217,6 +229,11 @@ impl HostFunctions for FakeHost { self.amendment_enabled } + fn cache_ledger_obj(&self, obj_id: &[u8], cache_idx: i32) -> HostResult { + self.cached.borrow_mut().push((obj_id.to_vec(), cache_idx)); + self.cache_slot + } + fn get_current_ledger_obj_field(&self, field: i32, out: &mut [u8]) -> HostResult { self.fields_asked.borrow_mut().push(field); match self.fields.get(&field) { @@ -263,6 +280,8 @@ pub mod import { pub const BASE_FEE: &str = r#"(import "host_lib" "base_fee" (func $base_fee (param i32 i32) (result i32)))"#; pub const AMENDMENT_ENABLED: &str = r#"(import "host_lib" "amendment_enabled" (func $amendment_enabled (param i32 i32) (result i32)))"#; + pub const CACHE_LE: &str = + r#"(import "host_lib" "cache_le" (func $cache_le (param i32 i32 i32) (result i32)))"#; pub const HOME_LE_FIELD: &str = r#"(import "host_lib" "home_le_field" (func $home_le_field (param i32 i32 i32) (result i32)))"#; pub const SHA512_HALF: &str = r#"(import "host_lib" "sha512_half" (func $sha512_half (param i32 i32 i32 i32) (result i32)))"#; pub const TRACE: &str = diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 8c5b08ddd9..3b358aefc5 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -59,6 +59,12 @@ public: [[nodiscard]] std::int32_t isAmendmentEnabled(rust::Slice amendment) const noexcept; + // The object id must be a 32-byte uint256, else `InvalidParams`. `cacheIdx` selects + // the slot (0 = pick a free one). Answers the slot used, or a negative + // `HostFunctionError` code. + [[nodiscard]] std::int32_t + cacheLedgerObj(rust::Slice objId, std::int32_t cacheIdx) const noexcept; + [[nodiscard]] std::int32_t getCurrentLedgerObjField(std::int32_t field, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 2a932061de..e1815a487d 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -135,6 +135,22 @@ HostContext::isAmendmentEnabled(rust::Slice amendment) const }); } +std::int32_t +HostContext::cacheLedgerObj(rust::Slice objId, std::int32_t cacheIdx) + const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (objId.size() != uint256::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const slot = hostFunctions_.cacheLedgerObj(uint256::fromVoid(objId.data()), cacheIdx); + if (!slot) + return hfErrorToInt(slot.error()); + + return *slot; + }); +} + std::int32_t HostContext::getCurrentLedgerObjField(std::int32_t field, rust::Slice out) const noexcept