mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-25 14:40:13 +00:00
Compare commits
4 Commits
ripple/se/
...
timothyban
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
436dc7824b | ||
|
|
f04386dcf7 | ||
|
|
58a3a4e5ba | ||
|
|
7e4338bb2e |
@@ -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.
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -81,7 +81,7 @@ PathAsset::holds() const
|
||||
}
|
||||
|
||||
template <ValidPathAsset T>
|
||||
[[nodiscard]] [[nodiscard]] T const&
|
||||
[[nodiscard]] T const&
|
||||
PathAsset::get() const
|
||||
{
|
||||
if (!holds<T>())
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user