Skip to content

Commit

Permalink
Feat(xcc): Estimate EVM gas cost for xcc precompile (#564)
Browse files Browse the repository at this point in the history
  • Loading branch information
birchmd committed Aug 10, 2022
1 parent a2b4e7f commit dc6a30c
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 13 deletions.
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 @@ -25,8 +25,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};
}
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<u64> for EthGas {
type Output = EthGas;

Expand Down

0 comments on commit dc6a30c

Please sign in to comment.