go-ethereum/eth/hooks/engine_v2_hooks.go
wgr523 1089f0b4fe
record total minted API v2 (#1769)
* feat: GetTokenSupply API, total minted and burned

* feat: token supply API finish burned token. rename minted record functions

* fix(api): handle edge case about minus 1 for epoch in token supply

* fix: check both total minted and burned before breaking loop

* style: modify minor style

* style: modify by comment and rebase code

* chore: modify test based on statedb_utils
2025-12-09 19:43:19 +08:00

612 lines
22 KiB
Go

package hooks
import (
"errors"
"fmt"
"math/big"
"slices"
"time"
"github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/common/math"
"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/tracing"
"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, parentHash 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, parentHash)
parentNumber := number.Uint64() - 1
currentHash := parentHash
var round types.Round
// 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
// extract round number from the lastest block
r, err := adaptor.EngineV2.GetRoundNumber(parentHeader)
if err != nil {
log.Error("[V2 Hook Penalty] Fail to get round", "error", err)
return nil, err
}
round = r
break
}
log.Info("[V2 Hook Penalty] parentHeader is nil, wait block to be written 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 written in to disk", "parentNumber", parentNumber)
return []common.Address{}, errors.New("parentHeader is nil")
}
}
for i := uint64(1); ; i++ {
parentHeader := chain.GetHeader(parentHash, parentNumber)
if parentHeader == nil {
log.Error("[HookPenalty] fail to get parent header")
return []common.Address{}, fmt.Errorf("hook penalty fail to get parent header at number: %v, hash: %v", parentNumber, parentHash)
}
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)
}
currentConfig := chain.Config().XDPoS.V2.Config(uint64(round))
// add list not miner to penalties
preMasternodes := adaptor.EngineV2.GetMasternodesByHash(chain, currentHash)
penalties := []common.Address{}
minimunMinerBlockPerEpoch := common.MinimunMinerBlockPerEpoch
if chain.Config().IsTIPUpgradePenalty(number) {
minimunMinerBlockPerEpoch = currentConfig.MinimumMinerBlockPerEpoch
}
for miner, total := range statMiners {
if total < minimunMinerBlockPerEpoch {
log.Info("[HookPenalty] Find a node does not create enough block", "addr", miner.Hex(), "total", total, "require", 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
if !chain.Config().IsTIPUpgradePenalty(number) {
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)
if block != nil {
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)
}
}
}
} else { // after penalty upgrade
limitPenaltyEpoch := 1
if currentConfig.LimitPenaltyEpoch > 0 {
// if non-zero parameter, use it
limitPenaltyEpoch = currentConfig.LimitPenaltyEpoch
}
comebackHeight := uint64(limitPenaltyEpoch)*chain.Config().XDPoS.Epoch + chain.Config().XDPoS.V2.SwitchBlock.Uint64()
if number.Uint64() > comebackHeight {
// penParolees record those who stayed enough epoch of LimitPenaltyEpoch
penParoleeMap := map[common.Address]int{}
// lastPenalty record the last epoch penalties
lastPenalty := []common.Address{}
for i := 0; i < limitPenaltyEpoch; i++ {
pens := adaptor.EngineV2.GetPreviousPenaltyByHash(chain, currentHash, i)
for _, p := range pens {
penParoleeMap[p]++
}
if i == 0 {
// record the last epoch penalties
lastPenalty = pens
}
}
// Loop for each block to check missing sign. with comeback nodes
mapBlockHash := map[common.Hash]bool{}
txSignerMap := map[common.Address]int{}
startRange := int(chain.Config().XDPoS.Epoch) - 1
// to prevent visiting outside index of listBlockHash
if startRange >= len(listBlockHash) {
startRange = len(listBlockHash) - 1
}
for i := startRange; i >= 0; i-- {
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)
if block != nil {
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] {
txSignerMap[from]++
}
}
}
// check addr in lastPenalty, and if they does not meet condition, add them to penalty
for _, p := range lastPenalty {
if penParoleeMap[p] == limitPenaltyEpoch {
// check if this node signs enough
if txSignerMap[p] >= currentConfig.MinimumSigningTx {
continue
}
}
// reaches here means that the node should still stays in penalty list
penalties = append(penalties, p)
}
}
}
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)
epochNum := chain.Config().XDPoS.V2.SwitchEpoch + uint64(round)/chain.Config().XDPoS.Epoch
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, and burned tokens in one epoch
signers, burnedInOneEpoch, 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]
if !chain.Config().IsTIPUpgradeReward(header.Number) {
// Get reward inflation.
originalReward := new(big.Int).Mul(new(big.Int).SetUint64(chain.Config().XDPoS.Reward), new(big.Int).SetUint64(params.Ether))
chainReward := util.RewardInflation(chain, originalReward, 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
}
// Add reward for coin holders.
rewardResults := 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, tracing.BalanceIncreaseRewardMineBlock)
}
}
rewardResults[signer] = rewards
}
rewardsMap["rewards"] = rewardResults
} else {
rewardsMap["signersProtector"] = signers[ProtectorNodeBeneficiary]
rewardsMap["signersObserver"] = signers[ObserverNodeBeneficiary]
rewardSum := new(big.Int)
type rewardWithType struct {
r float64
t Beneficiary
key string
}
for _, rwt := range []rewardWithType{
{currentConfig.MasternodeReward, MasterNodeBeneficiary, "rewards"},
{currentConfig.ProtectorReward, ProtectorNodeBeneficiary, "rewardsProtector"},
{currentConfig.ObserverReward, ObserverNodeBeneficiary, "rewardsObserver"},
} {
originalRewardFloat := new(big.Float).Mul(new(big.Float).SetFloat64(rwt.r), new(big.Float).SetUint64(params.Ether))
originalReward, _ := originalRewardFloat.Int(nil)
chainReward := util.RewardInflation(chain, originalReward, number, common.BlocksPerYear)
rewardSigners, err := CalculateRewardForSignerFixed(chainReward, signers[rwt.t])
if err != nil {
log.Error("[HookReward] Fail to calculate reward type 0 for masternode, 1 for protector, 2 for observer", "error", err, "type", rwt.t)
return nil, err
}
// Add reward for coin holders.
rewardResults := 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, tracing.BalanceIncreaseRewardMineBlock)
rewardSum.Add(rewardSum, reward)
}
}
rewardResults[signer] = rewards
}
rewardsMap[rwt.key] = rewardResults
}
// record the total reward into state db
totalMinted := new(big.Int)
totalBurned := new(big.Int)
nonce := stateBlock.GetNonce(common.MintedRecordAddressBinary)
if nonce == 0 {
// initialize MintedRecordAddress
stateBlock.PutMintedRecordOnsetEpoch(common.Uint64ToHash(epochNum))
stateBlock.PutMintedRecordOnsetBlock(common.Uint64ToHash(number))
} else {
epochNumIter := epochNum
for epochNumIter > 0 {
epochNumIter--
totalMinted = stateBlock.GetPostMinted(epochNumIter).Big()
totalBurned = stateBlock.GetPostBurned(epochNumIter).Big()
if totalMinted.Sign() != 0 || totalBurned.Sign() != 0 {
// if previous epoch has non-zero total minted or non-zero total burned, break the loop
break
}
}
}
totalMinted.Add(totalMinted, rewardSum)
// if overflow, set to maxU256 and log a warning
if totalMinted.Cmp(math.MaxBig256) > 0 {
totalMinted.Set(math.MaxBig256)
log.Warn("[HookReward] total minted overflow max u256")
}
log.Debug("[HookReward] total minted in hook", "value", totalMinted)
stateBlock.PutPostMinted(epochNum, common.BigToHash(totalMinted))
stateBlock.PutPostRewardBlock(epochNum, common.Uint64ToHash(number))
// Record total burned into statedb
totalBurned.Add(totalBurned, burnedInOneEpoch)
// if overflow, set to maxU256 and log a warning
if totalBurned.Cmp(math.MaxBig256) > 0 {
totalBurned.Set(math.MaxBig256)
log.Warn("[HookReward] total burned overflow max u256")
}
stateBlock.PutPostBurned(epochNum, common.BigToHash(totalBurned))
// Increment nonce so that statedb does not treat it as empty account
stateBlock.IncrementMintedRecordNonce()
}
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, *big.Int, 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{}
burnedInOneEpoch := new(big.Int)
// prevent overflow
if number == 0 {
return signers, burnedInOneEpoch, 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-- {
parentHash := h.ParentHash
h = chain.GetHeader(parentHash, i)
if h == nil {
log.Error("[GetSigningTxCount] fail to get header", "number", i, "hash", parentHash)
return nil, burnedInOneEpoch, fmt.Errorf("fail to get header in GetSigningTxCount at number: %v, hash: %v", i, parentHash)
}
if epochCount == 0 && h.BaseFee != nil {
// add burned for the first epoch during loop
burnedInOneEpoch.Add(burnedInOneEpoch, new(big.Int).Mul(h.BaseFee, new(big.Int).SetUint64(h.GasUsed)))
}
isEpochSwitch, _, err := c.IsEpochSwitch(h)
if err != nil {
return nil, burnedInOneEpoch, 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 := parentState.GetCandidates()
var ms []utils.Masternode
for _, candidate := range candidates {
// ignore "0x0000000000000000000000000000000000000000"
if !candidate.IsZero() {
v := parentState.GetCandidateCap(candidate)
ms = append(ms, utils.Masternode{Address: candidate, Stake: v})
}
}
slices.SortStableFunc(ms, func(a, b utils.Masternode) int {
return b.Stake.Cmp(a.Stake)
})
// 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
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 if len(observer) < currentConfig.MaxObverserNodes {
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)
if block != nil {
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, burnedInOneEpoch, 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, burnedInOneEpoch, 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
}
// Calculate reward for signers with fixed reward.
func CalculateRewardForSignerFixed(chainReward *big.Int, signers map[common.Address]*RewardLog) (map[common.Address]*big.Int, error) {
resultSigners := make(map[common.Address]*big.Int)
// Add reward for signers.
for signer, rLog := range signers {
// Add reward for signer.
calcReward := new(big.Int).SetBytes(chainReward.Bytes())
rLog.Reward = calcReward
resultSigners[signer] = calcReward
}
log.Info("Signers data", "percapitaReward", chainReward)
for addr, signer := range signers {
log.Debug("Signer reward", "signer", addr, "sign", signer.Sign, "reward", signer.Reward)
}
return resultSigners, nil
}