OSDN Git Service

Voter reward (#344)
authorwz <mars@bytom.io>
Wed, 24 Jul 2019 07:08:19 +0000 (15:08 +0800)
committerPaladz <yzhu101@uottawa.ca>
Wed, 24 Jul 2019 07:08:19 +0000 (15:08 +0800)
* add vote reward

* add config

* fix

* modify

* modify sql

* fix review

* modify sql

* add roundVoteBlockNums

* recover code

* modify format

* modify code

* optimized code

* modify format

* fix review

* fix review

* fix review

* single xpub

* modifu

* fix review

* fix

* modify format

* fix review

* modify coinbase

* fix bug

* fix

* fix review

* add Rollback

* modify

* fix

* fix

* fix voter rewward

* modify

* fix review

* fix review

* remove code file

* fix

* add readme

* fix

* rm file

* fix review

* rm chain_id

* modify readme

14 files changed:
cmd/reward/main.go [deleted file]
cmd/votereward/README.md [new file with mode: 0644]
cmd/votereward/main.go [new file with mode: 0644]
consensus/general.go
node/node.go
toolbar/apinode/block.go [moved from toolbar/api_node/block.go with 100% similarity]
toolbar/apinode/node.go [moved from toolbar/api_node/node.go with 100% similarity]
toolbar/apinode/node_test.go [moved from toolbar/api_node/node_test.go with 100% similarity]
toolbar/apinode/transaction.go [moved from toolbar/api_node/transaction.go with 100% similarity]
toolbar/common/address.go
toolbar/federation/service/node.go
toolbar/vote_reward/config/config.go
toolbar/vote_reward/settlementvotereward/settlementreward.go [new file with mode: 0644]
toolbar/vote_reward/synchron/block_keeper.go

diff --git a/cmd/reward/main.go b/cmd/reward/main.go
deleted file mode 100644 (file)
index 7905807..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-package main
-
-func main() {
-
-}
diff --git a/cmd/votereward/README.md b/cmd/votereward/README.md
new file mode 100644 (file)
index 0000000..ee6c5d3
--- /dev/null
@@ -0,0 +1,51 @@
+A `reward.json` would look like this:
+
+```json
+{
+  "node_ip": "http://127.0.0.1:9889",
+  "chain_id": "solonet",
+  "mysql": {
+    "connection": {
+      "host": "192.168.30.186",
+      "port": 3306,
+      "username": "root",
+      "password": "123456",
+      "database": "reward"
+    },
+    "log_mode": false
+  },
+  "reward_config": {
+    "xpub": "9742a39a0bcfb5b7ac8f56f1894fbb694b53ebf58f9a032c36cc22d57a06e49e94ff7199063fb7a78190624fa3530f611404b56fc9af91dcaf4639614512cb64",
+    "account_id": "bd775113-49e0-4678-94bf-2b853f1afe80",
+    "password": "123456",
+    "reward_ratio": 20,
+    "mining_address": "sp1qfpgjve27gx0r9t7vud8vypplkzytgrvqr74rwz"
+  }
+}
+```
+
+
+
+tool use
+
+params
+
+```shell
+distribution of reward.
+
+Usage:
+  reward [flags]
+
+Flags:
+      --config_file string         config file. default: reward.json (default "reward.json")
+  -h, --help                       help for reward
+      --reward_end_height uint     The end height of the distributive income reward interval, It is a multiple of the dpos consensus cycle(1200). example: 2400
+      --reward_start_height uint   The starting height of the distributive income reward interval, It is a multiple of the dpos consensus cycle(1200). example: 1200
+```
+
+example:
+
+```shell
+./votereward reward --reward_start_height 6000 --reward_end_height 7200
+```
+
diff --git a/cmd/votereward/main.go b/cmd/votereward/main.go
new file mode 100644 (file)
index 0000000..fd42aa1
--- /dev/null
@@ -0,0 +1,82 @@
+package main
+
+import (
+       "time"
+
+       log "github.com/sirupsen/logrus"
+       "github.com/spf13/cobra"
+       "github.com/tendermint/tmlibs/cli"
+
+       "github.com/vapor/consensus"
+       "github.com/vapor/toolbar/common"
+       cfg "github.com/vapor/toolbar/vote_reward/config"
+       "github.com/vapor/toolbar/vote_reward/settlementvotereward"
+       "github.com/vapor/toolbar/vote_reward/synchron"
+)
+
+const logModule = "reward"
+
+var (
+       rewardStartHeight uint64
+       rewardEndHeight   uint64
+       configFile        string
+)
+
+var RootCmd = &cobra.Command{
+       Use:   "reward",
+       Short: "distribution of reward.",
+       RunE:  runReward,
+}
+
+func init() {
+       RootCmd.Flags().Uint64Var(&rewardStartHeight, "reward_start_height", 0, "The starting height of the distributive income reward interval, It is a multiple of the dpos consensus cycle(1200). example: 1200")
+       RootCmd.Flags().Uint64Var(&rewardEndHeight, "reward_end_height", 0, "The end height of the distributive income reward interval, It is a multiple of the dpos consensus cycle(1200). example: 2400")
+       RootCmd.Flags().StringVar(&configFile, "config_file", "reward.json", "config file. default: reward.json")
+}
+
+func runReward(cmd *cobra.Command, args []string) error {
+       startTime := time.Now()
+       config := &cfg.Config{}
+       if err := cfg.LoadConfigFile(configFile, config); err != nil {
+               log.WithFields(log.Fields{"module": logModule, "config": configFile, "error": err}).Fatal("Failded to load config file.")
+       }
+
+       if err := consensus.InitActiveNetParams(config.ChainID); err != nil {
+               log.WithFields(log.Fields{"module": logModule, "error": err}).Fatal("Init ActiveNetParams.")
+       }
+       if rewardStartHeight >= rewardEndHeight || rewardStartHeight%consensus.ActiveNetParams.RoundVoteBlockNums != 0 || rewardEndHeight%consensus.ActiveNetParams.RoundVoteBlockNums != 0 {
+               log.Fatal("Please check the height range, which must be multiple of the number of block rounds.")
+       }
+
+       db, err := common.NewMySQLDB(config.MySQLConfig)
+       if err != nil {
+               log.WithFields(log.Fields{"module": logModule, "error": err}).Fatal("Failded to initialize mysql db.")
+       }
+
+       sync, err := synchron.NewChainKeeper(db, config, rewardEndHeight)
+       if err != nil {
+               log.WithFields(log.Fields{"module": logModule, "error": err}).Fatal("Failded to initialize NewChainKeeper.")
+       }
+
+       if err := sync.SyncBlock(); err != nil {
+               log.WithFields(log.Fields{"module": logModule, "error": err}).Fatal("Failded to sync block.")
+       }
+
+       s := settlementvotereward.NewSettlementReward(db, config, rewardStartHeight, rewardEndHeight)
+
+       if err := s.Settlement(); err != nil {
+               log.WithFields(log.Fields{"module": logModule, "error": err}).Fatal("Settlement vote rewards failure.")
+       }
+
+       log.WithFields(log.Fields{
+               "module":   logModule,
+               "duration": time.Since(startTime),
+       }).Info("Settlement vote reward complete")
+
+       return nil
+}
+
+func main() {
+       cmd := cli.PrepareBaseCmd(RootCmd, "REWARD", "./")
+       cmd.Execute()
+}
index 32620ac..f3bab4e 100644 (file)
@@ -2,6 +2,7 @@ package consensus
 
 import (
        "encoding/binary"
+       "fmt"
 
        "github.com/vapor/protocol/bc"
 )
@@ -225,3 +226,11 @@ func BytomMainNetParams(vaporParam *Params) *Params {
        }
        return &Params{Bech32HRPSegwit: bech32HRPSegwit}
 }
