Skip to content

Commit

Permalink
feat(Runtime): Dynamic gas pricing (#2813)
Browse files Browse the repository at this point in the history
Implement dynamic gas price charging.
See NEP for discussion: near/NEPs#67

List of changes:
- Introduce a new config `pessimistic_gas_price_inflation_ratio`. Pessimistic inflation ratio, by default `3%` comparing to full block inflation of about `1%` gas price inflation. It's higher to account for potential delayed receipts.
- Change `max_total_prepaid_gas` to `300 * 10^12`. It's higher than max burn gas per call, so it shouldn't affect existing contract much.
- Receipts gas price is still the gas price at which the gas was purchased, but the actual gas price is used to burn gas. The remaining balance amount of gas is refunded back to the account and to the access key.
- If the purchased gas price is lower than the current gas price, we try to use the unused gas amount to compensate for difference. This might happen due to really long delayed queues of receipts. If the difference is not possible to compensate, the amount is added to the `ApplyStats::gas_deficit_amount`. This amount reflect the balance that was not able to charge from the account and is needed for the balance checker.
- The actual gas price is used to calculate burnt amount to reward the execution contract and validators. Even if the originator/signer account bought gas at a cheaper price (due to long queues).
- Now even non-function call actions may generate refunds. E.g. a transfer from `alice` to `bob` will generate gas refund back to `alice`, to account for the potential increase in gas price. This increases the amount of refunds flying cross shard and likely will decrease our TPS for transfer transactions. Doesn't affect function calls much, because they almost always generate refunds.

Fixes most in the near/NEPs#67

This change will introduce the devx issue:

- ExecutionOutcome doesn't contain the actual gas_price or the block_height at which it was executed. It's available on the node side, when it pulls ExecutionOutcome for each block, so it can be included later for reporting. Without actual gas price, it's impossible to calculate the actual transaction cost.

Depends on near/near-api-js#340
  • Loading branch information
Evgeny Kuzyakov authored Jun 17, 2020
1 parent 06674af commit d55c888
Show file tree
Hide file tree
Showing 20 changed files with 642 additions and 254 deletions.
296 changes: 154 additions & 142 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion core/primitives/src/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ pub const DB_VERSION: DbVersion = 1;
pub type ProtocolVersion = u32;

/// Current latest version of the protocol.
pub const PROTOCOL_VERSION: ProtocolVersion = 25;
pub const PROTOCOL_VERSION: ProtocolVersion = 26;

pub const FIRST_BACKWARD_COMPATIBLE_PROTOCOL_VERSION: ProtocolVersion = PROTOCOL_VERSION;
20 changes: 18 additions & 2 deletions core/runtime-configs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use near_runtime_fees::RuntimeFeesConfig;
use near_vm_logic::VMConfig;

/// The structure that holds the parameters of the runtime, mostly economics.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(default)]
pub struct RuntimeConfig {
/// Amount of yN per byte required to have on the account.
Expand Down Expand Up @@ -47,7 +47,7 @@ impl RuntimeConfig {
}

/// The structure describes configuration for creation of new accounts.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct AccountCreationConfig {
/// The minimum length of the top-level account ID that is allowed to be created by any account.
pub min_allowed_top_level_account_length: u8,
Expand All @@ -64,3 +64,19 @@ impl Default for AccountCreationConfig {
}
}
}

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

