Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

122 changes: 121 additions & 1 deletion aptos-move/aptos-gas-meter/src/meter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use aptos_gas_algebra::{Fee, FeePerGasUnit, NumTypeNodes};
use aptos_gas_schedule::{
gas_feature_versions::*,
gas_params::{instr::*, txn::*},
value_graph_walk_cost,
};
use aptos_types::{
contract_event::ContractEvent, state_store::state_key::StateKey, write_set::WriteOpSize,
Expand All @@ -31,14 +32,21 @@ use move_vm_types::{
/// consisting all the gas parameters, which it can lookup when performing gas calculations.
pub struct StandardGasMeter<A> {
algebra: A,
/// When set, a cache-miss resource load bills for the deserialized graph.
/// Off by default so existing `new()` call sites keep historical costs
/// until the VM opts in via [`AptosGasMeter::enable_value_graph_load_billing`].
bill_value_graph_on_load: bool,
}

impl<A> StandardGasMeter<A>
where
A: GasAlgebra,
{
pub fn new(algebra: A) -> Self {
Self { algebra }
Self {
algebra,
bill_value_graph_on_load: false,
}
}

pub fn feature_version(&self) -> u64 {
Expand Down Expand Up @@ -216,6 +224,21 @@ where
if self.feature_version() <= 8 && val.is_none() && bytes_loaded != 0.into() {
return Err(PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message("in legacy versions, number of bytes loaded must be zero when the resource does not exist ".to_string()));
}
// Interpreter `charge_load_resource` runs on a cache miss: the value
// was just deserialized. IO gas below tracks stored bytes only; bill
// the graph walk so a compact blob cannot materialize a huge value
// for free. Cache hits never enter this callback.
if self.bill_value_graph_on_load {
if let Some(loaded) = &val {
let graph_size = self
.vm_gas_params()
.misc
.abs_val
.abstract_value_size(loaded, self.feature_version())?;
self.algebra
.charge_execution(value_graph_walk_cost(graph_size))?;
}
}
let cost = self
.io_pricing()
.calculate_read_gas(val.is_some(), bytes_loaded);
Expand Down Expand Up @@ -599,4 +622,101 @@ where
.charge_execution(KEYLESS_BASE_COST)
.map_err(|e| e.finish(Location::Undefined))
}

fn enable_value_graph_load_billing(&mut self, enabled: bool) {
self.bill_value_graph_on_load = enabled;
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{traits::GasAlgebra, AptosGasMeter, StandardGasAlgebra};
use aptos_gas_schedule::{
value_graph_walk_cost, InitialGasSchedule, VMGasParameters, LATEST_GAS_FEATURE_VERSION,
};
use aptos_vm_types::{
resolver::NoopBlockSynchronizationKillSwitch, storage::StorageGasParameters,
};
use move_core_types::{
account_address::AccountAddress,
gas_algebra::{InternalGas, NumBytes},
language_storage::TypeTag,
};
use move_vm_types::{gas::GasMeter, values::Value, views::TypeView};

struct DummyType;

impl TypeView for DummyType {
fn to_type_tag(&self) -> TypeTag {
TypeTag::Bool
}
}

fn execution_after_load(bill: bool, val: &Value) -> InternalGas {
let kill = NoopBlockSynchronizationKillSwitch {};
let mut meter = StandardGasMeter::new(StandardGasAlgebra::new(
LATEST_GAS_FEATURE_VERSION,
VMGasParameters::initial(),
StorageGasParameters::latest(),
false,
10_000_000u64,
&kill,
));
meter.enable_value_graph_load_billing(bill);
meter
.charge_load_resource(AccountAddress::ZERO, DummyType, Some(val), NumBytes::new(8))
.expect("load charge must succeed");
meter.algebra().execution_gas_used()
}

#[test]
fn resource_load_is_free_to_walk_the_graph_until_billing_is_enabled() {
let blob = Value::vector_u8((0u8..200).collect::<Vec<_>>());
let off = execution_after_load(false, &blob);
assert_eq!(
u64::from(off),
0,
"IO gas is not execution gas; the walk must stay unbilled when the flag is off"
);
}

#[test]
fn resource_load_bills_execution_gas_for_the_deserialized_graph() {
let blob = Value::vector_u8((0u8..200).collect::<Vec<_>>());
let on = execution_after_load(true, &blob);
let expected = {
let size = VMGasParameters::initial()
.misc
.abs_val
.abstract_value_size(&blob, LATEST_GAS_FEATURE_VERSION)
.expect("size");
value_graph_walk_cost(size)
};
assert_eq!(on, expected);
assert!(u64::from(on) > 1_101);
}

#[test]
fn missing_resource_does_not_pay_a_walk_charge() {
let kill = NoopBlockSynchronizationKillSwitch {};
let mut meter = StandardGasMeter::new(StandardGasAlgebra::new(
LATEST_GAS_FEATURE_VERSION,
VMGasParameters::initial(),
StorageGasParameters::latest(),
false,
10_000_000u64,
&kill,
));
meter.enable_value_graph_load_billing(true);
meter
.charge_load_resource(
AccountAddress::ZERO,
DummyType,
Option::<Value>::None,
NumBytes::new(0),
)
.expect("absent resource is billable for IO only");
assert_eq!(u64::from(meter.algebra().execution_gas_used()), 0);
}
}
9 changes: 9 additions & 0 deletions aptos-move/aptos-gas-meter/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,13 @@ pub trait AptosGasMeter: MoveGasMeter {
.inject_balance(extra_balance)
.map_err(|e| e.finish(Location::Undefined))
}

/// When enabled, resource loads also bill for walking the deserialized
/// value graph (CPU work that blob-length IO gas does not capture).
///
/// Default is a no-op so wrappers that do not track the flag stay
/// backward compatible. Production meters override this.
fn enable_value_graph_load_billing(&mut self, enabled: bool) {
let _ = enabled;
}
}
4 changes: 4 additions & 0 deletions aptos-move/aptos-gas-profiling/src/profiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,10 @@ where
) -> PartialVMResult<()>;
}

