use crate::dev::subcommands::tests_cmd::helpers::*;
use crate::rpc::Client;
use crate::rpc::eth::errors::{
EXECUTION_REVERTED_CODE, INVALID_INPUT_CODE, TRANSACTION_REJECTED_CODE,
};
use crate::rpc::eth::{
BlockNumberOrHash, EthUint64, Predefined,
types::{EthAddress, EthBytes, EthCallMessage},
};
use crate::rpc::prelude::*;
use crate::shim::address::Address;
use crate::shim::econ::BLOCK_GAS_LIMIT;
use crate::utils::encoding::keccak_256;
use anyhow::{Context as _, ensure};
use libtest_mimic::{Arguments, Failed, Trial};
use std::str::FromStr as _;
use tokio::sync::OnceCell;
const NESTED_GAS_HEX: &str = include_str!("contracts/nested_gas/nested_gas.hex");
const RECURSE_SIGNATURE: &str = "recurse(uint256)";
const REQUIRES_HIGH_GAS_SIGNATURE: &str = "requiresHighGasLimit()";
const REVERT_REASON: &str = "gas limit too low";
const CONTROL_DEPTH: u64 = 0;
const NESTED_DEPTH: u64 = 100;
const SENDER_FUND_AMT: &str = "10 FIL";
#[derive(Debug, clap::Args)]
pub struct EthGasTestCommand {}
impl EthGasTestCommand {
pub async fn run(self) -> anyhow::Result<()> {
let args = Arguments {
test_threads: Some(1),
..Default::default()
};
libtest_mimic::run(&args, tests()).exit();
}
}
fn tests() -> Vec<Trial> {
fn trial(name: &'static str, body: fn() -> anyhow::Result<()>) -> Trial {
Trial::test(name, move || {
body().map_err(|e| Failed::from(format!("{e:?}")))
})
}
vec![
trial("eth_estimate_gas_agrees_without_nesting", || {
block_on(estimate_agrees(CONTROL_DEPTH))
}),
trial("eth_estimate_gas_agrees_with_nesting", || {
block_on(estimate_agrees(NESTED_DEPTH))
}),
trial("eth_estimate_gas_is_sufficient_on_chain", || {
block_on(estimate_is_sufficient_on_chain())
}),
trial("eth_estimate_gas_reports_a_non_gas_failure", || {
block_on(estimate_reports_a_non_gas_failure())
}),
trial("eth_estimate_gas_honors_gas_cap", || {
block_on(estimate_honors_gas_cap())
}),
trial(
"eth_estimate_gas_under_cap_searches_past_gas_dependent_revert",
|| block_on(estimate_under_cap_searches_past_gas_dependent_revert()),
),
]
}
fn selector(signature: &str) -> Vec<u8> {
keccak_256(signature.as_bytes())
.get(..4)
.expect("keccak256 is 32 bytes")
.to_vec()
}
fn recurse_calldata(depth: u64) -> Vec<u8> {
let mut out = selector(RECURSE_SIGNATURE);
out.extend_from_slice(ðereum_types::U256::from(depth).to_big_endian());
out
}
async fn contract() -> anyhow::Result<&'static EthAddress> {
static CONTRACT: OnceCell<EthAddress> = OnceCell::const_new();
CONTRACT
.get_or_try_init(|| async {
let from = sender().await?;
let deploy = forest_evm_deploy_hex(from, NESTED_GAS_HEX)?;
let f4 = parse_f4_from_evm_deploy(&deploy)?;
eprintln!("deployed NestedGas at {f4}");
poll_until_actor_on("forest", f4, forest_client).await?;
poll_until_actor_on("lotus", f4, lotus_client).await?;
poll_until_next_epoch().await?;
EthAddress::from_filecoin_address(&f4)
})
.await
}
async fn sender() -> anyhow::Result<&'static str> {
static SENDER: OnceCell<String> = OnceCell::const_new();
Ok(SENDER
.get_or_try_init(|| async {
let addr = lotus_exec(&["wallet", "new", "delegated"])?;
let msg = send_from(
&FOREST_TEST_PRELOADED_ADDRESS,
&addr,
SENDER_FUND_AMT,
Backend::Local,
)?;
eprintln!("funding sender {addr} with {SENDER_FUND_AMT}, msg: {msg}");
let balance = poll_until_funded(&addr, Backend::Local).await?;
eprintln!("sender {addr} funded balance: {balance}");
let parsed = Address::from_str(&addr).context("parsing the sender address")?;
poll_until_actor_on("lotus", parsed, lotus_client).await?;
import_lotus_wallet_into_forest(&addr)?;
anyhow::Ok(addr)
})
.await?
.as_str())
}
async fn call_message(calldata: Vec<u8>, gas: Option<u64>) -> anyhow::Result<EthCallMessage> {
let (from, to) = tokio::try_join!(sender(), contract())?;
let from = Address::from_str(from).context("parsing the sender address")?;
Ok(EthCallMessage {
from: Some(EthAddress::from_filecoin_address(&from)?),
to: Some(*to),
data: Some(EthBytes(calldata)),
gas: gas.map(EthUint64),
..Default::default()
})
}
async fn estimate(
client: &Client,
calldata: Vec<u8>,
block: BlockNumberOrHash,
gas: Option<u64>,
) -> anyhow::Result<u64> {
let msg = call_message(calldata, gas).await?;
let gas = client
.call(EthEstimateGas::request((msg, Some(block)))?)
.await?;
Ok(gas.0)
}
async fn expect_estimate_error(
client: &Client,
calldata: Vec<u8>,
block: BlockNumberOrHash,
cap: u64,
code: i32,
expected: &str,
) -> anyhow::Result<()> {
let err = match estimate(client, calldata, block, Some(cap)).await {
Ok(gas) => anyhow::bail!("returned {gas} for a call that does not fit in a cap of {cap}"),
Err(e) => e,
};
let obj = rpc_call_err(&err)
.with_context(|| format!("expected a JSON-RPC error for a cap of {cap}: {err:?}"))?;
ensure!(
obj.code() == code && obj.message() == expected,
"expected code {code} `{expected}` for a cap of {cap}, got code {} `{}`",
obj.code(),
obj.message()
);
Ok(())
}
async fn common_block_number(a: &Client, b: &Client) -> anyhow::Result<i64> {
let (head_a, head_b) = tokio::try_join!(
async { anyhow::Ok(a.call(EthBlockNumber::request(())?).await?) },
async { anyhow::Ok(b.call(EthBlockNumber::request(())?).await?) },
)?;
Ok(head_a.0.min(head_b.0) as i64)
}
async fn poll_until_next_epoch() -> anyhow::Result<()> {
let current_epoch = common_block_number(&forest_client()?, &lotus_client()?).await?;
poll("both nodes one epoch past deploy", || async {
Ok(
(common_block_number(&forest_client()?, &lotus_client()?).await? > current_epoch)
.then_some(()),
)
})
.await
}
async fn pinned_common_block() -> anyhow::Result<(Client, Client, i64)> {
contract().await?;
let (forest_c, lotus_c) = (forest_client()?, lotus_client()?);
let block = common_block_number(&forest_c, &lotus_c).await?;
Ok((forest_c, lotus_c, block))
}
async fn estimate_agrees(depth: u64) -> anyhow::Result<()> {
let (forest_c, lotus_c, block) = pinned_common_block().await?;
let (forest, lotus) = tokio::try_join!(
async {
estimate(
&forest_c,
recurse_calldata(depth),
BlockNumberOrHash::from_block_number(block),
None,
)
.await
.context("EthEstimateGas on forest")
},
async {
estimate(
&lotus_c,
recurse_calldata(depth),
BlockNumberOrHash::from_block_number(block),
None,
)
.await
.context("EthEstimateGas on lotus")
},
)?;
eprintln!("depth={depth} block={block} forest={forest} lotus={lotus}");
ensure!(
forest == lotus,
"eth_estimateGas disagrees at recursion depth {depth} (block {block}): forest={forest} lotus={lotus}"
);
Ok(())
}
async fn estimate_is_sufficient_on_chain() -> anyhow::Result<()> {
let forest = forest_client()?;
let estimate = estimate(
&forest,
recurse_calldata(NESTED_DEPTH),
BlockNumberOrHash::PredefinedBlock(Predefined::Latest),
None,
)
.await?;
let from = sender().await?;
let cid = wallet_send_calldata(
from,
contract().await?,
&recurse_calldata(NESTED_DEPTH),
estimate,
)
.await
.with_context(|| {
format!(
"a transaction submitted at forest's own eth_estimateGas value ({estimate}) failed \
on chain; the estimate is not a usable gas limit"
)
})?;
eprintln!("submitted at forest's estimate {estimate}: {cid}");
Ok(())
}
async fn estimate_reports_a_non_gas_failure() -> anyhow::Result<()> {
let (forest_c, lotus_c, block) = pinned_common_block().await?;
for (node, client) in [("forest", &forest_c), ("lotus", &lotus_c)] {
let err = match estimate(
client,
selector(REQUIRES_HIGH_GAS_SIGNATURE),
BlockNumberOrHash::from_block_number(block),
None,
)
.await
{
Ok(gas) => anyhow::bail!(
"{node} returned an estimate ({gas}) for a message that reverts at that limit; \
a non-gas failure must be reported, not answered with a gas value"
),
Err(e) => e,
};
let obj = rpc_call_err(&err).with_context(|| {
format!("{node} returned a non-JSON-RPC error, cannot check parity: {err:?}")
})?;
eprintln!(
"{node} rejected the call: code={} has_data={} msg={}",
obj.code(),
obj.data().is_some(),
obj.message()
);
ensure!(
obj.message().contains(REVERT_REASON),
"{node} rejected the call without naming the revert reason `{REVERT_REASON}`: {}",
obj.message()
);
if node == "forest" {
ensure!(
obj.code() == EXECUTION_REVERTED_CODE,
"forest rejected with code {}, expected execution-reverted {EXECUTION_REVERTED_CODE}: {}",
obj.code(),
obj.message()
);
ensure!(
obj.data().is_some(),
"forest rejected without revert data; eth clients cannot ABI-decode the reason: {}",
obj.message()
);
}
}
Ok(())
}
async fn estimate_honors_gas_cap() -> anyhow::Result<()> {
let (forest, _, block) = pinned_common_block().await?;
let block = BlockNumberOrHash::from_block_number(block);
let calldata = recurse_calldata(NESTED_DEPTH);
let uncapped = estimate(&forest, calldata.clone(), block.clone(), None).await?;
let estimate_with_spare_cap =
estimate(&forest, calldata.clone(), block.clone(), Some(uncapped * 2)).await?;
ensure!(
estimate_with_spare_cap == uncapped,
"a cap above the need changed the estimate: capped={estimate_with_spare_cap} uncapped={uncapped}"
);
let tight = uncapped - 1;
let estimate_with_tight_cap =
estimate(&forest, calldata.clone(), block.clone(), Some(tight)).await?;
ensure!(
estimate_with_tight_cap <= tight,
"the estimate {estimate_with_tight_cap} exceeds the cap {tight}"
);
let call = call_message(calldata.clone(), Some(estimate_with_tight_cap)).await?;
forest
.call(EthCall::request((call, block.clone()))?)
.await
.with_context(|| {
format!("eth_call at the capped estimate {estimate_with_tight_cap} failed")
})?;
let half = uncapped / 2;
expect_estimate_error(
&forest,
calldata.clone(),
block.clone(),
half,
TRANSACTION_REJECTED_CODE,
&format!("out of gas: gas required exceeds: {half}"),
)
.await?;
expect_estimate_error(
&forest,
calldata,
block,
21_000,
INVALID_INPUT_CODE,
"gas required exceeds allowance (21000)",
)
.await
}
async fn estimate_under_cap_searches_past_gas_dependent_revert() -> anyhow::Result<()> {
let (forest, _, block) = pinned_common_block().await?;
let block = BlockNumberOrHash::from_block_number(block);
let threshold: u64 = 50_000_000;
let calldata = selector(REQUIRES_HIGH_GAS_SIGNATURE);
let gas = estimate(
&forest,
calldata.clone(),
block.clone(),
Some(BLOCK_GAS_LIMIT),
)
.await?;
ensure!(
gas > threshold,
"expected an estimate above {threshold}, got {gas}"
);
let call = call_message(calldata.clone(), Some(gas)).await?;
forest
.call(EthCall::request((call, block.clone()))?)
.await
.with_context(|| format!("eth_call at the estimate {gas} failed"))?;
let cap = threshold / 2;
expect_estimate_error(
&forest,
calldata,
block,
cap,
TRANSACTION_REJECTED_CODE,
&format!("out of gas: gas required exceeds: {cap}"),
)
.await
}