From ab8dc39e48d2f70d6d1a5969eec4f978ea1954da Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Mon, 6 Apr 2026 01:27:13 +0800 Subject: [PATCH] fix(consensus/XDPoS,core): fix verifyheaders parent lookups for epoch switches Problem: VerifyHeaders could fail on epoch-switch batches when consensus hooks looked up parent headers through ChainReader before the batch had been written to disk. Cause: the import paths passed the raw chain reader into VerifyHeaders, so helper lookups could only see DB-backed state and missed in-flight headers during block and header-only imports. Solution: wrap the VerifyHeaders entry points with a batch-aware verifyChainReader so current-batch headers and blocks are visible during validation, add focused regression tests for epoch-switch parent resolution, and keep wrapper reuse as a no-op instead of mutating an existing cache. --- consensus/XDPoS/XDPoS.go | 2 +- consensus/XDPoS/verify_chain_reader.go | 106 ++++++++ consensus/XDPoS/verify_chain_reader_test.go | 131 ++++++++++ .../verify_chain_reader_test.go | 228 ++++++++++++++++++ core/blockchain.go | 6 +- core/headerchain.go | 7 +- 6 files changed, 477 insertions(+), 3 deletions(-) create mode 100644 consensus/XDPoS/verify_chain_reader.go create mode 100644 consensus/XDPoS/verify_chain_reader_test.go create mode 100644 consensus/tests/engine_v2_tests/verify_chain_reader_test.go diff --git a/consensus/XDPoS/XDPoS.go b/consensus/XDPoS/XDPoS.go index 4d98085eb94d..0793a477487d 100644 --- a/consensus/XDPoS/XDPoS.go +++ b/consensus/XDPoS/XDPoS.go @@ -216,6 +216,7 @@ func (x *XDPoS) VerifyHeaders(chain consensus.ChainReader, headers []*types.Head abort := make(chan struct{}) results := make(chan error, len(headers)) + chain = NewVerifyHeadersChainReader(chain, headers, nil) // Split the headers list into v1 and v2 buckets var v1headers []*types.Header @@ -234,7 +235,6 @@ func (x *XDPoS) VerifyHeaders(chain consensus.ChainReader, headers []*types.Head } } - if v1headers != nil { x.EngineV1.VerifyHeaders(chain, v1headers, v1fullVerifies, abort, results) } diff --git a/consensus/XDPoS/verify_chain_reader.go b/consensus/XDPoS/verify_chain_reader.go new file mode 100644 index 000000000000..e18dbf45816d --- /dev/null +++ b/consensus/XDPoS/verify_chain_reader.go @@ -0,0 +1,106 @@ +package XDPoS + +import ( + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/consensus" + "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/XinFinOrg/XDPoSChain/params" +) + +type verifyChainReader struct { + chain consensus.ChainReader + headersByHash map[common.Hash]*types.Header + headersByNumber map[uint64]*types.Header + blocksByHashNo map[hashAndNumber]*types.Block +} + +type hashAndNumber struct { + hash common.Hash + number uint64 +} + +var _ consensus.ChainReader = (*verifyChainReader)(nil) + +func NewVerifyHeadersChainReader(chain consensus.ChainReader, headers []*types.Header, blocks []*types.Block) consensus.ChainReader { + if reader, ok := chain.(*verifyChainReader); ok { + return reader + } + reader := &verifyChainReader{ + chain: chain, + headersByHash: make(map[common.Hash]*types.Header, len(headers)), + headersByNumber: make(map[uint64]*types.Header, len(headers)), + blocksByHashNo: make(map[hashAndNumber]*types.Block, len(blocks)), + } + for _, header := range headers { + if header == nil || header.Number == nil { + continue + } + hash := header.Hash() + number := header.Number.Uint64() + reader.headersByHash[hash] = header + if _, exists := reader.headersByNumber[number]; !exists { + reader.headersByNumber[number] = header + } + } + for _, block := range blocks { + if block == nil { + continue + } + reader.blocksByHashNo[hashAndNumber{hash: block.Hash(), number: block.NumberU64()}] = block + } + return reader +} + +func (r *verifyChainReader) Config() *params.ChainConfig { + if r.chain == nil { + return nil + } + return r.chain.Config() +} + +func (r *verifyChainReader) CurrentHeader() *types.Header { + if r.chain == nil { + return nil + } + return r.chain.CurrentHeader() +} + +func (r *verifyChainReader) GetHeader(hash common.Hash, number uint64) *types.Header { + if header, ok := r.headersByHash[hash]; ok && header.Number != nil && header.Number.Uint64() == number { + return header + } + if r.chain == nil { + return nil + } + return r.chain.GetHeader(hash, number) +} + +func (r *verifyChainReader) GetHeaderByNumber(number uint64) *types.Header { + if header, ok := r.headersByNumber[number]; ok { + return header + } + if r.chain == nil { + return nil + } + return r.chain.GetHeaderByNumber(number) +} + +func (r *verifyChainReader) GetHeaderByHash(hash common.Hash) *types.Header { + if header, ok := r.headersByHash[hash]; ok { + return header + } + if r.chain == nil { + return nil + } + return r.chain.GetHeaderByHash(hash) +} + +func (r *verifyChainReader) GetBlock(hash common.Hash, number uint64) *types.Block { + if block, ok := r.blocksByHashNo[hashAndNumber{hash: hash, number: number}]; ok { + return block + } + if r.chain == nil { + return nil + } + return r.chain.GetBlock(hash, number) +} diff --git a/consensus/XDPoS/verify_chain_reader_test.go b/consensus/XDPoS/verify_chain_reader_test.go new file mode 100644 index 000000000000..018c4e5c4bae --- /dev/null +++ b/consensus/XDPoS/verify_chain_reader_test.go @@ -0,0 +1,131 @@ +package XDPoS + +import ( + "math/big" + "testing" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/XinFinOrg/XDPoSChain/params" + "github.com/XinFinOrg/XDPoSChain/trie" + "github.com/stretchr/testify/assert" +) + +func makeTestBlock(number int64, time int64, txs ...*types.Transaction) *types.Block { + header := &types.Header{ + Number: big.NewInt(number), + Time: big.NewInt(time), + Difficulty: big.NewInt(1), + GasLimit: 1_000_000, + } + return types.NewBlock(header, txs, nil, nil, trie.NewStackTrie(nil)) +} + +type stubVerifyChainReader struct { + config *params.ChainConfig + currentHeader *types.Header + headersByHash map[common.Hash]*types.Header + headersByNumber map[uint64]*types.Header + blocksByHashNo map[hashAndNumber]*types.Block +} + +func (s *stubVerifyChainReader) Config() *params.ChainConfig { return s.config } + +func (s *stubVerifyChainReader) CurrentHeader() *types.Header { return s.currentHeader } + +func (s *stubVerifyChainReader) GetHeader(hash common.Hash, number uint64) *types.Header { + header := s.GetHeaderByHash(hash) + if header == nil || header.Number == nil || header.Number.Uint64() != number { + return nil + } + return header +} + +func (s *stubVerifyChainReader) GetHeaderByNumber(number uint64) *types.Header { + if s.headersByNumber == nil { + return nil + } + return s.headersByNumber[number] +} + +func (s *stubVerifyChainReader) GetHeaderByHash(hash common.Hash) *types.Header { + if s.headersByHash == nil { + return nil + } + return s.headersByHash[hash] +} + +func (s *stubVerifyChainReader) GetBlock(hash common.Hash, number uint64) *types.Block { + if s.blocksByHashNo == nil { + return nil + } + return s.blocksByHashNo[hashAndNumber{hash: hash, number: number}] +} + +func TestVerifyChainReaderWithNilChainIsNilSafe(t *testing.T) { + batchHeader := &types.Header{Number: big.NewInt(1)} + reader := NewVerifyHeadersChainReader(nil, []*types.Header{batchHeader}, nil).(*verifyChainReader) + + assert.NotNil(t, reader) + assert.Nil(t, reader.Config()) + assert.Nil(t, reader.CurrentHeader()) + assert.Equal(t, batchHeader.Hash(), reader.GetHeaderByNumber(1).Hash()) + assert.Equal(t, batchHeader.Hash(), reader.GetHeaderByHash(batchHeader.Hash()).Hash()) + assert.Equal(t, batchHeader.Hash(), reader.GetHeader(batchHeader.Hash(), 1).Hash()) + assert.Nil(t, reader.GetBlock(batchHeader.Hash(), 1)) + assert.Nil(t, reader.GetHeader(common.Hash{}, 2)) + assert.Nil(t, reader.GetBlock(common.Hash{}, 2)) +} + +func TestVerifyChainReaderShadowsBatchEntries(t *testing.T) { + baseHeader := &types.Header{Number: big.NewInt(100), Time: big.NewInt(1)} + batchHeader := &types.Header{Number: big.NewInt(100), Time: big.NewInt(2)} + batchTx := types.NewTransaction(1, common.Address{0x1}, big.NewInt(1), 21000, big.NewInt(1), []byte{0x1}) + batchBlock := makeTestBlock(100, 2, batchTx) + batchHeader = batchBlock.Header() + currentHeader := &types.Header{Number: big.NewInt(99), Time: big.NewInt(3)} + baseBlock := types.NewBlockWithHeader(baseHeader) + + base := &stubVerifyChainReader{ + config: params.TestXDPoSMockChainConfig, + currentHeader: currentHeader, + headersByHash: map[common.Hash]*types.Header{baseHeader.Hash(): baseHeader}, + headersByNumber: map[uint64]*types.Header{ + 100: baseHeader, + }, + blocksByHashNo: map[hashAndNumber]*types.Block{ + {hash: baseHeader.Hash(), number: 100}: baseBlock, + }, + } + + reader := NewVerifyHeadersChainReader(base, []*types.Header{batchHeader}, []*types.Block{batchBlock}).(*verifyChainReader) + + assert.Equal(t, params.TestXDPoSMockChainConfig, reader.Config()) + assert.Equal(t, currentHeader.Hash(), reader.CurrentHeader().Hash()) + assert.Equal(t, batchHeader.Hash(), reader.GetHeaderByNumber(100).Hash()) + assert.Equal(t, batchHeader.Hash(), reader.GetHeaderByHash(batchHeader.Hash()).Hash()) + assert.Equal(t, batchHeader.Hash(), reader.GetHeader(batchHeader.Hash(), 100).Hash()) + assert.Equal(t, batchHeader.Hash(), reader.GetBlock(batchHeader.Hash(), 100).Hash()) + assert.Len(t, reader.GetBlock(batchHeader.Hash(), 100).Transactions(), 1) + assert.Equal(t, baseHeader.Hash(), reader.GetBlock(baseHeader.Hash(), 100).Hash()) +} + +func TestVerifyChainReaderReusesExistingWrapper(t *testing.T) { + firstTx := types.NewTransaction(1, common.Address{0x1}, big.NewInt(1), 21000, big.NewInt(1), []byte{0x1}) + secondTx := types.NewTransaction(2, common.Address{0x2}, big.NewInt(2), 21000, big.NewInt(1), []byte{0x2}) + firstBlock := makeTestBlock(100, 1, firstTx) + secondBlock := makeTestBlock(101, 2, secondTx) + + reader := NewVerifyHeadersChainReader(nil, []*types.Header{firstBlock.Header()}, []*types.Block{firstBlock}) + reused := NewVerifyHeadersChainReader(reader, []*types.Header{secondBlock.Header()}, nil) + + assert.Same(t, reader, reused) + wrapped := reused.(*verifyChainReader) + assert.Len(t, wrapped.GetBlock(firstBlock.Hash(), firstBlock.NumberU64()).Transactions(), 1) + assert.Nil(t, wrapped.GetHeaderByNumber(secondBlock.NumberU64())) + assert.Nil(t, wrapped.GetBlock(secondBlock.Hash(), secondBlock.NumberU64())) + + reused = NewVerifyHeadersChainReader(reused, nil, []*types.Block{secondBlock}) + wrapped = reused.(*verifyChainReader) + assert.Nil(t, wrapped.GetBlock(secondBlock.Hash(), secondBlock.NumberU64())) +} diff --git a/consensus/tests/engine_v2_tests/verify_chain_reader_test.go b/consensus/tests/engine_v2_tests/verify_chain_reader_test.go new file mode 100644 index 000000000000..8fadd2c9ebf3 --- /dev/null +++ b/consensus/tests/engine_v2_tests/verify_chain_reader_test.go @@ -0,0 +1,228 @@ +package engine_v2_tests + +import ( + "encoding/json" + "fmt" + "math/big" + "testing" + "time" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/consensus" + "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS" + "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/XinFinOrg/XDPoSChain/params" + "github.com/stretchr/testify/assert" +) + +func TestVerifyEpochSwitchHeadersWithBatchReader(t *testing.T) { + b, err := json.Marshal(params.TestXDPoSMockChainConfig) + assert.Nil(t, err) + configString := string(b) + + var config params.ChainConfig + err = json.Unmarshal([]byte(configString), &config) + assert.Nil(t, err) + + epochSwitchNumber := int(config.XDPoS.Epoch) * 2 + blockchain, _, currentBlock, signer, signFn, _ := PrepareXDCTestBlockChainForV2Engine(t, epochSwitchNumber-2, &config, nil) + adaptor := blockchain.Engine().(*XDPoS.XDPoS) + + parentBlockNumber := epochSwitchNumber - 1 + parentRound := int64(parentBlockNumber) - config.XDPoS.V2.SwitchBlock.Int64() + parentBlock := CreateBlock(blockchain, &config, currentBlock, parentBlockNumber, parentRound, signer.Hex(), signer, signFn, nil, nil, "") + + candidates, err := adaptor.EngineV2.GetSignersFromSnapshot(blockchain, &types.Header{Number: big.NewInt(int64(epochSwitchNumber))}) + assert.Nil(t, err) + epochSwitchRound := int64(config.XDPoS.Epoch) + maxMasternodes := config.XDPoS.V2.Config(uint64(epochSwitchRound)).MaxMasternodes + if len(candidates) > maxMasternodes { + candidates = candidates[:maxMasternodes] + } + validators := make([]byte, 0, len(candidates)*common.AddressLength) + for _, candidate := range candidates { + validators = append(validators, candidate[:]...) + } + leaderIndex := -1 + leader := common.Address{} + leaderSignFn := signFn + for i, candidate := range candidates { + switch candidate { + case acc1Addr: + leaderIndex = i + leader = candidate + _, leaderSignFn, err = getSignerAndSignFn(acc1Key) + case acc2Addr: + leaderIndex = i + leader = candidate + _, leaderSignFn, err = getSignerAndSignFn(acc2Key) + case acc3Addr: + leaderIndex = i + leader = candidate + _, leaderSignFn, err = getSignerAndSignFn(acc3Key) + case voterAddr: + leaderIndex = i + leader = candidate + _, leaderSignFn, err = getSignerAndSignFn(voterKey) + case signer: + leaderIndex = i + leader = candidate + leaderSignFn = signFn + } + if leaderIndex >= 0 { + epochSwitchRound += int64(i) + break + } + } + if leaderIndex < 0 { + t.Fatal("snapshot candidates do not include a signer with a known test key") + } + assert.Nil(t, err) + epochSwitchHeader := &types.Header{ + ParentHash: parentBlock.Hash(), + UncleHash: types.EmptyUncleHash, + TxHash: types.EmptyRootHash, + ReceiptHash: types.EmptyRootHash, + Root: common.HexToHash("35999dded35e8db12de7e6c1471eb9670c162eec616ecebbaf4fddd4676fb930"), + Coinbase: leader, + Difficulty: big.NewInt(1), + Number: big.NewInt(int64(epochSwitchNumber)), + GasLimit: 1200000000, + Time: big.NewInt(time.Now().Unix() - 1000000 + int64(epochSwitchNumber*10)), + Extra: generateV2Extra(epochSwitchRound, parentBlock, leader, leaderSignFn, nil), + Validators: validators, + } + sealHeader(blockchain, epochSwitchHeader, leader, leaderSignFn) + epochSwitchBlock := types.NewBlockWithHeader(epochSwitchHeader) + + adaptor.EngineV2.HookPenalty = func(chain consensus.ChainReader, number *big.Int, parentHash common.Hash, candidates []common.Address) ([]common.Address, error) { + parentNumber := number.Uint64() - 1 + byHashAndNumber := chain.GetHeader(parentHash, parentNumber) + if byHashAndNumber == nil { + return nil, fmt.Errorf("missing parent header by hash and number: %d", parentNumber) + } + byHash := chain.GetHeaderByHash(parentHash) + if byHash == nil { + return nil, fmt.Errorf("missing parent header by hash: %d", parentNumber) + } + byNumber := chain.GetHeaderByNumber(parentNumber) + if byNumber == nil { + return nil, fmt.Errorf("missing parent header by number: %d", parentNumber) + } + if byHash.Hash() != parentHash || byNumber.Hash() != parentHash || byHashAndNumber.Hash() != parentHash { + return nil, fmt.Errorf("batch parent header lookup returned unexpected header: got %s %s %s want %s", byHashAndNumber.Hash(), byHash.Hash(), byNumber.Hash(), parentHash) + } + return nil, nil + } + + headersToVerify := []*types.Header{parentBlock.Header(), epochSwitchBlock.Header()} + fullVerifies := []bool{true, true} + _, results := adaptor.VerifyHeaders(blockchain, headersToVerify, fullVerifies) + + for _, header := range headersToVerify { + select { + case result := <-results: + assert.Nil(t, result, "header %d should verify with batch-visible parent lookup", header.Number.Uint64()) + case <-time.After(5 * time.Second): + t.Fatalf("timed out while verifying header %d", header.Number.Uint64()) + } + } +} + +func TestInsertHeaderChainWithBatchReader(t *testing.T) { + b, err := json.Marshal(params.TestXDPoSMockChainConfig) + assert.Nil(t, err) + configString := string(b) + + var config params.ChainConfig + err = json.Unmarshal([]byte(configString), &config) + assert.Nil(t, err) + + epochSwitchNumber := int(config.XDPoS.Epoch) * 2 + blockchain, _, currentBlock, signer, signFn, _ := PrepareXDCTestBlockChainForV2Engine(t, epochSwitchNumber-2, &config, nil) + adaptor := blockchain.Engine().(*XDPoS.XDPoS) + + parentBlockNumber := epochSwitchNumber - 1 + parentRound := int64(parentBlockNumber) - config.XDPoS.V2.SwitchBlock.Int64() + parentBlock := CreateBlock(blockchain, &config, currentBlock, parentBlockNumber, parentRound, signer.Hex(), signer, signFn, nil, nil, "") + + candidates, err := adaptor.EngineV2.GetSignersFromSnapshot(blockchain, &types.Header{Number: big.NewInt(int64(epochSwitchNumber))}) + assert.Nil(t, err) + epochSwitchRound := int64(config.XDPoS.Epoch) + maxMasternodes := config.XDPoS.V2.Config(uint64(epochSwitchRound)).MaxMasternodes + if len(candidates) > maxMasternodes { + candidates = candidates[:maxMasternodes] + } + validators := make([]byte, 0, len(candidates)*common.AddressLength) + for _, candidate := range candidates { + validators = append(validators, candidate[:]...) + } + leaderIndex := -1 + leader := common.Address{} + leaderSignFn := signFn + for i, candidate := range candidates { + switch candidate { + case acc1Addr: + leaderIndex = i + leader = candidate + _, leaderSignFn, err = getSignerAndSignFn(acc1Key) + case acc2Addr: + leaderIndex = i + leader = candidate + _, leaderSignFn, err = getSignerAndSignFn(acc2Key) + case acc3Addr: + leaderIndex = i + leader = candidate + _, leaderSignFn, err = getSignerAndSignFn(acc3Key) + case voterAddr: + leaderIndex = i + leader = candidate + _, leaderSignFn, err = getSignerAndSignFn(voterKey) + case signer: + leaderIndex = i + leader = candidate + leaderSignFn = signFn + } + if leaderIndex >= 0 { + epochSwitchRound += int64(i) + break + } + } + if leaderIndex < 0 { + t.Fatal("snapshot candidates do not include a signer with a known test key") + } + assert.Nil(t, err) + epochSwitchHeader := &types.Header{ + ParentHash: parentBlock.Hash(), + UncleHash: types.EmptyUncleHash, + TxHash: types.EmptyRootHash, + ReceiptHash: types.EmptyRootHash, + Root: common.HexToHash("35999dded35e8db12de7e6c1471eb9670c162eec616ecebbaf4fddd4676fb930"), + Coinbase: leader, + Difficulty: big.NewInt(1), + Number: big.NewInt(int64(epochSwitchNumber)), + GasLimit: 1200000000, + Time: big.NewInt(time.Now().Unix() - 1000000 + int64(epochSwitchNumber*10)), + Extra: generateV2Extra(epochSwitchRound, parentBlock, leader, leaderSignFn, nil), + Validators: validators, + } + sealHeader(blockchain, epochSwitchHeader, leader, leaderSignFn) + + adaptor.EngineV2.HookPenalty = func(chain consensus.ChainReader, number *big.Int, parentHash common.Hash, candidates []common.Address) ([]common.Address, error) { + parentNumber := number.Uint64() - 1 + if chain.GetHeader(parentHash, parentNumber) == nil { + return nil, fmt.Errorf("missing parent header by hash and number: %d", parentNumber) + } + if chain.GetHeaderByHash(parentHash) == nil { + return nil, fmt.Errorf("missing parent header by hash: %d", parentNumber) + } + if chain.GetHeaderByNumber(parentNumber) == nil { + return nil, fmt.Errorf("missing parent header by number: %d", parentNumber) + } + return nil, nil + } + + headersToInsert := []*types.Header{parentBlock.Header(), epochSwitchHeader} + _, err = blockchain.InsertHeaderChain(headersToInsert, 1) + assert.Nil(t, err, "header-only import should see in-batch parent headers") +} diff --git a/core/blockchain.go b/core/blockchain.go index 36187dcf8626..4fd352abd1d5 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1641,7 +1641,11 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, [] seals[i] = verifySeals bc.downloadingBlock.Add(block.Hash(), struct{}{}) } - abort, results := bc.engine.VerifyHeaders(bc, headers, seals) + verifier := consensus.ChainReader(bc) + if _, ok := bc.engine.(*XDPoS.XDPoS); ok { + verifier = XDPoS.NewVerifyHeadersChainReader(bc, headers, chain) + } + abort, results := bc.engine.VerifyHeaders(verifier, headers, seals) defer close(abort) // Start a parallel signature recovery (signer will fluke on fork transition, minimal perf loss) diff --git a/core/headerchain.go b/core/headerchain.go index af64b5cb9570..d0981f98d0db 100644 --- a/core/headerchain.go +++ b/core/headerchain.go @@ -29,6 +29,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/common/lru" "github.com/XinFinOrg/XDPoSChain/consensus" + "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS" "github.com/XinFinOrg/XDPoSChain/core/rawdb" "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/ethdb" @@ -246,7 +247,11 @@ func (hc *HeaderChain) ValidateHeaderChain(chain []*types.Header, checkFreq int) seals[len(seals)-1] = true } - abort, results := hc.engine.VerifyHeaders(hc, chain, seals) + verifier := consensus.ChainReader(hc) + if _, ok := hc.engine.(*XDPoS.XDPoS); ok { + verifier = XDPoS.NewVerifyHeadersChainReader(hc, chain, nil) + } + abort, results := hc.engine.VerifyHeaders(verifier, chain, seals) defer close(abort) // Iterate over the headers and ensure they all check out