Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(xcc): Estimate EVM gas cost for xcc precompile #564

Merged
merged 3 commits into from
Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions engine-precompiles/src/xcc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -61,8 +80,10 @@ pub mod cross_contract_call {
}

impl<I: IO> Precompile for CrossContractCall<I> {
fn required_gas(_input: &[u8]) -> Result<EthGas, ExitError> {
Ok(costs::CROSS_CONTRACT_CALL)
fn required_gas(input: &[u8]) -> Result<EthGas, ExitError> {
// 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(
Expand All @@ -72,11 +93,16 @@ impl<I: IO> Precompile for CrossContractCall<I> {
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 {
Expand Down Expand Up @@ -114,6 +140,8 @@ impl<I: IO> Precompile for CrossContractCall<I> {
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(),
Expand All @@ -125,6 +153,7 @@ impl<I: IO> Precompile for CrossContractCall<I> {

Ok(PrecompileOutput {
logs: vec![promise_log],
cost,
..Default::default()
}
.into())
Expand Down
95 changes: 93 additions & 2 deletions engine-tests/src/tests/xcc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions engine-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
birchmd marked this conversation as resolved.
Show resolved Hide resolved
};
pub use primitive_types::{H160, H256, U256};
}
Expand Down
8 changes: 7 additions & 1 deletion engine-types/src/types/gas.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -75,6 +75,12 @@ impl Add<EthGas> for EthGas {
}
}

impl AddAssign<EthGas> for EthGas {
fn add_assign(&mut self, rhs: EthGas) {
self.0 += rhs.0
}
}

impl Div<usize> for EthGas {
type Output = EthGas;

Expand Down