OSDN Git Service

Reward util (#342)
authorwz <mars@bytom.io>
Tue, 23 Jul 2019 05:38:16 +0000 (13:38 +0800)
committerPaladz <yzhu101@uottawa.ca>
Tue, 23 Jul 2019 05:38:16 +0000 (13:38 +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

* delete code

* add util

* modify sync

* add test

* modify db

* restore file

* fix review

* modify error

* add block hash

* try edit the code format

* add unit test

* modify table name

* fix review

* fix review

20 files changed:
toolbar/api_node/block.go [new file with mode: 0644]
toolbar/api_node/node.go [new file with mode: 0644]
toolbar/api_node/node_test.go [new file with mode: 0644]
toolbar/api_node/transaction.go [new file with mode: 0644]
toolbar/common/http_util.go [moved from toolbar/federation/util/http_util.go with 98% similarity]
toolbar/federation/service/node.go [moved from toolbar/common/service/node.go with 94% similarity]
toolbar/federation/synchron/mainchain_keeper.go
toolbar/federation/synchron/sidechain_keeper.go
toolbar/reward/config/config.go [deleted file]
toolbar/reward/database/dump_reward.sql [deleted file]
toolbar/reward/database/orm/block_state.go [deleted file]
toolbar/reward/database/orm/vote_utxo.go [deleted file]
toolbar/reward/reward.go [deleted file]
toolbar/reward/synchron/block_keeper.go [deleted file]
toolbar/reward/synchron/errors.go [deleted file]
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/synchron/block_keeper.go [new file with mode: 0644]

diff --git a/toolbar/api_node/block.go b/toolbar/api_node/block.go
new file mode 100644 (file)
index 0000000..a9789a2
--- /dev/null
@@ -0,0 +1,33 @@
+package apinode
+
+import (
+       "encoding/json"
+
+       "github.com/vapor/api"
+       "github.com/vapor/errors"
+       "github.com/vapor/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)
+}
diff --git a/toolbar/api_node/node.go b/toolbar/api_node/node.go
new file mode 100644 (file)
index 0000000..a056723
--- /dev/null
@@ -0,0 +1,37 @@
+package apinode
+
+import (
+       "encoding/json"
+
+       "github.com/vapor/errors"
+       "github.com/vapor/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)
+       }
+
+       return json.Unmarshal(resp.Data, respData)
+}
diff --git a/toolbar/api_node/node_test.go b/toolbar/api_node/node_test.go
new file mode 100644 (file)
index 0000000..681bf02
--- /dev/null
@@ -0,0 +1,70 @@
+package apinode
+
+import (
+       "encoding/json"
+       "testing"
+
+       "github.com/vapor/consensus"
+       "github.com/vapor/errors"
+       "github.com/vapor/protocol/bc"
+)
+
+func buildTxRequest(accountID string, outputs map[string]uint64) ([]byte, error) {
+       totalBTM := uint64(10000000)
+       actions := []interface{}{}
+       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},
+       })
+       payload, err := json.Marshal(&buildTxReq{Actions: actions})
+       if err != nil {
+               return nil, errors.Wrap(err, "Marshal spend request")
+       }
+
+       return payload, nil
+}
+
+type args struct {
+       accountID string
+       outputs   map[string]uint64
+}
+
+func TestBuildTxRequest(t *testing.T) {
+       cases := []struct {
+               args args
+               want string
+       }{
+               {
+                       args: args{
+                               accountID: "9bb77612-350e-4d53-81e2-525b28247ba5",
+                               outputs:   map[string]uint64{"sp1qlryy65a5apylphqp6axvhx7nd6y2zlexuvn7gf": 100},
+                       },
+                       want: `{"actions":[{"type":"control_address","address":"sp1qlryy65a5apylphqp6axvhx7nd6y2zlexuvn7gf","asset_id":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","amount":100},{"type":"spend_account","account_id":"9bb77612-350e-4d53-81e2-525b28247ba5","asset_id":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","amount":10000100}]}`,
+               },
+               {
+                       args: args{
+                               accountID: "9bb77612-350e-4d53-81e2-525b28247ba5",
+                               outputs:   map[string]uint64{"sp1qlryy65a5apylphqp6axvhx7nd6y2zlexuvn7gf": 100, "sp1qcgtxkhfzytul4lfttwex3skfqhm0tg6ms9da28": 200},
+                       },
+                       want: `{"actions":[{"type":"control_address","address":"sp1qlryy65a5apylphqp6axvhx7nd6y2zlexuvn7gf","asset_id":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","amount":100},{"type":"control_address","address":"sp1qcgtxkhfzytul4lfttwex3skfqhm0tg6ms9da28","asset_id":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","amount":200},{"type":"spend_account","account_id":"9bb77612-350e-4d53-81e2-525b28247ba5","asset_id":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","amount":10000300}]}`,
+               },
+       }
+
+       for i, c := range cases {
+               tx, err := buildTxRequest(c.args.accountID, c.args.outputs)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               if string(tx) != string(c.want) {
+                       t.Fatal(i, string(tx))
+               }
+       }
+}
diff --git a/toolbar/api_node/transaction.go b/toolbar/api_node/transaction.go
new file mode 100644 (file)
index 0000000..82ff3c9
--- /dev/null
@@ -0,0 +1,138 @@
+package apinode
+
+import (
+       "encoding/json"
+
+       "github.com/vapor/blockchain/txbuilder"
+       "github.com/vapor/consensus"
+       "github.com/vapor/errors"
+       "github.com/vapor/protocol/bc"
+       "github.com/vapor/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,
+       })
+}
+
+func (n *Node) BatchSendBTM(accountID, password string, outputs map[string]uint64) error {
+       totalBTM := uint64(10000000)
+       actions := []interface{}{}
+       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
+       }
+
+       _, err = n.SubmitTx(tpl.Transaction)
+       return err
+}
+
+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)
+}
similarity index 98%
rename from toolbar/federation/util/http_util.go
rename to toolbar/common/http_util.go
index b6a1cb3..33417ef 100644 (file)
@@ -1,4 +1,4 @@
-package util
+package common
 
 import (
        "bytes"
similarity index 94%
rename from toolbar/common/service/node.go
rename to toolbar/federation/service/node.go
index 3817288..2775e46 100644 (file)
@@ -4,8 +4,8 @@ import (
        "encoding/json"
 
        "github.com/vapor/errors"
-       "github.com/vapor/toolbar/federation/util"
        "github.com/vapor/protocol/bc"
+       "github.com/vapor/toolbar/common"
 )
 
 // Node can invoke the api which provide by the full node server
@@ -66,7 +66,7 @@ type response struct {
 
 func (n *Node) request(path string, payload []byte, respData interface{}) error {
        resp := &response{}
-       if err := util.Post(n.hostPort+path, payload, resp); err != nil {
+       if err := common.Post(n.hostPort+path, payload, resp); err != nil {
                return err
        }
 
index 030aa37..c8aec4a 100644 (file)
@@ -17,11 +17,11 @@ import (
        "github.com/vapor/consensus"
        "github.com/vapor/errors"
        "github.com/vapor/protocol/bc"
-       "github.com/vapor/toolbar/common/service"
        "github.com/vapor/toolbar/federation/common"
        "github.com/vapor/toolbar/federation/config"
        "github.com/vapor/toolbar/federation/database"
        "github.com/vapor/toolbar/federation/database/orm"
+       "github.com/vapor/toolbar/federation/service"
 )
 
 type mainchainKeeper struct {
index f576e62..80061f1 100644 (file)
@@ -13,11 +13,11 @@ import (
        "github.com/vapor/errors"
        "github.com/vapor/protocol/bc"
        "github.com/vapor/protocol/bc/types"
-       "github.com/vapor/toolbar/common/service"
        "github.com/vapor/toolbar/federation/common"
        "github.com/vapor/toolbar/federation/config"
        "github.com/vapor/toolbar/federation/database"
        "github.com/vapor/toolbar/federation/database/orm"
+       "github.com/vapor/toolbar/federation/service"
 )
 
 type sidechainKeeper struct {
diff --git a/toolbar/reward/config/config.go b/toolbar/reward/config/config.go
deleted file mode 100644 (file)
index 95ed77a..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-package config
-
-import (
-       "encoding/json"
-       "os"
-
-       log "github.com/sirupsen/logrus"
-
-       "github.com/vapor/crypto/ed25519/chainkd"
-       "github.com/vapor/toolbar/common"
-)
-
-func NewConfig() *Config {
-       if len(os.Args) <= 1 {
-               log.Fatal("Please setup the config file path")
-       }
-
-       return NewConfigWithPath(os.Args[1])
-}
-
-func NewConfigWithPath(path string) *Config {
-       configFile, err := os.Open(path)
-       if err != nil {
-               log.WithFields(log.Fields{"err": err, "file_path": os.Args[1]}).Fatal("fail to open config file")
-       }
-       defer configFile.Close()
-
-       cfg := &Config{}
-       if err := json.NewDecoder(configFile).Decode(cfg); err != nil {
-               log.WithField("err", err).Fatal("fail to decode config file")
-       }
-
-       return cfg
-}
-
-type Config struct {
-       MySQLConfig common.MySQLConfig `json:"mysql"`
-       Chain       Chain              `json:"chain"`
-       XPubs       []chainkd.XPub     `json:"xpubs"`
-}
-
-type Chain struct {
-       Name        string `json:"name"`
-       Upstream    string `json:"upstream"`
-       SyncSeconds uint64 `json:"sync_seconds"`
-}
diff --git a/toolbar/reward/database/dump_reward.sql b/toolbar/reward/database/dump_reward.sql
deleted file mode 100644 (file)
index 5993867..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-SET NAMES utf8mb4;
-SET FOREIGN_KEY_CHECKS = 0;
-
--- ----------------------------
--- Table structure for block_state
--- ----------------------------
-DROP TABLE IF EXISTS `block_state`;
-CREATE TABLE `block_state`  (
-  `height` int(11) NOT NULL,
-  `block_hash` varchar(64) NOT NULL
-) ENGINE = InnoDB DEFAULT CHARSET=utf8;
-
--- ----------------------------
--- Table structure for vote
--- ----------------------------
-DROP TABLE IF EXISTS `vote`;
-CREATE TABLE `vote`  (
-  `id` int(11) NOT NULL AUTO_INCREMENT,
-  `xpub` varchar(128) NOT NULL,
-  `voter_address` varchar(62) NOT NULL,
-  `vote_height` int(11) NOT NULL,
-  `vote_num` int(11) NOT NULL,
-  `veto_height` int(11) NOT NULL,
-  `output_id` varchar(64) NOT NULL,
-  PRIMARY KEY (`id`) USING BTREE,
-  UNIQUE INDEX `xpub`(`xpub`, `vote_height`, `output_id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 6 DEFAULT CHARSET=utf8;
-
-SET FOREIGN_KEY_CHECKS = 1;
diff --git a/toolbar/reward/database/orm/block_state.go b/toolbar/reward/database/orm/block_state.go
deleted file mode 100644 (file)
index 6fdd155..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-package orm
-
-type BlockState struct {
-       Height    uint64
-       BlockHash string
-}
diff --git a/toolbar/reward/database/orm/vote_utxo.go b/toolbar/reward/database/orm/vote_utxo.go
deleted file mode 100644 (file)
index 4395181..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-package orm
-
-type Utxo struct {
-       ID           uint64 `gorm:"primary_key"`
-       Xpub         string
-       VoterAddress string
-       VoteHeight   uint64
-       VoteNum      uint64
-       VetoHeight   uint64
-       OutputID     string
-}
diff --git a/toolbar/reward/reward.go b/toolbar/reward/reward.go
deleted file mode 100644 (file)
index 4625b5b..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-package reward
-
-type Reward interface {
-}
diff --git a/toolbar/reward/synchron/block_keeper.go b/toolbar/reward/synchron/block_keeper.go
deleted file mode 100644 (file)
index 6a3bda3..0000000
+++ /dev/null
@@ -1,185 +0,0 @@
-package synchron
-
-import (
-       "encoding/hex"
-       "time"
-
-       "github.com/jinzhu/gorm"
-       log "github.com/sirupsen/logrus"
-
-       "github.com/vapor/errors"
-       "github.com/vapor/protocol/bc"
-       "github.com/vapor/protocol/bc/types"
-       "github.com/vapor/toolbar/common"
-       "github.com/vapor/toolbar/common/service"
-       "github.com/vapor/toolbar/reward/config"
-       "github.com/vapor/toolbar/reward/database/orm"
-)
-
-type ChainKeeper struct {
-       cfg  *config.Chain
-       db   *gorm.DB
-       node *service.Node
-}
-
-func NewChainKeeper(db *gorm.DB, cfg *config.Config) *ChainKeeper {
-       return &ChainKeeper{
-               cfg:  &cfg.Chain,
-               db:   db,
-               node: service.NewNode(cfg.Chain.Upstream),
-       }
-}
-
-func (c *ChainKeeper) Run() {
-       ticker := time.NewTicker(time.Duration(c.cfg.SyncSeconds) * time.Second)
-       for ; true; <-ticker.C {
-               for {
-                       isUpdate, err := c.syncBlock()
-                       if err != nil {
-                               log.WithField("error", err).Errorln("blockKeeper fail on process block")
-                               break
-                       }
-
-                       if !isUpdate {
-                               break
-                       }
-               }
-       }
-}
-
-func (c *ChainKeeper) syncBlock() (bool, error) {
-       blockState := &orm.BlockState{}
-       if err := c.db.First(blockState).Error; err != nil {
-               return false, errors.Wrap(err, "query chain")
-       }
-
-       height, err := c.node.GetBlockCount()
-       if err != nil {
-               return false, err
-       }
-
-       if height == blockState.Height {
-               return false, nil
-       }
-
-       nextBlockStr, txStatus, err := c.node.GetBlockByHeight(blockState.Height + 1)
-       if err != nil {
-               return false, err
-       }
-
-       nextBlock := &types.Block{}
-       if err := nextBlock.UnmarshalText([]byte(nextBlockStr)); err != nil {
-               return false, errors.New("Unmarshal nextBlock")
-       }
-
-       // Normal case, the previous hash of next block equals to the hash of current block,
-       // just sync to database directly.
-       if nextBlock.PreviousBlockHash.String() == blockState.BlockHash {
-               return true, c.AttachBlock(nextBlock, txStatus)
-       }
-
-       log.WithField("block height", blockState.Height).Debug("the prev hash of remote is not equals the hash of current best block, must rollback")
-       currentBlockStr, txStatus, err := c.node.GetBlockByHash(blockState.BlockHash)
-       if err != nil {
-               return false, err
-       }
-
-       currentBlock := &types.Block{}
-       if err := nextBlock.UnmarshalText([]byte(currentBlockStr)); err != nil {
-               return false, errors.New("Unmarshal currentBlock")
-       }
-
-       return true, c.DetachBlock(currentBlock, txStatus)
-}
-
-func (c *ChainKeeper) AttachBlock(block *types.Block, txStatus *bc.TransactionStatus) error {
-       ormDB := c.db.Begin()
-       for pos, tx := range block.Transactions {
-               statusFail, err := txStatus.GetStatus(pos)
-               if err != nil {
-                       return err
-               }
-
-               if statusFail {
-                       log.WithFields(log.Fields{"block height": block.Height, "statusFail": statusFail}).Debug("AttachBlock")
-                       continue
-               }
-
-               for _, input := range tx.Inputs {
-                       vetoInput, ok := input.TypedInput.(*types.VetoInput)
-                       if !ok {
-                               continue
-                       }
-
-                       outputID, err := input.SpentOutputID()
-                       if err != nil {
-                               return err
-                       }
-                       utxo := &orm.Utxo{
-                               VoterAddress: common.GetAddressFromControlProgram(vetoInput.ControlProgram),
-                               OutputID:     outputID.String(),
-                       }
-                       // update data
-                       db := ormDB.Where(utxo).Update("veto_height", block.Height)
-                       if err := db.Error; err != nil {
-                               ormDB.Rollback()
-                               return err
-                       }
-
-                       if db.RowsAffected != 1 {
-                               ormDB.Rollback()
-                               return ErrInconsistentDB
-                       }
-
-               }
-
-               for index, output := range tx.Outputs {
-                       voteOutput, ok := output.TypedOutput.(*types.VoteOutput)
-                       if !ok {
-                               continue
-                       }
-                       pubkey := hex.EncodeToString(voteOutput.Vote)
-                       outputID := tx.OutputID(index)
-                       utxo := &orm.Utxo{
-                               Xpub:         pubkey,
-                               VoterAddress: common.GetAddressFromControlProgram(voteOutput.ControlProgram),
-                               VoteHeight:   block.Height,
-                               VoteNum:      voteOutput.Amount,
-                               VetoHeight:   0,
-                               OutputID:     outputID.String(),
-                       }
-                       // insert data
-                       if err := ormDB.Save(utxo).Error; err != nil {
-                               ormDB.Rollback()
-                               return err
-                       }
-               }
-       }
-
-       return ormDB.Commit().Error
-}
-
-func (c *ChainKeeper) DetachBlock(block *types.Block, txStatus *bc.TransactionStatus) error {
-       ormDB := c.db.Begin()
-
-       utxo := &orm.Utxo{
-               VoteHeight: block.Height,
-       }
-       // insert data
-       if err := ormDB.Where(utxo).Delete(&orm.Utxo{}).Error; err != nil {
-               ormDB.Rollback()
-               return err
-       }
-
-       utxo = &orm.Utxo{
-               VetoHeight: block.Height,
-       }
-
-       // update data
-       if err := ormDB.Where(utxo).Update("veto_height", 0).Error; err != nil {
-               ormDB.Rollback()
-               return err
-       }
-
-       return ormDB.Commit().Error
-}
diff --git a/toolbar/reward/synchron/errors.go b/toolbar/reward/synchron/errors.go
deleted file mode 100644 (file)
index 5e58fde..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-package synchron
-
-import (
-       "github.com/vapor/errors"
-)
-
-var (
-       ErrInconsistentDB = errors.New("inconsistent db status")
-       ErrOutputType     = errors.New("error output type")
-)
diff --git a/toolbar/vote_reward/config/config.go b/toolbar/vote_reward/config/config.go
new file mode 100644 (file)
index 0000000..b73bb95
--- /dev/null
@@ -0,0 +1,37 @@
+package config
+
+import (
+       "bytes"
+       "encoding/json"
+       "io/ioutil"
+       "os"
+
+       "github.com/vapor/toolbar/common"
+)
+
+type Config struct {
+       MySQLConfig common.MySQLConfig `json:"mysql"`
+       NodeIP      string             `json:"node_ip"`
+}
+
+func ExportConfigFile(configFile string, config *Config) error {
+       buf := new(bytes.Buffer)
+
+       encoder := json.NewEncoder(buf)
+       encoder.SetIndent("", "  ")
+       if err := encoder.Encode(config); err != nil {
+               return err
+       }
+
+       return ioutil.WriteFile(configFile, buf.Bytes(), 0644)
+}
+
+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/synchron/block_keeper.go b/toolbar/vote_reward/synchron/block_keeper.go
new file mode 100644 (file)
index 0000000..2b78a66
--- /dev/null
@@ -0,0 +1,171 @@
+package synchron
+
+import (
+       "encoding/hex"
+
+       "github.com/jinzhu/gorm"
+       log "github.com/sirupsen/logrus"
+
+       "github.com/vapor/errors"
+       "github.com/vapor/protocol/bc/types"
+       apinode "github.com/vapor/toolbar/api_node"
+       "github.com/vapor/toolbar/common"
+       "github.com/vapor/toolbar/vote_reward/config"
+       "github.com/vapor/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,
+       }
+
+       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
+       }
+
+       // Normal case, the previous hash of next block equals to the hash of current block,
+       // just sync to database directly.
+       if nextBlock.PreviousBlockHash.String() == chainStatus.BlockHash {
+               return c.AttachBlock(db, chainStatus, nextBlock)
+       }
+
+       log.WithField("block height", chainStatus.BlockHeight).Debug("the prev hash of remote is not equals the hash of current best block, must rollback")
+       currentBlock, err := c.node.GetBlockByHash(chainStatus.BlockHash)
+       if err != nil {
+               return err
+       }
+
+       return c.DetachBlock(db, chainStatus, currentBlock)
+}
+
+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(voteOutput.ControlProgram),
+                               VoteHeight:  block.Height,
+                               VoteNum:     voteOutput.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) DetachBlock(db *gorm.DB, chainStatus *orm.ChainStatus, block *types.Block) error {
+       if err := db.Where(&orm.Utxo{VoteHeight: block.Height}).Delete(&orm.Utxo{}).Error; err != nil {
+               return err
+       }
+
+       if err := db.Where(&orm.Utxo{VetoHeight: block.Height}).Update("veto_height", 0).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
+}