Skip to content

refactor(shadow-indexer): stop persisting canonical blocks - #4624

Open
jowparks wants to merge 5 commits into
mainfrom
joe/shadow-indexer-block-number-key
Open

refactor(shadow-indexer): stop persisting canonical blocks#4624
jowparks wants to merge 5 commits into
mainfrom
joe/shadow-indexer-block-number-key

Conversation

@jowparks

@jowparks jowparks commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

The ExEx wrote a row for every committed block, but the shadow metrics reader only ever consumes reorged-out and reverted ones. It now persists just those.

No migration: rows are still written with reorged_out set, so the reader's existing filter keeps hiding the canonical rows earlier builds left behind. shadow_blocks is empty until the first reorg, so the reader bootstraps at the genesis cursor instead of the table tip.

PRIMARY KEY(number, hash) and the (updated_at, number, hash) cursor index stay as they are. Re-keying on number alone would collapse repeated reorgs at the same height into one row and undercount them.

jowparks and others added 2 commits August 21, 2026 15:37
…g canonical blocks

The shadow indexer persisted a row for every committed block, so canonical
rows dominated a table whose only consumer reads reorged-out blocks. Stop
writing them, and re-key `shadow_blocks` from `PRIMARY KEY(number, hash)` to
`PRIMARY KEY(number)`. Every remaining row is by definition a reorged-out
shadow block, which makes `reorged_out` and the `hash` cursor tie-breaker
dead weight; both are removed.

Migration 0004 creates the new table and swaps it in, renaming the old one to
`shadow_blocks_legacy` rather than dropping it so the change stays
recoverable. The copy lists `created_at`/`updated_at` explicitly to stop
`DEFAULT now()` from restamping retained rows and re-emitting stale stats.

Same-height reorgs now collapse under `ON CONFLICT (number)`, so the reader's
`blocks_inspected_total` becomes poll-timing dependent. `ShadowIndexerMetrics::reorged_blocks_total`
counts them at write time to preserve an exact figure.

The shadow-metrics readiness probe is widened to fail `/readyz` loudly on a
stale schema. Migration 0004 only drops columns, so the columns the reader
projects all exist pre-migration and a projection alone cannot tell the two
schemas apart.

Deploy shadow-indexer before shadow-metrics: the indexer applies the
migration at startup.

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…nd empty-table cursor boot

The end-to-end test asserted the old contract, waiting for canonical rows. It
now proves their absence across the whole poll window, guarded by a liveness
check that the writer applied its migrations, since an emptiness assertion
alone would also pass if the extension never started.

Two paths had no coverage at all. Migrating populated legacy data was untested
because every existing test provisions a fresh schema from empty; the new test
seeds a 0003-era table and asserts the surviving rows, including that
timestamps are preserved bit-for-bit. `max_cursor` returning `None` was nearly
unreachable while canonical rows existed and is now a normal first boot, so
the bootstrap test covers genesis boot, rows arriving after it, and backlog
skipping.

Reader fixtures move to distinct block numbers: under the number-only key,
rows sharing a number silently collapse and a test would assert against a
smaller set than it intends.

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

Comment thread crates/execution/shadow-indexer-db/migrations/0004_number_only_key.sql Outdated
Keying shadow_blocks by number alone was never required to stop
persisting canonical blocks, and it forced three breaking schema
changes: dropping reorged_out, narrowing the primary key, and dropping
shadow_metrics_cursor.last_hash. Each one breaks an old binary against
the new schema, which is why the change needed a strict
indexer-before-metrics deploy order and a readiness check to refuse
booting against the wrong half of the roll.

Reverting the schema leaves the actual improvement intact: the ExEx no
longer writes a row per committed block, it just marks every row it does
write as reorged out. The reader's existing reorged_out filter keeps
hiding canonical rows written by earlier builds, so old and new binaries
interoperate in both directions and rollback is an image revert.

Retaining the composite key also removes the same-height collapse, so
blocks_inspected_total is exact again rather than a poll-timing-
dependent lower bound.

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@jowparks jowparks changed the title Key shadow_blocks by block number and stop storing canonical blocks Stop persisting canonical blocks in the shadow indexer Aug 21, 2026
Metric emission belongs to shadow-metrics; the indexer had no metrics
dependency before this branch and should not gain one for a single
counter. The counter only existed to compensate for same-height reorgs
collapsing under the number-only key, which is no longer in the branch.

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@jowparks jowparks changed the title Stop persisting canonical blocks in the shadow indexer refactor(shadow-indexer): stop persisting canonical blocks Aug 21, 2026
@jowparks
jowparks marked this pull request as ready for review August 21, 2026 23:26
The reader bootstrap paths it covered are unchanged by this branch, so
the coverage belongs with a change to the reader rather than here.

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Review Summary

No issues found.

Reviewed: exex.rs, writer.rs, README.md, shadow_indexer.rs (E2E test).

The refactor cleanly removes canonical-block persistence from the shadow indexer ExEx. Key observations:

  • emit_canonical_blocks is fully removed; all former call sites (ChainCommitted handler, tail of handle_chain_reorged) now return Ok(true) directly.
  • build_row drops the reorged_out parameter and hardcodes it to true, with a clear comment explaining the column is retained for backwards compatibility with the reader's existing filter.
  • FinishedHeight emission is unaffected — it keys off notification.committed_chain() and should_emit_finished_height, both independent of row persistence.
  • The E2E test replaces the positive assertion (rows exist) with a negative one (rows stay empty), adding a to_regclass liveness check that proves the writer actually started and applied migrations, preventing a vacuously-passing empty-table assertion. Connecting via a raw PgPoolOptions instead of ShadowDbConfig::init_pool avoids the test itself applying migrations — good design.
  • Unit tests are updated to match the new invariant (only reorged/reverted blocks are emitted).
  • Not block-production-sensitive: the shadow indexer is an observability ExEx on shadow canary nodes and does not participate in payload assembly, finalization, or consensus.

@github-actions

Copy link
Copy Markdown
Contributor

Base Std historical fork tests

Fork Result Passed Failed Skipped base/base base-anvil base-std
Beryl pass 616 0 13 c4441ac2 8d0f5b8a 4658f1b7
Cobalt pass 721 0 14 c4441ac2 9df661bc e30b3421

View run

@github-actions

Copy link
Copy Markdown
Contributor

Caution

This PR may regress performance. 2 benchmark(s) slower by more than 10% beyond the noise band: batch_transaction_encoding/encode_in_place (+10.5%), execution/Open 1024 nodes - 65,536 nodes (+54.5%).

Benchmark results (advisory)

Median time on the PR head versus the base branch, measured on the same host. Wall-clock, so a change is only flagged when it clears ±10% and the confidence intervals do not overlap. Only benchmarks past the ±10% threshold (plus new or dropped ones) are listed. This check never blocks a merge.

Benchmark Base Head Δ median
batch_transaction_encoding/encode_in_place 131.06 µs 144.76 µs +10.5% ⚠️ slower
execution/Open 1024 nodes - 65,536 nodes 47.22 µs 72.95 µs +54.5% ⚠️ slower

47 benchmark(s) within ±10% omitted.

View run · Re-run benchmarks

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.

2 participants