From 1089f0b4fe3bcd36fdab30b249e0523b11b79169 Mon Sep 17 00:00:00 2001 From: wgr523 Date: Tue, 9 Dec 2025 19:43:19 +0800 Subject: [PATCH] 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 --- .../tests/engine_v2_tests/reward_test.go | 41 +++++---- core/state/statedb_utils.go | 82 +++++++++++------ eth/hooks/engine_v2_hooks.go | 66 +++++++++----- internal/ethapi/api.go | 88 ++++++++++++++++--- internal/web3ext/web3ext.go | 6 +- 5 files changed, 203 insertions(+), 80 deletions(-) diff --git a/consensus/tests/engine_v2_tests/reward_test.go b/consensus/tests/engine_v2_tests/reward_test.go index fc9dbd24c4..ceb7913720 100644 --- a/consensus/tests/engine_v2_tests/reward_test.go +++ b/consensus/tests/engine_v2_tests/reward_test.go @@ -144,21 +144,22 @@ func TestHookRewardV2SplitReward(t *testing.T) { assert.Equal(t, 2, len(result)) // two signing account, 3 txs, reward is split by 1:2 (total reward is 250...000) for addr, x := range result { - if addr == acc1Addr { + switch addr { + case acc1Addr: r := x.(map[common.Address]*big.Int) owner := parentState.GetCandidateOwner(addr) a, _ := big.NewInt(0).SetString("149999999999999999999", 10) assert.Zero(t, a.Cmp(r[owner])) b, _ := big.NewInt(0).SetString("16666666666666666666", 10) assert.Zero(t, b.Cmp(r[config.XDPoS.FoudationWalletAddr])) - } else if addr == signer { + case signer: r := x.(map[common.Address]*big.Int) owner := parentState.GetCandidateOwner(addr) a, _ := big.NewInt(0).SetString("74999999999999999999", 10) assert.Zero(t, a.Cmp(r[owner])) b, _ := big.NewInt(0).SetString("8333333333333333333", 10) assert.Zero(t, b.Cmp(r[config.XDPoS.FoudationWalletAddr])) - } else { + default: assert.Fail(t, "wrong reward") } } @@ -226,21 +227,22 @@ func TestHookRewardAfterUpgrade(t *testing.T) { assert.Equal(t, 2, len(result)) // two signing account, both get fixed reward for addr, x := range result { - if addr == acc1Addr { + switch addr { + case acc1Addr: r := x.(map[common.Address]*big.Int) owner := parentState.GetCandidateOwner(addr) a, _ := big.NewInt(0).SetString("450000000000000000000", 10) assert.Zero(t, a.Cmp(r[owner]), "real reward is", r[owner]) b, _ := big.NewInt(0).SetString("50000000000000000000", 10) assert.Zero(t, b.Cmp(r[config.XDPoS.FoudationWalletAddr]), "real reward is", r[config.XDPoS.FoudationWalletAddr]) - } else if addr == signer { + case signer: r := x.(map[common.Address]*big.Int) owner := parentState.GetCandidateOwner(addr) a, _ := big.NewInt(0).SetString("450000000000000000000", 10) assert.Zero(t, a.Cmp(r[owner]), "real reward is", r[owner]) b, _ := big.NewInt(0).SetString("50000000000000000000", 10) assert.Zero(t, b.Cmp(r[config.XDPoS.FoudationWalletAddr]), "real reward is", r[config.XDPoS.FoudationWalletAddr]) - } else { + default: assert.Fail(t, "wrong reward") } } @@ -265,21 +267,22 @@ func TestHookRewardAfterUpgrade(t *testing.T) { // 2 protector both get fixed reward assert.Equal(t, 2, len(resultProtector)) for addr, x := range resultProtector { - if addr == protector1Addr { + switch addr { + case protector1Addr: r := x.(map[common.Address]*big.Int) owner := parentState.GetCandidateOwner(addr) a, _ := big.NewInt(0).SetString("360000000000000000000", 10) assert.Zero(t, a.Cmp(r[owner]), "real reward is", r[owner]) b, _ := big.NewInt(0).SetString("40000000000000000000", 10) assert.Zero(t, b.Cmp(r[config.XDPoS.FoudationWalletAddr]), "real reward is", r[config.XDPoS.FoudationWalletAddr]) - } else if addr == protector2Addr { + case protector2Addr: r := x.(map[common.Address]*big.Int) owner := parentState.GetCandidateOwner(addr) a, _ := big.NewInt(0).SetString("360000000000000000000", 10) assert.Zero(t, a.Cmp(r[owner]), "real reward is", r[owner]) b, _ := big.NewInt(0).SetString("40000000000000000000", 10) assert.Zero(t, b.Cmp(r[config.XDPoS.FoudationWalletAddr]), "real reward is", r[config.XDPoS.FoudationWalletAddr]) - } else { + default: assert.Fail(t, "wrong reward") } } @@ -295,11 +298,17 @@ func TestHookRewardAfterUpgrade(t *testing.T) { b, _ := big.NewInt(0).SetString("30012500000000000000", 10) // this value tests the float64 reward assert.Zero(t, b.Cmp(r[config.XDPoS.FoudationWalletAddr]), "real reward is", r[config.XDPoS.FoudationWalletAddr]) } - totalMinted := statedb.GetTotalMinted().Big() - totalExpect, _ := big.NewInt(0).SetString("2100125000000000000000", 10) - assert.Zero(t, totalMinted.Cmp(totalExpect), "statedb records wrong total minted") - lastEpochNum := statedb.GetLastEpochNum().Big().Int64() - assert.Equal(t, 3, int(lastEpochNum)) + epochNum := uint64(3) + totalMinted := statedb.GetPostMinted(epochNum).Big() + expectMinted, _ := big.NewInt(0).SetString("2100125000000000000000", 10) + assert.Zero(t, totalMinted.Cmp(expectMinted), "statedb records wrong total minted") + blockNum := statedb.GetPostRewardBlock(epochNum).Big().Int64() + assert.Equal(t, 2700, int(blockNum)) + onsetBlock := statedb.GetMintedRecordOnsetBlock().Big().Int64() + assert.Equal(t, 2700, int(onsetBlock)) + totalBurned := statedb.GetPostBurned(epochNum).Big().Int64() + // since no EIP 1559, so no burned + assert.Zero(t, totalBurned, "statedb records wrong total burned") common.TIPUpgradeReward = backup } @@ -364,9 +373,9 @@ func TestFinalizeAfterUpgrade(t *testing.T) { assert.Nil(t, err) // the recorded reward cannot be zero - minted := statedbAfterFinalize.GetTotalMinted() + epochNum := uint64(3) + minted := statedbAfterFinalize.GetPostMinted(epochNum) assert.False(t, minted.IsZero()) - t.Log("total minted", minted) common.TIPUpgradeReward = backup } diff --git a/core/state/statedb_utils.go b/core/state/statedb_utils.go index cf101c6beb..811a485870 100644 --- a/core/state/statedb_utils.go +++ b/core/state/statedb_utils.go @@ -152,34 +152,62 @@ func (s *StateDB) GetVoterCap(candidate, voter common.Address) *big.Int { return ret.Big() } -var ( - slotMintedRecordTotalMinted uint64 = 0 - slotMintedRecordLastEpochNum uint64 = 1 -) - -func (s *StateDB) GetTotalMinted() common.Hash { - hash := GetLocSimpleVariable(slotMintedRecordTotalMinted) - totalMinted := s.GetState(common.MintedRecordAddressBinary, hash) - return totalMinted -} - -func (s *StateDB) PutTotalMinted(value common.Hash) { - hash := GetLocSimpleVariable(slotMintedRecordTotalMinted) - s.SetState(common.MintedRecordAddressBinary, hash, value) -} - -func (s *StateDB) GetLastEpochNum() common.Hash { - hash := GetLocSimpleVariable(slotMintedRecordLastEpochNum) - totalMinted := s.GetState(common.MintedRecordAddressBinary, hash) - return totalMinted -} - -func (s *StateDB) PutLastEpochNum(value common.Hash) { - hash := GetLocSimpleVariable(slotMintedRecordLastEpochNum) - s.SetState(common.MintedRecordAddressBinary, hash, value) -} - func (s *StateDB) IncrementMintedRecordNonce() { nonce := s.GetNonce(common.MintedRecordAddressBinary) s.SetNonce(common.MintedRecordAddressBinary, nonce+1) } + +var ( + // Storage slot locations (32-byte keys) within MintedRecord SMC + slotMintedRecordOnsetEpoch = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001") + slotMintedRecordOnsetBlock = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000002") + slotMintedRecordPostMintedBase, _ = new(big.Int).SetString("0x0100000000000000000000000000000000000000000000000000000000000000", 0) + slotMintedRecordPostBurnedBase, _ = new(big.Int).SetString("0x0200000000000000000000000000000000000000000000000000000000000000", 0) + slotMintedRecordPostRewardBlockBase, _ = new(big.Int).SetString("0x0300000000000000000000000000000000000000000000000000000000000000", 0) +) + +func (s *StateDB) GetMintedRecordOnsetEpoch() common.Hash { + return s.GetState(common.MintedRecordAddressBinary, slotMintedRecordOnsetEpoch) +} + +func (s *StateDB) PutMintedRecordOnsetEpoch(value common.Hash) { + s.SetState(common.MintedRecordAddressBinary, slotMintedRecordOnsetEpoch, value) +} + +func (s *StateDB) GetMintedRecordOnsetBlock() common.Hash { + return s.GetState(common.MintedRecordAddressBinary, slotMintedRecordOnsetBlock) +} + +func (s *StateDB) PutMintedRecordOnsetBlock(value common.Hash) { + s.SetState(common.MintedRecordAddressBinary, slotMintedRecordOnsetBlock, value) +} + +func (s *StateDB) GetPostMinted(epoch uint64) common.Hash { + hash := common.BigToHash(new(big.Int).Add(slotMintedRecordPostMintedBase, new(big.Int).SetUint64(epoch))) + return s.GetState(common.MintedRecordAddressBinary, hash) +} + +func (s *StateDB) PutPostMinted(epoch uint64, value common.Hash) { + hash := common.BigToHash(new(big.Int).Add(slotMintedRecordPostMintedBase, new(big.Int).SetUint64(epoch))) + s.SetState(common.MintedRecordAddressBinary, hash, value) +} + +func (s *StateDB) GetPostBurned(epoch uint64) common.Hash { + hash := common.BigToHash(new(big.Int).Add(slotMintedRecordPostBurnedBase, new(big.Int).SetUint64(epoch))) + return s.GetState(common.MintedRecordAddressBinary, hash) +} + +func (s *StateDB) PutPostBurned(epoch uint64, value common.Hash) { + hash := common.BigToHash(new(big.Int).Add(slotMintedRecordPostBurnedBase, new(big.Int).SetUint64(epoch))) + s.SetState(common.MintedRecordAddressBinary, hash, value) +} + +func (s *StateDB) GetPostRewardBlock(epoch uint64) common.Hash { + hash := common.BigToHash(new(big.Int).Add(slotMintedRecordPostRewardBlockBase, new(big.Int).SetUint64(epoch))) + return s.GetState(common.MintedRecordAddressBinary, hash) +} + +func (s *StateDB) PutPostRewardBlock(epoch uint64, value common.Hash) { + hash := common.BigToHash(new(big.Int).Add(slotMintedRecordPostRewardBlockBase, new(big.Int).SetUint64(epoch))) + s.SetState(common.MintedRecordAddressBinary, hash, value) +} diff --git a/eth/hooks/engine_v2_hooks.go b/eth/hooks/engine_v2_hooks.go index d1f8ac1659..1ecfc6fc4a 100644 --- a/eth/hooks/engine_v2_hooks.go +++ b/eth/hooks/engine_v2_hooks.go @@ -8,6 +8,7 @@ import ( "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" @@ -285,8 +286,8 @@ func AttachConsensusV2Hooks(adaptor *XDPoS.XDPoS, bc *core.BlockChain, chainConf return nil, err } currentConfig := chain.Config().XDPoS.V2.Config(uint64(round)) - // Get signers/signing tx count - signers, err := GetSigningTxCount(adaptor, chain, header, parentState, currentConfig) + // 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 { @@ -361,24 +362,43 @@ func AttachConsensusV2Hooks(adaptor *XDPoS.XDPoS, bc *core.BlockChain, chainConf rewardsMap[rwt.key] = rewardResults } // record the total reward into state db - totalMinted := stateBlock.GetTotalMinted().Big() - lastEpochNum := stateBlock.GetLastEpochNum() - if lastEpochNum.IsZero() { - // if `lastEpochNum` is zero, the total minted has not included tokens before TIPUpgradeReward - // calculate the tokens before TIPUpgradeReward and set to totalMinted - // for now no-do + 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) - bigPower256 := new(big.Int).Lsh(big.NewInt(1), 256) - bigMaxU256 := new(big.Int).Sub(bigPower256, big.NewInt(1)) // if overflow, set to maxU256 and log a warning - if totalMinted.Cmp(bigMaxU256) >= 0 { - totalMinted.Set(bigMaxU256) + 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.PutTotalMinted(common.BigToHash(totalMinted)) - stateBlock.PutLastEpochNum(common.Uint64ToHash(epochNum)) + 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() } @@ -388,7 +408,7 @@ func AttachConsensusV2Hooks(adaptor *XDPoS.XDPoS, bc *core.BlockChain, chainConf } // 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) { +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 @@ -400,9 +420,11 @@ func GetSigningTxCount(c *XDPoS.XDPoS, chain consensus.ChainReader, header *type mapBlkHash := map[uint64]common.Hash{} + burnedInOneEpoch := new(big.Int) + // prevent overflow if number == 0 { - return signers, nil + return signers, burnedInOneEpoch, nil } data := make(map[common.Hash][]common.Address) @@ -417,11 +439,15 @@ func GetSigningTxCount(c *XDPoS.XDPoS, chain consensus.ChainReader, header *type h = chain.GetHeader(parentHash, i) if h == nil { log.Error("[GetSigningTxCount] fail to get header", "number", i, "hash", parentHash) - return nil, fmt.Errorf("fail to get header in GetSigningTxCount at number: %v, hash: %v", i, 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, err + return nil, burnedInOneEpoch, err } if isEpochSwitch && i != chain.Config().XDPoS.V2.SwitchBlock.Uint64()+1 { epochCount += 1 @@ -490,7 +516,7 @@ func GetSigningTxCount(c *XDPoS.XDPoS, chain consensus.ChainReader, header *type } // prevent overflow if i == 0 { - return signers, nil + return signers, burnedInOneEpoch, nil } } @@ -535,7 +561,7 @@ func GetSigningTxCount(c *XDPoS.XDPoS, chain consensus.ChainReader, header *type log.Info("Calculate reward at checkpoint", "startBlock", startBlockNumber, "endBlock", endBlockNumber) - return signers, nil + return signers, burnedInOneEpoch, nil } // Calculate reward for signers. diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 02ea5866d9..c6ab68551c 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -2617,25 +2617,85 @@ func (api *BlockChainAPI) GetStakerROIMasternode(masternode common.Address) floa return 100.0 / float64(totalCap.Div(totalCap, voterRewardAYear).Uint64()) } -type currentTotalMinted struct { - TotalMinted *hexutil.Big `json:"totalMinted"` - LastEpochNum *hexutil.Big `json:"lastEpochNum"` - BlockHash common.Hash `json:"blockHash"` - BlockNumber *hexutil.Big `json:"blockNumber"` +type supplyV1 struct { + Minted *hexutil.Big `json:"minted"` } -func (api *BlockChainAPI) GetCurrentTotalMinted(ctx context.Context) (*currentTotalMinted, error) { - statedb, header, err := api.b.StateAndHeaderByNumber(ctx, rpc.LatestBlockNumber) +type supplyV2 struct { + Minted *hexutil.Big `json:"minted"` + Burned *hexutil.Big `json:"burned"` +} + +type tokenSupply struct { + V1 *supplyV1 `json:"v1"` + V2 *supplyV2 `json:"v2"` + Minted *hexutil.Big `json:"minted"` + UpgradeEpochNum *hexutil.Big `json:"upgradeEpochNum"` + EpochNum *hexutil.Big `json:"epochNum"` + BlockHash common.Hash `json:"blockHash"` + BlockNumber *hexutil.Big `json:"blockNumber"` +} + +func (s *BlockChainAPI) GetTokenStats(ctx context.Context, epochNr rpc.EpochNumber) (*tokenSupply, error) { + engine, ok := s.b.Engine().(*XDPoS.XDPoS) + if !ok { + return nil, errors.New("undefined XDPoS consensus engine") + } + statedb, header, err := s.b.StateAndHeaderByNumber(ctx, rpc.LatestBlockNumber) + nonce := statedb.GetNonce(common.MintedRecordAddressBinary) + if nonce == 0 { + return nil, errors.New("mintedRecordAddress is not initialized due to Reward Upgrade is not applied") + } + currentRound, err := engine.EngineV2.GetRoundNumber(header) + currentEpoch := s.b.ChainConfig().XDPoS.V2.SwitchEpoch + uint64(currentRound)/s.b.ChainConfig().XDPoS.Epoch if err != nil { return nil, err } - totalMinted := statedb.GetTotalMinted().Big() - lastEpochNum := statedb.GetLastEpochNum().Big() - result := ¤tTotalMinted{ - TotalMinted: (*hexutil.Big)(totalMinted), - LastEpochNum: (*hexutil.Big)(lastEpochNum), - BlockHash: header.Hash(), - BlockNumber: (*hexutil.Big)(header.Number), + onsetEpoch := statedb.GetMintedRecordOnsetEpoch().Big().Uint64() + if epochNr >= 0 { + if uint64(epochNr) < onsetEpoch { + return nil, errors.New("epoch number is before reward upgrade") + } + if uint64(epochNr) > currentEpoch { + return nil, errors.New("epoch number is after current epoch") + } + } + epochNum := uint64(epochNr) + if epochNr == rpc.LatestEpochNumber { + epochNum = currentEpoch + } + postMinted := statedb.GetPostMinted(epochNum).Big() + number := statedb.GetPostRewardBlock(epochNum).Big() + targetHeader, err := s.b.HeaderByNumber(ctx, rpc.BlockNumber(number.Int64())) + if err != nil { + return nil, err + } + config := s.b.ChainConfig().XDPoS + if config == nil { + return nil, errors.New("xdpos config is nil") + } + preEpochMinted := new(big.Int).Mul(new(big.Int).SetUint64(config.Reward), new(big.Int).SetUint64(params.Ether)) + onsetEpochMinus := onsetEpoch + if onsetEpochMinus > 0 { + onsetEpochMinus-- + } else { + log.Warn("OnsetEpoch is 0 which could not happen", epochNum) + } + preMinted := new(big.Int).Mul(preEpochMinted, new(big.Int).SetUint64(onsetEpochMinus)) + postBurned := statedb.GetPostBurned(epochNum).Big() + result := &tokenSupply{ + V1: &supplyV1{ + Minted: (*hexutil.Big)(preMinted), + }, + V2: &supplyV2{ + Minted: (*hexutil.Big)(postMinted), + Burned: (*hexutil.Big)(postBurned), + }, + Minted: (*hexutil.Big)(new(big.Int).Add(postMinted, preMinted)), + UpgradeEpochNum: (*hexutil.Big)(new(big.Int).SetUint64(onsetEpoch)), + EpochNum: (*hexutil.Big)(new(big.Int).SetUint64(epochNum)), + BlockHash: targetHeader.Hash(), + BlockNumber: (*hexutil.Big)(number), } return result, nil } diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index f34842c0c5..ea47c723f9 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -578,9 +578,9 @@ web3._extend({ params: 1, }), new web3._extend.Method({ - name: 'getCurrentTotalMinted', - call: 'eth_getCurrentTotalMinted', - params: 0, + name: 'getTokenStats', + call: 'eth_getTokenStats', + params: 1, }), ], properties: [