Skip to main content

forest/tool/subcommands/api_cmd/
api_compare_tests.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use super::{CreateTestsArgs, ReportMode, RunIgnored, TestCriteriaOverride};
5use crate::blocks::{ElectionProof, Ticket, Tipset};
6use crate::chain::ChainStore;
7use crate::db::car::ManyCar;
8use crate::eth::{EthChainId as EthChainIdType, SAFE_EPOCH_DELAY};
9use crate::lotus_json::HasLotusJson;
10use crate::message::{Message as _, SignedMessage};
11use crate::rpc::FilterList;
12use crate::rpc::auth::AuthNewParams;
13use crate::rpc::beacon::BeaconGetEntry;
14use crate::rpc::eth::{
15    BlockNumberOrHash, EthInt64, ExtBlockNumberOrHash, ExtPredefined, Predefined,
16    new_eth_tx_from_signed_message, types::*,
17};
18use crate::rpc::gas::{GasEstimateGasLimit, GasEstimateMessageGas};
19use crate::rpc::miner::BlockTemplate;
20use crate::rpc::misc::ActorEventFilter;
21use crate::rpc::state::StateGetAllClaims;
22use crate::rpc::types::*;
23use crate::rpc::{Permission, prelude::*};
24use crate::shim::actors::MarketActorStateLoad as _;
25use crate::shim::actors::market;
26use crate::shim::executor::Receipt;
27use crate::shim::sector::SectorSize;
28use crate::shim::{
29    address::{Address, Protocol},
30    crypto::Signature,
31    econ::TokenAmount,
32    message::{METHOD_SEND, Message},
33    state_tree::StateTree,
34};
35use crate::state_manager::StateManager;
36use crate::tool::offline_server::server::handle_chain_config;
37use crate::tool::subcommands::api_cmd::NetworkChain;
38use crate::tool::subcommands::api_cmd::report::ReportBuilder;
39use crate::tool::subcommands::api_cmd::state_decode_params_tests::create_all_state_decode_params_tests;
40use crate::utils::proofs_api::{self, ensure_proof_params_downloaded};
41use crate::{Config, rpc};
42use ahash::HashMap;
43use bls_signatures::Serialize as _;
44use chrono::Utc;
45use cid::Cid;
46use fil_actors_shared::fvm_ipld_bitfield::BitField;
47use fil_actors_shared::v10::runtime::DomainSeparationTag;
48use fvm_ipld_blockstore::Blockstore;
49use ipld_core::ipld::Ipld;
50use itertools::Itertools as _;
51use jsonrpsee::types::ErrorCode;
52use libp2p::PeerId;
53use libsecp256k1::{PublicKey, SecretKey};
54use num_traits::Signed;
55use serde::de::DeserializeOwned;
56use serde::{Deserialize, Serialize};
57use serde_json::Value;
58use similar::{ChangeTag, TextDiff};
59use std::borrow::Cow;
60use std::path::Path;
61use std::time::Instant;
62use std::{
63    path::PathBuf,
64    str::FromStr,
65    sync::{Arc, LazyLock},
66    time::Duration,
67};
68use tokio::sync::Semaphore;
69use tokio::task::JoinSet;
70use tracing::debug;
71
72const COLLECTION_SAMPLE_SIZE: usize = 5;
73
74/// This address has been funded by the calibnet faucet and the private keys
75/// has been discarded. It should always have a non-zero balance.
76static KNOWN_CALIBNET_ADDRESS: LazyLock<Address> = LazyLock::new(|| {
77    crate::shim::address::Network::Testnet
78        .parse_address("t1c4dkec3qhrnrsa4mccy7qntkyq2hhsma4sq7lui")
79        .unwrap()
80        .into()
81});
82
83/// This address is known to be empty on calibnet. It should always have a zero balance.
84static KNOWN_EMPTY_CALIBNET_ADDRESS: LazyLock<Address> = LazyLock::new(|| {
85    crate::shim::address::Network::Testnet
86        .parse_address("t1qb2x5qctp34rxd7ucl327h5ru6aazj2heno7x5y")
87        .unwrap()
88        .into()
89});
90
91// this is the ID address of the `t1w2zb5a723izlm4q3khclsjcnapfzxcfhvqyfoly` address
92static KNOWN_CALIBNET_F0_ADDRESS: LazyLock<Address> = LazyLock::new(|| {
93    crate::shim::address::Network::Testnet
94        .parse_address("t0168923")
95        .unwrap()
96        .into()
97});
98
99static KNOWN_CALIBNET_F1_ADDRESS: LazyLock<Address> = LazyLock::new(|| {
100    crate::shim::address::Network::Testnet
101        .parse_address("t1w2zb5a723izlm4q3khclsjcnapfzxcfhvqyfoly")
102        .unwrap()
103        .into()
104});
105
106static KNOWN_CALIBNET_F2_ADDRESS: LazyLock<Address> = LazyLock::new(|| {
107    crate::shim::address::Network::Testnet
108        .parse_address("t2nfplhzpyeck5dcc4fokj5ar6nbs3mhbdmq6xu3q")
109        .unwrap()
110        .into()
111});
112
113static KNOWN_CALIBNET_F3_ADDRESS: LazyLock<Address> = LazyLock::new(|| {
114    crate::shim::address::Network::Testnet
115        .parse_address("t3wmbvnabsj6x2uki33phgtqqemmunnttowpx3chklrchy76pv52g5ajnaqdypxoomq5ubfk65twl5ofvkhshq")
116        .unwrap()
117        .into()
118});
119
120static KNOWN_CALIBNET_F4_ADDRESS: LazyLock<Address> = LazyLock::new(|| {
121    crate::shim::address::Network::Testnet
122        .parse_address("t410fx2cumi6pgaz64varl77xbuub54bgs3k5xsvn3ki")
123        .unwrap()
124        .into()
125});
126
127fn generate_eth_random_address() -> anyhow::Result<EthAddress> {
128    let rng = &mut crate::utils::rand::forest_os_rng();
129    let secret_key = SecretKey::random(rng);
130    let public_key = PublicKey::from_secret_key(&secret_key);
131    EthAddress::eth_address_from_pub_key(&public_key.serialize())
132}
133
134const TICKET_QUALITY_GREEDY: f64 = 0.9;
135const TICKET_QUALITY_OPTIMAL: f64 = 0.8;
136const ZERO_ADDRESS: &str = "0x0000000000000000000000000000000000000000";
137// miner actor address `t078216`
138const MINER_ADDRESS: Address = Address::new_id(78216); // https://calibration.filscan.io/en/miner/t078216
139const ACCOUNT_ADDRESS: Address = Address::new_id(1234); // account actor address `t01234`
140const EVM_ADDRESS: &str = "t410fbqoynu2oi2lxam43knqt6ordiowm2ywlml27z4i";
141
142/// Brief description of a single method call against a single host
143#[derive(
144    Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize, strum::Display,
145)]
146#[serde(rename_all = "snake_case")]
147pub enum TestSummary {
148    /// Server spoke JSON-RPC: no such method
149    MissingMethod,
150    /// Server spoke JSON-RPC: bad request (or other error)
151    Rejected(String),
152    /// Server doesn't seem to be speaking JSON-RPC
153    NotJsonRPC,
154    /// Transport or ask task management errors
155    InfraError,
156    /// Server returned JSON-RPC and it didn't match our schema
157    BadJson,
158    /// Server returned JSON-RPC, and it matched our schema, but failed validation
159    CustomCheckFailed,
160    /// Server timed out
161    Timeout,
162    /// Server returned JSON-RPC, and it matched our schema, and passed validation
163    Valid,
164}
165
166impl TestSummary {
167    fn from_err(err: &rpc::ClientError) -> Self {
168        match err {
169            rpc::ClientError::Call(it) => match it.code().into() {
170                ErrorCode::MethodNotFound => Self::MissingMethod,
171                _ => {
172                    // `lotus-gateway` adds `RPC error (-32603):` prefix to the error message that breaks tests,
173                    // normalize the error message first
174                    let message = normalized_error_message(it.message());
175                    Self::Rejected(message.to_string())
176                }
177            },
178            rpc::ClientError::ParseError(_) => Self::NotJsonRPC,
179            rpc::ClientError::RequestTimeout => Self::Timeout,
180            rpc::ClientError::Transport(_)
181            | rpc::ClientError::RestartNeeded(_)
182            | rpc::ClientError::InvalidSubscriptionId
183            | rpc::ClientError::InvalidRequestId(_)
184            | rpc::ClientError::Custom(_)
185            | rpc::ClientError::HttpNotImplemented
186            | rpc::ClientError::EmptyBatchRequest(_)
187            | rpc::ClientError::RegisterMethod(_) => Self::InfraError,
188            _ => unimplemented!(),
189        }
190    }
191}
192
193/// Data about a failed test. Used for debugging.
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct TestDump {
196    pub request: rpc::Request,
197    pub path: rpc::ApiPaths,
198    pub forest_response: Result<Value, String>,
199    pub lotus_response: Result<Value, String>,
200}
201
202impl std::fmt::Display for TestDump {
203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        writeln!(f, "Request path: {}", self.path.path())?;
205        writeln!(f, "Request dump: {:?}", self.request)?;
206        writeln!(f, "Request params JSON: {}", self.request.params)?;
207        let (forest_response, lotus_response) = (
208            self.forest_response
209                .as_ref()
210                .ok()
211                .and_then(|v| serde_json::to_string_pretty(v).ok()),
212            self.lotus_response
213                .as_ref()
214                .ok()
215                .and_then(|v| serde_json::to_string_pretty(v).ok()),
216        );
217        if let Some(forest_response) = &forest_response
218            && let Some(lotus_response) = &lotus_response
219        {
220            let diff = TextDiff::from_lines(forest_response, lotus_response);
221            let mut print_diff = Vec::new();
222            for change in diff.iter_all_changes() {
223                let sign = match change.tag() {
224                    ChangeTag::Delete => "-",
225                    ChangeTag::Insert => "+",
226                    ChangeTag::Equal => " ",
227                };
228                print_diff.push(format!("{sign}{change}"));
229            }
230            writeln!(f, "Forest response: {forest_response}")?;
231            writeln!(f, "Lotus response: {lotus_response}")?;
232            writeln!(f, "Diff: {}", print_diff.join("\n"))?;
233        } else {
234            if let Some(forest_response) = &forest_response {
235                writeln!(f, "Forest response: {forest_response}")?;
236            }
237            if let Some(lotus_response) = &lotus_response {
238                writeln!(f, "Lotus response: {lotus_response}")?;
239            }
240        };
241        Ok(())
242    }
243}
244
245/// Result of running a single RPC test
246pub struct TestResult {
247    /// Forest result after calling the RPC method.
248    pub forest_status: TestSummary,
249    /// Lotus result after calling the RPC method.
250    pub lotus_status: TestSummary,
251    /// Optional data dump if either status was invalid.
252    pub test_dump: Option<TestDump>,
253    /// Duration of the RPC call.
254    pub duration: Duration,
255}
256
257pub(super) enum PolicyOnRejected {
258    Fail,
259    Pass,
260    PassWithIdenticalError,
261    PassWithIdenticalErrorCaseInsensitive,
262    /// If Forest reason is a subset of Lotus reason, the test passes.
263    /// We don't always bubble up errors and format the error chain like Lotus.
264    PassWithQuasiIdenticalError,
265}
266
267pub(super) enum SortPolicy {
268    /// Recursively sorts both arrays and maps in a JSON value.
269    All,
270}
271
272pub(super) struct RpcTest {
273    pub request: rpc::Request,
274    pub check_syntax: Arc<dyn Fn(serde_json::Value) -> bool + Send + Sync>,
275    pub check_semantics: Arc<dyn Fn(serde_json::Value, serde_json::Value) -> bool + Send + Sync>,
276    pub ignore: Option<&'static str>,
277    pub policy_on_rejected: PolicyOnRejected,
278    pub sort_policy: Option<SortPolicy>,
279}
280
281fn sort_json(value: &mut Value) {
282    match value {
283        Value::Array(arr) => {
284            for v in arr.iter_mut() {
285                sort_json(v);
286            }
287            arr.sort_by_key(|a| a.to_string());
288        }
289        Value::Object(obj) => {
290            let mut sorted_map: serde_json::Map<String, Value> = serde_json::Map::new();
291            let mut keys: Vec<String> = obj.keys().cloned().collect();
292            keys.sort();
293            for k in keys {
294                let mut v = obj.remove(&k).unwrap();
295                sort_json(&mut v);
296                sorted_map.insert(k, v);
297            }
298            *obj = sorted_map;
299        }
300        _ => (),
301    }
302}
303
304/// Duplication between `<method>` and `<method>_raw` is a temporary measure, and
305/// should be removed when <https://github.com/ChainSafe/forest/issues/4032> is
306/// completed.
307impl RpcTest {
308    /// Check that an endpoint exists and that both the Lotus and Forest JSON
309    /// response follows the same schema.
310    fn basic<T>(request: rpc::Request<T>) -> Self
311    where
312        T: HasLotusJson,
313    {
314        Self::basic_raw(request.map_ty::<T::LotusJson>())
315    }
316    /// See [Self::basic], and note on this `impl` block.
317    fn basic_raw<T: DeserializeOwned>(request: rpc::Request<T>) -> Self {
318        Self {
319            request: request.map_ty(),
320            check_syntax: Arc::new(|it| match serde_json::from_value::<T>(it) {
321                Ok(_) => true,
322                Err(e) => {
323                    debug!(?e);
324                    false
325                }
326            }),
327            check_semantics: Arc::new(|_, _| true),
328            ignore: None,
329            policy_on_rejected: PolicyOnRejected::Fail,
330            sort_policy: None,
331        }
332    }
333    /// Check that an endpoint exists, has the same JSON schema, and do custom
334    /// validation over both responses.
335    fn validate<T: HasLotusJson>(
336        request: rpc::Request<T>,
337        validate: impl Fn(T, T) -> bool + Send + Sync + 'static,
338    ) -> Self {
339        Self::validate_raw(request.map_ty::<T::LotusJson>(), move |l, r| {
340            validate(T::from_lotus_json(l), T::from_lotus_json(r))
341        })
342    }
343    /// See [Self::validate], and note on this `impl` block.
344    fn validate_raw<T: DeserializeOwned>(
345        request: rpc::Request<T>,
346        validate: impl Fn(T, T) -> bool + Send + Sync + 'static,
347    ) -> Self {
348        Self {
349            request: request.map_ty(),
350            check_syntax: Arc::new(|value| match serde_json::from_value::<T>(value) {
351                Ok(_) => true,
352                Err(e) => {
353                    debug!("{e}");
354                    false
355                }
356            }),
357            check_semantics: Arc::new(move |forest_json, lotus_json| {
358                match (
359                    serde_json::from_value::<T>(forest_json),
360                    serde_json::from_value::<T>(lotus_json),
361                ) {
362                    (Ok(forest), Ok(lotus)) => validate(forest, lotus),
363                    (forest, lotus) => {
364                        if let Err(e) = forest {
365                            debug!("[forest] invalid json: {e}");
366                        }
367                        if let Err(e) = lotus {
368                            debug!("[lotus] invalid json: {e}");
369                        }
370                        false
371                    }
372                }
373            }),
374            ignore: None,
375            policy_on_rejected: PolicyOnRejected::Fail,
376            sort_policy: None,
377        }
378    }
379    /// Check that an endpoint exists and that Forest returns exactly the same
380    /// JSON as Lotus.
381    pub(crate) fn identity<T: PartialEq + HasLotusJson>(request: rpc::Request<T>) -> RpcTest {
382        Self::validate(request, |forest, lotus| forest == lotus)
383    }
384
385    fn ignore(mut self, msg: &'static str) -> Self {
386        self.ignore = Some(msg);
387        self
388    }
389
390    fn policy_on_rejected(mut self, policy: PolicyOnRejected) -> Self {
391        self.policy_on_rejected = policy;
392        self
393    }
394
395    fn sort_policy(mut self, policy: SortPolicy) -> Self {
396        self.sort_policy = Some(policy);
397        self
398    }
399
400    async fn run(&self, forest: &rpc::Client, lotus: &rpc::Client) -> TestResult {
401        let start = Instant::now();
402        let forest_resp = forest.call(self.request.clone()).await;
403        let forest_response = forest_resp.as_ref().map_err(|e| e.to_string()).cloned();
404        let lotus_resp = lotus.call(self.request.clone()).await;
405        let lotus_response = lotus_resp.as_ref().map_err(|e| e.to_string()).cloned();
406
407        let (forest_status, lotus_status) = match (forest_resp, lotus_resp) {
408            (Ok(forest), Ok(lotus))
409                if (self.check_syntax)(forest.clone()) && (self.check_syntax)(lotus.clone()) =>
410            {
411                let (forest, lotus) = if self.sort_policy.is_some() {
412                    let mut sorted_forest = forest.clone();
413                    sort_json(&mut sorted_forest);
414                    let mut sorted_lotus = lotus.clone();
415                    sort_json(&mut sorted_lotus);
416                    (sorted_forest, sorted_lotus)
417                } else {
418                    (forest, lotus)
419                };
420                let forest_status = if (self.check_semantics)(forest, lotus) {
421                    TestSummary::Valid
422                } else {
423                    TestSummary::CustomCheckFailed
424                };
425                (forest_status, TestSummary::Valid)
426            }
427            (forest_resp, lotus_resp) => {
428                let forest_status = forest_resp.map_or_else(
429                    |e| TestSummary::from_err(&e),
430                    |value| {
431                        if (self.check_syntax)(value) {
432                            TestSummary::Valid
433                        } else {
434                            TestSummary::BadJson
435                        }
436                    },
437                );
438                let lotus_status = lotus_resp.map_or_else(
439                    |e| TestSummary::from_err(&e),
440                    |value| {
441                        if (self.check_syntax)(value) {
442                            TestSummary::Valid
443                        } else {
444                            TestSummary::BadJson
445                        }
446                    },
447                );
448
449                (forest_status, lotus_status)
450            }
451        };
452
453        TestResult {
454            forest_status,
455            lotus_status,
456            test_dump: Some(TestDump {
457                request: self.request.clone(),
458                path: self.request.api_path().expect("invalid api paths"),
459                forest_response,
460                lotus_response,
461            }),
462            duration: start.elapsed(),
463        }
464    }
465}
466
467fn common_tests() -> Vec<RpcTest> {
468    vec![
469        // We don't check the `version` field as it differs between Lotus and Forest.
470        RpcTest::validate(Version::request(()).unwrap(), |forest, lotus| {
471            forest.api_version == lotus.api_version && forest.block_delay == lotus.block_delay
472        }),
473        RpcTest::basic(StartTime::request(()).unwrap()),
474        RpcTest::basic(Session::request(()).unwrap()),
475    ]
476}
477
478fn chain_tests() -> Vec<RpcTest> {
479    vec![
480        RpcTest::basic(ChainHead::request(()).unwrap()),
481        RpcTest::identity(ChainGetGenesis::request(()).unwrap()),
482    ]
483}
484
485fn chain_tests_with_tipset<DB: Blockstore>(
486    store: &Arc<DB>,
487    offline: bool,
488    tipset: &Tipset,
489) -> anyhow::Result<Vec<RpcTest>> {
490    let mut tests = vec![
491        RpcTest::identity(ChainGetTipSetByHeight::request((
492            tipset.epoch(),
493            Default::default(),
494        ))?),
495        RpcTest::identity(ChainGetTipSetAfterHeight::request((
496            tipset.epoch(),
497            Default::default(),
498        ))?),
499        RpcTest::identity(ChainGetTipSet::request((tipset.key().into(),))?),
500        RpcTest::identity(ChainGetTipSet::request((None.into(),))?)
501            .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
502        RpcTest::identity(ChainGetTipSetV2::request((TipsetSelector {
503            key: None.into(),
504            height: None,
505            tag: None,
506        },))?)
507        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
508        RpcTest::identity(ChainGetTipSetV2::request((TipsetSelector {
509            key: tipset.key().into(),
510            height: None,
511            tag: Some(TipsetTag::Latest),
512        },))?)
513        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
514        RpcTest::identity(ChainGetTipSetV2::request((TipsetSelector {
515            key: tipset.key().into(),
516            height: None,
517            tag: None,
518        },))?),
519        RpcTest::identity(ChainGetTipSetV2::request((TipsetSelector {
520            key: None.into(),
521            height: Some(TipsetHeight {
522                at: tipset.epoch(),
523                previous: true,
524                anchor: Some(TipsetAnchor {
525                    key: None.into(),
526                    tag: None,
527                }),
528            }),
529            tag: None,
530        },))?),
531        RpcTest::identity(ChainGetTipSetV2::request((TipsetSelector {
532            key: None.into(),
533            height: Some(TipsetHeight {
534                at: tipset.epoch(),
535                previous: true,
536                anchor: None,
537            }),
538            tag: None,
539        },))?)
540        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError)
541        .ignore("this case should pass when F3 is back on calibnet"),
542        validate_tagged_tipset_v2(
543            ChainGetTipSetV2::request((TipsetSelector {
544                key: None.into(),
545                height: None,
546                tag: Some(TipsetTag::Latest),
547            },))?,
548            offline,
549        ),
550        RpcTest::identity(ChainGetPath::request((
551            tipset.key().clone(),
552            tipset.parents().clone(),
553        ))?),
554        RpcTest::identity(ChainGetMessagesInTipset::request((tipset
555            .key()
556            .clone()
557            .into(),))?),
558        RpcTest::identity(ChainTipSetWeight::request((tipset.key().into(),))?),
559        RpcTest::basic(ChainGetFinalizedTipset::request(())?),
560    ];
561
562    if !offline {
563        tests.extend([
564            // Requires F3, disabled for offline RPC server
565            validate_tagged_tipset_v2(
566                ChainGetTipSetV2::request((TipsetSelector {
567                    key: None.into(),
568                    height: None,
569                    tag: Some(TipsetTag::Safe),
570                },))?,
571                offline,
572            ),
573            // Requires F3, disabled for offline RPC server
574            validate_tagged_tipset_v2(
575                ChainGetTipSetV2::request((TipsetSelector {
576                    key: None.into(),
577                    height: None,
578                    tag: Some(TipsetTag::Finalized),
579                },))?,
580                offline,
581            ),
582        ]);
583    }
584
585    for block in tipset.block_headers() {
586        let block_cid = *block.cid();
587        tests.extend([
588            RpcTest::identity(ChainReadObj::request((block_cid,))?),
589            RpcTest::identity(ChainHasObj::request((block_cid,))?),
590            RpcTest::identity(ChainGetBlock::request((block_cid,))?),
591            RpcTest::identity(ChainGetBlockMessages::request((block_cid,))?),
592            RpcTest::identity(ChainGetParentMessages::request((block_cid,))?),
593            RpcTest::identity(ChainGetParentReceipts::request((block_cid,))?),
594            RpcTest::identity(ChainStatObj::request((block.messages, None))?),
595            RpcTest::identity(ChainStatObj::request((
596                block.messages,
597                Some(block.messages),
598            ))?),
599        ]);
600
601        let (bls_messages, secp_messages) = crate::chain::store::block_messages(&store, block)?;
602        for msg_cid in sample_message_cids(bls_messages.iter(), secp_messages.iter()) {
603            tests.extend([RpcTest::identity(ChainGetMessage::request((msg_cid,))?)]);
604        }
605
606        for receipt in Receipt::get_receipts(store, block.message_receipts)? {
607            if let Some(events_root) = receipt.events_root() {
608                tests.extend([RpcTest::identity(ChainGetEvents::request((events_root,))?)
609                    .sort_policy(SortPolicy::All)]);
610            }
611        }
612    }
613
614    Ok(tests)
615}
616
617fn auth_tests() -> anyhow::Result<Vec<RpcTest>> {
618    // Note: The second optional parameter of `AuthNew` is not supported in Lotus
619    Ok(vec![
620        RpcTest::basic(AuthNew::request((
621            AuthNewParams::process_perms(Permission::Admin.to_string())?,
622            None,
623        ))?),
624        RpcTest::basic(AuthNew::request((
625            AuthNewParams::process_perms(Permission::Sign.to_string())?,
626            None,
627        ))?),
628        RpcTest::basic(AuthNew::request((
629            AuthNewParams::process_perms(Permission::Write.to_string())?,
630            None,
631        ))?),
632        RpcTest::basic(AuthNew::request((
633            AuthNewParams::process_perms(Permission::Read.to_string())?,
634            None,
635        ))?),
636    ])
637}
638
639fn mpool_tests() -> Vec<RpcTest> {
640    vec![
641        RpcTest::identity(MpoolGetNonce::request((*KNOWN_CALIBNET_ADDRESS,)).unwrap()),
642        // This should cause an error with `actor not found` in both Lotus and Forest. The messages
643        // are quite different, so we don't do strict equality check.
644        //  "forest_response": {
645        //    "Err": "ErrorObject { code: InternalError, message: \"Actor not found: addr=t1qb2x5qctp34rxd7ucl327h5ru6aazj2heno7x5y\", data: None }"
646        //  },
647        //  "lotus_response": {
648        //    "Err": "ErrorObject { code: ServerError(1), message: \"resolution lookup failed (t1qb2x5qctp34rxd7ucl327h5ru6aazj2heno7x5y): resolve address t1qb2x5qctp34rxd7ucl327h5ru6aazj2heno7x5y: actor not found\", data: None }"
649        //  }
650        RpcTest::identity(MpoolGetNonce::request((*KNOWN_EMPTY_CALIBNET_ADDRESS,)).unwrap())
651            .policy_on_rejected(PolicyOnRejected::Pass),
652        RpcTest::basic(MpoolPending::request((ApiTipsetKey(None),)).unwrap()),
653        RpcTest::basic(MpoolSelect::request((ApiTipsetKey(None), TICKET_QUALITY_GREEDY)).unwrap()),
654        RpcTest::basic(MpoolSelect::request((ApiTipsetKey(None), TICKET_QUALITY_OPTIMAL)).unwrap())
655            .ignore("https://github.com/ChainSafe/forest/issues/4490"),
656    ]
657}
658
659fn mpool_tests_with_tipset(tipset: &Tipset) -> Vec<RpcTest> {
660    vec![
661        RpcTest::basic(MpoolPending::request((tipset.key().into(),)).unwrap()),
662        RpcTest::basic(MpoolSelect::request((tipset.key().into(), TICKET_QUALITY_GREEDY)).unwrap()),
663        RpcTest::basic(
664            MpoolSelect::request((tipset.key().into(), TICKET_QUALITY_OPTIMAL)).unwrap(),
665        )
666        .ignore("https://github.com/ChainSafe/forest/issues/4490"),
667    ]
668}
669
670fn net_tests() -> Vec<RpcTest> {
671    // More net commands should be tested. Tracking issue:
672    // https://github.com/ChainSafe/forest/issues/3639
673    vec![
674        RpcTest::basic(NetAddrsListen::request(()).unwrap()),
675        RpcTest::basic(NetPeers::request(()).unwrap()),
676        RpcTest::identity(NetListening::request(()).unwrap()),
677        // Tests with a known peer id tend to be flaky, use a random peer id to test the unhappy path only
678        RpcTest::basic(NetAgentVersion::request((PeerId::random().to_string(),)).unwrap())
679            .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
680        RpcTest::basic(NetFindPeer::request((PeerId::random().to_string(),)).unwrap())
681            .policy_on_rejected(PolicyOnRejected::Pass)
682            .ignore("It times out in lotus when peer not found"),
683        RpcTest::basic(NetInfo::request(()).unwrap())
684            .ignore("Not implemented in Lotus. Why do we even have this method?"),
685        RpcTest::basic(NetAutoNatStatus::request(()).unwrap()),
686        RpcTest::identity(NetVersion::request(()).unwrap()),
687        RpcTest::identity(NetProtectAdd::request((vec![PeerId::random().to_string()],)).unwrap()),
688        RpcTest::identity(
689            NetProtectRemove::request((vec![PeerId::random().to_string()],)).unwrap(),
690        ),
691        RpcTest::basic(NetProtectList::request(()).unwrap()),
692    ]
693}
694
695fn node_tests() -> Vec<RpcTest> {
696    vec![
697        // This is a v1 RPC call. We don't support any v1 calls yet. Tracking
698        // issue: https://github.com/ChainSafe/forest/issues/3640
699        //RpcTest::basic(ApiInfo::node_status_req())
700    ]
701}
702
703fn event_tests_with_tipset<DB: Blockstore>(_store: &Arc<DB>, tipset: &Tipset) -> Vec<RpcTest> {
704    let epoch = tipset.epoch();
705    vec![
706        RpcTest::identity(GetActorEventsRaw::request((None,)).unwrap())
707            .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
708        RpcTest::identity(
709            GetActorEventsRaw::request((Some(ActorEventFilter {
710                addresses: vec![],
711                fields: Default::default(),
712                from_height: Some(epoch),
713                to_height: Some(epoch),
714                tipset_key: None,
715            }),))
716            .unwrap(),
717        )
718        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError)
719        .sort_policy(SortPolicy::All),
720        RpcTest::identity(
721            GetActorEventsRaw::request((Some(ActorEventFilter {
722                addresses: vec![],
723                fields: Default::default(),
724                from_height: Some(epoch - 100),
725                to_height: Some(epoch),
726                tipset_key: None,
727            }),))
728            .unwrap(),
729        )
730        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError)
731        .sort_policy(SortPolicy::All),
732        RpcTest::identity(
733            GetActorEventsRaw::request((Some(ActorEventFilter {
734                addresses: vec![],
735                fields: Default::default(),
736                from_height: None,
737                to_height: None,
738                tipset_key: Some(tipset.key().clone().into()),
739            }),))
740            .unwrap(),
741        )
742        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError)
743        .sort_policy(SortPolicy::All),
744        RpcTest::identity(
745            GetActorEventsRaw::request((Some(ActorEventFilter {
746                addresses: vec![
747                    Address::from_str("t410fvtakbtytk4otbnfymn4zn5ow252nj7lcpbtersq")
748                        .unwrap()
749                        .into(),
750                ],
751                fields: Default::default(),
752                from_height: Some(epoch - 100),
753                to_height: Some(epoch),
754                tipset_key: None,
755            }),))
756            .unwrap(),
757        )
758        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError)
759        .sort_policy(SortPolicy::All),
760        {
761            use std::collections::BTreeMap;
762
763            use base64::{Engine, prelude::BASE64_STANDARD};
764
765            use crate::lotus_json::LotusJson;
766            use crate::rpc::misc::ActorEventBlock;
767
768            let topic = BASE64_STANDARD
769                .decode("0Gprf0kYSUs3GSF9GAJ4bB9REqbB2I/iz+wAtFhPauw=")
770                .unwrap();
771            let mut fields: BTreeMap<String, Vec<ActorEventBlock>> = Default::default();
772            fields.insert(
773                "t1".into(),
774                vec![ActorEventBlock {
775                    codec: 85,
776                    value: LotusJson(topic),
777                }],
778            );
779            RpcTest::identity(
780                GetActorEventsRaw::request((Some(ActorEventFilter {
781                    addresses: vec![],
782                    fields,
783                    from_height: Some(epoch - 100),
784                    to_height: Some(epoch),
785                    tipset_key: None,
786                }),))
787                .unwrap(),
788            )
789            .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError)
790            .sort_policy(SortPolicy::All)
791        },
792    ]
793}
794
795fn miner_tests_with_tipset<DB: Blockstore>(
796    store: &Arc<DB>,
797    tipset: &Tipset,
798    miner_address: Option<Address>,
799) -> anyhow::Result<Vec<RpcTest>> {
800    // If no miner address is provided, we can't run any miner tests.
801    let Some(miner_address) = miner_address else {
802        return Ok(vec![]);
803    };
804
805    let mut tests = Vec::new();
806    for block in tipset.block_headers() {
807        let (bls_messages, secp_messages) = crate::chain::store::block_messages(store, block)?;
808        tests.push(miner_create_block_test(
809            miner_address,
810            tipset,
811            bls_messages,
812            secp_messages,
813        ));
814    }
815    tests.push(miner_create_block_no_messages_test(miner_address, tipset));
816    Ok(tests)
817}
818
819fn miner_create_block_test(
820    miner: Address,
821    tipset: &Tipset,
822    bls_messages: Vec<Message>,
823    secp_messages: Vec<SignedMessage>,
824) -> RpcTest {
825    // randomly sign BLS messages so we can test the BLS signature aggregation
826    let priv_key = bls_signatures::PrivateKey::generate(&mut crate::utils::rand::forest_rng());
827    let signed_bls_msgs = bls_messages
828        .into_iter()
829        .map(|message| {
830            let sig = priv_key.sign(message.cid().to_bytes());
831            SignedMessage {
832                message,
833                signature: Signature::new_bls(sig.as_bytes().to_vec()),
834            }
835        })
836        .collect_vec();
837
838    let block_template = BlockTemplate {
839        miner,
840        parents: tipset.parents().to_owned(),
841        ticket: Ticket::default(),
842        eproof: ElectionProof::default(),
843        beacon_values: tipset.block_headers().first().beacon_entries.to_owned(),
844        messages: [signed_bls_msgs, secp_messages].concat(),
845        epoch: tipset.epoch(),
846        timestamp: tipset.min_timestamp(),
847        winning_post_proof: Vec::default(),
848    };
849    RpcTest::identity(MinerCreateBlock::request((block_template,)).unwrap())
850}
851
852fn miner_create_block_no_messages_test(miner: Address, tipset: &Tipset) -> RpcTest {
853    let block_template = BlockTemplate {
854        miner,
855        parents: tipset.parents().to_owned(),
856        ticket: Ticket::default(),
857        eproof: ElectionProof::default(),
858        beacon_values: tipset.block_headers().first().beacon_entries.to_owned(),
859        messages: Vec::default(),
860        epoch: tipset.epoch(),
861        timestamp: tipset.min_timestamp(),
862        winning_post_proof: Vec::default(),
863    };
864    RpcTest::identity(MinerCreateBlock::request((block_template,)).unwrap())
865}
866
867fn state_tests_with_tipset<DB: Blockstore>(
868    store: &Arc<DB>,
869    tipset: &Tipset,
870) -> anyhow::Result<Vec<RpcTest>> {
871    let mut tests = vec![
872        RpcTest::identity(StateNetworkName::request(())?),
873        RpcTest::identity(StateGetNetworkParams::request(())?),
874        RpcTest::identity(StateMinerInitialPledgeForSector::request((
875            1,
876            SectorSize::_32GiB,
877            1024,
878            tipset.key().into(),
879        ))?),
880        RpcTest::identity(StateGetActor::request((
881            Address::SYSTEM_ACTOR,
882            tipset.key().into(),
883        ))?),
884        RpcTest::identity(StateGetActorV2::request((
885            Address::SYSTEM_ACTOR,
886            TipsetSelector {
887                key: tipset.key().into(),
888                ..Default::default()
889            },
890        ))?),
891        RpcTest::identity(StateGetID::request((
892            Address::SYSTEM_ACTOR,
893            TipsetSelector {
894                key: tipset.key().into(),
895                ..Default::default()
896            },
897        ))?),
898        RpcTest::identity(StateGetRandomnessFromTickets::request((
899            DomainSeparationTag::ElectionProofProduction as i64,
900            tipset.epoch(),
901            "dead beef".as_bytes().to_vec(),
902            tipset.key().into(),
903        ))?),
904        RpcTest::identity(StateGetRandomnessDigestFromTickets::request((
905            tipset.epoch(),
906            tipset.key().into(),
907        ))?),
908        RpcTest::identity(StateGetRandomnessFromBeacon::request((
909            DomainSeparationTag::ElectionProofProduction as i64,
910            tipset.epoch(),
911            "dead beef".as_bytes().to_vec(),
912            tipset.key().into(),
913        ))?),
914        RpcTest::identity(StateGetRandomnessDigestFromBeacon::request((
915            tipset.epoch(),
916            tipset.key().into(),
917        ))?),
918        // This should return `Address::new_id(0xdeadbeef)`
919        RpcTest::identity(StateLookupID::request((
920            Address::new_id(0xdeadbeef),
921            tipset.key().into(),
922        ))?),
923        RpcTest::identity(StateVerifiedRegistryRootKey::request((tipset
924            .key()
925            .into(),))?),
926        RpcTest::identity(StateVerifierStatus::request((
927            Address::VERIFIED_REGISTRY_ACTOR,
928            tipset.key().into(),
929        ))?),
930        RpcTest::identity(StateNetworkVersion::request((tipset.key().into(),))?),
931        RpcTest::identity(StateListMiners::request((tipset.key().into(),))?),
932        RpcTest::identity(StateListActors::request((tipset.key().into(),))?),
933        RpcTest::identity(MsigGetAvailableBalance::request((
934            Address::new_id(18101), // msig address id
935            tipset.key().into(),
936        ))?),
937        RpcTest::identity(MsigGetPending::request((
938            Address::new_id(18101), // msig address id
939            tipset.key().into(),
940        ))?),
941        RpcTest::identity(MsigGetVested::request((
942            Address::new_id(18101), // msig address id
943            tipset.parents().into(),
944            tipset.key().into(),
945        ))?),
946        RpcTest::identity(MsigGetVestingSchedule::request((
947            Address::new_id(18101), // msig address id
948            tipset.key().into(),
949        ))?),
950        RpcTest::identity(BeaconGetEntry::request((tipset.epoch(),))?),
951        RpcTest::identity(StateGetBeaconEntry::request((tipset.epoch(),))?),
952        // Not easily verifiable by using addresses extracted from blocks as most of those yield `null`
953        // for both Lotus and Forest. Therefore the actor addresses are hardcoded to values that allow
954        // for API compatibility verification.
955        RpcTest::identity(StateVerifiedClientStatus::request((
956            Address::VERIFIED_REGISTRY_ACTOR,
957            tipset.key().into(),
958        ))?),
959        RpcTest::identity(StateVerifiedClientStatus::request((
960            Address::DATACAP_TOKEN_ACTOR,
961            tipset.key().into(),
962        ))?),
963        RpcTest::identity(StateDealProviderCollateralBounds::request((
964            1,
965            true,
966            tipset.key().into(),
967        ))?),
968        RpcTest::identity(StateCirculatingSupply::request((tipset.key().into(),))?),
969        RpcTest::identity(StateVMCirculatingSupplyInternal::request((tipset
970            .key()
971            .into(),))?),
972        RpcTest::identity(StateMarketParticipants::request((tipset.key().into(),))?),
973        RpcTest::identity(StateMarketDeals::request((tipset.key().into(),))?),
974        RpcTest::identity(StateSectorPreCommitInfo::request((
975            Default::default(), // invalid address
976            u16::MAX as _,
977            tipset.key().into(),
978        ))?)
979        .policy_on_rejected(PolicyOnRejected::Pass),
980        RpcTest::identity(StateSectorGetInfo::request((
981            Default::default(), // invalid address
982            u16::MAX as _,      // invalid sector number
983            tipset.key().into(),
984        ))?)
985        .policy_on_rejected(PolicyOnRejected::Pass),
986        RpcTest::identity(StateGetAllocationIdForPendingDeal::request((
987            u16::MAX as _, // Invalid deal id
988            tipset.key().into(),
989        ))?),
990        RpcTest::identity(StateGetAllocationForPendingDeal::request((
991            u16::MAX as _, // Invalid deal id
992            tipset.key().into(),
993        ))?),
994        RpcTest::identity(StateCompute::request((
995            tipset.epoch(),
996            vec![],
997            tipset.key().into(),
998        ))?),
999    ];
1000
1001    tests.extend(read_state_api_tests(tipset)?);
1002    tests.extend(create_all_state_decode_params_tests(tipset)?);
1003
1004    for &pending_deal_id in
1005        StateGetAllocationIdForPendingDeal::get_allocations_for_pending_deals(store, tipset)?
1006            .keys()
1007            .take(COLLECTION_SAMPLE_SIZE)
1008    {
1009        tests.extend([
1010            RpcTest::identity(StateGetAllocationIdForPendingDeal::request((
1011                pending_deal_id,
1012                tipset.key().into(),
1013            ))?),
1014            RpcTest::identity(StateGetAllocationForPendingDeal::request((
1015                pending_deal_id,
1016                tipset.key().into(),
1017            ))?),
1018        ]);
1019    }
1020
1021    // Get deals
1022    let (deals, deals_map) = {
1023        let state = StateTree::new_from_root(store.clone(), tipset.parent_state())?;
1024        let actor = state.get_required_actor(&Address::MARKET_ACTOR)?;
1025        let market_state = market::State::load(&store, actor.code, actor.state)?;
1026        let proposals = market_state.proposals(&store)?;
1027        let mut deals = vec![];
1028        let mut deals_map = HashMap::default();
1029        proposals.for_each(|deal_id, deal_proposal| {
1030            deals.push(deal_id);
1031            deals_map.insert(deal_id, deal_proposal);
1032            Ok(())
1033        })?;
1034        (deals, deals_map)
1035    };
1036
1037    // Take 5 deals from each tipset
1038    for deal in deals.into_iter().take(COLLECTION_SAMPLE_SIZE) {
1039        tests.push(RpcTest::identity(StateMarketStorageDeal::request((
1040            deal,
1041            tipset.key().into(),
1042        ))?));
1043    }
1044
1045    for block in tipset.block_headers() {
1046        tests.extend([
1047            RpcTest::identity(StateMinerAllocated::request((
1048                block.miner_address,
1049                tipset.key().into(),
1050            ))?),
1051            RpcTest::identity(StateMinerActiveSectors::request((
1052                block.miner_address,
1053                tipset.key().into(),
1054            ))?),
1055            RpcTest::identity(StateLookupID::request((
1056                block.miner_address,
1057                tipset.key().into(),
1058            ))?),
1059            RpcTest::identity(StateLookupRobustAddress::request((
1060                block.miner_address,
1061                tipset.key().into(),
1062            ))?),
1063            RpcTest::identity(StateMinerSectors::request((
1064                block.miner_address,
1065                None,
1066                tipset.key().into(),
1067            ))?),
1068            RpcTest::identity(StateMinerPartitions::request((
1069                block.miner_address,
1070                0,
1071                tipset.key().into(),
1072            ))?),
1073            RpcTest::identity(StateMarketBalance::request((
1074                block.miner_address,
1075                tipset.key().into(),
1076            ))?),
1077            RpcTest::identity(StateMinerInfo::request((
1078                block.miner_address,
1079                tipset.key().into(),
1080            ))?),
1081            RpcTest::identity(StateMinerPower::request((
1082                block.miner_address,
1083                tipset.key().into(),
1084            ))?),
1085            RpcTest::identity(StateMinerDeadlines::request((
1086                block.miner_address,
1087                tipset.key().into(),
1088            ))?),
1089            RpcTest::identity(StateMinerProvingDeadline::request((
1090                block.miner_address,
1091                tipset.key().into(),
1092            ))?),
1093            RpcTest::identity(StateMinerAvailableBalance::request((
1094                block.miner_address,
1095                tipset.key().into(),
1096            ))?),
1097            RpcTest::identity(StateMinerFaults::request((
1098                block.miner_address,
1099                tipset.key().into(),
1100            ))?),
1101            RpcTest::identity(MinerGetBaseInfo::request((
1102                block.miner_address,
1103                block.epoch,
1104                tipset.key().into(),
1105            ))?),
1106            RpcTest::identity(StateMinerRecoveries::request((
1107                block.miner_address,
1108                tipset.key().into(),
1109            ))?),
1110            RpcTest::identity(StateMinerSectorCount::request((
1111                block.miner_address,
1112                tipset.key().into(),
1113            ))?),
1114            RpcTest::identity(StateGetClaims::request((
1115                block.miner_address,
1116                tipset.key().into(),
1117            ))?),
1118            RpcTest::identity(StateGetAllClaims::request((tipset.key().into(),))?),
1119            RpcTest::identity(StateGetAllAllocations::request((tipset.key().into(),))?),
1120            RpcTest::identity(StateSectorPreCommitInfo::request((
1121                block.miner_address,
1122                u16::MAX as _, // invalid sector number
1123                tipset.key().into(),
1124            ))?)
1125            .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
1126            RpcTest::identity(StateSectorGetInfo::request((
1127                block.miner_address,
1128                u16::MAX as _, // invalid sector number
1129                tipset.key().into(),
1130            ))?)
1131            .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
1132        ]);
1133        for claim_id in StateGetClaims::get_claims(store, &block.miner_address, tipset)?
1134            .keys()
1135            .take(COLLECTION_SAMPLE_SIZE)
1136        {
1137            tests.extend([RpcTest::identity(StateGetClaim::request((
1138                block.miner_address,
1139                *claim_id,
1140                tipset.key().into(),
1141            ))?)]);
1142        }
1143        for address in StateGetAllocations::get_valid_actor_addresses(store, tipset)?
1144            .take(COLLECTION_SAMPLE_SIZE)
1145        {
1146            tests.extend([RpcTest::identity(StateGetAllocations::request((
1147                address,
1148                tipset.key().into(),
1149            ))?)]);
1150            for allocation_id in StateGetAllocations::get_allocations(store, &address, tipset)?
1151                .keys()
1152                .take(COLLECTION_SAMPLE_SIZE)
1153            {
1154                tests.extend([RpcTest::identity(StateGetAllocation::request((
1155                    address,
1156                    *allocation_id,
1157                    tipset.key().into(),
1158                ))?)]);
1159            }
1160        }
1161        for sector in StateSectorGetInfo::get_sectors(store, &block.miner_address, tipset)?
1162            .into_iter()
1163            .take(COLLECTION_SAMPLE_SIZE)
1164        {
1165            tests.extend([
1166                RpcTest::identity(StateSectorGetInfo::request((
1167                    block.miner_address,
1168                    sector,
1169                    tipset.key().into(),
1170                ))?),
1171                RpcTest::identity(StateMinerSectors::request((
1172                    block.miner_address,
1173                    {
1174                        let mut bf = BitField::new();
1175                        bf.set(sector);
1176                        Some(bf)
1177                    },
1178                    tipset.key().into(),
1179                ))?),
1180                RpcTest::identity(StateSectorExpiration::request((
1181                    block.miner_address,
1182                    sector,
1183                    tipset.key().into(),
1184                ))?)
1185                .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
1186                RpcTest::identity(StateSectorPartition::request((
1187                    block.miner_address,
1188                    sector,
1189                    tipset.key().into(),
1190                ))?),
1191                RpcTest::identity(StateMinerSectorAllocated::request((
1192                    block.miner_address,
1193                    sector,
1194                    tipset.key().into(),
1195                ))?),
1196            ]);
1197        }
1198        for sector in StateSectorPreCommitInfo::get_sectors(store, &block.miner_address, tipset)?
1199            .into_iter()
1200            .take(COLLECTION_SAMPLE_SIZE)
1201        {
1202            tests.extend([RpcTest::identity(StateSectorPreCommitInfo::request((
1203                block.miner_address,
1204                sector,
1205                tipset.key().into(),
1206            ))?)]);
1207        }
1208        for info in StateSectorPreCommitInfo::get_sector_pre_commit_infos(
1209            store,
1210            &block.miner_address,
1211            tipset,
1212        )?
1213        .into_iter()
1214        .take(COLLECTION_SAMPLE_SIZE)
1215        .filter(|info| {
1216            !info.deal_ids.iter().any(|id| {
1217                if let Some(Ok(deal)) = deals_map.get(id) {
1218                    tipset.epoch() > deal.start_epoch || info.expiration > deal.end_epoch
1219                } else {
1220                    true
1221                }
1222            })
1223        }) {
1224            tests.extend([RpcTest::identity(
1225                StateMinerInitialPledgeCollateral::request((
1226                    block.miner_address,
1227                    info.clone(),
1228                    tipset.key().into(),
1229                ))?,
1230            )]);
1231            tests.extend([RpcTest::identity(
1232                StateMinerPreCommitDepositForPower::request((
1233                    block.miner_address,
1234                    info,
1235                    tipset.key().into(),
1236                ))?,
1237            )]);
1238        }
1239
1240        let (bls_messages, secp_messages) = crate::chain::store::block_messages(store, block)?;
1241        for msg_cid in sample_message_cids(bls_messages.iter(), secp_messages.iter()) {
1242            tests.extend([
1243                RpcTest::identity(StateReplay::request((tipset.key().into(), msg_cid))?),
1244                validate_message_lookup(
1245                    StateWaitMsg::request((msg_cid, 0, 10101, true))?
1246                        .with_timeout(Duration::from_secs(15)),
1247                ),
1248                validate_message_lookup(
1249                    StateWaitMsg::request((msg_cid, 0, 10101, false))?
1250                        .with_timeout(Duration::from_secs(15)),
1251                ),
1252                validate_message_lookup(StateSearchMsg::request((
1253                    None.into(),
1254                    msg_cid,
1255                    800,
1256                    true,
1257                ))?),
1258                validate_message_lookup(StateSearchMsg::request((
1259                    None.into(),
1260                    msg_cid,
1261                    800,
1262                    false,
1263                ))?),
1264                validate_message_lookup(StateSearchMsgLimited::request((msg_cid, 800))?),
1265            ]);
1266        }
1267        for msg in sample_messages(bls_messages.iter(), secp_messages.iter()) {
1268            tests.extend([
1269                RpcTest::identity(StateAccountKey::request((msg.from(), tipset.key().into()))?),
1270                RpcTest::identity(StateAccountKey::request((msg.from(), Default::default()))?),
1271                RpcTest::identity(StateLookupID::request((msg.from(), tipset.key().into()))?),
1272                RpcTest::identity(StateListMessages::request((
1273                    MessageFilter {
1274                        from: Some(msg.from()),
1275                        to: Some(msg.to()),
1276                    },
1277                    tipset.key().into(),
1278                    tipset.epoch(),
1279                ))?),
1280                RpcTest::identity(StateListMessages::request((
1281                    MessageFilter {
1282                        from: Some(msg.from()),
1283                        to: None,
1284                    },
1285                    tipset.key().into(),
1286                    tipset.epoch(),
1287                ))?),
1288                RpcTest::identity(StateListMessages::request((
1289                    MessageFilter {
1290                        from: None,
1291                        to: Some(msg.to()),
1292                    },
1293                    tipset.key().into(),
1294                    tipset.epoch(),
1295                ))?),
1296                RpcTest::identity(StateCall::request((msg.clone(), tipset.key().into()))?),
1297            ]);
1298        }
1299    }
1300
1301    Ok(tests)
1302}
1303
1304fn wallet_tests(worker_address: Option<Address>) -> Vec<RpcTest> {
1305    let prefunded_wallets = [
1306        // the following addresses should have 666 attoFIL each
1307        *KNOWN_CALIBNET_F0_ADDRESS,
1308        *KNOWN_CALIBNET_F1_ADDRESS,
1309        *KNOWN_CALIBNET_F2_ADDRESS,
1310        *KNOWN_CALIBNET_F3_ADDRESS,
1311        *KNOWN_CALIBNET_F4_ADDRESS,
1312        // This address should have 0 FIL
1313        *KNOWN_EMPTY_CALIBNET_ADDRESS,
1314    ];
1315
1316    let mut tests = vec![];
1317    for wallet in prefunded_wallets {
1318        tests.push(RpcTest::identity(
1319            WalletBalance::request((wallet,)).unwrap(),
1320        ));
1321        tests.push(RpcTest::identity(
1322            WalletValidateAddress::request((wallet.to_string(),)).unwrap(),
1323        ));
1324    }
1325
1326    let known_wallet = *KNOWN_CALIBNET_ADDRESS;
1327    // "Hello world!" signed with the above address:
1328    let signature = "44364ca78d85e53dda5ac6f719a4f2de3261c17f58558ab7730f80c478e6d43775244e7d6855afad82e4a1fd6449490acfa88e3fcfe7c1fe96ed549c100900b400";
1329    let text = "Hello world!".as_bytes().to_vec();
1330    let sig_bytes = hex::decode(signature).unwrap();
1331    let signature = match known_wallet.protocol() {
1332        Protocol::Secp256k1 => Signature::new_secp256k1(sig_bytes),
1333        Protocol::BLS => Signature::new_bls(sig_bytes),
1334        _ => panic!("Invalid signature (must be bls or secp256k1)"),
1335    };
1336
1337    tests.push(RpcTest::identity(
1338        WalletBalance::request((known_wallet,)).unwrap(),
1339    ));
1340    tests.push(RpcTest::identity(
1341        WalletValidateAddress::request((known_wallet.to_string(),)).unwrap(),
1342    ));
1343    tests.push(
1344        RpcTest::identity(
1345            // Both Forest and Lotus should fail miserably at invocking Cthulhu's name
1346            WalletValidateAddress::request((
1347                "Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn".to_string(),
1348            ))
1349            .unwrap(),
1350        )
1351        // Forest returns `Unknown address network`, Lotus `unknown address network`.
1352        .policy_on_rejected(PolicyOnRejected::PassWithIdenticalErrorCaseInsensitive),
1353    );
1354    tests.push(RpcTest::identity(
1355        WalletVerify::request((known_wallet, text, signature)).unwrap(),
1356    ));
1357
1358    // If a worker address is provided, we can test wallet methods requiring
1359    // a shared key.
1360    if let Some(worker_address) = worker_address {
1361        use base64::{Engine, prelude::BASE64_STANDARD};
1362        let msg =
1363            BASE64_STANDARD.encode("Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn".as_bytes());
1364        tests.push(RpcTest::identity(
1365            WalletSign::request((worker_address, msg.into())).unwrap(),
1366        ));
1367        tests.push(RpcTest::identity(
1368            WalletSign::request((worker_address, Vec::new())).unwrap(),
1369        ));
1370        let msg: Message = Message {
1371            from: worker_address,
1372            to: worker_address,
1373            value: TokenAmount::from_whole(1),
1374            method_num: METHOD_SEND,
1375            ..Default::default()
1376        };
1377        tests.push(RpcTest::identity(
1378            WalletSignMessage::request((worker_address, msg)).unwrap(),
1379        ));
1380    }
1381    tests
1382}
1383
1384fn eth_tests() -> Vec<RpcTest> {
1385    let mut tests = vec![];
1386    for use_alias in [false, true] {
1387        tests.push(RpcTest::identity(
1388            EthAccounts::request_with_alias((), use_alias).unwrap(),
1389        ));
1390        tests.push(RpcTest::basic(
1391            EthBlockNumber::request_with_alias((), use_alias).unwrap(),
1392        ));
1393        tests.push(RpcTest::identity(
1394            EthChainId::request_with_alias((), use_alias).unwrap(),
1395        ));
1396        // There is randomness in the result of this API, but at least check that the results are non-zero.
1397        tests.push(RpcTest::validate(
1398            EthGasPrice::request_with_alias((), use_alias).unwrap(),
1399            |forest, lotus| forest.0.is_positive() && lotus.0.is_positive(),
1400        ));
1401        tests.push(RpcTest::basic(
1402            EthSyncing::request_with_alias((), use_alias).unwrap(),
1403        ));
1404        tests.push(RpcTest::identity(
1405            EthGetBalance::request_with_alias(
1406                (
1407                    EthAddress::from_str("0xff38c072f286e3b20b3954ca9f99c05fbecc64aa").unwrap(),
1408                    BlockNumberOrHash::from_predefined(Predefined::Latest),
1409                ),
1410                use_alias,
1411            )
1412            .unwrap(),
1413        ));
1414        tests.push(RpcTest::identity(
1415            EthGetBalance::request_with_alias(
1416                (
1417                    EthAddress::from_str("0xff38c072f286e3b20b3954ca9f99c05fbecc64aa").unwrap(),
1418                    BlockNumberOrHash::from_predefined(Predefined::Pending),
1419                ),
1420                use_alias,
1421            )
1422            .unwrap(),
1423        ));
1424        tests.push(RpcTest::basic(
1425            Web3ClientVersion::request_with_alias((), use_alias).unwrap(),
1426        ));
1427        tests.push(RpcTest::basic(
1428            EthMaxPriorityFeePerGas::request_with_alias((), use_alias).unwrap(),
1429        ));
1430        tests.push(RpcTest::identity(
1431            EthProtocolVersion::request_with_alias((), use_alias).unwrap(),
1432        ));
1433
1434        let cases = [
1435            (
1436                Some(EthAddress::from_str("0x0c1d86d34e469770339b53613f3a2343accd62cb").unwrap()),
1437                Some(
1438                    "0xf8b2cb4f000000000000000000000000CbfF24DED1CE6B53712078759233Ac8f91ea71B6"
1439                        .parse()
1440                        .unwrap(),
1441                ),
1442            ),
1443            (Some(EthAddress::from_str(ZERO_ADDRESS).unwrap()), None),
1444            // Assert contract creation, which is invoked via setting the `to` field to `None` and
1445            // providing the contract bytecode in the `data` field.
1446            (
1447                None,
1448                Some(
1449                    EthBytes::from_str(
1450                        concat!("0x", include_str!("contracts/cthulhu/invoke.hex")).trim(),
1451                    )
1452                    .unwrap(),
1453                ),
1454            ),
1455        ];
1456
1457        for (to, data) in cases {
1458            let msg = EthCallMessage {
1459                to: to.clone(),
1460                data: data.clone(),
1461                ..EthCallMessage::default()
1462            };
1463
1464            tests.push(RpcTest::identity(
1465                EthCall::request_with_alias(
1466                    (
1467                        msg.clone(),
1468                        BlockNumberOrHash::from_predefined(Predefined::Latest),
1469                    ),
1470                    use_alias,
1471                )
1472                .unwrap(),
1473            ));
1474
1475            for tag in [
1476                ExtPredefined::Latest,
1477                ExtPredefined::Safe,
1478                ExtPredefined::Finalized,
1479            ] {
1480                tests.push(RpcTest::identity(
1481                    EthCallV2::request_with_alias(
1482                        (msg.clone(), ExtBlockNumberOrHash::PredefinedBlock(tag)),
1483                        use_alias,
1484                    )
1485                    .unwrap(),
1486                ));
1487            }
1488        }
1489
1490        let cases = [
1491            Some(EthAddressList::List(vec![])),
1492            Some(EthAddressList::List(vec![
1493                EthAddress::from_str("0x0c1d86d34e469770339b53613f3a2343accd62cb").unwrap(),
1494                EthAddress::from_str("0x89beb26addec4bc7e9f475aacfd084300d6de719").unwrap(),
1495            ])),
1496            Some(EthAddressList::Single(
1497                EthAddress::from_str("0x0c1d86d34e469770339b53613f3a2343accd62cb").unwrap(),
1498            )),
1499            None,
1500        ];
1501
1502        for address in cases {
1503            tests.push(RpcTest::basic(
1504                EthNewFilter::request_with_alias(
1505                    (EthFilterSpec {
1506                        address,
1507                        ..Default::default()
1508                    },),
1509                    use_alias,
1510                )
1511                .unwrap(),
1512            ));
1513        }
1514        tests.push(RpcTest::basic(
1515            EthNewPendingTransactionFilter::request_with_alias((), use_alias).unwrap(),
1516        ));
1517        tests.push(RpcTest::basic(
1518            EthNewBlockFilter::request_with_alias((), use_alias).unwrap(),
1519        ));
1520        tests.push(RpcTest::identity(
1521            EthUninstallFilter::request_with_alias((FilterID::new().unwrap(),), use_alias).unwrap(),
1522        ));
1523        tests.push(RpcTest::identity(
1524            EthAddressToFilecoinAddress::request(("0xff38c072f286e3b20b3954ca9f99c05fbecc64aa"
1525                .parse()
1526                .unwrap(),))
1527            .unwrap(),
1528        ));
1529        tests.push(RpcTest::identity(
1530            FilecoinAddressToEthAddress::request((*KNOWN_CALIBNET_F0_ADDRESS, None)).unwrap(),
1531        ));
1532        tests.push(RpcTest::identity(
1533            FilecoinAddressToEthAddress::request((*KNOWN_CALIBNET_F1_ADDRESS, None)).unwrap(),
1534        ));
1535        tests.push(RpcTest::identity(
1536            FilecoinAddressToEthAddress::request((*KNOWN_CALIBNET_F2_ADDRESS, None)).unwrap(),
1537        ));
1538        tests.push(RpcTest::identity(
1539            FilecoinAddressToEthAddress::request((*KNOWN_CALIBNET_F3_ADDRESS, None)).unwrap(),
1540        ));
1541        tests.push(RpcTest::identity(
1542            FilecoinAddressToEthAddress::request((*KNOWN_CALIBNET_F4_ADDRESS, None)).unwrap(),
1543        ));
1544    }
1545    tests
1546}
1547
1548fn eth_call_api_err_tests(epoch: i64) -> Vec<RpcTest> {
1549    let contract_codes = [
1550        include_str!("./contracts/arithmetic_err/arithmetic_overflow_err.hex"),
1551        include_str!("contracts/assert_err/assert_err.hex"),
1552        include_str!("./contracts/divide_by_zero_err/divide_by_zero_err.hex"),
1553        include_str!("./contracts/generic_panic_err/generic_panic_err.hex"),
1554        include_str!("./contracts/index_out_of_bounds_err/index_out_of_bounds_err.hex"),
1555        include_str!("./contracts/invalid_enum_err/invalid_enum_err.hex"),
1556        include_str!("./contracts/invalid_storage_array_err/invalid_storage_array_err.hex"),
1557        include_str!("./contracts/out_of_memory_err/out_of_memory_err.hex"),
1558        include_str!("./contracts/pop_empty_array_err/pop_empty_array_err.hex"),
1559        include_str!("./contracts/uninitialized_fn_err/uninitialized_fn_err.hex"),
1560    ];
1561
1562    let mut tests = Vec::new();
1563
1564    for &contract_hex in &contract_codes {
1565        let contract_code =
1566            EthBytes::from_str(contract_hex).expect("Contract bytecode should be valid hex");
1567
1568        let zero_address = EthAddress::from_str(ZERO_ADDRESS).unwrap();
1569        // Setting the `EthCallMessage` `to` field to null will deploy the contract.
1570        let msg = EthCallMessage {
1571            from: Some(zero_address),
1572            data: Some(contract_code),
1573            ..EthCallMessage::default()
1574        };
1575
1576        let eth_call_request =
1577            EthCall::request((msg.clone(), BlockNumberOrHash::from_block_number(epoch))).unwrap();
1578
1579        tests.push(
1580            RpcTest::identity(eth_call_request)
1581                .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
1582        );
1583
1584        let eth_call_v2_request =
1585            EthCallV2::request((msg, ExtBlockNumberOrHash::from_block_number(epoch))).unwrap();
1586
1587        tests.push(
1588            RpcTest::identity(eth_call_v2_request)
1589                .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
1590        );
1591    }
1592
1593    tests
1594}
1595
1596fn eth_tests_with_tipset<DB: Blockstore>(store: &Arc<DB>, shared_tipset: &Tipset) -> Vec<RpcTest> {
1597    let block_cid = shared_tipset.key().cid().unwrap();
1598    let block_hash: EthHash = block_cid.into();
1599
1600    let mut tests = vec![
1601        RpcTest::identity(
1602            EthGetBalance::request((
1603                EthAddress::from_str("0xff38c072f286e3b20b3954ca9f99c05fbecc64aa").unwrap(),
1604                BlockNumberOrHash::from_block_number(shared_tipset.epoch()),
1605            ))
1606            .unwrap(),
1607        ),
1608        RpcTest::identity(
1609            EthGetBalance::request((
1610                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1611                BlockNumberOrHash::from_block_number(shared_tipset.epoch()),
1612            ))
1613            .unwrap(),
1614        ),
1615        RpcTest::identity(
1616            EthGetBalance::request((
1617                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1618                BlockNumberOrHash::from_block_number_object(shared_tipset.epoch()),
1619            ))
1620            .unwrap(),
1621        ),
1622        RpcTest::identity(
1623            EthGetBalance::request((
1624                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1625                BlockNumberOrHash::from_block_hash_object(block_hash.clone(), false),
1626            ))
1627            .unwrap(),
1628        ),
1629        RpcTest::identity(
1630            EthGetBalance::request((
1631                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1632                BlockNumberOrHash::from_block_hash_object(block_hash.clone(), true),
1633            ))
1634            .unwrap(),
1635        ),
1636        RpcTest::identity(
1637            EthGetBalance::request((
1638                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1639                BlockNumberOrHash::from_predefined(Predefined::Earliest),
1640            ))
1641            .unwrap(),
1642        )
1643        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
1644        RpcTest::basic(
1645            EthGetBalance::request((
1646                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1647                BlockNumberOrHash::from_predefined(Predefined::Pending),
1648            ))
1649            .unwrap(),
1650        ),
1651        RpcTest::basic(
1652            EthGetBalance::request((
1653                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1654                BlockNumberOrHash::from_predefined(Predefined::Latest),
1655            ))
1656            .unwrap(),
1657        ),
1658        RpcTest::identity(
1659            EthGetBalance::request((
1660                generate_eth_random_address().unwrap(),
1661                BlockNumberOrHash::from_predefined(Predefined::Latest),
1662            ))
1663            .unwrap(),
1664        ),
1665        RpcTest::identity(
1666            EthGetBalanceV2::request((
1667                EthAddress::from_str("0xff38c072f286e3b20b3954ca9f99c05fbecc64aa").unwrap(),
1668                ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
1669            ))
1670            .unwrap(),
1671        ),
1672        RpcTest::identity(
1673            EthGetBalanceV2::request((
1674                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1675                ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
1676            ))
1677            .unwrap(),
1678        ),
1679        RpcTest::identity(
1680            EthGetBalanceV2::request((
1681                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1682                ExtBlockNumberOrHash::from_block_number_object(shared_tipset.epoch()),
1683            ))
1684            .unwrap(),
1685        ),
1686        RpcTest::identity(
1687            EthGetBalanceV2::request((
1688                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1689                ExtBlockNumberOrHash::from_block_hash_object(block_hash.clone(), false),
1690            ))
1691            .unwrap(),
1692        ),
1693        RpcTest::identity(
1694            EthGetBalanceV2::request((
1695                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1696                ExtBlockNumberOrHash::from_block_hash_object(block_hash.clone(), true),
1697            ))
1698            .unwrap(),
1699        ),
1700        RpcTest::identity(
1701            EthGetBalanceV2::request((
1702                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1703                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Earliest),
1704            ))
1705            .unwrap(),
1706        )
1707        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
1708        RpcTest::basic(
1709            EthGetBalanceV2::request((
1710                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1711                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Pending),
1712            ))
1713            .unwrap(),
1714        ),
1715        RpcTest::basic(
1716            EthGetBalanceV2::request((
1717                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1718                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),
1719            ))
1720            .unwrap(),
1721        ),
1722        RpcTest::basic(
1723            EthGetBalanceV2::request((
1724                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1725                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe),
1726            ))
1727            .unwrap(),
1728        ),
1729        RpcTest::basic(
1730            EthGetBalanceV2::request((
1731                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1732                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Finalized),
1733            ))
1734            .unwrap(),
1735        ),
1736        RpcTest::identity(
1737            EthGetBalanceV2::request((
1738                generate_eth_random_address().unwrap(),
1739                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),
1740            ))
1741            .unwrap(),
1742        ),
1743        RpcTest::identity(
1744            EthGetBlockByNumber::request((
1745                BlockNumberOrPredefined::BlockNumber(EthInt64(shared_tipset.epoch())),
1746                false,
1747            ))
1748            .unwrap(),
1749        ),
1750        RpcTest::identity(
1751            EthGetBlockByNumber::request((
1752                BlockNumberOrPredefined::BlockNumber(EthInt64(shared_tipset.epoch())),
1753                true,
1754            ))
1755            .unwrap(),
1756        ),
1757        RpcTest::identity(
1758            EthGetBlockByNumber::request((
1759                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Earliest),
1760                true,
1761            ))
1762            .unwrap(),
1763        )
1764        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
1765        RpcTest::basic(
1766            EthGetBlockByNumber::request((
1767                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Pending),
1768                true,
1769            ))
1770            .unwrap(),
1771        ),
1772        RpcTest::basic(
1773            EthGetBlockByNumber::request((
1774                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Latest),
1775                true,
1776            ))
1777            .unwrap(),
1778        ),
1779        RpcTest::basic(
1780            EthGetBlockByNumber::request((
1781                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Safe),
1782                true,
1783            ))
1784            .unwrap(),
1785        ),
1786        RpcTest::basic(
1787            EthGetBlockByNumber::request((
1788                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Finalized),
1789                true,
1790            ))
1791            .unwrap(),
1792        ),
1793        RpcTest::identity(
1794            EthGetBlockByNumberV2::request((
1795                BlockNumberOrPredefined::BlockNumber(EthInt64(shared_tipset.epoch())),
1796                false,
1797            ))
1798            .unwrap(),
1799        ),
1800        RpcTest::identity(
1801            EthGetBlockByNumberV2::request((
1802                BlockNumberOrPredefined::BlockNumber(EthInt64(shared_tipset.epoch())),
1803                true,
1804            ))
1805            .unwrap(),
1806        ),
1807        RpcTest::identity(
1808            EthGetBlockByNumberV2::request((
1809                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Earliest),
1810                true,
1811            ))
1812            .unwrap(),
1813        )
1814        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
1815        RpcTest::basic(
1816            EthGetBlockByNumberV2::request((
1817                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Pending),
1818                true,
1819            ))
1820            .unwrap(),
1821        ),
1822        RpcTest::basic(
1823            EthGetBlockByNumberV2::request((
1824                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Latest),
1825                true,
1826            ))
1827            .unwrap(),
1828        ),
1829        RpcTest::basic(
1830            EthGetBlockByNumberV2::request((
1831                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Safe),
1832                true,
1833            ))
1834            .unwrap(),
1835        ),
1836        RpcTest::basic(
1837            EthGetBlockByNumberV2::request((
1838                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Finalized),
1839                true,
1840            ))
1841            .unwrap(),
1842        ),
1843        RpcTest::identity(
1844            EthGetBlockReceipts::request((BlockNumberOrHash::from_block_hash_object(
1845                block_hash.clone(),
1846                true,
1847            ),))
1848            .unwrap(),
1849        ),
1850        // Nodes might be synced to different epochs, so we can't assert the exact result here.
1851        // Regardless, we want to check if the node returns a valid response and accepts predefined
1852        // values.
1853        RpcTest::basic(
1854            EthGetBlockReceipts::request((BlockNumberOrHash::from_predefined(Predefined::Latest),))
1855                .unwrap(),
1856        ),
1857        RpcTest::identity(
1858            EthGetBlockReceiptsV2::request((ExtBlockNumberOrHash::from_block_hash_object(
1859                block_hash.clone(),
1860                true,
1861            ),))
1862            .unwrap(),
1863        ),
1864        RpcTest::basic(
1865            EthGetBlockReceiptsV2::request((ExtBlockNumberOrHash::from_predefined(
1866                ExtPredefined::Safe,
1867            ),))
1868            .unwrap(),
1869        ),
1870        RpcTest::basic(
1871            EthGetBlockReceiptsV2::request((ExtBlockNumberOrHash::from_predefined(
1872                ExtPredefined::Finalized,
1873            ),))
1874            .unwrap(),
1875        ),
1876        RpcTest::identity(
1877            EthGetBlockTransactionCountByHash::request((block_hash.clone(),)).unwrap(),
1878        ),
1879        RpcTest::identity(
1880            EthGetBlockReceiptsLimited::request((
1881                BlockNumberOrHash::from_block_hash_object(block_hash.clone(), true),
1882                4,
1883            ))
1884            .unwrap(),
1885        )
1886        .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
1887        RpcTest::identity(
1888            EthGetBlockReceiptsLimited::request((
1889                BlockNumberOrHash::from_block_hash_object(block_hash.clone(), true),
1890                -1,
1891            ))
1892            .unwrap(),
1893        ),
1894        RpcTest::identity(
1895            EthGetBlockReceiptsLimitedV2::request((
1896                ExtBlockNumberOrHash::from_block_hash_object(block_hash.clone(), true),
1897                4,
1898            ))
1899            .unwrap(),
1900        )
1901        .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
1902        RpcTest::identity(
1903            EthGetBlockReceiptsLimitedV2::request((
1904                ExtBlockNumberOrHash::from_block_hash_object(block_hash.clone(), true),
1905                -1,
1906            ))
1907            .unwrap(),
1908        ),
1909        RpcTest::identity(
1910            EthGetBlockTransactionCountByNumber::request((EthInt64(shared_tipset.epoch()),))
1911                .unwrap(),
1912        ),
1913        RpcTest::identity(
1914            EthGetBlockTransactionCountByNumberV2::request((BlockNumberOrPredefined::BlockNumber(
1915                EthInt64(shared_tipset.epoch()),
1916            ),))
1917            .unwrap(),
1918        ),
1919        RpcTest::identity(
1920            EthGetBlockTransactionCountByNumberV2::request((
1921                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Safe),
1922            ))
1923            .unwrap(),
1924        ),
1925        RpcTest::identity(
1926            EthGetBlockTransactionCountByNumberV2::request((
1927                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Finalized),
1928            ))
1929            .unwrap(),
1930        ),
1931        RpcTest::identity(
1932            EthGetTransactionCount::request((
1933                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1934                BlockNumberOrHash::from_block_hash_object(block_hash.clone(), true),
1935            ))
1936            .unwrap(),
1937        ),
1938        RpcTest::identity(
1939            EthGetTransactionCount::request((
1940                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1941                BlockNumberOrHash::from_predefined(Predefined::Earliest),
1942            ))
1943            .unwrap(),
1944        )
1945        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
1946        RpcTest::basic(
1947            EthGetTransactionCount::request((
1948                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1949                BlockNumberOrHash::from_predefined(Predefined::Pending),
1950            ))
1951            .unwrap(),
1952        ),
1953        RpcTest::basic(
1954            EthGetTransactionCount::request((
1955                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1956                BlockNumberOrHash::from_predefined(Predefined::Latest),
1957            ))
1958            .unwrap(),
1959        ),
1960        RpcTest::identity(
1961            EthGetTransactionCount::request((
1962                generate_eth_random_address().unwrap(),
1963                BlockNumberOrHash::from_predefined(Predefined::Latest),
1964            ))
1965            .unwrap(),
1966        ),
1967        RpcTest::identity(
1968            EthGetTransactionCountV2::request((
1969                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1970                ExtBlockNumberOrHash::from_block_hash_object(block_hash.clone(), true),
1971            ))
1972            .unwrap(),
1973        ),
1974        RpcTest::identity(
1975            EthGetTransactionCountV2::request((
1976                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1977                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Earliest),
1978            ))
1979            .unwrap(),
1980        )
1981        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
1982        RpcTest::basic(
1983            EthGetTransactionCountV2::request((
1984                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1985                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Pending),
1986            ))
1987            .unwrap(),
1988        ),
1989        RpcTest::basic(
1990            EthGetTransactionCountV2::request((
1991                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1992                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),
1993            ))
1994            .unwrap(),
1995        ),
1996        RpcTest::basic(
1997            EthGetTransactionCountV2::request((
1998                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1999                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe),
2000            ))
2001            .unwrap(),
2002        ),
2003        RpcTest::basic(
2004            EthGetTransactionCountV2::request((
2005                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
2006                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Finalized),
2007            ))
2008            .unwrap(),
2009        ),
2010        RpcTest::identity(
2011            EthGetTransactionCountV2::request((
2012                generate_eth_random_address().unwrap(),
2013                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),
2014            ))
2015            .unwrap(),
2016        ),
2017        RpcTest::identity(
2018            EthGetStorageAt::request((
2019                // https://filfox.info/en/address/f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq
2020                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2021                EthBytes(vec![0xa]),
2022                BlockNumberOrHash::BlockNumber(EthInt64(shared_tipset.epoch())),
2023            ))
2024            .unwrap(),
2025        ),
2026        RpcTest::identity(
2027            EthGetStorageAt::request((
2028                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2029                EthBytes(vec![0xa]),
2030                BlockNumberOrHash::from_predefined(Predefined::Earliest),
2031            ))
2032            .unwrap(),
2033        )
2034        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2035        RpcTest::basic(
2036            EthGetStorageAt::request((
2037                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2038                EthBytes(vec![0xa]),
2039                BlockNumberOrHash::from_predefined(Predefined::Pending),
2040            ))
2041            .unwrap(),
2042        ),
2043        RpcTest::basic(
2044            EthGetStorageAt::request((
2045                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2046                EthBytes(vec![0xa]),
2047                BlockNumberOrHash::from_predefined(Predefined::Latest),
2048            ))
2049            .unwrap(),
2050        ),
2051        RpcTest::identity(
2052            EthGetStorageAt::request((
2053                generate_eth_random_address().unwrap(),
2054                EthBytes(vec![0x0]),
2055                BlockNumberOrHash::from_predefined(Predefined::Latest),
2056            ))
2057            .unwrap(),
2058        ),
2059        RpcTest::identity(
2060            EthGetStorageAtV2::request((
2061                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2062                EthBytes(vec![0xa]),
2063                ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2064            ))
2065            .unwrap(),
2066        ),
2067        RpcTest::basic(
2068            EthGetStorageAtV2::request((
2069                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2070                EthBytes(vec![0xa]),
2071                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe),
2072            ))
2073            .unwrap(),
2074        ),
2075        RpcTest::basic(
2076            EthGetStorageAtV2::request((
2077                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2078                EthBytes(vec![0xa]),
2079                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Finalized),
2080            ))
2081            .unwrap(),
2082        ),
2083        RpcTest::identity(
2084            EthGetStorageAtV2::request((
2085                generate_eth_random_address().unwrap(),
2086                EthBytes(vec![0x0]),
2087                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),
2088            ))
2089            .unwrap(),
2090        ),
2091        RpcTest::identity(
2092            EthFeeHistory::request((
2093                10.into(),
2094                BlockNumberOrPredefined::BlockNumber(shared_tipset.epoch().into()),
2095                None,
2096            ))
2097            .unwrap(),
2098        ),
2099        RpcTest::identity(
2100            EthFeeHistory::request((
2101                10.into(),
2102                BlockNumberOrPredefined::BlockNumber(shared_tipset.epoch().into()),
2103                Some(vec![10., 50., 90.]),
2104            ))
2105            .unwrap(),
2106        ),
2107        RpcTest::identity(
2108            EthFeeHistory::request((
2109                10.into(),
2110                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Earliest),
2111                None,
2112            ))
2113            .unwrap(),
2114        )
2115        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2116        RpcTest::basic(
2117            EthFeeHistory::request((
2118                10.into(),
2119                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Pending),
2120                Some(vec![10., 50., 90.]),
2121            ))
2122            .unwrap(),
2123        ),
2124        RpcTest::basic(
2125            EthFeeHistory::request((
2126                10.into(),
2127                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Latest),
2128                None,
2129            ))
2130            .unwrap(),
2131        ),
2132        RpcTest::basic(
2133            EthFeeHistory::request((
2134                10.into(),
2135                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Safe),
2136                None,
2137            ))
2138            .unwrap(),
2139        ),
2140        RpcTest::basic(
2141            EthFeeHistory::request((
2142                10.into(),
2143                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Finalized),
2144                Some(vec![10., 50., 90.]),
2145            ))
2146            .unwrap(),
2147        ),
2148        RpcTest::identity(
2149            EthFeeHistoryV2::request((
2150                10.into(),
2151                ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2152                None,
2153            ))
2154            .unwrap(),
2155        ),
2156        RpcTest::identity(
2157            EthFeeHistoryV2::request((
2158                10.into(),
2159                ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2160                Some(vec![10., 50., 90.]),
2161            ))
2162            .unwrap(),
2163        ),
2164        RpcTest::identity(
2165            EthFeeHistoryV2::request((
2166                10.into(),
2167                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Earliest),
2168                None,
2169            ))
2170            .unwrap(),
2171        )
2172        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2173        RpcTest::basic(
2174            EthFeeHistoryV2::request((
2175                10.into(),
2176                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Pending),
2177                Some(vec![10., 50., 90.]),
2178            ))
2179            .unwrap(),
2180        ),
2181        RpcTest::basic(
2182            EthFeeHistoryV2::request((
2183                10.into(),
2184                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),
2185                None,
2186            ))
2187            .unwrap(),
2188        ),
2189        RpcTest::basic(
2190            EthFeeHistoryV2::request((
2191                10.into(),
2192                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe),
2193                None,
2194            ))
2195            .unwrap(),
2196        ),
2197        RpcTest::basic(
2198            EthFeeHistoryV2::request((
2199                10.into(),
2200                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Finalized),
2201                Some(vec![10., 50., 90.]),
2202            ))
2203            .unwrap(),
2204        ),
2205        RpcTest::identity(
2206            EthGetCode::request((
2207                // https://filfox.info/en/address/f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq
2208                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2209                BlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2210            ))
2211            .unwrap(),
2212        ),
2213        RpcTest::identity(
2214            EthGetCode::request((
2215                // https://filfox.info/en/address/f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq
2216                Address::from_str("f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq")
2217                    .unwrap()
2218                    .try_into()
2219                    .unwrap(),
2220                BlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2221            ))
2222            .unwrap(),
2223        ),
2224        RpcTest::identity(
2225            EthGetCode::request((
2226                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2227                BlockNumberOrHash::from_predefined(Predefined::Earliest),
2228            ))
2229            .unwrap(),
2230        )
2231        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2232        RpcTest::basic(
2233            EthGetCode::request((
2234                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2235                BlockNumberOrHash::from_predefined(Predefined::Pending),
2236            ))
2237            .unwrap(),
2238        ),
2239        RpcTest::basic(
2240            EthGetCode::request((
2241                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2242                BlockNumberOrHash::from_predefined(Predefined::Latest),
2243            ))
2244            .unwrap(),
2245        ),
2246        RpcTest::identity(
2247            EthGetCode::request((
2248                generate_eth_random_address().unwrap(),
2249                BlockNumberOrHash::from_predefined(Predefined::Latest),
2250            ))
2251            .unwrap(),
2252        ),
2253        RpcTest::identity(
2254            EthGetCodeV2::request((
2255                // https://filfox.info/en/address/f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq
2256                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2257                ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2258            ))
2259            .unwrap(),
2260        ),
2261        RpcTest::basic(
2262            EthGetCodeV2::request((
2263                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2264                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe),
2265            ))
2266            .unwrap(),
2267        ),
2268        RpcTest::basic(
2269            EthGetCodeV2::request((
2270                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2271                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Finalized),
2272            ))
2273            .unwrap(),
2274        ),
2275        RpcTest::identity(
2276            EthGetCodeV2::request((
2277                generate_eth_random_address().unwrap(),
2278                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),
2279            ))
2280            .unwrap(),
2281        ),
2282        RpcTest::identity(
2283            EthGetTransactionByBlockNumberAndIndex::request((
2284                BlockNumberOrPredefined::BlockNumber(shared_tipset.epoch().into()),
2285                0.into(),
2286            ))
2287            .unwrap(),
2288        )
2289        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2290        RpcTest::identity(
2291            EthGetTransactionByBlockNumberAndIndex::request((
2292                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Earliest),
2293                0.into(),
2294            ))
2295            .unwrap(),
2296        )
2297        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2298        RpcTest::identity(
2299            EthGetTransactionByBlockNumberAndIndex::request((
2300                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Pending),
2301                0.into(),
2302            ))
2303            .unwrap(),
2304        )
2305        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2306        RpcTest::identity(
2307            EthGetTransactionByBlockNumberAndIndex::request((
2308                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Latest),
2309                0.into(),
2310            ))
2311            .unwrap(),
2312        )
2313        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2314        RpcTest::identity(
2315            EthGetTransactionByBlockNumberAndIndexV2::request((
2316                BlockNumberOrPredefined::BlockNumber(shared_tipset.epoch().into()),
2317                0.into(),
2318            ))
2319            .unwrap(),
2320        )
2321        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2322        RpcTest::identity(
2323            EthGetTransactionByBlockNumberAndIndexV2::request((
2324                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Safe),
2325                0.into(),
2326            ))
2327            .unwrap(),
2328        )
2329        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2330        RpcTest::identity(
2331            EthGetTransactionByBlockNumberAndIndexV2::request((
2332                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Finalized),
2333                0.into(),
2334            ))
2335            .unwrap(),
2336        )
2337        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2338        RpcTest::identity(
2339            EthGetTransactionByBlockHashAndIndex::request((block_hash.clone(), 0.into())).unwrap(),
2340        )
2341        .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
2342        RpcTest::identity(EthGetBlockByHash::request((block_hash.clone(), false)).unwrap()),
2343        RpcTest::identity(EthGetBlockByHash::request((block_hash.clone(), true)).unwrap()),
2344        RpcTest::identity(
2345            EthGetLogs::request((EthFilterSpec {
2346                from_block: Some(format!("0x{:x}", shared_tipset.epoch())),
2347                to_block: Some(format!("0x{:x}", shared_tipset.epoch())),
2348                ..Default::default()
2349            },))
2350            .unwrap(),
2351        )
2352        .sort_policy(SortPolicy::All)
2353        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2354        RpcTest::identity(
2355            EthGetLogs::request((EthFilterSpec {
2356                from_block: Some(format!("0x{:x}", shared_tipset.epoch())),
2357                to_block: Some(format!("0x{:x}", shared_tipset.epoch())),
2358                address: Some(EthAddressList::List(Vec::new())),
2359                ..Default::default()
2360            },))
2361            .unwrap(),
2362        )
2363        .sort_policy(SortPolicy::All)
2364        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2365        RpcTest::identity(
2366            EthGetLogs::request((EthFilterSpec {
2367                from_block: Some(format!("0x{:x}", shared_tipset.epoch() - 100)),
2368                to_block: Some(format!("0x{:x}", shared_tipset.epoch())),
2369                ..Default::default()
2370            },))
2371            .unwrap(),
2372        )
2373        .sort_policy(SortPolicy::All)
2374        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2375        RpcTest::identity(
2376            EthGetLogs::request((EthFilterSpec {
2377                address: Some(EthAddressList::Single(
2378                    EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2379                )),
2380                ..Default::default()
2381            },))
2382            .unwrap(),
2383        )
2384        .sort_policy(SortPolicy::All)
2385        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2386        RpcTest::identity(EthGetFilterLogs::request((FilterID::new().unwrap(),)).unwrap())
2387            .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
2388        RpcTest::identity(EthGetFilterChanges::request((FilterID::new().unwrap(),)).unwrap())
2389            .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
2390        RpcTest::identity(EthGetTransactionHashByCid::request((block_cid,)).unwrap()),
2391        RpcTest::identity(
2392            EthTraceBlock::request((ExtBlockNumberOrHash::from_block_number(
2393                shared_tipset.epoch(),
2394            ),))
2395            .unwrap(),
2396        ),
2397        RpcTest::identity(
2398            EthTraceBlock::request((ExtBlockNumberOrHash::from_predefined(
2399                ExtPredefined::Earliest,
2400            ),))
2401            .unwrap(),
2402        )
2403        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2404        RpcTest::basic(
2405            EthTraceBlock::request((ExtBlockNumberOrHash::from_predefined(
2406                ExtPredefined::Pending,
2407            ),))
2408            .unwrap(),
2409        ),
2410        RpcTest::basic(
2411            EthTraceBlock::request((ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),))
2412                .unwrap(),
2413        ),
2414        RpcTest::basic(
2415            EthTraceBlock::request((ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe),))
2416                .unwrap(),
2417        ),
2418        RpcTest::basic(
2419            EthTraceBlock::request((ExtBlockNumberOrHash::from_predefined(
2420                ExtPredefined::Finalized,
2421            ),))
2422            .unwrap(),
2423        ),
2424        RpcTest::identity(
2425            EthTraceBlockV2::request((ExtBlockNumberOrHash::from_block_number(
2426                shared_tipset.epoch(),
2427            ),))
2428            .unwrap(),
2429        ),
2430        RpcTest::basic(
2431            EthTraceBlockV2::request((ExtBlockNumberOrHash::from_predefined(
2432                ExtPredefined::Pending,
2433            ),))
2434            .unwrap(),
2435        ),
2436        RpcTest::basic(
2437            EthTraceBlockV2::request((ExtBlockNumberOrHash::from_predefined(
2438                ExtPredefined::Latest,
2439            ),))
2440            .unwrap(),
2441        ),
2442        RpcTest::basic(
2443            EthTraceBlockV2::request((ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe),))
2444                .unwrap(),
2445        ),
2446        RpcTest::basic(
2447            EthTraceBlockV2::request((ExtBlockNumberOrHash::from_predefined(
2448                ExtPredefined::Finalized,
2449            ),))
2450            .unwrap(),
2451        ),
2452        RpcTest::identity(
2453            EthTraceReplayBlockTransactions::request((
2454                ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2455                vec!["trace".to_string()],
2456            ))
2457            .unwrap(),
2458        ),
2459        RpcTest::identity(
2460            EthTraceReplayBlockTransactionsV2::request((
2461                ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2462                vec!["trace".to_string()],
2463            ))
2464            .unwrap(),
2465        ),
2466        RpcTest::identity(
2467            EthTraceFilter::request((EthTraceFilterCriteria {
2468                from_block: Some(format!("0x{:x}", shared_tipset.epoch() - 100)),
2469                to_block: Some(format!("0x{:x}", shared_tipset.epoch() - SAFE_EPOCH_DELAY)),
2470                ..Default::default()
2471            },))
2472            .unwrap(),
2473        )
2474        // both nodes could fail on, e.g., "too many results, maximum supported is 500, try paginating
2475        // requests with After and Count"
2476        .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
2477        RpcTest::identity(
2478            EthGetTransactionReceipt::request((
2479                // A transaction that should not exist, to test the `null` response in case
2480                // of missing transaction.
2481                EthHash::from_str(
2482                    "0xf234567890123456789d6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f70809",
2483                )
2484                .unwrap(),
2485            ))
2486            .unwrap(),
2487        ),
2488    ];
2489
2490    for block in shared_tipset.block_headers() {
2491        tests.extend([RpcTest::identity(
2492            FilecoinAddressToEthAddress::request((
2493                block.miner_address,
2494                Some(BlockNumberOrPredefined::PredefinedBlock(
2495                    ExtPredefined::Latest,
2496                )),
2497            ))
2498            .unwrap(),
2499        )]);
2500        let (bls_messages, secp_messages) =
2501            crate::chain::store::block_messages(store, block).unwrap();
2502        for msg in sample_messages(bls_messages.iter(), secp_messages.iter()) {
2503            tests.extend([RpcTest::identity(
2504                FilecoinAddressToEthAddress::request((
2505                    msg.from(),
2506                    Some(BlockNumberOrPredefined::PredefinedBlock(
2507                        ExtPredefined::Latest,
2508                    )),
2509                ))
2510                .unwrap(),
2511            )]);
2512            if let Ok(eth_to_addr) = EthAddress::try_from(msg.to) {
2513                tests.extend([RpcTest::identity(
2514                    EthEstimateGas::request((
2515                        EthCallMessage {
2516                            to: Some(eth_to_addr.clone()),
2517                            value: Some(msg.value.clone().into()),
2518                            data: Some(msg.params.clone().into()),
2519                            ..Default::default()
2520                        },
2521                        Some(BlockNumberOrHash::BlockNumber(shared_tipset.epoch().into())),
2522                    ))
2523                    .unwrap(),
2524                )
2525                .policy_on_rejected(PolicyOnRejected::Pass)]);
2526                tests.extend([RpcTest::identity(
2527                    EthEstimateGasV2::request((
2528                        EthCallMessage {
2529                            to: Some(eth_to_addr),
2530                            value: Some(msg.value.clone().into()),
2531                            data: Some(msg.params.clone().into()),
2532                            ..Default::default()
2533                        },
2534                        Some(ExtBlockNumberOrHash::BlockNumber(
2535                            shared_tipset.epoch().into(),
2536                        )),
2537                    ))
2538                    .unwrap(),
2539                )
2540                .policy_on_rejected(PolicyOnRejected::Pass)]);
2541            }
2542        }
2543    }
2544
2545    tests
2546}
2547
2548fn read_state_api_tests(tipset: &Tipset) -> anyhow::Result<Vec<RpcTest>> {
2549    let tests = vec![
2550        RpcTest::identity(StateReadState::request((
2551            Address::SYSTEM_ACTOR,
2552            tipset.key().into(),
2553        ))?),
2554        RpcTest::identity(StateReadState::request((
2555            Address::SYSTEM_ACTOR,
2556            Default::default(),
2557        ))?),
2558        RpcTest::identity(StateReadState::request((
2559            Address::CRON_ACTOR,
2560            tipset.key().into(),
2561        ))?),
2562        RpcTest::identity(StateReadState::request((
2563            Address::MARKET_ACTOR,
2564            tipset.key().into(),
2565        ))?),
2566        RpcTest::identity(StateReadState::request((
2567            Address::INIT_ACTOR,
2568            tipset.key().into(),
2569        ))?),
2570        RpcTest::identity(StateReadState::request((
2571            Address::POWER_ACTOR,
2572            tipset.key().into(),
2573        ))?),
2574        RpcTest::identity(StateReadState::request((
2575            Address::REWARD_ACTOR,
2576            tipset.key().into(),
2577        ))?),
2578        RpcTest::identity(StateReadState::request((
2579            Address::VERIFIED_REGISTRY_ACTOR,
2580            tipset.key().into(),
2581        ))?),
2582        RpcTest::identity(StateReadState::request((
2583            Address::DATACAP_TOKEN_ACTOR,
2584            tipset.key().into(),
2585        ))?),
2586        RpcTest::identity(StateReadState::request((
2587            // payment channel actor address `t066116`
2588            Address::new_id(66116), // https://calibration.filscan.io/en/address/t066116/
2589            tipset.key().into(),
2590        ))?),
2591        RpcTest::identity(StateReadState::request((
2592            // multisig actor address `t018101`
2593            Address::new_id(18101), // https://calibration.filscan.io/en/address/t018101/
2594            tipset.key().into(),
2595        ))?),
2596        RpcTest::identity(StateReadState::request((
2597            ACCOUNT_ADDRESS,
2598            tipset.key().into(),
2599        ))?),
2600        RpcTest::identity(StateReadState::request((
2601            MINER_ADDRESS,
2602            tipset.key().into(),
2603        ))?),
2604        RpcTest::identity(StateReadState::request((
2605            Address::from_str(EVM_ADDRESS).unwrap(), // evm actor
2606            tipset.key().into(),
2607        ))?),
2608    ];
2609
2610    Ok(tests)
2611}
2612
2613fn eth_state_tests_with_tipset<DB: Blockstore>(
2614    store: &Arc<DB>,
2615    shared_tipset: &Tipset,
2616    eth_chain_id: EthChainIdType,
2617) -> anyhow::Result<Vec<RpcTest>> {
2618    let mut tests = vec![];
2619
2620    for block in shared_tipset.block_headers() {
2621        let state = StateTree::new_from_root(store.clone(), shared_tipset.parent_state())?;
2622
2623        let (bls_messages, secp_messages) = crate::chain::store::block_messages(store, block)?;
2624        for smsg in sample_signed_messages(bls_messages.iter(), secp_messages.iter()) {
2625            let tx = new_eth_tx_from_signed_message(&smsg, &state, eth_chain_id)?;
2626            tests.push(RpcTest::identity(
2627                EthGetMessageCidByTransactionHash::request((tx.hash.clone(),))?,
2628            ));
2629            tests.push(RpcTest::identity(EthGetTransactionByHash::request((tx
2630                .hash
2631                .clone(),))?));
2632            tests.push(RpcTest::identity(EthGetTransactionByHashLimited::request(
2633                (tx.hash.clone(), shared_tipset.epoch()),
2634            )?));
2635            tests.push(RpcTest::identity(EthTraceTransaction::request((tx
2636                .hash
2637                .to_string(),))?));
2638            if smsg.message.from.protocol() == Protocol::Delegated
2639                && smsg.message.to.protocol() == Protocol::Delegated
2640            {
2641                tests.push(
2642                    RpcTest::identity(EthGetTransactionReceipt::request((tx.hash.clone(),))?)
2643                        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2644                );
2645                tests.push(
2646                    RpcTest::identity(EthGetTransactionReceiptLimited::request((tx.hash, 800))?)
2647                        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2648                );
2649            }
2650        }
2651    }
2652    tests.push(RpcTest::identity(
2653        EthGetMessageCidByTransactionHash::request((EthHash::from_str(
2654            "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355f",
2655        )?,))?,
2656    ));
2657
2658    // Test eth_call API errors
2659    tests.extend(eth_call_api_err_tests(shared_tipset.epoch()));
2660
2661    Ok(tests)
2662}
2663
2664fn gas_tests_with_tipset(shared_tipset: &Tipset) -> Vec<RpcTest> {
2665    // This is a testnet address with a few FILs. The private key has been
2666    // discarded. If calibnet is reset, a new address should be created.
2667    let addr = Address::from_str("t15ydyu3d65gznpp2qxwpkjsgz4waubeunn6upvla").unwrap();
2668    let message = Message {
2669        from: addr,
2670        to: addr,
2671        value: TokenAmount::from_whole(1),
2672        method_num: METHOD_SEND,
2673        ..Default::default()
2674    };
2675
2676    vec![
2677        // The tipset is only used for resolving the 'from' address and not when
2678        // computing the gas cost. This means that the `GasEstimateGasLimit` method
2679        // is inherently non-deterministic, but I'm fairly sure we're compensated for
2680        // everything. If not, this test will be flaky. Instead of disabling it, we
2681        // should relax the verification requirement.
2682        RpcTest::identity(
2683            GasEstimateGasLimit::request((message.clone(), shared_tipset.key().into())).unwrap(),
2684        ),
2685        // Gas estimation is inherently non-deterministic due to randomness in gas premium
2686        // calculation and network state changes. We validate that both implementations
2687        // return reasonable values within expected bounds rather than exact equality.
2688        RpcTest::validate(
2689            GasEstimateMessageGas::request((
2690                message,
2691                None, // No MessageSendSpec
2692                shared_tipset.key().into(),
2693            ))
2694            .unwrap(),
2695            |forest_api_msg, lotus_api_msg| {
2696                let forest_msg = forest_api_msg.message;
2697                let lotus_msg = lotus_api_msg.message;
2698                // Validate that the gas limit is identical (must be deterministic)
2699                if forest_msg.gas_limit != lotus_msg.gas_limit {
2700                    return false;
2701                }
2702
2703                // Validate gas fee cap and premium are within reasonable bounds (±5%)
2704                let forest_fee_cap = &forest_msg.gas_fee_cap;
2705                let lotus_fee_cap = &lotus_msg.gas_fee_cap;
2706                let forest_premium = &forest_msg.gas_premium;
2707                let lotus_premium = &lotus_msg.gas_premium;
2708
2709                // Gas fee cap and premium should not be negative
2710                if [forest_fee_cap, lotus_fee_cap, forest_premium, lotus_premium]
2711                    .iter()
2712                    .any(|amt| amt.is_negative())
2713                {
2714                    return false;
2715                }
2716
2717                forest_fee_cap.is_within_percent(lotus_fee_cap, 5)
2718                    && forest_premium.is_within_percent(lotus_premium, 5)
2719            },
2720        ),
2721    ]
2722}
2723
2724fn f3_tests() -> anyhow::Result<Vec<RpcTest>> {
2725    Ok(vec![
2726        // using basic because 2 nodes are not guaranteed to be at the same head
2727        RpcTest::basic(F3GetECPowerTable::request((None.into(),))?),
2728        RpcTest::basic(F3GetLatestCertificate::request(())?),
2729        RpcTest::basic(F3ListParticipants::request(())?),
2730        RpcTest::basic(F3GetProgress::request(())?),
2731        RpcTest::basic(F3GetOrRenewParticipationTicket::request((
2732            Address::new_id(1000),
2733            vec![],
2734            3,
2735        ))?),
2736        RpcTest::identity(F3IsRunning::request(())?),
2737        RpcTest::identity(F3GetCertificate::request((0,))?),
2738        RpcTest::identity(F3GetCertificate::request((50,))?),
2739        RpcTest::identity(F3GetManifest::request(())?),
2740    ])
2741}
2742
2743fn f3_tests_with_tipset(tipset: &Tipset) -> anyhow::Result<Vec<RpcTest>> {
2744    Ok(vec![
2745        RpcTest::identity(F3GetECPowerTable::request((tipset.key().into(),))?),
2746        RpcTest::identity(F3GetF3PowerTable::request((tipset.key().into(),))?),
2747    ])
2748}
2749
2750// Extract tests that use chain-specific data such as block CIDs or message
2751// CIDs. Right now, only the last `n_tipsets` tipsets are used.
2752fn snapshot_tests(
2753    store: Arc<ManyCar>,
2754    offline: bool,
2755    num_tipsets: usize,
2756    miner_address: Option<Address>,
2757    eth_chain_id: u64,
2758) -> anyhow::Result<Vec<RpcTest>> {
2759    let mut tests = vec![];
2760    // shared_tipset in the snapshot might not be finalized for the offline RPC server
2761    // use heaviest - SAFE_EPOCH_DELAY instead
2762    let shared_tipset = store
2763        .heaviest_tipset()?
2764        .chain(&store)
2765        .take(SAFE_EPOCH_DELAY as usize)
2766        .last()
2767        .expect("Infallible");
2768
2769    for tipset in shared_tipset.chain(&store).take(num_tipsets) {
2770        tests.extend(chain_tests_with_tipset(&store, offline, &tipset)?);
2771        tests.extend(miner_tests_with_tipset(&store, &tipset, miner_address)?);
2772        tests.extend(state_tests_with_tipset(&store, &tipset)?);
2773        tests.extend(eth_tests_with_tipset(&store, &tipset));
2774        tests.extend(event_tests_with_tipset(&store, &tipset));
2775        tests.extend(gas_tests_with_tipset(&tipset));
2776        tests.extend(mpool_tests_with_tipset(&tipset));
2777        tests.extend(eth_state_tests_with_tipset(&store, &tipset, eth_chain_id)?);
2778        tests.extend(f3_tests_with_tipset(&tipset)?);
2779    }
2780
2781    Ok(tests)
2782}
2783
2784fn sample_message_cids<'a>(
2785    bls_messages: impl Iterator<Item = &'a Message> + 'a,
2786    secp_messages: impl Iterator<Item = &'a SignedMessage> + 'a,
2787) -> impl Iterator<Item = Cid> + 'a {
2788    bls_messages
2789        .map(|m| m.cid())
2790        .unique()
2791        .take(COLLECTION_SAMPLE_SIZE)
2792        .chain(
2793            secp_messages
2794                .map(|m| m.cid())
2795                .unique()
2796                .take(COLLECTION_SAMPLE_SIZE),
2797        )
2798        .unique()
2799}
2800
2801fn sample_messages<'a>(
2802    bls_messages: impl Iterator<Item = &'a Message> + 'a,
2803    secp_messages: impl Iterator<Item = &'a SignedMessage> + 'a,
2804) -> impl Iterator<Item = &'a Message> + 'a {
2805    bls_messages
2806        .unique()
2807        .take(COLLECTION_SAMPLE_SIZE)
2808        .chain(
2809            secp_messages
2810                .map(SignedMessage::message)
2811                .unique()
2812                .take(COLLECTION_SAMPLE_SIZE),
2813        )
2814        .unique()
2815}
2816
2817fn sample_signed_messages<'a>(
2818    bls_messages: impl Iterator<Item = &'a Message> + 'a,
2819    secp_messages: impl Iterator<Item = &'a SignedMessage> + 'a,
2820) -> impl Iterator<Item = SignedMessage> + 'a {
2821    bls_messages
2822        .unique()
2823        .take(COLLECTION_SAMPLE_SIZE)
2824        .map(|msg| {
2825            let sig = Signature::new_bls(vec![]);
2826            SignedMessage::new_unchecked(msg.clone(), sig)
2827        })
2828        .chain(secp_messages.cloned().unique().take(COLLECTION_SAMPLE_SIZE))
2829        .unique()
2830}
2831
2832pub(super) async fn create_tests(
2833    CreateTestsArgs {
2834        offline,
2835        n_tipsets,
2836        miner_address,
2837        worker_address,
2838        eth_chain_id,
2839        snapshot_files,
2840    }: CreateTestsArgs,
2841) -> anyhow::Result<Vec<RpcTest>> {
2842    let mut tests = vec![];
2843    tests.extend(auth_tests()?);
2844    tests.extend(common_tests());
2845    tests.extend(chain_tests());
2846    tests.extend(mpool_tests());
2847    tests.extend(net_tests());
2848    tests.extend(node_tests());
2849    tests.extend(wallet_tests(worker_address));
2850    tests.extend(eth_tests());
2851    tests.extend(f3_tests()?);
2852    if !snapshot_files.is_empty() {
2853        let store = Arc::new(ManyCar::try_from(snapshot_files.clone())?);
2854        revalidate_chain(store.clone(), n_tipsets).await?;
2855        tests.extend(snapshot_tests(
2856            store,
2857            offline,
2858            n_tipsets,
2859            miner_address,
2860            eth_chain_id,
2861        )?);
2862    }
2863    tests.sort_by_key(|test| test.request.method_name.clone());
2864
2865    tests.extend(create_deferred_tests(snapshot_files)?);
2866    Ok(tests)
2867}
2868
2869// Some tests, especially those mutating the node's state, need to be run last.
2870fn create_deferred_tests(snapshot_files: Vec<PathBuf>) -> anyhow::Result<Vec<RpcTest>> {
2871    let mut tests = vec![];
2872
2873    if !snapshot_files.is_empty() {
2874        let store = Arc::new(ManyCar::try_from(snapshot_files)?);
2875        tests.push(RpcTest::identity(ChainSetHead::request((store
2876            .heaviest_tipset()?
2877            .key()
2878            .clone(),))?));
2879    }
2880
2881    Ok(tests)
2882}
2883
2884async fn revalidate_chain(db: Arc<ManyCar>, n_ts_to_validate: usize) -> anyhow::Result<()> {
2885    if n_ts_to_validate == 0 {
2886        return Ok(());
2887    }
2888    let chain_config = Arc::new(handle_chain_config(&NetworkChain::Calibnet)?);
2889
2890    let genesis_header = crate::genesis::read_genesis_header(
2891        None,
2892        chain_config.genesis_bytes(&db).await?.as_deref(),
2893        &db,
2894    )
2895    .await?;
2896    let chain_store = Arc::new(ChainStore::new(
2897        db.clone(),
2898        db.clone(),
2899        db.clone(),
2900        chain_config,
2901        genesis_header.clone(),
2902    )?);
2903    let state_manager = Arc::new(StateManager::new(chain_store.clone())?);
2904    let head_ts = db.heaviest_tipset()?;
2905
2906    // Set proof parameter data dir and make sure the proofs are available. Otherwise,
2907    // validation might fail due to missing proof parameters.
2908    proofs_api::maybe_set_proofs_parameter_cache_dir_env(&Config::default().client.data_dir);
2909    ensure_proof_params_downloaded().await?;
2910    state_manager.validate_tipsets(
2911        head_ts
2912            .chain(&db)
2913            .take(SAFE_EPOCH_DELAY as usize + n_ts_to_validate),
2914    )?;
2915
2916    Ok(())
2917}
2918
2919#[allow(clippy::too_many_arguments)]
2920pub(super) async fn run_tests(
2921    tests: impl IntoIterator<Item = RpcTest>,
2922    forest: impl Into<Arc<rpc::Client>>,
2923    lotus: impl Into<Arc<rpc::Client>>,
2924    max_concurrent_requests: usize,
2925    filter_file: Option<PathBuf>,
2926    filter: String,
2927    filter_version: Option<rpc::ApiPaths>,
2928    run_ignored: RunIgnored,
2929    fail_fast: bool,
2930    dump_dir: Option<PathBuf>,
2931    test_criteria_overrides: &[TestCriteriaOverride],
2932    report_dir: Option<PathBuf>,
2933    report_mode: ReportMode,
2934    n_retries: usize,
2935) -> anyhow::Result<()> {
2936    let forest = Into::<Arc<rpc::Client>>::into(forest);
2937    let lotus = Into::<Arc<rpc::Client>>::into(lotus);
2938    let semaphore = Arc::new(Semaphore::new(max_concurrent_requests));
2939    let mut tasks = JoinSet::new();
2940
2941    let filter_list = if let Some(filter_file) = &filter_file {
2942        FilterList::new_from_file(filter_file)?
2943    } else {
2944        FilterList::default().allow(filter.clone())
2945    };
2946
2947    // Always use ReportBuilder for consistency
2948    let mut report_builder = ReportBuilder::new(&filter_list, report_mode);
2949
2950    // deduplicate tests by their hash-able representations
2951    for test in tests.into_iter().unique_by(
2952        |RpcTest {
2953             request:
2954                 rpc::Request {
2955                     method_name,
2956                     params,
2957                     api_paths,
2958                     ..
2959                 },
2960             ignore,
2961             ..
2962         }| {
2963            (
2964                method_name.clone(),
2965                params.clone(),
2966                *api_paths,
2967                ignore.is_some(),
2968            )
2969        },
2970    ) {
2971        // By default, do not run ignored tests.
2972        if matches!(run_ignored, RunIgnored::Default) && test.ignore.is_some() {
2973            continue;
2974        }
2975        // If in `IgnoreOnly` mode, only run ignored tests.
2976        if matches!(run_ignored, RunIgnored::IgnoredOnly) && test.ignore.is_none() {
2977            continue;
2978        }
2979
2980        if !filter_list.authorize(&test.request.method_name) {
2981            continue;
2982        }
2983
2984        if let Some(filter_version) = filter_version
2985            && !test.request.api_paths.contains(filter_version)
2986        {
2987            continue;
2988        }
2989
2990        // Acquire a permit from the semaphore before spawning a test
2991        let semaphore = semaphore.clone();
2992        let forest = forest.clone();
2993        let lotus = lotus.clone();
2994        let test_criteria_overrides = test_criteria_overrides.to_vec();
2995        tasks.spawn(async move {
2996            let mut n_retries_left = n_retries;
2997            let mut backoff_secs = 2;
2998            loop {
2999                {
3000                    // Ignore the error since 'An acquire operation can only fail if the semaphore has been closed'
3001                    let _permit = semaphore.acquire().await;
3002                    let test_result = test.run(&forest, &lotus).await;
3003                    let success =
3004                        evaluate_test_success(&test_result, &test, &test_criteria_overrides);
3005                    if success || n_retries_left == 0 {
3006                        return (success, test, test_result);
3007                    }
3008                    // Release the semaphore before sleeping
3009                }
3010                // Sleep before each retry
3011                tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
3012                n_retries_left = n_retries_left.saturating_sub(1);
3013                backoff_secs = backoff_secs.saturating_mul(2);
3014            }
3015        });
3016    }
3017
3018    // If no tests to run after filtering, return early without saving/printing
3019    if tasks.is_empty() {
3020        return Ok(());
3021    }
3022
3023    while let Some(result) = tasks.join_next().await {
3024        match result {
3025            Ok((success, test, test_result)) => {
3026                let method_name = test.request.method_name.clone();
3027
3028                report_builder.track_test_result(
3029                    method_name.as_ref(),
3030                    success,
3031                    &test_result,
3032                    &test.request.params,
3033                );
3034
3035                // Dump test data if configured
3036                if let (Some(dump_dir), Some(test_dump)) = (&dump_dir, &test_result.test_dump) {
3037                    dump_test_data(dump_dir, success, test_dump)?;
3038                }
3039
3040                if !success && fail_fast {
3041                    break;
3042                }
3043            }
3044            Err(e) => tracing::warn!("{e}"),
3045        }
3046    }
3047
3048    let has_failures = report_builder.has_failures();
3049    report_builder.print_summary();
3050
3051    if let Some(path) = report_dir {
3052        report_builder.finalize_and_save(&path)?;
3053    }
3054
3055    anyhow::ensure!(!has_failures, "Some tests failed");
3056
3057    Ok(())
3058}
3059
3060/// Evaluate whether a test is successful based on the test result and criteria
3061fn evaluate_test_success(
3062    test_result: &TestResult,
3063    test: &RpcTest,
3064    test_criteria_overrides: &[TestCriteriaOverride],
3065) -> bool {
3066    match (&test_result.forest_status, &test_result.lotus_status) {
3067        (TestSummary::Valid, TestSummary::Valid) => true,
3068        (TestSummary::Valid, TestSummary::Timeout) => {
3069            test_criteria_overrides.contains(&TestCriteriaOverride::ValidAndTimeout)
3070        }
3071        (TestSummary::Timeout, TestSummary::Timeout) => {
3072            test_criteria_overrides.contains(&TestCriteriaOverride::TimeoutAndTimeout)
3073        }
3074        (TestSummary::Rejected(reason_forest), TestSummary::Rejected(reason_lotus)) => {
3075            match test.policy_on_rejected {
3076                PolicyOnRejected::Pass => true,
3077                PolicyOnRejected::PassWithIdenticalError => reason_forest == reason_lotus,
3078                PolicyOnRejected::PassWithIdenticalErrorCaseInsensitive => {
3079                    reason_forest.eq_ignore_ascii_case(reason_lotus)
3080                }
3081                PolicyOnRejected::PassWithQuasiIdenticalError => {
3082                    reason_lotus.contains(reason_forest) || reason_forest.contains(reason_lotus)
3083                }
3084                _ => false,
3085            }
3086        }
3087        _ => false,
3088    }
3089}
3090
3091fn normalized_error_message(s: &str) -> Cow<'_, str> {
3092    // remove `RPC error (-32603):` prefix added by `lotus-gateway`
3093    let lotus_gateway_error_prefix = lazy_regex::regex!(r#"^RPC\serror\s\(-?\d+\):\s*"#);
3094    lotus_gateway_error_prefix.replace(s, "")
3095}
3096
3097/// Dump test data to the specified directory
3098fn dump_test_data(dump_dir: &Path, success: bool, test_dump: &TestDump) -> anyhow::Result<()> {
3099    let dir = dump_dir.join(if success { "valid" } else { "invalid" });
3100    if !dir.is_dir() {
3101        std::fs::create_dir_all(&dir)?;
3102    }
3103    let file_name = format!(
3104        "{}_{}.json",
3105        test_dump
3106            .request
3107            .method_name
3108            .as_ref()
3109            .replace(".", "_")
3110            .to_lowercase(),
3111        Utc::now().timestamp_micros()
3112    );
3113    std::fs::write(
3114        dir.join(file_name),
3115        serde_json::to_string_pretty(test_dump)?,
3116    )?;
3117    Ok(())
3118}
3119
3120fn validate_message_lookup(req: rpc::Request<MessageLookup>) -> RpcTest {
3121    RpcTest::validate(req, |mut forest, mut lotus| {
3122        // TODO(hanabi1224): https://github.com/ChainSafe/forest/issues/3784
3123        forest.return_dec = Ipld::Null;
3124        lotus.return_dec = Ipld::Null;
3125        forest == lotus
3126    })
3127}
3128
3129fn validate_tagged_tipset_v2(req: rpc::Request<Option<Tipset>>, offline: bool) -> RpcTest {
3130    RpcTest::validate(req, move |forest, lotus| match (forest, lotus) {
3131        (None, None) => true,
3132        (Some(forest), Some(lotus)) => {
3133            if offline {
3134                true
3135            } else {
3136                (forest.epoch() - lotus.epoch()).abs() <= 2
3137            }
3138        }
3139        _ => false,
3140    })
3141}
3142
3143#[cfg(test)]
3144mod tests {
3145    use super::*;
3146
3147    #[test]
3148    fn test_normalized_error_message_1() {
3149        let s = "RPC error (-32603): exactly one tipset selection criteria must be specified";
3150        let r = normalized_error_message(s);
3151        assert_eq!(
3152            r.as_ref(),
3153            "exactly one tipset selection criteria must be specified"
3154        );
3155    }
3156
3157    #[test]
3158    fn test_normalized_error_message_2() {
3159        let s = "exactly one tipset selection criteria must be specified";
3160        let r = normalized_error_message(s);
3161        assert_eq!(
3162            r.as_ref(),
3163            "exactly one tipset selection criteria must be specified"
3164        );
3165    }
3166}