From 5e174de469fb55e44e0148cd228cec49b6d1bd6a Mon Sep 17 00:00:00 2001 From: posvyatokum Date: Tue, 4 Apr 2023 11:10:13 +0100 Subject: [PATCH 1/4] feat: create NodeStorage from checkpoint (#8876) This should be safe to use with already open db. Not sure about easy, though. home_dir, config, and archive flag are not available everywhere right now without any hustle. --- core/store/src/db.rs | 3 + core/store/src/db/colddb.rs | 4 ++ core/store/src/db/rocksdb.rs | 6 ++ core/store/src/db/splitdb.rs | 5 ++ core/store/src/db/testdb.rs | 4 ++ core/store/src/lib.rs | 4 +- core/store/src/opener.rs | 121 ++++++++++++++++++++++++++++++++++- 7 files changed, 145 insertions(+), 2 deletions(-) diff --git a/core/store/src/db.rs b/core/store/src/db.rs index 1017288eea2..b5b1faadac8 100644 --- a/core/store/src/db.rs +++ b/core/store/src/db.rs @@ -224,6 +224,9 @@ pub trait Database: Sync + Send { /// Returns statistics about the database if available. fn get_store_statistics(&self) -> Option; + + /// Create checkpoint in provided path + fn create_checkpoint(&self, path: &std::path::Path) -> anyhow::Result<()>; } fn assert_no_overwrite(col: DBCol, key: &[u8], value: &[u8], old_value: &[u8]) { diff --git a/core/store/src/db/colddb.rs b/core/store/src/db/colddb.rs index 4fe390f3a63..9d5d1f359a8 100644 --- a/core/store/src/db/colddb.rs +++ b/core/store/src/db/colddb.rs @@ -110,6 +110,10 @@ impl Database for ColdDB { fn get_store_statistics(&self) -> Option { self.cold.get_store_statistics() } + + fn create_checkpoint(&self, path: &std::path::Path) -> anyhow::Result<()> { + self.cold.create_checkpoint(path) + } } /// Adjust database operation to be performed on cold storage. diff --git a/core/store/src/db/rocksdb.rs b/core/store/src/db/rocksdb.rs index c83e0cc1777..c914756171a 100644 --- a/core/store/src/db/rocksdb.rs +++ b/core/store/src/db/rocksdb.rs @@ -385,6 +385,12 @@ impl Database for RocksDB { Some(result) } } + + fn create_checkpoint(&self, path: &std::path::Path) -> anyhow::Result<()> { + let cp = ::rocksdb::checkpoint::Checkpoint::new(&self.db)?; + cp.create_checkpoint(path)?; + Ok(()) + } } /// DB level options diff --git a/core/store/src/db/splitdb.rs b/core/store/src/db/splitdb.rs index bffb87b4f1b..46527c2e0c4 100644 --- a/core/store/src/db/splitdb.rs +++ b/core/store/src/db/splitdb.rs @@ -197,6 +197,11 @@ impl Database for SplitDB { log_assert_fail!("get_store_statistics is not allowed - the split storage has two stores"); None } + + fn create_checkpoint(&self, _path: &std::path::Path) -> anyhow::Result<()> { + log_assert_fail!("create_checkpoint is not allowed - the split storage has two stores"); + Ok(()) + } } #[cfg(test)] diff --git a/core/store/src/db/testdb.rs b/core/store/src/db/testdb.rs index ada6ce744f7..bbb41334a3e 100644 --- a/core/store/src/db/testdb.rs +++ b/core/store/src/db/testdb.rs @@ -127,4 +127,8 @@ impl Database for TestDB { fn get_store_statistics(&self) -> Option { self.stats.read().unwrap().clone() } + + fn create_checkpoint(&self, _path: &std::path::Path) -> anyhow::Result<()> { + Ok(()) + } } diff --git a/core/store/src/lib.rs b/core/store/src/lib.rs index c52031d9301..146df3db90b 100644 --- a/core/store/src/lib.rs +++ b/core/store/src/lib.rs @@ -54,7 +54,9 @@ pub mod test_utils; mod trie; pub use crate::config::{Mode, StoreConfig}; -pub use crate::opener::{StoreMigrator, StoreOpener, StoreOpenerError}; +pub use crate::opener::{ + checkpoint_hot_storage_and_cleanup_columns, StoreMigrator, StoreOpener, StoreOpenerError, +}; /// Specifies temperature of a storage. /// diff --git a/core/store/src/opener.rs b/core/store/src/opener.rs index ed4fa2f42da..5880416f9ee 100644 --- a/core/store/src/opener.rs +++ b/core/store/src/opener.rs @@ -1,9 +1,10 @@ use std::sync::Arc; +use strum::IntoEnumIterator; use crate::db::rocksdb::snapshot::{Snapshot, SnapshotError, SnapshotRemoveError}; use crate::db::rocksdb::RocksDB; use crate::metadata::{DbKind, DbMetadata, DbVersion, DB_VERSION}; -use crate::{Mode, NodeStorage, Store, StoreConfig, Temperature}; +use crate::{DBCol, DBTransaction, Mode, NodeStorage, Store, StoreConfig, Temperature}; #[derive(Debug, thiserror::Error)] pub enum StoreOpenerError { @@ -569,3 +570,121 @@ pub trait StoreMigrator { /// equal to [`DB_VERSION`]. fn migrate(&self, store: &Store, version: DbVersion) -> anyhow::Result<()>; } + +/// Creates checkpoint of hot storage in `home_dir.join(checkpoint_relative_path)` +/// +/// If `columns_to_keep` is None doesn't cleanup columns. +/// Otherwise deletes all columns that are not in `columns_to_keep`. +/// +/// Returns NodeStorage of checkpoint db. +/// `archive` -- is hot storage archival (needed to open checkpoint). +#[allow(dead_code)] +pub fn checkpoint_hot_storage_and_cleanup_columns( + db_storage: &NodeStorage, + home_dir: &std::path::Path, + checkpoint_relative_path: std::path::PathBuf, + columns_to_keep: Option>, + archive: bool, +) -> anyhow::Result { + let checkpoint_path = home_dir.join(checkpoint_relative_path); + + db_storage.hot_storage.create_checkpoint(&checkpoint_path)?; + + // As only path from config is used in StoreOpener, default config with custom path will do. + let mut config = StoreConfig::default(); + config.path = Some(checkpoint_path); + let opener = StoreOpener::new(home_dir, archive, &config, None); + let node_storage = opener.open()?; + + if let Some(columns_to_keep) = columns_to_keep { + let columns_to_keep_set: std::collections::HashSet = + std::collections::HashSet::from_iter(columns_to_keep.into_iter()); + let mut transaction = DBTransaction::new(); + + for col in DBCol::iter() { + if !columns_to_keep_set.contains(&col) { + transaction.delete_all(col); + } + } + + node_storage.hot_storage.write(transaction)?; + } + + Ok(node_storage) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn check_keys_existence(store: &Store, column: &DBCol, keys: &Vec>, expected: bool) { + for key in keys { + assert_eq!(store.exists(*column, &key).unwrap(), expected, "Column {:?}", column); + } + } + + #[test] + fn test_checkpoint_hot_storage_and_cleanup_columns() { + let (home_dir, opener) = NodeStorage::test_opener(); + let node_storage = opener.open().unwrap(); + + let keys = vec![vec![0], vec![1], vec![2], vec![3]]; + let columns = vec![DBCol::Block, DBCol::Chunks, DBCol::BlockHeader]; + + let mut store_update = node_storage.get_hot_store().store_update(); + for column in columns { + for key in &keys { + store_update.insert(column, key, &vec![42]); + } + } + store_update.commit().unwrap(); + + let store = checkpoint_hot_storage_and_cleanup_columns( + &node_storage, + &home_dir.path(), + std::path::PathBuf::from("checkpoint_none"), + None, + false, + ) + .unwrap(); + check_keys_existence(&store.get_hot_store(), &DBCol::Block, &keys, true); + check_keys_existence(&store.get_hot_store(), &DBCol::Chunks, &keys, true); + check_keys_existence(&store.get_hot_store(), &DBCol::BlockHeader, &keys, true); + + let store = checkpoint_hot_storage_and_cleanup_columns( + &node_storage, + &home_dir.path(), + std::path::PathBuf::from("checkpoint_some"), + Some(vec![DBCol::Block]), + false, + ) + .unwrap(); + check_keys_existence(&store.get_hot_store(), &DBCol::Block, &keys, true); + check_keys_existence(&store.get_hot_store(), &DBCol::Chunks, &keys, false); + check_keys_existence(&store.get_hot_store(), &DBCol::BlockHeader, &keys, false); + + let store = checkpoint_hot_storage_and_cleanup_columns( + &node_storage, + &home_dir.path(), + std::path::PathBuf::from("checkpoint_all"), + Some(vec![DBCol::Block, DBCol::Chunks, DBCol::BlockHeader]), + false, + ) + .unwrap(); + check_keys_existence(&store.get_hot_store(), &DBCol::Block, &keys, true); + check_keys_existence(&store.get_hot_store(), &DBCol::Chunks, &keys, true); + check_keys_existence(&store.get_hot_store(), &DBCol::BlockHeader, &keys, true); + + let store = checkpoint_hot_storage_and_cleanup_columns( + &node_storage, + &home_dir.path(), + std::path::PathBuf::from("checkpoint_empty"), + Some(vec![]), + false, + ) + .unwrap(); + check_keys_existence(&store.get_hot_store(), &DBCol::Block, &keys, false); + check_keys_existence(&store.get_hot_store(), &DBCol::Chunks, &keys, false); + check_keys_existence(&store.get_hot_store(), &DBCol::BlockHeader, &keys, false); + } +} From bd5c0fbc5b7e4a1d024104042a3d1089668f9fe4 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Tue, 4 Apr 2023 19:23:08 +0900 Subject: [PATCH 2/4] Fix: rosetta zero balance accounts tests (#8833) (My bad, didn't realize that the `S-automerge` tag would merge both directions, so this is a separate PR.) Also a bit of clean-up w.r.t. magic numbers. Implicit account test no longer has to worry about the `LIQUID_BALANCE_FOR_STORAGE` subaccount. --- Cargo.lock | 1 + chain/rosetta-rpc/Cargo.toml | 14 +-- chain/rosetta-rpc/src/utils.rs | 2 +- pytest/tests/sanity/rosetta.py | 218 ++++++++++++++++++++++----------- 4 files changed, 158 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aecfd360c87..4a9177f0228 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3807,6 +3807,7 @@ dependencies = [ "near-network", "near-o11y", "near-primitives", + "node-runtime", "paperclip", "serde", "serde_json", diff --git a/chain/rosetta-rpc/Cargo.toml b/chain/rosetta-rpc/Cargo.toml index fa64ea010ff..9c28c0f9893 100644 --- a/chain/rosetta-rpc/Cargo.toml +++ b/chain/rosetta-rpc/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "near-rosetta-rpc" -version = "0.0.0" authors.workspace = true -publish = false edition.workspace = true +name = "near-rosetta-rpc" +publish = false +version = "0.0.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -24,15 +24,15 @@ thiserror.workspace = true tokio.workspace = true validator.workspace = true -near-primitives.workspace = true near-account-id.workspace = true -near-crypto.workspace = true near-chain-configs.workspace = true -near-client.workspace = true near-client-primitives.workspace = true +near-client.workspace = true +near-crypto.workspace = true near-network.workspace = true near-o11y.workspace = true - +near-primitives.workspace = true +node-runtime.workspace = true [dev-dependencies] insta.workspace = true diff --git a/chain/rosetta-rpc/src/utils.rs b/chain/rosetta-rpc/src/utils.rs index cf818956f47..9b55f60af01 100644 --- a/chain/rosetta-rpc/src/utils.rs +++ b/chain/rosetta-rpc/src/utils.rs @@ -271,7 +271,7 @@ where /// Zero-balance account (NEP-448) fn is_zero_balance_account(account: &near_primitives::account::Account) -> bool { - account.storage_usage() <= 770 + account.storage_usage() <= node_runtime::ZERO_BALANCE_ACCOUNT_STORAGE_LIMIT } /// Tokens not locked due to staking (=liquid) but reserved for state. diff --git a/pytest/tests/sanity/rosetta.py b/pytest/tests/sanity/rosetta.py index d067542b596..18f20251ba3 100644 --- a/pytest/tests/sanity/rosetta.py +++ b/pytest/tests/sanity/rosetta.py @@ -25,6 +25,10 @@ TxIdentifier = typing.Union[str, JsonDict] +def account_identifier(account_id: str) -> JsonDict: + return {'address': account_id} + + def block_identifier(block_id: BlockIdentifier) -> JsonDict: if isinstance(block_id, int): return {'index': block_id} @@ -261,6 +265,36 @@ def transfer(self, *, src: key.Key, dst: key.Key, amount: int, }, }, **kw) + def add_full_access_key(self, account: key.Key, public_key_hex: str, + **kw) -> RosettaExecResult: + return self.exec_operations( + account, { + "operation_identifier": { + "index": 0 + }, + "type": "INITIATE_ADD_KEY", + "account": { + "address": account.account_id + } + }, { + "operation_identifier": { + "index": 1 + }, + "related_operations": [{ + "index": 0 + }], + "type": "ADD_KEY", + "account": { + "address": account.account_id + }, + "metadata": { + "public_key": { + "hex_bytes": public_key_hex, + "curve_type": "edwards25519" + } + } + }, **kw) + def delete_account(self, account: key.Key, refund_to: key.Key, **kw) -> RosettaExecResult: return self.exec_operations( @@ -325,6 +359,11 @@ def get_transaction(self, *, block_id: BlockIdentifier, transaction_identifier=tx_identifier(tx_id)) return res['transaction'] + def get_account_balances(self, *, account_id: str) -> JsonDict: + res = self.rpc('/account/balance', + account_identifier=account_identifier(account_id)) + return res['balances'] + class RosettaTestCase(unittest.TestCase): node = None @@ -350,6 +389,109 @@ def setUpClass(cls) -> None: def tearDownClass(cls) -> None: cls.node.cleanup() + def test_zero_balance_account(self) -> None: + """Tests storage staking requirements for low-storage accounts. + + Creates an implicit account by sending it 1 yoctoNEAR (not enough to + cover storage). However, the zero-balance allowance established in + NEP-448 should cover the storage staking requirement. Then, we + transfer 10**22 yoctoNEAR to the account, which should be enough to + cover the storage staking requirement for the 6 full-access keys we + then add to the account, exceeding the zero-balance account allowance. + """ + + test_amount = 10**22 + key_space_cost = 41964925000000000000 + validator = self.node.validator_key + implicit = key.Key.implicit_account() + + # first transfer 1 yoctoNEAR to create the account + # not enough to cover storage, but the zero-balance allowance should cover it + result = self.rosetta.transfer(src=validator, dst=implicit, amount=1) + + block = result.block() + tx = result.transaction() + json_res = self.node.get_tx(result.near_hash, implicit.account_id) + json_res = json_res['result'] + receipt_ids = json_res['transaction_outcome']['outcome']['receipt_ids'] + receipt_id = {'hash': 'receipt:' + receipt_ids[0]} + + # Fetch the receipt through Rosetta RPC. + result = RosettaExecResult(self.rosetta, block, receipt_id) + related = result.related(0) + + balances = self.rosetta.get_account_balances( + account_id=implicit.account_id) + + # even though 1 yoctoNEAR is not enough to cover the storage cost, + # since the account should be consuming less than 770 bytes of storage, + # it should be allowed nonetheless. + self.assertEqual(balances, [{ + 'value': '1', + 'currency': { + 'symbol': 'NEAR', + 'decimals': 24 + } + }]) + + # transfer the rest of the amount + result = self.rosetta.transfer(src=validator, + dst=implicit, + amount=(test_amount - 1)) + + block = result.block() + tx = result.transaction() + json_res = self.node.get_tx(result.near_hash, implicit.account_id) + json_res = json_res['result'] + receipt_ids = json_res['transaction_outcome']['outcome']['receipt_ids'] + receipt_id = {'hash': 'receipt:' + receipt_ids[0]} + + # Fetch the receipt through Rosetta RPC. + result = RosettaExecResult(self.rosetta, block, receipt_id) + related = result.related(0) + + balances = self.rosetta.get_account_balances( + account_id=implicit.account_id) + + self.assertEqual(balances, [{ + 'value': str(test_amount), + 'currency': { + 'symbol': 'NEAR', + 'decimals': 24 + } + }]) + + # add 6 keys to go over the zero-balance account free storage allowance + public_keys_hex = [ + "17595386a67d36afc73872e60916f83217d789dc60b5d037563998e6651111cf", + "7940aac79a425f194621ab5c4e38b7841dddae90b20eaf28f7f78caec911bcf4", + "0554fffef36614d7c49b3088c4c1fb66613ff05fb30927b582b43aed0b25b549", + "09d36e25c5a3ac440a798252982dd92b67d8de60894df3177cb4ff30a890cafd", + "e0ca119be7211f3dfed1768fc9ab235b6af06a205077ef23166dd1cbfd2ac7fc", + "98f1a49296fb7156980d325a25e1bfeb4f123dd98c90fa0492699c55387f7ef3", + ] + for pk in public_keys_hex: + result = self.rosetta.add_full_access_key(implicit, pk) + + block = result.block() + tx = result.transaction() + json_res = self.node.get_tx(result.near_hash, implicit.account_id) + json_res = json_res['result'] + receipt_ids = json_res['transaction_outcome']['outcome'][ + 'receipt_ids'] + receipt_id = {'hash': 'receipt:' + receipt_ids[0]} + + # Fetch the receipt through Rosetta RPC. + result = RosettaExecResult(self.rosetta, block, receipt_id) + related = result.related(0) + + balances = self.rosetta.get_account_balances( + account_id=implicit.account_id) + + # no longer a zero-balance account + self.assertEqual(test_amount - key_space_cost * len(public_keys_hex), + int(balances[0]['value'])) + def test_get_block(self) -> None: """Tests getting blocks and transactions. @@ -375,32 +517,13 @@ def test_get_block(self) -> None: 'decimals': 24, 'symbol': 'NEAR' }, - 'value': '999999999998180000000000000000000' + 'value': '1000000000000000000000000000000000' }, 'operation_identifier': { 'index': 0 }, 'status': 'SUCCESS', 'type': 'TRANSFER' - }, { - 'account': { - 'address': 'near', - 'sub_account': { - 'address': 'LIQUID_BALANCE_FOR_STORAGE' - } - }, - 'amount': { - 'currency': { - 'decimals': 24, - 'symbol': 'NEAR' - }, - 'value': '1820000000000000000000' - }, - 'operation_identifier': { - 'index': 1 - }, - 'status': 'SUCCESS', - 'type': 'TRANSFER' }, { 'account': { 'address': 'test0' @@ -413,7 +536,7 @@ def test_get_block(self) -> None: 'value': '950000000000000000000000000000000' }, 'operation_identifier': { - 'index': 2 + 'index': 1 }, 'status': 'SUCCESS', 'type': 'TRANSFER' @@ -432,7 +555,7 @@ def test_get_block(self) -> None: 'value': '50000000000000000000000000000000' }, 'operation_identifier': { - 'index': 3 + 'index': 2 }, 'status': 'SUCCESS', 'type': 'TRANSFER' @@ -567,7 +690,7 @@ def test_implicit_account(self) -> None: validator = self.node.validator_key implicit = key.Key.implicit_account() - ### 1. Create implicit account. + # 1. Create implicit account. logger.info(f'Creating implicit account: {implicit.account_id}') result = self.rosetta.transfer(src=validator, dst=implicit, @@ -666,31 +789,7 @@ def test_implicit_account(self) -> None: 'address': implicit.account_id, }, 'amount': { - 'value': '8180000000000000000000', - 'currency': { - 'symbol': 'NEAR', - 'decimals': 24 - } - } - }, { - 'operation_identifier': { - 'index': 1 - }, - 'type': 'TRANSFER', - 'status': 'SUCCESS', - 'metadata': { - 'predecessor_id': { - 'address': 'test0' - } - }, - 'account': { - 'address': implicit.account_id, - 'sub_account': { - 'address': 'LIQUID_BALANCE_FOR_STORAGE' - } - }, - 'amount': { - 'value': '1820000000000000000000', + 'value': '10000000000000000000000', 'currency': { 'symbol': 'NEAR', 'decimals': 24 @@ -738,7 +837,7 @@ def test_implicit_account(self) -> None: 'transaction_identifier': related.identifier }, related.transaction()) - ### 2. Delete the account. + # 2. Delete the account. logger.info(f'Deleting implicit account: {implicit.account_id}') result = self.rosetta.delete_account(implicit, refund_to=validator) @@ -808,32 +907,13 @@ def test_implicit_account(self) -> None: 'decimals': 24, 'symbol': 'NEAR' }, - 'value': '-8128890300000000000000' + 'value': '-9948890300000000000000' }, 'operation_identifier': { 'index': 0 }, 'status': 'SUCCESS', 'type': 'TRANSFER' - }, { - 'account': { - 'address': implicit.account_id, - 'sub_account': { - 'address': 'LIQUID_BALANCE_FOR_STORAGE' - } - }, - 'amount': { - 'currency': { - 'decimals': 24, - 'symbol': 'NEAR' - }, - 'value': '-1820000000000000000000' - }, - 'operation_identifier': { - 'index': 1 - }, - 'status': 'SUCCESS', - 'type': 'TRANSFER' }], 'related_transactions': [{ 'direction': 'forward', From e470c9af35e72bb232246c9f645a2b8c43f72e58 Mon Sep 17 00:00:00 2001 From: Andrei Kashin Date: Thu, 23 Mar 2023 11:05:48 +0000 Subject: [PATCH 3/4] feature: Limit compute usage of a chunk 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: - https://github.com/near/nearcore/issues/8859 - https://github.com/near/nearcore/issues/8860 --- chain/chain/src/test_utils/kv_runtime.rs | 1 + chain/chain/src/types.rs | 2 + .../primitives/src/runtime/parameter_table.rs | 4 + core/primitives/src/transaction.rs | 12 +- runtime/near-vm-logic/src/logic.rs | 8 +- runtime/runtime/Cargo.toml | 1 + runtime/runtime/src/actions.rs | 7 +- runtime/runtime/src/config.rs | 6 +- runtime/runtime/src/lib.rs | 211 +++++++++++++++--- tools/state-viewer/src/contract_accounts.rs | 1 + 10 files changed, 222 insertions(+), 31 deletions(-) diff --git a/chain/chain/src/test_utils/kv_runtime.rs b/chain/chain/src/test_utils/kv_runtime.rs index 20919c43eca..c7b136591e1 100644 --- a/chain/chain/src/test_utils/kv_runtime.rs +++ b/chain/chain/src/test_utils/kv_runtime.rs @@ -1183,6 +1183,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/types.rs b/chain/chain/src/types.rs index ad182530e29..bc4a5309c70 100644 --- a/chain/chain/src/types.rs +++ b/chain/chain/src/types.rs @@ -637,6 +637,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, @@ -649,6 +650,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/core/primitives/src/runtime/parameter_table.rs b/core/primitives/src/runtime/parameter_table.rs index 3cb95aeba44..5f65910ff1c 100644 --- a/core/primitives/src/runtime/parameter_table.rs +++ b/core/primitives/src/runtime/parameter_table.rs @@ -96,6 +96,10 @@ impl TryFrom<&ParameterValue> for ParameterCost { fn try_from(value: &ParameterValue) -> Result { match value { ParameterValue::ParameterCost { gas, compute } => { + if !cfg!(feature = "protocol_feature_compute_costs") { + assert_eq!(compute, gas, "Compute cost must match gas cost"); + } + Ok(ParameterCost { gas: *gas, compute: *compute }) } // If not specified, compute costs default to gas costs. diff --git a/core/primitives/src/transaction.rs b/core/primitives/src/transaction.rs index d32e6e40c68..8e175114ea7 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_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}; @@ -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", &pretty::Slice(&self.logs)) .field("receipt_ids", &pretty::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/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index 94a20d0fbe7..d26a16f4235 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/Cargo.toml b/runtime/runtime/Cargo.toml index 6306e2933e2..e29994b2212 100644 --- a/runtime/runtime/Cargo.toml +++ b/runtime/runtime/Cargo.toml @@ -34,6 +34,7 @@ near-vm-runner.workspace = true default = [] dump_errors_schema = ["near-vm-errors/dump_errors_schema"] protocol_feature_flat_state = ["near-store/protocol_feature_flat_state", "near-vm-logic/protocol_feature_flat_state"] +protocol_feature_compute_costs = ["near-primitives/protocol_feature_compute_costs"] nightly_protocol = ["near-primitives/nightly_protocol"] no_cpu_compatibility_checks = ["near-vm-runner/no_cpu_compatibility_checks"] 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 3a9147600f8..e4bc959a1e3 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,22 @@ 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"), + )?; + + // TODO(#8032): Remove when compute costs are stabilized. + if !cfg!(feature = "protocol_feature_compute_costs") { + assert_eq!( + total_compute_usage, total_gas_burnt, + "Compute usage must match burnt gas" + ); + } + outcomes.push(outcome_with_id); } @@ -1277,7 +1304,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 +1330,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"), + )?; + // TODO(#8032): Remove when compute costs are stabilized. + if !cfg!(feature = "protocol_feature_compute_costs") { + 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 +1360,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 +1376,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 +1409,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 +1433,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,6 +1594,8 @@ impl Runtime { #[cfg(test)] mod tests { + #[cfg(feature = "protocol_feature_compute_costs")] + use assert_matches::assert_matches; use near_crypto::{InMemorySigner, KeyType, Signer}; use near_primitives::account::AccessKey; use near_primitives::contract::ContractCode; @@ -1550,6 +1611,8 @@ mod tests { use near_store::set_access_key; use near_store::test_utils::create_tries; use near_store::StoreCompiledContractCache; + #[cfg(feature = "protocol_feature_compute_costs")] + use near_vm_logic::{ExtCosts, ParameterCost}; use near_vm_runner::get_contract_cache_key; use near_vm_runner::internal::VMKind; use testlib::runtime_utils::{alice_account, bob_account}; @@ -1562,15 +1625,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(), @@ -1579,7 +1642,7 @@ mod tests { input_data_ids: vec![], actions, }), - }] + } } #[test] @@ -2377,7 +2440,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( @@ -2423,7 +2486,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( @@ -2462,7 +2525,7 @@ mod tests { let actions = 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( @@ -2489,6 +2552,104 @@ mod tests { .expect("Compiled contract should be cached") .expect("Compilation result should be non-empty"); } + + #[cfg(feature = "protocol_feature_compute_costs")] + #[test] + fn test_compute_usage_limit() { + let initial_balance = to_yocto(1_000_000); + let initial_locked = to_yocto(500_000); + let (runtime, tries, root, mut apply_state, signer, epoch_info_provider) = + setup_runtime(initial_balance, initial_locked, 1); + + let mut free_config = RuntimeConfig::free(); + let sha256_cost = + ParameterCost { gas: Gas::from(1u64), 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: 1, + 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: 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(), + 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(_)); + }); + } } /// 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![]), From 3940988c85d6170e6b24c4b0025743fa47c5a500 Mon Sep 17 00:00:00 2001 From: Andrei Kashin Date: Tue, 4 Apr 2023 11:18:14 +0100 Subject: [PATCH 4/4] Run formatter --- core/primitives/src/transaction.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/primitives/src/transaction.rs b/core/primitives/src/transaction.rs index 8e175114ea7..a0b760cb719 100644 --- a/core/primitives/src/transaction.rs +++ b/core/primitives/src/transaction.rs @@ -418,7 +418,7 @@ pub struct ExecutionOutcome { 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. + // 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]