基於比原鏈開發Dapp(四)-bufferserver源碼分析

##簡介

​    本章內容主要直接分析bufferserver源碼,也就是比原鏈官方Dapp-demo的後端接口,裏面包含了UTXO的託管邏輯、賬單邏輯等,還會介紹一些改進的源碼內容。

[儲蓄分紅合約後端bufferserver源碼](https://github.com/oysheng/bufferserver)

本次源碼分析主要根據bufferserver,2019年5月13號的版本,到此3個月沒有更新了。

### 源碼分析

​    我們來看看bufferserver的源碼,項目是用golang語言開發的web服務端,內容比較簡單也就幾個接口。先看看源碼的結構:

所有的golang項目首先都要看一下main.go,但是本項目有兩個,因爲一個是負責web的http接口的,另外一個是負責後端同步數據的。

先看看錶結構,dump.sql

##基礎配置表
CREATE TABLE `bases` (
  `id` int(11) NOT NULL AUTO_INCREMENT,    
  `asset_id` char(64) NOT NULL,            ##合約鎖定的資產ID
  `control_program` text NOT NULL,        ##合約的代碼,在第二章裏面提及通過equit工具生成
  PRIMARY KEY (`id`),
  KEY `asset_id` (`asset_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

#賬單表
CREATE TABLE `balances` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `address` varchar(256) NOT NULL,            ##地址
  `asset_id` char(64) NOT NULL,                ##涉及的asset_id
  `amount` bigint(20) DEFAULT '0',            ##交易的金額
  `tx_id` char(64) NOT NULL,                ##交易ID
  `status_fail` tinyint(1) DEFAULT '0',        ##狀態
  `is_confirmed` tinyint(1) DEFAULT '0',    ##交易是否確認
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,    ##創建時間
  PRIMARY KEY (`id`),
  KEY `address` (`address`),
  KEY `asset_id` (`asset_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

##UTXO表,最重要最核心這個表
CREATE TABLE `utxos` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `hash` char(64) NOT NULL,                        ###UTXO的哈希,其實就是錢包裏面UTXO的id
  `asset_id` char(64) NOT NULL,                    ##資產ID
  `amount` bigint(20) unsigned DEFAULT '0',         ##UTXO    的額度    
  `control_program` text NOT NULL,                ##該UTXO對應的鎖定合約    
  `is_spend` tinyint(1) DEFAULT '0',            ##是否已經使用
  `is_locked` tinyint(1) DEFAULT '0',            ##是否鎖定
  `submit_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,        ##提交時間
  `duration` bigint(20) unsigned DEFAULT '0',        ##鎖定時間
  PRIMARY KEY (`id`),
  UNIQUE KEY `hash` (`hash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

看到表結構,估計各位已經懂了核心邏輯,基本上就是同步UTXO過來,前端使用的時候鎖定,然後如果使用了就更改狀態,如果沒有使用就解放放開鎖,如圖。


我們來看看同步數據的main.go。

cmd/updater/mian.go
 

func main() {
    cfg := config.NewConfig()     //####### 1.
    db, err := database.NewMySQLDB(cfg.MySQL, cfg.Updater.BlockCenter.MySQLConnCfg)
    if err != nil {
        log.WithField("err", err).Panic("initialize mysql db error")
    }

    go synchron.NewBlockCenterKeeper(cfg, db.Master()).Run()   //####### 2.
    go synchron.NewBrowserKeeper(cfg, db.Master()).Run()       //####### 3.

    // keep the main func running in case of terminating goroutines
    var wg sync.WaitGroup
    wg.Add(1)
    wg.Wait()
}


1)啓動的時候讀取配置文件,config_local.json, 讀取相關配置;

2)定時任務,定時在blockcenter裏面同步對應的utxo數據,以及UTXO的狀態;

3)定時任務,定時判斷鎖定的UTXO,超過時間恢復狀態;

先來看看NewBlockCenterKeeper,blockcenter.go



func (b *blockCenterKeeper) Run() {
    ticker := time.NewTicker(time.Duration(b.cfg.Updater.BlockCenter.SyncSeconds) * time.Second) //####### 1.
    for ; true; <-ticker.C {
        if err := b.syncBlockCenter(); err != nil { //####### 2.
            log.WithField("err", err).Errorf("fail on bytom blockcenter")
        }
    }
}

1)非常簡單,初始化一個定時器,定時時間是b.cfg.Updater.BlockCenter.SyncSeconds = 60 秒;

2)定期調用syncBlockCenter()方法;

blockcenter.go 的syncBlockCenter 方法 

