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