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