forest/dev/subcommands/devnet_cmd/
eth_gas.rs1use crate::dev::subcommands::tests_cmd::helpers::*;
13use crate::rpc::Client;
14use crate::rpc::eth::errors::EXECUTION_REVERTED_CODE;
15use crate::rpc::eth::{
16 BlockNumberOrHash, Predefined,
17 types::{EthAddress, EthBytes, EthCallMessage},
18};
19use crate::rpc::prelude::*;
20use crate::shim::address::Address;
21use crate::utils::encoding::{hex, keccak_256};
22use anyhow::{Context as _, ensure};
23use cid::Cid;
24use jsonrpsee::core::ClientError;
25use libtest_mimic::{Arguments, Failed, Trial};
26use std::io::Write as _;
27use std::str::FromStr as _;
28use tempfile::NamedTempFile;
29use tokio::sync::OnceCell;
30
31const NESTED_GAS_HEX: &str = include_str!("contracts/nested_gas/nested_gas.hex");
34const RECURSE_SIGNATURE: &str = "recurse(uint256)";
35const REQUIRES_HIGH_GAS_SIGNATURE: &str = "requiresHighGasLimit()";
38const REVERT_REASON: &str = "gas limit too low";
40const GAS_SEARCH_FAILURE: &str = "gas search failed";
43const HEX_IN_CONTAINER: &str = "/tmp/nested_gas.hex";
44
45const CONTROL_DEPTH: u64 = 0;
48const NESTED_DEPTH: u64 = 100;
50const SENDER_FUND_AMT: &str = "10 FIL";
53
54#[derive(Debug, clap::Args)]
56pub struct EthGasTestCommand {}
57
58impl EthGasTestCommand {
59 pub async fn run(self) -> anyhow::Result<()> {
60 let args = Arguments {
61 test_threads: Some(1),
62 ..Default::default()
63 };
64 libtest_mimic::run(&args, tests()).exit();
65 }
66}
67
68fn tests() -> Vec<Trial> {
69 fn trial(name: &'static str, body: fn() -> anyhow::Result<()>) -> Trial {
70 Trial::test(name, move || {
71 body().map_err(|e| Failed::from(format!("{e:?}")))
72 })
73 }
74
75 vec![
76 trial("eth_estimate_gas_agrees_without_nesting", || {
77 block_on(estimate_agrees(CONTROL_DEPTH))
78 }),
79 trial("eth_estimate_gas_agrees_with_nesting", || {
80 block_on(estimate_agrees(NESTED_DEPTH))
81 }),
82 trial("eth_estimate_gas_is_sufficient_on_chain", || {
83 block_on(estimate_is_sufficient_on_chain())
84 }),
85 trial("eth_estimate_gas_reports_a_non_gas_failure", || {
86 block_on(estimate_reports_a_non_gas_failure())
87 }),
88 ]
89}
90
91fn selector(signature: &str) -> Vec<u8> {
93 keccak_256(signature.as_bytes())
94 .get(..4)
95 .expect("keccak256 is 32 bytes")
96 .to_vec()
97}
98
99fn recurse_calldata(depth: u64) -> Vec<u8> {
101 let mut out = selector(RECURSE_SIGNATURE);
102 out.extend_from_slice(ðereum_types::U256::from(depth).to_big_endian());
103 out
104}
105
106struct Deployed {
108 eth: EthAddress,
109 f4: Address,
110}
111
112async fn contract() -> anyhow::Result<&'static Deployed> {
114 static CONTRACT: OnceCell<Deployed> = OnceCell::const_new();
115 CONTRACT
116 .get_or_try_init(|| async {
117 let mut hex_file = NamedTempFile::new_in(std::env::temp_dir())
118 .context("staging the contract bytecode")?;
119 hex_file.write_all(NESTED_GAS_HEX.trim().as_bytes())?;
120 hex_file.flush()?;
121 docker(&[
122 "cp",
123 &hex_file.path().to_string_lossy(),
124 &format!("lotus:{HEX_IN_CONTAINER}"),
125 ])?;
126
127 let deploy = lotus_exec(&["evm", "deploy", "--hex", HEX_IN_CONTAINER])?;
128 let f4 = deploy
129 .lines()
130 .find_map(|l| l.trim().strip_prefix("f4 Address: "))
131 .with_context(|| format!("no `f4 Address:` in deploy output:\n{deploy}"))?;
132 let f4 = Address::from_str(f4.trim()).context("parsing the deployed f4 address")?;
133 eprintln!("deployed NestedGas at {f4}");
134 anyhow::Ok(Deployed {
135 eth: EthAddress::from_filecoin_address(&f4)?,
136 f4,
137 })
138 })
139 .await
140}
141
142async fn sender_addr() -> anyhow::Result<&'static Address> {
148 static SENDER: OnceCell<Address> = OnceCell::const_new();
149 SENDER
150 .get_or_try_init(|| async {
151 let addr = lotus_exec(&["wallet", "new", "delegated"])?;
152 let msg = send_from(
153 &FOREST_TEST_PRELOADED_ADDRESS,
154 &addr,
155 SENDER_FUND_AMT,
156 Backend::Local,
157 )?;
158 eprintln!("funding sender {addr} with {SENDER_FUND_AMT}, msg: {msg}");
159 let balance = poll_until_funded(&addr, Backend::Local).await?;
160 eprintln!("sender {addr} funded balance: {balance}");
161 Address::from_str(&addr).context("parsing the sender address")
162 })
163 .await
164}
165
166async fn estimate(
167 client: &Client,
168 calldata: Vec<u8>,
169 block: BlockNumberOrHash,
170) -> anyhow::Result<u64> {
171 let (sender, deployed) = tokio::try_join!(sender_addr(), contract())?;
172 let msg = EthCallMessage {
173 from: Some(EthAddress::from_filecoin_address(sender)?),
174 to: Some(deployed.eth),
175 data: Some(EthBytes(calldata)),
176 ..Default::default()
177 };
178 let gas = client
179 .call(EthEstimateGas::request((msg, Some(block)))?)
180 .await?;
181 Ok(gas.0)
182}
183
184async fn common_block_number(a: &Client, b: &Client) -> anyhow::Result<i64> {
188 let (head_a, head_b) = tokio::try_join!(
189 async { anyhow::Ok(a.call(EthBlockNumber::request(())?).await?) },
190 async { anyhow::Ok(b.call(EthBlockNumber::request(())?).await?) },
191 )?;
192 Ok(head_a.0.min(head_b.0) as i64)
193}
194
195async fn pinned_common_block() -> anyhow::Result<(Client, Client, i64)> {
199 tokio::try_join!(contract(), sender_addr())?;
200 let (forest_c, lotus_c) = (forest_client()?, lotus_client()?);
201 let block = common_block_number(&forest_c, &lotus_c).await?;
202 Ok((forest_c, lotus_c, block))
203}
204
205async fn estimate_agrees(depth: u64) -> anyhow::Result<()> {
207 let (forest_c, lotus_c, block) = pinned_common_block().await?;
208 let (forest, lotus) = tokio::try_join!(
209 async {
210 estimate(
211 &forest_c,
212 recurse_calldata(depth),
213 BlockNumberOrHash::from_block_number(block),
214 )
215 .await
216 .context("EthEstimateGas on forest")
217 },
218 async {
219 estimate(
220 &lotus_c,
221 recurse_calldata(depth),
222 BlockNumberOrHash::from_block_number(block),
223 )
224 .await
225 .context("EthEstimateGas on lotus")
226 },
227 )?;
228 eprintln!("depth={depth} block={block} forest={forest} lotus={lotus}");
229 ensure!(
230 forest == lotus,
231 "eth_estimateGas disagrees at recursion depth {depth} (block {block}): forest={forest} lotus={lotus}"
232 );
233 Ok(())
234}
235
236async fn estimate_is_sufficient_on_chain() -> anyhow::Result<()> {
238 let forest = forest_client()?;
239 let estimate = estimate(
242 &forest,
243 recurse_calldata(NESTED_DEPTH),
244 BlockNumberOrHash::PredefinedBlock(Predefined::Latest),
245 )
246 .await?;
247 let sender = sender_addr().await?.to_string();
248 let target = contract().await?.f4.to_string();
249 let params = hex::encode(recurse_calldata(NESTED_DEPTH));
250 let gas_limit = estimate.to_string();
251 let out = lotus_exec_retrying_mpool(&[
255 "send",
256 "--from",
257 &sender,
258 "--params-hex",
259 ¶ms,
260 "--gas-limit",
261 &gas_limit,
262 &target,
263 "0",
264 ])
265 .await?;
266 let cid = out
267 .lines()
268 .last()
269 .context("no cid from `lotus send`")?
270 .trim();
271 eprintln!("submitted at forest's estimate {estimate}: {cid}");
272
273 let lookup = forest
274 .call(
275 StateWaitMsg::request((Cid::from_str(cid)?, 0, 800, true))?.with_timeout(POLL_TIMEOUT),
276 )
277 .await?;
278 let exit = lookup.receipt.exit_code();
279 ensure!(
280 exit.is_success(),
281 "a transaction submitted at forest's own eth_estimateGas value ({estimate}) failed \
282 on chain with exit code {exit}; the estimate is not a usable gas limit"
283 );
284 Ok(())
285}
286
287async fn estimate_reports_a_non_gas_failure() -> anyhow::Result<()> {
291 let (forest_c, lotus_c, block) = pinned_common_block().await?;
292 for (node, client) in [("forest", &forest_c), ("lotus", &lotus_c)] {
293 let err = match estimate(
294 client,
295 selector(REQUIRES_HIGH_GAS_SIGNATURE),
296 BlockNumberOrHash::from_block_number(block),
297 )
298 .await
299 {
300 Ok(gas) => anyhow::bail!(
301 "{node} returned an estimate ({gas}) for a message that reverts at that limit; \
302 a non-gas failure must be reported, not answered with a gas value"
303 ),
304 Err(e) => e,
305 };
306 let Some(ClientError::Call(obj)) = err.downcast_ref::<ClientError>() else {
307 anyhow::bail!("{node} returned a non-JSON-RPC error, cannot check parity: {err:?}");
308 };
309 eprintln!(
310 "{node} rejected the call: code={} has_data={} msg={}",
311 obj.code(),
312 obj.data().is_some(),
313 obj.message()
314 );
315 ensure!(
317 obj.message().contains(GAS_SEARCH_FAILURE),
318 "{node} rejected the call before the gas search, so this no longer exercises the \
319 branch it is meant to pin (expected `{GAS_SEARCH_FAILURE}`): {}",
320 obj.message()
321 );
322 ensure!(
323 obj.message().contains(REVERT_REASON),
324 "{node} rejected the call without naming the revert reason `{REVERT_REASON}`: {}",
325 obj.message()
326 );
327
328 if node == "forest" {
332 ensure!(
333 obj.code() == EXECUTION_REVERTED_CODE,
334 "forest rejected with code {}, expected execution-reverted {EXECUTION_REVERTED_CODE}: {}",
335 obj.code(),
336 obj.message()
337 );
338 ensure!(
339 obj.data().is_some(),
340 "forest rejected without revert data; eth clients cannot ABI-decode the reason: {}",
341 obj.message()
342 );
343 }
344 }
345 Ok(())
346}