fn enable_value_graph_load_billing(&mut self, enabled: bool) {
self.base.enable_value_graph_load_billing(enabled);
}

fn charge_io_gas_for_transaction(&mut self, txn_size: NumBytes) -> VMResult<()> {
let (cost, res) = self.delegate_charge(|base| base.charge_io_gas_for_transaction(txn_size));

Expand Down
2 changes: 2 additions & 0 deletions aptos-move/aptos-gas-schedule/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@

mod gas_schedule;
mod traits;
mod value_graph;
mod ver;

pub use gas_schedule::*;
pub use traits::{FromOnChainGasSchedule, InitialGasSchedule, ToOnChainGasSchedule};
pub use value_graph::value_graph_walk_cost;
pub use ver::{gas_feature_versions, LATEST_GAS_FEATURE_VERSION};
59 changes: 59 additions & 0 deletions aptos-move/aptos-gas-schedule/src/value_graph.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

//! Pricing for walking an already-materialized Move value.
//!
//! BCS blobs and storage items can expand into a much larger in-memory node
//! graph than their byte length suggests. Charging only for those bytes leaves
//! a free-amplification hole: a cheap load or `from_bytes` can produce a huge
//! graph. The walk billed here is the same class of work as `cmp::compare`
//! (visit every node). We price it at three times that native so a traversal
//! cannot undercut a comparison of the same value.
//!
//! The multipliers are derived from the current `move_stdlib.cmp.compare`
//! schedule (`base = 367`, `per_abs_val_unit = 14`). They are not on-chain
//! parameters yet; a later gas-schedule version can promote them.

use aptos_gas_algebra::{AbstractValueSize, InternalGas, InternalGasPerAbstractValueUnit};

/// Three times `move_stdlib.cmp.compare.base`.
const WALK_BASE: InternalGas = InternalGas::new(1_101);

/// Three times `move_stdlib.cmp.compare.per_abs_val_unit`.
const WALK_PER_ABS_UNIT: InternalGasPerAbstractValueUnit = InternalGasPerAbstractValueUnit::new(42);

/// Execution gas for visiting `abstract_units` of a value graph.
///
/// Addition and multiplication saturate, so a huge graph over-charges rather
/// than wrapping to a cheap cost.
pub fn value_graph_walk_cost(abstract_units: AbstractValueSize) -> InternalGas {
WALK_BASE + WALK_PER_ABS_UNIT * abstract_units
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn empty_graph_still_pays_the_base() {
let cost = value_graph_walk_cost(AbstractValueSize::new(0));
assert_eq!(u64::from(cost), 1_101);
}

#[test]
fn cost_grows_linearly_with_abstract_size() {
let small = value_graph_walk_cost(AbstractValueSize::new(10));
let large = value_graph_walk_cost(AbstractValueSize::new(1_000));
assert!(u64::from(large) > u64::from(small) * 20);
assert_eq!(u64::from(small), 1_101 + 42 * 10);
assert_eq!(u64::from(large), 1_101 + 42 * 1_000);
}

#[test]
fn saturated_product_does_not_wrap_to_a_cheap_cost() {
// u64::MAX abstract units * 42 saturates; the billed amount must stay
// enormous rather than wrapping toward zero.
let cost = value_graph_walk_cost(AbstractValueSize::new(u64::MAX));
assert_eq!(u64::from(cost), u64::MAX);
}
}
4 changes: 4 additions & 0 deletions aptos-move/aptos-memory-usage-tracker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,4 +562,8 @@ where

fn charge_keyless(&mut self) -> VMResult<()>;
}

