Merge remote-tracking branch 'upstream/ripple/wasmi-host-functions' into mvadari/se-wasmi-tests-update-host-functions

# Conflicts:
#	.cspell.config.yaml
#	src/test/app/Wasm_test.cpp
#	src/test/app/wasm_fixtures/all_host_functions/src/lib.rs
#	src/test/app/wasm_fixtures/copyFixtures.py
#	src/test/app/wasm_fixtures/fixtures.cpp
#	src/test/app/wasm_fixtures/fixtures.h
This commit is contained in:
Mayukha Vadari
2026-09-08 13:51:51 -04:00
232 changed files with 4875 additions and 319 deletions

View File

@@ -1,6 +1,9 @@
benchmarks.libxrpl > xrpl.basics
benchmarks.libxrpl > xrpl.config
benchmarks.libxrpl > xrpl.nodestore
benchmarks.libxrpl > xrpl.protocol
benchmarks.libxrpl > xrpl.protocol_autogen
benchmarks.libxrpl > xrpl.tx
libxrpl.basics > xrpl.basics
libxrpl.conditions > xrpl.basics
libxrpl.conditions > xrpl.conditions

View File

@@ -29,6 +29,14 @@ function(xrpl_add_benchmark name)
# XrplCore.cmake. Each file compiles fine on its own.
set_target_properties(${target} PROPERTIES UNITY_BUILD OFF)
# Land next to `xrpl_tests` in the build root rather than buried under
# `src/benchmarks/libxrpl/`. A benchmark is something a person runs by hand,
# repeatedly, and comparing two of them should not mean typing two long paths.
set_target_properties(
${target}
PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
)
isolate_headers(
${target}
"${CMAKE_SOURCE_DIR}/src"

17
crates/Cargo.lock generated
View File

@@ -348,9 +348,9 @@ dependencies = [
[[package]]
name = "wasmi"
version = "2.0.0-beta.10"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab57cbb8db5ee46c6667b642544d7664adfbc0ea6a1ab219c92d734b795f36b1"
checksum = "78693fcdd618e0fc34af59c6b8efa9ac5d58c68df940beff4bedddb6acfe7c27"
dependencies = [
"spin",
"wasmi_collections",
@@ -361,27 +361,27 @@ dependencies = [
[[package]]
name = "wasmi_collections"
version = "2.0.0-beta.10"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ea3ee266456966465c55a1f440e33116caf2b05a4fc30da36cb0c9813059d5"
checksum = "8a8be2aa467cf2d29e96ff759472c36eeb44a3c81c67fc9cb76c9a24c519c557"
dependencies = [
"string-interner",
]
[[package]]
name = "wasmi_core"
version = "2.0.0-beta.10"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f8285efe48a9e1afbcdfcc19cd807b3eb20129b7e199c7a99efd30ba192926b"
checksum = "69372d5fda3ea3d1e0aa6603c7888110e0187e88ea17cd8fc2e2df0a0e1f37fa"
dependencies = [
"libm",
]
[[package]]
name = "wasmi_ir"
version = "2.0.0-beta.10"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6227be1aebba39b4815ab6a312d0528590f0db2473621ec9285606410889b0a6"
checksum = "8f17b774caa13c618c7244f1ee51fe23c5e7b8538a471fa46d9949779758aed6"
dependencies = [
"wasmi_core",
]
@@ -476,6 +476,7 @@ version = "0.1.0"
dependencies = [
"cxx",
"wat",
"xrpl-host-functions",
]
[[package]]

View File

@@ -9,3 +9,4 @@ crate-type = ["staticlib", "rlib"]
[dependencies]
cxx.workspace = true
wat = "1"
xrpl-host-functions = { path = "../xrpl-host-functions" }

View File

@@ -19,6 +19,18 @@ mod ffi {
/// Throws `rust::Error` on invalid input, which is what a test wants: a typo in a
/// fixture should fail the test that holds it, at the line that holds it.
fn compile_wat(wat: &str) -> Result<Vec<u8>>;
/// The gas a host function is charged before it runs, by its guest import name.
///
/// For the C++ gas benchmarks, which measure what a host call actually costs and
/// report it against what the table says it costs. Reading the declaration through
/// here rather than copying the numbers into C++ is the point: 61 transcribed
/// constants would drift from `lib.rs` the first time a price changed, and drift
/// silently, because a benchmark has nothing to fail.
///
/// Throws `rust::Error` on an unknown name — a typo should fail loudly rather than
/// quietly compare against zero.
fn host_function_gas(wasm_name: &str) -> Result<u64>;
}
}
@@ -26,9 +38,28 @@ fn compile_wat(wat: &str) -> Result<Vec<u8>, wat::Error> {
wat::parse_str(wat)
}
fn host_function_gas(wasm_name: &str) -> Result<u64, UnknownHostFunction> {
xrpl_host_functions::HostFunctionSpec::ALL
.iter()
.find(|op| op.wasm_name() == wasm_name)
.map(|op| op.gas())
.ok_or_else(|| UnknownHostFunction(wasm_name.to_owned()))
}
#[derive(Debug)]
struct UnknownHostFunction(String);
impl std::fmt::Display for UnknownHostFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "no host function is imported as `{}`", self.0)
}
}
impl std::error::Error for UnknownHostFunction {}
#[cfg(test)]
mod tests {
use super::compile_wat;
use super::*;
#[test]
fn a_module_assembles_to_something_beginning_with_the_wasm_magic() {
@@ -37,6 +68,33 @@ mod tests {
assert_eq!(&wasm[..4], b"\0asm");
}
#[test]
fn a_host_function_reports_the_gas_its_declaration_gives_it() {
// `trace` is the cheapest declaration in the table; the point is not the number but
// that the lookup reaches the same constant the engine charges from.
assert_eq!(
host_function_gas("trace").expect("trace is a host function"),
xrpl_host_functions::HostFunctionSpec::Trace.gas()
);
}
#[test]
fn every_host_function_is_reachable_by_its_import_name() {
for op in xrpl_host_functions::HostFunctionSpec::ALL {
assert_eq!(
host_function_gas(op.wasm_name()).expect("declared"),
op.gas(),
"{} must be reachable by name",
op.wasm_name()
);
}
}
#[test]
fn an_unknown_name_is_an_error_rather_than_zero_gas() {
host_function_gas("not_a_host_function").expect_err("must not resolve");
}
#[test]
fn a_typo_is_an_error_rather_than_a_module() {
let error = compile_wat("(module (func (export").expect_err("must not assemble");

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
edition.workspace = true
[dependencies]
wasmi = { version = "2.0.0-beta.10", default-features = false, features = ["std", "validate", "portable-dispatch"] }
wasmi = { version = "2.0.0", default-features = false, features = ["std", "validate", "portable-dispatch"] }
xrpl-host-functions = { path = "../xrpl-host-functions" }
[dev-dependencies]

View File

@@ -1247,3 +1247,142 @@ fn the_outcome_carries_whatever_the_guest_returned() {
assert_eq!(outcome.result, value);
}
}
/// The remaining binary operators marshal like `float_add`: both operands and the mode reach
/// the host, tagged by operator.
#[test]
fn float_sub_reads_both_operands_and_the_mode() {
let host = FakeHost::new().answering_float(support::Answer::filler(8));
let wat = module(
&[import::FLOAT_SUB, ONE_PAGE],
"(call $float_sub (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);
assert_eq!(
*host.float_binary_ops_asked.borrow(),
vec![("sub", vec![0u8; 8], vec![0u8; 8], 2)]
);
}
#[test]
fn float_mult_reads_both_operands_and_the_mode() {
let host = FakeHost::new().answering_float(support::Answer::filler(8));
let wat = module(
&[import::FLOAT_MULT, ONE_PAGE],
"(call $float_mult (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);
assert_eq!(
*host.float_binary_ops_asked.borrow(),
vec![("mult", vec![0u8; 8], vec![0u8; 8], 2)]
);
}
#[test]
fn float_div_reads_both_operands_and_the_mode() {
let host = FakeHost::new().answering_float(support::Answer::filler(8));
let wat = module(
&[import::FLOAT_DIV, ONE_PAGE],
"(call $float_div (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);
assert_eq!(
*host.float_binary_ops_asked.borrow(),
vec![("div", vec![0u8; 8], vec![0u8; 8], 2)]
);
}
#[test]
fn float_pow_reads_the_float_the_degree_and_the_mode() {
let host = FakeHost::new().answering_float(support::Answer::filler(8));
let wat = module(
&[import::FLOAT_POW, ONE_PAGE],
"(call $float_pow (i32.const 0) (i32.const 8) (i32.const 3) (i32.const 64) (i32.const 8) (i32.const 1))",
);
assert_eq!(status(&wat, &host), 8);
assert_eq!(
*host.float_unary_ops_asked.borrow(),
vec![("pow", vec![0u8; 8], 3, 1)]
);
}
/// The `ST*`-in operators read one serialized region and a mode, and write the float.
#[test]
fn float_from_stamount_reads_the_amount_and_the_mode() {
let host = FakeHost::new().answering_float(support::Answer::filler(8));
let wat = module(
&[import::FLOAT_FROM_STAMOUNT, ONE_PAGE],
"(call $float_from_stamount (i32.const 0) (i32.const 8) (i32.const 64) (i32.const 8) (i32.const 2))",
);
assert_eq!(status(&wat, &host), 8);
assert_eq!(
*host.float_from_stamount_asked.borrow(),
vec![(vec![0u8; 8], 2)]
);
}
#[test]
fn float_from_stnumber_reads_the_number_and_the_mode() {
let host = FakeHost::new().answering_float(support::Answer::filler(8));
let wat = module(
&[import::FLOAT_FROM_STNUMBER, ONE_PAGE],
"(call $float_from_stnumber (i32.const 0) (i32.const 8) (i32.const 64) (i32.const 8) (i32.const 2))",
);
assert_eq!(status(&wat, &host), 8);
assert_eq!(
*host.float_from_stnumber_asked.borrow(),
vec![(vec![0u8; 8], 2)]
);
}
/// `float_to_int` reads a float and a mode, writing the integer.
#[test]
fn float_to_int_reads_the_float_and_the_mode() {
let host = FakeHost::new().answering_float(support::Answer::filler(8));
let wat = module(
&[import::FLOAT_TO_INT, ONE_PAGE],
"(call $float_to_int (i32.const 0) (i32.const 8) (i32.const 64) (i32.const 8) (i32.const 2))",
);
assert_eq!(status(&wat, &host), 8);
assert_eq!(*host.float_to_int_asked.borrow(), vec![(vec![0u8; 8], 2)]);
}
/// `float_from_mant_exp` passes two scalars (an i64 mantissa and an i32 exponent) and a mode.
#[test]
fn float_from_mant_exp_passes_the_mantissa_exponent_and_mode() {
let host = FakeHost::new().answering_float(support::Answer::filler(8));
let wat = module(
&[import::FLOAT_FROM_MANT_EXP, ONE_PAGE],
"(call $float_from_mant_exp (i64.const 42) (i32.const 3) (i32.const 64) (i32.const 8) (i32.const 1))",
);
assert_eq!(status(&wat, &host), 8);
assert_eq!(*host.float_from_mant_exp_asked.borrow(), vec![(42, 3, 1)]);
}
/// A host input read from an unaligned pointer arrives intact: the host reads a byte slice, not
/// an aligned word, so a contract that hands it an odd offset is served the right bytes. (Ports
/// the old `bad_align` fixture, which exercised unaligned reads in the C-ABI wrapper.)
#[test]
fn an_unaligned_input_is_read_intact() {
let host = FakeHost::new().answering_float(support::Answer::filler(8));
let stores: String = (0..8u8)
.map(|i| {
format!(
"(i32.store8 (i32.const {}) (i32.const {}))",
i + 1,
0xa0 + i
)
})
.collect();
let wat = module(
&[import::FLOAT_FROM_STAMOUNT, ONE_PAGE],
&format!(
"{stores} (call $float_from_stamount (i32.const 1) (i32.const 8) (i32.const 64) (i32.const 8) (i32.const 0))"
),
);
assert_eq!(status(&wat, &host), 8);
assert_eq!(
*host.float_from_stamount_asked.borrow(),
vec![((0xa0u8..0xa8u8).collect::<Vec<u8>>(), 0)]
);
}

View File

@@ -593,3 +593,61 @@ fn a_memory64_memory_is_refused_by_screening() {
"{refusal}"
);
}
/// The corruption fixtures below are written as hex strings, which is how the old Beast suite
/// carried them — the bytes are deliberately malformed, so there is nothing to assemble them
/// from.
fn hex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
/// Malformed modules crafted to abuse the parser rather than merely be invalid — a vector
/// length that lies about its size, a section that overruns its payload, a locals-count bomb,
/// and a non-terminating LEB128 — are refused at compile like any other garbage. These guard
/// the parser against resource-exhaustion shapes (ported from the old Beast section-corruption
/// fixtures); the plainer "bad magic / wrong version" shapes are covered by `garbage_does_not_pass`.
#[test]
fn parser_abuse_shapes_are_refused() {
let cases = [
("vector length lies", "0061736d010000000105ffffffff0f"),
("section overruns its payload", "0061736d01000000010a0160"),
(
"locals-count bomb",
"0061736d01000000010401600000030201000a0f010d01ffffffff0f7f0b",
),
(
"non-terminating LEB128",
"0061736d0100000001058080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080",
),
];
for (label, h) in cases {
let refusal = xrpl_wasm_vm::check(&hex(h), ENTRY).expect_err(label);
assert_stage!(refusal, CheckError::Compile(_));
}
}
/// The plain structurally-malformed modules from the old section-corruption fixtures — a
/// corrupt magic, a wrong version, a lying section length, sections out of order, junk after
/// the last section, an unknown section id — are all refused at compile. Belt-and-suspenders
/// alongside `garbage_does_not_pass`: guards against a wasmi upgrade loosening the validator.
#[test]
fn structurally_malformed_modules_are_refused() {
let cases = [
("corrupt magic number", "0161736d01000000"),
("wrong version", "0061736d02000000"),
("lying section length", "0061736d01000000018080808008"),
("sections out of order", "0061736d010000000a02000b03020000"),
(
"junk after last section",
"0061736d01000000010a01600000000000000000",
),
("unknown section id", "0061736d01000000ff0100"),
];
for (label, h) in cases {
let refusal = xrpl_wasm_vm::check(&hex(h), ENTRY).expect_err(label);
assert_stage!(refusal, CheckError::Compile(_));
}
}

View File

@@ -598,3 +598,114 @@ fn a_memory64_module_is_rejected_at_compile() {
"rejected before instantiation, so nothing is charged: {failure}"
);
}
/// A function declaring more parameters than wasm allows (1000) is refused at compile, so a
/// contract cannot smuggle an unbounded signature past screening.
#[test]
fn a_function_with_too_many_params_is_refused() {
let host = FakeHost::new();
let params = " i32".repeat(1001);
let wat = format!(
"(module {ONE_PAGE} (func (param{params}) (result i32) (i32.const 0)) \
(func (export \"finish\") (result i32) (i32.const 0)))"
);
assert_stage!(failure(&wat, &host), RunError::Compile(_));
}
/// A function declaring more locals than wasm allows (50 000) is refused at compile.
#[test]
fn a_function_with_too_many_locals_is_refused() {
let host = FakeHost::new();
let locals = format!("(local{})", " i32".repeat(50_001));
let wat = module(&[ONE_PAGE], &format!("{locals} (i32.const 0)"));
assert_stage!(failure(&wat, &host), RunError::Compile(_));
}
/// Below the compile cap but past the engine's register frame, a locals-heavy function is
/// refused when the frame is built rather than at compile — still refused, just later.
#[test]
fn a_function_past_the_register_frame_is_refused() {
let host = FakeHost::new();
let locals = format!("(local{})", " i32".repeat(40_000));
let wat = module(&[ONE_PAGE], &format!("{locals} (i32.const 0)"));
assert_stage!(failure(&wat, &host), RunError::Trap(_));
}
/// Unbounded recursion is stopped by the engine's call-stack limit — it traps rather than
/// running the host's native stack off the end (the portable dispatcher makes loops safe;
/// this pins that guest *calls* are bounded too).
#[test]
fn unbounded_recursion_is_stopped_by_the_call_stack_limit() {
let host = FakeHost::new();
let wat = format!(
"(module {ONE_PAGE} \
(func $rec (param i32) (result i32) \
(if (result i32) (i32.eqz (local.get 0)) (then (i32.const 0)) \
(else (call $rec (i32.sub (local.get 0) (i32.const 1)))))) \
(func (export \"finish\") (result i32) (call $rec (i32.const 1000000))))"
);
assert_stage!(failure(&wat, &host), RunError::Trap(_));
}
/// A module with many functions currently compiles and runs: wasmi's only cap is its
/// 1,000,000 hard limit, so the ticket's ~24k-function module — a CodeMap-growth DoS, since
/// every validation appends to the engine's append-only CodeMap — is not refused here.
/// Enforcing a tighter bound (a function-count / average-bytes-per-function limit) belongs in
/// a future preflight pass that parses the module before the engine sees it. Ignored until
/// then, so this documents the gap without asserting it is acceptable.
#[test]
#[ignore = "CodeMap-DoS unmitigated; a function-count limit is deferred to preflight parsing"]
fn many_functions_currently_run_unbounded() {
let host = FakeHost::new();
let funcs: String = (0..24_000)
.map(|i| format!("(func $f{i} (result i32) (i32.const {}))", i % 7))
.collect();
let wat =
format!("(module {ONE_PAGE} {funcs} (func (export \"finish\") (result i32) (call $f0)))");
assert!(
run(&wat, &host).is_ok(),
"a large-function module currently compiles and runs"
);
}
/// The trap *kinds* wasmi distinguishes all reach the caller identically — a guest trap
/// charged as the contract's fault — so the `unreachable` representative pins the mapping.
/// These pin the individual kinds too, guarding against a wasmi upgrade reclassifying any of
/// them as something other than a trap.
#[test]
fn a_division_by_zero_traps() {
let host = FakeHost::new();
let wat = module(&[ONE_PAGE], "(i32.div_s (i32.const 1) (i32.const 0))");
assert_stage!(failure(&wat, &host), RunError::Trap(_));
}
#[test]
fn a_signed_integer_overflow_traps() {
let host = FakeHost::new();
let wat = module(
&[ONE_PAGE],
"(i32.div_s (i32.const 0x80000000) (i32.const -1))",
);
assert_stage!(failure(&wat, &host), RunError::Trap(_));
}
#[test]
fn an_indirect_call_to_a_null_table_entry_traps() {
let host = FakeHost::new();
let wat = format!(
"(module {ONE_PAGE} (type $t (func (result i32))) (table 1 funcref) \
(func (export \"finish\") (result i32) (call_indirect (type $t) (i32.const 0))))"
);
assert_stage!(failure(&wat, &host), RunError::Trap(_));
}
#[test]
fn an_indirect_call_with_a_mismatched_signature_traps() {
let host = FakeHost::new();
let wat = format!(
"(module {ONE_PAGE} (type $void (func)) (type $i32 (func (result i32))) \
(table 1 funcref) (elem (i32.const 0) $f) (func $f (type $void)) \
(func (export \"finish\") (result i32) (call_indirect (type $i32) (i32.const 0))))"
);
assert_stage!(failure(&wat, &host), RunError::Trap(_));
}

View File

@@ -20,3 +20,20 @@ target_link_libraries(
xrpl_add_benchmark(nodestore)
target_link_libraries(xrpl.bench.nodestore PRIVATE xrpl.imports.bench)
add_dependencies(xrpl.benchmarks xrpl.bench.nodestore)
# Gas calibration for the wasm host functions. The ledger and real host come from
# `xrpl.testkit.wasm` (built with the tests, but framework-free), so this target links
# no GTest and no GMock.
if(TARGET xrpl.testkit.wasm)
xrpl_add_benchmark(wasm)
target_link_libraries(
xrpl.bench.wasm
PRIVATE xrpl.imports.bench xrpl.testkit.wasm
)
add_dependencies(xrpl.benchmarks xrpl.bench.wasm)
else()
message(
STATUS
"xrpl.testkit.wasm not built (tests disabled); skipping xrpl.bench.wasm."
)
endif()

View File

@@ -0,0 +1,175 @@
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol_autogen/transactions/EscrowCreate.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <helpers/Account.h>
#include <helpers/TxTest.h>
#include <tx/wasm/fixtures/FloatConstants.h>
#include <tx/wasm/fixtures/NftSetup.h>
#include <tx/wasm/fixtures/WasmLedger.h>
#include <string>
#include <string_view>
#include <utility>
namespace xrpl::test::bench {
Fixtures::Fixtures()
: alice_{ledger_.fund("benchAlice")}
, bob_{ledger_.fund("benchBob")}
, signerListOwner_{ledger_.fund("benchSigners")}
, escrow_{keylet::account(AccountID{})}
, signedMessage_{signMessage("the quick brown fox jumps over the lazy dog")}
, nftId_{NftIds::makeNftId(alice_.id())}
{
ledger_.makeSignerList(signerListOwner_, 2, {{alice_, 1}, {bob_, 1}});
// The escrow has to be submitted after the accounts exist, which is why it is built here
// rather than in the initializer list: its keylet depends on the owner's sequence number at
// submission time.
auto const ownerSeq = ledger_.ledger.getAccountRoot(alice_.id()).getSequence();
auto const created = ledger_.ledger.submit(
transactions::EscrowCreateBuilder{alice_.id(), bob_.id(), XRP(100)}.setFinishAfter(
900'000'000),
alice_);
if (created.ter != tesSUCCESS)
{
fixtureFailed(std::string{"creating the escrow: "} + transToken(created.ter));
}
ledger_.ledger.close();
escrow_ = keylet::escrow(alice_.id(), SeqProxy::rawSequence(ownerSeq));
}
Account const&
Fixtures::alice() const
{
return alice_;
}
Account const&
Fixtures::bob() const
{
return bob_;
}
TxAssembler
Fixtures::memoTx()
{
auto assembler = escrowFinishTx(ledger_.ledger, alice_);
assembler.build = [inner = std::move(assembler.build)](STObject& obj) {
inner(obj);
auto memos = STArray{};
memos.push_back(makeMemo(WasmLedger::toBytes("hello")));
memos.push_back(makeMemo(WasmLedger::toBytes("world")));
obj.setFieldArray(sfMemos, memos);
};
return assembler;
}
FieldLocator
Fixtures::memoLocator()
{
return FieldLocator{{sfMemos.getCode(), 0, sfMemoData.getCode()}};
}
WasmHost
Fixtures::host()
{
auto assembler = memoTx();
return ledger_.makeHost(
keylet::account(alice_.id()), assembler.type, std::move(assembler.build));
}
WasmHost
Fixtures::cachedHost()
{
auto wasmHost = host();
if (!wasmHost->cacheLedgerObj(keylet::account(alice_.id()).key, 1).has_value())
{
fixtureFailed("caching the account root into slot 1");
}
return wasmHost;
}
WasmHost
Fixtures::signerListHost()
{
auto assembler = bareTx();
return ledger_.makeHost(
keylet::signerList(signerListOwner_.id()), assembler.type, std::move(assembler.build));
}
WasmHost
Fixtures::cachedSignerListHost()
{
auto assembler = bareTx();
auto wasmHost =
ledger_.makeHost(keylet::account(AccountID{}), assembler.type, std::move(assembler.build));
if (!wasmHost->cacheLedgerObj(keylet::signerList(signerListOwner_.id()).key, 1).has_value())
{
fixtureFailed("caching the signer list into slot 1");
}
return wasmHost;
}
WasmHost
Fixtures::tracingHost()
{
return ledger_.makeTracingHost();
}
Keylet const&
Fixtures::escrow() const
{
return escrow_;
}
WasmHost
Fixtures::escrowHost()
{
return ledger_.makeHost(escrow_);
}
Slice
Fixtures::floatX()
{
return FloatConstants::slice(FloatConstants::kPi);
}
Slice
Fixtures::floatY()
{
return FloatConstants::slice(FloatConstants::kTwo);
}
SignedMessage const&
Fixtures::signedMessage() const
{
return signedMessage_;
}
uint256 const&
Fixtures::nftId() const
{
return nftId_;
}
Fixtures&
Fixtures::instance()
{
static Fixtures kValue;
return kValue;
}
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,111 @@
#pragma once
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <helpers/Account.h>
#include <tx/wasm/fixtures/WasmLedger.h>
#include <cstdint>
namespace xrpl::test::bench {
// The ledger and canned inputs every `*.bench.cpp` measures against.
class Fixtures
{
public:
// The one set of fixtures every benchmark shares, built on first use.
static Fixtures&
instance();
// A sequence number for the keylets that take one. Arbitrary — a keylet hashes whatever it
// is given, so the value cannot change the cost.
static constexpr std::uint32_t kSeq = 42;
// Rounding mode 0 throughout the float family: modes select a tie-breaking rule, not a
// different algorithm, so they do not move the cost, and pinning one keeps the fourteen
// comparable.
static constexpr std::int32_t kRoundingMode = 0;
// Two funded accounts, enough for every keylet shape and every object below.
[[nodiscard]] Account const&
alice() const;
[[nodiscard]] Account const&
bob() const;
// The default host: its transaction carries a two-element memo array (something for the
// nested getters to walk to and the array-length getters to count) and its current object is
// Alice's account root.
[[nodiscard]] WasmHost
host();
// The same, with Alice's account root pinned to slot 1, for the `le_*` getters that read
// through a cache slot rather than the current object.
[[nodiscard]] WasmHost
cachedHost();
// An account root has no arrays, so the array-length getters that read a *ledger object*
// need a different one. A signer list has `sfSignerEntries`; without it those calls would
// answer `FieldNotFound` and the benchmark would time the rejection instead of the work.
[[nodiscard]] WasmHost
signerListHost();
[[nodiscard]] WasmHost
cachedSignerListHost();
// A host whose `trace` output is captured rather than dropped, so the log-enabled path can
// be measured against the log-disabled one that `host()` gives.
[[nodiscard]] WasmHost
tracingHost();
// A real escrow, created through the real transactor — the current object for
// `home_le_field`, the one getter whose cost depends on the object it reads rather than on
// its arguments.
[[nodiscard]] Keylet const&
escrow() const;
[[nodiscard]] WasmHost
escrowHost();
// `sfMemos[0].sfMemoData` — a two-step locator path, the shape the nested getters are priced
// for.
[[nodiscard]] static FieldLocator
memoLocator();
// Canonical float operands. Zeroed bytes decode as a non-canonical float and would be
// refused before any arithmetic ran, so the whole family shares these two known-good values.
[[nodiscard]] static Slice
floatX();
[[nodiscard]] static Slice
floatY();
// A signed message for `check_sig`. Signing is far more expensive than the verification
// being measured, so it happens once here rather than inside a timed loop.
[[nodiscard]] SignedMessage const&
signedMessage() const;
// A well-formed NFToken id with the fixture's known taxon, flags, fee and sequence baked in,
// so the id-extractor getters have real fields to pull out rather than zeros.
[[nodiscard]] uint256 const&
nftId() const;
private:
// Order matters, and that is the reason this is a constructor rather than a pile of lazy
// statics: the accounts have to be funded before the signer list and the escrow can be built
// on them.
Fixtures();
// The transaction the default host runs, carrying the memo array.
[[nodiscard]] TxAssembler
memoTx();
WasmLedger ledger_;
Account alice_;
Account bob_;
Account signerListOwner_;
Keylet escrow_;
SignedMessage signedMessage_;
uint256 nftId_;
};
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,44 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
// The harness checking itself.
//
// This file holds no host function — it belongs to the wasm directory rather than
// `host_functions/` because it measures the two reference points every per-function number is read
// against, and neither is a host call.
//
// `GuestInstruction` runs a contract whose "host call" is a couple of guest instructions. Its
// `implied_gas` and `charged_gas` are then two independent measurements of the same quantity — one
// from wall time via `secondsPerGas`, one from the engine's own fuel meter — and they should agree
// closely. When they diverge, `secondsPerGas` has measured something other than a guest
// instruction and no other number in the run is trustworthy. Read this first.
//
// The crossing floor is the other reference point, and it lives in `host_functions/LedgerSqn`:
// `ldgr_index` takes no input and answers from a header already in hand, so its impl is as close
// to nothing as a host function gets, and whatever its `ThroughVm` case costs above its `Impl`
// case is the price of leaving the guest — paid by every one of the 61 functions before any of
// them does any work.
//
// So the reading order across the suite is: this file, then `LedgerSqn`'s pair for the floor, then
// a function's own `Impl` number. Those three should account for its `ThroughVm` number; where
// they do not, the gap is size-dependent copying, which the swept cases (`Sha512Half`,
// `UpdateData`) expose.
void
guestInstruction(benchmark::State& state)
{
static constexpr std::string_view kBody = "(i32.add (local.get $r) (i32.const 1))";
// Empty import name: this case prices no host function, so there is nothing to look a
// declaration up for and it reports no `suggested_gas`.
benchmarkThroughVm(state, "", "", "", kBody, [] { return Fixtures::instance().host(); });
}
BENCHMARK(guestInstruction)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,180 @@
# WASM host functions — gas calibration
These are **not tests**: nothing asserts, and a number moving is not a build failure. They answer
the question the tests cannot — whether each `#[gas = N]` in
`crates/xrpl-host-functions/src/lib.rs` matches what the function actually costs.
```bash
cmake --build build --target xrpl.bench.wasm
./build/xrpl.bench.wasm # everything (~2 min)
./build/xrpl.bench.wasm --benchmark_filter=sha512Half
./build/xrpl.bench.wasm --benchmark_repetitions=25 --benchmark_report_aggregates_only=true
```
**Build Release.** Debug inflates the crossing far more than the impls: `Impl`-to-`Impl` ratios
survive it, `suggested_gas` does not.
## Reading the output
| Counter | Meaning |
| ------------------- | ---------------------------------------------------------------------------------- |
| `suggested_gas` | **the answer** — what this function should be priced at |
| `host_function_gas` | what `lib.rs` says today, read through the `wasm_testkit` bridge so it can't drift |
| `price_ratio` | `host_function_gas / suggested_gas`. **1.0 is correct; below 1 is underpriced** |
| `rel_error` | relative uncertainty of `suggested_gas`. **Quote this one** — ~1.2% when quiet |
| `unreliable` | `1` means do not act on this row |
| `implied_gas` | the raw measurement, before the crossing is added back |
| `charged_gas` | what the engine actually billed; confirms the right call was measured |
| `ns_per_call` | raw wall time, for debugging a suspicious ratio |
Sort by `price_ratio`. **Below 1 is the direction that matters** — an underpriced call is one a
contract can buy too cheaply, a denial-of-service vector rather than a rounding error:
```bash
./build/xrpl.bench.wasm --benchmark_format=json |
jq -r '.benchmarks[] | select(.price_ratio) | [.price_ratio, .name] | @tsv' | sort -n
```
`unreliable=1` when `rel_error` exceeds 25%, or when `suggested_gas` falls below the crossing floor
— a call whose own cost is small next to the crossing is read off the difference of two nearly
equal numbers.
### With `--benchmark_repetitions`
Adds `_mean` / `_median` / `_stddev` / `_cv` rows. One trap worth knowing:
**`cv` on `suggested_gas` for an `Impl` case is not an error bar.** The crossing floor is measured
once per process, so repetitions never resample it — and it is most of a cheap `Impl` case's value.
Over 25 repetitions on an idle machine, `Impl` `cv` reads **1.3%** against `ThroughVm`'s 2.3%, while
`rel_error` is 1.2% for both. The lowest number in the output is the least trustworthy one.
Use repetitions to confirm the machine is quiet and to get a median; quote `rel_error`. Under load
the `Impl` cases inflate first (3.0% against 1.8% at load ~10), which makes the gap between the two
families a usable load detector.
## How `suggested_gas` is measured
Gas is wasmi fuel — `set_fuel(gas)` meters guest instructions and host charges from one pool — so
one gas is about one guest instruction and the question becomes a ratio. Every step is a
subtraction, so fixed costs cancel:
```
secondsPerGas = (time_busy − time_idle) / (fuel_busy − fuel_idle) # pure-wasm loop, N vs 0
implied_gas = secondsPerCall / secondsPerGas
crossing_floor = (ldgr_index ThroughVm − ldgr_index Impl) / secondsPerGas
suggested_gas = implied_gas # ThroughVm — the guest already paid the crossing
suggested_gas = implied_gas + crossing_floor # Impl — a guest cannot call without paying it
price_ratio = host_function_gas / suggested_gas
rel_error = sqrt( ( sqrt((implied·caseErr)² + (floor·floorErr)²) / suggested )² + perGasErr² )
```
`secondsPerCall` is itself a subtraction: a `ThroughVm` case runs a contract making N host calls
against a **byte-identical** one making none, so compilation, instantiation and the guest's own loop
cancel. An `Impl` case times `kCallsPerRun` direct calls and divides.
`secondsPerGas` is measured **once** and shared by every case, deliberately — two routes to the same
price must divide by the same constant, and per-case calibration was tried and swung 6x between
runs. Its uncertainty is propagated arithmetically instead. See the comments in `WasmBench.cpp` for
why the three terms in `rel_error` do not all combine in quadrature.
**`guestInstruction` is the self-test — read it first.** It runs the calibration's own loop body, so
its `implied_gas` (wall time) and `charged_gas` (the fuel meter) are two independent measurements of
one quantity. Quiet Release machine: **≈13.7 against 13.007, ~5% high.** A persistent gap much
beyond that means every other number in the run shares it. It has already caught an estimator
mismatch worth +40%, and the memory leak below.
**`suggested_gas` for an `Impl`-only case is a lower bound** — the crossing floor is measured on a
call with no input, so a function that moves bytes pays more; `Sha512Half` and `UpdateData` sweep
that per-byte term. `ThroughVm` cases are deliberately one per crossing _shape_, not one per
function.
## VM overhead (`Vm.cpp`)
Everything above prices what a contract _asks for_. `Vm.cpp` prices getting it running at all. Of
the seven stages `runEscrowWasm` performs — compile, store, linker, instantiate, entry-point lookup,
call, fuel read — **only the call is metered**; the rest is wall time no transaction pays for.
These cases report `ns_per_op` and `gas_equivalent` (that time over the same `secondsPerGas`, so an
unpriced stage reads on the same axis as a priced one), plus `module_bytes` and `gas_per_byte` on
size sweeps.
| case | measures |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `preflightMinimal` | compile plus the import/export walk. The narrowest view of compilation reachable from C++: no host, no instance |
| `preflightRejects` | the same on a module refused at the import walk. Against `preflightMinimal`, what refusing costs versus accepting |
| `runMinimal` | a whole run of a do-nothing contract — every stage. Minus `preflightMinimal`, the stages that are not compilation |
| `compileScaling/N` | compilation against module size: `N` unreachable filler functions, preflight only |
| `runScaling/N` | the same modules through a whole run. Paired with `compileScaling` per size, separates size-dependent stages from fixed ones |
| `instantiateScaling/pages` | declared memory at constant module size, so what moves is the host allocating and zeroing pages |
Two of those pairings are the point of the file. `runMinimal − preflightMinimal` gives the fixed
cost of everything that is not compilation; `runScaling` against `compileScaling` shows compilation
appearing a second time, because the transactor validates and executes with no module cache between
them. The size sweeps matter more than the floor: a fixed cost is only a griefing concern if it is
large, but a slope against attacker-chosen module size is one at any height.
Two caveats when reading a sweep. `gas_per_byte` is an **average** carrying the case's fixed cost,
not a marginal rate — it overestimates, and falls toward the true slope as the module grows, so read
the convergence rather than any single row. And the `/4096` points get few iterations and go noisy
first; compare `rel_error` across the sweep before quoting the largest one.
The sweep stops at 4096 functions for want of a real cap to stop at — no maximum contract size is
enforced anywhere yet, the transactor not being wired.
The linker rebuild and the fuel-metering overhead are **not** separable from here — C++ sees only
`runEscrowWasm` and `preflightEscrowWasm`. Both need benchmarks inside `xrpl-wasm-vm`, where
`compile` and `wasm_engine` are `pub(crate)`.
### Compiling leaks — pin your iteration counts
`wasm_engine()` is a process-global `LazyLock<Engine>`, and what `Module::new` adds to it is never
released. Repeatedly preflighting one **60-byte** module:
| `--benchmark_repetitions` | peak RSS |
| ------------------------- | -------- |
| 1 | 0.41 GB |
| 5 | 1.46 GB |
| 15 | 4.20 GB |
Linear, at roughly **800 bytes per compile**. Within the suite this is why every `Vm.cpp` case pins
`->Iterations(...)`: automatic sizing ran `preflightMinimal` ~348k times per repetition, reaching
7.9 GB at 25 repetitions, after which every later case in the binary failed to compile — 720 errored
rows, all blaming cases that were innocent.
**Outside the suite it is worth a look.** A validator compiles twice per programmable-escrow
transaction against that same static engine. Whether that is unbounded growth in production depends
on wasmi internals not checked here — this is the C++-visible symptom, not a diagnosis.
## Gotchas, each of which has already cost someone an afternoon
- **The wasm ABI is not the trait's argument order.** `float_add(x, y, mode, out)` in Rust is
`(x_ptr, x_len, y_ptr, y_len, out_ptr, out_len, mode)` on the wire — scalars move _after_ the
output region. Check `register.rs`, not `lib.rs`, when writing WAT.
- **A soft host error still "succeeds".** The run completes and gas is charged _before_ the body, so
a wrong-argument case reports a plausible, confidently wrong number. The harness requires the
contract's result to be `>= 0`; the tell is a `ThroughVm` case coming out _faster_ than its `Impl`.
- **A host serves exactly one run** (`checkSelf` in `WasmVM.cpp`), so a benchmark builds a fresh host
per run and cannot pre-cache a slot.
- **`MAX_FIELD_BYTES` is 1024** — nothing crosses the boundary above 1 KiB, so size sweeps stop there.
- **Do not compare timings across builds at the cheap end.** Two builds whose timed regions were
byte-identical measured 2.18 ns and 2.52 ns for the same case — ~15% apart, from code layout alone.
Use `price_ratio` within one run, and reason from the code when judging whether a change costs
anything.
- **Every case pins `->Iterations(...)`, for two different reasons.** Host-function cases must
because with `UseManualTime` automatic sizing reads only the tiny reported residue and would ask
for millions of iterations; `Vm.cpp` cases must because compiling leaks.
## Layout
One `.cpp` per host function under `host_functions/`, mirroring
`src/tests/libxrpl/tx/wasm/host_functions/`, so adding a host function is a two-file checklist
rather than a judgement call. `Crossing.cpp` holds the harness's own reference points, `Vm.cpp` the
per-run overhead around them, `WasmBench.*` the measurement machinery, `BenchFixtures.*` the shared
ledger (one ledger, funded once, for the whole binary).
The ledger and real host come from `xrpl.testkit.wasm` — a framework-free library built alongside
the tests — so this target links **no GTest and no GMock**. See
`src/tests/libxrpl/tx/wasm/README.md` for how that library is split, and why its setup steps throw
rather than using `EXPECT_`.

View File

@@ -0,0 +1,149 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <tx/wasm/fixtures/WasmRun.h>
#include <cstddef>
#include <format>
#include <string>
namespace xrpl::test::bench {
namespace {
// What a run costs *around* the contract, rather than what its host calls cost. Only the guest's
// own execution is metered, so every stage measured here is wall time no transaction pays for.
// ../README.md has what each case measures and how to read `gas_equivalent`.
//
// **Every case must pin `->Iterations(...)`.** Compiling a module allocates against the
// process-global engine and is never reclaimed — roughly 800 bytes per compile — so Google
// Benchmark's automatic sizing, which targets a wall-clock budget rather than a compile count,
// reaches gigabytes resident and the whole binary stops being able to compile anything.
// The smallest module the engine accepts. Everything a run does to it is overhead by construction.
std::string
minimalWat()
{
return R"wat((module
(memory (export "memory") 1)
(func (export "escrow_finish") (result i32)
(i32.const 1)))
)wat";
}
// `count` unreachable functions on top of the minimal module: bigger without doing more.
//
// Each body is seeded with its own index so no two are identical and none folds to a constant the
// translator can drop — either would break the link between function count and byte count.
// Unreachable is fine: validation and translation visit every function a module declares, and that
// visit is exactly the cost being swept.
std::string
fillerWat(size_t count)
{
auto out = std::string{"(module\n (memory (export \"memory\") 1)\n"};
for (auto i = 0uz; i < count; ++i)
{
out += std::format(
" (func $f{0} (param i32) (result i32)\n"
" (i32.add (i32.mul (local.get 0) (i32.const {0})) (i32.const {0})))\n",
i);
}
out += " (func (export \"escrow_finish\") (result i32)\n (i32.const 1)))\n";
return out;
}
// The minimal module asking for `pages` of initial memory. `MAX_MEMORY_PAGES` is 128 (8 MiB), and a
// contract declares this for free — the host allocates and zeroes it before the first instruction.
std::string
pagesWat(size_t pages)
{
return std::format(
"(module\n"
" (memory (export \"memory\") {})\n"
" (func (export \"escrow_finish\") (result i32)\n"
" (i32.const 1)))\n",
pages);
}
// Compiles cleanly and is then refused: `env::malloc` is not a namespace the engine serves, so
// `check_imports` stops at it.
std::string
rejectedWat()
{
return R"wat((module
(import "env" "malloc" (func $malloc (param i32) (result i32)))
(memory (export "memory") 1)
(func (export "escrow_finish") (result i32)
(i32.const 1)))
)wat";
}
void
preflightMinimal(benchmark::State& state)
{
static auto const kWasm = assembleWat(minimalWat());
benchmarkPreflight(state, kWasm);
}
BENCHMARK(preflightMinimal)->UseManualTime()->Iterations(kBenchIterations);
void
preflightRejects(benchmark::State& state)
{
static auto const kWasm = assembleWat(rejectedWat());
benchmarkPreflight(state, kWasm, /*expectAccepted*/ false);
}
BENCHMARK(preflightRejects)->UseManualTime()->Iterations(kBenchIterations);
void
runMinimal(benchmark::State& state)
{
static auto const kWasm = assembleWat(minimalWat());
benchmarkRun(state, kWasm, [] { return Fixtures::instance().host(); });
}
BENCHMARK(runMinimal)->UseManualTime()->Iterations(kBenchIterations);
void
compileScaling(benchmark::State& state)
{
auto const wasm = assembleWat(fillerWat(static_cast<size_t>(state.range(0))));
benchmarkPreflight(state, wasm, /*expectAccepted*/ true, /*sizeSweep*/ true);
}
BENCHMARK(compileScaling)
->UseManualTime()
->Iterations(kBenchIterations)
->Arg(1)
->Arg(8)
->Arg(64)
->Arg(512)
->Arg(4096);
void
runScaling(benchmark::State& state)
{
auto const wasm = assembleWat(fillerWat(static_cast<size_t>(state.range(0))));
benchmarkRun(state, wasm, [] { return Fixtures::instance().host(); }, /*sizeSweep*/ true);
}
BENCHMARK(runScaling)
->UseManualTime()
->Iterations(kBenchIterations)
->Arg(1)
->Arg(8)
->Arg(64)
->Arg(512)
->Arg(4096);
void
instantiateScaling(benchmark::State& state)
{
auto const wasm = assembleWat(pagesWat(static_cast<size_t>(state.range(0))));
benchmarkRun(state, wasm, [] { return Fixtures::instance().host(); });
}
BENCHMARK(instantiateScaling)
->UseManualTime()
->Iterations(kBenchIterations)
->Arg(1)
->Arg(8)
->Arg(32)
->Arg(128);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,397 @@
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/wasm/HostFunc.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <xrpl/tx/wasm/WasmVM.h>
#include <benchmark/benchmark.h>
#include <rust/cxx.h>
#include <tx/wasm/fixtures/WasmLedger.h>
#include <tx/wasm/fixtures/WasmRun.h>
#include <xrpl_wasm_testkit_cxxbridge/lib.h>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <format>
#include <span>
#include <string>
#include <string_view>
namespace xrpl::test::bench {
int
callsWithinTransferBudget(std::int64_t bytesWrittenPerCall)
{
// Writes nothing back to the guest, so the budget does not apply.
if (bytesWrittenPerCall <= 0)
{
return kCallsPerRun;
}
auto const affordable = kTransferLimitBytes / bytesWrittenPerCall;
if (affordable < 1)
{
fixtureFailed("a single call would exceed the run's transfer budget");
}
return static_cast<int>(std::min<std::int64_t>(affordable, kCallsPerRun));
}
std::string
dataSegment(int offset, std::span<std::uint8_t const> bytes)
{
return std::format(" (data (i32.const {}) \"{}\")\n", offset, watEscaped(bytes));
}
std::string
dataSegment(int offset, Bytes const& bytes)
{
return dataSegment(offset, std::span<std::uint8_t const>{bytes.data(), bytes.size()});
}
std::string
makeLoopWat(std::string_view imports, std::string_view data, std::string_view body, int count)
{
static constexpr auto kTemplate = R"wat((module
{}
(memory (export "memory") 1)
{}
(func (export "escrow_finish") (result i32)
(local $i i32)
(local $r i32)
(local.set $i (i32.const {}))
(block $done
(loop $again
(br_if $done (i32.eqz (local.get $i)))
(local.set $r {})
(local.set $i (i32.sub (local.get $i) (i32.const 1)))
(br $again)))
(local.get $r)))
)wat";
return std::format(kTemplate, imports, data, count, body);
}
Timing
timeRun(HostFunctions& host, Bytes const& wasm)
{
auto const start = std::chrono::steady_clock::now();
auto outcome = runEscrowWasm(wasm, host, kBenchGas);
auto const elapsed = std::chrono::steady_clock::now() - start;
benchmark::DoNotOptimize(outcome);
return {
.seconds = std::chrono::duration<double>(elapsed).count(),
.gas = outcome.has_value() ? outcome->cost : std::int64_t{0}};
}
StageTimer::StageTimer(benchmark::State& state, std::int64_t moduleBytes)
: state_{state}, moduleBytes_{moduleBytes}
{
}
void
StageTimer::add(double seconds)
{
state_.SetIterationTime(seconds);
total_ += seconds;
sumSquares_ += seconds * seconds;
++rounds_;
}
void
StageTimer::report()
{
if (rounds_ == 0)
{
return;
}
auto const count = static_cast<double>(rounds_);
auto const mean = total_ / count;
auto const variance = std::max(0.0, (sumSquares_ / count) - (mean * mean));
auto const spread = mean > 0.0 ? std::sqrt(variance) / mean : 0.0;
auto const& calibration = Calibration::instance();
auto const perGas = calibration.secondsPerGas();
auto const equivalent = perGas > 0.0 ? mean / perGas : 0.0;
state_.counters["ns_per_op"] = mean * 1e9;
// What this stage would cost if it were charged at the rate the guest inside it is charged at.
// It is not charged, which is the point: this puts an unpriced stage in the host-function
// table's units.
state_.counters["gas_equivalent"] = equivalent;
if (moduleBytes_ > 0)
{
state_.counters["module_bytes"] = static_cast<double>(moduleBytes_);
// An average over the whole operation, not the marginal rate: it carries the case's fixed
// cost, so it overestimates and falls toward the true slope as the sweep grows.
state_.counters["gas_per_byte"] = equivalent / static_cast<double>(moduleBytes_);
}
// `gas_equivalent` divides by `secondsPerGas`, so the divisor's uncertainty is in every number
// here too.
auto const caseStdErr = spread / std::sqrt(count);
auto const perGasErr = calibration.secondsPerGasRelStdErr();
auto const totalErr = std::sqrt((caseStdErr * caseStdErr) + (perGasErr * perGasErr));
state_.counters["rel_error"] = totalErr;
state_.counters["unreliable"] = totalErr > kMaxRelativeSpread ? 1 : 0;
}
void
benchmarkPreflight(benchmark::State& state, Bytes const& wasm, bool expectAccepted, bool sizeSweep)
{
(void)Calibration::instance();
// Discarded: the reject case refuses on every iteration, and a real sink would put string
// formatting and I/O inside the measurement.
auto const journal = beast::Journal{beast::Journal::getNullSink()};
if (isTesSuccess(preflightEscrowWasm(wasm, journal)) != expectAccepted)
{
state.SkipWithError(
expectAccepted
? "the module was refused; the case would be measuring the reject path"
: "the module was accepted; the case would be measuring the accept path");
return;
}
StageTimer timer{state, sizeSweep ? static_cast<std::int64_t>(wasm.size()) : 0};
for (auto _ : state)
{
auto const start = std::chrono::steady_clock::now();
auto verdict = preflightEscrowWasm(wasm, journal);
auto const elapsed = std::chrono::steady_clock::now() - start;
benchmark::DoNotOptimize(verdict);
timer.add(std::chrono::duration<double>(elapsed).count());
}
timer.report();
}
namespace {
// Seconds of wall time one unit of gas buys on this machine.
//
// The estimator must be the *same* one the cases use — a mean, with the same clamp at zero. Since
// `implied_gas = secondsPerCall / secondsPerGas`, any difference between how divisor and dividend
// are estimated lands in every reported number: calibrating with a best-of while measuring with a
// mean once biased the whole report +40%.
//
// `guestInstruction` in Crossing.cpp is the check that this holds — it runs this exact loop body.
double
measureSecondsPerGas(double& relativeStandardError)
{
// A couple of guest instructions, no memory traffic, nothing the engine can fold away.
static constexpr auto kBody = std::string_view{"(i32.add (local.get $r) (i32.const 1))"};
auto const busy = assembleWat(makeLoopWat("", "", kBody, kCallsPerRun));
auto const idle = assembleWat(makeLoopWat("", "", kBody, 0));
auto fixture = WasmLedger{};
// Warm up, so the first-run penalty does not land on one side of the subtraction.
for (auto i = 0U; i < 8; ++i)
{
timeRun(*fixture.makeHost(), busy);
timeRun(*fixture.makeHost(), idle);
}
auto total = 0.0;
auto sumSquares = 0.0;
// Fuel is exact and deterministic, so any pair gives the same delta.
auto gasDelta = std::int64_t{1};
for (auto i = 0; i < kCalibrationPairs; ++i)
{
auto hotHost = fixture.makeHost();
auto const hot = timeRun(*hotHost, busy);
auto coldHost = fixture.makeHost();
auto const cold = timeRun(*coldHost, idle);
auto const delta = std::max(0.0, hot.seconds - cold.seconds);
total += delta;
sumSquares += delta * delta;
gasDelta = std::max(std::int64_t{1}, hot.gas - cold.gas);
}
auto const mean = total / kCalibrationPairs;
auto const variance = std::max(0.0, (sumSquares / kCalibrationPairs) - (mean * mean));
relativeStandardError = mean > 0.0 ? std::sqrt(variance / kCalibrationPairs) / mean : 0.0;
return mean / static_cast<double>(gasDelta);
}
// The crossing, in gas: `ldgr_index` through the VM minus `ldgr_index` called directly.
//
// Both halves are means, for the reason above: the VM half has to match `benchmarkThroughVm`'s
// estimator and the impl half `benchmarkImpl`'s. `secondsPerGas` is passed in rather than
// re-measured so it comes from the same snapshot.
double
measureCrossingFloorGas(double secondsPerGas, double& relativeStandardError)
{
static constexpr std::string_view kImport =
R"( (import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
)";
static constexpr std::string_view kBody = "(call $ldgr_index (i32.const 0) (i32.const 4))";
auto const loaded = assembleWat(makeLoopWat(kImport, "", kBody, kCallsPerRun));
auto const baseline = assembleWat(makeLoopWat(kImport, "", kBody, 0));
auto fixture = WasmLedger{};
auto vmTotal = 0.0;
auto vmSquares = 0.0;
auto guestOverheadGas = 0.0;
for (auto i = 0; i < kBenchIterations; ++i)
{
auto hotHost = fixture.makeHost();
auto const hot = timeRun(*hotHost, loaded);
auto coldHost = fixture.makeHost();
auto const cold = timeRun(*coldHost, baseline);
auto const perCall = std::max(0.0, hot.seconds - cold.seconds) / kCallsPerRun;
vmTotal += perCall;
vmSquares += perCall * perCall;
// Exact, from the fuel meter: what the guest burned per call beyond the call itself.
guestOverheadGas =
(static_cast<double>(hot.gas - cold.gas) / kCallsPerRun) - declaredGas("ldgr_index");
}
auto const vmSeconds = vmTotal / kBenchIterations;
// The impl side is the same call without the VM. Subtracting it leaves the crossing.
auto implTotal = 0.0;
auto implSquares = 0.0;
auto host = fixture.makeHost();
for (auto i = 0; i < kBenchIterations; ++i)
{
auto const start = std::chrono::steady_clock::now();
for (auto c = 0U; c < kCallsPerRun; ++c)
{
auto result = host->getLedgerSqn();
benchmark::DoNotOptimize(result);
}
auto const elapsed = std::chrono::steady_clock::now() - start;
auto const perCall = std::chrono::duration<double>(elapsed).count() / kCallsPerRun;
implTotal += perCall;
implSquares += perCall * perCall;
}
auto const implSeconds = implTotal / kBenchIterations;
if (secondsPerGas <= 0.0)
{
return 0.0;
}
// Take the guest's loop bookkeeping off here too: `report` removes it from every `ThroughVm`
// number, so leaving it in would make the two routes to one price disagree by that amount.
auto const crossing = std::max(0.0, vmSeconds - implSeconds) / secondsPerGas;
auto const floor = std::max(0.0, crossing - std::max(0.0, guestOverheadGas));
// Relative to the *difference*, not to either half: both contribute their error, and the
// denominator is what survives the subtraction. Not divided by `secondsPerGas` — that error is
// common-mode with the rest of `suggested_gas` and is applied once, to the sum, in `report`.
auto const vmVariance = std::max(0.0, (vmSquares / kBenchIterations) - (vmSeconds * vmSeconds));
auto const implVariance =
std::max(0.0, (implSquares / kBenchIterations) - (implSeconds * implSeconds));
auto const vmStdErr = std::sqrt(vmVariance / kBenchIterations);
auto const implStdErr = std::sqrt(implVariance / kBenchIterations);
auto const crossingSeconds = vmSeconds - implSeconds;
auto const crossingStdErr = std::sqrt((vmStdErr * vmStdErr) + (implStdErr * implStdErr));
relativeStandardError = crossingSeconds > 0.0 ? crossingStdErr / crossingSeconds : 0.0;
return floor;
}
} // namespace
Calibration const&
Calibration::instance()
{
static Calibration const kValue;
return kValue;
}
Calibration::Calibration()
: secondsPerGas_{measureSecondsPerGas(secondsPerGasRelStdErr_)}
, crossingFloorGas_{measureCrossingFloorGas(secondsPerGas_, crossingFloorRelStdErr_)}
{
}
double
declaredGas(std::string_view wasmName)
{
return static_cast<double>(
rs::wasm_testkit::host_function_gas(rust::Str{wasmName.data(), wasmName.size()}));
}
void
report(
benchmark::State& state,
double secondsPerCall,
double chargedGas,
double guestOverheadGas,
double relativeSpread,
std::int64_t rounds,
std::string_view wasmName,
bool throughVm)
{
auto const& calibration = Calibration::instance();
auto const perGas = calibration.secondsPerGas();
auto const measured = perGas > 0.0 ? secondsPerCall / perGas : 0.0;
// The timed number covers the host call *and* whatever the guest ran around it.
// `guestOverheadGas` is exact, so taking it off removes a bias rather than trading estimates.
auto const implied = std::max(0.0, measured - guestOverheadGas);
auto const suggested = throughVm ? implied : implied + calibration.crossingFloorGas();
state.counters["implied_gas"] = implied;
state.counters["ns_per_call"] = secondsPerCall * 1e9;
state.counters["charged_gas"] = chargedGas;
if (wasmName.empty())
{
return;
}
auto const declared = declaredGas(wasmName);
state.counters["host_function_gas"] = declared;
state.counters["suggested_gas"] = suggested;
// Below 1 is the direction that matters: an underpriced call is one a contract buys too
// cheaply.
state.counters["price_ratio"] = suggested > 0.0 ? declared / suggested : 0.0;
// Uncertainty **of `suggested_gas`**, not of `implied_gas` — different numbers once the floor
// is added. `implied` and the floor are independent timings, so their absolute errors add in
// quadrature over the sum; but both divide by `secondsPerGas`, so that error is common-mode and
// applies once to the total. Adding it per-term would count it twice.
//
// For `ThroughVm` the floor term is zero and `suggested == implied`, so this reduces exactly to
// the plain `sqrt(caseErr² + perGasErr²)`. Only `Impl` cases move — and for a cheap one the
// floor is most of `suggested_gas`, so an error bar describing `implied` alone described
// little.
auto const caseStdErr =
rounds > 0 ? relativeSpread / std::sqrt(static_cast<double>(rounds)) : relativeSpread;
auto const impliedErr = implied * caseStdErr;
auto const floorErr =
throughVm ? 0.0 : calibration.crossingFloorGas() * calibration.crossingFloorRelStdErr();
auto const independentErr = suggested > 0.0
? std::sqrt((impliedErr * impliedErr) + (floorErr * floorErr)) / suggested
: caseStdErr;
auto const perGasErr = calibration.secondsPerGasRelStdErr();
auto const totalErr = std::sqrt((independentErr * independentErr) + (perGasErr * perGasErr));
state.counters["rel_error"] = totalErr;
// A call whose own cost is small next to the crossing is read off the difference of two nearly
// equal numbers, so its `suggested_gas` is scatter rather than signal.
auto const floor = calibration.crossingFloorGas();
state.counters["unreliable"] =
(totalErr > kMaxRelativeSpread || (floor > 0.0 && suggested < floor)) ? 1 : 0;
}
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,369 @@
#pragma once
#include <xrpl/tx/wasm/HostFunc.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <xrpl/tx/wasm/WasmVM.h>
#include <benchmark/benchmark.h>
#include <tx/wasm/fixtures/WasmRun.h>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <type_traits>
// The gas-calibration harness. What the numbers mean and how to read a report are in ../README.md.
namespace xrpl::test::bench {
// Enough that a benchmark loop never ends early; running out would measure something shorter.
inline constexpr std::int64_t kBenchGas = 2'000'000'000;
// Host calls per run: enough that the per-call cost dominates the baseline subtraction's residue.
inline constexpr std::int32_t kCallsPerRun = 1000;
// Timed iterations per case. Pinned because automatic sizing cannot work here: a case reports a
// residue of nanoseconds while the iteration producing it ran two contracts and cost milliseconds,
// so sizing would ask for millions.
inline constexpr std::int32_t kBenchIterations = 50;
// Pairs the one-off calibration averages. Higher than `kBenchIterations` because `secondsPerGas`
// divides every reported number, and a bare wasm loop is cheap to repeat.
inline constexpr std::int32_t kCalibrationPairs = 400;
// Above this relative uncertainty, `suggested_gas` is reported as unreliable.
inline constexpr double kMaxRelativeSpread = 0.25;
// `TRANSFER_LIMIT_BYTES` in crates/xrpl-wasm-vm/src/vm.rs: what a run may write into guest memory
// before `charge_transfer` starts refusing calls.
inline constexpr std::int64_t kTransferLimitBytes = 1 << 20;
// How many calls a run can afford, given the bytes each has the host **write into guest memory**.
// Output direction only — what the guest passes in is borrowed, and costs nothing against the
// budget. Never raises the count to meet a floor; a case that cannot afford one call fails loudly.
int
callsWithinTransferBudget(std::int64_t bytesWrittenPerCall);
// One run of a contract: how long it took, and what the engine charged it.
struct Timing
{
double seconds{};
std::int64_t gas{};
};
// A `(data ...)` segment placing `bytes` at `offset` in guest memory, so the timed loop measures
// the host call rather than the guest arranging its arguments. `watEscaped` in WasmRun.h says why
// zeroed memory will not do.
std::string
dataSegment(int offset, std::span<std::uint8_t const> bytes);
std::string
dataSegment(int offset, Bytes const& bytes);
// A contract that runs `body` `count` times and returns the last result.
std::string
makeLoopWat(std::string_view imports, std::string_view data, std::string_view body, int count);
// Run pre-assembled `wasm` once through the real VM, reporting wall time and gas.
Timing
timeRun(HostFunctions& host, Bytes const& wasm);
// What this machine costs, measured once and shared by every case: two cases pricing one function
// two ways (`Impl` + crossing floor, versus `ThroughVm`) must divide by the *same* `secondsPerGas`
// or they disagree for reasons unrelated to the function.
class Calibration
{
public:
static Calibration const&
instance();
// Seconds of wall time one unit of gas buys here.
[[nodiscard]] double
secondsPerGas() const
{
return secondsPerGas_;
}
// The gas a host call costs before it does anything: region decode, bounds checks, the cxx hop.
[[nodiscard]] double
crossingFloorGas() const
{
return crossingFloorGas_;
}
// Propagated into every case's `rel_error`, which is what lets one snapshot stay shared:
// `--benchmark_repetitions` resamples per-case timings but never this divisor.
[[nodiscard]] double
secondsPerGasRelStdErr() const
{
return secondsPerGasRelStdErr_;
}
// An additive term in every `Impl` case's `suggested_gas`, and most of the cheap ones.
[[nodiscard]] double
crossingFloorRelStdErr() const
{
return crossingFloorRelStdErr_;
}
private:
Calibration();
double secondsPerGasRelStdErr_{};
double crossingFloorRelStdErr_{};
double secondsPerGas_{};
double crossingFloorGas_{};
};
// What the gas table declares for a host function, by guest import name, read through the
// `wasm_testkit` bridge so it cannot drift.
double
declaredGas(std::string_view wasmName);
// Attach the calibration counters to a finished case.
//
// `guestOverheadGas` is fuel the guest burned around the call — loop bookkeeping and the
// `i32.const`s pushing arguments. From the fuel meter, so it is exact. Zero for `Impl` cases.
void
report(
benchmark::State& state,
double secondsPerCall,
double chargedGas,
double guestOverheadGas,
double relativeSpread,
std::int64_t rounds,
std::string_view wasmName,
bool throughVm);
// Accumulates one whole-operation case and turns it into counters.
//
// The two harnesses below subtract setup away, amortizing over `kCallsPerRun` calls against a
// baseline. Here that is inverted: setup is the subject, timed whole, nothing amortized.
class StageTimer
{
public:
// Pass zero for `moduleBytes` unless the case belongs to a sweep that *varies* module size —
// the per-byte counter it enables is an average over the whole operation, meaningless where the
// module is constant.
StageTimer(benchmark::State& state, std::int64_t moduleBytes);
void
add(double seconds);
// Attach the counters. Call once, after the loop.
void
report();
private:
benchmark::State& state_;
std::int64_t moduleBytes_{};
double total_{};
double sumSquares_{};
std::int64_t rounds_{};
};
// Measure `preflightEscrowWasm`: compile, then the walk over the module's imports and exports.
//
// `expectAccepted` is what the module *should* do. A refused module stops at the first fault and is
// far cheaper, so a case that silently flipped verdict would report a confident number for an
// operation it never performed.
void
benchmarkPreflight(
benchmark::State& state,
Bytes const& wasm,
bool expectAccepted = true,
bool sizeSweep = false);
// Measure a whole `runEscrowWasm` — every stage, unamortized.
template <class SetUp>
void
benchmarkRun(benchmark::State& state, Bytes const& wasm, SetUp&& setUp, bool sizeSweep = false)
{
// Force the calibration and the engine's lazy construction before the clock starts, so the
// first case does not absorb them into its own first iteration.
[[maybe_unused]] auto const& calibration = Calibration::instance();
auto probe = setUp();
if (auto const check = runEscrowWasm(wasm, *probe, kBenchGas); !check.has_value())
{
state.SkipWithError("the benchmarked contract did not run to completion");
return;
}
auto timer = StageTimer{state, sizeSweep ? static_cast<std::int64_t>(wasm.size()) : 0};
for (auto _ : state)
{
auto host = setUp();
timer.add(timeRun(*host, wasm).seconds);
}
timer.report();
}
// Measure a host function through the whole stack — guest, VM, marshalling, real impl, real ledger
// — with everything but the host calls subtracted away.
template <class SetUp>
void
benchmarkThroughVm(
benchmark::State& state,
std::string_view wasmName,
std::string_view imports,
std::string_view data,
std::string_view body,
SetUp&& setUp,
int calls = kCallsPerRun)
{
auto const loaded = assembleWat(makeLoopWat(imports, data, body, calls));
auto const baseline = assembleWat(makeLoopWat(imports, data, body, 0));
// A host serves exactly one run — `runEscrowWasm` asserts it was handed a clean one
// (`checkSelf` in WasmVM.cpp). Hence `setUp` being a factory rather than a host.
auto probe = setUp();
// A soft host error still completes the run, so require both that it completed and that the
// last host call returned a non-negative result, which every body leaves in `$r`.
auto const check = runEscrowWasm(loaded, *probe, kBenchGas);
if (!check.has_value())
{
state.SkipWithError("the benchmarked contract did not run to completion");
return;
}
if (check->result < 0)
{
state.SkipWithError(
"the benchmarked host call returned error code " + std::to_string(check->result) +
"; the case would be measuring the rejection path, not the work");
return;
}
auto totalSeconds = 0.0;
auto sumSquares = 0.0;
auto totalGas = 0.0;
auto rounds = std::int64_t{0};
for (auto _ : state)
{
auto hotHost = setUp();
auto const hot = timeRun(*hotHost, loaded);
auto coldHost = setUp();
auto const cold = timeRun(*coldHost, baseline);
// Clamped: on a noisy machine a pair can invert, and a negative iteration time would make
// Google Benchmark's statistics meaningless.
auto const perCall = std::max(0.0, hot.seconds - cold.seconds) / calls;
state.SetIterationTime(perCall);
totalSeconds += perCall;
sumSquares += perCall * perCall;
totalGas += static_cast<double>(hot.gas - cold.gas) / calls;
++rounds;
}
if (rounds > 0)
{
auto const meanSeconds = totalSeconds / rounds;
auto const variance = std::max(0.0, (sumSquares / rounds) - (meanSeconds * meanSeconds));
auto const spread = meanSeconds > 0.0 ? std::sqrt(variance) / meanSeconds : 0.0;
// Zero for the empty-name case — `guestInstruction`, which prices no host function. There
// is no host call to separate scaffolding *from*, and subtracting the full charge would
// leave `implied_gas = measured - charged`, ~0 by construction: it would turn the harness's
// one self-test into a tautology that cannot fail.
auto const chargedPerCall = totalGas / rounds;
auto const overhead = wasmName.empty() ? 0.0 : chargedPerCall - declaredGas(wasmName);
report(
state,
meanSeconds,
chargedPerCall,
std::max(0.0, overhead),
spread,
rounds,
wasmName,
true);
}
}
// Measure a host function's impl alone — no guest, no VM, no marshalling. Against the `ThroughVm`
// case for the same function, the difference is what crossing the boundary costs.
template <class SetUp, class Call>
void
benchmarkImpl(benchmark::State& state, std::string_view wasmName, SetUp&& setUp, Call&& call)
{
auto host = setUp();
// A call that errors returns early and is far cheaper than one that works, so a subtly wrong
// argument yields a confident and always *too low* price. Probed either side of the loop rather
// than inside it, where the branch would land in the measurement: *before* catches wrong
// arguments, *after* catches a call that stopped working once the loop exhausted something.
//
// The `requires` skips host functions that answer nothing (`trace`) or answer a bare value.
auto const checkSucceeds = [&](char const* when) {
if constexpr (requires { call(*host).has_value(); })
{
if (auto const probe = call(*host); !probe.has_value())
{
state.SkipWithError(
std::string{"the benchmarked host call returned error code "} +
std::to_string(static_cast<int>(probe.error())) + " " + when +
" the timed loop; the case would be measuring the rejection path");
return false;
}
}
return true;
};
if (!checkSucceeds("before"))
{
return;
}
auto totalSeconds = 0.0;
auto sumSquares = 0.0;
auto rounds = std::int64_t{0};
for (auto _ : state)
{
auto const start = std::chrono::steady_clock::now();
for (int i = 0; i < kCallsPerRun; ++i)
{
// `trace` answers nothing, so `ClobberMemory` stands in for `DoNotOptimize`.
if constexpr (std::is_void_v<decltype(call(*host))>)
{
call(*host);
benchmark::ClobberMemory();
}
else
{
auto result = call(*host);
benchmark::DoNotOptimize(result);
}
}
auto const elapsed = std::chrono::steady_clock::now() - start;
auto const perCall = std::chrono::duration<double>(elapsed).count() / kCallsPerRun;
state.SetIterationTime(perCall);
totalSeconds += perCall;
sumSquares += perCall * perCall;
++rounds;
}
if (!checkSucceeds("after"))
{
return;
}
if (rounds > 0)
{
auto const meanSeconds = totalSeconds / rounds;
auto const variance = std::max(0.0, (sumSquares / rounds) - (meanSeconds * meanSeconds));
auto const spread = meanSeconds > 0.0 ? std::sqrt(variance) / meanSeconds : 0.0;
// Nothing charged and no guest scaffolding; `report` adds the crossing back in.
report(state, meanSeconds, 0.0, 0.0, spread, rounds, wasmName, false);
}
}
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
accountKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"accountroot_id"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.accountKeylet(Fixtures::instance().alice().id()); });
}
BENCHMARK(accountKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,31 @@
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/UintTypes.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
ammKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"amm_id"};
auto const usd = Asset{Issue{toCurrency("USD"), Fixtures::instance().alice().id()}};
auto const xrp = Asset{xrpIssue()};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[&usd, &xrp](auto& host) { return host.ammKeylet(usd, xrp); });
}
BENCHMARK(ammKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
baseFeeImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"base_fee"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.getBaseFee(); });
}
BENCHMARK(baseFeeImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,28 @@
#include <xrpl/protocol/Indexes.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
cacheLedgerObjImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"cache_le"};
auto const key = keylet::account(Fixtures::instance().alice().id()).key;
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[&key](auto& host) { return host.cacheLedgerObj(key, 1); });
}
BENCHMARK(cacheLedgerObjImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,26 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
checkKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"check_id"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.checkKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
});
}
BENCHMARK(checkKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,63 @@
#include <xrpl/basics/Slice.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <cstdint>
#include <format>
#include <string_view>
namespace xrpl::test::bench {
namespace {
constexpr std::string_view kWasmName = "check_sig";
constexpr std::string_view kImport =
R"( (import "host_lib" "check_sig" (func $check_sig (param i32 i32 i32 i32 i32 i32) (result i32)))
)";
constexpr std::int32_t kMessageOffset = 0;
constexpr std::int32_t kSignatureOffset = 256;
constexpr std::int32_t kPubkeyOffset = 512;
void
checkSignatureThroughVm(benchmark::State& state)
{
auto const& m = Fixtures::instance().signedMessage();
static auto const kData = dataSegment(kMessageOffset, m.message) +
dataSegment(kSignatureOffset, m.signature) + dataSegment(kPubkeyOffset, m.publicKey);
static auto const kBody = std::format(
"(call $check_sig (i32.const {}) (i32.const {}) (i32.const {}) (i32.const {}) "
"(i32.const {}) (i32.const {}))",
kMessageOffset,
m.message.size(),
kSignatureOffset,
m.signature.size(),
kPubkeyOffset,
m.publicKey.size());
benchmarkThroughVm(
state, kWasmName, kImport, kData, kBody, [] { return Fixtures::instance().host(); });
}
BENCHMARK(checkSignatureThroughVm)->UseManualTime()->Iterations(kBenchIterations);
void
checkSignatureImpl(benchmark::State& state)
{
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
auto const& m = Fixtures::instance().signedMessage();
return host.checkSignature(
Slice{m.message.data(), m.message.size()},
Slice{m.signature.data(), m.signature.size()},
Slice{m.publicKey.data(), m.publicKey.size()});
});
}
BENCHMARK(checkSignatureImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,32 @@
#include <xrpl/basics/Slice.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
credentialKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"credential_id"};
static constexpr auto kType = std::string_view{"termsandconditions"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.credentialKeylet(
Fixtures::instance().alice().id(),
Fixtures::instance().bob().id(),
Slice{kType.data(), kType.size()});
});
}
BENCHMARK(credentialKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,26 @@
#include <xrpl/protocol/SField.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
currentLedgerObjArrayLenImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"home_le_arr_len"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().signerListHost(); },
[](auto& host) { return host.getCurrentLedgerObjArrayLen(sfSignerEntries); });
}
BENCHMARK(currentLedgerObjArrayLenImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,41 @@
#include <xrpl/protocol/SField.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <format>
#include <string_view>
namespace xrpl::test::bench {
namespace {
constexpr std::string_view kWasmName = "home_le_field";
constexpr std::string_view kImport =
R"( (import "host_lib" "home_le_field" (func $home_le_field (param i32 i32 i32) (result i32)))
)";
void
currentLedgerObjFieldThroughVm(benchmark::State& state)
{
static auto const kBody = std::format(
"(call $home_le_field (i32.const {}) (i32.const 0) (i32.const 32))", sfAccount.getCode());
benchmarkThroughVm(
state, kWasmName, kImport, "", kBody, [] { return Fixtures::instance().escrowHost(); });
}
BENCHMARK(currentLedgerObjFieldThroughVm)->UseManualTime()->Iterations(kBenchIterations);
void
currentLedgerObjFieldImpl(benchmark::State& state)
{
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().escrowHost(); },
[](auto& host) { return host.getCurrentLedgerObjField(sfAccount); });
}
BENCHMARK(currentLedgerObjFieldImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,30 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
currentLedgerObjNestedArrayLenImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"home_le_inner_arr_len"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().signerListHost(); },
[](auto& host) {
return host.getCurrentLedgerObjNestedArrayLen(
FieldLocator{{sfSignerEntries.getCode()}});
});
}
BENCHMARK(currentLedgerObjNestedArrayLenImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,29 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
currentLedgerObjNestedFieldImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"home_le_inner"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.getCurrentLedgerObjNestedField(FieldLocator{{sfAccount.getCode()}});
});
}
BENCHMARK(currentLedgerObjNestedFieldImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,27 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
delegateKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"delegate_id"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.delegateKeylet(
Fixtures::instance().alice().id(), Fixtures::instance().bob().id());
});
}
BENCHMARK(delegateKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,27 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
depositPreauthKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"deposit_preauth_id"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.depositPreauthKeylet(
Fixtures::instance().alice().id(), Fixtures::instance().bob().id());
});
}
BENCHMARK(depositPreauthKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
didKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"did_id"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.didKeylet(Fixtures::instance().alice().id()); });
}
BENCHMARK(didKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,55 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <tx/wasm/fixtures/WasmLedger.h>
#include <cstdint>
#include <string_view>
namespace xrpl::test::bench {
namespace {
constexpr std::string_view kWasmName = "escrow_id";
constexpr std::string_view kImport =
R"( (import "host_lib" "escrow_id" (func $escrow_id (param i32 i32 i32 i32 i32 i32) (result i32)))
)";
constexpr std::string_view kBody =
"(call $escrow_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 4) "
"(i32.const 64) (i32.const 32))";
void
escrowKeyletThroughVm(benchmark::State& state)
{
static auto const kData = [] {
auto seq = Bytes(4);
for (auto i = 0U; i < 4; ++i)
{
seq[i] = static_cast<std::uint8_t>((Fixtures::kSeq >> (8 * i)) & 0xFF);
}
return dataSegment(0, WasmLedger::toBytes(Fixtures::instance().alice().id())) +
dataSegment(32, seq);
}();
benchmarkThroughVm(
state, kWasmName, kImport, kData, kBody, [] { return Fixtures::instance().host(); });
}
BENCHMARK(escrowKeyletThroughVm)->UseManualTime()->Iterations(kBenchIterations);
void
escrowKeyletImpl(benchmark::State& state)
{
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.escrowKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
});
}
BENCHMARK(escrowKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,45 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <tx/wasm/fixtures/FloatConstants.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
constexpr std::string_view kWasmName = "float_add";
constexpr std::string_view kImport =
R"( (import "host_lib" "float_add" (func $float_add (param i32 i32 i32 i32 i32 i32 i32) (result i32)))
)";
constexpr std::string_view kBody =
"(call $float_add (i32.const 0) (i32.const 12) (i32.const 16) (i32.const 12) "
"(i32.const 64) (i32.const 12) (i32.const 0))";
void
floatAddThroughVm(benchmark::State& state)
{
static auto const kData =
dataSegment(0, FloatConstants::kPi) + dataSegment(16, FloatConstants::kTwo);
benchmarkThroughVm(
state, kWasmName, kImport, kData, kBody, [] { return Fixtures::instance().host(); });
}
BENCHMARK(floatAddThroughVm)->UseManualTime()->Iterations(kBenchIterations);
void
floatAddImpl(benchmark::State& state)
{
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.floatAdd(Fixtures::floatX(), Fixtures::floatY(), Fixtures::kRoundingMode);
});
}
BENCHMARK(floatAddImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
floatCompareImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"float_cmp"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.floatCompare(Fixtures::floatX(), Fixtures::floatY()); });
}
BENCHMARK(floatCompareImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,27 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
floatDivideImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"float_div"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.floatDivide(
Fixtures::floatX(), Fixtures::floatY(), Fixtures::kRoundingMode);
});
}
BENCHMARK(floatDivideImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
floatFromIntImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"float_from_int"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.floatFromInt(3141592653589793, Fixtures::kRoundingMode); });
}
BENCHMARK(floatFromIntImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,26 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
floatFromMantExpImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"float_from_mant_exp"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.floatFromMantExp(3141592653589793, -15, Fixtures::kRoundingMode);
});
}
BENCHMARK(floatFromMantExpImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,31 @@
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/UintTypes.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
floatFromStAmountImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"float_from_stamount"};
auto const amount =
STAmount{Issue{toCurrency("USD"), Fixtures::instance().alice().id()}, 1234567, -3};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[&amount](auto& host) { return host.floatFromSTAmount(amount, Fixtures::kRoundingMode); });
}
BENCHMARK(floatFromStAmountImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,30 @@
#include <xrpl/basics/Number.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STNumber.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
floatFromStNumberImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"float_from_stnumber"};
auto const number = STNumber{sfNumber, Number(3141592653589793, -15)};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[&number](auto& host) { return host.floatFromSTNumber(number, Fixtures::kRoundingMode); });
}
BENCHMARK(floatFromStNumberImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
floatFromUintImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"float_from_uint"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.floatFromUint(3141592653589793u, Fixtures::kRoundingMode); });
}
BENCHMARK(floatFromUintImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,27 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
floatMultiplyImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"float_mult"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.floatMultiply(
Fixtures::floatX(), Fixtures::floatY(), Fixtures::kRoundingMode);
});
}
BENCHMARK(floatMultiplyImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,42 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <tx/wasm/fixtures/FloatConstants.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
constexpr std::string_view kWasmName = "float_pow";
constexpr std::string_view kImport =
R"( (import "host_lib" "float_pow" (func $float_pow (param i32 i32 i32 i32 i32 i32) (result i32)))
)";
constexpr std::string_view kBody =
"(call $float_pow (i32.const 0) (i32.const 12) (i32.const 7) "
"(i32.const 64) (i32.const 12) (i32.const 0))";
void
floatPowerThroughVm(benchmark::State& state)
{
static auto const kData = dataSegment(0, FloatConstants::kPi);
benchmarkThroughVm(
state, kWasmName, kImport, kData, kBody, [] { return Fixtures::instance().host(); });
}
BENCHMARK(floatPowerThroughVm)->UseManualTime()->Iterations(kBenchIterations);
void
floatPowerImpl(benchmark::State& state)
{
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.floatPower(Fixtures::floatX(), 7, Fixtures::kRoundingMode); });
}
BENCHMARK(floatPowerImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,27 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
floatSubtractImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"float_sub"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.floatSubtract(
Fixtures::floatX(), Fixtures::floatY(), Fixtures::kRoundingMode);
});
}
BENCHMARK(floatSubtractImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
floatToIntImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"float_to_int"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.floatToInt(Fixtures::floatX(), Fixtures::kRoundingMode); });
}
BENCHMARK(floatToIntImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,41 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <tx/wasm/fixtures/FloatConstants.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
constexpr std::string_view kWasmName = "float_to_mant_exp";
constexpr std::string_view kImport =
R"( (import "host_lib" "float_to_mant_exp" (func $split (param i32 i32 i32 i32 i32 i32) (result i32)))
)";
constexpr std::string_view kBody =
"(call $split (i32.const 0) (i32.const 12) (i32.const 64) (i32.const 8) "
"(i32.const 128) (i32.const 4))";
void
floatToMantExpThroughVm(benchmark::State& state)
{
static auto const kData = dataSegment(0, FloatConstants::kPi);
benchmarkThroughVm(
state, kWasmName, kImport, kData, kBody, [] { return Fixtures::instance().host(); });
}
BENCHMARK(floatToMantExpThroughVm)->UseManualTime()->Iterations(kBenchIterations);
void
floatToMantExpImpl(benchmark::State& state)
{
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.floatToMantExp(Fixtures::floatX()); });
}
BENCHMARK(floatToMantExpImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,34 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <tx/wasm/fixtures/NftSetup.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
getNFTImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"nft_uri"};
// A really minted token, so the lookup walks a real page rather than failing fast — a
// not-found answer would measure the rejection instead of the work.
static constexpr auto kUri = std::string_view{"ipfs://benchmark"};
// Its own ledger rather than the shared `Fixtures`: minting mutates state, and the shared
// one is deliberately read-only after construction.
static auto nft = WasmLedger{};
static auto const kOwner = nft.fund("benchNftOwner");
static auto const kMinted = mintNft(nft, kOwner, kUri);
benchmarkImpl(
state,
kWasmName,
[] { return nft.makeHost(); },
[](auto& host) { return host.getNFT(kOwner.id(), kMinted); });
}
BENCHMARK(getNFTImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,54 @@
#include <xrpl/protocol/Feature.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string>
#include <string_view>
namespace xrpl::test::bench {
namespace {
constexpr std::string_view kWasmName = "amendment_enabled";
std::string const&
benchAmendment()
{
static auto const kValue = std::string{"TokenEscrow"};
return kValue;
}
void
isAmendmentEnabledByIdImpl(benchmark::State& state)
{
auto const feature = getRegisteredFeature(benchAmendment());
if (!feature.has_value())
{
state.SkipWithError("the benchmarked amendment is not registered");
return;
}
auto const id = *feature;
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[&id](auto& host) { return host.isAmendmentEnabled(id); });
}
BENCHMARK(isAmendmentEnabledByIdImpl)->UseManualTime()->Iterations(kBenchIterations);
void
isAmendmentEnabledByNameImpl(benchmark::State& state)
{
// The gap over the id form is precisely what the shared price of 100 asserts does not exist.
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.isAmendmentEnabled(std::string_view{benchAmendment()}); });
}
BENCHMARK(isAmendmentEnabledByNameImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,26 @@
#include <xrpl/protocol/SField.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
ledgerObjArrayLenImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"le_arr_len"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().cachedSignerListHost(); },
[](auto& host) { return host.getLedgerObjArrayLen(1, sfSignerEntries); });
}
BENCHMARK(ledgerObjArrayLenImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,26 @@
#include <xrpl/protocol/SField.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
ledgerObjFieldImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"le_field"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().cachedHost(); },
[](auto& host) { return host.getLedgerObjField(1, sfAccount); });
}
BENCHMARK(ledgerObjFieldImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,29 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
ledgerObjNestedArrayLenImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"le_inner_arr_len"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().cachedSignerListHost(); },
[](auto& host) {
return host.getLedgerObjNestedArrayLen(1, FieldLocator{{sfSignerEntries.getCode()}});
});
}
BENCHMARK(ledgerObjNestedArrayLenImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,29 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
ledgerObjNestedFieldImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"le_inner"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().cachedHost(); },
[](auto& host) {
return host.getLedgerObjNestedField(1, FieldLocator{{sfAccount.getCode()}});
});
}
BENCHMARK(ledgerObjNestedFieldImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,38 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
constexpr std::string_view kWasmName = "ldgr_index";
constexpr std::string_view kImport =
R"( (import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
)";
void
ledgerSqnThroughVm(benchmark::State& state)
{
benchmarkThroughVm(
state, kWasmName, kImport, "", "(call $ldgr_index (i32.const 0) (i32.const 4))", [] {
return Fixtures::instance().host();
});
}
BENCHMARK(ledgerSqnThroughVm)->UseManualTime()->Iterations(kBenchIterations);
void
ledgerSqnImpl(benchmark::State& state)
{
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.getLedgerSqn(); });
}
BENCHMARK(ledgerSqnImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,26 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
mptokenIssuanceKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"mpt_issuance_id"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.mptokenIssuanceKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
});
}
BENCHMARK(mptokenIssuanceKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,31 @@
#include <xrpl/protocol/Indexes.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
mptokenKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"mptoken_id"};
auto const mptid = makeMptID(1, Fixtures::instance().alice().id());
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[&mptid](auto& host) {
return host.mptokenKeylet(mptid, Fixtures::instance().bob().id());
});
}
BENCHMARK(mptokenKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
nftFlagsImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"nft_flags"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.getNFTFlags(Fixtures::instance().nftId()); });
}
BENCHMARK(nftFlagsImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
nftIssuerImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"nft_issuer"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.getNFTIssuer(Fixtures::instance().nftId()); });
}
BENCHMARK(nftIssuerImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
nftSequenceImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"nft_serial"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.getNFTSequence(Fixtures::instance().nftId()); });
}
BENCHMARK(nftSequenceImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
nftTaxonImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"nft_taxon"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.getNFTTaxon(Fixtures::instance().nftId()); });
}
BENCHMARK(nftTaxonImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
nftTransferFeeImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"nft_xfer_fee"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.getNFTTransferFee(Fixtures::instance().nftId()); });
}
BENCHMARK(nftTransferFeeImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,26 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
nftokenOfferKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"nft_offer_id"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.nftokenOfferKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
});
}
BENCHMARK(nftokenOfferKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,26 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
offerKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"offer_id"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.offerKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
});
}
BENCHMARK(offerKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,26 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
oracleKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"oracle_id"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.oracleKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
});
}
BENCHMARK(oracleKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
parentLedgerHashImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"parent_ldgr_hash"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.getParentLedgerHash(); });
}
BENCHMARK(parentLedgerHashImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
parentLedgerTimeImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"parent_ldgr_time"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.getParentLedgerTime(); });
}
BENCHMARK(parentLedgerTimeImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,27 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
paychannelKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"paychan_id"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.paychannelKeylet(
Fixtures::instance().alice().id(), Fixtures::instance().bob().id(), Fixtures::kSeq);
});
}
BENCHMARK(paychannelKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,26 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
permissionedDomainKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"permissioned_domain_id"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.permissionedDomainKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
});
}
BENCHMARK(permissionedDomainKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,69 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <cstddef>
#include <format>
#include <string_view>
namespace xrpl::test::bench {
namespace {
constexpr std::string_view kWasmName = "sha512_half";
constexpr std::string_view kImport =
R"( (import "host_lib" "sha512_half" (func $sha512_half (param i32 i32 i32 i32) (result i32)))
)";
void
sha512HalfThroughVm(benchmark::State& state)
{
// Zeroed guest memory is a perfectly good hash input: `sha512_half` validates nothing about
// its bytes, so there is no data segment to seed.
auto const body = std::format(
"(call $sha512_half (i32.const 0) (i32.const {}) (i32.const 8192) (i32.const 32))",
state.range(0));
// A hash is 32 bytes back to the guest whatever the input length, and the transfer budget
// counts only what the host writes — so the input sweep does not shrink the call count.
benchmarkThroughVm(
state,
kWasmName,
kImport,
"",
body,
[] { return Fixtures::instance().host(); },
callsWithinTransferBudget(32));
state.SetBytesProcessed(state.iterations() * state.range(0));
}
BENCHMARK(sha512HalfThroughVm)
->RangeMultiplier(8)
->Range(8, xrpl::kMaxWasmDataLength)
->UseManualTime()
->Iterations(kBenchIterations);
void
sha512HalfImpl(benchmark::State& state)
{
auto const data = Bytes(static_cast<std::size_t>(state.range(0)), 0x42);
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[&data](auto& host) {
return host.computeSha512HalfHash(Slice{data.data(), data.size()});
});
state.SetBytesProcessed(state.iterations() * state.range(0));
}
BENCHMARK(sha512HalfImpl)
->RangeMultiplier(8)
->Range(8, xrpl::kMaxWasmDataLength)
->UseManualTime()
->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,24 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
signerListKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"signers_id"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.signerListKeylet(Fixtures::instance().alice().id()); });
}
BENCHMARK(signerListKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,26 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
ticketKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"ticket_id"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.ticketKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
});
}
BENCHMARK(ticketKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,40 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
constexpr std::string_view kWasmName = "trace";
constexpr std::string_view kMessage = "benchmark trace message";
constexpr std::string_view kData = "0123456789abcdef";
// The path a validator actually runs: journal pointed at a null sink.
void
traceDisabledImpl(benchmark::State& state)
{
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.trace(kMessage, kData); });
}
BENCHMARK(traceDisabledImpl)->UseManualTime()->Iterations(kBenchIterations);
// The same call against a host whose sink records what it is given. The gap over the case above
// is the cost the flat 30 does not cover.
void
traceEnabledImpl(benchmark::State& state)
{
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().tracingHost(); },
[](auto& host) { return host.trace(kMessage, kData); });
}
BENCHMARK(traceEnabledImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,31 @@
#include <xrpl/protocol/UintTypes.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
trustLineKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"trustline_id"};
auto const currency = toCurrency("USD");
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[&currency](auto& host) {
return host.trustLineKeylet(
Fixtures::instance().alice().id(), Fixtures::instance().bob().id(), currency);
});
}
BENCHMARK(trustLineKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,26 @@
#include <xrpl/protocol/SField.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
txArrayLenImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"tx_arr_len"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.getTxArrayLen(sfMemos); });
}
BENCHMARK(txArrayLenImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,26 @@
#include <xrpl/protocol/SField.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
txFieldImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"tx_field"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.getTxField(sfAccount); });
}
BENCHMARK(txFieldImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,27 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
txNestedArrayLenImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"tx_inner_arr_len"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.getTxNestedArrayLen(FieldLocator{{sfMemos.getCode()}}); });
}
BENCHMARK(txNestedArrayLenImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,55 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <cstdint>
#include <string_view>
namespace xrpl::test::bench {
namespace {
constexpr std::string_view kWasmName = "tx_inner";
constexpr std::string_view kImport =
R"( (import "host_lib" "tx_inner" (func $tx_inner (param i32 i32 i32 i32) (result i32)))
)";
constexpr std::string_view kBody =
"(call $tx_inner (i32.const 0) (i32.const 12) (i32.const 64) (i32.const 64))";
void
txNestedFieldThroughVm(benchmark::State& state)
{
// The locator bytes are seeded rather than stored by the guest, so the loop measures the host
// call and not three `i32.store`s.
static auto const kData = dataSegment(0, [] {
auto bytes = Bytes{};
for (auto const step : {sfMemos.getCode(), 0, sfMemoData.getCode()})
{
for (auto i = 0U; i < 4; ++i)
{
bytes.push_back(static_cast<std::uint8_t>((step >> (8 * i)) & 0xFF));
}
}
return bytes;
}());
benchmarkThroughVm(
state, kWasmName, kImport, kData, kBody, [] { return Fixtures::instance().host(); });
}
BENCHMARK(txNestedFieldThroughVm)->UseManualTime()->Iterations(kBenchIterations);
void
txNestedFieldImpl(benchmark::State& state)
{
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) { return host.getTxNestedField(Fixtures::memoLocator()); });
}
BENCHMARK(txNestedFieldImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,62 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <cstddef>
#include <format>
#include <string_view>
namespace xrpl::test::bench {
namespace {
constexpr std::string_view kWasmName = "set_data";
constexpr std::string_view kImport =
R"( (import "host_lib" "set_data" (func $set_data (param i32 i32) (result i32)))
)";
void
updateDataThroughVm(benchmark::State& state)
{
auto const body = std::format("(call $set_data (i32.const 0) (i32.const {}))", state.range(0));
benchmarkThroughVm(
state,
kWasmName,
kImport,
"",
body,
[] { return Fixtures::instance().host(); },
// `set_data` answers a scalar and writes nothing into guest memory, so the
// transfer budget does not constrain it however large the input gets.
callsWithinTransferBudget(0));
state.SetBytesProcessed(state.iterations() * state.range(0));
}
BENCHMARK(updateDataThroughVm)
->RangeMultiplier(4)
->Range(8, kMaxWasmDataLength)
->UseManualTime()
->Iterations(kBenchIterations);
void
updateDataImpl(benchmark::State& state)
{
auto const data = Bytes(static_cast<std::size_t>(state.range(0)), 0x42);
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[&data](auto& host) { return host.updateData(Slice{data.data(), data.size()}); });
state.SetBytesProcessed(state.iterations() * state.range(0));
}
BENCHMARK(updateDataImpl)
->RangeMultiplier(4)
->Range(8, kMaxWasmDataLength)
->UseManualTime()
->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -0,0 +1,26 @@
#include <benchmark/benchmark.h>
#include <benchmarks/libxrpl/wasm/BenchFixtures.h>
#include <benchmarks/libxrpl/wasm/WasmBench.h>
#include <string_view>
namespace xrpl::test::bench {
namespace {
void
vaultKeyletImpl(benchmark::State& state)
{
static constexpr auto kWasmName = std::string_view{"vault_id"};
benchmarkImpl(
state,
kWasmName,
[] { return Fixtures::instance().host(); },
[](auto& host) {
return host.vaultKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
});
}
BENCHMARK(vaultKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
} // namespace
} // namespace xrpl::test::bench

View File

@@ -1,3 +1,10 @@
namespace xrpl::test {
// TODO: Disabled pending migration to the redesigned Wasm VM test harness
// (src/tests/libxrpl/tx/wasm/), which replaced TestHostFunctions.h and the
// APIs this suite depends on. Timothy Banks will migrate these tests.
#if 0
#include <expected>
#ifdef _DEBUG
// #define DEBUG_OUTPUT 1
@@ -25,8 +32,6 @@
#include <utility>
#include <vector>
namespace xrpl::test {
std::vector<uint8_t>
hexToBytes(std::string const& hex)
{
@@ -1429,4 +1434,6 @@ struct Wasm_test : public beast::unit_test::Suite
BEAST_DEFINE_TESTSUITE(Wasm, app, xrpl);
#endif
} // namespace xrpl::test

View File

@@ -5,15 +5,24 @@ include(verify_headers)
# Test requirements.
find_package(GTest REQUIRED)
# Single combined gtest binary built from the shared test helpers and all test
# modules below.
add_executable(
xrpl_tests
main.cpp
add_library(
xrpl.testkit.wasm
STATIC
helpers/Account.cpp
helpers/TestSink.cpp
helpers/TxTest.cpp
tx/wasm/fixtures/NftSetup.cpp
tx/wasm/fixtures/WasmLedger.cpp
tx/wasm/fixtures/WasmRun.cpp
)
target_include_directories(xrpl.testkit.wasm PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(
xrpl.testkit.wasm
PUBLIC xrpl.libxrpl xrpl_wasm_testkit_cxxbridge
)
add_dependencies(xrpl.testkit.wasm xrpl_crates)
add_executable(xrpl_tests main.cpp)
patch_nix_binary(xrpl_tests)
set_target_properties(
xrpl_tests
@@ -21,10 +30,10 @@ set_target_properties(
)
# Lets test sources include the shared helpers as <helpers/...>.
target_include_directories(xrpl_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(xrpl_tests PRIVATE GTest::gtest GTest::gmock xrpl.libxrpl)
target_link_libraries(xrpl_tests PRIVATE xrpl_wasm_testkit_cxxbridge)
add_dependencies(xrpl_tests xrpl_crates)
target_link_libraries(
xrpl_tests
PRIVATE GTest::gtest GTest::gmock xrpl.libxrpl xrpl.testkit.wasm
)
# One source subdirectory per module. Network unit tests are currently not
# supported on Windows.
@@ -55,6 +64,13 @@ foreach(module IN LISTS test_modules)
"${CMAKE_CURRENT_SOURCE_DIR}/${module}/*.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/${module}.cpp"
)
# The framework-free half of tx/wasm/fixtures/ is compiled into xrpl.testkit.wasm,
# which this binary links; the rest of that folder belongs here.
list(
FILTER sources
EXCLUDE
REGEX "/fixtures/(NftSetup|WasmLedger|WasmRun)\\.cpp$"
)
target_sources(xrpl_tests PRIVATE ${sources})
# Expose the module's private headers under their canonical include path.

View File

@@ -1,33 +0,0 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/nft.h>
#include <helpers/Account.h>
#include <tx/wasm/RealHostFixture.h>
#include <cstdint>
#include <optional>
#include <string_view>
namespace xrpl::test {
struct NFTTest : RealHostFixture
{
static constexpr std::uint16_t kFlags = nft::kFlagTransferable | nft::kFlagBurnable;
static constexpr std::uint16_t kFee = 314;
static constexpr std::uint32_t kTaxon = 12345;
static constexpr std::uint32_t kSequence = 7;
static uint256
makeNftId(AccountID const& issuer);
// Mint a real NFToken owned by `issuer` (taxon 0) and return its id, read back from the
// owner's NFTokenPage. TxTest applies to the open ledger, which produces no metadata, so
// the id is recovered from ledger state rather than from the mint's metadata.
uint256
mintNFT(Account const& issuer, std::optional<std::string_view> uri = std::nullopt);
};
} // namespace xrpl::test

View File

@@ -6,8 +6,8 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <helpers/CaptureSink.h>
#include <tx/wasm/MockHostFunctions.h>
#include <tx/wasm/WasmFixture.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <tx/wasm/fixtures/WasmRun.h>
#include <string>
#include <string_view>
@@ -29,7 +29,7 @@ constexpr std::string_view kRunnableWat = R"wat(
} // namespace
// `preflightEscrowWasm` takes no host, so this fixture holds none - which is the point of
// the signature, and what deriving from `WasmTest` would hide. Only a journal, to read the
// the signature, and what deriving from `MockVmTest` would hide. Only a journal, to read the
// refusal out of.
struct PreflightTest : testing::Test
{

View File

@@ -0,0 +1,90 @@
# WASM host-function tests — layering
These tests are deliberately **layered**: each layer isolates one thing, so a failure points at
one place instead of "somewhere in the stack." If a folder looks thin, the breadth it seems to be
missing lives in a sibling layer.
## The layers
| Layer | Location | host | VM | ledger | Answers |
| ------------------------------------- | --------------------------------------------------- | ---- | --- | ------ | ----------------------------------------------------------------------------------- |
| Engine / gas / limits / ABI | `crates/xrpl-wasm-vm`, `crates/xrpl-host-functions` | mock | ✓ | ✗ | gas, transfer budget, memory/field limits, preflight, VM limits, generated ABI |
| `host_context/` (`HostContextTest`) | `.../host_context` | mock | ✗ | ✗ | the `HostContext` marshalling shim alone (byte order, buffer sizing, `SField` xlat) |
| `host_calls/` (`HostCallTest`) | `.../host_calls` | mock | ✓ | ✗ | per-function **wire contract** — what the host was asked, what came back |
| `host_functions/` (`RealHostFixture`) | `.../host_functions` | real | ✗ | real | each function's **actual answer** vs. a real `TxTest` ledger |
| `e2e/` (`RealVmTest`) | `.../e2e` | real | ✓ | real | **full-stack integration** — VM + `HostContext` + real impl + real ledger |
Run the C++ side with:
```bash
./build/xrpl_tests --gtest_filter='*Impl.*:*Call.*:*E2e.*:WasmVMTest.*:WasmVMDeathTest.*:PreflightTest.*'
```
(707 tests, 136 suites.) The engine-level coverage is Rust: `cd crates && cargo test`.
## `fixtures/` — split by whether it needs a test framework
| | |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **No GTest** — the `xrpl.testkit.wasm` library | `WasmLedger` (real genesis ledger + the real host over it), `WasmRun` (WAT assembler), `NftSetup`, `FloatConstants` |
| **GTest** → `xrpl_tests` | `RealHostFixture` (`: testing::Test, WasmLedger` + `expectValue`/`expectError`/`expectKeyletMatches`), `FloatFixture`, `NFTFixture`, `MockHostFunctions`, `WasmFixture`, `RealVmTest`, `HostContextFixture` |
A benchmark wants a ledger and a host, not GTest's lifecycle. Both binaries link the library;
`xrpl.bench.wasm` links no GTest and no GMock at all.
Setup steps in `WasmLedger` and `NftSetup` **throw** (`fixtureFailed`) rather than using `EXPECT_`.
Not stylistic: an `EXPECT_` outside a running test is recorded and discarded, so a benchmark whose
escrow was never created would still run its host call, take the not-found path, and report a
cheap, plausible, completely wrong price. **If you add a setup step that can fail, throw.**
## Gas calibration
The benchmarks that price these host functions live in `src/benchmarks/libxrpl/wasm/`, mirroring
this tree one file per function, and have their own README. They link `xrpl.testkit.wasm` (above)
for the ledger and host, and no test framework.
## What `e2e/` covers — the rule
**`e2e/` covers every marshalling shape and cross-call convention exactly once. It does not cover
every function.** That is a completeness claim on the axis e2e uniquely tests, not a sample.
`host_calls` pins what the bridge _asks_ with a _canned_ answer; `host_functions` pins what the
real impl _answers_. The type system guarantees they agree on signatures. Nothing guarantees they
agree on **conventions** — units, endianness, buffer layout — because in neither test does a real
guest write bytes a real host reads. That is exactly the `seq`-as-little-endian-region bug: every
internal test passed, and it was caught by cross-checking the guest SDK.
Convention mismatch is a property of a call's **shape**, not of the function. All 19 keylets share
one shape, so a 19th keylet e2e proves nothing the 1st did. The inventory is meant to be exhaustive:
| Shape / convention | Covered by | Why it is its own row |
| ----------------------------------------- | -------------------------- | -------------------------------------------------------- |
| no-input scalar getter | `LedgerSqnE2e` | header read; the minimal call |
| field code in, bytes out (ledger object) | `CurrentLedgerObjFieldE2e` | `SField` translation over a real object |
| field code in, bytes out (transaction) | `TxFieldE2e` | a different source than a ledger object |
| region in, bytes out + `u32` region | `CacheLedgerObjE2e` | the 4-byte little-endian region convention |
| slot in, bytes out — **cross-call state** | `CacheLedgerObjE2e` | the slot table is the only host state outliving one call |
| locator (path of i32 steps) | `TxNestedFieldE2e` | a wire format the guest writes and the host walks |
| **two** output regions | `FloatToMantExpE2e` | two bounds checks, two writes, an ordering between them |
| write / mutation | `SetDataE2e` | the one thing a contract changes |
| **error** path from a real impl | `HostErrorE2e` | a soft code from a real failure, not a staged one |
| realistic multi-call contract | `HostFunctionTourE2e` | the old `all_host_functions` tour shape, as one test |
Adding a function needs no new e2e case unless it introduces a shape not in that table. Per-function
breadth lives in `host_functions/` and `host_calls/`, one case each.
## Out of scope
**The guest SDK** (`xrpl-std` / `xrpl-escrow`, external `xrpl-wasm-stdlib` repo) is not exercised
here — that is the SDK repo's own suite. These tests hand-write the ABI in WAT (raw imports,
literal field codes, hand-built byte layouts), deliberately bypassing all SDK code. Agreement is
verified _transitively_: the SDK repo tests the SDK against the ABI spec, this repo tests the host
against the same spec. That would not catch a drift where both diverge on an ambiguous point;
closing it needs a **cross-repo integration test** (compiled guests against a real host) in CI
where the Rust→wasm toolchain exists.
**Transactor-level (L5) tests** are deferred: the redesign does not yet wire `runEscrowWasm` into
the `EscrowFinish` transactor, so there is no caller under `src/xrpld`. When it is wired, these
need a home as C++ transactor tests over a real `Env` — `set_data` persistence (including on
`tecBYTECODE_REJECTED`), `sfGasUsed` / `sfVMReturnCode` in transaction metadata, and owner-reserve
accounting for a bytecode-bearing escrow. The layers here deliberately stop at the VM boundary.

View File

@@ -6,7 +6,8 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/WasmFixture.h>
#include <tx/wasm/fixtures/WasmFixture.h>
#include <tx/wasm/fixtures/WasmRun.h>
#include <array>
#include <cstdint>
@@ -52,7 +53,7 @@ constexpr std::string_view kNoMemoryWat = R"wat(
} // namespace
class WasmVMTest : public WasmTest
class WasmVMTest : public MockVmTest
{
};

View File

@@ -0,0 +1,63 @@
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/fixtures/RealHostFixture.h>
#include <tx/wasm/fixtures/RealVmTest.h>
#include <tx/wasm/fixtures/WasmRun.h>
#include <cstdint>
#include <format>
#include <string>
namespace xrpl::test {
// The keylet -> cache -> read round trip: the only place a contract's host calls depend on
// each other. Every other e2e case here is one call in isolation. This one is three, and each
// consumes what the last produced: `accountroot_id` computes a key into guest memory, `cache_le`
// hands those same bytes back to the host and answers with a slot number, and `le_field`
// uses that slot to read the object. The slot table is the one piece of host state that
// outlives a single call, so this is the only test at any layer that can catch the two ends
// of that state disagreeing — `host_calls` mocks the host, so its slot numbers are whatever
// the mock was told to return, and `host_functions` calls the impl directly, so its slots
// never cross the guest boundary at all.
struct CacheLedgerObjE2e : RealVmTest
{
};
TEST_F(CacheLedgerObjE2e, ContractComputesAKeyCachesTheObjectAndReadsItsField)
{
auto const owner = fund("owner");
auto const wat = std::format(
R"wat(
(module
(import "host_lib" "accountroot_id" (func $accountroot_id (param i32 i32 i32 i32) (result i32)))
(import "host_lib" "cache_le" (func $cache_le (param i32 i32 i32) (result i32)))
(import "host_lib" "le_field" (func $le_field (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0) "{}")
(func (export "escrow_finish") (result i32)
(local $slot i32)
(local $r i32)
;; The account's AccountRoot keylet, computed by the host into offset 64.
(local.set $r (call $accountroot_id (i32.const 0) (i32.const 20) (i32.const 64) (i32.const 32)))
(if (i32.lt_s (local.get $r) (i32.const 0)) (then (return (local.get $r))))
;; Those same 32 bytes handed straight back: cache the object they name.
(local.set $slot (call $cache_le (i32.const 64) (i32.const 32) (i32.const 0)))
(if (i32.lt_s (local.get $slot) (i32.const 0)) (then (return (local.get $slot))))
;; And read a field of it through the slot the host just assigned.
(call $le_field (local.get $slot) (i32.const {}) (i32.const 128) (i32.const 32))))
)wat",
watEscaped(RealHostFixture::toBytes(owner.id())),
sfAccount.getCode());
auto const outcome = run(wat);
ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
// 20 bytes: the `sfAccount` the contract read back is the account it started from, so
// the key it computed found the right object.
EXPECT_EQ(
outcome->result, static_cast<std::int32_t>(RealHostFixture::toBytes(owner.id()).size()));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,66 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol_autogen/transactions/EscrowCreate.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <helpers/TxTest.h>
#include <tx/wasm/fixtures/RealVmTest.h>
#include <cstdint>
#include <format>
#include <string>
namespace xrpl::test {
// A contract reads a field of its current ledger object (a real escrow) end to end: the real
// VM runs the guest, `HostContext` marshals the field code into an `SField`, the real impl
// reads the real ledger, and the byte count comes back to the guest. `host_calls/` proves the
// marshalling with a mock and `host_functions/` proves the impl's answer without a VM; this
// proves the two agree over a real ledger.
struct CurrentLedgerObjFieldE2e : RealVmTest
{
// Create a real escrow owned by `owner` and return its keylet — the object the contract
// runs against.
Keylet
makeEscrow(Account const& owner, Account const& dest)
{
ledger.createAccount(owner, XRP(1000));
ledger.createAccount(dest, XRP(1000));
auto const ownerSeq = ledger.getAccountRoot(owner.id()).getSequence();
auto const r = ledger.submit(
transactions::EscrowCreateBuilder{owner.id(), dest.id(), XRP(100)}.setFinishAfter(
900'000'000),
owner);
EXPECT_EQ(r.ter, tesSUCCESS) << transToken(r.ter);
ledger.close();
return keylet::escrow(owner.id(), SeqProxy::rawSequence(ownerSeq));
}
};
TEST_F(CurrentLedgerObjFieldE2e, ContractReadsAFieldOfItsRealEscrow)
{
auto const owner = Account{"owner"};
auto const escrow = makeEscrow(owner, Account{"dest"});
// Ask the current object for `sfAccount` and return the byte count the host wrote — 20 for
// an account id — proving the read reached the real ledger and came back through the VM.
auto const wat = std::format(
R"wat(
(module
(import "host_lib" "home_le_field" (func $home_le_field (param i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "escrow_finish") (result i32)
(call $home_le_field (i32.const {}) (i32.const 0) (i32.const 32))))
)wat",
sfAccount.getCode());
auto const outcome = run(wat, escrow);
ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
EXPECT_EQ(outcome->result, static_cast<std::int32_t>(toBytes(owner.id()).size()));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,66 @@
#include <xrpl/protocol/TER.h>
#include <gtest/gtest.h>
#include <tx/wasm/fixtures/FloatFixture.h>
#include <tx/wasm/fixtures/RealVmTest.h>
#include <tx/wasm/fixtures/WasmRun.h>
#include <cstdint>
#include <format>
#include <string>
namespace xrpl::test {
// The only host function that writes to *two* output regions, and so the only place the
// "one call, one answer" assumption in every other marshalling path is not what happens.
// `float_to_mant_exp` splits a float into an eight-byte mantissa and a four-byte exponent,
// each into its own guest buffer, and answers with a status rather than a byte count. Two
// regions means two independent bounds checks, two writes, and an ordering between them —
// none of which the single-output shapes exercise. `host_calls` pins that wiring against a
// mock; this proves the real impl drives it the same way, with the guest reading both
// halves back out of its own memory.
struct FloatToMantExpE2e : RealVmTest
{
};
TEST_F(FloatToMantExpE2e, ContractReadsBothHalvesOfASplitFloat)
{
// Pi's canonical encoding in, mantissa to offset 64, exponent to offset 128. The
// contract returns the low half of the mantissa so the assertion checks that real bytes
// landed in the guest's buffer, not merely that the call reported success.
auto const wat = std::format(
R"wat(
(module
(import "host_lib" "float_to_mant_exp" (func $split (param i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0) "{}")
(func (export "escrow_finish") (result i32)
(local $r i32)
(local.set $r (call $split
(i32.const 0) (i32.const 12)
(i32.const 64) (i32.const 8)
(i32.const 128) (i32.const 4)))
(if (i32.lt_s (local.get $r) (i32.const 0)) (then (return (local.get $r))))
(i32.load (i32.const 64))))
)wat",
watEscaped(FloatTest::kPi));
auto const outcome = run(wat);
ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
// The expected value is derived from the input rather than written out as a literal,
// because the derivation is the interesting part: a float stores its mantissa in the
// first eight bytes **big-endian**, while `float_to_mant_exp` writes it to the guest
// **little-endian**. So the guest's `i32.load` at the start of the mantissa buffer sees
// the *low* 32 bits of a number whose bytes arrived in the opposite order. Getting that
// flip wrong is exactly the convention mismatch this layer exists to catch, and a
// hard-coded constant would hide it.
auto mantissa = std::int64_t{0};
for (auto i = 0U; i < 8; ++i)
{
mantissa = (mantissa << 8) | FloatTest::kPi[i];
}
EXPECT_EQ(outcome->result, static_cast<std::int32_t>(mantissa & 0xFFFFFFFF));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,52 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/fixtures/RealVmTest.h>
#include <cstdint>
#include <format>
#include <string>
namespace xrpl::test {
// The error channel, driven by a real failure rather than a mock's canned one.
// Every other e2e case here proves a success path. But a contract spends most of its life
// reacting to codes, and the path a *real* error takes is different from the one a mock
// error takes: the impl returns a `HostFunctionError`, `HostContext` turns it into a wire
// code, and the engine hands that back to the guest as a negative i32 without disturbing the
// run. `host_calls` proves the middle step against a mock that was *told* to fail; nothing
// until now has proved that a real impl's real failure comes out the far end intact.
struct HostErrorE2e : RealVmTest
{
};
TEST_F(HostErrorE2e, ARealHostErrorReachesTheGuestAsItsWireCode)
{
// The contract runs against an account root, then asks it for `sfMemoData` — a field
// that object does not carry. The impl genuinely fails to find it, so the code the
// guest reads was produced by the real lookup rather than staged.
auto const owner = fund("owner");
auto const wat = std::format(
R"wat(
(module
(import "host_lib" "home_le_field" (func $home_le_field (param i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "escrow_finish") (result i32)
(call $home_le_field (i32.const {}) (i32.const 0) (i32.const 32))))
)wat",
sfMemoData.getCode());
auto const outcome = run(wat, keylet::account(owner.id()));
// The run itself succeeds: a soft host error is an answer to the contract, not a fault
// in it. Reporting it as a failed run would be the interesting bug here.
ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
EXPECT_EQ(outcome->result, static_cast<std::int32_t>(HostFunctionError::FieldNotFound));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,49 @@
#include <xrpl/protocol/TER.h>
#include <gtest/gtest.h>
#include <tx/wasm/fixtures/RealVmTest.h>
#include <string_view>
namespace xrpl::test {
// A single contract that tours several host functions end to end — a ledger-header read, the
// base fee, a hash, a keylet, and a data write — returning 1 only if every call succeeds.
struct HostFunctionTourE2e : RealVmTest
{
};
TEST_F(HostFunctionTourE2e, AContractTouringManyHostFunctionsSucceeds)
{
// Each call must return >= 0 (a byte count, i.e. success); the guest returns the first
// negative error code, or 1 if the whole tour succeeds. Output regions are disjoint so no
// call clobbers another, and buffers are generous so exact value sizes don't matter.
static constexpr auto kWat = std::string_view{R"wat(
(module
(import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
(import "host_lib" "base_fee" (func $base_fee (param i32 i32) (result i32)))
(import "host_lib" "sha512_half" (func $sha512_half (param i32 i32 i32 i32) (result i32)))
(import "host_lib" "accountroot_id" (func $accountroot_id (param i32 i32 i32 i32) (result i32)))
(import "host_lib" "set_data" (func $set_data (param i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "escrow_finish") (result i32)
(local $r i32)
(local.set $r (call $ldgr_index (i32.const 0) (i32.const 32)))
(if (i32.lt_s (local.get $r) (i32.const 0)) (then (return (local.get $r))))
(local.set $r (call $base_fee (i32.const 32) (i32.const 32)))
(if (i32.lt_s (local.get $r) (i32.const 0)) (then (return (local.get $r))))
(local.set $r (call $sha512_half (i32.const 0) (i32.const 4) (i32.const 64) (i32.const 32)))
(if (i32.lt_s (local.get $r) (i32.const 0)) (then (return (local.get $r))))
(local.set $r (call $accountroot_id (i32.const 0) (i32.const 20) (i32.const 128) (i32.const 32)))
(if (i32.lt_s (local.get $r) (i32.const 0)) (then (return (local.get $r))))
(local.set $r (call $set_data (i32.const 0) (i32.const 8)))
(if (i32.lt_s (local.get $r) (i32.const 0)) (then (return (local.get $r))))
(i32.const 1)))
)wat"};
auto const outcome = run(kWat);
ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
EXPECT_EQ(outcome->result, 1) << "every host call in the tour should have succeeded";
}
} // namespace xrpl::test

View File

@@ -0,0 +1,33 @@
#include <xrpl/protocol/TER.h>
#include <gtest/gtest.h>
#include <tx/wasm/fixtures/RealVmTest.h>
#include <cstdint>
#include <string_view>
namespace xrpl::test {
// The real ledger's sequence.
struct LedgerSqnE2e : RealVmTest
{
};
TEST_F(LedgerSqnE2e, ContractReadsTheRealLedgerSequence)
{
// Ask the host for the ledger sequence into offset 0, then return the i32 stored there.
static constexpr auto kWat = std::string_view{R"wat(
(module
(import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "escrow_finish") (result i32)
(drop (call $ldgr_index (i32.const 0) (i32.const 4)))
(i32.load (i32.const 0))))
)wat"};
auto const outcome = run(kWat);
ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
EXPECT_EQ(outcome->result, static_cast<std::int32_t>(ledger.getOpenLedger().header().seq));
}
} // namespace xrpl::test

View File

@@ -0,0 +1,31 @@
#include <xrpl/protocol/TER.h>
#include <gtest/gtest.h>
#include <tx/wasm/fixtures/RealVmTest.h>
#include <string_view>
namespace xrpl::test {
// A contract writes its data field end to end.
struct SetDataE2e : RealVmTest
{
};
TEST_F(SetDataE2e, ContractWritesItsData)
{
// `set_data` over 8 bytes of (zero-initialized) memory returns the byte count it stored.
static constexpr auto kWat = std::string_view{R"wat(
(module
(import "host_lib" "set_data" (func $set_data (param i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "escrow_finish") (result i32)
(call $set_data (i32.const 0) (i32.const 8))))
)wat"};
auto const outcome = run(kWat);
ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
EXPECT_EQ(outcome->result, 8);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,47 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <helpers/TxTest.h>
#include <tx/wasm/fixtures/RealVmTest.h>
#include <tx/wasm/fixtures/WasmLedger.h>
#include <cstdint>
#include <format>
#include <string>
namespace xrpl::test {
// A contract reads a field of its transaction end to end.
struct TxFieldE2e : RealVmTest
{
};
TEST_F(TxFieldE2e, ContractReadsAFieldOfItsTransaction)
{
auto const owner = Account{"owner"};
ledger.createAccount(owner, XRP(1000));
static constexpr auto kScale = std::uint8_t{8};
auto const tx = mptIssuanceCreateTx(owner, kScale);
// Ask the tx for `sfAssetScale` (a single byte) and return the i32 the guest loads — the
// scale, zero-extended — so the assertion checks the value flowed through, not just a count.
auto const wat = std::format(
R"wat(
(module
(import "host_lib" "tx_field" (func $tx_field (param i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "escrow_finish") (result i32)
(drop (call $tx_field (i32.const {}) (i32.const 0) (i32.const 4)))
(i32.load (i32.const 0))))
)wat",
sfAssetScale.getCode());
auto const outcome = run(wat, keylet::account(owner.id()), tx.type, tx.build);
ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
EXPECT_EQ(outcome->result, kScale);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,64 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TER.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/fixtures/RealVmTest.h>
#include <tx/wasm/fixtures/WasmLedger.h>
#include <format>
#include <string>
#include <utility>
namespace xrpl::test {
// The locator convention, end to end.
struct TxNestedFieldE2e : RealVmTest
{
// An EscrowFinish carrying a memo, so the locator has a real leaf to reach.
TxAssembler
withMemo(Account const& acct)
{
auto assembler = escrowFinishTx(ledger, acct);
assembler.build = [inner = std::move(assembler.build)](STObject& obj) {
inner(obj);
auto memos = STArray{};
auto memo = STObject::makeInnerObject(sfMemo);
memo.setFieldVL(sfMemoData, Slice{"hello", 5});
memos.push_back(std::move(memo));
obj.setFieldArray(sfMemos, memos);
};
return assembler;
}
};
TEST_F(TxNestedFieldE2e, ContractWalksALocatorToANestedTransactionField)
{
auto const owner = fund("owner");
auto assembler = withMemo(owner);
auto const wat = std::format(
R"wat(
(module
(import "host_lib" "tx_inner" (func $tx_inner (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(func (export "escrow_finish") (result i32)
(i32.store (i32.const 0) (i32.const {}))
(i32.store (i32.const 4) (i32.const 0))
(i32.store (i32.const 8) (i32.const {}))
(call $tx_inner (i32.const 0) (i32.const 12) (i32.const 64) (i32.const 32))))
)wat",
sfMemos.getCode(),
sfMemoData.getCode());
auto const outcome = run(wat, keylet::account(owner.id()), assembler.type, assembler.build);
ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
// Five bytes: "hello", the memo's data, reached through the locator.
EXPECT_EQ(outcome->result, 5);
}
} // namespace xrpl::test

View File

@@ -3,15 +3,17 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <tx/wasm/RealHostFixture.h>
#include <cstdint>
#include <limits>
#include <string_view>
// Canonical float encodings, with no ledger and no test framework behind them — just the byte
// patterns the float host functions take and return. Benchmarks include this directly;
// `FloatFixture.h` mixes it into the GTest fixture the `host_functions/` tests use.
namespace xrpl::test {
struct FloatTest : RealHostFixture
struct FloatConstants
{
static constexpr std::int64_t kMin64 = std::numeric_limits<std::int64_t>::min();
static constexpr std::int64_t kMax64 = std::numeric_limits<std::int64_t>::max();

View File

@@ -0,0 +1,15 @@
#pragma once
#include <tx/wasm/fixtures/FloatConstants.h>
#include <tx/wasm/fixtures/RealHostFixture.h>
// The float constants with a real ledger and GTest attached, for the `host_functions/Float*`
// tests. The constants alone are in FloatConstants.h, which links no test framework.
namespace xrpl::test {
struct FloatTest : RealHostFixture, FloatConstants
{
};
} // namespace xrpl::test

View File

@@ -1,4 +1,4 @@
#include <tx/wasm/HostContextFixture.h>
#include <tx/wasm/fixtures/HostContextFixture.h>
#include <xrpl/tx/wasm/WasmCommon.h>

View File

@@ -8,7 +8,7 @@
#include <gtest/gtest.h>
#include <helpers/CaptureSink.h>
#include <rust/cxx.h>
#include <tx/wasm/MockHostFunctions.h>
#include <tx/wasm/fixtures/MockHostFunctions.h>
#include <cstddef>
#include <cstdint>

View File

@@ -0,0 +1,26 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <helpers/Account.h>
#include <tx/wasm/fixtures/NftSetup.h>
#include <tx/wasm/fixtures/RealHostFixture.h>
#include <optional>
#include <string_view>
// The NFToken helpers with a real ledger and GTest attached, for the `host_functions/NFT*` tests.
// The ledger-only versions are in NftSetup.h, which links no test framework.
namespace xrpl::test {
struct NFTTest : RealHostFixture, NftIds
{
uint256
mintNFT(Account const& issuer, std::optional<std::string_view> uri = std::nullopt)
{
return mintNft(*this, issuer, uri);
}
};
} // namespace xrpl::test

View File

@@ -1,4 +1,4 @@
#include <tx/wasm/NFTFixture.h>
#include <tx/wasm/fixtures/NftSetup.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/AccountID.h>
@@ -10,28 +10,33 @@
#include <xrpl/protocol_autogen/transactions/NFTokenMint.h> // IWYU pragma: keep
#include <xrpl/tx/transactors/nft/NFTokenMint.h>
#include <gtest/gtest.h>
#include <helpers/Account.h>
#include <tx/wasm/fixtures/WasmLedger.h>
#include <optional>
#include <string>
#include <string_view>
namespace xrpl::test {
uint256
NFTTest::makeNftId(AccountID const& issuer)
NftIds::makeNftId(AccountID const& issuer)
{
return NFTokenMint::createNFTokenID(kFlags, kFee, issuer, nft::toTaxon(kTaxon), kSequence);
}
uint256
NFTTest::mintNFT(Account const& issuer, std::optional<std::string_view> uri)
mintNft(WasmLedger& fixture, Account const& issuer, std::optional<std::string_view> uri)
{
auto& ledger = fixture.ledger;
auto builder = transactions::NFTokenMintBuilder{issuer.id(), 0u};
if (uri)
builder.setURI(Slice{uri->data(), uri->size()});
auto const r = ledger.submit(builder, issuer);
EXPECT_EQ(r.ter, tesSUCCESS) << transToken(r.ter);
if (r.ter != tesSUCCESS)
{
fixtureFailed(std::string{"minting the NFToken: "} + transToken(r.ter));
}
ledger.close();
// The single minted token lives in the owner's first NFTokenPage.
@@ -39,14 +44,21 @@ NFTTest::mintNFT(Account const& issuer, std::optional<std::string_view> uri)
auto const first = keylet::nftokenPageMin(issuer.id()).key;
auto const last = keylet::nftokenPageMax(issuer.id()).key;
auto const pageKey = view.succ(first, last.next());
EXPECT_TRUE(pageKey.has_value());
auto const page = pageKey ? view.read(Keylet{ltNFTOKEN_PAGE, *pageKey}) : nullptr;
EXPECT_NE(page, nullptr);
if (!page)
return uint256{};
if (!pageKey.has_value())
{
fixtureFailed("finding the minted token's NFTokenPage");
}
auto const page = view.read(Keylet{ltNFTOKEN_PAGE, *pageKey});
if (page == nullptr)
{
fixtureFailed("reading the minted token's NFTokenPage");
}
auto const& tokens = page->getFieldArray(sfNFTokens);
EXPECT_FALSE(tokens.empty());
return tokens.empty() ? uint256{} : tokens[0].getFieldH256(sfNFTokenID);
if (tokens.empty())
{
fixtureFailed("the NFTokenPage holds no tokens");
}
return tokens[0].getFieldH256(sfNFTokenID);
}
} // namespace xrpl::test

View File

@@ -0,0 +1,46 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/nft.h>
#include <helpers/Account.h>
#include <tx/wasm/fixtures/WasmLedger.h>
#include <cstdint>
#include <optional>
#include <string_view>
// NFToken setup, built on `WasmLedger` rather than on a GTest fixture so a benchmark can mint a
// token without linking a test framework. Tests reach the same helpers through
// `RealHostFixture`, which derives from `WasmLedger`.
namespace xrpl::test {
// The fields baked into `makeNftId`, so a caller can assert an extractor returned the right one.
struct NftIds
{
static constexpr std::uint16_t kFlags = nft::kFlagTransferable | nft::kFlagBurnable;
static constexpr std::uint16_t kFee = 314;
static constexpr std::uint32_t kTaxon = 12345;
static constexpr std::uint32_t kSequence = 7;
// A well-formed id carrying the constants above. Computed, not minted: the id-extractor host
// functions read the id itself and never touch the ledger.
static uint256
makeNftId(AccountID const& issuer);
};
// Mint a real NFToken owned by `issuer` (taxon 0) and return its id, read back from the owner's
// NFTokenPage. `TxTest` applies to the open ledger, which produces no metadata, so the id comes
// from ledger state rather than from the mint's metadata.
//
// Throws via `fixtureFailed` if the mint or the page lookup fails; see WasmLedger.h for why that
// is a throw and not an `EXPECT_`.
uint256
mintNft(
WasmLedger& fixture,
Account const& issuer,
std::optional<std::string_view> uri = std::nullopt);
} // namespace xrpl::test

Some files were not shown because too many files have changed in this diff Show More