Skip to content

feat: sqlite indexer - #6720

Open
hanabi1224 wants to merge 62 commits into
mainfrom
hm/sqlite-indexer
Open

feat: sqlite indexer#6720
hanabi1224 wants to merge 62 commits into
mainfrom
hm/sqlite-indexer

Conversation

@hanabi1224

@hanabi1224 hanabi1224 commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

Summary of changes

Changes introduced in this pull request:

Reference issue to close (if applicable)

Closes #7017

Other information and links

Change checklist

  • I have performed a self-review of my own code,
  • I have made corresponding changes to the documentation. All new code adheres to the team's documentation standards,
  • I have added tests that prove my fix is effective or that my feature works (if possible),
  • I have made sure the CHANGELOG is up-to-date. All user-facing changes should be reflected in this document.

Outside contributions

  • I have read and agree to the CONTRIBUTING document.
  • I have read and agree to the AI Policy document. I understand that failure to comply with the guidelines will lead to rejection of the pull request.

Summary by CodeRabbit

  • New Features

    • Added SQL-backed chain indexing with automatic population, maintenance, garbage collection, and fork handling.
    • Added Filecoin.ChainValidateIndex and the forest-cli index validate-backfill command for validating and optionally backfilling index data.
    • Large-range EthGetLogs requests can now use the SQL index for improved retrieval.
  • Documentation

    • Documented FOREST_RPC_SQL_RANGE_THRESHOLD, configurable with a default of 500.
    • Added unreleased changelog information for SQL index backfilling and validation.

@hanabi1224 hanabi1224 added the RPC requires calibnet RPC checks to run on CI label Mar 11, 2026
@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a SQLite-backed chain indexer with schema initialization, validation, backfilling, indexed event queries, daemon integration, RPC support, and the forest-cli index validate-backfill command.

Changes

SQLite chain indexer

