consensus: fix missing block header error in vote verification, fix #1850 (#1891)

Previously, vote verification would log errors and fail when the referenced block header was not yet available locally, especially when nodes in the same round processed votes before receiving the block header. This commit changes the logic to defer verification and log at debug level if the header is missing, preventing unnecessary error logs and aligning with upstream geth behavior for out-of-order message arrival during consensus voting.
This commit is contained in:
Daniel Liu 2026-01-05 18:10:49 +08:00 committed by GitHub
parent 2855f1b48c
commit 84ac794e22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 190 additions and 0 deletions

View file

@ -22,6 +22,14 @@ func (x *XDPoS_v2) VerifyVoteMessage(chain consensus.ChainReader, vote *types.Vo
return false, nil
}
// If we don't yet have the referenced header locally, defer verification.
// Votes may arrive before the block header itself; avoid logging an error
// for that normal timing condition and let the vote be retried later.
if chain.GetHeaderByHash(vote.ProposedBlockInfo.Hash) == nil {
log.Debug("[VerifyVoteMessage] referenced header not present yet, defer verification", "blockNum", vote.ProposedBlockInfo.Number, "blockHash", vote.ProposedBlockInfo.Hash)
return false, nil
}
epochInfo, err := x.getEpochSwitchInfo(chain, nil, vote.ProposedBlockInfo.Hash)
if err != nil {
log.Error("[VerifyVoteMessage] Fail to get epochInfo when verifying vote message!", "blockNum", vote.ProposedBlockInfo.Number, "blockHash", vote.ProposedBlockInfo.Hash, "voteHash", vote.Hash(), "voteGapNumber", vote.GapNumber, "err", err)

View file

@ -0,0 +1,182 @@
package engine_v2
import (
"context"
"log/slog"
"math/big"
"sync"
"testing"
"github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/core/types"
"github.com/XinFinOrg/XDPoSChain/log"
"github.com/XinFinOrg/XDPoSChain/params"
"github.com/stretchr/testify/assert"
)
// memoryHandler captures log records for inspection in tests.
type memoryHandler struct {
mu sync.Mutex
attrs []slog.Attr
records []slog.Record
}
func newMemoryHandler() *memoryHandler {
return &memoryHandler{}
}
func (h *memoryHandler) Enabled(_ context.Context, _ slog.Level) bool { return true }
func (h *memoryHandler) Handle(_ context.Context, r slog.Record) error {
clone := r.Clone()
h.mu.Lock()
defer h.mu.Unlock()
h.records = append(h.records, clone)
return nil
}
func (h *memoryHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &memoryHandler{attrs: append(append([]slog.Attr{}, h.attrs...), attrs...)}
}
func (h *memoryHandler) WithGroup(_ string) slog.Handler { return h }
func (h *memoryHandler) Records() []slog.Record {
h.mu.Lock()
defer h.mu.Unlock()
out := make([]slog.Record, len(h.records))
copy(out, h.records)
return out
}
// MockChainReader is a mock implementation of consensus.ChainReader
type MockChainReader struct {
headers map[common.Hash]*types.Header
}
// NewMockChainReader creates a new mock chain reader
func NewMockChainReader() *MockChainReader {
return &MockChainReader{
headers: make(map[common.Hash]*types.Header),
}
}
// AddHeader adds a header to the mock chain
func (m *MockChainReader) AddHeader(header *types.Header) {
m.headers[header.Hash()] = header
}
// Config implements consensus.ChainReader
func (m *MockChainReader) Config() *params.ChainConfig {
return nil
}
// CurrentHeader implements consensus.ChainReader
func (m *MockChainReader) CurrentHeader() *types.Header {
return nil
}
// GetHeader implements consensus.ChainReader
func (m *MockChainReader) GetHeader(hash common.Hash, number uint64) *types.Header {
return nil
}
// GetHeaderByNumber implements consensus.ChainReader
func (m *MockChainReader) GetHeaderByNumber(number uint64) *types.Header {
return nil
}
// GetHeaderByHash implements consensus.ChainReader
func (m *MockChainReader) GetHeaderByHash(hash common.Hash) *types.Header {
return m.headers[hash]
}
// GetBlock implements consensus.ChainReader
func (m *MockChainReader) GetBlock(hash common.Hash, number uint64) *types.Block {
return nil
}
// TestVerifyVoteMessage_HeaderNotPresent tests the behavior when a vote arrives
// before its corresponding block header is available.
//
// This test verifies that VerifyVoteMessage handles the normal timing condition
// where votes may arrive before the block header itself, returning (false, nil)
// and logging at Debug level rather than Error level.
func TestVerifyVoteMessage_HeaderNotPresent(t *testing.T) {
// Create a mock chain reader with no headers
mockChain := NewMockChainReader()
// Capture logs to ensure Debug (and no Error) is emitted
memHandler := newMemoryHandler()
log.SetDefault(log.NewLogger(memHandler))
defer log.SetDefault(log.NewLogger(log.DiscardHandler()))
// Create the XDPoS_v2 engine
engine := &XDPoS_v2{
currentRound: 10,
lock: sync.RWMutex{},
}
// Create a vote for a block that doesn't exist in the chain
vote := &types.Vote{
ProposedBlockInfo: &types.BlockInfo{
Hash: common.StringToHash("nonexistent-block"),
Round: 10,
Number: big.NewInt(100),
},
Signature: make([]byte, 65),
GapNumber: 0,
}
// Call VerifyVoteMessage
verified, err := engine.VerifyVoteMessage(mockChain, vote)
// Verify the expected behavior:
// 1. Should return false (not verified)
assert.False(t, verified, "Should return false when header is not present")
// 2. Should return nil error (deferred verification)
assert.NoError(t, err, "Should not error when header is absent; vote will be retried")
// 3. Should log at Debug level and not emit Error-level logs
records := memHandler.Records()
var hasDebug, hasError bool
for _, rec := range records {
switch rec.Level {
case slog.LevelDebug:
hasDebug = true
case slog.LevelError, log.LevelCrit:
hasError = true
}
}
assert.True(t, hasDebug, "Expected a debug log when header is missing")
assert.False(t, hasError, "Should not emit error-level logs for missing header")
}
// TestVerifyVoteMessage_VoteRoundTooOld tests that votes with rounds below
// the current round are rejected immediately
func TestVerifyVoteMessage_VoteRoundTooOld(t *testing.T) {
mockChain := NewMockChainReader()
engine := &XDPoS_v2{
currentRound: 10,
lock: sync.RWMutex{},
}
// Create a vote with a round number less than current round
vote := &types.Vote{
ProposedBlockInfo: &types.BlockInfo{
Hash: common.StringToHash("some-block"),
Round: 5, // Less than currentRound (10)
Number: big.NewInt(50),
},
Signature: make([]byte, 65),
GapNumber: 0,
}
verified, err := engine.VerifyVoteMessage(mockChain, vote)
// Should reject the vote without error
assert.False(t, verified, "Should return false for vote with round < currentRound")
assert.NoError(t, err, "Should not return an error for old round votes")
}