diff --git a/chain/chain/src/tests/simple_chain.rs b/chain/chain/src/tests/simple_chain.rs index a272a9ae9be..f43efcb79e7 100644 --- a/chain/chain/src/tests/simple_chain.rs +++ b/chain/chain/src/tests/simple_chain.rs @@ -25,7 +25,7 @@ fn empty_chain() { let hash = chain.head().unwrap().last_block_hash; // The hashes here will have to be modified after each change to genesis file. #[cfg(feature = "nightly_protocol")] - assert_eq!(hash, CryptoHash::from_str("4iPPuWZ2BZj6i6zGCa96xFTQhp3FHkY2CzUCJFUUryt8").unwrap()); + assert_eq!(hash, CryptoHash::from_str("2VFkBfWwcTqyVJ83zy78n5WUNadwGuJbLc2KEp9SJ8dV").unwrap()); #[cfg(not(feature = "nightly_protocol"))] assert_eq!(hash, CryptoHash::from_str("8UF2TCELQ2sSqorskN5myyC7h1XfgxYm68JHJMKo5n8X").unwrap()); assert_eq!(count_utc, 1); @@ -54,7 +54,7 @@ fn build_chain() { #[cfg(feature = "nightly_protocol")] assert_eq!( prev_hash, - CryptoHash::from_str("zcVm8wC8eBt2b5C2uTNch2UyfXCwjs3qgYGZwyXcUAA").unwrap() + CryptoHash::from_str("299HrY4hpubeFXa3V9DNtR36dGEtiz4AVfMbfL6hT2sq").unwrap() ); #[cfg(not(feature = "nightly_protocol"))] assert_eq!( @@ -77,7 +77,7 @@ fn build_chain() { #[cfg(feature = "nightly_protocol")] assert_eq!( chain.head().unwrap().last_block_hash, - CryptoHash::from_str("CDfAT886U5up6bQZ3QNVcvxtuVM6sNyJnF6Nk6RMHnEZ").unwrap() + CryptoHash::from_str("A1ZqLuyanSg6YeD3HxGco2tJYEAsmHvAva5n4dsPTgij").unwrap() ); #[cfg(not(feature = "nightly_protocol"))] assert_eq!( diff --git a/core/primitives-core/Cargo.toml b/core/primitives-core/Cargo.toml index 7156f6f27f3..20f6ea5b63d 100644 --- a/core/primitives-core/Cargo.toml +++ b/core/primitives-core/Cargo.toml @@ -31,6 +31,7 @@ serde_json = "1" default = [] protocol_feature_alt_bn128 = [] protocol_feature_routing_exchange_algorithm = [] +protocol_feature_function_call_weight = [] deepsize_feature = [ "deepsize", "near-account-id/deepsize_feature", diff --git a/core/primitives-core/src/types.rs b/core/primitives-core/src/types.rs index a44a530d3f7..4332cd71a1d 100644 --- a/core/primitives-core/src/types.rs +++ b/core/primitives-core/src/types.rs @@ -25,6 +25,24 @@ pub type Balance = u128; /// Gas is a type for storing amount of gas. pub type Gas = u64; +/// Weight of unused gas to distribute to scheduled function call actions. +/// Used in `promise_batch_action_function_call_weight` host function. +#[cfg(feature = "protocol_feature_function_call_weight")] +#[derive(Clone, Debug, PartialEq)] +pub struct GasWeight(pub u64); + +/// Result from a gas distribution among function calls with ratios. +#[cfg(feature = "protocol_feature_function_call_weight")] +#[must_use] +#[non_exhaustive] +#[derive(Debug, PartialEq)] +pub enum GasDistribution { + /// All remaining gas was distributed to functions. + All, + /// There were no function call actions with a ratio specified. + NoRatios, +} + /// Number of blocks in current group. pub type NumBlocks = u64; /// Number of shards in current group. diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index 641f7fb52d8..d911930dd84 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -46,6 +46,7 @@ protocol_feature_chunk_only_producers = [] protocol_feature_routing_exchange_algorithm = ["near-primitives-core/protocol_feature_routing_exchange_algorithm"] protocol_feature_access_key_nonce_for_implicit_accounts = [] protocol_feature_fix_staking_threshold = [] +protocol_feature_function_call_weight = ["near-primitives-core/protocol_feature_function_call_weight"] nightly_protocol_features = [ "nightly_protocol", "protocol_feature_alt_bn128", @@ -53,6 +54,7 @@ nightly_protocol_features = [ "protocol_feature_routing_exchange_algorithm", "protocol_feature_access_key_nonce_for_implicit_accounts", "protocol_feature_fix_staking_threshold", + "protocol_feature_function_call_weight", ] nightly_protocol = [] deepsize_feature = [ diff --git a/core/primitives/src/version.rs b/core/primitives/src/version.rs index bbb73bbfb52..b8eb4d71bba 100644 --- a/core/primitives/src/version.rs +++ b/core/primitives/src/version.rs @@ -150,6 +150,8 @@ pub enum ProtocolFeature { /// alpha is min stake ratio #[cfg(feature = "protocol_feature_fix_staking_threshold")] FixStakingThreshold, + #[cfg(feature = "protocol_feature_function_call_weight")] + FunctionCallWeight, } /// Both, outgoing and incoming tcp connections to peers, will be rejected if `peer's` @@ -166,7 +168,7 @@ const STABLE_PROTOCOL_VERSION: ProtocolVersion = 52; pub const PROTOCOL_VERSION: ProtocolVersion = STABLE_PROTOCOL_VERSION; /// Current latest nightly version of the protocol. #[cfg(feature = "nightly_protocol")] -pub const PROTOCOL_VERSION: ProtocolVersion = 126; +pub const PROTOCOL_VERSION: ProtocolVersion = 127; /// The points in time after which the voting for the protocol version should start. #[allow(dead_code)] @@ -226,6 +228,8 @@ impl ProtocolFeature { ProtocolFeature::RoutingExchangeAlgorithm => 117, #[cfg(feature = "protocol_feature_fix_staking_threshold")] ProtocolFeature::FixStakingThreshold => 126, + #[cfg(feature = "protocol_feature_function_call_weight")] + ProtocolFeature::FunctionCallWeight => 127, } } } diff --git a/runtime/near-vm-logic/Cargo.toml b/runtime/near-vm-logic/Cargo.toml index 5b08750dd55..aa2e42f0ccf 100644 --- a/runtime/near-vm-logic/Cargo.toml +++ b/runtime/near-vm-logic/Cargo.toml @@ -44,6 +44,10 @@ protocol_feature_alt_bn128 = [ "near-primitives-core/protocol_feature_alt_bn128", "near-vm-errors/protocol_feature_alt_bn128", ] +protocol_feature_function_call_weight = [ + "near-primitives/protocol_feature_function_call_weight", + "near-primitives-core/protocol_feature_function_call_weight", +] # Use this feature to enable counting of fees and costs applied. costs_counting = [] diff --git a/runtime/near-vm-logic/src/dependencies.rs b/runtime/near-vm-logic/src/dependencies.rs index a8132a9c27c..3b1311828b6 100644 --- a/runtime/near-vm-logic/src/dependencies.rs +++ b/runtime/near-vm-logic/src/dependencies.rs @@ -2,6 +2,8 @@ use crate::types::{PublicKey, ReceiptIndex}; use near_primitives_core::types::{AccountId, Balance, Gas}; +#[cfg(feature = "protocol_feature_function_call_weight")] +use near_primitives_core::types::{GasDistribution, GasWeight}; use near_vm_errors::VMLogicError; /// An abstraction over the memory of the smart contract. @@ -276,6 +278,57 @@ pub trait External { prepaid_gas: Gas, ) -> Result<()>; + /// Attach the [`FunctionCallAction`] action to an existing receipt. This method has similar + /// functionality to [`append_action_function_call`](Self::append_action_function_call) except + /// that it allows specifying a weight to use leftover gas from the current execution. + /// + /// `prepaid_gas` and `gas_weight` can either be specified or both. If a `gas_weight` is + /// specified, the action should be allocated gas in + /// [`distribute_unused_gas`](Self::distribute_unused_gas). + /// + /// For more information, see [crate::VMLogic::promise_batch_action_function_call_weight]. + /// + /// # Arguments + /// + /// * `receipt_index` - an index of Receipt to append an action + /// * `method_name` - a name of the contract method to call + /// * `arguments` - a Wasm code to attach + /// * `attached_deposit` - amount of tokens to transfer with the call + /// * `prepaid_gas` - amount of prepaid gas to attach to the call + /// * `gas_weight` - relative weight of unused gas to distribute to the function call action + /// + /// # Example + /// + /// ``` + /// # use near_vm_logic::mocks::mock_external::MockedExternal; + /// # use near_vm_logic::External; + /// + /// # let mut external = MockedExternal::new(); + /// let receipt_index = external.create_receipt(vec![], "charli.near".parse().unwrap()).unwrap(); + /// external.append_action_function_call_weight( + /// receipt_index, + /// b"method_name".to_vec(), + /// b"{serialised: arguments}".to_vec(), + /// 100000u128, + /// 100u64, + /// 2, + /// ).unwrap(); + /// ``` + /// + /// # Panics + /// + /// Panics if the `receipt_index` does not refer to a known receipt. + #[cfg(feature = "protocol_feature_function_call_weight")] + fn append_action_function_call_weight( + &mut self, + receipt_index: ReceiptIndex, + method_name: Vec, + arguments: Vec, + attached_deposit: Balance, + prepaid_gas: Gas, + gas_weight: GasWeight, + ) -> Result<()>; + /// Attach the [`TransferAction`] action to an existing receipt. /// /// # Arguments @@ -486,4 +539,16 @@ pub trait External { /// Returns total stake of validators in the current epoch. fn validator_total_stake(&self) -> Result; + + /// Distribute the gas among the scheduled function calls that specify a gas weight. + /// + /// # Arguments + /// + /// * `gas` - amount of unused gas to distribute + /// + /// # Returns + /// + /// Function returns a [GasDistribution] that indicates how the gas was distributed. + #[cfg(feature = "protocol_feature_function_call_weight")] + fn distribute_unused_gas(&mut self, gas: Gas) -> GasDistribution; } diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index 331ac536ca3..bf12124e598 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -16,6 +16,8 @@ use near_primitives_core::runtime::fees::{ use near_primitives_core::types::{ AccountId, Balance, EpochHeight, Gas, ProtocolVersion, StorageUsage, }; +#[cfg(feature = "protocol_feature_function_call_weight")] +use near_primitives_core::types::{GasDistribution, GasWeight}; use near_vm_errors::InconsistentStateError; use near_vm_errors::{HostError, VMLogicError}; use std::collections::HashMap; @@ -1518,6 +1520,102 @@ impl<'a> VMLogic<'a> { arguments_ptr: u64, amount_ptr: u64, gas: Gas, + ) -> Result<()> { + let append_action_fn = |vm: &mut Self, receipt_idx, method_name, arguments, amount, gas| { + vm.ext.append_action_function_call(receipt_idx, method_name, arguments, amount, gas) + }; + self.internal_promise_batch_action_function_call( + promise_idx, + method_name_len, + method_name_ptr, + arguments_len, + arguments_ptr, + amount_ptr, + gas, + append_action_fn, + ) + } + + /// Appends `FunctionCall` action to the batch of actions for the given promise pointed by + /// `promise_idx`. This function allows not specifying a specific gas value and allowing the + /// runtime to assign remaining gas based on a weight. + /// + /// # Gas + /// + /// Gas can be specified using a static amount, a weight of remaining prepaid gas, or a mixture + /// of both. To omit a static gas amount, `0` can be passed for the `gas` parameter. + /// To omit assigning remaining gas, `0` can be passed as the `gas_weight` parameter. + /// + /// The gas weight parameter works as the following: + /// + /// All unused prepaid gas from the current function call is split among all function calls + /// which supply this gas weight. The amount attached to each respective call depends on the + /// value of the weight. + /// + /// For example, if 40 gas is leftover from the current method call and three functions specify + /// the weights 1, 5, 2 then 5, 25, 10 gas will be added to each function call respectively, + /// using up all remaining available gas. + /// + /// If the `gas_weight` parameter is set as a large value, the amount of distributed gas + /// to each action can be 0 or a very low value because the amount of gas per weight is + /// based on the floor division of the amount of gas by the sum of weights. + /// + /// Any remaining gas will be distributed to the last scheduled function call with a weight + /// specified. + /// + /// # Errors + /// + /// * If `promise_idx` does not correspond to an existing promise returns `InvalidPromiseIndex`. + /// * If the promise pointed by the `promise_idx` is an ephemeral promise created by + /// `promise_and` returns `CannotAppendActionToJointPromise`. + /// * If `method_name_len + method_name_ptr` or `arguments_len + arguments_ptr` or + /// `amount_ptr + 16` points outside the memory of the guest or host returns + /// `MemoryAccessViolation`. + /// * If called as view function returns `ProhibitedInView`. + #[cfg(feature = "protocol_feature_function_call_weight")] + pub fn promise_batch_action_function_call_weight( + &mut self, + promise_idx: u64, + method_name_len: u64, + method_name_ptr: u64, + arguments_len: u64, + arguments_ptr: u64, + amount_ptr: u64, + gas: Gas, + gas_weight: GasWeight, + ) -> Result<()> { + let append_action_fn = |vm: &mut Self, receipt_idx, method_name, arguments, amount, gas| { + vm.ext.append_action_function_call_weight( + receipt_idx, + method_name, + arguments, + amount, + gas, + gas_weight, + ) + }; + self.internal_promise_batch_action_function_call( + promise_idx, + method_name_len, + method_name_ptr, + arguments_len, + arguments_ptr, + amount_ptr, + gas, + append_action_fn, + ) + } + + fn internal_promise_batch_action_function_call( + &mut self, + promise_idx: u64, + method_name_len: u64, + method_name_ptr: u64, + arguments_len: u64, + arguments_ptr: u64, + amount_ptr: u64, + gas: Gas, + append_action_fn: impl FnOnce(&mut Self, u64, Vec, Vec, u128, u64) -> Result<()>, ) -> Result<()> { self.gas_counter.pay_base(base)?; if self.context.is_view() { @@ -1553,8 +1651,7 @@ impl<'a> VMLogic<'a> { self.deduct_balance(amount)?; - self.ext.append_action_function_call(receipt_idx, method_name, arguments, amount, gas)?; - Ok(()) + append_action_fn(self, receipt_idx, method_name, arguments, amount, gas) } /// Appends `Transfer` action to the batch of actions for the given promise pointed by @@ -2509,8 +2606,24 @@ impl<'a> VMLogic<'a> { })) } - /// Computes the outcome of execution. - pub fn outcome(self) -> VMOutcome { + /// Computes the outcome of the execution. + /// + /// If `FunctionCallWeight` protocol feature (127) is enabled, unused gas will be + /// distributed to functions that specify a gas weight. If there are no functions with + /// a gas weight, the outcome will contain unused gas as usual. + #[cfg_attr(not(feature = "protocol_feature_function_call_weight"), allow(unused_mut))] + pub fn compute_outcome_and_distribute_gas(mut self) -> VMOutcome { + #[cfg(feature = "protocol_feature_function_call_weight")] + if !self.context.is_view() { + // Distribute unused gas to scheduled function calls + let unused_gas = self.context.prepaid_gas - self.gas_counter.used_gas(); + + // Distribute the unused gas and prepay for the gas. + if matches!(self.ext.distribute_unused_gas(unused_gas), GasDistribution::All) { + self.gas_counter.prepay_gas(unused_gas).unwrap(); + } + } + let burnt_gas = self.gas_counter.burnt_gas(); let used_gas = self.gas_counter.used_gas(); diff --git a/runtime/near-vm-logic/src/mocks/mock_external.rs b/runtime/near-vm-logic/src/mocks/mock_external.rs index b627f71dadb..c348af4829f 100644 --- a/runtime/near-vm-logic/src/mocks/mock_external.rs +++ b/runtime/near-vm-logic/src/mocks/mock_external.rs @@ -1,4 +1,6 @@ use crate::{External, ValuePtr}; +#[cfg(feature = "protocol_feature_function_call_weight")] +use near_primitives::types::{GasDistribution, GasWeight}; use near_primitives_core::types::{AccountId, Balance, Gas}; use near_vm_errors::HostError; use serde::{Deserialize, Serialize}; @@ -10,6 +12,15 @@ pub struct MockedExternal { pub fake_trie: HashMap, Vec>, receipts: Vec, pub validators: HashMap, + #[cfg(feature = "protocol_feature_function_call_weight")] + gas_weights: Vec<(FunctionCallActionIndex, GasWeight)>, +} + +#[derive(Clone)] +#[cfg(feature = "protocol_feature_function_call_weight")] +struct FunctionCallActionIndex { + receipt_index: usize, + action_index: usize, } pub struct MockedValuePtr { @@ -118,6 +129,34 @@ impl External for MockedExternal { Ok(()) } + #[cfg(feature = "protocol_feature_function_call_weight")] + fn append_action_function_call_weight( + &mut self, + receipt_index: u64, + method_name: Vec, + arguments: Vec, + attached_deposit: u128, + prepaid_gas: Gas, + gas_weight: GasWeight, + ) -> Result<()> { + let receipt_index = receipt_index as usize; + let receipt = self.receipts.get_mut(receipt_index).unwrap(); + if gas_weight.0 > 0 { + self.gas_weights.push(( + FunctionCallActionIndex { receipt_index, action_index: receipt.actions.len() }, + gas_weight, + )); + } + + receipt.actions.push(Action::FunctionCall(FunctionCallAction { + method_name, + args: arguments, + deposit: attached_deposit, + gas: prepaid_gas, + })); + Ok(()) + } + fn append_action_transfer(&mut self, receipt_index: u64, amount: u128) -> Result<()> { self.receipts .get_mut(receipt_index as usize) @@ -209,6 +248,57 @@ impl External for MockedExternal { fn validator_total_stake(&self) -> Result { Ok(self.validators.values().sum()) } + + /// Distributes the gas passed in by splitting it among weights defined in `gas_weights`. + /// This will sum all weights, retrieve the gas per weight, then update each function + /// to add the respective amount of gas. Once all gas is distributed, the remainder of + /// the gas not assigned due to precision loss is added to the last function with a weight. + #[cfg(feature = "protocol_feature_function_call_weight")] + fn distribute_unused_gas(&mut self, gas: Gas) -> GasDistribution { + let gas_weight_sum: u128 = + self.gas_weights.iter().map(|(_, GasWeight(weight))| *weight as u128).sum(); + if gas_weight_sum != 0 { + // Floor division that will ensure gas allocated is <= gas to distribute + let gas_per_weight = (gas as u128 / gas_weight_sum) as u64; + + let mut distribute_gas = |metadata: &FunctionCallActionIndex, assigned_gas: u64| { + let FunctionCallActionIndex { receipt_index, action_index } = metadata; + if let Some(Action::FunctionCall(FunctionCallAction { ref mut gas, .. })) = self + .receipts + .get_mut(*receipt_index) + .and_then(|receipt| receipt.actions.get_mut(*action_index)) + { + *gas += assigned_gas; + } else { + panic!( + "Invalid index for assigning unused gas weight \ + (promise_index={}, action_index={})", + receipt_index, action_index + ); + } + }; + + let mut distributed = 0; + for (action_index, GasWeight(weight)) in &self.gas_weights { + // This can't overflow because the gas_per_weight is floor division + // of the weight sum. + let assigned_gas = gas_per_weight * weight; + + distribute_gas(action_index, assigned_gas); + + distributed += assigned_gas + } + + // Distribute remaining gas to final action. + if let Some((last_idx, _)) = self.gas_weights.last() { + distribute_gas(last_idx, gas - distributed); + } + self.gas_weights.clear(); + GasDistribution::All + } else { + GasDistribution::NoRatios + } + } } #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/runtime/near-vm-logic/src/tests/gas_counter.rs b/runtime/near-vm-logic/src/tests/gas_counter.rs index 322bb7129fe..3e2c9137373 100644 --- a/runtime/near-vm-logic/src/tests/gas_counter.rs +++ b/runtime/near-vm-logic/src/tests/gas_counter.rs @@ -14,7 +14,7 @@ fn test_dont_burn_gas_when_exceeding_attached_gas_limit() { let index = promise_create(&mut logic, b"rick.test", 0, 0).expect("should create a promise"); promise_batch_action_function_call(&mut logic, index, 0, gas_limit * 2) .expect_err("should fail with gas limit"); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); // Just avoid hard-coding super-precise amount of gas burnt. assert!(outcome.burnt_gas < gas_limit / 2); @@ -33,7 +33,7 @@ fn test_limit_wasm_gas_after_attaching_gas() { promise_batch_action_function_call(&mut logic, index, 0, gas_limit / 2) .expect("should add action to receipt"); logic.gas((op_limit / 2) as u32).expect_err("should fail with gas limit"); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.used_gas, gas_limit); assert!(gas_limit / 2 < outcome.burnt_gas); @@ -49,7 +49,7 @@ fn test_cant_burn_more_than_max_gas_burnt_gas() { let mut logic = logic_builder.build_with_prepaid_gas(gas_limit * 2); logic.gas(op_limit * 3).expect_err("should fail with gas limit"); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.burnt_gas, gas_limit); assert_eq!(outcome.used_gas, gas_limit * 2); @@ -64,7 +64,7 @@ fn test_cant_burn_more_than_prepaid_gas() { let mut logic = logic_builder.build_with_prepaid_gas(gas_limit); logic.gas(op_limit * 3).expect_err("should fail with gas limit"); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.burnt_gas, gas_limit); assert_eq!(outcome.used_gas, gas_limit); @@ -80,7 +80,7 @@ fn test_hit_max_gas_burnt_limit() { promise_create(&mut logic, b"rick.test", 0, gas_limit / 2).expect("should create a promise"); logic.gas(op_limit * 2).expect_err("should fail with gas limit"); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.burnt_gas, gas_limit); assert!(outcome.used_gas > gas_limit * 2); @@ -96,12 +96,95 @@ fn test_hit_prepaid_gas_limit() { promise_create(&mut logic, b"rick.test", 0, gas_limit / 2).expect("should create a promise"); logic.gas(op_limit * 2).expect_err("should fail with gas limit"); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.burnt_gas, gas_limit); assert_eq!(outcome.used_gas, gas_limit); } +#[cfg(feature = "protocol_feature_function_call_weight")] +fn function_call_weight_check(function_calls: impl IntoIterator) { + use near_primitives::types::GasWeight; + + let gas_limit = 10u64.pow(14); + + let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit); + let mut logic = logic_builder.build_with_prepaid_gas(gas_limit); + + for (static_gas, gas_weight) in function_calls { + let index = promise_batch_create(&mut logic, "rick.test").expect("should create a promise"); + promise_batch_action_function_call_weight( + &mut logic, + index, + 0, + static_gas, + GasWeight(gas_weight), + ) + .expect("batch action function call should succeed"); + } + let outcome = logic.compute_outcome_and_distribute_gas(); + + // Verify that all gas was consumed (assumes at least one ratio is provided) + assert_eq!(outcome.used_gas, gas_limit); +} + +#[cfg(feature = "protocol_feature_function_call_weight")] +#[test] +fn function_call_weight_single_smoke_test() { + // Single function call + function_call_weight_check([(0, 1)]); + + // Single function with static gas + function_call_weight_check([(888, 1)]); + + // Large weight + function_call_weight_check([(0, 88888)]); + + // Weight larger than gas limit + function_call_weight_check([(0, 11u64.pow(14))]); + + // Split two + function_call_weight_check([(0, 3), (0, 2)]); + + // Split two with static gas + function_call_weight_check([(1_000_000, 3), (3_000_000, 2)]); + + // Many different gas weights + function_call_weight_check([ + (1_000_000, 3), + (3_000_000, 2), + (0, 1), + (1_000_000_000, 0), + (0, 4), + ]); + + // Weight over u64 bounds + function_call_weight_check([(0, u64::MAX), (0, 1000)]); + + // Weights with one zero and one non-zero + function_call_weight_check([(0, 0), (0, 1)]) +} + +#[cfg(feature = "protocol_feature_function_call_weight")] +#[test] +fn function_call_no_weight_refund() { + use near_primitives::types::GasWeight; + + let gas_limit = 10u64.pow(14); + + let mut logic_builder = VMLogicBuilder::default().max_gas_burnt(gas_limit); + let mut logic = logic_builder.build_with_prepaid_gas(gas_limit); + + let index = promise_batch_create(&mut logic, "rick.test").expect("should create a promise"); + promise_batch_action_function_call_weight(&mut logic, index, 0, 1000, GasWeight(0)) + .expect("batch action function call should succeed"); + + let outcome = logic.compute_outcome_and_distribute_gas(); + + // Verify that unused gas was not allocated to function call + assert!(outcome.used_gas < gas_limit); +} + impl VMLogicBuilder { fn max_gas_burnt(mut self, max_gas_burnt: Gas) -> Self { self.config.limit_config.max_gas_burnt = max_gas_burnt; diff --git a/runtime/near-vm-logic/src/tests/helpers.rs b/runtime/near-vm-logic/src/tests/helpers.rs index 1cdd521ec5e..9240d4f1199 100644 --- a/runtime/near-vm-logic/src/tests/helpers.rs +++ b/runtime/near-vm-logic/src/tests/helpers.rs @@ -1,4 +1,6 @@ use crate::{with_ext_cost_counter, VMLogic}; +#[cfg(feature = "protocol_feature_function_call_weight")] +use near_primitives_core::types::GasWeight; use near_primitives_core::{config::ExtCosts, types::Gas}; use near_vm_errors::VMLogicError; use std::collections::HashMap; @@ -25,6 +27,12 @@ pub fn promise_create( ) } +#[allow(dead_code)] +pub fn promise_batch_create(logic: &mut VMLogic, account_id: &str) -> Result { + logic.promise_batch_create(account_id.len() as _, account_id.as_ptr() as _) +} + +#[allow(dead_code)] pub fn promise_batch_action_function_call( logic: &mut VMLogic<'_>, promise_index: u64, @@ -45,6 +53,31 @@ pub fn promise_batch_action_function_call( ) } +#[cfg(feature = "protocol_feature_function_call_weight")] +#[allow(dead_code)] +pub fn promise_batch_action_function_call_weight( + logic: &mut VMLogic<'_>, + promise_index: u64, + amount: u128, + gas: Gas, + ratio: GasWeight, +) -> Result<()> { + let method_id = b"promise_batch_action"; + let args = b"promise_batch_action_args"; + + logic.promise_batch_action_function_call_weight( + promise_index, + method_id.len() as _, + method_id.as_ptr() as _, + args.len() as _, + args.as_ptr() as _, + amount.to_le_bytes().as_ptr() as _, + gas, + ratio, + ) +} + +#[allow(dead_code)] pub fn promise_batch_action_add_key_with_function_call( logic: &mut VMLogic<'_>, promise_index: u64, diff --git a/runtime/near-vm-logic/src/tests/miscs.rs b/runtime/near-vm-logic/src/tests/miscs.rs index d833a67aa35..547496cf3f5 100644 --- a/runtime/near-vm-logic/src/tests/miscs.rs +++ b/runtime/near-vm-logic/src/tests/miscs.rs @@ -15,7 +15,7 @@ fn test_valid_utf8() { let string_bytes = "j ñ r'ø qò$`5 y'5 øò{%÷ `Võ%".as_bytes().to_vec(); let len = string_bytes.len() as u64; logic.log_utf8(len, string_bytes.as_ptr() as _).expect("Valid utf-8 string_bytes"); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.logs[0], String::from_utf8(string_bytes).unwrap()); assert_costs(map! { ExtCosts::base: 1, @@ -35,7 +35,7 @@ fn test_invalid_utf8() { let string_bytes = [128].to_vec(); let len = string_bytes.len() as u64; assert_eq!(logic.log_utf8(len, string_bytes.as_ptr() as _), Err(HostError::BadUTF8.into())); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.logs.len(), 0); assert_costs(map! { ExtCosts::base: 1, @@ -59,7 +59,7 @@ fn test_valid_null_terminated_utf8() { .log_utf8(u64::MAX, string_bytes.as_ptr() as _) .expect("Valid null-terminated utf-8 string_bytes"); string_bytes.pop(); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); let len = bytes_len as u64; assert_costs(map! { ExtCosts::base: 1, @@ -91,7 +91,7 @@ fn test_log_max_limit() { ExtCosts::utf8_decoding_base: 1, }); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.logs.len(), 0); } @@ -115,7 +115,7 @@ fn test_log_total_length_limit() { Err(HostError::TotalLogLengthExceeded { length: limit + 1, limit }.into()) ); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.logs.len() as u64, num_logs - 1); } @@ -149,7 +149,7 @@ fn test_log_number_limit() { ExtCosts::utf8_decoding_byte: len * max_number_logs, }); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.logs.len() as u64, max_number_logs); } @@ -189,7 +189,7 @@ fn test_log_utf16_number_limit() { ExtCosts::utf16_decoding_byte: len * max_number_logs, }); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.logs.len() as u64, max_number_logs); } @@ -230,7 +230,7 @@ fn test_log_total_length_limit_mixed() { Err(HostError::TotalLogLengthExceeded { length: limit + 1, limit }.into()) ); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.logs.len() as u64, num_logs_each * 2); } @@ -256,7 +256,7 @@ fn test_log_utf8_max_limit_null_terminated() { ExtCosts::utf8_decoding_base: 1, }); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.logs.len(), 0); } @@ -284,7 +284,7 @@ fn test_valid_log_utf16() { ExtCosts::log_base: 1, ExtCosts::log_byte: string.len() as u64, }); - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.logs[0], string); } @@ -369,7 +369,7 @@ fn test_valid_log_utf16_null_terminated() { logic.log_utf16(u64::MAX, utf16_bytes.as_ptr() as _).expect("Valid utf-16 string_bytes"); let len = utf16_bytes.len() as u64; - let outcome = logic.outcome(); + let outcome = logic.compute_outcome_and_distribute_gas(); assert_eq!(outcome.logs[0], string); assert_costs(map! { ExtCosts::base: 1, diff --git a/runtime/near-vm-runner/src/imports.rs b/runtime/near-vm-runner/src/imports.rs index 8d6935f49fd..bf0083923fc 100644 --- a/runtime/near-vm-runner/src/imports.rs +++ b/runtime/near-vm-runner/src/imports.rs @@ -155,6 +155,16 @@ imports! { amount_ptr: u64, gas: u64 ] -> []>, + #["protocol_feature_function_call_weight", FunctionCallRatio] promise_batch_action_function_call_weight<[ + promise_index: u64, + method_name_len: u64, + method_name_ptr: u64, + arguments_len: u64, + arguments_ptr: u64, + amount_ptr: u64, + gas: u64, + gas_weight: u64 + ] -> []>, promise_batch_action_transfer<[promise_index: u64, amount_ptr: u64] -> []>, promise_batch_action_stake<[ promise_index: u64, diff --git a/runtime/near-vm-runner/src/wasmer2_runner.rs b/runtime/near-vm-runner/src/wasmer2_runner.rs index 6076a5abad3..47f9a14f57f 100644 --- a/runtime/near-vm-runner/src/wasmer2_runner.rs +++ b/runtime/near-vm-runner/src/wasmer2_runner.rs @@ -449,7 +449,7 @@ impl Wasmer2VM { return (None, Some(e)); } let err = self.run_method(artifact, import, method_name).err(); - (Some(logic.outcome()), err) + (Some(logic.compute_outcome_and_distribute_gas()), err) } } @@ -571,7 +571,7 @@ impl crate::runner::VM for Wasmer2VM { // TODO: remove, as those costs are incorrectly computed, and we shall account it on deployment. if logic.add_contract_compile_fee(code.code().len() as u64).is_err() { return ( - Some(logic.outcome()), + Some(logic.compute_outcome_and_distribute_gas()), Some(VMError::FunctionCallError(FunctionCallError::HostError( near_vm_errors::HostError::GasExceeded, ))), @@ -582,7 +582,7 @@ impl crate::runner::VM for Wasmer2VM { return (None, Some(e)); } let err = self.run_method(&artifact, import, method_name).err(); - (Some(logic.outcome()), err) + (Some(logic.compute_outcome_and_distribute_gas()), err) } fn precompile( diff --git a/runtime/near-vm-runner/src/wasmer_runner.rs b/runtime/near-vm-runner/src/wasmer_runner.rs index d0b7abcbb30..9b27ee21456 100644 --- a/runtime/near-vm-runner/src/wasmer_runner.rs +++ b/runtime/near-vm-runner/src/wasmer_runner.rs @@ -261,7 +261,7 @@ pub(crate) fn run_wasmer0_module<'a>( } let err = run_method(&module, &import_object, method_name).err(); - (Some(logic.outcome()), err) + (Some(logic.compute_outcome_and_distribute_gas()), err) } pub(crate) fn wasmer0_vm_hash() -> u64 { @@ -346,7 +346,7 @@ impl crate::runner::VM for Wasmer0VM { // TODO: remove, as those costs are incorrectly computed, and we shall account it on deployment. if logic.add_contract_compile_fee(code.code().len() as u64).is_err() { return ( - Some(logic.outcome()), + Some(logic.compute_outcome_and_distribute_gas()), Some(VMError::FunctionCallError(FunctionCallError::HostError( near_vm_errors::HostError::GasExceeded, ))), @@ -361,7 +361,7 @@ impl crate::runner::VM for Wasmer0VM { } let err = run_method(&module, &import_object, method_name).err(); - (Some(logic.outcome()), err) + (Some(logic.compute_outcome_and_distribute_gas()), err) } fn precompile( diff --git a/runtime/near-vm-runner/src/wasmtime_runner.rs b/runtime/near-vm-runner/src/wasmtime_runner.rs index 073a3e1fb0d..1d5347b6a94 100644 --- a/runtime/near-vm-runner/src/wasmtime_runner.rs +++ b/runtime/near-vm-runner/src/wasmtime_runner.rs @@ -227,7 +227,7 @@ impl crate::runner::VM for WasmtimeVM { // TODO: remove, as those costs are incorrectly computed, and we shall account it on deployment. if logic.add_contract_compile_fee(code.code().len() as u64).is_err() { return ( - Some(logic.outcome()), + Some(logic.compute_outcome_and_distribute_gas()), Some(VMError::FunctionCallError(FunctionCallError::HostError( near_vm_errors::HostError::GasExceeded, ))), @@ -282,10 +282,16 @@ impl crate::runner::VM for WasmtimeVM { Ok(instance) => match instance.get_func(&mut store, method_name) { Some(func) => match func.typed::<(), (), _>(&mut store) { Ok(run) => match run.call(&mut store, ()) { - Ok(_) => (Some(logic.outcome()), None), - Err(err) => (Some(logic.outcome()), Some(err.into_vm_error())), + Ok(_) => (Some(logic.compute_outcome_and_distribute_gas()), None), + Err(err) => ( + Some(logic.compute_outcome_and_distribute_gas()), + Some(err.into_vm_error()), + ), }, - Err(err) => (Some(logic.outcome()), Some(err.into_vm_error())), + Err(err) => ( + Some(logic.compute_outcome_and_distribute_gas()), + Some(err.into_vm_error()), + ), }, None => ( None, @@ -294,7 +300,9 @@ impl crate::runner::VM for WasmtimeVM { ))), ), }, - Err(err) => (Some(logic.outcome()), Some(err.into_vm_error())), + Err(err) => { + (Some(logic.compute_outcome_and_distribute_gas()), Some(err.into_vm_error())) + } } } diff --git a/runtime/runtime/Cargo.toml b/runtime/runtime/Cargo.toml index 1ca745871a6..6f5ac4be698 100644 --- a/runtime/runtime/Cargo.toml +++ b/runtime/runtime/Cargo.toml @@ -55,6 +55,10 @@ protocol_feature_alt_bn128 = [ "near-vm-runner/protocol_feature_alt_bn128", "near-vm-errors/protocol_feature_alt_bn128", ] +protocol_feature_function_call_weight = [ + "near-primitives/protocol_feature_function_call_weight", + "near-vm-logic/protocol_feature_function_call_weight", +] sandbox = ["near-vm-logic/sandbox", "near-vm-runner/sandbox"] [dev-dependencies] diff --git a/runtime/runtime/src/ext.rs b/runtime/runtime/src/ext.rs index f860173bc54..e6e4138d1ee 100644 --- a/runtime/runtime/src/ext.rs +++ b/runtime/runtime/src/ext.rs @@ -14,7 +14,9 @@ use near_primitives::transaction::{ DeployContractAction, FunctionCallAction, StakeAction, TransferAction, }; use near_primitives::trie_key::{trie_key_parsers, TrieKey}; -use near_primitives::types::{AccountId, Balance, EpochId, EpochInfoProvider}; +use near_primitives::types::{AccountId, Balance, EpochId, EpochInfoProvider, Gas}; +#[cfg(feature = "protocol_feature_function_call_weight")] +use near_primitives::types::{GasDistribution, GasWeight}; use near_primitives::utils::create_data_id; use near_primitives::version::ProtocolVersion; use near_store::{get_code, TrieUpdate, TrieUpdateValuePtr}; @@ -35,6 +37,15 @@ pub struct RuntimeExt<'a> { last_block_hash: &'a CryptoHash, epoch_info_provider: &'a dyn EpochInfoProvider, current_protocol_version: ProtocolVersion, + + #[cfg(feature = "protocol_feature_function_call_weight")] + gas_weights: Vec<(FunctionCallActionIndex, GasWeight)>, +} + +#[cfg(feature = "protocol_feature_function_call_weight")] +struct FunctionCallActionIndex { + receipt_index: usize, + action_index: usize, } /// Error used by `RuntimeExt`. @@ -93,6 +104,9 @@ impl<'a> RuntimeExt<'a> { last_block_hash, epoch_info_provider, current_protocol_version, + + #[cfg(feature = "protocol_feature_function_call_weight")] + gas_weights: vec![], } } @@ -140,13 +154,19 @@ impl<'a> RuntimeExt<'a> { .collect() } - fn append_action(&mut self, receipt_index: u64, action: Action) { - self.action_receipts + /// Appends an action and returns the index the action was inserted in the receipt + fn append_action(&mut self, receipt_index: u64, action: Action) -> usize { + let actions = &mut self + .action_receipts .get_mut(receipt_index as usize) .expect("receipt index should be present") .1 - .actions - .push(action); + .actions; + + actions.push(action); + + // Return index that action was inserted at + actions.len() - 1 } #[inline] @@ -254,13 +274,44 @@ impl<'a> External for RuntimeExt<'a> { Ok(()) } + #[cfg(feature = "protocol_feature_function_call_weight")] + fn append_action_function_call_weight( + &mut self, + receipt_index: u64, + method_name: Vec, + args: Vec, + attached_deposit: u128, + prepaid_gas: Gas, + gas_weight: GasWeight, + ) -> ExtResult<()> { + let action_index = self.append_action( + receipt_index, + Action::FunctionCall(FunctionCallAction { + method_name: String::from_utf8(method_name) + .map_err(|_| HostError::InvalidMethodName)?, + args, + gas: prepaid_gas, + deposit: attached_deposit, + }), + ); + + if gas_weight.0 > 0 { + self.gas_weights.push(( + FunctionCallActionIndex { receipt_index: receipt_index as usize, action_index }, + gas_weight, + )); + } + + Ok(()) + } + fn append_action_function_call( &mut self, receipt_index: u64, method_name: Vec, args: Vec, attached_deposit: u128, - prepaid_gas: u64, + prepaid_gas: Gas, ) -> ExtResult<()> { self.append_action( receipt_index, @@ -389,4 +440,55 @@ impl<'a> External for RuntimeExt<'a> { .validator_total_stake(self.epoch_id, self.prev_block_hash) .map_err(|e| ExternalError::ValidatorError(e).into()) } + + /// Distributes the gas passed in by splitting it among weights defined in `gas_weights`. + /// This will sum all weights, retrieve the gas per weight, then update each function + /// to add the respective amount of gas. Once all gas is distributed, the remainder of + /// the gas not assigned due to precision loss is added to the last function with a weight. + #[cfg(feature = "protocol_feature_function_call_weight")] + fn distribute_unused_gas(&mut self, gas: u64) -> GasDistribution { + let gas_weight_sum: u128 = + self.gas_weights.iter().map(|(_, GasWeight(weight))| *weight as u128).sum(); + if gas_weight_sum != 0 { + // Floor division that will ensure gas allocated is <= gas to distribute + let gas_per_weight = (gas as u128 / gas_weight_sum) as u64; + + let mut distribute_gas = |metadata: &FunctionCallActionIndex, assigned_gas: u64| { + let FunctionCallActionIndex { receipt_index, action_index } = metadata; + if let Some(Action::FunctionCall(FunctionCallAction { ref mut gas, .. })) = self + .action_receipts + .get_mut(*receipt_index) + .and_then(|(_, receipt)| receipt.actions.get_mut(*action_index)) + { + *gas += assigned_gas; + } else { + panic!( + "Invalid index for assigning unused gas weight \ + (promise_index={}, action_index={})", + receipt_index, action_index + ); + } + }; + + let mut distributed = 0; + for (action_index, GasWeight(weight)) in &self.gas_weights { + // This can't overflow because the gas_per_weight is floor division + // of the weight sum. + let assigned_gas = gas_per_weight * weight; + + distribute_gas(action_index, assigned_gas); + + distributed += assigned_gas + } + + // Distribute remaining gas to final action. + if let Some((last_idx, _)) = self.gas_weights.last() { + distribute_gas(last_idx, gas - distributed); + } + self.gas_weights.clear(); + GasDistribution::All + } else { + GasDistribution::NoRatios + } + } }