Skip to content

Commit

Permalink
feature: Limit compute usage of a chunk
Browse files Browse the repository at this point in the history
The commit introduces aggregation of compute usage across all operations
performed during chunk application and limiting of this compute usage
to 1s.

This should not change the behavior of nodes in the short run because
compute costs match gas costs and this is validated by the
`debug_assert`.

There are two follow ups to this work:
- near#8859
- near#8860
  • Loading branch information
aborg-dev committed Apr 3, 2023
1 parent 24c4f49 commit 97e4147
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 23 deletions.
1 change: 1 addition & 0 deletions chain/chain/src/test_utils/kv_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,7 @@ impl RuntimeAdapter for KeyValueRuntime {
logs: vec![],
receipt_ids: new_receipt_hashes,
gas_burnt: 0,
compute_usage: 0,
tokens_burnt: 0,
executor_id: to.clone(),
metadata: ExecutionMetadata::V1,
Expand Down
2 changes: 2 additions & 0 deletions chain/chain/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ mod tests {
logs: vec!["outcome1".to_string()],
receipt_ids: vec![hash(&[1])],
gas_burnt: 100,
compute_usage: 200,
tokens_burnt: 10000,
executor_id: "alice".parse().unwrap(),
metadata: ExecutionMetadata::V1,
Expand All @@ -649,6 +650,7 @@ mod tests {
logs: vec!["outcome2".to_string()],
receipt_ids: vec![],
gas_burnt: 0,
compute_usage: 0,
tokens_burnt: 0,
executor_id: "bob".parse().unwrap(),
metadata: ExecutionMetadata::V1,
Expand Down
8 changes: 7 additions & 1 deletion core/primitives/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use near_crypto::{PublicKey, Signature};
use near_o11y::pretty;
use near_primitives_core::profile::{ProfileDataV2, ProfileDataV3};
use near_primitives_core::types::Compute;
use std::borrow::Borrow;
use std::fmt;
use std::hash::{Hash, Hasher};
Expand Down Expand Up @@ -415,6 +416,10 @@ pub struct ExecutionOutcome {
pub receipt_ids: Vec<CryptoHash>,
/// The amount of the gas burnt by the given transaction or receipt.
pub gas_burnt: Gas,
/// The amount of compute time spent by the given transaction or receipt.
// TODO(#8859): Treat this field in the same way as `gas_burnt`.
#[borsh_skip]
pub compute_usage: Compute,
/// The amount of tokens burnt corresponding to the burnt gas amount.
/// This value doesn't always equal to the `gas_burnt` multiplied by the gas price, because
/// the prepaid gas price might be lower than the actual gas price and it creates a deficit.
Expand All @@ -437,7 +442,7 @@ pub enum ExecutionMetadata {
V1,
/// V2: With ProfileData by legacy `Cost` enum
V2(ProfileDataV2),
// V3: With ProfileData by gas parameters
/// V3: With ProfileData by gas parameters
V3(ProfileDataV3),
}

Expand Down Expand Up @@ -598,6 +603,7 @@ mod tests {
logs: vec!["123".to_string(), "321".to_string()],
receipt_ids: vec![],
gas_burnt: 123,
compute_usage: 456,
tokens_burnt: 1234000,
executor_id: "alice".parse().unwrap(),
metadata: ExecutionMetadata::V1,
Expand Down
8 changes: 6 additions & 2 deletions runtime/near-vm-logic/src/logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ use near_primitives_core::config::ExtCosts::*;
use near_primitives_core::config::{ActionCosts, ExtCosts, VMConfig};
use near_primitives_core::runtime::fees::{transfer_exec_fee, transfer_send_fee};
use near_primitives_core::types::{
AccountId, Balance, EpochHeight, Gas, ProtocolVersion, StorageUsage,
AccountId, Balance, Compute, EpochHeight, Gas, GasDistribution, GasWeight, ProtocolVersion,
StorageUsage,
};
use near_primitives_core::types::{GasDistribution, GasWeight};
use near_vm_errors::{FunctionCallError, InconsistentStateError};
use near_vm_errors::{HostError, VMLogicError};
use std::mem::size_of;
Expand Down Expand Up @@ -2792,13 +2792,15 @@ impl<'a> VMLogic<'a> {

let mut profile = self.gas_counter.profile_data();
profile.compute_wasm_instruction_cost(burnt_gas);
let compute_usage = profile.total_compute_usage(&self.config.ext_costs);

VMOutcome {
balance: self.current_account_balance,
storage_usage: self.current_storage_usage,
return_data: self.return_data,
burnt_gas,
used_gas,
compute_usage,
logs: self.logs,
profile,
action_receipts: self.receipt_manager.action_receipts,
Expand Down Expand Up @@ -2917,6 +2919,7 @@ pub struct VMOutcome {
pub return_data: ReturnData,
pub burnt_gas: Gas,
pub used_gas: Gas,
pub compute_usage: Compute,
pub logs: Vec<String>,
/// Data collected from making a contract call
pub profile: ProfileDataV3,
Expand Down Expand Up @@ -2950,6 +2953,7 @@ impl VMOutcome {
return_data: ReturnData::None,
burnt_gas: 0,
used_gas: 0,
compute_usage: 0,
logs: Vec::new(),
profile: ProfileDataV3::default(),
action_receipts: Vec::new(),
Expand Down
7 changes: 5 additions & 2 deletions runtime/runtime/src/actions.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::config::{
safe_add_gas, total_prepaid_exec_fees, total_prepaid_gas, total_prepaid_send_fees,
RuntimeConfig,
safe_add_compute, safe_add_gas, total_prepaid_exec_fees, total_prepaid_gas,
total_prepaid_send_fees, RuntimeConfig,
};
use crate::ext::{ExternalError, RuntimeExt};
use crate::{metrics, ActionResult, ApplyState};
Expand Down Expand Up @@ -254,6 +254,7 @@ pub(crate) fn action_function_call(
// return a real `gas_used` instead of the `gas_burnt` into `ActionResult` even for
// `FunctionCall`s error.
result.gas_used = safe_add_gas(result.gas_used, outcome.used_gas)?;
result.compute_usage = safe_add_compute(result.compute_usage, outcome.compute_usage)?;
result.logs.extend(outcome.logs);
result.profile.merge(&outcome.profile);
if execution_succeeded {
Expand Down Expand Up @@ -687,6 +688,8 @@ pub(crate) fn apply_delegate_action(
// gas_used is incremented because otherwise the gas will be refunded. Refund function checks only gas_used.
result.gas_used = safe_add_gas(result.gas_used, prepaid_send_fees)?;
result.gas_burnt = safe_add_gas(result.gas_burnt, prepaid_send_fees)?;
// TODO(#8806): Support compute costs for actions. For now they match burnt gas.
result.compute_usage = safe_add_compute(result.compute_usage, prepaid_send_fees)?;
result.new_receipts.push(new_receipt);

Ok(())
Expand Down
6 changes: 5 additions & 1 deletion runtime/runtime/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use near_primitives::runtime::fees::{transfer_exec_fee, transfer_send_fee, Runti
use near_primitives::transaction::{
Action, AddKeyAction, DeployContractAction, FunctionCallAction, Transaction,
};
use near_primitives::types::{AccountId, Balance, Gas};
use near_primitives::types::{AccountId, Balance, Compute, Gas};
use near_primitives::version::{is_implicit_account_creation_enabled, ProtocolVersion};

/// Describes the cost of converting this transaction into a receipt.
Expand Down Expand Up @@ -59,6 +59,10 @@ pub fn safe_add_balance(a: Balance, b: Balance) -> Result<Balance, IntegerOverfl
a.checked_add(b).ok_or_else(|| IntegerOverflowError {})
}

pub fn safe_add_compute(a: Compute, b: Compute) -> Result<Compute, IntegerOverflowError> {
a.checked_add(b).ok_or_else(|| IntegerOverflowError {})
}

#[macro_export]
macro_rules! safe_add_balance_apply {
($x: expr) => {$x};
Expand Down
79 changes: 62 additions & 17 deletions runtime/runtime/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::actions::*;
use crate::balance_checker::check_balance;
use crate::config::{
exec_fee, safe_add_balance, safe_add_gas, safe_gas_to_balance, total_deposit,
exec_fee, safe_add_balance, safe_add_compute, safe_add_gas, safe_gas_to_balance, total_deposit,
total_prepaid_exec_fees, total_prepaid_gas, RuntimeConfig,
};
use crate::genesis::{GenesisStateApplier, StorageComputer};
Expand Down Expand Up @@ -35,7 +35,7 @@ use near_primitives::transaction::{
};
use near_primitives::trie_key::TrieKey;
use near_primitives::types::{
validator_stake::ValidatorStake, AccountId, Balance, EpochInfoProvider, Gas,
validator_stake::ValidatorStake, AccountId, Balance, Compute, EpochInfoProvider, Gas,
RawStateChangesWithTrieKey, ShardId, StateChangeCause, StateRoot,
};
use near_primitives::utils::{
Expand Down Expand Up @@ -125,6 +125,7 @@ pub struct ActionResult {
pub gas_burnt: Gas,
pub gas_burnt_for_function_call: Gas,
pub gas_used: Gas,
pub compute_usage: Compute,
pub result: Result<ReturnData, ActionError>,
pub logs: Vec<LogEntry>,
pub new_receipts: Vec<Receipt>,
Expand All @@ -147,6 +148,7 @@ impl ActionResult {
next_result.gas_burnt_for_function_call,
)?;
self.gas_used = safe_add_gas(self.gas_used, next_result.gas_used)?;
self.compute_usage = safe_add_compute(self.compute_usage, next_result.compute_usage)?;
self.profile.merge(&next_result.profile);
self.result = next_result.result;
self.logs.append(&mut next_result.logs);
Expand All @@ -171,6 +173,7 @@ impl Default for ActionResult {
gas_burnt: 0,
gas_burnt_for_function_call: 0,
gas_used: 0,
compute_usage: 0,
result: Ok(ReturnData::None),
logs: vec![],
new_receipts: vec![],
Expand Down Expand Up @@ -263,6 +266,8 @@ impl Runtime {
logs: vec![],
receipt_ids: vec![receipt.receipt_id],
gas_burnt: verification_result.gas_burnt,
// TODO(#8806): Support compute costs for actions. For now they match burnt gas.
compute_usage: verification_result.gas_burnt,
tokens_burnt: verification_result.burnt_amount,
executor_id: transaction.signer_id.clone(),
// TODO: profile data is only counted in apply_action, which only happened at process_receipt
Expand Down Expand Up @@ -295,16 +300,17 @@ impl Runtime {
actions: &[Action],
epoch_info_provider: &dyn EpochInfoProvider,
) -> Result<ActionResult, RuntimeError> {
// println!("enter apply_action");
let mut result = ActionResult::default();
let exec_fees = exec_fee(
&apply_state.config.fees,
action,
&receipt.receiver_id,
apply_state.current_protocol_version,
);
result.gas_burnt += exec_fees;
result.gas_used += exec_fees;
let mut result = ActionResult::default();
result.gas_used = exec_fees;
result.gas_burnt = exec_fees;
// TODO(#8806): Support compute costs for actions. For now they match burnt gas.
result.compute_usage = exec_fees;
let account_id = &receipt.receiver_id;
let is_the_only_action = actions.len() == 1;
let is_refund = AccountId::is_system(&receipt.predecessor_id);
Expand Down Expand Up @@ -499,9 +505,11 @@ impl Runtime {
let mut account = get_account(state_update, account_id)?;
let mut actor_id = receipt.predecessor_id.clone();
let mut result = ActionResult::default();
let exec_fee = apply_state.config.fees.fee(ActionCosts::new_action_receipt).exec_fee();
result.gas_used = exec_fee;
result.gas_burnt = exec_fee;
let exec_fees = apply_state.config.fees.fee(ActionCosts::new_action_receipt).exec_fee();
result.gas_used = exec_fees;
result.gas_burnt = exec_fees;
// TODO(#8806): Support compute costs for actions. For now they match burnt gas.
result.compute_usage = exec_fees;
// Executing actions one by one
for (action_index, action) in action_receipt.actions.iter().enumerate() {
let action_hash = create_action_hash(
Expand Down Expand Up @@ -586,6 +594,7 @@ impl Runtime {
apply_state.current_protocol_version
) {
result.gas_burnt = 0;
result.compute_usage = 0;
result.gas_used = 0;
}

Expand Down Expand Up @@ -749,6 +758,7 @@ impl Runtime {
logs: result.logs,
receipt_ids,
gas_burnt: result.gas_burnt,
compute_usage: result.compute_usage,
tokens_burnt,
executor_id: account_id.clone(),
metadata: ExecutionMetadata::V3(result.profile),
Expand Down Expand Up @@ -1253,6 +1263,7 @@ impl Runtime {
// charge any gas for refund receipts, we still count the gas use towards the block gas
// limit
let mut total_gas_burnt = gas_used_for_migrations;
let mut total_compute_usage = gas_used_for_migrations;

for signed_transaction in transactions {
let (receipt, outcome_with_id) = self.process_transaction(
Expand All @@ -1268,6 +1279,15 @@ impl Runtime {
}

total_gas_burnt = safe_add_gas(total_gas_burnt, outcome_with_id.outcome.gas_burnt)?;
total_compute_usage =
safe_add_compute(total_compute_usage, outcome_with_id.outcome.compute_usage)?;

// TODO(#8032): Remove when compute costs are stabilized.
debug_assert_eq!(
total_compute_usage, total_gas_burnt,
"Compute usage must match burnt gas"
);

outcomes.push(outcome_with_id);
}

Expand All @@ -1277,7 +1297,8 @@ impl Runtime {

let mut process_receipt = |receipt: &Receipt,
state_update: &mut TrieUpdate,
total_gas_burnt: &mut Gas|
total_gas_burnt: &mut Gas,
total_compute_usage: &mut Compute|
-> Result<_, RuntimeError> {
let _span = tracing::debug_span!(
target: "runtime",
Expand All @@ -1302,12 +1323,21 @@ impl Runtime {
if let Some(outcome_with_id) = result? {
*total_gas_burnt =
safe_add_gas(*total_gas_burnt, outcome_with_id.outcome.gas_burnt)?;
*total_compute_usage =
safe_add_compute(*total_compute_usage, outcome_with_id.outcome.compute_usage)?;
// TODO(#8032): Remove when compute costs are stabilized.
debug_assert_eq!(
total_compute_usage, total_gas_burnt,
"Compute usage must match burnt gas"
);
outcomes.push(outcome_with_id);
}
Ok(())
};

let gas_limit = apply_state.gas_limit.unwrap_or(Gas::max_value());
// TODO(#8859): Introduce a dedicated `compute_limit` for the chunk.
// For now compute limit always matches the gas limit.
let compute_limit = apply_state.gas_limit.unwrap_or(Gas::max_value());

// We first process local receipts. They contain staking, local contract calls, etc.
if let Some(prefetcher) = &mut prefetcher {
Expand All @@ -1316,18 +1346,23 @@ impl Runtime {
_ = prefetcher.prefetch_receipts_data(&local_receipts);
}
for receipt in local_receipts.iter() {
if total_gas_burnt < gas_limit {
if total_compute_usage < compute_limit {
// NOTE: We don't need to validate the local receipt, because it's just validated in
// the `verify_and_charge_transaction`.
process_receipt(receipt, &mut state_update, &mut total_gas_burnt)?;
process_receipt(
receipt,
&mut state_update,
&mut total_gas_burnt,
&mut total_compute_usage,
)?;
} else {
Self::delay_receipt(&mut state_update, &mut delayed_receipts_indices, receipt)?;
}
}

// Then we process the delayed receipts. It's a backlog of receipts from the past blocks.
while delayed_receipts_indices.first_index < delayed_receipts_indices.next_available_index {
if total_gas_burnt >= gas_limit {
if total_compute_usage >= compute_limit {
break;
}
let key = TrieKey::DelayedReceipt { index: delayed_receipts_indices.first_index };
Expand Down Expand Up @@ -1360,7 +1395,12 @@ impl Runtime {
state_update.remove(key);
// Math checked above: first_index is less than next_available_index
delayed_receipts_indices.first_index += 1;
process_receipt(&receipt, &mut state_update, &mut total_gas_burnt)?;
process_receipt(
&receipt,
&mut state_update,
&mut total_gas_burnt,
&mut total_compute_usage,
)?;
processed_delayed_receipts.push(receipt);
}

Expand All @@ -1379,8 +1419,13 @@ impl Runtime {
apply_state.current_protocol_version,
)
.map_err(RuntimeError::ReceiptValidationError)?;
if total_gas_burnt < gas_limit {
process_receipt(receipt, &mut state_update, &mut total_gas_burnt)?;
if total_compute_usage < compute_limit {
process_receipt(
receipt,
&mut state_update,
&mut total_gas_burnt,
&mut total_compute_usage,
)?;
} else {
Self::delay_receipt(&mut state_update, &mut delayed_receipts_indices, receipt)?;
}
Expand Down
1 change: 1 addition & 0 deletions tools/state-viewer/src/contract_accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ mod tests {
logs: vec![],
receipt_ids,
gas_burnt: 100,
compute_usage: 200,
tokens_burnt: 2000,
executor_id: "someone.near".parse().unwrap(),
status: ExecutionStatus::SuccessValue(vec![]),
Expand Down

0 comments on commit 97e4147

Please sign in to comment.