fn enable_value_graph_load_billing(&mut self, enabled: bool) {
self.base.enable_value_graph_load_billing(enabled);
}
}
45 changes: 42 additions & 3 deletions aptos-move/aptos-native-interface/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ use aptos_gas_algebra::{
AbstractValueSize, DynamicExpression, GasExpression, GasQuantity, InternalGasUnit,
};
use aptos_gas_schedule::{
gas_feature_versions::RELEASE_V1_32, AbstractValueSizeGasParameters, MiscGasParameters,
NativeGasParameters,
gas_feature_versions::RELEASE_V1_32, value_graph_walk_cost, AbstractValueSizeGasParameters,
MiscGasParameters, NativeGasParameters,
};
use aptos_types::on_chain_config::{Features, TimedFeatureFlag, TimedFeatures};
use move_binary_format::errors::{PartialVMResult, VMResult};
use move_core_types::{
gas_algebra::InternalGas, identifier::Identifier, language_storage::ModuleId,
account_address::AccountAddress, gas_algebra::InternalGas, identifier::Identifier,
language_storage::ModuleId,
};
use move_vm_runtime::{native_functions::NativeContext, Function};
use move_vm_types::values::Value;
Expand Down Expand Up @@ -128,6 +129,44 @@ impl SafeNativeContext<'_, '_, '_, '_> {
.abstract_value_size(val, self.gas_feature_version)
}

/// Heap size and abstract graph size of a resource already in the
/// session cache. `None` if the slot is empty (deleted / never stored).
///
/// Field borrows stay disjoint: layout parameters are immutable while
/// the session cache is read through `inner`.
pub fn session_cached_resource_graph_sizes(
&mut self,
address: AccountAddress,
ty: &move_vm_types::loaded_data::runtime_types::Type,
) -> PartialVMResult<Option<(u64, AbstractValueSize)>> {
let version = self.gas_feature_version;
let params = &self.misc_gas_params.abs_val;
let gv = self.inner.session_cached_resource(address, ty)?;
gv.view()
.map(|val| {
let heap = params.abstract_heap_size(&val, version)?;
let graph = params.abstract_value_size(&val, version)?;
Ok((u64::from(heap), graph))
})
.transpose()
}

/// Bills execution gas for walking a materialized value graph, but only
/// when [`TimedFeatureFlag::MeterValueNodesOnDeserialize`] is on.
///
/// Used after deserialize (and for serialize-side BCS walks). A caller
/// that already knows the flag is off should skip the size computation.
#[must_use = "must always propagate the error returned by this function to the native function that called it using the ? operator"]
pub fn bill_value_graph_walk(
&mut self,
abstract_units: AbstractValueSize,
) -> SafeNativeResult<()> {
if !self.timed_feature_enabled(TimedFeatureFlag::MeterValueNodesOnDeserialize) {
return Ok(());
}
self.charge(value_graph_walk_cost(abstract_units))
}

/// Computes the abstract size of the input value.
pub fn abs_val_size_dereferenced(&self, val: &Value) -> PartialVMResult<AbstractValueSize> {
self.misc_gas_params
Expand Down
12 changes: 12 additions & 0 deletions aptos-move/aptos-vm/src/aptos_vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1958,6 +1958,10 @@ impl AptosVM {
initial_balance,
code_storage,
);
gas_meter.enable_value_graph_load_billing(
self.timed_features()
.is_enabled(TimedFeatureFlag::MeterValueNodesOnDeserialize),
);

let (status, output) = self.execute_user_transaction_impl(
resolver,
Expand Down Expand Up @@ -2352,6 +2356,10 @@ impl AptosVM {
max_gas_amount.into(),
&NoopBlockSynchronizationKillSwitch {},
);
gas_meter.enable_value_graph_load_billing(
vm.timed_features()
.is_enabled(TimedFeatureFlag::MeterValueNodesOnDeserialize),
);

let resolver = state_view.as_move_resolver();
let module_storage = state_view.as_aptos_code_storage(&env);
Expand Down Expand Up @@ -2858,6 +2866,10 @@ impl VMValidator for AptosVM {
initial_balance,
&NoopBlockSynchronizationKillSwitch {},
);
gas_meter.enable_value_graph_load_billing(
self.timed_features()
.is_enabled(TimedFeatureFlag::MeterValueNodesOnDeserialize),
);
let storage = TraversalStorage::new();

// Increment the counter for transactions verified.
Expand Down
4 changes: 4 additions & 0 deletions aptos-move/aptos-vm/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ impl AptosVM {
gas_meter_balance: u64,
) -> (VMStatus, VMOutput) {
use crate::gas::make_prod_gas_meter;
use aptos_gas_meter::AptosGasMeter;
use move_vm_runtime::module_traversal::{TraversalContext, TraversalStorage};

let txn_data = TransactionMetadata::new(txn, self.timed_features());
Expand All @@ -101,6 +102,9 @@ impl AptosVM {
gas_meter_balance.into(),
&NoopBlockSynchronizationKillSwitch {},
);
gas_meter.enable_value_graph_load_billing(self.timed_features().is_enabled(
aptos_types::on_chain_config::TimedFeatureFlag::MeterValueNodesOnDeserialize,
));

let change_set_configs = &self
.storage_gas_params(&log_context)
Expand Down
1 change: 1 addition & 0 deletions aptos-move/e2e-move-tests/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,6 @@ mod token_objects;
mod transaction_context;
mod type_too_large;
mod upgrade_compatibility;
mod value_graph_gas;
mod vector_numeric_address;
mod vm;
Loading
Loading