mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-26 23:19:07 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
17
crates/Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -9,3 +9,4 @@ crate-type = ["staticlib", "rlib"]
|
||||
[dependencies]
|
||||
cxx.workspace = true
|
||||
wat = "1"
|
||||
xrpl-host-functions = { path = "../xrpl-host-functions" }
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(_));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(_));
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
175
src/benchmarks/libxrpl/wasm/BenchFixtures.cpp
Normal file
175
src/benchmarks/libxrpl/wasm/BenchFixtures.cpp
Normal 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
|
||||
111
src/benchmarks/libxrpl/wasm/BenchFixtures.h
Normal file
111
src/benchmarks/libxrpl/wasm/BenchFixtures.h
Normal 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
|
||||
44
src/benchmarks/libxrpl/wasm/Crossing.cpp
Normal file
44
src/benchmarks/libxrpl/wasm/Crossing.cpp
Normal 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
|
||||
180
src/benchmarks/libxrpl/wasm/README.md
Normal file
180
src/benchmarks/libxrpl/wasm/README.md
Normal 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_`.
|
||||
149
src/benchmarks/libxrpl/wasm/Vm.cpp
Normal file
149
src/benchmarks/libxrpl/wasm/Vm.cpp
Normal 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
|
||||
397
src/benchmarks/libxrpl/wasm/WasmBench.cpp
Normal file
397
src/benchmarks/libxrpl/wasm/WasmBench.cpp
Normal 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
|
||||
369
src/benchmarks/libxrpl/wasm/WasmBench.h
Normal file
369
src/benchmarks/libxrpl/wasm/WasmBench.h
Normal 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
|
||||
24
src/benchmarks/libxrpl/wasm/host_functions/AccountKeylet.cpp
Normal file
24
src/benchmarks/libxrpl/wasm/host_functions/AccountKeylet.cpp
Normal 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
|
||||
31
src/benchmarks/libxrpl/wasm/host_functions/AmmKeylet.cpp
Normal file
31
src/benchmarks/libxrpl/wasm/host_functions/AmmKeylet.cpp
Normal 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
|
||||
24
src/benchmarks/libxrpl/wasm/host_functions/BaseFee.cpp
Normal file
24
src/benchmarks/libxrpl/wasm/host_functions/BaseFee.cpp
Normal 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
|
||||
@@ -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
|
||||
26
src/benchmarks/libxrpl/wasm/host_functions/CheckKeylet.cpp
Normal file
26
src/benchmarks/libxrpl/wasm/host_functions/CheckKeylet.cpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
24
src/benchmarks/libxrpl/wasm/host_functions/DidKeylet.cpp
Normal file
24
src/benchmarks/libxrpl/wasm/host_functions/DidKeylet.cpp
Normal 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
|
||||
55
src/benchmarks/libxrpl/wasm/host_functions/EscrowKeylet.cpp
Normal file
55
src/benchmarks/libxrpl/wasm/host_functions/EscrowKeylet.cpp
Normal 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
|
||||
45
src/benchmarks/libxrpl/wasm/host_functions/FloatAdd.cpp
Normal file
45
src/benchmarks/libxrpl/wasm/host_functions/FloatAdd.cpp
Normal 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
|
||||
24
src/benchmarks/libxrpl/wasm/host_functions/FloatCompare.cpp
Normal file
24
src/benchmarks/libxrpl/wasm/host_functions/FloatCompare.cpp
Normal 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
|
||||
27
src/benchmarks/libxrpl/wasm/host_functions/FloatDivide.cpp
Normal file
27
src/benchmarks/libxrpl/wasm/host_functions/FloatDivide.cpp
Normal 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
|
||||
24
src/benchmarks/libxrpl/wasm/host_functions/FloatFromInt.cpp
Normal file
24
src/benchmarks/libxrpl/wasm/host_functions/FloatFromInt.cpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
24
src/benchmarks/libxrpl/wasm/host_functions/FloatFromUint.cpp
Normal file
24
src/benchmarks/libxrpl/wasm/host_functions/FloatFromUint.cpp
Normal 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
|
||||
27
src/benchmarks/libxrpl/wasm/host_functions/FloatMultiply.cpp
Normal file
27
src/benchmarks/libxrpl/wasm/host_functions/FloatMultiply.cpp
Normal 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
|
||||
42
src/benchmarks/libxrpl/wasm/host_functions/FloatPower.cpp
Normal file
42
src/benchmarks/libxrpl/wasm/host_functions/FloatPower.cpp
Normal 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
|
||||
27
src/benchmarks/libxrpl/wasm/host_functions/FloatSubtract.cpp
Normal file
27
src/benchmarks/libxrpl/wasm/host_functions/FloatSubtract.cpp
Normal 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
|
||||
24
src/benchmarks/libxrpl/wasm/host_functions/FloatToInt.cpp
Normal file
24
src/benchmarks/libxrpl/wasm/host_functions/FloatToInt.cpp
Normal 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
|
||||
@@ -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
|
||||
34
src/benchmarks/libxrpl/wasm/host_functions/GetNFT.cpp
Normal file
34
src/benchmarks/libxrpl/wasm/host_functions/GetNFT.cpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
38
src/benchmarks/libxrpl/wasm/host_functions/LedgerSqn.cpp
Normal file
38
src/benchmarks/libxrpl/wasm/host_functions/LedgerSqn.cpp
Normal 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
|
||||
@@ -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
|
||||
31
src/benchmarks/libxrpl/wasm/host_functions/MptokenKeylet.cpp
Normal file
31
src/benchmarks/libxrpl/wasm/host_functions/MptokenKeylet.cpp
Normal 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
|
||||
24
src/benchmarks/libxrpl/wasm/host_functions/NFTFlags.cpp
Normal file
24
src/benchmarks/libxrpl/wasm/host_functions/NFTFlags.cpp
Normal 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
|
||||
24
src/benchmarks/libxrpl/wasm/host_functions/NFTIssuer.cpp
Normal file
24
src/benchmarks/libxrpl/wasm/host_functions/NFTIssuer.cpp
Normal 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
|
||||
24
src/benchmarks/libxrpl/wasm/host_functions/NFTSequence.cpp
Normal file
24
src/benchmarks/libxrpl/wasm/host_functions/NFTSequence.cpp
Normal 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
|
||||
24
src/benchmarks/libxrpl/wasm/host_functions/NFTTaxon.cpp
Normal file
24
src/benchmarks/libxrpl/wasm/host_functions/NFTTaxon.cpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
26
src/benchmarks/libxrpl/wasm/host_functions/OfferKeylet.cpp
Normal file
26
src/benchmarks/libxrpl/wasm/host_functions/OfferKeylet.cpp
Normal 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
|
||||
26
src/benchmarks/libxrpl/wasm/host_functions/OracleKeylet.cpp
Normal file
26
src/benchmarks/libxrpl/wasm/host_functions/OracleKeylet.cpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
69
src/benchmarks/libxrpl/wasm/host_functions/Sha512Half.cpp
Normal file
69
src/benchmarks/libxrpl/wasm/host_functions/Sha512Half.cpp
Normal 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
|
||||
@@ -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
|
||||
26
src/benchmarks/libxrpl/wasm/host_functions/TicketKeylet.cpp
Normal file
26
src/benchmarks/libxrpl/wasm/host_functions/TicketKeylet.cpp
Normal 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
|
||||
40
src/benchmarks/libxrpl/wasm/host_functions/Trace.cpp
Normal file
40
src/benchmarks/libxrpl/wasm/host_functions/Trace.cpp
Normal 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
|
||||
@@ -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(); },
|
||||
[¤cy](auto& host) {
|
||||
return host.trustLineKeylet(
|
||||
Fixtures::instance().alice().id(), Fixtures::instance().bob().id(), currency);
|
||||
});
|
||||
}
|
||||
BENCHMARK(trustLineKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
|
||||
|
||||
} // namespace
|
||||
} // namespace xrpl::test::bench
|
||||
26
src/benchmarks/libxrpl/wasm/host_functions/TxArrayLen.cpp
Normal file
26
src/benchmarks/libxrpl/wasm/host_functions/TxArrayLen.cpp
Normal 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
|
||||
26
src/benchmarks/libxrpl/wasm/host_functions/TxField.cpp
Normal file
26
src/benchmarks/libxrpl/wasm/host_functions/TxField.cpp
Normal 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
|
||||
@@ -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
|
||||
55
src/benchmarks/libxrpl/wasm/host_functions/TxNestedField.cpp
Normal file
55
src/benchmarks/libxrpl/wasm/host_functions/TxNestedField.cpp
Normal 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
|
||||
62
src/benchmarks/libxrpl/wasm/host_functions/UpdateData.cpp
Normal file
62
src/benchmarks/libxrpl/wasm/host_functions/UpdateData.cpp
Normal 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
|
||||
26
src/benchmarks/libxrpl/wasm/host_functions/VaultKeylet.cpp
Normal file
26
src/benchmarks/libxrpl/wasm/host_functions/VaultKeylet.cpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
{
|
||||
|
||||
90
src/tests/libxrpl/tx/wasm/README.md
Normal file
90
src/tests/libxrpl/tx/wasm/README.md
Normal 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.
|
||||
@@ -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
|
||||
{
|
||||
};
|
||||
|
||||
|
||||
63
src/tests/libxrpl/tx/wasm/e2e/CacheLedgerObj.cpp
Normal file
63
src/tests/libxrpl/tx/wasm/e2e/CacheLedgerObj.cpp
Normal 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
|
||||
66
src/tests/libxrpl/tx/wasm/e2e/CurrentLedgerObjField.cpp
Normal file
66
src/tests/libxrpl/tx/wasm/e2e/CurrentLedgerObjField.cpp
Normal 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
|
||||
66
src/tests/libxrpl/tx/wasm/e2e/FloatToMantExp.cpp
Normal file
66
src/tests/libxrpl/tx/wasm/e2e/FloatToMantExp.cpp
Normal 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
|
||||
52
src/tests/libxrpl/tx/wasm/e2e/HostError.cpp
Normal file
52
src/tests/libxrpl/tx/wasm/e2e/HostError.cpp
Normal 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
|
||||
49
src/tests/libxrpl/tx/wasm/e2e/HostFunctionTour.cpp
Normal file
49
src/tests/libxrpl/tx/wasm/e2e/HostFunctionTour.cpp
Normal 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
|
||||
33
src/tests/libxrpl/tx/wasm/e2e/LedgerSqn.cpp
Normal file
33
src/tests/libxrpl/tx/wasm/e2e/LedgerSqn.cpp
Normal 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
|
||||
31
src/tests/libxrpl/tx/wasm/e2e/SetData.cpp
Normal file
31
src/tests/libxrpl/tx/wasm/e2e/SetData.cpp
Normal 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
|
||||
47
src/tests/libxrpl/tx/wasm/e2e/TxField.cpp
Normal file
47
src/tests/libxrpl/tx/wasm/e2e/TxField.cpp
Normal 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
|
||||
64
src/tests/libxrpl/tx/wasm/e2e/TxNestedField.cpp
Normal file
64
src/tests/libxrpl/tx/wasm/e2e/TxNestedField.cpp
Normal 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
|
||||
@@ -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();
|
||||
15
src/tests/libxrpl/tx/wasm/fixtures/FloatFixture.h
Normal file
15
src/tests/libxrpl/tx/wasm/fixtures/FloatFixture.h
Normal 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
|
||||
@@ -1,4 +1,4 @@
|
||||
#include <tx/wasm/HostContextFixture.h>
|
||||
#include <tx/wasm/fixtures/HostContextFixture.h>
|
||||
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
|
||||
@@ -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>
|
||||
26
src/tests/libxrpl/tx/wasm/fixtures/NFTFixture.h
Normal file
26
src/tests/libxrpl/tx/wasm/fixtures/NFTFixture.h
Normal 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
|
||||
@@ -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
|
||||
46
src/tests/libxrpl/tx/wasm/fixtures/NftSetup.h
Normal file
46
src/tests/libxrpl/tx/wasm/fixtures/NftSetup.h
Normal 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
Reference in New Issue
Block a user