+
+func InitActiveNetParams(chainID string) error {
+       var exist bool
+       if ActiveNetParams, exist = NetParams[chainID]; !exist {
+               return fmt.Errorf("chain_id[%v] don't exist", chainID)
+       }
+       return nil
+}
index 6994823..531ff83 100644 (file)
@@ -75,7 +75,9 @@ func NewNode(config *cfg.Config) *Node {
        }).Info()
 
        initLogFile(config)
-       initActiveNetParams(config)
+       if err := consensus.InitActiveNetParams(config.ChainID); err != nil {
+               log.Fatalf("Failed to init ActiveNetParams:[%s]", err.Error())
+       }
        initCommonConfig(config)
 
        // Get store
@@ -125,7 +127,7 @@ func NewNode(config *cfg.Config) *Node {
                }
        }
        fastSyncDB := dbm.NewDB("fastsync", config.DBBackend, config.DBDir())
-       syncManager, err := netsync.NewSyncManager(config, chain, txPool, dispatcher,fastSyncDB)
+       syncManager, err := netsync.NewSyncManager(config, chain, txPool, dispatcher, fastSyncDB)
        if err != nil {
                cmn.Exit(cmn.Fmt("Failed to create sync manager: %v", err))
        }
@@ -186,14 +188,6 @@ func lockDataDirectory(config *cfg.Config) error {
        return nil
 }
 
