From 3df751629b0461637eef6d863f5f679c908c7eb4 Mon Sep 17 00:00:00 2001 From: wz Date: Tue, 23 Jul 2019 13:38:16 +0800 Subject: [PATCH] Reward util (#342) * 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 --- toolbar/api_node/block.go | 33 ++++ toolbar/api_node/node.go | 37 +++++ toolbar/api_node/node_test.go | 70 +++++++++ toolbar/api_node/transaction.go | 138 +++++++++++++++++ toolbar/{federation/util => common}/http_util.go | 2 +- toolbar/{common => federation}/service/node.go | 4 +- toolbar/federation/synchron/mainchain_keeper.go | 2 +- toolbar/federation/synchron/sidechain_keeper.go | 2 +- toolbar/reward/config/config.go | 46 ------ toolbar/reward/database/dump_reward.sql | 29 ---- toolbar/reward/database/orm/block_state.go | 6 - toolbar/reward/database/orm/vote_utxo.go | 11 -- toolbar/reward/reward.go | 4 - toolbar/reward/synchron/block_keeper.go | 185 ----------------------- toolbar/reward/synchron/errors.go | 10 -- toolbar/vote_reward/config/config.go | 37 +++++ toolbar/vote_reward/database/dump_reward.sql | 63 ++++++++ toolbar/vote_reward/database/orm/block_state.go | 6 + toolbar/vote_reward/database/orm/utxo.go | 11 ++ toolbar/vote_reward/synchron/block_keeper.go | 171 +++++++++++++++++++++ 20 files changed, 571 insertions(+), 296 deletions(-) create mode 100644 toolbar/api_node/block.go create mode 100644 toolbar/api_node/node.go create mode 100644 toolbar/api_node/node_test.go create mode 100644 toolbar/api_node/transaction.go rename toolbar/{federation/util => common}/http_util.go (98%) rename toolbar/{common => federation}/service/node.go (94%) delete mode 100644 toolbar/reward/config/config.go delete mode 100644 toolbar/reward/database/dump_reward.sql delete mode 100644 toolbar/reward/database/orm/block_state.go delete mode 100644 toolbar/reward/database/orm/vote_utxo.go delete mode 100644 toolbar/reward/reward.go delete mode 100644 toolbar/reward/synchron/block_keeper.go delete mode 100644 toolbar/reward/synchron/errors.go create mode 100644 toolbar/vote_reward/config/config.go create mode 100644 toolbar/vote_reward/database/dump_reward.sql create mode 100644 toolbar/vote_reward/database/orm/block_state.go create mode 100644 toolbar/vote_reward/database/orm/utxo.go create mode 100644 toolbar/vote_reward/synchron/block_keeper.go diff --git a/toolbar/api_node/block.go b/toolbar/api_node/block.go new file mode 100644 index 00000000..a9789a28 --- /dev/null +++ b/toolbar/api_node/block.go @@ -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 index 00000000..a0567232 --- /dev/null +++ b/toolbar/api_node/node.go @@ -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 index 00000000..681bf029 --- /dev/null +++ b/toolbar/api_node/node_test.go @@ -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 index 00000000..82ff3c95 --- /dev/null +++ b/toolbar/api_node/transaction.go @@ -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) +} diff --git a/toolbar/federation/util/http_util.go b/toolbar/common/http_util.go similarity index 98% rename from toolbar/federation/util/http_util.go rename to toolbar/common/http_util.go index b6a1cb31..33417efb 100644 --- a/toolbar/federation/util/http_util.go +++ b/toolbar/common/http_util.go @@ -1,4 +1,4 @@ -package util +package common import ( "bytes" diff --git a/toolbar/common/service/node.go b/toolbar/federation/service/node.go similarity index 94% rename from toolbar/common/service/node.go rename to toolbar/federation/service/node.go index 38172888..2775e469 100644 --- a/toolbar/common/service/node.go +++ b/toolbar/federation/service/node.go @@ -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 } diff --git a/toolbar/federation/synchron/mainchain_keeper.go b/toolbar/federation/synchron/mainchain_keeper.go index 030aa37b..c8aec4ae 100644 --- a/toolbar/federation/synchron/mainchain_keeper.go +++ b/toolbar/federation/synchron/mainchain_keeper.go @@ -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 { diff --git a/toolbar/federation/synchron/sidechain_keeper.go b/toolbar/federation/synchron/sidechain_keeper.go index f576e62f..80061f18 100644 --- a/toolbar/federation/synchron/sidechain_keeper.go +++ b/toolbar/federation/synchron/sidechain_keeper.go @@ -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 index 95ed77ad..00000000 --- a/toolbar/reward/config/config.go +++ /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 index 59938678..00000000 --- a/toolbar/reward/database/dump_reward.sql +++ /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 index 6fdd1550..00000000 --- a/toolbar/reward/database/orm/block_state.go +++ /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 index 43951817..00000000 --- a/toolbar/reward/database/orm/vote_utxo.go +++ /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 index 4625b5bd..00000000 --- a/toolbar/reward/reward.go +++ /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 index 6a3bda38..00000000 --- a/toolbar/reward/synchron/block_keeper.go +++ /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 index 5e58fde8..00000000 --- a/toolbar/reward/synchron/errors.go +++ /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 index 00000000..b73bb95f --- /dev/null +++ b/toolbar/vote_reward/config/config.go @@ -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 index 00000000..4730d2fc --- /dev/null +++ b/toolbar/vote_reward/database/dump_reward.sql @@ -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 index 00000000..75d1369d --- /dev/null +++ b/toolbar/vote_reward/database/orm/block_state.go @@ -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 index 00000000..d47ea555 --- /dev/null +++ b/toolbar/vote_reward/database/orm/utxo.go @@ -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 index 00000000..2b78a663 --- /dev/null +++ b/toolbar/vote_reward/synchron/block_keeper.go @@ -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 +} -- 2.11.0