From 8da36db515f8cb6abf189d4429fa5e490f7d3d2b Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 14:55:57 -0400 Subject: [PATCH 01/46] feat: Hook up parent_ldgr_time host function --- crates/xrpl-host-functions/src/lib.rs | 5 +++++ .../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 | 13 +++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 +++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 22 +++++++++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 15 +++++++++++++ include/xrpl/tx/wasm/HostContext.h | 3 +++ src/libxrpl/tx/wasm/HostContext.cpp | 12 ++++++++++ 11 files changed, 95 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index c2082a9d0f..c2e5de4827 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -106,6 +106,11 @@ host_functions! { #[wasm_name = "ldgr_index"] fn get_ledger_sqn(&self, out: &mut [u8]) -> HostResult; + /// The close time of the parent (last-closed) ledger, as 4 little-endian bytes. + #[gas = 60] + #[wasm_name = "parent_ldgr_time"] + fn get_parent_ledger_time(&self, out: &mut [u8]) -> 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 a760a2b3af..16e8336f39 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -31,6 +31,10 @@ impl HostFunctions for FakeHost { put(out, &7u32.to_le_bytes()) } + fn get_parent_ledger_time(&self, out: &mut [u8]) -> HostResult { + put(out, &9u32.to_le_bytes()) + } + /// 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 { @@ -65,6 +69,8 @@ fn the_trait_is_implementable() { assert_eq!(host.get_ledger_sqn(&mut out), Ok(4)); assert_eq!(out[..4], [7, 0, 0, 0]); + assert_eq!(host.get_parent_ledger_time(&mut out), Ok(4)); + assert_eq!(out[..4], [9, 0, 0, 0]); 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)); @@ -135,6 +141,7 @@ fn the_spec_table_matches_the_declarations() { table, [ ("ldgr_index", 60), + ("parent_ldgr_time", 60), ("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 5a4c77048e..9a68c16c9c 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -159,6 +159,10 @@ mod ffi { #[cxx_name = "getLedgerSqn"] fn get_ledger_sqn(self: &HostContext, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "getParentLedgerTime"] + fn get_parent_ledger_time(self: &HostContext, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "getCurrentLedgerObjField"] fn get_current_ledger_obj_field(self: &HostContext, field: i32, out: &mut [u8]) -> i32; @@ -212,6 +216,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.get_ledger_sqn(out)) } + fn get_parent_ledger_time(&self, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.get_parent_ledger_time(out)) + } + fn get_current_ledger_obj_field(&self, field: i32, out: &mut [u8]) -> HostResult { bytes_written(self.ctx.get_current_ledger_obj_field(field, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index db6db25fda..b6c828598a 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -188,6 +188,9 @@ mod tests { fn get_ledger_sqn(&self, _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_parent_ledger_time(&self, _out: &mut [u8]) -> 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 ef933f1bf9..28a7c5830d 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -35,6 +35,19 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::GetParentLedgerTime => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetParentLedgerTime, |c| { + let out = Region::new(out_ptr, out_len); + write_into(c, out, |host, out| host.get_parent_ledger_time(out)) + }) + }, + ), 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 9b7ac613ed..fabe7c86f6 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -56,6 +56,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $ldgr_index (i32.const 0) (i32.const 4))", 2, ), + HostFunctionSpec::GetParentLedgerTime => ( + import::PARENT_LDGR_TIME, + "(call $parent_ldgr_time (i32.const 0) (i32.const 4))", + 2, + ), 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 d9987d58ea..c6981e1f12 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -27,6 +27,28 @@ fn ldgr_index_writes_the_sequence_number_where_the_guest_asked() { assert_eq!(status(&wat, &host), 4, "the byte count"); } +/// A second scalar getter travels the same path: the value the host supplies lands +/// where the guest asked, and the status is the byte count. The default parent +/// ledger time is distinct from the sequence number, so this cannot pass by reading +/// the wrong one. +#[test] +fn parent_ldgr_time_writes_the_close_time_where_the_guest_asked() { + let host = FakeHost::new(); + + let wat = module( + &[import::PARENT_LDGR_TIME, ONE_PAGE], + "(drop (call $parent_ldgr_time (i32.const 64) (i32.const 4))) + (i32.load (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 9, "the 4 LE bytes the host wrote"); + + let wat = module( + &[import::PARENT_LDGR_TIME, ONE_PAGE], + "(call $parent_ldgr_time (i32.const 64) (i32.const 4))", + ); + assert_eq!(status(&wat, &host), 4, "the byte count"); +} + /// 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 cfa29883da..99b115ee29 100644 --- a/crates/xrpl-wasm-vm/tests/preflight.rs +++ b/crates/xrpl-wasm-vm/tests/preflight.rs @@ -98,8 +98,9 @@ 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; 5] = [ +const ALL_IMPORTS: [&str; 6] = [ import::LDGR_INDEX, + import::PARENT_LDGR_TIME, 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 80ab51395e..8f280596fe 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -103,6 +103,8 @@ pub enum Trace { pub struct FakeHost { /// What `get_ledger_sqn` answers. pub ledger_sqn: Answer, + /// What `get_parent_ledger_time` answers. + pub parent_ledger_time: Answer, /// What `get_current_ledger_obj_field` answers, by field selector. An /// unlisted selector answers `FieldNotFound`. pub fields: HashMap, @@ -121,6 +123,9 @@ impl Default for FakeHost { FakeHost { // 4 little-endian bytes, as the declaration's doc comment specifies. ledger_sqn: Answer::bytes(7u32.to_le_bytes()), + // A distinct value from the sequence number, so a test cannot pass by + // reading one where it meant the other. + parent_ledger_time: Answer::bytes(9u32.to_le_bytes()), fields: HashMap::new(), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), @@ -140,6 +145,11 @@ impl FakeHost { self } + pub fn answering_parent_ledger_time(mut self, answer: Answer) -> FakeHost { + self.parent_ledger_time = answer; + self + } + pub fn answering_field(mut self, field: i32, answer: Answer) -> FakeHost { self.fields.insert(field, answer); self @@ -160,6 +170,10 @@ impl HostFunctions for FakeHost { self.ledger_sqn.fill(out) } + fn get_parent_ledger_time(&self, out: &mut [u8]) -> HostResult { + self.parent_ledger_time.fill(out) + } + 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) { @@ -201,6 +215,7 @@ impl HostFunctions for FakeHost { pub mod import { pub const LDGR_INDEX: &str = r#"(import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))"#; + pub const PARENT_LDGR_TIME: &str = r#"(import "host_lib" "parent_ldgr_time" (func $parent_ldgr_time (param 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 7f0f71f26a..cebeb52e0c 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -44,6 +44,9 @@ public: [[nodiscard]] std::int32_t getLedgerSqn(rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t + getParentLedgerTime(rust::Slice out) 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 bdf22a802e..60dcb7ba2f 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -71,6 +71,18 @@ HostContext::getLedgerSqn(rust::Slice out) const noexcept }); } +std::int32_t +HostContext::getParentLedgerTime(rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const time = hostFunctions_.getParentLedgerTime(); + if (!time) + return hfErrorToInt(time.error()); + + return answerScalar(out, *time); + }); +} + std::int32_t HostContext::getCurrentLedgerObjField(std::int32_t field, rust::Slice out) const noexcept From 6abd492ebb5e8233e2adfb620addb530d338bb07 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 15:04:35 -0400 Subject: [PATCH 02/46] feat: Hook up parent_ldgr_hash host function --- crates/xrpl-host-functions/src/lib.rs | 5 ++++ .../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 | 13 ++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 25 +++++++++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 14 +++++++++++ include/xrpl/tx/wasm/HostContext.h | 3 +++ src/libxrpl/tx/wasm/HostContext.cpp | 12 +++++++++ 11 files changed, 97 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index c2e5de4827..a9ff193d3f 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -111,6 +111,11 @@ host_functions! { #[wasm_name = "parent_ldgr_time"] fn get_parent_ledger_time(&self, out: &mut [u8]) -> HostResult; + /// The hash of the parent (last-closed) ledger, as 32 bytes. + #[gas = 60] + #[wasm_name = "parent_ldgr_hash"] + fn get_parent_ledger_hash(&self, out: &mut [u8]) -> 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 16e8336f39..ce306e9093 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -35,6 +35,10 @@ impl HostFunctions for FakeHost { put(out, &9u32.to_le_bytes()) } + fn get_parent_ledger_hash(&self, out: &mut [u8]) -> HostResult { + put(out, &[0xab; HASH_LEN]) + } + /// 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 { @@ -71,6 +75,8 @@ fn the_trait_is_implementable() { assert_eq!(out[..4], [7, 0, 0, 0]); assert_eq!(host.get_parent_ledger_time(&mut out), Ok(4)); assert_eq!(out[..4], [9, 0, 0, 0]); + assert_eq!(host.get_parent_ledger_hash(&mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 0xab); 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)); @@ -142,6 +148,7 @@ fn the_spec_table_matches_the_declarations() { [ ("ldgr_index", 60), ("parent_ldgr_time", 60), + ("parent_ldgr_hash", 60), ("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 9a68c16c9c..00939b6e26 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -163,6 +163,10 @@ mod ffi { #[cxx_name = "getParentLedgerTime"] fn get_parent_ledger_time(self: &HostContext, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "getParentLedgerHash"] + fn get_parent_ledger_hash(self: &HostContext, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "getCurrentLedgerObjField"] fn get_current_ledger_obj_field(self: &HostContext, field: i32, out: &mut [u8]) -> i32; @@ -220,6 +224,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.get_parent_ledger_time(out)) } + fn get_parent_ledger_hash(&self, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.get_parent_ledger_hash(out)) + } + fn get_current_ledger_obj_field(&self, field: i32, out: &mut [u8]) -> HostResult { bytes_written(self.ctx.get_current_ledger_obj_field(field, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index b6c828598a..8d018454e5 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -191,6 +191,9 @@ mod tests { fn get_parent_ledger_time(&self, _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_parent_ledger_hash(&self, _out: &mut [u8]) -> 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 28a7c5830d..89678afe26 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -48,6 +48,19 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::GetParentLedgerHash => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetParentLedgerHash, |c| { + let out = Region::new(out_ptr, out_len); + write_into(c, out, |host, out| host.get_parent_ledger_hash(out)) + }) + }, + ), 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 fabe7c86f6..ff732879b4 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -61,6 +61,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $parent_ldgr_time (i32.const 0) (i32.const 4))", 2, ), + HostFunctionSpec::GetParentLedgerHash => ( + import::PARENT_LDGR_HASH, + "(call $parent_ldgr_hash (i32.const 0) (i32.const 32))", + 2, + ), 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 c6981e1f12..1b1956e4a9 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -49,6 +49,31 @@ fn parent_ldgr_time_writes_the_close_time_where_the_guest_asked() { assert_eq!(status(&wat, &host), 4, "the byte count"); } +/// A 32-byte value (a ledger hash) travels the same getter path as the 4-byte +/// scalars: every byte lands where the guest asked, and the status is the length. +#[test] +fn parent_ldgr_hash_writes_all_32_bytes_where_the_guest_asked() { + let host = FakeHost::new(); + + let wat = module( + &[import::PARENT_LDGR_HASH, ONE_PAGE], + "(call $parent_ldgr_hash (i32.const 64) (i32.const 32))", + ); + assert_eq!(status(&wat, &host), 32, "the byte count"); + + // The default hash is 0, 1, 2, ..., so its first four bytes load as 0x03020100. + let wat = module( + &[import::PARENT_LDGR_HASH, ONE_PAGE], + "(drop (call $parent_ldgr_hash (i32.const 64) (i32.const 32))) + (i32.load (i32.const 64))", + ); + assert_eq!( + status(&wat, &host), + 0x03020100, + "the first four bytes the host wrote" + ); +} + /// 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 99b115ee29..c097c0a3bc 100644 --- a/crates/xrpl-wasm-vm/tests/preflight.rs +++ b/crates/xrpl-wasm-vm/tests/preflight.rs @@ -98,9 +98,10 @@ 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; 6] = [ +const ALL_IMPORTS: [&str; 7] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, + import::PARENT_LDGR_HASH, 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 8f280596fe..3d063b61b1 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -105,6 +105,8 @@ pub struct FakeHost { pub ledger_sqn: Answer, /// What `get_parent_ledger_time` answers. pub parent_ledger_time: Answer, + /// What `get_parent_ledger_hash` answers. + pub parent_ledger_hash: Answer, /// What `get_current_ledger_obj_field` answers, by field selector. An /// unlisted selector answers `FieldNotFound`. pub fields: HashMap, @@ -126,6 +128,8 @@ impl Default for FakeHost { // A distinct value from the sequence number, so a test cannot pass by // reading one where it meant the other. parent_ledger_time: Answer::bytes(9u32.to_le_bytes()), + // 32 bytes counting up from 0, the length of a real ledger hash. + parent_ledger_hash: Answer::filler(32), fields: HashMap::new(), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), @@ -150,6 +154,11 @@ impl FakeHost { self } + pub fn answering_parent_ledger_hash(mut self, answer: Answer) -> FakeHost { + self.parent_ledger_hash = answer; + self + } + pub fn answering_field(mut self, field: i32, answer: Answer) -> FakeHost { self.fields.insert(field, answer); self @@ -174,6 +183,10 @@ impl HostFunctions for FakeHost { self.parent_ledger_time.fill(out) } + fn get_parent_ledger_hash(&self, out: &mut [u8]) -> HostResult { + self.parent_ledger_hash.fill(out) + } + 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) { @@ -216,6 +229,7 @@ pub mod import { pub const LDGR_INDEX: &str = r#"(import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))"#; pub const PARENT_LDGR_TIME: &str = r#"(import "host_lib" "parent_ldgr_time" (func $parent_ldgr_time (param i32 i32) (result i32)))"#; + pub const PARENT_LDGR_HASH: &str = r#"(import "host_lib" "parent_ldgr_hash" (func $parent_ldgr_hash (param 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 cebeb52e0c..ca287762e5 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -47,6 +47,9 @@ public: [[nodiscard]] std::int32_t getParentLedgerTime(rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t + getParentLedgerHash(rust::Slice out) 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 60dcb7ba2f..9bdac61f35 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -83,6 +83,18 @@ HostContext::getParentLedgerTime(rust::Slice out) const noexcept }); } +std::int32_t +HostContext::getParentLedgerHash(rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const hash = hostFunctions_.getParentLedgerHash(); + if (!hash) + return hfErrorToInt(hash.error()); + + return answer(out, hash->data(), hash->size()); + }); +} + std::int32_t HostContext::getCurrentLedgerObjField(std::int32_t field, rust::Slice out) const noexcept From 9a6efde771295f50335388fa1d24cf9d1f7fd9ca Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 15:19:52 -0400 Subject: [PATCH 03/46] feat: Hook up base_fee host function --- crates/xrpl-host-functions/src/lib.rs | 5 +++++ .../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 | 13 +++++++++++++ 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 | 15 +++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 3 +++ src/libxrpl/tx/wasm/HostContext.cpp | 12 ++++++++++++ 11 files changed, 92 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index a9ff193d3f..6eb070bf09 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -116,6 +116,11 @@ host_functions! { #[wasm_name = "parent_ldgr_hash"] fn get_parent_ledger_hash(&self, out: &mut [u8]) -> HostResult; + /// The base fee of the ledger being built, in drops, as 4 little-endian bytes. + #[gas = 60] + #[wasm_name = "base_fee"] + fn get_base_fee(&self, out: &mut [u8]) -> 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 ce306e9093..f026c31001 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -39,6 +39,10 @@ impl HostFunctions for FakeHost { put(out, &[0xab; HASH_LEN]) } + fn get_base_fee(&self, out: &mut [u8]) -> HostResult { + put(out, &10u32.to_le_bytes()) + } + /// 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 { @@ -77,6 +81,8 @@ fn the_trait_is_implementable() { assert_eq!(out[..4], [9, 0, 0, 0]); assert_eq!(host.get_parent_ledger_hash(&mut out), Ok(HASH_LEN)); assert_eq!(out[0], 0xab); + assert_eq!(host.get_base_fee(&mut out), Ok(4)); + assert_eq!(out[..4], [10, 0, 0, 0]); 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)); @@ -149,6 +155,7 @@ fn the_spec_table_matches_the_declarations() { ("ldgr_index", 60), ("parent_ldgr_time", 60), ("parent_ldgr_hash", 60), + ("base_fee", 60), ("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 00939b6e26..999515c918 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -167,6 +167,10 @@ mod ffi { #[cxx_name = "getParentLedgerHash"] fn get_parent_ledger_hash(self: &HostContext, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "getBaseFee"] + fn get_base_fee(self: &HostContext, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "getCurrentLedgerObjField"] fn get_current_ledger_obj_field(self: &HostContext, field: i32, out: &mut [u8]) -> i32; @@ -228,6 +232,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.get_parent_ledger_hash(out)) } + fn get_base_fee(&self, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.get_base_fee(out)) + } + fn get_current_ledger_obj_field(&self, field: i32, out: &mut [u8]) -> HostResult { bytes_written(self.ctx.get_current_ledger_obj_field(field, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 8d018454e5..f5998af76c 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -194,6 +194,9 @@ mod tests { fn get_parent_ledger_hash(&self, _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_base_fee(&self, _out: &mut [u8]) -> 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 89678afe26..6dad076d90 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -61,6 +61,19 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::GetBaseFee => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetBaseFee, |c| { + let out = Region::new(out_ptr, out_len); + write_into(c, out, |host, out| host.get_base_fee(out)) + }) + }, + ), 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 ff732879b4..3896edaa10 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -66,6 +66,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $parent_ldgr_hash (i32.const 0) (i32.const 32))", 2, ), + HostFunctionSpec::GetBaseFee => ( + import::BASE_FEE, + "(call $base_fee (i32.const 0) (i32.const 4))", + 2, + ), 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 1b1956e4a9..45ed5c68c7 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -74,6 +74,25 @@ fn parent_ldgr_hash_writes_all_32_bytes_where_the_guest_asked() { ); } +/// A third scalar getter, to pin the pattern rather than a single instance of it. +#[test] +fn base_fee_writes_the_fee_where_the_guest_asked() { + let host = FakeHost::new(); + + let wat = module( + &[import::BASE_FEE, ONE_PAGE], + "(drop (call $base_fee (i32.const 64) (i32.const 4))) + (i32.load (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 10, "the 4 LE bytes the host wrote"); + + let wat = module( + &[import::BASE_FEE, ONE_PAGE], + "(call $base_fee (i32.const 64) (i32.const 4))", + ); + assert_eq!(status(&wat, &host), 4, "the byte count"); +} + /// 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 c097c0a3bc..f8eb7bca4a 100644 --- a/crates/xrpl-wasm-vm/tests/preflight.rs +++ b/crates/xrpl-wasm-vm/tests/preflight.rs @@ -98,10 +98,11 @@ 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; 7] = [ +const ALL_IMPORTS: [&str; 8] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, + import::BASE_FEE, 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 3d063b61b1..a4da822a6b 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -107,6 +107,8 @@ pub struct FakeHost { pub parent_ledger_time: Answer, /// What `get_parent_ledger_hash` answers. pub parent_ledger_hash: Answer, + /// What `get_base_fee` answers. + pub base_fee: Answer, /// What `get_current_ledger_obj_field` answers, by field selector. An /// unlisted selector answers `FieldNotFound`. pub fields: HashMap, @@ -130,6 +132,8 @@ impl Default for FakeHost { parent_ledger_time: Answer::bytes(9u32.to_le_bytes()), // 32 bytes counting up from 0, the length of a real ledger hash. parent_ledger_hash: Answer::filler(32), + // A distinct value again, so no getter can pass by reading another's answer. + base_fee: Answer::bytes(10u32.to_le_bytes()), fields: HashMap::new(), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), @@ -159,6 +163,11 @@ impl FakeHost { self } + pub fn answering_base_fee(mut self, answer: Answer) -> FakeHost { + self.base_fee = answer; + self + } + pub fn answering_field(mut self, field: i32, answer: Answer) -> FakeHost { self.fields.insert(field, answer); self @@ -187,6 +196,10 @@ impl HostFunctions for FakeHost { self.parent_ledger_hash.fill(out) } + fn get_base_fee(&self, out: &mut [u8]) -> HostResult { + self.base_fee.fill(out) + } + 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) { @@ -230,6 +243,8 @@ pub mod import { r#"(import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))"#; pub const PARENT_LDGR_TIME: &str = r#"(import "host_lib" "parent_ldgr_time" (func $parent_ldgr_time (param i32 i32) (result i32)))"#; pub const PARENT_LDGR_HASH: &str = r#"(import "host_lib" "parent_ldgr_hash" (func $parent_ldgr_hash (param i32 i32) (result i32)))"#; + pub const BASE_FEE: &str = + r#"(import "host_lib" "base_fee" (func $base_fee (param 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 ca287762e5..7f3767dd41 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -50,6 +50,9 @@ public: [[nodiscard]] std::int32_t getParentLedgerHash(rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t + getBaseFee(rust::Slice out) 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 9bdac61f35..c67387024f 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -95,6 +95,18 @@ HostContext::getParentLedgerHash(rust::Slice out) const noexcept }); } +std::int32_t +HostContext::getBaseFee(rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const fee = hostFunctions_.getBaseFee(); + if (!fee) + return hfErrorToInt(fee.error()); + + return answerScalar(out, *fee); + }); +} + std::int32_t HostContext::getCurrentLedgerObjField(std::int32_t field, rust::Slice out) const noexcept From accd0cac6ca7788d43eef6502af5106d01cfd1eb Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 16:06:05 -0400 Subject: [PATCH 04/46] feat: Hook up amendment_enabled host function --- crates/xrpl-host-functions/src/lib.rs | 7 +++++ .../tests/generated_abi.rs | 8 ++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 23 +++++++++++++++ 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 | 27 ++++++++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 18 ++++++++++++ include/xrpl/tx/wasm/HostContext.h | 6 ++++ src/libxrpl/tx/wasm/HostContext.cpp | 28 +++++++++++++++++++ 11 files changed, 141 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 6eb070bf09..2fb728e728 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -121,6 +121,13 @@ 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. + #[gas = 100] + #[wasm_name = "amendment_enabled"] + fn is_amendment_enabled(&self, amendment: &[u8]) -> 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 f026c31001..f3ba86c99f 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -43,6 +43,11 @@ impl HostFunctions for FakeHost { put(out, &10u32.to_le_bytes()) } + /// Returns a flag rather than bytes, and reads its input: enabled unless empty. + fn is_amendment_enabled(&self, amendment: &[u8]) -> HostResult { + Ok(i32::from(!amendment.is_empty())) + } + /// 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 { @@ -83,6 +88,8 @@ fn the_trait_is_implementable() { assert_eq!(out[0], 0xab); assert_eq!(host.get_base_fee(&mut out), Ok(4)); 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.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)); @@ -156,6 +163,7 @@ fn the_spec_table_matches_the_declarations() { ("parent_ldgr_time", 60), ("parent_ldgr_hash", 60), ("base_fee", 60), + ("amendment_enabled", 100), ("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 999515c918..6314cbad74 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -171,6 +171,12 @@ mod ffi { #[cxx_name = "getBaseFee"] fn get_base_fee(self: &HostContext, out: &mut [u8]) -> i32; + /// Reads the amendment (id or name) and answers `1`/`0`, or a negative + /// `HostError` code. + #[namespace = "xrpl"] + #[cxx_name = "isAmendmentEnabled"] + fn is_amendment_enabled(self: &HostContext, amendment: &[u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "getCurrentLedgerObjField"] fn get_current_ledger_obj_field(self: &HostContext, field: i32, out: &mut [u8]) -> i32; @@ -219,6 +225,15 @@ 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 { + if n < 0 { + return Err(HostError::from_code(n)); + } + Ok(n) +} + impl HostFunctions for CxxHost<'_> { fn get_ledger_sqn(&self, out: &mut [u8]) -> HostResult { bytes_written(self.ctx.get_ledger_sqn(out)) @@ -236,6 +251,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.get_base_fee(out)) } + fn is_amendment_enabled(&self, amendment: &[u8]) -> HostResult { + flag(self.ctx.is_amendment_enabled(amendment)) + } + fn get_current_ledger_obj_field(&self, field: i32, out: &mut [u8]) -> HostResult { bytes_written(self.ctx.get_current_ledger_obj_field(field, out)) } @@ -534,6 +553,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)); } /// An exception caught on the C++ side arrives as `-1`, which has to reach the @@ -543,6 +565,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)); } // ----------------------------------------------------------------------- diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index f5998af76c..02f284cfdc 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -197,6 +197,9 @@ mod tests { fn get_base_fee(&self, _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn is_amendment_enabled(&self, _amendment: &[u8]) -> 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 6dad076d90..7de7b569bb 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -74,6 +74,20 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::IsAmendmentEnabled => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + ptr: i32, + len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::IsAmendmentEnabled, |c| { + let host = c.data().host; + let amendment = read_borrowed(c, Region::new(ptr, len))?; + host.is_amendment_enabled(amendment) + }) + }, + ), 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 3896edaa10..7542748e8b 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -71,6 +71,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $base_fee (i32.const 0) (i32.const 4))", 2, ), + HostFunctionSpec::IsAmendmentEnabled => ( + import::AMENDMENT_ENABLED, + "(call $amendment_enabled (i32.const 0) (i32.const 32))", + 2, + ), 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 45ed5c68c7..3ebd26b321 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -93,6 +93,33 @@ fn base_fee_writes_the_fee_where_the_guest_asked() { assert_eq!(status(&wat, &host), 4, "the byte count"); } +/// A call that reads an input region and returns a scalar flag, rather than writing +/// bytes to an output region: the amendment reaches the host, and its verdict comes +/// back as the call's status. +#[test] +fn amendment_enabled_reads_the_input_and_returns_the_flag() { + let host = FakeHost::new(); // enabled by default + + let wat = module( + &[import::AMENDMENT_ENABLED, ONE_PAGE], + "(call $amendment_enabled (i32.const 64) (i32.const 32))", + ); + assert_eq!(status(&wat, &host), 1, "the enabled flag"); + assert_eq!( + *host.amendments_asked.borrow(), + [vec![0u8; 32]], + "the 32-byte region reached the host" + ); + + // A host that reports the amendment disabled answers 0 — a value, not an error. + let host = FakeHost::new().answering_amendment_enabled(Ok(0)); + let wat = module( + &[import::AMENDMENT_ENABLED, ONE_PAGE], + "(call $amendment_enabled (i32.const 0) (i32.const 32))", + ); + assert_eq!(status(&wat, &host), 0, "the disabled flag"); +} + /// 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 f8eb7bca4a..01af4598b5 100644 --- a/crates/xrpl-wasm-vm/tests/preflight.rs +++ b/crates/xrpl-wasm-vm/tests/preflight.rs @@ -98,11 +98,12 @@ 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; 8] = [ +const ALL_IMPORTS: [&str; 9] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, import::BASE_FEE, + import::AMENDMENT_ENABLED, 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 a4da822a6b..d9d01a5bd1 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -109,6 +109,10 @@ pub struct FakeHost { pub parent_ledger_hash: Answer, /// What `get_base_fee` answers. pub base_fee: Answer, + /// What `is_amendment_enabled` answers, whatever amendment it is given. + pub amendment_enabled: HostResult, + /// Every amendment `is_amendment_enabled` was asked about. + pub amendments_asked: RefCell>>, /// What `get_current_ledger_obj_field` answers, by field selector. An /// unlisted selector answers `FieldNotFound`. pub fields: HashMap, @@ -134,6 +138,9 @@ impl Default for FakeHost { parent_ledger_hash: Answer::filler(32), // A distinct value again, so no getter can pass by reading another's answer. base_fee: Answer::bytes(10u32.to_le_bytes()), + // 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()), fields: HashMap::new(), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), @@ -168,6 +175,11 @@ impl FakeHost { self } + pub fn answering_amendment_enabled(mut self, answer: HostResult) -> FakeHost { + self.amendment_enabled = answer; + self + } + pub fn answering_field(mut self, field: i32, answer: Answer) -> FakeHost { self.fields.insert(field, answer); self @@ -200,6 +212,11 @@ impl HostFunctions for FakeHost { self.base_fee.fill(out) } + fn is_amendment_enabled(&self, amendment: &[u8]) -> HostResult { + self.amendments_asked.borrow_mut().push(amendment.to_vec()); + self.amendment_enabled + } + 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) { @@ -245,6 +262,7 @@ pub mod import { pub const PARENT_LDGR_HASH: &str = r#"(import "host_lib" "parent_ldgr_hash" (func $parent_ldgr_hash (param i32 i32) (result i32)))"#; 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 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 7f3767dd41..8c5b08ddd9 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -53,6 +53,12 @@ public: [[nodiscard]] std::int32_t getBaseFee(rust::Slice out) const noexcept; + // The amendment is either a 32-byte id or a name; a 32-byte input is tried as an + // id first and falls back to a name lookup. Answers 1 or 0, or a negative + // `HostFunctionError` code. + [[nodiscard]] std::int32_t + isAmendmentEnabled(rust::Slice amendment) 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 c67387024f..2a932061de 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -107,6 +107,34 @@ HostContext::getBaseFee(rust::Slice out) const noexcept }); } +std::int32_t +HostContext::isAmendmentEnabled(rust::Slice amendment) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + // A 32-byte input may be an amendment id; try that first and fall through to + // a name lookup if it is not an enabled amendment - the 32 bytes could spell + // a name instead. + if (amendment.size() == uint256::size()) + { + auto const enabled = + hostFunctions_.isAmendmentEnabled(uint256::fromVoid(amendment.data())); + if (enabled && *enabled == 1) + return *enabled; + } + + if (amendment.size() > 64) + return hfErrorToInt(HostFunctionError::DataFieldTooLarge); + + auto const name = + std::string_view(reinterpret_cast(amendment.data()), amendment.size()); + auto const enabled = hostFunctions_.isAmendmentEnabled(name); + if (!enabled) + return hfErrorToInt(enabled.error()); + + return *enabled; + }); +} + std::int32_t HostContext::getCurrentLedgerObjField(std::int32_t field, rust::Slice out) const noexcept From 3a2cf64a698611eea67176b39958b392bd4ad983 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 16:20:50 -0400 Subject: [PATCH 05/46] 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 From 1656a19fe645ffd1fd3da40ef1cc670d624f93d0 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 16:40:51 -0400 Subject: [PATCH 06/46] feat: Hook up tx_field host function --- crates/xrpl-host-functions/src/lib.rs | 6 +++++ .../tests/generated_abi.rs | 11 ++++++++++ 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 | 15 +++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 22 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 3 +++ src/libxrpl/tx/wasm/HostContext.cpp | 17 ++++++++++++++ 11 files changed, 106 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 6911a44648..53c0bbceaf 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -135,6 +135,12 @@ host_functions! { #[wasm_name = "cache_le"] fn cache_ledger_obj(&self, obj_id: &[u8], cache_idx: i32) -> HostResult; + /// The serialized bytes of one field of the transaction being executed, selected + /// by its `SField` code. + #[gas = 70] + #[wasm_name = "tx_field"] + fn get_tx_field(&self, field: i32, out: &mut [u8]) -> 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 e12d7dc861..800dcc51b1 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -53,6 +53,14 @@ impl HostFunctions for FakeHost { Ok(if cache_idx == 0 { 1 } else { cache_idx }) } + /// A field getter over the transaction; fails on a negative selector. + fn get_tx_field(&self, field: i32, out: &mut [u8]) -> HostResult { + if field < 0 { + return Err(HostError::FieldNotFound); + } + put(out, &[field as u8]) + } + /// 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 { @@ -97,6 +105,8 @@ fn the_trait_is_implementable() { 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_tx_field(5, &mut out), Ok(1)); + assert_eq!(out[0], 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)); @@ -172,6 +182,7 @@ fn the_spec_table_matches_the_declarations() { ("base_fee", 60), ("amendment_enabled", 100), ("cache_le", 5000), + ("tx_field", 70), ("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 f90d29141b..71f4e3a6a8 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -183,6 +183,10 @@ mod ffi { #[cxx_name = "cacheLedgerObj"] fn cache_ledger_obj(self: &HostContext, obj_id: &[u8], cache_idx: i32) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "getTxField"] + fn get_tx_field(self: &HostContext, field: i32, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "getCurrentLedgerObjField"] fn get_current_ledger_obj_field(self: &HostContext, field: i32, out: &mut [u8]) -> i32; @@ -265,6 +269,10 @@ impl HostFunctions for CxxHost<'_> { scalar(self.ctx.cache_ledger_obj(obj_id, cache_idx)) } + fn get_tx_field(&self, field: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.get_tx_field(field, out)) + } + fn get_current_ledger_obj_field(&self, field: i32, out: &mut [u8]) -> HostResult { bytes_written(self.ctx.get_current_ledger_obj_field(field, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index ff16f17454..0205dd7ea8 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -203,6 +203,9 @@ mod tests { fn cache_ledger_obj(&self, _obj_id: &[u8], _cache_idx: i32) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_tx_field(&self, _field: i32, _out: &mut [u8]) -> 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 dceaf0f2c6..8c8c6e3837 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -103,6 +103,20 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::GetTxField => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + field: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetTxField, |c| { + let out = Region::new(out_ptr, out_len); + write_into(c, out, |host, out| host.get_tx_field(field, out)) + }) + }, + ), 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 a8a5f8d79a..1bfa24ba5a 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -81,6 +81,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $cache_le (i32.const 0) (i32.const 32) (i32.const 0))", 3, ), + HostFunctionSpec::GetTxField => ( + import::TX_FIELD, + "(call $tx_field (i32.const 1) (i32.const 0) (i32.const 4))", + 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 fe7700bdcf..71ffd179cd 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -156,6 +156,21 @@ fn the_output_region_is_the_pointer_the_guest_gave() { } } +/// A field getter over the transaction: the selector reaches the host, and the bytes +/// it answers land where the guest asked. It has its own answer set, distinct from +/// the current-object field getter's. +#[test] +fn tx_field_passes_the_selector_and_writes_the_field() { + let host = FakeHost::new().answering_tx_field(17, support::Answer::bytes([0xab, 0xcd])); + + let wat = module( + &[import::TX_FIELD, ONE_PAGE], + "(call $tx_field (i32.const 17) (i32.const 0) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 2); + assert_eq!(*host.tx_fields_asked.borrow(), vec![17]); +} + /// 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 e3ce04b519..0118837548 100644 --- a/crates/xrpl-wasm-vm/tests/preflight.rs +++ b/crates/xrpl-wasm-vm/tests/preflight.rs @@ -98,13 +98,14 @@ 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; 10] = [ +const ALL_IMPORTS: [&str; 11] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, import::BASE_FEE, import::AMENDMENT_ENABLED, import::CACHE_LE, + import::TX_FIELD, 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 33a0aa475c..e9808f2e11 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -117,6 +117,11 @@ pub struct FakeHost { pub cache_slot: HostResult, /// Every (object id, requested slot) `cache_ledger_obj` was asked to cache. pub cached: RefCell, i32)>>, + /// What `get_tx_field` answers, by field selector. An unlisted selector answers + /// `FieldNotFound`. + pub tx_fields: HashMap, + /// Every field selector `get_tx_field` was asked for. + pub tx_fields_asked: RefCell>, /// What `get_current_ledger_obj_field` answers, by field selector. An /// unlisted selector answers `FieldNotFound`. pub fields: HashMap, @@ -148,6 +153,8 @@ impl Default for FakeHost { // Slot 1 by default; slot assignment is the host's job, not the ABI's. cache_slot: Ok(1), cached: RefCell::new(Vec::new()), + tx_fields: HashMap::new(), + tx_fields_asked: RefCell::new(Vec::new()), fields: HashMap::new(), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), @@ -192,6 +199,11 @@ impl FakeHost { self } + pub fn answering_tx_field(mut self, field: i32, answer: Answer) -> FakeHost { + self.tx_fields.insert(field, answer); + self + } + pub fn answering_field(mut self, field: i32, answer: Answer) -> FakeHost { self.fields.insert(field, answer); self @@ -234,6 +246,14 @@ impl HostFunctions for FakeHost { self.cache_slot } + fn get_tx_field(&self, field: i32, out: &mut [u8]) -> HostResult { + self.tx_fields_asked.borrow_mut().push(field); + match self.tx_fields.get(&field) { + Some(answer) => answer.fill(out), + None => Err(HostError::FieldNotFound), + } + } + 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) { @@ -282,6 +302,8 @@ pub mod import { 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 TX_FIELD: &str = + r#"(import "host_lib" "tx_field" (func $tx_field (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 3b358aefc5..eac7591177 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -65,6 +65,9 @@ public: [[nodiscard]] std::int32_t cacheLedgerObj(rust::Slice objId, std::int32_t cacheIdx) const noexcept; + [[nodiscard]] std::int32_t + getTxField(std::int32_t field, rust::Slice out) 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 e1815a487d..124f0833d2 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -151,6 +151,23 @@ HostContext::cacheLedgerObj(rust::Slice objId, std::int32_t }); } +std::int32_t +HostContext::getTxField(std::int32_t field, rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const& knownSFields = SField::getKnownCodeToField(); + auto const it = knownSFields.find(field); + if (it == knownSFields.end()) + return hfErrorToInt(HostFunctionError::InvalidField); + + auto const value = hostFunctions_.getTxField(*it->second); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::getCurrentLedgerObjField(std::int32_t field, rust::Slice out) const noexcept From ce5e724b93be2d5492df86593886f7f184dfb23e Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 16:49:58 -0400 Subject: [PATCH 07/46] feat: Hook up le_field host function --- crates/xrpl-host-functions/src/lib.rs | 6 +++++ .../tests/generated_abi.rs | 16 +++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 18 +++++++++++++ crates/xrpl-wasm-vm/src/abi.rs | 8 ++++++ crates/xrpl-wasm-vm/src/register.rs | 17 ++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 15 +++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 27 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 4 +++ src/libxrpl/tx/wasm/HostContext.cpp | 20 ++++++++++++++ 11 files changed, 138 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 53c0bbceaf..e317316cbb 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -146,6 +146,12 @@ host_functions! { #[wasm_name = "home_le_field"] fn get_current_ledger_obj_field(&self, field: i32, out: &mut [u8]) -> HostResult; + /// The serialized bytes of one field of a previously cached ledger object, + /// selected by its cache slot and the field's `SField` code. + #[gas = 70] + #[wasm_name = "le_field"] + fn get_ledger_obj_field(&self, cache_idx: i32, field: i32, out: &mut [u8]) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 800dcc51b1..2361325d2f 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -69,6 +69,19 @@ impl HostFunctions for FakeHost { put(out, &[field as u8]) } + /// A field getter over a cached object, keyed by slot and selector. + fn get_ledger_obj_field( + &self, + cache_idx: i32, + field: i32, + out: &mut [u8], + ) -> HostResult { + if cache_idx <= 0 || field < 0 { + return Err(HostError::FieldNotFound); + } + put(out, &[cache_idx as u8, field as u8]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -109,6 +122,8 @@ fn the_trait_is_implementable() { assert_eq!(out[0], 5); assert_eq!(host.get_current_ledger_obj_field(3, &mut out), Ok(1)); assert_eq!(out[0], 3); + assert_eq!(host.get_ledger_obj_field(2, 4, &mut out), Ok(2)); + assert_eq!(out[..2], [2, 4]); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -184,6 +199,7 @@ fn the_spec_table_matches_the_declarations() { ("cache_le", 5000), ("tx_field", 70), ("home_le_field", 70), + ("le_field", 70), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 71f4e3a6a8..c9402426b9 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -191,6 +191,15 @@ mod ffi { #[cxx_name = "getCurrentLedgerObjField"] fn get_current_ledger_obj_field(self: &HostContext, field: i32, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "getLedgerObjField"] + fn get_ledger_obj_field( + self: &HostContext, + cache_idx: i32, + field: i32, + out: &mut [u8], + ) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -277,6 +286,15 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.get_current_ledger_obj_field(field, out)) } + fn get_ledger_obj_field( + &self, + cache_idx: i32, + field: i32, + out: &mut [u8], + ) -> HostResult { + bytes_written(self.ctx.get_ledger_obj_field(cache_idx, field, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 0205dd7ea8..b3f5eb7c44 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -209,6 +209,14 @@ mod tests { fn get_current_ledger_obj_field(&self, _field: i32, _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_ledger_obj_field( + &self, + _cache_idx: i32, + _field: i32, + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 8c8c6e3837..ea5ab4aa25 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -137,6 +137,23 @@ pub(crate) fn register_host_functions( ) }, ), + HostFunctionSpec::GetLedgerObjField => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + cache_idx: i32, + field: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetLedgerObjField, |c| { + let out = Region::new(out_ptr, out_len); + write_into(c, out, |host, out| { + host.get_ledger_obj_field(cache_idx, field, out) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 1bfa24ba5a..cbe9439c7f 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -91,6 +91,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $home_le_field (i32.const 1) (i32.const 0) (i32.const 4))", 3, ), + HostFunctionSpec::GetLedgerObjField => ( + import::LE_FIELD, + "(call $le_field (i32.const 1) (i32.const 1) (i32.const 0) (i32.const 4))", + 4, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 71ffd179cd..7d5fe8ab21 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -171,6 +171,21 @@ fn tx_field_passes_the_selector_and_writes_the_field() { assert_eq!(*host.tx_fields_asked.borrow(), vec![17]); } +/// A field getter over a cached object: both the slot and the selector reach the +/// host, keyed together, and the answered bytes land where the guest asked. +#[test] +fn le_field_passes_the_slot_and_selector_through() { + let host = + FakeHost::new().answering_le_field(2, 17, support::Answer::bytes([0xab, 0xcd, 0xef])); + + let wat = module( + &[import::LE_FIELD, ONE_PAGE], + "(call $le_field (i32.const 2) (i32.const 17) (i32.const 0) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 3); + assert_eq!(*host.le_fields_asked.borrow(), vec![(2, 17)]); +} + /// 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 0118837548..9edce35a06 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; 11] = [ +const ALL_IMPORTS: [&str; 12] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -107,6 +107,7 @@ const ALL_IMPORTS: [&str; 11] = [ import::CACHE_LE, import::TX_FIELD, import::HOME_LE_FIELD, + import::LE_FIELD, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index e9808f2e11..9822971633 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -125,6 +125,11 @@ pub struct FakeHost { /// What `get_current_ledger_obj_field` answers, by field selector. An /// unlisted selector answers `FieldNotFound`. pub fields: HashMap, + /// What `get_ledger_obj_field` answers, by (cache slot, field selector). An + /// unlisted key answers `FieldNotFound`. + pub le_fields: HashMap<(i32, i32), Answer>, + /// Every (cache slot, field selector) `get_ledger_obj_field` was asked for. + pub le_fields_asked: RefCell>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -156,6 +161,8 @@ impl Default for FakeHost { tx_fields: HashMap::new(), tx_fields_asked: RefCell::new(Vec::new()), fields: HashMap::new(), + le_fields: HashMap::new(), + le_fields_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -209,6 +216,11 @@ impl FakeHost { self } + pub fn answering_le_field(mut self, cache_idx: i32, field: i32, answer: Answer) -> FakeHost { + self.le_fields.insert((cache_idx, field), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -262,6 +274,19 @@ impl HostFunctions for FakeHost { } } + fn get_ledger_obj_field( + &self, + cache_idx: i32, + field: i32, + out: &mut [u8], + ) -> HostResult { + self.le_fields_asked.borrow_mut().push((cache_idx, field)); + match self.le_fields.get(&(cache_idx, field)) { + Some(answer) => answer.fill(out), + None => Err(HostError::FieldNotFound), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -305,6 +330,8 @@ pub mod import { pub const TX_FIELD: &str = r#"(import "host_lib" "tx_field" (func $tx_field (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 LE_FIELD: &str = + r#"(import "host_lib" "le_field" (func $le_field (param i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index eac7591177..a3ec432d6f 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -71,6 +71,10 @@ public: [[nodiscard]] std::int32_t getCurrentLedgerObjField(std::int32_t field, rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t + getLedgerObjField(std::int32_t cacheIdx, std::int32_t field, rust::Slice out) + const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 124f0833d2..9003f9e10e 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -186,6 +186,26 @@ HostContext::getCurrentLedgerObjField(std::int32_t field, rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const& knownSFields = SField::getKnownCodeToField(); + auto const it = knownSFields.find(field); + if (it == knownSFields.end()) + return hfErrorToInt(HostFunctionError::InvalidField); + + auto const value = hostFunctions_.getLedgerObjField(cacheIdx, *it->second); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 881d040a220040f4418538bd17153829c120f507 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 16:56:20 -0400 Subject: [PATCH 08/46] feat: Hook up tx_inner host function --- crates/xrpl-host-functions/src/lib.rs | 7 +++++ .../tests/generated_abi.rs | 11 ++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 ++++++ crates/xrpl-wasm-vm/src/abi.rs | 3 ++ crates/xrpl-wasm-vm/src/register.rs | 18 ++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 20 +++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 22 +++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 6 ++++ src/libxrpl/tx/wasm/HostContext.cpp | 28 +++++++++++++++++++ 11 files changed, 130 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index e317316cbb..c714f447c3 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -152,6 +152,13 @@ host_functions! { #[wasm_name = "le_field"] 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. + #[gas = 110] + #[wasm_name = "tx_inner"] + fn get_tx_nested_field(&self, locator: &[u8], out: &mut [u8]) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 2361325d2f..21839c05b5 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -82,6 +82,14 @@ impl HostFunctions for FakeHost { put(out, &[cache_idx as u8, field as u8]) } + /// A nested-field getter over the transaction, keyed by the locator bytes. + fn get_tx_nested_field(&self, locator: &[u8], out: &mut [u8]) -> HostResult { + if locator.is_empty() { + return Err(HostError::LocatorMalformed); + } + put(out, &[locator[0], locator.len() as u8]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -124,6 +132,8 @@ fn the_trait_is_implementable() { assert_eq!(out[0], 3); assert_eq!(host.get_ledger_obj_field(2, 4, &mut out), Ok(2)); assert_eq!(out[..2], [2, 4]); + assert_eq!(host.get_tx_nested_field(&[9, 0, 0, 0], &mut out), Ok(2)); + assert_eq!(out[..2], [9, 4]); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -200,6 +210,7 @@ fn the_spec_table_matches_the_declarations() { ("tx_field", 70), ("home_le_field", 70), ("le_field", 70), + ("tx_inner", 110), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index c9402426b9..9d68635395 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -200,6 +200,10 @@ mod ffi { out: &mut [u8], ) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "getTxNestedField"] + fn get_tx_nested_field(self: &HostContext, locator: &[u8], out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -295,6 +299,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.get_ledger_obj_field(cache_idx, field, out)) } + fn get_tx_nested_field(&self, locator: &[u8], out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.get_tx_nested_field(locator, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index b3f5eb7c44..ed159d4c5a 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -217,6 +217,9 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_tx_nested_field(&self, _locator: &[u8], _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 ea5ab4aa25..5540360d95 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -154,6 +154,24 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::GetTxNestedField => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + loc_ptr: i32, + loc_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetTxNestedField, |c| { + let out = Region::new(out_ptr, out_len); + let locator = Region::new(loc_ptr, loc_len); + write_buffered(c, out, |host, data, buf| { + host.get_tx_nested_field(locator.read(data)?, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 cbe9439c7f..ad4d4e3abe 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -96,6 +96,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $le_field (i32.const 1) (i32.const 1) (i32.const 0) (i32.const 4))", 4, ), + HostFunctionSpec::GetTxNestedField => ( + import::TX_INNER, + "(call $tx_inner (i32.const 0) (i32.const 4) (i32.const 8) (i32.const 4))", + 4, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 7d5fe8ab21..5732752cea 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -186,6 +186,26 @@ fn le_field_passes_the_slot_and_selector_through() { assert_eq!(*host.le_fields_asked.borrow(), vec![(2, 17)]); } +/// A nested-field getter: the locator is read from one region and the answer written +/// to another — the read-input-write-output path. The guest lays the locator down in +/// memory, and the bytes the host answers land where it asked. +#[test] +fn tx_inner_reads_the_locator_and_writes_the_field() { + // An eight-byte, two-step locator, as it lands in little-endian guest memory. + let locator = vec![17u8, 0, 0, 0, 2, 0, 0, 0]; + let host = + FakeHost::new().answering_tx_nested(locator.clone(), support::Answer::bytes([0xaa, 0xbb])); + + let wat = module( + &[import::TX_INNER, ONE_PAGE], + "(i32.store (i32.const 0) (i32.const 17)) + (i32.store (i32.const 4) (i32.const 2)) + (call $tx_inner (i32.const 0) (i32.const 8) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 2, "the field bytes the host wrote"); + assert_eq!(*host.tx_nested_asked.borrow(), vec![locator]); +} + /// 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 9edce35a06..4725baf5bc 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; 12] = [ +const ALL_IMPORTS: [&str; 13] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -108,6 +108,7 @@ const ALL_IMPORTS: [&str; 12] = [ import::TX_FIELD, import::HOME_LE_FIELD, import::LE_FIELD, + import::TX_INNER, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 9822971633..908a965057 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -130,6 +130,11 @@ pub struct FakeHost { pub le_fields: HashMap<(i32, i32), Answer>, /// Every (cache slot, field selector) `get_ledger_obj_field` was asked for. pub le_fields_asked: RefCell>, + /// What `get_tx_nested_field` answers, by locator bytes. An unlisted locator + /// answers `FieldNotFound`. + pub tx_nested: HashMap, Answer>, + /// Every locator `get_tx_nested_field` was asked for. + pub tx_nested_asked: RefCell>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -163,6 +168,8 @@ impl Default for FakeHost { fields: HashMap::new(), le_fields: HashMap::new(), le_fields_asked: RefCell::new(Vec::new()), + tx_nested: HashMap::new(), + tx_nested_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -221,6 +228,11 @@ impl FakeHost { self } + pub fn answering_tx_nested(mut self, locator: Vec, answer: Answer) -> FakeHost { + self.tx_nested.insert(locator, answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -287,6 +299,14 @@ impl HostFunctions for FakeHost { } } + fn get_tx_nested_field(&self, locator: &[u8], out: &mut [u8]) -> HostResult { + self.tx_nested_asked.borrow_mut().push(locator.to_vec()); + match self.tx_nested.get(locator) { + Some(answer) => answer.fill(out), + None => Err(HostError::FieldNotFound), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -332,6 +352,8 @@ pub mod import { 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 LE_FIELD: &str = r#"(import "host_lib" "le_field" (func $le_field (param i32 i32 i32 i32) (result i32)))"#; + pub const TX_INNER: &str = + r#"(import "host_lib" "tx_inner" (func $tx_inner (param i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index a3ec432d6f..6083a93920 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -75,6 +75,12 @@ public: getLedgerObjField(std::int32_t cacheIdx, std::int32_t field, rust::Slice out) const noexcept; + // The locator is a path of little-endian i32 steps, so its byte length must be a + // non-zero multiple of 4, else `LocatorMalformed`. + [[nodiscard]] std::int32_t + getTxNestedField(rust::Slice locator, rust::Slice out) + const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 9003f9e10e..2a1d2f37f4 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include namespace xrpl { @@ -206,6 +208,32 @@ HostContext::getLedgerObjField( }); } +std::int32_t +HostContext::getTxNestedField( + rust::Slice locator, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + // A path of i32 steps: non-empty and a whole number of them. + if (locator.empty() || (locator.size() & 3) != 0) + return hfErrorToInt(HostFunctionError::LocatorMalformed); + + // Copy into an aligned int32 buffer rather than aliasing the slice, whose + // bytes carry no int32 alignment guarantee. The wire byte order is kept; the + // field getters below apply `adjustWasmEndianess` when they read a step. + std::uint32_t const steps = locator.size() / sizeof(std::int32_t); + std::vector locBuf(steps); + std::memcpy(locBuf.data(), locator.data(), locator.size()); + FieldLocator const fl(std::move(locBuf)); + + auto const value = hostFunctions_.getTxNestedField(fl); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From fe325ea96a4376c6de39dd29db881d2260c3ff97 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 17:01:09 -0400 Subject: [PATCH 09/46] feat: Hook up home_le_inner host function --- crates/xrpl-host-functions/src/lib.rs | 10 +++++++ .../tests/generated_abi.rs | 18 +++++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 16 +++++++++++ crates/xrpl-wasm-vm/src/abi.rs | 7 +++++ crates/xrpl-wasm-vm/src/register.rs | 22 +++++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 17 ++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 27 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 5 ++++ src/libxrpl/tx/wasm/HostContext.cpp | 22 +++++++++++++++ 11 files changed, 151 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index c714f447c3..e31e3f9c4f 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -159,6 +159,16 @@ host_functions! { #[wasm_name = "tx_inner"] fn get_tx_nested_field(&self, locator: &[u8], out: &mut [u8]) -> HostResult; + /// The serialized bytes of a nested field of the current (escrow) ledger object, + /// reached by a `locator`, as with [`Self::get_tx_nested_field`]. + #[gas = 110] + #[wasm_name = "home_le_inner"] + fn get_current_ledger_obj_nested_field( + &self, + locator: &[u8], + out: &mut [u8], + ) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 21839c05b5..96ab5b7dce 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -90,6 +90,18 @@ impl HostFunctions for FakeHost { put(out, &[locator[0], locator.len() as u8]) } + /// The same, over the current ledger object. + fn get_current_ledger_obj_nested_field( + &self, + locator: &[u8], + out: &mut [u8], + ) -> HostResult { + if locator.is_empty() { + return Err(HostError::LocatorMalformed); + } + put(out, &[locator.len() as u8, locator[0]]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -134,6 +146,11 @@ fn the_trait_is_implementable() { assert_eq!(out[..2], [2, 4]); assert_eq!(host.get_tx_nested_field(&[9, 0, 0, 0], &mut out), Ok(2)); assert_eq!(out[..2], [9, 4]); + assert_eq!( + host.get_current_ledger_obj_nested_field(&[9, 0, 0, 0], &mut out), + Ok(2) + ); + assert_eq!(out[..2], [4, 9]); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -211,6 +228,7 @@ fn the_spec_table_matches_the_declarations() { ("home_le_field", 70), ("le_field", 70), ("tx_inner", 110), + ("home_le_inner", 110), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 9d68635395..8f7ee09c10 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -204,6 +204,14 @@ mod ffi { #[cxx_name = "getTxNestedField"] fn get_tx_nested_field(self: &HostContext, locator: &[u8], out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "getCurrentLedgerObjNestedField"] + fn get_current_ledger_obj_nested_field( + self: &HostContext, + locator: &[u8], + out: &mut [u8], + ) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -303,6 +311,14 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.get_tx_nested_field(locator, out)) } + fn get_current_ledger_obj_nested_field( + &self, + locator: &[u8], + out: &mut [u8], + ) -> HostResult { + bytes_written(self.ctx.get_current_ledger_obj_nested_field(locator, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index ed159d4c5a..ec0ef2cca4 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -220,6 +220,13 @@ mod tests { fn get_tx_nested_field(&self, _locator: &[u8], _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_current_ledger_obj_nested_field( + &self, + _locator: &[u8], + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 5540360d95..a08bd0d2d1 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -172,6 +172,28 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::GetCurrentLedgerObjNestedField => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + loc_ptr: i32, + loc_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged( + &mut caller, + HostFunctionSpec::GetCurrentLedgerObjNestedField, + |c| { + let out = Region::new(out_ptr, out_len); + let locator = Region::new(loc_ptr, loc_len); + write_buffered(c, out, |host, data, buf| { + host.get_current_ledger_obj_nested_field(locator.read(data)?, buf) + }) + }, + ) + }, + ), HostFunctionSpec::Sha512Half => 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 ad4d4e3abe..447caa424b 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -101,6 +101,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $tx_inner (i32.const 0) (i32.const 4) (i32.const 8) (i32.const 4))", 4, ), + HostFunctionSpec::GetCurrentLedgerObjNestedField => ( + import::HOME_LE_INNER, + "(call $home_le_inner (i32.const 0) (i32.const 4) (i32.const 8) (i32.const 4))", + 4, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 5732752cea..5187694a2f 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -206,6 +206,23 @@ fn tx_inner_reads_the_locator_and_writes_the_field() { assert_eq!(*host.tx_nested_asked.borrow(), vec![locator]); } +/// The same read-input-write-output path over the current object, with its own +/// answer set distinct from the transaction's nested getter. +#[test] +fn home_le_inner_reads_the_locator_and_writes_the_field() { + let locator = vec![5u8, 0, 0, 0]; + let host = FakeHost::new() + .answering_home_le_nested(locator.clone(), support::Answer::bytes([0xcc, 0xdd, 0xee])); + + let wat = module( + &[import::HOME_LE_INNER, ONE_PAGE], + "(i32.store (i32.const 0) (i32.const 5)) + (call $home_le_inner (i32.const 0) (i32.const 4) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 3, "the field bytes the host wrote"); + assert_eq!(*host.home_le_nested_asked.borrow(), vec![locator]); +} + /// 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 4725baf5bc..972a0fa080 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; 13] = [ +const ALL_IMPORTS: [&str; 14] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -109,6 +109,7 @@ const ALL_IMPORTS: [&str; 13] = [ import::HOME_LE_FIELD, import::LE_FIELD, import::TX_INNER, + import::HOME_LE_INNER, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 908a965057..28b8427971 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -135,6 +135,11 @@ pub struct FakeHost { pub tx_nested: HashMap, Answer>, /// Every locator `get_tx_nested_field` was asked for. pub tx_nested_asked: RefCell>>, + /// What `get_current_ledger_obj_nested_field` answers, by locator bytes. An + /// unlisted locator answers `FieldNotFound`. + pub home_le_nested: HashMap, Answer>, + /// Every locator `get_current_ledger_obj_nested_field` was asked for. + pub home_le_nested_asked: RefCell>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -170,6 +175,8 @@ impl Default for FakeHost { le_fields_asked: RefCell::new(Vec::new()), tx_nested: HashMap::new(), tx_nested_asked: RefCell::new(Vec::new()), + home_le_nested: HashMap::new(), + home_le_nested_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -233,6 +240,11 @@ impl FakeHost { self } + pub fn answering_home_le_nested(mut self, locator: Vec, answer: Answer) -> FakeHost { + self.home_le_nested.insert(locator, answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -307,6 +319,20 @@ impl HostFunctions for FakeHost { } } + fn get_current_ledger_obj_nested_field( + &self, + locator: &[u8], + out: &mut [u8], + ) -> HostResult { + self.home_le_nested_asked + .borrow_mut() + .push(locator.to_vec()); + match self.home_le_nested.get(locator) { + Some(answer) => answer.fill(out), + None => Err(HostError::FieldNotFound), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -354,6 +380,7 @@ pub mod import { r#"(import "host_lib" "le_field" (func $le_field (param i32 i32 i32 i32) (result i32)))"#; pub const TX_INNER: &str = r#"(import "host_lib" "tx_inner" (func $tx_inner (param i32 i32 i32 i32) (result i32)))"#; + pub const HOME_LE_INNER: &str = r#"(import "host_lib" "home_le_inner" (func $home_le_inner (param i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 6083a93920..2b1c5e3418 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -81,6 +81,11 @@ public: getTxNestedField(rust::Slice locator, rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t + getCurrentLedgerObjNestedField( + rust::Slice locator, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 2a1d2f37f4..a36d04e094 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -234,6 +234,28 @@ HostContext::getTxNestedField( }); } +std::int32_t +HostContext::getCurrentLedgerObjNestedField( + rust::Slice locator, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (locator.empty() || (locator.size() & 3) != 0) + return hfErrorToInt(HostFunctionError::LocatorMalformed); + + std::uint32_t const steps = locator.size() / sizeof(std::int32_t); + std::vector locBuf(steps); + std::memcpy(locBuf.data(), locator.data(), locator.size()); + FieldLocator const fl(std::move(locBuf)); + + auto const value = hostFunctions_.getCurrentLedgerObjNestedField(fl); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 8cae773691dcc765083d2690b934877987bd509f Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 17:05:46 -0400 Subject: [PATCH 10/46] feat: Hook up le_inner host function --- crates/xrpl-host-functions/src/lib.rs | 11 +++++++ .../tests/generated_abi.rs | 19 +++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 21 ++++++++++++ crates/xrpl-wasm-vm/src/abi.rs | 8 +++++ crates/xrpl-wasm-vm/src/register.rs | 27 +++++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 +++ crates/xrpl-wasm-vm/tests/host_calls.rs | 20 +++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 33 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 6 ++++ src/libxrpl/tx/wasm/HostContext.cpp | 23 +++++++++++++ 11 files changed, 175 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index e31e3f9c4f..adfccb0af1 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -169,6 +169,17 @@ host_functions! { out: &mut [u8], ) -> HostResult; + /// The serialized bytes of a nested field of a previously cached ledger object, + /// selected by its cache slot and reached by a `locator`. + #[gas = 110] + #[wasm_name = "le_inner"] + fn get_ledger_obj_nested_field( + &self, + cache_idx: i32, + locator: &[u8], + out: &mut [u8], + ) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 96ab5b7dce..0dc50c5a3b 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -102,6 +102,19 @@ impl HostFunctions for FakeHost { put(out, &[locator.len() as u8, locator[0]]) } + /// The same, over a cached object keyed by slot. + fn get_ledger_obj_nested_field( + &self, + cache_idx: i32, + locator: &[u8], + out: &mut [u8], + ) -> HostResult { + if cache_idx <= 0 || locator.is_empty() { + return Err(HostError::LocatorMalformed); + } + put(out, &[cache_idx as u8, locator[0]]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -151,6 +164,11 @@ fn the_trait_is_implementable() { Ok(2) ); assert_eq!(out[..2], [4, 9]); + assert_eq!( + host.get_ledger_obj_nested_field(3, &[9, 0, 0, 0], &mut out), + Ok(2) + ); + assert_eq!(out[..2], [3, 9]); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -229,6 +247,7 @@ fn the_spec_table_matches_the_declarations() { ("le_field", 70), ("tx_inner", 110), ("home_le_inner", 110), + ("le_inner", 110), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 8f7ee09c10..fca7d57683 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -212,6 +212,15 @@ mod ffi { out: &mut [u8], ) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "getLedgerObjNestedField"] + fn get_ledger_obj_nested_field( + self: &HostContext, + cache_idx: i32, + locator: &[u8], + out: &mut [u8], + ) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -319,6 +328,18 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.get_current_ledger_obj_nested_field(locator, out)) } + fn get_ledger_obj_nested_field( + &self, + cache_idx: i32, + locator: &[u8], + out: &mut [u8], + ) -> HostResult { + bytes_written( + self.ctx + .get_ledger_obj_nested_field(cache_idx, locator, out), + ) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index ec0ef2cca4..daa8cf9f17 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -227,6 +227,14 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_ledger_obj_nested_field( + &self, + _cache_idx: i32, + _locator: &[u8], + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 a08bd0d2d1..1dc57f0751 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -194,6 +194,33 @@ pub(crate) fn register_host_functions( ) }, ), + HostFunctionSpec::GetLedgerObjNestedField => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + cache_idx: i32, + loc_ptr: i32, + loc_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged( + &mut caller, + HostFunctionSpec::GetLedgerObjNestedField, + |c| { + let out = Region::new(out_ptr, out_len); + let locator = Region::new(loc_ptr, loc_len); + write_buffered(c, out, |host, data, buf| { + host.get_ledger_obj_nested_field( + cache_idx, + locator.read(data)?, + buf, + ) + }) + }, + ) + }, + ), HostFunctionSpec::Sha512Half => 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 447caa424b..f415169c6b 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -106,6 +106,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $home_le_inner (i32.const 0) (i32.const 4) (i32.const 8) (i32.const 4))", 4, ), + HostFunctionSpec::GetLedgerObjNestedField => ( + import::LE_INNER, + "(call $le_inner (i32.const 1) (i32.const 0) (i32.const 4) (i32.const 8) (i32.const 4))", + 5, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 5187694a2f..77e1c4205a 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -223,6 +223,26 @@ fn home_le_inner_reads_the_locator_and_writes_the_field() { assert_eq!(*host.home_le_nested_asked.borrow(), vec![locator]); } +/// The nested getter over a cached object: the slot leads, the locator is read from +/// memory, and the two reach the host keyed together. +#[test] +fn le_inner_reads_the_slot_and_locator_and_writes_the_field() { + let locator = vec![5u8, 0, 0, 0]; + let host = FakeHost::new().answering_le_nested( + 3, + locator.clone(), + support::Answer::bytes([0x11, 0x22]), + ); + + let wat = module( + &[import::LE_INNER, ONE_PAGE], + "(i32.store (i32.const 0) (i32.const 5)) + (call $le_inner (i32.const 3) (i32.const 0) (i32.const 4) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 2, "the field bytes the host wrote"); + assert_eq!(*host.le_nested_asked.borrow(), vec![(3, locator)]); +} + /// 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 972a0fa080..70ab994903 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; 14] = [ +const ALL_IMPORTS: [&str; 15] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -110,6 +110,7 @@ const ALL_IMPORTS: [&str; 14] = [ import::LE_FIELD, import::TX_INNER, import::HOME_LE_INNER, + import::LE_INNER, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 28b8427971..e93d33c1a0 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -140,6 +140,11 @@ pub struct FakeHost { pub home_le_nested: HashMap, Answer>, /// Every locator `get_current_ledger_obj_nested_field` was asked for. pub home_le_nested_asked: RefCell>>, + /// What `get_ledger_obj_nested_field` answers, by (cache slot, locator bytes). An + /// unlisted key answers `FieldNotFound`. + pub le_nested: HashMap<(i32, Vec), Answer>, + /// Every (cache slot, locator) `get_ledger_obj_nested_field` was asked for. + pub le_nested_asked: RefCell)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -177,6 +182,8 @@ impl Default for FakeHost { tx_nested_asked: RefCell::new(Vec::new()), home_le_nested: HashMap::new(), home_le_nested_asked: RefCell::new(Vec::new()), + le_nested: HashMap::new(), + le_nested_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -245,6 +252,16 @@ impl FakeHost { self } + pub fn answering_le_nested( + mut self, + cache_idx: i32, + locator: Vec, + answer: Answer, + ) -> FakeHost { + self.le_nested.insert((cache_idx, locator), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -333,6 +350,21 @@ impl HostFunctions for FakeHost { } } + fn get_ledger_obj_nested_field( + &self, + cache_idx: i32, + locator: &[u8], + out: &mut [u8], + ) -> HostResult { + self.le_nested_asked + .borrow_mut() + .push((cache_idx, locator.to_vec())); + match self.le_nested.get(&(cache_idx, locator.to_vec())) { + Some(answer) => answer.fill(out), + None => Err(HostError::FieldNotFound), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -381,6 +413,7 @@ pub mod import { pub const TX_INNER: &str = r#"(import "host_lib" "tx_inner" (func $tx_inner (param i32 i32 i32 i32) (result i32)))"#; pub const HOME_LE_INNER: &str = r#"(import "host_lib" "home_le_inner" (func $home_le_inner (param i32 i32 i32 i32) (result i32)))"#; + pub const LE_INNER: &str = r#"(import "host_lib" "le_inner" (func $le_inner (param i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 2b1c5e3418..a4bd47c26c 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -86,6 +86,12 @@ public: rust::Slice locator, rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t + getLedgerObjNestedField( + std::int32_t cacheIdx, + rust::Slice locator, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index a36d04e094..49d4e641b0 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -256,6 +256,29 @@ HostContext::getCurrentLedgerObjNestedField( }); } +std::int32_t +HostContext::getLedgerObjNestedField( + std::int32_t cacheIdx, + rust::Slice locator, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (locator.empty() || (locator.size() & 3) != 0) + return hfErrorToInt(HostFunctionError::LocatorMalformed); + + std::uint32_t const steps = locator.size() / sizeof(std::int32_t); + std::vector locBuf(steps); + std::memcpy(locBuf.data(), locator.data(), locator.size()); + FieldLocator const fl(std::move(locBuf)); + + auto const value = hostFunctions_.getLedgerObjNestedField(cacheIdx, fl); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 93db40a25ee56d275191176bad7db392fc65c97c Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 17:11:36 -0400 Subject: [PATCH 11/46] feat: Hook up tx_arr_len host function --- crates/xrpl-host-functions/src/lib.rs | 7 ++++++ .../tests/generated_abi.rs | 11 ++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 9 ++++++++ crates/xrpl-wasm-vm/src/abi.rs | 3 +++ crates/xrpl-wasm-vm/src/register.rs | 9 ++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 3 +++ crates/xrpl-wasm-vm/tests/host_calls.rs | 14 ++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 22 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 5 +++++ src/libxrpl/tx/wasm/HostContext.cpp | 17 ++++++++++++++ 11 files changed, 102 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index adfccb0af1..0e2a7b447b 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -180,6 +180,13 @@ host_functions! { out: &mut [u8], ) -> 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. + #[gas = 40] + #[wasm_name = "tx_arr_len"] + fn get_tx_array_len(&self, field: i32) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 0dc50c5a3b..11f2dcddd3 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -115,6 +115,14 @@ impl HostFunctions for FakeHost { put(out, &[cache_idx as u8, locator[0]]) } + /// A scalar-in, scalar-out count; `NoArray` on a negative selector. + fn get_tx_array_len(&self, field: i32) -> HostResult { + if field < 0 { + return Err(HostError::NoArray); + } + Ok(field) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -169,6 +177,8 @@ fn the_trait_is_implementable() { Ok(2) ); assert_eq!(out[..2], [3, 9]); + assert_eq!(host.get_tx_array_len(3), Ok(3)); + assert_eq!(host.get_tx_array_len(-1), Err(HostError::NoArray)); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -248,6 +258,7 @@ fn the_spec_table_matches_the_declarations() { ("tx_inner", 110), ("home_le_inner", 110), ("le_inner", 110), + ("tx_arr_len", 40), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index fca7d57683..5dc06cfea5 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -221,6 +221,11 @@ mod ffi { out: &mut [u8], ) -> i32; + /// Answers the array's element count directly, or a negative `HostError` code. + #[namespace = "xrpl"] + #[cxx_name = "getTxArrayLen"] + fn get_tx_array_len(self: &HostContext, field: i32) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -340,6 +345,10 @@ impl HostFunctions for CxxHost<'_> { ) } + fn get_tx_array_len(&self, field: i32) -> HostResult { + scalar(self.ctx.get_tx_array_len(field)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index daa8cf9f17..638c119412 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -235,6 +235,9 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_tx_array_len(&self, _field: i32) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 1dc57f0751..99c86b86b0 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -221,6 +221,15 @@ pub(crate) fn register_host_functions( ) }, ), + HostFunctionSpec::GetTxArrayLen => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, field: i32| -> Result { + charged(&mut caller, HostFunctionSpec::GetTxArrayLen, |c| { + c.data().host.get_tx_array_len(field) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 f415169c6b..7181ac4ae8 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -111,6 +111,9 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $le_inner (i32.const 1) (i32.const 0) (i32.const 4) (i32.const 8) (i32.const 4))", 5, ), + HostFunctionSpec::GetTxArrayLen => { + (import::TX_ARR_LEN, "(call $tx_arr_len (i32.const 1))", 1) + } HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 77e1c4205a..660b50c4ff 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -243,6 +243,20 @@ fn le_inner_reads_the_slot_and_locator_and_writes_the_field() { assert_eq!(*host.le_nested_asked.borrow(), vec![(3, locator)]); } +/// A scalar-in, scalar-out call — no memory regions at all: the field selector +/// reaches the host and the array length comes back as the status. +#[test] +fn tx_arr_len_passes_the_selector_and_returns_the_count() { + let host = FakeHost::new().answering_tx_arr_len(17, 5); + + let wat = module( + &[import::TX_ARR_LEN, ONE_PAGE], + "(call $tx_arr_len (i32.const 17))", + ); + assert_eq!(status(&wat, &host), 5, "the array length"); + assert_eq!(*host.tx_arr_lens_asked.borrow(), vec![17]); +} + /// 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 70ab994903..ab166d8b21 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; 15] = [ +const ALL_IMPORTS: [&str; 16] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -111,6 +111,7 @@ const ALL_IMPORTS: [&str; 15] = [ import::TX_INNER, import::HOME_LE_INNER, import::LE_INNER, + import::TX_ARR_LEN, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index e93d33c1a0..af022d52b1 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -145,6 +145,11 @@ pub struct FakeHost { pub le_nested: HashMap<(i32, Vec), Answer>, /// Every (cache slot, locator) `get_ledger_obj_nested_field` was asked for. pub le_nested_asked: RefCell)>>, + /// What `get_tx_array_len` answers, by field selector. An unlisted selector + /// answers `NoArray`. + pub tx_arr_lens: HashMap, + /// Every field selector `get_tx_array_len` was asked for. + pub tx_arr_lens_asked: RefCell>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -184,6 +189,8 @@ impl Default for FakeHost { home_le_nested_asked: RefCell::new(Vec::new()), le_nested: HashMap::new(), le_nested_asked: RefCell::new(Vec::new()), + tx_arr_lens: HashMap::new(), + tx_arr_lens_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -262,6 +269,11 @@ impl FakeHost { self } + pub fn answering_tx_arr_len(mut self, field: i32, len: i32) -> FakeHost { + self.tx_arr_lens.insert(field, len); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -365,6 +377,14 @@ impl HostFunctions for FakeHost { } } + fn get_tx_array_len(&self, field: i32) -> HostResult { + self.tx_arr_lens_asked.borrow_mut().push(field); + match self.tx_arr_lens.get(&field) { + Some(&len) => Ok(len), + None => Err(HostError::NoArray), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -414,6 +434,8 @@ pub mod import { r#"(import "host_lib" "tx_inner" (func $tx_inner (param i32 i32 i32 i32) (result i32)))"#; pub const HOME_LE_INNER: &str = r#"(import "host_lib" "home_le_inner" (func $home_le_inner (param i32 i32 i32 i32) (result i32)))"#; pub const LE_INNER: &str = r#"(import "host_lib" "le_inner" (func $le_inner (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const TX_ARR_LEN: &str = + r#"(import "host_lib" "tx_arr_len" (func $tx_arr_len (param 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index a4bd47c26c..08fc35deac 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -92,6 +92,11 @@ public: rust::Slice locator, rust::Slice out) const noexcept; + // Answers the array's element count directly, or a negative `HostFunctionError` + // code (`NoArray` if the field is not an array). + [[nodiscard]] std::int32_t + getTxArrayLen(std::int32_t field) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 49d4e641b0..b03e0d1e07 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -279,6 +279,23 @@ HostContext::getLedgerObjNestedField( }); } +std::int32_t +HostContext::getTxArrayLen(std::int32_t field) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const& knownSFields = SField::getKnownCodeToField(); + auto const it = knownSFields.find(field); + if (it == knownSFields.end()) + return hfErrorToInt(HostFunctionError::InvalidField); + + auto const len = hostFunctions_.getTxArrayLen(*it->second); + if (!len) + return hfErrorToInt(len.error()); + + return *len; + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From c619ae0263523f79932b6ad0696033c94e580b69 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 17:15:30 -0400 Subject: [PATCH 12/46] feat: Hook up home_le_arr_len host function --- crates/xrpl-host-functions/src/lib.rs | 6 +++++ .../tests/generated_abi.rs | 14 ++++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 +++++++ crates/xrpl-wasm-vm/src/abi.rs | 3 +++ crates/xrpl-wasm-vm/src/register.rs | 11 ++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 +++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 14 ++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 22 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 3 +++ src/libxrpl/tx/wasm/HostContext.cpp | 17 ++++++++++++++ 11 files changed, 105 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 0e2a7b447b..de887aec9f 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -187,6 +187,12 @@ host_functions! { #[wasm_name = "tx_arr_len"] fn get_tx_array_len(&self, field: i32) -> HostResult; + /// The number of elements in an array field of the current (escrow) ledger + /// object, as with [`Self::get_tx_array_len`]. + #[gas = 40] + #[wasm_name = "home_le_arr_len"] + fn get_current_ledger_obj_array_len(&self, field: i32) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 11f2dcddd3..f3139674c3 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -123,6 +123,14 @@ impl HostFunctions for FakeHost { Ok(field) } + /// The same, over the current ledger object. + fn get_current_ledger_obj_array_len(&self, field: i32) -> HostResult { + if field < 0 { + return Err(HostError::NoArray); + } + Ok(field + 1) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -179,6 +187,11 @@ fn the_trait_is_implementable() { assert_eq!(out[..2], [3, 9]); assert_eq!(host.get_tx_array_len(3), Ok(3)); assert_eq!(host.get_tx_array_len(-1), Err(HostError::NoArray)); + assert_eq!(host.get_current_ledger_obj_array_len(3), Ok(4)); + assert_eq!( + host.get_current_ledger_obj_array_len(-1), + Err(HostError::NoArray) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -259,6 +272,7 @@ fn the_spec_table_matches_the_declarations() { ("home_le_inner", 110), ("le_inner", 110), ("tx_arr_len", 40), + ("home_le_arr_len", 40), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 5dc06cfea5..c35ddfc6f1 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -226,6 +226,10 @@ mod ffi { #[cxx_name = "getTxArrayLen"] fn get_tx_array_len(self: &HostContext, field: i32) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "getCurrentLedgerObjArrayLen"] + fn get_current_ledger_obj_array_len(self: &HostContext, field: i32) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -349,6 +353,10 @@ impl HostFunctions for CxxHost<'_> { scalar(self.ctx.get_tx_array_len(field)) } + fn get_current_ledger_obj_array_len(&self, field: i32) -> HostResult { + scalar(self.ctx.get_current_ledger_obj_array_len(field)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 638c119412..cf0ef59fcc 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -238,6 +238,9 @@ mod tests { fn get_tx_array_len(&self, _field: i32) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_current_ledger_obj_array_len(&self, _field: i32) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 99c86b86b0..2662e8c6dc 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -230,6 +230,17 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::GetCurrentLedgerObjArrayLen => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, field: i32| -> Result { + charged( + &mut caller, + HostFunctionSpec::GetCurrentLedgerObjArrayLen, + |c| c.data().host.get_current_ledger_obj_array_len(field), + ) + }, + ), HostFunctionSpec::Sha512Half => 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 7181ac4ae8..a94425dde6 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -114,6 +114,11 @@ fn call_for(op: HostFunctionSpec) -> Call { HostFunctionSpec::GetTxArrayLen => { (import::TX_ARR_LEN, "(call $tx_arr_len (i32.const 1))", 1) } + HostFunctionSpec::GetCurrentLedgerObjArrayLen => ( + import::HOME_LE_ARR_LEN, + "(call $home_le_arr_len (i32.const 1))", + 1, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 660b50c4ff..6a853afad0 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -257,6 +257,20 @@ fn tx_arr_len_passes_the_selector_and_returns_the_count() { assert_eq!(*host.tx_arr_lens_asked.borrow(), vec![17]); } +/// The same scalar-in, scalar-out count over the current object, with its own answer +/// set distinct from the transaction's. +#[test] +fn home_le_arr_len_passes_the_selector_and_returns_the_count() { + let host = FakeHost::new().answering_home_le_arr_len(17, 8); + + let wat = module( + &[import::HOME_LE_ARR_LEN, ONE_PAGE], + "(call $home_le_arr_len (i32.const 17))", + ); + assert_eq!(status(&wat, &host), 8, "the array length"); + assert_eq!(*host.home_le_arr_lens_asked.borrow(), vec![17]); +} + /// 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 ab166d8b21..7290438539 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; 16] = [ +const ALL_IMPORTS: [&str; 17] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -112,6 +112,7 @@ const ALL_IMPORTS: [&str; 16] = [ import::HOME_LE_INNER, import::LE_INNER, import::TX_ARR_LEN, + import::HOME_LE_ARR_LEN, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index af022d52b1..8a9baa9f77 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -150,6 +150,11 @@ pub struct FakeHost { pub tx_arr_lens: HashMap, /// Every field selector `get_tx_array_len` was asked for. pub tx_arr_lens_asked: RefCell>, + /// What `get_current_ledger_obj_array_len` answers, by field selector. An + /// unlisted selector answers `NoArray`. + pub home_le_arr_lens: HashMap, + /// Every field selector `get_current_ledger_obj_array_len` was asked for. + pub home_le_arr_lens_asked: RefCell>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -191,6 +196,8 @@ impl Default for FakeHost { le_nested_asked: RefCell::new(Vec::new()), tx_arr_lens: HashMap::new(), tx_arr_lens_asked: RefCell::new(Vec::new()), + home_le_arr_lens: HashMap::new(), + home_le_arr_lens_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -274,6 +281,11 @@ impl FakeHost { self } + pub fn answering_home_le_arr_len(mut self, field: i32, len: i32) -> FakeHost { + self.home_le_arr_lens.insert(field, len); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -385,6 +397,14 @@ impl HostFunctions for FakeHost { } } + fn get_current_ledger_obj_array_len(&self, field: i32) -> HostResult { + self.home_le_arr_lens_asked.borrow_mut().push(field); + match self.home_le_arr_lens.get(&field) { + Some(&len) => Ok(len), + None => Err(HostError::NoArray), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -436,6 +456,8 @@ pub mod import { pub const LE_INNER: &str = r#"(import "host_lib" "le_inner" (func $le_inner (param i32 i32 i32 i32 i32) (result i32)))"#; pub const TX_ARR_LEN: &str = r#"(import "host_lib" "tx_arr_len" (func $tx_arr_len (param i32) (result i32)))"#; + pub const HOME_LE_ARR_LEN: &str = + r#"(import "host_lib" "home_le_arr_len" (func $home_le_arr_len (param 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 08fc35deac..692fff900c 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -97,6 +97,9 @@ public: [[nodiscard]] std::int32_t getTxArrayLen(std::int32_t field) const noexcept; + [[nodiscard]] std::int32_t + getCurrentLedgerObjArrayLen(std::int32_t field) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index b03e0d1e07..91358aef95 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -296,6 +296,23 @@ HostContext::getTxArrayLen(std::int32_t field) const noexcept }); } +std::int32_t +HostContext::getCurrentLedgerObjArrayLen(std::int32_t field) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const& knownSFields = SField::getKnownCodeToField(); + auto const it = knownSFields.find(field); + if (it == knownSFields.end()) + return hfErrorToInt(HostFunctionError::InvalidField); + + auto const len = hostFunctions_.getCurrentLedgerObjArrayLen(*it->second); + if (!len) + return hfErrorToInt(len.error()); + + return *len; + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 930ba88921603625a71e6a6be4b43b6079a1ebc5 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 17:18:56 -0400 Subject: [PATCH 13/46] feat: Hook up le_arr_len host function --- crates/xrpl-host-functions/src/lib.rs | 6 +++++ .../tests/generated_abi.rs | 11 ++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 +++++++ crates/xrpl-wasm-vm/src/abi.rs | 3 +++ crates/xrpl-wasm-vm/src/register.rs | 12 ++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 +++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 14 ++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 22 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 3 +++ src/libxrpl/tx/wasm/HostContext.cpp | 17 ++++++++++++++ 11 files changed, 103 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index de887aec9f..d8602d8127 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -193,6 +193,12 @@ host_functions! { #[wasm_name = "home_le_arr_len"] fn get_current_ledger_obj_array_len(&self, field: i32) -> HostResult; + /// The number of elements in an array field of a previously cached ledger object, + /// selected by its cache slot and `SField` code. + #[gas = 40] + #[wasm_name = "le_arr_len"] + fn get_ledger_obj_array_len(&self, cache_idx: i32, field: i32) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index f3139674c3..93ae0338ad 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -131,6 +131,14 @@ impl HostFunctions for FakeHost { Ok(field + 1) } + /// The same, over a cached object keyed by slot. + fn get_ledger_obj_array_len(&self, cache_idx: i32, field: i32) -> HostResult { + if cache_idx <= 0 || field < 0 { + return Err(HostError::NoArray); + } + Ok(cache_idx + field) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -192,6 +200,8 @@ fn the_trait_is_implementable() { host.get_current_ledger_obj_array_len(-1), Err(HostError::NoArray) ); + assert_eq!(host.get_ledger_obj_array_len(2, 3), Ok(5)); + assert_eq!(host.get_ledger_obj_array_len(0, 3), Err(HostError::NoArray)); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -273,6 +283,7 @@ fn the_spec_table_matches_the_declarations() { ("le_inner", 110), ("tx_arr_len", 40), ("home_le_arr_len", 40), + ("le_arr_len", 40), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index c35ddfc6f1..7f5f96222e 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -230,6 +230,10 @@ mod ffi { #[cxx_name = "getCurrentLedgerObjArrayLen"] fn get_current_ledger_obj_array_len(self: &HostContext, field: i32) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "getLedgerObjArrayLen"] + fn get_ledger_obj_array_len(self: &HostContext, cache_idx: i32, field: i32) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -357,6 +361,10 @@ impl HostFunctions for CxxHost<'_> { scalar(self.ctx.get_current_ledger_obj_array_len(field)) } + fn get_ledger_obj_array_len(&self, cache_idx: i32, field: i32) -> HostResult { + scalar(self.ctx.get_ledger_obj_array_len(cache_idx, field)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index cf0ef59fcc..74894ea7c8 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -241,6 +241,9 @@ mod tests { fn get_current_ledger_obj_array_len(&self, _field: i32) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_ledger_obj_array_len(&self, _cache_idx: i32, _field: i32) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 2662e8c6dc..7772194a3d 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -241,6 +241,18 @@ pub(crate) fn register_host_functions( ) }, ), + HostFunctionSpec::GetLedgerObjArrayLen => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + cache_idx: i32, + field: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetLedgerObjArrayLen, |c| { + c.data().host.get_ledger_obj_array_len(cache_idx, field) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 a94425dde6..7ccf30ddc1 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -119,6 +119,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $home_le_arr_len (i32.const 1))", 1, ), + HostFunctionSpec::GetLedgerObjArrayLen => ( + import::LE_ARR_LEN, + "(call $le_arr_len (i32.const 1) (i32.const 1))", + 2, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 6a853afad0..c5a72ac6cd 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -271,6 +271,20 @@ fn home_le_arr_len_passes_the_selector_and_returns_the_count() { assert_eq!(*host.home_le_arr_lens_asked.borrow(), vec![17]); } +/// The scalar count over a cached object: the slot leads, and both it and the +/// selector reach the host keyed together. +#[test] +fn le_arr_len_passes_the_slot_and_selector_and_returns_the_count() { + let host = FakeHost::new().answering_le_arr_len(2, 17, 9); + + let wat = module( + &[import::LE_ARR_LEN, ONE_PAGE], + "(call $le_arr_len (i32.const 2) (i32.const 17))", + ); + assert_eq!(status(&wat, &host), 9, "the array length"); + assert_eq!(*host.le_arr_lens_asked.borrow(), vec![(2, 17)]); +} + /// 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 7290438539..19c39132f4 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; 17] = [ +const ALL_IMPORTS: [&str; 18] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -113,6 +113,7 @@ const ALL_IMPORTS: [&str; 17] = [ import::LE_INNER, import::TX_ARR_LEN, import::HOME_LE_ARR_LEN, + import::LE_ARR_LEN, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 8a9baa9f77..c4ce465631 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -155,6 +155,11 @@ pub struct FakeHost { pub home_le_arr_lens: HashMap, /// Every field selector `get_current_ledger_obj_array_len` was asked for. pub home_le_arr_lens_asked: RefCell>, + /// What `get_ledger_obj_array_len` answers, by (cache slot, field selector). An + /// unlisted key answers `NoArray`. + pub le_arr_lens: HashMap<(i32, i32), i32>, + /// Every (cache slot, field selector) `get_ledger_obj_array_len` was asked for. + pub le_arr_lens_asked: RefCell>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -198,6 +203,8 @@ impl Default for FakeHost { tx_arr_lens_asked: RefCell::new(Vec::new()), home_le_arr_lens: HashMap::new(), home_le_arr_lens_asked: RefCell::new(Vec::new()), + le_arr_lens: HashMap::new(), + le_arr_lens_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -286,6 +293,11 @@ impl FakeHost { self } + pub fn answering_le_arr_len(mut self, cache_idx: i32, field: i32, len: i32) -> FakeHost { + self.le_arr_lens.insert((cache_idx, field), len); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -405,6 +417,14 @@ impl HostFunctions for FakeHost { } } + fn get_ledger_obj_array_len(&self, cache_idx: i32, field: i32) -> HostResult { + self.le_arr_lens_asked.borrow_mut().push((cache_idx, field)); + match self.le_arr_lens.get(&(cache_idx, field)) { + Some(&len) => Ok(len), + None => Err(HostError::NoArray), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -458,6 +478,8 @@ pub mod import { r#"(import "host_lib" "tx_arr_len" (func $tx_arr_len (param i32) (result i32)))"#; pub const HOME_LE_ARR_LEN: &str = r#"(import "host_lib" "home_le_arr_len" (func $home_le_arr_len (param i32) (result i32)))"#; + pub const LE_ARR_LEN: &str = + r#"(import "host_lib" "le_arr_len" (func $le_arr_len (param 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 692fff900c..84580cef85 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -100,6 +100,9 @@ public: [[nodiscard]] std::int32_t getCurrentLedgerObjArrayLen(std::int32_t field) const noexcept; + [[nodiscard]] std::int32_t + getLedgerObjArrayLen(std::int32_t cacheIdx, std::int32_t field) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 91358aef95..0b39e2bdc5 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -313,6 +313,23 @@ HostContext::getCurrentLedgerObjArrayLen(std::int32_t field) const noexcept }); } +std::int32_t +HostContext::getLedgerObjArrayLen(std::int32_t cacheIdx, std::int32_t field) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const& knownSFields = SField::getKnownCodeToField(); + auto const it = knownSFields.find(field); + if (it == knownSFields.end()) + return hfErrorToInt(HostFunctionError::InvalidField); + + auto const len = hostFunctions_.getLedgerObjArrayLen(cacheIdx, *it->second); + if (!len) + return hfErrorToInt(len.error()); + + return *len; + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 1d9485c9269b9e0d6e5d0fc1fcbe384c1cb6cfc8 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 17:23:06 -0400 Subject: [PATCH 14/46] feat: Hook up tx_inner_arr_len host function --- crates/xrpl-host-functions/src/lib.rs | 6 +++++ .../tests/generated_abi.rs | 14 +++++++++++ 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 | 16 +++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 23 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 3 +++ src/libxrpl/tx/wasm/HostContext.cpp | 20 ++++++++++++++++ 11 files changed, 114 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index d8602d8127..1b355b1127 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -199,6 +199,12 @@ host_functions! { #[wasm_name = "le_arr_len"] 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. + #[gas = 70] + #[wasm_name = "tx_inner_arr_len"] + fn get_tx_nested_array_len(&self, locator: &[u8]) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 93ae0338ad..6b4c82e172 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -139,6 +139,14 @@ impl HostFunctions for FakeHost { Ok(cache_idx + field) } + /// A nested array-length getter, keyed by the locator bytes. + fn get_tx_nested_array_len(&self, locator: &[u8]) -> HostResult { + if locator.is_empty() { + return Err(HostError::LocatorMalformed); + } + Ok(locator.len() as i32) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -202,6 +210,11 @@ fn the_trait_is_implementable() { ); assert_eq!(host.get_ledger_obj_array_len(2, 3), Ok(5)); assert_eq!(host.get_ledger_obj_array_len(0, 3), Err(HostError::NoArray)); + assert_eq!(host.get_tx_nested_array_len(&[9, 0, 0, 0]), Ok(4)); + assert_eq!( + host.get_tx_nested_array_len(&[]), + Err(HostError::LocatorMalformed) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -284,6 +297,7 @@ fn the_spec_table_matches_the_declarations() { ("tx_arr_len", 40), ("home_le_arr_len", 40), ("le_arr_len", 40), + ("tx_inner_arr_len", 70), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 7f5f96222e..571728198c 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -234,6 +234,10 @@ mod ffi { #[cxx_name = "getLedgerObjArrayLen"] fn get_ledger_obj_array_len(self: &HostContext, cache_idx: i32, field: i32) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "getTxNestedArrayLen"] + fn get_tx_nested_array_len(self: &HostContext, locator: &[u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -365,6 +369,10 @@ impl HostFunctions for CxxHost<'_> { scalar(self.ctx.get_ledger_obj_array_len(cache_idx, field)) } + fn get_tx_nested_array_len(&self, locator: &[u8]) -> HostResult { + scalar(self.ctx.get_tx_nested_array_len(locator)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 74894ea7c8..63b8459759 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -244,6 +244,9 @@ mod tests { fn get_ledger_obj_array_len(&self, _cache_idx: i32, _field: i32) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_tx_nested_array_len(&self, _locator: &[u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 7772194a3d..11be276a22 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -253,6 +253,20 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::GetTxNestedArrayLen => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + loc_ptr: i32, + loc_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetTxNestedArrayLen, |c| { + let host = c.data().host; + let locator = read_borrowed(c, Region::new(loc_ptr, loc_len))?; + host.get_tx_nested_array_len(locator) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 7ccf30ddc1..4ae9b2b7cf 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -124,6 +124,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $le_arr_len (i32.const 1) (i32.const 1))", 2, ), + HostFunctionSpec::GetTxNestedArrayLen => ( + import::TX_INNER_ARR_LEN, + "(call $tx_inner_arr_len (i32.const 0) (i32.const 4))", + 2, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index c5a72ac6cd..08d631797b 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -285,6 +285,22 @@ fn le_arr_len_passes_the_slot_and_selector_and_returns_the_count() { assert_eq!(*host.le_arr_lens_asked.borrow(), vec![(2, 17)]); } +/// A nested array-length getter: the locator is read from memory and the count comes +/// back as the status — read-input, scalar-out, no output buffer. +#[test] +fn tx_inner_arr_len_reads_the_locator_and_returns_the_count() { + let locator = vec![5u8, 0, 0, 0]; + let host = FakeHost::new().answering_tx_nested_arr_len(locator.clone(), 6); + + let wat = module( + &[import::TX_INNER_ARR_LEN, ONE_PAGE], + "(i32.store (i32.const 0) (i32.const 5)) + (call $tx_inner_arr_len (i32.const 0) (i32.const 4))", + ); + assert_eq!(status(&wat, &host), 6, "the array length"); + assert_eq!(*host.tx_nested_arr_lens_asked.borrow(), vec![locator]); +} + /// 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 19c39132f4..08578d172b 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; 18] = [ +const ALL_IMPORTS: [&str; 19] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -114,6 +114,7 @@ const ALL_IMPORTS: [&str; 18] = [ import::TX_ARR_LEN, import::HOME_LE_ARR_LEN, import::LE_ARR_LEN, + import::TX_INNER_ARR_LEN, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index c4ce465631..2bfc049644 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -160,6 +160,11 @@ pub struct FakeHost { pub le_arr_lens: HashMap<(i32, i32), i32>, /// Every (cache slot, field selector) `get_ledger_obj_array_len` was asked for. pub le_arr_lens_asked: RefCell>, + /// What `get_tx_nested_array_len` answers, by locator bytes. An unlisted locator + /// answers `NoArray`. + pub tx_nested_arr_lens: HashMap, i32>, + /// Every locator `get_tx_nested_array_len` was asked for. + pub tx_nested_arr_lens_asked: RefCell>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -205,6 +210,8 @@ impl Default for FakeHost { home_le_arr_lens_asked: RefCell::new(Vec::new()), le_arr_lens: HashMap::new(), le_arr_lens_asked: RefCell::new(Vec::new()), + tx_nested_arr_lens: HashMap::new(), + tx_nested_arr_lens_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -298,6 +305,11 @@ impl FakeHost { self } + pub fn answering_tx_nested_arr_len(mut self, locator: Vec, len: i32) -> FakeHost { + self.tx_nested_arr_lens.insert(locator, len); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -425,6 +437,16 @@ impl HostFunctions for FakeHost { } } + fn get_tx_nested_array_len(&self, locator: &[u8]) -> HostResult { + self.tx_nested_arr_lens_asked + .borrow_mut() + .push(locator.to_vec()); + match self.tx_nested_arr_lens.get(locator) { + Some(&len) => Ok(len), + None => Err(HostError::NoArray), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -480,6 +502,7 @@ pub mod import { r#"(import "host_lib" "home_le_arr_len" (func $home_le_arr_len (param i32) (result i32)))"#; pub const LE_ARR_LEN: &str = r#"(import "host_lib" "le_arr_len" (func $le_arr_len (param i32 i32) (result i32)))"#; + pub const TX_INNER_ARR_LEN: &str = r#"(import "host_lib" "tx_inner_arr_len" (func $tx_inner_arr_len (param 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 84580cef85..b691a2b249 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -103,6 +103,9 @@ public: [[nodiscard]] std::int32_t getLedgerObjArrayLen(std::int32_t cacheIdx, std::int32_t field) const noexcept; + [[nodiscard]] std::int32_t + getTxNestedArrayLen(rust::Slice locator) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 0b39e2bdc5..fff8a56f11 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -330,6 +330,26 @@ HostContext::getLedgerObjArrayLen(std::int32_t cacheIdx, std::int32_t field) con }); } +std::int32_t +HostContext::getTxNestedArrayLen(rust::Slice locator) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (locator.empty() || (locator.size() & 3) != 0) + return hfErrorToInt(HostFunctionError::LocatorMalformed); + + std::uint32_t const steps = locator.size() / sizeof(std::int32_t); + std::vector locBuf(steps); + std::memcpy(locBuf.data(), locator.data(), locator.size()); + FieldLocator const fl(std::move(locBuf)); + + auto const len = hostFunctions_.getTxNestedArrayLen(fl); + if (!len) + return hfErrorToInt(len.error()); + + return *len; + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From e23f8e266a5bb52136508a78649b40770fa75b8d Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 17:27:13 -0400 Subject: [PATCH 15/46] feat: Hook up home_le_inner_arr_len host function --- crates/xrpl-host-functions/src/lib.rs | 6 +++++ .../tests/generated_abi.rs | 17 ++++++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 +++++++ crates/xrpl-wasm-vm/src/abi.rs | 3 +++ crates/xrpl-wasm-vm/src/register.rs | 18 +++++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 16 +++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 23 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 3 +++ src/libxrpl/tx/wasm/HostContext.cpp | 21 +++++++++++++++++ 11 files changed, 122 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 1b355b1127..5e1a541751 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -205,6 +205,12 @@ host_functions! { #[wasm_name = "tx_inner_arr_len"] fn get_tx_nested_array_len(&self, locator: &[u8]) -> HostResult; + /// The number of elements in a nested array field of the current (escrow) ledger + /// object, reached by a `locator`, as with [`Self::get_tx_nested_array_len`]. + #[gas = 70] + #[wasm_name = "home_le_inner_arr_len"] + fn get_current_ledger_obj_nested_array_len(&self, locator: &[u8]) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 6b4c82e172..4806091d90 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -147,6 +147,14 @@ impl HostFunctions for FakeHost { Ok(locator.len() as i32) } + /// The same, over the current ledger object. + fn get_current_ledger_obj_nested_array_len(&self, locator: &[u8]) -> HostResult { + if locator.is_empty() { + return Err(HostError::LocatorMalformed); + } + Ok(locator.len() as i32 + 1) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -215,6 +223,14 @@ fn the_trait_is_implementable() { host.get_tx_nested_array_len(&[]), Err(HostError::LocatorMalformed) ); + assert_eq!( + host.get_current_ledger_obj_nested_array_len(&[9, 0, 0, 0]), + Ok(5) + ); + assert_eq!( + host.get_current_ledger_obj_nested_array_len(&[]), + Err(HostError::LocatorMalformed) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -298,6 +314,7 @@ fn the_spec_table_matches_the_declarations() { ("home_le_arr_len", 40), ("le_arr_len", 40), ("tx_inner_arr_len", 70), + ("home_le_inner_arr_len", 70), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 571728198c..317f4e932e 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -238,6 +238,10 @@ mod ffi { #[cxx_name = "getTxNestedArrayLen"] fn get_tx_nested_array_len(self: &HostContext, locator: &[u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "getCurrentLedgerObjNestedArrayLen"] + fn get_current_ledger_obj_nested_array_len(self: &HostContext, locator: &[u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -373,6 +377,10 @@ impl HostFunctions for CxxHost<'_> { scalar(self.ctx.get_tx_nested_array_len(locator)) } + fn get_current_ledger_obj_nested_array_len(&self, locator: &[u8]) -> HostResult { + scalar(self.ctx.get_current_ledger_obj_nested_array_len(locator)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 63b8459759..d59766935b 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -247,6 +247,9 @@ mod tests { fn get_tx_nested_array_len(&self, _locator: &[u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_current_ledger_obj_nested_array_len(&self, _locator: &[u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 11be276a22..db40efda87 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -267,6 +267,24 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::GetCurrentLedgerObjNestedArrayLen => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + loc_ptr: i32, + loc_len: i32| + -> Result { + charged( + &mut caller, + HostFunctionSpec::GetCurrentLedgerObjNestedArrayLen, + |c| { + let host = c.data().host; + let locator = read_borrowed(c, Region::new(loc_ptr, loc_len))?; + host.get_current_ledger_obj_nested_array_len(locator) + }, + ) + }, + ), HostFunctionSpec::Sha512Half => 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 4ae9b2b7cf..f6e19ff35f 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -129,6 +129,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $tx_inner_arr_len (i32.const 0) (i32.const 4))", 2, ), + HostFunctionSpec::GetCurrentLedgerObjNestedArrayLen => ( + import::HOME_LE_INNER_ARR_LEN, + "(call $home_le_inner_arr_len (i32.const 0) (i32.const 4))", + 2, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 08d631797b..30e0caa810 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -301,6 +301,22 @@ fn tx_inner_arr_len_reads_the_locator_and_returns_the_count() { assert_eq!(*host.tx_nested_arr_lens_asked.borrow(), vec![locator]); } +/// The same read-input, scalar-out count over the current object, with its own answer +/// set distinct from the transaction's. +#[test] +fn home_le_inner_arr_len_reads_the_locator_and_returns_the_count() { + let locator = vec![5u8, 0, 0, 0]; + let host = FakeHost::new().answering_home_le_nested_arr_len(locator.clone(), 7); + + let wat = module( + &[import::HOME_LE_INNER_ARR_LEN, ONE_PAGE], + "(i32.store (i32.const 0) (i32.const 5)) + (call $home_le_inner_arr_len (i32.const 0) (i32.const 4))", + ); + assert_eq!(status(&wat, &host), 7, "the array length"); + assert_eq!(*host.home_le_nested_arr_lens_asked.borrow(), vec![locator]); +} + /// 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 08578d172b..3bf2dcc9e0 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; 19] = [ +const ALL_IMPORTS: [&str; 20] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -115,6 +115,7 @@ const ALL_IMPORTS: [&str; 19] = [ import::HOME_LE_ARR_LEN, import::LE_ARR_LEN, import::TX_INNER_ARR_LEN, + import::HOME_LE_INNER_ARR_LEN, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 2bfc049644..0b151cca9c 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -165,6 +165,11 @@ pub struct FakeHost { pub tx_nested_arr_lens: HashMap, i32>, /// Every locator `get_tx_nested_array_len` was asked for. pub tx_nested_arr_lens_asked: RefCell>>, + /// What `get_current_ledger_obj_nested_array_len` answers, by locator bytes. An + /// unlisted locator answers `NoArray`. + pub home_le_nested_arr_lens: HashMap, i32>, + /// Every locator `get_current_ledger_obj_nested_array_len` was asked for. + pub home_le_nested_arr_lens_asked: RefCell>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -212,6 +217,8 @@ impl Default for FakeHost { le_arr_lens_asked: RefCell::new(Vec::new()), tx_nested_arr_lens: HashMap::new(), tx_nested_arr_lens_asked: RefCell::new(Vec::new()), + home_le_nested_arr_lens: HashMap::new(), + home_le_nested_arr_lens_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -310,6 +317,11 @@ impl FakeHost { self } + pub fn answering_home_le_nested_arr_len(mut self, locator: Vec, len: i32) -> FakeHost { + self.home_le_nested_arr_lens.insert(locator, len); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -447,6 +459,16 @@ impl HostFunctions for FakeHost { } } + fn get_current_ledger_obj_nested_array_len(&self, locator: &[u8]) -> HostResult { + self.home_le_nested_arr_lens_asked + .borrow_mut() + .push(locator.to_vec()); + match self.home_le_nested_arr_lens.get(locator) { + Some(&len) => Ok(len), + None => Err(HostError::NoArray), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -503,6 +525,7 @@ pub mod import { pub const LE_ARR_LEN: &str = r#"(import "host_lib" "le_arr_len" (func $le_arr_len (param i32 i32) (result i32)))"#; pub const TX_INNER_ARR_LEN: &str = r#"(import "host_lib" "tx_inner_arr_len" (func $tx_inner_arr_len (param i32 i32) (result i32)))"#; + pub const HOME_LE_INNER_ARR_LEN: &str = r#"(import "host_lib" "home_le_inner_arr_len" (func $home_le_inner_arr_len (param 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index b691a2b249..79ed5d5882 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -106,6 +106,9 @@ public: [[nodiscard]] std::int32_t getTxNestedArrayLen(rust::Slice locator) const noexcept; + [[nodiscard]] std::int32_t + getCurrentLedgerObjNestedArrayLen(rust::Slice locator) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index fff8a56f11..535e4ce7bf 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -350,6 +350,27 @@ HostContext::getTxNestedArrayLen(rust::Slice locator) const }); } +std::int32_t +HostContext::getCurrentLedgerObjNestedArrayLen( + rust::Slice locator) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (locator.empty() || (locator.size() & 3) != 0) + return hfErrorToInt(HostFunctionError::LocatorMalformed); + + std::uint32_t const steps = locator.size() / sizeof(std::int32_t); + std::vector locBuf(steps); + std::memcpy(locBuf.data(), locator.data(), locator.size()); + FieldLocator const fl(std::move(locBuf)); + + auto const len = hostFunctions_.getCurrentLedgerObjNestedArrayLen(fl); + if (!len) + return hfErrorToInt(len.error()); + + return *len; + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 229377abd95d4ec8159b207c53a16cffafa43a95 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 17:30:57 -0400 Subject: [PATCH 16/46] feat: Hook up le_inner_arr_len host function --- crates/xrpl-host-functions/src/lib.rs | 6 ++++ .../tests/generated_abi.rs | 17 +++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 12 ++++++++ crates/xrpl-wasm-vm/src/abi.rs | 7 +++++ crates/xrpl-wasm-vm/src/register.rs | 19 +++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 16 +++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 28 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 4 +++ src/libxrpl/tx/wasm/HostContext.cpp | 22 +++++++++++++++ 11 files changed, 138 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 5e1a541751..3bb3626f52 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -211,6 +211,12 @@ host_functions! { #[wasm_name = "home_le_inner_arr_len"] fn get_current_ledger_obj_nested_array_len(&self, locator: &[u8]) -> HostResult; + /// The number of elements in a nested array field of a previously cached ledger + /// object, selected by its cache slot and reached by a `locator`. + #[gas = 70] + #[wasm_name = "le_inner_arr_len"] + fn get_ledger_obj_nested_array_len(&self, cache_idx: i32, locator: &[u8]) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 4806091d90..1634639e70 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -155,6 +155,14 @@ impl HostFunctions for FakeHost { Ok(locator.len() as i32 + 1) } + /// The same, over a cached object keyed by slot. + fn get_ledger_obj_nested_array_len(&self, cache_idx: i32, locator: &[u8]) -> HostResult { + if cache_idx <= 0 || locator.is_empty() { + return Err(HostError::LocatorMalformed); + } + Ok(cache_idx + locator.len() as i32) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -231,6 +239,14 @@ fn the_trait_is_implementable() { host.get_current_ledger_obj_nested_array_len(&[]), Err(HostError::LocatorMalformed) ); + assert_eq!( + host.get_ledger_obj_nested_array_len(2, &[9, 0, 0, 0]), + Ok(6) + ); + assert_eq!( + host.get_ledger_obj_nested_array_len(0, &[9, 0, 0, 0]), + Err(HostError::LocatorMalformed) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -315,6 +331,7 @@ fn the_spec_table_matches_the_declarations() { ("le_arr_len", 40), ("tx_inner_arr_len", 70), ("home_le_inner_arr_len", 70), + ("le_inner_arr_len", 70), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 317f4e932e..4c05014fb2 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -242,6 +242,14 @@ mod ffi { #[cxx_name = "getCurrentLedgerObjNestedArrayLen"] fn get_current_ledger_obj_nested_array_len(self: &HostContext, locator: &[u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "getLedgerObjNestedArrayLen"] + fn get_ledger_obj_nested_array_len( + self: &HostContext, + cache_idx: i32, + locator: &[u8], + ) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -381,6 +389,10 @@ impl HostFunctions for CxxHost<'_> { scalar(self.ctx.get_current_ledger_obj_nested_array_len(locator)) } + fn get_ledger_obj_nested_array_len(&self, cache_idx: i32, locator: &[u8]) -> HostResult { + scalar(self.ctx.get_ledger_obj_nested_array_len(cache_idx, locator)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index d59766935b..5d886841a6 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -250,6 +250,13 @@ mod tests { fn get_current_ledger_obj_nested_array_len(&self, _locator: &[u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_ledger_obj_nested_array_len( + &self, + _cache_idx: i32, + _locator: &[u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 db40efda87..ffbebde30c 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -285,6 +285,25 @@ pub(crate) fn register_host_functions( ) }, ), + HostFunctionSpec::GetLedgerObjNestedArrayLen => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + cache_idx: i32, + loc_ptr: i32, + loc_len: i32| + -> Result { + charged( + &mut caller, + HostFunctionSpec::GetLedgerObjNestedArrayLen, + |c| { + let host = c.data().host; + let locator = read_borrowed(c, Region::new(loc_ptr, loc_len))?; + host.get_ledger_obj_nested_array_len(cache_idx, locator) + }, + ) + }, + ), HostFunctionSpec::Sha512Half => 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 f6e19ff35f..c43a54343c 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -134,6 +134,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $home_le_inner_arr_len (i32.const 0) (i32.const 4))", 2, ), + HostFunctionSpec::GetLedgerObjNestedArrayLen => ( + import::LE_INNER_ARR_LEN, + "(call $le_inner_arr_len (i32.const 1) (i32.const 0) (i32.const 4))", + 3, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 30e0caa810..5c16eb1c33 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -317,6 +317,22 @@ fn home_le_inner_arr_len_reads_the_locator_and_returns_the_count() { assert_eq!(*host.home_le_nested_arr_lens_asked.borrow(), vec![locator]); } +/// The nested array-length getter over a cached object: the slot leads, the locator +/// is read from memory, and the two reach the host keyed together. +#[test] +fn le_inner_arr_len_reads_the_slot_and_locator_and_returns_the_count() { + let locator = vec![5u8, 0, 0, 0]; + let host = FakeHost::new().answering_le_nested_arr_len(3, locator.clone(), 8); + + let wat = module( + &[import::LE_INNER_ARR_LEN, ONE_PAGE], + "(i32.store (i32.const 0) (i32.const 5)) + (call $le_inner_arr_len (i32.const 3) (i32.const 0) (i32.const 4))", + ); + assert_eq!(status(&wat, &host), 8, "the array length"); + assert_eq!(*host.le_nested_arr_lens_asked.borrow(), vec![(3, locator)]); +} + /// 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 3bf2dcc9e0..2b53cad340 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; 20] = [ +const ALL_IMPORTS: [&str; 21] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -116,6 +116,7 @@ const ALL_IMPORTS: [&str; 20] = [ import::LE_ARR_LEN, import::TX_INNER_ARR_LEN, import::HOME_LE_INNER_ARR_LEN, + import::LE_INNER_ARR_LEN, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 0b151cca9c..df7f10dbd1 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -170,6 +170,11 @@ pub struct FakeHost { pub home_le_nested_arr_lens: HashMap, i32>, /// Every locator `get_current_ledger_obj_nested_array_len` was asked for. pub home_le_nested_arr_lens_asked: RefCell>>, + /// What `get_ledger_obj_nested_array_len` answers, by (cache slot, locator bytes). + /// An unlisted key answers `NoArray`. + pub le_nested_arr_lens: HashMap<(i32, Vec), i32>, + /// Every (cache slot, locator) `get_ledger_obj_nested_array_len` was asked for. + pub le_nested_arr_lens_asked: RefCell)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -219,6 +224,8 @@ impl Default for FakeHost { tx_nested_arr_lens_asked: RefCell::new(Vec::new()), home_le_nested_arr_lens: HashMap::new(), home_le_nested_arr_lens_asked: RefCell::new(Vec::new()), + le_nested_arr_lens: HashMap::new(), + le_nested_arr_lens_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -322,6 +329,16 @@ impl FakeHost { self } + pub fn answering_le_nested_arr_len( + mut self, + cache_idx: i32, + locator: Vec, + len: i32, + ) -> FakeHost { + self.le_nested_arr_lens.insert((cache_idx, locator), len); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -469,6 +486,16 @@ impl HostFunctions for FakeHost { } } + fn get_ledger_obj_nested_array_len(&self, cache_idx: i32, locator: &[u8]) -> HostResult { + self.le_nested_arr_lens_asked + .borrow_mut() + .push((cache_idx, locator.to_vec())); + match self.le_nested_arr_lens.get(&(cache_idx, locator.to_vec())) { + Some(&len) => Ok(len), + None => Err(HostError::NoArray), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -526,6 +553,7 @@ pub mod import { r#"(import "host_lib" "le_arr_len" (func $le_arr_len (param i32 i32) (result i32)))"#; pub const TX_INNER_ARR_LEN: &str = r#"(import "host_lib" "tx_inner_arr_len" (func $tx_inner_arr_len (param i32 i32) (result i32)))"#; pub const HOME_LE_INNER_ARR_LEN: &str = r#"(import "host_lib" "home_le_inner_arr_len" (func $home_le_inner_arr_len (param i32 i32) (result i32)))"#; + pub const LE_INNER_ARR_LEN: &str = r#"(import "host_lib" "le_inner_arr_len" (func $le_inner_arr_len (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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 79ed5d5882..9d4cf3ea03 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -109,6 +109,10 @@ public: [[nodiscard]] std::int32_t getCurrentLedgerObjNestedArrayLen(rust::Slice locator) const noexcept; + [[nodiscard]] std::int32_t + getLedgerObjNestedArrayLen(std::int32_t cacheIdx, rust::Slice locator) + const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 535e4ce7bf..e8f343a562 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -371,6 +371,28 @@ HostContext::getCurrentLedgerObjNestedArrayLen( }); } +std::int32_t +HostContext::getLedgerObjNestedArrayLen( + std::int32_t cacheIdx, + rust::Slice locator) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (locator.empty() || (locator.size() & 3) != 0) + return hfErrorToInt(HostFunctionError::LocatorMalformed); + + std::uint32_t const steps = locator.size() / sizeof(std::int32_t); + std::vector locBuf(steps); + std::memcpy(locBuf.data(), locator.data(), locator.size()); + FieldLocator const fl(std::move(locBuf)); + + auto const len = hostFunctions_.getLedgerObjNestedArrayLen(cacheIdx, fl); + if (!len) + return hfErrorToInt(len.error()); + + return *len; + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 3caaecff076b54bd3ba4d93093d234809266d1b0 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 17:39:53 -0400 Subject: [PATCH 17/46] feat: Hook up check_sig host function --- crates/xrpl-host-functions/src/lib.rs | 17 ++++++++++++ .../tests/generated_abi.rs | 13 ++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 14 ++++++++++ crates/xrpl-wasm-vm/src/abi.rs | 8 ++++++ crates/xrpl-wasm-vm/src/register.rs | 20 ++++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 26 +++++++++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 22 ++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 7 +++++ src/libxrpl/tx/wasm/HostContext.cpp | 18 +++++++++++++ 11 files changed, 152 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 3bb3626f52..be7e0addba 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -217,6 +217,23 @@ 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. + /// + /// GAS DISCREPANCY: this 300 is the value the C-ABI fork registered + /// (`rippled-wasm-host-functions`, WasmVM.cpp), which this port follows. The + /// prior C++ integration in this tree charged 35000 for the same call — 100x + /// more, and closer to the real cost of signature verification. The value is + /// consensus-critical, so confirm which is intended before this ships. + #[gas = 300] + #[wasm_name = "check_sig"] + fn check_signature( + &self, + message: &[u8], + signature: &[u8], + pubkey: &[u8], + ) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 1634639e70..26074bc2ce 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -163,6 +163,16 @@ impl HostFunctions for FakeHost { Ok(cache_idx + locator.len() as i32) } + /// Reads three regions and returns a verdict: valid unless the signature is empty. + fn check_signature( + &self, + _message: &[u8], + signature: &[u8], + _pubkey: &[u8], + ) -> HostResult { + Ok(i32::from(!signature.is_empty())) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -247,6 +257,8 @@ fn the_trait_is_implementable() { host.get_ledger_obj_nested_array_len(0, &[9, 0, 0, 0]), Err(HostError::LocatorMalformed) ); + assert_eq!(host.check_signature(b"msg", b"sig", b"pk"), Ok(1)); + assert_eq!(host.check_signature(b"msg", b"", b"pk"), Ok(0)); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -332,6 +344,7 @@ fn the_spec_table_matches_the_declarations() { ("tx_inner_arr_len", 70), ("home_le_inner_arr_len", 70), ("le_inner_arr_len", 70), + ("check_sig", 300), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 4c05014fb2..cdc220bffe 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -250,6 +250,16 @@ mod ffi { locator: &[u8], ) -> i32; + /// Answers `1`/`0` for a valid/invalid signature, or a negative `HostError`. + #[namespace = "xrpl"] + #[cxx_name = "checkSignature"] + fn check_signature( + self: &HostContext, + message: &[u8], + signature: &[u8], + pubkey: &[u8], + ) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -393,6 +403,10 @@ impl HostFunctions for CxxHost<'_> { scalar(self.ctx.get_ledger_obj_nested_array_len(cache_idx, locator)) } + fn check_signature(&self, message: &[u8], signature: &[u8], pubkey: &[u8]) -> HostResult { + scalar(self.ctx.check_signature(message, signature, pubkey)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 5d886841a6..7b95bd69d6 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -257,6 +257,14 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn check_signature( + &self, + _message: &[u8], + _signature: &[u8], + _pubkey: &[u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 ffbebde30c..1c5d2e9f57 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -304,6 +304,26 @@ pub(crate) fn register_host_functions( ) }, ), + HostFunctionSpec::CheckSignature => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + msg_ptr: i32, + msg_len: i32, + sig_ptr: i32, + sig_len: i32, + pk_ptr: i32, + pk_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::CheckSignature, |c| { + let host = c.data().host; + let message = read_borrowed(c, Region::new(msg_ptr, msg_len))?; + let signature = read_borrowed(c, Region::new(sig_ptr, sig_len))?; + let pubkey = read_borrowed(c, Region::new(pk_ptr, pk_len))?; + host.check_signature(message, signature, pubkey) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 c43a54343c..1d515a727c 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -139,6 +139,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $le_inner_arr_len (i32.const 1) (i32.const 0) (i32.const 4))", 3, ), + HostFunctionSpec::CheckSignature => ( + import::CHECK_SIG, + "(call $check_sig (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0))", + 6, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 5c16eb1c33..51aa5807e8 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -333,6 +333,32 @@ fn le_inner_arr_len_reads_the_slot_and_locator_and_returns_the_count() { assert_eq!(*host.le_nested_arr_lens_asked.borrow(), vec![(3, locator)]); } +/// A call that reads three input regions and returns a scalar verdict: the message, +/// signature, and pubkey all reach the host, and the verdict comes back as the status. +#[test] +fn check_sig_reads_all_three_regions_and_returns_the_verdict() { + let host = FakeHost::new(); // valid by default + + // message @0 len 3, signature @8 len 4, pubkey @16 len 5 — memory is zeroed. + let wat = module( + &[import::CHECK_SIG, ONE_PAGE], + "(call $check_sig + (i32.const 0) (i32.const 3) + (i32.const 8) (i32.const 4) + (i32.const 16) (i32.const 5))", + ); + assert_eq!(status(&wat, &host), 1, "the valid verdict"); + assert_eq!( + *host.sigs_checked.borrow(), + [(vec![0u8; 3], vec![0u8; 4], vec![0u8; 5])], + "the three regions reached the host at their declared lengths" + ); + + // An invalid signature comes back as 0 — a value, not an error. + let host = FakeHost::new().answering_check_sig(Ok(0)); + assert_eq!(status(&wat, &host), 0, "the invalid verdict"); +} + /// 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 2b53cad340..97f589d763 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; 21] = [ +const ALL_IMPORTS: [&str; 22] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -117,6 +117,7 @@ const ALL_IMPORTS: [&str; 21] = [ import::TX_INNER_ARR_LEN, import::HOME_LE_INNER_ARR_LEN, import::LE_INNER_ARR_LEN, + import::CHECK_SIG, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index df7f10dbd1..0b8c3f1939 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -175,6 +175,10 @@ pub struct FakeHost { pub le_nested_arr_lens: HashMap<(i32, Vec), i32>, /// Every (cache slot, locator) `get_ledger_obj_nested_array_len` was asked for. pub le_nested_arr_lens_asked: RefCell)>>, + /// What `check_signature` answers, whatever it is given. + pub sig_valid: HostResult, + /// Every (message, signature, pubkey) `check_signature` was asked to verify. + pub sigs_checked: RefCell, Vec, Vec)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -226,6 +230,9 @@ impl Default for FakeHost { home_le_nested_arr_lens_asked: RefCell::new(Vec::new()), le_nested_arr_lens: HashMap::new(), le_nested_arr_lens_asked: RefCell::new(Vec::new()), + // Valid by default; the verification itself is the host's job, not the ABI's. + sig_valid: Ok(1), + sigs_checked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -339,6 +346,11 @@ impl FakeHost { self } + pub fn answering_check_sig(mut self, answer: HostResult) -> FakeHost { + self.sig_valid = answer; + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -496,6 +508,15 @@ impl HostFunctions for FakeHost { } } + fn check_signature(&self, message: &[u8], signature: &[u8], pubkey: &[u8]) -> HostResult { + self.sigs_checked.borrow_mut().push(( + message.to_vec(), + signature.to_vec(), + pubkey.to_vec(), + )); + self.sig_valid + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -554,6 +575,7 @@ pub mod import { pub const TX_INNER_ARR_LEN: &str = r#"(import "host_lib" "tx_inner_arr_len" (func $tx_inner_arr_len (param i32 i32) (result i32)))"#; pub const HOME_LE_INNER_ARR_LEN: &str = r#"(import "host_lib" "home_le_inner_arr_len" (func $home_le_inner_arr_len (param i32 i32) (result i32)))"#; pub const LE_INNER_ARR_LEN: &str = r#"(import "host_lib" "le_inner_arr_len" (func $le_inner_arr_len (param i32 i32 i32) (result i32)))"#; + pub const CHECK_SIG: &str = r#"(import "host_lib" "check_sig" (func $check_sig (param i32 i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 9d4cf3ea03..75ad57d064 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -113,6 +113,13 @@ public: getLedgerObjNestedArrayLen(std::int32_t cacheIdx, rust::Slice locator) const noexcept; + // Answers 1/0 for a valid/invalid signature, or a negative `HostFunctionError`. + [[nodiscard]] std::int32_t + checkSignature( + rust::Slice message, + rust::Slice signature, + rust::Slice pubkey) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index e8f343a562..61df2c6bf3 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -393,6 +393,24 @@ HostContext::getLedgerObjNestedArrayLen( }); } +std::int32_t +HostContext::checkSignature( + rust::Slice message, + rust::Slice signature, + rust::Slice pubkey) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const valid = hostFunctions_.checkSignature( + Slice{message.data(), message.size()}, + Slice{signature.data(), signature.size()}, + Slice{pubkey.data(), pubkey.size()}); + if (!valid) + return hfErrorToInt(valid.error()); + + return *valid; + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 17fb37871c9d98a1be3b40f8d367efe022cc490e Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 17:46:07 -0400 Subject: [PATCH 18/46] feat: Hook up accountroot_id host function --- crates/xrpl-host-functions/src/lib.rs | 6 ++++ .../tests/generated_abi.rs | 16 ++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 +++++ crates/xrpl-wasm-vm/src/abi.rs | 3 ++ crates/xrpl-wasm-vm/src/register.rs | 18 +++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 +++ crates/xrpl-wasm-vm/tests/host_calls.rs | 31 +++++++++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 23 ++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 5 +++ src/libxrpl/tx/wasm/HostContext.cpp | 17 ++++++++++ 11 files changed, 134 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index be7e0addba..cd343fbf68 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -234,6 +234,12 @@ host_functions! { pubkey: &[u8], ) -> 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. + #[gas = 350] + #[wasm_name = "accountroot_id"] + fn account_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 26074bc2ce..bde6b3cf94 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -173,6 +173,15 @@ impl HostFunctions for FakeHost { Ok(i32::from(!signature.is_empty())) } + /// A keylet getter: reads an account, writes a 32-byte keylet; `InvalidAccount` + /// on an empty account. + fn account_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult { + if account.is_empty() { + return Err(HostError::InvalidAccount); + } + put(out, &[account[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -259,6 +268,12 @@ fn the_trait_is_implementable() { ); assert_eq!(host.check_signature(b"msg", b"sig", b"pk"), Ok(1)); assert_eq!(host.check_signature(b"msg", b"", b"pk"), Ok(0)); + assert_eq!(host.account_keylet(&[7; 20], &mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 7); + assert_eq!( + host.account_keylet(&[], &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -345,6 +360,7 @@ fn the_spec_table_matches_the_declarations() { ("home_le_inner_arr_len", 70), ("le_inner_arr_len", 70), ("check_sig", 300), + ("accountroot_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index cdc220bffe..2f030715f4 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -260,6 +260,10 @@ mod ffi { pubkey: &[u8], ) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "accountKeylet"] + fn account_keylet(self: &HostContext, account: &[u8], out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -407,6 +411,10 @@ impl HostFunctions for CxxHost<'_> { scalar(self.ctx.check_signature(message, signature, pubkey)) } + fn account_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.account_keylet(account, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 7b95bd69d6..daf03874c9 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -265,6 +265,9 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn account_keylet(&self, _account: &[u8], _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 1c5d2e9f57..2287d06396 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -324,6 +324,24 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::AccountKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::AccountKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + write_buffered(c, out, |host, data, buf| { + host.account_keylet(account.read(data)?, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 1d515a727c..cd4c52ba1f 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -144,6 +144,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $check_sig (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0))", 6, ), + HostFunctionSpec::AccountKeylet => ( + import::ACCOUNTROOT_ID, + "(call $accountroot_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 32))", + 4, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 51aa5807e8..30e28cd705 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -359,6 +359,37 @@ fn check_sig_reads_all_three_regions_and_returns_the_verdict() { assert_eq!(status(&wat, &host), 0, "the invalid verdict"); } +/// A keylet getter: reads an account region and writes a 32-byte keylet back — the +/// read-input-write-output path. The account reaches the host and the keylet lands +/// where the guest asked. +#[test] +fn accountroot_id_reads_the_account_and_writes_the_keylet() { + // Guest memory is zeroed, so a 20-byte account read is all zeros. + let account = vec![0u8; 20]; + let host = + FakeHost::new().answering_account_keylet(account.clone(), support::Answer::filler(32)); + + let wat = module( + &[import::ACCOUNTROOT_ID, ONE_PAGE], + "(call $accountroot_id (i32.const 0) (i32.const 20) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!(*host.account_keylets_asked.borrow(), vec![account]); + + // The keylet bytes land at the output pointer: filler is 0, 1, 2, ..., so the + // first four load as 0x03020100. + let wat = module( + &[import::ACCOUNTROOT_ID, ONE_PAGE], + "(drop (call $accountroot_id (i32.const 0) (i32.const 20) (i32.const 64) (i32.const 64))) + (i32.load (i32.const 64))", + ); + assert_eq!( + status(&wat, &host), + 0x03020100, + "the first four keylet bytes" + ); +} + /// 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 97f589d763..2fac1dc7ab 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; 22] = [ +const ALL_IMPORTS: [&str; 23] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -118,6 +118,7 @@ const ALL_IMPORTS: [&str; 22] = [ import::HOME_LE_INNER_ARR_LEN, import::LE_INNER_ARR_LEN, import::CHECK_SIG, + import::ACCOUNTROOT_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 0b8c3f1939..778205be91 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -179,6 +179,11 @@ pub struct FakeHost { pub sig_valid: HostResult, /// Every (message, signature, pubkey) `check_signature` was asked to verify. pub sigs_checked: RefCell, Vec, Vec)>>, + /// What `account_keylet` answers, by account bytes. An unlisted account answers + /// `InvalidAccount`. + pub account_keylets: HashMap, Answer>, + /// Every account `account_keylet` was asked for. + pub account_keylets_asked: RefCell>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -233,6 +238,8 @@ impl Default for FakeHost { // Valid by default; the verification itself is the host's job, not the ABI's. sig_valid: Ok(1), sigs_checked: RefCell::new(Vec::new()), + account_keylets: HashMap::new(), + account_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -351,6 +358,11 @@ impl FakeHost { self } + pub fn answering_account_keylet(mut self, account: Vec, answer: Answer) -> FakeHost { + self.account_keylets.insert(account, answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -517,6 +529,16 @@ impl HostFunctions for FakeHost { self.sig_valid } + fn account_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult { + self.account_keylets_asked + .borrow_mut() + .push(account.to_vec()); + match self.account_keylets.get(account) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -576,6 +598,7 @@ pub mod import { pub const HOME_LE_INNER_ARR_LEN: &str = r#"(import "host_lib" "home_le_inner_arr_len" (func $home_le_inner_arr_len (param i32 i32) (result i32)))"#; pub const LE_INNER_ARR_LEN: &str = r#"(import "host_lib" "le_inner_arr_len" (func $le_inner_arr_len (param i32 i32 i32) (result i32)))"#; pub const CHECK_SIG: &str = r#"(import "host_lib" "check_sig" (func $check_sig (param i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const ACCOUNTROOT_ID: &str = r#"(import "host_lib" "accountroot_id" (func $accountroot_id (param i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 75ad57d064..62bbf4085e 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -120,6 +120,11 @@ public: rust::Slice signature, rust::Slice pubkey) const noexcept; + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + accountKeylet(rust::Slice account, rust::Slice out) + const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 61df2c6bf3..9cd9ab83bf 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -411,6 +412,22 @@ HostContext::checkSignature( }); } +std::int32_t +HostContext::accountKeylet(rust::Slice account, rust::Slice out) + const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.accountKeylet(AccountID::fromVoid(account.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 13196b839ee50770066a6afc995815b55eb71330 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 17:53:59 -0400 Subject: [PATCH 19/46] feat: Hook up amm_id host function --- crates/xrpl-host-functions/src/lib.rs | 7 +++ .../tests/generated_abi.rs | 15 +++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 +++ crates/xrpl-wasm-vm/src/abi.rs | 3 + crates/xrpl-wasm-vm/src/register.rs | 21 +++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++ crates/xrpl-wasm-vm/tests/host_calls.rs | 24 ++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 28 +++++++++ include/xrpl/tx/wasm/HostContext.h | 8 +++ src/libxrpl/tx/wasm/HostContext.cpp | 57 +++++++++++++++++++ 11 files changed, 178 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index cd343fbf68..ca1efb98fa 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -240,6 +240,13 @@ host_functions! { #[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. + #[gas = 450] + #[wasm_name = "amm_id"] + fn amm_keylet(&self, asset1: &[u8], asset2: &[u8], out: &mut [u8]) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index bde6b3cf94..e5b99aa542 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -182,6 +182,14 @@ impl HostFunctions for FakeHost { put(out, &[account[0]; HASH_LEN]) } + /// A two-asset keylet getter; `InvalidParams` if the two assets are equal. + fn amm_keylet(&self, asset1: &[u8], asset2: &[u8], out: &mut [u8]) -> HostResult { + if asset1 == asset2 { + return Err(HostError::InvalidParams); + } + put(out, &[asset1.len() as u8; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -274,6 +282,12 @@ fn the_trait_is_implementable() { host.account_keylet(&[], &mut out), Err(HostError::InvalidAccount) ); + assert_eq!(host.amm_keylet(&[1; 20], &[2; 40], &mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 20); + assert_eq!( + host.amm_keylet(&[1; 20], &[1; 20], &mut out), + Err(HostError::InvalidParams) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -361,6 +375,7 @@ fn the_spec_table_matches_the_declarations() { ("le_inner_arr_len", 70), ("check_sig", 300), ("accountroot_id", 350), + ("amm_id", 450), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 2f030715f4..01776c6ed0 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -264,6 +264,10 @@ mod ffi { #[cxx_name = "accountKeylet"] fn account_keylet(self: &HostContext, account: &[u8], out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "ammKeylet"] + fn amm_keylet(self: &HostContext, asset1: &[u8], asset2: &[u8], out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -415,6 +419,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.account_keylet(account, out)) } + fn amm_keylet(&self, asset1: &[u8], asset2: &[u8], out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.amm_keylet(asset1, asset2, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index daf03874c9..d9344095e6 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -268,6 +268,9 @@ mod tests { fn account_keylet(&self, _account: &[u8], _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn amm_keylet(&self, _asset1: &[u8], _asset2: &[u8], _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 2287d06396..6d276f3c4e 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -342,6 +342,27 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::AmmKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + a1_ptr: i32, + a1_len: i32, + a2_ptr: i32, + a2_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::AmmKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let asset1 = Region::new(a1_ptr, a1_len); + let asset2 = Region::new(a2_ptr, a2_len); + write_buffered(c, out, |host, data, buf| { + host.amm_keylet(asset1.read(data)?, asset2.read(data)?, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 cd4c52ba1f..13d95a9039 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -149,6 +149,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $accountroot_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 32))", 4, ), + HostFunctionSpec::AmmKeylet => ( + import::AMM_ID, + "(call $amm_id (i32.const 0) (i32.const 20) (i32.const 24) (i32.const 40) (i32.const 0) (i32.const 32))", + 6, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 30e28cd705..d8000aa898 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -390,6 +390,30 @@ fn accountroot_id_reads_the_account_and_writes_the_keylet() { ); } +/// A keylet getter that reads two input regions: both assets reach the host as a +/// pair, and the keylet lands where the guest asked. +#[test] +fn amm_id_reads_two_assets_and_writes_the_keylet() { + // Two distinct all-zero assets of different lengths (20 and 40 bytes). + let asset1 = vec![0u8; 20]; + let asset2 = vec![0u8; 40]; + let host = FakeHost::new().answering_amm_keylet( + asset1.clone(), + asset2.clone(), + support::Answer::filler(32), + ); + + let wat = module( + &[import::AMM_ID, ONE_PAGE], + "(call $amm_id + (i32.const 0) (i32.const 20) + (i32.const 64) (i32.const 40) + (i32.const 128) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!(*host.amm_keylets_asked.borrow(), vec![(asset1, asset2)]); +} + /// 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 2fac1dc7ab..285c5d016b 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; 23] = [ +const ALL_IMPORTS: [&str; 24] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -119,6 +119,7 @@ const ALL_IMPORTS: [&str; 23] = [ import::LE_INNER_ARR_LEN, import::CHECK_SIG, import::ACCOUNTROOT_ID, + import::AMM_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 778205be91..43c0bff329 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -184,6 +184,11 @@ pub struct FakeHost { pub account_keylets: HashMap, Answer>, /// Every account `account_keylet` was asked for. pub account_keylets_asked: RefCell>>, + /// What `amm_keylet` answers, by (asset1, asset2) bytes. An unlisted pair answers + /// `InvalidParams`. + pub amm_keylets: HashMap<(Vec, Vec), Answer>, + /// Every (asset1, asset2) pair `amm_keylet` was asked for. + pub amm_keylets_asked: RefCell, Vec)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -240,6 +245,8 @@ impl Default for FakeHost { sigs_checked: RefCell::new(Vec::new()), account_keylets: HashMap::new(), account_keylets_asked: RefCell::new(Vec::new()), + amm_keylets: HashMap::new(), + amm_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -363,6 +370,16 @@ impl FakeHost { self } + pub fn answering_amm_keylet( + mut self, + asset1: Vec, + asset2: Vec, + answer: Answer, + ) -> FakeHost { + self.amm_keylets.insert((asset1, asset2), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -539,6 +556,16 @@ impl HostFunctions for FakeHost { } } + fn amm_keylet(&self, asset1: &[u8], asset2: &[u8], out: &mut [u8]) -> HostResult { + self.amm_keylets_asked + .borrow_mut() + .push((asset1.to_vec(), asset2.to_vec())); + match self.amm_keylets.get(&(asset1.to_vec(), asset2.to_vec())) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidParams), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -599,6 +626,7 @@ pub mod import { pub const LE_INNER_ARR_LEN: &str = r#"(import "host_lib" "le_inner_arr_len" (func $le_inner_arr_len (param i32 i32 i32) (result i32)))"#; pub const CHECK_SIG: &str = r#"(import "host_lib" "check_sig" (func $check_sig (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const ACCOUNTROOT_ID: &str = r#"(import "host_lib" "accountroot_id" (func $accountroot_id (param i32 i32 i32 i32) (result i32)))"#; + pub const AMM_ID: &str = r#"(import "host_lib" "amm_id" (func $amm_id (param i32 i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 62bbf4085e..5af72a16bf 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -125,6 +125,14 @@ public: accountKeylet(rust::Slice account, rust::Slice out) const noexcept; + // Each asset is decoded by length (24 = MPT, 20 = XRP, 40 = issue), else + // `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + ammKeylet( + rust::Slice asset1, + rust::Slice asset2, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 9cd9ab83bf..1bc7005fa3 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -3,7 +3,10 @@ #include #include #include +#include +#include #include +#include #include #include @@ -12,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +60,36 @@ answerScalar(rust::Slice out, T value) return answer(out, reinterpret_cast(&wire), sizeof(wire)); } +// 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. +std::expected +parseAsset(rust::Slice bytes) +{ + if (bytes.size() == MPTID::size()) + return Asset{MPTID::fromVoid(bytes.data())}; + + if (bytes.size() == Currency::size()) + { + auto const issue = Issue{Currency::fromVoid(bytes.data()), xrpAccount()}; + if (!issue.native()) + return std::unexpected(HostFunctionError::InvalidParams); + return Asset{issue}; + } + + if (bytes.size() == Currency::size() + AccountID::size()) + { + auto const issue = Issue( + Currency::fromVoid(bytes.data()), AccountID::fromVoid(bytes.data() + Currency::size())); + if (issue.native()) + return std::unexpected(HostFunctionError::InvalidParams); + return Asset{issue}; + } + + return std::unexpected(HostFunctionError::InvalidParams); +} + } // namespace HostContext::HostContext(HostFunctions& hostFunctions) : hostFunctions_(hostFunctions) @@ -428,6 +462,29 @@ HostContext::accountKeylet(rust::Slice account, rust::Slice< }); } +std::int32_t +HostContext::ammKeylet( + rust::Slice asset1, + rust::Slice asset2, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + auto const a1 = parseAsset(asset1); + if (!a1) + return hfErrorToInt(a1.error()); + + auto const a2 = parseAsset(asset2); + if (!a2) + return hfErrorToInt(a2.error()); + + auto const value = hostFunctions_.ammKeylet(*a1, *a2); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From e60029d5a0f243681fcaa82e101aa1cbf66c882f Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 17:59:57 -0400 Subject: [PATCH 20/46] feat: Hook up check_id host function --- crates/xrpl-host-functions/src/lib.rs | 7 +++++ .../tests/generated_abi.rs | 15 ++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 ++++++ crates/xrpl-wasm-vm/src/abi.rs | 3 ++ crates/xrpl-wasm-vm/src/register.rs | 19 +++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 17 +++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 28 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 8 ++++++ src/libxrpl/tx/wasm/HostContext.cpp | 20 +++++++++++++ 11 files changed, 132 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index ca1efb98fa..39a5f20662 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -247,6 +247,13 @@ host_functions! { #[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 XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index e5b99aa542..f1761235b3 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -190,6 +190,14 @@ impl HostFunctions for FakeHost { put(out, &[asset1.len() as u8; HASH_LEN]) } + /// A keylet from an account and a sequence; `InvalidAccount` on an empty account. + fn check_keylet(&self, account: &[u8], _seq: i32, out: &mut [u8]) -> HostResult { + if account.is_empty() { + return Err(HostError::InvalidAccount); + } + put(out, &[account[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -288,6 +296,12 @@ fn the_trait_is_implementable() { host.amm_keylet(&[1; 20], &[1; 20], &mut out), Err(HostError::InvalidParams) ); + assert_eq!(host.check_keylet(&[7; 20], 5, &mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 7); + assert_eq!( + host.check_keylet(&[], 5, &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -376,6 +390,7 @@ fn the_spec_table_matches_the_declarations() { ("check_sig", 300), ("accountroot_id", 350), ("amm_id", 450), + ("check_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 01776c6ed0..5db3eabaaf 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -268,6 +268,10 @@ mod ffi { #[cxx_name = "ammKeylet"] fn amm_keylet(self: &HostContext, asset1: &[u8], asset2: &[u8], out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "checkKeylet"] + fn check_keylet(self: &HostContext, account: &[u8], seq: i32, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -423,6 +427,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.amm_keylet(asset1, asset2, out)) } + fn check_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.check_keylet(account, seq, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index d9344095e6..78d8bfa523 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -271,6 +271,9 @@ mod tests { fn amm_keylet(&self, _asset1: &[u8], _asset2: &[u8], _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn check_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 6d276f3c4e..82544efaab 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -363,6 +363,25 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::CheckKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + seq: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::CheckKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + write_buffered(c, out, |host, data, buf| { + host.check_keylet(account.read(data)?, seq, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 13d95a9039..ab2a610ac9 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -154,6 +154,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $amm_id (i32.const 0) (i32.const 20) (i32.const 24) (i32.const 40) (i32.const 0) (i32.const 32))", 6, ), + HostFunctionSpec::CheckKeylet => ( + import::CHECK_ID, + "(call $check_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", + 5, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index d8000aa898..8ac05a152d 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -414,6 +414,23 @@ fn amm_id_reads_two_assets_and_writes_the_keylet() { assert_eq!(*host.amm_keylets_asked.borrow(), vec![(asset1, asset2)]); } +/// A keylet getter that reads an account region and also takes a scalar seq: both +/// reach the host keyed together, and the keylet lands where the guest asked. +#[test] +fn check_id_reads_the_account_and_seq_and_writes_the_keylet() { + // Guest memory is zeroed, so a 20-byte account read is all zeros. + let account = vec![0u8; 20]; + let host = + FakeHost::new().answering_check_keylet(account.clone(), 5, support::Answer::filler(32)); + + let wat = module( + &[import::CHECK_ID, ONE_PAGE], + "(call $check_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!(*host.check_keylets_asked.borrow(), vec![(account, 5)]); +} + /// 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 285c5d016b..c494de5630 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; 24] = [ +const ALL_IMPORTS: [&str; 25] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -120,6 +120,7 @@ const ALL_IMPORTS: [&str; 24] = [ import::CHECK_SIG, import::ACCOUNTROOT_ID, import::AMM_ID, + import::CHECK_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 43c0bff329..9ef3aade86 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -189,6 +189,11 @@ pub struct FakeHost { pub amm_keylets: HashMap<(Vec, Vec), Answer>, /// Every (asset1, asset2) pair `amm_keylet` was asked for. pub amm_keylets_asked: RefCell, Vec)>>, + /// What `check_keylet` answers, by (account bytes, seq). An unlisted key answers + /// `InvalidAccount`. + pub check_keylets: HashMap<(Vec, i32), Answer>, + /// Every (account, seq) `check_keylet` was asked for. + pub check_keylets_asked: RefCell, i32)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -247,6 +252,8 @@ impl Default for FakeHost { account_keylets_asked: RefCell::new(Vec::new()), amm_keylets: HashMap::new(), amm_keylets_asked: RefCell::new(Vec::new()), + check_keylets: HashMap::new(), + check_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -380,6 +387,16 @@ impl FakeHost { self } + pub fn answering_check_keylet( + mut self, + account: Vec, + seq: i32, + answer: Answer, + ) -> FakeHost { + self.check_keylets.insert((account, seq), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -566,6 +583,16 @@ impl HostFunctions for FakeHost { } } + fn check_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult { + self.check_keylets_asked + .borrow_mut() + .push((account.to_vec(), seq)); + match self.check_keylets.get(&(account.to_vec(), seq)) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -627,6 +654,7 @@ pub mod import { pub const CHECK_SIG: &str = r#"(import "host_lib" "check_sig" (func $check_sig (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const ACCOUNTROOT_ID: &str = r#"(import "host_lib" "accountroot_id" (func $accountroot_id (param i32 i32 i32 i32) (result i32)))"#; pub const AMM_ID: &str = r#"(import "host_lib" "amm_id" (func $amm_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const CHECK_ID: &str = r#"(import "host_lib" "check_id" (func $check_id (param i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 5af72a16bf..8e0b3c5a11 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -133,6 +133,14 @@ public: rust::Slice asset2, rust::Slice out) const noexcept; + // The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's + // u32 as its i32 bit pattern. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + checkKeylet( + rust::Slice account, + std::int32_t seq, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 1bc7005fa3..a739bb9e54 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -485,6 +485,26 @@ HostContext::ammKeylet( }); } +std::int32_t +HostContext::checkKeylet( + rust::Slice account, + std::int32_t seq, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + // The guest's u32 seq arrives as its i32 bit pattern; recover it. + auto const value = hostFunctions_.checkKeylet( + AccountID::fromVoid(account.data()), static_cast(seq)); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 9c423d274368dd5440629c6c36a07585ec8909bf Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 18:04:25 -0400 Subject: [PATCH 21/46] feat: Hook up credential_id host function --- crates/xrpl-host-functions/src/lib.rs | 13 +++++++ .../tests/generated_abi.rs | 28 +++++++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 23 ++++++++++++ crates/xrpl-wasm-vm/src/abi.rs | 9 +++++ crates/xrpl-wasm-vm/src/register.rs | 29 +++++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 +++ crates/xrpl-wasm-vm/tests/host_calls.rs | 31 ++++++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 35 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 9 +++++ src/libxrpl/tx/wasm/HostContext.cpp | 22 ++++++++++++ 11 files changed, 206 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 39a5f20662..f088d018d9 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -254,6 +254,19 @@ host_functions! { #[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. + #[gas = 350] + #[wasm_name = "credential_id"] + fn credential_keylet( + &self, + subject: &[u8], + issuer: &[u8], + credential_type: &[u8], + out: &mut [u8], + ) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index f1761235b3..8ab48d1525 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -198,6 +198,24 @@ impl HostFunctions for FakeHost { put(out, &[account[0]; HASH_LEN]) } + /// A keylet from subject, issuer, and credential type; `InvalidAccount` if either + /// account is empty, `InvalidParams` if the type is empty. + fn credential_keylet( + &self, + subject: &[u8], + issuer: &[u8], + credential_type: &[u8], + out: &mut [u8], + ) -> HostResult { + if subject.is_empty() || issuer.is_empty() { + return Err(HostError::InvalidAccount); + } + if credential_type.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[subject[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -302,6 +320,15 @@ fn the_trait_is_implementable() { host.check_keylet(&[], 5, &mut out), Err(HostError::InvalidAccount) ); + assert_eq!( + host.credential_keylet(&[7; 20], &[8; 20], b"cred", &mut out), + Ok(HASH_LEN) + ); + assert_eq!(out[0], 7); + assert_eq!( + host.credential_keylet(&[], &[8; 20], b"cred", &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -391,6 +418,7 @@ fn the_spec_table_matches_the_declarations() { ("accountroot_id", 350), ("amm_id", 450), ("check_id", 350), + ("credential_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 5db3eabaaf..210a192ed1 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -272,6 +272,16 @@ mod ffi { #[cxx_name = "checkKeylet"] fn check_keylet(self: &HostContext, account: &[u8], seq: i32, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "credentialKeylet"] + fn credential_keylet( + self: &HostContext, + subject: &[u8], + issuer: &[u8], + credential_type: &[u8], + out: &mut [u8], + ) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -431,6 +441,19 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.check_keylet(account, seq, out)) } + fn credential_keylet( + &self, + subject: &[u8], + issuer: &[u8], + credential_type: &[u8], + out: &mut [u8], + ) -> HostResult { + bytes_written( + self.ctx + .credential_keylet(subject, issuer, credential_type, out), + ) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 78d8bfa523..f8d7b535e1 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -274,6 +274,15 @@ mod tests { fn check_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn credential_keylet( + &self, + _subject: &[u8], + _issuer: &[u8], + _credential_type: &[u8], + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 82544efaab..8034d7bf1e 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -382,6 +382,35 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::CredentialKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + subj_ptr: i32, + subj_len: i32, + iss_ptr: i32, + iss_len: i32, + ct_ptr: i32, + ct_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::CredentialKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let subject = Region::new(subj_ptr, subj_len); + let issuer = Region::new(iss_ptr, iss_len); + let cred_type = Region::new(ct_ptr, ct_len); + write_buffered(c, out, |host, data, buf| { + host.credential_keylet( + subject.read(data)?, + issuer.read(data)?, + cred_type.read(data)?, + buf, + ) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 ab2a610ac9..97ab613b50 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -159,6 +159,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $check_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", 5, ), + HostFunctionSpec::CredentialKeylet => ( + import::CREDENTIAL_ID, + "(call $credential_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 4) (i32.const 44) (i32.const 20))", + 8, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 8ac05a152d..994aa87608 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -431,6 +431,37 @@ fn check_id_reads_the_account_and_seq_and_writes_the_keylet() { assert_eq!(*host.check_keylets_asked.borrow(), vec![(account, 5)]); } +/// A keylet getter that reads three input regions — two accounts and a credential +/// type: all three reach the host keyed together, and the keylet lands where asked. +#[test] +fn credential_id_reads_subject_issuer_and_type() { + // Guest memory is zeroed, so the two 20-byte accounts and the 4-byte type read + // as zeros of their declared lengths. + let subject = vec![0u8; 20]; + let issuer = vec![0u8; 20]; + let cred_type = vec![0u8; 4]; + let host = FakeHost::new().answering_credential_keylet( + subject.clone(), + issuer.clone(), + cred_type.clone(), + support::Answer::filler(32), + ); + + let wat = module( + &[import::CREDENTIAL_ID, ONE_PAGE], + "(call $credential_id + (i32.const 0) (i32.const 20) + (i32.const 20) (i32.const 20) + (i32.const 40) (i32.const 4) + (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!( + *host.credential_keylets_asked.borrow(), + vec![(subject, issuer, cred_type)] + ); +} + /// 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 c494de5630..d968769999 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; 25] = [ +const ALL_IMPORTS: [&str; 26] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -121,6 +121,7 @@ const ALL_IMPORTS: [&str; 25] = [ import::ACCOUNTROOT_ID, import::AMM_ID, import::CHECK_ID, + import::CREDENTIAL_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 9ef3aade86..300ddd3047 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -194,6 +194,11 @@ pub struct FakeHost { pub check_keylets: HashMap<(Vec, i32), Answer>, /// Every (account, seq) `check_keylet` was asked for. pub check_keylets_asked: RefCell, i32)>>, + /// What `credential_keylet` answers, by (subject, issuer, type) bytes. An unlisted + /// key answers `InvalidAccount`. + pub credential_keylets: HashMap<(Vec, Vec, Vec), Answer>, + /// Every (subject, issuer, type) `credential_keylet` was asked for. + pub credential_keylets_asked: RefCell, Vec, Vec)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -254,6 +259,8 @@ impl Default for FakeHost { amm_keylets_asked: RefCell::new(Vec::new()), check_keylets: HashMap::new(), check_keylets_asked: RefCell::new(Vec::new()), + credential_keylets: HashMap::new(), + credential_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -397,6 +404,18 @@ impl FakeHost { self } + pub fn answering_credential_keylet( + mut self, + subject: Vec, + issuer: Vec, + credential_type: Vec, + answer: Answer, + ) -> FakeHost { + self.credential_keylets + .insert((subject, issuer, credential_type), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -593,6 +612,21 @@ impl HostFunctions for FakeHost { } } + fn credential_keylet( + &self, + subject: &[u8], + issuer: &[u8], + credential_type: &[u8], + out: &mut [u8], + ) -> HostResult { + let key = (subject.to_vec(), issuer.to_vec(), credential_type.to_vec()); + self.credential_keylets_asked.borrow_mut().push(key.clone()); + match self.credential_keylets.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -655,6 +689,7 @@ pub mod import { pub const ACCOUNTROOT_ID: &str = r#"(import "host_lib" "accountroot_id" (func $accountroot_id (param i32 i32 i32 i32) (result i32)))"#; pub const AMM_ID: &str = r#"(import "host_lib" "amm_id" (func $amm_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const CHECK_ID: &str = r#"(import "host_lib" "check_id" (func $check_id (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const CREDENTIAL_ID: &str = r#"(import "host_lib" "credential_id" (func $credential_id (param i32 i32 i32 i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 8e0b3c5a11..f39302dd31 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -141,6 +141,15 @@ public: std::int32_t seq, rust::Slice out) const noexcept; + // Subject and issuer must each be 20 bytes, else `InvalidParams`. Writes the + // 32-byte keylet. + [[nodiscard]] std::int32_t + credentialKeylet( + rust::Slice subject, + rust::Slice issuer, + rust::Slice credentialType, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index a739bb9e54..5bd1e3fe3f 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -505,6 +505,28 @@ HostContext::checkKeylet( }); } +std::int32_t +HostContext::credentialKeylet( + rust::Slice subject, + rust::Slice issuer, + rust::Slice credentialType, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (subject.size() != AccountID::size() || issuer.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.credentialKeylet( + AccountID::fromVoid(subject.data()), + AccountID::fromVoid(issuer.data()), + Slice{credentialType.data(), credentialType.size()}); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 3e113db4f5973e25a1c8b6bf85da2f46a41e0a4d Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 18:07:49 -0400 Subject: [PATCH 22/46] feat: Hook up delegate_id host function --- crates/xrpl-host-functions/src/lib.rs | 11 +++++++ .../tests/generated_abi.rs | 27 ++++++++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 18 +++++++++++ crates/xrpl-wasm-vm/src/abi.rs | 8 +++++ crates/xrpl-wasm-vm/src/register.rs | 21 ++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 +++ crates/xrpl-wasm-vm/tests/host_calls.rs | 26 +++++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 32 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 7 ++++ src/libxrpl/tx/wasm/HostContext.cpp | 19 +++++++++++ 11 files changed, 176 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index f088d018d9..1cb3ca9cb4 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -267,6 +267,17 @@ 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. + #[gas = 350] + #[wasm_name = "delegate_id"] + fn delegate_keylet( + &self, + account: &[u8], + authorize: &[u8], + out: &mut [u8], + ) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 8ab48d1525..ee929b2cf1 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -216,6 +216,23 @@ impl HostFunctions for FakeHost { put(out, &[subject[0]; HASH_LEN]) } + /// A keylet from two accounts; `InvalidAccount` if either is empty, `InvalidParams` + /// if they are equal. + fn delegate_keylet( + &self, + account: &[u8], + authorize: &[u8], + out: &mut [u8], + ) -> HostResult { + if account.is_empty() || authorize.is_empty() { + return Err(HostError::InvalidAccount); + } + if account == authorize { + return Err(HostError::InvalidParams); + } + put(out, &[account[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -329,6 +346,15 @@ fn the_trait_is_implementable() { host.credential_keylet(&[], &[8; 20], b"cred", &mut out), Err(HostError::InvalidAccount) ); + assert_eq!( + host.delegate_keylet(&[7; 20], &[8; 20], &mut out), + Ok(HASH_LEN) + ); + assert_eq!(out[0], 7); + assert_eq!( + host.delegate_keylet(&[], &[8; 20], &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -419,6 +445,7 @@ fn the_spec_table_matches_the_declarations() { ("amm_id", 450), ("check_id", 350), ("credential_id", 350), + ("delegate_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 210a192ed1..82db4c7c8f 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -282,6 +282,15 @@ mod ffi { out: &mut [u8], ) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "delegateKeylet"] + fn delegate_keylet( + self: &HostContext, + account: &[u8], + authorize: &[u8], + out: &mut [u8], + ) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -454,6 +463,15 @@ impl HostFunctions for CxxHost<'_> { ) } + fn delegate_keylet( + &self, + account: &[u8], + authorize: &[u8], + out: &mut [u8], + ) -> HostResult { + bytes_written(self.ctx.delegate_keylet(account, authorize, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index f8d7b535e1..3072b32028 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -283,6 +283,14 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn delegate_keylet( + &self, + _account: &[u8], + _authorize: &[u8], + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 8034d7bf1e..e1cb00795d 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -411,6 +411,27 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::DelegateKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + auth_ptr: i32, + auth_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::DelegateKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + let authorize = Region::new(auth_ptr, auth_len); + write_buffered(c, out, |host, data, buf| { + host.delegate_keylet(account.read(data)?, authorize.read(data)?, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 97ab613b50..5cdba9a128 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -164,6 +164,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $credential_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 4) (i32.const 44) (i32.const 20))", 8, ), + HostFunctionSpec::DelegateKeylet => ( + import::DELEGATE_ID, + "(call $delegate_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 32))", + 6, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 994aa87608..efa5fd8b82 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -462,6 +462,32 @@ fn credential_id_reads_subject_issuer_and_type() { ); } +/// A two-account keylet getter: both accounts reach the host as a pair, and the +/// keylet lands where the guest asked. +#[test] +fn delegate_id_reads_both_accounts_and_writes_the_keylet() { + let account = vec![0u8; 20]; + let authorize = vec![0u8; 20]; + let host = FakeHost::new().answering_delegate_keylet( + account.clone(), + authorize.clone(), + support::Answer::filler(32), + ); + + let wat = module( + &[import::DELEGATE_ID, ONE_PAGE], + "(call $delegate_id + (i32.const 0) (i32.const 20) + (i32.const 20) (i32.const 20) + (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!( + *host.delegate_keylets_asked.borrow(), + vec![(account, authorize)] + ); +} + /// 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 d968769999..f5c877f318 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; 26] = [ +const ALL_IMPORTS: [&str; 27] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -122,6 +122,7 @@ const ALL_IMPORTS: [&str; 26] = [ import::AMM_ID, import::CHECK_ID, import::CREDENTIAL_ID, + import::DELEGATE_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 300ddd3047..707aaf0f5f 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -199,6 +199,11 @@ pub struct FakeHost { pub credential_keylets: HashMap<(Vec, Vec, Vec), Answer>, /// Every (subject, issuer, type) `credential_keylet` was asked for. pub credential_keylets_asked: RefCell, Vec, Vec)>>, + /// What `delegate_keylet` answers, by (account, authorize) bytes. An unlisted key + /// answers `InvalidAccount`. + pub delegate_keylets: HashMap<(Vec, Vec), Answer>, + /// Every (account, authorize) `delegate_keylet` was asked for. + pub delegate_keylets_asked: RefCell, Vec)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -261,6 +266,8 @@ impl Default for FakeHost { check_keylets_asked: RefCell::new(Vec::new()), credential_keylets: HashMap::new(), credential_keylets_asked: RefCell::new(Vec::new()), + delegate_keylets: HashMap::new(), + delegate_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -416,6 +423,16 @@ impl FakeHost { self } + pub fn answering_delegate_keylet( + mut self, + account: Vec, + authorize: Vec, + answer: Answer, + ) -> FakeHost { + self.delegate_keylets.insert((account, authorize), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -627,6 +644,20 @@ impl HostFunctions for FakeHost { } } + fn delegate_keylet( + &self, + account: &[u8], + authorize: &[u8], + out: &mut [u8], + ) -> HostResult { + let key = (account.to_vec(), authorize.to_vec()); + self.delegate_keylets_asked.borrow_mut().push(key.clone()); + match self.delegate_keylets.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -690,6 +721,7 @@ pub mod import { pub const AMM_ID: &str = r#"(import "host_lib" "amm_id" (func $amm_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const CHECK_ID: &str = r#"(import "host_lib" "check_id" (func $check_id (param i32 i32 i32 i32 i32) (result i32)))"#; pub const CREDENTIAL_ID: &str = r#"(import "host_lib" "credential_id" (func $credential_id (param i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const DELEGATE_ID: &str = r#"(import "host_lib" "delegate_id" (func $delegate_id (param i32 i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index f39302dd31..684d249fd0 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -150,6 +150,13 @@ public: rust::Slice credentialType, rust::Slice out) const noexcept; + // Both accounts must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + delegateKeylet( + rust::Slice account, + rust::Slice authorize, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 5bd1e3fe3f..277ce42e74 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -527,6 +527,25 @@ HostContext::credentialKeylet( }); } +std::int32_t +HostContext::delegateKeylet( + rust::Slice account, + rust::Slice authorize, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size() || authorize.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.delegateKeylet( + AccountID::fromVoid(account.data()), AccountID::fromVoid(authorize.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From a321a5dbfbbc2d5b329ffeda0b9336b19c7f137b Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 20:45:52 -0400 Subject: [PATCH 23/46] feat: Hook up deposit_preauth_id host function --- crates/xrpl-host-functions/src/lib.rs | 12 +++++++ .../tests/generated_abi.rs | 26 ++++++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 18 ++++++++++ crates/xrpl-wasm-vm/src/abi.rs | 8 +++++ crates/xrpl-wasm-vm/src/register.rs | 25 +++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 +++ crates/xrpl-wasm-vm/tests/host_calls.rs | 25 +++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 35 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 7 ++++ src/libxrpl/tx/wasm/HostContext.cpp | 19 ++++++++++ 11 files changed, 182 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 1cb3ca9cb4..c263b243dc 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -278,6 +278,18 @@ host_functions! { out: &mut [u8], ) -> 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. + #[gas = 350] + #[wasm_name = "deposit_preauth_id"] + fn deposit_preauth_keylet( + &self, + account: &[u8], + authorize: &[u8], + out: &mut [u8], + ) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index ee929b2cf1..796856a729 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -233,6 +233,22 @@ impl HostFunctions for FakeHost { put(out, &[account[0]; HASH_LEN]) } + /// The same two-account shape, for a `DepositPreauth`. + fn deposit_preauth_keylet( + &self, + account: &[u8], + authorize: &[u8], + out: &mut [u8], + ) -> HostResult { + if account.is_empty() || authorize.is_empty() { + return Err(HostError::InvalidAccount); + } + if account == authorize { + return Err(HostError::InvalidParams); + } + put(out, &[authorize[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -355,6 +371,15 @@ fn the_trait_is_implementable() { host.delegate_keylet(&[], &[8; 20], &mut out), Err(HostError::InvalidAccount) ); + assert_eq!( + host.deposit_preauth_keylet(&[7; 20], &[8; 20], &mut out), + Ok(HASH_LEN) + ); + assert_eq!(out[0], 8); + assert_eq!( + host.deposit_preauth_keylet(&[7; 20], &[7; 20], &mut out), + Err(HostError::InvalidParams) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -446,6 +471,7 @@ fn the_spec_table_matches_the_declarations() { ("check_id", 350), ("credential_id", 350), ("delegate_id", 350), + ("deposit_preauth_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 82db4c7c8f..f1fe6a41cc 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -291,6 +291,15 @@ mod ffi { out: &mut [u8], ) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "depositPreauthKeylet"] + fn deposit_preauth_keylet( + self: &HostContext, + account: &[u8], + authorize: &[u8], + out: &mut [u8], + ) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -472,6 +481,15 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.delegate_keylet(account, authorize, out)) } + fn deposit_preauth_keylet( + &self, + account: &[u8], + authorize: &[u8], + out: &mut [u8], + ) -> HostResult { + bytes_written(self.ctx.deposit_preauth_keylet(account, authorize, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 3072b32028..44f1269f55 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -291,6 +291,14 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn deposit_preauth_keylet( + &self, + _account: &[u8], + _authorize: &[u8], + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 e1cb00795d..37721058b7 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -432,6 +432,31 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::DepositPreauthKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + auth_ptr: i32, + auth_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::DepositPreauthKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + let authorize = Region::new(auth_ptr, auth_len); + write_buffered(c, out, |host, data, buf| { + host.deposit_preauth_keylet( + account.read(data)?, + authorize.read(data)?, + buf, + ) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 5cdba9a128..c9ef1c97e9 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -169,6 +169,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $delegate_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 32))", 6, ), + HostFunctionSpec::DepositPreauthKeylet => ( + import::DEPOSIT_PREAUTH_ID, + "(call $deposit_preauth_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 32))", + 6, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index efa5fd8b82..f180aefedf 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -488,6 +488,31 @@ fn delegate_id_reads_both_accounts_and_writes_the_keylet() { ); } +/// The same two-account keylet shape as delegate, with its own answer set. +#[test] +fn deposit_preauth_id_reads_both_accounts_and_writes_the_keylet() { + let account = vec![0u8; 20]; + let authorize = vec![0u8; 20]; + let host = FakeHost::new().answering_deposit_preauth_keylet( + account.clone(), + authorize.clone(), + support::Answer::filler(32), + ); + + let wat = module( + &[import::DEPOSIT_PREAUTH_ID, ONE_PAGE], + "(call $deposit_preauth_id + (i32.const 0) (i32.const 20) + (i32.const 20) (i32.const 20) + (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!( + *host.deposit_preauth_keylets_asked.borrow(), + vec![(account, authorize)] + ); +} + /// 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 f5c877f318..9edd436d58 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; 27] = [ +const ALL_IMPORTS: [&str; 28] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -123,6 +123,7 @@ const ALL_IMPORTS: [&str; 27] = [ import::CHECK_ID, import::CREDENTIAL_ID, import::DELEGATE_ID, + import::DEPOSIT_PREAUTH_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 707aaf0f5f..38d7931d36 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -204,6 +204,11 @@ pub struct FakeHost { pub delegate_keylets: HashMap<(Vec, Vec), Answer>, /// Every (account, authorize) `delegate_keylet` was asked for. pub delegate_keylets_asked: RefCell, Vec)>>, + /// What `deposit_preauth_keylet` answers, by (account, authorize) bytes. An + /// unlisted key answers `InvalidAccount`. + pub deposit_preauth_keylets: HashMap<(Vec, Vec), Answer>, + /// Every (account, authorize) `deposit_preauth_keylet` was asked for. + pub deposit_preauth_keylets_asked: RefCell, Vec)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -268,6 +273,8 @@ impl Default for FakeHost { credential_keylets_asked: RefCell::new(Vec::new()), delegate_keylets: HashMap::new(), delegate_keylets_asked: RefCell::new(Vec::new()), + deposit_preauth_keylets: HashMap::new(), + deposit_preauth_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -433,6 +440,17 @@ impl FakeHost { self } + pub fn answering_deposit_preauth_keylet( + mut self, + account: Vec, + authorize: Vec, + answer: Answer, + ) -> FakeHost { + self.deposit_preauth_keylets + .insert((account, authorize), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -658,6 +676,22 @@ impl HostFunctions for FakeHost { } } + fn deposit_preauth_keylet( + &self, + account: &[u8], + authorize: &[u8], + out: &mut [u8], + ) -> HostResult { + let key = (account.to_vec(), authorize.to_vec()); + self.deposit_preauth_keylets_asked + .borrow_mut() + .push(key.clone()); + match self.deposit_preauth_keylets.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -722,6 +756,7 @@ pub mod import { pub const CHECK_ID: &str = r#"(import "host_lib" "check_id" (func $check_id (param i32 i32 i32 i32 i32) (result i32)))"#; pub const CREDENTIAL_ID: &str = r#"(import "host_lib" "credential_id" (func $credential_id (param i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const DELEGATE_ID: &str = r#"(import "host_lib" "delegate_id" (func $delegate_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const DEPOSIT_PREAUTH_ID: &str = r#"(import "host_lib" "deposit_preauth_id" (func $deposit_preauth_id (param i32 i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 684d249fd0..d7f0d8685f 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -157,6 +157,13 @@ public: rust::Slice authorize, rust::Slice out) const noexcept; + // Both accounts must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + depositPreauthKeylet( + rust::Slice account, + rust::Slice authorize, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 277ce42e74..f340260982 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -546,6 +546,25 @@ HostContext::delegateKeylet( }); } +std::int32_t +HostContext::depositPreauthKeylet( + rust::Slice account, + rust::Slice authorize, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size() || authorize.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.depositPreauthKeylet( + AccountID::fromVoid(account.data()), AccountID::fromVoid(authorize.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From c5605b6bcdc1e9ced1ad0a6d8df38a89e22bf4ca Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 20:50:40 -0400 Subject: [PATCH 24/46] feat: Hook up did_id host function --- crates/xrpl-host-functions/src/lib.rs | 6 +++++ .../tests/generated_abi.rs | 15 +++++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 +++++++ crates/xrpl-wasm-vm/src/abi.rs | 3 +++ crates/xrpl-wasm-vm/src/register.rs | 18 +++++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 +++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 14 ++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 22 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 5 +++++ src/libxrpl/tx/wasm/HostContext.cpp | 16 ++++++++++++++ 11 files changed, 114 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index c263b243dc..b5fc0e7a00 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -290,6 +290,12 @@ host_functions! { out: &mut [u8], ) -> 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 XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 796856a729..06bd8bccdc 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -249,6 +249,14 @@ impl HostFunctions for FakeHost { put(out, &[authorize[0]; HASH_LEN]) } + /// A single-account keylet, for a `DID`. + fn did_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult { + if account.is_empty() { + return Err(HostError::InvalidAccount); + } + put(out, &[account[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -380,6 +388,12 @@ fn the_trait_is_implementable() { host.deposit_preauth_keylet(&[7; 20], &[7; 20], &mut out), Err(HostError::InvalidParams) ); + assert_eq!(host.did_keylet(&[7; 20], &mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 7); + assert_eq!( + host.did_keylet(&[], &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -472,6 +486,7 @@ fn the_spec_table_matches_the_declarations() { ("credential_id", 350), ("delegate_id", 350), ("deposit_preauth_id", 350), + ("did_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index f1fe6a41cc..34b4ad199e 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -300,6 +300,10 @@ mod ffi { out: &mut [u8], ) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "didKeylet"] + fn did_keylet(self: &HostContext, account: &[u8], out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -490,6 +494,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.deposit_preauth_keylet(account, authorize, out)) } + fn did_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.did_keylet(account, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 44f1269f55..b0336de410 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -299,6 +299,9 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn did_keylet(&self, _account: &[u8], _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 37721058b7..480913a75e 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -457,6 +457,24 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::DidKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::DidKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + write_buffered(c, out, |host, data, buf| { + host.did_keylet(account.read(data)?, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 c9ef1c97e9..f3d62bbb4a 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -174,6 +174,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $deposit_preauth_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 32))", 6, ), + HostFunctionSpec::DidKeylet => ( + import::DID_ID, + "(call $did_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 32))", + 4, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index f180aefedf..a462f8feaf 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -513,6 +513,20 @@ fn deposit_preauth_id_reads_both_accounts_and_writes_the_keylet() { ); } +/// A single-account keylet getter (like accountroot), with its own answer set. +#[test] +fn did_id_reads_the_account_and_writes_the_keylet() { + let account = vec![0u8; 20]; + let host = FakeHost::new().answering_did_keylet(account.clone(), support::Answer::filler(32)); + + let wat = module( + &[import::DID_ID, ONE_PAGE], + "(call $did_id (i32.const 0) (i32.const 20) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!(*host.did_keylets_asked.borrow(), vec![account]); +} + /// 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 9edd436d58..dea1571b2c 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; 28] = [ +const ALL_IMPORTS: [&str; 29] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -124,6 +124,7 @@ const ALL_IMPORTS: [&str; 28] = [ import::CREDENTIAL_ID, import::DELEGATE_ID, import::DEPOSIT_PREAUTH_ID, + import::DID_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 38d7931d36..6eb69db451 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -209,6 +209,11 @@ pub struct FakeHost { pub deposit_preauth_keylets: HashMap<(Vec, Vec), Answer>, /// Every (account, authorize) `deposit_preauth_keylet` was asked for. pub deposit_preauth_keylets_asked: RefCell, Vec)>>, + /// What `did_keylet` answers, by account bytes. An unlisted account answers + /// `InvalidAccount`. + pub did_keylets: HashMap, Answer>, + /// Every account `did_keylet` was asked for. + pub did_keylets_asked: RefCell>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -275,6 +280,8 @@ impl Default for FakeHost { delegate_keylets_asked: RefCell::new(Vec::new()), deposit_preauth_keylets: HashMap::new(), deposit_preauth_keylets_asked: RefCell::new(Vec::new()), + did_keylets: HashMap::new(), + did_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -451,6 +458,11 @@ impl FakeHost { self } + pub fn answering_did_keylet(mut self, account: Vec, answer: Answer) -> FakeHost { + self.did_keylets.insert(account, answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -692,6 +704,14 @@ impl HostFunctions for FakeHost { } } + fn did_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult { + self.did_keylets_asked.borrow_mut().push(account.to_vec()); + match self.did_keylets.get(account) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -757,6 +777,8 @@ pub mod import { pub const CREDENTIAL_ID: &str = r#"(import "host_lib" "credential_id" (func $credential_id (param i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const DELEGATE_ID: &str = r#"(import "host_lib" "delegate_id" (func $delegate_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const DEPOSIT_PREAUTH_ID: &str = r#"(import "host_lib" "deposit_preauth_id" (func $deposit_preauth_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const DID_ID: &str = + r#"(import "host_lib" "did_id" (func $did_id (param i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index d7f0d8685f..41f424dd28 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -164,6 +164,11 @@ public: rust::Slice authorize, rust::Slice out) const noexcept; + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + didKeylet(rust::Slice account, rust::Slice out) + const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index f340260982..1290462771 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -565,6 +565,22 @@ HostContext::depositPreauthKeylet( }); } +std::int32_t +HostContext::didKeylet(rust::Slice account, rust::Slice out) + const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.didKeylet(AccountID::fromVoid(account.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 78c8128c98501502abfb3f58477f65a5e0c9b437 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 20:56:18 -0400 Subject: [PATCH 25/46] feat: Hook up escrow_id host function --- crates/xrpl-host-functions/src/lib.rs | 7 +++++ .../tests/generated_abi.rs | 15 +++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 ++++++ crates/xrpl-wasm-vm/src/abi.rs | 3 +++ crates/xrpl-wasm-vm/src/register.rs | 19 +++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 15 +++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 27 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 8 ++++++ src/libxrpl/tx/wasm/HostContext.cpp | 20 ++++++++++++++ 11 files changed, 129 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index b5fc0e7a00..8f3874e6f2 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -296,6 +296,13 @@ host_functions! { #[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. + #[gas = 350] + #[wasm_name = "escrow_id"] + fn escrow_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 06bd8bccdc..074392872c 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -257,6 +257,14 @@ impl HostFunctions for FakeHost { put(out, &[account[0]; HASH_LEN]) } + /// The account-and-sequence shape, for an `Escrow`. + fn escrow_keylet(&self, account: &[u8], _seq: i32, out: &mut [u8]) -> HostResult { + if account.is_empty() { + return Err(HostError::InvalidAccount); + } + put(out, &[account[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -394,6 +402,12 @@ fn the_trait_is_implementable() { host.did_keylet(&[], &mut out), Err(HostError::InvalidAccount) ); + assert_eq!(host.escrow_keylet(&[7; 20], 5, &mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 7); + assert_eq!( + host.escrow_keylet(&[], 5, &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -487,6 +501,7 @@ fn the_spec_table_matches_the_declarations() { ("delegate_id", 350), ("deposit_preauth_id", 350), ("did_id", 350), + ("escrow_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 34b4ad199e..997e81890a 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -304,6 +304,10 @@ mod ffi { #[cxx_name = "didKeylet"] fn did_keylet(self: &HostContext, account: &[u8], out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "escrowKeylet"] + fn escrow_keylet(self: &HostContext, account: &[u8], seq: i32, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -498,6 +502,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.did_keylet(account, out)) } + fn escrow_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.escrow_keylet(account, seq, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index b0336de410..935786061a 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -302,6 +302,9 @@ mod tests { fn did_keylet(&self, _account: &[u8], _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn escrow_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 480913a75e..52ade0c04d 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -475,6 +475,25 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::EscrowKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + seq: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::EscrowKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + write_buffered(c, out, |host, data, buf| { + host.escrow_keylet(account.read(data)?, seq, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 f3d62bbb4a..036dc14980 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -179,6 +179,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $did_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 32))", 4, ), + HostFunctionSpec::EscrowKeylet => ( + import::ESCROW_ID, + "(call $escrow_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", + 5, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index a462f8feaf..0feab266b3 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -527,6 +527,21 @@ fn did_id_reads_the_account_and_writes_the_keylet() { assert_eq!(*host.did_keylets_asked.borrow(), vec![account]); } +/// The account-and-sequence keylet shape (like check), with its own answer set. +#[test] +fn escrow_id_reads_the_account_and_seq_and_writes_the_keylet() { + let account = vec![0u8; 20]; + let host = + FakeHost::new().answering_escrow_keylet(account.clone(), 5, support::Answer::filler(32)); + + let wat = module( + &[import::ESCROW_ID, ONE_PAGE], + "(call $escrow_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!(*host.escrow_keylets_asked.borrow(), vec![(account, 5)]); +} + /// 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 dea1571b2c..7113f09ed1 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; 29] = [ +const ALL_IMPORTS: [&str; 30] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -125,6 +125,7 @@ const ALL_IMPORTS: [&str; 29] = [ import::DELEGATE_ID, import::DEPOSIT_PREAUTH_ID, import::DID_ID, + import::ESCROW_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 6eb69db451..e44c27334f 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -214,6 +214,11 @@ pub struct FakeHost { pub did_keylets: HashMap, Answer>, /// Every account `did_keylet` was asked for. pub did_keylets_asked: RefCell>>, + /// What `escrow_keylet` answers, by (account bytes, seq). An unlisted key answers + /// `InvalidAccount`. + pub escrow_keylets: HashMap<(Vec, i32), Answer>, + /// Every (account, seq) `escrow_keylet` was asked for. + pub escrow_keylets_asked: RefCell, i32)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -282,6 +287,8 @@ impl Default for FakeHost { deposit_preauth_keylets_asked: RefCell::new(Vec::new()), did_keylets: HashMap::new(), did_keylets_asked: RefCell::new(Vec::new()), + escrow_keylets: HashMap::new(), + escrow_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -463,6 +470,16 @@ impl FakeHost { self } + pub fn answering_escrow_keylet( + mut self, + account: Vec, + seq: i32, + answer: Answer, + ) -> FakeHost { + self.escrow_keylets.insert((account, seq), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -712,6 +729,15 @@ impl HostFunctions for FakeHost { } } + fn escrow_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult { + let key = (account.to_vec(), seq); + self.escrow_keylets_asked.borrow_mut().push(key.clone()); + match self.escrow_keylets.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -779,6 +805,7 @@ pub mod import { pub const DEPOSIT_PREAUTH_ID: &str = r#"(import "host_lib" "deposit_preauth_id" (func $deposit_preauth_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const DID_ID: &str = r#"(import "host_lib" "did_id" (func $did_id (param i32 i32 i32 i32) (result i32)))"#; + pub const ESCROW_ID: &str = r#"(import "host_lib" "escrow_id" (func $escrow_id (param i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 41f424dd28..f74e91ac66 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -169,6 +169,14 @@ public: didKeylet(rust::Slice account, rust::Slice out) const noexcept; + // The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's + // u32 as its i32 bit pattern. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + escrowKeylet( + rust::Slice account, + std::int32_t seq, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 1290462771..9f85d06a3b 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -581,6 +581,26 @@ HostContext::didKeylet(rust::Slice account, rust::Slice account, + std::int32_t seq, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + // The guest's u32 seq arrives as its i32 bit pattern; recover it. + auto const value = hostFunctions_.escrowKeylet( + AccountID::fromVoid(account.data()), static_cast(seq)); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 50623665c73acc513693da3677988e9bb0631345 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 21:00:11 -0400 Subject: [PATCH 26/46] feat: Hook up trustline_id host function --- crates/xrpl-host-functions/src/lib.rs | 13 +++++++ .../tests/generated_abi.rs | 28 +++++++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 23 ++++++++++++ crates/xrpl-wasm-vm/src/abi.rs | 9 +++++ crates/xrpl-wasm-vm/src/register.rs | 29 +++++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 +++ crates/xrpl-wasm-vm/tests/host_calls.rs | 29 +++++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 35 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 9 +++++ src/libxrpl/tx/wasm/HostContext.cpp | 23 ++++++++++++ 11 files changed, 205 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 8f3874e6f2..b356cf50ee 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -303,6 +303,19 @@ host_functions! { #[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. + #[gas = 400] + #[wasm_name = "trustline_id"] + fn trust_line_keylet( + &self, + account1: &[u8], + account2: &[u8], + currency: &[u8], + out: &mut [u8], + ) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 074392872c..8b7d17f4fa 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -265,6 +265,24 @@ impl HostFunctions for FakeHost { put(out, &[account[0]; HASH_LEN]) } + /// A keylet from two accounts and a currency; `InvalidAccount` if either account + /// is empty, `InvalidParams` if they are equal or the currency is empty. + fn trust_line_keylet( + &self, + account1: &[u8], + account2: &[u8], + currency: &[u8], + out: &mut [u8], + ) -> HostResult { + if account1.is_empty() || account2.is_empty() { + return Err(HostError::InvalidAccount); + } + if account1 == account2 || currency.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[account1[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -408,6 +426,15 @@ fn the_trait_is_implementable() { host.escrow_keylet(&[], 5, &mut out), Err(HostError::InvalidAccount) ); + assert_eq!( + host.trust_line_keylet(&[7; 20], &[8; 20], &[1; 20], &mut out), + Ok(HASH_LEN) + ); + assert_eq!(out[0], 7); + assert_eq!( + host.trust_line_keylet(&[7; 20], &[7; 20], &[1; 20], &mut out), + Err(HostError::InvalidParams) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -502,6 +529,7 @@ fn the_spec_table_matches_the_declarations() { ("deposit_preauth_id", 350), ("did_id", 350), ("escrow_id", 350), + ("trustline_id", 400), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 997e81890a..e7d95ad035 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -308,6 +308,16 @@ mod ffi { #[cxx_name = "escrowKeylet"] fn escrow_keylet(self: &HostContext, account: &[u8], seq: i32, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "trustLineKeylet"] + fn trust_line_keylet( + self: &HostContext, + account1: &[u8], + account2: &[u8], + currency: &[u8], + out: &mut [u8], + ) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -506,6 +516,19 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.escrow_keylet(account, seq, out)) } + fn trust_line_keylet( + &self, + account1: &[u8], + account2: &[u8], + currency: &[u8], + out: &mut [u8], + ) -> HostResult { + bytes_written( + self.ctx + .trust_line_keylet(account1, account2, currency, out), + ) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 935786061a..cf943942b1 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -305,6 +305,15 @@ mod tests { fn escrow_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn trust_line_keylet( + &self, + _account1: &[u8], + _account2: &[u8], + _currency: &[u8], + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 52ade0c04d..1bf9f738d1 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -494,6 +494,35 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::TrustLineKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + a1_ptr: i32, + a1_len: i32, + a2_ptr: i32, + a2_len: i32, + cur_ptr: i32, + cur_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::TrustLineKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let account1 = Region::new(a1_ptr, a1_len); + let account2 = Region::new(a2_ptr, a2_len); + let currency = Region::new(cur_ptr, cur_len); + write_buffered(c, out, |host, data, buf| { + host.trust_line_keylet( + account1.read(data)?, + account2.read(data)?, + currency.read(data)?, + buf, + ) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 036dc14980..cd1c00bb1e 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -184,6 +184,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $escrow_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", 5, ), + HostFunctionSpec::TrustLineKeylet => ( + import::TRUSTLINE_ID, + "(call $trustline_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 20) (i32.const 60) (i32.const 32))", + 8, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 0feab266b3..027ef8c8eb 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -542,6 +542,35 @@ fn escrow_id_reads_the_account_and_seq_and_writes_the_keylet() { assert_eq!(*host.escrow_keylets_asked.borrow(), vec![(account, 5)]); } +/// A keylet getter reading three regions — two accounts and a currency: all three +/// reach the host as a triple, and the keylet lands where the guest asked. +#[test] +fn trustline_id_reads_two_accounts_and_a_currency() { + let account1 = vec![0u8; 20]; + let account2 = vec![0u8; 20]; + let currency = vec![0u8; 20]; + let host = FakeHost::new().answering_trust_line_keylet( + account1.clone(), + account2.clone(), + currency.clone(), + support::Answer::filler(32), + ); + + let wat = module( + &[import::TRUSTLINE_ID, ONE_PAGE], + "(call $trustline_id + (i32.const 0) (i32.const 20) + (i32.const 20) (i32.const 20) + (i32.const 40) (i32.const 20) + (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!( + *host.trust_line_keylets_asked.borrow(), + vec![(account1, account2, currency)] + ); +} + /// 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 7113f09ed1..7a9b09f4f1 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; 30] = [ +const ALL_IMPORTS: [&str; 31] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -126,6 +126,7 @@ const ALL_IMPORTS: [&str; 30] = [ import::DEPOSIT_PREAUTH_ID, import::DID_ID, import::ESCROW_ID, + import::TRUSTLINE_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index e44c27334f..15a1a1acdf 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -219,6 +219,11 @@ pub struct FakeHost { pub escrow_keylets: HashMap<(Vec, i32), Answer>, /// Every (account, seq) `escrow_keylet` was asked for. pub escrow_keylets_asked: RefCell, i32)>>, + /// What `trust_line_keylet` answers, by (account1, account2, currency) bytes. An + /// unlisted key answers `InvalidAccount`. + pub trust_line_keylets: HashMap<(Vec, Vec, Vec), Answer>, + /// Every (account1, account2, currency) `trust_line_keylet` was asked for. + pub trust_line_keylets_asked: RefCell, Vec, Vec)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -289,6 +294,8 @@ impl Default for FakeHost { did_keylets_asked: RefCell::new(Vec::new()), escrow_keylets: HashMap::new(), escrow_keylets_asked: RefCell::new(Vec::new()), + trust_line_keylets: HashMap::new(), + trust_line_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -480,6 +487,18 @@ impl FakeHost { self } + pub fn answering_trust_line_keylet( + mut self, + account1: Vec, + account2: Vec, + currency: Vec, + answer: Answer, + ) -> FakeHost { + self.trust_line_keylets + .insert((account1, account2, currency), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -738,6 +757,21 @@ impl HostFunctions for FakeHost { } } + fn trust_line_keylet( + &self, + account1: &[u8], + account2: &[u8], + currency: &[u8], + out: &mut [u8], + ) -> HostResult { + let key = (account1.to_vec(), account2.to_vec(), currency.to_vec()); + self.trust_line_keylets_asked.borrow_mut().push(key.clone()); + match self.trust_line_keylets.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -806,6 +840,7 @@ pub mod import { pub const DID_ID: &str = r#"(import "host_lib" "did_id" (func $did_id (param i32 i32 i32 i32) (result i32)))"#; pub const ESCROW_ID: &str = r#"(import "host_lib" "escrow_id" (func $escrow_id (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const TRUSTLINE_ID: &str = r#"(import "host_lib" "trustline_id" (func $trustline_id (param i32 i32 i32 i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index f74e91ac66..324f849f1c 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -177,6 +177,15 @@ public: std::int32_t seq, rust::Slice out) const noexcept; + // Both accounts and the currency must each be 20 bytes, else `InvalidParams`. + // Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + trustLineKeylet( + rust::Slice account1, + rust::Slice account2, + rust::Slice currency, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 9f85d06a3b..fea825b173 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -601,6 +601,29 @@ HostContext::escrowKeylet( }); } +std::int32_t +HostContext::trustLineKeylet( + rust::Slice account1, + rust::Slice account2, + rust::Slice currency, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account1.size() != AccountID::size() || account2.size() != AccountID::size() || + currency.size() != Currency::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.trustLineKeylet( + AccountID::fromVoid(account1.data()), + AccountID::fromVoid(account2.data()), + Currency::fromVoid(currency.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From c1008c473c6db4b75ab671bb012bd6ab8832f218 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 21:04:38 -0400 Subject: [PATCH 27/46] feat: Hook up mpt_issuance_id host function --- crates/xrpl-host-functions/src/lib.rs | 12 +++++++ .../tests/generated_abi.rs | 23 +++++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 18 ++++++++++ crates/xrpl-wasm-vm/src/abi.rs | 8 +++++ crates/xrpl-wasm-vm/src/register.rs | 19 +++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 +++ crates/xrpl-wasm-vm/tests/host_calls.rs | 18 ++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 34 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 8 +++++ src/libxrpl/tx/wasm/HostContext.cpp | 20 +++++++++++ 11 files changed, 167 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index b356cf50ee..8458fa4a5d 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -316,6 +316,18 @@ host_functions! { out: &mut [u8], ) -> 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. + #[gas = 350] + #[wasm_name = "mpt_issuance_id"] + fn mptoken_issuance_keylet( + &self, + issuer: &[u8], + seq: i32, + out: &mut [u8], + ) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 8b7d17f4fa..0b04c597cd 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -283,6 +283,19 @@ impl HostFunctions for FakeHost { put(out, &[account1[0]; HASH_LEN]) } + /// The issuer-and-sequence shape, for an `MPTokenIssuance`. + fn mptoken_issuance_keylet( + &self, + issuer: &[u8], + _seq: i32, + out: &mut [u8], + ) -> HostResult { + if issuer.is_empty() { + return Err(HostError::InvalidAccount); + } + put(out, &[issuer[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -435,6 +448,15 @@ fn the_trait_is_implementable() { host.trust_line_keylet(&[7; 20], &[7; 20], &[1; 20], &mut out), Err(HostError::InvalidParams) ); + assert_eq!( + host.mptoken_issuance_keylet(&[7; 20], 5, &mut out), + Ok(HASH_LEN) + ); + assert_eq!(out[0], 7); + assert_eq!( + host.mptoken_issuance_keylet(&[], 5, &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -530,6 +552,7 @@ fn the_spec_table_matches_the_declarations() { ("did_id", 350), ("escrow_id", 350), ("trustline_id", 400), + ("mpt_issuance_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index e7d95ad035..8210042e49 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -318,6 +318,15 @@ mod ffi { out: &mut [u8], ) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "mptokenIssuanceKeylet"] + fn mptoken_issuance_keylet( + self: &HostContext, + issuer: &[u8], + seq: i32, + out: &mut [u8], + ) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -529,6 +538,15 @@ impl HostFunctions for CxxHost<'_> { ) } + fn mptoken_issuance_keylet( + &self, + issuer: &[u8], + seq: i32, + out: &mut [u8], + ) -> HostResult { + bytes_written(self.ctx.mptoken_issuance_keylet(issuer, seq, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index cf943942b1..614d0216cd 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -314,6 +314,14 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn mptoken_issuance_keylet( + &self, + _issuer: &[u8], + _seq: i32, + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 1bf9f738d1..fcea025a70 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -523,6 +523,25 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::MptokenIssuanceKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + seq: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::MptokenIssuanceKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let issuer = Region::new(acc_ptr, acc_len); + write_buffered(c, out, |host, data, buf| { + host.mptoken_issuance_keylet(issuer.read(data)?, seq, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 cd1c00bb1e..43accf5194 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -189,6 +189,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $trustline_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 40) (i32.const 20) (i32.const 60) (i32.const 32))", 8, ), + HostFunctionSpec::MptokenIssuanceKeylet => ( + import::MPT_ISSUANCE_ID, + "(call $mpt_issuance_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", + 5, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 027ef8c8eb..456bcbccb5 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -571,6 +571,24 @@ fn trustline_id_reads_two_accounts_and_a_currency() { ); } +/// The issuer-and-sequence keylet shape (like escrow), with its own answer set. +#[test] +fn mpt_issuance_id_reads_the_issuer_and_seq() { + let issuer = vec![0u8; 20]; + let host = FakeHost::new().answering_mpt_issuance_keylet( + issuer.clone(), + 5, + support::Answer::filler(32), + ); + + let wat = module( + &[import::MPT_ISSUANCE_ID, ONE_PAGE], + "(call $mpt_issuance_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!(*host.mpt_issuance_keylets_asked.borrow(), vec![(issuer, 5)]); +} + /// 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 7a9b09f4f1..01d167b607 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; 31] = [ +const ALL_IMPORTS: [&str; 32] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -127,6 +127,7 @@ const ALL_IMPORTS: [&str; 31] = [ import::DID_ID, import::ESCROW_ID, import::TRUSTLINE_ID, + import::MPT_ISSUANCE_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 15a1a1acdf..52000126bb 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -224,6 +224,11 @@ pub struct FakeHost { pub trust_line_keylets: HashMap<(Vec, Vec, Vec), Answer>, /// Every (account1, account2, currency) `trust_line_keylet` was asked for. pub trust_line_keylets_asked: RefCell, Vec, Vec)>>, + /// What `mptoken_issuance_keylet` answers, by (issuer bytes, seq). An unlisted key + /// answers `InvalidAccount`. + pub mpt_issuance_keylets: HashMap<(Vec, i32), Answer>, + /// Every (issuer, seq) `mptoken_issuance_keylet` was asked for. + pub mpt_issuance_keylets_asked: RefCell, i32)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -296,6 +301,8 @@ impl Default for FakeHost { escrow_keylets_asked: RefCell::new(Vec::new()), trust_line_keylets: HashMap::new(), trust_line_keylets_asked: RefCell::new(Vec::new()), + mpt_issuance_keylets: HashMap::new(), + mpt_issuance_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -499,6 +506,16 @@ impl FakeHost { self } + pub fn answering_mpt_issuance_keylet( + mut self, + issuer: Vec, + seq: i32, + answer: Answer, + ) -> FakeHost { + self.mpt_issuance_keylets.insert((issuer, seq), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -772,6 +789,22 @@ impl HostFunctions for FakeHost { } } + fn mptoken_issuance_keylet( + &self, + issuer: &[u8], + seq: i32, + out: &mut [u8], + ) -> HostResult { + let key = (issuer.to_vec(), seq); + self.mpt_issuance_keylets_asked + .borrow_mut() + .push(key.clone()); + match self.mpt_issuance_keylets.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -841,6 +874,7 @@ pub mod import { r#"(import "host_lib" "did_id" (func $did_id (param i32 i32 i32 i32) (result i32)))"#; pub const ESCROW_ID: &str = r#"(import "host_lib" "escrow_id" (func $escrow_id (param i32 i32 i32 i32 i32) (result i32)))"#; pub const TRUSTLINE_ID: &str = r#"(import "host_lib" "trustline_id" (func $trustline_id (param i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const MPT_ISSUANCE_ID: &str = r#"(import "host_lib" "mpt_issuance_id" (func $mpt_issuance_id (param i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 324f849f1c..0d430a425a 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -186,6 +186,14 @@ public: rust::Slice currency, rust::Slice out) const noexcept; + // The issuer id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's + // u32 as its i32 bit pattern. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + mptokenIssuanceKeylet( + rust::Slice issuer, + std::int32_t seq, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index fea825b173..ba9bf3846f 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -624,6 +624,26 @@ HostContext::trustLineKeylet( }); } +std::int32_t +HostContext::mptokenIssuanceKeylet( + rust::Slice issuer, + std::int32_t seq, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (issuer.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + // The guest's u32 seq arrives as its i32 bit pattern; recover it. + auto const value = hostFunctions_.mptokenIssuanceKeylet( + AccountID::fromVoid(issuer.data()), static_cast(seq)); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 2821cc3e8e76134bd0c6376ec9f5da60d3b1ed93 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 21:08:26 -0400 Subject: [PATCH 28/46] feat: Hook up mptoken_id host function --- crates/xrpl-host-functions/src/lib.rs | 6 +++++ .../tests/generated_abi.rs | 22 +++++++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 ++++++ crates/xrpl-wasm-vm/src/abi.rs | 8 ++++++ crates/xrpl-wasm-vm/src/register.rs | 21 +++++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 23 ++++++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 27 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 8 ++++++ src/libxrpl/tx/wasm/HostContext.cpp | 19 +++++++++++++ 11 files changed, 149 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 8458fa4a5d..3d2eb392a6 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -328,6 +328,12 @@ host_functions! { out: &mut [u8], ) -> 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. + #[gas = 500] + #[wasm_name = "mptoken_id"] + fn mptoken_keylet(&self, mptid: &[u8], holder: &[u8], out: &mut [u8]) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 0b04c597cd..6d6ea9ba45 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -296,6 +296,18 @@ impl HostFunctions for FakeHost { put(out, &[issuer[0]; HASH_LEN]) } + /// A keylet from an MPT id and a holder; `InvalidParams` if the id is empty, + /// `InvalidAccount` if the holder is empty. + fn mptoken_keylet(&self, mptid: &[u8], holder: &[u8], out: &mut [u8]) -> HostResult { + if mptid.is_empty() { + return Err(HostError::InvalidParams); + } + if holder.is_empty() { + return Err(HostError::InvalidAccount); + } + put(out, &[mptid[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -457,6 +469,15 @@ fn the_trait_is_implementable() { host.mptoken_issuance_keylet(&[], 5, &mut out), Err(HostError::InvalidAccount) ); + assert_eq!( + host.mptoken_keylet(&[9; 24], &[8; 20], &mut out), + Ok(HASH_LEN) + ); + assert_eq!(out[0], 9); + assert_eq!( + host.mptoken_keylet(&[], &[8; 20], &mut out), + Err(HostError::InvalidParams) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -553,6 +574,7 @@ fn the_spec_table_matches_the_declarations() { ("escrow_id", 350), ("trustline_id", 400), ("mpt_issuance_id", 350), + ("mptoken_id", 500), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 8210042e49..9ca13ba351 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -327,6 +327,10 @@ mod ffi { out: &mut [u8], ) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "mptokenKeylet"] + fn mptoken_keylet(self: &HostContext, mptid: &[u8], holder: &[u8], out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -547,6 +551,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.mptoken_issuance_keylet(issuer, seq, out)) } + fn mptoken_keylet(&self, mptid: &[u8], holder: &[u8], out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.mptoken_keylet(mptid, holder, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 614d0216cd..873cd825d2 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -322,6 +322,14 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn mptoken_keylet( + &self, + _mptid: &[u8], + _holder: &[u8], + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 fcea025a70..253c1fea4d 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -542,6 +542,27 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::MptokenKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + mpt_ptr: i32, + mpt_len: i32, + holder_ptr: i32, + holder_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::MptokenKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let mptid = Region::new(mpt_ptr, mpt_len); + let holder = Region::new(holder_ptr, holder_len); + write_buffered(c, out, |host, data, buf| { + host.mptoken_keylet(mptid.read(data)?, holder.read(data)?, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 43accf5194..847f0ce468 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -194,6 +194,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $mpt_issuance_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", 5, ), + HostFunctionSpec::MptokenKeylet => ( + import::MPTOKEN_ID, + "(call $mptoken_id (i32.const 0) (i32.const 24) (i32.const 24) (i32.const 20) (i32.const 44) (i32.const 20))", + 6, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 456bcbccb5..1ea3d873db 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -589,6 +589,29 @@ fn mpt_issuance_id_reads_the_issuer_and_seq() { assert_eq!(*host.mpt_issuance_keylets_asked.borrow(), vec![(issuer, 5)]); } +/// A keylet from a 24-byte MPT id and a 20-byte holder: both reach the host as a +/// pair, and the keylet lands where the guest asked. +#[test] +fn mptoken_id_reads_the_mptid_and_holder() { + let mptid = vec![0u8; 24]; + let holder = vec![0u8; 20]; + let host = FakeHost::new().answering_mptoken_keylet( + mptid.clone(), + holder.clone(), + support::Answer::filler(32), + ); + + let wat = module( + &[import::MPTOKEN_ID, ONE_PAGE], + "(call $mptoken_id + (i32.const 0) (i32.const 24) + (i32.const 24) (i32.const 20) + (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!(*host.mptoken_keylets_asked.borrow(), vec![(mptid, holder)]); +} + /// 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 01d167b607..e468a5deee 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; 32] = [ +const ALL_IMPORTS: [&str; 33] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -128,6 +128,7 @@ const ALL_IMPORTS: [&str; 32] = [ import::ESCROW_ID, import::TRUSTLINE_ID, import::MPT_ISSUANCE_ID, + import::MPTOKEN_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 52000126bb..1ec0684b2f 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -229,6 +229,11 @@ pub struct FakeHost { pub mpt_issuance_keylets: HashMap<(Vec, i32), Answer>, /// Every (issuer, seq) `mptoken_issuance_keylet` was asked for. pub mpt_issuance_keylets_asked: RefCell, i32)>>, + /// What `mptoken_keylet` answers, by (mptid, holder) bytes. An unlisted key answers + /// `InvalidParams`. + pub mptoken_keylets: HashMap<(Vec, Vec), Answer>, + /// Every (mptid, holder) `mptoken_keylet` was asked for. + pub mptoken_keylets_asked: RefCell, Vec)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -303,6 +308,8 @@ impl Default for FakeHost { trust_line_keylets_asked: RefCell::new(Vec::new()), mpt_issuance_keylets: HashMap::new(), mpt_issuance_keylets_asked: RefCell::new(Vec::new()), + mptoken_keylets: HashMap::new(), + mptoken_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -516,6 +523,16 @@ impl FakeHost { self } + pub fn answering_mptoken_keylet( + mut self, + mptid: Vec, + holder: Vec, + answer: Answer, + ) -> FakeHost { + self.mptoken_keylets.insert((mptid, holder), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -805,6 +822,15 @@ impl HostFunctions for FakeHost { } } + fn mptoken_keylet(&self, mptid: &[u8], holder: &[u8], out: &mut [u8]) -> HostResult { + let key = (mptid.to_vec(), holder.to_vec()); + self.mptoken_keylets_asked.borrow_mut().push(key.clone()); + match self.mptoken_keylets.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidParams), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -875,6 +901,7 @@ pub mod import { pub const ESCROW_ID: &str = r#"(import "host_lib" "escrow_id" (func $escrow_id (param i32 i32 i32 i32 i32) (result i32)))"#; pub const TRUSTLINE_ID: &str = r#"(import "host_lib" "trustline_id" (func $trustline_id (param i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const MPT_ISSUANCE_ID: &str = r#"(import "host_lib" "mpt_issuance_id" (func $mpt_issuance_id (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const MPTOKEN_ID: &str = r#"(import "host_lib" "mptoken_id" (func $mptoken_id (param i32 i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 0d430a425a..6597a9f366 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -194,6 +194,14 @@ public: std::int32_t seq, rust::Slice out) const noexcept; + // The MPT id must be 24 bytes and the holder 20, else `InvalidParams`. Writes the + // 32-byte keylet. + [[nodiscard]] std::int32_t + mptokenKeylet( + rust::Slice mptid, + rust::Slice holder, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index ba9bf3846f..a488b5fec5 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -644,6 +644,25 @@ HostContext::mptokenIssuanceKeylet( }); } +std::int32_t +HostContext::mptokenKeylet( + rust::Slice mptid, + rust::Slice holder, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (mptid.size() != MPTID::size() || holder.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.mptokenKeylet( + MPTID::fromVoid(mptid.data()), AccountID::fromVoid(holder.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From f52eb08d8a1ca542867dca2cb2915b0e92ebc60f Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 21:12:57 -0400 Subject: [PATCH 29/46] feat: Hook up nft_offer_id host function --- crates/xrpl-host-functions/src/lib.rs | 12 +++++++++ .../tests/generated_abi.rs | 18 +++++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 13 +++++++++ crates/xrpl-wasm-vm/src/abi.rs | 8 ++++++ crates/xrpl-wasm-vm/src/register.rs | 19 +++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 15 +++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 27 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 8 ++++++ src/libxrpl/tx/wasm/HostContext.cpp | 20 ++++++++++++++ 11 files changed, 147 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 3d2eb392a6..22befeeb69 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -334,6 +334,18 @@ host_functions! { #[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. + #[gas = 350] + #[wasm_name = "nft_offer_id"] + fn nftoken_offer_keylet( + &self, + account: &[u8], + seq: i32, + out: &mut [u8], + ) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 6d6ea9ba45..13300699c6 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -308,6 +308,14 @@ impl HostFunctions for FakeHost { put(out, &[mptid[0]; HASH_LEN]) } + /// The account-and-sequence shape, for an `NFTokenOffer`. + fn nftoken_offer_keylet(&self, account: &[u8], _seq: i32, out: &mut [u8]) -> HostResult { + if account.is_empty() { + return Err(HostError::InvalidAccount); + } + put(out, &[account[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -478,6 +486,15 @@ fn the_trait_is_implementable() { host.mptoken_keylet(&[], &[8; 20], &mut out), Err(HostError::InvalidParams) ); + assert_eq!( + host.nftoken_offer_keylet(&[7; 20], 5, &mut out), + Ok(HASH_LEN) + ); + assert_eq!(out[0], 7); + assert_eq!( + host.nftoken_offer_keylet(&[], 5, &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -575,6 +592,7 @@ fn the_spec_table_matches_the_declarations() { ("trustline_id", 400), ("mpt_issuance_id", 350), ("mptoken_id", 500), + ("nft_offer_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 9ca13ba351..baff06d0a2 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -331,6 +331,15 @@ mod ffi { #[cxx_name = "mptokenKeylet"] fn mptoken_keylet(self: &HostContext, mptid: &[u8], holder: &[u8], out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "nftokenOfferKeylet"] + fn nftoken_offer_keylet( + self: &HostContext, + account: &[u8], + seq: i32, + out: &mut [u8], + ) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -555,6 +564,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.mptoken_keylet(mptid, holder, out)) } + fn nftoken_offer_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.nftoken_offer_keylet(account, seq, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 873cd825d2..edfcecf4b2 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -330,6 +330,14 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn nftoken_offer_keylet( + &self, + _account: &[u8], + _seq: i32, + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 253c1fea4d..dcf38a865f 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -563,6 +563,25 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::NftokenOfferKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + seq: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::NftokenOfferKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + write_buffered(c, out, |host, data, buf| { + host.nftoken_offer_keylet(account.read(data)?, seq, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 847f0ce468..6934a54892 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -199,6 +199,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $mptoken_id (i32.const 0) (i32.const 24) (i32.const 24) (i32.const 20) (i32.const 44) (i32.const 20))", 6, ), + HostFunctionSpec::NftokenOfferKeylet => ( + import::NFT_OFFER_ID, + "(call $nft_offer_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", + 5, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 1ea3d873db..ae806c914c 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -612,6 +612,21 @@ fn mptoken_id_reads_the_mptid_and_holder() { assert_eq!(*host.mptoken_keylets_asked.borrow(), vec![(mptid, holder)]); } +/// Another account-and-sequence keylet, with its own answer set. +#[test] +fn nft_offer_id_reads_the_account_and_seq() { + let account = vec![0u8; 20]; + let host = + FakeHost::new().answering_nft_offer_keylet(account.clone(), 5, support::Answer::filler(32)); + + let wat = module( + &[import::NFT_OFFER_ID, ONE_PAGE], + "(call $nft_offer_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!(*host.nft_offer_keylets_asked.borrow(), vec![(account, 5)]); +} + /// 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 e468a5deee..dee98fac84 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; 33] = [ +const ALL_IMPORTS: [&str; 34] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -129,6 +129,7 @@ const ALL_IMPORTS: [&str; 33] = [ import::TRUSTLINE_ID, import::MPT_ISSUANCE_ID, import::MPTOKEN_ID, + import::NFT_OFFER_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 1ec0684b2f..67415927e3 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -234,6 +234,11 @@ pub struct FakeHost { pub mptoken_keylets: HashMap<(Vec, Vec), Answer>, /// Every (mptid, holder) `mptoken_keylet` was asked for. pub mptoken_keylets_asked: RefCell, Vec)>>, + /// What `nftoken_offer_keylet` answers, by (account bytes, seq). An unlisted key + /// answers `InvalidAccount`. + pub nft_offer_keylets: HashMap<(Vec, i32), Answer>, + /// Every (account, seq) `nftoken_offer_keylet` was asked for. + pub nft_offer_keylets_asked: RefCell, i32)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -310,6 +315,8 @@ impl Default for FakeHost { mpt_issuance_keylets_asked: RefCell::new(Vec::new()), mptoken_keylets: HashMap::new(), mptoken_keylets_asked: RefCell::new(Vec::new()), + nft_offer_keylets: HashMap::new(), + nft_offer_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -533,6 +540,16 @@ impl FakeHost { self } + pub fn answering_nft_offer_keylet( + mut self, + account: Vec, + seq: i32, + answer: Answer, + ) -> FakeHost { + self.nft_offer_keylets.insert((account, seq), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -831,6 +848,15 @@ impl HostFunctions for FakeHost { } } + fn nftoken_offer_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult { + let key = (account.to_vec(), seq); + self.nft_offer_keylets_asked.borrow_mut().push(key.clone()); + match self.nft_offer_keylets.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -902,6 +928,7 @@ pub mod import { pub const TRUSTLINE_ID: &str = r#"(import "host_lib" "trustline_id" (func $trustline_id (param i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const MPT_ISSUANCE_ID: &str = r#"(import "host_lib" "mpt_issuance_id" (func $mpt_issuance_id (param i32 i32 i32 i32 i32) (result i32)))"#; pub const MPTOKEN_ID: &str = r#"(import "host_lib" "mptoken_id" (func $mptoken_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const NFT_OFFER_ID: &str = r#"(import "host_lib" "nft_offer_id" (func $nft_offer_id (param i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 6597a9f366..2945edfb0d 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -202,6 +202,14 @@ public: rust::Slice holder, rust::Slice out) const noexcept; + // The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's + // u32 as its i32 bit pattern. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + nftokenOfferKeylet( + rust::Slice account, + std::int32_t seq, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index a488b5fec5..28fb972b35 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -663,6 +663,26 @@ HostContext::mptokenKeylet( }); } +std::int32_t +HostContext::nftokenOfferKeylet( + rust::Slice account, + std::int32_t seq, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + // The guest's u32 seq arrives as its i32 bit pattern; recover it. + auto const value = hostFunctions_.nftokenOfferKeylet( + AccountID::fromVoid(account.data()), static_cast(seq)); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 5f6f367b2365e1352d3dffa7277c4dfed0ec3da1 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 21:20:07 -0400 Subject: [PATCH 30/46] feat: Hook up oracle_id host function --- crates/xrpl-host-functions/src/lib.rs | 7 +++++ .../tests/generated_abi.rs | 15 +++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 ++++++ crates/xrpl-wasm-vm/src/abi.rs | 3 +++ crates/xrpl-wasm-vm/src/register.rs | 19 +++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 16 +++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 27 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 8 ++++++ src/libxrpl/tx/wasm/HostContext.cpp | 20 ++++++++++++++ 11 files changed, 130 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 22befeeb69..ff0b957f33 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -346,6 +346,13 @@ host_functions! { out: &mut [u8], ) -> HostResult; + /// 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. + #[gas = 350] + #[wasm_name = "offer_id"] + fn offer_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 13300699c6..f51e862112 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -316,6 +316,14 @@ impl HostFunctions for FakeHost { put(out, &[account[0]; HASH_LEN]) } + /// The same account-and-sequence shape, for an `Offer`. + fn offer_keylet(&self, account: &[u8], _seq: i32, out: &mut [u8]) -> HostResult { + if account.is_empty() { + return Err(HostError::InvalidAccount); + } + put(out, &[account[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -495,6 +503,12 @@ fn the_trait_is_implementable() { host.nftoken_offer_keylet(&[], 5, &mut out), Err(HostError::InvalidAccount) ); + assert_eq!(host.offer_keylet(&[7; 20], 5, &mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 7); + assert_eq!( + host.offer_keylet(&[], 5, &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -593,6 +607,7 @@ fn the_spec_table_matches_the_declarations() { ("mpt_issuance_id", 350), ("mptoken_id", 500), ("nft_offer_id", 350), + ("offer_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index baff06d0a2..1c67fcee76 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -340,6 +340,10 @@ mod ffi { out: &mut [u8], ) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "offerKeylet"] + fn offer_keylet(self: &HostContext, account: &[u8], seq: i32, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -568,6 +572,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.nftoken_offer_keylet(account, seq, out)) } + fn offer_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.offer_keylet(account, seq, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index edfcecf4b2..1af16e33bb 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -338,6 +338,9 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn offer_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 dcf38a865f..18f52f62c5 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -582,6 +582,25 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::OfferKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + seq: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::OfferKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + write_buffered(c, out, |host, data, buf| { + host.offer_keylet(account.read(data)?, seq, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 6934a54892..bd1b372b18 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -204,6 +204,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $nft_offer_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", 5, ), + HostFunctionSpec::OfferKeylet => ( + import::OFFER_ID, + "(call $offer_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", + 5, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index ae806c914c..0e22a51cbc 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -627,6 +627,22 @@ fn nft_offer_id_reads_the_account_and_seq() { assert_eq!(*host.nft_offer_keylets_asked.borrow(), vec![(account, 5)]); } +/// A third account-and-sequence keylet, distinct from the NFT-offer set, to pin the +/// pattern rather than a single instance of it. +#[test] +fn offer_id_reads_the_account_and_seq() { + let account = vec![0u8; 20]; + let host = + FakeHost::new().answering_offer_keylet(account.clone(), 5, support::Answer::filler(32)); + + let wat = module( + &[import::OFFER_ID, ONE_PAGE], + "(call $offer_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!(*host.offer_keylets_asked.borrow(), vec![(account, 5)]); +} + /// 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 dee98fac84..b4052fac3f 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; 34] = [ +const ALL_IMPORTS: [&str; 35] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -130,6 +130,7 @@ const ALL_IMPORTS: [&str; 34] = [ import::MPT_ISSUANCE_ID, import::MPTOKEN_ID, import::NFT_OFFER_ID, + import::OFFER_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 67415927e3..3d1ff8468c 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -239,6 +239,11 @@ pub struct FakeHost { pub nft_offer_keylets: HashMap<(Vec, i32), Answer>, /// Every (account, seq) `nftoken_offer_keylet` was asked for. pub nft_offer_keylets_asked: RefCell, i32)>>, + /// What `offer_keylet` answers, by (account bytes, seq). An unlisted key + /// answers `InvalidAccount`. + pub offer_keylets: HashMap<(Vec, i32), Answer>, + /// Every (account, seq) `offer_keylet` was asked for. + pub offer_keylets_asked: RefCell, i32)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -317,6 +322,8 @@ impl Default for FakeHost { mptoken_keylets_asked: RefCell::new(Vec::new()), nft_offer_keylets: HashMap::new(), nft_offer_keylets_asked: RefCell::new(Vec::new()), + offer_keylets: HashMap::new(), + offer_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -550,6 +557,16 @@ impl FakeHost { self } + pub fn answering_offer_keylet( + mut self, + account: Vec, + seq: i32, + answer: Answer, + ) -> FakeHost { + self.offer_keylets.insert((account, seq), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -857,6 +874,15 @@ impl HostFunctions for FakeHost { } } + fn offer_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult { + let key = (account.to_vec(), seq); + self.offer_keylets_asked.borrow_mut().push(key.clone()); + match self.offer_keylets.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -929,6 +955,7 @@ pub mod import { pub const MPT_ISSUANCE_ID: &str = r#"(import "host_lib" "mpt_issuance_id" (func $mpt_issuance_id (param i32 i32 i32 i32 i32) (result i32)))"#; pub const MPTOKEN_ID: &str = r#"(import "host_lib" "mptoken_id" (func $mptoken_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const NFT_OFFER_ID: &str = r#"(import "host_lib" "nft_offer_id" (func $nft_offer_id (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const OFFER_ID: &str = r#"(import "host_lib" "offer_id" (func $offer_id (param i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 2945edfb0d..cb99158dd2 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -210,6 +210,14 @@ public: std::int32_t seq, rust::Slice out) const noexcept; + // The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's + // u32 as its i32 bit pattern. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + offerKeylet( + rust::Slice account, + std::int32_t seq, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 28fb972b35..827462a538 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -683,6 +683,26 @@ HostContext::nftokenOfferKeylet( }); } +std::int32_t +HostContext::offerKeylet( + rust::Slice account, + std::int32_t seq, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + // The guest's u32 seq arrives as its i32 bit pattern; recover it. + auto const value = hostFunctions_.offerKeylet( + AccountID::fromVoid(account.data()), static_cast(seq)); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 58e47b01012be9f282d8dccdbd62430c7379a6a8 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 21:26:29 -0400 Subject: [PATCH 31/46] feat: Hook up oracle_id host function --- crates/xrpl-host-functions/src/lib.rs | 7 +++++ .../tests/generated_abi.rs | 15 +++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 ++++++ crates/xrpl-wasm-vm/src/abi.rs | 8 ++++++ crates/xrpl-wasm-vm/src/register.rs | 19 +++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 16 +++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 27 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 8 ++++++ src/libxrpl/tx/wasm/HostContext.cpp | 20 ++++++++++++++ 11 files changed, 135 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index ff0b957f33..2374c66924 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -353,6 +353,13 @@ host_functions! { #[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 XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index f51e862112..a12bfedf2a 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -324,6 +324,14 @@ impl HostFunctions for FakeHost { put(out, &[account[0]; HASH_LEN]) } + /// The same account-and-scalar shape, for an `Oracle` keyed by document id. + fn oracle_keylet(&self, account: &[u8], _doc_id: i32, out: &mut [u8]) -> HostResult { + if account.is_empty() { + return Err(HostError::InvalidAccount); + } + put(out, &[account[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -509,6 +517,12 @@ fn the_trait_is_implementable() { host.offer_keylet(&[], 5, &mut out), Err(HostError::InvalidAccount) ); + assert_eq!(host.oracle_keylet(&[7; 20], 5, &mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 7); + assert_eq!( + host.oracle_keylet(&[], 5, &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -608,6 +622,7 @@ fn the_spec_table_matches_the_declarations() { ("mptoken_id", 500), ("nft_offer_id", 350), ("offer_id", 350), + ("oracle_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 1c67fcee76..c674164150 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -344,6 +344,10 @@ mod ffi { #[cxx_name = "offerKeylet"] fn offer_keylet(self: &HostContext, account: &[u8], seq: i32, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "oracleKeylet"] + fn oracle_keylet(self: &HostContext, account: &[u8], doc_id: i32, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -576,6 +580,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.offer_keylet(account, seq, out)) } + fn oracle_keylet(&self, account: &[u8], doc_id: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.oracle_keylet(account, doc_id, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 1af16e33bb..4a6f6dbca8 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -341,6 +341,14 @@ mod tests { fn offer_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn oracle_keylet( + &self, + _account: &[u8], + _doc_id: i32, + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 18f52f62c5..e6e9c582bb 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -601,6 +601,25 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::OracleKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + doc_id: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::OracleKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + write_buffered(c, out, |host, data, buf| { + host.oracle_keylet(account.read(data)?, doc_id, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 bd1b372b18..5fed6a4f2d 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -209,6 +209,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $offer_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", 5, ), + HostFunctionSpec::OracleKeylet => ( + import::ORACLE_ID, + "(call $oracle_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", + 5, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 0e22a51cbc..c40c8ea75b 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -643,6 +643,22 @@ fn offer_id_reads_the_account_and_seq() { assert_eq!(*host.offer_keylets_asked.borrow(), vec![(account, 5)]); } +/// The account-and-scalar keylet, keyed on a document id rather than a sequence; its +/// own answer set, to keep it distinct from the other account-and-scalar getters. +#[test] +fn oracle_id_reads_the_account_and_doc_id() { + let account = vec![0u8; 20]; + let host = + FakeHost::new().answering_oracle_keylet(account.clone(), 5, support::Answer::filler(32)); + + let wat = module( + &[import::ORACLE_ID, ONE_PAGE], + "(call $oracle_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!(*host.oracle_keylets_asked.borrow(), vec![(account, 5)]); +} + /// 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 b4052fac3f..ec0a8df454 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; 35] = [ +const ALL_IMPORTS: [&str; 36] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -131,6 +131,7 @@ const ALL_IMPORTS: [&str; 35] = [ import::MPTOKEN_ID, import::NFT_OFFER_ID, import::OFFER_ID, + import::ORACLE_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 3d1ff8468c..215fb29c13 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -244,6 +244,11 @@ pub struct FakeHost { pub offer_keylets: HashMap<(Vec, i32), Answer>, /// Every (account, seq) `offer_keylet` was asked for. pub offer_keylets_asked: RefCell, i32)>>, + /// What `oracle_keylet` answers, by (account bytes, doc id). An unlisted key + /// answers `InvalidAccount`. + pub oracle_keylets: HashMap<(Vec, i32), Answer>, + /// Every (account, doc id) `oracle_keylet` was asked for. + pub oracle_keylets_asked: RefCell, i32)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -324,6 +329,8 @@ impl Default for FakeHost { nft_offer_keylets_asked: RefCell::new(Vec::new()), offer_keylets: HashMap::new(), offer_keylets_asked: RefCell::new(Vec::new()), + oracle_keylets: HashMap::new(), + oracle_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -567,6 +574,16 @@ impl FakeHost { self } + pub fn answering_oracle_keylet( + mut self, + account: Vec, + doc_id: i32, + answer: Answer, + ) -> FakeHost { + self.oracle_keylets.insert((account, doc_id), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -883,6 +900,15 @@ impl HostFunctions for FakeHost { } } + fn oracle_keylet(&self, account: &[u8], doc_id: i32, out: &mut [u8]) -> HostResult { + let key = (account.to_vec(), doc_id); + self.oracle_keylets_asked.borrow_mut().push(key.clone()); + match self.oracle_keylets.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -956,6 +982,7 @@ pub mod import { pub const MPTOKEN_ID: &str = r#"(import "host_lib" "mptoken_id" (func $mptoken_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const NFT_OFFER_ID: &str = r#"(import "host_lib" "nft_offer_id" (func $nft_offer_id (param i32 i32 i32 i32 i32) (result i32)))"#; pub const OFFER_ID: &str = r#"(import "host_lib" "offer_id" (func $offer_id (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const ORACLE_ID: &str = r#"(import "host_lib" "oracle_id" (func $oracle_id (param i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index cb99158dd2..48bde56df7 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -218,6 +218,14 @@ public: std::int32_t seq, rust::Slice out) const noexcept; + // The account id must be 20 bytes, else `InvalidParams`. `docId` carries the + // guest's u32 as its i32 bit pattern. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + oracleKeylet( + rust::Slice account, + std::int32_t docId, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 827462a538..52705b1c1d 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -703,6 +703,26 @@ HostContext::offerKeylet( }); } +std::int32_t +HostContext::oracleKeylet( + rust::Slice account, + std::int32_t docId, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + // The guest's u32 docId arrives as its i32 bit pattern; recover it. + auto const value = hostFunctions_.oracleKeylet( + AccountID::fromVoid(account.data()), static_cast(docId)); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From ca560ed6b0e62df89076f9103c286586b67113e5 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 21:31:09 -0400 Subject: [PATCH 32/46] feat: Hook up paychan_id host function --- crates/xrpl-host-functions/src/lib.rs | 14 ++++++++ .../tests/generated_abi.rs | 25 +++++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 20 +++++++++++ crates/xrpl-wasm-vm/src/abi.rs | 9 +++++ crates/xrpl-wasm-vm/src/register.rs | 27 ++++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 +++ crates/xrpl-wasm-vm/tests/host_calls.rs | 24 +++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 35 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 9 +++++ src/libxrpl/tx/wasm/HostContext.cpp | 23 ++++++++++++ 11 files changed, 193 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 2374c66924..4dcc509937 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -360,6 +360,20 @@ host_functions! { #[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. + #[gas = 350] + #[wasm_name = "paychan_id"] + fn paychannel_keylet( + &self, + account: &[u8], + destination: &[u8], + seq: i32, + out: &mut [u8], + ) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index a12bfedf2a..d8ef7520a4 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -332,6 +332,21 @@ impl HostFunctions for FakeHost { put(out, &[account[0]; HASH_LEN]) } + /// A two-account-and-sequence shape, for a `PayChannel`; `InvalidAccount` if + /// either account is empty. + fn paychannel_keylet( + &self, + account: &[u8], + destination: &[u8], + _seq: i32, + out: &mut [u8], + ) -> HostResult { + if account.is_empty() || destination.is_empty() { + return Err(HostError::InvalidAccount); + } + put(out, &[account[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -523,6 +538,15 @@ fn the_trait_is_implementable() { host.oracle_keylet(&[], 5, &mut out), Err(HostError::InvalidAccount) ); + assert_eq!( + host.paychannel_keylet(&[7; 20], &[8; 20], 5, &mut out), + Ok(HASH_LEN) + ); + assert_eq!(out[0], 7); + assert_eq!( + host.paychannel_keylet(&[7; 20], &[], 5, &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -623,6 +647,7 @@ fn the_spec_table_matches_the_declarations() { ("nft_offer_id", 350), ("offer_id", 350), ("oracle_id", 350), + ("paychan_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index c674164150..69ca769ad8 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -348,6 +348,16 @@ mod ffi { #[cxx_name = "oracleKeylet"] fn oracle_keylet(self: &HostContext, account: &[u8], doc_id: i32, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "paychannelKeylet"] + fn paychannel_keylet( + self: &HostContext, + account: &[u8], + destination: &[u8], + seq: i32, + out: &mut [u8], + ) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -584,6 +594,16 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.oracle_keylet(account, doc_id, out)) } + fn paychannel_keylet( + &self, + account: &[u8], + destination: &[u8], + seq: i32, + out: &mut [u8], + ) -> HostResult { + bytes_written(self.ctx.paychannel_keylet(account, destination, seq, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 4a6f6dbca8..bdbd9fc674 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -349,6 +349,15 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn paychannel_keylet( + &self, + _account: &[u8], + _destination: &[u8], + _seq: i32, + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 e6e9c582bb..469b72066a 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -620,6 +620,33 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::PaychannelKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + dst_ptr: i32, + dst_len: i32, + seq: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::PaychannelKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + let destination = Region::new(dst_ptr, dst_len); + write_buffered(c, out, |host, data, buf| { + host.paychannel_keylet( + account.read(data)?, + destination.read(data)?, + seq, + buf, + ) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 5fed6a4f2d..c8597eb295 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -214,6 +214,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $oracle_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", 5, ), + HostFunctionSpec::PaychannelKeylet => ( + import::PAYCHAN_ID, + "(call $paychan_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 5) (i32.const 40) (i32.const 20))", + 7, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index c40c8ea75b..941b7798ec 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -659,6 +659,30 @@ fn oracle_id_reads_the_account_and_doc_id() { assert_eq!(*host.oracle_keylets_asked.borrow(), vec![(account, 5)]); } +/// A keylet that reads two account regions and a scalar: both accounts and the +/// sequence reach the host, keyed together, and the answered bytes land where asked. +#[test] +fn paychan_id_reads_both_accounts_and_the_seq() { + let account = vec![0u8; 20]; + let destination = vec![0u8; 20]; + let host = FakeHost::new().answering_paychannel_keylet( + account.clone(), + destination.clone(), + 5, + support::Answer::filler(32), + ); + + let wat = module( + &[import::PAYCHAN_ID, ONE_PAGE], + "(call $paychan_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!( + *host.paychannel_keylets_asked.borrow(), + vec![(account, destination, 5)] + ); +} + /// 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 ec0a8df454..67b181b113 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; 36] = [ +const ALL_IMPORTS: [&str; 37] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -132,6 +132,7 @@ const ALL_IMPORTS: [&str; 36] = [ import::NFT_OFFER_ID, import::OFFER_ID, import::ORACLE_ID, + import::PAYCHAN_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 215fb29c13..e9feedde13 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -249,6 +249,11 @@ pub struct FakeHost { pub oracle_keylets: HashMap<(Vec, i32), Answer>, /// Every (account, doc id) `oracle_keylet` was asked for. pub oracle_keylets_asked: RefCell, i32)>>, + /// What `paychannel_keylet` answers, by (account, destination, seq). An unlisted + /// key answers `InvalidAccount`. + pub paychannel_keylets: HashMap<(Vec, Vec, i32), Answer>, + /// Every (account, destination, seq) `paychannel_keylet` was asked for. + pub paychannel_keylets_asked: RefCell, Vec, i32)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -331,6 +336,8 @@ impl Default for FakeHost { offer_keylets_asked: RefCell::new(Vec::new()), oracle_keylets: HashMap::new(), oracle_keylets_asked: RefCell::new(Vec::new()), + paychannel_keylets: HashMap::new(), + paychannel_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -584,6 +591,18 @@ impl FakeHost { self } + pub fn answering_paychannel_keylet( + mut self, + account: Vec, + destination: Vec, + seq: i32, + answer: Answer, + ) -> FakeHost { + self.paychannel_keylets + .insert((account, destination, seq), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -909,6 +928,21 @@ impl HostFunctions for FakeHost { } } + fn paychannel_keylet( + &self, + account: &[u8], + destination: &[u8], + seq: i32, + out: &mut [u8], + ) -> HostResult { + let key = (account.to_vec(), destination.to_vec(), seq); + self.paychannel_keylets_asked.borrow_mut().push(key.clone()); + match self.paychannel_keylets.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -983,6 +1017,7 @@ pub mod import { pub const NFT_OFFER_ID: &str = r#"(import "host_lib" "nft_offer_id" (func $nft_offer_id (param i32 i32 i32 i32 i32) (result i32)))"#; pub const OFFER_ID: &str = r#"(import "host_lib" "offer_id" (func $offer_id (param i32 i32 i32 i32 i32) (result i32)))"#; pub const ORACLE_ID: &str = r#"(import "host_lib" "oracle_id" (func $oracle_id (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const PAYCHAN_ID: &str = r#"(import "host_lib" "paychan_id" (func $paychan_id (param i32 i32 i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 48bde56df7..19e6d5fdfe 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -226,6 +226,15 @@ public: std::int32_t docId, rust::Slice out) const noexcept; + // Both account ids must be 20 bytes, else `InvalidParams`. `seq` carries the + // guest's u32 as its i32 bit pattern. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + paychannelKeylet( + rust::Slice account, + rust::Slice destination, + std::int32_t seq, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 52705b1c1d..739334d84e 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -723,6 +723,29 @@ HostContext::oracleKeylet( }); } +std::int32_t +HostContext::paychannelKeylet( + rust::Slice account, + rust::Slice destination, + std::int32_t seq, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size() || destination.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + // The guest's u32 seq arrives as its i32 bit pattern; recover it. + auto const value = hostFunctions_.paychannelKeylet( + AccountID::fromVoid(account.data()), + AccountID::fromVoid(destination.data()), + static_cast(seq)); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 6eab6c7c285be21e8069863688a7daf878b7bb6b Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 21:35:33 -0400 Subject: [PATCH 33/46] feat: Hook up permissioned_domain_id host function --- crates/xrpl-host-functions/src/lib.rs | 12 +++++++ .../tests/generated_abi.rs | 23 +++++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 18 +++++++++++ crates/xrpl-wasm-vm/src/abi.rs | 8 +++++ crates/xrpl-wasm-vm/src/register.rs | 23 +++++++++++++ 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 | 32 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 8 +++++ src/libxrpl/tx/wasm/HostContext.cpp | 20 ++++++++++++ 11 files changed, 170 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 4dcc509937..151a4ffbfe 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -374,6 +374,18 @@ host_functions! { out: &mut [u8], ) -> HostResult; + /// 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. + #[gas = 350] + #[wasm_name = "permissioned_domain_id"] + fn permissioned_domain_keylet( + &self, + account: &[u8], + seq: i32, + out: &mut [u8], + ) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index d8ef7520a4..3ba4d96e2d 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -347,6 +347,19 @@ impl HostFunctions for FakeHost { put(out, &[account[0]; HASH_LEN]) } + /// The same account-and-sequence shape, for a `PermissionedDomain`. + fn permissioned_domain_keylet( + &self, + account: &[u8], + _seq: i32, + out: &mut [u8], + ) -> HostResult { + if account.is_empty() { + return Err(HostError::InvalidAccount); + } + put(out, &[account[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -547,6 +560,15 @@ fn the_trait_is_implementable() { host.paychannel_keylet(&[7; 20], &[], 5, &mut out), Err(HostError::InvalidAccount) ); + assert_eq!( + host.permissioned_domain_keylet(&[7; 20], 5, &mut out), + Ok(HASH_LEN) + ); + assert_eq!(out[0], 7); + assert_eq!( + host.permissioned_domain_keylet(&[], 5, &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -648,6 +670,7 @@ fn the_spec_table_matches_the_declarations() { ("offer_id", 350), ("oracle_id", 350), ("paychan_id", 350), + ("permissioned_domain_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 69ca769ad8..02776c4b1e 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -358,6 +358,15 @@ mod ffi { out: &mut [u8], ) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "permissionedDomainKeylet"] + fn permissioned_domain_keylet( + self: &HostContext, + account: &[u8], + seq: i32, + out: &mut [u8], + ) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -604,6 +613,15 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.paychannel_keylet(account, destination, seq, out)) } + fn permissioned_domain_keylet( + &self, + account: &[u8], + seq: i32, + out: &mut [u8], + ) -> HostResult { + bytes_written(self.ctx.permissioned_domain_keylet(account, seq, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index bdbd9fc674..a5bd7ce8c5 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -358,6 +358,14 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn permissioned_domain_keylet( + &self, + _account: &[u8], + _seq: i32, + _out: &mut [u8], + ) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 469b72066a..22c4407d54 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -647,6 +647,29 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::PermissionedDomainKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + seq: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged( + &mut caller, + HostFunctionSpec::PermissionedDomainKeylet, + |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + write_buffered(c, out, |host, data, buf| { + host.permissioned_domain_keylet(account.read(data)?, seq, buf) + }) + }, + ) + }, + ), HostFunctionSpec::Sha512Half => 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 c8597eb295..e062f1c06c 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -219,6 +219,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $paychan_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 5) (i32.const 40) (i32.const 20))", 7, ), + HostFunctionSpec::PermissionedDomainKeylet => ( + import::PERMISSIONED_DOMAIN_ID, + "(call $permissioned_domain_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", + 5, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 941b7798ec..09dfdd807e 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -683,6 +683,25 @@ fn paychan_id_reads_both_accounts_and_the_seq() { ); } +/// Another account-and-sequence keylet, with its own answer set, for a permissioned +/// domain. +#[test] +fn permissioned_domain_id_reads_the_account_and_seq() { + let account = vec![0u8; 20]; + let host = FakeHost::new().answering_permissioned_domain_keylet( + account.clone(), + 5, + support::Answer::filler(32), + ); + + let wat = module( + &[import::PERMISSIONED_DOMAIN_ID, ONE_PAGE], + "(call $permissioned_domain_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!(*host.domain_keylets_asked.borrow(), vec![(account, 5)]); +} + /// 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 67b181b113..1542caf3c1 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; 37] = [ +const ALL_IMPORTS: [&str; 38] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -133,6 +133,7 @@ const ALL_IMPORTS: [&str; 37] = [ import::OFFER_ID, import::ORACLE_ID, import::PAYCHAN_ID, + import::PERMISSIONED_DOMAIN_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index e9feedde13..9f7c632b7e 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -254,6 +254,11 @@ pub struct FakeHost { pub paychannel_keylets: HashMap<(Vec, Vec, i32), Answer>, /// Every (account, destination, seq) `paychannel_keylet` was asked for. pub paychannel_keylets_asked: RefCell, Vec, i32)>>, + /// What `permissioned_domain_keylet` answers, by (account bytes, seq). An unlisted + /// key answers `InvalidAccount`. + pub domain_keylets: HashMap<(Vec, i32), Answer>, + /// Every (account, seq) `permissioned_domain_keylet` was asked for. + pub domain_keylets_asked: RefCell, i32)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -338,6 +343,8 @@ impl Default for FakeHost { oracle_keylets_asked: RefCell::new(Vec::new()), paychannel_keylets: HashMap::new(), paychannel_keylets_asked: RefCell::new(Vec::new()), + domain_keylets: HashMap::new(), + domain_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -603,6 +610,16 @@ impl FakeHost { self } + pub fn answering_permissioned_domain_keylet( + mut self, + account: Vec, + seq: i32, + answer: Answer, + ) -> FakeHost { + self.domain_keylets.insert((account, seq), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -943,6 +960,20 @@ impl HostFunctions for FakeHost { } } + fn permissioned_domain_keylet( + &self, + account: &[u8], + seq: i32, + out: &mut [u8], + ) -> HostResult { + let key = (account.to_vec(), seq); + self.domain_keylets_asked.borrow_mut().push(key.clone()); + match self.domain_keylets.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -1018,6 +1049,7 @@ pub mod import { pub const OFFER_ID: &str = r#"(import "host_lib" "offer_id" (func $offer_id (param i32 i32 i32 i32 i32) (result i32)))"#; pub const ORACLE_ID: &str = r#"(import "host_lib" "oracle_id" (func $oracle_id (param i32 i32 i32 i32 i32) (result i32)))"#; pub const PAYCHAN_ID: &str = r#"(import "host_lib" "paychan_id" (func $paychan_id (param i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const PERMISSIONED_DOMAIN_ID: &str = r#"(import "host_lib" "permissioned_domain_id" (func $permissioned_domain_id (param i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 19e6d5fdfe..ef8028ea17 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -235,6 +235,14 @@ public: std::int32_t seq, rust::Slice out) const noexcept; + // The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's + // u32 as its i32 bit pattern. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + permissionedDomainKeylet( + rust::Slice account, + std::int32_t seq, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 739334d84e..b4f90c839b 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -746,6 +746,26 @@ HostContext::paychannelKeylet( }); } +std::int32_t +HostContext::permissionedDomainKeylet( + rust::Slice account, + std::int32_t seq, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + // The guest's u32 seq arrives as its i32 bit pattern; recover it. + auto const value = hostFunctions_.permissionedDomainKeylet( + AccountID::fromVoid(account.data()), static_cast(seq)); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From d6a66d7249636134400fd7440dddd13f0831a781 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 21:41:38 -0400 Subject: [PATCH 34/46] feat: Hook up signers_id host function --- crates/xrpl-host-functions/src/lib.rs | 6 +++++ .../tests/generated_abi.rs | 15 ++++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 +++++++ crates/xrpl-wasm-vm/src/abi.rs | 3 +++ crates/xrpl-wasm-vm/src/register.rs | 18 +++++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 16 +++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 23 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 5 ++++ src/libxrpl/tx/wasm/HostContext.cpp | 17 ++++++++++++++ 11 files changed, 118 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 151a4ffbfe..0c85ed7944 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -386,6 +386,12 @@ host_functions! { out: &mut [u8], ) -> 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 XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 3ba4d96e2d..bce0dbe243 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -360,6 +360,14 @@ impl HostFunctions for FakeHost { put(out, &[account[0]; HASH_LEN]) } + /// The account-only shape, for a `SignerList`. + fn signer_list_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult { + if account.is_empty() { + return Err(HostError::InvalidAccount); + } + put(out, &[account[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -569,6 +577,12 @@ fn the_trait_is_implementable() { host.permissioned_domain_keylet(&[], 5, &mut out), Err(HostError::InvalidAccount) ); + assert_eq!(host.signer_list_keylet(&[7; 20], &mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 7); + assert_eq!( + host.signer_list_keylet(&[], &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -671,6 +685,7 @@ fn the_spec_table_matches_the_declarations() { ("oracle_id", 350), ("paychan_id", 350), ("permissioned_domain_id", 350), + ("signers_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 02776c4b1e..1326364176 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -367,6 +367,10 @@ mod ffi { out: &mut [u8], ) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "signerListKeylet"] + fn signer_list_keylet(self: &HostContext, account: &[u8], out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -622,6 +626,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.permissioned_domain_keylet(account, seq, out)) } + fn signer_list_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.signer_list_keylet(account, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index a5bd7ce8c5..1a25377a03 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -366,6 +366,9 @@ mod tests { ) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn signer_list_keylet(&self, _account: &[u8], _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 22c4407d54..265563c46d 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -670,6 +670,24 @@ pub(crate) fn register_host_functions( ) }, ), + HostFunctionSpec::SignerListKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::SignerListKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + write_buffered(c, out, |host, data, buf| { + host.signer_list_keylet(account.read(data)?, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 e062f1c06c..07db191a31 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -224,6 +224,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $permissioned_domain_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", 5, ), + HostFunctionSpec::SignerListKeylet => ( + import::SIGNERS_ID, + "(call $signers_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 32))", + 4, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 09dfdd807e..026c830bdc 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -702,6 +702,22 @@ fn permissioned_domain_id_reads_the_account_and_seq() { assert_eq!(*host.domain_keylets_asked.borrow(), vec![(account, 5)]); } +/// An account-only keylet: the account reaches the host and the answered bytes land +/// where the guest asked, with no scalar in the shape. +#[test] +fn signers_id_reads_the_account() { + let account = vec![0u8; 20]; + let host = + FakeHost::new().answering_signer_list_keylet(account.clone(), support::Answer::filler(32)); + + let wat = module( + &[import::SIGNERS_ID, ONE_PAGE], + "(call $signers_id (i32.const 0) (i32.const 20) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!(*host.signer_list_keylets_asked.borrow(), vec![account]); +} + /// 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 1542caf3c1..a05ce0f41f 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; 38] = [ +const ALL_IMPORTS: [&str; 39] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -134,6 +134,7 @@ const ALL_IMPORTS: [&str; 38] = [ import::ORACLE_ID, import::PAYCHAN_ID, import::PERMISSIONED_DOMAIN_ID, + import::SIGNERS_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 9f7c632b7e..0d48cb25aa 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -259,6 +259,11 @@ pub struct FakeHost { pub domain_keylets: HashMap<(Vec, i32), Answer>, /// Every (account, seq) `permissioned_domain_keylet` was asked for. pub domain_keylets_asked: RefCell, i32)>>, + /// What `signer_list_keylet` answers, by account bytes. An unlisted account + /// answers `InvalidAccount`. + pub signer_list_keylets: HashMap, Answer>, + /// Every account `signer_list_keylet` was asked for. + pub signer_list_keylets_asked: RefCell>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -345,6 +350,8 @@ impl Default for FakeHost { paychannel_keylets_asked: RefCell::new(Vec::new()), domain_keylets: HashMap::new(), domain_keylets_asked: RefCell::new(Vec::new()), + signer_list_keylets: HashMap::new(), + signer_list_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -620,6 +627,11 @@ impl FakeHost { self } + pub fn answering_signer_list_keylet(mut self, account: Vec, answer: Answer) -> FakeHost { + self.signer_list_keylets.insert(account, answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -974,6 +986,16 @@ impl HostFunctions for FakeHost { } } + fn signer_list_keylet(&self, account: &[u8], out: &mut [u8]) -> HostResult { + self.signer_list_keylets_asked + .borrow_mut() + .push(account.to_vec()); + match self.signer_list_keylets.get(account) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -1050,6 +1072,7 @@ pub mod import { pub const ORACLE_ID: &str = r#"(import "host_lib" "oracle_id" (func $oracle_id (param i32 i32 i32 i32 i32) (result i32)))"#; pub const PAYCHAN_ID: &str = r#"(import "host_lib" "paychan_id" (func $paychan_id (param i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const PERMISSIONED_DOMAIN_ID: &str = r#"(import "host_lib" "permissioned_domain_id" (func $permissioned_domain_id (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const SIGNERS_ID: &str = r#"(import "host_lib" "signers_id" (func $signers_id (param i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index ef8028ea17..8adf9d4622 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -243,6 +243,11 @@ public: std::int32_t seq, rust::Slice out) const noexcept; + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + signerListKeylet(rust::Slice account, rust::Slice out) + const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index b4f90c839b..e78b02d053 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -766,6 +766,23 @@ HostContext::permissionedDomainKeylet( }); } +std::int32_t +HostContext::signerListKeylet( + rust::Slice account, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.signerListKeylet(AccountID::fromVoid(account.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 7e7014c7ca67adf41dcf70bda6acfe95548a840a Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 21:45:31 -0400 Subject: [PATCH 35/46] feat: Hook up ticket_id host function --- crates/xrpl-host-functions/src/lib.rs | 7 +++++ .../tests/generated_abi.rs | 15 +++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 ++++++ crates/xrpl-wasm-vm/src/abi.rs | 3 +++ crates/xrpl-wasm-vm/src/register.rs | 19 +++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 15 +++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 27 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 8 ++++++ src/libxrpl/tx/wasm/HostContext.cpp | 20 ++++++++++++++ 11 files changed, 129 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 0c85ed7944..8954a91e64 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -392,6 +392,13 @@ host_functions! { #[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. + #[gas = 350] + #[wasm_name = "ticket_id"] + fn ticket_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index bce0dbe243..a3df0f5c56 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -368,6 +368,14 @@ impl HostFunctions for FakeHost { put(out, &[account[0]; HASH_LEN]) } + /// The same account-and-sequence shape, for a `Ticket`. + fn ticket_keylet(&self, account: &[u8], _seq: i32, out: &mut [u8]) -> HostResult { + if account.is_empty() { + return Err(HostError::InvalidAccount); + } + put(out, &[account[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -583,6 +591,12 @@ fn the_trait_is_implementable() { host.signer_list_keylet(&[], &mut out), Err(HostError::InvalidAccount) ); + assert_eq!(host.ticket_keylet(&[7; 20], 5, &mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 7); + assert_eq!( + host.ticket_keylet(&[], 5, &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -686,6 +700,7 @@ fn the_spec_table_matches_the_declarations() { ("paychan_id", 350), ("permissioned_domain_id", 350), ("signers_id", 350), + ("ticket_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 1326364176..7d1d7ace21 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -371,6 +371,10 @@ mod ffi { #[cxx_name = "signerListKeylet"] fn signer_list_keylet(self: &HostContext, account: &[u8], out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "ticketKeylet"] + fn ticket_keylet(self: &HostContext, account: &[u8], seq: i32, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -630,6 +634,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.signer_list_keylet(account, out)) } + fn ticket_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.ticket_keylet(account, seq, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 1a25377a03..ca2bce12e6 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -369,6 +369,9 @@ mod tests { fn signer_list_keylet(&self, _account: &[u8], _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn ticket_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 265563c46d..55734898f5 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -688,6 +688,25 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::TicketKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + seq: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::TicketKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + write_buffered(c, out, |host, data, buf| { + host.ticket_keylet(account.read(data)?, seq, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 07db191a31..35379440ab 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -229,6 +229,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $signers_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 32))", 4, ), + HostFunctionSpec::TicketKeylet => ( + import::TICKET_ID, + "(call $ticket_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", + 5, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 026c830bdc..105a483d71 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -718,6 +718,21 @@ fn signers_id_reads_the_account() { assert_eq!(*host.signer_list_keylets_asked.borrow(), vec![account]); } +/// Another account-and-sequence keylet, with its own answer set, for a ticket. +#[test] +fn ticket_id_reads_the_account_and_seq() { + let account = vec![0u8; 20]; + let host = + FakeHost::new().answering_ticket_keylet(account.clone(), 5, support::Answer::filler(32)); + + let wat = module( + &[import::TICKET_ID, ONE_PAGE], + "(call $ticket_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!(*host.ticket_keylets_asked.borrow(), vec![(account, 5)]); +} + /// 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 a05ce0f41f..450436dd52 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; 39] = [ +const ALL_IMPORTS: [&str; 40] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -135,6 +135,7 @@ const ALL_IMPORTS: [&str; 39] = [ import::PAYCHAN_ID, import::PERMISSIONED_DOMAIN_ID, import::SIGNERS_ID, + import::TICKET_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 0d48cb25aa..c8d09d923b 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -264,6 +264,11 @@ pub struct FakeHost { pub signer_list_keylets: HashMap, Answer>, /// Every account `signer_list_keylet` was asked for. pub signer_list_keylets_asked: RefCell>>, + /// What `ticket_keylet` answers, by (account bytes, seq). An unlisted key answers + /// `InvalidAccount`. + pub ticket_keylets: HashMap<(Vec, i32), Answer>, + /// Every (account, seq) `ticket_keylet` was asked for. + pub ticket_keylets_asked: RefCell, i32)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -352,6 +357,8 @@ impl Default for FakeHost { domain_keylets_asked: RefCell::new(Vec::new()), signer_list_keylets: HashMap::new(), signer_list_keylets_asked: RefCell::new(Vec::new()), + ticket_keylets: HashMap::new(), + ticket_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -632,6 +639,16 @@ impl FakeHost { self } + pub fn answering_ticket_keylet( + mut self, + account: Vec, + seq: i32, + answer: Answer, + ) -> FakeHost { + self.ticket_keylets.insert((account, seq), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -996,6 +1013,15 @@ impl HostFunctions for FakeHost { } } + fn ticket_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult { + let key = (account.to_vec(), seq); + self.ticket_keylets_asked.borrow_mut().push(key.clone()); + match self.ticket_keylets.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -1073,6 +1099,7 @@ pub mod import { pub const PAYCHAN_ID: &str = r#"(import "host_lib" "paychan_id" (func $paychan_id (param i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const PERMISSIONED_DOMAIN_ID: &str = r#"(import "host_lib" "permissioned_domain_id" (func $permissioned_domain_id (param i32 i32 i32 i32 i32) (result i32)))"#; pub const SIGNERS_ID: &str = r#"(import "host_lib" "signers_id" (func $signers_id (param i32 i32 i32 i32) (result i32)))"#; + pub const TICKET_ID: &str = r#"(import "host_lib" "ticket_id" (func $ticket_id (param i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 8adf9d4622..d2d4120935 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -248,6 +248,14 @@ public: signerListKeylet(rust::Slice account, rust::Slice out) const noexcept; + // The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's + // u32 as its i32 bit pattern. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + ticketKeylet( + rust::Slice account, + std::int32_t seq, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index e78b02d053..0a4fe25a83 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -783,6 +783,26 @@ HostContext::signerListKeylet( }); } +std::int32_t +HostContext::ticketKeylet( + rust::Slice account, + std::int32_t seq, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + // The guest's u32 seq arrives as its i32 bit pattern; recover it. + auto const value = hostFunctions_.ticketKeylet( + AccountID::fromVoid(account.data()), static_cast(seq)); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 98abdef208441a3af710dd2007bcd9923d16409f Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 21:49:46 -0400 Subject: [PATCH 36/46] feat: Hook up vault_id host function --- crates/xrpl-host-functions/src/lib.rs | 7 +++++ .../tests/generated_abi.rs | 15 +++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 8 ++++++ crates/xrpl-wasm-vm/src/abi.rs | 3 +++ crates/xrpl-wasm-vm/src/register.rs | 19 +++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 5 ++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 15 +++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 3 ++- crates/xrpl-wasm-vm/tests/support/mod.rs | 27 +++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 8 ++++++ src/libxrpl/tx/wasm/HostContext.cpp | 20 ++++++++++++++ 11 files changed, 129 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 8954a91e64..7f51efd52c 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -399,6 +399,13 @@ host_functions! { #[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; + /// The XRPL `sha512Half` of `data`: the first [`HASH_LEN`] bytes of its SHA-512. #[gas = 2000] #[wasm_name = "sha512_half"] diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index a3df0f5c56..4bf9c7dddb 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -376,6 +376,14 @@ impl HostFunctions for FakeHost { put(out, &[account[0]; HASH_LEN]) } + /// The same account-and-sequence shape, for a `Vault`. + fn vault_keylet(&self, account: &[u8], _seq: i32, out: &mut [u8]) -> HostResult { + if account.is_empty() { + return Err(HostError::InvalidAccount); + } + put(out, &[account[0]; HASH_LEN]) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { let mut digest = [0; HASH_LEN]; digest[0] = data.len() as u8; @@ -597,6 +605,12 @@ fn the_trait_is_implementable() { host.ticket_keylet(&[], 5, &mut out), Err(HostError::InvalidAccount) ); + assert_eq!(host.vault_keylet(&[7; 20], 5, &mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 7); + assert_eq!( + host.vault_keylet(&[], 5, &mut out), + Err(HostError::InvalidAccount) + ); assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", true), Ok(())); @@ -701,6 +715,7 @@ fn the_spec_table_matches_the_declarations() { ("permissioned_domain_id", 350), ("signers_id", 350), ("ticket_id", 350), + ("vault_id", 350), ("sha512_half", 2000), ("trace", 500), ("trace_num", 500), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 7d1d7ace21..5eac97b36b 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -375,6 +375,10 @@ mod ffi { #[cxx_name = "ticketKeylet"] fn ticket_keylet(self: &HostContext, account: &[u8], seq: i32, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] + #[cxx_name = "vaultKeylet"] + fn vault_keylet(self: &HostContext, account: &[u8], seq: i32, out: &mut [u8]) -> i32; + #[namespace = "xrpl"] #[cxx_name = "sha512Half"] fn sha512_half(self: &HostContext, data: &[u8], out: &mut [u8]) -> i32; @@ -638,6 +642,10 @@ impl HostFunctions for CxxHost<'_> { bytes_written(self.ctx.ticket_keylet(account, seq, out)) } + fn vault_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.vault_keylet(account, seq, out)) + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { bytes_written(self.ctx.sha512_half(data, out)) } diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index ca2bce12e6..2ecdbeb816 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -372,6 +372,9 @@ mod tests { fn ticket_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn vault_keylet(&self, _account: &[u8], _seq: i32, _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } fn sha512_half(&self, _data: &[u8], _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 55734898f5..aaea45ac46 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -707,6 +707,25 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::VaultKeylet => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + seq: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::VaultKeylet, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + write_buffered(c, out, |host, data, buf| { + host.vault_keylet(account.read(data)?, seq, buf) + }) + }) + }, + ), HostFunctionSpec::Sha512Half => 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 35379440ab..a7a38f1e15 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -234,6 +234,11 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $ticket_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", 5, ), + HostFunctionSpec::VaultKeylet => ( + import::VAULT_ID, + "(call $vault_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", + 5, + ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, "(call $sha512_half (i32.const 0) (i32.const 4) (i32.const 0) (i32.const 32))", diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 105a483d71..d8c2fe1b16 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -733,6 +733,21 @@ fn ticket_id_reads_the_account_and_seq() { assert_eq!(*host.ticket_keylets_asked.borrow(), vec![(account, 5)]); } +/// The last account-and-sequence keylet, with its own answer set, for a vault. +#[test] +fn vault_id_reads_the_account_and_seq() { + let account = vec![0u8; 20]; + let host = + FakeHost::new().answering_vault_keylet(account.clone(), 5, support::Answer::filler(32)); + + let wat = module( + &[import::VAULT_ID, ONE_PAGE], + "(call $vault_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); + assert_eq!(*host.vault_keylets_asked.borrow(), vec![(account, 5)]); +} + /// 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 450436dd52..912b0eed7e 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; 40] = [ +const ALL_IMPORTS: [&str; 41] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -136,6 +136,7 @@ const ALL_IMPORTS: [&str; 40] = [ import::PERMISSIONED_DOMAIN_ID, import::SIGNERS_ID, import::TICKET_ID, + import::VAULT_ID, import::SHA512_HALF, import::TRACE, import::TRACE_NUM, diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index c8d09d923b..861019940b 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -269,6 +269,11 @@ pub struct FakeHost { pub ticket_keylets: HashMap<(Vec, i32), Answer>, /// Every (account, seq) `ticket_keylet` was asked for. pub ticket_keylets_asked: RefCell, i32)>>, + /// What `vault_keylet` answers, by (account bytes, seq). An unlisted key answers + /// `InvalidAccount`. + pub vault_keylets: HashMap<(Vec, i32), Answer>, + /// Every (account, seq) `vault_keylet` was asked for. + pub vault_keylets_asked: RefCell, i32)>>, /// What `sha512_half` answers, whatever it is given. pub digest: Answer, /// Every field selector `get_current_ledger_obj_field` was asked for. @@ -359,6 +364,8 @@ impl Default for FakeHost { signer_list_keylets_asked: RefCell::new(Vec::new()), ticket_keylets: HashMap::new(), ticket_keylets_asked: RefCell::new(Vec::new()), + vault_keylets: HashMap::new(), + vault_keylets_asked: RefCell::new(Vec::new()), digest: Answer::filler(32), fields_asked: RefCell::new(Vec::new()), digested: RefCell::new(Vec::new()), @@ -649,6 +656,16 @@ impl FakeHost { self } + pub fn answering_vault_keylet( + mut self, + account: Vec, + seq: i32, + answer: Answer, + ) -> FakeHost { + self.vault_keylets.insert((account, seq), answer); + self + } + pub fn answering_digest(mut self, answer: Answer) -> FakeHost { self.digest = answer; self @@ -1022,6 +1039,15 @@ impl HostFunctions for FakeHost { } } + fn vault_keylet(&self, account: &[u8], seq: i32, out: &mut [u8]) -> HostResult { + let key = (account.to_vec(), seq); + self.vault_keylets_asked.borrow_mut().push(key.clone()); + match self.vault_keylets.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidAccount), + } + } + fn sha512_half(&self, data: &[u8], out: &mut [u8]) -> HostResult { self.digested.borrow_mut().push(data.to_vec()); self.digest.fill(out) @@ -1100,6 +1126,7 @@ pub mod import { pub const PERMISSIONED_DOMAIN_ID: &str = r#"(import "host_lib" "permissioned_domain_id" (func $permissioned_domain_id (param i32 i32 i32 i32 i32) (result i32)))"#; pub const SIGNERS_ID: &str = r#"(import "host_lib" "signers_id" (func $signers_id (param i32 i32 i32 i32) (result i32)))"#; pub const TICKET_ID: &str = r#"(import "host_lib" "ticket_id" (func $ticket_id (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const VAULT_ID: &str = r#"(import "host_lib" "vault_id" (func $vault_id (param i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index d2d4120935..8b82c83c9a 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -256,6 +256,14 @@ public: std::int32_t seq, rust::Slice out) const noexcept; + // The account id must be 20 bytes, else `InvalidParams`. `seq` carries the guest's + // u32 as its i32 bit pattern. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + vaultKeylet( + rust::Slice account, + std::int32_t seq, + rust::Slice out) const noexcept; + [[nodiscard]] std::int32_t sha512Half(rust::Slice data, rust::Slice out) const noexcept; diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 0a4fe25a83..5291fed8f2 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -803,6 +803,26 @@ HostContext::ticketKeylet( }); } +std::int32_t +HostContext::vaultKeylet( + rust::Slice account, + std::int32_t seq, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + // The guest's u32 seq arrives as its i32 bit pattern; recover it. + auto const value = hostFunctions_.vaultKeylet( + AccountID::fromVoid(account.data()), static_cast(seq)); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + std::int32_t HostContext::sha512Half(rust::Slice data, rust::Slice out) const noexcept From 98cf3a05328b60107412ef4ef8d2b04014c095f4 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 21:58:24 -0400 Subject: [PATCH 37/46] 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 From 7d52867e3c94cfe4e4e70ef7db96e16c6d91ed73 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Tue, 11 Aug 2026 09:44:00 -0400 Subject: [PATCH 38/46] feat: Hook up nft_uri, nft_issuer, nft_taxon, nft_flags, nft_xfer_fee, nft_serial host functions --- crates/xrpl-host-functions/src/lib.rs | 36 +++++ .../tests/generated_abi.rs | 72 ++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 48 +++++++ crates/xrpl-wasm-vm/src/abi.rs | 18 +++ crates/xrpl-wasm-vm/src/register.rs | 103 +++++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 30 +++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 99 ++++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 8 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 125 ++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 36 +++++ src/libxrpl/tx/wasm/HostContext.cpp | 97 ++++++++++++++ 11 files changed, 671 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index c44b4add21..36fe13b7ca 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -427,4 +427,40 @@ host_functions! { #[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. + #[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. + #[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. + #[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. + #[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. + #[gas = 60] + #[wasm_name = "nft_serial"] + fn get_nft_sequence(&self, nft_id: &[u8], 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 db520485c3..543857139d 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -406,6 +406,55 @@ impl HostFunctions for FakeHost { fn update_data(&self, data: &[u8]) -> HostResult { Ok(data.len() as i32) } + + /// Reads an account and an nft id, writes a byte value; `InvalidParams` if either + /// is empty. + fn get_nft(&self, account: &[u8], nft_id: &[u8], out: &mut [u8]) -> HostResult { + if account.is_empty() || nft_id.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[account[0]; HASH_LEN]) + } + + /// Reads an nft id, writes a byte value; `InvalidParams` on an empty id. + fn get_nft_issuer(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + if nft_id.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[nft_id[0]; HASH_LEN]) + } + + /// The same, for the taxon. + fn get_nft_taxon(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + if nft_id.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &nft_id[0].to_le_bytes()) + } + + /// Reads an nft id and returns a scalar; `InvalidParams` on an empty id. + fn get_nft_flags(&self, nft_id: &[u8]) -> HostResult { + if nft_id.is_empty() { + return Err(HostError::InvalidParams); + } + Ok(i32::from(nft_id[0])) + } + + /// The same, for the transfer fee. + fn get_nft_transfer_fee(&self, nft_id: &[u8]) -> HostResult { + if nft_id.is_empty() { + return Err(HostError::InvalidParams); + } + Ok(i32::from(nft_id[0])) + } + + /// The same byte-output shape, for the sequence number. + fn get_nft_sequence(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + if nft_id.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &nft_id[0].to_le_bytes()) + } } #[test] @@ -621,6 +670,23 @@ fn the_trait_is_implementable() { 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.get_nft(&[7; 20], &[9; 32], &mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 7); + assert_eq!( + host.get_nft(&[], &[9; 32], &mut out), + Err(HostError::InvalidParams) + ); + assert_eq!(host.get_nft_issuer(&[9; 32], &mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 9); + assert_eq!( + host.get_nft_issuer(&[], &mut out), + Err(HostError::InvalidParams) + ); + assert_eq!(host.get_nft_taxon(&[9; 32], &mut out), Ok(1)); + assert_eq!(host.get_nft_flags(&[9; 32]), Ok(9)); + 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.traced.borrow(), ["hello/2/true", "count=-1"]); } @@ -726,6 +792,12 @@ fn the_spec_table_matches_the_declarations() { ("trace", 500), ("trace_num", 500), ("set_data", 1000), + ("nft_uri", 5000), + ("nft_issuer", 70), + ("nft_taxon", 60), + ("nft_flags", 60), + ("nft_xfer_fee", 60), + ("nft_serial", 60), ] ); } diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 3e1a766816..0ed1ad1053 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -395,6 +395,30 @@ mod ffi { #[namespace = "xrpl"] #[cxx_name = "updateData"] fn update_data(self: &HostContext, data: &[u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "getNFT"] + fn get_nft(self: &HostContext, account: &[u8], nft_id: &[u8], out: &mut [u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "getNFTIssuer"] + fn get_nft_issuer(self: &HostContext, nft_id: &[u8], out: &mut [u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "getNFTTaxon"] + fn get_nft_taxon(self: &HostContext, nft_id: &[u8], out: &mut [u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "getNFTFlags"] + fn get_nft_flags(self: &HostContext, nft_id: &[u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "getNFTTransferFee"] + fn get_nft_transfer_fee(self: &HostContext, nft_id: &[u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "getNFTSequence"] + fn get_nft_sequence(self: &HostContext, nft_id: &[u8], out: &mut [u8]) -> i32; } } @@ -665,6 +689,30 @@ impl HostFunctions for CxxHost<'_> { fn update_data(&self, data: &[u8]) -> HostResult { scalar(self.ctx.update_data(data)) } + + fn get_nft(&self, account: &[u8], nft_id: &[u8], out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.get_nft(account, nft_id, out)) + } + + fn get_nft_issuer(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.get_nft_issuer(nft_id, out)) + } + + fn get_nft_taxon(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.get_nft_taxon(nft_id, out)) + } + + fn get_nft_flags(&self, nft_id: &[u8]) -> HostResult { + scalar(self.ctx.get_nft_flags(nft_id)) + } + + fn get_nft_transfer_fee(&self, nft_id: &[u8]) -> HostResult { + scalar(self.ctx.get_nft_transfer_fee(nft_id)) + } + + fn get_nft_sequence(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.get_nft_sequence(nft_id, out)) + } } fn run_escrow( diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index f009278ba5..85a83d3e32 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -387,6 +387,24 @@ mod tests { fn update_data(&self, _data: &[u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_nft(&self, _account: &[u8], _nft_id: &[u8], _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn get_nft_issuer(&self, _nft_id: &[u8], _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn get_nft_taxon(&self, _nft_id: &[u8], _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn get_nft_flags(&self, _nft_id: &[u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn get_nft_transfer_fee(&self, _nft_id: &[u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn get_nft_sequence(&self, _nft_id: &[u8], _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 1919117b20..e40a47e7bc 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -795,6 +795,109 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::GetNft => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + nft_ptr: i32, + nft_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetNft, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + let nft_id = Region::new(nft_ptr, nft_len); + write_buffered(c, out, |host, data, buf| { + host.get_nft(account.read(data)?, nft_id.read(data)?, buf) + }) + }) + }, + ), + HostFunctionSpec::GetNftIssuer => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + nft_ptr: i32, + nft_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetNftIssuer, |c| { + let out = Region::new(out_ptr, out_len); + let nft_id = Region::new(nft_ptr, nft_len); + write_buffered(c, out, |host, data, buf| { + host.get_nft_issuer(nft_id.read(data)?, buf) + }) + }) + }, + ), + HostFunctionSpec::GetNftTaxon => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + nft_ptr: i32, + nft_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetNftTaxon, |c| { + let out = Region::new(out_ptr, out_len); + let nft_id = Region::new(nft_ptr, nft_len); + write_buffered(c, out, |host, data, buf| { + host.get_nft_taxon(nft_id.read(data)?, buf) + }) + }) + }, + ), + HostFunctionSpec::GetNftFlags => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + nft_ptr: i32, + nft_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetNftFlags, |c| { + let host = c.data().host; + let nft_id = read_borrowed(c, Region::new(nft_ptr, nft_len))?; + host.get_nft_flags(nft_id) + }) + }, + ), + HostFunctionSpec::GetNftTransferFee => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + nft_ptr: i32, + nft_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetNftTransferFee, |c| { + let host = c.data().host; + let nft_id = read_borrowed(c, Region::new(nft_ptr, nft_len))?; + host.get_nft_transfer_fee(nft_id) + }) + }, + ), + HostFunctionSpec::GetNftSequence => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + nft_ptr: i32, + nft_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetNftSequence, |c| { + let out = Region::new(out_ptr, out_len); + let nft_id = Region::new(nft_ptr, nft_len); + write_buffered(c, out, |host, data, buf| { + host.get_nft_sequence(nft_id.read(data)?, buf) + }) + }) + }, + ), }?; } Ok(()) diff --git a/crates/xrpl-wasm-vm/tests/budgets.rs b/crates/xrpl-wasm-vm/tests/budgets.rs index 5faf35a512..78908b904f 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -259,6 +259,36 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $set_data (i32.const 0) (i32.const 8))", 2, ), + HostFunctionSpec::GetNft => ( + import::NFT_URI, + "(call $nft_uri (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 32) (i32.const 52) (i32.const 12))", + 6, + ), + HostFunctionSpec::GetNftIssuer => ( + import::NFT_ISSUER, + "(call $nft_issuer (i32.const 0) (i32.const 32) (i32.const 32) (i32.const 20))", + 4, + ), + HostFunctionSpec::GetNftTaxon => ( + import::NFT_TAXON, + "(call $nft_taxon (i32.const 0) (i32.const 32) (i32.const 32) (i32.const 4))", + 4, + ), + HostFunctionSpec::GetNftFlags => ( + import::NFT_FLAGS, + "(call $nft_flags (i32.const 0) (i32.const 32))", + 2, + ), + HostFunctionSpec::GetNftTransferFee => ( + import::NFT_XFER_FEE, + "(call $nft_xfer_fee (i32.const 0) (i32.const 32))", + 2, + ), + HostFunctionSpec::GetNftSequence => ( + import::NFT_SERIAL, + "(call $nft_serial (i32.const 0) (i32.const 32) (i32.const 32) (i32.const 4))", + 4, + ), }; Call { import, diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 3d9415d87e..a138aea120 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -767,6 +767,105 @@ fn set_data_passes_the_data_through_and_returns_the_count() { ); } +/// A getter that reads two input regions — an account and an nft id — and writes the +/// answer to a third: both inputs reach the host, keyed together, and the bytes it +/// answers land where the guest asked. +#[test] +fn nft_uri_reads_the_account_and_id_and_writes_the_uri() { + let account = vec![0u8; 20]; + let nft_id = vec![0u8; 32]; + let host = FakeHost::new().answering_get_nft( + account.clone(), + nft_id.clone(), + support::Answer::bytes([0xab, 0xcd, 0xef]), + ); + + let wat = module( + &[import::NFT_URI, ONE_PAGE], + "(call $nft_uri (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 32) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 3, "the uri length"); + assert_eq!(*host.nfts_asked.borrow(), vec![(account, nft_id)]); +} + +/// A single-input byte getter: the nft id reaches the host and the issuer bytes it +/// answers land where the guest asked. +#[test] +fn nft_issuer_reads_the_id_and_writes_the_issuer() { + let nft_id = vec![0u8; 32]; + let host = FakeHost::new().answering_nft_issuer(nft_id.clone(), support::Answer::filler(20)); + + let wat = module( + &[import::NFT_ISSUER, ONE_PAGE], + "(call $nft_issuer (i32.const 0) (i32.const 32) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 20, "the issuer length"); + assert_eq!(*host.nft_issuers_asked.borrow(), vec![nft_id]); +} + +/// A u32-valued getter whose four bytes the host writes to the output region: the id +/// reaches the host, and the little-endian bytes land where the guest asked. +#[test] +fn nft_taxon_reads_the_id_and_writes_four_bytes() { + let nft_id = vec![0u8; 32]; + let host = + FakeHost::new().answering_nft_taxon(nft_id.clone(), support::Answer::bytes([7, 0, 0, 0])); + + let wat = module( + &[import::NFT_TAXON, ONE_PAGE], + "(drop (call $nft_taxon (i32.const 0) (i32.const 32) (i32.const 64) (i32.const 4))) + (i32.load (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 7, "the taxon the host wrote"); + assert_eq!(*host.nft_taxons_asked.borrow(), vec![nft_id]); +} + +/// A single-input scalar getter: the nft id reaches the host and the flags it reports +/// come back as the call's status, no output region involved. +#[test] +fn nft_flags_reads_the_id_and_returns_the_flags() { + let nft_id = vec![0u8; 32]; + let host = FakeHost::new().answering_nft_flags(Ok(11)); + + let wat = module( + &[import::NFT_FLAGS, ONE_PAGE], + "(call $nft_flags (i32.const 0) (i32.const 32))", + ); + assert_eq!(status(&wat, &host), 11, "the flags the host reported"); + assert_eq!(*host.nft_flags_asked.borrow(), vec![nft_id]); +} + +/// A second scalar getter, to pin the pattern: the transfer fee comes back as the +/// status. +#[test] +fn nft_xfer_fee_reads_the_id_and_returns_the_fee() { + let nft_id = vec![0u8; 32]; + let host = FakeHost::new().answering_nft_transfer_fee(Ok(314)); + + let wat = module( + &[import::NFT_XFER_FEE, ONE_PAGE], + "(call $nft_xfer_fee (i32.const 0) (i32.const 32))", + ); + assert_eq!(status(&wat, &host), 314, "the fee the host reported"); + assert_eq!(*host.nft_fee_asked.borrow(), vec![nft_id]); +} + +/// The last NFT getter, a u32 sequence written to the output region. +#[test] +fn nft_serial_reads_the_id_and_writes_four_bytes() { + let nft_id = vec![0u8; 32]; + let host = FakeHost::new() + .answering_nft_sequence(nft_id.clone(), support::Answer::bytes([42, 0, 0, 0])); + + let wat = module( + &[import::NFT_SERIAL, ONE_PAGE], + "(drop (call $nft_serial (i32.const 0) (i32.const 32) (i32.const 64) (i32.const 4))) + (i32.load (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 42, "the sequence the host wrote"); + assert_eq!(*host.nft_sequences_asked.borrow(), vec![nft_id]); +} + /// 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 056f8066a6..a3932e434d 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; 42] = [ +const ALL_IMPORTS: [&str; 48] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -141,6 +141,12 @@ const ALL_IMPORTS: [&str; 42] = [ import::TRACE, import::TRACE_NUM, import::SET_DATA, + import::NFT_URI, + import::NFT_ISSUER, + import::NFT_TAXON, + import::NFT_FLAGS, + import::NFT_XFER_FEE, + import::NFT_SERIAL, ]; #[test] diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 09f81e2b35..73de5a4d61 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -286,6 +286,32 @@ pub struct FakeHost { pub update_data_answer: HostResult, /// Every data blob `update_data` was given. pub update_data_asked: RefCell>>, + /// What `get_nft` answers, by (account, nft id) bytes. An unlisted key answers + /// `InvalidParams`. + pub nfts: HashMap<(Vec, Vec), Answer>, + /// Every (account, nft id) `get_nft` was asked for. + pub nfts_asked: RefCell, Vec)>>, + /// What `get_nft_issuer` answers, by nft id. An unlisted id answers `InvalidParams`. + pub nft_issuers: HashMap, Answer>, + /// Every nft id `get_nft_issuer` was asked for. + pub nft_issuers_asked: RefCell>>, + /// What `get_nft_taxon` answers, by nft id. An unlisted id answers `InvalidParams`. + pub nft_taxons: HashMap, Answer>, + /// Every nft id `get_nft_taxon` was asked for. + pub nft_taxons_asked: RefCell>>, + /// What `get_nft_flags` answers, whatever nft id it is given. + pub nft_flags_answer: HostResult, + /// Every nft id `get_nft_flags` was asked for. + pub nft_flags_asked: RefCell>>, + /// What `get_nft_transfer_fee` answers, whatever nft id it is given. + pub nft_fee_answer: HostResult, + /// Every nft id `get_nft_transfer_fee` was asked for. + pub nft_fee_asked: RefCell>>, + /// What `get_nft_sequence` answers, by nft id. An unlisted id answers + /// `InvalidParams`. + pub nft_sequences: HashMap, Answer>, + /// Every nft id `get_nft_sequence` was asked for. + pub nft_sequences_asked: RefCell>>, } impl Default for FakeHost { @@ -376,6 +402,18 @@ impl Default for FakeHost { traces: RefCell::new(Vec::new()), update_data_answer: Ok(0), update_data_asked: RefCell::new(Vec::new()), + nfts: HashMap::new(), + nfts_asked: RefCell::new(Vec::new()), + nft_issuers: HashMap::new(), + nft_issuers_asked: RefCell::new(Vec::new()), + nft_taxons: HashMap::new(), + nft_taxons_asked: RefCell::new(Vec::new()), + nft_flags_answer: Ok(0), + nft_flags_asked: RefCell::new(Vec::new()), + nft_fee_answer: Ok(0), + nft_fee_asked: RefCell::new(Vec::new()), + nft_sequences: HashMap::new(), + nft_sequences_asked: RefCell::new(Vec::new()), } } } @@ -682,6 +720,41 @@ impl FakeHost { self } + pub fn answering_get_nft( + mut self, + account: Vec, + nft_id: Vec, + answer: Answer, + ) -> FakeHost { + self.nfts.insert((account, nft_id), answer); + self + } + + pub fn answering_nft_issuer(mut self, nft_id: Vec, answer: Answer) -> FakeHost { + self.nft_issuers.insert(nft_id, answer); + self + } + + pub fn answering_nft_taxon(mut self, nft_id: Vec, answer: Answer) -> FakeHost { + self.nft_taxons.insert(nft_id, answer); + self + } + + pub fn answering_nft_flags(mut self, answer: HostResult) -> FakeHost { + self.nft_flags_answer = answer; + self + } + + pub fn answering_nft_transfer_fee(mut self, answer: HostResult) -> FakeHost { + self.nft_fee_answer = answer; + self + } + + pub fn answering_nft_sequence(mut self, nft_id: Vec, answer: Answer) -> FakeHost { + self.nft_sequences.insert(nft_id, answer); + self + } + pub fn traces(&self) -> Vec { self.traces.borrow().clone() } @@ -1085,6 +1158,49 @@ impl HostFunctions for FakeHost { self.update_data_asked.borrow_mut().push(data.to_vec()); self.update_data_answer } + + fn get_nft(&self, account: &[u8], nft_id: &[u8], out: &mut [u8]) -> HostResult { + let key = (account.to_vec(), nft_id.to_vec()); + self.nfts_asked.borrow_mut().push(key.clone()); + match self.nfts.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidParams), + } + } + + fn get_nft_issuer(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + self.nft_issuers_asked.borrow_mut().push(nft_id.to_vec()); + match self.nft_issuers.get(nft_id) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidParams), + } + } + + fn get_nft_taxon(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + self.nft_taxons_asked.borrow_mut().push(nft_id.to_vec()); + match self.nft_taxons.get(nft_id) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidParams), + } + } + + fn get_nft_flags(&self, nft_id: &[u8]) -> HostResult { + self.nft_flags_asked.borrow_mut().push(nft_id.to_vec()); + self.nft_flags_answer + } + + fn get_nft_transfer_fee(&self, nft_id: &[u8]) -> HostResult { + self.nft_fee_asked.borrow_mut().push(nft_id.to_vec()); + self.nft_fee_answer + } + + fn get_nft_sequence(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + self.nft_sequences_asked.borrow_mut().push(nft_id.to_vec()); + match self.nft_sequences.get(nft_id) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidParams), + } + } } // --------------------------------------------------------------------------- @@ -1150,6 +1266,15 @@ pub mod import { 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)))"#; + pub const NFT_URI: &str = r#"(import "host_lib" "nft_uri" (func $nft_uri (param i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const NFT_ISSUER: &str = r#"(import "host_lib" "nft_issuer" (func $nft_issuer (param i32 i32 i32 i32) (result i32)))"#; + pub const NFT_TAXON: &str = + r#"(import "host_lib" "nft_taxon" (func $nft_taxon (param i32 i32 i32 i32) (result i32)))"#; + pub const NFT_FLAGS: &str = + r#"(import "host_lib" "nft_flags" (func $nft_flags (param i32 i32) (result i32)))"#; + 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)))"#; } /// 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 14e7838bd1..fa44678ee0 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -278,6 +278,42 @@ public: // stored, or a negative `HostFunctionError` code. [[nodiscard]] std::int32_t updateData(rust::Slice data) const noexcept; + + // The account id must be 20 bytes and the nft id 32 bytes, else `InvalidParams`. + // Writes the token's URI bytes. + [[nodiscard]] std::int32_t + getNFT( + rust::Slice account, + rust::Slice nftId, + rust::Slice out) const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Writes the 20-byte issuer + // account encoded in the id. + [[nodiscard]] std::int32_t + getNFTIssuer(rust::Slice nftId, rust::Slice out) + const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Writes the taxon as its four + // little-endian bytes. + [[nodiscard]] std::int32_t + getNFTTaxon(rust::Slice nftId, rust::Slice out) + const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Returns the flags, or a + // negative `HostFunctionError` code. + [[nodiscard]] std::int32_t + getNFTFlags(rust::Slice nftId) const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Returns the transfer fee, or a + // negative `HostFunctionError` code. + [[nodiscard]] std::int32_t + getNFTTransferFee(rust::Slice nftId) const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Writes the sequence number as + // its four little-endian bytes. + [[nodiscard]] std::int32_t + getNFTSequence(rust::Slice nftId, rust::Slice out) + const noexcept; }; } // namespace xrpl diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 2aea22d671..3af6aa1071 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -874,4 +874,101 @@ HostContext::updateData(rust::Slice data) const noexcept }); } +std::int32_t +HostContext::getNFT( + rust::Slice account, + rust::Slice nftId, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size() || nftId.size() != uint256::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.getNFT( + AccountID::fromVoid(account.data()), uint256::fromVoid(nftId.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + +std::int32_t +HostContext::getNFTIssuer(rust::Slice nftId, rust::Slice out) + const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (nftId.size() != uint256::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.getNFTIssuer(uint256::fromVoid(nftId.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + +std::int32_t +HostContext::getNFTTaxon(rust::Slice nftId, rust::Slice out) + const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (nftId.size() != uint256::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.getNFTTaxon(uint256::fromVoid(nftId.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answerScalar(out, *value); + }); +} + +std::int32_t +HostContext::getNFTFlags(rust::Slice nftId) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (nftId.size() != uint256::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.getNFTFlags(uint256::fromVoid(nftId.data())); + if (!value) + return hfErrorToInt(value.error()); + + return *value; + }); +} + +std::int32_t +HostContext::getNFTTransferFee(rust::Slice nftId) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (nftId.size() != uint256::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.getNFTTransferFee(uint256::fromVoid(nftId.data())); + if (!value) + return hfErrorToInt(value.error()); + + return *value; + }); +} + +std::int32_t +HostContext::getNFTSequence(rust::Slice nftId, rust::Slice out) + const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (nftId.size() != uint256::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.getNFTSequence(uint256::fromVoid(nftId.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answerScalar(out, *value); + }); +} + } // namespace xrpl From 97f32869dfe040a50615ae5cfdce4e9282d2fd23 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Tue, 11 Aug 2026 10:10:41 -0400 Subject: [PATCH 39/46] 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 From 0749043d09c7e4f5a0897adff835188a59678620 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Tue, 11 Aug 2026 12:03:39 -0400 Subject: [PATCH 40/46] feat: Clean up a few errant host function signatures --- crates/xrpl-wasm-vm/src/abi.rs | 11 ++++ crates/xrpl-wasm-vm/src/register.rs | 82 ++++++++++++++++++------ crates/xrpl-wasm-vm/tests/budgets.rs | 40 ++++++------ crates/xrpl-wasm-vm/tests/host_calls.rs | 30 ++++++--- crates/xrpl-wasm-vm/tests/support/mod.rs | 20 +++--- 5 files changed, 122 insertions(+), 61 deletions(-) diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 3dba7f9785..9cb2a88788 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -78,6 +78,17 @@ pub(crate) fn read_borrowed<'a>( input.read(mem.data(caller)) } +/// Decode a guest `u32` argument — a keylet's sequence number or document id — from +/// its four little-endian bytes, carried on to the host as its `i32` bit pattern. +/// +/// 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`. +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)) +} + /// Service a call whose answer is bytes, written straight into the guest's output /// region. /// diff --git a/crates/xrpl-wasm-vm/src/register.rs b/crates/xrpl-wasm-vm/src/register.rs index c6397204be..34e9aa1212 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -1,4 +1,6 @@ -use crate::abi::{charged, read_borrowed, write_buffered, write_into, write_mant_exp}; +use crate::abi::{ + charged, read_borrowed, read_u32_arg, write_buffered, write_into, write_mant_exp, +}; use crate::region::Region; use crate::vm::VmState; use wasmi::{Caller, Linker}; @@ -369,15 +371,19 @@ pub(crate) fn register_host_functions( |mut caller: Caller<'_, VmState<'_>>, acc_ptr: i32, acc_len: i32, - seq: i32, + seq_ptr: i32, + seq_len: i32, out_ptr: i32, out_len: i32| -> Result { charged(&mut caller, HostFunctionSpec::CheckKeylet, |c| { let out = Region::new(out_ptr, out_len); let account = Region::new(acc_ptr, acc_len); + let seq = Region::new(seq_ptr, seq_len); write_buffered(c, out, |host, data, buf| { - host.check_keylet(account.read(data)?, seq, buf) + let account = account.read(data)?; + let seq = read_u32_arg(seq.read(data)?)?; + host.check_keylet(account, seq, buf) }) }) }, @@ -481,15 +487,19 @@ pub(crate) fn register_host_functions( |mut caller: Caller<'_, VmState<'_>>, acc_ptr: i32, acc_len: i32, - seq: i32, + seq_ptr: i32, + seq_len: i32, out_ptr: i32, out_len: i32| -> Result { charged(&mut caller, HostFunctionSpec::EscrowKeylet, |c| { let out = Region::new(out_ptr, out_len); let account = Region::new(acc_ptr, acc_len); + let seq = Region::new(seq_ptr, seq_len); write_buffered(c, out, |host, data, buf| { - host.escrow_keylet(account.read(data)?, seq, buf) + let account = account.read(data)?; + let seq = read_u32_arg(seq.read(data)?)?; + host.escrow_keylet(account, seq, buf) }) }) }, @@ -529,15 +539,19 @@ pub(crate) fn register_host_functions( |mut caller: Caller<'_, VmState<'_>>, acc_ptr: i32, acc_len: i32, - seq: i32, + seq_ptr: i32, + seq_len: i32, out_ptr: i32, out_len: i32| -> Result { charged(&mut caller, HostFunctionSpec::MptokenIssuanceKeylet, |c| { let out = Region::new(out_ptr, out_len); let issuer = Region::new(acc_ptr, acc_len); + let seq = Region::new(seq_ptr, seq_len); write_buffered(c, out, |host, data, buf| { - host.mptoken_issuance_keylet(issuer.read(data)?, seq, buf) + let issuer = issuer.read(data)?; + let seq = read_u32_arg(seq.read(data)?)?; + host.mptoken_issuance_keylet(issuer, seq, buf) }) }) }, @@ -569,15 +583,19 @@ pub(crate) fn register_host_functions( |mut caller: Caller<'_, VmState<'_>>, acc_ptr: i32, acc_len: i32, - seq: i32, + seq_ptr: i32, + seq_len: i32, out_ptr: i32, out_len: i32| -> Result { charged(&mut caller, HostFunctionSpec::NftokenOfferKeylet, |c| { let out = Region::new(out_ptr, out_len); let account = Region::new(acc_ptr, acc_len); + let seq = Region::new(seq_ptr, seq_len); write_buffered(c, out, |host, data, buf| { - host.nftoken_offer_keylet(account.read(data)?, seq, buf) + let account = account.read(data)?; + let seq = read_u32_arg(seq.read(data)?)?; + host.nftoken_offer_keylet(account, seq, buf) }) }) }, @@ -588,15 +606,19 @@ pub(crate) fn register_host_functions( |mut caller: Caller<'_, VmState<'_>>, acc_ptr: i32, acc_len: i32, - seq: i32, + seq_ptr: i32, + seq_len: i32, out_ptr: i32, out_len: i32| -> Result { charged(&mut caller, HostFunctionSpec::OfferKeylet, |c| { let out = Region::new(out_ptr, out_len); let account = Region::new(acc_ptr, acc_len); + let seq = Region::new(seq_ptr, seq_len); write_buffered(c, out, |host, data, buf| { - host.offer_keylet(account.read(data)?, seq, buf) + let account = account.read(data)?; + let seq = read_u32_arg(seq.read(data)?)?; + host.offer_keylet(account, seq, buf) }) }) }, @@ -607,15 +629,19 @@ pub(crate) fn register_host_functions( |mut caller: Caller<'_, VmState<'_>>, acc_ptr: i32, acc_len: i32, - doc_id: i32, + doc_ptr: i32, + doc_len: i32, out_ptr: i32, out_len: i32| -> Result { charged(&mut caller, HostFunctionSpec::OracleKeylet, |c| { let out = Region::new(out_ptr, out_len); let account = Region::new(acc_ptr, acc_len); + let doc_id = Region::new(doc_ptr, doc_len); write_buffered(c, out, |host, data, buf| { - host.oracle_keylet(account.read(data)?, doc_id, buf) + let account = account.read(data)?; + let doc_id = read_u32_arg(doc_id.read(data)?)?; + host.oracle_keylet(account, doc_id, buf) }) }) }, @@ -628,7 +654,8 @@ pub(crate) fn register_host_functions( acc_len: i32, dst_ptr: i32, dst_len: i32, - seq: i32, + seq_ptr: i32, + seq_len: i32, out_ptr: i32, out_len: i32| -> Result { @@ -636,11 +663,12 @@ pub(crate) fn register_host_functions( let out = Region::new(out_ptr, out_len); let account = Region::new(acc_ptr, acc_len); let destination = Region::new(dst_ptr, dst_len); + let seq = Region::new(seq_ptr, seq_len); write_buffered(c, out, |host, data, buf| { host.paychannel_keylet( account.read(data)?, destination.read(data)?, - seq, + read_u32_arg(seq.read(data)?)?, buf, ) }) @@ -653,7 +681,8 @@ pub(crate) fn register_host_functions( |mut caller: Caller<'_, VmState<'_>>, acc_ptr: i32, acc_len: i32, - seq: i32, + seq_ptr: i32, + seq_len: i32, out_ptr: i32, out_len: i32| -> Result { @@ -663,8 +692,11 @@ pub(crate) fn register_host_functions( |c| { let out = Region::new(out_ptr, out_len); let account = Region::new(acc_ptr, acc_len); + let seq = Region::new(seq_ptr, seq_len); write_buffered(c, out, |host, data, buf| { - host.permissioned_domain_keylet(account.read(data)?, seq, buf) + let account = account.read(data)?; + let seq = read_u32_arg(seq.read(data)?)?; + host.permissioned_domain_keylet(account, seq, buf) }) }, ) @@ -694,15 +726,19 @@ pub(crate) fn register_host_functions( |mut caller: Caller<'_, VmState<'_>>, acc_ptr: i32, acc_len: i32, - seq: i32, + seq_ptr: i32, + seq_len: i32, out_ptr: i32, out_len: i32| -> Result { charged(&mut caller, HostFunctionSpec::TicketKeylet, |c| { let out = Region::new(out_ptr, out_len); let account = Region::new(acc_ptr, acc_len); + let seq = Region::new(seq_ptr, seq_len); write_buffered(c, out, |host, data, buf| { - host.ticket_keylet(account.read(data)?, seq, buf) + let account = account.read(data)?; + let seq = read_u32_arg(seq.read(data)?)?; + host.ticket_keylet(account, seq, buf) }) }) }, @@ -713,15 +749,19 @@ pub(crate) fn register_host_functions( |mut caller: Caller<'_, VmState<'_>>, acc_ptr: i32, acc_len: i32, - seq: i32, + seq_ptr: i32, + seq_len: i32, out_ptr: i32, out_len: i32| -> Result { charged(&mut caller, HostFunctionSpec::VaultKeylet, |c| { let out = Region::new(out_ptr, out_len); let account = Region::new(acc_ptr, acc_len); + let seq = Region::new(seq_ptr, seq_len); write_buffered(c, out, |host, data, buf| { - host.vault_keylet(account.read(data)?, seq, buf) + let account = account.read(data)?; + let seq = read_u32_arg(seq.read(data)?)?; + host.vault_keylet(account, seq, buf) }) }) }, diff --git a/crates/xrpl-wasm-vm/tests/budgets.rs b/crates/xrpl-wasm-vm/tests/budgets.rs index d20eca02a1..baa5e51d41 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -156,8 +156,8 @@ fn call_for(op: HostFunctionSpec) -> Call { ), HostFunctionSpec::CheckKeylet => ( import::CHECK_ID, - "(call $check_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", - 5, + "(call $check_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))", + 6, ), HostFunctionSpec::CredentialKeylet => ( import::CREDENTIAL_ID, @@ -181,8 +181,8 @@ fn call_for(op: HostFunctionSpec) -> Call { ), HostFunctionSpec::EscrowKeylet => ( import::ESCROW_ID, - "(call $escrow_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", - 5, + "(call $escrow_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))", + 6, ), HostFunctionSpec::TrustLineKeylet => ( import::TRUSTLINE_ID, @@ -191,8 +191,8 @@ fn call_for(op: HostFunctionSpec) -> Call { ), HostFunctionSpec::MptokenIssuanceKeylet => ( import::MPT_ISSUANCE_ID, - "(call $mpt_issuance_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", - 5, + "(call $mpt_issuance_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))", + 6, ), HostFunctionSpec::MptokenKeylet => ( import::MPTOKEN_ID, @@ -201,28 +201,28 @@ fn call_for(op: HostFunctionSpec) -> Call { ), HostFunctionSpec::NftokenOfferKeylet => ( import::NFT_OFFER_ID, - "(call $nft_offer_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", - 5, + "(call $nft_offer_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))", + 6, ), HostFunctionSpec::OfferKeylet => ( import::OFFER_ID, - "(call $offer_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", - 5, + "(call $offer_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))", + 6, ), HostFunctionSpec::OracleKeylet => ( import::ORACLE_ID, - "(call $oracle_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", - 5, + "(call $oracle_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))", + 6, ), HostFunctionSpec::PaychannelKeylet => ( import::PAYCHAN_ID, - "(call $paychan_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 5) (i32.const 40) (i32.const 20))", - 7, + "(call $paychan_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 40) (i32.const 20))", + 8, ), HostFunctionSpec::PermissionedDomainKeylet => ( import::PERMISSIONED_DOMAIN_ID, - "(call $permissioned_domain_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", - 5, + "(call $permissioned_domain_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))", + 6, ), HostFunctionSpec::SignerListKeylet => ( import::SIGNERS_ID, @@ -231,13 +231,13 @@ fn call_for(op: HostFunctionSpec) -> Call { ), HostFunctionSpec::TicketKeylet => ( import::TICKET_ID, - "(call $ticket_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", - 5, + "(call $ticket_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))", + 6, ), HostFunctionSpec::VaultKeylet => ( import::VAULT_ID, - "(call $vault_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 32) (i32.const 32))", - 5, + "(call $vault_id (i32.const 0) (i32.const 20) (i32.const 0) (i32.const 4) (i32.const 32) (i32.const 32))", + 6, ), HostFunctionSpec::Sha512Half => ( import::SHA512_HALF, diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 5f5be107b8..eb84734d53 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -425,7 +425,8 @@ fn check_id_reads_the_account_and_seq_and_writes_the_keylet() { let wat = module( &[import::CHECK_ID, ONE_PAGE], - "(call $check_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + "(i32.store (i32.const 20) (i32.const 5)) + (call $check_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 4) (i32.const 64) (i32.const 64))", ); assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); assert_eq!(*host.check_keylets_asked.borrow(), vec![(account, 5)]); @@ -536,7 +537,8 @@ fn escrow_id_reads_the_account_and_seq_and_writes_the_keylet() { let wat = module( &[import::ESCROW_ID, ONE_PAGE], - "(call $escrow_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + "(i32.store (i32.const 20) (i32.const 5)) + (call $escrow_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 4) (i32.const 64) (i32.const 64))", ); assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); assert_eq!(*host.escrow_keylets_asked.borrow(), vec![(account, 5)]); @@ -583,7 +585,8 @@ fn mpt_issuance_id_reads_the_issuer_and_seq() { let wat = module( &[import::MPT_ISSUANCE_ID, ONE_PAGE], - "(call $mpt_issuance_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + "(i32.store (i32.const 20) (i32.const 5)) + (call $mpt_issuance_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 4) (i32.const 64) (i32.const 64))", ); assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); assert_eq!(*host.mpt_issuance_keylets_asked.borrow(), vec![(issuer, 5)]); @@ -621,7 +624,8 @@ fn nft_offer_id_reads_the_account_and_seq() { let wat = module( &[import::NFT_OFFER_ID, ONE_PAGE], - "(call $nft_offer_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + "(i32.store (i32.const 20) (i32.const 5)) + (call $nft_offer_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 4) (i32.const 64) (i32.const 64))", ); assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); assert_eq!(*host.nft_offer_keylets_asked.borrow(), vec![(account, 5)]); @@ -637,7 +641,8 @@ fn offer_id_reads_the_account_and_seq() { let wat = module( &[import::OFFER_ID, ONE_PAGE], - "(call $offer_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + "(i32.store (i32.const 20) (i32.const 5)) + (call $offer_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 4) (i32.const 64) (i32.const 64))", ); assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); assert_eq!(*host.offer_keylets_asked.borrow(), vec![(account, 5)]); @@ -653,7 +658,8 @@ fn oracle_id_reads_the_account_and_doc_id() { let wat = module( &[import::ORACLE_ID, ONE_PAGE], - "(call $oracle_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + "(i32.store (i32.const 20) (i32.const 5)) + (call $oracle_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 4) (i32.const 64) (i32.const 64))", ); assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); assert_eq!(*host.oracle_keylets_asked.borrow(), vec![(account, 5)]); @@ -674,7 +680,8 @@ fn paychan_id_reads_both_accounts_and_the_seq() { let wat = module( &[import::PAYCHAN_ID, ONE_PAGE], - "(call $paychan_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + "(i32.store (i32.const 24) (i32.const 5)) + (call $paychan_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 20) (i32.const 24) (i32.const 4) (i32.const 64) (i32.const 64))", ); assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); assert_eq!( @@ -696,7 +703,8 @@ fn permissioned_domain_id_reads_the_account_and_seq() { let wat = module( &[import::PERMISSIONED_DOMAIN_ID, ONE_PAGE], - "(call $permissioned_domain_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + "(i32.store (i32.const 20) (i32.const 5)) + (call $permissioned_domain_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 4) (i32.const 64) (i32.const 64))", ); assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); assert_eq!(*host.domain_keylets_asked.borrow(), vec![(account, 5)]); @@ -727,7 +735,8 @@ fn ticket_id_reads_the_account_and_seq() { let wat = module( &[import::TICKET_ID, ONE_PAGE], - "(call $ticket_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + "(i32.store (i32.const 20) (i32.const 5)) + (call $ticket_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 4) (i32.const 64) (i32.const 64))", ); assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); assert_eq!(*host.ticket_keylets_asked.borrow(), vec![(account, 5)]); @@ -742,7 +751,8 @@ fn vault_id_reads_the_account_and_seq() { let wat = module( &[import::VAULT_ID, ONE_PAGE], - "(call $vault_id (i32.const 0) (i32.const 20) (i32.const 5) (i32.const 64) (i32.const 64))", + "(i32.store (i32.const 20) (i32.const 5)) + (call $vault_id (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 4) (i32.const 64) (i32.const 64))", ); assert_eq!(status(&wat, &host), 32, "the 32-byte keylet length"); assert_eq!(*host.vault_keylets_asked.borrow(), vec![(account, 5)]); diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 350f3a1f9b..56300c2a2e 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -1406,24 +1406,24 @@ pub mod import { pub const CHECK_SIG: &str = r#"(import "host_lib" "check_sig" (func $check_sig (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const ACCOUNTROOT_ID: &str = r#"(import "host_lib" "accountroot_id" (func $accountroot_id (param i32 i32 i32 i32) (result i32)))"#; pub const AMM_ID: &str = r#"(import "host_lib" "amm_id" (func $amm_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; - pub const CHECK_ID: &str = r#"(import "host_lib" "check_id" (func $check_id (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const CHECK_ID: &str = r#"(import "host_lib" "check_id" (func $check_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const CREDENTIAL_ID: &str = r#"(import "host_lib" "credential_id" (func $credential_id (param i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const DELEGATE_ID: &str = r#"(import "host_lib" "delegate_id" (func $delegate_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const DEPOSIT_PREAUTH_ID: &str = r#"(import "host_lib" "deposit_preauth_id" (func $deposit_preauth_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const DID_ID: &str = r#"(import "host_lib" "did_id" (func $did_id (param i32 i32 i32 i32) (result i32)))"#; - pub const ESCROW_ID: &str = r#"(import "host_lib" "escrow_id" (func $escrow_id (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const ESCROW_ID: &str = r#"(import "host_lib" "escrow_id" (func $escrow_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const TRUSTLINE_ID: &str = r#"(import "host_lib" "trustline_id" (func $trustline_id (param i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; - pub const MPT_ISSUANCE_ID: &str = r#"(import "host_lib" "mpt_issuance_id" (func $mpt_issuance_id (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const MPT_ISSUANCE_ID: &str = r#"(import "host_lib" "mpt_issuance_id" (func $mpt_issuance_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const MPTOKEN_ID: &str = r#"(import "host_lib" "mptoken_id" (func $mptoken_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; - pub const NFT_OFFER_ID: &str = r#"(import "host_lib" "nft_offer_id" (func $nft_offer_id (param i32 i32 i32 i32 i32) (result i32)))"#; - pub const OFFER_ID: &str = r#"(import "host_lib" "offer_id" (func $offer_id (param i32 i32 i32 i32 i32) (result i32)))"#; - pub const ORACLE_ID: &str = r#"(import "host_lib" "oracle_id" (func $oracle_id (param i32 i32 i32 i32 i32) (result i32)))"#; - pub const PAYCHAN_ID: &str = r#"(import "host_lib" "paychan_id" (func $paychan_id (param i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; - pub const PERMISSIONED_DOMAIN_ID: &str = r#"(import "host_lib" "permissioned_domain_id" (func $permissioned_domain_id (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const NFT_OFFER_ID: &str = r#"(import "host_lib" "nft_offer_id" (func $nft_offer_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const OFFER_ID: &str = r#"(import "host_lib" "offer_id" (func $offer_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const ORACLE_ID: &str = r#"(import "host_lib" "oracle_id" (func $oracle_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const PAYCHAN_ID: &str = r#"(import "host_lib" "paychan_id" (func $paychan_id (param i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const PERMISSIONED_DOMAIN_ID: &str = r#"(import "host_lib" "permissioned_domain_id" (func $permissioned_domain_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; pub const SIGNERS_ID: &str = r#"(import "host_lib" "signers_id" (func $signers_id (param i32 i32 i32 i32) (result i32)))"#; - pub const TICKET_ID: &str = r#"(import "host_lib" "ticket_id" (func $ticket_id (param i32 i32 i32 i32 i32) (result i32)))"#; - pub const VAULT_ID: &str = r#"(import "host_lib" "vault_id" (func $vault_id (param i32 i32 i32 i32 i32) (result i32)))"#; + pub const TICKET_ID: &str = r#"(import "host_lib" "ticket_id" (func $ticket_id (param i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const VAULT_ID: &str = r#"(import "host_lib" "vault_id" (func $vault_id (param i32 i32 i32 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 = r#"(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))"#; From c5d25e3055e52e1d1cdeddf5a56cf1b907e33850 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Tue, 11 Aug 2026 12:22:39 -0400 Subject: [PATCH 41/46] feat: Self code review changes --- src/libxrpl/tx/wasm/HostContext.cpp | 232 +++++++++++++++++++++++++++- 1 file changed, 227 insertions(+), 5 deletions(-) diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index eb620c5a32..4bcc1580f3 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -45,7 +45,9 @@ std::int32_t answer(rust::Slice out, std::uint8_t const* value, std::size_t size) { if (size <= out.size()) + { std::memcpy(out.data(), value, size); + } return static_cast(size); } @@ -72,22 +74,28 @@ std::expected parseAsset(rust::Slice bytes) { if (bytes.size() == MPTID::size()) + { return Asset{MPTID::fromVoid(bytes.data())}; + } if (bytes.size() == Currency::size()) { auto const issue = Issue{Currency::fromVoid(bytes.data()), xrpAccount()}; if (!issue.native()) + { return std::unexpected(HostFunctionError::InvalidParams); + } return Asset{issue}; } if (bytes.size() == Currency::size() + AccountID::size()) { - auto const issue = Issue( - Currency::fromVoid(bytes.data()), AccountID::fromVoid(bytes.data() + Currency::size())); + auto const issue = Issue{ + Currency::fromVoid(bytes.data()), AccountID::fromVoid(bytes.data() + Currency::size())}; if (issue.native()) + { return std::unexpected(HostFunctionError::InvalidParams); + } return Asset{issue}; } @@ -100,9 +108,11 @@ std::expected parseUint64(rust::Slice bytes) { if (bytes.size() != sizeof(std::uint64_t)) + { return std::unexpected(HostFunctionError::InvalidParams); + } - std::uint64_t x = 0; + std::uint64_t x{}; std::memcpy(&x, bytes.data(), sizeof(x)); return adjustWasmEndianess(x); } @@ -126,7 +136,7 @@ parseST(rust::Slice bytes) } // namespace -HostContext::HostContext(HostFunctions& hostFunctions) : hostFunctions_(hostFunctions) +HostContext::HostContext(HostFunctions& hostFunctions) : hostFunctions_{hostFunctions} { } @@ -136,7 +146,9 @@ HostContext::getLedgerSqn(rust::Slice out) const noexcept return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { auto const sqn = hostFunctions_.getLedgerSqn(); if (!sqn) + { return hfErrorToInt(sqn.error()); + } return answerScalar(out, *sqn); }); @@ -148,7 +160,9 @@ HostContext::getParentLedgerTime(rust::Slice out) const noexcept return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { auto const time = hostFunctions_.getParentLedgerTime(); if (!time) + { return hfErrorToInt(time.error()); + } return answerScalar(out, *time); }); @@ -160,7 +174,9 @@ HostContext::getParentLedgerHash(rust::Slice out) const noexcept return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { auto const hash = hostFunctions_.getParentLedgerHash(); if (!hash) + { return hfErrorToInt(hash.error()); + } return answer(out, hash->data(), hash->size()); }); @@ -172,7 +188,9 @@ HostContext::getBaseFee(rust::Slice out) const noexcept return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { auto const fee = hostFunctions_.getBaseFee(); if (!fee) + { return hfErrorToInt(fee.error()); + } return answerScalar(out, *fee); }); @@ -190,17 +208,23 @@ HostContext::isAmendmentEnabled(rust::Slice amendment) const auto const enabled = hostFunctions_.isAmendmentEnabled(uint256::fromVoid(amendment.data())); if (enabled && *enabled == 1) + { return *enabled; + } } if (amendment.size() > 64) + { return hfErrorToInt(HostFunctionError::DataFieldTooLarge); + } auto const name = - std::string_view(reinterpret_cast(amendment.data()), amendment.size()); + std::string_view{reinterpret_cast(amendment.data()), amendment.size()}; auto const enabled = hostFunctions_.isAmendmentEnabled(name); if (!enabled) + { return hfErrorToInt(enabled.error()); + } return *enabled; }); @@ -212,11 +236,15 @@ HostContext::cacheLedgerObj(rust::Slice objId, std::int32_t { 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; }); @@ -229,11 +257,15 @@ HostContext::getTxField(std::int32_t field, rust::Slice out) const auto const& knownSFields = SField::getKnownCodeToField(); auto const it = knownSFields.find(field); if (it == knownSFields.end()) + { return hfErrorToInt(HostFunctionError::InvalidField); + } auto const value = hostFunctions_.getTxField(*it->second); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -247,11 +279,15 @@ HostContext::getCurrentLedgerObjField(std::int32_t field, rust::Slicesecond); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -267,11 +303,15 @@ HostContext::getLedgerObjField( auto const& knownSFields = SField::getKnownCodeToField(); auto const it = knownSFields.find(field); if (it == knownSFields.end()) + { return hfErrorToInt(HostFunctionError::InvalidField); + } auto const value = hostFunctions_.getLedgerObjField(cacheIdx, *it->second); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -285,7 +325,9 @@ HostContext::getTxNestedField( return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { // A path of i32 steps: non-empty and a whole number of them. if (locator.empty() || (locator.size() & 3) != 0) + { return hfErrorToInt(HostFunctionError::LocatorMalformed); + } // Copy into an aligned int32 buffer rather than aliasing the slice, whose // bytes carry no int32 alignment guarantee. The wire byte order is kept; the @@ -297,7 +339,9 @@ HostContext::getTxNestedField( auto const value = hostFunctions_.getTxNestedField(fl); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -310,7 +354,9 @@ HostContext::getCurrentLedgerObjNestedField( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (locator.empty() || (locator.size() & 3) != 0) + { return hfErrorToInt(HostFunctionError::LocatorMalformed); + } std::uint32_t const steps = locator.size() / sizeof(std::int32_t); std::vector locBuf(steps); @@ -319,7 +365,9 @@ HostContext::getCurrentLedgerObjNestedField( auto const value = hostFunctions_.getCurrentLedgerObjNestedField(fl); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -333,7 +381,9 @@ HostContext::getLedgerObjNestedField( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (locator.empty() || (locator.size() & 3) != 0) + { return hfErrorToInt(HostFunctionError::LocatorMalformed); + } std::uint32_t const steps = locator.size() / sizeof(std::int32_t); std::vector locBuf(steps); @@ -342,7 +392,9 @@ HostContext::getLedgerObjNestedField( auto const value = hostFunctions_.getLedgerObjNestedField(cacheIdx, fl); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -355,11 +407,15 @@ HostContext::getTxArrayLen(std::int32_t field) const noexcept auto const& knownSFields = SField::getKnownCodeToField(); auto const it = knownSFields.find(field); if (it == knownSFields.end()) + { return hfErrorToInt(HostFunctionError::InvalidField); + } auto const len = hostFunctions_.getTxArrayLen(*it->second); if (!len) + { return hfErrorToInt(len.error()); + } return *len; }); @@ -372,11 +428,15 @@ HostContext::getCurrentLedgerObjArrayLen(std::int32_t field) const noexcept auto const& knownSFields = SField::getKnownCodeToField(); auto const it = knownSFields.find(field); if (it == knownSFields.end()) + { return hfErrorToInt(HostFunctionError::InvalidField); + } auto const len = hostFunctions_.getCurrentLedgerObjArrayLen(*it->second); if (!len) + { return hfErrorToInt(len.error()); + } return *len; }); @@ -389,11 +449,15 @@ HostContext::getLedgerObjArrayLen(std::int32_t cacheIdx, std::int32_t field) con auto const& knownSFields = SField::getKnownCodeToField(); auto const it = knownSFields.find(field); if (it == knownSFields.end()) + { return hfErrorToInt(HostFunctionError::InvalidField); + } auto const len = hostFunctions_.getLedgerObjArrayLen(cacheIdx, *it->second); if (!len) + { return hfErrorToInt(len.error()); + } return *len; }); @@ -404,7 +468,9 @@ HostContext::getTxNestedArrayLen(rust::Slice locator) const { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (locator.empty() || (locator.size() & 3) != 0) + { return hfErrorToInt(HostFunctionError::LocatorMalformed); + } std::uint32_t const steps = locator.size() / sizeof(std::int32_t); std::vector locBuf(steps); @@ -413,7 +479,9 @@ HostContext::getTxNestedArrayLen(rust::Slice locator) const auto const len = hostFunctions_.getTxNestedArrayLen(fl); if (!len) + { return hfErrorToInt(len.error()); + } return *len; }); @@ -425,7 +493,9 @@ HostContext::getCurrentLedgerObjNestedArrayLen( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (locator.empty() || (locator.size() & 3) != 0) + { return hfErrorToInt(HostFunctionError::LocatorMalformed); + } std::uint32_t const steps = locator.size() / sizeof(std::int32_t); std::vector locBuf(steps); @@ -434,7 +504,9 @@ HostContext::getCurrentLedgerObjNestedArrayLen( auto const len = hostFunctions_.getCurrentLedgerObjNestedArrayLen(fl); if (!len) + { return hfErrorToInt(len.error()); + } return *len; }); @@ -447,7 +519,9 @@ HostContext::getLedgerObjNestedArrayLen( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (locator.empty() || (locator.size() & 3) != 0) + { return hfErrorToInt(HostFunctionError::LocatorMalformed); + } std::uint32_t const steps = locator.size() / sizeof(std::int32_t); std::vector locBuf(steps); @@ -456,7 +530,9 @@ HostContext::getLedgerObjNestedArrayLen( auto const len = hostFunctions_.getLedgerObjNestedArrayLen(cacheIdx, fl); if (!len) + { return hfErrorToInt(len.error()); + } return *len; }); @@ -474,7 +550,9 @@ HostContext::checkSignature( Slice{signature.data(), signature.size()}, Slice{pubkey.data(), pubkey.size()}); if (!valid) + { return hfErrorToInt(valid.error()); + } return *valid; }); @@ -486,11 +564,15 @@ HostContext::accountKeylet(rust::Slice account, rust::Slice< { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } auto const value = hostFunctions_.accountKeylet(AccountID::fromVoid(account.data())); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -505,15 +587,21 @@ HostContext::ammKeylet( return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { auto const a1 = parseAsset(asset1); if (!a1) + { return hfErrorToInt(a1.error()); + } auto const a2 = parseAsset(asset2); if (!a2) + { return hfErrorToInt(a2.error()); + } auto const value = hostFunctions_.ammKeylet(*a1, *a2); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -527,13 +615,17 @@ HostContext::checkKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } // The guest's u32 seq arrives as its i32 bit pattern; recover it. auto const value = hostFunctions_.checkKeylet( AccountID::fromVoid(account.data()), static_cast(seq)); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -548,14 +640,18 @@ HostContext::credentialKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (subject.size() != AccountID::size() || issuer.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } auto const value = hostFunctions_.credentialKeylet( AccountID::fromVoid(subject.data()), AccountID::fromVoid(issuer.data()), Slice{credentialType.data(), credentialType.size()}); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -569,12 +665,16 @@ HostContext::delegateKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account.size() != AccountID::size() || authorize.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } auto const value = hostFunctions_.delegateKeylet( AccountID::fromVoid(account.data()), AccountID::fromVoid(authorize.data())); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -588,12 +688,16 @@ HostContext::depositPreauthKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account.size() != AccountID::size() || authorize.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } auto const value = hostFunctions_.depositPreauthKeylet( AccountID::fromVoid(account.data()), AccountID::fromVoid(authorize.data())); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -605,11 +709,15 @@ HostContext::didKeylet(rust::Slice account, rust::Slicedata(), value->size()); }); @@ -623,13 +731,17 @@ HostContext::escrowKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } // The guest's u32 seq arrives as its i32 bit pattern; recover it. auto const value = hostFunctions_.escrowKeylet( AccountID::fromVoid(account.data()), static_cast(seq)); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -645,14 +757,18 @@ HostContext::trustLineKeylet( return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account1.size() != AccountID::size() || account2.size() != AccountID::size() || currency.size() != Currency::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } auto const value = hostFunctions_.trustLineKeylet( AccountID::fromVoid(account1.data()), AccountID::fromVoid(account2.data()), Currency::fromVoid(currency.data())); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -666,13 +782,17 @@ HostContext::mptokenIssuanceKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (issuer.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } // The guest's u32 seq arrives as its i32 bit pattern; recover it. auto const value = hostFunctions_.mptokenIssuanceKeylet( AccountID::fromVoid(issuer.data()), static_cast(seq)); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -686,12 +806,16 @@ HostContext::mptokenKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (mptid.size() != MPTID::size() || holder.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } auto const value = hostFunctions_.mptokenKeylet( MPTID::fromVoid(mptid.data()), AccountID::fromVoid(holder.data())); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -705,13 +829,17 @@ HostContext::nftokenOfferKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } // The guest's u32 seq arrives as its i32 bit pattern; recover it. auto const value = hostFunctions_.nftokenOfferKeylet( AccountID::fromVoid(account.data()), static_cast(seq)); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -725,13 +853,17 @@ HostContext::offerKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } // The guest's u32 seq arrives as its i32 bit pattern; recover it. auto const value = hostFunctions_.offerKeylet( AccountID::fromVoid(account.data()), static_cast(seq)); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -745,13 +877,17 @@ HostContext::oracleKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } // The guest's u32 docId arrives as its i32 bit pattern; recover it. auto const value = hostFunctions_.oracleKeylet( AccountID::fromVoid(account.data()), static_cast(docId)); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -766,7 +902,9 @@ HostContext::paychannelKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account.size() != AccountID::size() || destination.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } // The guest's u32 seq arrives as its i32 bit pattern; recover it. auto const value = hostFunctions_.paychannelKeylet( @@ -774,7 +912,9 @@ HostContext::paychannelKeylet( AccountID::fromVoid(destination.data()), static_cast(seq)); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -788,13 +928,17 @@ HostContext::permissionedDomainKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } // The guest's u32 seq arrives as its i32 bit pattern; recover it. auto const value = hostFunctions_.permissionedDomainKeylet( AccountID::fromVoid(account.data()), static_cast(seq)); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -807,11 +951,15 @@ HostContext::signerListKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } auto const value = hostFunctions_.signerListKeylet(AccountID::fromVoid(account.data())); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -825,13 +973,17 @@ HostContext::ticketKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } // The guest's u32 seq arrives as its i32 bit pattern; recover it. auto const value = hostFunctions_.ticketKeylet( AccountID::fromVoid(account.data()), static_cast(seq)); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -845,13 +997,17 @@ HostContext::vaultKeylet( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account.size() != AccountID::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } // The guest's u32 seq arrives as its i32 bit pattern; recover it. auto const value = hostFunctions_.vaultKeylet( AccountID::fromVoid(account.data()), static_cast(seq)); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -864,7 +1020,9 @@ HostContext::sha512Half(rust::Slice data, rust::Slicedata(), digest->size()); }); @@ -877,7 +1035,9 @@ HostContext::trace(rust::Str msg, rust::Slice data, bool asH auto const status = hostFunctions_.trace( std::string_view{msg.data(), msg.size()}, Slice{data.data(), data.size()}, asHex); if (!status) + { return hfErrorToInt(status.error()); + } return *status; }); @@ -890,7 +1050,9 @@ HostContext::traceNum(rust::Str msg, std::int64_t number) const noexcept auto const status = hostFunctions_.traceNum(std::string_view{msg.data(), msg.size()}, number); if (!status) + { return hfErrorToInt(status.error()); + } return *status; }); @@ -902,7 +1064,9 @@ 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; }); @@ -916,12 +1080,16 @@ HostContext::getNFT( { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (account.size() != AccountID::size() || nftId.size() != uint256::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } auto const value = hostFunctions_.getNFT( AccountID::fromVoid(account.data()), uint256::fromVoid(nftId.data())); if (!value) + { return hfErrorToInt(value.error()); + } return answer(out, value->data(), value->size()); }); @@ -933,11 +1101,15 @@ HostContext::getNFTIssuer(rust::Slice nftId, rust::Slicedata(), value->size()); }); @@ -949,11 +1121,15 @@ HostContext::getNFTTaxon(rust::Slice nftId, rust::Slice nftId) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (nftId.size() != uint256::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } auto const value = hostFunctions_.getNFTFlags(uint256::fromVoid(nftId.data())); if (!value) + { return hfErrorToInt(value.error()); + } return *value; }); @@ -979,11 +1159,15 @@ HostContext::getNFTTransferFee(rust::Slice nftId) const noex { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { if (nftId.size() != uint256::size()) + { return hfErrorToInt(HostFunctionError::InvalidParams); + } auto const value = hostFunctions_.getNFTTransferFee(uint256::fromVoid(nftId.data())); if (!value) + { return hfErrorToInt(value.error()); + } return *value; }); @@ -995,11 +1179,15 @@ HostContext::getNFTSequence(rust::Slice nftId, rust::Slicedata(), value->size()); }); @@ -1027,11 +1217,15 @@ HostContext::floatFromUint( 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()); }); @@ -1046,11 +1240,15 @@ HostContext::floatFromSTAmount( 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()); }); @@ -1065,11 +1263,15 @@ HostContext::floatFromSTNumber( 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()); }); @@ -1084,7 +1286,9 @@ HostContext::floatToInt( 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); }); @@ -1099,7 +1303,9 @@ HostContext::floatToMantExp( 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. @@ -1119,7 +1325,9 @@ HostContext::floatFromMantExp( 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()); }); @@ -1133,7 +1341,9 @@ HostContext::floatCompare(rust::Slice x, rust::Slicedata(), value->size()); }); @@ -1167,7 +1379,9 @@ HostContext::floatSubtract( 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()); }); @@ -1184,7 +1398,9 @@ HostContext::floatMultiply( 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()); }); @@ -1201,7 +1417,9 @@ HostContext::floatDivide( 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()); }); @@ -1217,7 +1435,9 @@ HostContext::floatRoot( 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()); }); @@ -1233,7 +1453,9 @@ HostContext::floatPower( 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()); }); From 00dd93e77d03f7b1cb43cfcb82d8d9d02a3888a6 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Tue, 11 Aug 2026 13:59:33 -0400 Subject: [PATCH 42/46] feat: Self code review changes --- src/libxrpl/tx/wasm/HostContext.cpp | 950 +++++++++++----------------- 1 file changed, 357 insertions(+), 593 deletions(-) diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 4bcc1580f3..d60f3fcc25 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -112,7 +112,7 @@ parseUint64(rust::Slice bytes) return std::unexpected(HostFunctionError::InvalidParams); } - std::uint64_t x{}; + auto x = std::uint64_t{}; std::memcpy(&x, bytes.data(), sizeof(x)); return adjustWasmEndianess(x); } @@ -125,7 +125,7 @@ parseST(rust::Slice bytes) { try { - SerialIter sit{Slice{bytes.data(), bytes.size()}}; + auto sit = SerialIter{Slice{bytes.data(), bytes.size()}}; return T{sit, sfGeneric}; } catch (std::exception const&) @@ -134,6 +134,215 @@ parseST(rust::Slice bytes) } } +template +std::int32_t +invokeWithLocator( + rust::Slice locator, + rust::Slice out, + Functor&& functor) +{ + if (locator.empty() || (locator.size() & 3) != 0) + { + return hfErrorToInt(HostFunctionError::LocatorMalformed); + } + + std::uint32_t const steps = locator.size() / sizeof(std::int32_t); + auto locBuf = std::vector(steps); + std::memcpy(locBuf.data(), locator.data(), locator.size()); + auto const fl = FieldLocator{std::move(locBuf)}; + + auto const value = functor(fl); + if (!value) + { + return hfErrorToInt(value.error()); + } + + return answer(out, value->data(), value->size()); +} + +template +std::int32_t +invokeWithLocator(rust::Slice locator, Functor&& functor) +{ + if (locator.empty() || (locator.size() & 3) != 0) + { + return hfErrorToInt(HostFunctionError::LocatorMalformed); + } + + std::uint32_t const steps = locator.size() / sizeof(std::int32_t); + auto locBuf = std::vector(steps); + std::memcpy(locBuf.data(), locator.data(), locator.size()); + auto const fl = FieldLocator{std::move(locBuf)}; + + auto const value = functor(fl); + if (!value) + { + return hfErrorToInt(value.error()); + } + + return *value; +} + +template +std::int32_t +invokeWithField(std::int32_t field, rust::Slice out, Functor&& functor) +{ + auto const& knownSFields = SField::getKnownCodeToField(); + auto const it = knownSFields.find(field); + if (it == std::end(knownSFields)) + { + return hfErrorToInt(HostFunctionError::InvalidField); + } + + auto const value = functor(*it->second); + if (!value) + { + return hfErrorToInt(value.error()); + } + + return answer(out, value->data(), value->size()); +} + +template +std::int32_t +invokeWithField(std::int32_t field, Functor&& functor) +{ + auto const& knownSFields = SField::getKnownCodeToField(); + auto const it = knownSFields.find(field); + if (it == std::end(knownSFields)) + { + return hfErrorToInt(HostFunctionError::InvalidField); + } + + auto const len = functor(*it->second); + if (!len) + { + return hfErrorToInt(len.error()); + } + + return *len; +} + +template +std::int32_t +invokeWithAccount( + rust::Slice account, + rust::Slice out, + Functor&& functor) +{ + if (account.size() != AccountID::size()) + { + return hfErrorToInt(HostFunctionError::InvalidParams); + } + + auto const value = functor(AccountID::fromVoid(account.data())); + if (!value) + { + return hfErrorToInt(value.error()); + } + + return answer(out, value->data(), value->size()); +} + +template +std::int32_t +invokeWithAccounts( + rust::Slice account1, + rust::Slice account2, + rust::Slice out, + Functor&& functor) +{ + if (account1.size() != AccountID::size() || account2.size() != AccountID::size()) + { + return hfErrorToInt(HostFunctionError::InvalidParams); + } + + auto const value = + functor(AccountID::fromVoid(account1.data()), AccountID::fromVoid(account2.data())); + if (!value) + { + return hfErrorToInt(value.error()); + } + + return answer(out, value->data(), value->size()); +} + +template +std::int32_t +invokeNFT(rust::Slice nftId, rust::Slice out, Functor&& functor) +{ + if (nftId.size() != uint256::size()) + { + return hfErrorToInt(HostFunctionError::InvalidParams); + } + + auto const value = functor(uint256::fromVoid(nftId.data())); + if (!value) + { + return hfErrorToInt(value.error()); + } + + if constexpr (Scalar) + { + return answerScalar(out, *value); + } + else + { + return answer(out, value->data(), value->size()); + } +} + +template +std::int32_t +invokeNFT(rust::Slice nftId, Functor&& functor) +{ + if (nftId.size() != uint256::size()) + { + return hfErrorToInt(HostFunctionError::InvalidParams); + } + + auto const value = functor(uint256::fromVoid(nftId.data())); + if (!value) + { + return hfErrorToInt(value.error()); + } + + return *value; +} + +template +std::int32_t +invokeFloat(rust::Slice out, Functor&& functor) +{ + auto const value = functor(); + if (!value) + { + return hfErrorToInt(value.error()); + } + + if constexpr (Scalar) + { + return answerScalar(out, *value); + } + else + { + return answer(out, value->data(), value->size()); + } +} + +template +std::int32_t +invokeFloat(Functor&& functor) +{ + auto const value = functor(); + if (!value) + { + return hfErrorToInt(value.error()); + } + + return *value; +} + } // namespace HostContext::HostContext(HostFunctions& hostFunctions) : hostFunctions_{hostFunctions} @@ -213,7 +422,8 @@ HostContext::isAmendmentEnabled(rust::Slice amendment) const } } - if (amendment.size() > 64) + static constexpr auto kMaxAmendmentSize = 64UZ; + if (amendment.size() > kMaxAmendmentSize) { return hfErrorToInt(HostFunctionError::DataFieldTooLarge); } @@ -254,20 +464,9 @@ std::int32_t HostContext::getTxField(std::int32_t field, rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - auto const& knownSFields = SField::getKnownCodeToField(); - auto const it = knownSFields.find(field); - if (it == knownSFields.end()) - { - return hfErrorToInt(HostFunctionError::InvalidField); - } - - auto const value = hostFunctions_.getTxField(*it->second); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithField(field, out, [&](auto const& innerField) { + return hostFunctions_.getTxField(innerField); + }); }); } @@ -276,20 +475,9 @@ HostContext::getCurrentLedgerObjField(std::int32_t field, rust::Slicesecond); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithField(field, out, [&](auto const& innerField) { + return hostFunctions_.getCurrentLedgerObjField(innerField); + }); }); } @@ -300,20 +488,9 @@ HostContext::getLedgerObjField( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - auto const& knownSFields = SField::getKnownCodeToField(); - auto const it = knownSFields.find(field); - if (it == knownSFields.end()) - { - return hfErrorToInt(HostFunctionError::InvalidField); - } - - auto const value = hostFunctions_.getLedgerObjField(cacheIdx, *it->second); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithField(field, out, [&](auto const& innerField) { + return hostFunctions_.getLedgerObjField(cacheIdx, innerField); + }); }); } @@ -323,27 +500,9 @@ HostContext::getTxNestedField( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - // A path of i32 steps: non-empty and a whole number of them. - if (locator.empty() || (locator.size() & 3) != 0) - { - return hfErrorToInt(HostFunctionError::LocatorMalformed); - } - - // Copy into an aligned int32 buffer rather than aliasing the slice, whose - // bytes carry no int32 alignment guarantee. The wire byte order is kept; the - // field getters below apply `adjustWasmEndianess` when they read a step. - std::uint32_t const steps = locator.size() / sizeof(std::int32_t); - std::vector locBuf(steps); - std::memcpy(locBuf.data(), locator.data(), locator.size()); - FieldLocator const fl(std::move(locBuf)); - - auto const value = hostFunctions_.getTxNestedField(fl); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithLocator(locator, out, [&](FieldLocator const& fl) { + return hostFunctions_.getTxNestedField(fl); + }); }); } @@ -353,23 +512,9 @@ HostContext::getCurrentLedgerObjNestedField( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (locator.empty() || (locator.size() & 3) != 0) - { - return hfErrorToInt(HostFunctionError::LocatorMalformed); - } - - std::uint32_t const steps = locator.size() / sizeof(std::int32_t); - std::vector locBuf(steps); - std::memcpy(locBuf.data(), locator.data(), locator.size()); - FieldLocator const fl(std::move(locBuf)); - - auto const value = hostFunctions_.getCurrentLedgerObjNestedField(fl); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithLocator(locator, out, [&](FieldLocator const& fl) { + return hostFunctions_.getCurrentLedgerObjNestedField(fl); + }); }); } @@ -380,23 +525,9 @@ HostContext::getLedgerObjNestedField( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (locator.empty() || (locator.size() & 3) != 0) - { - return hfErrorToInt(HostFunctionError::LocatorMalformed); - } - - std::uint32_t const steps = locator.size() / sizeof(std::int32_t); - std::vector locBuf(steps); - std::memcpy(locBuf.data(), locator.data(), locator.size()); - FieldLocator const fl(std::move(locBuf)); - - auto const value = hostFunctions_.getLedgerObjNestedField(cacheIdx, fl); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithLocator(locator, out, [&](FieldLocator const& fl) { + return hostFunctions_.getLedgerObjNestedField(cacheIdx, fl); + }); }); } @@ -404,20 +535,9 @@ std::int32_t HostContext::getTxArrayLen(std::int32_t field) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - auto const& knownSFields = SField::getKnownCodeToField(); - auto const it = knownSFields.find(field); - if (it == knownSFields.end()) - { - return hfErrorToInt(HostFunctionError::InvalidField); - } - - auto const len = hostFunctions_.getTxArrayLen(*it->second); - if (!len) - { - return hfErrorToInt(len.error()); - } - - return *len; + return invokeWithField(field, [&](auto const& innerField) { + return hostFunctions_.getTxArrayLen(innerField); + }); }); } @@ -425,20 +545,9 @@ std::int32_t HostContext::getCurrentLedgerObjArrayLen(std::int32_t field) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - auto const& knownSFields = SField::getKnownCodeToField(); - auto const it = knownSFields.find(field); - if (it == knownSFields.end()) - { - return hfErrorToInt(HostFunctionError::InvalidField); - } - - auto const len = hostFunctions_.getCurrentLedgerObjArrayLen(*it->second); - if (!len) - { - return hfErrorToInt(len.error()); - } - - return *len; + return invokeWithField(field, [&](auto const& innerField) { + return hostFunctions_.getCurrentLedgerObjArrayLen(innerField); + }); }); } @@ -446,20 +555,9 @@ std::int32_t HostContext::getLedgerObjArrayLen(std::int32_t cacheIdx, std::int32_t field) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - auto const& knownSFields = SField::getKnownCodeToField(); - auto const it = knownSFields.find(field); - if (it == knownSFields.end()) - { - return hfErrorToInt(HostFunctionError::InvalidField); - } - - auto const len = hostFunctions_.getLedgerObjArrayLen(cacheIdx, *it->second); - if (!len) - { - return hfErrorToInt(len.error()); - } - - return *len; + return invokeWithField(field, [&](auto const& innerField) { + return hostFunctions_.getLedgerObjArrayLen(cacheIdx, innerField); + }); }); } @@ -467,23 +565,9 @@ std::int32_t HostContext::getTxNestedArrayLen(rust::Slice locator) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (locator.empty() || (locator.size() & 3) != 0) - { - return hfErrorToInt(HostFunctionError::LocatorMalformed); - } - - std::uint32_t const steps = locator.size() / sizeof(std::int32_t); - std::vector locBuf(steps); - std::memcpy(locBuf.data(), locator.data(), locator.size()); - FieldLocator const fl(std::move(locBuf)); - - auto const len = hostFunctions_.getTxNestedArrayLen(fl); - if (!len) - { - return hfErrorToInt(len.error()); - } - - return *len; + return invokeWithLocator(locator, [&](FieldLocator const& fl) { + return hostFunctions_.getTxNestedArrayLen(fl); + }); }); } @@ -492,23 +576,9 @@ HostContext::getCurrentLedgerObjNestedArrayLen( rust::Slice locator) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (locator.empty() || (locator.size() & 3) != 0) - { - return hfErrorToInt(HostFunctionError::LocatorMalformed); - } - - std::uint32_t const steps = locator.size() / sizeof(std::int32_t); - std::vector locBuf(steps); - std::memcpy(locBuf.data(), locator.data(), locator.size()); - FieldLocator const fl(std::move(locBuf)); - - auto const len = hostFunctions_.getCurrentLedgerObjNestedArrayLen(fl); - if (!len) - { - return hfErrorToInt(len.error()); - } - - return *len; + return invokeWithLocator(locator, [&](FieldLocator const& fl) { + return hostFunctions_.getCurrentLedgerObjNestedArrayLen(fl); + }); }); } @@ -518,23 +588,9 @@ HostContext::getLedgerObjNestedArrayLen( rust::Slice locator) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (locator.empty() || (locator.size() & 3) != 0) - { - return hfErrorToInt(HostFunctionError::LocatorMalformed); - } - - std::uint32_t const steps = locator.size() / sizeof(std::int32_t); - std::vector locBuf(steps); - std::memcpy(locBuf.data(), locator.data(), locator.size()); - FieldLocator const fl(std::move(locBuf)); - - auto const len = hostFunctions_.getLedgerObjNestedArrayLen(cacheIdx, fl); - if (!len) - { - return hfErrorToInt(len.error()); - } - - return *len; + return invokeWithLocator(locator, [&](FieldLocator const& fl) { + return hostFunctions_.getLedgerObjNestedArrayLen(cacheIdx, fl); + }); }); } @@ -563,18 +619,9 @@ HostContext::accountKeylet(rust::Slice account, rust::Slice< const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - auto const value = hostFunctions_.accountKeylet(AccountID::fromVoid(account.data())); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccount(account, out, [&](auto const& accountId) { + return hostFunctions_.accountKeylet(accountId); + }); }); } @@ -614,20 +661,9 @@ HostContext::checkKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - // The guest's u32 seq arrives as its i32 bit pattern; recover it. - auto const value = hostFunctions_.checkKeylet( - AccountID::fromVoid(account.data()), static_cast(seq)); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccount(account, out, [&](auto const& accountId) { + return hostFunctions_.checkKeylet(accountId, static_cast(seq)); + }); }); } @@ -639,21 +675,11 @@ HostContext::credentialKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (subject.size() != AccountID::size() || issuer.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - auto const value = hostFunctions_.credentialKeylet( - AccountID::fromVoid(subject.data()), - AccountID::fromVoid(issuer.data()), - Slice{credentialType.data(), credentialType.size()}); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccounts( + subject, issuer, out, [&](auto const& account1, auto const& account2) { + return hostFunctions_.credentialKeylet( + account1, account2, Slice{credentialType.data(), credentialType.size()}); + }); }); } @@ -664,19 +690,10 @@ HostContext::delegateKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account.size() != AccountID::size() || authorize.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - auto const value = hostFunctions_.delegateKeylet( - AccountID::fromVoid(account.data()), AccountID::fromVoid(authorize.data())); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccounts( + account, authorize, out, [&](auto const& account1, auto const& account2) { + return hostFunctions_.delegateKeylet(account1, account2); + }); }); } @@ -687,19 +704,10 @@ HostContext::depositPreauthKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account.size() != AccountID::size() || authorize.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - auto const value = hostFunctions_.depositPreauthKeylet( - AccountID::fromVoid(account.data()), AccountID::fromVoid(authorize.data())); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccounts( + account, authorize, out, [&](auto const& account1, auto const& account2) { + return hostFunctions_.depositPreauthKeylet(account1, account2); + }); }); } @@ -708,18 +716,9 @@ HostContext::didKeylet(rust::Slice account, rust::Slicedata(), value->size()); + return invokeWithAccount(account, out, [&](auto const& accountId) { + return hostFunctions_.didKeylet(accountId); + }); }); } @@ -730,20 +729,9 @@ HostContext::escrowKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - // The guest's u32 seq arrives as its i32 bit pattern; recover it. - auto const value = hostFunctions_.escrowKeylet( - AccountID::fromVoid(account.data()), static_cast(seq)); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccount(account, out, [&](auto const& accountId) { + return hostFunctions_.escrowKeylet(accountId, static_cast(seq)); + }); }); } @@ -755,22 +743,16 @@ HostContext::trustLineKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account1.size() != AccountID::size() || account2.size() != AccountID::size() || - currency.size() != Currency::size()) + if (currency.size() != Currency::size()) { return hfErrorToInt(HostFunctionError::InvalidParams); } - auto const value = hostFunctions_.trustLineKeylet( - AccountID::fromVoid(account1.data()), - AccountID::fromVoid(account2.data()), - Currency::fromVoid(currency.data())); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccounts( + account1, account2, out, [&](auto const& innerAccount1, auto const& innerAccount2) { + return hostFunctions_.trustLineKeylet( + innerAccount1, innerAccount2, Currency::fromVoid(currency.data())); + }); }); } @@ -781,20 +763,9 @@ HostContext::mptokenIssuanceKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (issuer.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - // The guest's u32 seq arrives as its i32 bit pattern; recover it. - auto const value = hostFunctions_.mptokenIssuanceKeylet( - AccountID::fromVoid(issuer.data()), static_cast(seq)); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccount(issuer, out, [&](auto const& accountId) { + return hostFunctions_.mptokenIssuanceKeylet(accountId, static_cast(seq)); + }); }); } @@ -828,20 +799,9 @@ HostContext::nftokenOfferKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - // The guest's u32 seq arrives as its i32 bit pattern; recover it. - auto const value = hostFunctions_.nftokenOfferKeylet( - AccountID::fromVoid(account.data()), static_cast(seq)); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccount(account, out, [&](auto const& accountId) { + return hostFunctions_.nftokenOfferKeylet(accountId, static_cast(seq)); + }); }); } @@ -852,20 +812,9 @@ HostContext::offerKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - // The guest's u32 seq arrives as its i32 bit pattern; recover it. - auto const value = hostFunctions_.offerKeylet( - AccountID::fromVoid(account.data()), static_cast(seq)); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccount(account, out, [&](auto const& accountId) { + return hostFunctions_.offerKeylet(accountId, static_cast(seq)); + }); }); } @@ -876,20 +825,9 @@ HostContext::oracleKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - // The guest's u32 docId arrives as its i32 bit pattern; recover it. - auto const value = hostFunctions_.oracleKeylet( - AccountID::fromVoid(account.data()), static_cast(docId)); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccount(account, out, [&](auto const& accountId) { + return hostFunctions_.oracleKeylet(accountId, static_cast(docId)); + }); }); } @@ -901,22 +839,11 @@ HostContext::paychannelKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account.size() != AccountID::size() || destination.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - // The guest's u32 seq arrives as its i32 bit pattern; recover it. - auto const value = hostFunctions_.paychannelKeylet( - AccountID::fromVoid(account.data()), - AccountID::fromVoid(destination.data()), - static_cast(seq)); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccounts( + account, destination, out, [&](auto const& account1, auto const& account2) { + return hostFunctions_.paychannelKeylet( + account1, account2, static_cast(seq)); + }); }); } @@ -927,20 +854,10 @@ HostContext::permissionedDomainKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - // The guest's u32 seq arrives as its i32 bit pattern; recover it. - auto const value = hostFunctions_.permissionedDomainKeylet( - AccountID::fromVoid(account.data()), static_cast(seq)); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccount(account, out, [&](auto const& accountId) { + return hostFunctions_.permissionedDomainKeylet( + accountId, static_cast(seq)); + }); }); } @@ -950,18 +867,9 @@ HostContext::signerListKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - auto const value = hostFunctions_.signerListKeylet(AccountID::fromVoid(account.data())); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccount(account, out, [&](auto const& accountId) { + return hostFunctions_.signerListKeylet(accountId); + }); }); } @@ -972,20 +880,9 @@ HostContext::ticketKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - // The guest's u32 seq arrives as its i32 bit pattern; recover it. - auto const value = hostFunctions_.ticketKeylet( - AccountID::fromVoid(account.data()), static_cast(seq)); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccount(account, out, [&](auto const& accountId) { + return hostFunctions_.ticketKeylet(accountId, static_cast(seq)); + }); }); } @@ -996,20 +893,9 @@ HostContext::vaultKeylet( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account.size() != AccountID::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - // The guest's u32 seq arrives as its i32 bit pattern; recover it. - auto const value = hostFunctions_.vaultKeylet( - AccountID::fromVoid(account.data()), static_cast(seq)); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeWithAccount(account, out, [&](auto const& accountId) { + return hostFunctions_.vaultKeylet(accountId, static_cast(seq)); + }); }); } @@ -1079,19 +965,13 @@ HostContext::getNFT( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (account.size() != AccountID::size() || nftId.size() != uint256::size()) + if (account.size() != AccountID::size()) { return hfErrorToInt(HostFunctionError::InvalidParams); } - - auto const value = hostFunctions_.getNFT( - AccountID::fromVoid(account.data()), uint256::fromVoid(nftId.data())); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeNFT(nftId, out, [&](auto const& nft) { + return hostFunctions_.getNFT(AccountID::fromVoid(account.data()), nft); + }); }); } @@ -1100,18 +980,8 @@ HostContext::getNFTIssuer(rust::Slice nftId, rust::Slicedata(), value->size()); + return invokeNFT( + nftId, out, [&](auto const& nft) { return hostFunctions_.getNFTIssuer(nft); }); }); } @@ -1120,18 +990,8 @@ HostContext::getNFTTaxon(rust::Slice nftId, rust::Slice( + nftId, out, [&](auto const& nft) { return hostFunctions_.getNFTTaxon(nft); }); }); } @@ -1139,18 +999,7 @@ std::int32_t HostContext::getNFTFlags(rust::Slice nftId) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (nftId.size() != uint256::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - auto const value = hostFunctions_.getNFTFlags(uint256::fromVoid(nftId.data())); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return *value; + return invokeNFT(nftId, [&](auto const& nft) { return hostFunctions_.getNFTFlags(nft); }); }); } @@ -1158,18 +1007,8 @@ std::int32_t HostContext::getNFTTransferFee(rust::Slice nftId) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - if (nftId.size() != uint256::size()) - { - return hfErrorToInt(HostFunctionError::InvalidParams); - } - - auto const value = hostFunctions_.getNFTTransferFee(uint256::fromVoid(nftId.data())); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return *value; + return invokeNFT( + nftId, [&](auto const& nft) { return hostFunctions_.getNFTTransferFee(nft); }); }); } @@ -1178,18 +1017,8 @@ HostContext::getNFTSequence(rust::Slice nftId, rust::Slice( + nftId, out, [&](auto const& nft) { return hostFunctions_.getNFTSequence(nft); }); }); } @@ -1198,13 +1027,7 @@ HostContext::floatFromInt(std::int64_t x, std::int32_t mode, rust::Slicedata(), value->size()); + return invokeFloat(out, [&] { return hostFunctions_.floatFromInt(x, mode); }); }); } @@ -1220,14 +1043,7 @@ HostContext::floatFromUint( { return hfErrorToInt(parsed.error()); } - - auto const value = hostFunctions_.floatFromUint(*parsed, mode); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeFloat(out, [&] { return hostFunctions_.floatFromUint(*parsed, mode); }); }); } @@ -1243,14 +1059,8 @@ HostContext::floatFromSTAmount( { return hfErrorToInt(parsed.error()); } - - auto const value = hostFunctions_.floatFromSTAmount(*parsed, mode); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeFloat( + out, [&] { return hostFunctions_.floatFromSTAmount(*parsed, mode); }); }); } @@ -1266,14 +1076,8 @@ HostContext::floatFromSTNumber( { return hfErrorToInt(parsed.error()); } - - auto const value = hostFunctions_.floatFromSTNumber(*parsed, mode); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invokeFloat( + out, [&] { return hostFunctions_.floatFromSTNumber(*parsed, mode); }); }); } @@ -1284,13 +1088,8 @@ HostContext::floatToInt( 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); + return invokeFloat( + out, [&] { return hostFunctions_.floatToInt(Slice{x.data(), x.size()}, mode); }); }); } @@ -1323,13 +1122,8 @@ HostContext::floatFromMantExp( 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()); + return invokeFloat( + out, [&] { return hostFunctions_.floatFromMantExp(mantissa, exponent, mode); }); }); } @@ -1338,14 +1132,10 @@ HostContext::floatCompare(rust::Slice x, 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()); + return invokeFloat(out, [&] { + return hostFunctions_.floatAdd( + Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode); + }); }); } @@ -1376,14 +1162,10 @@ HostContext::floatSubtract( 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()); + return invokeFloat(out, [&] { + return hostFunctions_.floatSubtract( + Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode); + }); }); } @@ -1395,14 +1177,10 @@ HostContext::floatMultiply( 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()); + return invokeFloat(out, [&] { + return hostFunctions_.floatMultiply( + Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode); + }); }); } @@ -1414,14 +1192,10 @@ HostContext::floatDivide( 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()); + return invokeFloat(out, [&] { + return hostFunctions_.floatDivide( + Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode); + }); }); } @@ -1433,13 +1207,8 @@ HostContext::floatRoot( 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()); + return invokeFloat( + out, [&] { return hostFunctions_.floatRoot(Slice{x.data(), x.size()}, n, mode); }); }); } @@ -1451,13 +1220,8 @@ HostContext::floatPower( 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()); + return invokeFloat( + out, [&] { return hostFunctions_.floatPower(Slice{x.data(), x.size()}, n, mode); }); }); } From 952450255f616181dc59eb5c4ab9ae334fd7c761 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Tue, 11 Aug 2026 14:16:42 -0400 Subject: [PATCH 43/46] feat: Self code review changes --- src/libxrpl/tx/wasm/HostContext.cpp | 154 ++++++++++------------------ 1 file changed, 52 insertions(+), 102 deletions(-) diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index d60f3fcc25..d8aea84d42 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -330,9 +330,29 @@ invokeFloat(rust::Slice out, Functor&& functor) } } +template +std::int32_t +invoke(rust::Slice out, Functor&& functor) +{ + auto const value = functor(); + if (!value) + { + return hfErrorToInt(value.error()); + } + + if constexpr (Scalar) + { + return answerScalar(out, *value); + } + else + { + return answer(out, value->data(), value->size()); + } +} + template std::int32_t -invokeFloat(Functor&& functor) +invoke(Functor&& functor) { auto const value = functor(); if (!value) @@ -353,13 +373,7 @@ std::int32_t HostContext::getLedgerSqn(rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - auto const sqn = hostFunctions_.getLedgerSqn(); - if (!sqn) - { - return hfErrorToInt(sqn.error()); - } - - return answerScalar(out, *sqn); + return invoke(out, [&] { return hostFunctions_.getLedgerSqn(); }); }); } @@ -367,13 +381,7 @@ std::int32_t HostContext::getParentLedgerTime(rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - auto const time = hostFunctions_.getParentLedgerTime(); - if (!time) - { - return hfErrorToInt(time.error()); - } - - return answerScalar(out, *time); + return invoke(out, [&] { return hostFunctions_.getParentLedgerTime(); }); }); } @@ -381,13 +389,7 @@ std::int32_t HostContext::getParentLedgerHash(rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - auto const hash = hostFunctions_.getParentLedgerHash(); - if (!hash) - { - return hfErrorToInt(hash.error()); - } - - return answer(out, hash->data(), hash->size()); + return invoke(out, [&] { return hostFunctions_.getParentLedgerHash(); }); }); } @@ -395,13 +397,7 @@ std::int32_t HostContext::getBaseFee(rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - auto const fee = hostFunctions_.getBaseFee(); - if (!fee) - { - return hfErrorToInt(fee.error()); - } - - return answerScalar(out, *fee); + return invoke(out, [&] { return hostFunctions_.getBaseFee(); }); }); } @@ -430,13 +426,7 @@ HostContext::isAmendmentEnabled(rust::Slice amendment) const auto const name = std::string_view{reinterpret_cast(amendment.data()), amendment.size()}; - auto const enabled = hostFunctions_.isAmendmentEnabled(name); - if (!enabled) - { - return hfErrorToInt(enabled.error()); - } - - return *enabled; + return invoke([&] { return hostFunctions_.isAmendmentEnabled(name); }); }); } @@ -449,14 +439,9 @@ HostContext::cacheLedgerObj(rust::Slice objId, std::int32_t { return hfErrorToInt(HostFunctionError::InvalidParams); } - - auto const slot = hostFunctions_.cacheLedgerObj(uint256::fromVoid(objId.data()), cacheIdx); - if (!slot) - { - return hfErrorToInt(slot.error()); - } - - return *slot; + return invoke([&] { + return hostFunctions_.cacheLedgerObj(uint256::fromVoid(objId.data()), cacheIdx); + }); }); } @@ -601,16 +586,12 @@ HostContext::checkSignature( rust::Slice pubkey) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - auto const valid = hostFunctions_.checkSignature( - Slice{message.data(), message.size()}, - Slice{signature.data(), signature.size()}, - Slice{pubkey.data(), pubkey.size()}); - if (!valid) - { - return hfErrorToInt(valid.error()); - } - - return *valid; + return invoke([&] { + return hostFunctions_.checkSignature( + Slice{message.data(), message.size()}, + Slice{signature.data(), signature.size()}, + Slice{pubkey.data(), pubkey.size()}); + }); }); } @@ -643,14 +624,7 @@ HostContext::ammKeylet( { return hfErrorToInt(a2.error()); } - - auto const value = hostFunctions_.ammKeylet(*a1, *a2); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invoke(out, [&] { return hostFunctions_.ammKeylet(*a1, *a2); }); }); } @@ -780,15 +754,10 @@ HostContext::mptokenKeylet( { return hfErrorToInt(HostFunctionError::InvalidParams); } - - auto const value = hostFunctions_.mptokenKeylet( - MPTID::fromVoid(mptid.data()), AccountID::fromVoid(holder.data())); - if (!value) - { - return hfErrorToInt(value.error()); - } - - return answer(out, value->data(), value->size()); + return invoke(out, [&] { + return hostFunctions_.mptokenKeylet( + MPTID::fromVoid(mptid.data()), AccountID::fromVoid(holder.data())); + }); }); } @@ -904,13 +873,9 @@ HostContext::sha512Half(rust::Slice data, rust::Slicedata(), digest->size()); + return invoke(out, [&] { + return hostFunctions_.computeSha512HalfHash(Slice{data.data(), data.size()}); + }); }); } @@ -918,14 +883,10 @@ std::int32_t HostContext::trace(rust::Str msg, rust::Slice data, bool asHex) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - auto const status = hostFunctions_.trace( - std::string_view{msg.data(), msg.size()}, Slice{data.data(), data.size()}, asHex); - if (!status) - { - return hfErrorToInt(status.error()); - } - - return *status; + return invoke([&] { + return hostFunctions_.trace( + std::string_view{msg.data(), msg.size()}, Slice{data.data(), data.size()}, asHex); + }); }); } @@ -933,14 +894,9 @@ std::int32_t HostContext::traceNum(rust::Str msg, std::int64_t number) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - auto const status = - hostFunctions_.traceNum(std::string_view{msg.data(), msg.size()}, number); - if (!status) - { - return hfErrorToInt(status.error()); - } - - return *status; + return invoke([&] { + return hostFunctions_.traceNum(std::string_view{msg.data(), msg.size()}, number); + }); }); } @@ -948,13 +904,7 @@ 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; + return invoke([&] { return hostFunctions_.updateData(Slice{data.data(), data.size()}); }); }); } @@ -1132,7 +1082,7 @@ HostContext::floatCompare(rust::Slice x, rust::Slice Date: Tue, 11 Aug 2026 14:31:55 -0400 Subject: [PATCH 44/46] feat: Self code review changes --- src/libxrpl/tx/wasm/HostContext.cpp | 46 ++++++++--------------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index d8aea84d42..a256f6018b 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -310,26 +310,6 @@ invokeNFT(rust::Slice nftId, Functor&& functor) return *value; } -template -std::int32_t -invokeFloat(rust::Slice out, Functor&& functor) -{ - auto const value = functor(); - if (!value) - { - return hfErrorToInt(value.error()); - } - - if constexpr (Scalar) - { - return answerScalar(out, *value); - } - else - { - return answer(out, value->data(), value->size()); - } -} - template std::int32_t invoke(rust::Slice out, Functor&& functor) @@ -977,7 +957,7 @@ HostContext::floatFromInt(std::int64_t x, std::int32_t mode, rust::Slice(out, [&] { return hostFunctions_.floatFromInt(x, mode); }); + return invoke(out, [&] { return hostFunctions_.floatFromInt(x, mode); }); }); } @@ -993,7 +973,7 @@ HostContext::floatFromUint( { return hfErrorToInt(parsed.error()); } - return invokeFloat(out, [&] { return hostFunctions_.floatFromUint(*parsed, mode); }); + return invoke(out, [&] { return hostFunctions_.floatFromUint(*parsed, mode); }); }); } @@ -1009,8 +989,7 @@ HostContext::floatFromSTAmount( { return hfErrorToInt(parsed.error()); } - return invokeFloat( - out, [&] { return hostFunctions_.floatFromSTAmount(*parsed, mode); }); + return invoke(out, [&] { return hostFunctions_.floatFromSTAmount(*parsed, mode); }); }); } @@ -1026,8 +1005,7 @@ HostContext::floatFromSTNumber( { return hfErrorToInt(parsed.error()); } - return invokeFloat( - out, [&] { return hostFunctions_.floatFromSTNumber(*parsed, mode); }); + return invoke(out, [&] { return hostFunctions_.floatFromSTNumber(*parsed, mode); }); }); } @@ -1038,7 +1016,7 @@ HostContext::floatToInt( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - return invokeFloat( + return invoke( out, [&] { return hostFunctions_.floatToInt(Slice{x.data(), x.size()}, mode); }); }); } @@ -1072,7 +1050,7 @@ HostContext::floatFromMantExp( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - return invokeFloat( + return invoke( out, [&] { return hostFunctions_.floatFromMantExp(mantissa, exponent, mode); }); }); } @@ -1097,7 +1075,7 @@ HostContext::floatAdd( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - return invokeFloat(out, [&] { + return invoke(out, [&] { return hostFunctions_.floatAdd( Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode); }); @@ -1112,7 +1090,7 @@ HostContext::floatSubtract( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - return invokeFloat(out, [&] { + return invoke(out, [&] { return hostFunctions_.floatSubtract( Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode); }); @@ -1127,7 +1105,7 @@ HostContext::floatMultiply( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - return invokeFloat(out, [&] { + return invoke(out, [&] { return hostFunctions_.floatMultiply( Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode); }); @@ -1142,7 +1120,7 @@ HostContext::floatDivide( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - return invokeFloat(out, [&] { + return invoke(out, [&] { return hostFunctions_.floatDivide( Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode); }); @@ -1157,7 +1135,7 @@ HostContext::floatRoot( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - return invokeFloat( + return invoke( out, [&] { return hostFunctions_.floatRoot(Slice{x.data(), x.size()}, n, mode); }); }); } @@ -1170,7 +1148,7 @@ HostContext::floatPower( rust::Slice out) const noexcept { return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { - return invokeFloat( + return invoke( out, [&] { return hostFunctions_.floatPower(Slice{x.data(), x.size()}, n, mode); }); }); } From 694fbb7ce3be2ccbae58254aea8e7662a0e7db9f Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Tue, 11 Aug 2026 15:57:15 -0400 Subject: [PATCH 45/46] fix: Merge upstream branch --- crates/xrpl-host-functions/src/lib.rs | 5 ---- .../tests/generated_abi.rs | 2 -- crates/xrpl-wasm-vm-ffi/src/lib.rs | 17 ------------ crates/xrpl-wasm-vm/src/abi.rs | 6 ++--- crates/xrpl-wasm-vm/src/register.rs | 26 +++++++++---------- include/xrpl/json/json_reader.h | 4 +-- src/libxrpl/json/json_reader.cpp | 7 ++--- 7 files changed, 22 insertions(+), 45 deletions(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index 5297d54b2f..e8ae0e7392 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -394,11 +394,6 @@ host_functions! { #[wasm_name = "trace"] fn trace(&self, msg: &str, data: &[u8], data_type: TraceDataType) -> HostResult<()>; - /// Writes `msg` and `number` to the trace log. - #[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. diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index 5504bc8fac..a7b087a85c 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -785,7 +785,6 @@ fn the_trait_is_implementable() { assert_eq!(host.sha512_half(b"abc", &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 3); assert_eq!(host.trace("hello", b"xy", TraceDataType::AsHex), Ok(())); - assert_eq!(host.trace_num("count", -1), Ok(())); assert_eq!(host.update_data(b"abcd"), Ok(4)); assert_eq!(host.get_nft(&[7; 20], &[9; 32], &mut out), Ok(HASH_LEN)); assert_eq!(out[0], 7); @@ -930,7 +929,6 @@ fn the_spec_table_matches_the_declarations() { ("vault_id", 350), ("sha512_half", 2000), ("trace", 30), - ("trace_num", 500), ("set_data", 1000), ("nft_uri", 5000), ("nft_issuer", 70), diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index d24ee71c12..fc49626613 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -1183,28 +1183,11 @@ mod tests { assert_eq!(bytes_written(0), Ok(0)); assert_eq!(bytes_written(-3), Err(HostError::BufferTooSmall)); assert_eq!(bytes_written(-14), Err(HostError::NoMemExported)); - assert_eq!(reported(0), Ok(())); - assert_eq!(reported(-14), Err(HostError::NoMemExported)); 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 `InternalFatal`, the code - /// `HostContext` answers with when a body throws. The engine stops the run on it and - /// the transaction is `tecINTERNAL`, rather than the contract being handed a code to - /// interpret. - /// - /// It arrives through the sign test like any other code, which is the point of - /// choosing a negative sentinel: `usize::try_from` rejects it, so this needs no case - /// of its own here and a positive length cannot be mistaken for it. - #[test] - 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!(scalar(-1), Err(HostError::Internal)); - } - #[test] fn a_caught_cxx_exception_arrives_as_internal_fatal() { assert_eq!(bytes_written(i32::MIN), Err(HostError::InternalFatal)); diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index 73b1b564e0..57e9dd6a36 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -286,7 +286,7 @@ pub(crate) fn write_mant_exp( mantissa_out: Region, exponent_out: Region, call: impl FnOnce(&dyn HostFunctions, &[u8], &mut [u8], &mut [u8]) -> HostResult, -) -> HostResult { +) -> CallResult { let mem = memory(caller)?; let (data, state) = mem.data_and_store_mut(&mut *caller); let host: &dyn HostFunctions = state.host; @@ -306,7 +306,7 @@ pub(crate) fn write_mant_exp( .get_mut(mant_range) .ok_or(HostError::PointerOutOfBounds)?; if mant_dst.len() < MANTISSA_BYTES { - return Err(HostError::BufferTooSmall); + return Err(HostError::BufferTooSmall.into()); } mant_dst[..MANTISSA_BYTES].copy_from_slice(&state.out_buffer[..MANTISSA_BYTES]); @@ -315,7 +315,7 @@ pub(crate) fn write_mant_exp( .get_mut(exp_range) .ok_or(HostError::PointerOutOfBounds)?; if exp_dst.len() < EXPONENT_BYTES { - return Err(HostError::BufferTooSmall); + return Err(HostError::BufferTooSmall.into()); } exp_dst[..EXPONENT_BYTES] .copy_from_slice(&state.out_buffer[MANTISSA_BYTES..MANTISSA_BYTES + EXPONENT_BYTES]); diff --git a/crates/xrpl-wasm-vm/src/register.rs b/crates/xrpl-wasm-vm/src/register.rs index b6bc6e69fd..7a31a34b9c 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -87,7 +87,7 @@ pub(crate) fn register_host_functions( charged(&mut caller, HostFunctionSpec::IsAmendmentEnabled, |c| { let host = c.data().host; let amendment = read_borrowed(c, Region::new(ptr, len))?; - host.is_amendment_enabled(amendment) + Ok(host.is_amendment_enabled(amendment)?) }) }, ), @@ -102,7 +102,7 @@ pub(crate) fn register_host_functions( 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) + Ok(host.cache_ledger_obj(obj_id, cache_idx)?) }) }, ), @@ -229,7 +229,7 @@ pub(crate) fn register_host_functions( op.wasm_name(), |mut caller: Caller<'_, VmState<'_>>, field: i32| -> Result { charged(&mut caller, HostFunctionSpec::GetTxArrayLen, |c| { - c.data().host.get_tx_array_len(field) + Ok(c.data().host.get_tx_array_len(field)?) }) }, ), @@ -240,7 +240,7 @@ pub(crate) fn register_host_functions( charged( &mut caller, HostFunctionSpec::GetCurrentLedgerObjArrayLen, - |c| c.data().host.get_current_ledger_obj_array_len(field), + |c| Ok(c.data().host.get_current_ledger_obj_array_len(field)?), ) }, ), @@ -252,7 +252,7 @@ pub(crate) fn register_host_functions( field: i32| -> Result { charged(&mut caller, HostFunctionSpec::GetLedgerObjArrayLen, |c| { - c.data().host.get_ledger_obj_array_len(cache_idx, field) + Ok(c.data().host.get_ledger_obj_array_len(cache_idx, field)?) }) }, ), @@ -266,7 +266,7 @@ pub(crate) fn register_host_functions( charged(&mut caller, HostFunctionSpec::GetTxNestedArrayLen, |c| { let host = c.data().host; let locator = read_borrowed(c, Region::new(loc_ptr, loc_len))?; - host.get_tx_nested_array_len(locator) + Ok(host.get_tx_nested_array_len(locator)?) }) }, ), @@ -283,7 +283,7 @@ pub(crate) fn register_host_functions( |c| { let host = c.data().host; let locator = read_borrowed(c, Region::new(loc_ptr, loc_len))?; - host.get_current_ledger_obj_nested_array_len(locator) + Ok(host.get_current_ledger_obj_nested_array_len(locator)?) }, ) }, @@ -302,7 +302,7 @@ pub(crate) fn register_host_functions( |c| { let host = c.data().host; let locator = read_borrowed(c, Region::new(loc_ptr, loc_len))?; - host.get_ledger_obj_nested_array_len(cache_idx, locator) + Ok(host.get_ledger_obj_nested_array_len(cache_idx, locator)?) }, ) }, @@ -323,7 +323,7 @@ pub(crate) fn register_host_functions( let message = read_borrowed(c, Region::new(msg_ptr, msg_len))?; let signature = read_borrowed(c, Region::new(sig_ptr, sig_len))?; let pubkey = read_borrowed(c, Region::new(pk_ptr, pk_len))?; - host.check_signature(message, signature, pubkey) + Ok(host.check_signature(message, signature, pubkey)?) }) }, ), @@ -827,7 +827,7 @@ pub(crate) fn register_host_functions( 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(host.update_data(data)?) }) }, ), @@ -898,7 +898,7 @@ pub(crate) fn register_host_functions( charged(&mut caller, HostFunctionSpec::GetNftFlags, |c| { let host = c.data().host; let nft_id = read_borrowed(c, Region::new(nft_ptr, nft_len))?; - host.get_nft_flags(nft_id) + Ok(host.get_nft_flags(nft_id)?) }) }, ), @@ -912,7 +912,7 @@ pub(crate) fn register_host_functions( charged(&mut caller, HostFunctionSpec::GetNftTransferFee, |c| { let host = c.data().host; let nft_id = read_borrowed(c, Region::new(nft_ptr, nft_len))?; - host.get_nft_transfer_fee(nft_id) + Ok(host.get_nft_transfer_fee(nft_id)?) }) }, ), @@ -1077,7 +1077,7 @@ pub(crate) fn register_host_functions( 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) + Ok(host.float_compare(x, y)?) }) }, ), diff --git a/include/xrpl/json/json_reader.h b/include/xrpl/json/json_reader.h index f7775b963d..ed60f49ce4 100644 --- a/include/xrpl/json/json_reader.h +++ b/include/xrpl/json/json_reader.h @@ -74,8 +74,8 @@ public: * their location in the parsed document. An empty string is returned if no * error occurred during parsing. */ - static [[nodiscard]] std::string - getFormattedErrorMessages(); + [[nodiscard]] std::string + getFormattedErrorMessages() const; static constexpr unsigned kNestLimit{25}; diff --git a/src/libxrpl/json/json_reader.cpp b/src/libxrpl/json/json_reader.cpp index 0d1f159f5f..8598f94491 100644 --- a/src/libxrpl/json/json_reader.cpp +++ b/src/libxrpl/json/json_reader.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace json { // Implementation of class Reader @@ -77,7 +78,7 @@ Reader::parse(std::istream& sin, Value& root) // Since std::string is reference-counted, this at least does not // create an extra copy. - std::string const doc; + std::string doc; std::getline(sin, doc, (char)EOF); return parse(doc, root); } @@ -612,7 +613,7 @@ Reader::decodeDouble(Token& token) return addError("Unable to parse token length", token); } - double const value = 0; + double value = 0; auto const [ptr, ec] = fast_float::from_chars(token.start, token.end, value); // Reject anything from_chars could not turn into a finite double: @@ -895,7 +896,7 @@ Reader::getLocationLineAndColumn(Location location) const } std::string -Reader::getFormattedErrorMessages() +Reader::getFormattedErrorMessages() const { std::string formattedMessage; From e91a30d0047ef2275ccc574748f9093c44dc017b Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Tue, 11 Aug 2026 16:03:15 -0400 Subject: [PATCH 46/46] fix: Merge upstream branch --- src/tests/libxrpl/tx/wasm/host_calls/Trace.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/libxrpl/tx/wasm/host_calls/Trace.cpp b/src/tests/libxrpl/tx/wasm/host_calls/Trace.cpp index 9336595ead..349785e1fa 100644 --- a/src/tests/libxrpl/tx/wasm/host_calls/Trace.cpp +++ b/src/tests/libxrpl/tx/wasm/host_calls/Trace.cpp @@ -11,6 +11,7 @@ #include #include // For `TraceDataType`, which the bridge declares and this header defines. +#include #include #include