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