#[test]
fn test_max_prepaid_gas() {
let config = RuntimeConfig::default();
assert!(
config.wasm_config.limit_config.max_total_prepaid_gas
/ config.transaction_costs.min_receipt_with_function_call_gas()
<= 63,
"The maximum desired depth of receipts should be at most 63"
);
}
}
10 changes: 7 additions & 3 deletions neard/res/genesis_config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"protocol_version": 25,
"protocol_version": 26,
"genesis_time": "1970-01-01T00:00:00.000000000Z",
"chain_id": "sample",
"genesis_height": 0,
Expand Down Expand Up @@ -125,6 +125,10 @@
"burnt_gas_reward": [
3,
10
],
"pessimistic_gas_price_inflation_ratio": [
103,
100
]
},
"wasm_config": {
Expand Down Expand Up @@ -190,7 +194,7 @@
"max_number_registers": 100,
"max_number_logs": 100,
"max_total_log_length": 16384,
"max_total_prepaid_gas": 10000000000000000,
"max_total_prepaid_gas": 300000000000000,
"max_actions_per_receipt": 100,
"max_number_bytes_method_names": 2000,
"max_length_method_name": 256,
Expand Down Expand Up @@ -230,4 +234,4 @@
"fishermen_threshold": "10000000000000000000000000",
"minimum_stake_divisor": 10,
"records": []
}
}
7 changes: 7 additions & 0 deletions neard/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -953,4 +953,11 @@ mod test {
let genesis_config = GenesisConfig::from_json(&genesis_config_str);
assert_eq!(genesis_config.protocol_version, PROTOCOL_VERSION);
}

#[test]
fn test_res_genesis_fees_are_default() {
let genesis_config_str = include_str!("../res/genesis_config.json");
let genesis_config = GenesisConfig::from_json(&genesis_config_str);
assert_eq!(genesis_config.runtime_config, RuntimeConfig::default());
}
}
42 changes: 42 additions & 0 deletions runtime/near-runtime-fees/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct Fee {
}

impl Fee {
#[inline]
pub fn send_fee(&self, sir: bool) -> Gas {
if sir {
self.send_sir
Expand All @@ -33,6 +34,11 @@ impl Fee {
pub fn exec_fee(&self) -> Gas {
self.execution
}

/// The minimum fee to send and execute.
fn min_send_and_exec_fee(&self) -> Gas {
std::cmp::min(self.send_sir, self.send_not_sir) + self.execution
}
}

#[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Eq)]
Expand All @@ -49,6 +55,9 @@ pub struct RuntimeFeesConfig {

/// Fraction of the burnt gas to reward to the contract account for execution.
pub burnt_gas_reward: Rational,

/// Pessimistic gas price inflation ratio.
pub pessimistic_gas_price_inflation_ratio: Rational,
}

/// Describes the cost of creating a data receipt, `DataReceipt`.
Expand Down Expand Up @@ -204,6 +213,7 @@ impl Default for RuntimeFeesConfig {
num_extra_bytes_record: 40,
},
burnt_gas_reward: Rational::new(3, 10),
pessimistic_gas_price_inflation_ratio: Rational::new(103, 100),
}
}
}
Expand Down Expand Up @@ -238,6 +248,38 @@ impl RuntimeFeesConfig {
num_extra_bytes_record: 0,
},
burnt_gas_reward: Rational::from_integer(0),
pessimistic_gas_price_inflation_ratio: Rational::from_integer(0),
}
}

/// The minimum amount of gas required to create and execute a new receipt with a function call
/// action.
/// This amount is used to determine how many receipts can be created, send and executed for
/// some amount of prepaid gas using function calls.
pub fn min_receipt_with_function_call_gas(&self) -> Gas {
self.action_receipt_creation_config.min_send_and_exec_fee()
+ self.action_creation_config.function_call_cost.min_send_and_exec_fee()
}
}

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

#[test]
fn test_data_roundtrip_is_more_expensive() {
// We have an assumption that the deepest receipts we can create is by creating recursive
// function call promises (calling function call from a function call).
// If the cost of a data receipt is cheaper than the cost of a function call, then it's
// possible to create a promise with a dependency which will be executed in two blocks that
// is cheaper than just two recursive function calls.
// That's why we need to enforce that the cost of the data receipt is not less than a
// function call. Otherwise we'd have to modify the way we compute the maximum depth.
let transaction_costs = RuntimeFeesConfig::default();
assert!(
transaction_costs.data_receipt_creation_config.base_cost.min_send_and_exec_fee()
>= transaction_costs.min_receipt_with_function_call_gas(),
"The data receipt cost can't be larger than the cost of a receipt with a function call"
);
}
}
8 changes: 5 additions & 3 deletions runtime/near-vm-logic/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

