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 from = sender_addr().await?.to_string();
128 let deploy = lotus_exec_retrying_transient(&[
129 "evm",
130 "deploy",
131 "--from",
132 &from,
133 "--hex",
134 HEX_IN_CONTAINER,
135 ])
136 .await?;
137 let f4 = deploy
138 .lines()
139 .find_map(|l| l.trim().strip_prefix("f4 Address: "))
140 .with_context(|| format!("no `f4 Address:` in deploy output:\n{deploy}"))?;
141 let f4 = Address::from_str(f4.trim()).context("parsing the deployed f4 address")?;
142 eprintln!("deployed NestedGas at {f4}");
143 anyhow::Ok(Deployed {
144 eth: EthAddress::from_filecoin_address(&f4)?,
145 f4,
146 })
147 })
148 .await
149}
150
151async fn sender_addr() -> anyhow::Result<&'static Address> {
158 static SENDER: OnceCell<Address> = OnceCell::const_new();
159 SENDER
160 .get_or_try_init(|| async {
161 let addr = lotus_exec(&["wallet", "new", "delegated"])?;
162 let msg = send_from(
163 &FOREST_TEST_PRELOADED_ADDRESS,
164 &addr,
165 SENDER_FUND_AMT,
166 Backend::Local,
167 )?;
168 eprintln!("funding sender {addr} with {SENDER_FUND_AMT}, msg: {msg}");
169 let balance = poll_until_funded(&addr, Backend::Local).await?;
170 eprintln!("sender {addr} funded balance: {balance}");
171 let sender = Address::from_str(&addr).context("parsing the sender address")?;
172 poll_until_actor_on("lotus", sender, lotus_client).await?;
173 Ok(sender)
174 })
175 .await
176}
177
178async fn estimate(
179 client: &Client,
180 calldata: Vec<u8>,
181 block: BlockNumberOrHash,
182) -> anyhow::Result<u64> {
183 let (sender, deployed) = tokio::try_join!(sender_addr(), contract())?;
184 let msg = EthCallMessage {
185 from: Some(EthAddress::from_filecoin_address(sender)?),
186 to: Some(deployed.eth),
187 data: Some(EthBytes(calldata)),
188 ..Default::default()
189 };
190 let gas = client
191 .call(EthEstimateGas::request((msg, Some(block)))?)
192 .await?;
193 Ok(gas.0)
194}
195
196async fn common_block_number(a: &Client, b: &Client) -> anyhow::Result<i64> {
200 let (head_a, head_b) = tokio::try_join!(
201 async { anyhow::Ok(a.call(EthBlockNumber::request(())?).await?) },
202 async { anyhow::Ok(b.call(EthBlockNumber::request(())?).await?) },
203 )?;
204 Ok(head_a.0.min(head_b.0) as i64)
205}
206
207async fn pinned_common_block() -> anyhow::Result<(Client, Client, i64)> {
211 tokio::try_join!(contract(), sender_addr())?;
212 let (forest_c, lotus_c) = (forest_client()?, lotus_client()?);
213 let block = common_block_number(&forest_c, &lotus_c).await?;
214 Ok((forest_c, lotus_c, block))
215}
216
217async fn estimate_agrees(depth: u64) -> anyhow::Result<()> {
219 let (forest_c, lotus_c, block) = pinned_common_block().await?;
220 let (forest, lotus) = tokio::try_join!(
221 async {
222 estimate(
223 &forest_c,
224 recurse_calldata(depth),
225 BlockNumberOrHash::from_block_number(block),
226 )
227 .await
228 .context("EthEstimateGas on forest")
229 },
230 async {
231 estimate(
232 &lotus_c,
233 recurse_calldata(depth),
234 BlockNumberOrHash::from_block_number(block),
235 )
236 .await
237 .context("EthEstimateGas on lotus")
238 },
239 )?;
240 eprintln!("depth={depth} block={block} forest={forest} lotus={lotus}");
241 ensure!(
242 forest == lotus,
243 "eth_estimateGas disagrees at recursion depth {depth} (block {block}): forest={forest} lotus={lotus}"
244 );
245 Ok(())
246}
247
248async fn estimate_is_sufficient_on_chain() -> anyhow::Result<()> {
250 let forest = forest_client()?;
251 let estimate = estimate(
254 &forest,
255 recurse_calldata(NESTED_DEPTH),
256 BlockNumberOrHash::PredefinedBlock(Predefined::Latest),
257 )
258 .await?;
259 let sender = sender_addr().await?.to_string();
260 let target = contract().await?.f4.to_string();
261 let params = hex::encode(recurse_calldata(NESTED_DEPTH));
262 let gas_limit = estimate.to_string();
263 let out = lotus_exec_retrying_transient(&[
267 "send",
268 "--from",
269 &sender,
270 "--params-hex",
271 ¶ms,
272 "--gas-limit",
273 &gas_limit,
274 &target,
275 "0",
276 ])
277 .await?;
278 let cid = out
279 .lines()
280 .last()
281 .context("no cid from `lotus send`")?
282 .trim();
283 eprintln!("submitted at forest's estimate {estimate}: {cid}");
284
285 let lookup = poll_until_message_executed(&forest, Cid::from_str(cid)?).await?;
286 let exit = lookup.receipt.exit_code();
287 ensure!(
288 exit.is_success(),
289 "a transaction submitted at forest's own eth_estimateGas value ({estimate}) failed \
290 on chain with exit code {exit}; the estimate is not a usable gas limit"
291 );
292 Ok(())
293}
294
295async fn estimate_reports_a_non_gas_failure() -> anyhow::Result<()> {
299 let (forest_c, lotus_c, block) = pinned_common_block().await?;
300 for (node, client) in [("forest", &forest_c), ("lotus", &lotus_c)] {
301 let err = match estimate(
302 client,
303 selector(REQUIRES_HIGH_GAS_SIGNATURE),
304 BlockNumberOrHash::from_block_number(block),
305 )
306 .await
307 {
308 Ok(gas) => anyhow::bail!(
309 "{node} returned an estimate ({gas}) for a message that reverts at that limit; \
310 a non-gas failure must be reported, not answered with a gas value"
311 ),
312 Err(e) => e,
313 };
314 let Some(ClientError::Call(obj)) = err.downcast_ref::<ClientError>() else {
315 anyhow::bail!("{node} returned a non-JSON-RPC error, cannot check parity: {err:?}");
316 };
317 eprintln!(
318 "{node} rejected the call: code={} has_data={} msg={}",
319 obj.code(),
320 obj.data().is_some(),
321 obj.message()
322 );
323 ensure!(
325 obj.message().contains(GAS_SEARCH_FAILURE),
326 "{node} rejected the call before the gas search, so this no longer exercises the \
327 branch it is meant to pin (expected `{GAS_SEARCH_FAILURE}`): {}",
328 obj.message()
329 );
330 ensure!(
331 obj.message().contains(REVERT_REASON),
332 "{node} rejected the call without naming the revert reason `{REVERT_REASON}`: {}",
333 obj.message()
334 );
335
336 if node == "forest" {
340 ensure!(
341 obj.code() == EXECUTION_REVERTED_CODE,
342 "forest rejected with code {}, expected execution-reverted {EXECUTION_REVERTED_CODE}: {}",
343 obj.code(),
344 obj.message()
345 );
346 ensure!(
347 obj.data().is_some(),
348 "forest rejected without revert data; eth clients cannot ABI-decode the reason: {}",
349 obj.message()
350 );
351 }
352 }
353 Ok(())
354}