Skip to content

Commit d93b6cd

Browse files
committed
fix(core): store gap snapshot before writeHeadBlock to close crash window
UpdateMasternodes (called via UpdateM1) was invoked after writeHeadBlock, leaving a window where the node head pointed to a gap block but no corresponding snapshot existed in the database. If the process was killed or restarted during the lengthy per-candidate EVM calls in UpdateM1, the snapshot would never be written. On the next boot the node would load the gap block as its head, then fail with: Cannot find snapshot from last gap block err="leveldb: not found" on every block in the following epoch, silently dropping out of consensus. Fix: add updateM1ForBlock(block, statedb) which reads candidates and stakes directly from the committed state trie (same as downloader.generateSnapshot) without depending on bc.CurrentBlock() or bc.CurrentHeader(). Both the canonical-chain path (writeBlockWithState) and the reorg path (reorg) now call updateM1ForBlock with bc.StateAt(block.Root()) before writeHeadBlock, so the snapshot is durable before the head markers are persisted. The original UpdateM1 is retained unchanged for external callers and tests.
1 parent ed1f41a commit d93b6cd

2 files changed

Lines changed: 203 additions & 10 deletions

File tree

consensus/tests/engine_v1_tests/block_signer_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,119 @@ func TestCallUpdateM1WithSmartContractTranscation(t *testing.T) {
201201
}
202202
}
203203