Layer / File(s) Summary
SQLite storage and indexer construction
Cargo.toml, src/utils/sqlite/*, src/db/mod.rs, src/chain/store/indexer/*, src/shim/*
Adds SQLite setup, index tables, prepared statements, indexer construction, and supporting address and actor types.
Index population, validation, and event persistence
src/chain/store/indexer.rs, src/daemon/context.rs, src/daemon/mod.rs, src/chain/store/indexer/tests.rs
Adds tipset indexing, fork handling, garbage collection, snapshot population, backfilling, validation, event persistence, and daemon lifecycle wiring.
Indexed EthGetLogs queries
src/chain/store/indexer/events.rs, src/rpc/methods/eth.rs, src/rpc/methods/eth/filter/mod.rs, docs/docs/users/reference/env_variables.md
Adds indexed event filters and routes large tipset ranges through SQLite using the configurable threshold.
RPC and CLI index validation
src/rpc/methods/chain.rs, src/rpc/methods/chain/types.rs, src/rpc/mod.rs, src/cli/subcommands/index_cmd.rs, scripts/tests/api_compare/*, CHANGELOG.md
Adds Filecoin.ChainValidateIndex, the validation response type, CLI range processing, integration-test setup, and changelog documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: lesnyrumcajs, sudo-shashank, akaladarshi

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
Loading
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
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes unrelated cleanup and wording changes, such as the F3 description edit and variable renames. Remove unrelated cleanup and wording changes, or move them into a separate pull request.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: introducing a SQLite indexer.
Linked Issues check ✅ Passed The PR adds a SQLite indexer and integrates it with large-range EthGetLogs queries, addressing issue #7017.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hm/sqlite-indexer
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch hm/sqlite-indexer

Comment @coderabbitai help to get the list of available commands.

Comment thread src/daemon/mod.rs Outdated
let HeadChange::Apply(ts) = receiver.recv().await?;
// Continuously listen for head changes
loop {
let HeadChange::Apply(ts) = receiver.recv().await?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 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.

@hanabi1224
hanabi1224 marked this pull request as ready for review August 4, 2026 12:22
@hanabi1224
hanabi1224 requested a review from a team as a code owner August 4, 2026 12:22
@hanabi1224
hanabi1224 requested review from LesnyRumcajs and sudo-shashank and removed request for a team August 4, 2026 12:22
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 14.21976% with 929 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.40%. Comparing base (a379991) to head (a75c0f2).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/chain/store/indexer.rs 5.64% 632 Missing and 3 partials ⚠️
src/chain/store/indexer/events.rs 0.00% 165 Missing ⚠️
src/cli/subcommands/index_cmd.rs 0.00% 44 Missing ⚠️
src/daemon/mod.rs 4.00% 24 Missing ⚠️
src/daemon/context.rs 51.11% 19 Missing and 3 partials ⚠️
src/rpc/methods/eth.rs 42.30% 13 Missing and 2 partials ⚠️
src/rpc/methods/chain.rs 0.00% 9 Missing ⚠️
src/rpc/methods/eth/filter/mod.rs 80.00% 3 Missing and 2 partials ⚠️
src/rpc/mod.rs 0.00% 3 Missing ⚠️
src/shim/address.rs 0.00% 3 Missing ⚠️
... and 2 more
Additional details and impacted files
Files with missing lines Coverage Δ
src/beacon/drand.rs 74.86% <ø> (ø)
src/chain/store/indexer/ddls.rs 100.00% <100.00%> (ø)
src/cli/subcommands/mod.rs 8.33% <ø> (ø)
src/db/mod.rs 64.15% <ø> (ø)
src/message_pool/msgpool/test_provider.rs 99.04% <100.00%> (ø)
src/rpc/methods/sync.rs 73.68% <100.00%> (+0.15%) ⬆️
src/tool/offline_server/server.rs 30.35% <100.00%> (+0.31%) ⬆️
src/tool/subcommands/api_cmd/test_snapshot.rs 85.00% <100.00%> (+0.06%) ⬆️
src/utils/mod.rs 77.04% <ø> (ø)
...tool/subcommands/api_cmd/generate_test_snapshot.rs 5.79% <0.00%> (-0.03%) ⬇️
... and 11 more

... and 14 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a379991...a75c0f2. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

🧹 Nitpick comments (6)
src/chain/store/indexer.rs (2)

163-193: 🚀 Performance & Scalability | 🔵 Trivial

Consider batching the gc delete to bound the write-lock hold time.

gc holds the write lock for the whole DELETE FROM tipset_message WHERE height < ?. index_loop acquires 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 into event and event_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 until rows_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 lift

The AMT root check issues one transaction per message and one query per event.

amt_root_for_events on line 476 runs once per executed message. Each call opens a transaction (line 550), runs get_event_id_and_emitter_id, then runs get_event_entries once per event (line 567). For a tipset with m messages and n events the cost is m transactions and m + n queries.

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 by event_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 value

Consider associated consts instead of a Default impl over &'static str fields.

Every field is a &'static str with a fixed value. The Default impl repeats each name three times: in the struct, in the let binding, and in the struct literal. A typo in the mapping compiles and silently binds the wrong SQL. Associated constants or a pub const module remove that risk and remove roughly 80 lines.

Also consider a clearer name. PreparedStatements suggests 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 win

The 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_tx inserts message_cid = NULL for a tipset with no messages (src/chain/store/indexer.rs lines 703-711). The ON CONFLICT (tipset_key_cid, message_cid) DO UPDATE SET reverted = 0 clause on line 85 therefore never matches that row.

Today the duplicate is prevented only by the preceding restore_tipset_if_exists_with_tx existence check. The constraint provides no backstop. If that ordering changes, empty tipsets get duplicate rows and get_non_reverted_tipset_message_count stays correct only because it filters message_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 DDLS is 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 win

Add tests for the indexing and validation paths.

test_indexer_new asserts 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.

Chain4U plus MemoryDB already give this test a synthetic chain, so the following cases are reachable without a snapshot:

  • index_tipset then get_indexed_tipset_data for a tipset with messages, then assert the counts.
  • revert_tipset then index_tipset for the same tipset, and assert reverted returns to 0.
  • validate_index with backfill = false against an empty index, and assert it returns the check_backfill_required error rather than writing.
  • A tipset with two message-bearing messages that both emit events, and assert event_index restarts at 0 per message.
  • amt_root_for_events against 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 win

Use .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

📥 Commits

Reviewing files that changed from the base of the PR and between 43dd059 and 788cec7.

📒 Files selected for processing (33)
  • CHANGELOG.md
  • Cargo.toml
  • docs/docs/users/reference/env_variables.md
  • scripts/tests/api_compare/docker-compose.yml
  • scripts/tests/api_compare/filter-list-gateway
  • scripts/tests/api_compare/filter-list-offline
  • src/beacon/drand.rs
  • src/chain/store/indexer.rs
  • src/chain/store/indexer/ddls.rs
  • src/chain/store/indexer/events.rs
  • src/chain/store/indexer/tests.rs
  • src/chain/store/mod.rs
  • src/cli/subcommands/index_cmd.rs
  • src/cli/subcommands/mod.rs
  • src/daemon/context.rs
  • src/daemon/mod.rs
  • src/db/mod.rs
  • src/lib.rs
  • src/message_pool/msgpool/test_provider.rs
  • src/rpc/methods/chain.rs
  • src/rpc/methods/chain/types.rs
  • src/rpc/methods/eth.rs
  • src/rpc/methods/eth/filter/mod.rs
  • src/rpc/methods/sync.rs
  • src/rpc/mod.rs
  • src/shim/address.rs
  • src/shim/mod.rs
  • src/tool/offline_server/server.rs
  • src/tool/subcommands/api_cmd/api_compare_tests.rs
  • src/tool/subcommands/api_cmd/generate_test_snapshot.rs
  • src/tool/subcommands/api_cmd/test_snapshot.rs
  • src/utils/mod.rs
  • src/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

Comment thread src/chain/store/indexer.rs
Comment on lines +126 to +148
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());
}
}
}
}

@coderabbitai coderabbitai Bot Aug 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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:


🏁 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.toml

Repository: 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/cli

Repository: 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.lock

Repository: 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/daemon

Repository: 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' src

Repository: 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 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.

Comment thread src/chain/store/indexer.rs Outdated
Comment on lines +327 to +339
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)
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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=rust

Repository: 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.rs

Repository: 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.rs

Repository: 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))))
PY

Repository: 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.

Comment thread src/chain/store/indexer.rs Outdated
Comment thread src/daemon/context.rs
Comment thread src/daemon/mod.rs
Comment on lines +167 to +172
// 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}");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 when index_loop exits. 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.

Comment thread src/rpc/methods/eth.rs Outdated
Comment thread src/rpc/methods/eth/filter/mod.rs Outdated
Comment thread src/rpc/methods/eth/filter/mod.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Index events during snapshot population.

populate_after_snapshot_import calls index_tipset_with_tx, which does not call index_events_with_tx. The snapshot import path in src/daemon/mod.rs treats this method as the complete index population step. Event rows can therefore remain absent, causing indexed eth_getLogs queries to return incomplete results.

Use index_tipset_and_parent_events_with_tx here, 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 win

Require the recomputation callback only when recomputation is needed.

recompute_tipset_state_func is optional in SqliteIndexer, 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 the recompute closure, immediately before invoking the callback. Alternatively, make the callback mandatory in SqliteIndexer::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

📥 Commits

Reviewing files that changed from the base of the PR and between 3885cd2 and 96e4d88.

📒 Files selected for processing (4)
  • src/chain/store/indexer.rs
  • src/cli/subcommands/index_cmd.rs
  • src/daemon/context.rs
  • src/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

@LesnyRumcajs

Copy link
Copy Markdown
Member

No green checkmark and lots of 🐰 to address! No review!

@LesnyRumcajs

Copy link
Copy Markdown
Member

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 96e4d88 and 20a46ad.

📒 Files selected for processing (2)
  • src/tool/subcommands/api_cmd/api_compare_tests.rs
  • src/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)

Comment thread src/tool/subcommands/api_cmd/api_compare_tests.rs Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

RPC requires calibnet RPC checks to run on CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQLite indexer

2 participants