##簡介
本章內容主要直接分析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的內容分了四章已經全部講完了,期待之後有更好的解決方案再繼續更新。