func (b *blockCenterKeeper) syncBlockCenter() error {
    var bases []*orm.Base
    if err := b.db.Find(&bases).Error; err != nil {
        return errors.Wrap(err, "query bases")
    }

    filter := make(map[string]interface{})
    for _, base := range bases {
        filter["asset"] = base.AssetID
        filter["script"] = base.ControlProgram
        filter["unconfirmed"] = true
        req := &common.Display{Filter: filter}
        resUTXOs, err := b.service.ListBlockCenterUTXOs(req)  //####### 1.
        if err != nil {
            return errors.Wrap(err, "list blockcenter utxos")
        }
        //####### 2.
        if err := b.updateOrSaveUTXO(base.AssetID, base.ControlProgram, resUTXOs); err != nil {
            return err
        }
        //####### 3.
        if err := b.updateUTXOStatus(base.AssetID, base.ControlProgram, resUTXOs); err != nil {
            return err
        }
    }

    if err := b.delIrrelevantUTXO(); err != nil {//####### 4.
        return err
    }

    return nil
}
 

1)調用blockcenter接口,查詢UTXO列表;

2)updateOrSaveUTXO方法,插入或者更新UTXO鎖定狀態;‘

2)updateUTXOStatus方法,更新UTXO的使用狀態;

調用**blockcenter接口**,非常簡單,不過要注意這裏程序裏面unconfirmed = true方式去調用,

當unconfirmed=false的時候,返回的是所有已經確定交易的UTXO;

當unconfirmed=true的時候,返回的是包含已確認的、未確認的交易衍生出來的UTXO;

**PS:這裏有個大坑,我搞了一筆肯定會失敗的交易,衍生出來的UTXO,一樣會返回過來,容易產生鏈式錯誤,所以我們應該儘可能保證我們的DAPP對應合約交易是一定會成功的,這個很容易,最怕惡意攻擊,具體在第三章內容已經提及過了。**

blockcenter.go 的**updateOrSaveUTXO**

 

func (b *blockCenterKeeper) updateOrSaveUTXO(asset string, program string, bcUTXOs []*service.AttachUtxo) error {
    for _, butxo := range bcUTXOs {
        utxo := orm.Utxo{Hash: butxo.Hash}
        //####### 1.
        if err := b.db.Where(utxo).First(&utxo).Error; err != nil && err != gorm.ErrRecordNotFound {
            return errors.Wrap(err, "query utxo")
        } else if err == gorm.ErrRecordNotFound {
            //####### 2.
            utxo := &orm.Utxo{
                Hash:           butxo.Hash,
                AssetID:        butxo.Asset,
                Amount:         butxo.Amount,
                ControlProgram: program,
                IsSpend:        false,
                IsLocked:       false,
                Duration:       uint64(600),
            }
            if err := b.db.Save(utxo).Error; err != nil {
                return errors.Wrap(err, "save utxo")
            }
            continue
        }
        //####### 3.
        if time.Now().Unix()-utxo.SubmitTime.Unix() < int64(utxo.Duration) {
            continue
        }
        //####### 4.
        if err := b.db.Model(&orm.Utxo{}).Where(&orm.Utxo{Hash: butxo.Hash}).Where("is_locked = true").Update("is_locked", false).Error; err != nil {
            return errors.Wrap(err, "update utxo unlocked")
        }
    }
    return nil
}



 

1)通過utxo的hash,查詢自己的數據庫,如果查到就賦值給utxo;

2)如果查不到就會報錯gorm.ErrRecordNotFound,就定義一個utxo,插入數據庫表;

3)判斷裏面表裏面鎖定的時間是否超過了,因爲有可能有些utxo數據被鎖定了;

4)如果超過時間,該utxo還依然存在,那麼代表UTXO沒有被消耗掉,那麼直接解鎖;

