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,
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, 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, 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, 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, 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, true,
1846            ),))
1847            .unwrap(),
1848        ),
1849        // Nodes might be synced to different epochs, so we can't assert the exact result here.
1850        // Regardless, we want to check if the node returns a valid response and accepts predefined
1851        // values.
1852        RpcTest::basic(
1853            EthGetBlockReceipts::request((BlockNumberOrHash::from_predefined(Predefined::Latest),))
1854                .unwrap(),
1855        ),
1856        RpcTest::identity(
1857            EthGetBlockReceiptsV2::request((ExtBlockNumberOrHash::from_block_hash_object(
1858                block_hash, true,
1859            ),))
1860            .unwrap(),
1861        ),
1862        RpcTest::basic(
1863            EthGetBlockReceiptsV2::request((ExtBlockNumberOrHash::from_predefined(
1864                ExtPredefined::Safe,
1865            ),))
1866            .unwrap(),
1867        ),
1868        RpcTest::basic(
1869            EthGetBlockReceiptsV2::request((ExtBlockNumberOrHash::from_predefined(
1870                ExtPredefined::Finalized,
1871            ),))
1872            .unwrap(),
1873        ),
1874        RpcTest::identity(EthGetBlockTransactionCountByHash::request((block_hash,)).unwrap()),
1875        RpcTest::identity(
1876            EthGetBlockReceiptsLimited::request((
1877                BlockNumberOrHash::from_block_hash_object(block_hash, true),
1878                4,
1879            ))
1880            .unwrap(),
1881        )
1882        .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
1883        RpcTest::identity(
1884            EthGetBlockReceiptsLimited::request((
1885                BlockNumberOrHash::from_block_hash_object(block_hash, true),
1886                -1,
1887            ))
1888            .unwrap(),
1889        ),
1890        RpcTest::identity(
1891            EthGetBlockReceiptsLimitedV2::request((
1892                ExtBlockNumberOrHash::from_block_hash_object(block_hash, true),
1893                4,
1894            ))
1895            .unwrap(),
1896        )
1897        .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
1898        RpcTest::identity(
1899            EthGetBlockReceiptsLimitedV2::request((
1900                ExtBlockNumberOrHash::from_block_hash_object(block_hash, true),
1901                -1,
1902            ))
1903            .unwrap(),
1904        ),
1905        RpcTest::identity(
1906            EthGetBlockTransactionCountByNumber::request((EthInt64(shared_tipset.epoch()),))
1907                .unwrap(),
1908        ),
1909        RpcTest::identity(
1910            EthGetBlockTransactionCountByNumberV2::request((BlockNumberOrPredefined::BlockNumber(
1911                EthInt64(shared_tipset.epoch()),
1912            ),))
1913            .unwrap(),
1914        ),
1915        RpcTest::identity(
1916            EthGetBlockTransactionCountByNumberV2::request((
1917                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Safe),
1918            ))
1919            .unwrap(),
1920        ),
1921        RpcTest::identity(
1922            EthGetBlockTransactionCountByNumberV2::request((
1923                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Finalized),
1924            ))
1925            .unwrap(),
1926        ),
1927        RpcTest::identity(
1928            EthGetTransactionCount::request((
1929                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1930                BlockNumberOrHash::from_block_hash_object(block_hash, true),
1931            ))
1932            .unwrap(),
1933        ),
1934        RpcTest::identity(
1935            EthGetTransactionCount::request((
1936                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1937                BlockNumberOrHash::from_predefined(Predefined::Earliest),
1938            ))
1939            .unwrap(),
1940        )
1941        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
1942        RpcTest::basic(
1943            EthGetTransactionCount::request((
1944                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1945                BlockNumberOrHash::from_predefined(Predefined::Pending),
1946            ))
1947            .unwrap(),
1948        ),
1949        RpcTest::basic(
1950            EthGetTransactionCount::request((
1951                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1952                BlockNumberOrHash::from_predefined(Predefined::Latest),
1953            ))
1954            .unwrap(),
1955        ),
1956        RpcTest::identity(
1957            EthGetTransactionCount::request((
1958                generate_eth_random_address().unwrap(),
1959                BlockNumberOrHash::from_predefined(Predefined::Latest),
1960            ))
1961            .unwrap(),
1962        ),
1963        RpcTest::identity(
1964            EthGetTransactionCountV2::request((
1965                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1966                ExtBlockNumberOrHash::from_block_hash_object(block_hash, true),
1967            ))
1968            .unwrap(),
1969        ),
1970        RpcTest::identity(
1971            EthGetTransactionCountV2::request((
1972                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1973                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Earliest),
1974            ))
1975            .unwrap(),
1976        )
1977        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
1978        RpcTest::basic(
1979            EthGetTransactionCountV2::request((
1980                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1981                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Pending),
1982            ))
1983            .unwrap(),
1984        ),
1985        RpcTest::basic(
1986            EthGetTransactionCountV2::request((
1987                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1988                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),
1989            ))
1990            .unwrap(),
1991        ),
1992        RpcTest::basic(
1993            EthGetTransactionCountV2::request((
1994                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
1995                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe),
1996            ))
1997            .unwrap(),
1998        ),
1999        RpcTest::basic(
2000            EthGetTransactionCountV2::request((
2001                EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(),
2002                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Finalized),
2003            ))
2004            .unwrap(),
2005        ),
2006        RpcTest::identity(
2007            EthGetTransactionCountV2::request((
2008                generate_eth_random_address().unwrap(),
2009                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),
2010            ))
2011            .unwrap(),
2012        ),
2013        RpcTest::identity(
2014            EthGetStorageAt::request((
2015                // https://filfox.info/en/address/f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq
2016                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2017                EthBytes(vec![0xa]),
2018                BlockNumberOrHash::BlockNumber(EthInt64(shared_tipset.epoch())),
2019            ))
2020            .unwrap(),
2021        ),
2022        RpcTest::identity(
2023            EthGetStorageAt::request((
2024                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2025                EthBytes(vec![0xa]),
2026                BlockNumberOrHash::from_predefined(Predefined::Earliest),
2027            ))
2028            .unwrap(),
2029        )
2030        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2031        RpcTest::basic(
2032            EthGetStorageAt::request((
2033                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2034                EthBytes(vec![0xa]),
2035                BlockNumberOrHash::from_predefined(Predefined::Pending),
2036            ))
2037            .unwrap(),
2038        ),
2039        RpcTest::basic(
2040            EthGetStorageAt::request((
2041                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2042                EthBytes(vec![0xa]),
2043                BlockNumberOrHash::from_predefined(Predefined::Latest),
2044            ))
2045            .unwrap(),
2046        ),
2047        RpcTest::identity(
2048            EthGetStorageAt::request((
2049                generate_eth_random_address().unwrap(),
2050                EthBytes(vec![0x0]),
2051                BlockNumberOrHash::from_predefined(Predefined::Latest),
2052            ))
2053            .unwrap(),
2054        ),
2055        RpcTest::identity(
2056            EthGetStorageAtV2::request((
2057                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2058                EthBytes(vec![0xa]),
2059                ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2060            ))
2061            .unwrap(),
2062        ),
2063        RpcTest::basic(
2064            EthGetStorageAtV2::request((
2065                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2066                EthBytes(vec![0xa]),
2067                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe),
2068            ))
2069            .unwrap(),
2070        ),
2071        RpcTest::basic(
2072            EthGetStorageAtV2::request((
2073                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2074                EthBytes(vec![0xa]),
2075                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Finalized),
2076            ))
2077            .unwrap(),
2078        ),
2079        RpcTest::identity(
2080            EthGetStorageAtV2::request((
2081                generate_eth_random_address().unwrap(),
2082                EthBytes(vec![0x0]),
2083                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),
2084            ))
2085            .unwrap(),
2086        ),
2087        RpcTest::identity(
2088            EthFeeHistory::request((
2089                10.into(),
2090                BlockNumberOrPredefined::BlockNumber(shared_tipset.epoch().into()),
2091                None,
2092            ))
2093            .unwrap(),
2094        ),
2095        RpcTest::identity(
2096            EthFeeHistory::request((
2097                10.into(),
2098                BlockNumberOrPredefined::BlockNumber(shared_tipset.epoch().into()),
2099                Some(vec![10., 50., 90.]),
2100            ))
2101            .unwrap(),
2102        ),
2103        RpcTest::identity(
2104            EthFeeHistory::request((
2105                10.into(),
2106                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Earliest),
2107                None,
2108            ))
2109            .unwrap(),
2110        )
2111        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2112        RpcTest::basic(
2113            EthFeeHistory::request((
2114                10.into(),
2115                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Pending),
2116                Some(vec![10., 50., 90.]),
2117            ))
2118            .unwrap(),
2119        ),
2120        RpcTest::basic(
2121            EthFeeHistory::request((
2122                10.into(),
2123                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Latest),
2124                None,
2125            ))
2126            .unwrap(),
2127        ),
2128        RpcTest::basic(
2129            EthFeeHistory::request((
2130                10.into(),
2131                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Safe),
2132                None,
2133            ))
2134            .unwrap(),
2135        ),
2136        RpcTest::basic(
2137            EthFeeHistory::request((
2138                10.into(),
2139                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Finalized),
2140                Some(vec![10., 50., 90.]),
2141            ))
2142            .unwrap(),
2143        ),
2144        RpcTest::identity(
2145            EthFeeHistoryV2::request((
2146                10.into(),
2147                ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2148                None,
2149            ))
2150            .unwrap(),
2151        ),
2152        RpcTest::identity(
2153            EthFeeHistoryV2::request((
2154                10.into(),
2155                ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2156                Some(vec![10., 50., 90.]),
2157            ))
2158            .unwrap(),
2159        ),
2160        RpcTest::identity(
2161            EthFeeHistoryV2::request((
2162                10.into(),
2163                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Earliest),
2164                None,
2165            ))
2166            .unwrap(),
2167        )
2168        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2169        RpcTest::basic(
2170            EthFeeHistoryV2::request((
2171                10.into(),
2172                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Pending),
2173                Some(vec![10., 50., 90.]),
2174            ))
2175            .unwrap(),
2176        ),
2177        RpcTest::basic(
2178            EthFeeHistoryV2::request((
2179                10.into(),
2180                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),
2181                None,
2182            ))
2183            .unwrap(),
2184        ),
2185        RpcTest::basic(
2186            EthFeeHistoryV2::request((
2187                10.into(),
2188                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe),
2189                None,
2190            ))
2191            .unwrap(),
2192        ),
2193        RpcTest::basic(
2194            EthFeeHistoryV2::request((
2195                10.into(),
2196                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Finalized),
2197                Some(vec![10., 50., 90.]),
2198            ))
2199            .unwrap(),
2200        ),
2201        RpcTest::identity(
2202            EthGetCode::request((
2203                // https://filfox.info/en/address/f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq
2204                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2205                BlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2206            ))
2207            .unwrap(),
2208        ),
2209        RpcTest::identity(
2210            EthGetCode::request((
2211                // https://filfox.info/en/address/f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq
2212                Address::from_str("f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq")
2213                    .unwrap()
2214                    .try_into()
2215                    .unwrap(),
2216                BlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2217            ))
2218            .unwrap(),
2219        ),
2220        RpcTest::identity(
2221            EthGetCode::request((
2222                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2223                BlockNumberOrHash::from_predefined(Predefined::Earliest),
2224            ))
2225            .unwrap(),
2226        )
2227        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2228        RpcTest::basic(
2229            EthGetCode::request((
2230                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2231                BlockNumberOrHash::from_predefined(Predefined::Pending),
2232            ))
2233            .unwrap(),
2234        ),
2235        RpcTest::basic(
2236            EthGetCode::request((
2237                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2238                BlockNumberOrHash::from_predefined(Predefined::Latest),
2239            ))
2240            .unwrap(),
2241        ),
2242        RpcTest::identity(
2243            EthGetCode::request((
2244                generate_eth_random_address().unwrap(),
2245                BlockNumberOrHash::from_predefined(Predefined::Latest),
2246            ))
2247            .unwrap(),
2248        ),
2249        RpcTest::identity(
2250            EthGetCodeV2::request((
2251                // https://filfox.info/en/address/f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq
2252                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2253                ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2254            ))
2255            .unwrap(),
2256        ),
2257        RpcTest::basic(
2258            EthGetCodeV2::request((
2259                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2260                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe),
2261            ))
2262            .unwrap(),
2263        ),
2264        RpcTest::basic(
2265            EthGetCodeV2::request((
2266                EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2267                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Finalized),
2268            ))
2269            .unwrap(),
2270        ),
2271        RpcTest::identity(
2272            EthGetCodeV2::request((
2273                generate_eth_random_address().unwrap(),
2274                ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),
2275            ))
2276            .unwrap(),
2277        ),
2278        RpcTest::identity(
2279            EthGetTransactionByBlockNumberAndIndex::request((
2280                BlockNumberOrPredefined::BlockNumber(shared_tipset.epoch().into()),
2281                0.into(),
2282            ))
2283            .unwrap(),
2284        )
2285        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2286        RpcTest::identity(
2287            EthGetTransactionByBlockNumberAndIndex::request((
2288                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Earliest),
2289                0.into(),
2290            ))
2291            .unwrap(),
2292        )
2293        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2294        RpcTest::identity(
2295            EthGetTransactionByBlockNumberAndIndex::request((
2296                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Pending),
2297                0.into(),
2298            ))
2299            .unwrap(),
2300        )
2301        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2302        RpcTest::identity(
2303            EthGetTransactionByBlockNumberAndIndex::request((
2304                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Latest),
2305                0.into(),
2306            ))
2307            .unwrap(),
2308        )
2309        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2310        RpcTest::identity(
2311            EthGetTransactionByBlockNumberAndIndexV2::request((
2312                BlockNumberOrPredefined::BlockNumber(shared_tipset.epoch().into()),
2313                0.into(),
2314            ))
2315            .unwrap(),
2316        )
2317        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2318        RpcTest::identity(
2319            EthGetTransactionByBlockNumberAndIndexV2::request((
2320                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Safe),
2321                0.into(),
2322            ))
2323            .unwrap(),
2324        )
2325        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2326        RpcTest::identity(
2327            EthGetTransactionByBlockNumberAndIndexV2::request((
2328                BlockNumberOrPredefined::PredefinedBlock(ExtPredefined::Finalized),
2329                0.into(),
2330            ))
2331            .unwrap(),
2332        )
2333        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2334        RpcTest::identity(
2335            EthGetTransactionByBlockHashAndIndex::request((block_hash, 0.into())).unwrap(),
2336        )
2337        .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
2338        RpcTest::identity(EthGetBlockByHash::request((block_hash, false)).unwrap()),
2339        RpcTest::identity(EthGetBlockByHash::request((block_hash, true)).unwrap()),
2340        RpcTest::identity(
2341            EthGetLogs::request((EthFilterSpec {
2342                from_block: Some(format!("0x{:x}", shared_tipset.epoch())),
2343                to_block: Some(format!("0x{:x}", shared_tipset.epoch())),
2344                ..Default::default()
2345            },))
2346            .unwrap(),
2347        )
2348        .sort_policy(SortPolicy::All)
2349        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2350        RpcTest::identity(
2351            EthGetLogs::request((EthFilterSpec {
2352                from_block: Some(format!("0x{:x}", shared_tipset.epoch())),
2353                to_block: Some(format!("0x{:x}", shared_tipset.epoch())),
2354                address: Some(EthAddressList::List(Vec::new())),
2355                ..Default::default()
2356            },))
2357            .unwrap(),
2358        )
2359        .sort_policy(SortPolicy::All)
2360        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2361        RpcTest::identity(
2362            EthGetLogs::request((EthFilterSpec {
2363                from_block: Some(format!("0x{:x}", shared_tipset.epoch() - 100)),
2364                to_block: Some(format!("0x{:x}", shared_tipset.epoch())),
2365                ..Default::default()
2366            },))
2367            .unwrap(),
2368        )
2369        .sort_policy(SortPolicy::All)
2370        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2371        RpcTest::identity(
2372            EthGetLogs::request((EthFilterSpec {
2373                address: Some(EthAddressList::Single(
2374                    EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(),
2375                )),
2376                ..Default::default()
2377            },))
2378            .unwrap(),
2379        )
2380        .sort_policy(SortPolicy::All)
2381        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2382        RpcTest::identity(EthGetFilterLogs::request((FilterID::new().unwrap(),)).unwrap())
2383            .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
2384        RpcTest::identity(EthGetFilterChanges::request((FilterID::new().unwrap(),)).unwrap())
2385            .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
2386        RpcTest::identity(EthGetTransactionHashByCid::request((block_cid,)).unwrap()),
2387        RpcTest::identity(
2388            EthTraceBlock::request((ExtBlockNumberOrHash::from_block_number(
2389                shared_tipset.epoch(),
2390            ),))
2391            .unwrap(),
2392        ),
2393        RpcTest::identity(
2394            EthTraceBlock::request((ExtBlockNumberOrHash::from_predefined(
2395                ExtPredefined::Earliest,
2396            ),))
2397            .unwrap(),
2398        )
2399        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2400        RpcTest::basic(
2401            EthTraceBlock::request((ExtBlockNumberOrHash::from_predefined(
2402                ExtPredefined::Pending,
2403            ),))
2404            .unwrap(),
2405        ),
2406        RpcTest::basic(
2407            EthTraceBlock::request((ExtBlockNumberOrHash::from_predefined(ExtPredefined::Latest),))
2408                .unwrap(),
2409        ),
2410        RpcTest::basic(
2411            EthTraceBlock::request((ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe),))
2412                .unwrap(),
2413        ),
2414        RpcTest::basic(
2415            EthTraceBlock::request((ExtBlockNumberOrHash::from_predefined(
2416                ExtPredefined::Finalized,
2417            ),))
2418            .unwrap(),
2419        ),
2420        RpcTest::identity(
2421            EthTraceBlockV2::request((ExtBlockNumberOrHash::from_block_number(
2422                shared_tipset.epoch(),
2423            ),))
2424            .unwrap(),
2425        ),
2426        RpcTest::basic(
2427            EthTraceBlockV2::request((ExtBlockNumberOrHash::from_predefined(
2428                ExtPredefined::Pending,
2429            ),))
2430            .unwrap(),
2431        ),
2432        RpcTest::basic(
2433            EthTraceBlockV2::request((ExtBlockNumberOrHash::from_predefined(
2434                ExtPredefined::Latest,
2435            ),))
2436            .unwrap(),
2437        ),
2438        RpcTest::basic(
2439            EthTraceBlockV2::request((ExtBlockNumberOrHash::from_predefined(ExtPredefined::Safe),))
2440                .unwrap(),
2441        ),
2442        RpcTest::basic(
2443            EthTraceBlockV2::request((ExtBlockNumberOrHash::from_predefined(
2444                ExtPredefined::Finalized,
2445            ),))
2446            .unwrap(),
2447        ),
2448        RpcTest::identity(
2449            EthTraceReplayBlockTransactions::request((
2450                ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2451                vec!["trace".to_string()],
2452            ))
2453            .unwrap(),
2454        ),
2455        RpcTest::identity(
2456            EthTraceReplayBlockTransactionsV2::request((
2457                ExtBlockNumberOrHash::from_block_number(shared_tipset.epoch()),
2458                vec!["trace".to_string()],
2459            ))
2460            .unwrap(),
2461        ),
2462        RpcTest::identity(
2463            EthTraceFilter::request((EthTraceFilterCriteria {
2464                from_block: Some(format!("0x{:x}", shared_tipset.epoch() - 100)),
2465                to_block: Some(format!("0x{:x}", shared_tipset.epoch() - SAFE_EPOCH_DELAY)),
2466                ..Default::default()
2467            },))
2468            .unwrap(),
2469        )
2470        // both nodes could fail on, e.g., "too many results, maximum supported is 500, try paginating
2471        // requests with After and Count"
2472        .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError),
2473        RpcTest::identity(
2474            EthGetTransactionReceipt::request((
2475                // A transaction that should not exist, to test the `null` response in case
2476                // of missing transaction.
2477                EthHash::from_str(
2478                    "0xf234567890123456789d6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f70809",
2479                )
2480                .unwrap(),
2481            ))
2482            .unwrap(),
2483        ),
2484    ];
2485
2486    for block in shared_tipset.block_headers() {
2487        tests.extend([RpcTest::identity(
2488            FilecoinAddressToEthAddress::request((
2489                block.miner_address,
2490                Some(BlockNumberOrPredefined::PredefinedBlock(
2491                    ExtPredefined::Latest,
2492                )),
2493            ))
2494            .unwrap(),
2495        )]);
2496        let (bls_messages, secp_messages) =
2497            crate::chain::store::block_messages(store, block).unwrap();
2498        for msg in sample_messages(bls_messages.iter(), secp_messages.iter()) {
2499            tests.extend([RpcTest::identity(
2500                FilecoinAddressToEthAddress::request((
2501                    msg.from(),
2502                    Some(BlockNumberOrPredefined::PredefinedBlock(
2503                        ExtPredefined::Latest,
2504                    )),
2505                ))
2506                .unwrap(),
2507            )]);
2508            if let Ok(eth_to_addr) = EthAddress::try_from(msg.to) {
2509                tests.extend([RpcTest::identity(
2510                    EthEstimateGas::request((
2511                        EthCallMessage {
2512                            to: Some(eth_to_addr),
2513                            value: Some(msg.value.clone().into()),
2514                            data: Some(msg.params.clone().into()),
2515                            ..Default::default()
2516                        },
2517                        Some(BlockNumberOrHash::BlockNumber(shared_tipset.epoch().into())),
2518                    ))
2519                    .unwrap(),
2520                )
2521                .policy_on_rejected(PolicyOnRejected::Pass)]);
2522                tests.extend([RpcTest::identity(
2523                    EthEstimateGasV2::request((
2524                        EthCallMessage {
2525                            to: Some(eth_to_addr),
2526                            value: Some(msg.value.clone().into()),
2527                            data: Some(msg.params.clone().into()),
2528                            ..Default::default()
2529                        },
2530                        Some(ExtBlockNumberOrHash::BlockNumber(
2531                            shared_tipset.epoch().into(),
2532                        )),
2533                    ))
2534                    .unwrap(),
2535                )
2536                .policy_on_rejected(PolicyOnRejected::Pass)]);
2537            }
2538        }
2539    }
2540
2541    tests
2542}
2543
2544fn read_state_api_tests(tipset: &Tipset) -> anyhow::Result<Vec<RpcTest>> {
2545    let tests = vec![
2546        RpcTest::identity(StateReadState::request((
2547            Address::SYSTEM_ACTOR,
2548            tipset.key().into(),
2549        ))?),
2550        RpcTest::identity(StateReadState::request((
2551            Address::SYSTEM_ACTOR,
2552            Default::default(),
2553        ))?),
2554        RpcTest::identity(StateReadState::request((
2555            Address::CRON_ACTOR,
2556            tipset.key().into(),
2557        ))?),
2558        RpcTest::identity(StateReadState::request((
2559            Address::MARKET_ACTOR,
2560            tipset.key().into(),
2561        ))?),
2562        RpcTest::identity(StateReadState::request((
2563            Address::INIT_ACTOR,
2564            tipset.key().into(),
2565        ))?),
2566        RpcTest::identity(StateReadState::request((
2567            Address::POWER_ACTOR,
2568            tipset.key().into(),
2569        ))?),
2570        RpcTest::identity(StateReadState::request((
2571            Address::REWARD_ACTOR,
2572            tipset.key().into(),
2573        ))?),
2574        RpcTest::identity(StateReadState::request((
2575            Address::VERIFIED_REGISTRY_ACTOR,
2576            tipset.key().into(),
2577        ))?),
2578        RpcTest::identity(StateReadState::request((
2579            Address::DATACAP_TOKEN_ACTOR,
2580            tipset.key().into(),
2581        ))?),
2582        RpcTest::identity(StateReadState::request((
2583            // payment channel actor address `t066116`
2584            Address::new_id(66116), // https://calibration.filscan.io/en/address/t066116/
2585            tipset.key().into(),
2586        ))?),
2587        RpcTest::identity(StateReadState::request((
2588            // multisig actor address `t018101`
2589            Address::new_id(18101), // https://calibration.filscan.io/en/address/t018101/
2590            tipset.key().into(),
2591        ))?),
2592        RpcTest::identity(StateReadState::request((
2593            ACCOUNT_ADDRESS,
2594            tipset.key().into(),
2595        ))?),
2596        RpcTest::identity(StateReadState::request((
2597            MINER_ADDRESS,
2598            tipset.key().into(),
2599        ))?),
2600        RpcTest::identity(StateReadState::request((
2601            Address::from_str(EVM_ADDRESS).unwrap(), // evm actor
2602            tipset.key().into(),
2603        ))?),
2604    ];
2605
2606    Ok(tests)
2607}
2608
2609fn eth_state_tests_with_tipset<DB: Blockstore>(
2610    store: &Arc<DB>,
2611    shared_tipset: &Tipset,
2612    eth_chain_id: EthChainIdType,
2613) -> anyhow::Result<Vec<RpcTest>> {
2614    let mut tests = vec![];
2615
2616    for block in shared_tipset.block_headers() {
2617        let state = StateTree::new_from_root(store.clone(), shared_tipset.parent_state())?;
2618
2619        let (bls_messages, secp_messages) = crate::chain::store::block_messages(store, block)?;
2620        for smsg in sample_signed_messages(bls_messages.iter(), secp_messages.iter()) {
2621            let tx = new_eth_tx_from_signed_message(&smsg, &state, eth_chain_id)?;
2622            tests.push(RpcTest::identity(
2623                EthGetMessageCidByTransactionHash::request((tx.hash,))?,
2624            ));
2625            tests.push(RpcTest::identity(EthGetTransactionByHash::request((
2626                tx.hash,
2627            ))?));
2628            tests.push(RpcTest::identity(EthGetTransactionByHashLimited::request(
2629                (tx.hash, shared_tipset.epoch()),
2630            )?));
2631            tests.push(RpcTest::identity(EthTraceTransaction::request((tx
2632                .hash
2633                .to_string(),))?));
2634            if smsg.message.from.protocol() == Protocol::Delegated
2635                && smsg.message.to.protocol() == Protocol::Delegated
2636            {
2637                tests.push(
2638                    RpcTest::identity(EthGetTransactionReceipt::request((tx.hash,))?)
2639                        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2640                );
2641                tests.push(
2642                    RpcTest::identity(EthGetTransactionReceiptLimited::request((tx.hash, 800))?)
2643                        .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError),
2644                );
2645            }
2646        }
2647    }
2648    tests.push(RpcTest::identity(
2649        EthGetMessageCidByTransactionHash::request((EthHash::from_str(
2650            "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355f",
2651        )?,))?,
2652    ));
2653
2654    // Test eth_call API errors
2655    tests.extend(eth_call_api_err_tests(shared_tipset.epoch()));
2656
2657    Ok(tests)
2658}
2659
2660fn gas_tests_with_tipset(shared_tipset: &Tipset) -> Vec<RpcTest> {
2661    // This is a testnet address with a few FILs. The private key has been
2662    // discarded. If calibnet is reset, a new address should be created.
2663    let addr = Address::from_str("t15ydyu3d65gznpp2qxwpkjsgz4waubeunn6upvla").unwrap();
2664    let message = Message {
2665        from: addr,
2666        to: addr,
2667        value: TokenAmount::from_whole(1),
2668        method_num: METHOD_SEND,
2669        ..Default::default()
2670    };
2671
2672    vec![
2673        // The tipset is only used for resolving the 'from' address and not when
2674        // computing the gas cost. This means that the `GasEstimateGasLimit` method
2675        // is inherently non-deterministic, but I'm fairly sure we're compensated for
2676        // everything. If not, this test will be flaky. Instead of disabling it, we
2677        // should relax the verification requirement.
2678        RpcTest::identity(
2679            GasEstimateGasLimit::request((message.clone(), shared_tipset.key().into())).unwrap(),
2680        ),
2681        // Gas estimation is inherently non-deterministic due to randomness in gas premium
2682        // calculation and network state changes. We validate that both implementations
2683        // return reasonable values within expected bounds rather than exact equality.
2684        RpcTest::validate(
2685            GasEstimateMessageGas::request((
2686                message,
2687                None, // No MessageSendSpec
2688                shared_tipset.key().into(),
2689            ))
2690            .unwrap(),
2691            |forest_api_msg, lotus_api_msg| {
2692                let forest_msg = forest_api_msg.message;
2693                let lotus_msg = lotus_api_msg.message;
2694                // Validate that the gas limit is identical (must be deterministic)
2695                if forest_msg.gas_limit != lotus_msg.gas_limit {
2696                    return false;
2697                }
2698
2699                // Validate gas fee cap and premium are within reasonable bounds (±5%)
2700                let forest_fee_cap = &forest_msg.gas_fee_cap;
2701                let lotus_fee_cap = &lotus_msg.gas_fee_cap;
2702                let forest_premium = &forest_msg.gas_premium;
2703                let lotus_premium = &lotus_msg.gas_premium;
2704
2705                // Gas fee cap and premium should not be negative
2706                if [forest_fee_cap, lotus_fee_cap, forest_premium, lotus_premium]
2707                    .iter()
2708                    .any(|amt| amt.is_negative())
2709                {
2710                    return false;
2711                }
2712
2713                forest_fee_cap.is_within_percent(lotus_fee_cap, 5)
2714                    && forest_premium.is_within_percent(lotus_premium, 5)
2715            },
2716        ),
2717    ]
2718}
2719
2720fn f3_tests() -> anyhow::Result<Vec<RpcTest>> {
2721    Ok(vec![
2722        // using basic because 2 nodes are not guaranteed to be at the same head
2723        RpcTest::basic(F3GetECPowerTable::request((None.into(),))?),
2724        RpcTest::basic(F3GetLatestCertificate::request(())?),
2725        RpcTest::basic(F3ListParticipants::request(())?),
2726        RpcTest::basic(F3GetProgress::request(())?),
2727        RpcTest::basic(F3GetOrRenewParticipationTicket::request((
2728            Address::new_id(1000),
2729            vec![],
2730            3,
2731        ))?),
2732        RpcTest::identity(F3IsRunning::request(())?),
2733        RpcTest::identity(F3GetCertificate::request((0,))?),
2734        RpcTest::identity(F3GetCertificate::request((50,))?),
2735        RpcTest::identity(F3GetManifest::request(())?),
2736    ])
2737}
2738
2739fn f3_tests_with_tipset(tipset: &Tipset) -> anyhow::Result<Vec<RpcTest>> {
2740    Ok(vec![
2741        RpcTest::identity(F3GetECPowerTable::request((tipset.key().into(),))?),
2742        RpcTest::identity(F3GetF3PowerTable::request((tipset.key().into(),))?),
2743    ])
2744}
2745
2746// Extract tests that use chain-specific data such as block CIDs or message
2747// CIDs. Right now, only the last `n_tipsets` tipsets are used.
2748fn snapshot_tests(
2749    store: Arc<ManyCar>,
2750    offline: bool,
2751    num_tipsets: usize,
2752    miner_address: Option<Address>,
2753    eth_chain_id: u64,
2754) -> anyhow::Result<Vec<RpcTest>> {
2755    let mut tests = vec![];
2756    // shared_tipset in the snapshot might not be finalized for the offline RPC server
2757    // use heaviest - SAFE_EPOCH_DELAY instead
2758    let shared_tipset = store
2759        .heaviest_tipset()?
2760        .chain(&store)
2761        .take(SAFE_EPOCH_DELAY as usize)
2762        .last()
2763        .expect("Infallible");
2764
2765    for tipset in shared_tipset.chain(&store).take(num_tipsets) {
2766        tests.extend(chain_tests_with_tipset(&store, offline, &tipset)?);
2767        tests.extend(miner_tests_with_tipset(&store, &tipset, miner_address)?);
2768        tests.extend(state_tests_with_tipset(&store, &tipset)?);
2769        tests.extend(eth_tests_with_tipset(&store, &tipset));
2770        tests.extend(event_tests_with_tipset(&store, &tipset));
2771        tests.extend(gas_tests_with_tipset(&tipset));
2772        tests.extend(mpool_tests_with_tipset(&tipset));
2773        tests.extend(eth_state_tests_with_tipset(&store, &tipset, eth_chain_id)?);
2774        tests.extend(f3_tests_with_tipset(&tipset)?);
2775    }
2776
2777    Ok(tests)
2778}
2779
2780fn sample_message_cids<'a>(
2781    bls_messages: impl Iterator<Item = &'a Message> + 'a,
2782    secp_messages: impl Iterator<Item = &'a SignedMessage> + 'a,
2783) -> impl Iterator<Item = Cid> + 'a {
2784    bls_messages
2785        .map(|m| m.cid())
2786        .unique()
2787        .take(COLLECTION_SAMPLE_SIZE)
2788        .chain(
2789            secp_messages
2790                .map(|m| m.cid())
2791                .unique()
2792                .take(COLLECTION_SAMPLE_SIZE),
2793        )
2794        .unique()
2795}
2796
2797fn sample_messages<'a>(
2798    bls_messages: impl Iterator<Item = &'a Message> + 'a,
2799    secp_messages: impl Iterator<Item = &'a SignedMessage> + 'a,
2800) -> impl Iterator<Item = &'a Message> + 'a {
2801    bls_messages
2802        .unique()
2803        .take(COLLECTION_SAMPLE_SIZE)
2804        .chain(
2805            secp_messages
2806                .map(SignedMessage::message)
2807                .unique()
2808                .take(COLLECTION_SAMPLE_SIZE),
2809        )
2810        .unique()
2811}
2812
2813fn sample_signed_messages<'a>(
2814    bls_messages: impl Iterator<Item = &'a Message> + 'a,
2815    secp_messages: impl Iterator<Item = &'a SignedMessage> + 'a,
2816) -> impl Iterator<Item = SignedMessage> + 'a {
2817    bls_messages
2818        .unique()
2819        .take(COLLECTION_SAMPLE_SIZE)
2820        .map(|msg| {
2821            let sig = Signature::new_bls(vec![]);
2822            SignedMessage::new_unchecked(msg.clone(), sig)
2823        })
2824        .chain(secp_messages.cloned().unique().take(COLLECTION_SAMPLE_SIZE))
2825        .unique()
2826}
2827
2828pub(super) async fn create_tests(
2829    CreateTestsArgs {
2830        offline,
2831        n_tipsets,
2832        miner_address,
2833        worker_address,
2834        eth_chain_id,
2835        snapshot_files,
2836    }: CreateTestsArgs,
2837) -> anyhow::Result<Vec<RpcTest>> {
2838    let mut tests = vec![];
2839    tests.extend(auth_tests()?);
2840    tests.extend(common_tests());
2841    tests.extend(chain_tests());
2842    tests.extend(mpool_tests());
2843    tests.extend(net_tests());
2844    tests.extend(node_tests());
2845    tests.extend(wallet_tests(worker_address));
2846    tests.extend(eth_tests());
2847    tests.extend(f3_tests()?);
2848    if !snapshot_files.is_empty() {
2849        let store = Arc::new(ManyCar::try_from(snapshot_files.clone())?);
2850        revalidate_chain(store.clone(), n_tipsets).await?;
2851        tests.extend(snapshot_tests(
2852            store,
2853            offline,
2854            n_tipsets,
2855            miner_address,
2856            eth_chain_id,
2857        )?);
2858    }
2859    tests.sort_by_key(|test| test.request.method_name.clone());
2860
2861    tests.extend(create_deferred_tests(snapshot_files)?);
2862    Ok(tests)
2863}
2864
2865// Some tests, especially those mutating the node's state, need to be run last.
2866fn create_deferred_tests(snapshot_files: Vec<PathBuf>) -> anyhow::Result<Vec<RpcTest>> {
2867    let mut tests = vec![];
2868
2869    if !snapshot_files.is_empty() {
2870        let store = Arc::new(ManyCar::try_from(snapshot_files)?);
2871        tests.push(RpcTest::identity(ChainSetHead::request((store
2872            .heaviest_tipset()?
2873            .key()
2874            .clone(),))?));
2875    }
2876
2877    Ok(tests)
2878}
2879
2880async fn revalidate_chain(db: Arc<ManyCar>, n_ts_to_validate: usize) -> anyhow::Result<()> {
2881    if n_ts_to_validate == 0 {
2882        return Ok(());
2883    }
2884    let chain_config = Arc::new(handle_chain_config(&NetworkChain::Calibnet)?);
2885
2886    let genesis_header = crate::genesis::read_genesis_header(
2887        None,
2888        chain_config.genesis_bytes(&db).await?.as_deref(),
2889        &db,
2890    )
2891    .await?;
2892    let chain_store = Arc::new(ChainStore::new(
2893        db.clone(),
2894        db.clone(),
2895        db.clone(),
2896        chain_config,
2897        genesis_header.clone(),
2898    )?);
2899    let state_manager = Arc::new(StateManager::new(chain_store.clone())?);
2900    let head_ts = db.heaviest_tipset()?;
2901
2902    // Set proof parameter data dir and make sure the proofs are available. Otherwise,
2903    // validation might fail due to missing proof parameters.
2904    proofs_api::maybe_set_proofs_parameter_cache_dir_env(&Config::default().client.data_dir);
2905    ensure_proof_params_downloaded().await?;
2906    state_manager.validate_tipsets(
2907        head_ts
2908            .chain(&db)
2909            .take(SAFE_EPOCH_DELAY as usize + n_ts_to_validate),
2910    )?;
2911
2912    Ok(())
2913}
2914
2915#[allow(clippy::too_many_arguments)]
2916pub(super) async fn run_tests(
2917    tests: impl IntoIterator<Item = RpcTest>,
2918    forest: impl Into<Arc<rpc::Client>>,
2919    lotus: impl Into<Arc<rpc::Client>>,
2920    max_concurrent_requests: usize,
2921    filter_file: Option<PathBuf>,
2922    filter: String,
2923    filter_version: Option<rpc::ApiPaths>,
2924    run_ignored: RunIgnored,
2925    fail_fast: bool,
2926    dump_dir: Option<PathBuf>,
2927    test_criteria_overrides: &[TestCriteriaOverride],
2928    report_dir: Option<PathBuf>,
2929    report_mode: ReportMode,
2930    n_retries: usize,
2931) -> anyhow::Result<()> {
2932    let forest = Into::<Arc<rpc::Client>>::into(forest);
2933    let lotus = Into::<Arc<rpc::Client>>::into(lotus);
2934    let semaphore = Arc::new(Semaphore::new(max_concurrent_requests));
2935    let mut tasks = JoinSet::new();
2936
2937    let filter_list = if let Some(filter_file) = &filter_file {
2938        FilterList::new_from_file(filter_file)?
2939    } else {
2940        FilterList::default().allow(filter.clone())
2941    };
2942
2943    // Always use ReportBuilder for consistency
2944    let mut report_builder = ReportBuilder::new(&filter_list, report_mode);
2945
2946    // deduplicate tests by their hash-able representations
2947    for test in tests.into_iter().unique_by(
2948        |RpcTest {
2949             request:
2950                 rpc::Request {
2951                     method_name,
2952                     params,
2953                     api_paths,
2954                     ..
2955                 },
2956             ignore,
2957             ..
2958         }| {
2959            (
2960                method_name.clone(),
2961                params.clone(),
2962                *api_paths,
2963                ignore.is_some(),
2964            )
2965        },
2966    ) {
2967        // By default, do not run ignored tests.
2968        if matches!(run_ignored, RunIgnored::Default) && test.ignore.is_some() {
2969            continue;
2970        }
2971        // If in `IgnoreOnly` mode, only run ignored tests.
2972        if matches!(run_ignored, RunIgnored::IgnoredOnly) && test.ignore.is_none() {
2973            continue;
2974        }
2975
2976        if !filter_list.authorize(&test.request.method_name) {
2977            continue;
2978        }
2979
2980        if let Some(filter_version) = filter_version
2981            && !test.request.api_paths.contains(filter_version)
2982        {
2983            continue;
2984        }
2985
2986        // Acquire a permit from the semaphore before spawning a test
2987        let semaphore = semaphore.clone();
2988        let forest = forest.clone();
2989        let lotus = lotus.clone();
2990        let test_criteria_overrides = test_criteria_overrides.to_vec();
2991        tasks.spawn(async move {
2992            let mut n_retries_left = n_retries;
2993            let mut backoff_secs = 2;
2994            loop {
2995                {
2996                    // Ignore the error since 'An acquire operation can only fail if the semaphore has been closed'
2997                    let _permit = semaphore.acquire().await;
2998                    let test_result = test.run(&forest, &lotus).await;
2999                    let success =
3000                        evaluate_test_success(&test_result, &test, &test_criteria_overrides);
3001                    if success || n_retries_left == 0 {
3002                        return (success, test, test_result);
3003                    }
3004                    // Release the semaphore before sleeping
3005                }
3006                // Sleep before each retry
3007                tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
3008                n_retries_left = n_retries_left.saturating_sub(1);
3009                backoff_secs = backoff_secs.saturating_mul(2);
3010            }
3011        });
3012    }
3013
3014    // If no tests to run after filtering, return early without saving/printing
3015    if tasks.is_empty() {
3016        return Ok(());
3017    }
3018
3019    while let Some(result) = tasks.join_next().await {
3020        match result {
3021            Ok((success, test, test_result)) => {
3022                let method_name = test.request.method_name.clone();
3023
3024                report_builder.track_test_result(
3025                    method_name.as_ref(),
3026                    success,
3027                    &test_result,
3028                    &test.request.params,
3029                );
3030
3031                // Dump test data if configured
3032                if let (Some(dump_dir), Some(test_dump)) = (&dump_dir, &test_result.test_dump) {
3033                    dump_test_data(dump_dir, success, test_dump)?;
3034                }
3035
3036                if !success && fail_fast {
3037                    break;
3038                }
3039            }
3040            Err(e) => tracing::warn!("{e}"),
3041        }
3042    }
3043
3044    let has_failures = report_builder.has_failures();
3045    report_builder.print_summary();
3046
3047    if let Some(path) = report_dir {
3048        report_builder.finalize_and_save(&path)?;
3049    }
3050
3051    anyhow::ensure!(!has_failures, "Some tests failed");
3052
3053    Ok(())
3054}
3055
3056/// Evaluate whether a test is successful based on the test result and criteria
3057fn evaluate_test_success(
3058    test_result: &TestResult,
3059    test: &RpcTest,
3060    test_criteria_overrides: &[TestCriteriaOverride],
3061) -> bool {
3062    match (&test_result.forest_status, &test_result.lotus_status) {
3063        (TestSummary::Valid, TestSummary::Valid) => true,
3064        (TestSummary::Valid, TestSummary::Timeout) => {
3065            test_criteria_overrides.contains(&TestCriteriaOverride::ValidAndTimeout)
3066        }
3067        (TestSummary::Timeout, TestSummary::Timeout) => {
3068            test_criteria_overrides.contains(&TestCriteriaOverride::TimeoutAndTimeout)
3069        }
3070        (TestSummary::Rejected(reason_forest), TestSummary::Rejected(reason_lotus)) => {
3071            match test.policy_on_rejected {
3072                PolicyOnRejected::Pass => true,
3073                PolicyOnRejected::PassWithIdenticalError => reason_forest == reason_lotus,
3074                PolicyOnRejected::PassWithIdenticalErrorCaseInsensitive => {
3075                    reason_forest.eq_ignore_ascii_case(reason_lotus)
3076                }
3077                PolicyOnRejected::PassWithQuasiIdenticalError => {
3078                    reason_lotus.contains(reason_forest) || reason_forest.contains(reason_lotus)
3079                }
3080                _ => false,
3081            }
3082        }
3083        _ => false,
3084    }
3085}
3086
3087fn normalized_error_message(s: &str) -> Cow<'_, str> {
3088    // remove `RPC error (-32603):` prefix added by `lotus-gateway`
3089    let lotus_gateway_error_prefix = lazy_regex::regex!(r#"^RPC\serror\s\(-?\d+\):\s*"#);
3090    lotus_gateway_error_prefix.replace(s, "")
3091}
3092
3093/// Dump test data to the specified directory
3094fn dump_test_data(dump_dir: &Path, success: bool, test_dump: &TestDump) -> anyhow::Result<()> {
3095    let dir = dump_dir.join(if success { "valid" } else { "invalid" });
3096    if !dir.is_dir() {
3097        std::fs::create_dir_all(&dir)?;
3098    }
3099    let file_name = format!(
3100        "{}_{}.json",
3101        test_dump
3102            .request
3103            .method_name
3104            .as_ref()
3105            .replace(".", "_")
3106            .to_lowercase(),
3107        Utc::now().timestamp_micros()
3108    );
3109    std::fs::write(
3110        dir.join(file_name),
3111        serde_json::to_string_pretty(test_dump)?,
3112    )?;
3113    Ok(())
3114}
3115
3116fn validate_message_lookup(req: rpc::Request<MessageLookup>) -> RpcTest {
3117    RpcTest::validate(req, |mut forest, mut lotus| {
3118        // TODO(hanabi1224): https://github.com/ChainSafe/forest/issues/3784
3119        forest.return_dec = Ipld::Null;
3120        lotus.return_dec = Ipld::Null;
3121        forest == lotus
3122    })
3123}
3124
3125fn validate_tagged_tipset_v2(req: rpc::Request<Option<Tipset>>, offline: bool) -> RpcTest {
3126    RpcTest::validate(req, move |forest, lotus| match (forest, lotus) {
3127        (None, None) => true,
3128        (Some(forest), Some(lotus)) => {
3129            if offline {
3130                true
3131            } else {
3132                (forest.epoch() - lotus.epoch()).abs() <= 2
3133            }
3134        }
3135        _ => false,
3136    })
3137}
3138
3139#[cfg(test)]
3140mod tests {
3141    use super::*;
3142
3143    #[test]
3144    fn test_normalized_error_message_1() {
3145        let s = "RPC error (-32603): exactly one tipset selection criteria must be specified";
3146        let r = normalized_error_message(s);
3147        assert_eq!(
3148            r.as_ref(),
3149            "exactly one tipset selection criteria must be specified"
3150        );
3151    }
3152
3153    #[test]
3154    fn test_normalized_error_message_2() {
3155        let s = "exactly one tipset selection criteria must be specified";
3156        let r = normalized_error_message(s);
3157        assert_eq!(
3158            r.as_ref(),
3159            "exactly one tipset selection criteria must be specified"
3160        );
3161    }
3162}