Skip to content

feat(chainstate): indexer logic#2496

Open
iquidus wants to merge 1 commit intoindexer-json-persisterfrom
indexer-logic
Open

feat(chainstate): indexer logic#2496
iquidus wants to merge 1 commit intoindexer-json-persisterfrom
indexer-logic

Conversation

@iquidus
Copy link
Contributor

@iquidus iquidus commented Feb 6, 2026

Adds the indexer logic for the chainstate indexer

Note: this is the 6th PR in a series of chainstate indexer PRs

Adds the indexer logic for the chainstate indexer
@codecov
Copy link

codecov bot commented Feb 6, 2026

Codecov Report

❌ Patch coverage is 0% with 256 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.02%. Comparing base (c0d4f44) to head (84c2080).

Files with missing lines Patch % Lines
chainstate/indexer.go 0.00% 256 Missing ⚠️

❌ Your patch check has failed because the patch coverage (0.00%) is below the target coverage (50.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@                    Coverage Diff                     @@
##           indexer-json-persister    #2496      +/-   ##
==========================================================
- Coverage                   39.19%   39.02%   -0.17%     
==========================================================
  Files                         556      557       +1     
  Lines                       51399    51655     +256     
==========================================================
+ Hits                        20147    20160      +13     
- Misses                      28701    28950     +249     
+ Partials                     2551     2545       -6     
Flag Coverage Δ
litt-tests 32.99% <ø> (ø)
unit-tests 39.81% <0.00%> (-0.20%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an on-chain event indexer for the chainstate package that periodically polls Ethereum blocks, filters relevant contract events (RegistryCoordinator, BLSApkRegistry, EjectionManager), and persists derived operator state via the chainstate store/persister.

Changes:

  • Introduces chainstate.Indexer with startup, polling loop, and batch block indexing logic.
  • Implements event filtering/handling for operator registration/deregistration, socket updates, BLS pubkey registration, quorum membership changes, and ejections.
  • Wires persistence by loading snapshots on startup and periodically saving store state to JSON.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

// BLS APK Registry
blsApkRegistryAddr, err = registryCoordinator.BlsApkRegistry(nil)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

Contract calls are made with nil *bind.CallOpts (no context). This can ignore cancellation/timeouts and potentially hang during startup/shutdown; pass &bind.CallOpts{Context: ctx} (and BlockNumber if needed) instead of nil when calling BlsApkRegistry.

Suggested change
blsApkRegistryAddr, err = registryCoordinator.BlsApkRegistry(nil)
blsApkRegistryAddr, err = registryCoordinator.BlsApkRegistry(&bind.CallOpts{Context: ctx})

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +167
lastIndexed = i.config.StartBlockNumber
if lastIndexed == 0 {
// If no start block configured, start from current block
currentBlock, err := i.ethClient.BlockNumber(ctx)
if err != nil {
return fmt.Errorf("failed to get current block: %w", err)
}
lastIndexed = currentBlock
i.logger.Info("Starting indexing from current block", "block", lastIndexed)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

Start block handling appears off-by-one and contradicts the config semantics: when lastIndexed==0 you set lastIndexed=StartBlockNumber, which makes indexing begin at StartBlockNumber+1. If StartBlockNumber is intended to be the first block to index (per IndexerConfig docs), initialize lastIndexed to StartBlockNumber-1 (careful with 0) or set fromBlock directly. Also, the "Starting indexing from current block" log is misleading because the next indexed block will be currentBlock+1.

Suggested change
lastIndexed = i.config.StartBlockNumber
if lastIndexed == 0 {
// If no start block configured, start from current block
currentBlock, err := i.ethClient.BlockNumber(ctx)
if err != nil {
return fmt.Errorf("failed to get current block: %w", err)
}
lastIndexed = currentBlock
i.logger.Info("Starting indexing from current block", "block", lastIndexed)
if i.config.StartBlockNumber > 0 {
// StartBlockNumber is the first block to index; set lastIndexed just before it
lastIndexed = i.config.StartBlockNumber - 1
} else {
// If no start block configured, start from the block after the current head
currentBlock, err := i.ethClient.BlockNumber(ctx)
if err != nil {
return fmt.Errorf("failed to get current block: %w", err)
}
lastIndexed = currentBlock
i.logger.Info(
"Starting indexing from next block after current head",
"currentHead", currentBlock,
"firstBlock", currentBlock+1,
)

Copilot uses AI. Check for mistakes.
g2Point := &core.G2Point{G2Affine: &g2Affine}

// Convert operator address to operator ID using the contract
operatorID, err := i.registryCoordinator.GetOperatorId(nil, event.Operator)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

This contract read uses nil *bind.CallOpts, which drops the caller context and can block indefinitely if the RPC call stalls. Use &bind.CallOpts{Context: ctx} so cancellation propagates through indexing.

Suggested change
operatorID, err := i.registryCoordinator.GetOperatorId(nil, event.Operator)
operatorID, err := i.registryCoordinator.GetOperatorId(&bind.CallOpts{Context: ctx}, event.Operator)

Copilot uses AI. Check for mistakes.
return fmt.Errorf("failed to update operator pubkey: %w", err)
}

i.logger.Debug("Indexed BLS pubkey registration", "operator_id", fmt.Sprintf("%x", event.Operator), "block", event.Raw.BlockNumber)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The debug log labels the value as "operator_id" but formats event.Operator (address) instead of the resolved operatorID. This makes logs misleading when debugging pubkey indexing; log the operatorID (and optionally include the address as a separate field).

Suggested change
i.logger.Debug("Indexed BLS pubkey registration", "operator_id", fmt.Sprintf("%x", event.Operator), "block", event.Raw.BlockNumber)
i.logger.Debug("Indexed BLS pubkey registration", "operator_id", fmt.Sprintf("%x", operatorID), "operator", event.Operator, "block", event.Raw.BlockNumber)

Copilot uses AI. Check for mistakes.
Comment on lines +187 to +195
if err := i.indexRegistryCoordinatorEvents(ctx, fromBlock, toBlock); err != nil {
return fmt.Errorf("failed to index registry coordinator events: %w", err)
}
if err := i.indexBLSApkRegistryEvents(ctx, fromBlock, toBlock); err != nil {
return fmt.Errorf("failed to index BLS APK registry events: %w", err)
}
if err := i.indexEjectionManagerEvents(ctx, fromBlock, toBlock); err != nil {
return fmt.Errorf("failed to index ejection manager events: %w", err)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

is there any reason to do these operations sequentially vs parallel? iiuc the length of the RPC request processing timing is proportional to the block range you're providing meaning this could be time consuming in moments of a larger range

Copy link
Contributor

Choose a reason for hiding this comment

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

tangentially couldn't you just construct a single RPC call that takes all three event signatures to downsize this to a single RPC call?

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.

3 participants