diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index 2b0ca4fcb7..a881e6698b 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -1104,7 +1104,10 @@ func (c *Bor) Prepare(chain consensus.ChainHeaderReader, header *types.Header, w if currentSigner.signer != (common.Address{}) { succession, err = snap.GetSignerSuccessionNumber(currentSigner.signer) if err != nil { - return err + // If the signer is not in the active validator set, use succession 0 + // so that the pending block header is still valid for RPC queries. + // Seal() will independently reject the block if unauthorized. + succession = 0 } } @@ -1154,15 +1157,9 @@ func (c *Bor) Prepare(chain consensus.ChainHeaderReader, header *types.Header, w // Wait before start the block production if needed (previously this wait was on Seal) if c.config.IsGiugliano(header.Number) && waitOnPrepare { - var successionNumber int // if signer is not empty (RPC nodes have empty signer) if currentSigner.signer != (common.Address{}) { - var err error - successionNumber, err = snap.GetSignerSuccessionNumber(currentSigner.signer) - if err != nil { - return err - } - if successionNumber == 0 { + if succession == 0 { <-time.After(delay) } } diff --git a/miner/worker.go b/miner/worker.go index 9a633a12f8..33a5094b0b 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -973,7 +973,12 @@ func (w *worker) taskLoop() { w.pendingMu.Unlock() if err := w.engine.Seal(w.chain, task.block, task.state.Witness(), w.resultCh, stopCh); err != nil { - log.Warn("Block sealing failed", "err", err) + switch err.(type) { + case *bor.UnauthorizedSignerError: + log.Debug("Block sealing skipped (not in validator set)", "err", err) + default: + log.Warn("Block sealing failed", "err", err) + } w.pendingMu.Lock() delete(w.pendingTasks, sealHash) w.pendingMu.Unlock() diff --git a/miner/worker_test.go b/miner/worker_test.go index 8da774b8e4..ffe6986ac2 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -59,6 +59,57 @@ import ( borSpan "github.com/ethereum/go-ethereum/consensus/bor/heimdall/span" ) +// TestPendingStateNotStaleForNonValidator verifies that a Bor node whose signer +// is NOT in the active validator set still keeps its pending snapshot fresh. +// Regression test: previously, Prepare() returned UnauthorizedSignerError for +// non-validators, which caused prepareWork to fail and the snapshot to never +// update, leading to stale trie errors on "pending" RPC queries. +func TestPendingStateNotStaleForNonValidator(t *testing.T) { + chainConfig := *params.BorUnittestChainConfig + + engine, ctrl := getFakeBorFromConfig(t, &chainConfig) + defer engine.Close() + defer ctrl.Finish() + + // Set up the worker normally, then re-authorize with a key NOT in the + // validator set. newTestWorkerBackend authorizes with testBankAddress + // (which IS the validator), so we must override AFTER backend creation. + nonValidatorKey, _ := crypto.GenerateKey() + nonValidatorAddr := crypto.PubkeyToAddress(nonValidatorKey.PublicKey) + + db := rawdb.NewMemoryDatabase() + backend := newTestWorkerBackend(t, &chainConfig, engine, db) + w := newWorker(DefaultTestConfig(), &chainConfig, engine, backend, new(event.TypeMux), nil, false, false) + defer w.close() + + // Override the signer to one NOT in the validator set. + engine.(*bor.Bor).Authorize(nonValidatorAddr, func(account accounts.Account, s string, data []byte) ([]byte, error) { + return crypto.Sign(crypto.Keccak256(data), nonValidatorKey) + }) + w.setEtherbase(nonValidatorAddr) + + // Start the worker. It will call commitWork which calls Prepare. + // Before the fix: Prepare fails with UnauthorizedSignerError, snapshot + // is never set, pending() returns nil. + // After the fix: Prepare succeeds (defaults succession to 0), snapshot + // is updated, pending() returns valid state. + w.start() + + // Give the worker time to process the start event and run commitWork. + time.Sleep(1 * time.Second) + + pendingBlock, _, pendingState := w.pending() + require.NotNil(t, pendingBlock, "pending block should not be nil for non-validator node") + require.NotNil(t, pendingState, "pending state should not be nil for non-validator node") + + // The pending state must be readable without errors (not a stale trie). + balance := pendingState.GetBalance(testBankAddress) + require.False(t, balance.IsZero(), + "pending state balance for funded account should not be zero (would indicate stale trie)") + require.NoError(t, pendingState.Error(), + "pending state should have no database errors") +} + // nolint : paralleltest func TestGenerateBlockAndImportClique(t *testing.T) { testGenerateBlockAndImport(t, true, false)