From bfbeb6183bb906cf430b36fb2f9754672d09814b Mon Sep 17 00:00:00 2001 From: Anton Puhach Date: Tue, 18 Feb 2025 21:06:59 +0100 Subject: [PATCH] impl --- core/chain-configs/src/test_genesis.rs | 2 +- core/primitives/src/test_utils.rs | 12 +- .../contract_distribution_cross_shard.rs | 90 ----- .../src/test_loop/tests/global_contracts.rs | 351 ++++++++++++++++++ integration-tests/src/test_loop/tests/mod.rs | 1 + .../src/test_loop/utils/transactions.rs | 51 --- 6 files changed, 359 insertions(+), 148 deletions(-) create mode 100644 integration-tests/src/test_loop/tests/global_contracts.rs diff --git a/core/chain-configs/src/test_genesis.rs b/core/chain-configs/src/test_genesis.rs index 2faa54c28c2..bb0b51b178a 100644 --- a/core/chain-configs/src/test_genesis.rs +++ b/core/chain-configs/src/test_genesis.rs @@ -379,7 +379,7 @@ impl TestGenesisBuilder { pub fn add_user_accounts_simple( mut self, - accounts: &Vec, + accounts: &[AccountId], initial_balance: Balance, ) -> Self { for account_id in accounts { diff --git a/core/primitives/src/test_utils.rs b/core/primitives/src/test_utils.rs index 1fe13786621..78c31923f1c 100644 --- a/core/primitives/src/test_utils.rs +++ b/core/primitives/src/test_utils.rs @@ -279,14 +279,14 @@ impl SignedTransaction { pub fn deploy_global_contract( nonce: Nonce, - contract_id: AccountId, + account_id: AccountId, code: Vec, signer: &Signer, block_hash: CryptoHash, deploy_mode: GlobalContractDeployMode, ) -> SignedTransaction { - let signer_id = contract_id.clone(); - let receiver_id = contract_id; + let signer_id = account_id.clone(); + let receiver_id = account_id; SignedTransaction::from_actions( nonce, signer_id, @@ -303,13 +303,13 @@ impl SignedTransaction { pub fn use_global_contract( nonce: Nonce, - contract_id: &AccountId, + account_id: &AccountId, signer: &Signer, block_hash: CryptoHash, contract_identifier: GlobalContractIdentifier, ) -> SignedTransaction { - let signer_id = contract_id.clone(); - let receiver_id = contract_id.clone(); + let signer_id = account_id.clone(); + let receiver_id = account_id.clone(); SignedTransaction::from_actions( nonce, signer_id, diff --git a/integration-tests/src/test_loop/tests/contract_distribution_cross_shard.rs b/integration-tests/src/test_loop/tests/contract_distribution_cross_shard.rs index 0de6d67ad95..af89ff790f8 100644 --- a/integration-tests/src/test_loop/tests/contract_distribution_cross_shard.rs +++ b/integration-tests/src/test_loop/tests/contract_distribution_cross_shard.rs @@ -4,8 +4,6 @@ use near_chain_configs::test_genesis::{ build_genesis_and_epoch_config_store, GenesisAndEpochConfigParams, ValidatorsSpec, }; use near_o11y::testonly::init_test_logger; -use near_primitives::action::{GlobalContractDeployMode, GlobalContractIdentifier}; -use near_primitives::hash::CryptoHash; use near_primitives::shard_layout::ShardLayout; use near_primitives::types::AccountId; use near_primitives::version::PROTOCOL_VERSION; @@ -21,7 +19,6 @@ use crate::test_loop::utils::get_head_height; use crate::test_loop::utils::transactions::{ call_contract, check_txs, deploy_contract, make_accounts, }; -use crate::test_loop::utils::transactions::{deploy_global_contract, use_global_contract}; const EPOCH_LENGTH: u64 = 10; const GENESIS_HEIGHT: u64 = 1000; @@ -77,93 +74,6 @@ fn test_contract_distribution_cross_shard() { env.shutdown_and_drain_remaining_events(Duration::seconds(20)); } -#[cfg_attr(not(feature = "nightly_protocol"), ignore)] -#[test] -fn test_global_contract_by_hash() { - let (env, accounts, contract, rpc_id) = setup_global_contract_test(); - let deploy_mode = GlobalContractDeployMode::CodeHash; - test_global_contract(env, deploy_mode, accounts.as_slice(), &contract, rpc_id); -} - -#[cfg_attr(not(feature = "nightly_protocol"), ignore)] -#[test] -fn test_global_contract_by_account_id() { - let (env, accounts, contract, rpc_id) = setup_global_contract_test(); - let deploy_mode = GlobalContractDeployMode::AccountId; - test_global_contract(env, deploy_mode, accounts.as_slice(), &contract, rpc_id); -} - -fn setup_global_contract_test() -> (TestLoopEnv, Vec, ContractCode, AccountId) { - init_test_logger(); - let accounts = make_accounts(NUM_ACCOUNTS); - - let (env, rpc_id) = setup(&accounts); - - let rpc_index = 8; - assert_eq!(accounts[rpc_index], rpc_id); - - let contract = ContractCode::new(near_test_contracts::rs_contract().to_vec(), None); - (env, accounts, contract, rpc_id) -} - -fn test_global_contract( - mut env: TestLoopEnv, - deploy_mode: GlobalContractDeployMode, - accounts: &[AccountId], - contract: &ContractCode, - rpc_id: AccountId, -) { - let mut nonce = 1; - let deploy_tx = deploy_global_contract( - &mut env.test_loop, - &env.datas, - &rpc_id, - &accounts[0], - contract.code().into(), - deploy_mode.clone(), - nonce, - ); - nonce += 1; - env.test_loop.run_for(Duration::seconds(3)); - check_txs(&env.test_loop.data, &env.datas, &rpc_id, &[deploy_tx]); - let identifier = match deploy_mode { - GlobalContractDeployMode::CodeHash => { - let code_hash = CryptoHash::hash_bytes(contract.code()); - GlobalContractIdentifier::CodeHash(code_hash) - } - GlobalContractDeployMode::AccountId => { - GlobalContractIdentifier::AccountId(accounts[0].clone()) - } - }; - // test on accounts from different shards - for account in [&accounts[1], &accounts[6]] { - let use_tx = use_global_contract( - &mut env.test_loop, - &env.datas, - &rpc_id, - account, - identifier.clone(), - nonce, - ); - nonce += 1; - env.test_loop.run_for(Duration::seconds(3)); - check_txs(&env.test_loop.data, &env.datas, &rpc_id, &[use_tx]); - let call_tx = call_contract( - &mut env.test_loop, - &env.datas, - &rpc_id, - account, - account, - "log_something".to_owned(), - vec![], - nonce, - ); - env.test_loop.run_for(Duration::seconds(3)); - check_txs(&env.test_loop.data, &env.datas, &rpc_id, &[call_tx]); - } - env.shutdown_and_drain_remaining_events(Duration::seconds(20)); -} - fn setup(accounts: &Vec) -> (TestLoopEnv, AccountId) { let builder = TestLoopBuilder::new(); diff --git a/integration-tests/src/test_loop/tests/global_contracts.rs b/integration-tests/src/test_loop/tests/global_contracts.rs new file mode 100644 index 00000000000..e61277be920 --- /dev/null +++ b/integration-tests/src/test_loop/tests/global_contracts.rs @@ -0,0 +1,351 @@ +use std::collections::BTreeMap; +use std::sync::Arc; + +use assert_matches::assert_matches; +use near_async::time::Duration; +use near_chain_configs::test_genesis::{ + TestEpochConfigBuilder, TestGenesisBuilder, ValidatorsSpec, +}; +use near_client::test_utils::test_loop::ClientQueries; +use near_client::Client; +use near_o11y::testonly::init_test_logger; +use near_parameters::{ActionCosts, RuntimeConfigStore}; +use near_primitives::action::{GlobalContractDeployMode, GlobalContractIdentifier}; +use near_primitives::epoch_manager::EpochConfigStore; +use near_primitives::errors::{ActionError, ActionErrorKind, TxExecutionError}; +use near_primitives::hash::CryptoHash; +use near_primitives::shard_layout::ShardLayout; +use near_primitives::test_utils::create_user_test_signer; +use near_primitives::transaction::SignedTransaction; +use near_primitives::types::{AccountId, Balance, StorageUsage}; +use near_primitives::version::PROTOCOL_VERSION; +use near_primitives::views::{ + AccountView, FinalExecutionOutcomeView, FinalExecutionStatus, QueryRequest, QueryResponseKind, +}; +use near_vm_runner::ContractCode; + +use crate::test_loop::builder::TestLoopBuilder; +use crate::test_loop::env::TestLoopEnv; +use crate::test_loop::utils::transactions::{self}; +use crate::test_loop::utils::{ONE_NEAR, TGAS}; + +const GAS_PRICE: Balance = 1; + +#[cfg_attr(not(feature = "nightly_protocol"), ignore)] +#[test] +fn test_global_contract_by_hash() { + test_deploy_and_call_global_contract(GlobalContractDeployMode::CodeHash); +} + +#[cfg_attr(not(feature = "nightly_protocol"), ignore)] +#[test] +fn test_global_contract_by_account_id() { + test_deploy_and_call_global_contract(GlobalContractDeployMode::AccountId); +} + +#[cfg_attr(not(feature = "nightly_protocol"), ignore)] +#[test] +fn test_global_contract_deploy_insufficient_balance_for_storage() { + let mut env = GlobalContractsTestEnv::setup(ONE_NEAR); + + let tx = env.deploy_global_contract_tx(GlobalContractDeployMode::CodeHash); + let outcome = env.execute_tx(tx); + assert_matches!( + outcome.status, + FinalExecutionStatus::Failure(TxExecutionError::ActionError(ActionError { + kind: ActionErrorKind::LackBalanceForState { .. }, + index: _ + })) + ); + + env.shutdown(); +} + +#[cfg_attr(not(feature = "nightly_protocol"), ignore)] +#[test] +fn test_use_non_existent_global_contract() { + let mut env = GlobalContractsTestEnv::setup(ONE_NEAR); + + let identifier = env.global_contract_identifier(&GlobalContractDeployMode::CodeHash); + let tx = env.use_global_contract_tx(&env.account_shard_0.clone(), identifier); + let outcome = env.execute_tx(tx); + assert_matches!( + outcome.status, + FinalExecutionStatus::Failure(TxExecutionError::ActionError(ActionError { + kind: ActionErrorKind::GlobalContractDoesNotExist { .. }, + index: _ + })) + ); + + env.shutdown(); +} + +fn test_deploy_and_call_global_contract(deploy_mode: GlobalContractDeployMode) { + const INITIAL_BALANCE: Balance = 1000 * ONE_NEAR; + let mut env = GlobalContractsTestEnv::setup(INITIAL_BALANCE); + + env.deploy_global_contract(deploy_mode.clone()); + let deploy_cost = INITIAL_BALANCE - env.get_account_state(env.deploy_account.clone()).amount; + assert_eq!(deploy_cost, env.deploy_global_contract_cost()); + + for account in [env.account_shard_0.clone(), env.account_shard_1.clone()] { + let identifier = env.global_contract_identifier(&deploy_mode); + let baseline_storage_usage = env.get_account_state(account.clone()).storage_usage; + + env.use_global_contract(&account, identifier.clone()); + let account_state = env.get_account_state(account.clone()); + let use_cost = INITIAL_BALANCE - account_state.amount; + assert_eq!(use_cost, env.use_global_contract_cost(&identifier)); + assert_eq!( + account_state.storage_usage, + baseline_storage_usage + identifier.len() as StorageUsage + ); + + env.call_global_contract(&account); + + // Deploy regular contract to check if storage usage is updated correctly + env.deploy_regular_contract(&account); + let account_state = env.get_account_state(account.clone()); + assert_eq!( + account_state.storage_usage, + baseline_storage_usage + env.contract.code().len() as StorageUsage + ); + } + + env.shutdown(); +} + +struct GlobalContractsTestEnv { + env: TestLoopEnv, + runtime_config_store: RuntimeConfigStore, + contract: ContractCode, + deploy_account: AccountId, + account_shard_0: AccountId, + account_shard_1: AccountId, + rpc: AccountId, + nonce: u64, +} + +impl GlobalContractsTestEnv { + fn setup(initial_balance: Balance) -> Self { + init_test_logger(); + + let [account_shard_0, account_shard_1, deploy_account, rpc] = + ["account0", "account2", "account", "rpc"].map(|acc| acc.parse::().unwrap()); + + let shard_layout = ShardLayout::simple_v1(&["account1"]); + let block_and_chunk_producers = ["cp0", "cp1"]; + let chunk_validators_only = ["cv0", "cv1"]; + let validators_spec = + ValidatorsSpec::desired_roles(&block_and_chunk_producers, &chunk_validators_only); + + let genesis = TestGenesisBuilder::new() + .genesis_time_from_clock(&near_async::time::FakeClock::default().clock()) + .validators_spec(validators_spec.clone()) + .shard_layout(shard_layout.clone()) + .add_user_accounts_simple( + &[account_shard_0.clone(), account_shard_1.clone(), deploy_account.clone()], + initial_balance, + ) + .gas_prices(GAS_PRICE, GAS_PRICE) + .build(); + let epoch_config = TestEpochConfigBuilder::new() + .shard_layout(shard_layout) + .validators_spec(validators_spec) + .build(); + let epoch_config_store = EpochConfigStore::test(BTreeMap::from([( + genesis.config.protocol_version, + Arc::new(epoch_config), + )])); + + let clients = block_and_chunk_producers + .iter() + .chain(chunk_validators_only.iter()) + .map(|acc| acc.parse().unwrap()) + .chain(std::iter::once(rpc.clone())) + .collect(); + let runtime_config_store = RuntimeConfigStore::new(None); + let env = TestLoopBuilder::new() + .genesis(genesis) + .clients(clients) + .epoch_config_store(epoch_config_store) + .runtime_config_store(runtime_config_store.clone()) + .build(); + let contract = ContractCode::new(near_test_contracts::rs_contract().to_vec(), None); + + Self { + env, + runtime_config_store, + account_shard_0, + account_shard_1, + deploy_account, + contract, + rpc, + nonce: 1, + } + } + + fn deploy_global_contract_tx( + &mut self, + deploy_mode: GlobalContractDeployMode, + ) -> SignedTransaction { + SignedTransaction::deploy_global_contract( + self.next_nonce(), + self.deploy_account.clone(), + self.contract.code().to_vec(), + &create_user_test_signer(&self.deploy_account), + self.get_tx_block_hash(), + deploy_mode, + ) + } + + fn deploy_global_contract(&mut self, deploy_mode: GlobalContractDeployMode) { + let tx = self.deploy_global_contract_tx(deploy_mode); + self.run_tx(tx); + } + + fn deploy_regular_contract(&mut self, account: &AccountId) { + let tx = SignedTransaction::deploy_contract( + self.next_nonce(), + &account, + self.contract.code().to_vec(), + &create_user_test_signer(&account), + self.get_tx_block_hash(), + ); + self.run_tx(tx); + } + + fn use_global_contract_tx( + &mut self, + account: &AccountId, + identifier: GlobalContractIdentifier, + ) -> SignedTransaction { + SignedTransaction::use_global_contract( + self.next_nonce(), + &account, + &create_user_test_signer(&account), + self.get_tx_block_hash(), + identifier, + ) + } + + fn use_global_contract(&mut self, account: &AccountId, identifier: GlobalContractIdentifier) { + let tx = self.use_global_contract_tx(account, identifier); + self.run_tx(tx); + } + + fn call_global_contract(&mut self, account: &AccountId) { + let tx = SignedTransaction::call( + self.next_nonce(), + account.clone(), + account.clone(), + &create_user_test_signer(account), + 0, + "log_something".to_owned(), + vec![], + 300 * TGAS, + self.get_tx_block_hash(), + ); + self.run_tx(tx); + } + + fn deploy_global_contract_cost(&self) -> Balance { + let contract_size = self.contract.code().len(); + let runtime_config = self.runtime_config_store.get_config(PROTOCOL_VERSION); + let fees = &runtime_config.fees; + let gas_fees = fees.action_fees[ActionCosts::new_action_receipt].send_fee(true) + + fees.action_fees[ActionCosts::deploy_global_contract_base].send_fee(true) + + fees.action_fees[ActionCosts::deploy_global_contract_byte].send_fee(true) + * contract_size as u64 + + fees.action_fees[ActionCosts::new_action_receipt].exec_fee() + + fees.action_fees[ActionCosts::deploy_global_contract_base].exec_fee() + + fees.action_fees[ActionCosts::deploy_global_contract_byte].exec_fee() + * contract_size as u64; + let storage_cost = + runtime_config.fees.storage_usage_config.global_contract_storage_amount_per_byte + * contract_size as u128; + (gas_fees as Balance) * GAS_PRICE + storage_cost + } + + fn use_global_contract_cost(&self, identifier: &GlobalContractIdentifier) -> Balance { + let identifier_len = identifier.len(); + let runtime_config = self.runtime_config_store.get_config(PROTOCOL_VERSION); + let fees = &runtime_config.fees; + let gas_fees = fees.action_fees[ActionCosts::new_action_receipt].send_fee(true) + + fees.action_fees[ActionCosts::use_global_contract_base].send_fee(true) + + fees.action_fees[ActionCosts::use_global_contract_byte].send_fee(true) + * identifier_len as u64 + + fees.action_fees[ActionCosts::new_action_receipt].exec_fee() + + fees.action_fees[ActionCosts::use_global_contract_base].exec_fee() + + fees.action_fees[ActionCosts::use_global_contract_byte].exec_fee() + * identifier_len as u64; + let _storage_cost = runtime_config.fees.storage_usage_config.storage_amount_per_byte + * identifier_len as u128; + (gas_fees as Balance) * GAS_PRICE + } + + fn get_account_state(&mut self, account: AccountId) -> AccountView { + // Need to wait a bit for RPC node to catch up with the results + // of previously submitted txs + self.env.test_loop.run_for(Duration::seconds(2)); + let clients: Vec<&Client> = self + .env + .datas + .iter() + .map(|data| &self.env.test_loop.data.get(&data.client_sender.actor_handle()).client) + .collect(); + let response = clients + .runtime_query(&account, QueryRequest::ViewAccount { account_id: account.clone() }); + let QueryResponseKind::ViewAccount(account_view) = response.kind else { unreachable!() }; + account_view + } + + fn next_nonce(&mut self) -> u64 { + let ret = self.nonce; + self.nonce += 1; + ret + } + + fn get_tx_block_hash(&self) -> CryptoHash { + transactions::get_shared_block_hash(&self.env.datas, &self.env.test_loop.data) + } + + fn execute_tx(&mut self, tx: SignedTransaction) -> FinalExecutionOutcomeView { + transactions::execute_tx( + &mut self.env.test_loop, + &self.rpc, + tx, + &self.env.datas, + Duration::seconds(5), + ) + .unwrap() + } + + fn run_tx(&mut self, tx: SignedTransaction) { + transactions::run_tx( + &mut self.env.test_loop, + &self.rpc, + tx, + &self.env.datas, + Duration::seconds(5), + ); + } + + fn global_contract_identifier( + &self, + deploy_mode: &GlobalContractDeployMode, + ) -> GlobalContractIdentifier { + match deploy_mode { + GlobalContractDeployMode::CodeHash => { + GlobalContractIdentifier::CodeHash(*self.contract.hash()) + } + GlobalContractDeployMode::AccountId => { + GlobalContractIdentifier::AccountId(self.deploy_account.clone()) + } + } + } + + fn shutdown(self) { + self.env.shutdown_and_drain_remaining_events(Duration::seconds(10)); + } +} diff --git a/integration-tests/src/test_loop/tests/mod.rs b/integration-tests/src/test_loop/tests/mod.rs index b782c1d5098..bcdfb85c3ce 100644 --- a/integration-tests/src/test_loop/tests/mod.rs +++ b/integration-tests/src/test_loop/tests/mod.rs @@ -10,6 +10,7 @@ mod epoch_sync; mod fix_chunk_producer_stake_threshold; mod fix_min_stake_ratio; mod fix_stake_threshold; +mod global_contracts; mod in_memory_tries; mod malicious_chunk_producer; mod max_receipt_size; diff --git a/integration-tests/src/test_loop/utils/transactions.rs b/integration-tests/src/test_loop/utils/transactions.rs index 57b61f04a51..0a652adc167 100644 --- a/integration-tests/src/test_loop/utils/transactions.rs +++ b/integration-tests/src/test_loop/utils/transactions.rs @@ -13,7 +13,6 @@ use near_client::test_utils::test_loop::ClientQueries; use near_client::{Client, ProcessTxResponse}; use near_crypto::Signer; use near_network::client::ProcessTxRequest; -use near_primitives::action::{GlobalContractDeployMode, GlobalContractIdentifier}; use near_primitives::block::Tip; use near_primitives::errors::InvalidTxError; use near_primitives::hash::CryptoHash; @@ -304,56 +303,6 @@ pub fn deploy_contract( tx_hash } -pub fn deploy_global_contract( - test_loop: &mut TestLoopV2, - node_datas: &[TestData], - rpc_id: &AccountId, - account_id: &AccountId, - code: Vec, - deploy_mode: GlobalContractDeployMode, - nonce: u64, -) -> CryptoHash { - let block_hash = get_shared_block_hash(node_datas, &test_loop.data); - let signer = create_user_test_signer(&account_id); - - let tx = SignedTransaction::deploy_global_contract( - nonce, - account_id.clone(), - code, - &signer, - block_hash, - deploy_mode, - ); - let tx_hash = tx.get_hash(); - submit_tx(node_datas, rpc_id, tx); - - tx_hash -} - -pub fn use_global_contract( - test_loop: &mut TestLoopV2, - node_datas: &[TestData], - rpc_id: &AccountId, - account_id: &AccountId, - global_contract_identifier: GlobalContractIdentifier, - nonce: u64, -) -> CryptoHash { - let block_hash = get_shared_block_hash(node_datas, &test_loop.data); - let signer = create_user_test_signer(&account_id); - - let tx = SignedTransaction::use_global_contract( - nonce, - account_id, - &signer, - block_hash, - global_contract_identifier, - ); - let tx_hash = tx.get_hash(); - submit_tx(node_datas, rpc_id, tx); - - tx_hash -} - /// Call the contract deployed at contract id from the sender id. /// /// This function does not wait until the transactions is executed.