-func initActiveNetParams(config *cfg.Config) {
-       var exist bool
-       consensus.ActiveNetParams, exist = consensus.NetParams[config.ChainID]
-       if !exist {
-               cmn.Exit(cmn.Fmt("chain_id[%v] don't exist", config.ChainID))
-       }
-}
-
 func initLogFile(config *cfg.Config) {
        if config.LogFile == "" {
                return
index 53b6c9b..d6f7193 100644 (file)
@@ -1,9 +1,12 @@
 package common
 
 import (
+       "errors"
+
        "github.com/vapor/common"
        "github.com/vapor/consensus"
        "github.com/vapor/consensus/segwit"
+       "github.com/vapor/protocol/vm/vmutil"
 )
 
 func GetAddressFromControlProgram(prog []byte) string {
@@ -37,3 +40,25 @@ func buildP2SHAddress(scriptHash []byte) string {
 
        return address.EncodeAddress()
 }
+
+func GetControlProgramFromAddress(address string) ([]byte, error) {
+       decodeaddress, err := common.DecodeAddress(address, &consensus.ActiveNetParams)
+       if err != nil {
+               return nil, err
+       }
+
+       redeemContract := decodeaddress.ScriptAddress()
+       program := []byte{}
+       switch decodeaddress.(type) {
+       case *common.AddressWitnessPubKeyHash:
+               program, err = vmutil.P2WPKHProgram(redeemContract)
+       case *common.AddressWitnessScriptHash:
+               program, err = vmutil.P2WSHProgram(redeemContract)
+       default:
+               return nil, errors.New("Invalid address")
+       }
+       if err != nil {
+               return nil, err
+       }
+       return program, nil
+}
index 2775e46..9b5a856 100644 (file)
@@ -13,7 +13,7 @@ type Node struct {
        hostPort string
 }
 
-// Node create a api client with target server
+// NewNode create a api client with target server
 func NewNode(hostPort string) *Node {
        return &Node{hostPort: hostPort}
 }
index b73bb95..d15b3c6 100644 (file)
@@ -5,13 +5,28 @@ import (
        "encoding/json"
        "io/ioutil"
        "os"
+       "path"
 
        "github.com/vapor/toolbar/common"
 )
 
 type Config struct {
-       MySQLConfig common.MySQLConfig `json:"mysql"`
        NodeIP      string             `json:"node_ip"`
+       ChainID     string             `json:"chain_id"`
+       MySQLConfig common.MySQLConfig `json:"mysql"`
+       RewardConf  *RewardConfig      `json:"reward_config"`
+}
+
+func ConfigFile() string {
+       return path.Join("./", "reward.json")
+}
+
+type RewardConfig struct {
+       XPub          string `json:"xpub"`
+       AccountID     string `json:"account_id"`
+       Password      string `json:"password"`
+       MiningAddress string `json:"mining_address"`
+       RewardRatio   uint64 `json:"reward_ratio"`
 }
 
 func ExportConfigFile(configFile string, config *Config) error {
diff --git a/toolbar/vote_reward/settlementvotereward/settlementreward.go b/toolbar/vote_reward/settlementvotereward/settlementreward.go
new file mode 100644 (file)
index 0000000..c367dae
--- /dev/null
@@ -0,0 +1,123 @@
+package settlementvotereward
+
+import (
+       "bytes"
+       "math/big"
+
+       "github.com/jinzhu/gorm"
+
+       "github.com/vapor/consensus"
+       "github.com/vapor/errors"
+       "github.com/vapor/protocol/bc/types"
+       "github.com/vapor/toolbar/apinode"
+       "github.com/vapor/toolbar/common"
+       "github.com/vapor/toolbar/vote_reward/config"
+)
+
+type voteResult struct {
+       VoteAddress string
+       VoteNum     uint64
+}
+
+type SettlementReward struct {
+       rewardCfg   *config.RewardConfig
+       node        *apinode.Node
+       db          *gorm.DB
+       rewards     map[string]uint64
+       startHeight uint64
+       endHeight   uint64
+}
+
+func NewSettlementReward(db *gorm.DB, cfg *config.Config, startHeight, endHeight uint64) *SettlementReward {
+       return &SettlementReward{
+               db:          db,
+               rewardCfg:   cfg.RewardConf,
+               node:        apinode.NewNode(cfg.NodeIP),
+               rewards:     make(map[string]uint64),
+               startHeight: startHeight,
+               endHeight:   endHeight,
+       }
+}
+
+func (s *SettlementReward) getVoteResultFromDB(height uint64) (voteResults []*voteResult, err error) {
+       query := s.db.Table("utxos").Select("vote_address, sum(vote_num) as vote_num")
+       query = query.Where("(veto_height >= ? or veto_height = 0) and vote_height <= ? and xpub = ?", height-consensus.ActiveNetParams.RoundVoteBlockNums+1, height-consensus.ActiveNetParams.RoundVoteBlockNums, s.rewardCfg.XPub)
+       query = query.Group("vote_address")
+       if err := query.Scan(&voteResults).Error; err != nil {
+               return nil, err
+       }
+
+       return voteResults, nil
+}
+
+func (s *SettlementReward) Settlement() error {
+       for height := s.startHeight + consensus.ActiveNetParams.RoundVoteBlockNums; height <= s.endHeight; height += consensus.ActiveNetParams.RoundVoteBlockNums {
+               coinbaseHeight := height + 1
+               totalReward, err := s.getCoinbaseReward(coinbaseHeight)
+               if err != nil {
+                       return errors.Wrapf(err, "get total reward at coinbase_height: %d", coinbaseHeight)
+               }
+
+               voteResults, err := s.getVoteResultFromDB(height)
+               if err != nil {
+                       return err
+               }
+
+               s.calcVoterRewards(voteResults, totalReward)
+       }
+
+       // send transactions
+       return s.node.BatchSendBTM(s.rewardCfg.AccountID, s.rewardCfg.Password, s.rewards)
+}
+
+func (s *SettlementReward) getCoinbaseReward(height uint64) (uint64, error) {
+       block, err := s.node.GetBlockByHeight(height)
+       if err != nil {
+               return 0, err
+       }
+
+       miningControl, err := common.GetControlProgramFromAddress(s.rewardCfg.MiningAddress)
+       if err != nil {
+               return 0, err
+       }
+
+       for _, output := range block.Transactions[0].Outputs {
+               output, ok := output.TypedOutput.(*types.IntraChainOutput)
+               if !ok {
+                       return 0, errors.New("Output type error")
+               }
+
+               if output.Amount == 0 {
+                       continue
+               }
+
+               if bytes.Equal(miningControl, output.ControlProgram) {
+                       amount := big.NewInt(0).SetUint64(output.Amount)
+                       rewardRatio := big.NewInt(0).SetUint64(s.rewardCfg.RewardRatio)
+                       amount.Mul(amount, rewardRatio).Div(amount, big.NewInt(100))
+
+                       return amount.Uint64(), nil
+               }
+       }
+       return 0, errors.New("No reward found")
+}
+
+func (s *SettlementReward) calcVoterRewards(voteResults []*voteResult, totalReward uint64) {
+       totalVoteNum := uint64(0)
+       for _, voteResult := range voteResults {
+               totalVoteNum += voteResult.VoteNum
+       }
+
+       for _, voteResult := range voteResults {
+               // voteNum / totalVoteNum  * totalReward
+               voteNum := big.NewInt(0).SetUint64(voteResult.VoteNum)
+               total := big.NewInt(0).SetUint64(totalVoteNum)
+               reward := big.NewInt(0).SetUint64(totalReward)
+
+               amount := voteNum.Mul(voteNum, reward).Div(voteNum, total).Uint64()
+
+               if amount != 0 {
+                       s.rewards[voteResult.VoteAddress] += amount
+               }
+       }
+}
index 2b78a66..9903480 100644 (file)
@@ -8,7 +8,7 @@ import (
 
        "github.com/vapor/errors"
        "github.com/vapor/protocol/bc/types"
-       apinode "github.com/vapor/toolbar/api_node"
+       "github.com/vapor/toolbar/apinode"
        "github.com/vapor/toolbar/common"
        "github.com/vapor/toolbar/vote_reward/config"
        "github.com/vapor/toolbar/vote_reward/database/orm"