fix(RPC): backport F3 finality resolution to eth V1 methods#6644
fix(RPC): backport F3 finality resolution to eth V1 methods#6644hanabi1224 wants to merge 4 commits intomainfrom
Conversation
|
Important Review skippedReview was skipped due to path filters ⛔ Files ignored due to path filters (3)
CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis pull request backports F3 finality resolution to ETH v1 RPC methods by refactoring the type system and adding async tipset resolution logic. The changes replace legacy block-parameter types with a unified BlockNumberOrHash interface, expose new chain resolution helpers, and introduce an environment variable for controlling F3 finality behavior in v1 APIs. Changes
Sequence DiagramsequenceDiagram
participant Client as ETH RPC Client
participant Handler as RPC Method Handler
participant Resolver as BlockNumberOrHash
participant PredefinedResolve as Predefined Resolver
participant ChainStore as Chain Store
Client->>Handler: eth_getBalance(address, blockParam, ...)
Handler->>Resolver: BlockNumberOrHash::from(blockParam)
alt blockParam is Predefined Tag
Resolver->>PredefinedResolve: resolve_predefined_tipset_v1(ctx)
PredefinedResolve->>ChainStore: Get latest/finalized/safe tipset
alt Tag is finalized
PredefinedResolve->>ChainStore: Resolve via F3 finality<br/>(if enabled)
end
ChainStore-->>PredefinedResolve: Return Tipset
PredefinedResolve-->>Resolver: Tipset
else blockParam is Block Number/Hash
Resolver->>ChainStore: Lookup block by number/hash
ChainStore-->>Resolver: Tipset
end
Resolver-->>Handler: Tipset
Handler->>Handler: Process request with resolved tipset
Handler-->>Client: RPC Response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
4082f71 to
f94db17
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/rpc/methods/eth.rs (1)
2712-2718:⚠️ Potential issue | 🟠 MajorBug: V1 handler
EthGetTransactionByBlockNumberAndIndexusestipset_by_block_number_or_hash_v2instead of_v1.This method has
API_PATHS: BitFlags<ApiPaths> = ApiPaths::all()(non-V2), yet it callstipset_by_block_number_or_hash_v2. All other V1 handlers in this PR consistently use_v1. The V2 counterpart at line 2740 also correctly uses_v2. This means V1 callers of this endpoint won't respect theFOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTIONenv flag.Proposed fix
let ts = BlockNumberOrHash::from(block_param) - .tipset_by_block_number_or_hash_v2(&ctx, ResolveNullTipset::TakeOlder) + .tipset_by_block_number_or_hash_v1(&ctx, ResolveNullTipset::TakeOlder) .await?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/rpc/methods/eth.rs` around lines 2712 - 2718, The V1 handler EthGetTransactionByBlockNumberAndIndex currently calls tipset_by_block_number_or_hash_v2 which bypasses V1 feature gating; change the call in the async fn handle (the BlockNumberOrHash::from(block_param) chain) to use tipset_by_block_number_or_hash_v1 with the same ResolveNullTipset::TakeOlder and ctx so V1 callers respect FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION and match other V1 handlers.
🧹 Nitpick comments (10)
docs/docs/users/reference/env_variables.md (1)
61-61: Tighten phrasing in the description.Consider shortening to “Whether to disable …” for concision.
✍️ Suggested tweak
-| `FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION` | 1 or true | empty | 1 | Whether or not to disable F3 finality resolution in Eth v1 RPC methods | +| `FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION` | 1 or true | empty | 1 | Whether to disable F3 finality resolution in Eth v1 RPC methods |🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/docs/users/reference/env_variables.md` at line 61, Update the description for the environment variable FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION to a more concise phrasing: replace "Whether or not to disable F3 finality resolution in Eth v1 RPC methods" with "Whether to disable F3 finality resolution in Eth v1 RPC methods" in the docs table row to tighten the wording.src/rpc/methods/eth/types.rs (1)
270-275: Add a doc comment for the public enum.
BlockNumberOrPredefinedis public; please add a short doc comment describing its purpose.📝 Suggested doc comment
+/// Block selector used by eth RPCs; accepts either a predefined tag or a block number. #[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema, derive_more::From)] #[serde(untagged)] pub enum BlockNumberOrPredefined {As per coding guidelines “Document all public functions and structs with doc comments”.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/rpc/methods/eth/types.rs` around lines 270 - 275, The public enum BlockNumberOrPredefined lacks a doc comment; add a concise Rust doc comment (///) above the enum BlockNumberOrPredefined explaining that it represents either an explicit block number (BlockNumber variant holding EthInt64) or a predefined block specifier (PredefinedBlock variant holding Predefined), and mention its serialization behavior if relevant (serde untagged) so callers understand its usage in RPC types.src/tool/subcommands/api_cmd/test_snapshots.txt (1)
143-148: Verify new snapshot artifacts are available remotely.Please confirm these new snapshot files have been uploaded to the snapshot bucket; otherwise the fetch step will fail for these test cases.
Based on learnings “In the Forest project, .rpcsnap.json.zst snapshot files listed in src/tool/subcommands/api_cmd/test_snapshots.txt are not stored in the repository itself but are downloaded from a DigitalOcean (DO) bucket when needed. The manifest file serves as an index/catalog of available snapshots.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/tool/subcommands/api_cmd/test_snapshots.txt` around lines 143 - 148, The test snapshot manifest src/tool/subcommands/api_cmd/test_snapshots.txt lists several .rpcsnap.json.zst artifacts (e.g., filecoin_filecoinaddresstoethaddress_1771943760273649.rpcsnap.json.zst, _1771943760275947..., _1771943760315677..., _1771946498262342..., _1771946498262483..., _1771946498262885...) but these are not stored in-repo and must exist in the remote snapshot bucket; verify each of these exact files has been uploaded to the DigitalOcean snapshot bucket, ensure the bucket manifest/catalog used by the fetch step includes these entries, confirm object ACLs/public access or credentials permit the fetch, and if any are missing upload them (or update the manifest file referenced by the fetch logic) so the test fetch step succeeds.src/tool/subcommands/api_cmd/api_compare_tests.rs (1)
1475-1483: Minor inconsistency: usefrom_predefinedinstead of the variant directly.Every other call site in this file uses
BlockNumberOrHash::from_predefined(tag). The direct variant form works, but it exposes the internal enum structure unnecessarily.♻️ Suggested fix
- (msg.clone(), BlockNumberOrHash::PredefinedBlock(tag)), + (msg.clone(), BlockNumberOrHash::from_predefined(tag)),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/tool/subcommands/api_cmd/api_compare_tests.rs` around lines 1475 - 1483, Replace the direct enum construction BlockNumberOrHash::PredefinedBlock(tag) with the helper constructor BlockNumberOrHash::from_predefined(tag) in the loop that builds tests (inside the for tag in [Predefined::Latest, Predefined::Safe, Predefined::Finalized] block). Update the EthCallV2::request_with_alias call used when creating RpcTest::identity so it passes BlockNumberOrHash::from_predefined(tag) instead of the PredefinedBlock variant to match other call sites and avoid exposing the enum internals.src/rpc/methods/chain.rs (2)
1049-1049: Consider simplifying with.map(Some)instead ofSome(…).transpose().Both are semantically identical —
Result<Tipset>→Result<Option<Tipset>>— but.map(Some)is more idiomatic and easier to read at a glance:- TipsetTag::Finalized => Some(Self::get_latest_finalized_tipset(ctx).await).transpose(), + TipsetTag::Finalized => Self::get_latest_finalized_tipset(ctx).await.map(Some),(The
Safearm on line 1050 uses the same existing pattern; both could be changed for consistency, or left as-is — it's a pure readability preference.)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/rpc/methods/chain.rs` at line 1049, The match arm using TipsetTag::Finalized currently wraps an awaited Result in Some(...).transpose(); replace that pattern by mapping the Result to an Option via .map(Some) on the async call (i.e., change the expression involving get_latest_finalized_tipset(ctx).await to get_latest_finalized_tipset(ctx).await.map(Some)); do the same for the Safe arm that uses get_latest_safe_tipset(ctx).await so both arms use the more idiomatic .map(Some) conversion from Result<Tipset> to Result<Option<Tipset>>.
1071-1077: New public function is missing a doc comment (required by coding guidelines).A brief description clarifying this is the EC-only (non-F3) safe-tipset path would also aid consumers in
eth.rs.📝 Suggested doc comment
+ /// Returns the EC-based safe tipset, i.e. the tipset at [`SAFE_HEIGHT_DISTANCE`] epochs + /// behind the current head, without consulting F3. Use [`get_latest_safe_tipset`] when + /// F3 finality should be preferred. pub fn get_ec_safe_tipset(ctx: &Ctx<impl Blockstore>) -> anyhow::Result<Tipset> {As per coding guidelines: "Document all public functions and structs with doc comments".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/rpc/methods/chain.rs` around lines 1071 - 1077, Add a Rust doc comment (///) to the public function get_ec_safe_tipset describing its purpose and behaviour: state that it returns the EC-only (non-F3) safe tipset by computing safe_height = head.epoch() - SAFE_HEIGHT_DISTANCE and resolving the tipset at that height with ResolveNullTipset::TakeOlder; note that this is the EC-only safe-tipset path (not F3) and mention that consumers like eth.rs rely on this contract. Ensure the comment sits immediately above the get_ec_safe_tipset declaration and follows crate doc style and tone.src/rpc/methods/eth.rs (4)
362-377:resolve_common_predefined_tipsetreturnsOk(None)forSafeandFinalized, delegating to callers.The wildcard arm
_ => Ok(None)matchesSafeandFinalized, which is correct since these are handled by the caller (resolve_predefined_tipset_v1/_v2). However, if new variants are ever added toPredefined, they'd silently fall through here too.Consider using an explicit match instead of the wildcard to get a compile-time reminder if new variants are added:
Suggested change
- _ => Ok(None), + Predefined::Safe | Predefined::Finalized => Ok(None),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/rpc/methods/eth.rs` around lines 362 - 377, The match in resolve_common_predefined_tipset currently uses a wildcard arm that returns Ok(None), which will silently accept any future Predefined variants; change the match to explicitly list all known variants (Earliest, Pending, Latest, Safe, Finalized) and return Ok(None) for Safe and Finalized so the compiler will error if a new variant is added, updating the match in the resolve_common_predefined_tipset function to remove the `_ => Ok(None)` arm and replace it with explicit arms for Safe and Finalized (and keep the existing behavior for Earliest, Pending, Latest).
439-452:BlockNumberOrHash::from_strshadowsFromStr::from_str.The method
from_stris defined as an inherent method (not implementing theFromStrtrait), which silently shadows any potentialFromStrtrait implementation. This is fine functionally, but ifFromStris ever derived or implemented, the inherent method will always take precedence, which could cause confusion.Also, the addition of
"safe"and"finalized"parsing at lines 444-445 is correct and aligns with the PR objective.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/rpc/methods/eth.rs` around lines 439 - 452, The current inherent method BlockNumberOrHash::from_str shadows the standard FromStr trait; replace it by implementing std::str::FromStr for BlockNumberOrHash (impl FromStr for BlockNumberOrHash { type Err = Error; fn from_str(&str) -> Result<Self, Self::Err> { ... } }) reusing the existing logic (keep the "earliest", "pending", "latest"/"", "safe", "finalized" branches and the hex branch that calls hex_str_to_epoch and then from_block_number/from_predefined); remove or rename the original inherent from_str to avoid shadowing (e.g., rename to parse_str) so there is no ambiguity between the trait impl and an inherent method.
3857-3865:EthTraceCallserves V1 and V2 but always uses_v2resolution.This single handler serves both
V1 | V2paths. Usingtipset_by_block_number_or_hash_v2means V1 callers passing"safe"or"finalized"asblock_paramwill get F3-based resolution regardless ofFOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION. The default case ("latest") is unaffected since both paths resolve it identically.If the intent is to respect the env flag for V1, consider either splitting into V1/V2 handlers (like most other methods in this PR) or passing the API version dynamically.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/rpc/methods/eth.rs` around lines 3857 - 3865, The handler for EthTraceCall always uses tipset_by_block_number_or_hash_v2, causing V1 callers using BlockNumberOrHash values like "safe" or "finalized" to get F3 resolution regardless of FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION; update the handler to respect API version by branching on the request version (or split into separate V1/V2 handlers): if the caller is V1 use tipset_by_block_number_or_hash (or the legacy resolver) when block_param is "safe"|"finalized", otherwise use tipset_by_block_number_or_hash_v2 for V2; adjust the code around EthTraceCall::handle, the BlockNumberOrHash parsing, and the call to tipset_by_block_number_or_hash_v2 (and ResolveNullTipset::TakeOlder) so V1 flows consult the env flag and legacy resolver while V2 continues to use the _v2 resolver.
3186-3191:FilecoinAddressToEthAddressdefaults toPredefined::Finalizedand always uses_v2.This endpoint is served on
all_with_v2()but always resolves viatipset_by_block_number_or_hash_v2. V1 callers (who omitblock_param) will always get F3-finalized resolution, bypassing theFOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTIONopt-out. Same concern asEthTraceCall– this may be intentional since there's no separate V2 handler, but it's inconsistent with the V1 backport pattern used elsewhere in this PR.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/rpc/methods/eth.rs` around lines 3186 - 3191, The FilecoinAddressToEthAddress handler currently defaults block_param to Predefined::Finalized and always calls tipset_by_block_number_or_hash_v2, causing V1 callers (served via all_with_v2()) to be forced to F3-finalized resolution and bypass the FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION opt-out; update the logic in the FilecoinAddressToEthAddress handler to detect V1 requests (as other V1 backports do) and: if V1 and the FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION flag is set, avoid defaulting to Predefined::Finalized and call the non-_v2 resolver (tipset_by_block_number_or_hash or the v1 variant) instead; otherwise keep the existing v2 path (leave the block_param default and call tipset_by_block_number_or_hash_v2) so v2 behavior remains unchanged. Reference symbols: FilecoinAddressToEthAddress, block_param, Predefined::Finalized, tipset_by_block_number_or_hash_v2, all_with_v2(), and FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/rpc/methods/eth.rs`:
- Around line 2712-2718: The V1 handler EthGetTransactionByBlockNumberAndIndex
currently calls tipset_by_block_number_or_hash_v2 which bypasses V1 feature
gating; change the call in the async fn handle (the
BlockNumberOrHash::from(block_param) chain) to use
tipset_by_block_number_or_hash_v1 with the same ResolveNullTipset::TakeOlder and
ctx so V1 callers respect FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION and match
other V1 handlers.
---
Nitpick comments:
In `@docs/docs/users/reference/env_variables.md`:
- Line 61: Update the description for the environment variable
FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION to a more concise phrasing: replace
"Whether or not to disable F3 finality resolution in Eth v1 RPC methods" with
"Whether to disable F3 finality resolution in Eth v1 RPC methods" in the docs
table row to tighten the wording.
In `@src/rpc/methods/chain.rs`:
- Line 1049: The match arm using TipsetTag::Finalized currently wraps an awaited
Result in Some(...).transpose(); replace that pattern by mapping the Result to
an Option via .map(Some) on the async call (i.e., change the expression
involving get_latest_finalized_tipset(ctx).await to
get_latest_finalized_tipset(ctx).await.map(Some)); do the same for the Safe arm
that uses get_latest_safe_tipset(ctx).await so both arms use the more idiomatic
.map(Some) conversion from Result<Tipset> to Result<Option<Tipset>>.
- Around line 1071-1077: Add a Rust doc comment (///) to the public function
get_ec_safe_tipset describing its purpose and behaviour: state that it returns
the EC-only (non-F3) safe tipset by computing safe_height = head.epoch() -
SAFE_HEIGHT_DISTANCE and resolving the tipset at that height with
ResolveNullTipset::TakeOlder; note that this is the EC-only safe-tipset path
(not F3) and mention that consumers like eth.rs rely on this contract. Ensure
the comment sits immediately above the get_ec_safe_tipset declaration and
follows crate doc style and tone.
In `@src/rpc/methods/eth.rs`:
- Around line 362-377: The match in resolve_common_predefined_tipset currently
uses a wildcard arm that returns Ok(None), which will silently accept any future
Predefined variants; change the match to explicitly list all known variants
(Earliest, Pending, Latest, Safe, Finalized) and return Ok(None) for Safe and
Finalized so the compiler will error if a new variant is added, updating the
match in the resolve_common_predefined_tipset function to remove the `_ =>
Ok(None)` arm and replace it with explicit arms for Safe and Finalized (and keep
the existing behavior for Earliest, Pending, Latest).
- Around line 439-452: The current inherent method BlockNumberOrHash::from_str
shadows the standard FromStr trait; replace it by implementing std::str::FromStr
for BlockNumberOrHash (impl FromStr for BlockNumberOrHash { type Err = Error; fn
from_str(&str) -> Result<Self, Self::Err> { ... } }) reusing the existing logic
(keep the "earliest", "pending", "latest"/"", "safe", "finalized" branches and
the hex branch that calls hex_str_to_epoch and then
from_block_number/from_predefined); remove or rename the original inherent
from_str to avoid shadowing (e.g., rename to parse_str) so there is no ambiguity
between the trait impl and an inherent method.
- Around line 3857-3865: The handler for EthTraceCall always uses
tipset_by_block_number_or_hash_v2, causing V1 callers using BlockNumberOrHash
values like "safe" or "finalized" to get F3 resolution regardless of
FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION; update the handler to respect API
version by branching on the request version (or split into separate V1/V2
handlers): if the caller is V1 use tipset_by_block_number_or_hash (or the legacy
resolver) when block_param is "safe"|"finalized", otherwise use
tipset_by_block_number_or_hash_v2 for V2; adjust the code around
EthTraceCall::handle, the BlockNumberOrHash parsing, and the call to
tipset_by_block_number_or_hash_v2 (and ResolveNullTipset::TakeOlder) so V1 flows
consult the env flag and legacy resolver while V2 continues to use the _v2
resolver.
- Around line 3186-3191: The FilecoinAddressToEthAddress handler currently
defaults block_param to Predefined::Finalized and always calls
tipset_by_block_number_or_hash_v2, causing V1 callers (served via all_with_v2())
to be forced to F3-finalized resolution and bypass the
FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION opt-out; update the logic in the
FilecoinAddressToEthAddress handler to detect V1 requests (as other V1 backports
do) and: if V1 and the FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION flag is set,
avoid defaulting to Predefined::Finalized and call the non-_v2 resolver
(tipset_by_block_number_or_hash or the v1 variant) instead; otherwise keep the
existing v2 path (leave the block_param default and call
tipset_by_block_number_or_hash_v2) so v2 behavior remains unchanged. Reference
symbols: FilecoinAddressToEthAddress, block_param, Predefined::Finalized,
tipset_by_block_number_or_hash_v2, all_with_v2(), and
FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTION.
In `@src/rpc/methods/eth/types.rs`:
- Around line 270-275: The public enum BlockNumberOrPredefined lacks a doc
comment; add a concise Rust doc comment (///) above the enum
BlockNumberOrPredefined explaining that it represents either an explicit block
number (BlockNumber variant holding EthInt64) or a predefined block specifier
(PredefinedBlock variant holding Predefined), and mention its serialization
behavior if relevant (serde untagged) so callers understand its usage in RPC
types.
In `@src/tool/subcommands/api_cmd/api_compare_tests.rs`:
- Around line 1475-1483: Replace the direct enum construction
BlockNumberOrHash::PredefinedBlock(tag) with the helper constructor
BlockNumberOrHash::from_predefined(tag) in the loop that builds tests (inside
the for tag in [Predefined::Latest, Predefined::Safe, Predefined::Finalized]
block). Update the EthCallV2::request_with_alias call used when creating
RpcTest::identity so it passes BlockNumberOrHash::from_predefined(tag) instead
of the PredefinedBlock variant to match other call sites and avoid exposing the
enum internals.
In `@src/tool/subcommands/api_cmd/test_snapshots.txt`:
- Around line 143-148: The test snapshot manifest
src/tool/subcommands/api_cmd/test_snapshots.txt lists several .rpcsnap.json.zst
artifacts (e.g.,
filecoin_filecoinaddresstoethaddress_1771943760273649.rpcsnap.json.zst,
_1771943760275947..., _1771943760315677..., _1771946498262342...,
_1771946498262483..., _1771946498262885...) but these are not stored in-repo and
must exist in the remote snapshot bucket; verify each of these exact files has
been uploaded to the DigitalOcean snapshot bucket, ensure the bucket
manifest/catalog used by the fetch step includes these entries, confirm object
ACLs/public access or credentials permit the fetch, and if any are missing
upload them (or update the manifest file referenced by the fetch logic) so the
test fetch step succeeds.
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
CHANGELOG.mddocs/dictionary.txtdocs/docs/users/reference/env_variables.mdsrc/eth/mod.rssrc/rpc/methods/chain.rssrc/rpc/methods/eth.rssrc/rpc/methods/eth/types.rssrc/tool/subcommands/api_cmd/api_compare_tests.rssrc/tool/subcommands/api_cmd/test_snapshot.rssrc/tool/subcommands/api_cmd/test_snapshots.txt
💤 Files with no reviewable changes (1)
- src/eth/mod.rs
Codecov Report❌ Patch coverage is
Additional details and impacted files
... and 9 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
Summary of changes
Changes introduced in this pull request:
Reference issue to close (if applicable)
Closes #6631
Other information and links
Change checklist
Outside contributions
Summary by CodeRabbit
New Features
Documentation
FOREST_ETH_V1_DISABLE_F3_FINALITY_RESOLUTIONenvironment variable documentation to control F3 finality behavior in ETH v1 RPC endpoints.