Skip to main content

forest/tool/subcommands/api_cmd/
api_compare_tests.rs

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