diff --git a/Cargo.lock b/Cargo.lock
index c77d77bc5d9..64d3ba6413f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4099,6 +4099,7 @@ dependencies = [
name = "aptos-table-natives"
version = "0.1.0"
dependencies = [
+ "aptos-gas-algebra",
"aptos-gas-schedule",
"aptos-native-interface",
"aptos-types",
diff --git a/aptos-move/aptos-gas-meter/src/meter.rs b/aptos-move/aptos-gas-meter/src/meter.rs
index 379a2d39108..8f1a7a0ad9c 100644
--- a/aptos-move/aptos-gas-meter/src/meter.rs
+++ b/aptos-move/aptos-gas-meter/src/meter.rs
@@ -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,
@@ -31,6 +32,10 @@ use move_vm_types::{
/// consisting all the gas parameters, which it can lookup when performing gas calculations.
pub struct StandardGasMeter {
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 StandardGasMeter
@@ -38,7 +43,10 @@ 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 {
@@ -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);
@@ -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::>());
+ 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::>());
+ 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::::None,
+ NumBytes::new(0),
+ )
+ .expect("absent resource is billable for IO only");
+ assert_eq!(u64::from(meter.algebra().execution_gas_used()), 0);
+ }
}
diff --git a/aptos-move/aptos-gas-meter/src/traits.rs b/aptos-move/aptos-gas-meter/src/traits.rs
index a18e66127d5..e39df5108a3 100644
--- a/aptos-move/aptos-gas-meter/src/traits.rs
+++ b/aptos-move/aptos-gas-meter/src/traits.rs
@@ -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;
+ }
}
diff --git a/aptos-move/aptos-gas-profiling/src/profiler.rs b/aptos-move/aptos-gas-profiling/src/profiler.rs
index 87433532c79..a78b5ac492f 100644
--- a/aptos-move/aptos-gas-profiling/src/profiler.rs
+++ b/aptos-move/aptos-gas-profiling/src/profiler.rs
@@ -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));
diff --git a/aptos-move/aptos-gas-schedule/src/lib.rs b/aptos-move/aptos-gas-schedule/src/lib.rs
index 93156c25266..a6d7371bef4 100644
--- a/aptos-move/aptos-gas-schedule/src/lib.rs
+++ b/aptos-move/aptos-gas-schedule/src/lib.rs
@@ -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};
diff --git a/aptos-move/aptos-gas-schedule/src/value_graph.rs b/aptos-move/aptos-gas-schedule/src/value_graph.rs
new file mode 100644
index 00000000000..7360f358627
--- /dev/null
+++ b/aptos-move/aptos-gas-schedule/src/value_graph.rs
@@ -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);
+ }
+}
diff --git a/aptos-move/aptos-memory-usage-tracker/src/lib.rs b/aptos-move/aptos-memory-usage-tracker/src/lib.rs
index d8e4adb74b4..bef02ccd585 100644
--- a/aptos-move/aptos-memory-usage-tracker/src/lib.rs
+++ b/aptos-move/aptos-memory-usage-tracker/src/lib.rs
@@ -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);
+ }
}
diff --git a/aptos-move/aptos-native-interface/src/context.rs b/aptos-move/aptos-native-interface/src/context.rs
index 606799a9064..b28747d10aa 100644
--- a/aptos-move/aptos-native-interface/src/context.rs
+++ b/aptos-move/aptos-native-interface/src/context.rs
@@ -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;
@@ -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