--- /dev/null
+## database
+
+- Create a MySQL database locally or with server installation
+- Import table structure to MySQL database, table structure path: bytom/toolbar/vote_reward/database/dump_reward.sql
+
+
+
+## configuration file
+
+- Default file name:reward.json
+- A `reward.json` would look like this:
+
+```json
+{
+ "node_ip": "http://127.0.0.1:9888", // node API address, replace with self node API address
+ "chain_id": "mainnet", //Node network type
+ "mysql": { // Mysql connection information
+ "connection": {
+ "host": "192.168.30.186",
+ "port": 3306,
+ "username": "root",
+ "password": "123456",
+ "database": "reward"
+ },
+ "log_mode": false // default
+ },
+ "reward_config": {
+ "xpub": "9742a39a0bcfb5b7ac8f56f1894fbb694b53ebf58f9a032c36cc22d57a06e49e94ff7199063fb7a78190624fa3530f611404b56fc9af91dcaf4639614512cb64", // Node public key (from dashboard Settings), replaced with its own
+ "account_id": "bd775113-49e0-4678-94bf-2b853f1afe80", // accountID
+ "password": "123456",// The password corresponding to the account ID
+ "reward_ratio": 20,// The percentage of a reward given to a voter per block
+ "mining_address": "sp1qfpgjve27gx0r9t7vud8vypplkzytgrvqr74rwz" // The address that receives the block reward, use the get-mining- address for mining address, for example, curl -x POST http://127.0.0.1:9889/get-mining-address -d '{}'
+ }
+}
+```
+
+
+
+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
+```
+
+
+
+Note:
+
+When an error (Gas credit has been spent) is returned, UTXO needs to be merged.
\ No newline at end of file
--- /dev/null
+package main
+
+import (
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/spf13/cobra"
+ "github.com/tendermint/tmlibs/cli"
+
+ "github.com/bytom/bytom/consensus"
+ "github.com/bytom/bytom/toolbar/common"
+ cfg "github.com/bytom/bytom/toolbar/vote_reward/config"
+ "github.com/bytom/bytom/toolbar/vote_reward/settlementvotereward"
+ "github.com/bytom/bytom/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 {
+ log.Info("This tool belongs to an open-source project, we can not guarantee this tool is bug-free. Please check the code before using, developers will not be responsible for any asset loss due to bug!")
+ 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.BlocksOfEpoch != 0 || rewardEndHeight%consensus.ActiveNetParams.BlocksOfEpoch != 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.")
+ }
+
+ db.LogMode(true)
+
+ keeper, err := synchron.NewChainKeeper(db, config, rewardEndHeight)
+ if err != nil {
+ log.WithFields(log.Fields{"module": logModule, "error": err}).Fatal("Failded to initialize NewChainKeeper.")
+ }
+
+ if err := keeper.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()
+}
import (
"encoding/binary"
+ "fmt"
"strings"
"github.com/bytom/bytom/protocol/bc"
VotePendingBlockNumber: 10,
},
}
+
+// InitActiveNetParams load the config by chain ID
+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
+}
github.com/davecgh/go-spew v1.1.1
github.com/fortytw2/leaktest v1.3.0 // indirect
github.com/go-kit/kit v0.10.0 // indirect
+ github.com/go-sql-driver/mysql v1.5.0
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/golang/protobuf v1.4.3
github.com/golang/snappy v0.0.3 // indirect
github.com/grandcat/zeroconf v0.0.0-20190424104450-85eadb44205c
github.com/hashicorp/go-version v1.3.0
github.com/holiman/uint256 v1.1.1
+ github.com/jinzhu/gorm v1.9.16
github.com/johngb/langreg v0.0.0-20150123211413-5c6abc6d19d2
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/kr/secureheader v0.2.0
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
+github.com/bytom/vapor v4.8.11+incompatible h1:sO8CbrkiK3I65htiXuml8SCL1ZjbnbKn915CtS6L3HU=
+github.com/bytom/vapor v4.8.11+incompatible/go.mod h1:v/ibQL+K6miAS8OcWmfGLJe8274fa0GR0KRdfWpwdnY=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
+github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
+github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/johngb/langreg v0.0.0-20150123211413-5c6abc6d19d2 h1:R0Yc1jK2pjDwZeIXmcbELtKLedE+PjuI0S5cguGxTxw=
github.com/johngb/langreg v0.0.0-20150123211413-5c6abc6d19d2/go.mod h1:m/usUv5KgruWsRUejHsR568dyOh5pJ1wVoKZKMuEPhI=
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8=
github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
+github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
--- /dev/null
+package apinode
+
+import (
+ "encoding/json"
+
+ "github.com/bytom/bytom/api"
+ "github.com/bytom/bytom/errors"
+ "github.com/bytom/bytom/protocol/bc/types"
+)
+
+func (n *Node) GetBlockByHash(hash string) (*types.Block, error) {
+ return n.getRawBlock(&getRawBlockReq{BlockHash: hash})
+}
+
+func (n *Node) GetBlockByHeight(height uint64) (*types.Block, error) {
+ return n.getRawBlock(&getRawBlockReq{BlockHeight: height})
+}
+
+type getRawBlockReq struct {
+ BlockHeight uint64 `json:"block_height"`
+ BlockHash string `json:"block_hash"`
+}
+
+func (n *Node) getRawBlock(req *getRawBlockReq) (*types.Block, error) {
+ url := "/get-raw-block"
+ payload, err := json.Marshal(req)
+ if err != nil {
+ return nil, errors.Wrap(err, "json marshal")
+ }
+ resp := &api.GetRawBlockResp{}
+ return resp.RawBlock, n.request(url, payload, resp)
+}
+
+// bytomChainStatusResp is the response of bytom chain status
+type bytomChainStatusResp struct {
+ FinalizedHeight uint64 `json:"finalized_height"`
+}
+
+// GetFinalizedHeight return the finalized block height of connected node
+func (n *Node) GetFinalizedHeight() (uint64, error) {
+ url := "/chain-status"
+ res := &bytomChainStatusResp{}
+ return res.FinalizedHeight, n.request(url, nil, res)
+}
--- /dev/null
+package apinode
+
+import (
+ "encoding/json"
+
+ "github.com/bytom/bytom/errors"
+ "github.com/bytom/bytom/toolbar/common"
+)
+
+// Node can invoke the api which provide by the full node server
+type Node struct {
+ hostPort string
+}
+
+// NewNode create a api client with target server
+func NewNode(hostPort string) *Node {
+ return &Node{hostPort: hostPort}
+}
+
+type response struct {
+ Status string `json:"status"`
+ Data json.RawMessage `json:"data"`
+ ErrDetail string `json:"error_detail"`
+}
+
+func (n *Node) request(path string, payload []byte, respData interface{}) error {
+ resp := &response{}
+ if err := common.Post(n.hostPort+path, payload, resp); err != nil {
+ return err
+ }
+
+ if resp.Status != "success" {
+ return errors.New(resp.ErrDetail)
+ }
+
+ if resp.Data == nil {
+ return nil
+ }
+
+ return json.Unmarshal(resp.Data, respData)
+}
--- /dev/null
+package apinode
+
+import (
+ "encoding/hex"
+ "encoding/json"
+
+ "github.com/bytom/bytom/blockchain/txbuilder"
+ "github.com/bytom/bytom/consensus"
+ "github.com/bytom/bytom/errors"
+ "github.com/bytom/bytom/protocol/bc"
+ "github.com/bytom/bytom/protocol/bc/types"
+)
+
+type SpendAccountAction struct {
+ AccountID string `json:"account_id"`
+ *bc.AssetAmount
+}
+
+func (s *SpendAccountAction) MarshalJSON() ([]byte, error) {
+ return json.Marshal(&struct {
+ Type string `json:"type"`
+ AccountID string `json:"account_id"`
+ *bc.AssetAmount
+ }{
+ Type: "spend_account",
+ AccountID: s.AccountID,
+ AssetAmount: s.AssetAmount,
+ })
+}
+
+type ControlAddressAction struct {
+ Address string `json:"address"`
+ *bc.AssetAmount
+}
+
+func (c *ControlAddressAction) MarshalJSON() ([]byte, error) {
+ return json.Marshal(&struct {
+ Type string `json:"type"`
+ Address string `json:"address"`
+ *bc.AssetAmount
+ }{
+ Type: "control_address",
+ Address: c.Address,
+ AssetAmount: c.AssetAmount,
+ })
+}
+
+type RetireAction struct {
+ *bc.AssetAmount
+ Arbitrary []byte
+}
+
+func (r *RetireAction) MarshalJSON() ([]byte, error) {
+ return json.Marshal(&struct {
+ Type string `json:"type"`
+ Arbitrary string `json:"arbitrary"`
+ *bc.AssetAmount
+ }{
+ Type: "retire",
+ Arbitrary: hex.EncodeToString(r.Arbitrary),
+ AssetAmount: r.AssetAmount,
+ })
+}
+
+func (n *Node) BatchSendBTM(accountID, password string, outputs map[string]uint64, memo []byte) (string, error) {
+ totalBTM := uint64(10000000)
+ actions := []interface{}{}
+ if len(memo) > 0 {
+ actions = append(actions, &RetireAction{
+ Arbitrary: memo,
+ AssetAmount: &bc.AssetAmount{AssetId: consensus.BTMAssetID, Amount: 1},
+ })
+ }
+
+ for address, amount := range outputs {
+ actions = append(actions, &ControlAddressAction{
+ Address: address,
+ AssetAmount: &bc.AssetAmount{AssetId: consensus.BTMAssetID, Amount: amount},
+ })
+ totalBTM += amount
+ }
+
+ actions = append(actions, &SpendAccountAction{
+ AccountID: accountID,
+ AssetAmount: &bc.AssetAmount{AssetId: consensus.BTMAssetID, Amount: totalBTM},
+ })
+
+ tpl, err := n.buildTx(actions)
+ if err != nil {
+ return "", err
+ }
+
+ tpl, err = n.signTx(tpl, password)
+ if err != nil {
+ return "", err
+ }
+
+ return n.SubmitTx(tpl.Transaction)
+}
+
+type buildTxReq struct {
+ Actions []interface{} `json:"actions"`
+}
+
+func (n *Node) buildTx(actions []interface{}) (*txbuilder.Template, error) {
+ url := "/build-transaction"
+ payload, err := json.Marshal(&buildTxReq{Actions: actions})
+ if err != nil {
+ return nil, errors.Wrap(err, "Marshal spend request")
+ }
+
+ result := &txbuilder.Template{}
+ return result, n.request(url, payload, result)
+}
+
+type signTxReq struct {
+ Tx *txbuilder.Template `json:"transaction"`
+ Password string `json:"password"`
+}
+
+type signTxResp struct {
+ Tx *txbuilder.Template `json:"transaction"`
+ SignComplete bool `json:"sign_complete"`
+}
+
+func (n *Node) signTx(tpl *txbuilder.Template, password string) (*txbuilder.Template, error) {
+ url := "/sign-transaction"
+ payload, err := json.Marshal(&signTxReq{Tx: tpl, Password: password})
+ if err != nil {
+ return nil, errors.Wrap(err, "json marshal")
+ }
+
+ resp := &signTxResp{}
+ if err := n.request(url, payload, resp); err != nil {
+ return nil, err
+ }
+
+ if !resp.SignComplete {
+ return nil, errors.New("sign fail")
+ }
+
+ return resp.Tx, nil
+}
+
+type submitTxReq struct {
+ Tx *types.Tx `json:"raw_transaction"`
+}
+
+type submitTxResp struct {
+ TxID string `json:"tx_id"`
+}
+
+func (n *Node) SubmitTx(tx *types.Tx) (string, error) {
+ url := "/submit-transaction"
+ payload, err := json.Marshal(submitTxReq{Tx: tx})
+ if err != nil {
+ return "", errors.Wrap(err, "json marshal")
+ }
+
+ res := &submitTxResp{}
+ return res.TxID, n.request(url, payload, res)
+}
--- /dev/null
+package common
+
+import (
+ "errors"
+
+ "github.com/bytom/bytom/common"
+ "github.com/bytom/bytom/consensus"
+ "github.com/bytom/bytom/consensus/segwit"
+ "github.com/bytom/bytom/protocol/vm/vmutil"
+)
+
+func GetAddressFromControlProgram(prog []byte) string {
+ if segwit.IsP2WPKHScript(prog) {
+ if pubHash, err := segwit.GetHashFromStandardProg(prog); err == nil {
+ return buildP2PKHAddress(pubHash)
+ }
+ } else if segwit.IsP2WSHScript(prog) {
+ if scriptHash, err := segwit.GetHashFromStandardProg(prog); err == nil {
+ return buildP2SHAddress(scriptHash)
+ }
+ }
+
+ return ""
+}
+
+func buildP2PKHAddress(pubHash []byte) string {
+ address, err := common.NewAddressWitnessPubKeyHash(pubHash, &consensus.ActiveNetParams)
+ if err != nil {
+ return ""
+ }
+
+ return address.EncodeAddress()
+}
+
+func buildP2SHAddress(scriptHash []byte) string {
+ address, err := common.NewAddressWitnessScriptHash(scriptHash, &consensus.ActiveNetParams)
+ if err != nil {
+ return ""
+ }
+
+ 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
+}
--- /dev/null
+package common
+
+type MySQLConfig struct {
+ Connection MySQLConnection `json:"connection"`
+ LogMode bool `json:"log_mode"`
+}
+
+type MySQLConnection struct {
+ Host string `json:"host"`
+ Port uint `json:"port"`
+ Username string `json:"username"`
+ Password string `json:"password"`
+ DbName string `json:"database"`
+}
--- /dev/null
+package common
+
+import (
+ "fmt"
+
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/jinzhu/gorm"
+
+ "github.com/bytom/bytom/errors"
+)
+
+func NewMySQLDB(cfg MySQLConfig) (*gorm.DB, error) {
+ dsnTemplate := "%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=true&loc=Local"
+ dsn := fmt.Sprintf(dsnTemplate, cfg.Connection.Username, cfg.Connection.Password, cfg.Connection.Host, cfg.Connection.Port, cfg.Connection.DbName)
+ db, err := gorm.Open("mysql", dsn)
+ if err != nil {
+ return nil, errors.Wrap(err, "open db cluster")
+ }
+
+ db.LogMode(cfg.LogMode)
+ if err = db.DB().Ping(); err != nil {
+ return nil, errors.Wrap(err, "ping db")
+ }
+
+ return db, nil
+}
--- /dev/null
+package common
+
+import (
+ "bytes"
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+)
+
+func Get(url string, result interface{}) error {
+ client := &http.Client{}
+ resp, err := client.Get(url)
+ if err != nil {
+ return err
+ }
+
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+
+ return json.Unmarshal(body, result)
+}
+
+func Post(url string, payload []byte, result interface{}) error {
+ req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
+ if err != nil {
+ return err
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+
+ defer resp.Body.Close()
+ if result == nil {
+ return nil
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+
+ return json.Unmarshal(body, result)
+}
--- /dev/null
+package config
+
+import (
+ "encoding/json"
+ "os"
+
+ "github.com/bytom/bytom/toolbar/common"
+)
+
+type Config struct {
+ NodeIP string `json:"node_ip"`
+ ChainID string `json:"chain_id"`
+ MySQLConfig common.MySQLConfig `json:"mysql"`
+ RewardConf *RewardConfig `json:"reward_config"`
+}
+
+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 LoadConfigFile(configFile string, config *Config) error {
+ file, err := os.Open(configFile)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ return json.NewDecoder(file).Decode(config)
+}
--- /dev/null
+# ************************************************************
+# Sequel Pro SQL dump
+# Version 4541
+#
+# http://www.sequelpro.com/
+# https://github.com/sequelpro/sequelpro
+#
+# Host: 127.0.0.1 (MySQL 5.7.24)
+# Database: vote_reward
+# Generation Time: 2019-07-22 13:41:50 +0000
+# ************************************************************
+
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+
+# Dump of table chain_statuses
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `chain_statuses`;
+
+CREATE TABLE `chain_statuses` (
+ `block_height` int(11) NOT NULL,
+ `block_hash` varchar(64) NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+
+
+# Dump of table utxos
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `utxos`;
+
+CREATE TABLE `utxos` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `output_id` varchar(64) NOT NULL,
+ `xpub` varchar(128) NOT NULL,
+ `vote_address` varchar(62) NOT NULL,
+ `vote_num` bigint(21) NOT NULL,
+ `vote_height` int(11) NOT NULL,
+ `veto_height` int(11) NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `output_id` (`output_id`),
+ KEY `xpub` (`xpub`),
+ KEY `vote_height` (`vote_height`),
+ KEY `veto_height` (`veto_height`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+
+
+
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
--- /dev/null
+package orm
+
+type ChainStatus struct {
+ BlockHeight uint64
+ BlockHash string
+}
--- /dev/null
+package orm
+
+type Utxo struct {
+ ID uint64 `gorm:"primary_key"`
+ OutputID string
+ Xpub string
+ VoteAddress string
+ VoteNum uint64
+ VoteHeight uint64
+ VetoHeight uint64
+}
--- /dev/null
+package settlementvotereward
+
+import (
+ "bytes"
+ "encoding/json"
+ "math/big"
+
+ "github.com/jinzhu/gorm"
+
+ "github.com/bytom/bytom/consensus"
+ "github.com/bytom/bytom/errors"
+ "github.com/bytom/bytom/toolbar/apinode"
+ "github.com/bytom/bytom/toolbar/common"
+ "github.com/bytom/bytom/toolbar/vote_reward/config"
+)
+
+var (
+ errNotFoundReward = errors.New("No reward found")
+ errNotRewardTx = errors.New("No reward transaction")
+)
+
+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
+}
+
+type memo struct {
+ StartHeight uint64 `json:"start_height"`
+ EndHeight uint64 `json:"end_height"`
+ NodePubkey string `json:"node_pubkey"`
+ RewardRatio uint64 `json:"reward_ratio"`
+}
+
+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.BlocksOfEpoch+1, height-consensus.ActiveNetParams.BlocksOfEpoch, 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.BlocksOfEpoch; height <= s.endHeight; height += consensus.ActiveNetParams.BlocksOfEpoch {
+ totalReward, err := s.getCoinbaseReward(height + 1)
+ if err == errNotFoundReward {
+ continue
+ }
+
+ if err != nil {
+ return errors.Wrapf(err, "get total reward at height: %d", height)
+ }
+
+ voteResults, err := s.getVoteResultFromDB(height)
+ if err != nil {
+ return err
+ }
+
+ s.calcVoterRewards(voteResults, totalReward)
+ }
+
+ if len(s.rewards) == 0 {
+ return errNotRewardTx
+ }
+
+ data, err := json.Marshal(&memo{
+ StartHeight: s.startHeight,
+ EndHeight: s.endHeight,
+ NodePubkey: s.rewardCfg.XPub,
+ RewardRatio: s.rewardCfg.RewardRatio,
+ })
+ if err != nil {
+ return err
+ }
+
+ // send transactions
+ _, err = s.node.BatchSendBTM(s.rewardCfg.AccountID, s.rewardCfg.Password, s.rewards, data)
+ return err
+}
+
+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 {
+ 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, errNotFoundReward
+}
+
+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
+ }
+ }
+}
--- /dev/null
+package synchron
+
+import (
+ "encoding/hex"
+
+ "github.com/jinzhu/gorm"
+
+ "github.com/bytom/bytom/errors"
+ "github.com/bytom/bytom/protocol/bc/types"
+ "github.com/bytom/bytom/toolbar/apinode"
+ "github.com/bytom/bytom/toolbar/common"
+ "github.com/bytom/bytom/toolbar/vote_reward/config"
+ "github.com/bytom/bytom/toolbar/vote_reward/database/orm"
+)
+
+var ErrInconsistentDB = errors.New("inconsistent db status")
+
+type ChainKeeper struct {
+ db *gorm.DB
+ node *apinode.Node
+ targetHeight uint64
+}
+
+func NewChainKeeper(db *gorm.DB, cfg *config.Config, targetHeight uint64) (*ChainKeeper, error) {
+ keeper := &ChainKeeper{
+ db: db,
+ node: apinode.NewNode(cfg.NodeIP),
+ targetHeight: targetHeight,
+ }
+
+ finalizedHeight, err := keeper.node.GetFinalizedHeight()
+ if err != nil {
+ return nil, errors.Wrap(err, "fail on get finalized height")
+ }
+
+ if targetHeight > finalizedHeight {
+ return nil, errors.New("reward end height is more than finalized height")
+ }
+
+ chainStatus := &orm.ChainStatus{}
+ if err := db.First(chainStatus).Error; err == nil {
+ return keeper, nil
+ } else if err != gorm.ErrRecordNotFound {
+ return nil, errors.Wrap(err, "fail on get chainStatus")
+ }
+
+ if err := keeper.initBlockState(); err != nil {
+ return nil, errors.Wrap(err, "fail on init chainStatus")
+ }
+ return keeper, nil
+}
+
+func (c *ChainKeeper) SyncBlock() error {
+ for {
+ chainStatus := &orm.ChainStatus{}
+ if err := c.db.First(chainStatus).Error; err != nil {
+ return errors.Wrap(err, "fail on syncBlock query chainStatus")
+ }
+
+ if chainStatus.BlockHeight >= c.targetHeight {
+ break
+ }
+
+ dbTX := c.db.Begin()
+ if err := c.syncChainStatus(dbTX, chainStatus); err != nil {
+ dbTX.Rollback()
+ return err
+ }
+
+ if err := dbTX.Commit().Error; err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (c *ChainKeeper) syncChainStatus(db *gorm.DB, chainStatus *orm.ChainStatus) error {
+ nextBlock, err := c.node.GetBlockByHeight(chainStatus.BlockHeight + 1)
+ if err != nil {
+ return err
+ }
+
+ return c.AttachBlock(db, chainStatus, nextBlock)
+}
+
+func (c *ChainKeeper) AttachBlock(db *gorm.DB, chainStatus *orm.ChainStatus, block *types.Block) error {
+ for _, tx := range block.Transactions {
+ for _, input := range tx.Inputs {
+ if input.TypedInput.InputType() != types.VetoInputType {
+ continue
+ }
+
+ outputID, err := input.SpentOutputID()
+ if err != nil {
+ return err
+ }
+
+ result := db.Model(&orm.Utxo{}).Where(&orm.Utxo{OutputID: outputID.String()}).Update("veto_height", block.Height)
+ if err := result.Error; err != nil {
+ return err
+ } else if result.RowsAffected != 1 {
+ return ErrInconsistentDB
+ }
+ }
+
+ for i, output := range tx.Outputs {
+ voteOutput, ok := output.TypedOutput.(*types.VoteOutput)
+ if !ok {
+ continue
+ }
+
+ utxo := &orm.Utxo{
+ Xpub: hex.EncodeToString(voteOutput.Vote),
+ VoteAddress: common.GetAddressFromControlProgram(output.ControlProgram),
+ VoteHeight: block.Height,
+ VoteNum: output.Amount,
+ OutputID: tx.OutputID(i).String(),
+ }
+
+ if err := db.Save(utxo).Error; err != nil {
+ return err
+ }
+ }
+ }
+
+ return c.updateChainStatus(db, chainStatus, block)
+}
+
+func (c *ChainKeeper) initBlockState() error {
+ block, err := c.node.GetBlockByHeight(0)
+ if err != nil {
+ return errors.Wrap(err, "fail on get genenis block")
+ }
+
+ blockHash := block.Hash()
+ chainStatus := &orm.ChainStatus{
+ BlockHeight: block.Height,
+ BlockHash: blockHash.String(),
+ }
+ return c.db.Save(chainStatus).Error
+}
+
+func (c *ChainKeeper) updateChainStatus(db *gorm.DB, chainStatus *orm.ChainStatus, block *types.Block) error {
+ blockHash := block.Hash()
+ result := db.Model(&orm.ChainStatus{}).Where(chainStatus).Updates(&orm.ChainStatus{
+ BlockHeight: block.Height,
+ BlockHash: blockHash.String(),
+ })
+ if err := result.Error; err != nil {
+ return err
+ } else if result.RowsAffected != 1 {
+ return ErrInconsistentDB
+ }
+ return nil
+}