From 072a32baa51e11e079f447eb9ae6d16d9e87b75c Mon Sep 17 00:00:00 2001 From: Andrei Kashin Date: Tue, 18 Apr 2023 11:44:44 +0200 Subject: [PATCH] feature: Re-introduce Compute Costs (#8915) This PR re-introduces changes from https://github.com/near/nearcore/pull/8805 together with https://github.com/near/nearcore/pull/8892 after fixing https://github.com/near/nearcore/issues/8908: The PR addresses https://github.com/near/nearcore/issues/8265 by introducing aggregation of compute usage across all operations performed during chunk application and limiting this compute usage to 1s. This should not change the behavior of nodes in the short run because compute costs match gas costs which is validated by the assert, so any discrepancy should be caught on canary nodes. --- chain/chain/src/test_utils/kv_runtime.rs | 1 + chain/chain/src/tests/simple_chain.rs | 4 +- chain/chain/src/types.rs | 2 + .../jsonrpc-tests/res/genesis_config.json | 4 +- core/primitives/Cargo.toml | 2 - core/primitives/src/transaction.rs | 12 +- core/primitives/src/version.rs | 13 +- runtime/near-vm-logic/src/logic.rs | 8 +- runtime/runtime/src/actions.rs | 7 +- runtime/runtime/src/config.rs | 6 +- runtime/runtime/src/lib.rs | 258 ++++++++++++++++-- tools/state-viewer/src/contract_accounts.rs | 1 + 12 files changed, 272 insertions(+), 46 deletions(-) diff --git a/chain/chain/src/test_utils/kv_runtime.rs b/chain/chain/src/test_utils/kv_runtime.rs index 7af4586d8b2..8f6e5b8ad21 100644 --- a/chain/chain/src/test_utils/kv_runtime.rs +++ b/chain/chain/src/test_utils/kv_runtime.rs @@ -1170,6 +1170,7 @@ impl RuntimeAdapter for KeyValueRuntime { logs: vec![], receipt_ids: new_receipt_hashes, gas_burnt: 0, + compute_usage: Some(0), tokens_burnt: 0, executor_id: to.clone(), metadata: ExecutionMetadata::V1, diff --git a/chain/chain/src/tests/simple_chain.rs b/chain/chain/src/tests/simple_chain.rs index 82d89b3594b..9458de249e3 100644 --- a/chain/chain/src/tests/simple_chain.rs +++ b/chain/chain/src/tests/simple_chain.rs @@ -45,7 +45,7 @@ fn build_chain() { if cfg!(feature = "nightly") { insta::assert_display_snapshot!(hash, @"3Dkg6hjpnYvMuoyEdSLnEXza6Ct2ZV9xoridA37AJzSz"); } else { - insta::assert_display_snapshot!(hash, @"3fK7Uu3HC9y9DPMsDNaAP8sv56UCK2ZV1txRhTriF9qb"); + insta::assert_display_snapshot!(hash, @"DuT1f8dmu3xYvFfsEFiAycgeLpJWQM74PZ8JtjT7SGyK"); } for i in 1..5 { @@ -75,7 +75,7 @@ fn build_chain() { if cfg!(feature = "nightly") { insta::assert_display_snapshot!(hash, @"6uCZwfkpE8qV54n5MvZXqTt8RMHYDduX4eE7quNzgLNk"); } else { - insta::assert_display_snapshot!(hash, @"7hQ4bvvu2TmgFVhELFThZPHFoBPwa74VT6D8uk79xPzB"); + insta::assert_display_snapshot!(hash, @"Ce8Ehs6S2RmXUdp2WnuyQFBGs4S3Pvs5sBwuSMZW7pqS"); } } diff --git a/chain/chain/src/types.rs b/chain/chain/src/types.rs index a88be70c394..02674fe8d83 100644 --- a/chain/chain/src/types.rs +++ b/chain/chain/src/types.rs @@ -631,6 +631,7 @@ mod tests { logs: vec!["outcome1".to_string()], receipt_ids: vec![hash(&[1])], gas_burnt: 100, + compute_usage: Some(200), tokens_burnt: 10000, executor_id: "alice".parse().unwrap(), metadata: ExecutionMetadata::V1, @@ -643,6 +644,7 @@ mod tests { logs: vec!["outcome2".to_string()], receipt_ids: vec![], gas_burnt: 0, + compute_usage: Some(0), tokens_burnt: 0, executor_id: "bob".parse().unwrap(), metadata: ExecutionMetadata::V1, diff --git a/chain/jsonrpc/jsonrpc-tests/res/genesis_config.json b/chain/jsonrpc/jsonrpc-tests/res/genesis_config.json index eea75629d9e..f682359642b 100644 --- a/chain/jsonrpc/jsonrpc-tests/res/genesis_config.json +++ b/chain/jsonrpc/jsonrpc-tests/res/genesis_config.json @@ -1,5 +1,5 @@ { - "protocol_version": 60, + "protocol_version": 61, "genesis_time": "1970-01-01T00:00:00.000000000Z", "chain_id": "sample", "genesis_height": 0, @@ -69,4 +69,4 @@ ], "use_production_config": false, "records": [] -} +} \ No newline at end of file diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index e4347735301..c10d36f92fb 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -50,14 +50,12 @@ protocol_feature_fix_staking_threshold = [] protocol_feature_fix_contract_loading_cost = [] protocol_feature_reject_blocks_with_outdated_protocol_version = [] protocol_feature_flat_state = [] -protocol_feature_compute_costs = [] nightly = [ "nightly_protocol", "protocol_feature_fix_staking_threshold", "protocol_feature_fix_contract_loading_cost", "protocol_feature_reject_blocks_with_outdated_protocol_version", "protocol_feature_flat_state", - "protocol_feature_compute_costs", ] nightly_protocol = [] diff --git a/core/primitives/src/transaction.rs b/core/primitives/src/transaction.rs index 6ff6388b1ec..23a6ddd6600 100644 --- a/core/primitives/src/transaction.rs +++ b/core/primitives/src/transaction.rs @@ -9,6 +9,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use near_crypto::{PublicKey, Signature}; use near_fmt::{AbbrBytes, Slice}; 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}; @@ -415,6 +416,13 @@ pub struct ExecutionOutcome { pub receipt_ids: Vec, /// 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`. + // At the moment this field is only set at runtime and is not persisted in the database. + // This means that when execution outcomes are read from the database, this value will not be + // set and any code that attempts to use it will crash. + #[borsh_skip] + pub compute_usage: Option, /// 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. @@ -437,7 +445,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), } @@ -453,6 +461,7 @@ impl fmt::Debug for ExecutionOutcome { .field("logs", &Slice(&self.logs)) .field("receipt_ids", &Slice(&self.receipt_ids)) .field("burnt_gas", &self.gas_burnt) + .field("compute_usage", &self.compute_usage.unwrap_or_default()) .field("tokens_burnt", &self.tokens_burnt) .field("status", &self.status) .field("metadata", &self.metadata) @@ -598,6 +607,7 @@ mod tests { logs: vec!["123".to_string(), "321".to_string()], receipt_ids: vec![], gas_burnt: 123, + compute_usage: Some(456), tokens_burnt: 1234000, executor_id: "alice".parse().unwrap(), metadata: ExecutionMetadata::V1, diff --git a/core/primitives/src/version.rs b/core/primitives/src/version.rs index 807152d3f0c..033fb1b83b0 100644 --- a/core/primitives/src/version.rs +++ b/core/primitives/src/version.rs @@ -139,6 +139,12 @@ pub enum ProtocolFeature { /// Meta Transaction NEP-366: https://github.com/near/NEPs/blob/master/neps/nep-0366.md DelegateAction, + /// Decouple compute and gas costs of operations to safely limit the compute time it takes to + /// process the chunk. + /// + /// Compute Costs NEP-455: https://github.com/near/NEPs/blob/master/neps/nep-0455.md + ComputeCosts, + /// In case not all validator seats are occupied our algorithm provide incorrect minimal seat /// price - it reports as alpha * sum_stake instead of alpha * sum_stake / (1 - alpha), where /// alpha is min stake ratio @@ -152,8 +158,6 @@ pub enum ProtocolFeature { RejectBlocksWithOutdatedProtocolVersions, #[cfg(feature = "protocol_feature_flat_state")] FlatStorageReads, - #[cfg(feature = "protocol_feature_compute_costs")] - ComputeCosts, } /// Both, outgoing and incoming tcp connections to peers, will be rejected if `peer's` @@ -163,7 +167,7 @@ pub const PEER_MIN_ALLOWED_PROTOCOL_VERSION: ProtocolVersion = STABLE_PROTOCOL_V /// Current protocol version used on the mainnet. /// Some features (e. g. FixStorageUsage) require that there is at least one epoch with exactly /// the corresponding version -const STABLE_PROTOCOL_VERSION: ProtocolVersion = 60; +const STABLE_PROTOCOL_VERSION: ProtocolVersion = 61; /// Largest protocol version supported by the current binary. pub const PROTOCOL_VERSION: ProtocolVersion = if cfg!(feature = "nightly_protocol") { @@ -238,6 +242,7 @@ impl ProtocolFeature { ProtocolFeature::Ed25519Verify | ProtocolFeature::ZeroBalanceAccount | ProtocolFeature::DelegateAction => 59, + ProtocolFeature::ComputeCosts => 61, // Nightly features #[cfg(feature = "protocol_feature_fix_staking_threshold")] @@ -248,8 +253,6 @@ impl ProtocolFeature { ProtocolFeature::RejectBlocksWithOutdatedProtocolVersions => 132, #[cfg(feature = "protocol_feature_flat_state")] ProtocolFeature::FlatStorageReads => 135, - #[cfg(feature = "protocol_feature_compute_costs")] - ProtocolFeature::ComputeCosts => 136, } } } diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index 32c72e9b4e0..98c7d439cdd 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -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; @@ -2794,6 +2794,7 @@ 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, @@ -2801,6 +2802,7 @@ impl<'a> VMLogic<'a> { return_data: self.return_data, burnt_gas, used_gas, + compute_usage, logs: self.logs, profile, action_receipts: self.receipt_manager.action_receipts, @@ -2919,6 +2921,7 @@ pub struct VMOutcome { pub return_data: ReturnData, pub burnt_gas: Gas, pub used_gas: Gas, + pub compute_usage: Compute, pub logs: Vec, /// Data collected from making a contract call pub profile: ProfileDataV3, @@ -2952,6 +2955,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(), diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 61a20c40020..fa0e2191337 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -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}; @@ -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 { @@ -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(()) diff --git a/runtime/runtime/src/config.rs b/runtime/runtime/src/config.rs index 2d4228d2a32..a95d7a10508 100644 --- a/runtime/runtime/src/config.rs +++ b/runtime/runtime/src/config.rs @@ -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. @@ -59,6 +59,10 @@ pub fn safe_add_balance(a: Balance, b: Balance) -> Result Result { + a.checked_add(b).ok_or_else(|| IntegerOverflowError {}) +} + #[macro_export] macro_rules! safe_add_balance_apply { ($x: expr) => {$x}; diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index bfe078e44ca..3883d2075d8 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -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}; @@ -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::{ @@ -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, pub logs: Vec, pub new_receipts: Vec, @@ -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); @@ -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![], @@ -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: Some(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 @@ -295,16 +300,17 @@ impl Runtime { actions: &[Action], epoch_info_provider: &dyn EpochInfoProvider, ) -> Result { - // 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); @@ -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( @@ -586,6 +594,7 @@ impl Runtime { apply_state.current_protocol_version ) { result.gas_burnt = 0; + result.compute_usage = 0; result.gas_used = 0; } @@ -749,6 +758,7 @@ impl Runtime { logs: result.logs, receipt_ids, gas_burnt: result.gas_burnt, + compute_usage: Some(result.compute_usage), tokens_burnt, executor_id: account_id.clone(), metadata: ExecutionMetadata::V3(result.profile), @@ -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 = total_gas_burnt; for signed_transaction in transactions { let (receipt, outcome_with_id) = self.process_transaction( @@ -1268,6 +1279,21 @@ 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 + .expect("`process_transaction` must populate compute usage"), + )?; + + if !checked_feature!("stable", ComputeCosts, apply_state.current_protocol_version) { + assert_eq!( + total_compute_usage, total_gas_burnt, + "Compute usage must match burnt gas" + ); + } + outcomes.push(outcome_with_id); } @@ -1277,7 +1303,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", @@ -1302,12 +1329,28 @@ 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 + .expect("`process_receipt` must populate compute usage"), + )?; + + if !checked_feature!("stable", ComputeCosts, apply_state.current_protocol_version) { + 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 { @@ -1316,10 +1359,15 @@ 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)?; } @@ -1327,7 +1375,7 @@ impl Runtime { // 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 }; @@ -1360,7 +1408,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); } @@ -1379,8 +1432,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)?; } @@ -1535,18 +1593,20 @@ impl Runtime { #[cfg(test)] mod tests { + use assert_matches::assert_matches; use near_crypto::{InMemorySigner, KeyType, Signer}; use near_primitives::account::AccessKey; use near_primitives::hash::hash; use near_primitives::shard_layout::ShardUId; use near_primitives::test_utils::{account_new, MockEpochInfoProvider}; use near_primitives::transaction::{ - AddKeyAction, DeleteKeyAction, FunctionCallAction, TransferAction, + AddKeyAction, DeleteKeyAction, DeployContractAction, FunctionCallAction, TransferAction, }; use near_primitives::types::MerkleHash; use near_primitives::version::PROTOCOL_VERSION; use near_store::test_utils::create_tries; use near_store::{set_access_key, StoreCompiledContractCache}; + use near_vm_logic::{ExtCosts, ParameterCost}; use testlib::runtime_utils::{alice_account, bob_account}; use super::*; @@ -1557,15 +1617,15 @@ mod tests { near * 10u128.pow(24) } - fn create_receipts_with_actions( + fn create_receipt_with_actions( account_id: AccountId, signer: Arc, actions: Vec, - ) -> Vec { - vec![Receipt { + ) -> Receipt { + Receipt { predecessor_id: account_id.clone(), receiver_id: account_id.clone(), - receipt_id: CryptoHash::default(), + receipt_id: CryptoHash::hash_borsh(actions.clone()), receipt: ReceiptEnum::Action(ActionReceipt { signer_id: account_id, signer_public_key: signer.public_key(), @@ -1574,7 +1634,7 @@ mod tests { input_data_ids: vec![], actions, }), - }] + } } #[test] @@ -2372,7 +2432,7 @@ mod tests { }), ]; - let receipts = create_receipts_with_actions(alice_account(), signer, actions); + let receipts = vec![create_receipt_with_actions(alice_account(), signer, actions)]; let apply_result = runtime .apply( @@ -2418,7 +2478,7 @@ mod tests { let actions = vec![Action::DeleteKey(DeleteKeyAction { public_key: signer.public_key() })]; - let receipts = create_receipts_with_actions(alice_account(), signer, actions); + let receipts = vec![create_receipt_with_actions(alice_account(), signer, actions)]; let apply_result = runtime .apply( @@ -2457,11 +2517,9 @@ mod tests { let wasm_code = near_test_contracts::rs_contract().to_vec(); let actions = - vec![Action::DeployContract(near_primitives::transaction::DeployContractAction { - code: wasm_code.clone(), - })]; + vec![Action::DeployContract(DeployContractAction { code: wasm_code.clone() })]; - let receipts = create_receipts_with_actions(alice_account(), signer, actions); + let receipts = vec![create_receipt_with_actions(alice_account(), signer, actions)]; let apply_result = runtime .apply( @@ -2494,6 +2552,148 @@ mod tests { .expect("Compiled contract should be cached") .expect("Compilation result should be non-empty"); } + + #[test] + fn test_compute_usage_limit() { + let (runtime, tries, root, mut apply_state, signer, epoch_info_provider) = + setup_runtime(to_yocto(1_000_000), to_yocto(500_000), 1); + + let mut free_config = RuntimeConfig::free(); + let sha256_cost = ParameterCost { + gas: Gas::from(1_000_000u64), + compute: Compute::from(10_000_000_000_000u64), + }; + free_config.wasm_config.ext_costs.costs[ExtCosts::sha256_base] = sha256_cost.clone(); + apply_state.config = Arc::new(free_config); + // This allows us to execute 1 receipt with a function call per apply. + apply_state.gas_limit = Some(sha256_cost.compute); + + let deploy_contract_receipt = create_receipt_with_actions( + alice_account(), + signer.clone(), + vec![Action::DeployContract(DeployContractAction { + code: near_test_contracts::rs_contract().to_vec(), + })], + ); + + let first_call_receipt = create_receipt_with_actions( + alice_account(), + signer.clone(), + vec![Action::FunctionCall(FunctionCallAction { + method_name: "ext_sha256".to_string(), + args: b"first".to_vec(), + gas: sha256_cost.gas, + deposit: 0, + })], + ); + + let second_call_receipt = create_receipt_with_actions( + alice_account(), + signer.clone(), + vec![Action::FunctionCall(FunctionCallAction { + method_name: "ext_sha256".to_string(), + args: b"second".to_vec(), + gas: sha256_cost.gas, + deposit: 0, + })], + ); + + let apply_result = runtime + .apply( + tries.get_trie_for_shard(ShardUId::single_shard(), root), + &None, + &apply_state, + &vec![ + deploy_contract_receipt.clone(), + first_call_receipt.clone(), + second_call_receipt.clone(), + ], + &[], + &epoch_info_provider, + Default::default(), + ) + .unwrap(); + let mut store_update = tries.store_update(); + let root = tries.apply_all( + &apply_result.trie_changes, + ShardUId::single_shard(), + &mut store_update, + ); + store_update.commit().unwrap(); + + // Only first two receipts should fit into the chunk due to the compute usage limit. + assert_matches!(&apply_result.outcomes[..], [first, second] => { + assert_eq!(first.id, deploy_contract_receipt.receipt_id); + assert_matches!(first.outcome.status, ExecutionStatus::SuccessValue(_)); + + assert_eq!(second.id, first_call_receipt.receipt_id); + assert_eq!(second.outcome.compute_usage.unwrap(), sha256_cost.compute); + assert_matches!(second.outcome.status, ExecutionStatus::SuccessValue(_)); + }); + + let apply_result = runtime + .apply( + tries.get_trie_for_shard(ShardUId::single_shard(), root), + &None, + &apply_state, + &[], + &[], + &epoch_info_provider, + Default::default(), + ) + .unwrap(); + + assert_matches!(&apply_result.outcomes[..], [ExecutionOutcomeWithId { id, outcome }] => { + assert_eq!(*id, second_call_receipt.receipt_id); + assert_eq!(outcome.compute_usage.unwrap(), sha256_cost.compute); + assert_matches!(outcome.status, ExecutionStatus::SuccessValue(_)); + }); + } + + #[test] + fn test_compute_usage_limit_with_failed_receipt() { + let (runtime, tries, root, apply_state, signer, epoch_info_provider) = + setup_runtime(to_yocto(1_000_000), to_yocto(500_000), 10u64.pow(15)); + + let deploy_contract_receipt = create_receipt_with_actions( + alice_account(), + signer.clone(), + vec![Action::DeployContract(DeployContractAction { + code: near_test_contracts::rs_contract().to_vec(), + })], + ); + + let first_call_receipt = create_receipt_with_actions( + alice_account(), + signer.clone(), + vec![Action::FunctionCall(FunctionCallAction { + method_name: "ext_sha256".to_string(), + args: b"first".to_vec(), + gas: 1, + deposit: 0, + })], + ); + + let apply_result = runtime + .apply( + tries.get_trie_for_shard(ShardUId::single_shard(), root), + &None, + &apply_state, + &vec![deploy_contract_receipt.clone(), first_call_receipt.clone()], + &[], + &epoch_info_provider, + Default::default(), + ) + .unwrap(); + + assert_matches!(&apply_result.outcomes[..], [first, second] => { + assert_eq!(first.id, deploy_contract_receipt.receipt_id); + assert_matches!(first.outcome.status, ExecutionStatus::SuccessValue(_)); + + assert_eq!(second.id, first_call_receipt.receipt_id); + assert_matches!(second.outcome.status, ExecutionStatus::Failure(_)); + }); + } } /// Interface provided for gas cost estimations. diff --git a/tools/state-viewer/src/contract_accounts.rs b/tools/state-viewer/src/contract_accounts.rs index 540227fc6a3..910d64aeda4 100644 --- a/tools/state-viewer/src/contract_accounts.rs +++ b/tools/state-viewer/src/contract_accounts.rs @@ -677,6 +677,7 @@ mod tests { logs: vec![], receipt_ids, gas_burnt: 100, + compute_usage: Some(200), tokens_burnt: 2000, executor_id: "someone.near".parse().unwrap(), status: ExecutionStatus::SuccessValue(vec![]),