Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion consensus/XDPoS/engines/engine_v2/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,22 @@ func (x *XDPoS_v2) Initial(chain consensus.ChainReader, header *types.Header) er
x.lock.Lock()
defer x.lock.Unlock()

return x.initial(chain, header)
if err := x.initial(chain, header); err != nil {
return err
}
// Startup-only repair, skipped for chain readers that cannot open state.
// Runs under x.lock, which is safe here: Initial is only reached from the
// startup path before the protocol starts, and the historical state reads
// do not take any chain locks.
if gapChain, ok := chain.(GapStateReader); ok {
x.RepairGapSnapshots(gapChain)
}
return nil
}

// initial sets the v2 parameters from the chain. It must only be called while
// holding x.lock, from either the startup path (Initial) or header verification.
// The startup-only gap snapshot repair is deliberately kept in Initial, not here.
func (x *XDPoS_v2) initial(chain consensus.ChainReader, header *types.Header) error {
log.Warn("[initial] initial v2 related parameters")

Expand Down
128 changes: 128 additions & 0 deletions consensus/XDPoS/engines/engine_v2/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package engine_v2

import (
"encoding/json"
"errors"
"fmt"

"github.com/XinFinOrg/XDPoSChain/common"
xdc_sort "github.com/XinFinOrg/XDPoSChain/common/sort"
"github.com/XinFinOrg/XDPoSChain/consensus"
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils"
"github.com/XinFinOrg/XDPoSChain/core/rawdb"
"github.com/XinFinOrg/XDPoSChain/core/state"
"github.com/XinFinOrg/XDPoSChain/ethdb"
"github.com/XinFinOrg/XDPoSChain/log"
)
Expand Down Expand Up @@ -112,3 +116,127 @@ func (x *XDPoS_v2) getSnapshot(chain consensus.ChainReader, number uint64, isGap
x.snapshots.Add(snap.Hash, snap)
return snap, nil
}

// GapStateReader is a chain reader that can also open historical state and
// expose its database, which the startup repair needs to rebuild a snapshot
// from its gap block.
type GapStateReader interface {
consensus.ChainReader
ChainDb() ethdb.Database
StateAt(root common.Hash) (*state.StateDB, error)
}

// ErrNoCandidates is returned by BuildSnapshotFromState when the gap block
// state yields no masternode candidates.
var ErrNoCandidates = errors.New("no masternode candidates in state")

// BuildSnapshotFromState derives a gap block snapshot from the state committed
// at that block. The ordering must stay identical to core.BlockChain.UpdateM1
// and Downloader.generateSnapshot: a different equal-stake order yields a
// different masternode set.
func BuildSnapshotFromState(statedb *state.StateDB, number uint64, hash common.Hash) (*SnapshotV2, error) {
var ms []utils.Masternode
for _, candidate := range statedb.GetCandidates() {
if candidate.IsZero() {
continue
}
ms = append(ms, utils.Masternode{Address: candidate, Stake: statedb.GetCandidateCap(candidate)})
}
// GetCandidates and GetCandidateCap return zero values when the voting
// contract storage cannot be read, memoizing the failure in StateDB.Error().
// Persisting a snapshot derived from such reads would store an empty or
// partial masternode set that permanently masks the hole, so surface the
// read error instead.
if err := statedb.Error(); err != nil {
return nil, fmt.Errorf("reading masternode candidates from state: %w", err)
}
if len(ms) == 0 {
// An empty snapshot loads back fine and would permanently mask the hole.
return nil, ErrNoCandidates
}
xdc_sort.Slice(ms, func(i, j int) bool {
return ms[i].Stake.Cmp(ms[j].Stake) >= 0
})

candidates := make([]common.Address, len(ms))
for i, m := range ms {
candidates[i] = m.Address
}
return NewSnapshot(number, hash, candidates), nil
}

// repairGapCandidates returns the gap block numbers at or below head whose
// snapshot can still matter to the running chain. getSnapshot maps head to the
// gap block between Gap and Gap+Epoch blocks back, which is always one of these.
func (x *XDPoS_v2) repairGapCandidates(head uint64) []uint64 {
epoch, gap := x.config.Epoch, x.config.Gap
if epoch == 0 || gap == 0 || gap >= epoch {
return nil
}
offset := epoch - gap
if head < offset {
return nil
}
latest := head - (head-offset)%epoch
if latest < epoch {
return []uint64{latest}
}
return []uint64{latest - epoch, latest}
}

// RepairGapSnapshots restores gap block snapshots missing from the database,
// which happens when the process exits between writeHeadBlock and UpdateM1.
// Meant to run once at startup. Failures are only logged: a node that is still
// syncing legitimately has no state to rebuild from.
func (x *XDPoS_v2) RepairGapSnapshots(chain GapStateReader) {
head := chain.CurrentHeader()
if head == nil {
return
}
// Probe and store through the chain's own database rather than x.db, so a
// mismatched engine database cannot cause silent wrong-database writes.
db := chain.ChainDb()
for _, gapNum := range x.repairGapCandidates(head.Number.Uint64()) {
// The snapshot at V2 SwitchBlock-Gap is owned by initial(), and gap
// blocks below the switch belong to the v1 engine. Config validation
// forces SwitchBlock to align with an epoch switch (SwitchBlock % Epoch
// == 0), so <= never skips a v2 gap block.
if gapNum <= x.config.V2.SwitchBlock.Uint64() {
continue
}
gapHeader := chain.GetHeaderByNumber(gapNum)
if gapHeader == nil {
// gapNum is at or below the current head, so the canonical header must exist.
log.Warn("[RepairGapSnapshots] missing canonical gap header", "number", gapNum, "head", head.Number)
continue
}
gapHash := gapHeader.Hash()
// Only a genuinely absent key is repaired. If we cannot reliably determine
// whether a snapshot is present (e.g. I/O error), skip repair to avoid
// overwriting a snapshot that may have been persisted through a reorg.
has, err := rawdb.HasXdposV2Snapshot(db, gapHash)
if err != nil {
log.Debug("[RepairGapSnapshots] cannot probe stored snapshot", "number", gapNum, "hash", gapHash, "err", err)
continue
}
if has {
continue
}
statedb, err := chain.StateAt(gapHeader.Root)
if err != nil {
log.Warn("[RepairGapSnapshots] gap block state unavailable", "number", gapNum, "hash", gapHash, "root", gapHeader.Root, "err", err)
continue
}
snap, err := BuildSnapshotFromState(statedb, gapNum, gapHash)
if err != nil {
log.Warn("[RepairGapSnapshots] cannot derive snapshot", "number", gapNum, "hash", gapHash, "err", err)
continue
}
if err := StoreSnapshot(snap, db); err != nil {
log.Warn("[RepairGapSnapshots] cannot store snapshot", "number", gapNum, "hash", gapHash, "err", err)
continue
}
x.snapshots.Add(snap.Hash, snap)
log.Warn("[RepairGapSnapshots] repaired missing snapshot", "number", gapNum, "hash", gapHash, "candidates", len(snap.NextEpochCandidates))
}
}
Loading
Loading