Skip to content

fix(indexer): degrade-not-halt + self-repair the secondary index (network-wide db-corruption halt) - #147

Merged
arkadianet merged 7 commits into
mainfrom
fix/indexer-degraded-secondary-index
Jun 30, 2026
Merged

fix(indexer): degrade-not-halt + self-repair the secondary index (network-wide db-corruption halt)#147
arkadianet merged 7 commits into
mainfrom
fix/indexer-degraded-secondary-index

Conversation

@arkadianet

@arkadianet arkadianet commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

Every indexer-enabled node halts deterministically at a fixed mainnet height, reporting db-corruption. It is not corruption — the chain/UTXO state is intact. A latent bug in the derived (non-consensus) secondary index trips a strict consistency check and treats it as fatal. This PR makes the indexer degrade instead of halt, then self-repair to full correctness — so a node operator who just updates the binary and restarts recovers automatically, with no reindex.

Consensus, validation, and the chain database are untouched throughout — this only changes the derived indexer.redb secondary index and how it is read.

Root cause

The secondary index stores, per address/template/token, a segment of signed global-box-indices ±gi (sign = spent). Creation writes +gi; the spend sign-flips it to −gi. Two latent issues surface together on mainnet:

  1. Missing creation entry (the halt). One ancient box's +gi entry is absent from the shared template segment. Its first-ever spend lands at its storage-rent eligibility height, the indexer tries to flip a segment entry that was never appended, raises SegmentTopologyError, and halts as DbCorruption. Same shared code + same on-chain box ⇒ every indexer node stops at the same height.
  2. Legacy high-version opaque trees (surfaced during the fix). Some legacy boxes carry an ErgoTree whose header version exceeds the activated script version, stored size-delimited (opaque). The rebuild (and every index read) went through the strict consensus box reader, which hard-rejects those — re-validating consensus while reading the node's own already-validated stored data.

