From 92d940c798d3567a14085a86c0f7eab381dc1c57 Mon Sep 17 00:00:00 2001 From: kamuikatsurgi Date: Mon, 16 Feb 2026 20:49:50 +0530 Subject: [PATCH 1/6] exp: disable verifyPendingHeaders --- core/blockchain.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 2563e3f19a..1f40888de3 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -574,11 +574,6 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, } } } - // The first thing the node will do is reconstruct the verification data for - // the head block (ethash cache or clique voting snapshot). Might as well do - // it in advance. - // BOR - commented out intentionally - // bc.engine.VerifyHeader(bc, bc.CurrentHeader()) // Check the current state of the block hashes and make sure that we do not have any of the bad blocks in our chain for hash := range BadHashes { @@ -4233,6 +4228,8 @@ func (bc *BlockChain) startHeaderVerificationLoop() { // verifyPendingHeaders checks headers after the latest finalized block // and rewinds the chain if invalid headers are found. func (bc *BlockChain) verifyPendingHeaders() { + return + // Get the latest finalized block hasMilestone, milestoneNumber, _ := bc.checker.GetWhitelistedMilestone() if !hasMilestone { From 51b2ae2d41743feee0b8b743cd80079ab807f904 Mon Sep 17 00:00:00 2001 From: kamuikatsurgi Date: Tue, 17 Feb 2026 12:07:06 +0530 Subject: [PATCH 2/6] fix: cap verify pending headers at default span length + 1 --- consensus/bor/bor.go | 2 +- core/blockchain.go | 20 +++++++++++++++----- params/config.go | 7 ++++++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index 7ef3a1e2da..0ff39cdc87 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -52,7 +52,7 @@ import ( ) const ( - defaultSpanLength = 6400 // Default span length i.e. number of bor blocks in a span + defaultSpanLength = params.DefaultSpanLength zerothSpanEnd = 255 // End block of 0th span checkpointInterval = 1024 // Number of blocks after which to save the vote snapshot to the database inmemorySnapshots = 128 // Number of recent vote snapshots to keep in memory diff --git a/core/blockchain.go b/core/blockchain.go index 1f40888de3..b94ae61a15 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -4227,9 +4227,9 @@ func (bc *BlockChain) startHeaderVerificationLoop() { // verifyPendingHeaders checks headers after the latest finalized block // and rewinds the chain if invalid headers are found. +// Verification is capped to params.DefaultSpanLength + 1 blocks from the current head. +// This covers atleast 2 spans for max reorg protection without unbounded memory growth. func (bc *BlockChain) verifyPendingHeaders() { - return - // Get the latest finalized block hasMilestone, milestoneNumber, _ := bc.checker.GetWhitelistedMilestone() if !hasMilestone { @@ -4248,9 +4248,19 @@ func (bc *BlockChain) verifyPendingHeaders() { return // Rio is not enabled yet } - // Collect headers from finalized block + 1 to current head - var headers []*types.Header - for i := milestoneNumber + 1; i <= currentHead.Number.Uint64(); i++ { + // Cap verification window to span_duration + 1 blocks from the head. + // This covers 2 spans reorg depth, preventing unbounded memory growth and + // header reads when the gap between milestone and head is large. + startBlock := milestoneNumber + 1 + headNumber := currentHead.Number.Uint64() + + if headNumber > params.DefaultSpanLength && headNumber-params.DefaultSpanLength > startBlock { + startBlock = headNumber - params.DefaultSpanLength + } + + // Collect headers from startBlock to current head + headers := make([]*types.Header, 0, headNumber-startBlock+1) + for i := startBlock; i <= headNumber; i++ { header := bc.GetHeaderByNumber(i) if header == nil { log.Debug("Missing header during verification", "number", i) diff --git a/params/config.go b/params/config.go index 6e71f8dbdb..b6db170b3f 100644 --- a/params/config.go +++ b/params/config.go @@ -901,6 +901,11 @@ type BlockRangeOverrideValidatorSet struct { Validators []common.Address `json:"validators"` } +// DefaultSpanLength is the number of bor blocks in a span. This must match +// heimdall-v2's bor module Params.span_duration to ensure reorg protection +// boundaries stay consistent between the execution and consensus layers. +const DefaultSpanLength = 6400 + // BorConfig is the consensus engine configs for Matic bor based sealing. type BorConfig struct { Period map[string]uint64 `json:"period"` // Number of seconds between blocks to enforce @@ -1142,7 +1147,7 @@ func (c *ChainConfig) Description() string { banner += fmt.Sprintf(" - Lisovo: #%-8v\n", c.Bor.LisovoBlock) } if c.Bor.LisovoProBlock != nil { - banner += fmt.Sprintf(" - Lisovo Pro: #%-8v\n", c.Bor.LisovoProBlock) + banner += fmt.Sprintf(" - Lisovo Pro: #%-8v\n", c.Bor.LisovoProBlock) } return banner } From 47598ef803e88249c762efaeea414922e83fdc1b Mon Sep 17 00:00:00 2001 From: kamuikatsurgi Date: Tue, 17 Feb 2026 12:13:53 +0530 Subject: [PATCH 3/6] fix: add reorg metrics in verify pending headers --- core/blockchain.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/blockchain.go b/core/blockchain.go index b94ae61a15..715b042687 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -4292,10 +4292,16 @@ func (bc *BlockChain) verifyPendingHeaders() { "number", header.Number.Uint64(), "hash", header.Hash(), "err", err) // Rewind to the last valid block if lastValidNumber < currentHead.Number.Uint64() { + dropCount := int64(currentHead.Number.Uint64() - lastValidNumber) + log.Warn("Rewinding chain due to invalid header", - "from", currentHead.Number.Uint64(), "to", lastValidNumber) + "from", currentHead.Number.Uint64(), "to", lastValidNumber, "drop", dropCount) + if err := bc.SetHead(lastValidNumber); err != nil { log.Error("Failed to rewind chain", "err", err) + } else { + blockReorgMeter.Mark(1) + blockReorgDropMeter.Mark(dropCount) } } return From c83863d677f8b3ec69dc3c366d438c44753eaa55 Mon Sep 17 00:00:00 2001 From: kamuikatsurgi Date: Tue, 17 Feb 2026 14:52:02 +0530 Subject: [PATCH 4/6] chore: add tests --- core/blockchain_test.go | 165 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 7dc9a39635..a521c487c6 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -5138,6 +5138,171 @@ func TestHeaderVerificationWithNilChecker(t *testing.T) { } } +// headerCountingEngine wraps ethash and records how many headers VerifyHeaders receives. +type headerCountingEngine struct { + *ethash.Ethash + headersVerified atomic.Int64 +} + +func (m *headerCountingEngine) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) (chan<- struct{}, <-chan error) { + m.headersVerified.Store(int64(len(headers))) + return m.Ethash.VerifyHeaders(chain, headers) +} + +// TestVerifyPendingHeadersCapBoundary tests that verifyPendingHeaders caps the +// verification window to DefaultSpanLength + 1 headers from the current head. +func TestVerifyPendingHeadersCapBoundary(t *testing.T) { + t.Run("GapSmallerThanCap", func(t *testing.T) { + engine := &headerCountingEngine{Ethash: ethash.NewFaker()} + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } + genesis := &Genesis{ + Config: &config, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + mockValidator := &mockChainValidator{ + hasMilestone: true, + milestoneNumber: 3, + milestoneHash: common.HexToHash("0x123"), + } + + _, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, 20, nil) + + cfg := DefaultConfig() + cfg.Checker = mockValidator + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) + if err != nil { + t.Fatalf("failed to create blockchain: %v", err) + } + defer chain.Stop() + + if _, err := chain.InsertChain(blocks, false); err != nil { + t.Fatalf("failed to insert chain: %v", err) + } + + chain.verifyPendingHeaders() + + // Gap is 20 - 3 = 17, which is less than DefaultSpanLength (6400). + // All 17 headers (blocks 4-20) should be verified. + if got := engine.headersVerified.Load(); got != 17 { + t.Errorf("expected 17 headers verified (no cap), got %d", got) + } + }) + + t.Run("GapLargerThanCap", func(t *testing.T) { + engine := &headerCountingEngine{Ethash: ethash.NewFaker()} + chainLength := int(params.DefaultSpanLength) + 100 + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } + genesis := &Genesis{ + Config: &config, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + mockValidator := &mockChainValidator{ + hasMilestone: true, + milestoneNumber: 0, + milestoneHash: common.HexToHash("0x123"), + } + + _, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, chainLength, nil) + + cfg := DefaultConfig() + cfg.Checker = mockValidator + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) + if err != nil { + t.Fatalf("failed to create blockchain: %v", err) + } + defer chain.Stop() + + if _, err := chain.InsertChain(blocks, false); err != nil { + t.Fatalf("failed to insert chain: %v", err) + } + + chain.verifyPendingHeaders() + + // Gap is 6500 - 0 = 6500, which exceeds DefaultSpanLength (6400). + // Only DefaultSpanLength + 1 = 6401 headers should be verified. + expected := int64(params.DefaultSpanLength + 1) + if got := engine.headersVerified.Load(); got != expected { + t.Errorf("expected %d headers verified (capped), got %d", expected, got) + } + }) +} + +// TestVerifyPendingHeadersReorgMetrics tests that reorg metrics are recorded +// when verifyPendingHeaders rewinds the chain due to an invalid header. +func TestVerifyPendingHeadersReorgMetrics(t *testing.T) { + failingHeaders := map[uint64]bool{6: true} + engine := &mockFailingEngine{ + Ethash: ethash.NewFaker(), + shouldFailHeader: failingHeaders, + allowInitialInsertion: true, + } + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } + genesis := &Genesis{ + Config: &config, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + mockValidator := &mockChainValidator{ + hasMilestone: true, + milestoneNumber: 3, + milestoneHash: common.HexToHash("0x123"), + } + + _, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, 8, nil) + + cfg := DefaultConfig() + cfg.Checker = mockValidator + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) + if err != nil { + t.Fatalf("failed to create blockchain: %v", err) + } + defer chain.Stop() + + if _, err := chain.InsertChain(blocks, false); err != nil { + t.Fatalf("failed to insert chain: %v", err) + } + + engine.markInsertionComplete() + + // Snapshot metrics before + reorgCountBefore := blockReorgMeter.Snapshot().Count() + reorgDropBefore := blockReorgDropMeter.Snapshot().Count() + + chain.verifyPendingHeaders() + + // Chain should have rewound to block 5 + newHead := chain.CurrentBlock().Number.Uint64() + if newHead != 5 { + t.Errorf("expected head to rewind to 5, got %d", newHead) + } + + // Reorg execute meter should have incremented by 1 + reorgCountAfter := blockReorgMeter.Snapshot().Count() + if reorgCountAfter-reorgCountBefore != 1 { + t.Errorf("expected blockReorgMeter to increment by 1, got %d", reorgCountAfter-reorgCountBefore) + } + + // Reorg drop meter should have incremented by 3 (dropped blocks 6, 7, 8) + reorgDropAfter := blockReorgDropMeter.Snapshot().Count() + if reorgDropAfter-reorgDropBefore != 3 { + t.Errorf("expected blockReorgDropMeter to increment by 3, got %d", reorgDropAfter-reorgDropBefore) + } +} + // TestEIP7702 deploys two delegation designations and calls them. It writes one // value to storage which is verified after. func TestEIP7702(t *testing.T) { From b25837603546f6366299b06e639743d11eaebe5d Mon Sep 17 00:00:00 2001 From: kamuikatsurgi Date: Wed, 18 Feb 2026 11:14:41 +0530 Subject: [PATCH 5/6] refactor: use milestone fetcher and milestone's end block as the start block --- core/blockchain.go | 99 ++++++++++++++++---------------- core/blockchain_test.go | 123 ++++++++++++++++++++++++---------------- eth/backend.go | 11 ++++ 3 files changed, 134 insertions(+), 99 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 715b042687..2863519b74 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -228,7 +228,12 @@ type BlockChainConfig struct { // This defines the cutoff block for history expiry. // Blocks before this number may be unavailable in the chain database. HistoryPruningCutoff uint64 - Stateless bool // Whether the node is in stateless mode + + // Whether the node is in stateless mode or not. + Stateless bool + + // MilestoneFetcher returns the latest milestone end block from Heimdall. + MilestoneFetcher func(ctx context.Context) (uint64, error) } // DefaultConfig returns the default config. @@ -390,14 +395,14 @@ type BlockChain struct { stateSizer *state.SizeTracker // State size tracking // Bor related changes - borReceiptsCache *lru.Cache[common.Hash, *types.Receipt] // Cache for the most recent bor receipt receipts per block - stateSyncMu sync.RWMutex // Mutex to protect the stateSyncData access - borReceiptsRLPCache *lru.Cache[common.Hash, rlp.RawValue] // Cache for the most recent bor receipt RLPs per block - stateSyncData []*types.StateSyncData // State sync data - stateSyncFeed event.Feed // State sync feed - chain2HeadFeed event.Feed // Reorg/NewHead/Fork data feed - chainSideFeed event.Feed // Side chain data feed (removed from geth but needed in bor) - checker ethereum.ChainValidator + borReceiptsCache *lru.Cache[common.Hash, *types.Receipt] // Cache for the most recent bor receipt receipts per block + stateSyncMu sync.RWMutex // Mutex to protect the stateSyncData access + borReceiptsRLPCache *lru.Cache[common.Hash, rlp.RawValue] // Cache for the most recent bor receipt RLPs per block + stateSyncData []*types.StateSyncData // State sync data + stateSyncFeed event.Feed // State sync feed + chain2HeadFeed event.Feed // Reorg/NewHead/Fork data feed + chainSideFeed event.Feed // Side chain data feed (removed from geth but needed in bor) + milestoneFetcher func(ctx context.Context) (uint64, error) // Function to fetch the latest milestone end block from Heimdall. } // NewBlockChain returns a fully initialised block chain using information @@ -452,7 +457,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, borReceiptsCache: lru.NewCache[common.Hash, *types.Receipt](receiptsCacheLimit), borReceiptsRLPCache: lru.NewCache[common.Hash, rlp.RawValue](receiptsCacheLimit), logger: cfg.VmConfig.Tracer, - checker: cfg.Checker, + milestoneFetcher: cfg.MilestoneFetcher, } bc.hc, err = NewHeaderChain(db, chainConfig, engine, bc.insertStopped) @@ -4200,9 +4205,9 @@ func (bc *BlockChain) ProcessBlockWithWitnesses(block *types.Block, witness *sta // verifies headers after the latest finalized block and rewinds the chain if // invalid headers are detected. func (bc *BlockChain) startHeaderVerificationLoop() { - if bc.checker == nil { - log.Warn("chain validator service is not set, skipping header verification loop") - return // No checker available + if bc.milestoneFetcher == nil { + log.Warn("milestone fetcher is not set, skipping header verification loop") + return } bc.wg.Add(1) @@ -4211,7 +4216,7 @@ func (bc *BlockChain) startHeaderVerificationLoop() { ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() - log.Info("Started header verification loop") + log.Info("Starting header verification loop") for { select { @@ -4225,63 +4230,57 @@ func (bc *BlockChain) startHeaderVerificationLoop() { }() } -// verifyPendingHeaders checks headers after the latest finalized block -// and rewinds the chain if invalid headers are found. -// Verification is capped to params.DefaultSpanLength + 1 blocks from the current head. -// This covers atleast 2 spans for max reorg protection without unbounded memory growth. +// verifyPendingHeaders fetches the latest milestone from Heimdall and verifies +// all headers between that milestone's end block and the current chain head. If an invalid +// header is found, the chain is rewound to the last valid block. func (bc *BlockChain) verifyPendingHeaders() { - // Get the latest finalized block - hasMilestone, milestoneNumber, _ := bc.checker.GetWhitelistedMilestone() - if !hasMilestone { - return // No finalized block yet - } - currentHead := bc.CurrentBlock() - if currentHead.Number.Uint64() <= milestoneNumber { - return // Nothing to verify - } chainConfig := bc.Config() - - // We don't need to verify headers before Rio if chainConfig.Bor == nil || !chainConfig.Bor.IsRio(currentHead.Number) { - return // Rio is not enabled yet + return } - // Cap verification window to span_duration + 1 blocks from the head. - // This covers 2 spans reorg depth, preventing unbounded memory growth and - // header reads when the gap between milestone and head is large. - startBlock := milestoneNumber + 1 - headNumber := currentHead.Number.Uint64() + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() - if headNumber > params.DefaultSpanLength && headNumber-params.DefaultSpanLength > startBlock { - startBlock = headNumber - params.DefaultSpanLength + milestoneEndBlock, err := bc.milestoneFetcher(ctx) + if err != nil { + log.Error("Failed to fetch milestone end block from Heimdall for header verification", "err", err) + return } - // Collect headers from startBlock to current head + headNumber := currentHead.Number.Uint64() + if milestoneEndBlock >= headNumber { + return // Still syncing or synced to the milestone end block, nothing to verify. + } + + startBlock := milestoneEndBlock + 1 + + // Collect headers from startBlock to current head. headers := make([]*types.Header, 0, headNumber-startBlock+1) for i := startBlock; i <= headNumber; i++ { header := bc.GetHeaderByNumber(i) if header == nil { log.Debug("Missing header during verification", "number", i) - return // Missing header, skip verification + return } headers = append(headers, header) } if len(headers) == 0 { + log.Debug("No headers to verify") return } - log.Debug("Verifying pending headers", "from", headers[0].Number.Uint64(), - "to", headers[len(headers)-1].Number.Uint64(), "count", len(headers)) + log.Debug("Verifying pending headers", + "from", headers[0].Number.Uint64(), "to", headers[len(headers)-1].Number.Uint64(), "count", len(headers)) - // Verify headers abort, results := bc.engine.VerifyHeaders(bc, headers) defer close(abort) - // Check results and find the last valid header - lastValidNumber := milestoneNumber + // Check results and find the last valid header. + lastValidNumber := milestoneEndBlock for _, header := range headers { select { case <-bc.quit: @@ -4290,15 +4289,15 @@ func (bc *BlockChain) verifyPendingHeaders() { if err != nil { log.Warn("Invalid header detected during background verification", "number", header.Number.Uint64(), "hash", header.Hash(), "err", err) - // Rewind to the last valid block - if lastValidNumber < currentHead.Number.Uint64() { - dropCount := int64(currentHead.Number.Uint64() - lastValidNumber) - log.Warn("Rewinding chain due to invalid header", - "from", currentHead.Number.Uint64(), "to", lastValidNumber, "drop", dropCount) + if lastValidNumber < headNumber { + dropCount := int64(headNumber - lastValidNumber) + + log.Warn("Rewinding chain due to an invalid header", + "from", headNumber, "to", lastValidNumber, "drop", dropCount) if err := bc.SetHead(lastValidNumber); err != nil { - log.Error("Failed to rewind chain", "err", err) + log.Error("Failed to rewind chain to the last valid header", "err", err) } else { blockReorgMeter.Mark(1) blockReorgDropMeter.Mark(dropCount) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index a521c487c6..4bae525a3e 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -5064,24 +5064,24 @@ func TestVerifyPendingHeaders(t *testing.T) { func testVerifyPendingHeaders(t *testing.T, scheme string) { engine := ethash.NewFaker() + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } genesis := &Genesis{ - Config: params.TestChainConfig, + Config: &config, BaseFee: big.NewInt(params.InitialBaseFee), } // Generate blocks _, blocks, _ := GenerateChainWithGenesis(genesis, engine, 8, nil) - // Test with mock validator - mockValidator := &mockChainValidator{ - hasMilestone: true, - milestoneNumber: 3, - milestoneHash: common.HexToHash("0x123"), - } - - // Create blockchain + // Create blockchain with milestone fetcher cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 3, nil + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -5105,15 +5105,16 @@ func testVerifyPendingHeaders(t *testing.T, scheme string) { } } -// TestHeaderVerificationWithNilChecker tests that verification is skipped when checker is nil -func TestHeaderVerificationWithNilChecker(t *testing.T) { +// TestHeaderVerificationWithNilFetcher tests that the verification loop is skipped +// when MilestoneFetcher is nil. +func TestHeaderVerificationWithNilFetcher(t *testing.T) { engine := ethash.NewFaker() genesis := &Genesis{ Config: params.TestChainConfig, BaseFee: big.NewInt(params.InitialBaseFee), } - // Create blockchain with nil checker + // Create blockchain without MilestoneFetcher chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, DefaultConfig()) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -5128,13 +5129,13 @@ func TestHeaderVerificationWithNilChecker(t *testing.T) { initialHead := chain.CurrentBlock().Number.Uint64() - // Wait a bit - the verification loop should not run since checker is nil + // Wait a bit - the verification loop should not run since milestoneFetcher is nil time.Sleep(2 * time.Second) // Head should not have changed newHead := chain.CurrentBlock().Number.Uint64() if newHead != initialHead { - t.Errorf("Head should not have changed when checker is nil, got %d, want %d", newHead, initialHead) + t.Errorf("Head should not have changed when milestoneFetcher is nil, got %d, want %d", newHead, initialHead) } } @@ -5149,10 +5150,10 @@ func (m *headerCountingEngine) VerifyHeaders(chain consensus.ChainHeaderReader, return m.Ethash.VerifyHeaders(chain, headers) } -// TestVerifyPendingHeadersCapBoundary tests that verifyPendingHeaders caps the -// verification window to DefaultSpanLength + 1 headers from the current head. -func TestVerifyPendingHeadersCapBoundary(t *testing.T) { - t.Run("GapSmallerThanCap", func(t *testing.T) { +// TestVerifyPendingHeadersMilestoneFetcher tests that verifyPendingHeaders +// verifies only headers between the Heimdall milestone and the chain head. +func TestVerifyPendingHeadersMilestoneFetcher(t *testing.T) { + t.Run("VerifiesFromMilestoneToHead", func(t *testing.T) { engine := &headerCountingEngine{Ethash: ethash.NewFaker()} config := *params.TestChainConfig @@ -5164,16 +5165,12 @@ func TestVerifyPendingHeadersCapBoundary(t *testing.T) { BaseFee: big.NewInt(params.InitialBaseFee), } - mockValidator := &mockChainValidator{ - hasMilestone: true, - milestoneNumber: 3, - milestoneHash: common.HexToHash("0x123"), - } - _, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, 20, nil) cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 3, nil // milestone at block 3 + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -5186,16 +5183,14 @@ func TestVerifyPendingHeadersCapBoundary(t *testing.T) { chain.verifyPendingHeaders() - // Gap is 20 - 3 = 17, which is less than DefaultSpanLength (6400). - // All 17 headers (blocks 4-20) should be verified. + // Should verify blocks 4-20 = 17 headers if got := engine.headersVerified.Load(); got != 17 { - t.Errorf("expected 17 headers verified (no cap), got %d", got) + t.Errorf("expected 17 headers verified, got %d", got) } }) - t.Run("GapLargerThanCap", func(t *testing.T) { + t.Run("SkipsWhenMilestoneAheadOfHead", func(t *testing.T) { engine := &headerCountingEngine{Ethash: ethash.NewFaker()} - chainLength := int(params.DefaultSpanLength) + 100 config := *params.TestChainConfig config.Bor = ¶ms.BorConfig{ @@ -5206,16 +5201,50 @@ func TestVerifyPendingHeadersCapBoundary(t *testing.T) { BaseFee: big.NewInt(params.InitialBaseFee), } - mockValidator := &mockChainValidator{ - hasMilestone: true, - milestoneNumber: 0, - milestoneHash: common.HexToHash("0x123"), + _, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, 10, nil) + + cfg := DefaultConfig() + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 100, nil // milestone ahead of head (still syncing) + } + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) + if err != nil { + t.Fatalf("failed to create blockchain: %v", err) + } + defer chain.Stop() + + if _, err := chain.InsertChain(blocks, false); err != nil { + t.Fatalf("failed to insert chain: %v", err) + } + + engine.headersVerified.Store(0) // reset counter after initial insertion + + chain.verifyPendingHeaders() + + // Should not verify anything since milestone > head + if got := engine.headersVerified.Load(); got != 0 { + t.Errorf("expected 0 headers verified when milestone ahead of head, got %d", got) + } + }) + + t.Run("SkipsOnFetcherError", func(t *testing.T) { + engine := &headerCountingEngine{Ethash: ethash.NewFaker()} + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } + genesis := &Genesis{ + Config: &config, + BaseFee: big.NewInt(params.InitialBaseFee), } - _, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, chainLength, nil) + _, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, 10, nil) cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 0, fmt.Errorf("heimdall unavailable") + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -5226,13 +5255,13 @@ func TestVerifyPendingHeadersCapBoundary(t *testing.T) { t.Fatalf("failed to insert chain: %v", err) } + engine.headersVerified.Store(0) // reset counter after initial insertion + chain.verifyPendingHeaders() - // Gap is 6500 - 0 = 6500, which exceeds DefaultSpanLength (6400). - // Only DefaultSpanLength + 1 = 6401 headers should be verified. - expected := int64(params.DefaultSpanLength + 1) - if got := engine.headersVerified.Load(); got != expected { - t.Errorf("expected %d headers verified (capped), got %d", expected, got) + // Should not verify anything when fetcher returns error + if got := engine.headersVerified.Load(); got != 0 { + t.Errorf("expected 0 headers verified on fetcher error, got %d", got) } }) } @@ -5256,16 +5285,12 @@ func TestVerifyPendingHeadersReorgMetrics(t *testing.T) { BaseFee: big.NewInt(params.InitialBaseFee), } - mockValidator := &mockChainValidator{ - hasMilestone: true, - milestoneNumber: 3, - milestoneHash: common.HexToHash("0x123"), - } - _, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, 8, nil) cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 3, nil // milestone at block 3 + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) diff --git a/eth/backend.go b/eth/backend.go index be0f82ba94..06bf4a3375 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -319,6 +319,17 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { options.Overrides = &overrides options.Checker = checker + // Wire MilestoneFetcher so verifyPendingHeaders queries Heimdall directly. + if borEngine, ok := eth.engine.(*bor.Bor); ok && borEngine.HeimdallClient != nil { + options.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + m, err := borEngine.HeimdallClient.FetchMilestone(ctx) + if err != nil { + return 0, err + } + return m.EndBlock, nil + } + } + // check if Parallel EVM is enabled // if enabled, use parallel state processor if config.ParallelEVM.Enable { From 13e2c7858881399704dfb446546f50204204bbf4 Mon Sep 17 00:00:00 2001 From: kamuikatsurgi Date: Wed, 18 Feb 2026 13:15:34 +0530 Subject: [PATCH 6/6] fix: tests --- core/blockchain_test.go | 90 +++++++++++++++++++---------------------- tests/bor/bor_test.go | 15 +------ 2 files changed, 44 insertions(+), 61 deletions(-) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 4bae525a3e..4358922c42 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -4829,24 +4829,23 @@ func testHeaderVerificationLoop(t *testing.T, scheme string) { // Test case 1: Valid chain - no rewinds should happen t.Run("ValidChain", func(t *testing.T) { engine := ethash.NewFaker() + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } genesis := &Genesis{ - Config: params.TestChainConfig, + Config: &config, BaseFee: big.NewInt(params.InitialBaseFee), } - // Create a mock validator that has a finalized block at height 3 - mockValidator := &mockChainValidator{ - hasMilestone: true, - milestoneNumber: 3, - milestoneHash: common.HexToHash("0x123"), - } - // Generate blocks _, blocks, _ := GenerateChainWithGenesis(genesis, engine, 8, nil) - // Create blockchain with mock validator cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 3, nil + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -4891,19 +4890,14 @@ func testHeaderVerificationLoop(t *testing.T, scheme string) { BaseFee: big.NewInt(params.InitialBaseFee), } - // Create a mock validator that has a finalized block at height 3 - mockValidator := &mockChainValidator{ - hasMilestone: true, - milestoneNumber: 3, - milestoneHash: common.HexToHash("0x123"), - } - // Generate blocks _, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, 8, nil) - // Create blockchain with mock validator and failing engine + // Create blockchain with milestone fetcher and failing engine cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 3, nil // milestone at block 3 + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -4934,25 +4928,26 @@ func testHeaderVerificationLoop(t *testing.T, scheme string) { } }) - // Test case 3: No finalized block - verification should not run + // Test case 3: Fetcher returns error - verification should not run t.Run("NoFinalizedBlock", func(t *testing.T) { engine := ethash.NewFaker() + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } genesis := &Genesis{ - Config: params.TestChainConfig, + Config: &config, BaseFee: big.NewInt(params.InitialBaseFee), } - // Create a mock validator with no finalized block - mockValidator := &mockChainValidator{ - hasMilestone: false, - } - // Generate blocks _, blocks, _ := GenerateChainWithGenesis(genesis, engine, 5, nil) - // Create blockchain with mock validator cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 0, fmt.Errorf("no milestone available") + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -4976,27 +4971,26 @@ func testHeaderVerificationLoop(t *testing.T, scheme string) { } }) - // Test case 4: Current head at finalized block - no verification needed + // Test case 4: Milestone at head - no verification needed t.Run("HeadAtFinalizedBlock", func(t *testing.T) { engine := ethash.NewFaker() + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } genesis := &Genesis{ - Config: params.TestChainConfig, + Config: &config, BaseFee: big.NewInt(params.InitialBaseFee), } // Generate blocks _, blocks, _ := GenerateChainWithGenesis(genesis, engine, 5, nil) - // Create a mock validator where finalized block equals current head - mockValidator := &mockChainValidator{ - hasMilestone: true, - milestoneNumber: 5, // Same as head - milestoneHash: blocks[4].Hash(), - } - - // Create blockchain with mock validator cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 5, nil // milestone at head + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -5023,23 +5017,23 @@ func testHeaderVerificationLoop(t *testing.T, scheme string) { // Test case 5: Verify proper shutdown when blockchain stops t.Run("ProperShutdown", func(t *testing.T) { engine := ethash.NewFaker() + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } genesis := &Genesis{ - Config: params.TestChainConfig, + Config: &config, BaseFee: big.NewInt(params.InitialBaseFee), } - mockValidator := &mockChainValidator{ - hasMilestone: true, - milestoneNumber: 2, - milestoneHash: common.HexToHash("0x123"), - } - // Generate blocks _, blocks, _ := GenerateChainWithGenesis(genesis, engine, 5, nil) - // Create blockchain cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 2, nil + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) diff --git a/tests/bor/bor_test.go b/tests/bor/bor_test.go index f91c1657e0..64fbc10a97 100644 --- a/tests/bor/bor_test.go +++ b/tests/bor/bor_test.go @@ -30,6 +30,7 @@ import ( "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/bor" "github.com/ethereum/go-ethereum/consensus/bor/clerk" + borMilestone "github.com/ethereum/go-ethereum/consensus/bor/heimdall/milestone" borSpan "github.com/ethereum/go-ethereum/consensus/bor/heimdall/span" "github.com/ethereum/go-ethereum/consensus/bor/valset" "github.com/ethereum/go-ethereum/consensus/ethash" @@ -2786,7 +2787,7 @@ func TestVerifyPendingHeadersSpanRotationReorg(t *testing.T) { h2.EXPECT().GetSpan(gomock.Any(), uint64(1)).Return(&span1, nil).AnyTimes() h2.EXPECT().GetLatestSpan(gomock.Any()).Return(&span1, nil).AnyTimes() h2.EXPECT().FetchCheckpoint(gomock.Any(), int64(-1)).Return(nil, fmt.Errorf("no checkpoint available")).AnyTimes() - h2.EXPECT().FetchMilestone(gomock.Any()).Return(nil, fmt.Errorf("no milestone available")).AnyTimes() + h2.EXPECT().FetchMilestone(gomock.Any()).Return(&borMilestone.Milestone{EndBlock: 15}, nil).AnyTimes() h2.EXPECT().FetchStatus(gomock.Any()).Return(&ctypes.SyncInfo{CatchingUp: false}, nil).AnyTimes() h2.EXPECT().StateSyncEvents(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*clerk.EventRecordWithTime{getSampleEventRecord(t)}, nil).AnyTimes() @@ -2799,17 +2800,6 @@ func TestVerifyPendingHeadersSpanRotationReorg(t *testing.T) { borEngs[1].PurgeCache() log.Info("Purged caches on validator 2 to apply new span data") - // Set a milestone at block 15 on validator 2's chain - // This will cause verifyPendingHeaders to check blocks 16-18 - milestoneBlock := nodes[1].BlockChain().GetHeaderByNumber(15) - require.NotNil(t, milestoneBlock, "Milestone block 15 should exist") - - log.Info("Setting milestone at block 15", - "hash", milestoneBlock.Hash(), - "miner", milestoneBlock.Coinbase) - - nodes[1].Downloader().ChainValidator.ProcessMilestone(15, milestoneBlock.Hash()) - log.Info("Waiting for header verification loop to detect invalid headers...") timeout := time.After(30 * time.Second) @@ -2851,7 +2841,6 @@ func TestVerifyPendingHeadersSpanRotationReorg(t *testing.T) { block15 := nodes[1].BlockChain().GetHeaderByNumber(15) require.NotNil(t, block15, "Block 15 should still exist") - require.Equal(t, milestoneBlock.Hash(), block15.Hash(), "Block 15 hash should match milestone") for blockNum := uint64(16); blockNum <= uint64(18); blockNum++ { header := nodes[1].BlockChain().GetHeaderByNumber(blockNum)