OSDN Git Service

feat(toolbar): add vote reward tool (#2058)
authorDeKaiju <longjinglv@163.com>
Fri, 30 Jul 2021 03:31:49 +0000 (11:31 +0800)
committerGitHub <noreply@github.com>
Fri, 30 Jul 2021 03:31:49 +0000 (11:31 +0800)
* feat(toolbar): add vote reward tool

* refactor(toolbar): code refactoring

* refactor(toolbar): code refactoring

* refactor(toolbar): code refactoring

* refactor(toolbar): code refactoring

Co-authored-by: Paladz <yzhu101@uottawa.ca>
18 files changed:
cmd/votereward/README.md [new file with mode: 0644]
cmd/votereward/main.go [new file with mode: 0644]
consensus/general.go
go.mod
go.sum
toolbar/apinode/block.go [new file with mode: 0644]
toolbar/apinode/node.go [new file with mode: 0644]
toolbar/apinode/transaction.go [new file with mode: 0644]
toolbar/common/address.go [new file with mode: 0644]
toolbar/common/config.go [new file with mode: 0644]
toolbar/common/db.go [new file with mode: 0644]
toolbar/common/http_util.go [new file with mode: 0644]
toolbar/vote_reward/config/config.go [new file with mode: 0644]
toolbar/vote_reward/database/dump_reward.sql [new file with mode: 0644]
toolbar/vote_reward/database/orm/block_state.go [new file with mode: 0644]
toolbar/vote_reward/database/orm/utxo.go [new file with mode: 0644]
toolbar/vote_reward/settlementvotereward/settlementreward.go [new file with mode: 0644]
toolbar/vote_reward/synchron/block_keeper.go [new file with mode: 0644]

diff --git a/cmd/votereward/README.md b/cmd/votereward/README.md
new file mode 100644 (file)
index 0000000..b33abcb
--- /dev/null
@@ -0,0 +1,66 @@
+## 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
diff --git a/cmd/votereward/main.go b/cmd/votereward/main.go
new file mode 100644 (file)
index 0000000..49cd9cf
--- /dev/null
@@ -0,0 +1,85 @@
+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()
+}
index ebfd08a..429eb61 100644 (file)
@@ -2,6 +2,7 @@ package consensus
 
 import (
        "encoding/binary"
+       "fmt"
        "strings"
 
        "github.com/bytom/bytom/protocol/bc"
@@ -144,3 +145,12 @@ var SoloNetParams = Params{
                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
+}
diff --git a/go.mod b/go.mod
index ab85d2b..5e99c11 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -18,6 +18,7 @@ require (
        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
@@ -26,6 +27,7 @@ require (
        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
diff --git a/go.sum b/go.sum
index 24b1bc6..b6540b0 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -20,6 +20,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 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=
@@ -29,6 +30,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
 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=
@@ -58,6 +60,8 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
 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=
@@ -89,6 +93,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 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=
@@ -99,6 +104,7 @@ github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaB
 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=
@@ -118,7 +124,10 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
 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=
@@ -126,6 +135,7 @@ github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFG
 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=
@@ -225,6 +235,11 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
 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=
@@ -260,6 +275,7 @@ github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkL
 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=
@@ -270,6 +286,7 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
 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=
diff --git a/toolbar/apinode/block.go b/toolbar/apinode/block.go
new file mode 100644 (file)
index 0000000..ffd2d00
--- /dev/null
@@ -0,0 +1,44 @@
+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)
+}
diff --git a/toolbar/apinode/node.go b/toolbar/apinode/node.go
new file mode 100644 (file)
index 0000000..ed106ea
--- /dev/null
@@ -0,0 +1,41 @@
+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)
+}
diff --git a/toolbar/apinode/transaction.go b/toolbar/apinode/transaction.go
new file mode 100644 (file)
index 0000000..2714b0a
--- /dev/null
@@ -0,0 +1,162 @@
+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)
+}
diff --git a/toolbar/common/address.go b/toolbar/common/address.go
new file mode 100644 (file)
index 0000000..b2eb879
--- /dev/null
@@ -0,0 +1,64 @@
+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
+}
diff --git a/toolbar/common/config.go b/toolbar/common/config.go
new file mode 100644 (file)
index 0000000..eec5a90
--- /dev/null
@@ -0,0 +1,14 @@
+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"`
+}
diff --git a/toolbar/common/db.go b/toolbar/common/db.go
new file mode 100644 (file)
index 0000000..9c69f09
--- /dev/null
@@ -0,0 +1,26 @@
+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
+}
diff --git a/toolbar/common/http_util.go b/toolbar/common/http_util.go
new file mode 100644 (file)
index 0000000..33417ef
--- /dev/null
@@ -0,0 +1,50 @@
+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)
+}
diff --git a/toolbar/vote_reward/config/config.go b/toolbar/vote_reward/config/config.go
new file mode 100644 (file)
index 0000000..e1c920e
--- /dev/null
@@ -0,0 +1,33 @@
+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)
+}
diff --git a/toolbar/vote_reward/database/dump_reward.sql b/toolbar/vote_reward/database/dump_reward.sql
new file mode 100644 (file)
index 0000000..4730d2f
--- /dev/null
@@ -0,0 +1,63 @@
+# ************************************************************
+# 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 */;
diff --git a/toolbar/vote_reward/database/orm/block_state.go b/toolbar/vote_reward/database/orm/block_state.go
new file mode 100644 (file)
index 0000000..75d1369
--- /dev/null
@@ -0,0 +1,6 @@
+package orm
+
+type ChainStatus struct {
+       BlockHeight uint64
+       BlockHash   string
+}
diff --git a/toolbar/vote_reward/database/orm/utxo.go b/toolbar/vote_reward/database/orm/utxo.go
new file mode 100644 (file)
index 0000000..d47ea55
--- /dev/null
@@ -0,0 +1,11 @@
+package orm
+
+type Utxo struct {
+       ID          uint64 `gorm:"primary_key"`
+       OutputID    string
+       Xpub        string
+       VoteAddress string
+       VoteNum     uint64
+       VoteHeight  uint64
+       VetoHeight  uint64
+}
diff --git a/toolbar/vote_reward/settlementvotereward/settlementreward.go b/toolbar/vote_reward/settlementvotereward/settlementreward.go
new file mode 100644 (file)
index 0000000..4210060
--- /dev/null
@@ -0,0 +1,148 @@
+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
+               }
+       }
+}
diff --git a/toolbar/vote_reward/synchron/block_keeper.go b/toolbar/vote_reward/synchron/block_keeper.go
new file mode 100644 (file)
index 0000000..6d0bcae
--- /dev/null
@@ -0,0 +1,155 @@
+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
+}