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