feat(chainstate): indexer logic#2496
Conversation
Adds the indexer logic for the chainstate indexer
Codecov Report❌ Patch coverage is
❌ 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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.Indexerwith 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) |
There was a problem hiding this comment.
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.
| blsApkRegistryAddr, err = registryCoordinator.BlsApkRegistry(nil) | |
| blsApkRegistryAddr, err = registryCoordinator.BlsApkRegistry(&bind.CallOpts{Context: ctx}) |
| 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) |
There was a problem hiding this comment.
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.
| 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, | |
| ) |
| g2Point := &core.G2Point{G2Affine: &g2Affine} | ||
|
|
||
| // Convert operator address to operator ID using the contract | ||
| operatorID, err := i.registryCoordinator.GetOperatorId(nil, event.Operator) |
There was a problem hiding this comment.
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.
| operatorID, err := i.registryCoordinator.GetOperatorId(nil, event.Operator) | |
| operatorID, err := i.registryCoordinator.GetOperatorId(&bind.CallOpts{Context: ctx}, event.Operator) |
| 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) |
There was a problem hiding this comment.
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).
| 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) |
| 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) | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
tangentially couldn't you just construct a single RPC call that takes all three event signatures to downsize this to a single RPC call?
Adds the indexer logic for the chainstate indexer
Note: this is the 6th PR in a series of chainstate indexer PRs