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