diff --git a/internal/worker/regular.go b/internal/worker/regular.go index ec93d59..c40e07d 100644 --- a/internal/worker/regular.go +++ b/internal/worker/regular.go @@ -190,8 +190,9 @@ func (rw *RegularWorker) commitProgress(lastSuccess uint64, lastSuccessHash stri // determineStartingBlock picks a start block from the last indexed block and the // chain head. GetLatestBlock returns (0, nil) for a cold start, so a non-nil -// kvErr means the store is genuinely down. When there is no prior block to -// resume from, the chain head is the only safe anchor and we wait for it. +// kvErr means the store is genuinely down. On a cold start with a healthy store, +// a configured start_block (> 0) is used as the anchor; otherwise the chain head +// is the only safe anchor and we wait for it. func (rw *RegularWorker) determineStartingBlock() uint64 { kvLatest, kvErr := rw.blockStore.GetLatestBlock(rw.chain.GetNetworkInternalCode()) @@ -217,7 +218,21 @@ func (rw *RegularWorker) determineStartingBlock() uint64 { if kvErr != nil { rw.logger.Error("Block store unavailable, starting from chain head", "chain", rw.chain.GetName(), "error", kvErr) + return rw.waitForChainHead() } + + // Cold start with a healthy store and no prior block: honor the configured + // start_block so operators can backfill from a chosen height. The regular + // loop then walks forward from here to the chain head. A down store falls + // through to the chain head above, so a broken store never triggers a + // re-backfill from start_block on every restart. + if rw.config.StartBlock > 0 { + start := uint64(rw.config.StartBlock) + rw.logger.Info("Cold start from configured start_block", + "chain", rw.chain.GetName(), "start_block", start) + return start + } + return rw.waitForChainHead() } diff --git a/internal/worker/regular_test.go b/internal/worker/regular_test.go index dad74de..cd8d088 100644 --- a/internal/worker/regular_test.go +++ b/internal/worker/regular_test.go @@ -197,6 +197,45 @@ func TestRegularWorkerDetermineStartingBlockColdStartChainUp(t *testing.T) { require.Equal(t, uint64(500), rw.determineStartingBlock()) } +func TestRegularWorkerDetermineStartingBlockColdStartUsesConfiguredStartBlock(t *testing.T) { + t.Parallel() + + // Cold start with a healthy store and a configured start_block: anchor on it + // rather than the chain head, so a backfill begins from the chosen height. + chain := &stubIndexer{name: "ethereum", internalCode: "ETH", networkType: enum.NetworkTypeEVM, latest: 500} + store := &stubBlockStore{latestBlock: 0} + rw := newTestRegularWorker(chain, store, 0, 2) + rw.config.StartBlock = 100 + + require.Equal(t, uint64(100), rw.determineStartingBlock()) +} + +func TestRegularWorkerDetermineStartingBlockResumeIgnoresStartBlock(t *testing.T) { + t.Parallel() + + // A prior KV checkpoint always wins over start_block: resume from the head + // (queuing catchup for the gap), never rewind to the configured start_block. + chain := &stubIndexer{name: "ethereum", internalCode: "ETH", networkType: enum.NetworkTypeEVM, latest: 500} + store := &stubBlockStore{latestBlock: 300} + rw := newTestRegularWorker(chain, store, 300, 2) + rw.config.StartBlock = 100 + + require.Equal(t, uint64(500), rw.determineStartingBlock()) +} + +func TestRegularWorkerDetermineStartingBlockStoreDownIgnoresStartBlock(t *testing.T) { + t.Parallel() + + // Store down: fall back to the chain head even when start_block is set, so a + // broken store never re-triggers a backfill from start_block on each restart. + chain := &stubIndexer{name: "ethereum", internalCode: "ETH", networkType: enum.NetworkTypeEVM, latest: 500} + store := &stubBlockStore{getLatestBlockErr: errors.New("redis down")} + rw := newTestRegularWorker(chain, store, 0, 2) + rw.config.StartBlock = 100 + + require.Equal(t, uint64(500), rw.determineStartingBlock()) +} + func TestRegularWorkerDetermineStartingBlockChainCancelledReturnsZero(t *testing.T) { t.Parallel()