go-ethereum/eth/hooks/engine_v1_hooks.go
Daniel Liu 001bd7ef3b
fix(eth/hooks,contracts): use parent-state randomize data for checkpoint validator verification (#2186)
Read randomize values at the checkpoint parent height during v1 HookValidator and HookVerifyMNs instead of the latest chain state. This fixes a sync-time race where historical checkpoint headers could be rejected with ErrInvalidCheckpointValidators and then accepted later after local state advanced.

The accompanying regression test now derives expected validators independently from HookValidator and the M2 generation path uses a local RNG source, removing CI-only nondeterminism during repeated verification.
2026-03-21 15:26:54 +08:00

346 lines
12 KiB
Go

package hooks
import (
"bytes"
"errors"
"math/big"
"time"
"github.com/XinFinOrg/XDPoSChain/accounts/abi/bind"
"github.com/XinFinOrg/XDPoSChain/common"
xdc_sort "github.com/XinFinOrg/XDPoSChain/common/sort"
"github.com/XinFinOrg/XDPoSChain/consensus"
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS"
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils"
"github.com/XinFinOrg/XDPoSChain/contracts"
contractValidator "github.com/XinFinOrg/XDPoSChain/contracts/validator/contract"
"github.com/XinFinOrg/XDPoSChain/core"
"github.com/XinFinOrg/XDPoSChain/core/state"
"github.com/XinFinOrg/XDPoSChain/core/tracing"
"github.com/XinFinOrg/XDPoSChain/core/types"
"github.com/XinFinOrg/XDPoSChain/core/vm"
"github.com/XinFinOrg/XDPoSChain/eth/util"
"github.com/XinFinOrg/XDPoSChain/log"
"github.com/XinFinOrg/XDPoSChain/params"
)
func AttachConsensusV1Hooks(adaptor *XDPoS.XDPoS, bc *core.BlockChain, chainConfig *params.ChainConfig) {
// Hook scans for bad masternodes and decide to penalty them
adaptor.EngineV1.HookPenalty = func(chain consensus.ChainReader, blockNumberEpoc uint64) ([]common.Address, error) {
canonicalState, err := bc.State()
if canonicalState == nil || err != nil {
log.Crit("Can't get state at head of canonical chain", "head number", bc.CurrentHeader().Number.Uint64(), "err", err)
}
prevEpoc := blockNumberEpoc - chain.Config().XDPoS.Epoch
start := time.Now()
prevHeader := chain.GetHeaderByNumber(prevEpoc)
penSigners := adaptor.GetMasternodes(chain, prevHeader)
if len(penSigners) > 0 {
// Loop for each block to check missing sign.
for i := prevEpoc; i < blockNumberEpoc; i++ {
if i%common.MergeSignRange == 0 || !chainConfig.IsTIP2019(big.NewInt(int64(i))) {
bheader := chain.GetHeaderByNumber(i)
bhash := bheader.Hash()
block := chain.GetBlock(bhash, i)
if len(penSigners) > 0 {
signedMasternodes, err := contracts.GetSignersFromContract(canonicalState, block)
if err != nil {
return nil, err
}
if len(signedMasternodes) > 0 {
// Check signer signed?
for _, signed := range signedMasternodes {
for j, addr := range penSigners {
if signed == addr {
// Remove it from dupSigners.
penSigners = append(penSigners[:j], penSigners[j+1:]...)
}
}
}
}
} else {
break
}
}
}
}
log.Debug("Time Calculated HookPenalty ", "block", blockNumberEpoc, "time", common.PrettyDuration(time.Since(start)))
return penSigners, nil
}
// Hook scans for bad masternodes and decide to penalty them
adaptor.EngineV1.HookPenaltyTIPSigning = func(chain consensus.ChainReader, header *types.Header, candidates []common.Address) ([]common.Address, error) {
prevEpoc := header.Number.Uint64() - chain.Config().XDPoS.Epoch
combackEpoch := uint64(0)
comebackLength := (common.LimitPenaltyEpoch + 1) * chain.Config().XDPoS.Epoch
if header.Number.Uint64() > comebackLength {
combackEpoch = header.Number.Uint64() - comebackLength
}
start := time.Now()
listBlockHash := make([]common.Hash, chain.Config().XDPoS.Epoch)
// get list block hash & stats total created block
statMiners := make(map[common.Address]int)
listBlockHash[0] = header.ParentHash
parentnumber := header.Number.Uint64() - 1
parentHash := header.ParentHash
for i := uint64(1); i < chain.Config().XDPoS.Epoch; i++ {
parentHeader := chain.GetHeader(parentHash, parentnumber)
miner, _ := adaptor.RecoverSigner(parentHeader)
value, exist := statMiners[miner]
if exist {
value = value + 1
} else {
value = 1
}
statMiners[miner] = value
parentHash = parentHeader.ParentHash
parentnumber--
listBlockHash[i] = parentHash
}
// add list not miner to penalties
prevHeader := chain.GetHeaderByNumber(prevEpoc)
preMasternodes := adaptor.GetMasternodes(chain, prevHeader)
penalties := []common.Address{}
for miner, total := range statMiners {
if total < common.MinimunMinerBlockPerEpoch {
log.Debug("Find a node not enough requirement create block", "addr", miner.Hex(), "total", total)
penalties = append(penalties, miner)
}
}
for _, addr := range preMasternodes {
if _, exist := statMiners[addr]; !exist {
log.Debug("Find a node don't create block", "addr", addr.Hex())
penalties = append(penalties, addr)
}
}
// get list check penalties signing block & list master nodes wil comeback
penComebacks := []common.Address{}
if combackEpoch > 0 {
combackHeader := chain.GetHeaderByNumber(combackEpoch)
penalties := common.ExtractAddressFromBytes(combackHeader.Penalties)
for _, penaltie := range penalties {
for _, addr := range candidates {
if penaltie == addr {
penComebacks = append(penComebacks, penaltie)
}
}
}
}
// Loop for each block to check missing sign. with comeback nodes
mapBlockHash := map[common.Hash]bool{}
for i := common.RangeReturnSigner - 1; i >= 0; i-- {
if len(penComebacks) > 0 {
blockNumber := header.Number.Uint64() - uint64(i) - 1
bhash := listBlockHash[i]
if blockNumber%common.MergeSignRange == 0 {
mapBlockHash[bhash] = true
}
signingTxs, ok := adaptor.GetCachedSigningTxs(bhash)
if !ok {
block := chain.GetBlock(bhash, blockNumber)
txs := block.Transactions()
signingTxs = adaptor.CacheSigningTxs(bhash, txs)
}
// Check signer signed?
for _, tx := range signingTxs {
blkHash := common.BytesToHash(tx.Data()[len(tx.Data())-32:])
from := *tx.From()
if mapBlockHash[blkHash] {
for j, addr := range penComebacks {
if from == addr {
// Remove it from dupSigners.
penComebacks = append(penComebacks[:j], penComebacks[j+1:]...)
break
}
}
}
}
} else {
break
}
}
log.Debug("Time Calculated HookPenaltyTIPSigning ", "block", header.Number, "hash", header.Hash().Hex(), "pen comeback nodes", len(penComebacks), "not enough miner", len(penalties), "time", common.PrettyDuration(time.Since(start)))
penalties = append(penalties, penComebacks...)
if chain.Config().IsTIPRandomize(header.Number) {
return penalties, nil
}
return penComebacks, nil
}
// Hook prepares validators M2 for the current epoch at checkpoint block
adaptor.EngineV1.HookValidator = func(header *types.Header, signers []common.Address) ([]byte, error) {
start := time.Now()
validators, err := getValidatorsAtNumber(bc, signers, parentBlockNumber(header))
if err != nil {
return []byte{}, err
}
header.Validators = validators
log.Debug("Time Calculated HookValidator ", "block", header.Number.Uint64(), "time", common.PrettyDuration(time.Since(start)))
return validators, nil
}
// Hook verifies masternodes set
adaptor.EngineV1.HookVerifyMNs = func(header *types.Header, signers []common.Address) error {
number := header.Number.Int64()
if number > 0 && number%common.EpocBlockRandomize == 0 {
start := time.Now()
validators, err := getValidatorsAtNumber(bc, signers, parentBlockNumber(header))
log.Debug("Time Calculated HookVerifyMNs ", "block", header.Number.Uint64(), "time", common.PrettyDuration(time.Since(start)))
if err != nil {
return err
}
if !bytes.Equal(header.Validators, validators) {
return utils.ErrInvalidCheckpointValidators
}
}
return nil
}
/*
HookGetSignersFromContract return list masternode for current state (block)
This is a solution for work around issue return wrong list signers from snapshot
*/
adaptor.EngineV1.HookGetSignersFromContract = func(block common.Hash) ([]common.Address, error) {
client, err := bc.GetClient()
if err != nil {
return nil, err
}
addr := common.MasternodeVotingSMCBinary
validator, err := contractValidator.NewXDCValidator(addr, client)
if err != nil {
return nil, err
}
opts := new(bind.CallOpts)
var (
candidateAddresses []common.Address
candidates []utils.Masternode
)
stateDB, err := bc.StateAt(bc.GetBlockByHash(block).Root())
if err != nil {
return nil, err
}
if stateDB == nil {
return nil, errors.New("nil stateDB in HookGetSignersFromContract")
}
candidateAddresses = stateDB.GetCandidates()
for _, address := range candidateAddresses {
v, err := validator.GetCandidateCap(opts, address)
if err != nil {
return nil, err
}
candidates = append(candidates, utils.Masternode{Address: address, Stake: v})
}
// sort candidates by stake descending
xdc_sort.Slice(candidates, func(i, j int) bool {
return candidates[i].Stake.Cmp(candidates[j].Stake) >= 0
})
if len(candidates) > 150 {
candidates = candidates[:150]
}
result := []common.Address{}
for _, candidate := range candidates {
result = append(result, candidate.Address)
}
return result, nil
}
// Hook calculates reward for masternodes
adaptor.EngineV1.HookReward = func(chain consensus.ChainReader, stateBlock vm.StateDB, parentState *state.StateDB, header *types.Header) (map[string]interface{}, error) {
number := header.Number.Uint64()
rCheckpoint := chain.Config().XDPoS.RewardCheckpoint
foundationWalletAddr := chain.Config().XDPoS.FoundationWalletAddr
if foundationWalletAddr == (common.Address{}) {
log.Error("Foundation Wallet Address is empty", "error", foundationWalletAddr)
return nil, errors.New("foundation Wallet Address is empty")
}
rewards := make(map[string]interface{})
if number > 0 && number-rCheckpoint > 0 && foundationWalletAddr != (common.Address{}) {
start := time.Now()
// Get signers in blockSigner smartcontract.
// Get reward inflation.
chainReward := new(big.Int).Mul(new(big.Int).SetUint64(chain.Config().XDPoS.Reward), new(big.Int).SetUint64(params.Ether))
chainReward = util.RewardInflation(chain, chainReward, number, common.BlocksPerYear)
totalSigner := new(uint64)
signers, err := contracts.GetRewardForCheckpoint(adaptor, chain, header, rCheckpoint, totalSigner)
log.Debug("Time Get Signers", "block", header.Number.Uint64(), "time", common.PrettyDuration(time.Since(start)))
if err != nil {
log.Crit("Fail to get signers for reward checkpoint", "error", err)
}
rewards["signers"] = signers
rewardSigners, err := contracts.CalculateRewardForSigner(chainReward, signers, *totalSigner)
if err != nil {
log.Crit("Fail to calculate reward for signers", "error", err)
}
// Add reward for coin holders.
voterResults := make(map[common.Address]interface{})
if len(signers) > 0 {
for signer, calcReward := range rewardSigners {
rewards, err := contracts.CalculateRewardForHolders(foundationWalletAddr, parentState, signer, calcReward, number)
if err != nil {
log.Crit("Fail to calculate reward for holders.", "error", err)
}
if len(rewards) > 0 {
for holder, reward := range rewards {
stateBlock.AddBalance(holder, reward, tracing.BalanceIncreaseRewardMineBlock)
}
}
voterResults[signer] = rewards
}
}
rewards["rewards"] = voterResults
log.Debug("Time Calculated HookReward ", "block", header.Number.Uint64(), "time", common.PrettyDuration(time.Since(start)))
}
return rewards, nil
}
}
func getValidatorsAtNumber(bc *core.BlockChain, masternodes []common.Address, blockNumber *big.Int) ([]byte, error) {
if bc.Config().XDPoS == nil {
return nil, core.ErrNotXDPoS
}
client, err := bc.GetClient()
if err != nil {
return nil, err
}
// Check m2 exists on chaindb.
// Get secrets and opening at epoc block checkpoint.
var candidates []int64
lenSigners := int64(len(masternodes))
if lenSigners > 0 {
for _, addr := range masternodes {
random, err := contracts.GetRandomizeFromContractAtNumber(client, addr, blockNumber)
if err != nil {
return nil, err
}
candidates = append(candidates, random)
}
// Get randomize m2 list.
m2, err := contracts.GenM2FromRandomize(candidates, lenSigners)
if err != nil {
return nil, err
}
return contracts.BuildValidatorFromM2(m2), nil
}
return nil, core.ErrNotFoundM1
}
func parentBlockNumber(header *types.Header) *big.Int {
if header == nil || header.Number == nil || header.Number.Sign() == 0 {
return nil
}
return new(big.Int).Sub(header.Number, common.Big1)
}