feat: sqlite indexer - #6720
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a SQLite-backed chain indexer with schema initialization, validation, backfilling, indexed event queries, daemon integration, RPC support, and the ChangesSQLite chain indexer
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant ForestCLI
participant ChainValidateIndex
participant SqliteIndexer
participant ChainStore
ForestCLI->>ChainValidateIndex: validate epoch with backfill flag
ChainValidateIndex->>SqliteIndexer: validate_index(epoch, backfill)
SqliteIndexer->>ChainStore: load canonical tipset data
ChainStore-->>SqliteIndexer: return chain data
SqliteIndexer-->>ChainValidateIndex: return ChainIndexValidation
ChainValidateIndex-->>ForestCLI: return validation result
sequenceDiagram
participant EthClient
participant EthGetLogs
participant ParsedFilter
participant SqliteIndexer
participant SQLite
EthClient->>EthGetLogs: submit event filter
EthGetLogs->>ParsedFilter: parse tipset range
ParsedFilter-->>EthGetLogs: select SQL index for large range
EthGetLogs->>SqliteIndexer: query indexed events
SqliteIndexer->>SQLite: execute filtered event query
SQLite-->>EthGetLogs: return collected events
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
| let HeadChange::Apply(ts) = receiver.recv().await?; | ||
| // Continuously listen for head changes | ||
| loop { | ||
| let HeadChange::Apply(ts) = receiver.recv().await?; |
There was a problem hiding this comment.
@coderabbitai HeadChange::Revert is missing in Forest. Create an issue to track implementing it. Lotus interface: https://github.com/filecoin-project/lotus/blob/04dd7f2e3611a38f1b9ac374196c691fe3447bae/chain/events/events.go#L26
type TipSetObserver interface {
Apply(ctx context.Context, from, to *types.TipSet) error
Revert(ctx context.Context, from, to *types.TipSet) error
}There was a problem hiding this comment.
🧩 Analysis chain
@hanabi1224, I've created a GitHub issue to track the implementation of HeadChange::Revert in Forest. The issue includes a reference to the Lotus TipSetObserver interface, the current Forest behavior (silently ignoring revert events in the indexer loop), and links back to this PR and comment.
🏁 Script executed:
#!/bin/bash
gh issue create \
--repo ChainSafe/forest \
--title "Implement HeadChange::Revert in Forest" \
--body "## Summary
Forest currently only handles \`HeadChange::Apply\` events in the chain indexer and related services. The \`HeadChange::Revert\` variant is missing, which means chain reorganizations (reorgs) are not properly handled.
## Lotus Reference
The Lotus implementation defines a \`TipSetObserver\` interface with both \`Apply\` and \`Revert\` methods:
\`\`\`go
type TipSetObserver interface {
Apply(ctx context.Context, from, to *types.TipSet) error
Revert(ctx context.Context, from, to *types.TipSet) error
}
\`\`\`
Reference: https://github.com/filecoin-project/lotus/blob/04dd7f2e3611a38f1b9ac374196c691fe3447bae/chain/events/events.go#L26
## Current Forest Behavior
In \`src/daemon/mod.rs\`, the indexer loop only matches \`HeadChange::Apply\`:
\`\`\`rust
loop {
let HeadChange::Apply(ts) = receiver.recv().await?;
// ...
}
\`\`\`
The \`HeadChange::Revert\` case is silently ignored, meaning index state is not rolled back on chain reorgs.
## Expected Behavior
Forest should implement a \`HeadChange::Revert\` handler analogous to the Lotus \`TipSetObserver::Revert\` method, to correctly undo index state when a tipset is reverted during a chain reorganization.
## References
- Raised in PR: https://github.com/ChainSafe/forest/pull/6720
- Comment: https://github.com/ChainSafe/forest/pull/6720#discussion_r2923416164
- Requested by: `@hanabi1224`
" \
--assignee hanabi1224
Repository: ChainSafe/forest
Length of output: 47
@hanabi1224, the issue has been created and assigned to you: #6729
It tracks implementing HeadChange::Revert in Forest to properly handle chain reorganizations, with a reference to the Lotus TipSetObserver interface and a link back to this PR/comment.
🧠 Learnings used
Learnt from: LesnyRumcajs
Repo: ChainSafe/forest PR: 6702
File: src/chain/store/base_fee.rs:58-72
Timestamp: 2026-03-09T09:59:01.183Z
Learning: In `src/chain/store/base_fee.rs` (Forest, ChainSafe/forest), the FIP-0115 activation condition in `compute_base_fee` uses `ts.epoch() >= next_upgrade_height` (where `ts` is the parent tipset), not `ts.epoch() + 1 >= next_upgrade_height`. This is intentional and matches the Lotus reference implementation in `chain/store/basefee.go`.
Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 5930
File: build.rs:64-77
Timestamp: 2025-08-13T09:43:20.301Z
Learning: hanabi1224 prefers hard compile-time errors in build scripts rather than runtime safeguards or collision detection, believing it's better to fail fast and fix root causes of issues like malformed snapshot names.
Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 6057
File: src/cli/subcommands/f3_cmd.rs:0-0
Timestamp: 2025-09-09T10:37:17.947Z
Learning: hanabi1224 prefers having default timeouts (like 10m for --no-progress-timeout) to prevent commands from hanging indefinitely, even when the timeout flag isn't explicitly provided by users. This fail-fast approach is preferred over requiring explicit flag usage.
Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 6381
File: src/lotus_json/actors/states/cron_state.rs:8-8
Timestamp: 2026-01-05T12:54:40.850Z
Learning: In Rust code reviews, do not derive Eq for a struct if any field does not implement Eq (e.g., types from external dependencies). If a type like CronStateLotusJson includes fields wrapping external dependencies that lack Eq, derive PartialEq (or implement PartialEq manually) but avoid deriving Eq. This ensures comparisons compile and reflect actual equivalence semantics. When needed, consider implementing custom PartialEq (and possibly Eq) only after ensuring all fields (or wrappers) implement Eq, or keep PartialEq-only if full equality semantics cannot be expressed.
Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 6381
File: src/lotus_json/actors/states/evm_state.rs:41-44
Timestamp: 2026-01-05T12:56:13.802Z
Learning: In Rust codebases (e.g., Forest), do not add #[cfg(test)] to functions already annotated with #[test]. The #[test] attribute ensures the function is compiled only for tests, so a separate #[cfg(test)] is redundant and can be removed if present. Apply this check to all Rust files that contain #[test] functions.
Learnt from: hanabi1224
Repo: ChainSafe/forest PR: 6666
File: src/tool/subcommands/archive_cmd.rs:628-631
Timestamp: 2026-03-02T09:43:34.946Z
Learning: In Rust sources related to Forest snapshot handling, implement the rule: enable message_receipts and events (message_receipts: true, events: true) only for GC snapshots as defined in src/db/gc/snapshot.rs, since these are internal snapshots created during garbage collection. For user-facing export commands such as src/tool/subcommands/archive_cmd.rs, disable receipts and events by default (message_receipts: false, events: false) to keep user-facing snapshots smaller, unless explicitly requested. This guidance targets Rust files; apply consistently across similar snapshot-related modules, using the narrowest applicable scope when extending beyond the two specified files.
80855f0 to
9865f72
Compare
There was a problem hiding this comment.
Actionable comments posted: 19
🧹 Nitpick comments (6)
src/chain/store/indexer.rs (2)
163-193: 🚀 Performance & Scalability | 🔵 TrivialConsider batching the gc delete to bound the write-lock hold time.
gcholds the write lock for the wholeDELETE FROM tipset_message WHERE height < ?.index_loopacquires the same lock on line 132. On a node whose index has never been pruned, the first delete touches every row below the retention window and cascades intoeventandevent_entry. Head-change indexing stalls for that whole period.A bounded loop, for example
DELETE ... WHERE height < ? AND id IN (SELECT id FROM tipset_message WHERE height < ? LIMIT 10000)repeated untilrows_affected() == 0, releases the lock between batches. Adding a duration metric for the gc pass would also make the stall visible.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/chain/store/indexer.rs` around lines 163 - 193, Update gc so it deletes eligible tipsets in bounded batches instead of one full delete while holding the write lock. Reacquire the lock for each batch and repeat the existing remove_tipsets_before_height operation until rows_affected() is zero, preserving the current retention threshold and logging; use the existing index_loop lock coordination to ensure other indexing work can run between batches.
474-499: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy liftThe AMT root check issues one transaction per message and one query per event.
amt_root_for_eventson line 476 runs once per executed message. Each call opens a transaction (line 550), runsget_event_id_and_emitter_id, then runsget_event_entriesonce per event (line 567). For a tipset withmmessages andnevents the cost ismtransactions andm + nqueries.The transaction on line 550 is also never committed or rolled back explicitly. It rolls back on drop, which is correct for a read but adds needless write-lock contention on the SQLite connection.
Fetch all entries for the tipset in one query joined on
event_id, group them in memory byevent_id, and build every message's AMT from that single result set. That reduces the whole verification to two queries.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/chain/store/indexer.rs` around lines 474 - 499, Replace the per-message amt_root_for_events verification path with a tipset-wide bulk fetch: query all event entries for the tipset joined by event_id in one read-only operation, then group the results in memory and build each message’s AMT from that shared result set. Update the loop over executed_messages to reuse those computed roots while preserving the existing mismatch and missing-root validation, reducing verification to the existing tipset query plus the bulk entries query.src/chain/store/indexer/ddls.rs (2)
46-126: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider associated consts instead of a
Defaultimpl over&'static strfields.Every field is a
&'static strwith a fixed value. TheDefaultimpl repeats each name three times: in the struct, in theletbinding, and in the struct literal. A typo in the mapping compiles and silently binds the wrong SQL. Associated constants or apub constmodule remove that risk and remove roughly 80 lines.Also consider a clearer name.
PreparedStatementssuggests SQLx prepared statement handles, but these are SQL source strings. SQLx prepares and caches them per connection.♻️ Sketch
pub mod stmts { pub const HAS_TIPSET: &str = "SELECT EXISTS(SELECT 1 FROM tipset_message WHERE tipset_key_cid = ?)"; pub const IS_INDEX_EMPTY: &str = "SELECT NOT EXISTS(SELECT 1 FROM tipset_message LIMIT 1)"; // ... }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/chain/store/indexer/ddls.rs` around lines 46 - 126, Replace the PreparedStatements struct and its Default implementation with a constants module or associated constants containing each SQL string exactly once, using consistent names and preserving the existing query values. Rename the abstraction to reflect that it stores SQL source strings rather than prepared statement handles, and update all references to the old type and fields to use the new constants.
10-13: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winThe
UNIQUE (tipset_key_cid, message_cid)constraint does not cover empty tipsets.SQLite treats each NULL as distinct in a UNIQUE index.
index_tipset_with_txinsertsmessage_cid = NULLfor a tipset with no messages (src/chain/store/indexer.rs lines 703-711). TheON CONFLICT (tipset_key_cid, message_cid) DO UPDATE SET reverted = 0clause on line 85 therefore never matches that row.Today the duplicate is prevented only by the preceding
restore_tipset_if_exists_with_txexistence check. The constraint provides no backstop. If that ordering changes, empty tipsets get duplicate rows andget_non_reverted_tipset_message_countstays correct only because it filtersmessage_cid IS NOT NULL.Two options: store a sentinel empty blob instead of NULL for the no-message row, or add a partial unique index for the NULL case.
♻️ Option: add a partial unique index
"CREATE INDEX IF NOT EXISTS event_entry_event_id ON event_entry(event_id)", + "CREATE UNIQUE INDEX IF NOT EXISTS idx_tipset_no_message ON tipset_message (tipset_key_cid) WHERE message_cid IS NULL", ];Note that
DDLSis declared as[&str; 10]on line 4, so the array length must also change.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/chain/store/indexer/ddls.rs` around lines 10 - 13, Update the DDL definitions around the tipset/message uniqueness constraint to enforce uniqueness for empty-tipset rows where message_cid is NULL, preferably by adding a partial unique index on tipset_key_cid for NULL message_cid values. Increase the DDLS array length from 10 to include the new statement, while preserving the existing composite UNIQUE constraint for non-NULL messages.src/chain/store/indexer/tests.rs (1)
11-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd tests for the indexing and validation paths.
test_indexer_newasserts only that construction succeeds. The cohort adds head-change indexing, revert, garbage collection, backfill, index validation, and event AMT reconstruction. None of those are covered.
Chain4UplusMemoryDBalready give this test a synthetic chain, so the following cases are reachable without a snapshot:
index_tipsetthenget_indexed_tipset_datafor a tipset with messages, then assert the counts.revert_tipsetthenindex_tipsetfor the same tipset, and assertrevertedreturns to 0.validate_indexwithbackfill = falseagainst an empty index, and assert it returns thecheck_backfill_requirederror rather than writing.- A tipset with two message-bearing messages that both emit events, and assert
event_indexrestarts at 0 per message.amt_root_for_eventsagainst a known receipt events root.I can draft these tests. Do you want me to open an issue to track the coverage, or propose the test module directly?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/chain/store/indexer/tests.rs` around lines 11 - 36, Expand test_indexer_new into focused tests covering index_tipset/get_indexed_tipset_data counts, revert_tipset followed by reindexing with reverted returning to zero, validate_index with backfill disabled returning check_backfill_required without writing, per-message event_index resetting to zero for two event-emitting messages, and amt_root_for_events against a known receipt events root, using the existing Chain4U and MemoryDB fixtures.src/utils/sqlite/mod.rs (1)
89-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
.context()to keep the SQLx error as the source.
anyhow::anyhow!("...: {e}")flattens the SQLx error into a string. The error chain is lost..context()preserves the source error and matches the pattern used on line 86.♻️ Proposed change
- init(db, schema_version).await.map_err(|e| { - anyhow::anyhow!("failed to initialize db version {schema_version}: {e}") - })?; + init(db, schema_version) + .await + .with_context(|| format!("failed to initialize {name} db version {schema_version}"))?;Based on the coding guideline "Use
anyhow::Result<T>for most operations and add context with.context()when errors occur".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/sqlite/mod.rs` around lines 89 - 91, Update the error handling around the init call in the database initialization flow to use anyhow’s `.context()` instead of `map_err` with formatted `anyhow!`. Preserve the existing message including the schema version while retaining the original SQLx error as the source, matching the established pattern near this code.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/chain/store/indexer.rs`:
- Line 442: The async call sites in verify_indexed_data and index_events_with_tx
currently invoke synchronous load_executed_messages directly, blocking Tokio
workers during blockstore reads or tipset recomputation. Move each invocation
into tokio::task::spawn_blocking, clone the required handles, and make captured
msg_ts and receipt_ts values owned Tipset instances; await and propagate the
blocking task result while preserving existing error handling.
- Around line 844-855: Update the event-entry persistence loop around
event.entries() to store the complete u64 flags value as an 8-byte
representation instead of casting to u8, and update amt_root_for_events to
decode that same big-endian 8-byte format. Because this changes the on-disk
encoding, add the corresponding schema migration entry to the version_migrations
vector passed to sqlite::init_db, preserving round-trip compatibility for newly
written entries.
- Around line 57-60: The error message in the anyhow::ensure! validation check
uses "greater than" language but the actual condition accepts values equal to
EPOCHS_IN_DAY via the >= operator. Update the error message string to say
"greater than or equal to" instead of "greater than" so it accurately reflects
the >= boundary check and prevents operators from over-correcting to a value one
higher than necessary.
- Around line 795-857: Move the event counter initialization inside the
per-message loop in the executed_messages processing flow, so each message
starts its event_index at zero. Update the counter used by insert_event while
preserving the existing event ordering and message_id handling.
- Around line 571-585: In the event-reading loop, stop silently recovering from
corrupt or non-contiguous data: replace the optional `flags.first()` handling
with an explicit failure identifying the row when the flags blob is empty, and
use each row’s stored `event_index` rather than the loop ordinal when calling
`events.set`. Validate or propagate errors so missing indices are reported
directly while preserving the existing `Entry` construction for valid rows.
- Around line 606-627: Update the recompute closure in the receipt/event loading
flow to accept the triggering error and describe the failed operation
generically rather than always claiming receipt loading failed. Pass the
Receipt::get_receipts error from its Err branch and the StampedEvent::get_events
error from the later call site into the closure, including each error’s details
in the warning log.
- Around line 223-249: Update populate to commit and reopen the database
transaction in bounded batches while walking head.chain, avoiding one
transaction for the entire history. Apply self.options.gc_retention_epochs when
non-zero and stop at the resulting retention horizon. Change the
index_tipset_with_tx failure log to warn level, track the epoch reached, and
include it in the final success message so partial population is distinguishable
from a complete run.
- Around line 327-339: The conditional block around backfill_missing_tipset
unconditionally attempts backfilling after any verification failure from
get_and_verify_indexed_data, ignoring the backfill flag. Guard the
backfill_missing_tipset call with a check_backfill_required(epoch, backfill)
validation before proceeding. Additionally, preserve the original error from the
first get_and_verify_indexed_data call so that if the retry also fails, you can
return the original verification error instead of the retry error.
- Around line 126-148: In the index_loop method, update the error handling for
head_changes_rx.recv().await to distinguish RecvError::Lagged from fatal errors.
When Lagged occurs, resynchronize the index with the current canonical head
before continuing to consume new events, ensuring any necessary reverts and
applies are processed. Additionally, replace the warning-only approach for
revert_tipset and index_tipset errors with explicit retry logic or durable
failure-state exposure to prevent gaps in the index from skipped operations.
In `@src/chain/store/indexer/events.rs`:
- Around line 152-154: Update the event query construction around the
tipset-specific branch so the e.reverted=? false clause is added only for
height-range queries, not when a specific tipset_cid is requested. Keep reverted
historical events excluded for non-specific queries and preserve the existing
argument handling.
- Around line 198-216: Replace the key-filter joins generated in
get_events_for_filter with correlated EXISTS predicates against event_entry,
preserving the indexed, key, codec, and value conditions and their bound
arguments. Remove the alias and joins.push usage in this self.keys loop, while
keeping the existing OR grouping for multiple values and ensuring each base
event is returned only once.
In `@src/cli/subcommands/index_cmd.rs`:
- Around line 214-223: Update the validation loop handling around
ChainValidateIndex to count each Err(e) while continuing to process all epochs
and logging the failure. Include the failure count in the existing summary as
needed, then return an error after the summary when any validation failed;
preserve Ok(()) only when all validation calls succeed.
- Line 190: Update the ChainHead::call operation in the index validation flow to
add the requested anyhow context before propagating the error, and import
anyhow::Context as _ so the extension method is available. Preserve the existing
ChainHead call and await behavior while ensuring the failure message identifies
chain-head retrieval for index validation.
In `@src/daemon/context.rs`:
- Around line 61-100: Update AppContext::init so chain_indexer is constructed
only when the indexer lifecycle is enabled and the node mode is eligible for
indexing, matching maybe_start_indexer_service. Ensure stateless and devnet
configurations pass None to RPC instead of exposing an unpopulated indexer;
preserve the existing construction and callbacks for eligible nodes.
- Around line 86-96: Update the RecomputeTipsetStateFunc callback in the
state-manager setup to use asynchronous StateManager::compute_tipset_state
instead of compute_tipset_state_blocking, preserving NO_CALLBACK and
VMTrace::NotTraced; propagate the resulting async signature through
load_executed_messages and all affected callers, including validate_index, so VM
recomputation runs via spawn_blocking rather than Tokio workers.
In `@src/daemon/mod.rs`:
- Around line 167-172: Gate indexed RPC reads on index readiness: in
src/daemon/mod.rs lines 167-172, mark the chain index unavailable before
populate and only make it available after successful completion, keeping it
unavailable on failure; in src/daemon/mod.rs lines 805-808, handle index_loop
exit by retrying or marking the index unavailable so RPC falls back to the
non-indexed event path until the index is current. Update the existing chain
indexer readiness/state symbols used by start_services and index_loop rather
than only logging failures.
In `@src/rpc/methods/eth.rs`:
- Around line 3407-3421: Add an event-coverage validation around the large-range
branch in the filter event retrieval flow before using
chain_indexer.get_events_for_filter. Because snapshots populated through
index_tipset_with_tx may lack indexed events, fall back to
ctx.eth_event_handler.get_events_for_parsed_filter when coverage is incomplete.
Ensure indexed results apply the same unresolved-emitter policy as
SkipEvent::OnUnresolvedAddress used by the in-memory path, rather than returning
ID addresses.
In `@src/rpc/methods/eth/filter/mod.rs`:
- Line 779: Update the documentation for is_large_range_for_sql in
src/rpc/methods/eth/filter/mod.rs at lines 779-779 to say “at or above the
threshold” and correct “prefering” to “preferring”; update
docs/docs/users/reference/env_variables.md at lines 81-81 to say “at or above
which” and correct “prefered” to “preferred,” matching the inclusive range
behavior.
- Around line 791-793: Update is_large_range_for_sql to handle the
ParsedFilterTipsets::Range sentinel where range.end() is -1 for latest or
pending, resolving it to the current maximum block before calculating the range
size and SQL threshold. Preserve the existing behavior for ordinary ranges and
ensure earliest-to-latest selects the SQL path when appropriate.
---
Nitpick comments:
In `@src/chain/store/indexer.rs`:
- Around line 163-193: Update gc so it deletes eligible tipsets in bounded
batches instead of one full delete while holding the write lock. Reacquire the
lock for each batch and repeat the existing remove_tipsets_before_height
operation until rows_affected() is zero, preserving the current retention
threshold and logging; use the existing index_loop lock coordination to ensure
other indexing work can run between batches.
- Around line 474-499: Replace the per-message amt_root_for_events verification
path with a tipset-wide bulk fetch: query all event entries for the tipset
joined by event_id in one read-only operation, then group the results in memory
and build each message’s AMT from that shared result set. Update the loop over
executed_messages to reuse those computed roots while preserving the existing
mismatch and missing-root validation, reducing verification to the existing
tipset query plus the bulk entries query.
In `@src/chain/store/indexer/ddls.rs`:
- Around line 46-126: Replace the PreparedStatements struct and its Default
implementation with a constants module or associated constants containing each
SQL string exactly once, using consistent names and preserving the existing
query values. Rename the abstraction to reflect that it stores SQL source
strings rather than prepared statement handles, and update all references to the
old type and fields to use the new constants.
- Around line 10-13: Update the DDL definitions around the tipset/message
uniqueness constraint to enforce uniqueness for empty-tipset rows where
message_cid is NULL, preferably by adding a partial unique index on
tipset_key_cid for NULL message_cid values. Increase the DDLS array length from
10 to include the new statement, while preserving the existing composite UNIQUE
constraint for non-NULL messages.
In `@src/chain/store/indexer/tests.rs`:
- Around line 11-36: Expand test_indexer_new into focused tests covering
index_tipset/get_indexed_tipset_data counts, revert_tipset followed by
reindexing with reverted returning to zero, validate_index with backfill
disabled returning check_backfill_required without writing, per-message
event_index resetting to zero for two event-emitting messages, and
amt_root_for_events against a known receipt events root, using the existing
Chain4U and MemoryDB fixtures.
In `@src/utils/sqlite/mod.rs`:
- Around line 89-91: Update the error handling around the init call in the
database initialization flow to use anyhow’s `.context()` instead of `map_err`
with formatted `anyhow!`. Preserve the existing message including the schema
version while retaining the original SQLx error as the source, matching the
established pattern near this code.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 4dbccef9-8eac-43fb-aa96-a3da9a4dfc07
📒 Files selected for processing (33)
CHANGELOG.mdCargo.tomldocs/docs/users/reference/env_variables.mdscripts/tests/api_compare/docker-compose.ymlscripts/tests/api_compare/filter-list-gatewayscripts/tests/api_compare/filter-list-offlinesrc/beacon/drand.rssrc/chain/store/indexer.rssrc/chain/store/indexer/ddls.rssrc/chain/store/indexer/events.rssrc/chain/store/indexer/tests.rssrc/chain/store/mod.rssrc/cli/subcommands/index_cmd.rssrc/cli/subcommands/mod.rssrc/daemon/context.rssrc/daemon/mod.rssrc/db/mod.rssrc/lib.rssrc/message_pool/msgpool/test_provider.rssrc/rpc/methods/chain.rssrc/rpc/methods/chain/types.rssrc/rpc/methods/eth.rssrc/rpc/methods/eth/filter/mod.rssrc/rpc/methods/sync.rssrc/rpc/mod.rssrc/shim/address.rssrc/shim/mod.rssrc/tool/offline_server/server.rssrc/tool/subcommands/api_cmd/api_compare_tests.rssrc/tool/subcommands/api_cmd/generate_test_snapshot.rssrc/tool/subcommands/api_cmd/test_snapshot.rssrc/utils/mod.rssrc/utils/sqlite/mod.rs
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
filecoin-project/lotus(manual)
💤 Files with no reviewable changes (2)
- src/utils/mod.rs
- src/beacon/drand.rs
| pub async fn index_loop( | ||
| &self, | ||
| mut head_changes_rx: tokio::sync::broadcast::Receiver<HeadChanges>, | ||
| ) -> anyhow::Result<()> { | ||
| loop { | ||
| let HeadChanges { reverts, applies } = head_changes_rx.recv().await?; | ||
| let _lock = self.acquire_write_lock().await; | ||
| for ts in reverts { | ||
| if let Err(e) = self.revert_tipset(&ts).await { | ||
| tracing::warn!( | ||
| "failed to revert new head@{}({}): {e}", | ||
| ts.epoch(), | ||
| ts.key() | ||
| ); | ||
| } | ||
| } | ||
| for ts in applies { | ||
| if let Err(e) = self.index_tipset(&ts).await { | ||
| tracing::warn!("failed to index new head@{}({}): {e}", ts.epoch(), ts.key()); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🌐 Web query:
tokio broadcast Receiver recv RecvError Lagged receiver still usable after lagged
💡 Result:
Yes, a tokio::sync::broadcast::Receiver remains fully usable after returning a RecvError::Lagged error [1][2]. When a receiver falls too far behind and the channel's buffer overwrites unread messages, the next call to recv returns Err(RecvError::Lagged) [3][1]. This error does not close, disconnect, or invalidate the receiver [2][4]. Instead, the receiver's internal cursor is automatically advanced to the oldest message currently retained in the channel's buffer [3][2]. Following the Lagged error, you can continue to call recv to receive the oldest available messages currently stored in the channel [1][2]. If no further messages have been sent or if those messages are subsequently overwritten before you call recv again, you may receive another Lagged error or proceed to receive new values as they arrive [2][4]. The RecvError::Lagged variant also includes a value (a u64) representing the number of messages that were skipped [5][2].
Citations:
- 1: https://docs.rs/tokio/latest/tokio/sync/broadcast/
- 2: https://github.com/tokio-rs/tokio/blob/master/tokio/src/sync/broadcast.rs
- 3: https://docs.rs/tokio/latest/%20tokio/sync/broadcast/struct.Receiver.html
- 4: https://doc.servo.org/tokio/sync/broadcast/struct.Receiver.html
- 5: https://docs.rs/tokio/latest/tokio/sync/broadcast/error/enum.RecvError.html
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- indexer structure and usages ---'
rg -n -C 8 'index_loop|broadcast::|HeadChanges|revert_tipset|index_tipset|validate-backfill' src/chain src/rpc src | head -n 260
printf '%s\n' '--- index_loop source ---'
sed -n '110,160p' src/chain/store/indexer.rs
printf '%s\n' '--- EthGetLogs routing ---'
sed -n '3385,3430p' src/rpc/methods/eth.rs
printf '%s\n' '--- relevant Cargo tokio version ---'
rg -n 'tokio' Cargo.toml */Cargo.tomlRepository: ChainSafe/forest
Length of output: 21814
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- broadcast capacity and publication ---'
rg -n -C 12 'SINK_CAP|head_changes_tx\.(send|subscribe)|index_loop\(' src
printf '%s\n' '--- head update publication ---'
sed -n '230,285p' src/chain/store/chain_store.rs
printf '%s\n' '--- indexer startup and task supervision ---'
rg -n -C 15 'chain_indexer|index_loop|gc_loop|spawn.*index|JoinHandle|validate' src/daemon src/chain src/rpc | head -n 320
printf '%s\n' '--- validation and backfill implementation ---'
rg -n -C 18 'backfill_missing_tipset|ChainIndexValidation|validate.*index|validate-backfill|validate_backfill' src/chain src/rpc src/cliRepository: ChainSafe/forest
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- service supervisor definition and behavior ---'
rg -n -C 12 'struct Services|impl.*Services|fn spawn|services\.spawn|stopped unexpectedly' src/daemon
printf '%s\n' '--- index transaction and parent retry path ---'
sed -n '245,350p' src/chain/store/indexer.rs
sed -n '654,765p' src/chain/store/indexer.rs
printf '%s\n' '--- indexer tests and error handling coverage ---'
rg -n -C 8 'index_loop|index_tipset|failed to index|Lagged|RecvError|validate_index' src tests
printf '%s\n' '--- pinned Tokio version ---'
rg -n -A 4 -B 4 '^name = "tokio"$' Cargo.lockRepository: ChainSafe/forest
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
indexer = Path("src/chain/store/indexer.rs").read_text()
daemon = Path("src/daemon/mod.rs").read_text()
start = indexer.index(" pub async fn index_loop(")
end = indexer.index(" pub async fn gc_loop(", start)
loop = indexer[start:end]
assert "head_changes_rx.recv().await?;" in loop
assert "Err(RecvError::Lagged" not in loop
assert "if let Err(e) = indexer.index_loop(head_changes_rx).await" in daemon
wrapper_start = daemon.index("if let Err(e) = indexer.index_loop(head_changes_rx).await")
wrapper = daemon[wrapper_start:daemon.index(" }\n });", wrapper_start)]
assert "Ok(())" in wrapper
capacity = Path("src/chain/store/chain_store.rs").read_text()
assert "const SINK_CAP: usize = 200;" in capacity
print("index_loop propagates recv errors: yes")
print("index_loop handles RecvError::Lagged: no")
print("daemon wrapper returns Ok after index_loop exits: yes")
print("head-change broadcast capacity: 200")
PY
printf '%s\n' '--- daemon join supervision ---'
rg -n -A 22 -B 8 'async fn propagate_error|fn propagate_error' src/daemonRepository: ChainSafe/forest
Length of output: 1908
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- indexer population API and callers ---'
sed -n '180,252p' src/chain/store/indexer.rs
rg -n -C 12 '\.populate\(|populate\(\)' src
printf '%s\n' '--- index database initialization and startup population ---'
rg -n -C 15 'populate.*chain|populate_chain|chain index.*populate|populate_index' srcRepository: ChainSafe/forest
Length of output: 21191
Resynchronize the index after RecvError::Lagged
head_changes_rx.recv().await? exits index_loop when the 200-entry broadcast buffer overwrites unread changes. RecvError::Lagged(n) leaves the receiver usable, but continuing alone discards those changes. The daemon wrapper logs the error and returns Ok(()), so the supervisor does not restart the task. Large EthGetLogs ranges can then use a stale chain_indexer.
Resynchronize the index with the current canonical head, including reorg reverts, before consuming new events. Also retry or expose durable failure state for revert_tipset and index_tipset; warning and skipping can leave index gaps until a later retry or ChainValidateIndex backfill.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/chain/store/indexer.rs` around lines 126 - 148, In the index_loop method,
update the error handling for head_changes_rx.recv().await to distinguish
RecvError::Lagged from fatal errors. When Lagged occurs, resynchronize the index
with the current canonical head before continuing to consume new events,
ensuring any necessary reverts and applies are processed. Additionally, replace
the warning-only approach for revert_tipset and index_tipset errors with
explicit retry logic or durable failure-state exposure to prevent gaps in the
index from skipped operations.
There was a problem hiding this comment.
We have the same issue with nosql index at https://github.com/ChainSafe/forest/blob/main/src/daemon/db_util.rs#L833 create a issue to track switching to https://crates.io/crates/async-broadcast which supports disabling overflow
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
repo="ChainSafe/forest"
title="Prevent head-change broadcast overflow by migrating to async-broadcast"
body=$(cat <<'EOF'
## Summary
The head-change broadcast channel can overwrite unread updates when a consumer falls behind. This can leave an index incomplete or stale.
Migrate the head-change publication mechanism to [`async-broadcast`](https://crates.io/crates/async-broadcast) and disable overflow.
## Rationale
The NoSQL index has the same risk at `src/daemon/db_util.rs` near Line 833. The SQLite chain indexer introduced in `#6720` also consumes head changes. A lagging consumer must not silently lose head-change updates.
## Affected areas
- `src/daemon/db_util.rs` head-change publication and NoSQL index consumption.
- SQLite chain indexer head-change consumption in `src/chain/store/indexer.rs`.
- Any other consumers of the same head-change channel.
## Required changes
1. Replace the applicable Tokio broadcast channel with `async-broadcast`.
2. Configure the channel so that sends do not discard unread messages on overflow.
3. Update all producers and consumers for the new channel API.
4. Define and implement shutdown behavior so blocked senders or receivers can exit cleanly.
## Acceptance criteria
- A slow index consumer does not lose head-change updates because of channel-buffer overflow.
- Both the NoSQL index and SQLite chain indexer continue to process ordered head changes.
- Tests cover a consumer that falls behind the configured channel capacity.
- Shutdown does not leave channel tasks blocked.
Requested by: `@hanabi1224`
Backlinks:
- Pull request: https://github.com/ChainSafe/forest/pull/6720
- Review comment: https://github.com/ChainSafe/forest/pull/6720#discussion_r3712611454
EOF
)
issue_url=$(gh issue create \
--repo "$repo" \
--title "$title" \
--body "$body" \
--assignee "hanabi1224")
printf '%s\n' "$issue_url"Length of output: 2091
@hanabi1224, created and assigned follow-up issue #7442: Prevent head-change broadcast overflow by migrating to async-broadcast.
It covers the NoSQL index, the SQLite chain indexer, ordered delivery, overflow prevention, shutdown behavior, and test coverage.
You are interacting with an AI system.
| let ( | ||
| IndexedTipsetData { | ||
| indexed_messages_count, | ||
| indexed_events_count, | ||
| indexed_event_entries_count, | ||
| }, | ||
| backfilled, | ||
| ) = if let Ok(r) = self.get_and_verify_indexed_data(&ts).await { | ||
| (r, false) | ||
| } else { | ||
| self.backfill_missing_tipset(&ts).await?; | ||
| (self.get_and_verify_indexed_data(&ts).await?, true) | ||
| }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm the permission level and callers of the ChainValidateIndex RPC method.
rg -n -C10 'ChainValidateIndex|validate_index' --type=rustRepository: ChainSafe/forest
Length of output: 154
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
fd -i 'indexer.rs|chain.rs' src
printf '%s\n' '--- validate_index and RPC symbols ---'
rg -n -C8 'validate_index|ChainValidateIndex|chain_validate_index|backfill_missing_tipset|get_and_verify_indexed_data|check_backfill_required' src || true
printf '%s\n' '--- relevant indexer section ---'
sed -n '240,350p' src/chain/store/indexer.rsRepository: ChainSafe/forest
Length of output: 21625
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- RPC method declarations and permissions ---'
rg -n -C15 'ChainValidateIndex|ValidateIndex|validateIndex|validate_index' . || true
printf '%s\n' '--- lock and concurrent task context ---'
sed -n '110,185p' src/chain/store/indexer.rs
sed -n '90,130p' src/rpc/methods/chain.rs
printf '%s\n' '--- backfill call sites ---'
rg -n -C8 'backfill_missing_tipset|check_backfill_required' src/chain/store/indexer.rsRepository: ChainSafe/forest
Length of output: 28458
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
p = Path("src/chain/store/indexer.rs")
text = p.read_text()
for needle in ("async fn validate_index", "fn validate_index", "get_and_verify_indexed_data"):
print(f"--- {needle} ---")
for i, line in enumerate(text.splitlines(), 1):
if needle in line:
print(i, line)
PY
printf '%s\n' '--- file status and exact paths ---'
git ls-files | rg '(^|/)(indexer|chain)\.rs$|rpc.*chain'Repository: ChainSafe/forest
Length of output: 691
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- lock definition and index write paths ---'
rg -n -C12 'acquire_write_lock|write_lock|index_tipset_and_parent_events_with_tx|backfill_missing_tipset' src/chain/store/indexer.rs
printf '%s\n' '--- validation tests and error handling ---'
rg -n -C12 'validate_index|ChainValidateIndex|index verification failed|verify_indexed_data' --glob '*.rs' . || true
printf '%s\n' '--- standalone control-flow verifier ---'
python3 - <<'PY'
from pathlib import Path
import re
s = Path("src/chain/store/indexer.rs").read_text()
lock = re.search(r'let _lock = if backfill \{.*?\n \};', s, re.S)
branch = re.search(r'\) = if let Ok\(r\) = self\.get_and_verify_indexed_data\(&ts\)\.await \{.*?\n \};', s, re.S)
assert lock and branch
print("write lock conditional on backfill:", "if backfill" in lock.group(0))
print("verification-error branch calls check_backfill_required:",
"check_backfill_required" in branch.group(0))
print("verification-error branch calls backfill_missing_tipset:",
"backfill_missing_tipset" in branch.group(0))
print("verification error is bound for reuse:",
bool(re.search(r'Err\([^)]*\)', branch.group(0))))
PYRepository: ChainSafe/forest
Length of output: 29892
Guard verification-triggered backfills with backfill and preserve the original error.
This branch calls backfill_missing_tipset after any verification error, even when backfill is false. It also skips the write lock because validate_index acquires that lock only when backfill is true. Call check_backfill_required(epoch, backfill) before backfilling. Preserve the first verification error when the retry fails.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/chain/store/indexer.rs` around lines 327 - 339, The conditional block
around backfill_missing_tipset unconditionally attempts backfilling after any
verification failure from get_and_verify_indexed_data, ignoring the backfill
flag. Guard the backfill_missing_tipset call with a
check_backfill_required(epoch, backfill) validation before proceeding.
Additionally, preserve the original error from the first
get_and_verify_indexed_data call so that if the retry also fails, you can return
the original verification error instead of the retry error.
| // populate chain index if enabled | ||
| if let Some(chain_indexer) = &ctx.chain_indexer | ||
| && let Err(e) = chain_indexer.populate().await | ||
| { | ||
| tracing::warn!("failed to populate chain index from snapshot: {e}"); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Gate indexed RPC reads on index readiness.
start_services starts RPC before snapshot import and population. During populate, a large-range eth_getLogs request can use a partial SQLite index. If population or the index loop fails, these branches only log the error, while RPC continues to select the indexer when it exists. The query path has no fallback or completeness check, so it can return missing logs.
src/daemon/mod.rs#L167-L172: Mark the index unavailable until population completes. Keep it unavailable if population fails.src/daemon/mod.rs#L805-L808: Retry or mark the index unavailable whenindex_loopexits. Make RPC fall back to the non-indexed event path until the index is current.
📍 Affects 1 file
src/daemon/mod.rs#L167-L172(this comment)src/daemon/mod.rs#L805-L808
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/daemon/mod.rs` around lines 167 - 172, Gate indexed RPC reads on index
readiness: in src/daemon/mod.rs lines 167-172, mark the chain index unavailable
before populate and only make it available after successful completion, keeping
it unavailable on failure; in src/daemon/mod.rs lines 805-808, handle index_loop
exit by retrying or marking the index unavailable so RPC falls back to the
non-indexed event path until the index is current. Update the existing chain
indexer readiness/state symbols used by start_services and index_loop rather
than only logging failures.
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 (2)
src/chain/store/indexer.rs (2)
223-241: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winIndex events during snapshot population.
populate_after_snapshot_importcallsindex_tipset_with_tx, which does not callindex_events_with_tx. The snapshot import path insrc/daemon/mod.rstreats this method as the complete index population step. Event rows can therefore remain absent, causing indexedeth_getLogsqueries to return incomplete results.Use
index_tipset_and_parent_events_with_txhere, or run an equivalent event-indexing pass before committing.Proposed fix
- if let Err(e) = self.index_tipset_with_tx(&mut tx, &ts).await { + if let Err(e) = self + .index_tipset_and_parent_events_with_tx(&mut tx, &ts) + .await + {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/chain/store/indexer.rs` around lines 223 - 241, Update populate_after_snapshot_import to call index_tipset_and_parent_events_with_tx instead of index_tipset_with_tx while populating each tipset, ensuring event rows are indexed before the transaction commits and snapshot-based eth_getLogs queries are complete.
618-620: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRequire the recomputation callback only when recomputation is needed.
recompute_tipset_state_funcis optional inSqliteIndexer, but this code rejects the operation before loading receipts. A caller that constructs the indexer without the callback fails even when receipt and event data are valid.Move the
.context("recompute_tipset_state_func not set")check into therecomputeclosure, immediately before invoking the callback. Alternatively, make the callback mandatory inSqliteIndexer::new.Proposed fix
- let recompute_tipset_state_func = - recompute_tipset_state_func.context("recompute_tipset_state_func not set")?; let msgs = cs.messages_for_tipset(msg_ts)?; ... let recompute = || { + let recompute_tipset_state_func = recompute_tipset_state_func + .as_ref() + .context("recompute_tipset_state_func not set")?; let tsk_cid = receipt_ts.key().cid()?;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/chain/store/indexer.rs` around lines 618 - 620, Defer the required-callback check in the tipset state recomputation flow: remove the eager context check before loading messages and, inside the recompute closure immediately before invoking recompute_tipset_state_func, require the optional callback with the existing “recompute_tipset_state_func not set” context. Preserve normal receipt/event processing when recomputation is not needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/chain/store/indexer.rs`:
- Around line 223-241: Update populate_after_snapshot_import to call
index_tipset_and_parent_events_with_tx instead of index_tipset_with_tx while
populating each tipset, ensuring event rows are indexed before the transaction
commits and snapshot-based eth_getLogs queries are complete.
- Around line 618-620: Defer the required-callback check in the tipset state
recomputation flow: remove the eager context check before loading messages and,
inside the recompute closure immediately before invoking
recompute_tipset_state_func, require the optional callback with the existing
“recompute_tipset_state_func not set” context. Preserve normal receipt/event
processing when recomputation is not needed.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: d5268a58-23e9-4ce5-b082-0b704876a30e
📒 Files selected for processing (4)
src/chain/store/indexer.rssrc/cli/subcommands/index_cmd.rssrc/daemon/context.rssrc/daemon/mod.rs
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
filecoin-project/lotus(manual)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/cli/subcommands/index_cmd.rs
- src/daemon/mod.rs
- src/daemon/context.rs
|
No green checkmark and lots of 🐰 to address! No review! |
|
High level, though, I'd like to see some numbers to prove the feature works as intended, e.g., queries that would've been slow with regular logic without this indexer. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/tool/subcommands/api_cmd/api_compare_tests.rs`:
- Around line 605-606: Update the comparison setup around ChainValidateIndex
requests, including the unconditional request near the earlier call site, so
they are only added when !offline; alternatively configure these requests with
an explicit rejection policy that accepts the offline server’s disabled-indexer
response. Preserve the existing behavior for online comparisons.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 53eb39d2-53f5-41e5-a5e6-d84b86d38b0a
📒 Files selected for processing (2)
src/tool/subcommands/api_cmd/api_compare_tests.rssrc/tool/subcommands/api_cmd/test_snapshots_ignored.txt
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
filecoin-project/lotus(manual)
Summary of changes
Changes introduced in this pull request:
Reference issue to close (if applicable)
Closes #7017
Other information and links
Change checklist
Outside contributions
Summary by CodeRabbit
New Features
Filecoin.ChainValidateIndexand theforest-cli index validate-backfillcommand for validating and optionally backfilling index data.EthGetLogsrequests can now use the SQL index for improved retrieval.Documentation
FOREST_RPC_SQL_RANGE_THRESHOLD, configurable with a default of 500.