diff --git a/runtime/runtime-params-estimator/src/action_costs.rs b/runtime/runtime-params-estimator/src/action_costs.rs new file mode 100644 index 00000000000..d89c0da2b03 --- /dev/null +++ b/runtime/runtime-params-estimator/src/action_costs.rs @@ -0,0 +1,625 @@ +//! Estimation functions for action costs, separated by send and exec. +//! +//! Estimations in this module are more tailored towards specific gas parameters +//! compared to those in the parent module. But the estimations here potential +//! miss some overhead that is outside action verification and outside action +//! application. But in combination with the wholistic action cost estimation, +//! the picture should be fairly complete. + +use crate::estimator_context::{EstimatorContext, Testbed}; +use crate::gas_cost::GasCost; +use crate::transaction_builder::AccountRequirement; +use crate::utils::average_cost; +use near_crypto::{KeyType, PublicKey}; +use near_primitives::account::{AccessKey, AccessKeyPermission, FunctionCallPermission}; +use near_primitives::hash::CryptoHash; +use near_primitives::receipt::{ActionReceipt, Receipt}; +use near_primitives::transaction::{Action, ExecutionStatus}; +use near_primitives::types::AccountId; +use std::iter; + +/// A builder object for constructing action cost estimations. +/// +/// This modules uses `ActionEstimation` as a builder object to specify the +/// details of each action estimation. For example, creating an account has the +/// requirement that the account does not exist, yet. But for a staking action, +/// it must exist. The builder object makes it easy to specify these +/// requirements separately for each estimation, with only a small amount of +/// boiler-plate code repeated. +/// +/// Besides account id requirements, the builder also accepts a few other +/// settings. Most importantly, a vector of actions. This is what ultimately +/// defines the workload to be executed. +/// +/// Once `ActionEstimation` is complete, call either `verify_cost` or +/// `apply_cost` to receive just the execution cost or just the sender cost. +/// This will run a loop internally that spawns a bunch of actions using +/// different accounts. This allows to average the cost of a number of runs to +/// make the result more stable. +/// +/// By default, the inner actions are also multiplied within a receipt. This is +/// to reduce the overhead noise of the receipt cost, which can often dominate +/// compared to the cost of a single action inside. The only problem is that all +/// actions inside a receipt must share the receiver and the sender account ids. +/// This makes action duplication unsuitable for actions that cannot be +/// repeated, such as creating or deleting an account. In those cases, set inner +/// iterations to 1. +struct ActionEstimation { + /// generate account ids from the transaction builder with requirements + signer: AccountRequirement, + predecessor: AccountRequirement, + receiver: AccountRequirement, + /// the actions to estimate + actions: Vec, + /// how often actions are repeated in a receipt + inner_iters: usize, + /// how many receipts to measure + outer_iters: usize, + /// how many iterations to ignore for measurements + warmup: usize, + /// the gas metric to measure + metric: crate::config::GasMetric, + /// subtract the cost of an empty receipt from the measured cost + /// (`fasle` is only really useful for action receipt creation cost) + subtract_base: bool, +} + +impl ActionEstimation { + fn new(ctx: &mut EstimatorContext) -> Self { + Self { + signer: AccountRequirement::RandomUnused, + predecessor: AccountRequirement::RandomUnused, + receiver: AccountRequirement::RandomUnused, + actions: vec![], + inner_iters: 100, + outer_iters: ctx.config.iter_per_block, + warmup: ctx.config.warmup_iters_per_block, + metric: ctx.config.metric, + subtract_base: true, + } + } + + fn predecessor(mut self, predecessor: AccountRequirement) -> Self { + self.predecessor = predecessor; + self + } + + fn receiver(mut self, receiver: AccountRequirement) -> Self { + self.receiver = receiver; + self + } + + fn add_action(mut self, action: Action) -> Self { + self.actions.push(action); + self + } + + fn inner_iters(mut self, inner_iters: usize) -> Self { + self.inner_iters = inner_iters; + self + } + + fn subtract_base(mut self, yes: bool) -> Self { + self.subtract_base = yes; + self + } + + /// Estimate the gas cost for converting an action in a transaction to one in an + /// action receipt, without network costs. + /// + /// To convert a transaction into a receipt, each action has to be verified. + /// This happens on a different shard than the action execution and should + /// therefore be estimated and charged separately. + /// + /// Network costs should also be taken into account here but we don't do that, + /// yet. + #[track_caller] + fn verify_cost(&self, testbed: &mut Testbed) -> GasCost { + self.estimate_average_cost(testbed, Self::verify_actions_cost) + } + + /// Estimate the cost for executing the actions in the builder. + /// + /// This is the "apply" cost only, without validation, without sending and + /// without overhead that does not scale with the number of actions. + #[track_caller] + fn apply_cost(&self, testbed: &mut Testbed) -> GasCost { + self.estimate_average_cost(testbed, Self::apply_actions_cost) + } + + /// Estimate the cost of verifying a set of actions once. + #[track_caller] + fn verify_actions_cost(&self, testbed: &mut Testbed, actions: Vec) -> GasCost { + let tb = testbed.transaction_builder(); + let signer_id = tb.account_by_requirement(self.signer, None); + let predecessor_id = tb.account_by_requirement(self.predecessor, Some(&signer_id)); + let receiver_id = tb.account_by_requirement(self.receiver, Some(&signer_id)); + let tx = tb.transaction_from_actions(predecessor_id, receiver_id, actions); + let clock = GasCost::measure(self.metric); + testbed.verify_transaction(&tx).expect("tx verification should not fail in estimator"); + clock.elapsed() + } + + /// Estimate the cost of applying a set of actions once. + #[track_caller] + fn apply_actions_cost(&self, testbed: &mut Testbed, actions: Vec) -> GasCost { + let tb = testbed.transaction_builder(); + + let signer_id = tb.account_by_requirement(self.signer, None); + let predecessor_id = tb.account_by_requirement(self.predecessor, Some(&signer_id)); + let receiver_id = tb.account_by_requirement(self.receiver, Some(&signer_id)); + let signer_public_key = PublicKey::from_seed(KeyType::ED25519, &signer_id); + + let action_receipt = ActionReceipt { + signer_id, + signer_public_key, + gas_price: 100_000_000, + output_data_receivers: vec![], + input_data_ids: vec![], + actions, + }; + let receipt = Receipt { + predecessor_id, + receiver_id, + receipt_id: CryptoHash::new(), + receipt: near_primitives::receipt::ReceiptEnum::Action(action_receipt), + }; + let clock = GasCost::measure(self.metric); + let outcome = testbed.apply_action_receipt(&receipt); + let gas = clock.elapsed(); + match outcome.status { + ExecutionStatus::Unknown => panic!("receipt not applied"), + ExecutionStatus::Failure(err) => panic!("failed apply, {err:?}"), + ExecutionStatus::SuccessValue(_) | ExecutionStatus::SuccessReceiptId(_) => (), + } + gas + } + + /// Take a function that executes a list of actions on a testbed, execute + /// and measure it multiple times and return the average cost. + #[track_caller] + fn estimate_average_cost( + &self, + testbed: &mut Testbed, + estimated_fn: fn(&Self, &mut Testbed, Vec) -> GasCost, + ) -> GasCost { + let num_total_actions = self.actions.len() * self.inner_iters; + let actions: Vec = + self.actions.iter().cloned().cycle().take(num_total_actions).collect(); + + let gas_results = iter::repeat_with(|| estimated_fn(self, testbed, actions.clone())) + .skip(self.warmup) + .take(self.outer_iters) + .collect(); + + // This could be cached for efficiency. But experience so far shows that + // reusing caches values for many future estimations leads to the + // problem that a single "HIGH-VARIANCE" uncertain estimation can spoil + // all following estimations. In this case, rerunning is cheap and it + // ensures the base is computed in a very similar state of the machine as + // the measurement it is subtracted from. + let base = + if self.subtract_base { estimated_fn(self, testbed, vec![]) } else { GasCost::zero() }; + + let cost_per_tx = average_cost(gas_results); + (cost_per_tx - base) / self.inner_iters as u64 + } +} + +pub(crate) fn create_account_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(create_account_action()) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SubOfSigner) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn create_account_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(create_account_action()) + .receiver(AccountRequirement::SubOfSigner) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn create_account_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(create_account_action()) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SubOfSigner) + .inner_iters(1) // creating account works only once in a receipt + .add_action(create_transfer_action()) // must have balance for storage + .apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn delete_account_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(delete_account_action()) + .inner_iters(1) // only one account deletion possible + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn delete_account_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(delete_account_action()) + .inner_iters(1) // only one account deletion possible + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn delete_account_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(delete_account_action()) + .inner_iters(1) // only one account deletion possible + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn deploy_contract_base_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(deploy_action(ActionSize::Min)) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn deploy_contract_base_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(deploy_action(ActionSize::Min)) + .verify_cost(&mut ctx.testbed()) +} + +/// Note: This is not the best estimation because a dummy contract is clearly +/// not the worst-case scenario for gas costs. +pub(crate) fn deploy_contract_base_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(deploy_action(ActionSize::Min)) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn deploy_contract_byte_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(deploy_action(ActionSize::Max)) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .inner_iters(1) // circumvent TX size limit + .verify_cost(&mut ctx.testbed()) + / ActionSize::Max.deploy_contract() +} + +pub(crate) fn deploy_contract_byte_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(deploy_action(ActionSize::Max)) + .inner_iters(1) // circumvent TX size limit + .verify_cost(&mut ctx.testbed()) + / ActionSize::Max.deploy_contract() +} + +pub(crate) fn deploy_contract_byte_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(deploy_action(ActionSize::Max)) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .inner_iters(1) // circumvent TX size limit + .apply_cost(&mut ctx.testbed()) + / ActionSize::Max.deploy_contract() +} + +pub(crate) fn function_call_base_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(function_call_action(ActionSize::Min)) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn function_call_base_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(function_call_action(ActionSize::Min)) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn function_call_base_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(function_call_action(ActionSize::Min)) + .apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn function_call_byte_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(function_call_action(ActionSize::Max)) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .verify_cost(&mut ctx.testbed()) + / ActionSize::Max.function_call_payload() +} + +pub(crate) fn function_call_byte_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(function_call_action(ActionSize::Max)) + .verify_cost(&mut ctx.testbed()) + / ActionSize::Max.function_call_payload() +} + +pub(crate) fn function_call_byte_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(function_call_action(ActionSize::Max)) + .apply_cost(&mut ctx.testbed()) + / ActionSize::Max.function_call_payload() +} + +pub(crate) fn transfer_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(transfer_action()) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn transfer_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx).add_action(transfer_action()).verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn transfer_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx).add_action(transfer_action()).apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn stake_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(stake_action()) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn stake_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(stake_action()) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::RandomUnused) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn stake_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(stake_action()) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) // staking must be local + .apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn add_full_access_key_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(add_full_access_key_action()) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn add_full_access_key_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(add_full_access_key_action()) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn add_full_access_key_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(add_full_access_key_action()) + .inner_iters(1) // adding the same key a second time would fail + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn add_function_call_key_base_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(add_fn_access_key_action(ActionSize::Min)) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn add_function_call_key_base_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(add_fn_access_key_action(ActionSize::Min)) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn add_function_call_key_base_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(add_fn_access_key_action(ActionSize::Min)) + .inner_iters(1) // adding the same key a second time would fail + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .apply_cost(&mut ctx.testbed()) +} + +pub(crate) fn add_function_call_key_byte_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(add_fn_access_key_action(ActionSize::Max)) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .verify_cost(&mut ctx.testbed()) + / ActionSize::Max.key_methods_list() +} + +pub(crate) fn add_function_call_key_byte_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(add_fn_access_key_action(ActionSize::Max)) + .verify_cost(&mut ctx.testbed()) + / ActionSize::Max.key_methods_list() +} + +pub(crate) fn add_function_call_key_byte_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(add_fn_access_key_action(ActionSize::Max)) + .inner_iters(1) // adding the same key a second time would fail + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .apply_cost(&mut ctx.testbed()) + / ActionSize::Max.key_methods_list() +} + +pub(crate) fn delete_key_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .add_action(delete_key_action()) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn delete_key_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx).add_action(delete_key_action()).verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn delete_key_exec(ctx: &mut EstimatorContext) -> GasCost { + // Cannot delete a key without creating it first. Therefore, compute cost of + // (create) and of (create + delete) and return the difference. + let base = ActionEstimation::new(ctx) + .add_action(add_fn_access_key_action(ActionSize::Max)) + .predecessor(AccountRequirement::SameAsSigner) + .inner_iters(1) + .receiver(AccountRequirement::SameAsSigner) + .apply_cost(&mut ctx.testbed()); + + let total = ActionEstimation::new(ctx) + .add_action(add_fn_access_key_action(ActionSize::Max)) + .add_action(delete_key_action()) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .apply_cost(&mut ctx.testbed()); + + total - base +} + +pub(crate) fn new_action_receipt_send_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx) + .subtract_base(false) + .predecessor(AccountRequirement::SameAsSigner) + .receiver(AccountRequirement::SameAsSigner) + .verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn new_action_receipt_send_not_sir(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx).subtract_base(false).verify_cost(&mut ctx.testbed()) +} + +pub(crate) fn new_action_receipt_exec(ctx: &mut EstimatorContext) -> GasCost { + ActionEstimation::new(ctx).subtract_base(false).apply_cost(&mut ctx.testbed()) +} + +fn create_account_action() -> Action { + Action::CreateAccount(near_primitives::transaction::CreateAccountAction {}) +} + +fn create_transfer_action() -> Action { + Action::Transfer(near_primitives::transaction::TransferAction { deposit: 10u128.pow(24) }) +} + +fn stake_action() -> Action { + Action::Stake(near_primitives::transaction::StakeAction { + stake: 5u128.pow(28), // some arbitrary positive number + public_key: PublicKey::from_seed(KeyType::ED25519, "seed"), + }) +} + +fn delete_account_action() -> Action { + Action::DeleteAccount(near_primitives::transaction::DeleteAccountAction { + beneficiary_id: "bob.near".parse().unwrap(), + }) +} + +fn deploy_action(size: ActionSize) -> Action { + Action::DeployContract(near_primitives::transaction::DeployContractAction { + code: near_test_contracts::sized_contract(size.deploy_contract() as usize), + }) +} + +fn add_full_access_key_action() -> Action { + Action::AddKey(near_primitives::transaction::AddKeyAction { + public_key: PublicKey::from_seed(KeyType::ED25519, "full-access-key-seed"), + access_key: AccessKey { nonce: 0, permission: AccessKeyPermission::FullAccess }, + }) +} + +fn add_fn_access_key_action(size: ActionSize) -> Action { + // 3 bytes for "foo" and one for an implicit separator + let method_names = vec!["foo".to_owned(); size.key_methods_list() as usize / 4]; + // This is charged flat, therefore it should always be max len. + let receiver_id = "a".repeat(AccountId::MAX_LEN).parse().unwrap(); + Action::AddKey(near_primitives::transaction::AddKeyAction { + public_key: PublicKey::from_seed(KeyType::ED25519, "seed"), + access_key: AccessKey { + nonce: 0, + permission: AccessKeyPermission::FunctionCall(FunctionCallPermission { + allowance: Some(1), + receiver_id, + method_names, + }), + }, + }) +} + +fn delete_key_action() -> Action { + Action::DeleteKey(near_primitives::transaction::DeleteKeyAction { + public_key: PublicKey::from_seed(KeyType::ED25519, "seed"), + }) +} + +fn transfer_action() -> Action { + Action::Transfer(near_primitives::transaction::TransferAction { deposit: 77 }) +} + +fn function_call_action(size: ActionSize) -> Action { + let total_size = size.function_call_payload(); + let method_len = 4.min(total_size) as usize; + let method_name: String = "noop".chars().take(method_len).collect(); + let arg_len = total_size as usize - method_len; + Action::FunctionCall(near_primitives::transaction::FunctionCallAction { + method_name, + args: vec![1u8; arg_len], + gas: 3 * 10u64.pow(12), // 3 Tgas, to allow 100 copies in the same receipt + deposit: 10u128.pow(24), + }) +} + +/// Helper enum to select how large an action should be generated. +#[derive(Clone, Copy)] +enum ActionSize { + Min, + Max, +} +impl ActionSize { + fn function_call_payload(self) -> u64 { + match self { + // calling "noop" requires 4 bytes + ActionSize::Min => 4, + // max_arguments_length: 4_194_304 + // max_transaction_size: 4_194_304 + ActionSize::Max => (4_194_304 / 100) - 35, + } + } + + fn key_methods_list(self) -> u64 { + match self { + ActionSize::Min => 0, + // max_number_bytes_method_names: 2000 + ActionSize::Max => 2000, + } + } + + fn deploy_contract(self) -> u64 { + match self { + // small number that still allows to generate a valid contract + ActionSize::Min => 120, + // max_number_bytes_method_names: 2000 + // This size exactly touches tx limit with 1 deploy action. If this suddenly + // fails with `InvalidTxError(TransactionSizeExceeded`, it could be a + // protocol change due to the TX limit computation changing. + ActionSize::Max => 4 * 1024 * 1024 - 160, + } + } +} diff --git a/runtime/runtime-params-estimator/src/cost.rs b/runtime/runtime-params-estimator/src/cost.rs index a8ad2428e7a..0d779526768 100644 --- a/runtime/runtime-params-estimator/src/cost.rs +++ b/runtime/runtime-params-estimator/src/cost.rs @@ -34,6 +34,9 @@ pub enum Cost { /// Estimation: Measure the creation and execution of an empty action /// receipt, where sender and receiver are the same account. ActionSirReceiptCreation, + ActionReceiptCreationSendSir, + ActionReceiptCreationSendNotSir, + ActionReceiptCreationExec, /// Estimates `data_receipt_creation_config.base_cost`, which is charged for /// every data dependency of created receipts. This occurs either through /// calls to `promise_batch_then` or `value_return`. Dispatch and execution @@ -43,6 +46,9 @@ pub enum Cost { /// only one of them also creates a callback that depends on the promise /// results. The difference in execution cost is divided by 1000. DataReceiptCreationBase, + DataReceiptCreationBaseSendNotSir, + DataReceiptCreationBaseSendSir, + DataReceiptCreationBaseExec, /// Estimates `data_receipt_creation_config.cost_per_byte`, which is charged /// for every byte in data dependency of created receipts. This occurs /// either through calls to `promise_batch_then` or `value_return`. Dispatch @@ -53,6 +59,9 @@ pub enum Cost { /// creates small data receipts, the other large ones. The difference in /// execution cost is divided by the total byte difference. DataReceiptCreationPerByte, + DataReceiptCreationPerByteSendNotSir, + DataReceiptCreationPerByteSendSir, + DataReceiptCreationPerByteExec, /// Estimates `action_creation_config.create_account_cost` which is charged /// for `CreateAccount` actions, the same value on sending and executing. /// @@ -60,6 +69,9 @@ pub enum Cost { /// an initial balance to it. Subtract the base cost of creating a receipt. // TODO(jakmeier): consider also subtracting transfer fee ActionCreateAccount, + ActionCreateAccountSendSir, + ActionCreateAccountSendNotSir, + ActionCreateAccountExec, // Deploying a new contract for an account on the blockchain stores the WASM // code in the trie. Additionally, it also triggers a compilation of the // code to check that it is valid WASM. The compiled code is then stored in @@ -72,6 +84,9 @@ pub enum Cost { /// /// Estimation: Measure deployment cost of a "smallest" contract. ActionDeployContractBase, + ActionDeployContractBaseSendNotSir, + ActionDeployContractBaseSendSir, + ActionDeployContractBaseExec, /// Estimates `action_creation_config.deploy_contract_cost_per_byte`, which /// is charged for every byte in the WASM code when deploying the contract /// @@ -79,6 +94,9 @@ pub enum Cost { /// a transaction. Subtract base costs and apply least-squares on the /// results to find the per-byte costs. ActionDeployContractPerByte, + ActionDeployContractPerByteSendNotSir, + ActionDeployContractPerByteSendSir, + ActionDeployContractPerByteExec, /// Estimates `action_creation_config.function_call_cost`, which is the base /// cost for adding a `FunctionCallAction` to a receipt. It aims to account /// for all costs of calling a function that are already known on the caller @@ -90,6 +108,9 @@ pub enum Cost { /// transaction is divided by N. Executable loading cost is also subtracted /// from the final result because this is charged separately. ActionFunctionCallBase, + ActionFunctionCallBaseSendNotSir, + ActionFunctionCallBaseSendSir, + ActionFunctionCallBaseExec, /// Estimates `action_creation_config.function_call_cost_per_byte`, which is /// the incremental cost for each byte of the method name and method /// arguments cost for adding a `FunctionCallAction` to a receipt. @@ -99,20 +120,29 @@ pub enum Cost { /// call with no argument. Divide the difference by the length of the /// argument. ActionFunctionCallPerByte, + ActionFunctionCallPerByteSendNotSir, + ActionFunctionCallPerByteSendSir, + ActionFunctionCallPerByteExec, /// Estimates `action_creation_config.transfer_cost` which is charged for /// every `Action::Transfer`, the same value for sending and executing. /// /// Estimation: Measure a transaction with only a transfer and subtract the /// base cost of creating a receipt. ActionTransfer, + ActionTransferSendNotSir, + ActionTransferSendSir, + ActionTransferExec, /// Estimates `action_creation_config.stake_cost` which is charged for every /// `Action::Stake`, a slightly higher value for sending than executing. /// /// Estimation: Measure a transaction with only a staking action and /// subtract the base cost of creating a sir-receipt. - // TODO(jakmeier): find out and document the reasoning behind send vs exec - // values in this specific case + /// + /// Note: The exec cost is probably a copy-paste mistake. (#8185) ActionStake, + ActionStakeSendNotSir, + ActionStakeSendSir, + ActionStakeExec, /// Estimates `action_creation_config.add_key_cost.full_access_cost` which /// is charged for every `Action::AddKey` where the key is a full access /// key. The same value is charged for sending and executing. @@ -120,6 +150,9 @@ pub enum Cost { /// Estimation: Measure a transaction that adds a full access key and /// subtract the base cost of creating a sir-receipt. ActionAddFullAccessKey, + ActionAddFullAccessKeySendNotSir, + ActionAddFullAccessKeySendSir, + ActionAddFullAccessKeyExec, /// Estimates `action_creation_config.add_key_cost.function_call_cost` which /// is charged once for every `Action::AddKey` where the key is a function /// call key. The same value is charged for sending and executing. @@ -127,6 +160,9 @@ pub enum Cost { /// Estimation: Measure a transaction that adds a function call key and /// subtract the base cost of creating a sir-receipt. ActionAddFunctionAccessKeyBase, + ActionAddFunctionAccessKeyBaseSendNotSir, + ActionAddFunctionAccessKeyBaseSendSir, + ActionAddFunctionAccessKeyBaseExec, /// Estimates /// `action_creation_config.add_key_cost.function_call_cost_per_byte` which /// is charged once for every byte in null-terminated method names listed in @@ -138,6 +174,9 @@ pub enum Cost { /// single method and of creating a sir-receipt. The result is divided by /// total bytes in the method names. ActionAddFunctionAccessKeyPerByte, + ActionAddFunctionAccessKeyPerByteSendNotSir, + ActionAddFunctionAccessKeyPerByteSendSir, + ActionAddFunctionAccessKeyPerByteExec, /// Estimates `action_creation_config.delete_key_cost` which is charged for /// `DeleteKey` actions, the same value on sending and executing. It does /// not matter whether it is a function call or full access key. @@ -147,6 +186,9 @@ pub enum Cost { /// receipt. // TODO(jakmeier): check cost for function call keys with many methods ActionDeleteKey, + ActionDeleteKeySendNotSir, + ActionDeleteKeySendSir, + ActionDeleteKeyExec, /// Estimates `action_creation_config.delete_account_cost` which is charged /// for `DeleteAccount` actions, the same value on sending and executing. /// @@ -154,6 +196,9 @@ pub enum Cost { /// Subtract the base cost of creating a sir-receipt. /// TODO(jakmeier): Consider different account states. ActionDeleteAccount, + ActionDeleteAccountSendNotSir, + ActionDeleteAccountSendSir, + ActionDeleteAccountExec, /// Estimates `wasm_config.ext_costs.base` which is intended to be charged /// once on every host function call. However, this is currently diff --git a/runtime/runtime-params-estimator/src/estimator_context.rs b/runtime/runtime-params-estimator/src/estimator_context.rs index 47f0f8426f3..cbf0c1a2e98 100644 --- a/runtime/runtime-params-estimator/src/estimator_context.rs +++ b/runtime/runtime-params-estimator/src/estimator_context.rs @@ -7,14 +7,15 @@ use near_primitives::receipt::Receipt; use near_primitives::runtime::config_store::RuntimeConfigStore; use near_primitives::runtime::migration_data::{MigrationData, MigrationFlags}; use near_primitives::test_utils::MockEpochInfoProvider; -use near_primitives::transaction::{ExecutionStatus, SignedTransaction}; +use near_primitives::transaction::{ExecutionOutcome, ExecutionStatus, SignedTransaction}; use near_primitives::types::{Gas, MerkleHash}; use near_primitives::version::PROTOCOL_VERSION; -use near_store::{ShardTries, ShardUId, Store, StoreCompiledContractCache}; +use near_store::{ShardTries, ShardUId, Store, StoreCompiledContractCache, TrieUpdate}; use near_store::{TrieCache, TrieCachingStorage, TrieConfig}; use near_vm_logic::{ExtCosts, VMLimitConfig}; use node_runtime::{ApplyState, Runtime}; use std::collections::HashMap; +use std::rc::Rc; use std::sync::Arc; /// Global context shared by all cost calculating functions. @@ -240,10 +241,11 @@ impl Testbed<'_> { transactions: &[SignedTransaction], allow_failures: bool, ) -> Gas { + let trie = self.trie(); let apply_result = self .runtime .apply( - self.tries.get_trie_for_shard(ShardUId::single_shard(), self.root.clone()), + trie, &None, &self.apply_state, &self.prev_receipts, @@ -285,4 +287,61 @@ impl Testbed<'_> { } n } + + /// Process just the verification of a transaction, without action execution. + /// + /// Use this method for measuring the SEND cost of actions. This is the + /// workload done on the sender's shard before an action receipt is created. + /// Network costs for sending are not included. + pub(crate) fn verify_transaction( + &mut self, + tx: &SignedTransaction, + ) -> Result { + let mut state_update = TrieUpdate::new(Rc::new(self.trie())); + // gas price and block height can be anything, it doesn't affect performance + // but making it too small affects max_depth and thus pessimistic inflation + let gas_price = 100_000_000; + let block_height = None; + // do a full verification + let verify_signature = true; + node_runtime::verify_and_charge_transaction( + &self.apply_state.config, + &mut state_update, + gas_price, + tx, + verify_signature, + block_height, + PROTOCOL_VERSION, + ) + } + + /// Process only the execution step of an action receipt. + /// + /// Use this method to estimate action exec costs. + pub(crate) fn apply_action_receipt(&mut self, receipt: &Receipt) -> ExecutionOutcome { + let runtime = node_runtime::Runtime {}; + let mut state_update = TrieUpdate::new(Rc::new(self.trie())); + let mut outgoing_receipts = vec![]; + let mut validator_proposals = vec![]; + let mut stats = node_runtime::ApplyStats::default(); + // TODO: mock is not accurate, potential DB requests are skipped in the mock! + let epoch_info_provider = MockEpochInfoProvider::new([].into_iter()); + let exec_result = runtime + .apply_action_receipt( + &mut state_update, + &self.apply_state, + receipt, + &mut outgoing_receipts, + &mut validator_proposals, + &mut stats, + &epoch_info_provider, + ) + .expect("applying receipt in estimator should not fail"); + exec_result.outcome + } + + /// Instantiate a new trie for the estimator. + fn trie(&mut self) -> near_store::Trie { + self.tries.get_trie_for_shard(ShardUId::single_shard(), self.root.clone()) + } } diff --git a/runtime/runtime-params-estimator/src/lib.rs b/runtime/runtime-params-estimator/src/lib.rs index a5335c6e67b..f31766d1a3c 100644 --- a/runtime/runtime-params-estimator/src/lib.rs +++ b/runtime/runtime-params-estimator/src/lib.rs @@ -57,6 +57,7 @@ //! digging deeper. //! +mod action_costs; mod cost; mod cost_table; mod costs_to_runtime_config; @@ -128,18 +129,69 @@ pub use crate::rocksdb::RocksDBTestConfig; static ALL_COSTS: &[(Cost, fn(&mut EstimatorContext) -> GasCost)] = &[ (Cost::ActionReceiptCreation, action_receipt_creation), (Cost::ActionSirReceiptCreation, action_sir_receipt_creation), + (Cost::ActionReceiptCreationSendSir, action_costs::new_action_receipt_send_sir), + (Cost::ActionReceiptCreationSendNotSir, action_costs::new_action_receipt_send_not_sir), + (Cost::ActionReceiptCreationExec, action_costs::new_action_receipt_exec), (Cost::ActionTransfer, action_transfer), + (Cost::ActionTransferSendSir, action_costs::transfer_send_sir), + (Cost::ActionTransferSendNotSir, action_costs::transfer_send_not_sir), + (Cost::ActionTransferExec, action_costs::transfer_exec), (Cost::ActionCreateAccount, action_create_account), + (Cost::ActionCreateAccountSendSir, action_costs::create_account_send_sir), + (Cost::ActionCreateAccountSendNotSir, action_costs::create_account_send_not_sir), + (Cost::ActionCreateAccountExec, action_costs::create_account_exec), (Cost::ActionDeleteAccount, action_delete_account), + (Cost::ActionDeleteAccountSendSir, action_costs::delete_account_send_sir), + (Cost::ActionDeleteAccountSendNotSir, action_costs::delete_account_send_not_sir), + (Cost::ActionDeleteAccountExec, action_costs::delete_account_exec), (Cost::ActionAddFullAccessKey, action_add_full_access_key), + (Cost::ActionAddFullAccessKeySendSir, action_costs::add_full_access_key_send_sir), + (Cost::ActionAddFullAccessKeySendNotSir, action_costs::add_full_access_key_send_not_sir), + (Cost::ActionAddFullAccessKeyExec, action_costs::add_full_access_key_exec), (Cost::ActionAddFunctionAccessKeyBase, action_add_function_access_key_base), + ( + Cost::ActionAddFunctionAccessKeyBaseSendSir, + action_costs::add_function_call_key_base_send_sir, + ), + ( + Cost::ActionAddFunctionAccessKeyBaseSendNotSir, + action_costs::add_function_call_key_base_send_not_sir, + ), + (Cost::ActionAddFunctionAccessKeyBaseExec, action_costs::add_function_call_key_base_exec), (Cost::ActionAddFunctionAccessKeyPerByte, action_add_function_access_key_per_byte), + ( + Cost::ActionAddFunctionAccessKeyPerByteSendSir, + action_costs::add_function_call_key_byte_send_sir, + ), + ( + Cost::ActionAddFunctionAccessKeyPerByteSendNotSir, + action_costs::add_function_call_key_byte_send_not_sir, + ), + (Cost::ActionAddFunctionAccessKeyPerByteExec, action_costs::add_function_call_key_byte_exec), (Cost::ActionDeleteKey, action_delete_key), + (Cost::ActionDeleteKeySendSir, action_costs::delete_key_send_sir), + (Cost::ActionDeleteKeySendNotSir, action_costs::delete_key_send_not_sir), + (Cost::ActionDeleteKeyExec, action_costs::delete_key_exec), (Cost::ActionStake, action_stake), + (Cost::ActionStakeSendNotSir, action_costs::stake_send_not_sir), + (Cost::ActionStakeSendSir, action_costs::stake_send_sir), + (Cost::ActionStakeExec, action_costs::stake_exec), (Cost::ActionDeployContractBase, action_deploy_contract_base), + (Cost::ActionDeployContractBaseSendNotSir, action_costs::deploy_contract_base_send_not_sir), + (Cost::ActionDeployContractBaseSendSir, action_costs::deploy_contract_base_send_sir), + (Cost::ActionDeployContractBaseExec, action_costs::deploy_contract_base_exec), (Cost::ActionDeployContractPerByte, action_deploy_contract_per_byte), + (Cost::ActionDeployContractPerByteSendNotSir, action_costs::deploy_contract_byte_send_not_sir), + (Cost::ActionDeployContractPerByteSendSir, action_costs::deploy_contract_byte_send_sir), + (Cost::ActionDeployContractPerByteExec, action_costs::deploy_contract_byte_exec), (Cost::ActionFunctionCallBase, action_function_call_base), + (Cost::ActionFunctionCallBaseSendNotSir, action_costs::function_call_base_send_not_sir), + (Cost::ActionFunctionCallBaseSendSir, action_costs::function_call_base_send_sir), + (Cost::ActionFunctionCallBaseExec, action_costs::function_call_base_exec), (Cost::ActionFunctionCallPerByte, action_function_call_per_byte), + (Cost::ActionFunctionCallPerByteSendNotSir, action_costs::function_call_byte_send_not_sir), + (Cost::ActionFunctionCallPerByteSendSir, action_costs::function_call_byte_send_sir), + (Cost::ActionFunctionCallPerByteExec, action_costs::function_call_byte_exec), (Cost::HostFunctionCall, host_function_call), (Cost::WasmInstruction, wasm_instruction), (Cost::DataReceiptCreationBase, data_receipt_creation_base), diff --git a/runtime/runtime-params-estimator/src/transaction_builder.rs b/runtime/runtime-params-estimator/src/transaction_builder.rs index ef56a63f3d7..6ad09e264df 100644 --- a/runtime/runtime-params-estimator/src/transaction_builder.rs +++ b/runtime/runtime-params-estimator/src/transaction_builder.rs @@ -18,6 +18,18 @@ pub(crate) struct TransactionBuilder { unused_index: usize, } +/// Define how accounts should be generated. +#[derive(Clone, Copy)] +pub(crate) enum AccountRequirement { + /// Use a different random account on every iteration, account exists and + /// has estimator contract deployed. + RandomUnused, + /// Use the same account as the signer. Must not be used for signer id. + SameAsSigner, + /// Use sub account of the signer. Useful for `CreateAction` estimations. + SubOfSigner, +} + impl TransactionBuilder { pub(crate) fn new(accounts: Vec) -> TransactionBuilder { let n = accounts.len(); @@ -120,6 +132,20 @@ impl TransactionBuilder { } } + pub(crate) fn account_by_requirement( + &mut self, + src: AccountRequirement, + signer_id: Option<&AccountId>, + ) -> AccountId { + match src { + AccountRequirement::RandomUnused => self.random_unused_account(), + AccountRequirement::SameAsSigner => signer_id.expect("no signer_id provided").clone(), + AccountRequirement::SubOfSigner => { + format!("sub.{}", signer_id.expect("no signer_id")).parse().unwrap() + } + } + } + pub(crate) fn random_vec(&mut self, len: usize) -> Vec { (0..len).map(|_| self.rng().gen()).collect() } diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index baea4da1b63..50663158b00 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -448,7 +448,7 @@ impl Runtime { } // Executes when all Receipt `input_data_ids` are in the state - fn apply_action_receipt( + pub fn apply_action_receipt( // TODO: I don't want to make this public when merging to nearcore &self, state_update: &mut TrieUpdate, apply_state: &ApplyState,