From aacd081b5f3d644c092b3a2a0438530c0c8bd7da Mon Sep 17 00:00:00 2001 From: gabornyergesX Date: Wed, 25 Jun 2025 21:24:23 +0200 Subject: [PATCH] core: add graceful shutdown checks during block processing Fixes issue where node would continue processing blocks for several seconds after receiving shutdown signal (Ctrl+C), ignoring user interruption. The problem occurred because shutdown signals were only checked at the beginning of each block processing loop in insertChain(), but not during the actual block processing in processBlock(). Heavy blocks with high gas usage or many transactions could take several seconds to process, during which shutdown signals were completely ignored. This change adds shutdown signal checks at strategic points in processBlock(): - Before starting block processing - After state setup but before processing - After processing but before validation - After validation but before writing to disk These checks allow block processing to be interrupted gracefully at safe points, providing immediate response to shutdown signals. Fixes #31508 --- core/blockchain.go | 17 ++++++++++++++++ core/blockchain_test.go | 43 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/core/blockchain.go b/core/blockchain.go index d8b101f616..a3fdb8b1cb 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1956,6 +1956,11 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s ) defer interrupt.Store(true) // terminate the prefetch at the end + // Short circuit if shutting down + if bc.insertStopped() { + return nil, fmt.Errorf("block processing interrupted during shutdown") + } + if bc.cfg.NoPrefetch { statedb, err = state.New(parentRoot, bc.statedb) if err != nil { @@ -2006,6 +2011,10 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s }(time.Now(), throwaway, block) } + if bc.insertStopped() { + return nil, fmt.Errorf("block processing interrupted during shutdown") + } + // If we are past Byzantium, enable prefetching to pull in trie node paths // while processing transactions. Before Byzantium the prefetcher is mostly // useless due to the intermediate root hashing after each transaction. @@ -2046,6 +2055,10 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s } ptime := time.Since(pstart) + if bc.insertStopped() { + return nil, fmt.Errorf("block processing interrupted during shutdown") + } + vstart := time.Now() if err := bc.validator.ValidateState(block, statedb, res, false); err != nil { bc.reportBlock(block, res, err) @@ -2053,6 +2066,10 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s } vtime := time.Since(vstart) + if bc.insertStopped() { + return nil, fmt.Errorf("block processing interrupted during shutdown") + } + // If witnesses was generated and stateless self-validation requested, do // that now. Self validation should *never* run in production, it's more of // a tight integration to enable running *all* consensus tests through the diff --git a/core/blockchain_test.go b/core/blockchain_test.go index f47e922e18..740861ac59 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -25,6 +25,7 @@ import ( "math/rand" "os" "path" + "strings" "sync" "testing" "time" @@ -4407,3 +4408,45 @@ func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64, } } } + +// TestBlockProcessingInterrupt tests graceful shutdown during block processing +func TestBlockProcessingInterrupt(t *testing.T) { + testBlockProcessingInterrupt(t, rawdb.HashScheme) + testBlockProcessingInterrupt(t, rawdb.PathScheme) +} + +func testBlockProcessingInterrupt(t *testing.T, scheme string) { + var ( + genesis = &Genesis{ + BaseFee: big.NewInt(params.InitialBaseFee), + Config: params.AllEthashProtocolChanges, + } + engine = ethash.NewFaker() + ) + + options := DefaultConfig().WithStateScheme(scheme) + blockchain, _ := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, options) + defer blockchain.Stop() + + genDb, blocks := makeBlockChainWithGenesis(genesis, 1, engine, canonicalSeed) + defer genDb.Close() + + // Test interrupt during block processing + blockchain.InterruptInsert(true) + parent := blockchain.genesisBlock.Header() + _, err := blockchain.processBlock(parent.Root, blocks[0], true, false) + + if err == nil { + t.Error("Expected block processing to be interrupted, but it succeeded") + } + if !strings.Contains(fmt.Sprintf("%v", err), "block processing interrupted during shutdown") { + t.Errorf("Expected 'block processing interrupted during shutdown' error, got: %v", err) + } + + // Test processing works after reset + blockchain.InterruptInsert(false) + _, err = blockchain.processBlock(parent.Root, blocks[0], true, false) + if err != nil { + t.Errorf("Failed to process block after reset: %v", err) + } +}