diff --git a/engine-precompiles/src/xcc.rs b/engine-precompiles/src/xcc.rs index 1afcf9aa7..f4941c948 100644 --- a/engine-precompiles/src/xcc.rs +++ b/engine-precompiles/src/xcc.rs @@ -19,8 +19,27 @@ use evm_core::ExitError; pub mod costs { use crate::prelude::types::{EthGas, NearGas}; - // TODO(#483): Determine the correct amount of gas - pub(super) const CROSS_CONTRACT_CALL: EthGas = EthGas::new(0); + /// Base EVM gas cost for calling this precompile. + /// Value obtained from the following methodology: + /// 1. Estimate the cost of calling this precompile in terms of NEAR gas. + /// This is done by calling the precompile with inputs of different lengths + /// and performing a linear regression to obtain a function + /// `NEAR_gas = CROSS_CONTRACT_CALL_BASE + (input_length) * (CROSS_CONTRACT_CALL_BYTE)`. + /// 2. Convert the NEAR gas cost into an EVM gas cost using the conversion ratio below + /// (`CROSS_CONTRACT_CALL_NEAR_GAS`). + /// + /// This process is done in the `test_xcc_eth_gas_cost` test in + /// `engine-tests/src/tests/xcc.rs`. + pub const CROSS_CONTRACT_CALL_BASE: EthGas = EthGas::new(115_000); + /// Additional EVM gas cost per bytes of input given. + /// See `CROSS_CONTRACT_CALL_BASE` for estimation methodology. + pub const CROSS_CONTRACT_CALL_BYTE: EthGas = EthGas::new(2); + /// EVM gas cost per NEAR gas attached to the created promise. + /// This value is derived from the gas report https://hackmd.io/@birchmd/Sy4piXQ29 + /// The units on this quantity are `NEAR Gas / EVM Gas`. + /// The report gives a value `0.175 T(NEAR_gas) / k(EVM_gas)`. To convert the units to + /// `NEAR Gas / EVM Gas`, we simply multiply `0.175 * 10^12 / 10^3 = 175 * 10^6`. + pub const CROSS_CONTRACT_CALL_NEAR_GAS: u64 = 175_000_000; pub const ROUTER_EXEC: NearGas = NearGas::new(7_000_000_000_000); pub const ROUTER_SCHEDULE: NearGas = NearGas::new(5_000_000_000_000); @@ -61,8 +80,10 @@ pub mod cross_contract_call { } impl Precompile for CrossContractCall { - fn required_gas(_input: &[u8]) -> Result { - Ok(costs::CROSS_CONTRACT_CALL) + fn required_gas(input: &[u8]) -> Result { + // This only includes the cost we can easily derive without parsing the input. + // The other cost is added in later to avoid parsing the input more than once. + Ok(costs::CROSS_CONTRACT_CALL_BASE + costs::CROSS_CONTRACT_CALL_BYTE * input.len()) } fn run( @@ -72,11 +93,16 @@ impl Precompile for CrossContractCall { context: &Context, is_static: bool, ) -> EvmPrecompileResult { - if let Some(target_gas) = target_gas { - if Self::required_gas(input)? > target_gas { - return Err(ExitError::OutOfGas); + let mut cost = Self::required_gas(input)?; + let check_cost = |cost: EthGas| -> Result<(), ExitError> { + if let Some(target_gas) = target_gas { + if cost > target_gas { + return Err(ExitError::OutOfGas); + } } - } + Ok(()) + }; + check_cost(cost)?; // It's not allowed to call cross contract call precompile in static or delegate mode if is_static { @@ -114,6 +140,8 @@ impl Precompile for CrossContractCall { attached_gas: costs::ROUTER_SCHEDULE, }, }; + cost += EthGas::new(promise.attached_gas.as_u64() / costs::CROSS_CONTRACT_CALL_NEAR_GAS); + check_cost(cost)?; let promise_log = Log { address: cross_contract_call::ADDRESS.raw(), @@ -125,6 +153,7 @@ impl Precompile for CrossContractCall { Ok(PrecompileOutput { logs: vec![promise_log], + cost, ..Default::default() } .into()) diff --git a/engine-tests/src/tests/xcc.rs b/engine-tests/src/tests/xcc.rs index 542b7c81a..16fa4b086 100644 --- a/engine-tests/src/tests/xcc.rs +++ b/engine-tests/src/tests/xcc.rs @@ -3,14 +3,105 @@ use crate::tests::erc20_connector::sim_tests; use crate::tests::state_migration::deploy_evm; use aurora_engine_precompiles::xcc::{costs, cross_contract_call}; use aurora_engine_transactions::legacy::TransactionLegacy; -use aurora_engine_types::parameters::{CrossContractCallArgs, PromiseArgs, PromiseCreateArgs}; -use aurora_engine_types::types::{NearGas, Wei, Yocto}; +use aurora_engine_types::parameters::{ + CrossContractCallArgs, PromiseArgs, PromiseCreateArgs, PromiseWithCallbackArgs, +}; +use aurora_engine_types::types::{Address, EthGas, NearGas, Wei, Yocto}; +use aurora_engine_types::U256; use borsh::BorshSerialize; use near_primitives::transaction::Action; use near_primitives_core::contract::ContractCode; use std::fs; use std::path::Path; +#[test] +fn test_xcc_eth_gas_cost() { + let mut runner = test_utils::deploy_evm(); + runner.standalone_runner = None; + let xcc_wasm_bytes = contract_bytes(); + let _ = runner.call("factory_update", "aurora", xcc_wasm_bytes); + let mut signer = test_utils::Signer::random(); + runner.context.block_index = aurora_engine::engine::ZERO_ADDRESS_FIX_HEIGHT + 1; + + // Baseline transaction that does essentially nothing. + let (_, baseline) = runner + .submit_with_signer_profiled(&mut signer, |nonce| TransactionLegacy { + nonce, + gas_price: U256::zero(), + gas_limit: u64::MAX.into(), + to: Some(Address::from_array([0; 20])), + value: Wei::zero(), + data: Vec::new(), + }) + .unwrap(); + + let mut profile_for_promise = |p: PromiseArgs| -> (u64, u64) { + let data = CrossContractCallArgs::Eager(p).try_to_vec().unwrap(); + let input_length = data.len(); + let (_, profile) = runner + .submit_with_signer_profiled(&mut signer, |nonce| TransactionLegacy { + nonce, + gas_price: U256::zero(), + gas_limit: u64::MAX.into(), + to: Some(cross_contract_call::ADDRESS), + value: Wei::zero(), + data, + }) + .unwrap(); + // Subtract off baseline transaction to isolate just precompile things + ( + u64::try_from(input_length).unwrap(), + profile.all_gas() - baseline.all_gas(), + ) + }; + + let promise = PromiseCreateArgs { + target_account_id: "some_account.near".parse().unwrap(), + method: "some_method".into(), + args: b"hello_world".to_vec(), + attached_balance: Yocto::new(56), + attached_gas: NearGas::new(0), + }; + // Shorter input + let (x1, y1) = profile_for_promise(PromiseArgs::Create(promise.clone())); + // longer input + let (x2, y2) = profile_for_promise(PromiseArgs::Callback(PromiseWithCallbackArgs { + base: promise.clone(), + callback: promise, + })); + + // NEAR costs (inferred from a line through (x1, y1) and (x2, y2)) + let xcc_cost_per_byte = (y2 - y1) / (x2 - x1); + let xcc_base_cost = NearGas::new(y1 - xcc_cost_per_byte * x1); + + // Convert to EVM cost using conversion ratio + let xcc_base_cost = EthGas::new(xcc_base_cost.as_u64() / costs::CROSS_CONTRACT_CALL_NEAR_GAS); + let xcc_cost_per_byte = xcc_cost_per_byte / costs::CROSS_CONTRACT_CALL_NEAR_GAS; + + let within_5_percent = |a: u64, b: u64| -> bool { + let x = a.max(b); + let y = a.min(b); + + 20 * (x - y) <= x + }; + assert!( + within_5_percent( + xcc_base_cost.as_u64(), + costs::CROSS_CONTRACT_CALL_BASE.as_u64() + ), + "Incorrect xcc base cost. Expected: {} Actual: {}", + xcc_base_cost, + costs::CROSS_CONTRACT_CALL_BASE + ); + + assert!( + within_5_percent(xcc_cost_per_byte, costs::CROSS_CONTRACT_CALL_BYTE.as_u64()), + "Incorrect xcc per byte cost. Expected: {} Actual: {}", + xcc_cost_per_byte, + costs::CROSS_CONTRACT_CALL_BYTE + ); +} + #[test] fn test_xcc_precompile_eager() { test_xcc_precompile_common(false) diff --git a/engine-types/src/lib.rs b/engine-types/src/lib.rs index 7250dab40..a96a08c46 100644 --- a/engine-types/src/lib.rs +++ b/engine-types/src/lib.rs @@ -24,8 +24,8 @@ mod v0 { vec::Vec, }; pub use core::{ - cmp::Ordering, fmt::Display, marker::PhantomData, mem, ops::Add, ops::Div, ops::Mul, - ops::Sub, ops::SubAssign, + cmp::Ordering, fmt::Display, marker::PhantomData, mem, ops::Add, ops::AddAssign, ops::Div, + ops::Mul, ops::Sub, ops::SubAssign, }; pub use primitive_types::{H160, H256, U256}; } diff --git a/engine-types/src/types/gas.rs b/engine-types/src/types/gas.rs index 4af189265..8b4baa052 100644 --- a/engine-types/src/types/gas.rs +++ b/engine-types/src/types/gas.rs @@ -1,5 +1,5 @@ use crate::fmt::Formatter; -use crate::{Add, Display, Div, Mul, Sub}; +use crate::{Add, AddAssign, Display, Div, Mul, Sub}; use borsh::{BorshDeserialize, BorshSerialize}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -75,6 +75,12 @@ impl Add for EthGas { } } +impl AddAssign for EthGas { + fn add_assign(&mut self, rhs: EthGas) { + self.0 += rhs.0 + } +} + impl Div for EthGas { type Output = EthGas;