forest/tool/subcommands/api_cmd/
api_compare_tests.rs

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