Skip to content

Commit

Permalink
feat: implement NEP264, function call gas weight (near#6285)
Browse files Browse the repository at this point in the history
More of a rough draft because I don't have context around the runtime as to the best migration plan of these APIs. I've followed the recommendation of near#6150 to avoid refactoring to move action receipts to `VMLogic`, but this means the `External` trait needs to be updated, and it's unclear if anyone has preferences.

TODO:
- e2e test to cover `RuntimeExt` as current tests just cover `MockedExternal` (same functionality though)
  - Also these tests don't check that gas was distributed to the correct function calls
- Determine the best path for trait migration (would we do the refactor mentioned above before any releases?)
- Where does the protocol version number come from, and what should this one be numbered?
- Benchmark to gauge gas cost and verify existing impl doesn't have higher cost
- Would be nice to fuzz test this to make sure there aren't any weird edge cases
  • Loading branch information
austinabell authored Mar 10, 2022
1 parent dbf2557 commit 13057c5
Show file tree
Hide file tree
Showing 18 changed files with 579 additions and 42 deletions.
6 changes: 3 additions & 3 deletions chain/chain/src/tests/simple_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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!(
Expand All @@ -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!(
Expand Down
1 change: 1 addition & 0 deletions core/primitives-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 18 additions & 0 deletions core/primitives-core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions core/primitives/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,15 @@ 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",
"protocol_feature_chunk_only_producers",
"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 = [
Expand Down
6 changes: 5 additions & 1 deletion core/primitives/src/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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)]
Expand Down Expand Up @@ -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,
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions runtime/near-vm-logic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
65 changes: 65 additions & 0 deletions runtime/near-vm-logic/src/dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<u8>,
arguments: Vec<u8>,
attached_deposit: Balance,
prepaid_gas: Gas,
gas_weight: GasWeight,
) -> Result<()>;

/// Attach the [`TransferAction`] action to an existing receipt.
///
/// # Arguments
Expand Down Expand Up @@ -486,4 +539,16 @@ pub trait External {

/// Returns total stake of validators in the current epoch.
fn validator_total_stake(&self) -> Result<Balance>;

/// 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;
}
121 changes: 117 additions & 4 deletions runtime/near-vm-logic/src/logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<u8>, Vec<u8>, u128, u64) -> Result<()>,
) -> Result<()> {
self.gas_counter.pay_base(base)?;
if self.context.is_view() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();

Expand Down
Loading

0 comments on commit 13057c5

Please sign in to comment.