The fix (5 commits)

  1. degrade-not-halt — tolerate only the specific SegmentEntryMissing template/token drift (address index stays strict; structural errors still fatal); set a sticky INDEXER_META repair marker. The block still commits.
  2. self-repairing secondary index — a chain-free rebuild that wipes + re-derives the template/token segments from the intact primary tables (NUMERIC_BOXINDEXED_BOX), reusing the exact apply append/flip machinery so segments are byte-identical to a fresh linear index. Chunked, crash-safe (per-chunk checkpoint), runs at the top of the poll loop before any apply/rollback.
  3. lenient trusted reads — a VlqReader.trusted flag (default off; set only by deserialize_indexed_box, which only ever decodes the node's own consensus-validated stored rows) skips the box-script acceptance gates for the top-level tree and any tree nested in a register / SBox constant / spending-proof extension / size-delimited body. The structural parse is unchanged, so template-hash derivation is byte-identical. The strict consensus readers are byte-for-byte unchanged (gates run whenever !is_trusted()).
  4. rebuild robustness — (a) skip-not-halt fallback: a row that even the lenient reader can't decode (genuine corruption) is logged + counted + skipped so the rebuild always completes; (b) honest marker: a resume-safe persisted skip count, so a knowingly-incomplete rebuild is never logged as "fully repaired"; (c) interruptible rebuild: checks a cancel flag between chunks so a node drains promptly on SIGTERM and resumes from its checkpoint (previously it ignored SIGTERM for the whole multi-hour rebuild).
  5. chore(release): 0.5.0 — version bump + regenerated openapi snapshot.

Automatic recovery

A halted node simply updates the binary and restarts: it resumes from the last indexed block, re-attempts the failing block, tolerates the drift (degrade), sets the marker, then runs the rebuild to completion and catches up — no manual reindex, no operator steps.

Validation

  • Live on a mainnet archival node: reproduced the exact halt (box gi 17620319), confirmed automatic recovery, and ran the full 56M-box rebuild end-to-end. Graceful shutdown verified mid-rebuild (~14s drain).
  • Whole-workspace gate green: cargo clippy --all-targets --all-features -D warnings + cargo test --all (264 suites, 0 failures), cargo fmt --check.
  • Reviewed adversarially across multiple passes; final pass clean.

Test plan

New tests: real-box top-level lenient regression; nested SBox trusted-vs-strict; nested tree inside a size-delimited body trusted-vs-strict; skip-not-halt fallback end-to-end; cancel/resume; repair-marker stickiness; error-taxonomy. Strict consensus parsing is asserted unchanged (the strict reader still rejects the version-5 box).

Note: the end-to-end live rebuild on the validation node is finishing as this opens; flagged here for transparency.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a secondary-index repair and rebuild flow that can recover template and token data without stopping indexing.
    • Stored box data can now be read more leniently when it has already been validated, improving compatibility with legacy entries.
  • Bug Fixes

    • Indexing and rollback now better handle secondary-index drift instead of failing outright.
    • Improved detection and reporting for missing segment entries, with clearer error classification.
    • Rebuild progress is now safely tracked and resumable after interruption.
  • Chores

    • Updated the workspace and OpenAPI version to 0.5.0.

arkadianet and others added 5 commits June 30, 2026 16:41
The indexer hard-halts the WHOLE subsystem (reason=DbCorruption) when a
single sign-flip on a DERIVED secondary index (template/token box-segment)
finds no entry to flip. This deterministically freezes every node's
/blockchain/* the moment an ancient box (e.g. created 2022) is first spent
at its exact storage-rent eligibility height (creationHeight + 1_051_200):
its +gi creation entry is missing from the heavily-shared template segment
(present in the per-address segment), so the template spend-flip halts the
whole indexer. It reproduces identically on every node (fixed protocol
height, same canonical chain) and affects ANY indexer-enabled operator.
Consensus is unaffected — the indexer is a read-only, fully
reindex-reproducible consumer on a separate redb file.

Fix: split the flip-helper's "no entry of either sign present" case into a
distinct IndexerError::SegmentEntryMissing variant, and make the template
and token sign-flips on spend (apply) AND their unflips on rollback tolerate
ONLY that variant — log a loud WARN, bump a process-wide degraded counter
(`secondary_index_drift_skips()`), and skip rather than halt. Everything
else still propagates and halts, INCLUDING the sibling SegmentTopologyError
(double-flip / missing-spill / pop-mismatch) and DB decode/redb errors, so
genuine structural corruption is never masked. The PRIMARY address index
stays strict (SegmentEntryMissing there still halts, mapped to DbCorruption).
Rollback is covered so a reorg after a degraded apply does not re-halt. This
mirrors the reference node's ExtraIndexer (log-and-continue on a
findAndModBox miss).

Recovery is automatic: the halt is never persisted and the failed apply
never committed, so a node that updates to this binary and restarts resumes
from the last good height, re-applies the previously-fatal block (now
tolerated), and catches up to tip — no reindex or manual step. The index is
then degraded only for the affected box(es); a chain-free secondary-index
rebuild (follow-up) restores full correctness without a full reindex.

Follow-ups (not in this PR): surface the degraded count on the indexer
status API; add reorg-shape tests (boundary-cross from a head==512
pre-state, content-overlapping forks over a shared template, spend of a
deep-spill box post-reorg) to determine whether a live reorg path can still
create the gap vs. it being stale historical on-disk data; and an opt-in
chain-free rebuild that re-derives template/token segments from the intact
primary box table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds on the degrade-not-halt fix: instead of leaving the template/token
index permanently degraded (the affected box missing) after a tolerated
SegmentEntryMissing drift, the node now auto-repairs to FULL correctness on
update, with no operator action and without a full chain reindex.

Mechanism:
- Detect: when a secondary flip is skipped, apply/rollback set a sticky
  `secondary_repair_pending` marker in INDEXER_META, in the SAME write txn
  (durable iff the degraded block commits). It is NOT an IndexerMeta/UndoEntry
  field, so a reorg meta-restore can't clear it — sticky until a rebuild does.
- Repair (rebuild.rs): a chain-free rebuild re-derives template + token
  box-segments from the intact primary tables (NUMERIC_BOX + INDEXED_BOX, which
  retain spent boxes). Phase 0 wipes template/token segments (preserving token
  metadata + the shared ADDRESS spills); Phase 1 scans gi in order, REUSING the
  apply append/flip/flush path so segments are byte-identical to a fresh index.
  Chunked by gi with a per-chunk INDEXER_META checkpoint → bounded memory +
  crash-resumable; Phase 0 is idempotent and its completion gates Phase 1.
- Trigger (task.rs): at the caught-up transition, before flipping to CaughtUp
  (which opens the gated read API), the task runs/resumes the rebuild while
  status stays Syncing — so a half-rebuilt index is never served. Net: update
  the binary → catch up → repair → serve fully-correct, in ONE restart.

Consensus untouched (writes only indexer.redb secondary tables). Tests: an
integration test proves the rebuild reproduces template segments exactly
(incl. the spent flip), leaves the address index untouched, and clears the
marker; a unit test pins the marker set/read/clear + the sticky-across-
write_meta property.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ion opaque trees)

The chain-free rebuild (and every index-read path: by-id / by-address / by-template
materialization, apply, rollback) re-reads boxes from the intact primary INDEXED_BOX
table through the STRICT consensus box reader, which hard-rejects an ErgoTree whose
header version exceeds the activated script version (3) even when size-delimited
(opaque). A legacy mainnet box can carry such a tree (stored by an older node before
those gates existed; confirmed live: global index 5918565, header 0xcd = version 5),
so the rebuild halted with a spurious "db-corruption", and those rows were
un-materializable everywhere.

Re-validating consensus while reading the node's OWN already-validated stored data is
wrong. Add a `trusted` flag to `VlqReader` (default off; `.trusted()` / `set_trusted`):
when set, the four box-script acceptance gates are skipped — for the TOP-LEVEL tree and
for any tree NESTED in a register, `SBox` constant, or spending-proof context-extension
reached through the same reader. The flag rides on the reader, so registers and
context-extensions (read inline) inherit it automatically; the size-delimited ErgoTree
BODY sub-reader propagates it explicitly (`read_ergo_tree_tracking_wrap`). The full
structural `read_ergo_tree` parse still runs, so `ergo_tree_bytes` / template-hash
derivation are byte-identical.

`deserialize_indexed_box` is the ONLY production caller that sets `trusted` — every
INDEXED_BOX write originates from a typed, consensus-validated block transaction, never
untrusted bytes. The strict consensus readers are byte-for-byte unchanged (the gates run
whenever `!is_trusted()`). Regression tests cover the real top-level box, a nested SBox
constant, and a nested tree inside a size-delimited body (strict reject vs trusted accept).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rker, interruptible shutdown

Three robustness layers on the chain-free secondary-index rebuild, so it always completes,
never silently lies about completeness, and never wedges a node on shutdown:

- Skip-not-halt fallback: a primary row that even the trusted/lenient reader cannot decode
  (genuine corruption, not a tolerable high-version tree) is logged (gi+box_id), counted, and
  SKIPPED rather than halting the whole rebuild. Bounded strictly to DbDecode from the box
  read; a missing NUMERIC_BOX/INDEXED_BOX row is structural corruption and still halts.
  Skipping one box does not desync later segment math (entries are absolute gi; flips search
  by value).

- Honest completed-with-skips marker: a new INDEXER_META `secondary_repair_skipped` count,
  written with each chunk checkpoint (resume-safe accumulation) and reset at Phase-0 start. On
  completion the pending marker + checkpoint clear, but a non-zero skip count is KEPT as a
  durable "knowingly incomplete" signal and the rebuild WARNs instead of logging "fully
  repaired". Exposed via `IndexerStore::secondary_repair_skipped()`.

- Interruptible rebuild: the rebuild previously ran every chunk inside one `step()` call, so a
  node ignored SIGTERM for the entire multi-hour rebuild (a graceful stop timed out in
  testing). `rebuild_secondary_indexes_until(store, cancel)` now checks the cancel flag between
  Phase-1 chunks and Phase-0 wipe batches, returning early WITHOUT clearing the marker (the
  per-chunk checkpoint resumes it). `IndexerTask` threads the driver's cancel handle into the
  rebuild and, on cancel, does NOT fall through to reorg/forward-apply against a half-rebuilt
  index. Validated live: graceful stop now drains in ~14s mid-rebuild.

Tests: skip-fallback end-to-end (corrupt row skipped, recorded, rebuild completes) and
cancel/resume.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump the workspace version 0.4.3 -> 0.5.0 for the indexer self-repair release
(degrade-not-halt + chain-free secondary-index rebuild + lenient trusted stored-box
reads + rebuild robustness). Refresh Cargo.lock and regenerate the native OpenAPI
snapshot fixture, whose embedded info.version is sourced from CARGO_PKG_VERSION.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@arkadianet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d5b071ab-1e7a-4602-a346-dc1230e0e1d6

📥 Commits

Reviewing files that changed from the base of the PR and between a518d82 and a20b65b.

📒 Files selected for processing (6)
  • ergo-indexer/src/rebuild.rs
  • ergo-indexer/src/segment_buffer.rs
  • ergo-indexer/src/ser/boxes.rs
  • ergo-indexer/src/store/meta.rs
  • ergo-ser/src/ergo_tree.rs
  • ergo-ser/src/sigma_value.rs
📝 Walkthrough

Walkthrough

Adds a "degrade-not-halt" secondary index drift tolerance layer to the ergo-indexer: box-spend flips that find no segment entry now produce a new SegmentEntryMissing error, tolerated inline and recorded via a sticky secondary_repair_pending LMDB marker. A new rebuild module implements a two-phase, crash-safe rebuild from primary tables. VlqReader gains a trusted flag that suppresses consensus validation gates so stored legacy high-version ErgoTree boxes can be decoded during rebuild. Workspace version bumped to 0.5.0.

Changes

Trusted VlqReader for lenient stored-box decoding

Layer / File(s) Summary
VlqReader trusted flag API
ergo-primitives/src/reader.rs
Adds trusted: bool field docs and three public methods (trusted(), set_trusted(), is_trusted()) to VlqReader.
Consensus validation gated on trusted flag
ergo-ser/src/ergo_box.rs, ergo-ser/src/ergo_tree.rs, ergo-ser/src/sigma_value.rs
Gates check_tree_version_supported and related consensus checks behind !r.is_trusted() in read_ergo_box_candidate, read_ergo_tree_tracking_wrap, and skip_ergo_tree; propagates trust into size-delimited sub-readers; adds regression tests for strict-vs-trusted behavior.
deserialize_indexed_box uses trusted reader
ergo-indexer/src/ser/boxes.rs
Constructs VlqReader::new(bytes).trusted() in deserialize_indexed_box, adds explanatory comments, and adds a test decoding a legacy v5-tree INDEXED_BOX payload.

Secondary index drift tolerance and crash-safe rebuild

Layer / File(s) Summary
SegmentEntryMissing error variant and drift detection
ergo-indexer/src/error.rs, ergo-indexer/src/segment_buffer.rs, ergo-indexer/tests/error_taxonomy.rs
Adds SegmentEntryMissing { detail } to IndexerError (classified as DbCorruption); changes flip_helper to return SegmentEntryMissing for topology drift vs SegmentTopologyError for double-flip; adds tolerate_secondary_drift wrapper and process-wide secondary_index_drift_skips counter.
Secondary repair metadata persistence
ergo-indexer/src/store/meta.rs, ergo-indexer/src/store/mod.rs, ergo-indexer/src/store/segment.rs
Adds three INDEXER_META keys and read/write/clear helpers for the sticky pending marker, next_gi checkpoint, and skipped counter; exposes them as IndexerStore query methods; adds remove_spill helper.
Apply and rollback drift tolerance
ergo-indexer/src/apply.rs, ergo-indexer/src/rollback.rs
Wraps template and token secondary segment flips with tolerate_secondary_drift; tracks secondary_skipped per block; atomically writes the sticky repair pending marker when any flip was skipped.
Two-phase secondary index rebuild
ergo-indexer/src/rebuild.rs, ergo-indexer/src/lib.rs
Implements rebuild_secondary_indexes / rebuild_secondary_indexes_until: arms sticky marker, Phase 0 wipes derived template/token segments, Phase 1 re-derives them from NUMERIC_BOX/INDEXED_BOX chunk by chunk with checkpoint and cancellation support.
IndexerTask self-repair gate
ergo-indexer/src/task.rs
Adds cancel: Arc<AtomicBool> to IndexerTask; inserts a gate at the start of step() that runs rebuild_secondary_indexes_until when secondary_repair_pending, blocking reorg/apply until repair completes or is canceled.
Integration tests for rebuild
ergo-indexer/tests/rebuild.rs
Adds two integration tests verifying byte-exact template and token segment reproduction after rebuild, preservation of token metadata, and clearing of repair markers.

Version bump to 0.5.0

Layer / File(s) Summary
Workspace and API version bump
Cargo.toml, ergo-api/tests/fixtures/openapi_native.yaml
Bumps workspace package version and OpenAPI info.version from 0.4.3 to 0.5.0.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • arkadianet/ergo#120: Introduces check_tree_version_supported consensus rejection in ergo_box.rs, ergo_tree.rs, and sigma_value.rs—the exact same sites where this PR adds the is_trusted() gate to bypass those checks.
  • arkadianet/ergo#124: Modifies skip_ergo_tree's size-delimited nested SBox inner-tree handling in sigma_value.rs, the same code path where this PR adds the trusted-mode version-check bypass.
  • arkadianet/ergo#127: Modifies VlqReader and read_ergo_tree_tracking_wrap to propagate parser context into nested ErgoTree parsing, directly overlapping with this PR's trust-propagation change in the same functions.

Poem

🐇 Hop, hop through the segment maze,
Where missing entries earned old blame—
Now drift is tolerated, repair marker raised,
Phase 0 wipes, Phase 1 reclaims!
A trusted reader skips the gates,
And legacy boxes meet their fate… gently. 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the core change: secondary-index drift now degrades instead of halting, with self-repair added.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/indexer-degraded-secondary-index

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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: 2

🧹 Nitpick comments (4)
ergo-indexer/src/lib.rs (1)

34-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the rebuild module private if only the root API is supported.

Line 34 makes internal rebuild APIs externally reachable, while Line 50 already exposes the manual entrypoint. Prefer keeping the module private to avoid committing checkpoint/cancellation internals as public API.

Proposed change
-pub mod rebuild;
+mod rebuild;
🤖 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 `@ergo-indexer/src/lib.rs` at line 34, The rebuild module is being exported
publicly even though only the root API should be supported. Update the module
visibility in lib.rs so rebuild stays private, and keep the manual entrypoint
exposed through the existing root-level API instead; use the rebuild module name
and the root entrypoint export as the main places to adjust.
ergo-indexer/src/rebuild.rs (1)

668-722: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise a real Phase-1 checkpoint resume.

This test cancels before Phase 0 does any work, so it verifies the marker stays pending but not resume from secondary_repair_next_gi. Add a case that resumes from a committed checkpoint and asserts entries are not duplicated.

🤖 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 `@ergo-indexer/src/rebuild.rs` around lines 668 - 722, The current test only
cancels before any Phase-1 work, so it never exercises resuming from a committed
checkpoint in rebuild_secondary_indexes_until. Update the test to first let
rebuild progress far enough to commit a checkpoint in secondary_repair_next_gi,
then cancel and rerun to verify the rebuild resumes from that checkpoint rather
than starting over. Use the existing rebuild_secondary_indexes_until,
rebuild_secondary_indexes, and secondary_repair_pending/secondary_repair_next_gi
flow, and assert the template box entries remain exactly once without
duplication after the resume.
ergo-indexer/src/task.rs (1)

139-171: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Move the repair gate before the meta/tip snapshot.

rebuild_secondary_indexes_until can run for a long time, but meta and tip are captured before it. Move the read_meta() and committed_tip() calls below this gate so post-repair apply/rollback decisions use fresh state and the pending repair path stays chain-free.

Suggested structure
-        let meta = match store.read_meta() {
-            Ok(m) => m,
-            Err(e) => return IndexerPoll::Halted(e),
-        };
-
-        let tip = self.chain.committed_tip();
-
         // Self-repair gate — MUST run before the reorg + forward-apply paths.
         match store.secondary_repair_pending() {
             ...
         }
+
+        let meta = match store.read_meta() {
+            Ok(m) => m,
+            Err(e) => return IndexerPoll::Halted(e),
+        };
+
+        let tip = self.chain.committed_tip();
🤖 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 `@ergo-indexer/src/task.rs` around lines 139 - 171, The repair gate in the
polling flow is using stale chain state because `read_meta()` and
`committed_tip()` are captured before `secondary_repair_pending()` and
`rebuild_secondary_indexes_until()`. Move those snapshot reads to after the
repair gate in `IndexerTask::poll` so any post-repair reorg or forward-apply
decisions use fresh `meta`/`tip` values, and keep the chain-free rebuild path
isolated from the rest of the apply/rollback logic.
ergo-indexer/tests/rebuild.rs (1)

149-160: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Make these tests fail for a no-op rebuild.

Both tests snapshot an already-correct secondary segment and only assert after == before. Seed an actual broken/missing template or token segment before rebuild_secondary_indexes(&store) so the test proves the rebuild restores from primary tables.

Also applies to: 236-245

🤖 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 `@ergo-indexer/tests/rebuild.rs` around lines 149 - 160, The rebuild tests are
only comparing the secondary indexes before and after
`rebuild_secondary_indexes`, so they do not prove recovery from corruption. In
the affected test cases in `rebuild.rs`, use the existing `store`,
`read_template_box_entries`, `read_address_box_entries`, and
`rebuild_secondary_indexes` flow but first deliberately put the template/token
secondary segment into a broken or missing state before rebuilding, then assert
the rebuilt state matches the primary-table-derived expected entries rather than
the original snapshot.
🤖 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 `@ergo-indexer/src/segment_buffer.rs`:
- Around line 59-74: Clarify the semantics of the drift skip tracking in
segment_buffer.rs: `SECONDARY_DRIFT_SKIPS` and `secondary_index_drift_skips()`
represent a cumulative process-lifetime counter, not the current live degraded
state. Update the surrounding comment and any usage that treats
`secondary_index_drift_skips() > 0` as an active “queries may be missing”
indicator to instead rely on the durable pending marker or another live health
signal, and make sure the `secondary_index_drift_skips` symbol is documented as
historical/counting-only.

In `@ergo-indexer/src/store/meta.rs`:
- Around line 42-44: Update the checkpoint documentation in the chunk checkpoint
comment on the meta store type so it clearly distinguishes the `None` state from
stored `Some(0)`: `None` should mean Phase 0 wipe is still pending, while
`Some(0)` should mean the wipe has been committed and Phase 1 starts at GI 0.
Keep the wording aligned with the checkpoint handling in `meta.rs` and the
rebuild flow so the contract is unambiguous.

---

Nitpick comments:
In `@ergo-indexer/src/lib.rs`:
- Line 34: The rebuild module is being exported publicly even though only the
root API should be supported. Update the module visibility in lib.rs so rebuild
stays private, and keep the manual entrypoint exposed through the existing
root-level API instead; use the rebuild module name and the root entrypoint
export as the main places to adjust.

In `@ergo-indexer/src/rebuild.rs`:
- Around line 668-722: The current test only cancels before any Phase-1 work, so
it never exercises resuming from a committed checkpoint in
rebuild_secondary_indexes_until. Update the test to first let rebuild progress
far enough to commit a checkpoint in secondary_repair_next_gi, then cancel and
rerun to verify the rebuild resumes from that checkpoint rather than starting
over. Use the existing rebuild_secondary_indexes_until,
rebuild_secondary_indexes, and secondary_repair_pending/secondary_repair_next_gi
flow, and assert the template box entries remain exactly once without
duplication after the resume.

In `@ergo-indexer/src/task.rs`:
- Around line 139-171: The repair gate in the polling flow is using stale chain
state because `read_meta()` and `committed_tip()` are captured before
`secondary_repair_pending()` and `rebuild_secondary_indexes_until()`. Move those
snapshot reads to after the repair gate in `IndexerTask::poll` so any
post-repair reorg or forward-apply decisions use fresh `meta`/`tip` values, and
keep the chain-free rebuild path isolated from the rest of the apply/rollback
logic.

In `@ergo-indexer/tests/rebuild.rs`:
- Around line 149-160: The rebuild tests are only comparing the secondary
indexes before and after `rebuild_secondary_indexes`, so they do not prove
recovery from corruption. In the affected test cases in `rebuild.rs`, use the
existing `store`, `read_template_box_entries`, `read_address_box_entries`, and
`rebuild_secondary_indexes` flow but first deliberately put the template/token
secondary segment into a broken or missing state before rebuilding, then assert
the rebuilt state matches the primary-table-derived expected entries rather than
the original snapshot.
🪄 Autofix (Beta)

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

Review profile: CHILL

Plan: Pro

Run ID: 8d1fa660-a185-47dc-a4db-a6640e594464

📥 Commits

Reviewing files that changed from the base of the PR and between cd1cec4 and a518d82.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • Cargo.toml
  • ergo-api/tests/fixtures/openapi_native.yaml
  • ergo-indexer/src/apply.rs
  • ergo-indexer/src/error.rs
  • ergo-indexer/src/lib.rs
  • ergo-indexer/src/rebuild.rs
  • ergo-indexer/src/rollback.rs
  • ergo-indexer/src/segment_buffer.rs
  • ergo-indexer/src/ser/boxes.rs
  • ergo-indexer/src/store/meta.rs
  • ergo-indexer/src/store/mod.rs
  • ergo-indexer/src/store/segment.rs
  • ergo-indexer/src/task.rs
  • ergo-indexer/tests/error_taxonomy.rs
  • ergo-indexer/tests/rebuild.rs
  • ergo-primitives/src/reader.rs
  • ergo-ser/src/ergo_box.rs
  • ergo-ser/src/ergo_tree.rs
  • ergo-ser/src/sigma_value.rs

Comment thread ergo-indexer/src/segment_buffer.rs Outdated
Comment thread ergo-indexer/src/store/meta.rs Outdated
…ics (review feedback)

Two doc-accuracy fixes from automated review:
- `SECONDARY_DRIFT_SKIPS` / `secondary_index_drift_skips()` is a CUMULATIVE
  process-lifetime diagnostic counter, NOT a live "index degraded" bit. A
  successful in-process self-repair rebuild clears the real degradation but does
  not reset this counter, so a non-zero value can outlive the degradation it
  counted. Point readers at the durable `secondary_repair_pending` /
  `secondary_repair_skipped` markers for live state.
- `secondary_repair_next_gi`: ABSENT (`None`) and a stored `0` (`Some(0)`) are
  DISTINCT states the rebuild branches on — absent = Phase-0 wipe not yet
  committed (wipe must run); `0` = wipe committed, Phase 1 resumes from the first
  global index. Document the distinction (the old "Absent / 0" conflated them).

Doc comments only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@arkadianet

Copy link
Copy Markdown
Owner Author

Thanks @coderabbitai — both doc-accuracy nits addressed in 8d8c4bf:

  • segment_buffer.rs drift-skip counter: reworded as a cumulative process-lifetime diagnostic, explicitly NOT a live "degraded" bit (a successful self-repair does not reset it). Readers are pointed at the durable secondary_repair_pending / secondary_repair_skipped markers for live state.
  • meta.rs checkpoint: documented None (Phase-0 wipe not yet committed) vs stored 0 (Some(0) — wipe committed, Phase 1 resumes from GI 0) as the distinct states the rebuild branches on.

Doc comments only, no behavior change.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@arkadianet Thanks — that addresses my doc-accuracy concerns.

The clarified wording around secondary_index_drift_skips() as a cumulative process-lifetime diagnostic, plus the explicit None vs Some(0) checkpoint semantics, makes the intended behavior much clearer. 🐇


✏️ Learnings added
Learnt from: arkadianet
URL: https://github.com/arkadianet/ergo/pull/147

Timestamp: 2026-06-30T11:31:51.953Z
Learning: In `ergo-indexer/src/segment_buffer.rs`, `secondary_index_drift_skips()` is documented as a cumulative process-lifetime diagnostic counter, not a live degraded-state indicator; current repair/degradation state should instead be read from the durable metadata markers `secondary_repair_pending` and `secondary_repair_skipped`.

Learnt from: arkadianet
URL: https://github.com/arkadianet/ergo/pull/147

Timestamp: 2026-06-30T11:31:51.953Z
Learning: In `ergo-indexer/src/store/meta.rs`, the rebuild checkpoint semantics distinguish `None` for `secondary_repair_next_gi` (Phase 0 wipe not yet committed) from `Some(0)` / stored `0` (Phase 0 wipe committed and Phase 1 resumes from global index 0).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

rustfmt wrapping for the new code added in this branch (test `.expect()` chains,
`assert!` macros, and the meta-key insert). No behavior change. CI runs
`cargo fmt --all -- --check`, which this satisfies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@arkadianet
arkadianet merged commit d599867 into main Jun 30, 2026
8 checks passed
@arkadianet
arkadianet deleted the fix/indexer-degraded-secondary-index branch June 30, 2026 17:11
arkadianet added a commit that referenced this pull request Jun 30, 2026
…rds #146) (#148)

Promote the [Unreleased] block to a [0.5.0] - 2026-07-01 section and add the two
merged changes that lacked changelog entries: the indexer degrade-not-halt +
self-repair fix (#147) and the native wallet retrieve-rewards sweep (#146). The
v0.5.0 tag was already cut from main; this brings the changelog in line.

Co-authored-by: arkadianet <rkadias@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
arkadianet pushed a commit that referenced this pull request Jul 2, 2026
Phase 2A of the first-class UI effort: the node's health machinery becomes
observable, and the Charts view graduates from session sparklines to real
server-history charts.

Indexer health endpoint:
- New defaulted `IndexerQuery::health()` (ergo-indexer-types) returning
  IndexerHealthDto: the durable self-repair markers from the #147 incident
  machinery (repair_pending / repair_next_gi rebuild cursor /
  repair_skipped honest marker), the process-lifetime drift-skip counter,
  and the global box/tx totals. IndexerHandle overrides it with best-effort
  store reads (a degraded store degrades a field, never errors the
  snapshot); stubs inherit the healthy default.
- New `GET /api/v1/indexer/status` (utoipa-documented, native openapi
  snapshot regenerated): always-200 and never status-gated — like
  indexedHeight it must answer while syncing, repairing, or halted.
  `/blockchain/indexedHeight` stays pinned to its Scala-parity shape; the
  repair/totals superset lives here. 404 on indexer-less wiring = disabled.
  Pinned by a new 4-case endpoint test.

Overview integration:
- The Sync-pipeline panel now surfaces index health, silent when healthy:
  a progress row while a rebuild runs (cursor/total boxes), a queued row
  during the wipe phase, a "done · N box(es) skipped" honest-marker row
  after a knowingly-incomplete repair, and a red halt row with the reason.

Real charts (new zero-dep chart.js — axis-labelled SVG line/area + bar
histogram with pointer readouts; text lives in HTML around the stretched
SVG so nothing distorts; data via textContent only):
- Difficulty (last 720 blocks) from /api/v1/difficulty/history — the
  string-typed difficulty parsed once per point.
- Estimated hashrate per point (difficulty / target interval — the same
  derivation as the KPI band).
- Block-interval distribution histogram from the same series' timestamp
  deltas (<30s … >10m bins).
- Mempool age histogram from /transactions/poolHistogram (waiting tx by
  wait-time bucket).
Charts build once and update in place (hover state survives the 4s tick);
the series refetches only when the tip height advances.

Verified against live mainnet data via the dev proxy (real difficulty
epoch steps, 2-minute-modal interval distribution, live pool histogram).
fmt/clippy/cargo test --all green; openapi snapshot + runtime-mount +
header tests updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
arkadianet added a commit that referenced this pull request Jul 2, 2026
… real charts (#151)

* feat(ui): node-health surface — indexer self-repair status + real charts

Phase 2A of the first-class UI effort: the node's health machinery becomes
observable, and the Charts view graduates from session sparklines to real
server-history charts.

Indexer health endpoint:
- New defaulted `IndexerQuery::health()` (ergo-indexer-types) returning
  IndexerHealthDto: the durable self-repair markers from the #147 incident
  machinery (repair_pending / repair_next_gi rebuild cursor /
  repair_skipped honest marker), the process-lifetime drift-skip counter,
  and the global box/tx totals. IndexerHandle overrides it with best-effort
  store reads (a degraded store degrades a field, never errors the
  snapshot); stubs inherit the healthy default.
- New `GET /api/v1/indexer/status` (utoipa-documented, native openapi
  snapshot regenerated): always-200 and never status-gated — like
  indexedHeight it must answer while syncing, repairing, or halted.
  `/blockchain/indexedHeight` stays pinned to its Scala-parity shape; the
  repair/totals superset lives here. 404 on indexer-less wiring = disabled.
  Pinned by a new 4-case endpoint test.

Overview integration:
- The Sync-pipeline panel now surfaces index health, silent when healthy:
  a progress row while a rebuild runs (cursor/total boxes), a queued row
  during the wipe phase, a "done · N box(es) skipped" honest-marker row
  after a knowingly-incomplete repair, and a red halt row with the reason.

Real charts (new zero-dep chart.js — axis-labelled SVG line/area + bar
histogram with pointer readouts; text lives in HTML around the stretched
SVG so nothing distorts; data via textContent only):
- Difficulty (last 720 blocks) from /api/v1/difficulty/history — the
  string-typed difficulty parsed once per point.
- Estimated hashrate per point (difficulty / target interval — the same
  derivation as the KPI band).
- Block-interval distribution histogram from the same series' timestamp
  deltas (<30s … >10m bins).
- Mempool age histogram from /transactions/poolHistogram (waiting tx by
  wait-time bucket).
Charts build once and update in place (hover state survives the 4s tick);
the series refetches only when the tip height advances.

Verified against live mainnet data via the dev proxy (real difficulty
epoch steps, 2-minute-modal interval distribution, live pool histogram).
fmt/clippy/cargo test --all green; openapi snapshot + runtime-mount +
header tests updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): clamp bar height before deriving y (CodeRabbit)

For 0 < raw-height < 2 the bar rect's y was computed from the raw height
while the height itself was clamped to the 2-unit minimum, pushing the
bottom edge past the baseline (masked by viewBox clipping). Clamp first,
derive y from the clamped value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: arkadianet <rkadias@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant