Skip to main content

forest/rpc/methods/eth/
tipset_resolver.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use super::*;
5use crate::{
6    db::EthMappingsStore,
7    rpc::chain::{ChainGetTipSetFinalityStatus, SAFE_HEIGHT_DISTANCE},
8};
9use anyhow::Context as _;
10
11pub struct TipsetResolver<'a, DB>
12where
13    DB: Blockstore + Send + Sync + 'static,
14{
15    ctx: &'a Ctx<DB>,
16    api_version: ApiPaths,
17}
18
19impl<'a, DB> TipsetResolver<'a, DB>
20where
21    DB: Blockstore + EthMappingsStore + Send + Sync + 'static,
22{
23    /// Creates a TipsetResolver that holds a reference to the given chain context and the API version to use for tipset resolution.
24    pub fn new(ctx: &'a Ctx<DB>, api_version: ApiPaths) -> Self {
25        Self { ctx, api_version }
26    }
27
28    /// Resolve a tipset from a block identifier that may be a predefined tag, block height, or block hash.
29    ///
30    /// Attempts to resolve the provided `block_param` into a concrete `Tipset`. The parameter may be:
31    /// - a predefined tag (e.g., `Predefined::Latest`, `Predefined::Safe`, `Predefined::Finalized`),
32    /// - a block height (number or object form), or
33    /// - a block hash (raw hash or object form that can require canonicalization).
34    ///
35    /// # Parameters
36    ///
37    /// - `block_param` — block identifier to resolve; accepts any type convertible to `BlockNumberOrHash`.
38    /// - `resolve` — rule for how to treat null/unknown tipsets when resolving by height/hash.
39    ///
40    /// # Returns
41    ///
42    /// The resolved `Tipset` on success.
43    pub async fn tipset_by_block_number_or_hash(
44        &self,
45        block_param: impl Into<BlockNumberOrHash>,
46        resolve: ResolveNullTipset,
47    ) -> anyhow::Result<Tipset> {
48        match block_param.into() {
49            BlockNumberOrHash::PredefinedBlock(tag) => self.resolve_predefined_tipset(tag).await,
50            BlockNumberOrHash::BlockNumber(block_number)
51            | BlockNumberOrHash::BlockNumberObject(BlockNumber { block_number }) => {
52                resolve_block_number_tipset(self.ctx.chain_store(), block_number, resolve)
53            }
54            BlockNumberOrHash::BlockHash(block_hash) => {
55                resolve_block_hash_tipset(self.ctx.chain_store(), &block_hash, false, resolve)
56            }
57            BlockNumberOrHash::BlockHashObject(BlockHash {
58                block_hash,
59                require_canonical,
60            }) => resolve_block_hash_tipset(
61                self.ctx.chain_store(),
62                &block_hash,
63                require_canonical,
64                resolve,
65            ),
66        }
67    }
68
69    /// Resolve a predefined tipset according to the resolver's API version.
70    ///
71    /// # Returns
72    ///
73    /// The resolved `Tipset`, or an error if resolution fails.
74    async fn resolve_predefined_tipset(&self, tag: Predefined) -> anyhow::Result<Tipset> {
75        match self.api_version {
76            ApiPaths::V2 => self.resolve_predefined_tipset_v2(tag).await,
77            ApiPaths::V1 | ApiPaths::V0 => self.resolve_predefined_tipset_v1(tag).await,
78        }
79    }
80
81    /// Resolves a predefined tipset using the V1 resolution policy, or delegates to the V2 resolver when the
82    /// V1 finality-resolution override is not enabled.
83    ///
84    /// If the environment variable `FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION` is set to a truthy value,
85    /// this function first attempts common predefined tag resolution (e.g., Pending, Latest). If that yields
86    /// no result, the function uses expected-consensus finality to resolve the "safe" or "finalized" tipset
87    /// for the corresponding `Predefined` tag. When the environment variable is not set or is falsy,
88    /// resolution is delegated to the V2 resolver.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if the requested predefined tag is unknown or if tipset resolution fails.
93    async fn resolve_predefined_tipset_v1(&self, tag: Predefined) -> anyhow::Result<Tipset> {
94        const ETH_V1_DISABLE_F3_FINALITY_RESOLUTION_ENV_KEY: &str =
95            "FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION";
96
97        crate::def_is_env_truthy!(
98            f3_finality_disabled,
99            ETH_V1_DISABLE_F3_FINALITY_RESOLUTION_ENV_KEY
100        );
101
102        if f3_finality_disabled() {
103            if let Some(ts) = self.resolve_common_predefined_tipset(tag)? {
104                Ok(ts)
105            } else {
106                match tag {
107                    Predefined::Safe => self.get_ec_safe_tipset(),
108                    Predefined::Finalized => self.get_ec_finalized_tipset(),
109                    tag => anyhow::bail!("unknown block tag: {tag}"),
110                }
111            }
112        } else {
113            self.resolve_predefined_tipset_v2(tag).await
114        }
115    }
116
117    /// Resolves a predefined tipset according to the v2 API behavior.
118    ///
119    /// Uses a common predefined-tipset lookup first; if that yields no result, resolves
120    /// `Safe` and `Finalized` tags via the v2 chain getters. Returns an error for unknown tags
121    /// or on underlying resolution failures.
122    ///
123    /// # Returns
124    ///
125    /// The resolved `Tipset` on success.
126    async fn resolve_predefined_tipset_v2(&self, tag: Predefined) -> anyhow::Result<Tipset> {
127        if let Some(ts) = self.resolve_common_predefined_tipset(tag)? {
128            Ok(ts)
129        } else {
130            match tag {
131                Predefined::Safe => ChainGetTipSetV2::get_latest_safe_tipset(self.ctx).await,
132                Predefined::Finalized => {
133                    ChainGetTipSetV2::get_latest_finalized_tipset(self.ctx).await
134                }
135                tag => anyhow::bail!("unknown block tag: {tag}"),
136            }
137        }
138    }
139
140    /// Attempt to resolve a predefined block tag to a commonly-handled tipset.
141    ///
142    /// Returns `Some(Tipset)` for `Predefined::Pending` (current head) and
143    /// `Predefined::Latest` (the tipset at the head's parents). Returns `Ok(None)`
144    /// when the tag is not handled by this common-resolution path (caller should
145    /// try other resolution strategies). Resolving `Predefined::Earliest` fails
146    /// with an error.
147    fn resolve_common_predefined_tipset(&self, tag: Predefined) -> anyhow::Result<Option<Tipset>> {
148        let head = self.ctx.chain_store().heaviest_tipset();
149        match tag {
150            Predefined::Earliest => bail!("block param \"earliest\" is not supported"),
151            Predefined::Pending => Ok(Some(head)),
152            Predefined::Latest => Ok(Some(
153                self.ctx
154                    .chain_index()
155                    .load_required_tipset(head.parents())?,
156            )),
157            Predefined::Safe | Predefined::Finalized => Ok(None),
158        }
159    }
160
161    /// Returns the tipset considered "safe" relative to the current heaviest tipset.
162    ///
163    /// The safe tipset is the tipset at height `max(head.epoch() - SAFE_HEIGHT_DISTANCE, 0)`.
164    pub fn get_ec_safe_tipset(&self) -> anyhow::Result<Tipset> {
165        let head = self.ctx.chain_store().heaviest_tipset();
166        let safe_height = (head.epoch() - SAFE_HEIGHT_DISTANCE).max(0);
167        Ok(self.ctx.chain_index().load_required_tipset_by_height(
168            safe_height,
169            head,
170            ResolveNullTipset::TakeOlder,
171        )?)
172    }
173
174    /// Returns the tipset considered finalized by the expected-consensus finality calculator(`FRC-0089`).
175    pub fn get_ec_finalized_tipset(&self) -> anyhow::Result<Tipset> {
176        let head = self.ctx.chain_store().heaviest_tipset();
177        let (_, ec_finalized_tipset) =
178            ChainGetTipSetFinalityStatus::get_ec_finality_threshold_depth_and_tipset_with_cache(
179                self.ctx,
180                head.clone(),
181            )?;
182        ec_finalized_tipset.context("failed to resolve EC finalized tipset")
183    }
184}