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