go-ethereum/eth/hooks/engine_v2_hooks.go
wgr523 c36e4d54c0
Upgrade reward 2.0 (#865)
* refactor: reward hook gets prepared for upgrade

* feat: new reward hook, config change, unit tests

* add missing code

* feat: filter penalty in reward. add unit test

* update constant and config

---------

Co-authored-by: liam.lai <liam.lai@us>
2025-03-02 02:44:34 -08:00

477 lines
17 KiB
Go

package hooks
import (
"errors"
"math/big"
"time"
"github.com/XinFinOrg/XDPoSChain/common"
"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"
"github.com/XinFinOrg/XDPoSChain/core"
"github.com/XinFinOrg/XDPoSChain/core/state"
"github.com/XinFinOrg/XDPoSChain/core/types"
"github.com/XinFinOrg/XDPoSChain/eth/util"
"github.com/XinFinOrg/XDPoSChain/log"
"github.com/XinFinOrg/XDPoSChain/params"
)
// Declaring an enum type Beneficiary of reward
type Beneficiary int
// Enumerating reward beneficiary
const (
MasterNodeBeneficiary Beneficiary = iota
ProtectorNodeBeneficiary
ObserverNodeBeneficiary
)
type RewardLog struct {
Sign uint64 `json:"sign"`
Reward *big.Int `json:"reward"`
}
func AttachConsensusV2Hooks(adaptor *XDPoS.XDPoS, bc *core.BlockChain, chainConfig *params.ChainConfig) {
// Hook scans for bad masternodes and decide to penalty them
adaptor.EngineV2.HookPenalty = func(chain consensus.ChainReader, number *big.Int, currentHash common.Hash, candidates []common.Address) ([]common.Address, error) {
start := time.Now()
listBlockHash := []common.Hash{}
// get list block hash & stats total created block
statMiners := make(map[common.Address]int)
listBlockHash = append(listBlockHash, currentHash)
parentNumber := number.Uint64() - 1
parentHash := currentHash
// check and wait the latest block is already in the disk
// sometimes blocks are yet inserted into block
for timeout := 0; ; timeout++ {
parentHeader := chain.GetHeader(parentHash, parentNumber)
if parentHeader != nil { // found the latest block in the disk
break
}
log.Info("[V2 Hook Penalty] parentHeader is nil, wait block to be writen in disk", "parentNumber", parentNumber)
time.Sleep(time.Second) // 1s
if timeout > 30 { // wait over 30s
log.Error("[V2 Hook Penalty] parentHeader is nil, wait too long not writen in to disk", "parentNumber", parentNumber)
return []common.Address{}, errors.New("parentHeader is nil")
}
}
for i := uint64(1); ; i++ {
parentHeader := chain.GetHeader(parentHash, parentNumber)
isEpochSwitch, _, err := adaptor.EngineV2.IsEpochSwitch(parentHeader)
if err != nil {
log.Error("[HookPenalty] isEpochSwitch", "err", err)
return []common.Address{}, err
}
if isEpochSwitch {
break
}
miner := parentHeader.Coinbase // we can directly use coinbase, since it's verified
_, exist := statMiners[miner]
if exist {
statMiners[miner]++
} else {
statMiners[miner] = 1
}
parentNumber--
parentHash = parentHeader.ParentHash
listBlockHash = append(listBlockHash, parentHash)
}
// add list not miner to penalties
preMasternodes := adaptor.EngineV2.GetMasternodesByHash(chain, currentHash)
penalties := []common.Address{}
for miner, total := range statMiners {
if total < common.MinimunMinerBlockPerEpoch {
log.Info("[HookPenalty] Find a node does not create enough block", "addr", miner.Hex(), "total", total, "require", common.MinimunMinerBlockPerEpoch)
penalties = append(penalties, miner)
}
}
for _, addr := range preMasternodes {
if _, exist := statMiners[addr]; !exist {
log.Info("[HookPenalty] Find a node do not create any block", "addr", addr.Hex())
penalties = append(penalties, addr)
}
}
// get list check penalties signing block & list master nodes wil comeback
// start to calc comeback at v2 block + limitPenaltyEpochV2 to avoid reading v1 blocks
comebackHeight := (common.LimitPenaltyEpochV2+1)*chain.Config().XDPoS.Epoch + chain.Config().XDPoS.V2.SwitchBlock.Uint64()
penComebacks := []common.Address{}
if number.Uint64() > comebackHeight {
pens := adaptor.EngineV2.GetPreviousPenaltyByHash(chain, currentHash, common.LimitPenaltyEpochV2)
for _, p := range pens {
for _, addr := range candidates {
if p == addr {
log.Info("[HookPenalty] get previous penalty node and add into comeback list", "addr", addr)
penComebacks = append(penComebacks, p)
break
}
}
}
}
// Loop for each block to check missing sign. with comeback nodes
mapBlockHash := map[common.Hash]bool{}
startRange := common.RangeReturnSigner - 1
// to prevent visiting outside index of listBlockHash
if startRange >= len(listBlockHash) {
startRange = len(listBlockHash) - 1
}
for i := startRange; i >= 0; i-- {
if len(penComebacks) == 0 {
break
}
blockNumber := 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
}
}
}
}
}
for _, comeback := range penComebacks {
ok := true
for _, p := range penalties {
if p == comeback {
ok = false
break
}
}
if ok {
penalties = append(penalties, comeback)
}
}
for i, p := range penalties {
log.Info("[HookPenalty] Final penalty list", "index", i, "addr", p)
}
log.Info("[HookPenalty] Time Calculated HookPenaltyV2 ", "block", number, "time", common.PrettyDuration(time.Since(start)))
return penalties, nil
}
// Hook calculates reward for masternodes
adaptor.EngineV2.HookReward = func(chain consensus.ChainReader, stateBlock *state.StateDB, parentState *state.StateDB, header *types.Header) (map[string]interface{}, error) {
number := header.Number.Uint64()
foundationWalletAddr := chain.Config().XDPoS.FoudationWalletAddr
if foundationWalletAddr == (common.Address{}) {
log.Error("Foundation Wallet Address is empty", "error", foundationWalletAddr)
return nil, errors.New("foundation wallet address is empty")
}
rewardsMap := make(map[string]interface{})
// skip hook reward if this is the first v2
if number == chain.Config().XDPoS.V2.SwitchBlock.Uint64()+1 {
return rewardsMap, nil
}
start := time.Now()
round, err := adaptor.EngineV2.GetRoundNumber(header)
if err != nil {
log.Error("[HookReward] Fail to get round", "error", err)
return nil, err
}
currentConfig := chain.Config().XDPoS.V2.Config(uint64(round))
// Get signers/signing tx count
signers, err := GetSigningTxCount(adaptor, chain, header, parentState, currentConfig)
log.Debug("Time Get Signers", "block", header.Number.Uint64(), "time", common.PrettyDuration(time.Since(start)))
if err != nil {
log.Error("[HookReward] Fail to get signers count for reward checkpoint", "error", err)
return nil, err
}
rewardsMap["signers"] = signers[MasterNodeBeneficiary]
rewardSigners := make(map[common.Address]*big.Int)
rewardSignersProtector := make(map[common.Address]*big.Int)
rewardSignersObserver := make(map[common.Address]*big.Int)
if !chain.Config().IsTIPUpgradeReward(header.Number) {
// 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)
rewardSigners, err = CalculateRewardForSigner(chainReward, signers[MasterNodeBeneficiary])
if err != nil {
log.Error("[HookReward] Fail to calculate reward for masternode", "error", err)
return nil, err
}
} else {
rewardsMap["signersProtector"] = signers[ProtectorNodeBeneficiary]
rewardsMap["signersObserver"] = signers[ObserverNodeBeneficiary]
// Masternode rewards
chainReward := new(big.Int).Mul(new(big.Int).SetUint64(currentConfig.MasternodeReward), new(big.Int).SetUint64(params.Ether))
chainReward = util.RewardInflation(chain, chainReward, number, common.BlocksPerYear)
rewardSigners, err = CalculateRewardForSigner(chainReward, signers[MasterNodeBeneficiary])
if err != nil {
log.Error("[HookReward] Fail to calculate reward for masternode", "error", err)
return nil, err
}
// Protector rewards
chainReward = new(big.Int).Mul(new(big.Int).SetUint64(currentConfig.ProtectorReward), new(big.Int).SetUint64(params.Ether))
chainReward = util.RewardInflation(chain, chainReward, number, common.BlocksPerYear)
rewardSignersProtector, err = CalculateRewardForSigner(chainReward, signers[ProtectorNodeBeneficiary])
if err != nil {
log.Error("[HookReward] Fail to calculate reward for protector", "error", err)
return nil, err
}
// Observer rewards
chainReward = new(big.Int).Mul(new(big.Int).SetUint64(currentConfig.ObserverReward), new(big.Int).SetUint64(params.Ether))
chainReward = util.RewardInflation(chain, chainReward, number, common.BlocksPerYear)
rewardSignersObserver, err = CalculateRewardForSigner(chainReward, signers[ObserverNodeBeneficiary])
if err != nil {
log.Error("[HookReward] Fail to calculate reward for observer", "error", err)
return nil, err
}
}
// Add reward for coin holders.
voterResults := make(map[common.Address]interface{})
for signer, calcReward := range rewardSigners {
rewards, err := contracts.CalculateRewardForHolders(foundationWalletAddr, parentState, signer, calcReward, number)
if err != nil {
log.Error("[HookReward] Fail to calculate reward for holders.", "error", err)
return nil, err
}
if len(rewards) > 0 {
for holder, reward := range rewards {
stateBlock.AddBalance(holder, reward)
}
}
voterResults[signer] = rewards
}
rewardsMap["rewards"] = voterResults
voterResultsProtector := make(map[common.Address]interface{})
for signer, calcReward := range rewardSignersProtector {
rewards, err := contracts.CalculateRewardForHolders(foundationWalletAddr, parentState, signer, calcReward, number)
if err != nil {
log.Error("[HookReward] Fail to calculate reward for holders.", "error", err)
return nil, err
}
if len(rewards) > 0 {
for holder, reward := range rewards {
stateBlock.AddBalance(holder, reward)
}
}
voterResultsProtector[signer] = rewards
}
if len(voterResultsProtector) > 0 {
rewardsMap["rewardsProtector"] = voterResultsProtector
}
voterResultsObserver := make(map[common.Address]interface{})
for signer, calcReward := range rewardSignersObserver {
rewards, err := contracts.CalculateRewardForHolders(foundationWalletAddr, parentState, signer, calcReward, number)
if err != nil {
log.Error("[HookReward] Fail to calculate reward for holders.", "error", err)
return nil, err
}
if len(rewards) > 0 {
for holder, reward := range rewards {
stateBlock.AddBalance(holder, reward)
}
}
voterResultsObserver[signer] = rewards
}
if len(voterResultsObserver) > 0 {
rewardsMap["rewardsObserver"] = voterResultsObserver
}
log.Debug("Time Calculated HookReward ", "block", header.Number.Uint64(), "time", common.PrettyDuration(time.Since(start)))
return rewardsMap, nil
}
}
// get signing transaction sender count
func GetSigningTxCount(c *XDPoS.XDPoS, chain consensus.ChainReader, header *types.Header, parentState *state.StateDB, currentConfig *params.V2Config) (map[Beneficiary]map[common.Address]*RewardLog, error) {
// header should be a new epoch switch block
number := header.Number.Uint64()
rewardEpochCount := 2
signEpochCount := 1
signers := make(map[Beneficiary]map[common.Address]*RewardLog)
signers[MasterNodeBeneficiary] = make(map[common.Address]*RewardLog)
signers[ProtectorNodeBeneficiary] = make(map[common.Address]*RewardLog)
signers[ObserverNodeBeneficiary] = make(map[common.Address]*RewardLog)
mapBlkHash := map[uint64]common.Hash{}
// prevent overflow
if number == 0 {
return signers, nil
}
data := make(map[common.Hash][]common.Address)
epochCount := 0
var startBlockNumber, endBlockNumber uint64
nodesToKeep := make(map[Beneficiary][]common.Address)
h := header
for i := number - 1; ; i-- {
h = chain.GetHeader(h.ParentHash, i)
isEpochSwitch, _, err := c.IsEpochSwitch(h)
if err != nil {
return nil, err
}
if isEpochSwitch && i != chain.Config().XDPoS.V2.SwitchBlock.Uint64()+1 {
epochCount += 1
if epochCount == signEpochCount {
endBlockNumber = h.Number.Uint64() - 1
}
if epochCount == rewardEpochCount {
startBlockNumber = h.Number.Uint64() + 1
nodesToKeep[MasterNodeBeneficiary] = c.GetMasternodesFromCheckpointHeader(h)
// in reward upgrade, add protector and observer nodes
if chain.Config().IsTIPUpgradeReward(header.Number) {
candidates := state.GetCandidates(parentState)
var ms []utils.Masternode
for _, candidate := range candidates {
// ignore "0x0000000000000000000000000000000000000000"
if !candidate.IsZero() {
v := state.GetCandidateCap(parentState, candidate)
ms = append(ms, utils.Masternode{Address: candidate, Stake: v})
}
}
sort.Slice(ms, func(i, j int) bool {
return ms[i].Stake.Cmp(ms[j].Stake) >= 0
})
// find penalty and filter them out
penalties := common.ExtractAddressFromBytes(h.Penalties)
filterMap := make(map[common.Address]struct{})
for _, addr := range penalties {
filterMap[addr] = struct{}{}
}
for _, addr := range nodesToKeep[MasterNodeBeneficiary] {
filterMap[addr] = struct{}{}
}
// find top candidates
// maxMNP := currentConfig.MaxMasternodes + currentConfig.MaxProtectorNodes
protector := []common.Address{}
observer := []common.Address{}
for _, node := range ms {
if _, ok := filterMap[node.Address]; ok {
continue
}
if len(protector) < currentConfig.MaxProtectorNodes {
protector = append(protector, node.Address)
} else {
observer = append(observer, node.Address)
}
}
nodesToKeep[ProtectorNodeBeneficiary] = protector
nodesToKeep[ObserverNodeBeneficiary] = observer
}
break
}
}
mapBlkHash[i] = h.Hash()
signingTxs, ok := c.GetCachedSigningTxs(h.Hash())
if !ok {
log.Debug("Failed get from cached", "hash", h.Hash().String(), "number", i)
block := chain.GetBlock(h.Hash(), i)
txs := block.Transactions()
signingTxs = c.CacheSigningTxs(h.Hash(), txs)
}
for _, tx := range signingTxs {
blkHash := common.BytesToHash(tx.Data()[len(tx.Data())-32:])
from := *tx.From()
data[blkHash] = append(data[blkHash], from)
}
// prevent overflow
if i == 0 {
return signers, nil
}
}
for i := startBlockNumber; i <= endBlockNumber; i++ {
if i%common.MergeSignRange == 0 {
addrs := data[mapBlkHash[i]]
// Filter duplicate address.
if len(addrs) > 0 {
addrSigners := make(map[Beneficiary]map[common.Address]bool)
addrSigners[MasterNodeBeneficiary] = make(map[common.Address]bool)
addrSigners[ProtectorNodeBeneficiary] = make(map[common.Address]bool)
addrSigners[ObserverNodeBeneficiary] = make(map[common.Address]bool)
for _, addr := range addrs {
for _, beneficiary := range []Beneficiary{MasterNodeBeneficiary, ProtectorNodeBeneficiary, ObserverNodeBeneficiary} {
if _, ok := nodesToKeep[beneficiary]; ok {
for _, protector := range nodesToKeep[beneficiary] {
if addr == protector {
if _, ok := addrSigners[beneficiary][addr]; !ok {
addrSigners[beneficiary][addr] = true
}
break
}
}
}
}
}
for _, beneficiary := range []Beneficiary{MasterNodeBeneficiary, ProtectorNodeBeneficiary, ObserverNodeBeneficiary} {
for addr := range addrSigners[beneficiary] {
_, exist := signers[beneficiary][addr]
if exist {
signers[beneficiary][addr].Sign++
} else {
signers[beneficiary][addr] = &RewardLog{Sign: 1, Reward: new(big.Int)}
}
}
}
}
}
}
log.Info("Calculate reward at checkpoint", "startBlock", startBlockNumber, "endBlock", endBlockNumber)
return signers, nil
}
// Calculate reward for signers.
func CalculateRewardForSigner(chainReward *big.Int, signers map[common.Address]*RewardLog) (map[common.Address]*big.Int, error) {
totalSignerCount := uint64(0)
for _, rLog := range signers {
totalSignerCount += rLog.Sign
}
resultSigners := make(map[common.Address]*big.Int)
// Add reward for signers.
if totalSignerCount > 0 {
for signer, rLog := range signers {
// Add reward for signer.
calcReward := new(big.Int)
calcReward.Div(chainReward, new(big.Int).SetUint64(totalSignerCount))
calcReward.Mul(calcReward, new(big.Int).SetUint64(rLog.Sign))
rLog.Reward = calcReward
resultSigners[signer] = calcReward
}
}
log.Info("Signers data", "totalSigner", totalSignerCount, "totalReward", chainReward)
for addr, signer := range signers {
log.Debug("Signer reward", "signer", addr, "sign", signer.Sign, "reward", signer.Reward)
}
return resultSigners, nil
}