From 3caaecff076b54bd3ba4d93093d234809266d1b0 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Mon, 10 Aug 2026 17:39:53 -0400 Subject: [PATCH] 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