#[derive(Clone, Debug, Hash, Serialize, Deserialize)]
#[derive(Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq)]
pub struct VMConfig {
/// Costs for runtime externals
pub ext_costs: ExtCostsConfig,
Expand Down Expand Up @@ -134,8 +134,10 @@ impl Default for VMLimitConfig {
// Total logs size is 16Kib
max_total_log_length: 16 * 1024,

// Fills 10 blocks. It defines how long a single receipt might live.
max_total_prepaid_gas: 10 * 10u64.pow(15),
// Updating the maximum prepaid gas to limit the maximum depth of a transaction to 64
// blocks.
// This based on `63 * min_receipt_with_function_call_gas()`. Where 63 is max depth - 1.
max_total_prepaid_gas: 300 * 10u64.pow(12),

// Safety limit. Unlikely to hit it for most common transactions and receipts.
max_actions_per_receipt: 100,
Expand Down
2 changes: 1 addition & 1 deletion runtime/runtime-standalone/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ mod tests {
"{\"account_id\": \"status\", \"message\": \"caller status is ok!\"}"
.as_bytes()
.to_vec(),
10000000000000000,
300_000_000_000_000,
CryptoHash::default(),
)),
Ok(ExecutionOutcome { status: ExecutionStatus::SuccessValue(_), .. })
Expand Down
2 changes: 2 additions & 0 deletions runtime/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ sha2 = "0.8"
sha3 = "0.8"
lazy_static = "1.4"
num-rational = "0.2.4"
num-bigint = "0.2.6"
num-traits = "0.2.11"

borsh = "0.6.2"
cached = "0.12.0"
Expand Down
1 change: 1 addition & 0 deletions runtime/runtime/src/balance_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ mod tests {
&[receipt],
&ApplyStats {
tx_burnt_amount: total_validator_reward,
gas_deficit_amount: 0,
other_burnt_amount: 0,
slashed_burnt_amount: 0,
},
Expand Down
71 changes: 62 additions & 9 deletions runtime/runtime/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,42 @@ use near_runtime_fees::RuntimeFeesConfig;
// Just re-exporting RuntimeConfig for backwards compatibility.
pub use near_runtime_configs::RuntimeConfig;

use num_bigint::BigUint;
use num_rational::Rational;
use num_traits::cast::ToPrimitive;
use num_traits::pow::Pow;
use std::convert::TryFrom;

/// Describes the cost of converting this transaction into a receipt.
#[derive(Debug)]
pub struct TransactionCost {
/// Total amount of gas burnt for converting this transaction into a receipt.
pub gas_burnt: Gas,
/// Total amount of gas used for converting this transaction into a receipt. It includes gas
/// that is not yet spent, e.g. prepaid gas for function calls and future execution fees.
pub gas_used: Gas,
/// The remaining amount of gas used for converting this transaction into a receipt.
/// It includes gas that is not yet spent, e.g. prepaid gas for function calls and
/// future execution fees.
pub gas_remaining: Gas,
/// The gas price at which the gas was purchased in the receipt.
pub receipt_gas_price: Balance,
/// Total costs in tokens for this transaction (including all deposits).
pub total_cost: Balance,
/// The amount of tokens burnt by converting this transaction to a receipt.
pub burnt_amount: Balance,
}

/// Multiplies `gas_price` by the power of `inflation_base` with exponent `inflation_exponent`.
pub fn safe_gas_price_inflated(
gas_price: Balance,
inflation_base: Rational,
inflation_exponent: u8,
) -> Result<Balance, IntegerOverflowError> {
let numer = BigUint::from(*inflation_base.numer() as usize).pow(inflation_exponent);
let denom = BigUint::from(*inflation_base.denom() as usize).pow(inflation_exponent);
// Rounding up
let inflated_gas_price: BigUint = (numer * gas_price + &denom - 1u8) / denom;
inflated_gas_price.to_u128().ok_or_else(|| IntegerOverflowError {})
}

pub fn safe_gas_to_balance(gas_price: Balance, gas: Gas) -> Result<Balance, IntegerOverflowError> {
gas_price.checked_mul(Balance::from(gas)).ok_or_else(|| IntegerOverflowError {})
}
Expand Down Expand Up @@ -141,13 +163,30 @@ pub fn tx_cost(
gas_burnt,
total_send_fees(&config, sender_is_receiver, &transaction.actions)?,
)?;
let mut gas_used = safe_add_gas(gas_burnt, config.action_receipt_creation_config.exec_fee())?;
gas_used = safe_add_gas(gas_used, total_exec_fees(&config, &transaction.actions)?)?;
gas_used = safe_add_gas(gas_used, total_prepaid_gas(&transaction.actions)?)?;
let mut total_cost = safe_gas_to_balance(gas_price, gas_used)?;
total_cost = safe_add_balance(total_cost, total_deposit(&transaction.actions)?)?;
let prepaid_gas = total_prepaid_gas(&transaction.actions)?;
// If signer is equals to receiver the receipt will be processed at the same block as this
// transaction. Otherwise it will processed in the next block and the gas might be inflated.
let initial_receipt_hop = if transaction.signer_id == transaction.receiver_id { 0 } else { 1 };
let minimum_new_receipt_gas = config.min_receipt_with_function_call_gas();
// In case the config is free, we don't care about the maximum depth.
let maximum_depth =
if minimum_new_receipt_gas > 0 { prepaid_gas / minimum_new_receipt_gas } else { 0 };
let inflation_exponent =
u8::try_from(initial_receipt_hop + maximum_depth).map_err(|_| IntegerOverflowError {})?;
let receipt_gas_price = safe_gas_price_inflated(
gas_price,
config.pessimistic_gas_price_inflation_ratio,
inflation_exponent,
)?;

let mut gas_remaining =
safe_add_gas(prepaid_gas, config.action_receipt_creation_config.exec_fee())?;
gas_remaining = safe_add_gas(gas_remaining, total_exec_fees(&config, &transaction.actions)?)?;
let burnt_amount = safe_gas_to_balance(gas_price, gas_burnt)?;
Ok(TransactionCost { gas_burnt, gas_used, total_cost, burnt_amount })
let remaining_gas_amount = safe_gas_to_balance(receipt_gas_price, gas_remaining)?;
let mut total_cost = safe_add_balance(burnt_amount, remaining_gas_amount)?;
total_cost = safe_add_balance(total_cost, total_deposit(&transaction.actions)?)?;
Ok(TransactionCost { gas_burnt, gas_remaining, receipt_gas_price, total_cost, burnt_amount })
}

/// Total sum of gas that would need to be burnt before we start executing the given actions.
Expand Down Expand Up @@ -175,3 +214,17 @@ pub fn total_deposit(actions: &[Action]) -> Result<Balance, IntegerOverflowError
pub fn total_prepaid_gas(actions: &[Action]) -> Result<Gas, IntegerOverflowError> {
actions.iter().try_fold(0, |acc, action| safe_add_gas(acc, action.get_prepaid_gas()))
}

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

#[test]
fn test_safe_gas_price_inflated() {
assert_eq!(safe_gas_price_inflated(10000, Rational::new(101, 100), 1).unwrap(), 10100);
assert_eq!(safe_gas_price_inflated(10000, Rational::new(101, 100), 2).unwrap(), 10201);
// Rounded up
assert_eq!(safe_gas_price_inflated(10000, Rational::new(101, 100), 3).unwrap(), 10304);
assert_eq!(safe_gas_price_inflated(10000, Rational::new(101, 100), 32).unwrap(), 13750);
}
}
Loading

0 comments on commit d55c888

Please sign in to comment.