blockcenter.go 的**updateUTXOStatus**```go

 

func (b *blockCenterKeeper) updateUTXOStatus(asset string, program string, bcUTXOs []*service.AttachUtxo) error {
    utxoMap := make(map[string]bool)
    for _, butxo := range bcUTXOs {
        utxoMap[butxo.Hash] = true
    }

    var utxos []*orm.Utxo
    //####### 1.
    if err := b.db.Model(&orm.Utxo{}).Where(&orm.Utxo{AssetID: asset, ControlProgram: program}).Where("is_spend = false").Find(&utxos).Error; err != nil {
        return errors.Wrap(err, "list unspent utxos")
    }

    for _, u := range utxos {
        if _, ok := utxoMap[u.Hash]; ok {
            continue
        }
        //####### 2.
        if err := b.db.Model(&orm.Utxo{}).Where(&orm.Utxo{Hash: u.Hash}).Update("is_spend", true).Error; err != nil {
            return errors.Wrap(err, "update utxo spent")
        }
    }

    return nil
}

1)查詢所有的未消耗的UTXO列表;’

2)循環數據庫查出來未消耗的UTXO列表,如果不在blockcenter查詢回來的UTXO列表裏面,代表已經消耗掉了,更改狀態is_spend = true,非常簡單;

#### 總結:現在已經講完了其中一個定時任務,比較簡單,就是同步一下數據而已;

看看另外一個定時任務

browser.go
    
 

 
func NewBrowserKeeper(cfg *config.Config, db *gorm.DB) *browserKeeper {
    service := service.NewService(cfg.Updater.Browser.URL)
    return &browserKeeper{
        cfg:     cfg,
        db:      db,
        service: service,
    }
}

func (b *browserKeeper) Run() {
    ticker := time.NewTicker(time.Duration(b.cfg.Updater.Browser.SyncSeconds) * time.Second)
    for ; true; <-ticker.C {
        if err := b.syncBrowser(); err != nil {
            log.WithField("err", err).Errorf("fail on bytom browser")
        }
    }
}

func (b *browserKeeper) syncBrowser() error {
    var balances []*orm.Balance
    if err := b.db.Model(&orm.Balance{}).Where("status_fail = false").Where("is_confirmed = false").Find(&balances).Error; err != nil {
        return errors.Wrap(err, "query balances")
    }

    expireTime := time.Duration(b.cfg.Updater.Browser.ExpirationHours) * time.Hour
    for _, balance := range balances {
        if balance.TxID == "" {
            if err := b.db.Delete(&orm.Balance{ID: balance.ID}).Error; err != nil {
                return errors.Wrap(err, "delete without TxID balance record")
            }
            continue
        }

        res, err := b.service.GetTransactionStatus(balance.TxID)
        if err != nil {
            log.WithField("err", err).Errorf("fail on query transaction [%s] from bytom browser", balance.TxID)
            continue
        }

        if res.Height == 0 {
            if time.Now().Unix()-balance.CreatedAt.Unix() > int64(expireTime) {
                if err := b.db.Delete(&orm.Balance{ID: balance.ID}).Error; err != nil {
                    return errors.Wrap(err, "delete expiration balance record")
                }
            }
            continue
        }

        if err := b.db.Model(&orm.Balance{}).Where(&orm.Balance{ID: balance.ID}).Update("status_fail", res.StatusFail).Update("is_confirmed", true).Error; err != nil {
            return errors.Wrap(err, "update balance")
        }
    }

這裏不直接深入講解,因爲經歷上面的講解,已經非常容易理解,就是同步交易的狀態,更新本地的庫,但是balance的數據是前端接口同步過來的,這樣設計上就有問題,應該所有的交易同步都從後端自己去同步。

## **分享一下自己的源碼:**

新建兩個表

 

#累積表,記錄當前同步的高度
CREATE TABLE `stats` (
  `start_height` int(11) NOT NULL,        #開始統計的高度
  `current_height` int(11) NOT NULL,    #當前高度
  `base_id` int(11) NOT NULL
)
#交易表
CREATE TABLE `transactions` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `hash` char(64) NOT NULL,                        #當前交易涉及的合約utxo的哈希
  `asset_id` char(64) NOT NULL,                    #合約涉及的utxo的資產ID
  `amount` bigint(20) unsigned DEFAULT '0',            #涉及的資產數目
  `address` varchar(256) NOT NULL,                        #獲取的
  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,    #交易的時間戳
  `base_id` int(11) NOT NULL,                        #關聯累積表配置ID    
  `height` int(11) DEFAULT NULL,                    #當前高度
  `transaction_id` varchar(255) NOT NULL,            #交易ID
  `input_amount` bigint(20) unsigned DEFAULT '0',        #用戶支付出去的輸入數目,我們這個dapp例子是以BTM爲基準
  PRIMARY KEY (`id`),
  UNIQUE KEY `hash` (`hash`)
)


 

參考一下源碼 

//爬取統計邏輯-------------------------------start
    //查詢統計配置
    var stats []*orm.Stat
    if err := b.db.Find(&stats).Error; err != nil {
        return errors.Wrap(err, "query stats")
    }

    for _, stat := range stats {
        height := stat.StartHeight
        if(stat.CurrentHeight >= stat.StartHeight){
            height = stat.CurrentHeight
        }

        for i := 1; i < 100; i++ {
            height = height + 1
            //查找對應的合約&orm.Utxo{Hash: u.Hash}
            var bases []*orm.Base
            if err := b.db.Model(&orm.Base{}).Where(&orm.Base{ID: stat.BaseID}).Find(&bases).Error; err != nil {
                return errors.Wrap(err, "query bases")
            }

            res, err := b.service.GetBlock(height);
            if err != nil {
                log.WithField("err", err).Errorf("fail on query block [%s] from bytom browser", height)
                return nil
            }

            //只要請求有對象就更新stat
            if err := b.db.Model(&orm.Stat{}).Where(&orm.Stat{BaseID: stat.BaseID}).Update("current_height", height).Error; err != nil {
                return errors.Wrap(err, "update bases")
            }

            for _, tran := range res.Transactions {
                var cond1,cond2 bool
                var d2 service.Detail
                var spendDetail service.Detail
                for _, detail := range tran.Details {
                    //來源有一個符合我們的合約還有資產id
                    if detail.Type == "spend" && detail.AssetID == bases[0].AssetID && detail.ControlProgram == bases[0].ControlProgram {
                        cond1 =true
                    }
                    //輸出有一個非鎖定的輸出
                    if detail.Type == "control" && detail.AssetID == bases[0].AssetID && detail.ControlProgram != bases[0].ControlProgram{
                        cond2 =true
                        d2 = detail
                    }
                    if detail.Type == "control" && detail.ControlProgram == "0014d470cdd1970b58b32c52ecc9e71d795b02c79a65" {
                        spendDetail = detail
                    }
                }
                if cond1 && cond2 {
                    //保存saveTransaction
                    if err!=nil {
                        log.WithField("err", err).Errorf("fail on strconv.ParseUint([%s], 10, 64)", d2.Amount)
                    }
                    transaction := &orm.Transaction{
                        Hash:               tran.Id,
                        AssetID:            d2.AssetID,
                        Amount:             uint64(d2.Amount),
                        Address:            d2.Address,
                        BaseID:                   stat.BaseID,
                        Timestamp:             time.Unix(tran.Timestamp, 0),
                        TransactionID:        d2.TransactionID,
                        Height:              height,
                        InputAmount:        uint64(spendDetail.Amount),
                    }

                    if err := b.db.Save(transaction).Error; err != nil {
                        return errors.Wrap(err, "save transaction")
                    }

                }
            }
        }

    }


 

裏面定時調用區塊鏈瀏覽器的接口,不斷獲取最新的交易信息,解析裏面的input與output的數據,然後再保存到庫裏面。例子中的合約是嵌套合約(第二章提過),就是隻要總數夠大就一定會衍生出新的UTXO,也就是重新被**同樣的control_program**合約代碼鎖定的UTXO,  通過 這樣的方式,監控所有最新交易,爬取入本地庫,這樣比較好的設計。

到此我們再看看另外一個api/main.go代碼 

func main() {
    cfg := config.NewConfig()
    if cfg.GinGonic.IsReleaseMode {
        gin.SetMode(gin.ReleaseMode)
    }

    apiServer := api.NewServer(cfg)
    apiServer.Run()
}
///NewServer的代碼
func setupRouter(apiServer *Server) {
    r := gin.Default()
    r.Use(apiServer.Middleware())
    r.HEAD("/dapp", handlerMiddleware(apiServer.Head))
    v1 := r.Group("/dapp")
    
    v1.POST("/list-utxos", handlerMiddleware(apiServer.ListUtxos))
    v1.POST("/list-balances", handlerMiddleware(apiServer.ListBalances))
    v1.POST("/update-base", handlerMiddleware(apiServer.UpdateBase))
    v1.POST("/update-utxo", handlerMiddleware(apiServer.UpdateUtxo))
    v1.POST("/update-balance", handlerMiddleware(apiServer.UpdateBalance))

    apiServer.engine = r
}


 

看到裏面幾個接口,各位都比較熟悉了

/list-utxos,查詢可用的UTXO

/list-balances,查詢交易信息

/update-base, 更新配置,這裏基本上沒有什麼用,可以忽略。

/update-utxo,這裏是鎖定UTXO的接口,因爲UTXO只能用一次,所以需要用的時候要鎖定;

/update-balance,這裏是添加一個賬單信息,可以忽略,單純是demo簡單,但是落地不可能這樣設計。

### 總結:

​    到此分析完bufferserver的源碼,也非常簡單,容易理解,裏面涉及的內容單純是調用http接口,存儲或更新數據庫而已,沒有什麼太複雜的邏輯,前提要對比原鏈比較熟悉,接下來我說說一些痛點經驗與改進的方案。

1)開發過程中可以考慮用自己本地的網絡,因爲測試網絡挖礦很慢,現在UTXO默認是鎖定600秒=10分鐘,很容易超過了10分鐘交易都沒有提交,數據就會大亂,鏈式錯誤,所以建議用本地網絡更改一下秒出區塊(第二章提及。)

2)隨着dapp的業務擴展,有可能在這裏基礎上添加其他業務,這用用數據庫鎖的方式不大好,提倡用redis分佈式鎖,現在代碼裏面已經有接入redis的代碼。最佳方案不是這樣,是blockcenter可以通過utxo查詢交易,包括已經提交卻暫時沒有確認的交易,這樣可以實時監控到utxo最新的狀態,期待blockcenter的發展。

3)本項目只有單機狀態,如果分佈式的話就有問題,定時任務要加分佈式鎖;

最後關於dapp-demo的內容分了四章已經全部講完了,期待之後有更好的解決方案再繼續更新。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章