Compare commits

...

4 Commits

Author SHA1 Message Date
TimothyBanks
436dc7824b fix: Charge to transfer budget on update_data host call 2026-09-24 20:40:39 -04:00
Timothy Banks
f04386dcf7 fix: Pin dynamic fuel costs in wasmi engine (#8259) 2026-09-24 15:57:02 +01:00
pwang200
58a3a4e5ba gas price must > 0 (#8270) 2026-09-23 16:03:06 -04:00
Timothy Banks
7e4338bb2e fix: Address AI code review comments (#8269) 2026-09-23 18:47:23 +01:00
19 changed files with 550 additions and 126 deletions

View File

@@ -6,7 +6,7 @@ bridged into C++ via `cxxbridge`/the `cxx` crate.
The workspace is built unconditionally — `add_subdirectory(crates)` in the
top-level `CMakeLists.txt` is not behind an option, and
`xrpl_wasm_vm_ffi_cxxbridge` is a `PUBLIC` dependency of
`xrpl.libxrpl.ledger` (see `cmake/XrplCore.cmake`). The Rust toolchain pinned in
`xrpl.libxrpl.tx` (see `cmake/XrplCore.cmake`). The Rust toolchain pinned in
[`rust-toolchain.toml`](../rust-toolchain.toml) is therefore required to build
`libxrpl` at all; the Nix devshell provides it automatically.

View File

@@ -132,7 +132,7 @@ fn charge<T>(caller: &mut Caller<'_, T>, cost: u64) -> CallResult<()> {
}
}
fn charge_transfer(state: &VmState<'_>, n: usize) -> Result<(), HostError> {
pub(crate) fn charge_transfer(state: &VmState<'_>, n: usize) -> Result<(), HostError> {
let n = n as u64;
let remaining = state.transfer_budget.get();
match remaining.checked_sub(n) {

View File

@@ -24,7 +24,9 @@
//! `trace` is the sixtieth: its declared `HostResult<()>` gives it a
//! `CallResult<()>` body and the `charged_unreported` helper.
use crate::abi::{CallResult, guest_memory, write_buffered, write_into, write_mant_exp};
use crate::abi::{
CallResult, charge_transfer, guest_memory, write_buffered, write_into, write_mant_exp,
};
use crate::args::{InBytes, InStr, InU32, OutBytes, TraceCode};
use crate::vm::VmState;
use wasmi::Caller;
@@ -499,8 +501,9 @@ impl HostFunctionBodies for Bodies {
fn update_data(caller: &mut Caller<'_, VmState<'_>>, data: InBytes) -> CallResult<i32> {
let memory = guest_memory(caller)?;
let host = caller.data().host;
Ok(host.update_data(data.read(memory)?)?)
let bytes = data.read(memory)?;
charge_transfer(caller.data(), bytes.len())?;
Ok(caller.data().host.update_data(bytes)?)
}
fn get_nft(

View File

@@ -1,8 +1,8 @@
use std::cell::Cell;
use std::fmt;
use wasmi::{
CompilationMode, Config, Engine, Export, Linker, Memory, Module, Store, StoreLimits,
StoreLimitsBuilder, TrapCode,
CompilationMode, Config, CustomFuelCosts, EnforcedLimits, Engine, Export, Linker, Memory,
Module, Store, StoreLimits, StoreLimitsBuilder, TrapCode,
};
use xrpl_host_functions::HostFunctions;
@@ -262,6 +262,17 @@ pub(crate) fn wasm_engine() -> Engine {
// config.wasm_memory64(false);
config.wasm_wide_arithmetic(false);
config.allow_start_fn(false);
config.enforced_limits(EnforcedLimits::strict());
let fuel_costs = CustomFuelCosts {
bytes_copied_per_fuel: 64,
fuel_per_bytes_translated: 7,
fuel_per_bytes_validated: 2,
};
config.fuel_cost(fuel_costs);
// config.operator_costs is already guarded by the probe_fuel test under budgets.rs
// in that a change to operator costs in a future version will be a loud failure.
config.compilation_mode(CompilationMode::LazyTranslation);
Engine::new(&config)
}
@@ -396,6 +407,48 @@ mod tests {
assert_eq!(limits.memories(), 1);
}
/// The three size-proportional fuel rates, read back off the engine. wasmi
/// takes its own defaults for these unless told otherwise, so an upgrade that
/// changed one would retune our gas silently. `Config` keeps them
/// `pub(crate)` and exposes them only through `Debug`.
#[test]
fn the_dynamic_fuel_costs_are_pinned() {
let config = format!("{:?}", wasm_engine().config());
for rate in [
"bytes_copied_per_fuel: 64",
"fuel_per_bytes_translated: 7",
"fuel_per_bytes_validated: 2",
] {
assert!(config.contains(rate), "expected `{rate}` in {config}");
}
}
/// [`EnforcedLimits::strict`] is the one line in [`wasm_engine`] that takes a
/// value rather than stating one — the fields are `pub(crate)`, so the preset is
/// the only way to set them.
#[test]
fn the_enforced_limits_are_pinned() {
const EXPECTED: &str = concat!(
"EnforcedLimits { ",
"max_globals: Some(1000), ",
"max_functions: Some(10000), ",
"max_tables: Some(100), ",
"max_element_segments: Some(1000), ",
"max_memories: Some(1), ",
"max_data_segments: Some(1000), ",
"max_params: Some(32), ",
"max_results: Some(32), ",
"min_avg_bytes_per_function: Some(AvgBytesPerFunctionLimit { ",
"req_funcs_bytes: 1000, min_avg_bytes_per_function: 40 }) }",
);
let config = format!("{:?}", wasm_engine().config());
assert!(
config.contains(EXPECTED),
"expected `{EXPECTED}` in {config}"
);
}
/// The only place these numbers appear as literals; every other test derives
/// them from the constants.
#[test]

View File

@@ -855,6 +855,10 @@ fn a_write_may_deliver_what_is_left_of_the_budget_and_not_a_byte_more() {
/// guest memory, so there are no copied bytes to charge. What bounds how many reads
/// a run can make is gas, which every host call pays before its body runs.
///
/// `set_data` is the one input that *is* charged - see
/// [`set_data_spends_the_transfer_budget`] - because its bytes are copied out of
/// guest memory into the ledger rather than borrowed for the length of the call.
///
/// The observation is the write at the end, not the reads: the module reads four
/// times the whole budget first, so a rule that charged reads would have nothing
/// left, and the write would answer `OutOfTransferLimit` instead of a byte count.
@@ -942,6 +946,76 @@ fn only_the_output_half_of_a_read_write_spends_the_budget() {
);
}
/// `set_data` is the exception to the rule above: an input-only call that is charged
/// all the same, because its bytes are copied into the ledger object rather than
/// aliasing guest memory the way a `trace` or a `sha512_half` input does.
///
/// The count is the assertion worth reading. `write_into` asks the host first and
/// charges after, so [`writes_spend_the_transfer_budget`] sees one call past the
/// budget; here the charge precedes the call, so the refused one never arrives.
#[test]
fn set_data_spends_the_transfer_budget() {
/// 1 KiB blobs, exactly the whole budget.
const CALLS: u64 = TRANSFER_LIMIT_BYTES / MAX_FIELD_BYTES as u64;
let host = FakeHost::new().answering_update_data(Ok(MAX_FIELD_BYTES as i32));
let wat = until_refused(
import::SET_DATA,
&format!("(call $set_data (i32.const 0) (i32.const {MAX_FIELD_BYTES}))"),
WHILE_POSITIVE,
);
let outcome = run(&wat, &host).expect("the module should run");
assert_eq!(outcome.result, code(HostError::OutOfTransferLimit));
assert_eq!(
host.update_data_asked.borrow().len() as u64,
CALLS,
"the refused call must not reach the host at all"
);
}
/// What charging first costs: a call the host refuses has spent its bytes anyway.
///
/// That is the deliberate half of the trade. `set_data` mutates the ledger and the
/// engine cannot undo it, so a charge applied afterwards could fail with the data
/// already stored - answering `OutOfTransferLimit` for a call that happened. An
/// overcharge on a refused call is the lesser fault, and gas behaves the same way:
/// a failed host call keeps the gas it was charged before its body ran.
///
/// The budget is read back through a `home_le_field` write, since the refusals
/// themselves report the host's error rather than the budget's.
#[test]
fn a_failed_set_data_still_spends_the_budget() {
const CALLS: u64 = TRANSFER_LIMIT_BYTES / MAX_FIELD_BYTES as u64;
let host = FakeHost::new()
.answering_update_data(Err(HostError::InvalidParams))
.answering_field(1, Answer::filler(MAX_FIELD_BYTES));
let wat = module(
&[import::SET_DATA, import::HOME_LE_FIELD, ONE_PAGE],
&format!(
"(local $i i32)
(loop $l
(drop (call $set_data (i32.const 0) (i32.const {MAX_FIELD_BYTES})))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $l (i32.lt_u (local.get $i) (i32.const {CALLS}))))
(call $home_le_field (i32.const 1) (i32.const 0) (i32.const {MAX_FIELD_BYTES}))"
),
);
let outcome = run(&wat, &host).expect("the module should run");
assert_eq!(
host.update_data_asked.borrow().len() as u64,
CALLS,
"every call should have reached the host and been refused by it"
);
assert_eq!(
outcome.result,
code(HostError::OutOfTransferLimit),
"the refused calls spent the budget all the same"
);
}
// cspell:disable
/// Measures each pinned fuel figure straight from wasmi and asserts the constant
/// still matches. This is what fails first when a wasmi upgrade shifts the fuel

View File

@@ -168,7 +168,8 @@ fn a_declared_table_maximum_past_the_cap_is_allowed_but_unreachable() {
/// One row per feature `wasm_engine` turns off: the smallest module that uses
/// it, and the fragment of wasmi's refusal that names the feature. A row declaring
/// its own memory omits [`ONE_PAGE`], or it is refused for having two memories
/// instead.
/// instead — except `wasm_multi_memory`, where two memories are the point and one
/// of them has to be imported to reach the feature check at all.
fn disabled_features() -> Vec<(&'static str, Vec<&'static str>, &'static str, &'static str)> {
vec![
(
@@ -223,9 +224,14 @@ fn disabled_features() -> Vec<(&'static str, Vec<&'static str>, &'static str, &'
"(global.get $g)",
"non-constant operator",
),
// One memory imported, one defined. `EnforcedLimits::strict()` caps memories
// at one and checks the memory *section* before the validator sees it
// (`module/parser/mod.rs`, `process_memories`), so two *defined* memories are
// refused for exceeding the cap and never reach the feature check. An import
// is not in that section, so this is the shape that names the proposal.
(
"wasm_multi_memory",
vec![ONE_PAGE, "(memory 1)"],
vec![r#"(import "host_lib" "mem" (memory 1))"#, ONE_PAGE],
"(i32.const 0)",
"multiple memories",
),
@@ -278,7 +284,7 @@ fn every_disabled_feature_is_refused_by_name() {
}
}
/// The three knobs [`every_disabled_feature_is_refused_by_name`] cannot cover. The
/// The knobs [`every_disabled_feature_is_refused_by_name`] cannot cover. The
/// configuration is the same for every engine `wasm_engine` builds, so a test
/// observes the one `wasm_engine` makes: a knob masked by another, or with no
/// caller-visible effect, has no distinguishing module.
@@ -293,6 +299,14 @@ fn the_knobs_without_a_module_of_their_own() {
assert!(refusal.contains("floating-point"), "{refusal}");
assert!(!refusal.contains("saturating"), "{refusal}");
// `EnforcedLimits::strict()`'s `max_memories: Some(1)`, which masks
// `wasm_multi_memory(false)` for every module that defines its two memories
// rather than importing one — the case a real contract would hit. The feature
// flag itself is covered by name in [`disabled_features`].
let wat = module(&[ONE_PAGE, "(memory 1)"], "(i32.const 0)");
let refusal = assert_stage!(failure(&wat, &host), RunError::Compile(_)).to_string();
assert!(refusal.contains("limit of 1 memories"), "{refusal}");
// `ignore_custom_sections(true)`: governs whether wasmi retains custom
// sections, not accept/reject, so this pins only that one is harmless.
let wat = module(
@@ -647,20 +661,36 @@ fn unbounded_recursion_is_stopped_by_the_call_stack_limit() {
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.
/// The CodeMap-DoS defense, from the guest's side: a module of thousands of tiny
/// functions is refused in `Module::new`, before anything is translated.
///
/// Both of `EnforcedLimits::strict()`'s function rules are load-bearing here, which
/// is why the second half exists — dropping under the count cap does not get a
/// module past the defense, because the bodies then fail the minimum average. The
/// values themselves are pinned in `vm.rs`'s `the_enforced_limits_are_pinned`.
#[test]
#[ignore = "CodeMap-DoS unmitigated; a function-count limit is deferred to preflight parsing"]
fn many_functions_currently_run_unbounded() {
fn a_module_of_too_many_functions_is_refused() {
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)))");
let tiny_funcs = |count: usize| {
let funcs: String = (0..count)
.map(|i| format!("(func $f{i} (result i32) (i32.const {}))", i % 7))
.collect();
format!("(module {ONE_PAGE} {funcs} (func (export \"finish\") (result i32) (call $f0)))")
};
// `max_functions: Some(10000)`, checked before the bodies are looked at.
let wat = tiny_funcs(10_001);
let refusal = assert_stage!(failure(&wat, &host), RunError::Compile(_)).to_string();
assert!(refusal.contains("limit of 10000 functions"), "{refusal}");
// `min_avg_bytes_per_function: 40`, enforced once the bodies total 1 KiB. These
// average five bytes, so the count cap is not the only thing holding.
let wat = tiny_funcs(9_999);
let refusal = assert_stage!(failure(&wat, &host), RunError::Compile(_)).to_string();
assert!(
run(&wat, &host).is_ok(),
"a large-function module currently compiles and runs"
refusal.contains("minimum average bytes per function of 40"),
"{refusal}"
);
}

View File

@@ -300,11 +300,20 @@ template <class T>
static int32_t
calculateAdditionalReserve(T const& finishFunction)
{
if (!finishFunction)
return 1;
// First 500 bytes included in the normal reserve
// Each additional 500 bytes requires an additional reserve
return 1 + (finishFunction->size() / 500);
static auto constexpr kBytecodeReserveIncrement = 500;
if (!finishFunction)
return 1;
// Ceiling division answers 0 for an empty field, which would subtract less than
// the create added.
auto const size = finishFunction->size();
if (size == 0)
return 1;
return static_cast<int32_t>((size + kBytecodeReserveIncrement - 1) / kBytecodeReserveIncrement);
}
} // namespace xrpl

View File

@@ -11,15 +11,17 @@ namespace xrpl {
inline constexpr std::uint32_t kFeeUnitsDeprecated = 10;
// Number of micro-drops in one drop.
constexpr std::uint32_t microDropsPerDrop{1'000'000};
inline constexpr std::uint32_t microDropsPerDrop{1'000'000};
/**
* Hard protocol ceilings on the Feature Extension fee settings. A voted value
* can never exceed these, so `preflight`, which has no view, may bound against
* them.
* Hard protocol bounds on the Feature Extension fee settings. A voted value
* can never fall outside these, so `preflight`, which has no view, may bound
* against them. Gas and bytecode limits are capped; gas price has a floor
* (zero would make WASM execution effectively free).
*/
inline constexpr std::uint32_t kMaxGasLimit{2'000'000};
inline constexpr std::uint32_t kMaxBytecodeSizeLimit{200'000};
inline constexpr std::uint32_t kMinGasPrice{1};
// The following default values of fee settings will seed into FeeSettings
// and write to the ledger on featureSmartEscrow activation.

View File

@@ -81,7 +81,7 @@ PathAsset::holds() const
}
template <ValidPathAsset T>
[[nodiscard]] [[nodiscard]] T const&
[[nodiscard]] T const&
PathAsset::get() const
{
if (!holds<T>())

View File

@@ -87,37 +87,37 @@ public:
return true;
}
[[nodiscard]] [[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
getLedgerSqn() const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
getParentLedgerTime() const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Hash, HostFunctionError>
[[nodiscard]] virtual std::expected<Hash, HostFunctionError>
getParentLedgerHash() const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<uint32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<uint32_t, HostFunctionError>
getBaseFee() const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
isAmendmentEnabled(uint256 const& amendmentId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
isAmendmentEnabled(std::string_view const& amendmentName) const
{
return std::unexpected(HostFunctionError::Unimplemented);
@@ -129,73 +129,73 @@ public:
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getTxField(SField const& fname) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getCurrentLedgerObjField(SField const& fname) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getLedgerObjField(int32_t cacheIdx, SField const& fname) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getTxNestedField(FieldLocator const& locator) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getCurrentLedgerObjNestedField(FieldLocator const& locator) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getLedgerObjNestedField(int32_t cacheIdx, FieldLocator const& locator) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getTxArrayLen(SField const& fname) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getCurrentLedgerObjArrayLen(SField const& fname) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getLedgerObjArrayLen(int32_t cacheIdx, SField const& fname) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getTxNestedArrayLen(FieldLocator const& locator) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getCurrentLedgerObjNestedArrayLen(FieldLocator const& locator) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getLedgerObjNestedArrayLen(int32_t cacheIdx, FieldLocator const& locator) const
{
return std::unexpected(HostFunctionError::Unimplemented);
@@ -207,184 +207,184 @@ public:
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
checkSignature(Slice const& message, Slice const& signature, Slice const& pubkey) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Hash, HostFunctionError>
[[nodiscard]] virtual std::expected<Hash, HostFunctionError>
computeSha512HalfHash(Slice const& data) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
accountKeylet(AccountID const& account) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
ammKeylet(Asset const& issue1, Asset const& issue2) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
checkKeylet(AccountID const& account, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
credentialKeylet(AccountID const& subject, AccountID const& issuer, Slice const& credentialType)
const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
didKeylet(AccountID const& account) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
delegateKeylet(AccountID const& account, AccountID const& authorize) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
depositPreauthKeylet(AccountID const& account, AccountID const& authorize) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
escrowKeylet(AccountID const& account, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
trustLineKeylet(AccountID const& account1, AccountID const& account2, Currency const& currency)
const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
mptokenIssuanceKeylet(AccountID const& issuer, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
mptokenKeylet(MPTID const& mptid, AccountID const& holder) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
nftokenOfferKeylet(AccountID const& account, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
offerKeylet(AccountID const& account, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
oracleKeylet(AccountID const& account, std::uint32_t docId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
paychannelKeylet(AccountID const& account, AccountID const& destination, std::uint32_t seq)
const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
permissionedDomainKeylet(AccountID const& account, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
signerListKeylet(AccountID const& account) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
ticketKeylet(AccountID const& account, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
vaultKeylet(AccountID const& account, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
sponsorshipKeylet(AccountID const& sponsor, AccountID const& sponsee) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
loanBrokerKeylet(AccountID const& owner, std::uint32_t seq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
loanKeylet(uint256 const& loanBrokerID, std::uint32_t loanSeq) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getNFT(AccountID const& account, uint256 const& nftId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
getNFTIssuer(uint256 const& nftId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
getNFTTaxon(uint256 const& nftId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getNFTFlags(uint256 const& nftId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<int32_t, HostFunctionError>
getNFTTransferFee(uint256 const& nftId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
[[nodiscard]] virtual std::expected<std::uint32_t, HostFunctionError>
getNFTSequence(uint256 const& nftId) const
{
return std::unexpected(HostFunctionError::Unimplemented);
@@ -397,43 +397,43 @@ public:
{
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatFromInt(int64_t x, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatFromUint(uint64_t x, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatFromSTAmount(STAmount const& x, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatFromSTNumber(STNumber const& x, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<int64_t, HostFunctionError>
[[nodiscard]] virtual std::expected<int64_t, HostFunctionError>
floatToInt(Slice const& x, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<FloatPair, HostFunctionError>
[[nodiscard]] virtual std::expected<FloatPair, HostFunctionError>
floatToMantExp(Slice const& x) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatFromMantExp(int64_t mantissa, int32_t exponent, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
@@ -445,31 +445,31 @@ public:
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatAdd(Slice const& x, Slice const& y, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatSubtract(Slice const& x, Slice const& y, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatMultiply(Slice const& x, Slice const& y, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatDivide(Slice const& x, Slice const& y, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);
}
[[nodiscard]] [[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
[[nodiscard]] virtual std::expected<Bytes, HostFunctionError>
floatPower(Slice const& x, int32_t n, int32_t mode) const
{
return std::unexpected(HostFunctionError::Unimplemented);

View File

@@ -51,9 +51,9 @@ public:
std::expected<int32_t, HostFunctionError>
normalizeCacheIndex(int32_t cacheIdx) const
{
--cacheIdx;
if (cacheIdx < 0 || cacheIdx >= maxCache)
if (cacheIdx <= 0 || cacheIdx > maxCache)
return std::unexpected(HostFunctionError::SlotOutRange);
--cacheIdx;
if (!cache_[cacheIdx])
return std::unexpected(HostFunctionError::EmptySlot);
return cacheIdx;

View File

@@ -35,10 +35,16 @@ contract can buy too cheaply, a denial-of-service vector rather than a rounding
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
`unreliable=1` when `rel_error` exceeds 25%, or when `implied_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.
The two arms catch different failures, and the second is the one that fires in practice. `rel_error`
is about precision; the floor comparison is about how much of `suggested_gas` was measured for this
case at all. On a quiet Release machine the floor is ~36 gas, and the cheap `Impl` cases sit near 3 —
so the floor is over 90% of their price while `rel_error` reads a comfortable 2%. Expect roughly a
quarter of the priced rows to carry `unreliable=1`, all of them `Impl`.
### With `--benchmark_repetitions`
Adds `_mean` / `_median` / `_stddev` / `_cv` rows. One trap worth knowing:
@@ -114,13 +120,22 @@ appearing a second time, because the transactor validates and executes with no m
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,
One caveat 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 convergence rather than any single row. A quiet Release run converges 31.4 → 13.2 → 7.8 → 6.7 →
6.5 across `compileScaling`.
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 filler modules are shaped by the engine's limits, not chosen freely.**
`EnforcedLimits::strict()` refuses any module averaging under 40 bytes per function body once bodies
total 1 KiB — a limit wasmi added to defend lazy compilation against precisely the shape a size
sweep wants. So `fillerWat` gives each body `kFillerChain` mul/add pairs to clear that floor; one
pair averages 12 bytes and is refused outright. Thinning the bodies to get more functions per byte
does not make a harder module, it makes an inadmissible one.
The sweep tops out at 2048 functions, bounded by **bytes** rather than function count: ~158 KiB
against `kMaxBytecodeSizeLimit` of 200,000, where 4096 functions would be ~317 KiB and refused
earlier in the transactor. `strict()`'s own `max_functions` of 10,000 never binds — 40 bytes per
function against a 200,000-byte module caps any admissible contract at 5,000.
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
@@ -129,7 +144,7 @@ The linker rebuild and the fuel-metering overhead are **not** separable from her
### Pin your iteration counts
Pin `->Iterations(...)`: automatic sizing targets a wall-clock budget, not a compile count,
so the cheap cases get six-figure counts and `/4096` a handful — leaving no row comparable to
so the cheap cases get six-figure counts and `/2048` a handful — leaving no row comparable to
another or to the last run.
## Gotchas, each of which has already cost someone an afternoon

View File

@@ -15,7 +15,7 @@ namespace {
// README.md has what each case measures and how to read `gas_equivalent`.
//
// **Every case pins `->Iterations(...)`.** Automatic sizing targets a wall-clock budget rather than
// a compile count, so it gives the cheap cases six-figure counts and `/4096` a handful, leaving the
// a compile count, so it gives the cheap cases six-figure counts and `/2048` a handful, leaving the
// sweep's rows incomparable.
// The smallest module the engine accepts. Everything a run does to it is overhead by construction.
@@ -29,6 +29,24 @@ minimalWat()
)wat";
}
// How many mul/add pairs each filler body holds.
//
// **Not a tuning knob — a floor set by the engine.** `EnforcedLimits::strict()` refuses any module
// averaging under 40 bytes per function body, once bodies total 1 KiB. That limit exists to defend
// lazy compilation against exactly the shape this function generates, so a body has to be fat
// enough to be a module the engine would actually accept. One pair averages 12 bytes and is
// refused; four averages 35 and is still refused; six is the first that passes. Eight is used for
// margin, and lands around 60 bytes per function.
constexpr size_t kFillerChain = 8;
// The top of the size sweeps.
//
// Bounded by bytes rather than by function count: at ~77 bytes per function this is ~158 KiB,
// inside `kMaxBytecodeSizeLimit` (200,000), while 4096 functions would be ~317 KiB and past it.
// `EnforcedLimits::strict()`'s own `max_functions` of 10,000 never binds — 40 bytes per function
// minimum against a 200,000-byte module caps any accepted contract at 5,000 functions.
constexpr size_t kFillerMaxFunctions = 2048;
// `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
@@ -41,10 +59,13 @@ 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 += std::format(" (func $f{} (param i32) (result i32)\n (local.get 0)\n", i);
for (auto k = 0uz; k < kFillerChain; ++k)
{
out += std::format(
" (i32.mul (i32.const {})) (i32.add (i32.const {}))\n", i + k + 1, i + k + 2);
}
out += " )\n";
}
out += " (func (export \"escrow_finish\") (result i32)\n (i32.const 1)))\n";
return out;
@@ -113,7 +134,7 @@ BENCHMARK(compileScaling)
->Arg(8)
->Arg(64)
->Arg(512)
->Arg(4096);
->Arg(kFillerMaxFunctions);
void
runScaling(benchmark::State& state)
@@ -128,7 +149,7 @@ BENCHMARK(runScaling)
->Arg(8)
->Arg(64)
->Arg(512)
->Arg(4096);
->Arg(kFillerMaxFunctions);
void
instantiateScaling(benchmark::State& state)

View File

@@ -391,7 +391,7 @@ report(
// 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;
(totalErr > kMaxRelativeSpread || (floor > 0.0 && implied < floor)) ? 1 : 0;
}
} // namespace xrpl::test::bench

View File

@@ -131,7 +131,8 @@ Change::preclaim(PreclaimContext const& ctx)
!ctx.tx.isFieldPresent(sfGasPrice))
return temMALFORMED;
if (ctx.tx[sfGasLimit] > kMaxGasLimit ||
ctx.tx[sfBytecodeSizeLimit] > kMaxBytecodeSizeLimit)
ctx.tx[sfBytecodeSizeLimit] > kMaxBytecodeSizeLimit ||
ctx.tx[sfGasPrice] < kMinGasPrice)
return temBAD_FEE;
}
else

View File

@@ -29,6 +29,7 @@
#include <xrpl/tx/apply.h>
#include <cstdint>
#include <functional>
#include <limits>
#include <memory>
#include <optional>
@@ -351,6 +352,20 @@ class FeeVote_test : public beast::unit_test::Suite
BEAST_EXPECT(setup.gasLimit == kMaxGasLimit);
BEAST_EXPECT(setup.bytecodeSizeLimit == kMaxBytecodeSizeLimit);
}
{
// Zero is below kMinGasPrice, so the default is kept.
Section config;
config.append("gas_price = 0");
auto const setup = setupFeeVote(config);
BEAST_EXPECT(setup.gasPrice == defaultSetup.gasPrice);
}
{
// The floor is inclusive: a configured price of 1 is accepted.
Section config;
config.append({"gas_price = " + std::to_string(kMinGasPrice)});
auto const setup = setupFeeVote(config);
BEAST_EXPECT(setup.gasPrice == kMinGasPrice);
}
}
void
@@ -447,7 +462,7 @@ class FeeVote_test : public beast::unit_test::Suite
BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields));
}
// Test that Smart Escrow limits reject values above their maximums.
// Test that Smart Escrow limits reject values outside their bounds.
{
jtx::Env env(*this, jtx::testableAmendments());
auto ledger = std::make_shared<Ledger>(
@@ -479,6 +494,43 @@ class FeeVote_test : public beast::unit_test::Suite
.gasLimit = kMaxGasLimit,
.bytecodeSizeLimit = kMaxBytecodeSizeLimit + 1,
.gasPrice = 300});
// gasPrice == 0 is temBAD_FEE; gasLimit == 0 remains a valid kill
// switch.
testBadFields(
{.baseFeeDrops = XRPAmount{10},
.reserveBaseDrops = XRPAmount{200000},
.reserveIncrementDrops = XRPAmount{50000},
.gasLimit = kMaxGasLimit,
.bytecodeSizeLimit = kMaxBytecodeSizeLimit,
.gasPrice = 0});
}
// ttFEE at exactly kMinGasPrice applies and is stored.
{
jtx::Env env(*this, jtx::testableAmendments());
auto ledger = std::make_shared<Ledger>(
kCreateGenesis,
Rules{env.app().config().features},
env.app().config().fees.toFees(),
std::vector<uint256>{},
env.app().getNodeFamily());
ledger = std::make_shared<Ledger>(*ledger, env.app().getTimeKeeper().closeTime());
FeeSettingsFields const fields{
.baseFeeDrops = XRPAmount{10},
.reserveBaseDrops = XRPAmount{200000},
.reserveIncrementDrops = XRPAmount{50000},
.gasLimit = 100,
.bytecodeSizeLimit = 200,
.gasPrice = kMinGasPrice};
auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields);
OpenView accum(ledger.get());
BEAST_EXPECT(isTesSuccess(applyFeeAndTestResult(env, accum, feeTx)));
accum.apply(*ledger);
BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields));
}
// Test that the Smart Escrow fields are rejected if the
@@ -846,6 +898,34 @@ class FeeVote_test : public beast::unit_test::Suite
BEAST_EXPECT(val->isFieldPresent(sfBaseFee));
BEAST_EXPECT(val->getFieldU64(sfBaseFee) == setup.referenceFee);
}
// A local target of 0 is not emitted on the validation; the field is
// omitted so peers treat it as noVote.
{
Env env(*this, testableAmendments());
FeeSetup zeroPrice = setup;
zeroPrice.gasPrice = 0;
auto feeVote = makeFeeVote(zeroPrice, env.app().getJournal("FeeVote"));
auto ledger = std::make_shared<Ledger>(
kCreateGenesis,
Rules{env.app().config().features},
env.app().config().fees.toFees(),
std::vector<uint256>{},
env.app().getNodeFamily());
auto sec = randomSecretKey();
auto pub = derivePublicKey(KeyType::Secp256k1, sec);
auto val = std::make_shared<STValidation>(
env.app().getTimeKeeper().now(), pub, sec, calcNodeID(pub), [](STValidation& v) {
v.setFieldU32(sfLedgerSequence, 12345);
});
feeVote->doValidation(ledger->fees(), ledger->rules(), *val);
BEAST_EXPECT(!val->isFieldPresent(sfGasPrice));
}
}
void
@@ -952,8 +1032,18 @@ class FeeVote_test : public beast::unit_test::Suite
BEAST_EXPECT(env.current()->fees().bytecodeSizeLimit == kDefaultBytecodeSizeLimit);
BEAST_EXPECT(env.current()->fees().gasPrice == kDefaultGasPrice);
struct SeVoteOpts
{
// If set, each validation uses the returned price; nullopt omits
// sfGasPrice. If unset, every validation uses setup.gasPrice.
std::function<std::optional<std::uint32_t>(int)> gasPrice;
int nValidations = 5;
bool trustAll = false;
};
auto const createFeeTxFromVoting =
[&](FeeSetup const& setup) -> std::pair<STTx, std::shared_ptr<Ledger>> {
[&](FeeSetup const& setup,
SeVoteOpts const& opts = {}) -> std::pair<STTx, std::shared_ptr<Ledger>> {
auto feeVote = makeFeeVote(setup, env.app().getJournal("FeeVote"));
auto ledger = std::make_shared<Ledger>(
kCreateGenesis,
@@ -974,7 +1064,7 @@ class FeeVote_test : public beast::unit_test::Suite
// Create some mock validations with fee votes
std::vector<std::shared_ptr<STValidation>> validations;
for (int i = 0; i < 5; i++)
for (int i = 0; i < opts.nValidations; i++)
{
auto sec = randomSecretKey();
auto pub = derivePublicKey(KeyType::Secp256k1, sec);
@@ -992,9 +1082,17 @@ class FeeVote_test : public beast::unit_test::Suite
v.setFieldAmount(sfReserveIncrementDrops, XRPAmount{setup.ownerReserve});
v.setFieldU32(sfGasLimit, setup.gasLimit);
v.setFieldU32(sfBytecodeSizeLimit, setup.bytecodeSizeLimit);
v.setFieldU32(sfGasPrice, setup.gasPrice);
if (opts.gasPrice)
{
if (auto const price = opts.gasPrice(i))
v.setFieldU32(sfGasPrice, *price);
}
else
{
v.setFieldU32(sfGasPrice, setup.gasPrice);
}
});
if (i % 2)
if (opts.trustAll || (i % 2))
val->setTrusted();
validations.push_back(val);
}
@@ -1083,6 +1181,87 @@ class FeeVote_test : public beast::unit_test::Suite
setup.bytecodeSizeLimit = ledger->fees().bytecodeSizeLimit;
checkFeeTx(setup, feeTx, ledger);
}
// Local and peer votes of 0 are ignored; the fee tx keeps the ledger
// gas price. Other fee fields still change, so a ttFEE is produced.
{
FeeSetup setup;
setup.referenceFee = 42;
setup.accountReserve = 1234567;
setup.ownerReserve = 7654321;
setup.gasLimit = 100;
setup.bytecodeSizeLimit = 200;
setup.gasPrice = 0;
auto const [feeTx, ledger] = createFeeTxFromVoting(setup);
setup.gasPrice = ledger->fees().gasPrice;
checkFeeTx(setup, feeTx, ledger);
}
// Absent sfGasPrice is an abstention (noVote), not a vote for 0.
{
FeeSetup setup;
setup.referenceFee = 42;
setup.accountReserve = 1234567;
setup.ownerReserve = 7654321;
setup.gasLimit = 100;
setup.bytecodeSizeLimit = 200;
setup.gasPrice = 300;
auto const [feeTx, ledger] = createFeeTxFromVoting(
setup,
{.gasPrice = [](int) -> std::optional<std::uint32_t> { return std::nullopt; }});
setup.gasPrice = ledger->fees().gasPrice;
checkFeeTx(setup, feeTx, ledger);
}
// Invalid (0) votes are noVote (weight on current), not dropped.
// Four zeros and one 300: current outweighs 300. If zeros were
// ignored, the local 300 target would still win.
{
FeeSetup setup;
setup.referenceFee = 42;
setup.accountReserve = 1234567;
setup.ownerReserve = 7654321;
setup.gasLimit = 100;
setup.bytecodeSizeLimit = 200;
setup.gasPrice = 300;
auto const [feeTx, ledger] = createFeeTxFromVoting(
setup,
{.gasPrice = [](int i) -> std::optional<std::uint32_t> { return i == 0 ? 300 : 0; },
.trustAll = true});
setup.gasPrice = ledger->fees().gasPrice;
checkFeeTx(setup, feeTx, ledger);
}
// doVote accepts the inclusive floor (field >= kMinGasPrice).
{
FeeSetup setup;
setup.referenceFee = 42;
setup.accountReserve = 1234567;
setup.ownerReserve = 7654321;
setup.gasLimit = 100;
setup.bytecodeSizeLimit = 200;
setup.gasPrice = kMinGasPrice;
auto const [feeTx, ledger] = createFeeTxFromVoting(setup);
checkFeeTx(setup, feeTx, ledger);
}
// There is no protocol max for gas price; UINT32_MAX is a legal vote.
{
FeeSetup setup;
setup.referenceFee = 42;
setup.accountReserve = 1234567;
setup.ownerReserve = 7654321;
setup.gasLimit = 100;
setup.bytecodeSizeLimit = 200;
setup.gasPrice = std::numeric_limits<std::uint32_t>::max();
auto const [feeTx, ledger] = createFeeTxFromVoting(setup);
checkFeeTx(setup, feeTx, ledger);
}
}
// Activation cannot be driven through consensus here, so the ledger is

View File

@@ -1,4 +1,5 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
@@ -17,6 +18,7 @@
#include <tx/wasm/fixtures/WasmRun.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
@@ -143,9 +145,9 @@ TEST_F(BytecodeRun, TheBytecodeReserveIsHeldWhileTheEscrowLivesAndReleasedWhenIt
auto const wasm = assembleWat(gatedOnLedgerSqn(threshold));
auto const created = createEscrow(wasm);
// `calculateAdditionalReserve`: one increment for the escrow, plus one per 500 bytes.
auto const expected = 1U + static_cast<std::uint32_t>(wasm.size() / 500);
EXPECT_EQ(env.getOwnerCount(alice), expected);
auto const held = env.getOwnerCount(alice);
ASSERT_GT(held, 0U);
EXPECT_EQ(held, static_cast<std::uint32_t>(calculateAdditionalReserve(std::optional{wasm})));
while (currentSeq() < threshold)
{
@@ -156,6 +158,24 @@ TEST_F(BytecodeRun, TheBytecodeReserveIsHeldWhileTheEscrowLivesAndReleasedWhenIt
EXPECT_EQ(env.getOwnerCount(alice), 0U);
}
TEST(BytecodeReserve, TheBytecodeReserveIsCeilingDivision)
{
auto const reserveFor = [](std::size_t size) {
return calculateAdditionalReserve(std::optional{Bytes(size, 0x00)});
};
EXPECT_EQ(calculateAdditionalReserve(std::optional<Bytes>{}), 1);
EXPECT_EQ(reserveFor(0), 1);
EXPECT_EQ(reserveFor(1), 1);
EXPECT_EQ(reserveFor(499), 1);
EXPECT_EQ(reserveFor(500), 1);
EXPECT_EQ(reserveFor(501), 2);
EXPECT_EQ(reserveFor(1000), 2);
EXPECT_EQ(reserveFor(1001), 3);
EXPECT_EQ(reserveFor(1500), 3);
EXPECT_EQ(reserveFor(200'000), 400); // kMaxBytecodeSizeLimit
}
TEST_F(BytecodeRun, CreatingChargesTheAmountAndTheFee)
{
auto const before = env.getXrpBalance(alice);

View File

@@ -184,7 +184,10 @@ FeeVoteImpl::doValidation(Fees const& lastFees, Rules const& rules, STValidation
"bytecode size limit",
sfBytecodeSizeLimit);
}
vote(lastFees.gasPrice, target_.gasPrice, "gas price", sfGasPrice);
if (target_.gasPrice >= kMinGasPrice)
{
vote(lastFees.gasPrice, target_.gasPrice, "gas price", sfGasPrice);
}
}
}
@@ -205,22 +208,30 @@ FeeVoteImpl::doVoting(
detail::VotableValue incReserveVote(lastClosedLedger->fees().increment, target_.ownerReserve);
auto validOrCurrent = [](std::uint32_t target, std::uint32_t max, std::uint32_t current) {
return target <= max ? target : current;
};
auto validOrCurrent =
[](std::uint32_t target, std::uint32_t min, std::uint32_t max, std::uint32_t current) {
return (target >= min && target <= max) ? target : current;
};
detail::VotableValue gasLimitVote(
lastClosedLedger->fees().gasLimit,
validOrCurrent(target_.gasLimit, kMaxGasLimit, lastClosedLedger->fees().gasLimit));
validOrCurrent(target_.gasLimit, 0, kMaxGasLimit, lastClosedLedger->fees().gasLimit));
detail::VotableValue bytecodeSizeLimitVote(
lastClosedLedger->fees().bytecodeSizeLimit,
validOrCurrent(
target_.bytecodeSizeLimit,
0,
kMaxBytecodeSizeLimit,
lastClosedLedger->fees().bytecodeSizeLimit));
detail::VotableValue gasPriceVote(lastClosedLedger->fees().gasPrice, target_.gasPrice);
detail::VotableValue gasPriceVote(
lastClosedLedger->fees().gasPrice,
validOrCurrent(
target_.gasPrice,
kMinGasPrice,
std::numeric_limits<std::uint32_t>::max(),
lastClosedLedger->fees().gasPrice));
auto const& rules = lastClosedLedger->rules();
if (rules.enabled(featureXRPFees))
@@ -297,10 +308,11 @@ FeeVoteImpl::doVoting(
auto doVote = [](std::shared_ptr<STValidation> const& val,
detail::VotableValue<std::uint32_t>& value,
SF_UINT32 const& sfield,
std::uint32_t minValue,
std::uint32_t maxValue) {
if (auto const field = ~val->at(~sfield); field)
{
if (field.value() <= maxValue)
if (field.value() >= minValue && field.value() <= maxValue)
{
value.addVote(field.value());
}
@@ -319,9 +331,14 @@ FeeVoteImpl::doVoting(
{
if (!val->isTrusted())
continue;
doVote(val, gasLimitVote, sfGasLimit, kMaxGasLimit);
doVote(val, bytecodeSizeLimitVote, sfBytecodeSizeLimit, kMaxBytecodeSizeLimit);
doVote(val, gasPriceVote, sfGasPrice, std::numeric_limits<std::uint32_t>::max());
doVote(val, gasLimitVote, sfGasLimit, 0, kMaxGasLimit);
doVote(val, bytecodeSizeLimitVote, sfBytecodeSizeLimit, 0, kMaxBytecodeSizeLimit);
doVote(
val,
gasPriceVote,
sfGasPrice,
kMinGasPrice,
std::numeric_limits<std::uint32_t>::max());
}
}

View File

@@ -1258,7 +1258,7 @@ setupFeeVote(Section const& section)
setup.gasLimit = temp;
if (set(temp, Keys::kBytecodeSizeLimit, section) && temp <= kMaxBytecodeSizeLimit)
setup.bytecodeSizeLimit = temp;
if (set(temp, Keys::kGasPrice, section))
if (set(temp, Keys::kGasPrice, section) && temp >= kMinGasPrice)
setup.gasPrice = temp;
}
return setup;