204+
// Regression: for canonical gap blocks, the block promoted to head must always
205+
// have a readable snapshot.
206+
func TestCanonicalGapBlockHeadSnapshotConsistency(t *testing.T) {
207+
blockchain, backend, parentBlock, signer, signFn := PrepareXDCTestBlockChain(t, GAP-1, params.TestXDPoSMockChainConfig)
208+
tx, err := voteTX(37117, 0, acc1Addr.String())
209+
if err != nil {
210+
t.Fatal(err)
211+
}
212+
213+
header := &types.Header{
214+
Root: common.HexToHash("46234e9cd7e85a267f7f0435b15256a794a2f6d65cc98cdbd21dcd10a01d9772"),
215+
Number: big.NewInt(int64(GAP)),
216+
ParentHash: parentBlock.Hash(),
217+
Coinbase: common.HexToAddress("0xaaa0000000000000000000000000000000000450"),
218+
}
219+
gapBlock, err := createBlockFromHeader(blockchain, header, []*types.Transaction{tx}, signer, signFn, blockchain.Config())
220+
if err != nil {
221+
t.Fatal(err)
222+
}
223+
err = blockchain.InsertBlock(gapBlock)
224+
assert.Nil(t, err)
225+
226+
assert.Equal(t, gapBlock.Hash(), blockchain.CurrentBlock().Hash())
227+
headSigners, err := GetSnapshotSigner(blockchain, blockchain.CurrentBlock())
228+
if err != nil {
229+
t.Fatal(err)
230+
}
231+
assert.Equal(t, common.MaxMasternodes, len(headSigners))
232+
if headSigners[acc1Addr.Hex()] != true {
233+
debugMessage(backend, headSigners, t)
234+
t.Fatalf("head snapshot should include account 1 after canonical gap block")
235+
}
236+
}
237+
238+
// Regression: after reorg to a chain that includes a gap block, the new head
239+
// and its predecessor gap block must both have the reorged snapshot.
240+
func TestReorgGapBlockHeadSnapshotConsistency(t *testing.T) {
241+
blockchain, backend, parentBlock, signer, signFn := PrepareXDCTestBlockChain(t, GAP-1, params.TestXDPoSMockChainConfig)
242+
243+
// Build canonical block 450 A (vote acc1).
244+
txA, err := voteTX(37117, 0, acc1Addr.String())
245+
if err != nil {
246+
t.Fatal(err)
247+
}
248+
headerA := &types.Header{
249+
Root: common.HexToHash("46234e9cd7e85a267f7f0435b15256a794a2f6d65cc98cdbd21dcd10a01d9772"),
250+
Number: big.NewInt(int64(GAP)),
251+
ParentHash: parentBlock.Hash(),
252+
Coinbase: common.HexToAddress("0xaaa0000000000000000000000000000000000450"),
253+
}
254+
block450A, err := createBlockFromHeader(blockchain, headerA, []*types.Transaction{txA}, signer, signFn, blockchain.Config())
255+
if err != nil {
256+
t.Fatal(err)
257+
}
258+
err = blockchain.InsertBlock(block450A)
259+
assert.Nil(t, err)
260+
assert.Equal(t, block450A.Hash(), blockchain.CurrentBlock().Hash())
261+
262+
// Build forked block 450 B (vote acc2), should stay side chain for now.
263+
txB, err := voteTX(37117, 0, acc2Addr.String())
264+
if err != nil {
265+
t.Fatal(err)
266+
}
267+
headerB := &types.Header{
268+
Root: common.HexToHash("068dfa09d7b4093441c0cc4d9807a71bc586f6101c072d939b214c21cd136eb3"),
269+
Number: big.NewInt(int64(GAP)),
270+
ParentHash: parentBlock.Hash(),
271+
Coinbase: common.HexToAddress("0xbbb0000000000000000000000000000000000450"),
272+
}
273+
block450B, err := createBlockFromHeader(blockchain, headerB, []*types.Transaction{txB}, signer, signFn, blockchain.Config())
274+
if err != nil {
275+
t.Fatal(err)
276+
}
277+
err = blockchain.InsertBlock(block450B)
278+
assert.Nil(t, err)
279+
assert.Equal(t, block450A.Hash(), blockchain.CurrentBlock().Hash())
280+
281+
// Extend fork chain with 451 B to trigger reorg.
282+
header451B := &types.Header{
283+
Root: common.HexToHash("068dfa09d7b4093441c0cc4d9807a71bc586f6101c072d939b214c21cd136eb3"),
284+
Number: big.NewInt(int64(GAP + 1)),
285+
ParentHash: block450B.Hash(),
286+
Coinbase: common.HexToAddress("0xbbb0000000000000000000000000000000000451"),
287+
}
288+
block451B, err := createBlockFromHeader(blockchain, header451B, nil, signer, signFn, blockchain.Config())
289+
if err != nil {
290+
t.Fatal(err)
291+
}
292+
err = blockchain.InsertBlock(block451B)
293+
assert.Nil(t, err)
294+
assert.Equal(t, block451B.Hash(), blockchain.CurrentBlock().Hash())
295+
296+
gapSigners, err := GetSnapshotSigner(blockchain, block450B.Header())
297+
if err != nil {
298+
t.Fatal(err)
299+
}
300+
assert.Equal(t, common.MaxMasternodes, len(gapSigners))
301+
if gapSigners[acc2Addr.Hex()] != true {
302+
debugMessage(backend, gapSigners, t)
303+
t.Fatalf("reorged gap snapshot should include account 2")
304+
}
305+
306+
headSigners, err := GetSnapshotSigner(blockchain, blockchain.CurrentBlock())
307+
if err != nil {
308+
t.Fatal(err)
309+
}
310+
assert.Equal(t, common.MaxMasternodes, len(headSigners))
311+
if headSigners[acc2Addr.Hex()] != true {
312+
debugMessage(backend, headSigners, t)
313+
t.Fatalf("reorged head snapshot should include account 2")
314+
}
315+
}
316+
204317
// Should call updateM1 and update snapshot when a forked block(at gap block number) is inserted back into main chain (Edge case)
205318
func TestCallUpdateM1WhenForkedBlockBackToMainChain(t *testing.T) {
206319
blockchain, backend, currentBlock, signer, signFn := PrepareXDCTestBlockChain(t, GAP-1, params.TestXDPoSMockChainConfig)

core/blockchain.go

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,14 +1684,23 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
16841684

16851685
// Set new head.
16861686
if status == CanonStatTy {
1687-
// WriteBlock has already been called, no need to write again
1688-
bc.writeHeadBlock(block, false)
1689-
// prepare set of masternodes for the next epoch
1687+
// For gap blocks, store the masternode snapshot before persisting head markers.
1688+
// This eliminates the crash window between writeHeadBlock and UpdateM1 that
1689+
// would leave the head pointing to a gap block with no snapshot in the database.
1690+
// We open a fresh StateDB from the committed root (equivalent to what UpdateM1
1691+
// does via bc.State() after writeHeadBlock) so we do not depend on the
1692+
// post-Commit statedb object, whose internal caches may have been cleared.
16901693
if bc.chainConfig.XDPoS != nil && ((block.NumberU64() % bc.chainConfig.XDPoS.Epoch) == (bc.chainConfig.XDPoS.Epoch - bc.chainConfig.XDPoS.Gap)) {
1691-
if err := bc.UpdateM1(); err != nil {
1692-
log.Crit("Fail to update masternodes during writeBlockWithState", "number", block.Number, "hash", block.Hash().Hex(), "err", err)
1694+
gapState, stateErr := bc.StateAt(block.Root())
1695+
if stateErr != nil {
1696+
log.Warn("Fail to open gap block state during writeBlockWithState, fallback to contract reads", "number", block.NumberU64(), "hash", block.Hash().Hex(), "err", stateErr)
1697+
}
1698+
if err := bc.updateM1ForBlock(block, gapState); err != nil {
1699+
return NonStatTy, fmt.Errorf("failed to update masternodes during writeBlockWithState at block %d (%s): %w", block.NumberU64(), block.Hash().Hex(), err)
16931700
}
16941701
}
1702+
// WriteBlock has already been called, no need to write again
1703+
bc.writeHeadBlock(block, false)
16951704
}
16961705
// save cache BlockSigners
16971706
if bc.chainConfig.XDPoS != nil && bc.chainConfig.IsTIPSigning(block.Number()) {
@@ -2589,14 +2598,18 @@ func (bc *BlockChain) reorg(oldHead, newHead *types.Header) error {
25892598
bc.logsFeed.Send(rebirthLogs)
25902599
rebirthLogs = nil
25912600
}
2592-
// Update the head block
2593-
bc.writeHeadBlock(block, true)
2594-
// prepare set of masternodes for the next epoch
2601+
// For gap blocks, store the masternode snapshot before persisting head markers.
25952602
if bc.chainConfig.XDPoS != nil && ((block.NumberU64() % bc.chainConfig.XDPoS.Epoch) == (bc.chainConfig.XDPoS.Epoch - bc.chainConfig.XDPoS.Gap)) {
2596-
if err := bc.UpdateM1(); err != nil {
2597-
log.Crit("Fail to update masternodes during reorg", "number", block.Number, "hash", block.Hash().Hex(), "err", err)
2603+
gapState, stateErr := bc.StateAt(block.Root())
2604+
if stateErr != nil {
2605+
log.Warn("Fail to open gap block state during reorg, fallback to contract reads", "number", block.NumberU64(), "hash", block.Hash().Hex(), "err", stateErr)
2606+
}
2607+
if err := bc.updateM1ForBlock(block, gapState); err != nil {
2608+
return fmt.Errorf("failed to update masternodes during reorg at block %d (%s): %w", block.NumberU64(), block.Hash().Hex(), err)
25982609
}
25992610
}
2611+
// Update the head block
2612+
bc.writeHeadBlock(block, true)
26002613
}
26012614
if len(rebirthLogs) > 0 {
26022615
bc.logsFeed.Send(rebirthLogs)
@@ -2758,6 +2771,73 @@ func (bc *BlockChain) GetClient() (bind.ContractBackend, error) {
27582771
return bc.Client, nil
27592772
}
27602773

2774+
// updateM1ForBlock computes the masternode candidate set for a gap block using the
2775+
// provided committed StateDB, then asks the consensus engine to update/store the
2776+
// corresponding snapshot. Unlike UpdateM1, this method reads candidates and stakes
2777+
// directly from the state trie and does not depend on bc.CurrentBlock() or
2778+
// bc.CurrentHeader(), so it can safely be called before writeHeadBlock.
2779+
func (bc *BlockChain) updateM1ForBlock(block *types.Block, statedb *state.StateDB) error {
2780+
engine, ok := bc.Engine().(*XDPoS.XDPoS)
2781+
if bc.Config().XDPoS == nil || !ok {
2782+
return ErrNotXDPoS
2783+
}
2784+
log.Info("It's time to update new set of masternodes for the next epoch...", "number", block.Number(), "hash", block.Hash().Hex())
2785+
2786+
var (
2787+
ms []utils.Masternode
2788+
candidates []common.Address
2789+
)
2790+
if statedb != nil {
2791+
candidates = statedb.GetCandidates()
2792+
for _, candidate := range candidates {
2793+
v := statedb.GetCandidateCap(candidate)
2794+
if !candidate.IsZero() {
2795+
ms = append(ms, utils.Masternode{Address: candidate, Stake: v})
2796+
}
2797+
}
2798+
} else {
2799+
client, err := bc.GetClient()
2800+
if err != nil {
2801+
return fmt.Errorf("failed to get client for fallback candidate query: %w", err)
2802+
}
2803+
validator, err := contractValidator.NewXDCValidator(common.MasternodeVotingSMCBinary, client)
2804+
if err != nil {
2805+
return fmt.Errorf("failed to create validator contract for fallback candidate query: %w", err)
2806+
}
2807+
opts := &bind.CallOpts{BlockNumber: block.Number()}
2808+
candidates, err = validator.GetCandidates(opts)
2809+
if err != nil {
2810+
return fmt.Errorf("failed to get fallback candidate list: %w", err)
2811+
}
2812+
for _, candidate := range candidates {
2813+
v, err := validator.GetCandidateCap(opts, candidate)
2814+
if err != nil {
2815+
return fmt.Errorf("failed to get fallback candidate cap for %s: %w", candidate.Hex(), err)
2816+
}
2817+
if !candidate.IsZero() {
2818+
ms = append(ms, utils.Masternode{Address: candidate, Stake: v})
2819+
}
2820+
}
2821+
}
2822+
if len(ms) == 0 {
2823+
log.Error("No masternode found. Stopping node")
2824+
return errors.New("no masternode found")
2825+
}
2826+
xdc_sort.Slice(ms, func(i, j int) bool {
2827+
return ms[i].Stake.Cmp(ms[j].Stake) >= 0
2828+
})
2829+
log.Info("Ordered list of masternode candidates")
2830+
for _, m := range ms {
2831+
log.Info("", "address", m.Address, "stake", m.Stake)
2832+
}
2833+
log.Info("Updating new set of masternodes")
2834+
if err := engine.UpdateMasternodes(bc, block.Header(), ms); err != nil {
2835+
return err
2836+
}
2837+
log.Info("Masternodes are ready for the next epoch")
2838+
return nil
2839+
}
2840+
27612841
func (bc *BlockChain) UpdateM1() error {
27622842
engine, ok := bc.Engine().(*XDPoS.XDPoS)
27632843
if bc.Config().XDPoS == nil || !ok {

0 commit comments

Comments
 (0)