--- /dev/null
+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)
+}
--- /dev/null
+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)
+}
--- /dev/null
+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))
+ }
+ }
+}
--- /dev/null
+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)
+}
-package util
+package common
import (
"bytes"
"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
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
}
"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 {
"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 {
+++ /dev/null
-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"`
-}
+++ /dev/null
-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;
+++ /dev/null
-package orm
-
-type BlockState struct {
- Height uint64
- BlockHash string
-}
+++ /dev/null
-package orm
-
-type Utxo struct {
- ID uint64 `gorm:"primary_key"`
- Xpub string
- VoterAddress string
- VoteHeight uint64
- VoteNum uint64
- VetoHeight uint64
- OutputID string
-}
+++ /dev/null
-package reward
-
-type Reward interface {
-}
+++ /dev/null
-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
-}
+++ /dev/null
-package synchron
-
-import (
- "github.com/vapor/errors"
-)
-
-var (
- ErrInconsistentDB = errors.New("inconsistent db status")
- ErrOutputType = errors.New("error output type")
-)
--- /dev/null
+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)
+}
--- /dev/null
+# ************************************************************
+# Sequel Pro SQL dump
+# Version 4541
+#
+# http://www.sequelpro.com/
+# https://github.com/sequelpro/sequelpro
+#
+# Host: 127.0.0.1 (MySQL 5.7.24)
+# Database: vote_reward
+# Generation Time: 2019-07-22 13:41:50 +0000
+# ************************************************************
+
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+
+# Dump of table chain_statuses
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `chain_statuses`;
+
+CREATE TABLE `chain_statuses` (
+ `block_height` int(11) NOT NULL,
+ `block_hash` varchar(64) NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+
+
+# Dump of table utxos
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `utxos`;
+
+CREATE TABLE `utxos` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `output_id` varchar(64) NOT NULL,
+ `xpub` varchar(128) NOT NULL,
+ `vote_address` varchar(62) NOT NULL,
+ `vote_num` bigint(21) NOT NULL,
+ `vote_height` int(11) NOT NULL,
+ `veto_height` int(11) NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `output_id` (`output_id`),
+ KEY `xpub` (`xpub`),
+ KEY `vote_height` (`vote_height`),
+ KEY `veto_height` (`veto_height`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+
+
+
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
--- /dev/null
+package orm
+
+type ChainStatus struct {
+ BlockHeight uint64
+ BlockHash string
+}
--- /dev/null
+package orm
+
+type Utxo struct {
+ ID uint64 `gorm:"primary_key"`
+ OutputID string
+ Xpub string
+ VoteAddress string
+ VoteNum uint64
+ VoteHeight uint64
+ VetoHeight uint64
+}
--- /dev/null
+package 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
+}