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