Skip to main content

forest/tool/subcommands/api_cmd/
api_compare_tests.rs

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