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