1use crate::dev::subcommands::tests_cmd::helpers::*;
16use crate::rpc::Client;
17use crate::rpc::eth::errors::{EXECUTION_REVERTED_CODE, OUT_OF_GAS_CODE};
18use crate::rpc::eth::{
19 BlockNumberOrHash, EthBigInt, Predefined,
20 types::{EthAddress, EthBytes, EthCallMessage},
21};
22use crate::rpc::prelude::*;
23use crate::shim::address::Address;
24use crate::shim::econ::TokenAmount;
25use crate::shim::state_tree::ActorState;
26use crate::utils::encoding::{hex, keccak_256};
27use anyhow::{Context as _, ensure};
28use cid::Cid;
29use jsonrpsee::core::ClientError;
30use libtest_mimic::{Arguments, Failed, Trial};
31use std::io::Write as _;
32use std::str::FromStr as _;
33use tempfile::NamedTempFile;
34use tokio::sync::OnceCell;
35
36const SIMPLE_COIN_HEX: &str = include_str!("contracts/simple_coin/simple_coin.hex");
37const CONTRACT_A_HEX: &str = include_str!("contracts/contract_a/contract_a.hex");
38const CONTRACT_B_HEX: &str = include_str!("contracts/contract_b/contract_b.hex");
39const NESTED_GAS_HEX: &str = include_str!("contracts/nested_gas/nested_gas.hex");
40const ERRORS_HEX: &str = include_str!("contracts/errors/errors.hex");
41
42const SEND_COIN_SIGNATURE: &str = "sendCoin(address,uint256)";
43const SET_CONTRACT_B_SIGNATURE: &str = "setContractB(address)";
44const GET_BALANCE_SIGNATURE: &str = "getBalance(address)";
45const CALL_B_AND_READ_BACK: &str = "callBAndReadBack()";
46const CALL_B_AND_DOUBLE: &str = "callBAndDouble()";
47const RECURSE_SIGNATURE: &str = "recurse(uint256)";
48const FAIL_DIV_ZERO: &str = "failDivZero()";
49const FAIL_ASSERT: &str = "failAssert()";
50const FAIL_REVERT_REASON: &str = "failRevertReason()";
51const FAIL_REVERT_EMPTY: &str = "failRevertEmpty()";
52const FAIL_CUSTOM: &str = "failCustom()";
53
54const NESTED_DEPTH: u64 = 100;
55const DEPLOYER_FUND_AMT: &str = "10 FIL";
56const ROUND_TRIP_FUND_AMT: &str = "1 FIL";
57const RECURSIVE_FUND_AMT: &str = "10 FIL";
58const EOA_FUND_AMT: &str = "10 FIL";
59const PLACEHOLDER_FUND_AMT: &str = "2 FIL";
60const ESTIMATE_PARITY: f64 = 0.10;
61const GAS_PRICE: u64 = 1_000_000_000;
62const MIN_ESTIMATE_GAS: u64 = 21_000;
63const MAX_ESTIMATE_GAS: u64 = 10_000_000_000;
64const PANIC_ASSERT: &str =
66 "4e487b710000000000000000000000000000000000000000000000000000000000000001";
67const PANIC_DIV_ZERO: &str =
68 "4e487b710000000000000000000000000000000000000000000000000000000000000012";
69const OOG_INITCODE: &str = "5b600056";
71
72#[derive(Debug, clap::Args)]
74pub struct EthSkipSenderTestCommand {}
75
76impl EthSkipSenderTestCommand {
77 pub async fn run(self) -> anyhow::Result<()> {
78 let args = Arguments {
79 test_threads: Some(1),
80 ..Default::default()
81 };
82 libtest_mimic::run(&args, tests()).exit();
83 }
84}
85
86fn tests() -> Vec<Trial> {
87 fn trial(name: &'static str, body: fn() -> anyhow::Result<()>) -> Trial {
88 Trial::test(name, move || {
89 body().map_err(|e| Failed::from(format!("{e:?}")))
90 })
91 }
92
93 vec![
94 trial("round_trip_from_unfunded", || {
95 block_on(round_trip_from_unfunded())
96 }),
97 trial("parity_with_existing_sender", || {
98 block_on(parity_with_existing_sender())
99 }),
100 trial("round_trip_recursive", || block_on(round_trip_recursive())),
101 trial("call_sender_identity", || block_on(call_sender_identity())),
102 trial("skip_sender_state_isolation", || {
103 block_on(skip_sender_state_isolation())
104 }),
105 trial("skip_sender_historical_call", || {
106 block_on(skip_sender_historical_call())
107 }),
108 trial("cross_contract_from_contract", || {
109 block_on(cross_contract_from_contract())
110 }),
111 trial("cross_contract_from_missing", || {
112 block_on(cross_contract_from_missing())
113 }),
114 trial("cross_contract_from_eoa", || {
115 block_on(cross_contract_from_eoa())
116 }),
117 trial("cross_contract_double_callback", || {
118 block_on(cross_contract_double_callback())
119 }),
120 trial("call_skip_sender", || block_on(call_skip_sender())),
121 trial("estimate_gas_skip_sender", || {
122 block_on(estimate_gas_skip_sender())
123 }),
124 trial("funded_placeholder_sender", || {
125 block_on(funded_placeholder_sender())
126 }),
127 ]
128}
129
130fn selector(signature: &str) -> Vec<u8> {
131 keccak_256(signature.as_bytes())
132 .get(..4)
133 .expect("keccak256 is 32 bytes")
134 .to_vec()
135}
136
137fn abi_address_word(addr: EthAddress) -> Vec<u8> {
138 let mut word = vec![0u8; 12];
139 word.extend_from_slice(addr.0.as_bytes());
140 word
141}
142
143fn calldata(sig: &str, extra: &[u8]) -> Vec<u8> {
144 let mut out = selector(sig);
145 out.extend_from_slice(extra);
146 out
147}
148
149fn send_coin_calldata(to: EthAddress, amount: u64) -> Vec<u8> {
150 let mut extra = abi_address_word(to);
151 extra.extend_from_slice(ðereum_types::U256::from(amount).to_big_endian());
152 calldata(SEND_COIN_SIGNATURE, &extra)
153}
154
155fn set_contract_b_calldata(addr: EthAddress) -> Vec<u8> {
156 calldata(SET_CONTRACT_B_SIGNATURE, &abi_address_word(addr))
157}
158
159fn recurse_calldata(depth: u64) -> Vec<u8> {
160 calldata(
161 RECURSE_SIGNATURE,
162 ðereum_types::U256::from(depth).to_big_endian(),
163 )
164}
165
166fn get_balance_calldata(addr: EthAddress) -> Vec<u8> {
167 calldata(GET_BALANCE_SIGNATURE, &abi_address_word(addr))
168}
169
170fn simple_coin_initcode() -> anyhow::Result<EthBytes> {
171 Ok(EthBytes(
172 hex::decode(SIMPLE_COIN_HEX.trim()).context("decoding SimpleCoin initcode")?,
173 ))
174}
175
176fn oog_create(from: Option<EthAddress>) -> anyhow::Result<EthCallMessage> {
177 Ok(EthCallMessage {
178 from,
179 to: None,
180 data: Some(EthBytes(
181 hex::decode(OOG_INITCODE).context("decoding OOG initcode")?,
182 )),
183 ..Default::default()
184 })
185}
186
187fn non_existent(seed: u8) -> anyhow::Result<EthAddress> {
189 EthAddress::from_str(&format!("0xdeadbeef{:030}{seed:02x}", 0))
190 .context("parsing missing eth address")
191}
192
193fn latest() -> BlockNumberOrHash {
194 BlockNumberOrHash::PredefinedBlock(Predefined::Latest)
195}
196
197#[derive(Clone, Copy)]
199struct Deployed {
200 eth: EthAddress,
201 f4: Address,
202}
203
204struct Wallet {
206 cli: String,
207 f4: Address,
208 eth: EthAddress,
209}
210
211async fn deployer() -> anyhow::Result<&'static Address> {
214 static DEPLOYER: OnceCell<Address> = OnceCell::const_new();
215 DEPLOYER
216 .get_or_try_init(|| async {
217 let addr = lotus_exec(&["wallet", "new", "delegated"])?;
218 fund_on_chain(&addr, DEPLOYER_FUND_AMT).await
219 })
220 .await
221}
222
223async fn deploy_hex(label: &str, bytecode: &str, container_path: &str) -> anyhow::Result<Deployed> {
224 let deployer = deployer().await?;
225 let mut hex_file =
226 NamedTempFile::new_in(std::env::temp_dir()).context("staging the contract bytecode")?;
227 hex_file.write_all(bytecode.trim().as_bytes())?;
228 hex_file.flush()?;
229 docker(&[
230 "cp",
231 &hex_file.path().to_string_lossy(),
232 &format!("lotus:{container_path}"),
233 ])?;
234
235 let from = deployer.to_string();
236 let deploy =
237 lotus_exec_retrying_transient(&["evm", "deploy", "--from", &from, "--hex", container_path])
238 .await?;
239 let f4 = deploy
240 .lines()
241 .find_map(|l| l.trim().strip_prefix("f4 Address: "))
242 .with_context(|| format!("no `f4 Address:` in {label} deploy output:\n{deploy}"))?;
243 let f4 = Address::from_str(f4.trim()).context("parsing the deployed f4 address")?;
244 eprintln!("deployed {label} at {f4}");
245 poll_until_actor(f4).await?;
246 Ok(Deployed {
247 eth: EthAddress::from_filecoin_address(&f4)?,
248 f4,
249 })
250}
251
252async fn simple_coin() -> anyhow::Result<&'static Deployed> {
253 static CONTRACT: OnceCell<Deployed> = OnceCell::const_new();
254 CONTRACT
255 .get_or_try_init(|| deploy_hex("SimpleCoin", SIMPLE_COIN_HEX, "/tmp/simple_coin.hex"))
256 .await
257}
258
259async fn contract_b() -> anyhow::Result<&'static Deployed> {
260 static CONTRACT: OnceCell<Deployed> = OnceCell::const_new();
261 CONTRACT
262 .get_or_try_init(|| deploy_hex("ContractB", CONTRACT_B_HEX, "/tmp/contract_b.hex"))
263 .await
264}
265
266async fn nested_gas() -> anyhow::Result<&'static Deployed> {
267 static CONTRACT: OnceCell<Deployed> = OnceCell::const_new();
268 CONTRACT
269 .get_or_try_init(|| deploy_hex("NestedGas", NESTED_GAS_HEX, "/tmp/nested_gas_skip.hex"))
270 .await
271}
272
273async fn errors_contract() -> anyhow::Result<&'static Deployed> {
274 static CONTRACT: OnceCell<Deployed> = OnceCell::const_new();
275 CONTRACT
276 .get_or_try_init(|| deploy_hex("Errors", ERRORS_HEX, "/tmp/errors.hex"))
277 .await
278}
279
280struct TableEnv {
282 coin: EthAddress,
283 errors: EthAddress,
284 eoa: EthAddress,
285 eoa2: EthAddress,
286}
287
288async fn table_env() -> anyhow::Result<&'static TableEnv> {
289 static ENV: OnceCell<TableEnv> = OnceCell::const_new();
290 ENV.get_or_try_init(|| async {
291 let coin = simple_coin().await?;
292 let errors = errors_contract().await?;
293 let eoa = new_funded(EOA_FUND_AMT).await?;
294 let eoa2 = new_unfunded().await?;
295 Ok(TableEnv {
296 coin: coin.eth,
297 errors: errors.eth,
298 eoa: eoa.eth,
299 eoa2: eoa2.eth,
300 })
301 })
302 .await
303}
304
305async fn linked_contracts() -> anyhow::Result<&'static (Deployed, Deployed)> {
307 static LINKED: OnceCell<(Deployed, Deployed)> = OnceCell::const_new();
308 LINKED
309 .get_or_try_init(|| async {
310 let b = contract_b().await?;
311 let a = deploy_hex("ContractA", CONTRACT_A_HEX, "/tmp/contract_a.hex").await?;
312 invoke(&a.f4, &set_contract_b_calldata(b.eth)).await?;
313 Ok((a, *b))
314 })
315 .await
316}
317
318async fn poll_until_actor(addr: Address) -> anyhow::Result<ActorState> {
319 poll_until_actor_on("forest", addr, forest_client).await
320}
321
322async fn poll_until_lotus_actor(addr: Address) -> anyhow::Result<ActorState> {
325 poll_until_actor_on("lotus", addr, lotus_client).await
326}
327
328async fn fund_on_chain(cli_addr: &str, amount: &str) -> anyhow::Result<Address> {
333 let addr = Address::from_str(cli_addr).context("parsing funded delegated address")?;
334 let msg = send_from(
335 &FOREST_TEST_PRELOADED_ADDRESS,
336 cli_addr,
337 amount,
338 Backend::Local,
339 )?;
340 eprintln!("funding {cli_addr} with {amount}, msg: {msg}");
341 let balance = poll_until_funded(cli_addr, Backend::Local).await?;
342 eprintln!("{cli_addr} funded on forest, balance: {balance}");
343 poll_until_lotus_actor(addr).await?;
344 Ok(addr)
345}
346
347async fn wait_for_cid(forest: &Client, cid: Cid) -> anyhow::Result<()> {
348 let lookup = poll_until_message_executed(forest, cid).await?;
349 let exit = lookup.receipt.exit_code();
350 ensure!(
351 exit.is_success(),
352 "message {cid} failed on chain with exit code {exit}"
353 );
354 Ok(())
355}
356
357async fn lotus_send(
358 from: &Address,
359 to: &Address,
360 calldata: &[u8],
361 gas_limit: Option<u64>,
362) -> anyhow::Result<Cid> {
363 let forest = forest_client()?;
364 let from_s = from.to_string();
365 let to_s = to.to_string();
366 let params = hex::encode(calldata);
367 let gas = gas_limit.map(|g| g.to_string());
368 let mut args = vec![
369 "send",
370 "--from",
371 from_s.as_str(),
372 "--params-hex",
373 params.as_str(),
374 ];
375 if let Some(gas) = gas.as_deref() {
376 args.extend(["--gas-limit", gas]);
377 }
378 args.extend([to_s.as_str(), "0"]);
379 let out = lotus_exec_retrying_transient(&args).await?;
380 let cid = Cid::from_str(
381 out.lines()
382 .last()
383 .context("no cid from `lotus send`")?
384 .trim(),
385 )?;
386 if let Some(limit) = gas_limit {
387 eprintln!("submitted at estimate {limit}: {cid}");
388 wait_for_cid(&forest, cid)
389 .await
390 .with_context(|| format!("transaction submitted at eth_estimateGas {limit} failed"))?;
391 } else {
392 wait_for_cid(&forest, cid).await?;
393 }
394 Ok(cid)
395}
396
397async fn invoke(to: &Address, calldata: &[u8]) -> anyhow::Result<Cid> {
398 lotus_send(deployer().await?, to, calldata, None).await
399}
400
401async fn submit_at_gas_limit(
402 from: &Address,
403 to: &Address,
404 calldata: &[u8],
405 gas_limit: u64,
406) -> anyhow::Result<()> {
407 lotus_send(from, to, calldata, Some(gas_limit)).await?;
408 Ok(())
409}
410
411async fn eth_call_msg(
412 client: &Client,
413 msg: EthCallMessage,
414 block: BlockNumberOrHash,
415) -> anyhow::Result<EthBytes> {
416 Ok(client.call(EthCall::request((msg, block))?).await?)
417}
418
419async fn estimate_msg(client: &Client, msg: EthCallMessage) -> anyhow::Result<u64> {
420 Ok(client
421 .call(EthEstimateGas::request((msg, Some(latest())))?)
422 .await?
423 .0)
424}
425
426fn rpc_call_err(err: &anyhow::Error) -> Option<&jsonrpsee::types::ErrorObjectOwned> {
427 match err.downcast_ref::<ClientError>() {
428 Some(ClientError::Call(obj)) => Some(obj),
429 _ => None,
430 }
431}
432
433fn rpc_data(obj: &jsonrpsee::types::ErrorObjectOwned) -> Option<String> {
434 let raw = obj.data()?;
435 serde_json::from_str::<String>(raw.get())
436 .ok()
437 .or_else(|| Some(raw.get().trim_matches('"').to_string()))
438}
439
440#[derive(Clone)]
441enum Expect {
442 Success,
443 SuccessGas,
444 ErrContains(&'static str),
445 Reverted {
446 msg: &'static str,
447 data_contains: Option<String>,
448 data_eq: Option<&'static str>,
449 },
450 ErrCode {
451 code: i32,
452 contains: &'static str,
453 },
454}
455
456struct SkipSenderCase {
457 name: &'static str,
458 msg: EthCallMessage,
459 call: Option<Expect>,
460 estimate: Option<Expect>,
461}
462
463impl SkipSenderCase {
464 fn success(name: &'static str, msg: EthCallMessage) -> Self {
465 Self {
466 name,
467 msg,
468 call: Some(Expect::Success),
469 estimate: Some(Expect::SuccessGas),
470 }
471 }
472
473 fn err_both(name: &'static str, msg: EthCallMessage, needle: &'static str) -> Self {
474 Self {
475 name,
476 msg,
477 call: Some(Expect::ErrContains(needle)),
478 estimate: Some(Expect::ErrContains(needle)),
479 }
480 }
481
482 fn revert_both(name: &'static str, msg: EthCallMessage, expect: Expect) -> Self {
483 Self {
484 name,
485 msg,
486 call: Some(expect.clone()),
487 estimate: Some(expect),
488 }
489 }
490
491 fn call_only(name: &'static str, msg: EthCallMessage, expect: Expect) -> Self {
492 Self {
493 name,
494 msg,
495 call: Some(expect),
496 estimate: None,
497 }
498 }
499}
500
501fn fil(whole: u64) -> EthBigInt {
502 EthBigInt::from(TokenAmount::from_whole(whole))
503}
504
505fn skip_sender_cases(env: &TableEnv) -> anyhow::Result<Vec<SkipSenderCase>> {
506 let initcode = simple_coin_initcode()?;
507 let missing = non_existent(0x01)?;
508 let gas_price = Some(EthBigInt::from(GAS_PRICE));
509 let custom = hex::encode(selector("CustomError()"));
510 let revert_empty = Expect::Reverted {
511 msg: "none",
512 data_contains: None,
513 data_eq: Some("0x"),
514 };
515 let oog_err = Expect::ErrCode {
516 code: EXECUTION_REVERTED_CODE,
517 contains: "SysErrOutOfGas",
518 };
519
520 let transfer = |from: Option<EthAddress>, to: Option<EthAddress>| EthCallMessage {
521 from,
522 to,
523 ..Default::default()
524 };
525 let errors_from_eoa = |sig: &'static str| EthCallMessage {
526 from: Some(env.eoa),
527 to: Some(env.errors),
528 data: Some(EthBytes(selector(sig))),
529 ..Default::default()
530 };
531
532 Ok(vec![
533 SkipSenderCase::err_both(
534 "CreateFromContract",
535 EthCallMessage {
536 from: Some(env.coin),
537 to: None,
538 data: Some(initcode.clone()),
539 ..Default::default()
540 },
541 "disallowed caller",
542 ),
543 SkipSenderCase::success(
544 "CreateFromNonExistent",
545 EthCallMessage {
546 from: Some(missing),
547 to: None,
548 data: Some(initcode),
549 ..Default::default()
550 },
551 ),
552 SkipSenderCase::revert_both(
553 "OutOfGasFromNonExistent",
554 oog_create(Some(missing))?,
555 oog_err.clone(),
556 ),
557 SkipSenderCase {
558 name: "OutOfGasFromEoa",
559 msg: oog_create(Some(env.eoa))?,
560 call: Some(oog_err),
561 estimate: Some(Expect::ErrCode {
562 code: OUT_OF_GAS_CODE,
563 contains: "call ran out of gas",
564 }),
565 },
566 SkipSenderCase::success("FromContract", transfer(Some(env.coin), Some(env.eoa))),
567 SkipSenderCase::success(
568 "FromContractWithGasPrice",
569 EthCallMessage {
570 from: Some(env.coin),
571 to: Some(env.eoa),
572 gas_price,
573 ..Default::default()
574 },
575 ),
576 SkipSenderCase::success(
577 "FromContractToSelf",
578 EthCallMessage {
579 from: Some(env.coin),
580 to: Some(env.coin),
581 data: Some(EthBytes(get_balance_calldata(env.coin))),
582 ..Default::default()
583 },
584 ),
585 SkipSenderCase::err_both(
586 "FromContractWithValue",
587 EthCallMessage {
588 from: Some(env.coin),
589 to: Some(env.eoa),
590 value: Some(fil(1)),
591 ..Default::default()
592 },
593 "insufficient",
594 ),
595 SkipSenderCase::success("FromNonExistent", transfer(Some(missing), Some(env.eoa))),
596 SkipSenderCase::success(
597 "FromNonExistentWithGasPrice",
598 EthCallMessage {
599 from: Some(missing),
600 to: Some(env.eoa),
601 gas_price,
602 ..Default::default()
603 },
604 ),
605 SkipSenderCase::revert_both(
606 "FromNonExistentToContractWithData",
607 EthCallMessage {
608 from: Some(missing),
609 to: Some(env.errors),
610 data: Some(EthBytes(selector(FAIL_REVERT_EMPTY))),
611 ..Default::default()
612 },
613 revert_empty.clone(),
614 ),
615 SkipSenderCase::call_only(
616 "FromNonExistentWithValue",
617 EthCallMessage {
618 from: Some(missing),
619 to: Some(env.eoa),
620 value: Some(fil(1)),
621 ..Default::default()
622 },
623 Expect::ErrContains("insufficient"),
624 ),
625 SkipSenderCase::success("FromEOA", transfer(Some(env.eoa), Some(env.eoa2))),
626 SkipSenderCase::call_only("FromNil", transfer(None, Some(env.eoa)), Expect::Success),
627 SkipSenderCase::call_only(
628 "ValueOverBalance",
629 EthCallMessage {
630 from: Some(env.eoa),
631 to: Some(missing),
632 value: Some(fil(11)),
633 ..Default::default()
634 },
635 Expect::ErrContains("insufficient"),
636 ),
637 SkipSenderCase::revert_both(
638 "RevertDivideByZero",
639 errors_from_eoa(FAIL_DIV_ZERO),
640 Expect::Reverted {
641 msg: "DivideByZero",
642 data_contains: Some(PANIC_DIV_ZERO.to_string()),
643 data_eq: None,
644 },
645 ),
646 SkipSenderCase::revert_both(
647 "RevertAssert",
648 errors_from_eoa(FAIL_ASSERT),
649 Expect::Reverted {
650 msg: "Assert",
651 data_contains: Some(PANIC_ASSERT.to_string()),
652 data_eq: None,
653 },
654 ),
655 SkipSenderCase::revert_both(
656 "RevertWithReason",
657 errors_from_eoa(FAIL_REVERT_REASON),
658 Expect::Reverted {
659 msg: "my reason",
660 data_contains: None,
661 data_eq: None,
662 },
663 ),
664 SkipSenderCase::revert_both(
665 "RevertEmpty",
666 errors_from_eoa(FAIL_REVERT_EMPTY),
667 revert_empty,
668 ),
669 SkipSenderCase::revert_both(
670 "RevertCustomError",
671 errors_from_eoa(FAIL_CUSTOM),
672 Expect::Reverted {
673 msg: "",
674 data_contains: Some(custom),
675 data_eq: None,
676 },
677 ),
678 ])
679}
680
681fn assert_expect(
682 label: &str,
683 result: Result<u64, anyhow::Error>,
684 expect: &Expect,
685) -> anyhow::Result<()> {
686 match expect {
687 Expect::Success => {
688 result.with_context(|| format!("{label}: expected success"))?;
689 Ok(())
690 }
691 Expect::SuccessGas => {
692 let gas = result.with_context(|| format!("{label}: expected a gas estimate"))?;
693 ensure!(
694 gas >= MIN_ESTIMATE_GAS,
695 "{label}: estimate {gas} is below the 21_000 transfer floor"
696 );
697 ensure!(
698 gas < MAX_ESTIMATE_GAS,
699 "{label}: estimate {gas} looks like an overflow"
700 );
701 Ok(())
702 }
703 Expect::ErrContains(needle) => {
704 let err = result.err().with_context(|| {
705 format!("{label}: expected an error containing `{needle}`, but the call succeeded")
706 })?;
707 let text = match rpc_call_err(&err) {
708 Some(obj) => {
709 let mut s = obj.message().to_string();
710 if let Some(data) = rpc_data(obj) {
711 s.push(' ');
712 s.push_str(&data);
713 }
714 s
715 }
716 None => err.to_string(),
717 };
718 ensure!(
719 text.to_ascii_lowercase()
720 .contains(&needle.to_ascii_lowercase()),
721 "{label}: error `{text}` does not contain `{needle}`"
722 );
723 Ok(())
724 }
725 Expect::Reverted {
726 msg,
727 data_contains,
728 data_eq,
729 } => {
730 let err = result.err().with_context(|| {
731 format!("{label}: expected execution reverted, but the call succeeded")
732 })?;
733 let obj = rpc_call_err(&err)
734 .with_context(|| format!("{label}: expected a JSON-RPC error, got {err:#}"))?;
735 ensure!(
736 obj.code() == EXECUTION_REVERTED_CODE,
737 "{label}: expected execution-reverted code {EXECUTION_REVERTED_CODE}, got {}: {}",
738 obj.code(),
739 obj.message()
740 );
741 if !msg.is_empty() {
742 ensure!(
743 obj.message().contains(msg),
744 "{label}: revert message `{}` does not contain `{msg}`",
745 obj.message()
746 );
747 }
748 let data = rpc_data(obj).unwrap_or_default();
749 if let Some(want) = data_eq {
750 ensure!(data == *want, "{label}: revert data `{data}` != `{want}`");
751 }
752 if let Some(want) = data_contains {
753 ensure!(
754 data.contains(want),
755 "{label}: revert data `{data}` does not contain `{want}`"
756 );
757 }
758 Ok(())
759 }
760 Expect::ErrCode { code, contains } => {
761 let err = result.err().with_context(|| {
762 format!("{label}: expected error code {code}, but the call succeeded")
763 })?;
764 let obj = rpc_call_err(&err)
765 .with_context(|| format!("{label}: expected a JSON-RPC error, got {err:#}"))?;
766 ensure!(
767 obj.code() == *code,
768 "{label}: expected error code {code}, got {}: {}",
769 obj.code(),
770 obj.message()
771 );
772 ensure!(
773 obj.message().contains(contains),
774 "{label}: error `{}` does not contain `{contains}`",
775 obj.message()
776 );
777 Ok(())
778 }
779 }
780}
781
782async fn call_skip_sender() -> anyhow::Result<()> {
783 let forest = forest_client()?;
784 let env = table_env().await?;
785 for case in skip_sender_cases(env)? {
786 let Some(expect) = case.call else {
787 continue;
788 };
789 let label = format!("eth_call {}", case.name);
790 let result = eth_call_msg(&forest, case.msg, latest())
791 .await
792 .map(|_| 0u64);
793 assert_expect(&label, result, &expect)?;
794 }
795 Ok(())
796}
797
798async fn estimate_gas_skip_sender() -> anyhow::Result<()> {
799 let forest = forest_client()?;
800 let env = table_env().await?;
801 for case in skip_sender_cases(env)? {
802 let Some(expect) = case.estimate else {
803 continue;
804 };
805 let label = format!("eth_estimateGas {}", case.name);
806 assert_expect(&label, estimate_msg(&forest, case.msg).await, &expect)?;
807 }
808 Ok(())
809}
810
811async fn funded_placeholder_sender() -> anyhow::Result<()> {
812 let forest = forest_client()?;
813 let from = new_funded(PLACEHOLDER_FUND_AMT).await?;
814 let to = new_funded(ROUND_TRIP_FUND_AMT).await?;
815 eth_call_msg(
816 &forest,
817 EthCallMessage {
818 from: Some(from.eth),
819 to: Some(to.eth),
820 value: Some(fil(1)),
821 ..Default::default()
822 },
823 latest(),
824 )
825 .await
826 .context("value-bearing eth_call from a funded placeholder")?;
827 Ok(())
828}
829
830async fn estimate_gas(
831 client: &Client,
832 from: EthAddress,
833 to: EthAddress,
834 data: Vec<u8>,
835) -> anyhow::Result<u64> {
836 estimate_msg(
837 client,
838 EthCallMessage {
839 from: Some(from),
840 to: Some(to),
841 data: Some(EthBytes(data)),
842 ..Default::default()
843 },
844 )
845 .await
846}
847
848async fn eth_call(
849 client: &Client,
850 from: EthAddress,
851 to: EthAddress,
852 data: Vec<u8>,
853 block: BlockNumberOrHash,
854) -> anyhow::Result<EthBytes> {
855 eth_call_msg(
856 client,
857 EthCallMessage {
858 from: Some(from),
859 to: Some(to),
860 data: (!data.is_empty()).then_some(EthBytes(data)),
861 ..Default::default()
862 },
863 block,
864 )
865 .await
866}
867
868fn within_parity(skip: u64, funded: u64) -> bool {
869 let denom = funded.max(1) as f64;
870 (skip as f64 - funded as f64).abs() / denom <= ESTIMATE_PARITY
871}
872
873async fn new_unfunded() -> anyhow::Result<Wallet> {
874 let cli = lotus_exec(&["wallet", "new", "delegated"])?;
875 let f4 = Address::from_str(&cli).context("parsing unfunded delegated address")?;
876 let eth = EthAddress::from_filecoin_address(&f4)?;
877 Ok(Wallet { cli, f4, eth })
878}
879
880async fn new_funded(amount: &str) -> anyhow::Result<Wallet> {
881 let wallet = new_unfunded().await?;
882 fund_on_chain(&wallet.cli, amount).await?;
883 Ok(wallet)
884}
885
886async fn round_trip_from_unfunded() -> anyhow::Result<()> {
887 let forest = forest_client()?;
888 let coin = simple_coin().await?;
889 let recipient = non_existent(0x01)?;
890 let calldata = send_coin_calldata(recipient, 0);
891
892 let from = new_unfunded().await?;
893 let gas = estimate_gas(&forest, from.eth, coin.eth, calldata.clone())
894 .await
895 .context("eth_estimateGas from unfunded sender")?;
896 eprintln!("skip-sender estimate {gas} from {}", from.cli);
897
898 ensure!(
899 get_actor(&forest, from.f4).await?.is_none(),
900 "ephemeral placeholder for {} leaked onto chain during estimate",
901 from.f4
902 );
903
904 fund_on_chain(&from.cli, ROUND_TRIP_FUND_AMT).await?;
905 let actor = poll_until_actor(from.f4).await?;
906 ensure!(
907 actor.sequence == 0,
908 "pre-submit nonce of {} is {}, expected 0 (placeholder must not have incremented it)",
909 from.f4,
910 actor.sequence
911 );
912
913 submit_at_gas_limit(&from.f4, &coin.f4, &calldata, gas).await?;
914 let after = get_actor(&forest, from.f4)
915 .await?
916 .with_context(|| format!("actor {} missing after successful submit", from.f4))?;
917 ensure!(
918 after.sequence == 1,
919 "successful tx must use nonce 0; on-chain nonce is now {}",
920 after.sequence
921 );
922 Ok(())
923}
924
925async fn parity_with_existing_sender() -> anyhow::Result<()> {
926 let forest = forest_client()?;
927 let coin = simple_coin().await?;
928 let calldata = send_coin_calldata(non_existent(0x01)?, 0);
929
930 let skip = estimate_gas(&forest, non_existent(0x42)?, coin.eth, calldata.clone())
931 .await
932 .context("eth_estimateGas from missing from")?;
933 let placeholder = new_funded(ROUND_TRIP_FUND_AMT).await?;
934 let funded = estimate_gas(&forest, placeholder.eth, coin.eth, calldata)
935 .await
936 .context("eth_estimateGas from funded placeholder")?;
937 eprintln!("parity skip={skip} funded={funded}");
938 ensure!(
939 within_parity(skip, funded),
940 "skip-sender estimate {skip} vs funded-placeholder {funded} exceeds 10%"
941 );
942 Ok(())
943}
944
945async fn round_trip_recursive() -> anyhow::Result<()> {
946 let forest = forest_client()?;
947 let nested = nested_gas().await?;
948 let calldata = recurse_calldata(NESTED_DEPTH);
949
950 let from = new_unfunded().await?;
951 let gas = estimate_gas(&forest, from.eth, nested.eth, calldata.clone())
952 .await
953 .context("skip-sender eth_estimateGas recurse(100)")?;
954
955 let placeholder = new_funded(RECURSIVE_FUND_AMT).await?;
956 let funded = estimate_gas(&forest, placeholder.eth, nested.eth, calldata.clone())
957 .await
958 .context("funded-placeholder eth_estimateGas recurse(100)")?;
959 eprintln!("recursive skip={gas} funded={funded}");
960 ensure!(
961 within_parity(gas, funded),
962 "recursive skip-sender estimate {gas} vs funded-placeholder {funded} exceeds 10%"
963 );
964
965 fund_on_chain(&from.cli, RECURSIVE_FUND_AMT).await?;
966 submit_at_gas_limit(&from.f4, &nested.f4, &calldata, gas).await
967}
968
969async fn call_sender_identity() -> anyhow::Result<()> {
970 let forest = forest_client()?;
971 let coin = simple_coin().await?;
972 let sender_contract = contract_b().await?;
973 let with_coins = non_existent(0x21)?;
974 let without_coins = non_existent(0x22)?;
975 let recipient = non_existent(0x01)?;
976
977 for to in [sender_contract.eth, with_coins] {
978 invoke(&coin.f4, &send_coin_calldata(to, 100)).await?;
979 }
980
981 let spend = send_coin_calldata(recipient, 10);
982 for (label, from, want) in [
983 ("contract from", sender_contract.eth, 1u8),
984 ("credited missing from", with_coins, 1),
985 ("uncounted missing from", without_coins, 0),
986 ] {
987 let ret = eth_call(&forest, from, coin.eth, spend.clone(), latest())
988 .await
989 .with_context(|| format!("eth_call sendCoin from {label}"))?;
990 ensure!(
991 ret.0.len() == 32,
992 "{label}: sendCoin return must be a 32-byte ABI bool, got {} bytes",
993 ret.0.len()
994 );
995 ensure!(
996 ret.0.last() == Some(&want),
997 "{label}: callee must observe the requested from as msg.sender (want {want}, got {ret:?})"
998 );
999 }
1000 Ok(())
1001}
1002
1003async fn skip_sender_state_isolation() -> anyhow::Result<()> {
1004 let forest = forest_client()?;
1005 let to = EthAddress::from_filecoin_address(deployer().await?)?;
1006 let from = non_existent(0x03)?;
1007
1008 let first = eth_call(&forest, from, to, Vec::new(), latest()).await?;
1009 let second = eth_call(&forest, from, to, Vec::new(), latest()).await?;
1010 ensure!(
1011 first == second,
1012 "repeated skip-sender eth_call results must match"
1013 );
1014
1015 let fil = from.to_filecoin_address()?;
1016 ensure!(
1017 get_actor(&forest, fil).await?.is_none(),
1018 "skip-sender eth_call must not persist an actor for {fil}"
1019 );
1020
1021 let mut futs = Vec::with_capacity(8);
1022 for _ in 0..8 {
1023 futs.push(async move {
1024 let client = forest_client()?;
1025 eth_call(&client, from, to, Vec::new(), latest()).await
1026 });
1027 }
1028 let concurrent = futures::future::try_join_all(futs).await?;
1029 for (i, got) in concurrent.iter().enumerate() {
1030 ensure!(
1031 *got == first,
1032 "concurrent skip-sender eth_call {i} diverged from the first result"
1033 );
1034 }
1035 Ok(())
1036}
1037
1038async fn skip_sender_historical_call() -> anyhow::Result<()> {
1039 let forest = forest_client()?;
1040 let to = EthAddress::from_filecoin_address(deployer().await?)?;
1041 let from = non_existent(0x03)?;
1042 let head = forest
1043 .call(EthBlockNumber::request(())?)
1044 .await
1045 .map_err(|e| anyhow::anyhow!("{e:#}"))?;
1046 ensure!(
1047 head.0 > 2,
1048 "devnet head {} is too low for a head-2 historical eth_call",
1049 head.0
1050 );
1051 let hist = BlockNumberOrHash::from_block_number((head.0 - 2) as i64);
1052 eth_call(&forest, from, to, Vec::new(), hist)
1053 .await
1054 .context("historical skip-sender eth_call at head-2")?;
1055 Ok(())
1056}
1057
1058async fn assert_call_b(
1059 from: EthAddress,
1060 sig: &str,
1061 expected: u8,
1062 label: &str,
1063) -> anyhow::Result<()> {
1064 let forest = forest_client()?;
1065 let (a, _) = linked_contracts().await?;
1066 assert_abi_u256(
1067 eth_call(&forest, from, a.eth, selector(sig), latest())
1068 .await
1069 .with_context(|| label.to_string())?,
1070 expected,
1071 label,
1072 )
1073}
1074
1075async fn cross_contract_from_contract() -> anyhow::Result<()> {
1076 let (_, b) = linked_contracts().await?;
1077 assert_call_b(
1078 b.eth,
1079 CALL_B_AND_READ_BACK,
1080 42,
1081 "cross-contract callback from contract from",
1082 )
1083 .await
1084}
1085
1086async fn cross_contract_from_missing() -> anyhow::Result<()> {
1087 assert_call_b(
1088 non_existent(0x10)?,
1089 CALL_B_AND_READ_BACK,
1090 42,
1091 "cross-contract callback from missing from",
1092 )
1093 .await
1094}
1095
1096async fn cross_contract_from_eoa() -> anyhow::Result<()> {
1097 let from = new_funded(ROUND_TRIP_FUND_AMT).await?;
1098 assert_call_b(
1099 from.eth,
1100 CALL_B_AND_READ_BACK,
1101 42,
1102 "cross-contract callback from EOA from",
1103 )
1104 .await
1105}
1106
1107async fn cross_contract_double_callback() -> anyhow::Result<()> {
1108 assert_call_b(
1109 non_existent(0x11)?,
1110 CALL_B_AND_DOUBLE,
1111 84,
1112 "cross-contract double callback from missing from",
1113 )
1114 .await
1115}
1116
1117fn assert_abi_u256(ret: EthBytes, expected: u8, label: &str) -> anyhow::Result<()> {
1118 ensure!(
1119 ret.0.len() == 32,
1120 "{label}: expected 32-byte ABI uint256, got {} bytes",
1121 ret.0.len()
1122 );
1123 ensure!(
1124 ret.0.last() == Some(&expected),
1125 "{label}: expected {expected}, got {ret:?}"
1126 );
1127 Ok(())
1128}