##简介

​    本章内容主要直接分析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 goroutinesvar wg sync.WaitGroupwg.Add(1)wg.Wait()
}1)启动的时候读取配置文件,config_local.json, 读取相关配置;2)定时任务,定时在blockcenter里面同步对应的utxo数据,以及UTXO的状态;3)定时任务,定时判断锁定的UTXO,超过时间恢复状态;先来看看NewBlockCenterKeeper,blockcenter.gofunc (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.Baseif 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.AssetIDfilter["script"] = base.ControlProgramfilter["unconfirmed"] = truereq := &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.Balanceif 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.Hourfor _, 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.Statif err := b.db.Find(&stats).Error; err != nil {return errors.Wrap(err, "query stats")}for _, stat := range stats {height := stat.StartHeightif(stat.CurrentHeight >= stat.StartHeight){height = stat.CurrentHeight}for i := 1; i < 100; i++ {height = height + 1//查找对应的合约&orm.Utxo{Hash: u.Hash}var bases []*orm.Baseif 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}//只要请求有对象就更新statif 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 boolvar d2 service.Detailvar spendDetail service.Detailfor _, detail := range tran.Details {//来源有一个符合我们的合约还有资产idif 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 =trued2 = detail}if detail.Type == "control" && detail.ControlProgram == "0014d470cdd1970b58b32c52ecc9e71d795b02c79a65" {spendDetail = detail}}if cond1 && cond2 {//保存saveTransactionif 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的内容分了四章已经全部讲完了,期待之后有更好的解决方案再继续更新。

基于比原链开发Dapp(四)-bufferserver源码分析相关推荐

  1. 基于比原链开发Dapp(一)-架构设计

    ## 简介 ​    研究比原链已经一年了,用比原链做了几个dapp,而且最近还做了一个基于他们插件钱包的dapp,总结了一些遇到的坑,还有一些技术细节,接下来我会分成三章,从dapp设计架构上,到深 ...

  2. 基于比原链开发Dapp(三)-Dapp-demo前端源码分析

    # 简介 ​    本章内容会针对比原官方提供的dapp-demo,分析里面的前端源码,分析清楚整个demo的流程,然后针对里面开发过程遇到的坑,添加一下个人的见解还有解决的方案. ### 储蓄分红合 ...

  3. android agps,Android应用开发Android GPS ——AGPS源码分析及配置

    本文将带你了解Android应用开发Android GPS --AGPS源码分析及配置,希望本文对大家学Android有所帮助. " Android Framework GPS --AGPS ...

  4. 海豚php源码,基于 ThinkPHP5.1 实现的海豚后台登录源码分析

    基于 thinkphp5.1 实现的海豚后台登录源码分析 一. 首先来到登录代码处,部分代码截图,大家有兴趣可以自己去看源码 登录处开始 public function signin() { if ( ...

  5. Bytom Dapp 开发笔记(三):Dapp Demo前端源码分析

    本章内容会针对比原官方提供的dapp-demo,分析里面的前端源码,分析清楚整个demo的流程,然后针对里面开发过程遇到的坑,添加一下个人的见解还有解决的方案. 储蓄分红合约简述 为了方便理解,这里简 ...

  6. Django框架深入了解_01(Django请求生命周期、开发模式、cbv源码分析、restful规范、跨域、drf的安装及源码初识)

    阅读目录 一.Django请求生命周期: 二.WEB开发模式: 三.cbv源码分析: 四.认识RESTful 补充知识:跨域 五.基于原生django开发restful的接口 六.drf安装.使用.A ...

  7. SNMP功能开发简介 二 net-snmp源码分析报文处理流程图

    最近在开发snmp功能,核心实现是基于net-snmp,将net-snmp的代理基本功能移植到自己的程序中去,因为需要修改一些定制化的内容,所以需要对net-snmp的流程有所了解,网上这方面的资料比 ...

  8. 第6季2:基于RTSP协议的实时视频流传输的源码分析

    以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除. 前言 博文第一季2:HI3518EV200的初体验中,所提供的测试文件sample_venc实现了基于RTSP协议的实时视频流传输功能. ...

  9. serverless搭建html,基于ServerLess的极简网页计数器:源码分析与实践

    这几天基于支持HTML5无感认证的ServerLess平台开发了一款博客.门户网站等web平台常用的PV统计工具:page-counter .主要用到的技术是js+webpack. 回首看来,解决了以 ...

最新文章

  1. 网络工程师60道典型选择题
  2. 怎么安装python3-python3怎么安装
  3. Docker Swarm集群搭建
  4. python随机数比大小_1到范围内的随机数系统最大大小总是1模2^10
  5. 数据科学和人工智能技术笔记 七、特征工程
  6. RHEL6.3配置FTP服务器(2) 本地用户下载和上传
  7. Spring框架学习之SpringAOP(二)
  8. linux内核升级到3.4
  9. 手机怎么把图片转成PDF格式?这个方法很好用
  10. 样本量太小怎么做结构方程模型?
  11. SAP BASIS ADM100 中文版 Unit 9(5)
  12. CentOS 基础命令 III
  13. 杭电操作系统实验三--- 实现模拟shell(arm架构华为云)
  14. 5分钟就能做一个Excel动态图表,你确定不学学?
  15. Java 调用Google Map Api解析地址,解析经纬度实例
  16. 微信扫描二维码无法下载文件的解决办法
  17. 十大高人气“断货王”蓝牙耳机盘点,双11哪款蓝牙耳机值得入手?
  18. MySQL表查询关键字
  19. Python笔记:利用pygame模块实现三原色颜色滚动条效果
  20. 图卷积网络原理(二)【图信号与图的拉普拉斯矩阵】

热门文章

  1. 读书笔记:《代码大全第2版》 03.创建高质量的代码之创建高质量的类
  2. GEE Expected a homogeneous image collection, but an image with an incompatible band was encountere
  3. 「题解」CF1468M Similar Sets
  4. 12月2日——培训第11天
  5. FSM有限状态机学习及Unity3D案例讲解
  6. Spark源码解读之Shuffle计算引擎剖析
  7. IDEA无法切换中英文的问题
  8. 物流企业的类型有哪些?物流企业分类
  9. termux安装ubuntu18_TERMUX安装ubuntu并图形化
  10. 用chrome dev tools 强制js注入