
比原链(Bytom)是一种多样性比特资产的区块链交互协议。官方QQ群:144860289
去中心化交换协议的发展 从Bitshare,Stellar到以太坊上的Etherdelta,Bancor,0x协议,去中心化交换协议也经过了好几代发展和很多模式的探索,每一代都通过前面的协议的痛点来进行改进和深化, 主要分为: 链上orderbook,链上结算; 链下orderbook,链上结算; 基于智能合约管理的资金池; 链上orderbook,链上结算 最早的 基于以太坊的去中心化交换协议的成功探索非Etherdelta莫属,曾一度占据去中心化交换市场的半壁江山。Etherdelta是较为完全的去中心化模式,用户充值、挂单、吃单、结算及提现全部在链上完成。 具体运行机制如下: Etherdelta的整个运行都在链上完成,用户保管自己的私钥,平台不会触碰用户资产,保证了资产和交换的安全、透明。但其弊病也较为明显: 1) 由于所有的交换环节都在链上完成,且每一个挂单、撤单、吃单等操作都会消耗GAS费用,导致延时高、成本效益低下。 2) 存在矿工非法预先交换的可能。 链下orderbook,链上结算 为了解决纯链上效率低下,且手续费低廉的问题,0x协议引入了relayer(中继)的概念,所有订单都发给relayer,无需上链,只有成交才会上链。 0x的“链下订单中继、链上最终结算”运作模式如下: 0x协议的主要问题在于,如果需要共享订单,运用0x协议的交易所每一笔成交都需要广播出来,以便其他交易所得知和确认,因此单纯地使用0x协议无法实现瞬时成交;另外,由于需要将ETH转化为WETH,也增加了交换成本。 基于智能合约管理的资金池; 最具典型的资金池模式有Bancor和Kyber。所谓资金池,可以理解为平台利用智能合约建立了一个用于储备各类资产的池子,资金池中资产的提供方可以是普通用户或者做市商。 MOV去中心化交换协议介绍 我们在考察各个交换协议时,其实纯链上交换协议才是最发挥区块链价值的方案,但是因为以太坊等公链性能问题,导致像Etherdelta这样的纯链上方案受挫,才有0x这样的链下orderbook出现,来改善性能的问题。究其根本,是本身的基础设施不完善,导致的迫不得已的改变。所以比原链MOV从一开始就先着手解决区块链性能问题。 高速侧链是保障 MOV采用了高速侧链Vapor pro作为底层的基础设施,Vapor每0.5s出块,每个区块可以容纳8000笔交易,即每秒16000 tps,在增大区块,提升节点服务器的情况下,仍然有进一步提升空间。这个性能能够满足当前非高峰时段的用户需求,可以比肩部分的中心化的方案。 同时MOV采用了DPoS作为共识机制,虽然丧失了一定的去中心化,但是增加了可以进行链上撮合的门槛,提高准入门槛可以更好的防止部分有不良企图的“矿工”进行预先交易,同时因为链上撮合本身具有一定的撮合收入,所以通过这个经济激励可以防止DPoS的出块节点作恶来破坏系统(其作恶的成本高于其不作恶的正常收益)。 订单共享 0x协议为了解决性能问题,所以采用链下orderbook,但带来的问题就是订单的割裂,采用0x协议的各个不同参与方,为了自己的利益,必然不会将自己用户的订单共享出来,从而影响整体的交易深度,而mov采用链上orderbook,所有的用户订单都在链上,公开透明,所有参与撮合的共识节点都可以共享这个深度,从而增强mov上资产的流动性。 磁力合约的优势 因为比原链是基于比特币的UTXO模型,所以在UTXO模型上的磁力合约来做交换协议具有更大的优势,因为UTXO模型本身以资产为基本单位,和账户模型相比,对于资产的操作更加简易和方便,我们来对比两者的流程。 以0x为例,0x整个交互流程: Maker授权DEX合约访问自己的Token A余额 Maker创建订单(订单有固定的格式)并用私钥签名 Maker使用任意通信手段广播订单 Taker接收订单并愿意执行 Taker授权DEX合约访问自己的Token B余额 Taker提交订单给DEX DEX验证订单的合法性,在两个账户之间按照订单上的汇率进行转账 那么磁力合约的整个流程就简易很多: Maker创建一个磁力合约(在磁力合约中放入自己的资产,并指定要兑换的资产和数量) Taker创建一个磁力合约(在磁力合约中放入自己的资产,并指定要兑换的资产和数量) 共识节点根据合约中的价格和数量,触发能够匹配的磁力合约,并交换两者的资产。 不仅在流程上简单,手续费因为流程的简化也会更低,我们只要在用户设置磁力合约的时候收取手续费即可,事实上,我们还可以尝试0手续费,因为使用DPoS的模式,关于手续费,各个节点之间的博弈也不会太复杂。 跨链资产的生态 我们观察现行以太坊上的去中心化交换协议,还停留在以太坊本身的生态上,虽然不能否认以太坊生态的强大,但其实外面才是更大的世界,当然跨链是后续的主旋律,包括Cosmos和Polkdot都是想做跨链的事情,所以MOV在一开始就考虑到了跨链的事情,通过OFMF将比原链之外的资产映射到比原链上来,然后形成囊括所有数字资产的大生态,用户在mov中体验的是跟中心化一样的体验,可以交易多种资产,这些资产也不单独是在某一个链的生态。 MOV磁力合约详解 这里再详细展开一下MOV磁力合约,看它到底是如何实现的。 MOV磁力合约本质上是一个挂单合约,不管是Taker还是Maker都需要生成这样的一个合约,本质上其实并不区分Maker和Taker,只是根据挂单的先后来区分Maker和Taker,两者在相反的交易对上提升了交易深度,实际上也可以认为都是Maker。 挂单交易合约是高级版的币币交易合约,合约的本质目的是锁定任意数量的资产A,愿意以某特定的汇率兑换资产B。合约的内部应该保存有四个常量(资产A的ID不需要存因为合约锁定的是资产A):期望兑换的资产B的ID, 期望兑换的汇率(使用分子分母方式解决浮点支持问题),和挂单用户的公钥,挂单用户接受资产B的地址。合约可以通过三种模式解锁: 全部解锁:所有合约中的资产A都被兑换成了资产B并转入挂单用户的地址中。 部分解决:部分合约中的资产A被兑换成了资产B并转入挂单用户的地址中,剩余的资产A通过递归合约的模式从新锁定回合约本身(新生成的UTXO)。 取消挂单:挂单用户通过私钥签名将合约中的资产A都转回自己的地址。 磁力合约Equity的代码如下: MagneticContract source code: contract MagneticContract(requestedAsset: Asset, ratioNumerator: Integer, ratioDenominator: Integer, sellerProgram: Program, standardProgram: Program, sellerKey: PublicKey) locks valueAmount of valueAsset { clause partialTrade(exchangeAmount: Amount) { define actualAmount: Integer = exchangeAmount * ratioDenominator / ratioNumerator verify actualAmount > 0 && actualAmount < valueAmount lock exchangeAmount of requestedAsset with sellerProgram lock valueAmount-actualAmount of valueAsset with standardProgram unlock actualAmount of valueAsset } clause fullTrade() { define requestedAmount: Integer = valueAmount * ratioNumerator / ratioDenominator verify requestedAmount > 0 lock requestedAmount of requestedAsset with sellerProgram unlock valueAmount of valueAsset } clause cancel(sellerSig: Signature) { verify checkTxSig(sellerKey, sellerSig) unlock valueAmount of valueAsset } } fullTrade()就是全部解锁方法;partialTrade()是部分解锁,当触发部分解锁时,会讲为解锁的资产放入一个新生成磁力合约中去,从而等待下一次匹配;cancel()方法将用户的资产转回自己的地址,取消这个合约。 我们在看看磁力合约的输入参数: type MagneticContractArgs struct { RequestedAsset bc.AssetID RatioMolecule int64 RatioDenominator int64 SellerProgram []byte SellerKey []byte } RequestedAsset是想要兑换的的资产,RatioMolecule,RatioDenominator是想要兑换资产的汇率(RatioMolecule/RatioDenominator 就是汇率),因为当前BVM不支持浮点型,所以额外采用这个参数作为比例,SellerProgram,SellerKey就是合约创建者自己的合约和地址,目标资产就要锁定到合约创建者自己的账户里面。细心的朋友可能发现,这里面和Equity合约少了一个参数,也就是standardProgram,那么这个参数不用用户自己输入,系统会默认补齐,standardProgram 其实就代表原合约,因为部分匹配会使一部分资产仍然没有使用,就仍然返回到合约中。 最后通过一张图来更直白的描述一下磁力合约: 总结 我们来对比一下当前的几种去中心化交换协议: 交换协议 模式 去中心化程度 成本效益 用户体验 Etherdelta 链上orderbook,链上结算 ★★★★★ ★ ★★ 0x 链下orderbook,链上结算 ★★★★ ★★★ ★★★★ Bancor 基于智能合约管理的资金池 ★★ ★★★ ★★★★ mov 链上orderbook,链上结算 ★★★ ★★★★ ★★★★★ 最早的完全去中心化交换协议Etherdelta对交换的干扰最少,但完全上链的机制使得成本消耗高且体验较差。之后的几类去中心化交换协议可谓都是在鱼和熊掌之间权衡取舍:Bancor和kyber为代表的储备池模式,管理员在整个过程中参与度较高,如果储备池合约中管理员权限较高比如之前Bancor可以提走用户资产,将会对用户的资金安全造成威胁;二者上链过程比较简单,成本控制表现不错,交易效率也比较高,只是功能性与有orderbook的交换协议相比略逊一筹。0x的中继模式,平台不触碰用户资产,相对而言去中心化程度是较高的,但这也导致成本效益相对较低;交体验整体不错,但如果需要共享订单,就无法实现瞬时成交。 mov在结合这几个前人的基础上,通过提升基础设施的性能,通过DPoS提升撮合准入门槛,并实现链上订单共享,还很好的提升了用户体验,除了通过DPoS牺牲了一定去中心化外,在其他方面都得到了一定提升,随着mov的进一步开发和完善,必将发挥该方案的优势,从而让区块链能在资产交换领域发挥巨大的价值,能够让去中心化的资产兑换落地。
11月24日,比原链CTO James参加了Go中国举办的Gopher Meetup杭州站活动,与来自阿里、网易的技术专家带来Kubernetes、区块链、日志采集、云原生等话题的分享。James向大家介绍了Go语言特性在区块链中的应用还分析了Go语言成为区块链主流开发语言的原因。 比原链的系统架构 在区块链系统中内核层是最核心的,他承接了区块验证、交易验证、节点维护、打包挖矿等重多职责。通信层掌管了区块链系统的网络服务,区块链的网络更像P2P的网络形式,他呈网状扩散,负责区块同步、交易同步、节点发现等重要的功能。钱包层是直接和用户发生交互的一层,他的职责是资产管理、私钥管理,并与内核层通信验证区块交易。 以Bytom为例,他的内核层分为了五个的模块。我们举例描述几个主要的模块。 孤儿块管理:孤儿块就是由矿工挖出但未成为主链区块的区块(在相同高度产生2个甚至更多的合法区块,一个区块成为主链,剩下的则称为孤儿块),孤儿块管理就是将未成为主链区块的孤儿块存储起来。举个例子,我现在挖到的区块是100,那么下一个区块理论上是101,但是在网络层发生延时的时候,可能会发生先出102再出101的情况,那么孤儿块管理会将这些父块未到的子块先缓存起来。 共识层:确认一个块是否合法。分为区块头验证和交易验证。区块头验证需要验证它的父块和时间戳,同是需要算力来保证记账权利。交易验证比原特别的设计了一层BC层,这层在交易验证时会获得更好的性能,交易验证还和智能合约相关,交易被验证时参数会参入虚拟机验证该交易是否合法。 区块树管理:又称为Block Index,作用是记录全网所有的块,保存了全网所有块的一张镜像图。因为有孤儿块,所有它并不是链式结构的,会有分叉的情况,所以称为区块树。举个例子,区块树管理好比有一个分布式系统,但无法保证每个节点的状态一致,可能会出现同一个高度产生同时参生区块的情况。区块树管理就是具备了在节点状态不一致的情况下让系统回滚到正确的区块这个功能。 数据存储:将区块数据做持久化存储。包含两种数据,第一种是区块数据,会在网络上进行广播的原生区块信息;第二种是UTXO数据,存储UTXO数据是为了更快的验证一笔UTXO是否可以花费,而不需要去遍历所有区块信息。 交易池:维护了所有全网发出的但是还未被确认的交易。跟它关联最大的是挖矿模块,挖矿模块每次要产生一个新区块的时候,它会从交易池拿一些交易打包成块,然后用Tensority共识算法进行工作量验算。举个例子,在节点打包交易之前,交易处在一个未确认的状态之下,交易池会将这些未确认的交易保存起来,分配给后面的矿工用于打包。 WHY GOLANG? 第一点,区块链是多模块异步协同工作的,分成了P2P层、钱包层、内核层。其中内核层、情报层里面都有不同的很多子模块,在负责不同的事情。Go语言做得很出色非常适合做这方面的开发。 第二点,区块链项目有核心团队但是主要还是依赖社区参与,Go语言编译上相对于其他语言稍微严格一点,在制定规范后可以使不同开发者代码风格相近,有利于协同工作的展开。 第三点,Go语言社区完善,有很多非常好的开源库支持,使用起来非常方便用户体验良好。 第四点,主流语言的头部效应,在区块链的项目里面超过85%的项目都是基于Go语言开发的,大部分区块链工程师都擅长Go语言,所以当新的项目开始的时候,会首选Go语言。 对区块链感兴趣的程序员可以投递简历至:james@bytom.io
本章内容会针对比原官方提供的dapp-demo,分析里面的前端源码,分析清楚整个demo的流程,然后针对里面开发过程遇到的坑,添加一下个人的见解还有解决的方案。 储蓄分红合约简述 为了方便理解,这里简单说说储蓄分红合约的内容,具体可以查看储蓄分红合约详细说明,储蓄分红,顾名思义就是储蓄之后,当达到一定的时间,按照比例返回本息这样的意思,所以demo中拆分成saving(储蓄)与profit(提现)两个页面,本章内容是针对合约交易的提交,所以只针对储蓄页面说明。 比原官方Dapp-demo使用说明 比原官方demo地址 1)访问的前提需要用chrome打开比原官方demo地址,同时安装bycoin插件,在应用商店搜索就行; 2)安装完bycoin,需要初始化用户信息,新建或者导入备份文件去恢复用户; 3)填写指定资产数量,点击确定; 4)弹出合约交易专用页面,填写密码,点击确认; 5)查看交易流水 前端源代码分析 源码 : 储蓄分红合约前端源代码 (本章内容讲解的是 2019年7月10号 最新版的代码) 前端代码是基于前端框架react去做的,很容易读懂,结构如上,我们来看看作为储蓄页面(saving)Bytom-Dapp-Demo1srccomponentslayoutsaveindex.jsx //提交后的方法 FixedLimitDeposit(amount, address) //####### 1. .then(()=> { //####### 2. this.refs.btn.removeAttribute("disabled"); this.setState({ error:'', msg:`Submit success!!! you spent ${amount} deposite asset,and gain ${amount} billasset.` }) }).catch(err => { //####### 3. this.refs.btn.removeAttribute("disabled"); this.setState({ error:err, msg: '' }) }) 1)接收了输入框的金额,还有当前用户的地址; 2)成功后提示内容; 3)失败后提示内容; 接下来到FixedLimitDeposit方法 export function FixedLimitDeposit(amount, address) { const object = { address: address, amount: amount, parameter: [amount, address] } return submitContract(listDepositUTXO, createContractTransaction, updateDatatbaseBalance, object) //####### 1. } 1) 传入三个方法体分别是 listDepositUTXO(查找当前合约所有的UTXO), createContractTransaction(创建提交前的合约参数),updateDatatbaseBalance(更新用户的提交列表) 进入Dapp-Demo1srccomponentslayoutsaveaction.js 的 submitContract方法 return new Promise((resolve, reject) => { //list available utxo return listDepositUTXO().then(resp => { //####### 1. //create the Contract Transaction return createContractTransaction(resp, amount, address).then(object =>{ //####### 2. const input = object.input const output = object.output const args = object.args const utxo = object.utxo //Lock UTXO return updateUtxo({"hash": utxo}) //####### 3. .then(()=>{ //Transactions return window.bytom.send_advanced_transaction({input, output, gas: GetContractArgs().gas*100000000, args}) //####### 4. .then((resp) => { //Update Balance return updateDatatbaseBalance(resp, ...updateParameters).then(()=>{//####### 5. resolve() }).catch(err => { throw err }) }) .catch(err => { throw err.message }) }) .catch(err => { throw err }) }).catch(err => { throw err }) }).catch(err => { reject(err) }) }) 1) 首先调用listDepositUTXO 拿到当前节约锁定的所有UTXO的信息,待会详细说明; 2)调用 createContractTransaction 方法,组装好合约的对应信息参数; 3)选取要使用的 UTXO后,调用updateUtxo 告诉bufferserver ,该UTXO已经被使用,更改状态,防止其他人调用了; 4)执行window.bytom.send_advanced_transaction方法,参考插件钱包API,是高级交易方法,这个是bycoin插件的原生方法,调起 提交交易的页面,让用户输入密码; 5)交易确认后,调用 updateDatatbaseBalance 提交数据到后端; 再来看看api.js的listDepositUTXO 方法,所有与bufferserver交互的接口全部写到这个文件里面: function listDepositUTXO() { return listDappUTXO({//****** 1. "program": GetContractArgs().depositProgram, "asset": GetContractArgs().assetBill, "sort": { "by":"amount", "order":"desc" } }) } //Api call from Buffer server export function listDappUTXO(params) { let url switch (window.bytom.net){ case "testnet": url = "/dapptestnet/list-utxos" break default: url = "/dapp/list-utxos" } return post(url, params).then(resp => resp.data) } 很明显最终是调用bufferserver的/list-utxos方法,非常简单,值得一提的是 1)里面的结构体根据program(合约代码)与asset(资产ID)去查找UTXO,这里其实底层是调用官方的blockcenter接口的,后面会细说; 继续看看Dapp-Demo1srccomponentslayoutsaveaction.js 的createContractTransaction方法 function createContractTransaction(resp, amount, address){ return new Promise((resolve, reject) => { //utxo pre calculation const limit = GetContractArgs().radio * 100000000 //****** 1. if (resp.length === 0) { reject( 'Empty UTXO info, it might be that the utxo is locked. Please retry after 60s.') } else if (amount < limit) { reject( `Please enter an amount bigger or equal than ${limit}.`) } const result = matchesUTXO(resp, amount) //****** 2. const billAmount = result.amount const billAsset = result.asset const utxo = result.hash //contract calculation if (amount > billAmount) { reject('input amount must be smaller or equal to ' + billAmount + '.') } else { const input = [] const output = [] const args = contractArguments(amount, address) //****** 3. input.push(spendUTXOAction(utxo)) //****** 4. input.push(spendWalletAction(amount, GetContractArgs().assetDeposited)) //****** 5. if (amount < billAmount) { //****** 6. output.push(controlProgramAction(amount, GetContractArgs().assetDeposited, GetContractArgs().profitProgram)) output.push(controlAddressAction(amount, billAsset, address)) output.push(controlProgramAction((BigNumber(billAmount).minus(BigNumber(amount))).toNumber(), billAsset, GetContractArgs().depositProgram)) } else { output.push(controlProgramAction(amount, GetContractArgs().assetDeposited, GetContractArgs().profitProgram)) output.push(controlAddressAction(billAmount, billAsset, address)) } resolve({ //****** 7 input, output, args, utxo }) } }) } 这个方法比较复杂,我们一步一步来 这里先看看 7),最终返回的内容是 input、 output、 args、 utxo, 跟发送交易页面里面的input、 output、 args对应起来,如 图K (图K) 上一章说过,所有比原链的交易都要存在质量守恒定理,input与output的总和要相对应,合约交易里面执行方法必须需要参数,这里的args就代表传入的参数,utxo是代表 utxo的id 1)做一下限制,设置最少值 2)matchesUTXO ,根据上面的内容,刚刚已经通过listDepositUTXO 拿到所有可用的UTXO列表,这个时候要根据用户输入的数额amount,选择一个起码 大于或等于的 amount 的UTXO出来; 3)contractArguments ,构建args,就是合约里面方法的参数; 4)通常合约交易会有自己资产的input,合约UTXO的input,这里是要解锁的utxo的input; 5)这个是钱包资产的input; 6) 上一章内容说过,解锁合约的交易,必须根据合约里面的逻辑,计算出对应的结果,所以这里的逻辑跟合约里面逻辑是一样的,储蓄分红合约详细说明 如图; 判断逻辑一样,这里插件钱包跟上一章说的pc钱包接口的结构有所不同,但是原理一样。 最后我们看看srccomponentslayoutsaveaction.js 的updateDatatbaseBalance 方法 function updateDatatbaseBalance(resp, amount, address){ return updateBalances({ "tx_id": resp.transaction_hash, address, "asset": GetContractArgs().assetDeposited, "amount": -amount }).then(()=>{ return updateBalances({ "tx_id": resp.transaction_hash, address, "asset": GetContractArgs().assetBill, "amount": amount }) }).catch(err => { throw err }) } export function updateBalances(params) { let url switch (window.bytom.net) { case "testnet": url = "/dapptestnet/update-balance" break default: url = "/dapp/update-balance" } return post(url, params) } 同样是调用bufferserver,这里是调用update-balance方法,把成功后的交易提交到后端。 小结 上面介绍了dapp-demo前端代码的内容,介绍了里面的方法,除了插件api的调用比较复杂外,其他都是普通的应用逻辑调用,主要理解了质量守恒定理,剩下的都是对数据审核数据的问题,非常简单。 遇到的坑 有应用开发的读者应该一下子就能理解到问题核心吧,我现在在说说里面的坑; 1) UTXO锁定接口容易被刷; 假如我一个开发人员知道这个接口,狂刷你这个接口狂锁应用的UTXO,这样应用长期都会瘫痪状态; 解决方案:这个应该从应用方面去考虑,譬如接口加一些一次性的验证码,加refer的监控,加授权之类的方式,后端加上交易监控,去维持着各种情况UTXO的状态,比较抽象,而且比较复杂,不过这是必须的; 2)并发问题;跟1)一样,就算我是一个正常用户,选择了一个UTXO解锁后,居然通过http接口告诉后端去锁定,调起 输入密码页面 (图K),这个时候如果用户不输入密码不提交,在比原链上该UTXO是没有被解锁,但是bufferserver会却锁住了。 解决方案: 后端源码是锁定一段时间,如果没有使用,则定期解锁出来,这种情况也是需要应用的监控判断,维护所有utxo的状态,个人建议在发合约的时候,发多笔UTXO锁定合约,可用的UTXO就会变多,这个时候有些同学问,TPS岂不是也一样不高,如果用过火币的同学就知道了,区块链交易本来就不太注重TPS,而且火币的交易必须要超过60-100个区块,才确定一笔交易,这个看应用开发者如何去判断,取舍。 3)用户交易信息接口容易被刷;跟1)一样,交易完成后,直接通过http接口去提交数据,我狂刷,岂不是亿万富翁....; 解决方案:想要用户的交易信息,生成交易账单,可以直接用插件的接口,不过要通过合约编码去筛选出来,笔者是通过监控区块链浏览器所有交易,进入数据库交易表的方式,这样可以时时刻刻监控所以交易。 4)容易产生链式错误; 这里dapp-demo发的是一个合约的UTXO,假如用户提交交易之后会产生新的UTXO,但是这个UTXO还没有确认的,bufferserver的list-utxo接口会把还没有确认的UTXO,从而解决并发问题,但是我一个开发人员,知道合约的编码,随便写个交易提交了,虽然肯定会失败,但是需要时间,这个时候bufferserver也把这个肯定失败的UTXO返回过来前端,一直链式产生一堆交易,很容易产生链式失败。 解决方案:1)这里我跟比原官方的老大深深讨论过,最优方案当然是合约本身设置一个密码,输入参数必须要根据这个密码去加incode密过,传入合约交易参数,合约本身在解释的时候,再decode解密验证,保证出入的参数是官方的,这样就不会有人攻击.....不过结论是,暂时比原链的合约引擎不支持。 2)一定要隐藏好合约逻辑,其他人就没办法去恶意调用恶意占用,例如前端代码混淆,或者args参数是后端生成,另外建议比原的blockcenter的build-transaction接口参数可以加密这样,去掩盖合约逻辑。 PS:这是笔者对于以上问题的思考,有更好的解决方案,欢迎一起讨论。 总结 这种内容主要说了前端代码的源码分析,还有设计上的逻辑坑,具体的解决方案应该跟官方的开发人员沟通还有讨论,区块链的交易本来不追求大并发,但是也需要一定的并发性,笔者在第四章才根据bufferserver内容,在针对上面的问题,做出一些个人见解还有建议。
北京时间8月25日,2019比原链全球开发者大会在美国旧金山Fort Mason Cowell Theater成功举办。会议吸引了来自美国、俄罗斯、印度、比利时、加拿大、巴西、赞比亚、委内瑞拉等多个国家的技术达人,参赛项目涉及稳定币支付系统、供应链金融、合约开发工具、教育科技等多个领域。最终,基于比原链开发的支付平台Paypaw(贝爪)获得2019比原链全球开发者大会一等奖,斩获3万美元等值的BTM奖励。 本次大会是比原链第二次举办区块链全球开发者大会,大会还邀请了Matrixport合伙人Daniel、ETC Labs总监Darin、MATPool CTO金磊、Celer联合创始人董沫、Multiplied创始人Nancy、Algorand工程研究负责人Naveed、Harmony联合创始人Nick、盈信泰资本投资总监Will等多位业内重量级嘉宾。 4个最终入围项目现场路演,6位重量级嘉宾精彩演讲,3场圆桌论坛金句频出,现场掌声不断,20度的旧金山似乎也有着34度杭州的火热。2019比原链全球开发者大会的成功举办,不仅显示比原链生态的日益壮大,区块链应用落地的不断推进,也让我们看到了全球区块链开发者的热情。纵观整场活动,开源精神依然是比原链的坚持,应用落地成为行业参与者聚焦的核心。 区块链与开源精神终将改变世界 比原链创始人& CEO段新星在开幕致辞中首先谈到了创建比原链的初心。他认为数字资产是一个巨大的未来趋势,非常希望建立一个数据资产的互联网,以支持多资产和可编程经济。自2018年4月主网上线以来,为了解决扩容问题,今年比原链将区块链拓展成“一主链多侧链”的架构,主链使用PoW来保证资产安全,在侧链引入DPoS,将TPS从主链的不到100,增加到侧链的约8000,以适应实际应用。目前比原链的Vapor侧链已经拥有大约42个节点,同时团队也在为企业应用创建更多定制化的工具。 “当前,我们正面临一个去全球化的阶段,但比原链依然拥抱开源,依然坚信区块链以及开源运动的力量。这股力量能让我们跨越国界,为用户带来更加实用的服务,创造更多的工作机会,并最终改变世界。” 图为比原链创始人& CEO 段新星 一主多侧架构,比原链加速商业应用落地 区块链技术应用落地是本次全球开发者大会的主基调。比原链技术VP James在演讲中介绍称,Bystack是基于比原链的BaaS服务,主链用于实现资产安全和去中心化,侧链用于实现可扩展性并运行应用。而根据比原链Vapor侧链BBFT共识算法发明者王炜的数据,在BBFT算法下,侧链每秒验证的交易数可达到2万笔,交易确认时间仅仅0.6秒,分叉的可能性仅为0.27%。将区块链“不可能三角”变成可能,Bystack全速助力区块链商业应用。 据段新星介绍,比原链在商业应用方面已经取得巨大进展。在社区成员的支持下,基于比原链的应用和软件已经在中国、欧洲和美国建立起跨链交易、物流、支付等多个领域的应用。举个例子,已经有海运物流公司使用比原链作为跨境结算系统。此次大赛中也有基于侧链的高速支付系统。去中心化交易所、DeFi、硬件钱包应用同样在不断涌现。 会上,James公布了比原链未来一年的路线图。Bystack 将开放BaaS平台,推出开发者中心以及Bystack去中心化身份(DID)。同时,Bystack将持续改善核心数据层和网络协议,推出BBFT共识2.0和迭代和Bytom 1.2.0的升级。基于一主多侧架构的OFMF(开放联邦管理框架)、DApp开放框架、去中心化个人信用(DPC)解决方案也会陆续推出。 图为比原链技术VP James 比原链创始人长铗表示: “区块链技术正在以一种前所未见的速度快速发展,正在对人类社会产生巨大的影响。我衷心希望包括参会者在内的所有区块链行业爱好者可以携手并进,一起构建更安全、更高效的区块链,建设更加美好的未来世界。” 此外,在区块链的应用大潮中,xx即服务也逐渐从区块链本身拓展到挖矿业务中。MATPool CTO金磊在演讲中表示,作为比原链的官方矿池,MATPool正在研究推出MaaS(挖矿即服务),为算力提供方用户提供矿机托管服务,设置简单,接入方便,自动为用户寻找收益率最高的币种;算力需求方用户也可以随时找到所需的算力,统一费率,按分钟计费。MaaS将打破GPU算力供需双方孤立、无法连接的状态,将算力和计算有机地连接起来。 图为MATPool CTO金磊 值得一提的是,应用落地不只是比原链,也是各大公链的发力点。Celer Network联合创始人董沫在演讲中介绍称,Celer Network已经在游戏领域发力,推出C端手机应用CelerX来加速推动区块链游戏的落地。开发者可以利用CelerX Gaming SDK快速简单地创建HTML5游戏,无需任何区块链开发经验。 入围队伍现场路演,Paypaw斩获一等奖 在大会参赛项目的路演环节,2018比原链开发者大会的一等奖项目Bytomswap也来到了现场,率先登场为大家介绍了项目的最新进展。项目代表Anil Kumar介绍称,作为一个基于比原链的去中心化数字资产交易平台,Bytomswap目前已经可以支持跨链资产交换,后续将会推出一个面向比原链资产的标准通证命名注册系统,并在比原链钱包中加入一个用于去中心化交易的钱包插件。从较为基础的Demo到不断完善的产品,Bytomswap也同样在一步步走向落地。 图为Bytomswap项目代表Anil Kumar 在本届大会的最终入围项目中,有基于比原链的人工智能版权注册和保护平台ProofChain(速易证)、基于比原链的支付平台Paypaw(贝爪)、分布式拍卖系统ODIN Swap以及基于比原链的支付通道网络BytomLit。最终,Paypaw(贝爪)获得2019比原链全球开发者大会一等奖,斩获3万美元等值BTM。项目代表Dalin在谈到之所以选择在比原链上开发的原因时表示,比原链的DPoS侧链提供了近乎即时的交易确认,TPS高,安全性好,故而选择基于比原链开发这样一个应用。 图左为比原链创始人& CEO 段新星,图右为Paypaw(贝爪)项目代表Dalin 此外,ProofChain(速易证)获得二等奖,BytomLit、GoAskme、ODIN Swap Tools以及BTMHDW获得优秀奖。比原链团将与获奖团队持续保持沟通,并进行项目交流和检查,帮助项目更好地成长、落地。 思维碰撞,圆桌讨论精彩不断 本次大会一共有3场圆桌论坛。在主题为“公链挑战与趋势”的圆桌上,ETC Labs主管 Darin Kotalik提出,科普教育是未来几年公链面临的巨大挑战,人们必须要对区块链有基本的认识,分清楚公链和私链的区别。Harmony 联合创始人Nick White则表示,技术、监管以及应用是三个最重要的挑战。 从左到右依次为Multiplied CEO Nancy Li、ETC Labs主管 Darin Kotalik、Algorand 工程研究负责人Naveed Ihsanullah、Harmony 联合创始人Nick White以及比原链工程VP James 在PoW和PoS孰优孰劣的问题上,ETC Labs主管 Darin Kotalik认为,不管PoW还是PoS,不同的共识没有高低之分,重点在于你想要解决什么样的问题。比原链工程VP James则认为,对于公链,PoW是最好的共识算法,毕竟它已经在比特币上运行了10年,得到了验证。DPoS中心化程度比较高,可能相对来说危险一些。 面对行业目前非常关注的应用落地问题,Algorand 工程研究负责人Naveed Ihsanullah认为,要实现大规模应用,需要大量、甚至全球的开发者都参与开发,并针对用户的需求和实际用例开发应用。区块链大规模应用发生时就是用户在使用却没有感知的时候,让用户不需要理解应用背后的技术也能非常便利地使用。Harmony 联合创始人Nick White表示,DeFi是区块链的一个很好的用例,但是它在成熟度、安全性以及监管方面还存在问题,目前来看游戏是最佳突破口。游戏可以帮助更多用户了解区块链,在监管方面也没有金融应用方面的问题。 在主题为“数字资产行业的价值创造”的圆桌上,AnChain市场经理Steven Yang发言称,加密货币的早期参与者中,很多都是反对监管的;事实上,为了满足监管要求(KYC),项目却不得不向中心化靠拢。在实际应用方面,用户往往也对中心化的服务更加适应。Infstone商业负责人Sili Zhao则表示,目前很多人对区块链的认识仅限于银行业务和加密货币,从业者应该更多探索区块链在其他领域的应用落地。 从左到右依次为Vera Network & IoTBlock创始人 Denis Lam、Infstone商业负责人Sili Zhao、Anchain市场经理Steven Yang以及Reserve 工程VP Zera 在最后一场主题为“投资未来:如何寻找投资机会”的圆桌上,Matrixport 创始合伙人 Daniel Yan认为香港和新加坡是亚洲开展加密业务的最佳地点。而火币顾问、Ausvic Capital投资主管 Will Wang则从战略角度看待地点的选择,他认为地点的选择还是要看技术力量和人才的聚集。他还指出,一般来说,公司应该秉承“用户在先,技术在后”的理念,很多初创公司往往陷入误区,沉溺技术,反而忽略了用户的需求和体验。 OKcoin 投资/合作关系主管Cecilia Li在发言中表示,任何东西都可以通证化,关键还是资产本身是否有需求和价值,自身没有价值的东西任何手段都行不通。Taxa. Amino capital EIR 创始人TF Guo则为二级市场的投资者提供了一点个人的建议,投资加密资产可以,但是最好不要去交易,“Just build and hold”。 从左到右依次为Monday Capital创始合伙人Yiannis Varelas、OKcoin 投资/合作关系主管Cecilia Li、火币顾问、Ausvic Capital投资主管 Will Wang、Taxa. Amino capital EIR 创始人TF Guo以及Matrixport 创始合伙人 Daniel Yan
简介 这章的内容详细分析一下涉及智能合约Dapp的整个开发流程,注意是涉及只能合约,如果你只要一些基本转BTM功能没有太大意义,本内容补充一下官方提供的 比原链DAPP开发流程,详细实践过好踩到的一些坑,还有一些真正具体的技巧还有经验,个人认为非常有用,起码让开发者可以更快速地去操作。 资料说的储蓄分红合约太复杂了,简单说说逻辑,银行发了一笔股份资产,用合约锁定,用户去触发这个合约的方法,付出了钱兑换了对应份额的股份资产,当达到一定的高度,就可以通过用股份资产兑换回本金与分红(钱+利息)。 里面包含了两个合约~~ 整体流程 开发流程分为,1)编写智能合约;2)发合约交易;3)测试解锁合约方法;4)基于插件钱包开发Dapp前端;5)开发后端; 流程貌似非常简单,本人在1,2,3 步浪费了很多时间。其中有些坑踩过接下来介绍一下; 1)编写智能合约,上面提供的 比原链DAPP开发流程,写得很清楚,使用的是equity非常简单,直接下载最新版 用命令 【./equity TradeOffer --instance 】 就能得到一串编译后的合约程序代码,简称智能合约程序。 E:\GoWorks\src\github.com\equity\equity>equity.exe jiedai_6.txt --instance ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 260374 260474 260574 260674 260774 260874 260874 00141ccef16d2ac1ab22baa8acfa1633fdc32df d55aa b1f38553d95177c53755996baf523da006da977008f069792bb6a2c3b6a253fb ======= PartLoanCollateral ======= Instantiated program: 20b1f38553d95177c53755996baf523da006da977008f069792bb6a2c3b6a253fb1600141ccef16d2ac1ab22baa8acfa1633fdc32dfd55aa030afb03030afb0303a6fa030342fa0303def903037af9030316f90320ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff ffffff4d2b015b7a76529c641d010000640c0100005c7900a0695c790500f2052a01947600a0695379cd9f5579cda09a916164380000005a95639a00000054798ccd9f5679cda09a916164500000005895639a00000055798ccd9f5779cda09a916164680000005695639a00000056798ccd 9f5879cda09a916164800000005495639a00000057798ccd9f5979cda09a916164980000005295639a0000005195c3787ca169c3787c9f916164f5000000005e795479515e79c1695178c2516079c16952c3527994c251006079895f79895e79895d79895c79895b79895a79895979895879 895779895679890274787e008901c07ec1696307010000005e795479515e79c16951c3c2516079c169632b010000587acd9f6900c3c2515c7ac1632b010000755b7aaa5b7a8800c3c2515d7ac1747800c0 2)发合约交易, 先解释一下合约的逻辑,储蓄分红合约太复杂,所以我们用币币交易合约去举例子, contract TradeOffer(assetRequested: Asset, amountRequested: Amount, seller: Program, cancelKey: PublicKey) locks valueAmount of valueAsset { clause trade() { lock amountRequested of assetRequested with seller unlock valueAmount of valueAsset } clause cancel(sellerSig: Signature) { verify checkTxSig(cancelKey, sellerSig) unlock valueAmount of valueAsset } } 看看智能合约的交易图,方便小白理解: 所以储蓄分红合约一开始肯定要锁定一部分资产,所以必须部署合约交易。那么如何触发呢? 本人通过PC钱包的接口方式去部署合约,具体很多例子可以在智能合约学习文档看到。 PC钱包方式,所有交易都必须三部,build-transaction,sign-transaction,submit-transaction,三个接口。 踩过的坑: 调试智能合约很慢,要等到交易确认才能知道是否成功,而且报错不明显,不知道哪里出问题; 解决方案: 本地PC钱包solonet模式调试,更改源码,快速出块 difficulty/difficulty.go func CheckProofOfWork(hash, seed *bc.Hash, bits uint64) bool { compareHash := tensority.AIHash.Hash(hash, seed) return HashToBig(compareHash).Cmp(CompactToBig(bits)) <= 0 } 里面那句添加 ||true 如下 return HashToBig(compareHash).Cmp(CompactToBig(bits)) <= 0 || true 一开始没想到这样做,以为很快调试好,搞了三天晚上10点才调试完。 2.智能合约对于除法的支持很不友好,尽量不要用除法,一开始写了一个很复杂的合约,不知道错误,智能逐步改代码快速调试去定位,最后发现 A/B,如果A=B没问题,否则就直接报错,问过官方没有得到合适的回答,我尝试过是存证这种问题,非常坑。 3.程序必须计算好对应结果utxo 流转action的 input、ouput ;如下 { "base_transaction": null, "actions": [ { "output_id": "13fbd1e5df196a1488e85e3b5983e51444c49ef3695df789c9473abb636e0f5c", "arguments": [ { "type": "integer", "raw_data": { "value": 5500000000 } }, { "type": "data", "raw_data": { "value": "00141ccef16d2ac1ab22baa8acfa1633fdc32dfd55aa" } }, { "type": "integer", "raw_data": { "value": 0 } } ], "type": "spend_account_unspent_output" }, { "amount": 5500000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "control_program": "0014d470cdd1970b58b32c52ecc9e71d795b02c79a65", "type": "control_program" }, { "amount": 5000000000, "asset_id": "80013f81a66cb99977879e31639bb4fe4b12b4c7050fe518585d3f7f159d26a9", "control_program": "00141ccef16d2ac1ab22baa8acfa1633fdc32dfd55aa", "type": "control_program" }, { "amount": 9999995000000000, "asset_id": "80013f81a66cb99977879e31639bb4fe4b12b4c7050fe518585d3f7f159d26a9", "control_program": "20b1f38553d95177c53755996baf523da006da977008f069792bb6a2c3b6a253fb160014d470cdd1970b58b32c52ecc9e71d795b02c79a6503e1830403e1830403e256040322350403a21e0403e20e040307fb0320ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff4d2b015b7a76529c641d010000640c0100005c7900a0695c790500f2052a01947600a0695379cd9f5579cda09a916164380000005a95639a00000054798ccd9f5679cda09a916164500000005895639a00000055798ccd9f5779cda09a916164680000005695639a00000056798ccd9f5879cda09a916164800000005495639a00000057798ccd9f5979cda09a916164980000005295639a0000005195c3787ca169c3787c9f916164f5000000005e795479515e79c1695178c2516079c16952c3527994c251006079895f79895e79895d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696307010000005e795479515e79c16951c3c2516079c169632b010000587acd9f6900c3c2515c7ac1632b010000755b7aaa5b7a8800c3c2515d7ac1747800c0", "type": "control_program" }, { "account_id": "0U374V0300A02", "amount": 5500000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "type": "spend_account" }, { "account_id": "0U374V0300A02", "amount": 20000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "type": "spend_account" } ], "ttl": 10000 } 一个解锁合约交易要包含action类型有, spend_account_unspent_output (合约的参数), spend_account (输入的资产描述), control_program或者control_address (接收者资产描述), 可以理解成质量守恒。 如上面例子 spend_account_unspent_output 的action里面有个output_id =13fbd1e5df196a1488e85e3b5983e51444c49ef3695df789c9473abb636e0f5c,这个资产的小数位为8(这里没有体现),代表我要解锁这个utxo,他的值为 100000000.00000000 就是1亿。 拆分成两个action,一个 50.00000000,一个 99999950.00000000 只有btm = ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 需要用来等手续费,所以允许不守恒,最后由旷工挖矿拿到手续费。 总结:那么程序相当于要把合约里面的逻辑整合进去,才能计算好真正的input、output~~我理解是交易确认的时候,解锁合约的程序验证现在的input、ouput是否跟合约一样。 3)测试解锁合约方法,2)里面采坑已经说清楚这个问题了,补充一下就是最好一下子不要写太复杂的合约,从简单来开发调试。一定要注意质量守恒定律,只要懂了这个原理其实非常简单。 4)基于插件钱包开发Dapp前端, 这块具体可以看插件钱包API,储蓄分红合约前端源代码,里面说的非常清楚, 涉及到的接口,暂时他们API文档还没有整理出来,来自上一章说的blockcenter的接口 url地址 :testnet: 'http://app.bycoin.io:3020/', mainnet: 'https://api.bycoin.im:8000/' 核心用到的接口有: 根据合约与资产ID查询UTXO接口 /api/v1/btm/q/list-utxos 参数: { "filter": { "script":"20b1f38553d95177c53755996baf523da006da977008f069792bb6a2c3b6a253fb160014d470cdd1970b58b32c52ecc9e71d795b02c79a6503e1830403e1830403e256040322350403a21e0403e20e040307fb0320ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff4d2b015b7a76529c641d010000640c0100005c7900a0695c790500f2052a01947600a0695379cd9f5579cda09a916164380000005a95639a00000054798ccd9f5679cda09a916164500000005895639a0000798ccd9f5779cda09a916164680000005695639a00000056798ccd9f5879cda09a916164800000005495639a00000057798ccd9f5979cda09a916164980000005295639a0000005195c3787ca169c3787c9f916164f5000000005e795479515e79c1695178c2516079c16952c3527994c251006079895f79895e79895d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696307010000005e795479515e79c16951c3c2516079c169632b010000587acd9f6900c3c2515c7ac1632b010000755b7aaa5b7a8800c3c2515d7ac1747800c0", "asset":"80013f81a66cb99977879e31639bb4fe4b12b4c7058585d3f7f159d26a9" , "unconfirmed":false }, "sort": { "by":"amount", "order":"desc" } } unconfirmed ,代表是否确认的,这个对后期的并发问题非常有用,第三章我会详细说明。 结果 { "code": 200, "msg": "", "result": { "_links": {}, "data": [ { "hash": "16749b694a9f1bc6a7759cf66baefed4c864b65985e7488e8721184ecc4d6965", "asset": "80013f81a66cb99977879e31639bb4fe4b12b4c7058585d3f7f159d26a9", "amount": 3000000000 }, { "hash": "e5f75036b6f662ff705378b55dd29dc1a43acb23d701dd44a068cdab2c43ad0c", "asset": "80013f81a66cb99977879e31639bb4fe4b12b4c7058585d3f7f159d26a9", "amount": 15000000000 } ], "limit": 10, "start": 0 } } (自己准备参数调用一下,以上是例子而已) 查询用户地址信息与余额接口 /api/v1/btm/account/list-addresses 参数 {"guid":"b414005b-b501-4a0e-8b0f-e1cd762272f4"} 结果 { "code": 200, "msg": "", "result": { "_links": {}, "data": [{ "guid": "b414005b-b501-4a0e-8b0f-e1cd762272f4", "address": "bm1qp4t6thlyktt6sh02scs8dqcpnk3ufk9e9pmq9s", "label": "", "balances": [{ "asset": "80013f81a66cb99977879e31639bb4fe4b12b4c7050fe518585d3f7f159d26a9", "balance": "68900000000", "total_received": "69000000000", "total_sent": "100000000", "decimals": 8, "alias": "", "in_usd": "0.00", "in_cny": "0.00", "in_btc": "0.000000" }, { "asset": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "balance": "1329551000", "total_received": "53790000000", "total_sent": "52460449000", "decimals": 8, "alias": "btm", "in_usd": "1.45", "in_cny": "10.10", "in_btc": "0.000142" }] }], "limit": 10, "start": 0 } } ps: guid是专门插件钱包提供的,是唯一的,这个非常有用,第三章我会详细说。 查询交易信息 /api/v1/btm/account/list-transactions 参数 {"address":"bm1qp4t6thlyktt6sh02scs8dqcpnk3ufk9e9pmq9s","start":0,"limit":100} 结果 { "code": 200, "msg": "", "result": { "data": [{ "ID": 111, "Hash": "471e5b267f646546be33505773186ee9d8dde2180a515df67a90d1a5f9d17bd2", "AssetID": "80013f81a66cb99977879e31639bb4fe4b12b4c7050fe518585d3f7f159d26a9", "Amount": 7000000000, "Address": "bm1qp4t6thlyktt6sh02scs8dqcpnk3ufk9e9pmq9s", "BaseID": 5, "Timestamp": "2019-07-08T09:23:12+08:00", "Height": 263728, "TransactionID": "471e5b267f646546be33505773186ee9d8dde2180a515df67a90d1a5f9d17bd2", "InputAmount": 5700000000 }, { "ID": 64, "Hash": "e69631a8d6321d738793646399ffe022ac177a5732f562970e706ee76d49de82", "AssetID": "80013f81a66cb99977879e31639bb4fe4b12b4c7050fe518585d3f7f159d26a9", "Amount": 5000000000, "Address": "bm1qp4t6thlyktt6sh02scs8dqcpnk3ufk9e9pmq9s", "BaseID": 5, "Timestamp": "2019-07-05T16:37:07+08:00", "Height": 262170, "TransactionID": "e69631a8d6321d738793646399ffe022ac177a5732f562970e706ee76d49de82", "InputAmount": 5500000000 }, { "ID": 56, "Hash": "cf74906808a1a6bc6a056c148510d542a10d2cbc350a4d830c670aa5ba973873", "AssetID": "80013f81a66cb99977879e31639bb4fe4b12b4c7050fe518585d3f7f159d26a9", "Amount": 39000000000, "Address": "bm1qp4t6thlyktt6sh02scs8dqcpnk3ufk9e9pmq9s", "BaseID": 5, "Timestamp": "2019-07-03T14:59:22+08:00", "Height": 261006, "TransactionID": "cf74906808a1a6bc6a056c148510d542a10d2cbc350a4d830c670aa5ba973873", "InputAmount": 8900000000 }, { "ID": 54, "Hash": "6aedf609d47b3c06de2ce7dc9f2c99895124c80074573cd29407ac3b34ef8d40", "AssetID": "80013f81a66cb99977879e31639bb4fe4b12b4c7050fe518585d3f7f159d26a9", "Amount": 2000000000, "Address": "bm1qp4t6thlyktt6sh02scs8dqcpnk3ufk9e9pmq9s", "BaseID": 5, "Timestamp": "2019-07-03T12:11:12+08:00", "Height": 260936, "TransactionID": "6aedf609d47b3c06de2ce7dc9f2c99895124c80074573cd29407ac3b34ef8d40", "InputAmount": 5200000000 }] } } 5)开发后端,相当于bufferserver,第三章详细说明顺便我解析一下bufferserver的源码内容,还有里面踩过的坑。 总结: 这一章内容主要比较繁琐强调是调试合约方面,就是最核心的问题,这里抛出一个问题,就是UTXO问题,调试过程中非常繁琐,本来区块链不是做高并发,但是也存在并发问题,应该如何解决? 有使用过PC钱包的朋友肯定知道,里面PC钱包的UTXO,在交易过程中锁定了,没办法操作下一个,有些很多UTXO还好,如果只有一个,基本上调试跟实用都很麻烦~~~第三章我们基于原有bufferserver基础上根据官方的方案改一下,一定程度解决并发问题,大家期待一下。 参考资料: 比原链DAPP开发流程 储蓄分红合约Demo访问地址 储蓄分红合约后端bufferserver源码 储蓄分红合约前端源代码 储蓄分红合约详细说明 equity 智能合约学习文档 插件钱包API 作者:天才的饭桶
简介 研究比原链已经一年了,用比原链做了几个dapp,而且最近还做了一个基于他们插件钱包的dapp,总结了一些遇到的坑,还有一些技术细节,接下来我会分成三章,从dapp设计架构上,到深入到源码分析去帮各位介绍一下比原链的dapp,还有分析比原官方最近发布的dapp的架构。 Dapp架构设计 这个是所有工作的基础,从看完比原链源码使用过比原的钱包后,我们就在思考比原链的dapp如何做,应该说是区块链应用应该如何做,我们之前尝试过用以太坊、比特币、超级账本去做dapp。先总结一下区块链dapp的痛点: 1)没办法保证上链前数据的真实性; 2)Tps很低; 3)接入成本高,需要自己搭建节点; Dapp架构方案 我现在总结了两个基于比原链的dapp架构方案:(如果有新版或者比较好的解决方案欢迎交流) Dapp肯定离不开复杂的业务,所以肯定会用到比原链的智能合约,以下方案都支持智能合约。 一、搭建区块链node 其实就是自己搭建个节点,然后应用直接调用节点提供的接口,完成了区块链的业务内容,比原链的源码整合了钱包功能,搭建也比较方便,几句代码就可以搭建完了,但是这样的业务视乎不大合理,因为这种后端整合比原源码钱包(以下称为“pc钱包”)的方式,相当于把所有的账户信息都托管给dapp,其实就是一个集中的官方的钱包,所有的账户都归官方管,这样会有中心化问题,最后会被怀疑用不用这个区块链是否有必要。 比原链自己有一套用户的模块,用户可以使用pc钱包、客户端钱包、手机钱包等,自己的用户信息可以自己备份,交易信息全部公开全部可以到区块链浏览器里面查到。这个方案只是主要实现了交易上链。 ps: 当然其实还是可以变通一下,就是说把PC钱包的所有接口在dapp实现一次,然后结合业务,但是比原的源码是会不断更新,还要随着它的版本更新,然后更新自己的应用,显然不实际。 说一下里面的坑: 1)账户BTM问题,这种方案每个dapp账户底层都要绑定一个钱包的用户,可以展现地址用户自己充值、直接在dapp里面充值、完成任务派送这些等,但是初始化账户拥有BTM需要有时间过程,正常应用这样的体验,早就让用户关闭了。 2)UTXO问题,比原链是基于utxo未花费输出交易模型,当自己的UTXO参与的交易没有确定是无法使用的,但是dapp这里绑定的用户,不能保证他有足够多的UTXO,除非自己转账的时候让他拆分,否则会类似单线程的操作,也是比较慢。 3)用户无法获取自己的私钥,在比原链PC钱包,是一套私钥,派生多个账号这样,就是说一个钱包就一套私钥,这个不能给用户。这样又违背了区块链的去中心化的问题。 总的来说,这个方案是单纯保证了dapp交易上链,但是各方面明显不足。 二、插件钱包(Byone)方案 这个方案是今年比原链推出的dapp新型的解决方案,有解决到方案一的痛点,这个也是我比较提倡的方案,现在比原链的智能合约功能已经非常强大,如果做复杂的dapp,用这个方式比较好。 简单来说就基于chrome开发了一个插件钱包,安装完插件,用户直接可以创建账户,使用账户的转账功能,里面有BTM的转账功能,账户的备份功能....是比较完整的一个钱包,这个钱包最大的作用就是包含了丰富的开发者api,可以支持开发者去实现智能合约交易。 我们重点说一下这个结构的技术原理,如图 1)Dapp前端,就是前端页面,插件钱包是基于chrome的,所以这里代表的就是新的页面集成了插件钱包(Byone)的api。 2)Byone,就是在chrome应用商店里面可以搜索到,点击安装就行,当前版本是2.0.0,非常好用。 3)Bufferserver服务器,官方提供demo里面这模块属于缓存服务器,其实这个应该改成Dapp后端,实际业务逻辑还有很多需要后端辅助,例如排行榜、非BTM比原资产交易等。(这块后面重点开一章去说清楚),现在理解称为后端就可以。 4)Blockcenter,其实就是官方提供的服务,直接提供接口可以触发比原链的交易功能,这样解决了上面的方案,避免需要自己搭建node节点,让dapp开发者更加容易接入。 总结 两套方案里面,方案一个人认为属于早期的方案,随着比原链的发展,我们可以适当了解有助于我们理解比原链的架构设计,而且在方案二里面,也一定要用到PC钱包,它可以协助我们开发者提高开发效率100倍,没有夸张,是100倍。 插件钱包现在还在推广还有完善当中,不过功能已经非常成熟了,所以Dapp开发者赶紧抓紧去使用来开发属于自己的dapp吧。 下一章我们重点说一下Dapp的开发流程。以及遇到的一些坑,还有粘贴源码。 作者:天才的饭桶
Bystack是由比原链团队提出的一主多侧链架构的BaaS平台。其将区块链应用分为三层架构:底层账本层,侧链扩展层,业务适配层。底层账本层为Layer1,即为目前比较成熟的采用POW共识的Bytom公链。侧链扩展层为Layer2,为多侧链层,vapor侧链即处于Layer2。 (图片来自Bystack白皮书) Vapor侧链采用DPOS和BBFT共识,TPS可以达到数万。此处就分析一下连接Bytom主链和Vapor侧链的跨链模型。 主侧链协同工作模型 1、技术细节 POW当前因为能源浪费而饱受诟病,而且POW本身在提高TPS的过程中遇到诸多问题,理论上可以把块变大,可以往块里面塞更多的交易。TPS是每秒出块数*块里面的交易数。但是也存在问题:小节点吃不消存储这么大的容量的内容,会慢慢变成中心化的模式,因为只有大财团和大机构才有财力去组建机房设备,成为能出块的节点。同时传输也存在问题,网络带宽是有限的,块的大小与网络传输的边际是有关的,不可能无限的去增加块的大小,网络边际上的人拿不到新块的信息,也会降低去中心化的程度,这就是为什么POW不能在提高可靠性的情况下,提高TPS的原因。 而BFT虽然去中心化较弱,但其效率和吞吐量高,也不需要大量的共识计算,非常环保节能,很符合Bystack侧链高TPS的性能需求 (1)跨链模型架构 在Bystack的主侧链协同工作模型中,包括有主链、侧链和Federation。主链为bytom,采用基于对AI 计算友好型PoW(工作量证明)算法,主要负责价值锚定,价值传输和可信存证。侧链为Vapor,采用DPOS+BBFT共识,高TPS满足垂直领域业务。主链和侧链之间的资产流通主要依靠Federation。 (2)节点类型 跨链模型中的节点主要有收集人、验证人和联邦成员。收集人监控联邦地址,收集交易后生成Claim交易进行跨链。验证人则是侧链的出块人。联邦成员由侧链的用户投票通过选举产生,负责生成新的联邦合约地址。 (3)跨链交易流程 主链到侧链 主链用户将代币发送至联邦合约地址,收集人监控联邦地址,发现跨链交易后生成Claim交易,发送至侧链 侧链到主链 侧链用户发起提现交易,销毁侧链资产。收集人监控侧链至主链交易,向主链地址发送对应数量资产。最后联邦在侧链生成一笔完成提现的操作交易。 2、代码解析 跨链代码主要处于federation文件夹下,这里就这部分代码进行一个介绍。 (1)keeper启动 整个跨链的关键在于同步主链和侧链的区块,并处理区块中的跨链交易。这部份代码主要在mainchain_keerper.go和sidechain_keerper.go两部分中,分别对应处理主链和侧链的区块。keeper在Run函数中启动。 func (m *mainchainKeeper) Run() { ticker := time.NewTicker(time.Duration(m.cfg.SyncSeconds) * time.Second) for ; true; <-ticker.C { for { isUpdate, err := m.syncBlock() if err != nil { //.. } if !isUpdate { break } } } } Run函数中首先生成一个定时的Ticker,规定每隔SyncSeconds秒同步一次区块,处理区块中的交易。 (2)主侧链同步区块 Run函数会调用syncBlock函数同步区块。 func (m *mainchainKeeper) syncBlock() (bool, error) { chain := &orm.Chain{Name: m.chainName} if err := m.db.Where(chain).First(chain).Error; err != nil { return false, errors.Wrap(err, "query chain") } height, err := m.node.GetBlockCount() //.. if height <= chain.BlockHeight+m.cfg.Confirmations { return false, nil } nextBlockStr, txStatus, err := m.node.GetBlockByHeight(chain.BlockHeight + 1) //.. nextBlock := &types.Block{} if err := nextBlock.UnmarshalText([]byte(nextBlockStr)); err != nil { return false, errors.New("Unmarshal nextBlock") } if nextBlock.PreviousBlockHash.String() != chain.BlockHash { //... return false, ErrInconsistentDB } if err := m.tryAttachBlock(chain, nextBlock, txStatus); err != nil { return false, err } return true, nil } 这个函数受限会根据chainName从数据库中取出对应的chain。然后利用GetBlockCount函数获得chain的高度。然后进行一个伪确定性的检测。 height <= chain.BlockHeight+m.cfg.Confirmations 主要是为了判断链上的资产是否已经不可逆。这里Confirmations的值被设为10。如果不进行这个等待不可逆的过程,很可能主链资产跨链后,主链的最长链改变,导致这笔交易没有在主链被打包,而侧链却增加了相应的资产。在此之后,通过GetBlockByHeight函数获得chain的下一个区块。 nextBlockStr, txStatus, err := m.node.GetBlockByHeight(chain.BlockHeight + 1) 这里必须满足下个区块的上一个区块哈希等于当前chain中的这个头部区块哈希。这也符合区块链的定义。 if nextBlock.PreviousBlockHash.String() != chain.BlockHash { //.. } 在此之后,通过调用tryAttachBlock函数进一步调用processBlock函数处理区块。 (3)区块处理 processBlock函数会判断区块中交易是否为跨链的deposit或者是withdraw,并分别调用对应的函数去进行处理。 func (m *mainchainKeeper) processBlock(chain *orm.Chain, block *types.Block, txStatus *bc.TransactionStatus) error { if err := m.processIssuing(block.Transactions); err != nil { return err } for i, tx := range block.Transactions { if m.isDepositTx(tx) { if err := m.processDepositTx(chain, block, txStatus, uint64(i), tx); err != nil { return err } } if m.isWithdrawalTx(tx) { if err := m.processWithdrawalTx(chain, block, uint64(i), tx); err != nil { return err } } } return m.processChainInfo(chain, block) } 在这的processIssuing函数,它内部会遍历所有交易输入Input的资产类型,也就是AssetID。当这个AssetID不存在的时候,则会去在系统中创建一个对应的资产类型。每个Asset对应的数据结构如下所示。 m.assetStore.Add(&orm.Asset{ AssetID: assetID.String(), IssuanceProgram: hex.EncodeToString(inp.IssuanceProgram), VMVersion: inp.VMVersion, RawDefinitionByte: hex.EncodeToString(inp.AssetDefinition), }) 在processBlock函数中,还会判断区块中每笔交易是否为跨链交易。主要通过isDepositTx和isWithdrawalTx函数进行判断。 func (m *mainchainKeeper) isDepositTx(tx *types.Tx) bool { for _, output := range tx.Outputs { if bytes.Equal(output.OutputCommitment.ControlProgram, m.fedProg) { return true } } return false } func (m *mainchainKeeper) isWithdrawalTx(tx *types.Tx) bool { for _, input := range tx.Inputs { if bytes.Equal(input.ControlProgram(), m.fedProg) { return true } } return false } 看一下这两个函数,主要还是通过比较交易中的control program这个标识和mainchainKeeper这个结构体中的fedProg进行比较,如果相同则为跨链交易。fedProg在结构体中为一个字节数组。 type mainchainKeeper struct { cfg *config.Chain db *gorm.DB node *service.Node chainName string assetStore *database.AssetStore fedProg []byte } (4)跨链交易(主链到侧链的deposit)处理 这部分主要分为主链到侧链的deposit和侧链到主链的withdraw。先看比较复杂的主链到侧链的deposit这部分代码的处理。 func (m *mainchainKeeper) processDepositTx(chain *orm.Chain, block *types.Block, txStatus *bc.TransactionStatus, txIndex uint64, tx *types.Tx) error { //.. rawTx, err := tx.MarshalText() if err != nil { return err } ormTx := &orm.CrossTransaction{ //.. } if err := m.db.Create(ormTx).Error; err != nil { return errors.Wrap(err, fmt.Sprintf("create mainchain DepositTx %s", tx.ID.String())) } statusFail := txStatus.VerifyStatus[txIndex].StatusFail crossChainInputs, err := m.getCrossChainReqs(ormTx.ID, tx, statusFail) if err != nil { return err } for _, input := range crossChainInputs { if err := m.db.Create(input).Error; err != nil { return errors.Wrap(err, fmt.Sprintf("create DepositFromMainchain input: txid(%s), pos(%d)", tx.ID.String(), input.SourcePos)) } } return nil } 这里它创建了一个跨链交易orm。具体的结构如下。可以看到,这里它的结构体中包括有source和dest的字段。 ormTx := &orm.CrossTransaction{ ChainID: chain.ID, SourceBlockHeight: block.Height, SourceBlockTimestamp: block.Timestamp, SourceBlockHash: blockHash.String(), SourceTxIndex: txIndex, SourceMuxID: muxID.String(), SourceTxHash: tx.ID.String(), SourceRawTransaction: string(rawTx), DestBlockHeight: sql.NullInt64{Valid: false}, DestBlockTimestamp: sql.NullInt64{Valid: false}, DestBlockHash: sql.NullString{Valid: false}, DestTxIndex: sql.NullInt64{Valid: false}, DestTxHash: sql.NullString{Valid: false}, Status: common.CrossTxPendingStatus, } 创建这笔跨链交易后,它会将交易存入数据库中。 if err := m.db.Create(ormTx).Error; err != nil { return errors.Wrap(err, fmt.Sprintf("create mainchain DepositTx %s", tx.ID.String())) } 在此之后,这里会调用getCrossChainReqs。这个函数内部较为复杂,主要作用就是遍历交易的输出,返回一个跨链交易的请求数组。具体看下这个函数。 func (m *mainchainKeeper) getCrossChainReqs(crossTransactionID uint64, tx *types.Tx, statusFail bool) ([]*orm.CrossTransactionReq, error) { //.. switch { case segwit.IsP2WPKHScript(prog): //.. case segwit.IsP2WSHScript(prog): //.. } reqs := []*orm.CrossTransactionReq{} for i, rawOutput := range tx.Outputs { //.. req := &orm.CrossTransactionReq{ //.. } reqs = append(reqs, req) } return reqs, nil } 很显然,这个地方的交易类型有pay to public key hash 和 pay to script hash这两种。这里会根据不同的交易类型进行一个地址的获取。 switch { case segwit.IsP2WPKHScript(prog): if pubHash, err := segwit.GetHashFromStandardProg(prog); err == nil { fromAddress = wallet.BuildP2PKHAddress(pubHash, &vaporConsensus.MainNetParams) toAddress = wallet.BuildP2PKHAddress(pubHash, &vaporConsensus.VaporNetParams) } case segwit.IsP2WSHScript(prog): if scriptHash, err := segwit.GetHashFromStandardProg(prog); err == nil { fromAddress = wallet.BuildP2SHAddress(scriptHash, &vaporConsensus.MainNetParams) toAddress = wallet.BuildP2SHAddress(scriptHash, &vaporConsensus.VaporNetParams) } } 在此之后,函数会遍历所有交易的输出,然后创建跨链交易请求,具体的结构如下。 req := &orm.CrossTransactionReq{ CrossTransactionID: crossTransactionID, SourcePos: uint64(i), AssetID: asset.ID, AssetAmount: rawOutput.OutputCommitment.AssetAmount.Amount, Script: script, FromAddress: fromAddress, ToAddress: toAddress, } 创建完所有的跨链交易请求后,返回到processDepositTx中一个crossChainInputs数组中,并存入db。 for _, input := range crossChainInputs { if err := m.db.Create(input).Error; err != nil { return errors.Wrap(err, fmt.Sprintf("create DepositFromMainchain input: txid(%s), pos(%d)", tx.ID.String(), input.SourcePos)) } } 到这里,对主链到侧链的deposit已经处理完毕。 (5)跨链交易(侧链到主链的withdraw)交易处理 这部分比较复杂的逻辑主要在sidechain_keeper.go中的processWithdrawalTx函数中。这部分逻辑和上面主链到侧链的deposit逻辑类似。同样是创建了orm.crossTransaction结构体,唯一的改变就是交易的souce和dest相反。这里就不作具体描述了。 3、跨链优缺点 优点 (1) 跨链模型、代码较为完整。当前有很多项目使用跨链技术,但是真正实现跨链的寥寥无几。 (2) 可以根据不同需求实现侧链,满足多种场景 缺点 (1) 跨链速度较慢,需等待10个区块确认,这在目前Bytom网络上所需时间为30分钟左右 (2) 相较于comos、polkadot等项目,开发者要开发侧链接入主网成本较大 (3) 只支持资产跨链,不支持跨链智能合约调用 **4、**跨链模型平行对比Cosmos 可扩展性 bystack的主测链协同工作模型依靠Federation,未形成通用协议。其他开发者想要接入其跨链网络难度较大。Cosmos采用ibc协议,可扩展性较强。 代码开发进度 vapor侧链已经能够实现跨链。Cosmos目前暂无成熟跨链项目出现,ibc协议处于最终开发阶段。 跨链模型 vapor为主侧链模型,Cosmos为Hub-Zone的中继链模型。 5、参考建议 侧链使用bbft共识,非POW的情况下,无需等待10个交易确认,增快跨链速度。 作者:诗人
储蓄分红DAPP 储蓄分红合约简介 储蓄分红合约指的是项目方发起了一个锁仓计划(即储蓄合约和取现合约),用户可以在准备期自由选择锁仓金额参与该计划,等到锁仓到期之后还可以自动获取锁仓的利润。用户可以在准备期内(dueBlockHeight)参与储蓄,按照合约规定可以 1:1 获取同等数量的储蓄票据资产,同时用户锁仓的资产(deposit)将放到取现合约中,并且项目方是无法动用的,等到锁仓期限(expireBlockHeight)一到,用户便可以调用取现合约将自己储蓄的资产连本待息一同取出来。其示意图如下: 从上图中可以看出,项目方发布了一个利润为20%的锁仓项目,其中储蓄合约FixedLimitCollect锁定了1000个票据资产(bill),同时项目方将200个储蓄资产(deposit)锁定到利息合约中。待项目方发布完合约之后,所有用户便可以参与了。例如上图中user1调用合约储蓄了500,这500个储蓄资产将被锁定在取现合约FixedLimitProfit中,同时user1获得了500个票据资产,剩余找零的资产将继续锁定在储蓄合约FixedLimitCollect中,以此类推,user2和user3也是相同的流程,直到储蓄合约没有资产为止。取现合约FixedLimitProfit跟储蓄合约的模型大致相同,只是取现合约是由多个UTXO组成的,用户在取现的时候可以并行操作。但是如果合约中的面值不能支持用户一次性取现的话,需要分多次提取。例如user1拥有500个票据资产,而可以获得的本息总额为600,但是取现的UTXO面值为500,那么user1一次最多只能取500,剩下的100需要再构造一笔交易来提现。 合约源代码 // 储蓄合约 import "./FixedLimitProfit" contract FixedLimitCollect(assetDeposited: Asset, totalAmountBill: Amount, totalAmountCapital: Amount, dueBlockHeight: Integer, expireBlockHeight: Integer, additionalBlockHeight: Integer, banker: Program, bankerKey: PublicKey) locks billAmount of billAsset { clause collect(amountDeposited: Amount, saver: Program) { verify below(dueBlockHeight) verify amountDeposited <= billAmount && totalAmountBill <= totalAmountCapital define sAmountDeposited: Integer = amountDeposited/100000000 define sTotalAmountBill: Integer = totalAmountBill/100000000 verify sAmountDeposited > 0 && sTotalAmountBill > 0 if amountDeposited < billAmount { lock amountDeposited of assetDeposited with FixedLimitProfit(billAsset, totalAmountBill, totalAmountCapital, expireBlockHeight, additionalBlockHeight, banker, bankerKey) lock amountDeposited of billAsset with saver lock billAmount-amountDeposited of billAsset with FixedLimitCollect(assetDeposited, totalAmountBill, totalAmountCapital, dueBlockHeight, expireBlockHeight, additionalBlockHeight, banker, bankerKey) } else { lock amountDeposited of assetDeposited with FixedLimitProfit(billAsset, totalAmountBill, totalAmountCapital, expireBlockHeight, additionalBlockHeight, banker, bankerKey) lock billAmount of billAsset with saver } } clause cancel(bankerSig: Signature) { verify above(dueBlockHeight) verify checkTxSig(bankerKey, bankerSig) unlock billAmount of billAsset } } // 取现合约(本金加利息) contract FixedLimitProfit(assetBill: Asset, totalAmountBill: Amount, totalAmountCapital: Amount, expireBlockHeight: Integer, additionalBlockHeight: Integer, banker: Program, bankerKey: PublicKey) locks capitalAmount of capitalAsset { clause profit(amountBill: Amount, saver: Program) { verify above(expireBlockHeight) define sAmountBill: Integer = amountBill/100000000 define sTotalAmountBill: Integer = totalAmountBill/100000000 verify sAmountBill > 0 && sTotalAmountBill > 0 && amountBill < totalAmountBill define gain: Integer = totalAmountCapital*sAmountBill/sTotalAmountBill verify gain > 0 && gain <= capitalAmount if gain < capitalAmount { lock amountBill of assetBill with banker lock gain of capitalAsset with saver lock capitalAmount - gain of capitalAsset with FixedLimitProfit(assetBill, totalAmountBill, totalAmountCapital, expireBlockHeight, additionalBlockHeight, banker, bankerKey) } else { lock amountBill of assetBill with banker lock capitalAmount of capitalAsset with saver } } clause cancel(bankerSig: Signature) { verify above(additionalBlockHeight) verify checkTxSig(bankerKey, bankerSig) unlock capitalAmount of capitalAsset } } 合约的源代码说明可以具体参考Equity合约介绍. 注意事项: 时间期限不是具体的时间,而是通过区块高度来大概估算的(平均区块时间间隔大概为2.5分钟) 比原的精度是8, 即 1BTM = 100000000 neu,正常情况下参与计算都是以neu为单位的,然而虚拟机的int64类型的最大值是9223372036854775807,为了避免数值太大导致计算溢出,所以对计算的金额提出了金额限制(即amountBill/100000000) 另外clause cancel是项目方的管理方法,如果储蓄或者取现没有满额,项目方也可以回收剩余的资产 编译并实例化合约 编译Equity合约可以参考一下Equity编译器的介绍说明。假如储蓄合约FixedLimitCollect的参数如下: assetDeposited :c6b12af8326df37b8d77c77bfa2547e083cbacde15cc48da56d4aa4e4235a3ee totalAmountBill :10000000000 totalAmountCapital :20000000000 dueBlockHeight :1070 expireBlockHeight :1090 additionalBlockHeight :1100 banker :0014dedfd406c591aa221a047a260107f877da92fec5 bankerKey :055539eb36abcaaf127c63ae20e3d049cd28d0f1fe569df84da3aedb018ca1bf 其中bankerKey是管理员的publicKey,可以通过比原链的接口list-pubkeys来获取,注意管理员需要保存一下对应的rootXpub和Path,否则无法正确调用clause cancel。 实例化合约命令如下: // 储蓄合约 ./equity FixedLimitCollect --instance c6b12af8326df37b8d77c77bfa2547e083cbacde15cc48da56d4aa4e4235a3ee 10000000000 20000000000 1070 1090 1100 0014dedfd406c591aa221a047a260107f877da92fec5 055539eb36abcaaf127c63ae20e3d049cd28d0f1fe569df84da3aedb018ca1bf // 取现合约 ./equity FixedLimitProfit --instance c6b12af8326df37b8d77c77bfa2547e083cbacde15cc48da56d4aa4e4235a3ee 10000000000 20000000000 1090 1100 0014dedfd406c591aa221a047a260107f877da92fec5 055539eb36abcaaf127c63ae20e3d049cd28d0f1fe569df84da3aedb018ca1bf 发布合约交易 发布合约交易即将资产锁定到合约中。由于目前无法在比原的dashboard上构造合约交易,所以需要借助外部工具来发送合约交易,比如postman。按照上述示意图所示,项目方需要发布1000个储蓄资产的储蓄合约和200个利息资产取现合约。假设项目方需要发布1000个储蓄资产(假如精度为8,那么1000个在比原链中表示为100000000000)的锁仓合约,那么他需要将对应数量的票据锁定在储蓄合约中,其交易模板如下: { "base_transaction": null, "actions": [ { "account_id": "0ILGLSTC00A02", "amount": 20000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "type": "spend_account" }, { "account_id": "0ILGLSTC00A02", "amount": 100000000000, "asset_id": "13016eff73ffb7539a69e122f80f5c1cc94446773ac3f64dec290429f87e73b3", "type": "spend_account" }, { "amount": 100000000000, "asset_id": "13016eff73ffb7539a69e122f80f5c1cc94446773ac3f64dec290429f87e73b3", "control_program": "20055539eb36abcaaf127c63ae20e3d049cd28d0f1fe569df84da3aedb018ca1bf160014dedfd406c591aa221a047a260107f877da92fec5024c04024204022e040500c817a8040500e40b540220c6b12af8326df37b8d77c77bfa2547e083cbacde15cc48da56d4aa4e4235a3ee4d3b02597a642f0200005479cda069c35b797ca153795579a19a695a790400e1f5059653790400e1f505967c00a07c00a09a69c35b797c9f9161644d010000005b79c2547951005e79895d79895c79895b7989597989587989537a894caa587a649e0000005479cd9f6959790400e1f5059653790400e1f505967800a07800a09a5c7956799f9a6955797b957c96c37800a052797ba19a69c3787c9f91616487000000005b795479515b79c1695178c2515d79c16952c3527994c251005d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696399000000005b795479515b79c16951c3c2515d79c16963aa000000557acd9f69577a577aae7cac890274787e008901c07ec169515b79c2515d79c16952c35c7994c251005d79895c79895b79895a79895979895879895779895679895579890274787e008901c07ec169632a020000005b79c2547951005e79895d79895c79895b7989597989587989537a894caa587a649e0000005479cd9f6959790400e1f5059653790400e1f505967800a07800a09a5c7956799f9a6955797b957c96c37800a052797ba19a69c3787c9f91616487000000005b795479515b79c1695178c2515d79c16952c3527994c251005d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696399000000005b795479515b79c16951c3c2515d79c16963aa000000557acd9f69577a577aae7cac890274787e008901c07ec16951c3c2515d79c169633b020000547acd9f69587a587aae7cac747800c0", "type": "control_program" } ], "ttl": 0, "time_range": 1521625823 } 合约交易成功后,合约control_program对应的UTXO将会被所有用户查询到,使用比原链的接口list-unspent-outputs即可查询。 此外,开发者需要存储一下合约UTXO的assetID和program,以便在DAPP的前端页面的config配置文件和bufferserver缓冲服务器中调用。如上所示: // 储蓄合约 assetID:13016eff73ffb7539a69e122f80f5c1cc94446773ac3f64dec290429f87e73b3 program:20055539eb36abcaaf127c63ae20e3d049cd28d0f1fe569df84da3aedb018ca1bf160014dedfd406c591aa221a047a260107f877da92fec5024c04024204022e040500c817a8040500e40b540220c6b12af8326df37b8d77c77bfa2547e083cbacde15cc48da56d4aa4e4235a3ee4d3b02597a642f0200005479cda069c35b797ca153795579a19a695a790400e1f5059653790400e1f505967c00a07c00a09a69c35b797c9f9161644d010000005b79c2547951005e79895d79895c79895b7989597989587989537a894caa587a649e0000005479cd9f6959790400e1f5059653790400e1f505967800a07800a09a5c7956799f9a6955797b957c96c37800a052797ba19a69c3787c9f91616487000000005b795479515b79c1695178c2515d79c16952c3527994c251005d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696399000000005b795479515b79c16951c3c2515d79c16963aa000000557acd9f69577a577aae7cac890274787e008901c07ec169515b79c2515d79c16952c35c7994c251005d79895c79895b79895a79895979895879895779895679895579890274787e008901c07ec169632a020000005b79c2547951005e79895d79895c79895b7989597989587989537a894caa587a649e0000005479cd9f6959790400e1f5059653790400e1f505967800a07800a09a5c7956799f9a6955797b957c96c37800a052797ba19a69c3787c9f91616487000000005b795479515b79c1695178c2515d79c16952c3527994c251005d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696399000000005b795479515b79c16951c3c2515d79c16963aa000000557acd9f69577a577aae7cac890274787e008901c07ec16951c3c2515d79c169633b020000547acd9f69587a587aae7cac747800c0 // 取现合约 assetID:c6b12af8326df37b8d77c77bfa2547e083cbacde15cc48da56d4aa4e4235a3ee program:20055539eb36abcaaf127c63ae20e3d049cd28d0f1fe569df84da3aedb018ca1bf160014dedfd406c591aa221a047a260107f877da92fec5024c040242040500c817a8040500e40b540220c6b12af8326df37b8d77c77bfa2547e083cbacde15cc48da56d4aa4e4235a3ee4caa587a649e0000005479cd9f6959790400e1f5059653790400e1f505967800a07800a09a5c7956799f9a6955797b957c96c37800a052797ba19a69c3787c9f91616487000000005b795479515b79c1695178c2515d79c16952c3527994c251005d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696399000000005b795479515b79c16951c3c2515d79c16963aa000000557acd9f69577a577aae7cac747800c0 储蓄分红DAPP架构模型 比原链的DAPP总体框架模型描述了DAPP的大致结构模型,结合储蓄分红合约案例,其具体流程如下: DAPP前端 储蓄分红合约前端逻辑处理流程大致如下: 1)调用插件比原的chrome插件源码位于Bytom-JS-SDK,开发比原DAPP时调用插件的说明可以参考Dapp Developer Guide 2)配置合约参数 该Dapp demo中需要配置实例化的参数为assetDeposited、totalAmountBill、totalAmountCapital、dueBlockHeight、expireBlockHeight、additionalBlockHeight、banker、bankerKey。其前端配置文件为configure.json.js var config = { "solonet": { "depositProgram": "2091194ddbf3614cafbadb1274c33e61afd4d5044c6ec4c30f8202980199c30083160014c800033d5e94de5f22e23a6d3cbeaed87b55bd640600204aa9d101050010a5d4e8203310d9951697418af3cdbe7a9cdde1dc49bb5439503dacb33828d6c9ef5af5a24dfc01567a64f5010000c358797ca153795579a19a6957790400e1f5059653790400e1f505967c00a07c00a09a69c358797c9f91616429010000005879c2547951005b79895a7989597989587989537a894c9a567a649300000057790400e1f5059653790400e1f505967800a07800a09a5a7956799f9a6955797b957c96c37800a052797ba19a69c3787c9f9161647c0000000059795479515979c1695178c2515b79c16952c3527994c251005b79895a79895979895879895779895679890274787e008901c07ec169638e0000000059795479515979c16951c3c2515b79c169639a000000567a567aae7cac890274787e008901c07ec169515879c2515a79c16952c3597994c251005a79895979895879895779895679895579890274787e008901c07ec16963f0010000005879c2547951005b79895a7989597989587989537a894c9a567a649300000057790400e1f5059653790400e1f505967800a07800a09a5a7956799f9a6955797b957c96c37800a052797ba19a69c3787c9f9161647c0000000059795479515979c1695178c2515b79c16952c3527994c251005b79895a79895979895879895779895679890274787e008901c07ec169638e0000000059795479515979c16951c3c2515b79c169639a000000567a567aae7cac890274787e008901c07ec16951c3c2515a79c16963fc010000567a567aae7cac747800c0", "profitProgram": "2091194ddbf3614cafbadb1274c33e61afd4d5044c6ec4c30f8202980199c30083160014c800033d5e94de5f22e23a6d3cbeaed87b55bd640600204aa9d101050010a5d4e820666f298d34806a6db0528c3b3d081bc00fa58aa393e7c42f90d67eb7db2a524f4c9a567a649300000057790400e1f5059653790400e1f505967800a07800a09a5a7956799f9a6955797b957c96c37800a052797ba19a69c3787c9f9161647c0000000059795479515979c1695178c2515b79c16952c3527994c251005b79895a79895979895879895779895679890274787e008901c07ec169638e0000000059795479515979c16951c3c2515b79c169639a000000567a567aae7cac747800c0", "assetDeposited": "3310d9951697418af3cdbe7a9cdde1dc49bb5439503dacb33828d6c9ef5af5a2", "assetBill": "666f298d34806a6db0528c3b3d081bc00fa58aa393e7c42f90d67eb7db2a524f", "totalAmountBill": 1000000000000, "totalAmountCapital": 2000000000000, "dueBlockHeight": 0, "expireBlockHeight": 0, "banker": "0014c800033d5e94de5f22e23a6d3cbeaed87b55bd64", "gas": 0.4 }, "testnet":{ "depositProgram": "20f39af759065598406ca988f0dd79af9175dd7adcbe019317a2d605578b1597ac1600147211ec12410ce8bd0d71cab0a29be3ea61c71eb103c8260203da240203da2402060080f420e6b50600407a10f35a2000d38a1c946e8cba1a69493240f281cd925002a43b81f516c4391b5fb2ffdacd4d4302597a64370200005479cda069c35b790400e1f5059600a05c797ba19a53795579a19a695a790400e1f5059653790400e1f505967800a07800a09a6955797b957c9600a069c35b797c9f9161645b010000005b79c2547951005e79895d79895c79895b7989597989587989537a894ca4587a64980000005479cd9f6959790400e1f5059653790400e1f505967800a07800a09a5c7956799f9a6955797b957c967600a069c3787c9f91616481000000005b795479515b79c1695178c2515d79c16952c3527994c251005d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696393000000005b795479515b79c16951c3c2515d79c16963a4000000557acd9f69577a577aae7cac890274787e008901c07ec169515b79c2515d79c16952c35c7994c251005d79895c79895b79895a79895979895879895779895679895579890274787e008901c07ec1696332020000005b79c2547951005e79895d79895c79895b7989597989587989537a894ca4587a64980000005479cd9f6959790400e1f5059653790400e1f505967800a07800a09a5c7956799f9a6955797b957c967600a069c3787c9f91616481000000005b795479515b79c1695178c2515d79c16952c3527994c251005d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696393000000005b795479515b79c16951c3c2515d79c16963a4000000557acd9f69577a577aae7cac890274787e008901c07ec16951c3c2515d79c1696343020000547acd9f69587a587aae7cac747800c0", "profitProgram": "20f39af759065598406ca988f0dd79af9175dd7adcbe019317a2d605578b1597ac1600147211ec12410ce8bd0d71cab0a29be3ea61c71eb103c8260203da2402060080f420e6b50600407a10f35a20f855baf98778a892bad0371f5afca845191824dc8584585d566fbbc8ef1f304c4ca4587a64980000005479cd9f6959790400e1f5059653790400e1f505967800a07800a09a5c7956799f9a6955797b957c967600a069c3787c9f91616481000000005b795479515b79c1695178c2515d79c16952c3527994c251005d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696393000000005b795479515b79c16951c3c2515d79c16963a4000000557acd9f69577a577aae7cac747800c0", "assetDeposited": "00d38a1c946e8cba1a69493240f281cd925002a43b81f516c4391b5fb2ffdacd", "assetBill": "f855baf98778a892bad0371f5afca845191824dc8584585d566fbbc8ef1f304c", "totalAmountBill": 100000000000000, "totalAmountCapital": 200000000000000, "dueBlockHeight": 140506, "expireBlockHeight": 140506, "banker": "00147211ec12410ce8bd0d71cab0a29be3ea61c71eb1", "gas": 0.4 } } 3)前端预计算处理以储蓄合约FixedLimitCollect为例,前端需要对该合约进行verify语句的预判断逻辑,以防用户输入参数之后执行失败。此外,合约中billAmount of billAsset表示锁定的资产和数量,而billAmount、billAsset和utxohash都是储存在缓冲服务器的数据表里面,因此前端需要调用list-utxo查找与该资产asset和program相关的所有未花费的utxo。 具体可以参考DAPP DEMO前端案例。 4)交易组成 比原的交易是多输入多输出的模板结构,如果合约中包含了多个lock或unlock语句,那么就需要用户构造多输入多输出的交易模板,同时,构造交易还需要根据lock语句或unlock语句来变换。交易构造具体可以参考储蓄合约交易模型和取现合约交易模型的前端源代码。 交易input结构如下: spendUTXOAction(utxohash)表示花费的合约utxo,其中utxohash表示合约UTXO的hash,而spendWalletAction(amount, Constant.assetDeposited)表示用户输入的储蓄或取现的数量(仅包含中需要资产交换的合约中),而资产类型则由前端固定。 export function spendUTXOAction(utxohash){ return { "type": "spend_utxo", "output_id": utxohash } } export function spendWalletAction(amount, asset){ return { "amount": amount, "asset": asset, "type": "spend_wallet" } } const input = [] input.push(spendUTXOAction(utxohash)) input.push(spendWalletAction(amount, Constant.assetDeposited)) 交易output结构如下: 根据合约中if-else判定逻辑,下面便是储蓄分红合约的output的构造模型。 export function controlProgramAction(amount, asset, program){ return { "amount": amount, "asset": asset, "control_program": program, "type": "control_program" } } export function controlAddressAction(amount, asset, address){ return { "amount": amount, "asset": asset, "address": address, "type": "control_address" } } const output = [] if(amountDeposited < billAmount){ output.push(controlProgramAction(amountDeposited, Constant.assetDeposited, Constant.profitProgram)) output.push(controlAddressAction(amountDeposited, billAsset, saver)) output.push(controlProgramAction((billAmount-amountDeposited), billAsset, Constant.depositProgram)) }else{ output.push(controlProgramAction(amountDeposited, Constant.assetDeposited, Constant.profitProgram)) output.push(controlAddressAction(billAmount, billAsset, saver)) } 5)启动前端服务 编译前端命令如下: npm run build 启动之前需要先启动bufferserver缓冲服务器,然后再启动前端服务,其前端启动命令如下: npm start DAPP缓冲服务器 缓冲服务器主要是为了在管理合约UTXO层面做一些效率方面的处理,包括了对bycoin服务器是如何同步请求的,此外对DAPP的相关交易记录也进行了存储。具体可以参考一下bufferserver源代码。 1)储蓄分红合约的架构说明如下: 缓冲服务器构成,目前设计了3张数据表:base、utxo和balance表。其中base表用于初始化该DAPP关注的合约program,即在查找utxo集合的时候,仅仅只需过滤出对应的program和资产即可; utxo表是该DAPP合约的utxo集合,其数据是从bycoin服务器中实时同步过来的,主要是为了提高DAPP的并发性; balance表是为了记录用户参与该合约的交易列表。 后端服务由API进程和同步进程组成,其中API服务进程用于管理对外的用户请求,而同步进程包含了两个方面:一个是从bycoin服务器同步utxo,另一个是则是通过区块链浏览器查询交易状态 项目管理员调用update-base接口更新DAPP关注的合约program和asset。而utxo同步进程会根据base表的记录来定时扫描并更新本地的utxo表中的信息,并且根据超时时间定期解锁被锁定的utxo 用户在调用储蓄或取现之前需要查询合约的utxo是否可用,可用的utxo集合中包含了未确认的utxo。用户在前端在点击储蓄或取现按键的时候,会调用utxo最优匹配算法选择最佳的utxo,然后调用update-utxo接口对该utxo进行锁定,最后就用户就可以通过插件钱包调用bycoin服务器的构建交易接口来创建交易、签名交易和提交交易。倘若所有合约utxo都被锁定了,则会缩短第一个utxo的锁定时间为60s,设置该时间间隔是为了保证未确认的交易被成功验证并生成未确认的utxo。如果该时间间隔并没有产生新的utxo,则认为前面一个用户并没有产生交易,则60s后可以再次花费该utxo。 用户发送交易成功后会生成两条balance记录表,默认状态是失败的,其中交易ID用于向区块链浏览器查询交易状态,如果交易成功则会更新balance的交易状态。此外,前端页面的balance列表表只显示交易成功的记录。 2)编译bufferserver源代码 按照README安装部署服务需要的软件包Mysql和Redis,然后下载源代码并编译: make all 编译完成之后,在`target`目录下会生成可执行文件`api`和`updater`。 3)启动服务 使用root用户创建数据库和数据表,其命令如下: mysql -u root -p < database/dump.sql 修改配置文件`config_local.json`,字段说明参考[`README`](https://github.com/oysheng/bufferserver/blob/master/README.md)的`config`配置参数详解。 启动`api`和`updater`服务器,其中`api`是提供`JSON RPC`请求的服务进程,`updater`是提供同步`blockcenter`和区块链浏览器数据请求的服务进程。 ./target/api config_local.json ./target/updater config_local.json
从目前已经发布的DAPP来看,DAPP架构大致可以分成3种类型:插件钱包模式、全节点钱包模式和兼容模式。 插件钱包模式是借助封装了钱包的浏览器插件通过RPC协议与区块链节点通信,插件在运行时会将Web3框架注入到DAPP前端页面中,然后DApp通过Web3来与区块链节点通信。 全节点钱包模式需要项目方同步并持有一个区块链节点,并对外提供一个浏览器环境与用户进行交互。 兼容模式可以在插件钱包和全节点钱包下同时使用,即上述两种方式可以自由切换,安全性能相对较高。 接下来介绍的比原链DAPP的架构模式跟账户模型DAPP的插件钱包模式有些相似,都是由DAPP前端、插件钱包和合约程序共同组成,其中插件钱包需要连接去中心化的区块链服务器blockcenter,该服务器主要是为了管理插件钱包的相关信息。此外,比原链是UTXO模型的区块链系统,合约程序存在于无状态的UTXO中,如果要实现这样一个具体的DAPP,就需要在前端和后端多做一些逻辑的处理。 1. 编写、编译并实例化智能合约 编写智能合约 比原链的虚拟机是图灵完备的,理论上可以实现任意图灵计算机能实现的操作。而Equity作为比原链的智能合约语言,使用Equity语言可以实现许多典型的金融模型案例,但是为了解决停机问题,比原链也设置了手续费的上限,因此用户在设计合约的时候做一下权衡。 合约模板结构如下: contract contract_name(...) locks valueAmount of valueAsset { clause clause_name(...) { ... lock/unlock ... } ... } Equity语法结构简单,语句意思明确,有开发经验的童鞋一看基本能明白合约的意思。编写智能合约可以参考Equity合约介绍,文档中对Equity语言的语法和编译方法都做了详细的介绍。此外,文档还对一些典型的模板合约进行了介绍,开发者可以自己需求进行参考。 编译并实例化合约 编译合约目前支持两种方式,一种是使用Equity编译工具,另一种是调用比原链中编译合约的RPC接口compile; 而合约实例化是为了将合约脚本按照用户设定的参数进行锁定,编译并实例化合约可以参考编译并实例化合约的上半部分说明,该文档不仅介绍了合约的参数构造说明,还对编译合约的步骤进行详细说明。而编译器以及相关工具位于Equity编译器中,是使用go语言开发的,用户可以下载源代码并编译使用。 工具编译和实例化示例如下: // compile ./equity [contract_name] --bin // instance ./equity [contract_name] --instance [arguments ...] 2. 部署合约 部署合约即发送合约交易,调用比原链的build-transaction接口将指定数量的资产发送到合约program中,只需将输出output中接收方control_program设置为指定合约即可。用户可以参考合约交易说明中的锁定合约章节,交易的构造按照文档中介绍进行参考即可。如果合约交易发送成功,并且交易已经成功上链,便可以通过调用API接口list-unspent-outputs来查找该合约的UTXO。 部署合约交易模板大致如下: { "actions": [ // inputs { // btm fee }, { amount, asset, spend_account // spend user asset }, // outputs { amount, asset, contract_program // receive contract program with instantiated result } ], ... } 3. 搭建DAPP架构 比原的blockcenter服务器是官方开发的去中心化插件钱包服务器,开发者可以按照相关API接口来调用即可。比原链的DAPP总体框架模型如下: DAPP前端 搭建DAPP前端主要包含两个方面:一个是前端与插件钱包的交互,另一个是前端的逻辑处理、以及与缓冲服务器的交互。插件钱包是与区块链节点服务器通信的窗口,一个DAPP为了跟区块链节点进行通信,需要通过借助插件来与后台服务器节点进行交互。比原的插件钱包除了与后台服务器进行交互之外,还包含一些本地业务逻辑处理的接口API,具体内容可以参考一下DAPP开发者向导。由于比原链是基于UTXO模型的区块链系统,交易是由多输入和多输出构成的结构,并且交易输入或输出的位置也需要按照顺序来排列,因此开发DAPP需要前端处理一些构建交易的逻辑。除此之外,合约中的lock-unlock语句中涉及到数量的计算需要根据抽象语法树来进行预计算,计算的结果将用于构建交易,而verify、if-else等其他语句类型也需要进行相关的预校验,从而防止用户在执行合约的时候报错。 从功能层面来说,前端主要包含页面的设计、插件的调用、合约交易逻辑的处理、缓冲服务器的交互等。接下来对这几个重要的部分展开说明: 1)前端页面的设计主要是网页界面的设计,这个部分开发者可以自己选择页面模式 2)插件钱包已经进行了结构化的封装,并且提供了外部接口给DAPP开发者调用,开发者只需要将插件的参数按照规则进行填充,具体请参考DAPP开发者向导 3)比原链的合约交易是多输入多输出的交易结构,前端需要进行一些预判断逻辑的处理,然后再选择合适的合约交易模板结构。 4)DAPP的插件连接的是去中心化的bycoin服务器,该服务器从比原节点服务器上同步的所有区块信息和交易信息,该部分主要是在插件钱包层进行了高度的封装,用户只需按照接口调用即可。除此之外,需要开发者搭建一个缓冲服务器,不仅可以在管理合约UTXO层面做一些性能方面的处理,而且还可以为DAPP做一些数据存储。开发者可以根据实际需求来开发一些RPC请求接口,然后在前端页面设置相关条件来触发这些API的调用。 前端逻辑处理流程大致如下: 调用插件,比原的chrome插件源码位于Bytom-JS-SDK,开发比原DAPP时调用插件的说明可以参考Dapp Developer Guide,其网络配置如下: window.addEventListener('load', async function() { if (typeof window.bytom !== 'undefined') { let networks = { solonet: ... // solonet bycoin url testnet: ... // testnet bycoin url mainnet: ... // mainnet bycoin url }; ... startApp(); }); 配置合约参数,可以采用文件配置的方式,该步骤是为了让前端得到需要用到的一些已经固定化的合约参数,其前端配置文件为configure.json.js,其示例模型如下: var config = { "solonet": { ... // contract arguments "gas": 0.4 // btm fee }, "testnet":{ ... }, "mainnet":{ ... } } 前端预计算处理,如果合约中包含lock-unlock语句,并且Amount是一个数值表达式,那么前端来提取计算表达式并进行相应的预计算。此外,前端还需要预判下所有可验证的verify语句,从而判定交易是否可行,因为一旦前端对这些验证失败,合约将必然验证失败。此外,如果define或assign语句涉及的变量,前端也需预计算这些变量的值。 构建合约交易模板,由于解锁合约是解锁lock语句条件,构造交易需要根据lock语句或unlock语句来进行变换。解锁合约交易是由inputs和outputs构成,交易的第一个input输入一般都是是固定的,即合约UTXO的hash值,除此之外,其他输入输出都需要根据DAPP中的实际合约来进行变更,其模型大致如下: const input = [] input.push(spendUTXOAction(utxohash)) ... // other input const output = [] output.push(controlProgramAction(amount, asset, program)) ... // other output 启动前端服务 编译前端命令如下: npm run build 启动之前需要先启动`bufferserver`缓冲服务器,然后再启动前端服务,其前端启动命令如下: npm start DAPP缓冲服务器 缓冲服务器主要是为了在管理合约UTXO层面做一些效率方面的处理,包括了对bycoin服务器是如何同步请求的,此外对DAPP的相关交易记录也进行了存储。bycoin服务器是比原链的去中心化钱包服务器,缓冲服务器的UTXO跟它是同步更新的,比原官方插件钱包默认连接的就是该服务器。尽管bycoin服务器的也对比原链的所有UTXO进行了管理,但是由于UTXO数量比较大,如果直接在该层面处理会导致DAPP性能不佳,所以建议用户自己构建自己的缓冲服务器做进一步优化处理。此外,DAPP开发者也可以搭建了自己的去中心化钱包服务器,并且自己开发相关的插件。 缓冲服务器架构可以参考一下bufferserver案例的源代码,其编译和启动步骤如下: 编译bufferserver源代码 按照README安装部署服务需要的软件包Mysql和Redis,然后下载源代码并编译: make all 编译完成之后,在`target`目录下会生成可执行文件`api`和`updater`。 启动服务 使用root用户创建数据库和数据表,其命令如下: mysql -u root -p < database/dump.sql 修改配置文件`config_local.json`,配置说明参考[`README`](https://github.com/oysheng/bufferserver/blob/master/README.md)的`config`配置参数详解。 启动`api`和`updater`服务器,其中`api`是提供`JSON RPC`请求的服务进程,`updater`是提供同步`blockcenter`和区块链浏览器数据请求的服务进程。 ./target/api config_local.json ./target/updater config_local.json 启动缓冲服务器之后,便可以启动前端服务,然后打开`DAPP`的网页`URL`即可使用。 附:缓冲服务器的`JSON RPC`接口可以参考[`wiki`接口说明](https://github.com/oysheng/bufferserver/wiki)。 比原DAPP实例 比原DAPP实例说明,请参考储蓄分红DAPP
The API provides you with a convenient, powerful and simple way to read data from the bytom network and build your own services with it. The API allows to use the real-time analytical data about bytom blockchain transactions, addresses and assets. Below is the list of APIs, 1 request per second. Please do not exceed the limits or you'll be banned. If you have specific needs, please let us know, we are ready to help you. API Host Default api host: Network URL mainnet https://blockmeta.com/api/v2 testnet https://blockmeta.com/api/wisdom A complete request example via curl: // curl -X GET url/method curl -X GET https://blockmeta.com/api/v2/blocks API Methods address asset assets block blocks daily difficulty hash-rate miner nodes rank total transaction transactions unconfirmed-transactions utxo address Get address asset balance,receive asset amount,sent asset amount and recent transactions. Method /address/<string:address> /address/<string:address>/asset/<string:asset_id> Parameters String - address, address of account optional: String - asset_id, asset_id Integer - page, page number of address transactions Integer - limit, transaction number per page Example // Request curl -X GET https://blockmeta.com/api/v2/address/bm1qsk6dj6pym7yng0ev7wne7tm3d54ea2sjz5tyxk?limit=1&page=1 // Response { "address": "bm1qsk6dj6pym7yng0ev7wne7tm3d54ea2sjz5tyxk", "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "balance": 154263689273022, "receive": 382483033706493, "sent": 228219344433471, "join_timestamp": 1525523171, "miner": 0, "transactions": [ { "id": "7cce3bcf4fc261fd7693fffc4c71d092e97624e24529a386a49cbfdc222bbe76", "version": 1, "size": 81, "time_range": 0, "status_fail": false, "mux_id": "08514648d1ef7643f19db5f1ca4b456a6237d410001e8c75c03c6e9eb9efbc85", "height": 66586, "chain_status": "mainnet", "coinbase": 1, "detail": [ { "type": "coinbase", "asset_id": "0000000000000000000000000000000000000000000000000000000000000000", "amount": 0, "arbitrary": "003636353836", "transaction_id": "7cce3bcf4fc261fd7693fffc4c71d092e97624e24529a386a49cbfdc222bbe76", "io": 0 }, { "type": "control", "id": "f23771e954386c7c86d5a41e5bbaa03c61d8e1c4397c762143a9176b9a432ae4", "position": 0, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "amount": 41250000000, "control_program": "001485b4d96824df89343f2cf3a79f2f716d2b9eaa12", "address": "bm1qsk6dj6pym7yng0ev7wne7tm3d54ea2sjz5tyxk", "transaction_id": "7cce3bcf4fc261fd7693fffc4c71d092e97624e24529a386a49cbfdc222bbe76", "io": 1 } ] } ], "pagination": { "current": 1, "limit": 1, "total": 9532 } } asset Get asset details by asset id. Method /asset/<string:asset_id> Parameters String - asset_id, asset_id optional: Integer - page, page number of asset transactions Integer - limit, transaction number per page Example // Request curl -X GET https://blockmeta.com/api/v2/asset/04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141?page=1&limit=1 // Response { "issuance_program": "ae20dd98f21845f4e7c76224fd870a47ab9425210075ed4281ecd6ed1085f25b4c7a5151ad", "total_amount": 1000000000000, "decimals": 8, "description": "", "name": "", "symbol": "", "issue_timestamp": 1527315522, "asset_id": "04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141", "transactions": [ { "id": "7f706976ca9f85bdf9dabdfa1f9e2cfb3364795f90f58b8f644deb8b00246399", "version": 1, "size": 595, "time_range": 0, "status_fail": false, "mux_id": "8e495d3b10756fccf37d3107f148ad18b4ecaa42a1410b3183c3d4360cace985", "height": 37425, "chain_status": "mainnet", "coinbase": 0, "detail": [ { "type": "spend", "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "amount": 668600000, "control_program": "0014242b44609cb269ac51c9cb00913b3107b382ded9", "address": "bm1qys45gcyukf56c5wfevqfzwe3q7ec9hkej65lfd", "spent_output_id": "b8ff07e2e174ae3f94537604a64ad9b98b5fa61de4e476c7230aacbf75ebb7b5", "transaction_id": "7f706976ca9f85bdf9dabdfa1f9e2cfb3364795f90f58b8f644deb8b00246399", "io": 0 }, { "type": "spend", "asset_id": "04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141", "amount": 997700000000, "control_program": "0014b276cf1001cbb171d63620915d9f558348db6dad", "address": "bm1qkfmv7yqpewchr43kyzg4m864sdydkmddp5mfxf", "spent_output_id": "839719c68fbe8a7f780663e713805a2c581b4943ba24b8653b7d35a00a273d55", "transaction_id": "7f706976ca9f85bdf9dabdfa1f9e2cfb3364795f90f58b8f644deb8b00246399", "io": 0 }, { "type": "control", "id": "8a55da87a17f0fa5c132a53e5c11e62c3254b7dfa09deed71c5b579b62ba20e5", "position": 0, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "amount": 667700000, "control_program": "001410d780309dfb4aefda4e49bbf1a01881c465fe9e", "address": "bm1qzrtcqvyald9wlkjwfxalrgqcs8zxtl57tea5cw", "transaction_id": "7f706976ca9f85bdf9dabdfa1f9e2cfb3364795f90f58b8f644deb8b00246399", "io": 1 }, { "type": "control", "id": "7264906e06e4709817c230955bc8786be2a6e5a69cc223780e70681f7ea96fd3", "position": 1, "asset_id": "04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141", "amount": 997200000000, "control_program": "0014b8b64446c452d3e377ba62ab3f0c8650002a20eb", "address": "bm1qhzmyg3ky2tf7xaa6v24n7ryx2qqz5g8ty4jcrc", "transaction_id": "7f706976ca9f85bdf9dabdfa1f9e2cfb3364795f90f58b8f644deb8b00246399", "io": 1 }, { "type": "control", "id": "049c4fb1367f6675645f523cc943a6a6c41b069072d60711f088b92061f2fa4b", "position": 2, "asset_id": "04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141", "amount": 500000000, "control_program": "0014c1dcaedc48e0b3c029485d509a0fca157e987f31", "address": "bm1qc8w2ahzguzeuq22gt4gf5r72z4lfsle3ltk8d2", "transaction_id": "7f706976ca9f85bdf9dabdfa1f9e2cfb3364795f90f58b8f644deb8b00246399", "io": 1 } ] } ], "pagination": { "current": 1, "limit": 1, "total": 9 } } assets Get assets issued on bytom network Method /assets Parameters optional: Integer - page, page number of assets Integer - limit, asset number per page Example // Request curl -X GET https://blockmeta.com/api/v2/assets?page=3&limit=10 // Response { "assets": [ { "issuance_program": "ae2067efe306cb96f7f481bd88c6325a77900d32b276e64a8a576c06f5f7bf6988e95151ad", "total_amount": 210000000000000000, "decimals": 8, "description": "Bytom Official Issue", "name": "BTM", "symbol": "BTM", "s_timestamp": 1533010776.991276, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" }, { "issuance_program": "ae20237e66a9ef0b181c5b15ef6fc74e2f331c83aa6cb5606315f34eb81817c489c45151ad", "total_amount": 8400000000000000, "decimals": 8, "description": "", "name": "", "symbol": "", "issue_timestamp": 1533021975, "s_timestamp": 1533022247.9164958, "asset_id": "ce0ba852a65dfa445548e3a66f78aec1fd3011f74b7268ed0e0598928706ae80" }, { "issuance_program": "ae204f92fc3eb7c82cd27471966a098187583ce6d6bd02264c04e5d148df35fe8eed5151ad", "total_amount": 1000000000000000000, "decimals": 8, "description": "", "name": "", "symbol": "", "issue_timestamp": 1533092470, "s_timestamp": 1533092546.611149, "asset_id": "d5e348366a1c341dc60759e0de102bded16a3565c6038ffbe846a54073750e68" } ], "pagination": { "current": 3, "limit": 10, "total": 3 } } block Get block by block height or block hash Method /block/<int:height> /block/<string:hash> Parameters Integer - height, block height String - hash, block hash Example // Request curl -X GET https://blockmeta.com/api/v2/block/58680 curl -X GET https://blockmeta.com/api/v2/block/844e8d97fa66980f603f5a8f626dc610d3891058e056e0aa13797196fe704754 // Response { "hash": "844e8d97fa66980f603f5a8f626dc610d3891058e056e0aa13797196fe704754", "size": 414, "version": 1, "height": 58680, "previous_block_hash": "359912beb873c9526ff223393e2b84ce0100d7a47d90463fa036e0e73177d0d8", "timestamp": 1531984680, "nonce": 229687365138347316, "bits": 2017612633070008956, "difficulty": "8977184039", "transaction_merkle_root": "1f3f8260d63899be7e92fa33e6e4a60eb0acca08885abc3672c993c29cd9ed56", "transaction_status_hash": "c9c377e5192668bc0a367e4a4764f11e7c725ecced1d7b6a492974fab1b6d5bc", "hash_rate": 280537001, "transaction_count": 1, "chain_status": "mainnet", "transactions": [ { "id": "a641f07626381617303dedc3fa17a797982f30fc2e49a9abe98193b5aa5d743a", "version": 1, "size": 81, "time_range": 0, "status_fail": false, "mux_id": "3ea24fade77a8dee0110bfc8a90397344d31aa7de67d88e60641294c29e4c4bc", "height": 58680, "chain_status": "mainnet", "coinbase": 1, "detail": [ { "type": "coinbase", "asset_id": "0000000000000000000000000000000000000000000000000000000000000000", "amount": 0, "arbitrary": "003538363830", "transaction_id": "a641f07626381617303dedc3fa17a797982f30fc2e49a9abe98193b5aa5d743a", "io": 0 }, { "type": "control", "id": "f0937105fa1b60527d9ca475a2780e57e4e9708342ff2dcc963be52cd7ce08e8", "position": 0, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "amount": 41250000000, "control_program": "0014c190f77b1e7adadae97e69a6a7d7762649c6e04f", "address": "bm1qcxg0w7c70tdd46t7dxn204mkyeyudcz063s49e", "transaction_id": "a641f07626381617303dedc3fa17a797982f30fc2e49a9abe98193b5aa5d743a", "io": 1 } ] } ] } blocks Get latest blocks Method /blocks Parameters optional: Integer - page, page number of blocks Integer - limit, block number per page Example // Request curl -X GET https://blockmeta.com/api/v2/blocks?page=1&limit=10 // Response { "blocks": [ { "hash": "3715fc97b05258e95fa94020c76a827c901b8837fb16b86f3468781e70ca9d62", "size": 30552, "version": 1, "height": 66692, "previous_block_hash": "3350cd6f58095d1745d8e95588cab3157652bdcbe1d0c38ab5138b07b7aa07e1", "timestamp": 1533123349, "nonce": 4613934258141154516, "bits": 2017612633068329013, "difficulty": "11353365045", "transaction_merkle_root": "4e21072695092af28edfeedd96599e00999eddcb640b264f8d448e3289612a08", "transaction_status_hash": "cc25f1c0862b2b31d43d4dfb8e2a6b4e4c7e4a3ec18127494e349747e327d336", "hash_rate": 241560958, "transaction_count": 46, "chain_status": "mainnet" }, { "hash": "3350cd6f58095d1745d8e95588cab3157652bdcbe1d0c38ab5138b07b7aa07e1", "size": 3090, "version": 1, "height": 66691, "previous_block_hash": "ba46306d4a6b5d1fe9054d034fb65756b9ef3e89c6bbfa6917c670d6f47d42bd", "timestamp": 1533123302, "nonce": 89640530873195, "bits": 2017612633068329013, "difficulty": "11353365045", "transaction_merkle_root": "137ee94fe7a30cd0882e1b2e7172f4ad7f7722f7c4cf90be647702e897b79c1d", "transaction_status_hash": "ca8e7c022f80a5d5dbc02b8a73234c9ab9069fa0db0f235b2fb8ccd1d6036069", "hash_rate": 55113422, "transaction_count": 5, "chain_status": "mainnet" }, { "hash": "ba46306d4a6b5d1fe9054d034fb65756b9ef3e89c6bbfa6917c670d6f47d42bd", "size": 14476, "version": 1, "height": 66690, "previous_block_hash": "f49f8eb7c99a6450f58d151cd925fa98fd999783a21019e2df94c85e25dac8f1", "timestamp": 1533123096, "nonce": 3602968342337572314, "bits": 2017612633068329013, "difficulty": "11353365045", "transaction_merkle_root": "8b25a36e78568c326d5eb4d2e867e2469da4479fa5ea31ab83c3e57abcd4cb6e", "transaction_status_hash": "aa210fec0e249e9121e51546c9c9c3e3e3515ceb8e065511a64b218a442dc34e", "hash_rate": 103212409, "transaction_count": 22, "chain_status": "mainnet" }, { "hash": "f49f8eb7c99a6450f58d151cd925fa98fd999783a21019e2df94c85e25dac8f1", "size": 1754, "version": 1, "height": 66689, "previous_block_hash": "40b0f92cfeb3399bf0bc1f2ca2928251cb45448cf55bf7491952442dca276db7", "timestamp": 1533122986, "nonce": 4323828896408748377, "bits": 2017612633068329013, "difficulty": "11353365045", "transaction_merkle_root": "58b875a9273d86b233ce1312a7cbbc81db3f801127ef2a14936917fe06e022e2", "transaction_status_hash": "a4489d66751139d2d3f120b2dacf4a8c52e6cadd7de603dc8ef1c66c350cba74", "hash_rate": 810954646, "transaction_count": 3, "chain_status": "mainnet" }, { "hash": "40b0f92cfeb3399bf0bc1f2ca2928251cb45448cf55bf7491952442dca276db7", "size": 32558, "version": 1, "height": 66688, "previous_block_hash": "3ff5d46b3534c38cc70728143565ae921826446d4194c6fd50e0e517a3af6eb0", "timestamp": 1533122972, "nonce": 91358249423491, "bits": 2017612633068329013, "difficulty": "11353365045", "transaction_merkle_root": "b676607fd5b141bc1bbd23ee08eace3c54278aad20845af95ac0f2e469455b4b", "transaction_status_hash": "7f8b345fd77260927f389ecb461255d7aa0b16a92bb943f6575b6406cff28051", "hash_rate": 33003968, "transaction_count": 49, "chain_status": "mainnet" }, { "hash": "3ff5d46b3534c38cc70728143565ae921826446d4194c6fd50e0e517a3af6eb0", "size": 9124, "version": 1, "height": 66687, "previous_block_hash": "42c6d38a72136b818e08b5e4ac67224028c873526d4dd81ac132dc0caae372e4", "timestamp": 1533122628, "nonce": 74031462222925425, "bits": 2017612633068329013, "difficulty": "11353365045", "transaction_merkle_root": "35acfb92df484b57a4e362486c0bb241c95ad9e8880ee4dff96ed86bf7405d84", "transaction_status_hash": "06208aecbc8ebbba0fe1c6c8ecaed7898d4e9fc567af6289c242cf34dae958af", "hash_rate": 157685625, "transaction_count": 14, "chain_status": "mainnet" }, { "hash": "42c6d38a72136b818e08b5e4ac67224028c873526d4dd81ac132dc0caae372e4", "size": 1754, "version": 1, "height": 66686, "previous_block_hash": "d6a78e1f6b5ce84b042922e12efec2a827928735cbb9b3536154a7ac8104a5ed", "timestamp": 1533122556, "nonce": 91083873957844322, "bits": 2017612633068329013, "difficulty": "11353365045", "transaction_merkle_root": "7d075cbb8b797313ac90dc4aea9be649dc6504a5d3406c9b81806c455b2a55b2", "transaction_status_hash": "a4489d66751139d2d3f120b2dacf4a8c52e6cadd7de603dc8ef1c66c350cba74", "hash_rate": 202738661, "transaction_count": 3, "chain_status": "mainnet" }, { "hash": "d6a78e1f6b5ce84b042922e12efec2a827928735cbb9b3536154a7ac8104a5ed", "size": 39932, "version": 1, "height": 66685, "previous_block_hash": "c0b1eb3ef3e90e15acfaba57b6250c30bd5b1f564b7f9765787b670378a35bdc", "timestamp": 1533122500, "nonce": 4194339850092046483, "bits": 2017612633068329013, "difficulty": "11353365045", "transaction_merkle_root": "651a0b0ca5dd4a1ebe56324f953ef0ecb99a63e08f526ba7e288e19186b7184b", "transaction_status_hash": "a6b4433cd4cc98934a0334509aff9d2dea0ba685cbffb0db4c03eaf693a1a945", "hash_rate": 13049844, "transaction_count": 60, "chain_status": "mainnet" }, { "hash": "c0b1eb3ef3e90e15acfaba57b6250c30bd5b1f564b7f9765787b670378a35bdc", "size": 1754, "version": 1, "height": 66684, "previous_block_hash": "03266dfef52a070aa530eb0fd2888125283301c37f9645abdd8eeae32d96044f", "timestamp": 1533121630, "nonce": 4770121628526893245, "bits": 2017612633068329013, "difficulty": "11353365045", "transaction_merkle_root": "f7dee684af790dd4206de4c119087e5df3599226cae65f17d96bbae0092838e1", "transaction_status_hash": "a4489d66751139d2d3f120b2dacf4a8c52e6cadd7de603dc8ef1c66c350cba74", "hash_rate": 1032124095, "transaction_count": 3, "chain_status": "mainnet" }, { "hash": "03266dfef52a070aa530eb0fd2888125283301c37f9645abdd8eeae32d96044f", "size": 5774, "version": 1, "height": 66683, "previous_block_hash": "827da542f0da085ca66a2e83df2d53f20f2d906e5e3b1f2a4720327cb83039c7", "timestamp": 1533121619, "nonce": 76625012584420006, "bits": 2017612633068329013, "difficulty": "11353365045", "transaction_merkle_root": "baa16a745a24d4b3cb2ae2244f93630f6249d01a226050e985da218ec37e7165", "transaction_status_hash": "3c1af31e7207e35fb156b258d53fabb0a3d3dab4dda3080d396494fb188c340c", "hash_rate": 283834126, "transaction_count": 9, "chain_status": "mainnet" } ], "pagination": { "current": 1, "limit": 10, "total": 6670 } } daily Get daily statistic of bytom network Method /stat/daily Parameters optional: Integer - from, start timestamp of statistic Integer - to, end timestamp of statistic Example // Request curl -X GET https://blockmeta.com/api/v2/stat/daily // Response [ { "date": "2018-08-01", "mainnet_block_count": 377, "orphan_block_count": 1, "transaction_count": 488, "transaction_amount": 1261058177984834, "transaction_fee": 1067204216, "transaction_gas": 5336021.08, "new_address_count": 102, "new_asset_count": 1 } ] difficulty Get difficulty statistic of bytom network Method /stat/difficulty Parameters optional: Integer - from, start timestamp of statistic Integer - to, end timestamp of statistic Example // Request curl -X GET https://blockmeta.com/api/v2/stat/difficulty?from=1530874300&to=15312384200 // Response [ { "change_time": "2018-07-08 04:45:51", "difficulty": 10452505013, "change_rate": "14.3%" }, { "change_time": "2018-07-12 04:00:51", "difficulty": 9217527092, "change_rate": "-11.82%" }, { "change_time": "2018-07-15 20:49:28", "difficulty": 8721820207, "change_rate": "-5.38%" }, { "change_time": "2018-07-19 06:28:40", "difficulty": 8977184039, "change_rate": "2.93%" }, { "change_time": "2018-07-22 19:20:11", "difficulty": 8883415522, "change_rate": "-1.04%" }, { "change_time": "2018-07-26 03:21:49", "difficulty": 9322699206, "change_rate": "4.94%" }, { "change_time": "2018-07-29 02:45:08", "difficulty": 10964255885, "change_rate": "17.61%" }, { "change_time": "2018-08-01 11:53:14", "difficulty": 11353365045, "change_rate": "3.55%" } ] hash-rate Get hash rate statistic of bytom network Method /stat/hash-rate Parameters optional: Integer - from, start timestamp of statistic Integer - to, end timestamp of statistic Example // Request curl -X GET https://blockmeta.com/api/v2/stat/difficulty?from=1530874300&to=15312384200 // Response [ [ 1533052800, 63422994.50333333 ], [ 1533056400, 61562406.08166666 ], [ 1533060000, 71880656.02 ], [ 1533063600, 75958760.63583334 ], [ 1533067200, 93855763.4063889 ], [ 1533070800, 59036902.51777778 ], [ 1533074400, 78712127.97944444 ], [ 1533078000, 53553248.29472222 ], [ 1533081600, 90436467.12472223 ], [ 1533085200, 86353272.05444445 ], [ 1533088800, 75134306.41916667 ], [ 1533092400, 86312727.75277779 ], [ 1533096000, 65323902.23888889 ], [ 1533099600, 72487528.42305556 ], [ 1533103200, 56278212.83 ], [ 1533106800, 67019420.557222225 ], [ 1533110400, 66181084.608333334 ], [ 1533114000, 51030506.395833336 ], [ 1533117600, 85207861.46222222 ] ] miner Get miner statistic of bytom network Method /stat/miner Parameters optional: Integer - from, start timestamp of statistic Integer - to, end timestamp of statistic Example // Request curl -X GET https://blockmeta.com/api/v2/stat/miner?from=1533052800&to=1533139200 // Response [ { "address": "bm1q3yt265592czgh96r0uz63ta8fq40uzu5a8c2h0", "name": "鱼池", "mine_block_count": 198, "percent": "49.75%" }, { "address": "bm1qcxg0w7c70tdd46t7dxn204mkyeyudcz063s49e", "name": "双U", "mine_block_count": 79, "percent": "19.85%" }, { "address": "bm1qp92dpx6c69zrz9gwyde8glpww29yjlu7lxyf2y", "name": "国池", "mine_block_count": 40, "percent": "10.05%" }, { "address": "bm1qsk6dj6pym7yng0ev7wne7tm3d54ea2sjz5tyxk", "name": "未知矿池1号", "mine_block_count": 30, "percent": "7.54%" }, { "address": "bm1qap4qk88n6sk388lfy55s9z8antz5avp5z3spqp", "name": "蜘蛛", "mine_block_count": 25, "percent": "6.28%" }, { "address": "bm1q2lm5c7ajtutcjzhr4cvuge385ygynupe90m7xj", "name": "蚂蚁", "mine_block_count": 12, "percent": "3.02%" }, { "address": "bm1q3jymk8ruwslvx568qh98y2u7wuqhaz8gf3pt3q", "name": "未知矿池", "mine_block_count": 7, "percent": "1.76%" }, { "address": "bm1qxlvtg7078znef59g039h9f5k40atz7j05s7y3q", "name": "未知矿池", "mine_block_count": 6, "percent": "1.51%" }, { "address": "bm1qrwhwspf4mva328xtaeed9fjmgj2u8mqywv887z", "name": "蜜蜂", "mine_block_count": 1, "percent": "0.25%" } ] nodes Get information of all bytom nodes.country include cn,sg,jp,es,de,us,kr,ca,ru,uk Method /nodes Parameters optional: Integer - page, page number of data, page >= 1 Integer - limit, number of data per page, max limit is 200 String - country, country the node is located Example // Request curl -X GET https://blockmeta.com/api/v2/nodes?page=1&limit=2&country=cn // Response { "nodes": [ { "address": "47.101.167.203:46657", "status": "active", "height": 243658, "status_time": "2019-06-03T09:03:01Z", "rtt": 50359363, "network": "mainnet", "version": "1.0.9+00f77625", "is_seed": false, "coordinate": { "longitude": 120.1614, "latitude": 30.2936 }, "country": "China", "symbol": "cn", "name": "iczc" }, { "address": "193.112.67.165:46657", "status": "busy", "height": 243656, "status_time": "2019-06-03T09:01:59Z", "rtt": 51971962, "network": "mainnet", "version": "1.0.8+56443ac4", "is_seed": false, "coordinate": { "longitude": 116.3883, "latitude": 39.9289 }, "country": "China", "symbol": "cn", "name": "EONE" } ], "pagination": { "current": 1, "limit": 2, "total": 37 } } rank Get asset balance rankings of address Method /rank/<string:asset_id> Parameters optional: String - asset_id, asset_id Integer - page, page number of rankings Integer - limit, address number per page Example // Request https://blockmeta.com/api/v2/rank/04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141?page=1&limit=10 // Response { "address": [ { "address": "bm1qhzmyg3ky2tf7xaa6v24n7ryx2qqz5g8ty4jcrc", "asset_id": "04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141", "balance": 997200000000, "receive": 997200000000, "sent": 0, "join_timestamp": 1528890869 }, { "address": "bm1qc8w2ahzguzeuq22gt4gf5r72z4lfsle3ltk8d2", "asset_id": "04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141", "balance": 1500000000, "receive": 2500000000, "sent": 1000000000, "join_timestamp": 1528888504 }, { "address": "bm1qlf9cfmn9alm6zjljeaag35h80y4ha2adra7rcz", "asset_id": "04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141", "balance": 1190000000, "receive": 1190000000, "sent": 0, "join_timestamp": 1528890326 }, { "address": "bm1qx09067ax6t9t3vekk0uat78ajxhep7tkcezqve", "asset_id": "04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141", "balance": 100000000, "receive": 100000000, "sent": 0, "join_timestamp": 1528890326 }, { "address": "bm1qcrz3rnme38v6awccca2l6mmk3dsu5fhafu6v4q", "asset_id": "04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141", "balance": 10000000, "receive": 1300000000, "sent": 1290000000, "join_timestamp": 1528888504 }, { "address": "bm1qd83zunqeq5cfml6g540jlxfpe073tnqm4w5z8n", "asset_id": "04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141", "balance": 0, "receive": 1000000000000, "sent": 1000000000000, "join_timestamp": 1527315522 }, { "address": "bm1qjwdx4dcda3vendajtgrjpx6gxmvmagq52t8ag5", "asset_id": "04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141", "balance": 0, "receive": 1000000000000, "sent": 1000000000000, "join_timestamp": 1527317202 }, { "address": "bm1q406g9qk6ne7alanztt27q7snhte8h4kx2w8w4p", "asset_id": "04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141", "balance": 0, "receive": 999900000000, "sent": 999900000000, "join_timestamp": 1528885266 }, { "address": "bm1qv5wa2xsaaq02ardxty4sp5pwj26nx4j5eu9l9x", "asset_id": "04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141", "balance": 0, "receive": 100000000, "sent": 100000000, "join_timestamp": 1528885266 }, { "address": "bm1qkfmv7yqpewchr43kyzg4m864sdydkmddp5mfxf", "asset_id": "04356941e62729a4099a45c5e9c1945545d4023da08bf270ab726d4162165141", "balance": 0, "receive": 997700000000, "sent": 997700000000, "join_timestamp": 1528889587 } ], "pagination": { "current": 1, "limit": 10, "total": 2 } } total Get total statistic of bytom network Method /stat/total Parameters optional: Integer - from, start timestamp of statistic Integer - to, end timestamp of statistic Example // Request https://blockmeta.com/api/v2/stat/total // Response { "mainnet_block_count": 66577, "orphan_block_count": 3, "transaction_count": 217540, "transaction_amount": 2201666087354342581, "transaction_fee": 366284534845, "transaction_gas": 1831422674.225, "new_address_count": 165602, "new_asset_count": 22 } transaction Get transaction by transaction_id Method /transaction/<string:transaction_id> Parameters String - transaction_id, transaction id Example // Request https://blockmeta.com/api/v2/transaction/88def07e14825e78d552245846fd2b88eb109049b7b0185a6ab893f2eae1eead // Response { "id": "88def07e14825e78d552245846fd2b88eb109049b7b0185a6ab893f2eae1eead", "version": 1, "size": 78, "time_range": 0, "status_fail": false, "mux_id": "bae7a52e33bc64d0bd5c7103fb200ed80a263386f3a5b28aaa7556505dd4b140", "height": 12579, "chain_status": "mainnet", "coinbase": 1, "detail": [ { "type": "coinbase", "asset_id": "0000000000000000000000000000000000000000000000000000000000000000", "amount": 0, "arbitrary": "e384a3", "transaction_id": "88def07e14825e78d552245846fd2b88eb109049b7b0185a6ab893f2eae1eead", "io": 0 }, { "type": "control", "id": "55946ee277e75e945bdf619bbcf13fafc19f992d2ff7a252ddedd90582b307c5", "position": 0, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "amount": 41250000000, "control_program": "0014c190f77b1e7adadae97e69a6a7d7762649c6e04f", "address": "bm1qcxg0w7c70tdd46t7dxn204mkyeyudcz063s49e", "transaction_id": "88def07e14825e78d552245846fd2b88eb109049b7b0185a6ab893f2eae1eead", "io": 1 } ] } transactions Get latest transactions Method /transactions Parameters Integer - page, page number of data, page >= 1 Integer - limit, number of date per page, max limit is 200 Example // Request https://blockmeta.com/api/v2/transactions?page=1&limit=2 // Response { "transactions": [ { "id": "49362f0fe882d6f2f0497550269f21c2b138706cc7192a37e50d9a598548044b", "version": 1, "size": 82, "time_range": 0, "status_fail": false, "mux_id": "a3b111a5cb2915c8a3e81b8e5a2a121e14ced098ee52a11408da974a5f9027e4", "height": 243653, "timestamp": 1559551478, "chain_status": "mainnet", "coinbase": 1, "fee": 0, "transaction_amount": 41258000000, "confirmations": 1, "details": [ { "type": "coinbase", "asset_id": "0000000000000000000000000000000000000000000000000000000000000000", "amount": 0, "arbitrary": "00323433363533", "input_id": "d45562151da84c46f45123fb1cc81084848324bcea6473c2a90119a33e3b8301", "transaction_id": "49362f0fe882d6f2f0497550269f21c2b138706cc7192a37e50d9a598548044b", "status_fail": false, "io": 0 }, { "type": "control", "id": "bbfeb44d07f338baaf65f54701a4a31f989cab4babde8b62c4a11082b8228830", "position": 0, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "amount": 41258000000, "control_program": "00148916ad528556048b97437f05a8afa7482afe0b94", "address": "bm1q3yt265592czgh96r0uz63ta8fq40uzu5a8c2h0", "transaction_id": "49362f0fe882d6f2f0497550269f21c2b138706cc7192a37e50d9a598548044b", "status_fail": false, "io": 1, "decode_program": [ "DUP", "HASH160", "DATA_20 8916ad528556048b97437f05a8afa7482afe0b94", "EQUALVERIFY", "TXSIGHASH", "SWAP", "CHECKSIG" ], "asset_name": "BTM", "asset_decimals": "8" } ] }, { "id": "b8f38f5d5e2ee27a5b041bb61faafec5c507aaa3fc92b731c3c9b6b61e6aaf80", "version": 1, "size": 930, "time_range": 1559551455, "status_fail": false, "mux_id": "c6b8916d63e146e2cccc3477a2f7d716f15d5fd498c5f77160c50a2b957e4996", "height": 243653, "timestamp": 1559551478, "chain_status": "mainnet", "coinbase": 0, "fee": 8000000, "transaction_amount": 50459230073, "confirmations": 1, "details": [ { "type": "spend", "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "amount": 27896288081, "control_program": "0014d9a283348ff2ee94905b0011e07a131dc6e7c20a", "address": "bm1qmx3gxdy07thffyzmqqg7q7snrhrw0ss288flcu", "spent_output_id": "51df8e3b88360ec431ce2aed9d190e080625a28d9782e15516f0ccae0a6f8e81", "input_id": "e77366f5702a798538cd6fe1f021b0c800a74f2f3e0d1d169cd51c0eca5abe5d", "witness_arguments": [ "bf4a56a6320fa973649a34bcbf115c862a5430e71484c596671c465d00b67a2281d6bee55115015aabc2e081c13f3469d80ecb56de60468c67e0619b7f3a3c08", "8167cfa7199eaa405d8b06f0e39364d2e5ed76b089fd1c86128b942b48fce0cd" ], "transaction_id": "b8f38f5d5e2ee27a5b041bb61faafec5c507aaa3fc92b731c3c9b6b61e6aaf80", "status_fail": false, "io": 0, "decode_program": [ "DUP", "HASH160", "DATA_20 d9a283348ff2ee94905b0011e07a131dc6e7c20a", "EQUALVERIFY", "TXSIGHASH", "SWAP", "CHECKSIG" ], "asset_name": "BTM", "asset_decimals": "8" }, { "type": "spend", "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "amount": 12213562353, "control_program": "001498f4f39d15d0da7a74b0a9790e2a1ec68d509204", "address": "bm1qnr6088g46rd85a9s49usu2s7c6x4pysyfft9gv", "spent_output_id": "bf8777b87b1793c16267a9ddeb660fe738157897a0c2f23cc528191a9343a8c5", "input_id": "7ad066082839a732e6651d91cd4ac048025a6bc485ad8be1e4542cf28c2c479f", "witness_arguments": [ "b9c4e0748d1e4d4d6ea0718a522a2c075373bb430e0e1fe32b17130043146c0d77b2bc79745c068bad4d2a4f0d22563706f39ab4f8d61d8cc0b703ce8e68020b", "5acf8e75a59bad604a4a757356b1fdf5468fa3d54a07c68b69640d6a24d3200f" ], "transaction_id": "b8f38f5d5e2ee27a5b041bb61faafec5c507aaa3fc92b731c3c9b6b61e6aaf80", "status_fail": false, "io": 0, "decode_program": [ "DUP", "HASH160", "DATA_20 98f4f39d15d0da7a74b0a9790e2a1ec68d509204", "EQUALVERIFY", "TXSIGHASH", "SWAP", "CHECKSIG" ], "asset_name": "BTM", "asset_decimals": "8" }, { "type": "spend", "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "amount": 7399161717, "control_program": "00143dbce80e2110753f5ee4b2ee018ad91bd3b5c36e", "address": "bm1q8k7wsr3pzp6n7hhykthqrzker0fmtsmwwqjsx2", "spent_output_id": "eec00f99f435ecda5f59d319086cb63f41aeeed54f3dbe32087e410f0def0ec5", "input_id": "5273b33e4c87f749c017e65e308b325aaa4e8223f3ce794496675b61e0c7be9c", "witness_arguments": [ "e6b5594eb6e083fc7e130ffdfa8f326dfaed439cca9b040e5b22b80f4b02e07c107ed6ab762e27efd48340f317608f64f3cb35ba9d5ae02a937889db6567550a", "c628b33e97881eb3c3916f2c5a992fa8fee476021566386faa8d17a29fbad853" ], "transaction_id": "b8f38f5d5e2ee27a5b041bb61faafec5c507aaa3fc92b731c3c9b6b61e6aaf80", "status_fail": false, "io": 0, "decode_program": [ "DUP", "HASH160", "DATA_20 3dbce80e2110753f5ee4b2ee018ad91bd3b5c36e", "EQUALVERIFY", "TXSIGHASH", "SWAP", "CHECKSIG" ], "asset_name": "BTM", "asset_decimals": "8" }, { "type": "spend", "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "amount": 2958217922, "control_program": "0014b7c13ae88a97100b537cfc15f37852cfe3cd9c53", "address": "bm1qklqn46y2jugqk5muls2lx7zjel3um8znhfrvsr", "spent_output_id": "799b4efcc17700a20bfc60a6ed7169539849e29336ca9c33fd525f5f2fa343f8", "input_id": "5af25fee3cb0c8052f4a21f6d06dc6e16b28d49ffc03743be2f656ad132166aa", "witness_arguments": [ "483ae2e780258d63c0913479c9c37bf20d00130f54e74afd641b2d4b61a00a47bae2aef0195f02aee65d14cc3ecb6f779290ac90b13a0fc3cc714d9deeead90c", "a3297b8e42d58404a34662e71389cc816b863aaaec1ce1375ccb2f35707d1a00" ], "transaction_id": "b8f38f5d5e2ee27a5b041bb61faafec5c507aaa3fc92b731c3c9b6b61e6aaf80", "status_fail": false, "io": 0, "decode_program": [ "DUP", "HASH160", "DATA_20 b7c13ae88a97100b537cfc15f37852cfe3cd9c53", "EQUALVERIFY", "TXSIGHASH", "SWAP", "CHECKSIG" ], "asset_name": "BTM", "asset_decimals": "8" }, { "type": "control", "id": "da54a32deb7d37723086ce23b75687ad95722eff32075cc80fbc6542df9f2b09", "position": 0, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "amount": 859230073, "control_program": "0014c7ba9f5a385aebb29bf6973ab2f009410837e8c9", "address": "bm1qc7af7k3ctt4m9xlkjuat9uqfgyyr06xfpnagsv", "transaction_id": "b8f38f5d5e2ee27a5b041bb61faafec5c507aaa3fc92b731c3c9b6b61e6aaf80", "status_fail": false, "io": 1, "decode_program": [ "DUP", "HASH160", "DATA_20 c7ba9f5a385aebb29bf6973ab2f009410837e8c9", "EQUALVERIFY", "TXSIGHASH", "SWAP", "CHECKSIG" ], "asset_name": "BTM", "asset_decimals": "8" }, { "type": "control", "id": "0e5823d2b3b2bed2125344e4d1b0bfea5e7bb77d648221aa8fb652c496c6d8fc", "position": 1, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "amount": 49600000000, "control_program": "0014800b6d9b785834179d5e47e5b58818a051801409", "address": "bm1qsq9kmxmctq6p0827gljmtzqc5pgcq9qfp4qr40", "transaction_id": "b8f38f5d5e2ee27a5b041bb61faafec5c507aaa3fc92b731c3c9b6b61e6aaf80", "status_fail": false, "io": 1, "decode_program": [ "DUP", "HASH160", "DATA_20 800b6d9b785834179d5e47e5b58818a051801409", "EQUALVERIFY", "TXSIGHASH", "SWAP", "CHECKSIG" ], "asset_name": "BTM", "asset_decimals": "8" } ] } ], "pagination": { "current": 1, "limit": 2, "total": 535457 } } unconfirmed-transactions Get unconfirmed transactions from bytom node memory pool Method /unconfirmed-transactions Example // Request https://blockmeta.com/api/v2/unconfirmed-transactions // Response [ { "id": "e1a8f056f69f02be63a82374c3928ae2a49bf4c4e6a8f2ad7d231c1737121070", "version": 1, "size": 611, "time_range": 0, "status_fail": false, "mux_id": "507ab87ae0e6ae078275055d062b3c902e8bf0ac7a1407429778cbd439bc82ad", "fee": 1000000, "transaction_amount": 3546000000 }, { "id": "738446630c5c7801b39dcced868e5e3f692f68791d85b543c6cf227e53f1082f", "version": 1, "size": 332, "time_range": 260933, "status_fail": false, "mux_id": "6cb8e7c87ae20e09f773036c3bb9f1db0e4116055559d3d4b3f731d010320c3c", "fee": 449000, "transaction_amount": 3800000000 }, { "id": "52cd5f58b3e7990a693dade6d5aaf6343bc2b970132426edebf8238b34334403", "version": 1, "size": 468, "time_range": 0, "status_fail": false, "mux_id": "67f8abe784022969c1dad75a89c691ba9d7ccf5e3f446c455ed963769f65b440", "fee": 1200000, "transaction_amount": 0 }, { "id": "abed223d45c9eab5789174064746617132189672d83b04fefb53bf17986d65ea", "version": 1, "size": 339, "time_range": 1559551685, "status_fail": false, "mux_id": "36e4cdf903141e9cd2251a6bc18f05aeef75c23823842c0360c2746554362c43", "fee": 8000000, "transaction_amount": 5941788000000 } ] utxo Get assets utxo statistic of bytom network Method /stat/utxo Parameters optional: Integer - from, start timestamp of statistic Integer - to, end timestamp of statistic Example // Request https://blockmeta.com/api/v2/stat/utxo // Response [ { "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "utxo_count": 5508 }, { "asset_id": "d5e348366a1c341dc60759e0de102bded16a3565c6038ffbe846a54073750e68", "utxo_count": 3 } ]
共识算法是分布式系统保证节点数据状态一致性的方法,在区块链的共识算法分POW(工作量证明)和POS(权益证明)两大类。第一类POW模式是在公链项目中运用的最广泛应用的共识算法,比特币长达10年的运行已充分证明POW的安全性与稳定性。POW的特性是将去中心化与安全性发挥到了极致,但却牺牲了性能。 如比特币的峰值TPS为3.87, 平均每笔交易被打包入块需要10分钟;比原链的峰值TPS为36.32,平均每笔交易被打包入块需要2.5分钟。第二类的POS模式是由通过算法来选择出块共识节点,多用于联盟链和一些追求高TPS的新公链项目中。POS的特性是通过支持更小的出块间隔来达到最优的性能,但却牺牲了部分的安全性与去中心化。 Bystack是一个基于主侧链架构的区块链BaaS平台,将区块链分为Layer1和Layer2两层。 Layer1既比原链的主链,由POW算法保证最高级别的资产安全与去中心化。Layer1的TPS问题则通过跨链技术将资产转移到Layer2上来解决. 侧链(既Layer2)使用创新的BBFT共识算法使单条侧链的TPS达到20000以上,多条侧链配合可使TPS线性增长。 在未达到节点带宽与性能瓶颈的前提下,TPS = 区块交易数 *每秒确认的区块数。由于区块可以容纳的最大交易数可以通过简单的修改代码参数实现,所以提高每秒确认的区块数就成了提高TPS的关键方式。如比原链的每个区块最大可容纳5500笔左右的交易,在主链上因为平均每150秒出一个块的POW特性所以TPS是36.32.但上在侧链如将每秒进入最终确认的区块数提高到5个则可轻易的将TPS达到25000以上。 DPOS的问题 传统的DPOS共识算法如EOS已经完全可以做到支持每秒2个区块的出块速度,但却有一个等待最终确认的问题。因为一个传统的DPOS区块获得最终确认的依据是所有超级节点都在此块之后出过至少一个子块。这意味着假设有21个超级节点,每个节点每轮出6个块,平均每个出块时间为0.5秒。那么一个区块获得最终确认的时间需要60秒。 BFT的问题 基于BFT的POS因为BFT的特性所有每个块在产出之后可以得到快速的最终确认,但是却难以获得较高的TPS.原因是BFT每个区块分为三个状态,产生,预最终状态与最终确认状态。状态的改变是依靠收集到2/3节点的签名,而签名产生的效率依赖网络的延迟。假设部分超级节点在美国,部分在中国那么通信的延迟大约为200毫秒。那一个区块从产生到最终确认至少需要600毫秒的限制。所以在BFT的共识算法中网络延迟成为了高TPS的瓶颈。 DPOS BBFT共识算法 Bystack的共识算法是基于DPOS和BBFT算法特性的全新混合共识算法,通过将出块与BBFT签名异步进行的模式使得算法同时具有高TPS与快速最终确认的特性。在BBFT共识算法由全网用户投票选出n个共识节点进行出块。共识节轮流成为出块节点,当成为出块节点的共识节点将会以s秒一个块的速度连续出m个区块。当区块产生之后将直接广播至全网,但出块节点不会等待获取2/3的其他共识节点签名而是继续在当前块的基础上出下一个块。此时当前区块已是合法区块但是未获得最终确认,类似于比特币未获得6个块确认存在回滚的可能性。当其他共识节点收到区块并且验证通过之后将会对区块进行签名并广播到全网,当一个区块获得超过2/3的签名时就进入了最终确认状态。 TPS 实现高TPS的核心点是每个共识节点连续出m个区块。因为当每个节点只出一个块的话那么下一个共识节点出块需要等待上一个共识节点出的块,这里就需要考虑一个网络延迟带来的问题。如果把出块间隔设置小于网络延迟的,那会有大概率共识节点在出块时未收到上一个块造成分叉的状态。但当m设为一个稍大的数则可以将tps提升到带宽与节点性能的极限。假设当m=20,当下一个共识节点出块时因为网络延迟未收到最后1个块但却收到了之前的19个块,节点会接在上一轮第19个块之后出块。区块链会进入瞬间的分叉状态但会根据最长链原则在2个块之后全网状态统一。虽然损失了1个区块的TPS,但任保证了出块间隔小于网络延迟情况下的高出块率。 异步BFT 在BBFT的设计中出块与与共识节点的BFT签名是并行进行来抵消因网络延迟收集BFT签名对出块效率的影响。但不同于经典BFT算法中有产生,预最终状态与最终确认三个状态,BBFT根据区块链的特性改造使算法只有一个最终确认状态。但添加了两个额外的限制条件:第一个是当一个共识节点对相同高度的两个不同区块进行签名既发生欺诈;第二个是当一个共识节点对相同时间的两个不同区块进行签名既发生欺诈。通过这种方式的改造减少了共识节点之间的通信次数,从而降低了区块获得最终确认所花费的时间。同时BBFT还有区块获得直接确认与间接确认两种。第一种直接确认既区块获得了超过2/3的共识节点签名。第二种间接确认是一个区块未获得2/3的共识节点签名,但其子块获得了超过2/3共识节点的签名,BBFT则会认为此区块间接的获得了最终确认的状态。 容灾容错 支持只剩单共识节点存活的情况下支撑整个网络的运行到下一轮共识节点替换,但出块速度会下降为正常情况的1/n.用户可在此期间更改投票替换超级节点,在下一轮共识节点替换时网络既恢复正常状态。 支持1/3的共识节点作恶的情况下网络正常运行,当超过1/3的共识节点作恶区块将长时间不能进入最终确认功能直至网络运行到下一轮共识节点被替换。当超过1/2的共识节点作恶,恶意节点将控制网络。 BBFT共识出块情景分析 以下案例假设 n = 5, m = 3, s = 1,区块高度 = 100,时间戳为= 1557148900, 轮到3号共识节点准备出第一个块 完美状态 3号节点出高度为101, 时间戳为155714890区块A,广播至全网 区块A得到超过2/3的节点确认,进入最终确认状态 3号节点出高度为102, 时间戳为155714891区块B,广播至全网 区块B得到超过2/3的节点确认,进入最终确认状态 3号节点出高度为103, 时间戳为155714892区块C,广播至全网 区块C得到超过2/3的节点确认,进入最终确认状态 4号节点成功收到区块A, B, C并都处于最终状态,在此链的基础上继续连续出 4号节点出高度为104, 时间戳为155714893区块D,广播至全网 达到毫秒级最终确认,无回滚发生, 只有在网络延迟低与共识节点稳定的时候产生 理想状态 3号节点出高度为101, 时间戳为155714890区块A,广播至全网 3号节点出高度为102, 时间戳为155714891区块B,广播至全网 区块A得到超过2/3的节点确认,进入最终确认状态 3号节点出高度为103, 时间戳为155714892区块C,广播至全网 区块B得到超过2/3的节点确认,进入最终确认状态 4号节点成功收到区块A, B, C但只有A,B处于最终确认状态,在此链的基础上继续连续出块 4号节点出高度为104, 时间戳为155714893区块D,广播至全网 区块C得到超过2/3的节点确认,进入最终确认状态 达到秒级最终确认,无回滚发生,但因收集共识节点对区块的确认签名,导致最终确认的延迟。但由于所有区块已成功传递到下一个出块共识节点,所以不影响出块 出块共识节点异常状态 时间戳为155714890, 无新块产生 时间戳为155714891, 无新块产生 时间戳为155714892, 无新块产生 4号节点未收到任何区块,轮到挖矿后出高度为101,时间戳为155714893区块A广播至全网 区块A得到超过2/3的节点确认,进入最终确认状态 达到秒级最终确认,无回滚发生,因共识节点down机导致全网3秒内无节点出块。造成的影响是减慢了全网的出块速度,当单节点长期down机需要等待下一次投票时重新选出新一轮的共识节点可修复 网络延迟异常1 3号节点出高度为101, 时间戳为155714890区块A,广播至全网 区块A得到超过2/3的节点确认,进入最终确认状态 3号节点出高度为102, 时间戳为155714891区块B,广播至全网 区块B得到超过2/3的节点确认,进入最终确认状态 3号节点出高度为103, 时间戳为155714892区块C,广播至全网 区块C得到超过2/3的节点确认,进入最终确认状态 4号节点成功收到区块A, B但C区块由于延迟问题暂未收到 4号节点出高度为103, 时间戳为155714893区块D,广播至全网 由于2/3的共识节点已最终确认区块C, D无法获得最终确认 4号节点收到区块C与C的最终确认信息, 回滚区块D, 切换链至区块C 4号节点出高度为104, 时间戳为155714894区块E,广播至全网 区块E得到超过2/3的节点确认,进入最终确认状态 达到秒级最终确认,有回滚在所有没收到区块C的节点中发生,造成的影响是减慢了1个块的出块速度 网络延迟异常2 3号节点出高度为101, 时间戳为155714890区块A,广播至全网 区块A得到超过2/3的节点确认,进入最终确认状态 3号节点出高度为102, 时间戳为155714891区块B,广播至全网 区块B得到超过2/3的节点确认,进入最终确认状态 3号节点出高度为103, 时间戳为155714892区块C,广播至全网 4号节点成功收到区块A, B但C区块由于延迟问题暂未收到 4号节点出高度为103, 时间戳为155714893区块D,广播至全网 区块D得到超过2/3的节点确认,进入最终确认状态 3号节点收到区块D与D的最终确认信息, 回滚区块C, 切换链至区块D 4号节点出高度为104, 时间戳为155714894区块E,广播至全网 区块E得到超过2/3的节点确认,进入最终确认状态 达到秒级最终确认,有回滚在所有认同区块C的节点中发生,造成的影响是减慢了1个块的出块速度 网络延迟异常3 3号节点出高度为101, 时间戳为155714890区块A,广播至全网 区块A得到超过2/3的节点确认,进入最终确认状态 3号节点出高度为102, 时间戳为155714891区块B,广播至全网 区块B得到超过2/3的节点确认,进入最终确认状态 3号节点出高度为103, 时间戳为155714892区块C,广播至全网 4号节点成功收到区块A, B但C区块由于延迟问题暂未收到 4号节点出高度为103, 时间戳为155714893区块D,广播至全网 区块D得到超过2/3的节点确认,进入最终确认状态 3号节点收到区块D与D的最终确认信息, 回滚区块C, 切换链至区块D 4号节点出高度为104, 时间戳为155714894区块E,广播至全网 区块E得到超过2/3的节点确认,进入最终确认状态 达到秒级最终确认,有回滚在所有认同区块C的节点中发生,造成的影响是减慢了1个块的出块速度 网络延迟异常4 3号节点出高度为101, 时间戳为155714890区块A,广播至全网 区块A得到超过2/3的节点确认,进入最终确认状态 3号节点出高度为102, 时间戳为155714891区块B,广播至全网 区块B得到超过2/3的节点确认,进入最终确认状态 3号节点出高度为103, 时间戳为155714892区块C,广播至全网 4号节点成功收到区块A, B但C区块由于延迟问题暂未收到 4号节点出高度为103, 时间戳为155714893区块D,广播至全网 区块C, D各获得50%的共识节点投票,网络进入分叉状态 4号节点出高度为104, 时间戳为155714894区块E,广播至全网 区块E得到超过2/3的节点确认,进入最终确认状态 4号节点出高度为105, 时间戳为155714895区块E,广播至全网 达到秒级最终确认(极端情况分钟级发生概率和比特币回滚6区块差不多),有回滚在所有认同区块C的节点中发生,造成的影响是减慢了1个块的出块速度.此异常情况的极限状态是两条链各站约50%的算力并且发生持续竞争,直到稍占共识优势的链先进入了了最终确认状态。 参数对网络的影响 1.共识节点的个数其实代表了区块链网络的容错率,n越大则单点故障对网络造成的影响越小。但n的数量增大会导致BFT对区块签名数量要求的增加,会消耗更多的资源与延缓区块进入最终确认状态所需要的时间 2.每个节点连续出块的个数是为了在考虑到网络延迟的情况下仍可以保证高速出块的方法。当连续出块个数足够时出块时间理论上可达毫秒级。核心点就是当下一个出块共识节点有网络延迟未收到最后的3个区块,但之前的m-3个区已收到,可在m-3基础上继续出块。但m过大会导致单共识节点故障时长时间不出块 3.出块间隔时间明面上是高tps的保证,理论上当出块间隔为200毫秒时比Bytom的tps可达25000。但s设置的过小可能导致区块最终确认时间的延长。 论文链接:https://github.com/bystackcom/BBFT
Bytom Kit是一款为了帮助开发者更简单地理解Bytom的开发辅助工具,集合了校验、标注、解码、测试水龙头等功能。 该工具用python语言封装了一套比原的API和7个工具方法,如果有开发需求可以在项目的readme.md文件中查看使用方法。项目源码地址:https://github.com/Bytom/bytom-kit 。 下面具体看一下各个工具的使用方法: Key 链接:https://blockmeta.com/tools/key Key页面显示了熵(Entropy)、助记词(Mnemonics)、随机数(Seed)、派生私钥(Root Expanded Private Key)、派生公钥(Root Expanded Public Key),以及一个私钥的二维码。这一功能可以创建新的私钥key。 Address 链接: https://blockmeta.com/tools/address 地址页面主要创建新地址,页面显示了派生公钥(Expanded Public Key),账户索引(Account Index)、地址索引(Address Index)、更改标记(Change)、网络(Network)、地址路径(Address Path)、智能合约(Control Program)、地址(Address)、以及一个私钥的二维码。这一页面可以创建一个新地址。 Sign 地址: https://blockmeta.com/tools/sign 这个页面显示了派生私钥(Expanded Private Key)、消息(Message)、签名(Signature)。这一页面可以使用地址的密钥(私钥)对信息进行签名,输入私钥和信息内容,点击Sign Message可生成签名(Signature)。 Verify 地址: https://blockmeta.com/tools/verify 在这一页面,你可以通过输入公钥(Expanded Private Key)、信息(Message)、签名(Signature),、消息、签名。点击Verify Signature,可返回验证结果。 地址 Submit Transaction 链接: https://blockmeta.com/tools/txs 输入已经签名好的交易编码,点击Submit Transaction即可提交交易。 地址 Decode Raw Transaction 链接: https://blockmeta.com/tools/decode 输入已经签名好的交易(Raw Transaction),点击Decode Transaction可解码交易为十六进制字符串为json对象。 Testnet Faucet 链接: https://blockmeta.com/tools/faucet 比原水龙头工具接口,开发者可以使用该工具接口领取比原测试Token。
比原BMIP002协议 概述 比原链技术社区最近提出了一套资产规范提议,该提议允许在issue类型的交易中实现标准资产token。该标准定义资产在链上的基本功能,以及发行人通过智能合约管理资产的规范。 功能 资产是一种可以在区块链上发行的价值,给定资产的的所有单位都是可替代的。 每个资产都有全球唯一的资产ID,该资产ID来自发行程序和资产定义,发行程序通常定义一组可能的签名秘钥和阀值数量的签名,这些签名必需被提供以授权发布资产的新单元。 资产定义由提交给区块链的任意键值数据组成,提供所有参与者查看。 该标准为用户提供了一种发行资产的简单方法。它允许任何满足Bytom标准的token很容易被其他应用程序支持:比如钱包,区块链浏览器,到交易所。 一个例子是商家可以在很短的时间内在他们的商业应用中列出token。 规则 资产定义 以下规范使用标准均JSON模式的语法 语法 介绍 name 定义的资产token名字 symbol 定义的资产 decimals 定义token使用的小数位数 quorum 定义必须交易签名以发出此token单位的可能签名密钥的阈值数。 reissue 定义标志是否可以多次发布资产 description 包括由任意键值数据组成的资产描述 Issuance Program 创建资产时,Bytom会自动创建具有智能合约的发行程序。发行人可以根据需要多次发布任意数量的单位。自定义发行计划可能会对何时,以及由谁发布新单位强制执行进一步的限制。 提示: 以下代码片段未在Equity0.1.1(或更高版本)中编写。我们建议没有限制的发行资产程序和至少2个签名秘钥 contract IssueAsset(publicKey1: PublicKey, publicKey2: PublicKey, publicKey3: PublicKey) locks valueAmount of valueAsset { clause spend(sig1: Signature, sig2: Signature) { verify checkTxMultiSig([publicKey1, publicKey2, publicKey3], [sig1, sig2]) unlock valueAmount of valueAsset } } Issuance Program will be inactive after certain block height by adding the limitHeight parameter. contract IssueAssetWithLimit(publicKey1: PublicKey, publicKey2: PublicKey, publicKey3: PublicKey, limitHeight: Integer) locks valueAmount of valueAsset { clause spend(sig1: Signature, sig2: Signature, limitHeight: Integer) { verify below(limitHeight) verify checkTxMultiSig([publicKey1, publicKey2, publicKey3], [sig1, sig2]) unlock valueAmount of valueAsset } } 比原BMIP002协议在blockmeta中的展示 通过上面对BMIP002协议的介绍,我们知道BMIP002协议本质是对发行在比原链上的资产的一个规范,那接下来我们介绍一下这套规范在blockmeta中的具体展示。 blockmeta官网:https://blockmeta.com/ 在blockmeta中主要展示在3个地方,第一点:我们打开我们发行的资产详情页面,如图中圈起来的标识,如果满足BMIP002协议的资产,会显示YES。下图中的资产不支持BMIP002协议,所以显示NO。 第二点,打开资产列表页面,如果发行的资产支持BMIP002协议,资产名的又下角会显示logo。且在整个资产列表中,支持BMIP002协议的资产排序是靠前的。 最后一点是,我们打开一笔交易的详情,找到一笔交易的地址,我们打开地址详情页面,可以看到属于该地址的资产。如下图: 以上就是BMIP002协议在blockmeta中的具体展示。
安装使用插件钱包 1. 打开Google浏览器的应用商店,搜索Bystore 下载链接:http://t.cn/E6cFFwb 2. 然后点击添加到Chrome,就可以添加到我们的: 3. 使用google插件钱包 如果你使用的是测试网,可以去测试网水龙头领取BTM。测试网水龙头:http://test.blockmeta.com/faucet.php 搭建Dapp demo Dapp demo是一个基于比原的储蓄合约,该demo可以进行资产的锁仓储蓄,到期返还资产并给一定的利息。这个dapp很适合的场景就是股息分红,内部通过智能合约自动锁仓操作,到期资产自动解锁。所以我个人对这个dapp应用场景表示非常看好。 项目源码地址:https://github.com/Bytom/Bytom-Dapp-Demo 根据源码里面的readme.md文件进行搭建dapp,然后我们在本地打开http://127.0.0.1:8080 后就可以看该dapp应用。然后我们点开我们的账户如下图: 点击saving,我们看到的是储蓄资产界面,用户可以设置资产的金额,并储蓄资产 下图是我们收益的页面,我们可以看到自己储蓄的收益,如果是到期的话我们可以提出自己的收益。 Dapp调起Google插件的实现 初始化注入 检查插件,账户 调交易接口 下图是发送交易的API接口,接口的具体文档参考:https://github.com/Bytom/Bystore/wiki/API-reference。还有其他的API接口都在该文档里面。监听事件接口bytom.request(eventName, options)。 后端服务器接口 由于比原链采用的UTXO模型,该模型没有状态。但是在开发dapp的过程中需要关联用户的的地址。所以后端服务器主要是封装一层类似账户模型,方便dapp跟链进行交互。开发者开发dapp可以搭建改项目作为与链交互的服务器,自己搭建参考项目的readme. 后端服务器项目地址:https://github.com/oysheng/bufferserver Dapp开发流程梳理 通过上面的一系列步骤,我们已经大概明白基于比原链开发dapp的一个大致流程。流程总结就是: step1: 下载安装Chrome插件钱包,如果自己的dapp不需要跳过这一步。 step2: 如果需要自己搭建BlockCenter后端服务器,参考项目说明文件安装。不想搭建的话,直接用官方的服务,直接远程调用即可。 step3: 开发智能合约,并编译。然后将编译后的合约参数配置在dapp的配置文件,如下图:(全红部分是测试网合约配置参数) step4:调用Chrome插件钱包。 到此,在比原链上开发dapp的整套流程都已经梳理清楚,欢迎大家快速上手试试。开发出优秀的dapp应用。 Github: https://github.com/bycoinio/Bystore/
严格来说,tx-signer并不属于SDK,它是bytomd中构建交易、对交易签名两大模块的java实现版。因此,若想用tx-signer对交易进行离线签名,需要由你在本地保管好自己的私钥。 如果你的目的是完全脱离于bytomd全节点,可能需要自己做更多额外的工作。比如,在构建交易时,需要花费若干个utxo(Unspent Transaction Output)作为交易的输入,如果没有全节点则需要自身来维护utxo。当使用tx-signer构建完成一笔交易并签名后,若没有全节点的帮助,也需要自己实现P2P网络协议将交易广播到其他节点。 本文不会对以上技术细节进行讨论,而是利用bytomd全节点查询可用的utxo构建交易,对交易进行签名并序列化后,同样使用bytomd提交交易。 准备工作 将Maven依赖引入到你的项目中 获取SDK源码 git clone https://github.com/Bytom/bytom-java-sdk.git 打包成JAR包并安装到本地的Maven仓库 $ mvn clean install -DskipTests 在项目的POM文件中添加依赖。其中,第一个依赖是bytomd api的封装,可用于查询可用的utxo以及提交交易;第二个依赖用于构建交易以及对交易进行离线签名。 <dependency> <groupId>io.bytom</groupId> <artifactId>java-sdk</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>io.bytom</groupId> <artifactId>tx-signer</artifactId> <version>1.0.0</version> </dependency> 构建交易 普通交易 查询可用的utxo 在本文中,以下将全部使用全节点来查询可用的utxo,你也可以构建一套自己的utxo维护方案。 Client client = Client.generateClient(); UnspentOutput.QueryBuilder builder = new UnspentOutput.QueryBuilder(); builder.accountAlias = "bytom"; List<UnspentOutput> outputs = builder.list(client); 利用SDK只需要四行代码就能查询可用的utxo(SDK具体文档详见java-sdk documentation)。在QueryBuilder中可以指定是否为未确认的utxo(默认false),也可以通过from和count来进行分页查询(默认查询所有)。假设在当前账户下查询得到这样一个utxo: { "account_alias": "bytom", "id": "ffdc59d0478277298de4afa458dfa7623c051a46b7a84939fb8227083411b156", "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "asset_alias": "BTM", "amount": 41250000000, "account_id": "0G1R52O1G0A02", "address": "sm1qxls6ajp6fejc0j5kp8jwt2nj3kmsqazfumrkrr", "control_program_index": 1, "program": "001437e1aec83a4e6587ca9609e4e5aa728db7007449", "source_id": "2d3a5d920833778cc7c65d7c96fe5f3c4a1a61aa086ee093f44a0522dd499a34", "source_pos": 0, "valid_height": 4767, "change": false, "derive_rule": 0 } 构建交易 现在需要往0014c832e1579b4f96dc12dcfff39e8fe69a62d3f516这个control program转100个BTM。代码如下: String btmAssetID = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; // 下面的字段与utxo中的字段一一对应 SpendInput input = new SpendInput(); input.setAssetId(btmAssetID); input.setAmount(41250000000L); input.setProgram("001437e1aec83a4e6587ca9609e4e5aa728db7007449"); input.setSourcePosition(0); input.setSourceID("2d3a5d920833778cc7c65d7c96fe5f3c4a1a61aa086ee093f44a0522dd499a34"); input.setChange(false); input.setControlProgramIndex(1); // 选择使用BIP32还是BIP44来派生地址,默认BIP44 input.setBipProtocol(BIPProtocol.BIP44); // 账户对应的密钥索引 input.setKeyIndex(1); // 自身本地保管的私钥,用于对交易进行签名 input.setRootPrivateKey("4864bae85cf38bfbb347684abdbc01e311a24f99e2c7fe94f3c071d9c83d8a5a349722316972e382c339b79b7e1d83a565c6b3e7cf46847733a47044ae493257"); Transaction tx = new Transaction.Builder() .addInput(input) // 加入需要转入的output .addOutput(new Output(btmAssetID, 10000000000L, "0014c832e1579b4f96dc12dcfff39e8fe69a62d3f516")) // 剩余的BTM用于找零 .addOutput(new Output(btmAssetID, 31250000000L, "0014bb8a039726df1b649738e9973db14a4b4fd4becf")) .setTimeRange(0) .build(); String rawTransaction = tx.rawTransaction(); 对交易调用build方法后,自动会对交易进行本地的验证和签名操作。注意,在本地只是做简单的字段验证,本地验证通过并不代表交易合法。最后对交易调用rawTransaction方法返回交易序列化后的字符串。 提交交易 本文利用bytomd全节点来提交交易: HashMap<String, Object> body = new HashMap<>(); body.put("raw_transaction", "070100010160015e4b5cb973f5bef4eadde4c89b92ee73312b940e84164da0594149554cc8a2adeaffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80c480c1240201160014cb9f2391bafe2bc1159b2c4c8a0f17ba1b4dd94e6302401cb779288be890a28c5209036da1a27d9fe74a51c38e0a10db4817bcf4fd05f68580239eea7dcabf19f144c77bf13d3674b5139aa51a99ba58118386c190af0e20bcbe020b05e1b7d0825953d92bf47897be08cd7751a37adb95d6a2e5224f55ab02013dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80b095e42001160014a82f02bc37bc5ed87d5f9fca02f8a6a7d89cdd5c000149ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80d293ad03012200200824e931fb806bd77fdcd291aad3bd0a4493443a4120062bd659e64a3e0bac6600"); Transaction.SubmitResponse response = client.request("submit-transaction", body, Transaction.SubmitResponse.class); 交易提交成功后,response返回交易ID。 发行资产交易 查询可用的utxo 发行资产时,需要使用BTM作为手续费,因此第一步同样需要查询当前账户下可用的utxo,由于上面已经提到,这里不再赘述。 查询需要发行的资产信息 例如,需要发行的资产id为7b38dc897329a288ea31031724f5c55bcafec80468a546955023380af2faad14 Asset.QueryBuilder builder = new Asset.QueryBuilder(); builder.setId("7b38dc897329a288ea31031724f5c55bcafec80468a546955023380af2faad14"); List<Asset> assets = builder.list(client); 假设查询得到的资产信息如下: { "type": "asset", "xpubs": [ "5ff7f79f0fd4eb9ccb17191b0a1ac9bed5b4a03320a06d2ff8170dd51f9ad9089c4038ec7280b5eb6745ef3d36284e67f5cf2ed2a0177d462d24abf53c0399ed" ], "quorum": 1, "key_index": 3, "derive_rule": 0, "id": "7b38dc897329a288ea31031724f5c55bcafec80468a546955023380af2faad14", "alias": "棒棒鸡", "vm_version": 1, "issue_program": "ae20db11f9dfa39c9e66421c530fe027218edd3d5b1cd98f24c826f4d9c0cd131a475151ad", "raw_definition_byte": "7b0a202022646563696d616c73223a20382c0a2020226465736372697074696f6e223a207b7d2c0a2020226e616d65223a2022222c0a20202273796d626f6c223a2022220a7d", "definition": { "decimals": 8, "description": {}, "name": "", "symbol": "" } } 构建交易 现在需要发行1000个棒棒鸡资产: IssuanceInput issuanceInput = new IssuanceInput(); issuanceInput.setAssetId("7b38dc897329a288ea31031724f5c55bcafec80468a546955023380af2faad14"); issuanceInput.setAmount(100000000000L); // issue program issuanceInput.setProgram("ae20db11f9dfa39c9e66421c530fe027218edd3d5b1cd98f24c826f4d9c0cd131a475151ad"); // 可以不指定,不指定时将随机生成一个 issuanceInput.setNonce("ac9d5a527f5ab00a"); issuanceInput.setKeyIndex(5); // raw definition byte issuanceInput.setRawAssetDefinition("7b0a202022646563696d616c73223a20382c0a2020226465736372697074696f6e223a207b7d2c0a2020226e616d65223a2022222c0a20202273796d626f6c223a2022220a7d"); // 该资产对应的私钥 issuanceInput.setRootPrivateKey("4864bae85cf38bfbb347684abdbc01e311a24f99e2c7fe94f3c071d9c83d8a5a349722316972e382c339b79b7e1d83a565c6b3e7cf46847733a47044ae493257"); // 创建一个spend input作为手续费,假设当前有一个100BTM的utxo,并且使用1BTM作为手续费,则后续还要创建99BTM的找零地址 SpendInput feeInput = new SpendInput(btmAssetID, 10000000000L, "0014cb9f2391bafe2bc1159b2c4c8a0f17ba1b4dd94e"); feeInput.setKeyIndex(1); feeInput.setChange(true); feeInput.setSourceID("4b5cb973f5bef4eadde4c89b92ee73312b940e84164da0594149554cc8a2adea"); feeInput.setSourcePosition(2); feeInput.setControlProgramIndex(457); feeInput.setRootPrivateKey("4864bae85cf38bfbb347684abdbc01e311a24f99e2c7fe94f3c071d9c83d8a5a349722316972e382c339b79b7e1d83a565c6b3e7cf46847733a47044ae493257"); Transaction tx = new Transaction.Builder() .addInput(issuanceInput) .addInput(feeInput) // 该output用于接收发行的资产 .addOutput(new Output("7b38dc897329a288ea31031724f5c55bcafec80468a546955023380af2faad14", 100000000000L, "001437e1aec83a4e6587ca9609e4e5aa728db7007449")) // 找零 .addOutput(new Output(btmAssetID, 9800000000L, "00148be1104e04734e5edaba5eea2e85793896b77c56")) .setTimeRange(0) .build(); 提交交易 提交交易的方式与普通交易一致。 销毁资产交易 销毁资产跟发行资产类似,同样需要BTM作为手续费。 查询可用的utxo 查询方式与普通交易一致。 构建交易 这里以销毁一个BTM为例,假设查询得到一个100BTM的utxo: // 查询得到一个100BTM的utxo作为输入 SpendInput input = new SpendInput(btmAssetID, 10000000000L, "0014f1dc52048f439ac7fd74f8106a21da78f00de48f"); input.setRootPrivateKey(rootKey); input.setChange(true); input.setKeyIndex(1); input.setControlProgramIndex(41); input.setSourceID("0b2cff11d1d056d95237a5f2d06059e5395e86f60e69c1e8201ea624911c0c65"); input.setSourcePosition(0); // 销毁资产时,可添加一段附加的文本 String arbitrary = "77656c636f6d65efbc8ce6aca2e8bf8ee69da5e588b0e58e9fe5ad90e4b896e7958c"; // 销毁99个BTM,剩余1个BTM作为手续费 Output output = Output.newRetireOutput(btmAssetID, 9900000000L, arbitrary); Transaction transaction = new Transaction.Builder() .addInput(input) .addOutput(output) .setTimeRange(2000000) .build(); String rawTransaction = transaction.rawTransaction(); 提交交易 提交交易的方式与普通交易一致。 bytom java sdk:https://github.com/Bytom/bytom-java-sdk/
储蓄分红合约简介 储蓄分红合约指的是项目方发起了一个锁仓计划(即储蓄合约和取现合约),用户可以在准备期自由选择锁仓金额参与该计划,等到锁仓到期之后还可以自动获取锁仓的利润。用户可以在准备期内(dueBlockHeight)参与储蓄,按照合约规定可以 1:1 获取同等数量的储蓄票据资产,同时用户锁仓的资产(deposit)将放到取现合约中,并且项目方是无法动用的,等到锁仓期限(expireBlockHeight)一到,用户便可以调用取现合约将自己储蓄的资产连本待息一同取出来。其示意图如下: 从上图中可以看出,项目方发布了一个利润为20%的锁仓项目,其中储蓄合约FixedLimitCollect锁定了1000个票据资产(bill),同时项目方将200个储蓄资产(deposit)锁定到利息合约中。待项目方发布完合约之后,所有用户便可以参与了。例如上图中user1调用合约储蓄了500,这500个储蓄资产将被锁定在取现合约FixedLimitProfit中,同时user1获得了500个票据资产,剩余找零的资产将继续锁定在储蓄合约FixedLimitCollect中,以此类推,user2和user3也是相同的流程,直到储蓄合约没有资产为止。取现合约FixedLimitProfit跟储蓄合约的模型大致相同,只是取现合约是由多个UTXO组成的,用户在取现的时候可以并行操作。但是如果合约中的面值不能支持用户一次性取现的话,需要分多次提取。例如user1拥有500个票据资产,而可以获得的本息总额为600,但是取现的UTXO面值为500,那么user1一次最多只能取500,剩下的100需要再构造一笔交易来提现。 合约源代码 // 储蓄合约 import "./FixedLimitProfit" contract FixedLimitCollect(assetDeposited: Asset, totalAmountBill: Amount, totalAmountCapital: Amount, dueBlockHeight: Integer, expireBlockHeight: Integer, additionalBlockHeight: Integer, banker: Program, bankerKey: PublicKey) locks billAmount of billAsset { clause collect(amountDeposited: Amount, saver: Program) { verify below(dueBlockHeight) verify amountDeposited <= billAmount && totalAmountBill <= totalAmountCapital define sAmountDeposited: Integer = amountDeposited/100000000 define sTotalAmountBill: Integer = totalAmountBill/100000000 verify sAmountDeposited > 0 && sTotalAmountBill > 0 if amountDeposited < billAmount { lock amountDeposited of assetDeposited with FixedLimitProfit(billAsset, totalAmountBill, totalAmountCapital, expireBlockHeight, additionalBlockHeight, banker, bankerKey) lock amountDeposited of billAsset with saver lock billAmount-amountDeposited of billAsset with FixedLimitCollect(assetDeposited, totalAmountBill, totalAmountCapital, dueBlockHeight, expireBlockHeight, additionalBlockHeight, banker, bankerKey) } else { lock amountDeposited of assetDeposited with FixedLimitProfit(billAsset, totalAmountBill, totalAmountCapital, expireBlockHeight, additionalBlockHeight, banker, bankerKey) lock billAmount of billAsset with saver } } clause cancel(bankerSig: Signature) { verify above(dueBlockHeight) verify checkTxSig(bankerKey, bankerSig) unlock billAmount of billAsset } } // 取现合约(本金加利息) contract FixedLimitProfit(assetBill: Asset, totalAmountBill: Amount, totalAmountCapital: Amount, expireBlockHeight: Integer, additionalBlockHeight: Integer, banker: Program, bankerKey: PublicKey) locks capitalAmount of capitalAsset { clause profit(amountBill: Amount, saver: Program) { verify above(expireBlockHeight) define sAmountBill: Integer = amountBill/100000000 define sTotalAmountBill: Integer = totalAmountBill/100000000 verify sAmountBill > 0 && sTotalAmountBill > 0 && amountBill < totalAmountBill define gain: Integer = totalAmountCapital*sAmountBill/sTotalAmountBill verify gain > 0 && gain <= capitalAmount if gain < capitalAmount { lock amountBill of assetBill with banker lock gain of capitalAsset with saver lock capitalAmount - gain of capitalAsset with FixedLimitProfit(assetBill, totalAmountBill, totalAmountCapital, expireBlockHeight, additionalBlockHeight, banker, bankerKey) } else { lock amountBill of assetBill with banker lock capitalAmount of capitalAsset with saver } } clause cancel(bankerSig: Signature) { verify above(additionalBlockHeight) verify checkTxSig(bankerKey, bankerSig) unlock capitalAmount of capitalAsset } } 合约的源代码说明可以具体参考Equity合约介绍. 注意事项: 时间期限不是具体的时间,而是通过区块高度来大概估算的(平均区块时间间隔大概为2.5分钟) 比原的精度是8, 即 1BTM = 100000000 neu,正常情况下参与计算都是以neu为单位的,然而虚拟机的int64类型的最大值是9223372036854775807,为了避免数值太大导致计算溢出,所以对计算的金额提出了金额限制(即amountBill/100000000) 另外clause cancel是项目方的管理方法,如果储蓄或者取现没有满额,项目方也可以回收剩余的资产 编译并实例化合约 编译Equity合约可以参考一下Equity编译器的介绍说明。假如储蓄合约FixedLimitCollect的参数如下: assetDeposited :c6b12af8326df37b8d77c77bfa2547e083cbacde15cc48da56d4aa4e4235a3ee totalAmountBill :10000000000 totalAmountCapital :20000000000 dueBlockHeight :1070 expireBlockHeight :1090 additionalBlockHeight :1100 banker :0014dedfd406c591aa221a047a260107f877da92fec5 bankerKey :055539eb36abcaaf127c63ae20e3d049cd28d0f1fe569df84da3aedb018ca1bf 其中bankerKey是管理员的publicKey,可以通过比原链的接口list-pubkeys来获取,注意管理员需要保存一下对应的rootXpub和Path,否则无法正确调用clause cancel。 实例化合约命令如下: // 储蓄合约 ./equity FixedLimitCollect --instance c6b12af8326df37b8d77c77bfa2547e083cbacde15cc48da56d4aa4e4235a3ee 10000000000 20000000000 1070 1090 1100 0014dedfd406c591aa221a047a260107f877da92fec5 055539eb36abcaaf127c63ae20e3d049cd28d0f1fe569df84da3aedb018ca1bf // 取现合约 ./equity FixedLimitProfit --instance c6b12af8326df37b8d77c77bfa2547e083cbacde15cc48da56d4aa4e4235a3ee 10000000000 20000000000 1090 1100 0014dedfd406c591aa221a047a260107f877da92fec5 055539eb36abcaaf127c63ae20e3d049cd28d0f1fe569df84da3aedb018ca1bf 发布合约交易 发布合约交易即将资产锁定到合约中。由于目前无法在比原的dashboard上构造合约交易,所以需要借助外部工具来发送合约交易,比如postman。按照上述示意图所示,项目方需要发布1000个储蓄资产的储蓄合约和200个利息资产取现合约。假设项目方需要发布1000个储蓄资产(假如精度为8,那么1000个在比原链中表示为100000000000)的锁仓合约,那么他需要将对应数量的票据锁定在储蓄合约中,其交易模板如下: { "base_transaction": null, "actions": [ { "account_id": "0ILGLSTC00A02", "amount": 20000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "type": "spend_account" }, { "account_id": "0ILGLSTC00A02", "amount": 100000000000, "asset_id": "13016eff73ffb7539a69e122f80f5c1cc94446773ac3f64dec290429f87e73b3", "type": "spend_account" }, { "amount": 100000000000, "asset_id": "13016eff73ffb7539a69e122f80f5c1cc94446773ac3f64dec290429f87e73b3", "control_program": "20055539eb36abcaaf127c63ae20e3d049cd28d0f1fe569df84da3aedb018ca1bf160014dedfd406c591aa221a047a260107f877da92fec5024c04024204022e040500c817a8040500e40b540220c6b12af8326df37b8d77c77bfa2547e083cbacde15cc48da56d4aa4e4235a3ee4d3b02597a642f0200005479cda069c35b797ca153795579a19a695a790400e1f5059653790400e1f505967c00a07c00a09a69c35b797c9f9161644d010000005b79c2547951005e79895d79895c79895b7989597989587989537a894caa587a649e0000005479cd9f6959790400e1f5059653790400e1f505967800a07800a09a5c7956799f9a6955797b957c96c37800a052797ba19a69c3787c9f91616487000000005b795479515b79c1695178c2515d79c16952c3527994c251005d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696399000000005b795479515b79c16951c3c2515d79c16963aa000000557acd9f69577a577aae7cac890274787e008901c07ec169515b79c2515d79c16952c35c7994c251005d79895c79895b79895a79895979895879895779895679895579890274787e008901c07ec169632a020000005b79c2547951005e79895d79895c79895b7989597989587989537a894caa587a649e0000005479cd9f6959790400e1f5059653790400e1f505967800a07800a09a5c7956799f9a6955797b957c96c37800a052797ba19a69c3787c9f91616487000000005b795479515b79c1695178c2515d79c16952c3527994c251005d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696399000000005b795479515b79c16951c3c2515d79c16963aa000000557acd9f69577a577aae7cac890274787e008901c07ec16951c3c2515d79c169633b020000547acd9f69587a587aae7cac747800c0", "type": "control_program" } ], "ttl": 0, "time_range": 1521625823 } 合约交易成功后,合约control_program对应的UTXO将会被所有用户查询到,使用比原链的接口list-unspent-outputs即可查询。 此外,开发者需要存储一下合约UTXO的assetID和program,以便在DAPP的前端页面的config配置文件和bufferserver缓冲服务器中调用。如上所示: // 储蓄合约 assetID:13016eff73ffb7539a69e122f80f5c1cc94446773ac3f64dec290429f87e73b3 program:20055539eb36abcaaf127c63ae20e3d049cd28d0f1fe569df84da3aedb018ca1bf160014dedfd406c591aa221a047a260107f877da92fec5024c04024204022e040500c817a8040500e40b540220c6b12af8326df37b8d77c77bfa2547e083cbacde15cc48da56d4aa4e4235a3ee4d3b02597a642f0200005479cda069c35b797ca153795579a19a695a790400e1f5059653790400e1f505967c00a07c00a09a69c35b797c9f9161644d010000005b79c2547951005e79895d79895c79895b7989597989587989537a894caa587a649e0000005479cd9f6959790400e1f5059653790400e1f505967800a07800a09a5c7956799f9a6955797b957c96c37800a052797ba19a69c3787c9f91616487000000005b795479515b79c1695178c2515d79c16952c3527994c251005d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696399000000005b795479515b79c16951c3c2515d79c16963aa000000557acd9f69577a577aae7cac890274787e008901c07ec169515b79c2515d79c16952c35c7994c251005d79895c79895b79895a79895979895879895779895679895579890274787e008901c07ec169632a020000005b79c2547951005e79895d79895c79895b7989597989587989537a894caa587a649e0000005479cd9f6959790400e1f5059653790400e1f505967800a07800a09a5c7956799f9a6955797b957c96c37800a052797ba19a69c3787c9f91616487000000005b795479515b79c1695178c2515d79c16952c3527994c251005d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696399000000005b795479515b79c16951c3c2515d79c16963aa000000557acd9f69577a577aae7cac890274787e008901c07ec16951c3c2515d79c169633b020000547acd9f69587a587aae7cac747800c0 // 取现合约 assetID:c6b12af8326df37b8d77c77bfa2547e083cbacde15cc48da56d4aa4e4235a3ee program:20055539eb36abcaaf127c63ae20e3d049cd28d0f1fe569df84da3aedb018ca1bf160014dedfd406c591aa221a047a260107f877da92fec5024c040242040500c817a8040500e40b540220c6b12af8326df37b8d77c77bfa2547e083cbacde15cc48da56d4aa4e4235a3ee4caa587a649e0000005479cd9f6959790400e1f5059653790400e1f505967800a07800a09a5c7956799f9a6955797b957c96c37800a052797ba19a69c3787c9f91616487000000005b795479515b79c1695178c2515d79c16952c3527994c251005d79895c79895b79895a79895979895879895779895679890274787e008901c07ec1696399000000005b795479515b79c16951c3c2515d79c16963aa000000557acd9f69577a577aae7cac747800c0 本期的比原储蓄分红合约就介绍在这里,下一期我们将详细介绍如何基于该合约开发dapp应用
在比原链中,每个交易消耗之前交易生成的BUTXO 然后生成新的 BUTXO,账户的余额即所有属于该地址的未花费 BUTXO 集合,BTM 的全局状态即当前所有未花费的BUTXO 集合。我们用一个具体的例子说明。 比原的状态是通过BUTXO来实现的。比原的价值转移是通过转账实现的。更特别地是,我们可以通过创建转账花费1个或多个,并且将他们的BUTXO作为转账输入。比原BUTXO不能部分花费。如果我们花费0.5个比原(使用他们仅有的BUTXO,价值1比原),他们需要特意地发回0.5个比原。如果他们不发送这部分,那么这个0.5比原就会丢失,并且给到挖出转账的矿工。过程如下图所示: 比原链的BUTXO没有包含用户账户余额。通过比原,用户可以简单地持有私钥,在任何时间点都可以进行一个或者多个BUTXO。如下图: 总结: BUTXO = 未花费的交易输出; 传统的账户模型一个“账户”的余额就是一个数字; BUTXO模型中余额是由所有和“账户”相关的UTXO组成的; 那可能有小伙伴好奇,比原链的BUTXO和UTXO模型有什么区别呢? BUTXO是比原链在UTXO的模型上柔和了支持多资产,从而保证了资产交互操作的原子性,异步交易的时候可以进行验证,支持多资产上链,智能合约结果bool化。由于BUTXO的无状态,所以在一定程度上增强了用户的匿名性。如下图: 我们查看了比原的BUTXO结构体,发现比原的结构体中的多了AssetID,ControlProgram等字段。比原链中BUTXO的结构体如下: 这是比原链专门为了多资产和智能合约而扩充的,目的是支持多种资产上链,以及智能合约bool化。所以比原链的BUTXO和UTXO的最大区别就是支持多种资产和柔和智能合约。跟UTXO模型比,这是比原链BUTXO的最大优势。 此外,计算是在链外的,交易本身既是结果也是证明。节点只做验证即可,不需要对交易进行额外的计算,也没有额外的状态存储。交易本身的输出 的计算是在钱包完成的,这样交易的计算负担完全由钱包来承担,一定程度上减少了链的负担。BUTXO 模型是无状态的,更容易并发处理。所以比原链能更好的支持多种业务场景,能很好的服务多种资产在链发行,流通。
系统要求 我们建议选择主要的几家云主机平台的VPS服务,运行比原链节点对算力没有要求,但是请配置尽可能大的磁盘空间以适应区块链数据未来增长的需要。 节点服务器最小配置: 操作系统: Windows/Linux/Docker CPU: 2核 内存: 2G 硬盘: 40G 网络: 独立IP,2MB带宽 防火墙: 开启46657端口 Ubuntu接入文档 1 节点服务器部署 1.1 安装系统依赖库 sudo apt-get update sudo apt-get install build-essential git unzip wget vim 1.2 下载并解压节点 wget https://mirrors.tuna.tsinghua.edu.cn/osdn/bytom/70718/bytom-1.0.8-linux.zip unzip bytom-1.0.8-linux.zip chmod +777 ./bytomd 2 启动并运行节点 ./bytomd init --chain_id mainnet ./bytomd node --simd.enable Windows接入文档 1 安装系统依赖库 1.1 安装MinGW 官方链接: https://nuwen.net/mingw.html 下载链接: https://nuwen.net/files/mingw/mingw-16.1.exe 1.2 安装Golang 官方地址: https://golang.org/ 下载链接: https://studygolang.com/dl/golang/go1.12.windows-amd64.msi 参考链接: https://studygolang.com/dl 1.3 安装Git 官方地址: https://git-scm.com/ 下载链接: https://git-scm.com/download/win 2 下载并解压节点 下载链接: https://mirrors.tuna.tsinghua.edu.cn/osdn/bytom/70718/bytom-1.0.8-linux.zip 解压zip文件,并右键文件夹权限修改成可读写 3 启动并运行节点 ./bytomd.exe init --chain_id mainnet ./bytomd.exe node --simd.enable Docker接入文档 1 获取Docker镜像 docker pull bytom/bytom:latest 2 初始化节点 docker run -v ~/Bytom:/root/.bytom bytom/bytom:latest bytomd init --chain_id mainnet 3 运行节点 docker run -d -p 46657:46657 -v ~/Bytom:/root/.bytom bytom/bytom:latest bytomd node --web.closed --auth.disable
当我们基于比原做应用的时候,在构建交易过程中会遇到以下两种情况。多个地址向一个地址转账,还有一种就是从一个地址分批次向多个地址转账。那我们今天就来介绍一下这两种交易构建的具体流程,以及贴出具体实现的代码。 链式交易 当我们从多个钱包地址一次性转到一个地址的时候,为了提高用户体验。我们可以选择链式交易,把多笔交易一次性打包。那我们下面就来看一下链式交易的流程。 接下来我们来看一下build-transaction接口的代码实现过程,代码如下: // POST /build-chain-transactions func (a *API) buildChainTxs(ctx context.Context, buildReqs *BuildRequest) Response { //验证请求id subctx := reqid.NewSubContext(ctx, reqid.New()) //构建交易,方法的具体过程在下面 tmpls, err := a.buildTxs(subctx, buildReqs) if err != nil { return NewErrorResponse(err) } return NewSuccessResponse(tmpls) } 核心的实现方法,buildTxs方法的实现如下: func (a *API) buildTxs(ctx context.Context, req *BuildRequest) ([]*txbuilder.Template, error) { //验证参数的合法性 if err := a.checkRequestValidity(ctx, req); err != nil { return nil, err } //合并处理交易输入输出的类型组合 actions, err := a.mergeSpendActions(req) if err != nil { return nil, err } //构建一笔新的交易模板 builder := txbuilder.NewBuilder(time.Now().Add(req.TTL.Duration)) //声明交易模板 tpls := []*txbuilder.Template{} //遍历交易的输入输出类型组合 for _, action := range actions { //类型组合的输入为apend_account if action.ActionType() == "spend_account" { //构建花费的输入输出类型组合并且自动合并UTXO tpls, err = account.SpendAccountChain(ctx, builder, action) } else { //构建交易输入输出类型组合 err = action.Build(ctx, builder) } if err != nil { //回滚 builder.Rollback() return nil, err } } //构建交易 tpl, _, err := builder.Build() if err != nil { //回滚 builder.Rollback() return nil, err } tpls = append(tpls, tpl) return tpls, nil } build方法的实现过程: // Build build transactions with template func (b *TemplateBuilder) Build() (*Template, *types.TxData, error) { // Run any building callbacks. for _, cb := range b.callbacks { err := cb() if err != nil { return nil, nil, err } } tpl := &Template{} tx := b.base if tx == nil { tx = &types.TxData{ Version: 1, } } if b.timeRange != 0 { tx.TimeRange = b.timeRange } // Add all the built outputs. tx.Outputs = append(tx.Outputs, b.outputs...) // Add all the built inputs and their corresponding signing instructions. for i, in := range b.inputs { instruction := b.signingInstructions[i] instruction.Position = uint32(len(tx.Inputs)) // Empty signature arrays should be serialized as empty arrays, not null. if instruction.WitnessComponents == nil { instruction.WitnessComponents = []witnessComponent{} } tpl.SigningInstructions = append(tpl.SigningInstructions, instruction) tx.Inputs = append(tx.Inputs, in) } tpl.Transaction = types.NewTx(*tx) tpl.Fee = CalculateTxFee(tpl.Transaction) return tpl, tx, nil } 到此,我们的链式交易的代码到此就讲解到这儿。如果感兴趣想仔细阅读源码,点击源码地址:https://git.io/fhAsr 花费未确认的交易 下面我们来介绍一下花费未确认的交易,我们首先介绍一下什么是花费未确认的交易。我们知道UTXO模型在交易的过程中,如果交易未打包确认。再进行第二笔转账就会存在“双花”问题,就不能再发起交易或者需要等一段时间才能再发起一笔交易。如果使用花费未确认的交易就可以避免这个问题。 那么花费未确认的交易实现机制是什么样的呢?我们在创建第一笔交易的时候,会找零,此时交易是未确认的状态。找零存在交易池中,我们发第二笔交易的时候就直接使用在交易池中找零地址里面的资产。 那我们来看一下花费未确认交易的代码实现过程,花费过程结构体如下: type spendAction struct { accounts *Manager //存储账户及其相关的控制程序参数 bc.AssetAmount //资产id和资产数量的结构体 AccountID string `json:"account_id"` //账户id UseUnconfirmed bool `json:"use_unconfirmed"` //是否未确认 } 方法如下: // MergeSpendAction merge common assetID and accountID spend action func MergeSpendAction(actions []txbuilder.Action) []txbuilder.Action { //声明变量,map resultActions := []txbuilder.Action{} spendActionMap := make(map[string]*spendAction) //遍历交易的输入输出类型组合 for _, act := range actions { switch act := act.(type) { case *spendAction: //actionKey字符串拼接 actionKey := act.AssetId.String() + act.AccountID //遍历spendActionMap if tmpAct, ok := spendActionMap[actionKey]; ok { tmpAct.Amount += act.Amount tmpAct.UseUnconfirmed = tmpAct.UseUnconfirmed || act.UseUnconfirmed } else { spendActionMap[actionKey] = act resultActions = append(resultActions, act) } default: resultActions = append(resultActions, act) } } return resultActions } 上面只是简单的贴出了核心的实现代码,如果感兴趣想仔细阅读源码,点击地址:https://git.io/fhAsw 这一期的内容我们到此就结束了,如果你感兴趣可以加入我们的社区一起讨论。如果在阅读的过程中有什么疑问可以在下方给我们留言,我们将第一时间为你解答。
我们知道HD(分层确定性)钱包,基于 BIP-32;多币种和多帐户钱包,基于 BIP-44;最近比原社区的钱包开发者对比原的BIP-32和BIP-44协议有疑问,所以我今天就专门整理了一下该协议的内容以及在比原中涉及的代码。来做一个详细的介绍! BIP-32协议 BIP32协议原英文地址: 地址:https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#extended-keys BIP32:定义 Hierarchical Deterministic wallet (简称 “HD Wallet”),是一个系统可以从单一个 seed 产生一树状结构储存多组 keypairs(私钥和公钥)。好处是可以方便的备份、转移到其他相容装置(因为都只需要 seed),以及分层的权限控制等。具体的内容细节可以翻阅文档。 那么下面我列出了在比原链中BIP-32协议的具体实现,具体的实现的代码逻辑可以在:https://github.com/Bytom/bytom/blob/master/blockchain/signers/signers.go 找到下面的方法。研究代码实现过程. 比原链中BIP32协议的代码实现: BIP-44协议 BIP-44原英文协议地址 地址: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki BIP-44协议是基于BIP-0032和BIP-0043中描述的目的方案中描述的算法定义确定性钱包的逻辑层级。赋予树状结构中的各层特殊的意义。让同一个 seed 可以支援多币种、多帐户等。各层定义如下: m / purpose' / coin_type' / account' / change / address_index //purporse': 固定值44', 代表是BIP44 //coin_type': 这个代表的是币种, 可以兼容很多种币, 比如BTC是0', ETH是60,BTM是99' //btc一般是 m/44'/0'/0'/0 //eth一般是 m/44'/60'/0'/0 //btm一般是 m/44'/99'/0'/0 HD(分层确定性)钱包树状结构提供了极大的灵活性。每一个母扩展密钥有 40 亿个子密钥:20 亿个常规子密钥和 20 亿个强化子密钥。而每个子密钥又会有 40 亿个子密钥并 且以此类推。只要你愿意,这个树结构可以无限类推到无穷代。但是,又由于有了这个灵活性,对无限的树状结构进行导航就变得异常困难。尤其是对于在不同的HD钱包之间进行转移交易,因为内部组织到内部分支以及亚分支的可能性是无穷的. BIP-43 提出使用第一个强化子索引作为特殊的标识符表示 树状结构的“purpose”。基于BIP-43,HD(分层确定性)钱包应该使用且只用第一层级的树的分 支,而且有索引号码去识别结构并且有命名空间来定义剩余的树的目的地。举个例子,HD(分层确定性)钱包只使用分支 m/i’/是为了表明那个被索引号“i”定义的特殊为目地。 在BIP-43 标准下,为了延长的那个特殊规范,BIP-44提议了多账户结构作为 “purpose”。所有遵循BIP-44的HD(分层确定性)钱包依据只使用树的第一个分支的要求而被定 义:m/44’/。 BIP-44 指定了包含5个预定义树状层级的结构: 第一层的 purpose 总是被设定为 44’。 第二层的“coin_type”特指币种并且允许多元货币 HD 钱包中的货币在第二个层级下有自己的亚树状结构。常见的数字资产定义:Bitcoin is m/44’/0’、Ethereum Testnet is m/44’/1’,以及 Bytom is m/44’/99’。 完整注册数字资产类型列表: https://github.com/satoshilabs/slips/blob/master/slip-0044.md 第三层级是“account”,这可以允许使用者为了会计或者组织目的,而去再细 分他们的钱包到独立的逻辑性亚账户。 举个例子,一个HD钱包可能包含两个比 特币“账户”:m/44’/0’/0’ 和 m/44’/0’/1’。每个账户都是它自己亚树的根。 第四层级就是“change”。每一个HD钱包有两个亚树,一个是用来接收地址一个是用来创造找零地址。注意无论先前的层级是否使用强化衍生,这一层级使用的都 是常规衍生。这是为了允许这一层级的树可以在不安全环境下,输出扩展公钥。 被HD(分层确定性)钱包衍生的可用的地址是第四层级的子级,就是第五层级的树的“address_index”。比如,第三个层级的主账户收到比原支付的地址就是 M/44’/99’/0’/0/2。 下面介绍一下比原链中BIP-44协议的实现,我们在 :https://github.com/Bytom/bytom/blob/master/blockchain/signers/signers.go 找到下面的方法。可以研究具体的代码实现过程。 比原链中BIP44协议的实现: 比原中用BIP-44生成钱包地址 在项目中找到 CreateAddress 方法,这个方法会用到BIP-44协议来创建地址: 比原现在会默认使用BIP-44协议,所以在下面的switch语句中,会默认到BIP0044. 到此BIP-32协议和BIP-44协议已经介绍完,并且在比原的代码中的实现也已经列出来了。感兴趣的小伙伴可以读代码研究详细的实现过程,如果有什么问题,欢迎在比原技术社区提问! 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom
上期我们讲了在基于比原开发过程中链外存储可以用分布式存储IPFS,这期我们还给大家介绍另外一种链外存储的解决方案。bigchaindb:https://www.bigchaindb.com,下面我们讲一下集成过程。 step1:搭建Bytom节点 比原相关资料:https://github.com/Bytom-Community/Bytom_Docs 搭建bytom节点有很多方式,然后开启RPC调用模式。这里推荐用docker搭建比原节点: docker搭建比原节点,同时开启RPC访问模式(即终端交互模式)。 我这里都是在本地操作,所以对应的端口是:9888。启动好以后我们在postman里请求测试一下,如下图: 说明我们已经搭建好了比原节点,并且可以进行远程调用。下面我们用程序去调用比原节点: step2:搭建bigchaindb节点集群 BigchainDB是集区块链去中介化等特性和分布式数据库吞吐量高等特性的一款可扩展的区块链数据库。代码托管在github上:https://github.com/bigchaindb/bigchaindb BigchainDB官网是:https://www.bigchaindb.com 官网有很多资料,是学习BigchinDB的不二去处! 要想深入研究BigchianDB,可以阅读BigchainDB白皮书《BigchainDB:A Scalable Blockchain Database》可以https://www.bigchaindb.com/whitepaper/处下载到。好,开始搭一个独立的BigchianDB节点! 搭一个BigchainDB节点 搭建节点请参考:https://blog.csdn.net/q563730343/article/details/78654314?utm_source=blogxgwz8 为了方便,这里我们直接使用bigchaindb的测试网络,如果你是自己开发的话推荐自己搭建多节点。测试网络地址:https://test.bigchaindb.com,我们用postman请求测试一下,如下图: 上图是我们请求bigchaindb测试网络的,我们可以看到测试网路已经正常返回。现在我们就可以去找对应的开发插件进行开发。 step3:比原上进行资产登记并存储到bigchaindb 上面我们已经搭建好了比原和bigchaindb的节点,下面我们进行实际的开发。 首先我们去找符合自己开发语言的sdk,这样我们可以快速上手开发。bigchaindb的sdk有很多:https://github.com/bigchaindb>,请自己去筛选符合自己的sdk。我这里用go语言的sdk: 首先将自己需要的插件包下载下来放在自己的环境变量下面(我这里以golang 插件为例),然后我们跟bigchaindb的测试网进行连接。可以让我们的程序远程调用bigchaindb测试网,与之进行交互。详细的代码我们可以参考项目里面的文件,如下图: 连接好了以后我们在比原上创在一个资产,我们调用create-asset接口:https://docs.bytom.io/mydoc_rpc_call.cn#create-asset。然后创建了资产BYTOM资产,看下图我们已经创建成功了资产。然后我们将这个资产在bigchaindb上去创建并进行交易,如下图: 上面我们已经在比原链上创建了资产,然后我们将这笔资产在bigchaindb上创建。这样比原链上的资产就可以映射到bigchaindb中做一个存储,以及附带大量的资产凭证信息。整个资产的流转信息都存储在bigchaindb中。下图是我们用程序去创建交易资产: 资产创建好了以后我们就将资产存储到bigchaindb中,然后我们请求bigchaindb就可以返回我们创建的资产。 接下来我们就可以通过类似的方法去对我们的资产进行变更,流转。每一次流转变更返回的hsah都会通过比原上的交易存储,资产的详细整个流转记录都会存储在bigchaindb中,可以有效减少主链的数据。并提升效率。 目前项目还不完整,仅供参考;项目地址:https://github.com/BytomFans/bytom-bdb
本文介绍了基于Bytom开发过程中集成IPFS。 step1: 搭建bytom节点 比原相关资料:https://github.com/Bytom-Community/Bytom_Docs 搭建bytom节点有很多方式,然后开启RPC调用模式。这里推荐用docker搭建比原节点: docker搭建比原节点,同时开启RPC访问模式(即终端交互模式)。 我这里都是在本地操作,所以对应的端口是:9888。启动好以后我们在postman里请求测试一下,如下图: 说明我们已经搭建好了比原节点,并且可以进行远程调用。 step2: 搭建IPFS节点 IPFS中文社区官网:http://ipfser.org IPFS项目地址:https://github.com/ipfs 各种语言实现源码: GO:https://github.com/ipfs/go-ipfs JavaScript:https://github.com/ipfs/js-ipfs Python:https://github.com/ipfs/js-ipfs C:https://github.com/Agorise/c-ipfs 搭建IPFS节点,这里如果只是本地开发,就只需要搭建本地单节点就足够了。接下来我们进行具体的搭建。这里可以源码搭建和docker镜像搭建,具体过程可以参考(因为它支持多种语言,这里我们以go语言版本为例): 搭建IPFS环境:https://blog.csdn.net/weixin_41160534/article/details/81358613 搭建好以后一定要开启端口监听: ipfs daemon 先在我们已经搭建好了IPFS的节点,并且开启API网络监听。现在我们来写个测试demo(go语言版本),往网络上传文件。 package main import ( "fmt" "os" "strings" shell "github.com/ipfs/go-ipfs-api" ) func main() { // Where your local node is running on localhost:5001 sh := shell.NewShell("localhost:5001") cid, err := sh.Add(strings.NewReader("hello world!")) if err != nil { fmt.Fprintf(os.Stderr, "error: %s", err) os.Exit(1) } fmt.Println("added %s", cid) err = sh.Get("QmdFhFwGHy54T3e8cf1ZmovWbZ2yW4yNhNKsu5dSfcgh6H", "/Users/huangxinglong/Desktop") if err != nil { fmt.Fprintf(os.Stderr, "error: %s", err) os.Exit(1) } } 我们运行以后发现返回Hash,然后我们可以通过Hash把上传的文件获取到 "/Users/huangxinglong/Desktop"目录现在我们就可以进行开发了。 step3: 准备开发 搭建好比原节点和IPFS节点以后我们就可以根据自己选择的开发语言选择具体的插件。 IPFS的插件有如下几种语言的API插件: 比原链的API插件主要有: 如果选择的开发语言不是java,PHP,Node.js。需要我们自己去根据比原的开发文档,然后去集成。 step4: 具体案例 目的:根据比原上的交易将信息存储在IPFS中 首先根据自己选择的开发语言去选择对应的API插件,我选择的是go语言,IPFS有go语言的API插件:https://github.com/ipfs/go-ipfs-api。我们在开发过程中直接根据比原的开发文档去开发,开发文档地址:https://docs.bytom.io/ 首先我们去下载IPFS的go语言API插件放到自己的项目中,然后放到自己的项目vender目录下面: 然后我们在项目中连接IPFS节点,同时上传一个文件。然后我们会发现返回Hash。 然后我们调用比原create-key接口和create_account接口。创建key和account,然后用给自己的地址充值BTM。通过api请求3个接口,先 build → sign → submit,分别对应的api是 build-transaction、sign-transaction、submit-transaction。 然后我们就可以创建一笔交易。 存到IPFS上返回的hash是:QmP4UDViHc78un4SyHZV2ooHkSon2EttpamqPqXQ9WHcaV,将返回的hash放在arbitrary对应的字段,如下: build-transaction: { "base_transaction": null, "actions": [{ "account_id": "0KTCS3R5G0A02", "amount": 10000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "type": "spend_account" }, { "account_id": "0KTCS3R5G0A02", "amount": 100, "asset_id": "608037f96e8d1613d900c67a0730cc90e2a03311fb7d091588f7eb551a6103cd", "type": "spend_account" }, { "account_id": "0KTCS3R5G0A02", "amount": 100, "asset_id": "608037f96e8d1613d900c67a0730cc90e2a03311fb7d091588f7eb551a6103cd", "arbitrary": "QmP4UDViHc78un4SyHZV2ooHkSon2EttpamqPqXQ9WHcaV", "type": "retire" }], "ttl": 0, "time_range": 1521625823 } 然后依次调用sign-transaction和submit-transaction接口,就可以在交易过程中将信息存储在IPFS上,如果我们还需要做更复杂的操作,再根据比原开发文档去做相应的操作。 现在我们就可以根据比原上的交易hash,找到对应的IPFS存储Hash。获取到在IPFS上存储的文件。 具体项目过程请参考:https://github.com/BytomFans/bytom-ipfs
bytom是专注资产领域的公有区块链平台,最近开发者社区基于比原做了一款资产转换平台。我们可以在上面通过自己现有的资产在比原上发行资产。然后达到资产转换的目的。 一. 以太币资产转换成比原上的资产 首先打开bytomswap, 网址:https://www.bytomswap.io然后我们选择我们要交换的资产,我们可以发现支持很多种交换的资产。因为笔者刚好手里有以太币(ETH),所以就用imToken钱包里的以太币(ETH)来做资产交换,然后在比原上发行自己的资产。在接收地址中输入比原钱包的地址。点击Next。(imToken和bycoin都可以在Apple store 非大陆区搜到)然后我们看到下图出现一个ETH的二维码地址,这个地址主要是作为我们转账ETH的二维码地址。通过imToken钱包转0.0005个以太币到上图的地址。在imtoken上点击下一步,检查我们发送的交易,确认我们的交易无误,输入密码进行交易。在imtoken上查看交易详情,我们发现交易已经确认,然后显示了交易详细收据。然后我们在bytomswap点击Next就可以看到交易完成。然后我们可以去区块链浏览器上面查看,我们已经基于比原的合约创建了一种和以太币1:1的资产。然后我们去比原的bycoin钱包上查看我们已经有了0.0005的以太币资产。然后我们在bycoin上查看交易详情,可以看到详细信息 二. 比原上的资产转换成以太资产 现在我们将比原上以太资产转换成以太币(ETH),然后将我们imtoken钱包的以太地址拷贝到图中的地址中。然后点击Next,用比原的bycoin钱包扫码,然后进行比原上的以太资产转账操作。进行转账操作,我们转出0.0015在比原上的以太资产到imtoken中。确认发出后,我们可以看到还未确认的交易信息。打开我们的imtoken钱包,发现我们已经收到0.0015的以太资产。我们发现bytomswap上已经显示资产转换已经完成。我们可以看到以太坊的区块链浏览器,交易详细信息。表示我们的资产转换已经全部完成。 到此我们在bytomswap上进行资产转换整套流程已经全部完成。
微服务和容器目前比较流行,相信很多小伙伴都比较熟悉docker, 如果你不是太了解,可以查看文档docker学习手册。那如何用docker搭建比原链(Bytom)的节点呢? 在操作之前,请自行安装docker。然后在你的终端输入(windows对应cmd): docker 出现如下图说明你已经安装成功了docker: 获取bytom的docker镜像 docker pull bytom/bytom:latest 用docker images 查看自己下载的bytom镜像 docker images 然后出现如下图说明已经获取到了镜像: 初始化: docker run -v < Bytom / data / directory / on / host / machine >:/ root /.bytom bytom:latest bytomd init --chain_id < chainId > 默认的Bytom数据目录(在主机上)是: Mac: ~/Library/Bytom Linux: ~/.bytom windows: %APPDATA%\Bytom chainId 有三种选择: mainnet:连接到主网 testnet:连接到测试网 solonet:单节点 如下例(mac/testnet): docker run -v ~/Library/Bytom:/root/.bytom bytom/bytom:latest bytomd init --chain_id testnet 开启docker终端交互模式: docker run -it -p 9888:9888 -v ~/Library/Bytom:/root/.bytom bytom/bytom:latest 开启守护进程模式: docker run -d -p 9888:9888 -v ~/Library/Bytom:/root/.bytom bytom/bytom:latest bytomd node --web.closed --auth.disable 查看正在运行的容器: docker ps 下图中我们可以看到我们在运行的容器: 最后在浏览器中请求:http://0.0.0.0:9888,可以就可以查看我们钱包。
比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 在开发合约的过程中你们有没有遇到一些问题呢?比如编译合约的过程中不能实时动态的去检查我们所编译的合约文件是否正确,那么我今天就教大家一种很方便的方法。可以让小伙伴们在编写合约的过程中,可以随时检查自己的合约编写是否正确。 首先要确保我们有go语言开发环境且版本高于1.8,如果没有搭建go语言开发环境,请自行百度。确保go支持的版本已经正确安装: $ go version $ go env GOROOT GOPATH 获取源代码并编译,参考链接:https://github.com/Bytom/equity 编译完了以后我们可以在equity下执行: ./equity/equity --help 获取合约的命令帮助。返回的截图如下: 图中标的1,2,3,4 分别表示执行命令所带参数的含义。图中3对应的 instance参数表示实例化合约,4对应的shift表示指定执行合约里面具体的函数。然后在项目下面创建一个合约文件(合约文件最好不带任何后缀名),如下图: 然后编写合约,我是用vim编译的合约,大家可以自行选择用vim或者编辑器编写合约。如果编译合约的过程中存在问题,请参考合约开发文档:https://bytom.github.io/mydoc_RPC_call.cn.html。下图是我在vim中编写的合约。 合约编写完了以后,如果合约编写错误或者存在语法错误,会出现如下图所示的情况,请检查自己编写的合约 检查无误以后,在对应的目录下面执行合约文件,然后就可以输出下图所示的二进制。说明合约编写成功
比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 用比原链(Bytom)实现信息上链 很多了解比原链的都知道,比原链是专注信息和数字资产在链上交互和流转的公链项目,信息上链不是比原链核心能力,所以并没有在钱包端做一个功能入口,但是比原链提供了相关的接口可以将一些信息写到链上去。那如何实现信息上链呢?使用特殊的Retire操作,这个操作可以进行销毁资产的操作,但因为其可以附带信息,所以就可以实现信息上链的功能。 请往下看,也用postman请求演示,然后用golang写了一个接口的demo, 在用golang代码实现之前,我们先要做一些准备工作。 首先确保自己在本地已经搭建好了比原的节点,如果你还没有搭建好节点,请参考开发文档:https://docs.bytom.io/mydoc_build_environment.cn.html 确保自己账户是有足够BTM测试币,如果没有可以去比原链水龙头领取BTM测试币,领取地址: http://test.blockmeta.com/faucet.php 发行自己的资产,参考:http://8btc.com/forum.php?mod=viewthread&tid=242940&extra= 信息上链的本质就是其实就是创建并发送一笔交易,我们都知道通过api发起交易主要有三个步骤,先 build → sign → submit,分别对应的api是 build-transaction、sign-transaction、submit-transaction。用postman请求过程如下: 请求build-transaction接口: 请求参数: { "base_transaction": null, "actions": [{ "account_id": "0KTCS3R5G0A02", "amount": 10000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "type": "spend_account" }, { "account_id": "0KTCS3R5G0A02", "amount": 100, "asset_id": "608037f96e8d1613d900c67a0730cc90e2a03311fb7d091588f7eb551a6103cd", "type": "spend_account" }, { "account_id": "0KTCS3R5G0A02", "amount": 100, "asset_id": "608037f96e8d1613d900c67a0730cc90e2a03311fb7d091588f7eb551a6103cd", "arbitrary": "77656c636f6d65efbc8ce6aca2e8bf8ee69da5e588b0e58e9fe5ad90e4b896e7958c", "type": "retire" }], "ttl": 0, "time_range": 1521625823 } 请求sign-transaction接口: 请求参数: { "password": "huangxinglong123", "transaction": { "allow_additional_actions": false, "local": true, "raw_transaction": "0701dfd5c8d505020160015e560352e415b41be7648b2241ffdabf56259bc618525f62ac123dce32002110f0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0989fe3020001160014adb6632c5b10c6d5b6f97b8d1250f6e409e11c0101000161015f560352e415b41be7648b2241ffdabf56259bc618525f62ac123dce32002110f0608037f96e8d1613d900c67a0730cc90e2a03311fb7d091588f7eb551a6103cd9cc5b191f3190101160014dcfd9b78c24260823e318153665d511d6c4ecb1b010003013dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0ebbcde02011600147a9baebd37dba3f14960624ed8e6ca3cc9d5f73800013e608037f96e8d1613d900c67a0730cc90e2a03311fb7d091588f7eb551a6103cdb8c4b191f31901160014f0370fdf7a7bec7b34cc62fd5291071a3dc3d9b0000147608037f96e8d1613d900c67a0730cc90e2a03311fb7d091588f7eb551a6103cd6401246a2277656c636f6d65efbc8ce6aca2e8bf8ee69da5e588b0e58e9fe5ad90e4b896e7958c00", "signing_instructions": [{ "position": 0, "witness_components": [{ "keys": [{ "derivation_path": [ "0000002c", "00000099", "0100000000000000", "0100000000000000", "4600000000000000" ], "xpub": "1c03161a08a4dbb7df153815a28f733fec1ac7579f954c4834e5ce9f0ad8deb260ecb2066a8623b69aa936f5798f4dcb9572bc476f2c8171953ce054d58a759f" }], "quorum": 1, "signatures": null, "type": "raw_tx_signature" }, { "type": "data", "value": "4f089176a5bca95ec9227b8a87dfec947c59453805bf46d3f5a18f8032255b5a" }] }, { "position": 1, "witness_components": [{ "keys": [{ "derivation_path": [ "0000002c", "00000099", "0100000000000000", "0100000000000000", "4700000000000000" ], "xpub": "1c03161a08a4dbb7df153815a28f733fec1ac7579f954c4834e5ce9f0ad8deb260ecb2066a8623b69aa936f5798f4dcb9572bc476f2c8171953ce054d58a759f" }], "quorum": 1, "signatures": null, "type": "raw_tx_signature" }, { "type": "data", "value": "67512f9250f559699e32c72c8af29096b1556af145f6ecc0c306e6acc88bbfaa" }] }] } } 请求submit-transaction接口: 请求参数: { "raw_transaction": "0701dfd5c8d505020160015e560352e415b41be7648b2241ffdabf56259bc618525f62ac123dce32002110f0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0989fe3020001160014adb6632c5b10c6d5b6f97b8d1250f6e409e11c01630240c7004022db674ff2961b540d4edab846d550429ae9a92311ba375a4f452331422961fdcde3bf79631755dd12df409e24a849158d4aeab919cab81520fb7d1e02204f089176a5bca95ec9227b8a87dfec947c59453805bf46d3f5a18f8032255b5a0161015f560352e415b41be7648b2241ffdabf56259bc618525f62ac123dce32002110f0608037f96e8d1613d900c67a0730cc90e2a03311fb7d091588f7eb551a6103cd9cc5b191f3190101160014dcfd9b78c24260823e318153665d511d6c4ecb1b6302406b75ef5a9decfa31d4f5ae06e0fb14ca507ba4a03715874d1d831516945121573b9b858e4d7527d209c1f89f74e0aa4c4e38afd098cbadaff31b9107167099012067512f9250f559699e32c72c8af29096b1556af145f6ecc0c306e6acc88bbfaa03013dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0ebbcde02011600147a9baebd37dba3f14960624ed8e6ca3cc9d5f73800013e608037f96e8d1613d900c67a0730cc90e2a03311fb7d091588f7eb551a6103cdb8c4b191f31901160014f0370fdf7a7bec7b34cc62fd5291071a3dc3d9b0000147608037f96e8d1613d900c67a0730cc90e2a03311fb7d091588f7eb551a6103cd6401246a2277656c636f6d65efbc8ce6aca2e8bf8ee69da5e588b0e58e9fe5ad90e4b896e7958c00" } 响应参数: { "status": "success", "data": { "tx_id": "5ef27b930646d468bbb436d3406972ff201aa63702518f777e31dd6a2147dddc" } } 用上面返回的tx_id去比原的浏览器中去查看交易详情,就可以查看到我们上传的数据 参考代码: package main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" ) //build-transaction params //https://bytom.github.io/mydoc_RPC_call.cn.html#build-transaction type BytomAccount struct { AccountId string `json:"account_id"` Amount int `json:"amount"` AssetId string `json:"asset_id"` //Arbitrary string `json:"arbitrary"` Type string `json:"type"` } type BytomAccount1 struct { AccountId string `json:"account_id"` Amount int `json:"amount"` AssetId string `json:"asset_id"` Arbitrary string `json:"arbitrary"` Type string `json:"type"` } type BaseTransaction struct{} type TransactionParams struct { BaseTransaction *BaseTransaction `json:"base_transaction"` Actions []interface{} `json:"actions"` Ttl int `json:"ttl"` TimeRange int `json:"time_range"` } //sign-transaction params //https://bytom.github.io/mydoc_RPC_call.cn.html#build-transaction type Transaction struct { } type SignParams struct { Password string `json:"password"` Transaction Transaction `json:"transaction"` } //submit-transaction //https://bytom.github.io/mydoc_RPC_call.cn.html#build-transaction type SubmitParams struct { RawTransaction string `json:"raw_transaction"` } type SubmitResponse struct { TxId string `json:"tx_id"` } func main() { account1, account2, account3 := BytomAccount{}, BytomAccount{}, BytomAccount1{} account1.AccountId = "0KTCS3R5G0A02" account1.Amount = 10000000 account1.AssetId = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" account1.Type = "spend_account" account2.AccountId = "0KTCS3R5G0A02" account2.Amount = 100 account2.AssetId = "608037f96e8d1613d900c67a0730cc90e2a03311fb7d091588f7eb551a6103cd" account2.Type = "spend_account" account3.AccountId = "0KTCS3R5G0A02" account3.Amount = 100 account3.AssetId = "608037f96e8d1613d900c67a0730cc90e2a03311fb7d091588f7eb551a6103cd" account3.Arbitrary = "77656c636f6d65efbc8ce6aca2e8bf8ee69da5e588b0e58e9fe5ad90e4b896e7958c" account3.Type = "retire" //var array var actions []interface{} //append three params array_actions := append(actions, account1, account2, account3) transaction_params := &TransactionParams{} transaction_params.Actions = array_actions transaction_params.Ttl = 0 transaction_params.TimeRange = 1521625823 //本地测试网节点 //build-transaction port := "http://127.0.0.1:9888/build-transaction" value, err := SendTransactionRetire(transaction_params, port) if err != nil { fmt.Println("err:", err) } fmt.Println("build-transaction接口返回的参数:", value) //sign-transaction //........... //submit-transaction //........... } //send post request func SendTransactionRetire(params *TransactionParams, port string) (v interface{}, err error) { //以本地测试网节点连接 ParamsStr, err := json.Marshal(params) if err != nil { return nil, err } jsonStr := bytes.NewBuffer(ParamsStr) fmt.Println(jsonStr) req, err := http.NewRequest("POST", port, jsonStr) req.Header.Set("Content-Type", "application/json") req.Header.Add("Accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var bodyBytes []byte if resp.StatusCode == 200 { bodyBytes, err = ioutil.ReadAll(resp.Body) if err != nil { return nil, err } } return string(bodyBytes), nil } 上面的代码只是build-transaction一个步骤,另外sign-transaction和submit-transaction请求需要自己去组织参数进行请求。请求完submit-transaction 获得返回的交易hash,去区块链浏览器上查看自己的上链信息,区块链浏览器地址:http://52.82.46.157:8082/。 好了,通过以上的4个步骤,我们就可以借助比原链实现信息上链。如果你有什么疑问或者不明白,请在我们的社区联系我们,https://github.com/Bytom/bytom。
0XX API错误 BTM000", "Bytom API Error"非比原标准错误 BTM001", "Request timed out"API请求超时 BTM002", "Invalid request body"非法的API请求体 1XX为网络错误 BTM103", "A peer core is operating on a different blockchain network"区块链网络类型不匹配 2xx是签名相关的错误 BTM200 :"Quorum must be greater than 1 and less than or equal to the length of xpubs“ 需要签名的个数超过实际需求签名的个数 BTM201 :”Invalid xpub format"签名格式错误 BTM202 :"At least one xpub is required"缺少主公钥 BTM204 : "Root XPubs cannot contain the same key more than once"主公钥重复 7XX为交易相关的错误 72X - 73X 构建交易错误 BTM700 : "Funds of account are insufficient"资产余额不足 BTM701 : "Available funds of account are immature"coinbase交易未成熟,币不可花费 BTM702 : "Available UTXOs of account have been reserved资产被锁定五分钟,不可花费(一般密码输入错误会产生) BTM703 : "Not found UTXO with given hash"UTXO不属于当前钱包 BTM704 : "Invalid action type"action类型不存在 BTM705 : "Invalid action object"action输入内容错误 BTM706 : "Invalid action construction"action结构错误(只有输入或者只有输出) BTM707 : "One or more fields are missing"action输入内容缺失 BTM708 : "Invalid asset amount"资产数量格式错误(超过最大数量) BTM709 : "Not found account"账户不存在 BTM710 : "Not found asset"资产不存在 73X - 75X 验证交易错误 BTM730 :"Invalid transaction version"交易版本不对 BTM731 :"Invalid transaction size"交易大小不能为0 BTM732 :"Invalid transaction time range"超出交易时间范围,用于将停留时间过久的未确认交易作废 BTM733 : "Not standard transaction"不是标准的交易,使用合约地址接受BTM时报错 BTM734 : "Invalid coinbase transaction"非法coinbase交易 BTM735 : "Invalid coinbase assetID"非法的coinbase资产ID BTM736 : "Invalid coinbase arbitrary size"coinbase尺寸过大,附加数据超过一定限制 BTM737 : "No results in the transaction"交易action hash缺失 BTM738 : "Mismatched assetID"不匹配的资产ID,发布资产时资产ID错误 BTM739 : "Mismatched value source/dest position"不匹配的action位置 BTM740 : "Mismatched reference"不匹配的引用 BTM741 : "Mismatched value"不匹配的值,action的资产值不匹配 BTM742 : "Missing required field"不匹配的字段,action输入的资产值类型不匹配 BTM743 : "No source for value"输入源不存在 BTM744 : "Arithmetic overflow/underflow"计算溢出,资产计算值超出限制 BTM745 : "Invalid source or destination position"action位置不匹配 BTM746 : "Unbalanced asset amount between input and output"输入输出非BTM资产总量不平衡 BTM747 : "Gas credit has been spent"UTXO数量超过上限(当前为21个) BTM748 : "Gas usage calculate got a math error"Gas运算错误 76X - 78X 虚拟机错误 BTM760 :"Alt stack underflow"子虚拟机栈溢出 BTM761 : "Bad value"非法栈数据 BTM762 : "Wrong context"context值错误,context为虚拟机执行上下文 BTM763 : "Data stack underflow"虚拟机数据溢出 BTM764 : "Disallowed opcode"虚拟机指令不存在 BTM765 : "Division by zero"除零错误 BTM766 : "False result for executing VM"虚拟机执行结果为Fasle BTM767 : "Program size exceeds max int32"合约的字节大小超过int32上限 BTM768 : "Arithmetic range error"计算出错 BTM769 : "RETURN executed"执行opfail指令返回的结果 BTM770 : "Run limit exceeded because the BTM Fee is insufficient"Gas费用不足,引起合约终止 BTM771 : "Unexpected end of program"合约程序参数输入错误 BTM772 : "Unrecognized token"不识别的虚拟机指令数据 BTM773 : "Unexpected error"异常错误 BTM774 : "Unsupported VM because the version of VM is mismatched"虚拟机版本不匹配 BTM775 : "VERIFY failed"verify指令执行失败 8XX 为HSM相关错误 BTM800 :"Key Alias already exists"密钥别名重复 BTM801 :"Invalid after in query"此错误已废弃 BTM802 : "Key not found or wrong password"密钥不存在或者密码错误 BTM803 : "Requested key aliases exceeds limit"此错误已废弃 BTM804 :"Could not decrypt key with given passphrase"解密流程失败 BTM860", "Request could not be authenticated"access token错误
比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom tx_signer Java implementation of signing transaction offline to bytomd. Pre Get the source code $ git clone https://github.com/Bytom/bytom.git $GOPATH/src/github.com/bytom git checkout $ git checkout dev Why need dev branch? Because you could call decode transaction api from dev branch and obtain tx_id and some inputs ids. Build $ cd $GOPATH/src/github.com/bytom $ make bytomd # build bytomd $ make bytomcli # build bytomcli When successfully building the project, the bytom and bytomcli binary should be present in cmd/bytomd and cmd/bytomcli directory, respectively. Initialize First of all, initialize the node: $ cd ./cmd/bytomd $ ./bytomd init --chain_id solonet launch $ ./bytomd node --mining Usage Build jar first get source code git clone https://github.com/successli/tx_signer.git get jar package $ mvn assembly:assembly -Dmaven.test.skip=true You can get a jar with dependencies, and you can use it in your project. Test cases Need 3 Parameters: Private Keys Array Template Object After call build transaction api return a Template json object. build transaction api use bytom java sdk return a Template object. Raw Transaction call decode raw-transaction api from dev branch. decode raw-transaction api Call method: // return a Template object signed offline basically. Template result = signatures.generateSignatures(privates, template, rawTransaction); // use result's raw_transaction call sign transaction api to build another data but not need password or private key. Single-key Example: @Test // 使用 SDK 来构造 Template 对象参数, 单签 public void testSignSingleKey() throws BytomException { Client client = Client.generateClient(); String asset_id = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; String address = "sm1qvyus3s5d7jv782syuqe3qrh65fx23lgpzf33em"; // build transaction obtain a Template object Template template = new Transaction.Builder() .addAction( new Transaction.Action.SpendFromAccount() .setAccountId("0G0NLBNU00A02") .setAssetId(asset_id) .setAmount(40000000) ) .addAction( new Transaction.Action.SpendFromAccount() .setAccountId("0G0NLBNU00A02") .setAssetId(asset_id) .setAmount(300000000) ) .addAction( new Transaction.Action.ControlWithAddress() .setAddress(address) .setAssetId(asset_id) .setAmount(30000000) ).build(client); logger.info("template: " + template.toJson()); // use Template object's raw_transaction id to decode raw_transaction obtain a RawTransaction object RawTransaction decodedTx = RawTransaction.decode(client, template.rawTransaction); logger.info("decodeTx: " + decodedTx.toJson()); // need a private key array String[] privateKeys = new String[]{"10fdbc41a4d3b8e5a0f50dd3905c1660e7476d4db3dbd9454fa4347500a633531c487e8174ffc0cfa76c3be6833111a9b8cd94446e37a76ee18bb21a7d6ea66b"}; logger.info("private key:" + privateKeys[0]); // call offline sign method to obtain a basic offline signed template Signatures signatures = new SignaturesImpl(); Template basicSigned = signatures.generateSignatures(privateKeys, template, decodedTx); logger.info("basic signed raw: " + basicSigned.toJson()); // call sign transaction api to calculate whole raw_transaction id // sign password is None or another random String Template result = new Transaction.SignerBuilder().sign(client, basicSigned, ""); logger.info("result raw_transaction: " + result.toJson()); // success to submit transaction } Multi-keys Example: Need an account has two or more keys. @Test // 使用 SDK 来构造 Template 对象参数, 多签 public void testSignMultiKeys() throws BytomException { Client client = Client.generateClient(); String asset_id = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; String address = "sm1qvyus3s5d7jv782syuqe3qrh65fx23lgpzf33em"; // build transaction obtain a Template object // account 0G1RPP6OG0A06 has two keys Template template = new Transaction.Builder() .setTtl(10) .addAction( new Transaction.Action.SpendFromAccount() .setAccountId("0G1RPP6OG0A06") .setAssetId(asset_id) .setAmount(40000000) ) .addAction( new Transaction.Action.SpendFromAccount() .setAccountId("0G1RPP6OG0A06") .setAssetId(asset_id) .setAmount(300000000) ) .addAction( new Transaction.Action.ControlWithAddress() .setAddress(address) .setAssetId(asset_id) .setAmount(30000000) ).build(client); logger.info("template: " + template.toJson()); // use Template object's raw_transaction id to decode raw_transaction obtain a RawTransaction object RawTransaction decodedTx = RawTransaction.decode(client, template.rawTransaction); logger.info("decodeTx: " + decodedTx.toJson()); // need a private key array String[] privateKeys = new String[]{"08bdbd6c22856c5747c930f64d0e5d58ded17c4473910c6c0c3f94e485833a436247976253c8e29e961041ad8dfad9309744255364323163837cbef2483b4f67", "40c821f736f60805ad59b1fea158762fa6355e258601dfb49dda6f672092ae5adf072d5cab2ceaaa0d68dd3fe7fa04869d95afed8c20069f446a338576901e1b"}; logger.info("private key 1:" + privateKeys[0]); logger.info("private key 2:" + privateKeys[1]); // call offline sign method to obtain a basic offline signed template Signatures signatures = new SignaturesImpl(); Template basicSigned = signatures.generateSignatures(privateKeys, template, decodedTx); logger.info("basic signed raw: " + basicSigned.toJson()); // call sign transaction api to calculate whole raw_transaction id // sign password is None or another random String Template result = new Transaction.SignerBuilder().sign(client, basicSigned, ""); logger.info("result raw_transaction: " + result.toJson()); // success to submit transaction } Multi-keys and Multi-inputs Example: @Test // 使用 SDK 来构造 Template 对象参数, 多签, 多输入 public void testSignMultiKeysMultiInputs() throws BytomException { Client client = Client.generateClient(); String asset_id = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; String address = "sm1qvyus3s5d7jv782syuqe3qrh65fx23lgpzf33em"; // build transaction obtain a Template object Template template = new Transaction.Builder() .setTtl(10) // 1 input .addAction( new Transaction.Action.SpendFromAccount() .setAccountId("0G1RPP6OG0A06") // Multi-keys account .setAssetId(asset_id) .setAmount(40000000) ) .addAction( new Transaction.Action.SpendFromAccount() .setAccountId("0G1RPP6OG0A06") .setAssetId(asset_id) .setAmount(300000000) ) // 2 input .addAction( new Transaction.Action.SpendFromAccount() .setAccountId("0G1Q6V1P00A02") // Multi-keys account .setAssetId(asset_id) .setAmount(40000000) ) .addAction( new Transaction.Action.SpendFromAccount() .setAccountId("0G1Q6V1P00A02") .setAssetId(asset_id) .setAmount(300000000) ) .addAction( new Transaction.Action.ControlWithAddress() .setAddress(address) .setAssetId(asset_id) .setAmount(60000000) ).build(client); logger.info("template: " + template.toJson()); // use Template object's raw_transaction id to decode raw_transaction obtain a RawTransaction object RawTransaction decodedTx = RawTransaction.decode(client, template.rawTransaction); logger.info("decodeTx: " + decodedTx.toJson()); // need a private key array String[] privateKeys = new String[]{"08bdbd6c22856c5747c930f64d0e5d58ded17c4473910c6c0c3f94e485833a436247976253c8e29e961041ad8dfad9309744255364323163837cbef2483b4f67", "40c821f736f60805ad59b1fea158762fa6355e258601dfb49dda6f672092ae5adf072d5cab2ceaaa0d68dd3fe7fa04869d95afed8c20069f446a338576901e1b", "08bdbd6c22856c5747c930f64d0e5d58ded17c4473910c6c0c3f94e485833a436247976253c8e29e961041ad8dfad9309744255364323163837cbef2483b4f67"}; logger.info("private key 1:" + privateKeys[0]); logger.info("private key 2:" + privateKeys[1]); // call offline sign method to obtain a basic offline signed template Signatures signatures = new SignaturesImpl(); Template basicSigned = signatures.generateSignatures(privateKeys, template, decodedTx); logger.info("basic signed raw: " + basicSigned.toJson()); // call sign transaction api to calculate whole raw_transaction id // sign password is None or another random String Template result = new Transaction.SignerBuilder().sign(client, basicSigned, ""); logger.info("result raw_transaction: " + result.toJson()); // success to submit transaction }
比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 国密算法是指国家密码管理局制定的自主可控的国产算法,包括一系列密码学算法:SM1、SM2、SM3、SM4、SM7、SM9、以及祖冲之算法。最常用的三种商用密码算法是 SM2椭圆曲线公钥密码算法、SM3密码杂凑算法以及 SM4分组密码算法。 其中,SM2 算法属于椭圆曲线公钥密码系统,相较于 RSA 公钥密码系统,这种新型的公钥密码系统拥有加解密速度更快,使用的密钥更短的优点。SM2算法密钥长度为192至256位长度的安全性就能达到 RSA 算法2048至4096位密钥长度的安全要求。SM2的优异性能取决于求解椭圆曲线离散对数问题的困难性。对于一般椭圆曲线的离散对数问题,目前只存在指数级计算复杂度的求解方法,与大数分解问题及有限域上离散对数问题相比,椭圆曲线离散对数问题的求解难度要大得多。因此,在相同安全程度要求下,椭圆曲线密码较其它公钥密码所需的密钥规模要小得多。SM2数字签名算法适用于商用密码应用中的数字签名和验证,可满足多种密码应用中的身份鉴别和数据完整性、真实性的安全需求。在比原链主网中,交易的签名和验证使用的是 Ed25519签名算法,而在国密测试网中,使用 SM2算法替代。 SM3密码杂凑算法是哈希算法的一种,适用于商用密码应用中的数字签名和验证、消息认证码的生成与验证以及随机数的生成,可以满足多种密码应用的安全需求。在比原链主网中,在获取交易和区块头等摘要的过程中使用的哈希算法是 SHA3算法,而在国密测试网中,使用 SM3算法替代。 SM4分组密码算法是一种对称加密算法,使用同一个密钥对信息进行加密和解密。在比原链主网中,对用户的钱包进行加解密使用的是 AES-128算法,而在国密测试网中,使用 SM4算法替代。 2014年国务院办公厅就颁发了《国务院办公厅转发密码局等部门关于金融领域密码应用指导意见》,该意见就指出在我国涉及到金融领域信息安全的产品和系统要自主可控,到2020年实现国产密码在金融领域中的全面应用。而实际上,我国的金融信息安全产品的国产化率已经大幅度提前达到目标。在金融领域使用国产加密标准是机构走向合规化的重要一步。 比原链作为一种原子资产的交互协议,其宗旨是连通原子世界与比特世界,促进资产在两个世界间的交互和流转。为了完成这个目标,在国密测试网上使用国密密码学加密标准不仅仅是保障资产安全的重要措施,也是比原链满足政策要求的重要举措。 开发者体验国密测试网方式: 下载国密测试网源码: $ git clone ne https://github.com/bytom/bytom-gm.git $GO $GOPATH/src/rc/github.com/bytom-gm 安装: $ cd $GOPATH/src/rc/github.com/bytom-gm $ m $ make install 初次启动需要配置: $ bytomd init --chain_id --home 其中,可以选择gm-testnet或者solonet。 gm-testnet 启动的是国密测试网。 solonet 启动的是单节点网络。 <data_and_config_path>指定的是数据存放的目录。 启动节点: $ bytomd node --mining --home 开发者获取国密网测试币的方式可以在启动节点时开启--mining选项。 国密测试网的操作体验与主网类似,但是主网的地址前缀为bm,而国密测试网的地址前缀为gm。 目前,比原链正在按照原有计划执行,技术开发每周都发布一个稳定的迭代版本。目前已经发布了7个迭代版本,而社区运营也在有条不紊的进行,政策合规化也在积极与相关机构洽谈。可以说,比原链的项目进展伴随着国密测试网的发布更上一层楼。
比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 背景知识 Bytom 使用的 密钥类型为基于 ed25519 的 chainkd.XPub 代码见 bytom/crypto/ed25519/chainkd 文档见 https://chain.com/docs/1.2/protocol/specifications/chainkd` 预备 代码修改 首先适当修改代码,添加一些打印输出,使得我们更好的进行验证。 如果只是想看 xpub ,而不关心 xprv(对于验证导入导出助记词功能来说已经够了)的话可以跳过这一步,不必修改源码 。 在 bytom/blockchain/pseudohsm/pseudohsm.go 中的 func (h *HSM) createKeyFromMnemonic(alias string, auth string, mnemonic string) (*XPub, error) 添加输出打印 root XPub 对应的 私钥 和 公钥 func (h *HSM) createKeyFromMnemonic(alias string, auth string, mnemonic string) (*XPub, error) { // Generate a Bip32 HD wallet for the mnemonic and a user supplied password seed := mnem.NewSeed(mnemonic, "") xprv, xpub, err := chainkd.NewXKeys(bytes.NewBuffer(seed)) if err != nil { return nil, err } fmt.Println(hex.EncodeToString(xprv[:])) // Add info printing fmt.Println(hex.EncodeToString(xpub[:])) // Add info printing id := uuid.NewRandom() key := &XKey{ ID: id, KeyType: "bytom_kd", XPub: xpub, XPrv: xprv, Alias: alias, } file := h.keyStore.JoinPath(keyFileName(key.ID.String())) if err := h.keyStore.StoreKey(file, key, auth); err != nil { return nil, errors.Wrap(err, "storing keys") } return &XPub{XPub: xpub, Alias: alias, File: file}, nil } 工具准备 因为 bytomd 目前 dashboard 钱包图形界面还没有助记词相关功能,我们需要准备工具使用 POST 请求来使用。比如 curl 或者 postman 。 获取密钥对应助记词 dashboard 目前还没有 从 密钥导出助记词的功能,现有只是在创建密钥时有助记词相应输出信息。可以修改源码在查看密钥时 打印对应助记词。 因为只是测试,这里为了方便,直接查看在创建密钥时返回的助记词。 对 bytomd 发起 post 请求 /create-key { "alias": "create_key_test", "password": "createkeytest", "language": "en" } 可以看到 响应中 显示了 xpub 和 助记词 由于修改了源码,添加了打印信息,bytomd 命令行也打印出了相印的 xprv 和 xpub 步骤 如何通过导入助记词恢复密钥呢? 假设目前 已经有一个 xprv 50db5bfe21b08462972eadbce08ec92d078a45fa7a280d175a823f9e457faf447d1f501b69f895b830138fabc6f91e2b3b3c8df26642a34be4af27886b9134dc 对应的 助记词为 pudding room business river pattern box snap merit unfold speak hat task 发起 post 请求 /create-key { "alias": "nnemonic_test", "password": "nnemonicnnemonic", "nnemonic": "pudding room business river pattern box snap merit unfold speak hat task", "language": "en" } 返回相应: bytomd 输出 可以看到,恢复出来的 xprv 和 我们本来的 xprv 一致,验证成功。 dashaboard 中也能见到我们恢复的 密钥。
比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 发行资产 在比原链上发行资产比较方便快捷,使用节点的dashboard图形界面操作就可以,先在 “资产”一栏新建资产 新建完资产,需要上链,否则区块不认识你这个资产,那么在交易中的高级交易进行issue,点击Add action,总共三个action分别是 Issue、Control with address、Spend from account 输入账户的密码,等待交易确认,一旦确认成功资产就会发到刚才指定的地址上。可以只发一个地址,也可以发到多个地址,添加多个Control with address 即可。 编译合约 编译合约通过api进行编译,准备好postman或者其他可以post的工具,笔者使用的是postman。笔者看了官方的多个合约模版,于是准备部署一个简单的比较数字大小的合约。即是编译合约时会指定数值,如果解锁合约的人带上大于此数值则可以获得合约里的资产。笔者的合约代码如下: contract equals(target:Integer) locks value{ clause reveal(current:Integer){ verify current >= target unlock value } } 写好了合约代码,接下来通过 compile 的api 对合约进行编译 编译后会返回一个status 成功或者失败,和一串 data,都是为json格式的数据,主要需要 data里面的 program,即是该编译完成的合约程序,在部署合约时需要用到。 部署合约 部署合约其实就是 发起交易,在交易中带上合约程序。通过api发起交易主要有三个步骤,先 build → sign → submit,分别对应的api是 build-transaction、sign-transaction、submit-transaction。 build-transaction sign-transaction build-transaction 返回的数据,全部需要用到,即是 data 的所有数据,对该打包好的数据进行签名,需要创建账号的密钥密码。 如果签名成功的话,要看返回的json里面有个sign_complete字段,此字段为true才代表成功,方可进行submit-transaction。 submit-transaction 获取 sign-transaction 签名成功返回的 raw_transaction 字段数据,进行提交交易。 如果提交成功的话,会返回交易哈希即tx_id,可以通过 get-transaction 获取该交易的详情。在此合约就部署完成啦,对应数量的资产也已经被锁在合约里面了,等待解锁合约,解锁成功的人可以获得合约里的资产。 获取部署好的合约unspent-output 通过 list-unspent-outputs 获取未花费的合约详情,在解锁合约的时候也需要用到这个 unspent-output id。这里的id 可以在 get-transaction 的outputs 中找到,在outputs中找到 control_program 为刚才部署的合约program,则此笔交易的id就是这个合约的unpsent-output。 解锁合约 build-transaction 根据以上获得的unspent-output来解锁合约,其实解锁合约也是发起交易,和发布合约不同的是不需要编译,只需要build、sign、submit即可。build这里比较不一样,笔者解锁合约时在此处踩过太多的坑~~ 根据以上的参数进行build-transaction即可,注意value需要转为十六进制!!如果符号合约条件即可解锁成功,否则区块确认解锁合约的交易时会失败。 sign-transaction和submit-transaction 参考部署合约的就可以,完全是一样的。到此,发布和解锁合约就全部完成啦。学会了吗,快去实践哦,有问题也可以联系笔者哦 小胖子
作者:芈橙 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 本文解析的为比原提供的币币合约 模板如下: contract TradeOffer(assetRequested: Asset, amountRequested: Amount, seller: Program, cancelKey: PublicKey) locks offered { clause trade() requires payment: amountRequested of assetRequested { lock payment with seller unlock offered } clause cancel(sellerSig: Signature) { verify checkTxSig(cancelKey, sellerSig) unlock offered } } 导读: 初次接触比原只能合约的请点击比原智能合约入门 和 Equity 语言入门 学习,方便更好的理解该文档 锁定合约 第一步:调用create-account-receiver 生成 control_program 以下是相关代码片段: sendHttpPost("{\"account_id\":\"0IJVD7MNG0A02\"}","create-account-receiver","http://127.0.0.1:9888",""); 第二步调用list-pubkeys 获取 pubkey 以下是相关代码片段: sendHttpPost("{\"account_id\":\"0IJVD7MNG0A02\"}","list-pubkeys","http://127.0.0.1:9888",""); 第三步: 将1 2步获取的值调用compile接口编译合约获得program 合约程序 以下是相关代码片段: JSONObject param=new JSONObject(); JSONArray agrs=new JSONArray(); //合约的四个参数值 JSONObject assetParam=new JSONObject(); assetParam.put("string","81d097312645696daea84b761d2898d950d8fba0de06c9267d8513b16663dd3a"); agrs.put(assetParam); JSONObject amountParam=new JSONObject(); amountParam.put("integer",200000000l); agrs.put(amountParam); JSONObject programParam=new JSONObject(); programParam.put("string",control_program); agrs.put(programParam); JSONObject publicKeyParam=new JSONObject(); publicKeyParam.put("string",pubkey); agrs.put(publicKeyParam); param.put("agrs",agrs); param.put("contract","contract TradeOffer(assetRequested: Asset, amountRequested: Amount, seller: Program, cancelKey: PublicKey) locks offered { clause trade() requires payment: amountRequested of assetRequested { lock payment with seller unlock offered } clause cancel(sellerSig: Signature) { verify checkTxSig(cancelKey, sellerSig) unlock offered } }"); //调用编译合约 sendHttpPost(param.toString(),"list-pubkeys","http://127.0.0.1:9888",""); 第四步:将program 传入build-transaction接口去build一个交易的到data 以下是相关代码片段: param=new JSONObject(); agrs=new JSONArray(); JSONObject spendAccount=new JSONObject(); spendAccount.put("account_id","0H757LPD00A02"); spendAccount.put("amount",9909099090000l); spendAccount.put("asset_id","161b9767b664df907fa926a31f9e835236e57f3e9ccc5f80c12bd97723322652"); spendAccount.put("type","spend_account"); agrs.put(spendAccount); JSONObject controlAccount=new JSONObject(); controlAccount.put("control_program",program); controlAccount.put("amount",9909099090000l); controlAccount.put("asset_id","161b9767b664df907fa926a31f9e835236e57f3e9ccc5f80c12bd97723322652"); controlAccount.put("type","control_program"); agrs.put(controlAccount); JSONObject spendAccount2=new JSONObject(); spendAccount2.put("account_id","0H757LPD00A02"); spendAccount2.put("amount",6000000l); spendAccount2.put("asset_id","ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); spendAccount2.put("type","spend_account"); agrs.put(spendAccount2); param.put("actions",agrs); param.put("ttl",0); sendHttpPost(param.toString(),"build-transaction","http://127.0.0.1:9888",""); 第五步:输入密码调用sign-transaction签名第四步build的data 得到raw_transaction 以下是相关代码片段: param=new JSONObject(); param.put("password","xxx"); param.put("transaction",data); sendHttpPost(param.toString(),"sign-transaction","http://127.0.0.1:9888",""); 第六步:调用submit-transactions提交交易 以下是相关代码片段: param=new JSONObject(); param.put("raw_transaction",raw_transaction); sendHttpPost(param.toString(),"submit-transactions","http://127.0.0.1:9888",""); 解锁/取消合约 首先需要decode出生成合约时候的参数 调用list-unspent-outputs 获取生成的合约信息获取program 以下是相关代码片段: param=new JSONObject(); param.put("id",outputid); param.put("smart_contract",true); sendHttpPost(param.toString(),"list-unspent-outputs","http://127.0.0.1:9888",""); 调用decode-program 传入获取生成的合约参数信息 以下是相关代码片段: param=new JSONObject(); param.put("program",program); sendHttpPost(param.toString(),"decode-program","http://127.0.0.1:9888",""); 需要注意的是decode出来的为值是逆序的(后续会有文章详细介绍) 解锁/取消其实就是把生成合约的步骤中的第三步去掉,替换调用生成合约第四步的参数即可 取消合约的构造参数如下: spendAccountUnspentOutput = arguments: [{ type: 'raw_tx_signature', // 生成合约第二步的pubkeylist 详情 raw_data: { derivation_path: pubkeylist.pubkey_infos[0].derivation_path, xpub: pubkeylist.root_xpub } }, { type: 'data', raw_data: { // 参数偏移量 在一个合约里是固定的 value: '13000000' } }], output_id: output_id, type: 'spend_account_unspent_output' } const controlAction = { type: 'control_program', amount: 100000000, asset_id: asset_id, control_program:control_program } const gasAction = { type: 'spend_account', account_id:account_id, asset_alias: 'BTM', amount: 50000000 } 执行合约的参数构造如下: const spendAccountUnspentOutput = { arguments: [{ type: 'data', raw_data: { // 00000000 指的是第一个 clause,表示直接执行,无需跳转 value: '00000000' } }], output_id: output_id, type: 'spend_account_unspent_output' } // 合约执行提供的资产 const issueControlAction = { control_program: control_program, amount: 100000000, asset_id: asset_id, type: 'control_program' } // 合约执行提供的资产 const issueSpendAction = { account_id: account_id, amount: 100000000, asset_id: asset_id, type: 'spend_account' } // 矿工费 const gasAction = { type: 'spend_account', account_id: account_id, asset_alias: 'BTM', amount: 50000000 } // 合约执行获得资产对象 const controlAction = { type: 'control_program', amount: 100000000, asset_id: asset_id, control_program: compileData.control_program } build 操作其实就是指定输入输出的过程,详情请查看 官方build文档 和 官方api文档 备注 调用比原基于okhttp接口javautil 如下: public static String sendHttpPost(String bodyStr,String method,String bytomApiserverUrl,String bytomApiserverToken) throws IOException { OkHttpClient client = new OkHttpClient(); MediaType mediaType = MediaType.parse("application/json"); RequestBody body = RequestBody.create(mediaType, bodyStr); Request request = new Request.Builder() .url(bytomApiserverUrl+"/"+method) .post(body) .addHeader("cache-control", "no-cache") .addHeader("Connection", "close") .build(); if (bytomApiserverUrl==null || bytomApiserverUrl.contains("127.0.0.1") || bytomApiserverUrl.contains("localhost")){ }else { byte[] encodedAuth = Base64.encodeBase64(bytomApiserverToken.getBytes(Charset.forName("US-ASCII"))); String authHeader = "Basic " + new String(encodedAuth); request = new Request.Builder() .url(bytomApiserverUrl+"/"+method) .post(body) .addHeader("authorization", authHeader) .addHeader("cache-control", "no-cache") .addHeader("Connection", "close") .build(); } Response response = client.newCall(request).execute(); return response.body().string(); }
准备工作: 1、安装全节点钱包V1.0.5以上并同步完成; 2、已经发行一种资产,发行资产的方法具体见文章《如何在Bytom上发布资产?》 3、准备好一些BTM作为手续费; 设置谜语(锁定资产): 1、打开钱包,点击Equity合约按钮; 2、进入Equity合约页面,点击加载模板,可以看见猜谜合约; 3、点击猜谜合约,进入猜谜合约模板; 4、在锁定资产页面中,选择我们要奖励给猜对答案的资产奖励(当前不支持直接锁定BTM,所以我们需要新建一种资产进行锁定); 设置锁定GOLD资产,选择想要的数量(注意:1=1诺=0.00000001单位,所以你如果想给别人发1单位的资产,需要填入100000000),填入密码以及Gas费用(合约建议固定给0.4btm); 5、设置猜谜的谜底(正确答案),比如我填写了“计算即权力”,那么猜谜的人解锁时候输入这五个字即可解锁并获得锁定的资产,点击锁定资产。 6、猜谜合约成功设置,猜谜合约也是一种特殊的交易,所以需要等待交易上链; 7、交易成功上链,合约已设置完毕; 8、进入“查看详情”,找到这笔0.1 GOLD的资产ID,把资产ID发给需要猜谜的人,即可猜谜获得资产。 开始猜谜(解锁合约资产): 1、先要获取合约的ID,如上面那个:df2f4b844f9af89bb4ba3b9552cdbdd48836cb345067f82d5332f3f831efae30 2、进入Equity合约页面,进入合约解锁页面,输入上面的资产ID; 3、如果合约已经上链,那么进入解锁页面,可以看到锁定合约的内容及被锁定的资产和数量; 4、输入答案,指定资产的解锁账户,密码和交易Gas(仍然建议0.4 BTM),点击解锁资产; 5、解锁资产成功,解锁资产仍然是一笔特殊的交易,需要等待验证成功后,资产才会进入你的账户; 猜谜合约介绍完毕,可以看看你的账户是不是得到奖励了哦。 结语:当前猜谜合约提供了体验版本,有兴趣的朋友可以做一个口令红包Dapp,简化一下用户操作和体验。
比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom Bytom-Mobile-Wallet-SDK 是从bytom源码中抽离出的钱包层代码,并且对钱包层代码进行了改造。使用gomobile可以将代码 编译成Android和iOS平台可用的SDK,使用编译后的Android和iOS钱包SDK可以在移动端实现创建bytom密钥、账户、地址和交易签名功能。 Bytom-Mobile-Wallet-SDK源码简介 SDK源码放在项目的sdk文件夹中,android和ios文件夹是使用SDK的demo项目,bind.go 中首字母大写可以外部调用的函数会作为提供给Android和iOS调用的API。bytom创建的密钥对会存储在磁盘单独的文件中,而且对私钥进行了加密,账户地址数据是存储在go实现的leveldb中,所以Android和iOS平台也需要提供数据存储的路径。 func InitWallet(storagePath string) { hsm := pseudohsm.New(storagePath) walletDB := db.NewDB("wallet", "leveldb", storagePath) accounts := account.NewManager(walletDB) assets := asset.NewRegistry(walletDB) wallet := aWallet.NewWallet(walletDB, accounts, assets, hsm) api = aApi.API{Wallet: wallet} } Android和iOS平台调用其他钱包API的之前需要先调用InitWallet这个API,参数是磁盘上的绝对路径,InitWallet会对整个钱包进行一个初始化, 其中最重要是初始化leveldb的存储。其他的CreateKey、CreateAccount、CreateAccountReceiver是创建密钥、账户、地址等API,RestoreWallet API能够对钱包所有账户地址资产进行备份导出json格式的数据。 Bytom-Mobile-Wallet-SDK的编译 SDK代码的编译首先需要正确的安装golang和gomobile,golang需要1.7以上版本。 Android平台需要安装JDK、Android SDK、Android NDK,并且需要将Android SDK的platform-tools、ndk-bundle 添加到PATH系统环境变量中。iOS平台编译环境配置相对比较简单只需要安装Xcode就可以了。 Clone项目到本地$GOPATH/src下: git clone https://github.com/Bytom-Community/Bytom-Mobile-Wallet-SDK $GOPATH/src/github.com/bytom-community/mobile Android gomobile init -ndk ~/path/to/your/ndk cd $GOPATH/src/github.com/bytom-community/mobile gomobile bind -target=android github.com/bytom-community/mobile/sdk/ 如果需要减小SDK的体积给gomobile bind指令加上-ldflags=-s参数: gomobile bind -target=android -ldflags=-s github.com/bytom-community/mobile/sdk/ 执行指令后会在mobile文件夹生成wallet.aar和wallet-sources.jar文件。 iOS cd $GOPATH/src/github.com/bytom-community/mobile gomobile bind -target=ios github.com/bytom-community/mobile/sdk/ 如果需要减小SDK的体积给gomobile bind指令加上-ldflags=-w参数: $ gomobile bind -target=ios -ldflags=-w github.com/bytom-community/mobile/sdk/ 执行指令后会在mobile文件夹生成wallet.framework文件。 由于gomobile现在没有支持bitcode,所以生成的iOS SDK也不支持bitcode。 Bytom-Mobile-Wallet-SDK的使用 Android 拷贝wallet.aar和wallet-sources.ja到Android项目的app的libs文件夹下,并在app module中的build.gradle文件中添加: android { repositories { flatDir { dirs 'libs' } } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation(name: 'wallet', ext: 'aar') } sync project后可以在Android项目中对SDK的API进行调用: package io.bytom.community; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.widget.TextView; import wallet.Wallet; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView keyTextView = (TextView) findViewById(R.id.key_textview); String storagePath = getFilesDir().toString(); Log.d("storagePath", storagePath); Wallet.initWallet(storagePath); String keyResult = Wallet.createKey("Marshall", "123456"); Log.d("keyResult", keyResult); keyTextView.setText(keyResult); } } iOS 通过项目target的Linked frameworks and libraries把wallet.framework添加到项目,可以在iOS项目中对SDK的API进行调用: #import "ViewController.h" #import "Wallet/Wallet.h" // Gomobile bind generated framework @interface ViewController () @end @implementation ViewController @synthesize textLabel; - (void)loadView { [super loadView]; NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]; WalletInitWallet(docPath); textLabel.text = WalletCreateKey(@"kevin",@"123456"); } @end
比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 矿机配置 https://gist.github.com/HAOYUatHZ/a47400bde4a138825faef415387b532c 固件升级 https://service.bitmain.com.cn/support 两个都要刷,先后顺序没关系 update_1000.tar.gz 升级时间较长,升级期间请勿断电 配置节点 测试时可以考虑切换到 testnet 分支降低难度使cpu挖矿也能出块,./bytomd init --chain_id testnet 或 ./bytomd init --chain_id solonet init/node 初始化/启动时可以加上 -r "your/directory" 指定数据目录,若目录不存在则会自动新建该目录 流程 1、初始化节点先建个账户、地址,不然就挖到空地址 2、矿地址支持自定义,包括 非本地钱包地址 3、API doc 4、矿池向节点 getwork get-work 得到的 block_header 是动态压缩变长的需要进行解析 使用 golang 的话可以利用 "github.com/bytom/protocol/bc/types" 中 block_header.go 中的函数 UnmarshalText 使用别的语言的话参考 "github.com/bytom/protocol/bc/types" 中 block.go 中的函数 UnmarshalText, readFrom, ReadVarintXXX. ReadVarintXXX 需要参考 go函数 binary.ReadUvarint 5、解析完后进行下发 通信格式参考 https://github.com/Bytom/B3-Mimic/blob/master/docs/STRATUM-BTM.md - 收到任务有 login 和 矿池主动下发, 没走 getjob, 只走 login 和 池主动下发 - 这俩都是用 submit 提交 逻辑参考 https://github.com/Bytom/B3-Mimic/blob/master/main.go - Version, Height, Timestamp, Bits 要转小端 - 关于 target + btc.com 分享了一段 antpool 的代码 ~, 并说 target 用以对 bits 对应的 difficulty 放松难度,用来使矿机在单位时间内能够有提交,然后矿池再验证~ var Diff1 = StringToBig("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") func GetTargetHex(diff int64) string { padded := make([]byte, 32) diffBuff := new(big.Int).Div(Diff1, big.NewInt(diff)).Bytes() copy(padded[32-len(diffBuff):], diffBuff) buff := padded[0:4] targetHex := hex.EncodeToString(Reverse(buff)) return targetHex } 矿池下发的targethex是拿 标准难度(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) / 一个难度值得出的 这个值叫做矿池难度 一般会动态调整 保证矿机提交 share 的频率是稳定的 比如1分钟提交三次 提交得快了就会增加这个值 慢了就会降低这个值 target 是 16 进制的难度,1, 1024, …..等等,和前导 0 的个数有关,动态调整用来保证矿机每分钟至少提交三次,用来计算矿机算力以及防止矿机算力作弊 ffff3f00 对应 1024,c5a70000 对应 100001 6、提交完之后矿池需要做验证 header_hash 使用 golang 的话可以利用 "github.com/bytom/protocol/bc/types" 中 types.BlockHeader{} 的 Hash() 使用别的语言的话参考 https://github.com/Bytom/B3-Mimic/blob/master/docs/blhr_hash_V3.go 然后就要开始用 tensority 算 hash 结果 很遗憾现在 go 版本、cpp_openblas 版本、cpp_simd 版本都达不到理想的验证效果, 如果想做一个可用的矿池目前有必要上 gpu, 可以考虑 n 卡 1050,或者阿里云服务器 P4 cpp 的 tensority 逻辑在这里,并指出了如何针对 gpu 进行优化的建议,这样矩阵乘法能够跑进 2.5 ms, 整个 tensority 大概 6 ms init matlist 有开销,seed 其实 256 个区块才改变一次, 遇到新的 seed 每次 gpu tensority 可能需要 100 ms,但做了 cache 的话 init matlist 可以忽略,可以认为每次 tensority 只需要不超过 6 ms 用 golang 可以 cgo 调用 c 代码,参考 https://github.com/Bytom/bytom/blob/dev-ts-simd/mining/tensority/algorithm.go 改好 gpu 版本后可以参照这个进行调用 7、验证通过后使用 submit-work 接口进行提交 提交的结果 也是 BlockHeader type 的 使用 golang 的话可以利用 "github.com/bytom/protocol/bc/types" 中 block_header.go 中的函数 MmarshalText 使用别的语言的话参考 "github.com/bytom/protocol/bc/types" 中 block.go 中的函数 MarshalText, WriteTo, WriteVarintXXX. WriteVarintXXX 需要参考 go函数 binary.PutUvarint 8、retarget 见上面,动态调整使矿机每分钟提交三次 9、收益计算 略 批量转账 主网地址 bm 开头,长度普通地址42,多签62 工具 https://github.com/Bytom/bytom/tree/master/tools/sendbulktx 每次发币都会生成新的找零地址 bytom input有21个的限制
比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 该部分主要针对用户自己管理私钥和地址,并通过utxo来构建和发送交易。 1.创建私钥和公钥 2.根据公钥创建接收对象 3.找到可花费的utxo 4.通过utxo构造交易 5.组合交易的input和output构成交易模板 6.对构造的交易进行签名 7.提交交易上链 注意事项: 以下步骤以及功能改造仅供参考,具体代码实现需要用户根据实际情况进行调试,具体可以参考单元测试案例代码blockchain/txbuilder/txbuilder_test.go#L255 1.创建私钥和公钥 该部分功能可以参考代码crypto/ed25519/chainkd/util.go#L11,可以通过 NewXKeys(nil) 创建主私钥和主公钥 func NewXKeys(r io.Reader) (xprv XPrv, xpub XPub, err error) { xprv, err = NewXPrv(r) if err != nil { return } return xprv, xprv.XPub(), nil } 2.根据公钥创建接收对象 接收对象包含两种形式:address形式和program形式,两者是一一对应的,任选其一即可。其中创建单签地址参考代码account/accounts.go#L267进行相应改造为: func (m *Manager) createP2PKH(xpub chainkd.XPub) (*CtrlProgram, error) { pubKey := xpub.PublicKey() pubHash := crypto.Ripemd160(pubKey) // TODO: pass different params due to config address, err := common.NewAddressWitnessPubKeyHash(pubHash, &consensus.ActiveNetParams) if err != nil { return nil, err } control, err := vmutil.P2WPKHProgram([]byte(pubHash)) if err != nil { return nil, err } return &CtrlProgram{ Address: address.EncodeAddress(), ControlProgram: control, }, nil } 创建多签地址参考代码account/accounts.go#L294进行相应改造为: func (m *Manager) createP2SH(xpubs []chainkd.XPub) (*CtrlProgram, error) { derivedPKs := chainkd.XPubKeys(xpubs) signScript, err := vmutil.P2SPMultiSigProgram(derivedPKs, len(derivedPKs)) if err != nil { return nil, err } scriptHash := crypto.Sha256(signScript) // TODO: pass different params due to config address, err := common.NewAddressWitnessScriptHash(scriptHash, &consensus.ActiveNetParams) if err != nil { return nil, err } control, err := vmutil.P2WSHProgram(scriptHash) if err != nil { return nil, err } return &CtrlProgram{ Address: address.EncodeAddress(), ControlProgram: control, }, nil } 3.找到可花费的utxo 找到可花费的utxo,其实就是找到接收地址或接收program是你自己的unspend_output。其中utxo的结构为:(参考代码account/reserve.go#L39) // UTXO describes an individual account utxo. type UTXO struct { OutputID bc.Hash SourceID bc.Hash // Avoiding AssetAmount here so that new(utxo) doesn't produce an // AssetAmount with a nil AssetId. AssetID bc.AssetID Amount uint64 SourcePos uint64 ControlProgram []byte AccountID string Address string ControlProgramIndex uint64 ValidHeight uint64 Change bool } 涉及utxo构造交易的相关字段说明如下: SourceID 前一笔关联交易的mux_id, 根据该ID可以定位到前一笔交易的output AssetID utxo的资产ID Amount utxo的资产数目 SourcePos 该utxo在前一笔交易的output的位置 ControlProgram utxo的接收program Address utxo的接收地址 上述这些utxo的字段信息可以从get-block接口返回结果的transaction中找到,其相关的结构体如下:(参考代码api/block_retrieve.go#L26) // BlockTx is the tx struct for getBlock func type BlockTx struct { ID bc.Hash `json:"id"` Version uint64 `json:"version"` Size uint64 `json:"size"` TimeRange uint64 `json:"time_range"` Inputs []*query.AnnotatedInput `json:"inputs"` Outputs []*query.AnnotatedOutput `json:"outputs"` StatusFail bool `json:"status_fail"` MuxID bc.Hash `json:"mux_id"` } //AnnotatedOutput means an annotated transaction output. type AnnotatedOutput struct { Type string `json:"type"` OutputID bc.Hash `json:"id"` TransactionID *bc.Hash `json:"transaction_id,omitempty"` Position int `json:"position"` AssetID bc.AssetID `json:"asset_id"` AssetAlias string `json:"asset_alias,omitempty"` AssetDefinition *json.RawMessage `json:"asset_definition,omitempty"` Amount uint64 `json:"amount"` AccountID string `json:"account_id,omitempty"` AccountAlias string `json:"account_alias,omitempty"` ControlProgram chainjson.HexBytes `json:"control_program"` Address string `json:"address,omitempty"` } utxo跟get-block返回结果的字段对应关系如下: `SourceID` - `json:"mux_id"` `AssetID` - `json:"asset_id"` `Amount` - `json:"amount"` `SourcePos` - `json:"position"` `ControlProgram` - `json:"control_program"` `Address` - `json:"address,omitempty"` 4.通过utxo构造交易 通过utxo构造交易就是使用spend_account_unspent_output的方式来花费指定的utxo。 第一步,通过utxo构造交易输入TxInput和签名需要的数据信息SigningInstruction,该部分功能可以参考代码account/builder.go#L169进行相应改造为: // UtxoToInputs convert an utxo to the txinput func UtxoToInputs(xpubs []chainkd.XPub, u *UTXO) (*types.TxInput, *txbuilder.SigningInstruction, error) { txInput := types.NewSpendInput(nil, u.SourceID, u.AssetID, u.Amount, u.SourcePos, u.ControlProgram) sigInst := &txbuilder.SigningInstruction{} if u.Address == "" { return txInput, sigInst, nil } address, err := common.DecodeAddress(u.Address, &consensus.ActiveNetParams) if err != nil { return nil, nil, err } switch address.(type) { case *common.AddressWitnessPubKeyHash: derivedPK := xpubs[0].PublicKey() sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness([]byte(derivedPK))) case *common.AddressWitnessScriptHash: derivedPKs := chainkd.XPubKeys(xpubs) script, err := vmutil.P2SPMultiSigProgram(derivedPKs, len(derivedPKs)) if err != nil { return nil, nil, err } sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness(script)) default: return nil, nil, errors.New("unsupport address type") } return txInput, sigInst, nil } 第二步,通过utxo构造交易输出TxOutput该部分功能可以参考代码protocol/bc/types/txoutput.go#L20: // NewTxOutput create a new output struct func NewTxOutput(assetID bc.AssetID, amount uint64, controlProgram []byte) *TxOutput { return &TxOutput{ AssetVersion: 1, OutputCommitment: OutputCommitment{ AssetAmount: bc.AssetAmount{ AssetId: &assetID, Amount: amount, }, VMVersion: 1, ControlProgram: controlProgram, }, } } 5.组合交易的input和output构成交易模板 通过上面已经生成的交易信息构造交易txbuilder.Template,该部分功能可以参考blockchain/txbuilder/builder.go#L92进行改造为: type InputAndSigInst struct { input *types.TxInput sigInst *SigningInstruction } // Build build transactions with template func BuildTx(inputs []InputAndSigInst, outputs []*types.TxOutput) (*Template, *types.TxData, error) { tpl := &Template{} tx := &types.TxData{} // Add all the built outputs. tx.Outputs = append(tx.Outputs, outputs...) // Add all the built inputs and their corresponding signing instructions. for _, in := range inputs { // Empty signature arrays should be serialized as empty arrays, not null. in.sigInst.Position = uint32(len(inputs)) if in.sigInst.WitnessComponents == nil { in.sigInst.WitnessComponents = []witnessComponent{} } tpl.SigningInstructions = append(tpl.SigningInstructions, in.sigInst) tx.Inputs = append(tx.Inputs, in.input) } tpl.Transaction = types.NewTx(*tx) return tpl, tx, nil } 6.对构造的交易进行签名 账户模型是根据密码找到对应的私钥对交易进行签名,这里用户可以直接使用私钥对交易进行签名,可以参考签名代码blockchain/txbuilder/txbuilder.go#L82进行改造为:(以下改造仅支持单签交易,多签交易用户可以参照该示例进行改造) // Sign will try to sign all the witness func Sign(tpl *Template, xprv chainkd.XPrv) error { for i, sigInst := range tpl.SigningInstructions { h := tpl.Hash(uint32(i)).Byte32() sig := xprv.Sign(h[:]) rawTxSig := &RawTxSigWitness{ Quorum: 1, Sigs: []json.HexBytes{sig}, } sigInst.WitnessComponents = append([]witnessComponent(rawTxSig), sigInst.WitnessComponents...) } return materializeWitnesses(tpl) } 7.提交交易上链 该步骤无需更改任何内容,直接参照wiki中提交交易的APIsubmit-transaction的功能即可
作者:Derek 简介 Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 本章介绍Derek解读-Bytom源码分析-创世区块 作者使用MacOS操作系统,其他平台也大同小异 Golang Version: 1.8 创世区块介绍 区块链里的第一个区块创被称为创世区块。它是区块链里面所有区块的共同祖先。 在比原链中创世区块被硬编码到bytomd中,每一个比原节点都始于同一个创世区块,这能确保创世区块不会被改变。每个节点都把创世区块作为区块链的首区块,从而构建了一个安全的、可信的区块链。 获取创世区块 ./bytomcli get-block 0 { "bits": 2161727821137910500, "difficulty": "15154807", "hash": "a75483474799ea1aa6bb910a1a5025b4372bf20bef20f246a2c2dc5e12e8a053", "height": 0, "nonce": 9253507043297, "previous_block_hash": "0000000000000000000000000000000000000000000000000000000000000000", "size": 546, "timestamp": 1524549600, "transaction_merkle_root": "58e45ceb675a0b3d7ad3ab9d4288048789de8194e9766b26d8f42fdb624d4390", "transaction_status_hash": "c9c377e5192668bc0a367e4a4764f11e7c725ecced1d7b6a492974fab1b6d5bc", "transactions": [ { "id": "158d7d7c6a8d2464725d508fafca76f0838d998eacaacb42ccc58cfb0c155352", "inputs": [ { "amount": 0, "arbitrary": "496e666f726d6174696f6e20697320706f7765722e202d2d204a616e2f31312f323031332e20436f6d707574696e6720697320706f7765722e202d2d204170722f32342f323031382e", "asset_definition": {}, "asset_id": "0000000000000000000000000000000000000000000000000000000000000000", "type": "coinbase" } ], "outputs": [ { "address": "bm1q3jwsv0lhfmndnlag3kp6avpcq6pkd3xy8e5r88", "amount": 140700041250000000, "asset_definition": {}, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "control_program": "00148c9d063ff74ee6d9ffa88d83aeb038068366c4c4", "id": "e3325bf07c4385af4b60ad6ecc682ee0773f9b96e1cfbbae9f0f12b86b5f1093", "position": 0, "type": "control" } ], "size": 151, "status_fail": false, "time_range": 0, "version": 1 } ], "version": 1 } 使用bytomcli客户端查询高度为0的区块信息。我们可以看到以上输出结果。 bits: 目标值,挖矿时计算的hash之后要小于等于的目标值则新块构建成功 difficulty: 难度值,矿工找到下一个有效区块的难度。该参数并不存储在区块链上,是由bits计算得出 hash: 当前区块hash height: 当前区块高度 nonce: 随机数,挖矿时反复使用不同的nonce来生成不同哈希值 previous_block_hash: 当前区块的父区块hash值 size: 当前区块的字节数 timestamp: 出块时间 transaction_merkle_root: 创世区块的merkle树根节点 transactions: 当前块中的utxo交易 由于创世区块是第一个块,创世区块的父区块,也就是previous_block_hash参数,默认情况下为0000000000000000000000000000000000000000000000000000000000000000 时间戳timestamp为1524549600,时间为2018-04-24 14:00:00也就是比原链上主网的时间。 源码分析 获取区块链状态 ** protocol/protocol.go ** func NewChain(store Store, txPool *TxPool) (*Chain, error) { // ... storeStatus := store.GetStoreStatus() if storeStatus == nil { if err := c.initChainStatus(); err != nil { return nil, err } storeStatus = store.GetStoreStatus() } // ... } 当我们第一次启动比原链节点时,store.GetStoreStatus会从db中获取存储状态,获取存储状态的过程是从LevelDB中查询key为blockStore的数据,如果查询出错则认为是第一次运行比原链节点,那么就需要初始化比原主链。 初始化主链 ** protocol/protocol.go ** func (c *Chain) initChainStatus() error { genesisBlock := config.GenesisBlock() txStatus := bc.NewTransactionStatus() for i := range genesisBlock.Transactions { txStatus.SetStatus(i, false) } if err := c.store.SaveBlock(genesisBlock, txStatus); err != nil { return err } utxoView := state.NewUtxoViewpoint() bcBlock := types.MapBlock(genesisBlock) if err := utxoView.ApplyBlock(bcBlock, txStatus); err != nil { return err } node, err := state.NewBlockNode(&genesisBlock.BlockHeader, nil) if err != nil { return err } return c.store.SaveChainStatus(node, utxoView) } 初始化主链有几步操作: config.GenesisBlock()获取创世区块 设置创世区块中所有交易状态 存储创世区块到LevelDB state.NewUtxoViewpoint()用于临时小部分utxo状态存储集合 实例化BlockNode,BlockNode用于选择最佳链作为主链 保存最新主链状态 被硬编码的创世区块 ** config/genesis.go ** func genesisTx() *types.Tx { contract, err := hex.DecodeString("00148c9d063ff74ee6d9ffa88d83aeb038068366c4c4") if err != nil { log.Panicf("fail on decode genesis tx output control program") } txData := types.TxData{ Version: 1, Inputs: []*types.TxInput{ types.NewCoinbaseInput([]byte("Information is power. -- Jan/11/2013. Computing is power. -- Apr/24/2018.")), }, Outputs: []*types.TxOutput{ types.NewTxOutput(*consensus.BTMAssetID, consensus.InitialBlockSubsidy, contract), }, } return types.NewTx(txData) } func mainNetGenesisBlock() *types.Block { tx := genesisTx() txStatus := bc.NewTransactionStatus() txStatus.SetStatus(0, false) txStatusHash, err := bc.TxStatusMerkleRoot(txStatus.VerifyStatus) if err != nil { log.Panicf("fail on calc genesis tx status merkle root") } merkleRoot, err := bc.TxMerkleRoot([]*bc.Tx{tx.Tx}) if err != nil { log.Panicf("fail on calc genesis tx merkel root") } block := &types.Block{ BlockHeader: types.BlockHeader{ Version: 1, Height: 0, Nonce: 9253507043297, Timestamp: 1524549600, Bits: 2161727821137910632, BlockCommitment: types.BlockCommitment{ TransactionsMerkleRoot: merkleRoot, TransactionStatusHash: txStatusHash, }, }, Transactions: []*types.Tx{tx}, } return block } mainNetGenesisBlock主要有如下操作: 生成创世区块中的交易,默认就一笔交易 设置块中的交易状态为false 将创世区块设置为merkle树的根节点 实例化Block块并返回 genesisTx函数生成创世区块中的交易,默认就一笔交易,一笔交易中包含input输入和output输出。 input输入: 输入中有一句话"Information is power. -- Jan/11/2013. Computing is power. -- Apr/24/2018."这是为了纪念Aaron Swartz的精神 output输出: 输出中我们看到consensus.InitialBlockSubsidy创世区块的奖励。总共140700041250000000/1e8 = 1407000412。也就是14亿个BTM币。 计算即权力 引用比原链创始人长铗的话: 4月24号,我们主网上线,信息即权力,2013年Jaruary11;计算即权力,2018年April24。这句话是为了纪念Aaron Swartz的精神,信息即权力可以视为互联网宣言,致力于信息自由传播,让公民隐私得到保护。计算即权力,致力于让资产自由的交易,自由的流动,让公民的财富得到保护,我觉得这是非常好的纪念。
作者:Derek 简介 Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 本章介绍Derek解读-Bytom源码分析-持久化存储LevelDB 作者使用MacOS操作系统,其他平台也大同小异 Golang Version: 1.8 LevelDB介绍 比原链默认使用leveldb数据库。Leveldb是一个google实现的非常高效的kv数据库。LevelDB是单进程的服务,性能非常之高,在一台4核Q6600的CPU机器上,每秒钟写数据超过40w,而随机读的性能每秒钟超过10w。 由于Leveldb是单进程服务,不能同时有多个进程进行对一个数据库进行读写。同一时间只能有一个进程,或一个进程多并发的方式进行读写。 比原链在数据存储层上存储所有链上地址、资产交易等信息。 LevelDB的增删改查操作 LevelDB是google开发的一个高性能K/V存储,本节我们介绍下LevelDB如何对LevelDB增删改查。 package main import ( "fmt" dbm "github.com/tendermint/tmlibs/db" ) var ( Key = "TESTKEY" LevelDBDir = "/tmp/data" ) func main() { db := dbm.NewDB("test", "leveldb", LevelDBDir) defer db.Close() db.Set([]byte(Key), []byte("This is a test.")) value := db.Get([]byte(Key)) if value == nil { return } fmt.Printf("key:%v, value:%v\n", Key, string(value)) db.Delete([]byte(Key)) } // Output // key:TESTKEY, value:This is a test. 以上Output是执行该程序得到的输出结果。 该程序对leveld进行了增删改查操作。dbm.NewDB得到db对象,在/tmp/data目录下会生成一个叫test.db的目录。该目录存放该数据库的所有数据。 db.Set 设置key的value值,key不存在则新建,key存在则修改。 db.Get 得到key中value数据。 db.Delete 删除key及value的数据。 比原链的数据库 默认情况下,数据存储目录在--home参数下的data目录。以Darwin平台为例,默认数据库存储在 $HOME/Library/Bytom/data。 accesstoken.db token信息(钱包访问控制权限) core.db 核心数据库,存储主链相关数据。包括块信息、交易信息、资产信息等 discover.db 分布式网络中端到端的节点信息 trusthistory.db txdb.db 存储交易相关信息 txfeeds.db 目前比原链代码版本未使用该功能,暂不介绍 wallet.db 本地钱包数据库。存储用户、资产、交易、utox等信息 以上所有数据库都由database模块管理 比原数据库接口 在比原链中数据持久化存储由database模块管理,但是持久化相关接口在protocol/store.go中 type Store interface { BlockExist(*bc.Hash) bool GetBlock(*bc.Hash) (*types.Block, error) GetStoreStatus() *BlockStoreState GetTransactionStatus(*bc.Hash) (*bc.TransactionStatus, error) GetTransactionsUtxo(*state.UtxoViewpoint, []*bc.Tx) error GetUtxo(*bc.Hash) (*storage.UtxoEntry, error) LoadBlockIndex() (*state.BlockIndex, error) SaveBlock(*types.Block, *bc.TransactionStatus) error SaveChainStatus(*state.BlockNode, *state.UtxoViewpoint) error } BlockExist 根据hash判断区块是否存在 GetBlock 根据hash获取该区块 GetStoreStatus 获取store的存储状态 GetTransactionStatus 根据hash获取该块中所有交易的状态 GetTransactionsUtxo 缓存与输入txs相关的所有utxo GetUtxo(*bc.Hash) 根据hash获取该块内的所有utxo LoadBlockIndex 加载块索引,从db中读取所有block header信息并缓存在内存中 SaveBlock 存储块和交易状态 SaveChainStatus 设置主链的状态,当节点第一次启动时,节点会根据key为blockStore的内容判断是否初始化主链。 比原链数据库key前缀 ** database/leveldb/store.go ** var ( blockStoreKey = []byte("blockStore") blockPrefix = []byte("B:") blockHeaderPrefix = []byte("BH:") txStatusPrefix = []byte("BTS:") ) blockStoreKey 主链状态前缀 blockPrefix 块信息前缀 blockHeaderPrefix 块头信息前缀 txStatusPrefix 交易状态前缀 GetBlock查询块过程分析 ** database/leveldb/store.go ** func (s *Store) GetBlock(hash *bc.Hash) (*types.Block, error) { return s.cache.lookup(hash) } ** database/leveldb/cache.go ** func (c *blockCache) lookup(hash *bc.Hash) (*types.Block, error) { if b, ok := c.get(hash); ok { return b, nil } block, err := c.single.Do(hash.String(), func() (interface{}, error) { b := c.fillFn(hash) if b == nil { return nil, fmt.Errorf("There are no block with given hash %s", hash.String()) } c.add(b) return b, nil }) if err != nil { return nil, err } return block.(*types.Block), nil } GetBlock函数最终会执行lookup函数。lookup函数总共操作有两步: 从缓存中查询hash值,如果查到则返回 如果为从缓存中查询到则回调fillFn回调函数。fillFn回调函数会将从磁盘上获得到块信息存储到缓存中并返回该块的信息。 fillFn回调函数实际上调取的是database/leveldb/store.go下的GetBlock,它会从磁盘中获取block信息并返回。
作者:Derek 简介 Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 本章介绍bytom代码P2P网络中upnp端口映射 作者使用MacOS操作系统,其他平台也大同小异 Golang Version: 1.8 UPNP介绍 UPNP(Universal Plug and Play)通用即插即用。UPNP端口映射将一个外部端口映射到一个内网ip:port。从而实现p2p网络从外网能够穿透网关访问到内网的bytomd节点。 UPNP协议 SSDP(Simple Service Discovery Protocol 简单服务发现协议) GENA(Generic Event Notification Architecture 通用事件通知结构) SOAP(Simple Object Access Protocol 简单对象访问协议) XML(Extensible Markup Language 可扩张标记语言) UPNP代码 ** p2p/upnp/upnp.go ** 发现网络中支持UPNP功能的设备 从网络中发现支持UPNP功能的设备,并得到该设备的location和url等相关信息 type upnpNAT struct { serviceURL string // 设备的描述文件URL,用于得到该设备的描述信息 ourIP string // 节点本地ip地址 urnDomain string // 设备类型 } func Discover() (nat NAT, err error) { ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900") if err != nil { return } conn, err := net.ListenPacket("udp4", ":0") if err != nil { return } socket := conn.(*net.UDPConn) defer socket.Close() err = socket.SetDeadline(time.Now().Add(3 * time.Second)) if err != nil { return } st := "InternetGatewayDevice:1" // 多播请求:M-SEARCH SSDP协议定义的发现请求。 buf := bytes.NewBufferString( "M-SEARCH * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "ST: ssdp:all\r\n" + "MAN: \"ssdp:discover\"\r\n" + "MX: 2\r\n\r\n") message := buf.Bytes() answerBytes := make([]byte, 1024) for i := 0; i < 3; i++ { // 向239.255.255.250:1900发送一条多播请求 _, err = socket.WriteToUDP(message, ssdp) if err != nil { return } // 如果从网络中发现UPNP设备则会从239.255.255.250:1900收到响应消息 var n int n, _, err = socket.ReadFromUDP(answerBytes) for { n, _, err = socket.ReadFromUDP(answerBytes) if err != nil { break } answer := string(answerBytes[0:n]) if strings.Index(answer, st) < 0 { continue } // HTTP header field names are case-insensitive. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 // 获得设备location locString := "\r\nlocation:" answer = strings.ToLower(answer) locIndex := strings.Index(answer, locString) if locIndex < 0 { continue } loc := answer[locIndex+len(locString):] endIndex := strings.Index(loc, "\r\n") if endIndex < 0 { continue } // 获得设备的描述url和设备类型 locURL := strings.TrimSpace(loc[0:endIndex]) var serviceURL, urnDomain string serviceURL, urnDomain, err = getServiceURL(locURL) if err != nil { return } var ourIP net.IP ourIP, err = localIPv4() if err != nil { return } nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP.String(), urnDomain: urnDomain} return } } err = errors.New("UPnP port discovery failed.") return } 添加端口映射 向upnp设备发送一条http post请求,将内部网络ip:port和外部网络ip:port做映射 func (n *upnpNAT) AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) { // A single concatenation would break ARM compilation. message := "<u:AddPortMapping xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" + "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) message += "</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>" message += "<NewInternalPort>" + strconv.Itoa(internalPort) + "</NewInternalPort>" + "<NewInternalClient>" + n.ourIP + "</NewInternalClient>" + "<NewEnabled>1</NewEnabled><NewPortMappingDescription>" message += description + "</NewPortMappingDescription><NewLeaseDuration>" + strconv.Itoa(timeout) + "</NewLeaseDuration></u:AddPortMapping>" var response *http.Response response, err = soapRequest(n.serviceURL, "AddPortMapping", message, n.urnDomain) if response != nil { defer response.Body.Close() } if err != nil { return } // TODO: check response to see if the port was forwarded // log.Println(message, response) // JAE: // body, err := ioutil.ReadAll(response.Body) // fmt.Println(string(body), err) mappedExternalPort = externalPort _ = response return } 删除端口映射 向upnp设备发送一条http post请求,将内部网络ip:port和外部网络ip:port删除映射关系 func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) { message := "<u:DeletePortMapping xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" + "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) + "</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>" + "</u:DeletePortMapping>" var response *http.Response response, err = soapRequest(n.serviceURL, "DeletePortMapping", message, n.urnDomain) if response != nil { defer response.Body.Close() } if err != nil { return } // TODO: check response to see if the port was deleted // log.Println(message, response) _ = response return } 获取映射后的公网地址 func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) { info, err := n.getExternalIPAddress() if err != nil { return } addr = net.ParseIP(info.externalIpAddress) return } func (n *upnpNAT) getExternalIPAddress() (info statusInfo, err error) { message := "<u:GetExternalIPAddress xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" + "</u:GetExternalIPAddress>" var response *http.Response response, err = soapRequest(n.serviceURL, "GetExternalIPAddress", message, n.urnDomain) if response != nil { defer response.Body.Close() } if err != nil { return } var envelope Envelope data, err := ioutil.ReadAll(response.Body) reader := bytes.NewReader(data) xml.NewDecoder(reader).Decode(&envelope) info = statusInfo{envelope.Soap.ExternalIP.IPAddress} if err != nil { return } return }
作者:Derek ## 简介 Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 本章介绍bytom代码启动、节点初始化、及停止的过程 > 作者使用MacOS操作系统,其他平台也大同小异 > Golang Version: 1.8 ## 预备工作 ### 编译安装 详细步骤见官方 [bytom install](https://github.com/Bytom/bytom) ### 设置debug日志输出 开启debug输出文件、函数、行号等详细信息 ``` export BYTOM_DEBUG=debug ``` ### 初始化并启动bytomd 初始化 ``` ./bytomd init --chain_id testnet ``` bytomd目前支持两种网络,这里我们使用测试网 mainnet:主网 testnet:测试网 启动bytomd ``` ./bytomd node --mining --prof_laddr=":8011" ``` --prof_laddr=":8080" // 开启pprof输出性能指标 访问:http://127.0.0.1:8080/debug/pprof/ ## bytomd init初始化 入口函数 ** cmd/bytomd/main.go ** ``` func init() { log.SetFormatter(&log.TextFormatter{FullTimestamp: true, DisableColors: true}) // If environment variable BYTOM_DEBUG is not empty, // then add the hook to logrus and set the log level to DEBUG if os.Getenv("BYTOM_DEBUG") != "" { log.AddHook(ContextHook{}) log.SetLevel(log.DebugLevel) } } func main() { cmd := cli.PrepareBaseCmd(commands.RootCmd, "TM", os.ExpandEnv(config.DefaultDataDir())) cmd.Execute() } ``` init函数会在main执行之前做初始化操作,可以看到init中bytomd加载BYTOM_DEBUG变量来设置debug日志输出 command cli传参初始化 bytomd的cli解析使用[cobra](https://github.com/spf13/cobra)库 ** cmd/bytomd/commands ** * cmd/bytomd/commands/root.go 初始化--root传参。bytomd存储配置、keystore、数据的root目录。在MacOS下,默认路径是~/Library/Bytom/ * cmd/bytomd/commands/init.go 初始化--chain_id传参。选择网络类型,在启动bytomd时我们选择了testnet也就是测试网络 * cmd/bytomd/commands/version.go 初始化version传参 * cmd/bytomd/commands/run_node.go 初始化node节点运行时所需要的传参 初始化默认配置 用户传参只有一部分参数,那节点所需的其他参数需要从默认配置中加载。 ** cmd/bytomd/commands/root.go ** ``` var ( config = cfg.DefaultConfig() ) ``` 在root.go中有一个config全局变量加载了node所需的所有默认参数 ``` // Default configurable parameters. func DefaultConfig() *Config { return &Config{ BaseConfig: DefaultBaseConfig(), // node基础相关配置 P2P: DefaultP2PConfig(), // p2p网络相关配置 Wallet: DefaultWalletConfig(), // 钱包相关配置 Auth: DefaultRPCAuthConfig(), // 验证相关配置 Web: DefaultWebConfig(), // web相关配置 } } ``` 后面的文章会一一介绍每个配置的作用 ## bytomd 守护进程启动与退出 ** cmd/bytomd/commands/run_node.go ** ``` func runNode(cmd *cobra.Command, args []string) error { // Create & start node n := node.NewNode(config) if _, err := n.Start(); err != nil { return fmt.Errorf("Failed to start node: %v", err) } else { log.WithField("nodeInfo", n.SyncManager().Switch().NodeInfo()).Info("Started node") } // Trap signal, run forever. n.RunForever() return nil } ``` runNode函数有三步操作: node.NewNode:初始化node运行环境 n.Start:启动node n.RunForever:监听退出信号,收到ctrl+c操作则退出node。在linux中守进程一般监听SIGTERM信号(ctrl+c)作为退出守护进程的信号 ### 初始化node运行环境 在bytomd中有五个db数据库存储在--root参数下的data目录 * accesstoken.db // 存储token相关信息(钱包访问控制权限) * trusthistory.db // 存储p2p网络同步相关信息 * txdb.db // 存储交易相关信息 * txfeeds.db // * wallet.db // 存储钱包相关信息 ** node/node.go ** ``` func NewNode(config *cfg.Config) *Node { ctx := context.Background() initActiveNetParams(config) // Get store 初始化txdb数据库 txDB := dbm.NewDB("txdb", config.DBBackend, config.DBDir()) store := leveldb.NewStore(txDB) // 初始化accesstoken数据库 tokenDB := dbm.NewDB("accesstoken", config.DBBackend, config.DBDir()) accessTokens := accesstoken.NewStore(tokenDB) // 初始化event事件调度器,也叫任务调度器。一个任务可以被多次调用 // Make event switch eventSwitch := types.NewEventSwitch() _, err := eventSwitch.Start() if err != nil { cmn.Exit(cmn.Fmt("Failed to start switch: %v", err)) } // 初始化交易池 txPool := protocol.NewTxPool() chain, err := protocol.NewChain(store, txPool) if err != nil { cmn.Exit(cmn.Fmt("Failed to create chain structure: %v", err)) } var accounts *account.Manager = nil var assets *asset.Registry = nil var wallet *w.Wallet = nil var txFeed *txfeed.Tracker = nil // 初始化txfeeds数据库 txFeedDB := dbm.NewDB("txfeeds", config.DBBackend, config.DBDir()) txFeed = txfeed.NewTracker(txFeedDB, chain) if err = txFeed.Prepare(ctx); err != nil { log.WithField("error", err).Error("start txfeed") return nil } // 初始化keystore hsm, err := pseudohsm.New(config.KeysDir()) if err != nil { cmn.Exit(cmn.Fmt("initialize HSM failed: %v", err)) } // 初始化钱包,默认wallet是开启状态 if !config.Wallet.Disable { walletDB := dbm.NewDB("wallet", config.DBBackend, config.DBDir()) accounts = account.NewManager(walletDB, chain) assets = asset.NewRegistry(walletDB, chain) wallet, err = w.NewWallet(walletDB, accounts, assets, hsm, chain) if err != nil { log.WithField("error", err).Error("init NewWallet") } // Clean up expired UTXO reservations periodically. go accounts.ExpireReservations(ctx, expireReservationsPeriod) } newBlockCh := make(chan *bc.Hash, maxNewBlockChSize) // 初始化网络节点同步管理 syncManager, _ := netsync.NewSyncManager(config, chain, txPool, newBlockCh) // 初始化pprof,pprof用于输出性能指标,需要制定--prof_laddr参数来开启,在文章开头我们已经开启该功能 // run the profile server profileHost := config.ProfListenAddress if profileHost != "" { // Profiling bytomd programs.see (https://blog.golang.org/profiling-go-programs) // go tool pprof http://profileHose/debug/pprof/heap go func() { http.ListenAndServe(profileHost, nil) }() } // 初始化节点,填充节点所需的所有参数环境 node := &Node{ config: config, syncManager: syncManager, evsw: eventSwitch, accessTokens: accessTokens, wallet: wallet, chain: chain, txfeed: txFeed, miningEnable: config.Mining, } // 初始化挖矿 node.cpuMiner = cpuminer.NewCPUMiner(chain, accounts, txPool, newBlockCh) node.miningPool = miningpool.NewMiningPool(chain, accounts, txPool, newBlockCh) node.BaseService = *cmn.NewBaseService(nil, "Node", node) return node } ``` 目前bytomd只支持cpu挖矿,所以在代码中只有cpuminer的初始化信息 ### 启动node ** node/node.go ** ``` // Lanch web broser or not func lanchWebBroser() { log.Info("Launching System Browser with :", webAddress) if err := browser.Open(webAddress); err != nil { log.Error(err.Error()) return } } func (n *Node) initAndstartApiServer() { n.api = api.NewAPI(n.syncManager, n.wallet, n.txfeed, n.cpuMiner, n.miningPool, n.chain, n.config, n.accessTokens) listenAddr := env.String("LISTEN", n.config.ApiAddress) env.Parse() n.api.StartServer(*listenAddr) } func (n *Node) OnStart() error { if n.miningEnable { n.cpuMiner.Start() } n.syncManager.Start() n.initAndstartApiServer() if !n.config.Web.Closed { lanchWebBroser() } return nil } ``` OnStart() 启动node进程如下: * 启动挖矿功能 * 启动p2p网络同步 * 启动http协议的apiserver服务 * 打开浏览器访问bytond的交易页面 ### 停止node bytomd在启动时执行了n.RunForever()函数,该函数是由tendermint框架启动了监听信号的功能: ** vendor/github.com/tendermint/tmlibs/common/os.go ** ``` func TrapSignal(cb func()) { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { for sig := range c { fmt.Printf("captured %v, exiting...\n", sig) if cb != nil { cb() } os.Exit(1) } }() select {} } ``` TrapSignal函数监听了SIGTERM信号,bytomd才能成为不退出的守护进程。只有当触发了ctrl+c或kill bytomd_pid才能终止bytomd进程退出。退出时bytomd执行如下操作 ** node/node.go ** ``` func (n *Node) OnStop() { n.BaseService.OnStop() if n.miningEnable { n.cpuMiner.Stop() } n.syncManager.Stop() log.Info("Stopping Node") // TODO: gracefully disconnect from peers. } ``` bytomd会将挖矿功能停止,p2p网络停止等操作。
作者:Derek 简介 Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 本章介绍bytom代码P2P网络中addrbook地址簿 作者使用MacOS操作系统,其他平台也大同小异 Golang Version: 1.8 addrbook介绍 addrbook用于存储P2P网络中保留最近的对端节点地址 在MacOS下,默认的地址簿路径存储在~/Library/Bytom/addrbook.json 地址簿格式 ** ~/Library/Bytom/addrbook.json ** { "Key": "359be6d08bc0c6e21c84bbb2", "Addrs": [ { "Addr": { "IP": "122.224.11.144", "Port": 46657 }, "Src": { "IP": "198.74.61.131", "Port": 46657 }, "Attempts": 0, "LastAttempt": "2018-05-04T12:58:23.894057702+08:00", "LastSuccess": "0001-01-01T00:00:00Z", "BucketType": 1, "Buckets": [ 181, 10 ] } ] } 地址类型 在addrbook中存储的地址有两种: ** p2p/addrbook.go ** const ( bucketTypeNew = 0x01 // 标识新地址,不可靠地址(未成功连接过)。只存储在一个bucket中 bucketTypeOld = 0x02 // 标识旧地址,可靠地址(已成功连接过)。可以存储在多个bucket中,最多为maxNewBucketsPerAddress个 ) 注意: 一个地址的类型变更不在此文章中做介绍,后期的文章会讨论该问题 地址簿相关结构体 地址簿 type AddrBook struct { cmn.BaseService mtx sync.Mutex filePath string // 地址簿路径 routabilityStrict bool // 是否可路由,默认为true rand *rand.Rand key string // 地址簿标识,用于计算addrNew和addrOld的索引 ourAddrs map[string]*NetAddress // 存储本地网络地址,用于添加p2p地址时做排除使用 addrLookup map[string]*knownAddress // 存储新、旧地址集,用于查询 addrNew []map[string]*knownAddress // 存储新地址 addrOld []map[string]*knownAddress // 存储旧地址 wg sync.WaitGroup nOld int // 旧地址数量 nNew int // 新地址数量 } 已知地址 type knownAddress struct { Addr *NetAddress // 已知peer的addr Src *NetAddress // 已知peer的addr的来源addr Attempts int32 // 连接peer的重试次数 LastAttempt time.Time // 最近一次尝试连接的时间 LastSuccess time.Time // 最近一次尝试成功连接的时间 BucketType byte // 地址的类型(表示可靠地址或不可靠地址) Buckets []int // 当前addr所属的buckets } routabilityStrict参数表示地址簿是否存储的ip是否可路由。可路由是根据RFC划分,具体参考资料:RFC标准 初始化地址簿 // NewAddrBook creates a new address book. // Use Start to begin processing asynchronous address updates. func NewAddrBook(filePath string, routabilityStrict bool) *AddrBook { am := &AddrBook{ rand: rand.New(rand.NewSource(time.Now().UnixNano())), ourAddrs: make(map[string]*NetAddress), addrLookup: make(map[string]*knownAddress), filePath: filePath, routabilityStrict: routabilityStrict, } am.init() am.BaseService = *cmn.NewBaseService(nil, "AddrBook", am) return am } // When modifying this, don't forget to update loadFromFile() func (a *AddrBook) init() { // 地址簿唯一标识 a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits // New addr buckets, 默认为256个大小 a.addrNew = make([]map[string]*knownAddress, newBucketCount) for i := range a.addrNew { a.addrNew[i] = make(map[string]*knownAddress) } // Old addr buckets,默认为64个大小 a.addrOld = make([]map[string]*knownAddress, oldBucketCount) for i := range a.addrOld { a.addrOld[i] = make(map[string]*knownAddress) } } bytomd启动时加载本地地址簿 loadFromFile在bytomd启动时,首先会加载本地的地址簿 // OnStart implements Service. func (a *AddrBook) OnStart() error { a.BaseService.OnStart() a.loadFromFile(a.filePath) a.wg.Add(1) go a.saveRoutine() return nil } // Returns false if file does not exist. // cmn.Panics if file is corrupt. func (a *AddrBook) loadFromFile(filePath string) bool { // If doesn't exist, do nothing. // 如果本地地址簿不存在则直接返回 _, err := os.Stat(filePath) if os.IsNotExist(err) { return false } // 加载地址簿json内容 // Load addrBookJSON{} r, err := os.Open(filePath) if err != nil { cmn.PanicCrisis(cmn.Fmt("Error opening file %s: %v", filePath, err)) } defer r.Close() aJSON := &addrBookJSON{} dec := json.NewDecoder(r) err = dec.Decode(aJSON) if err != nil { cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", filePath, err)) } // 填充addrNew、addrOld等 // Restore all the fields... // Restore the key a.key = aJSON.Key // Restore .addrNew & .addrOld for _, ka := range aJSON.Addrs { for _, bucketIndex := range ka.Buckets { bucket := a.getBucket(ka.BucketType, bucketIndex) bucket[ka.Addr.String()] = ka } a.addrLookup[ka.Addr.String()] = ka if ka.BucketType == bucketTypeNew { a.nNew++ } else { a.nOld++ } } return true } 定时更新地址簿 bytomd会定时更新本地地址簿,默认2分钟一次 func (a *AddrBook) saveRoutine() { dumpAddressTicker := time.NewTicker(dumpAddressInterval) out: for { select { case <-dumpAddressTicker.C: a.saveToFile(a.filePath) case <-a.Quit: break out } } dumpAddressTicker.Stop() a.saveToFile(a.filePath) a.wg.Done() log.Info("Address handler done") } func (a *AddrBook) saveToFile(filePath string) { log.WithField("size", a.Size()).Info("Saving AddrBook to file") a.mtx.Lock() defer a.mtx.Unlock() // Compile Addrs addrs := []*knownAddress{} for _, ka := range a.addrLookup { addrs = append(addrs, ka) } aJSON := &addrBookJSON{ Key: a.key, Addrs: addrs, } jsonBytes, err := json.MarshalIndent(aJSON, "", "\t") if err != nil { log.WithField("err", err).Error("Failed to save AddrBook to file") return } err = cmn.WriteFileAtomic(filePath, jsonBytes, 0644) if err != nil { log.WithFields(log.Fields{ "file": filePath, "err": err, }).Error("Failed to save AddrBook to file") } } 添加新地址 当peer之间交换addr时,节点会收到对端节点已知的地址信息,这些信息会被当前节点添加到地址簿中 func (a *AddrBook) AddAddress(addr *NetAddress, src *NetAddress) { a.mtx.Lock() defer a.mtx.Unlock() log.WithFields(log.Fields{ "addr": addr, "src": src, }).Debug("Add address to book") a.addAddress(addr, src) } func (a *AddrBook) addAddress(addr, src *NetAddress) { // 验证地址是否为可路由地址 if a.routabilityStrict && !addr.Routable() { log.Error(cmn.Fmt("Cannot add non-routable address %v", addr)) return } // 验证地址是否为本地节点地址 if _, ok := a.ourAddrs[addr.String()]; ok { // Ignore our own listener address. return } // 验证地址是否存在地址集中 // 如果存在:则判断该地址是否为old可靠地址、是否超过了最大buckets中。否则根据该地址已经被ka.Buckets引用的个数来随机决定是否添加到地址集中 // 如果不存在:则添加到地址集中。并标识为bucketTypeNew地址类型 ka := a.addrLookup[addr.String()] if ka != nil { // Already old. if ka.isOld() { return } // Already in max new buckets. if len(ka.Buckets) == maxNewBucketsPerAddress { return } // The more entries we have, the less likely we are to add more. factor := int32(2 * len(ka.Buckets)) if a.rand.Int31n(factor) != 0 { return } } else { ka = newKnownAddress(addr, src) } // 找到该地址在地址集的索引位置并添加 bucket := a.calcNewBucket(addr, src) a.addToNewBucket(ka, bucket) log.Info("Added new address ", "address:", addr, " total:", a.size()) } 选择最优节点 地址簿中存储众多地址,在p2p网络中需选择最优的地址去连接 PickAddress(newBias int)函数中newBias是由pex_reactor产生的地址评分。如何计算地址分数在其他章节中再讲 根据地址评分随机选择地址可增加区块链安全性 // Pick an address to connect to with new/old bias. func (a *AddrBook) PickAddress(newBias int) *NetAddress { a.mtx.Lock() defer a.mtx.Unlock() if a.size() == 0 { return nil } // newBias地址分数限制在0-100分数之间 if newBias > 100 { newBias = 100 } if newBias < 0 { newBias = 0 } // Bias between new and old addresses. oldCorrelation := math.Sqrt(float64(a.nOld)) * (100.0 - float64(newBias)) newCorrelation := math.Sqrt(float64(a.nNew)) * float64(newBias) // 根据地址分数计算是否从addrOld或addrNew中随机选择一个地址 if (newCorrelation+oldCorrelation)*a.rand.Float64() < oldCorrelation { // pick random Old bucket. var bucket map[string]*knownAddress = nil num := 0 for len(bucket) == 0 && num < oldBucketCount { bucket = a.addrOld[a.rand.Intn(len(a.addrOld))] num++ } if num == oldBucketCount { return nil } // pick a random ka from bucket. randIndex := a.rand.Intn(len(bucket)) for _, ka := range bucket { if randIndex == 0 { return ka.Addr } randIndex-- } cmn.PanicSanity("Should not happen") } else { // pick random New bucket. var bucket map[string]*knownAddress = nil num := 0 for len(bucket) == 0 && num < newBucketCount { bucket = a.addrNew[a.rand.Intn(len(a.addrNew))] num++ } if num == newBucketCount { return nil } // pick a random ka from bucket. randIndex := a.rand.Intn(len(bucket)) for _, ka := range bucket { if randIndex == 0 { return ka.Addr } randIndex-- } cmn.PanicSanity("Should not happen") } return nil } 移除一个地址 当一个地址被标记为Bad时则从地址集中移除。目前bytomd的代码版本并未调用过 func (a *AddrBook) MarkBad(addr *NetAddress) { a.RemoveAddress(addr) } // RemoveAddress removes the address from the book. func (a *AddrBook) RemoveAddress(addr *NetAddress) { a.mtx.Lock() defer a.mtx.Unlock() ka := a.addrLookup[addr.String()] if ka == nil { return } log.WithField("addr", addr).Info("Remove address from book") a.removeFromAllBuckets(ka) } func (a *AddrBook) removeFromAllBuckets(ka *knownAddress) { for _, bucketIdx := range ka.Buckets { bucket := a.getBucket(ka.BucketType, bucketIdx) delete(bucket, ka.Addr.String()) } ka.Buckets = nil if ka.BucketType == bucketTypeNew { a.nNew-- } else { a.nOld-- } delete(a.addrLookup, ka.Addr.String()) }
钱包 后端服务节点:https://github.com/Bytom-Community/Bytom-Server Android前端:https://github.com/Bytom-Community/Bytom-Mobile-Wallet-Android iOS前端:https://github.com/Bytom-Community/Bytom-Mobile-Wallet-iOS 钱包前端SDK:https://github.com/Bytom-Community/Bytom-Mobile-Wallet-SDK SDK PHP SDK:https://github.com/lxlxw/bytom-php-sdk Java SDK:https://github.com/chainworld/java-bytom Java SDK:https://github.com/successli/Bytom-Java-SDK Python SDK:https://github.com/Bytom-Community/python-bytom Node SDK:https://github.com/Bytom/node-sdk 其他 UTXO合并工具:https://github.com/wincss/bytom-utils API工具:https://github.com/myeth/bytom-api-doc-util 文档类 Derek读比原:http://shanhuhai5739.github.io/ 剥开比原看源码系列:https://github.com/freewind/unwrap-bytom 比原UTXO管理:https://github.com/oysheng/bytom.doc 挖矿相关文档:https://github.com/bigbigbigfish/howtowriteaminerforbytom 各类文档:https://github.com/Bytom/wiki 浏览器 http://blockmeta.com/ http://www.btmscan.com/ https://tokenview.com/cn/analysis/btm 矿池 鱼池:https://labs.f2pool.com/labs 双U矿池:https://uupool.cn/btm/ BTCC矿池:https://btccpool.com/?lang=zh Pool矿池:https://pool.ren/ 蚂蚁矿池:https://antpool.com/ 蜘蛛矿池:https://pool.zhizhu.top/ 蜜蜂矿池:https://beepool.org/ viabtc矿池:https://pool.viabtc.com/ Mat矿池:http://matpool.io/ 第三方主网钱包 比特派:https://bitpie.com/ 币派:https://bepal.pro/ hyperpay:https://hyperpay.me/
作者:Derek 简介 Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 本章介绍bytom代码Api-Server接口服务 作者使用MacOS操作系统,其他平台也大同小异 Golang Version: 1.8 protobuf生成比原核心代码 protobuf介绍 Protocol buffers是一个灵活的、高效的、自动化的用于对结构化数据进行序列化的协议。Protocol buffers序列化后的码流更小、速度更快、操作更简单。只需要将序列化的数据结构(.proto文件),便可以生成的源代码。 protobuf 3.0语法介绍 protobuf 语法 protobuf 安装 安装protobuf 3.4.0 protobuf download ./configure make make install protoc —version 安装grpc-go export PATH=$PATH:$GOPATH/bin go get -u google.golang.org/grpc go get -u github.com/golang/protobuf/protoc-gen-go 查看比原bc.proto核心文件 protocol/bc/bc.proto syntax = "proto3"; package bc; message Hash { fixed64 v0 = 1; fixed64 v1 = 2; fixed64 v2 = 3; fixed64 v3 = 4; } message Program { uint64 vm_version = 1; bytes code = 2; } // This message type duplicates Hash, above. One alternative is to // embed a Hash inside an AssetID. But it's useful for AssetID to be // plain old data (without pointers). Another alternative is use Hash // in any protobuf types where an AssetID is called for, but it's // preferable to have type safety. message AssetID { fixed64 v0 = 1; fixed64 v1 = 2; fixed64 v2 = 3; fixed64 v3 = 4; } message AssetAmount { AssetID asset_id = 1; uint64 amount = 2; } message AssetDefinition { Program issuance_program = 1; Hash data = 2; } message ValueSource { Hash ref = 1; AssetAmount value = 2; uint64 position = 3; } message ValueDestination { Hash ref = 1; AssetAmount value = 2; uint64 position = 3; } message BlockHeader { uint64 version = 1; uint64 height = 2; Hash previous_block_id = 3; uint64 timestamp = 4; Hash transactions_root = 5; Hash transaction_status_hash = 6; uint64 nonce = 7; uint64 bits = 8; TransactionStatus transaction_status = 9; } message TxHeader { uint64 version = 1; uint64 serialized_size = 2; uint64 time_range = 3; repeated Hash result_ids = 4; } message TxVerifyResult { bool status_fail = 1; } message TransactionStatus { uint64 version = 1; repeated TxVerifyResult verify_status = 2; } message Mux { repeated ValueSource sources = 1; // issuances, spends, and muxes Program program = 2; repeated ValueDestination witness_destinations = 3; // outputs, retirements, and muxes repeated bytes witness_arguments = 4; } message Coinbase { ValueDestination witness_destination = 1; bytes arbitrary = 2; } message Output { ValueSource source = 1; Program control_program = 2; uint64 ordinal = 3; } message Retirement { ValueSource source = 1; uint64 ordinal = 2; } message Issuance { Hash nonce_hash = 1; AssetAmount value = 2; ValueDestination witness_destination = 3; AssetDefinition witness_asset_definition = 4; repeated bytes witness_arguments = 5; uint64 ordinal = 6; } message Spend { Hash spent_output_id = 1; ValueDestination witness_destination = 2; repeated bytes witness_arguments = 3; uint64 ordinal = 4; } 根据bc.proto生成bc.pb.go代码 protoc -I/usr/local/include -I. \ -I${GOPATH}/src \ --go_out=plugins=grpc:. \ ./*.proto 执行完上面命令,我们会看到当前目录下生成的bc.pb.go文件,该文件在比原链中承载这block、transaction、coinbase等重要数据结构
作者:Derek 简介 Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 本章介绍bytom代码Api-Server接口服务 作者使用MacOS操作系统,其他平台也大同小异 Golang Version: 1.8 Api-Server接口服务 Api Server是比原链中非常重要的一个功能,在比原链的架构中专门服务于bytomcli和dashboard,他的功能是接收并处理用户和矿池相关的请求。默认启动9888端口。总之主要功能如下: 接收并处理用户或矿池发送的请求 管理交易:打包、签名、提交等操作 管理本地比原钱包 管理本地p2p节点信息 管理本地矿工挖矿操作等 在Api Server服务过程中,在监听地址listener上接收bytomcli或dashboard的请求访问。对每一个请求,Api Server均会创建一个新的goroutine来处理请求。首先Api Server读取请求内容,解析请求,接着匹配相应的路由项,随后调用路由项的Handler回调函数来处理。最后Handler处理完请求之后给bytomcli响应该请求。 Api-Server源码分析 在bytomd启动过程中,bytomd使用golang标准库http.NewServeMux()创建一个router路由器,提供请求的路由分发功能。创建Api Server主要有三部分组成: 初始化http.NewServeMux()得到mux 为mux.Handle添加多个有效的router路由项。每一个路由项由HTTP请求方法(GET、POST、PUT、DELET)、URL和Handler回调函数组成 将监听地址作为参数,最终执行Serve(listener)开始服务于外部请求 创建Api对象 node/node.go func (n *Node) initAndstartApiServer() { n.api = api.NewAPI(n.syncManager, n.wallet, n.txfeed, n.cpuMiner, n.miningPool, n.chain, n.config, n.accessTokens) listenAddr := env.String("LISTEN", n.config.ApiAddress) env.Parse() n.api.StartServer(*listenAddr) } api/api.go func NewAPI(sync *netsync.SyncManager, wallet *wallet.Wallet, txfeeds *txfeed.Tracker, cpuMiner *cpuminer.CPUMiner, miningPool *miningpool.MiningPool, chain *protocol.Chain, config *cfg.Config, token *accesstoken.CredentialStore) *API { api := &API{ sync: sync, wallet: wallet, chain: chain, accessTokens: token, txFeedTracker: txfeeds, cpuMiner: cpuMiner, miningPool: miningPool, } api.buildHandler() api.initServer(config) return api } 首先,实例化api对象。Api-server管理的事情很多,所以参数也相对较多。listenAddr本地端口,如果系统没有设置LISTEN变量则使用config.ApiAddress配置地址,默认为9888 NewAPI函数我们看到有三个操作: 实例化api对象 api.buildHandler添加router路由项 api.initServer实例化http.Server,配置auth验证等 router路由项 func (a *API) buildHandler() { walletEnable := false m := http.NewServeMux() if a.wallet != nil { walletEnable = true m.Handle("/create-account", jsonHandler(a.createAccount)) m.Handle("/list-accounts", jsonHandler(a.listAccounts)) m.Handle("/delete-account", jsonHandler(a.deleteAccount)) // ... } } router路由项过多。这里只介绍关于账号相关的handler。其他的handler大同小异。 m.Handle("/create-account", jsonHandler(a.createAccount)) 我们可以看到一条router项由url和对应的handle回调函数组成。当我们请求的url匹配到/create-account时,Api-Server会执行a.createAccount函数,并将用户的传参也带过去。 启动Api-Server服务 api/api.go func (a *API) StartServer(address string) { log.WithField("api address:", address).Info("Rpc listen") listener, err := net.Listen("tcp", address) if err != nil { cmn.Exit(cmn.Fmt("Failed to register tcp port: %v", err)) } go func() { if err := a.server.Serve(listener); err != nil { log.WithField("error", errors.Wrap(err, "Serve")).Error("Rpc server") } }() } 通过golang标准库net.listen方法,监听本地的地址端口。由于http服务是一个持久运行的服务,我们启动一个go程专门运行http服务。当运行a.server.Serve没有任何报错时,我们可以看到服务器上启动的9888端口。此时Api-Server已经处于等待接收用户的请求。
作者:Derek 简介 Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 本章介绍bytom代码孤块管理 作者使用MacOS操作系统,其他平台也大同小异 Golang Version: 1.8 孤块介绍 什么是孤块 当节点收到了一个有效的区块,而在现有的主链中却未找到它的父区块,那么这个区块被认为是“孤块”。父区块是指当前区块的PreviousBlockHash字段指向上一区块的hash值。 接收到的孤块会被存储在孤块池中,直到它们的父区块被节点收到。一旦收到了父区块,节点就会将孤块从孤块池中取出,并且连接到它的父区块,让它作为区块链的一部分。 孤块出现的原因 当两个或多个区块在很短的时间间隔内被挖出来,节点有可能会以不同的顺序接收到它们,这个时候孤块现象就会出现。 我们假设有三个高度分别为100、101、102的块,分别以102、101、100的颠倒顺序被节点接收。此时节点将102、101放入到孤块管理缓存池中,等待彼此的父块。当高度为100的区块被同步进来时,会被验证区块和交易,然后存储到区块链上。这时会对孤块缓存池进行递归查询,根据高度为100的区块找到101的区块并存储到区块链上,再根据高度为101的区块找到102的区块并存储到区块链上。 孤块源码分析 孤块管理缓存池结构体 protocol/orphan_manage.go type OrphanManage struct { orphan map[bc.Hash]*types.Block prevOrphans map[bc.Hash][]*bc.Hash mtx sync.RWMutex } func NewOrphanManage() *OrphanManage { return &OrphanManage{ orphan: make(map[bc.Hash]*types.Block), prevOrphans: make(map[bc.Hash][]*bc.Hash), } } orphan 存储孤块,key为block hash,value为block结构体 prevOrphans 存储孤块的父块 mtx 互斥锁,保护map结构在多并发读写状态下保持数据一致 添加孤块到缓存池 func (o *OrphanManage) Add(block *types.Block) { blockHash := block.Hash() o.mtx.Lock() defer o.mtx.Unlock() if _, ok := o.orphan[blockHash]; ok { return } o.orphan[blockHash] = block o.prevOrphans[block.PreviousBlockHash] = append(o.prevOrphans[block.PreviousBlockHash], &blockHash) log.WithFields(log.Fields{"hash": blockHash.String(), "height": block.Height}).Info("add block to orphan") } 当一个孤块被添加到缓存池中,还需要记录该孤块的父块hash。用于父块hash的查询 查询孤块和父孤块 func (o *OrphanManage) Get(hash *bc.Hash) (*types.Block, bool) { o.mtx.RLock() block, ok := o.orphan[*hash] o.mtx.RUnlock() return block, ok } func (o *OrphanManage) GetPrevOrphans(hash *bc.Hash) ([]*bc.Hash, bool) { o.mtx.RLock() prevOrphans, ok := o.prevOrphans[*hash] o.mtx.RUnlock() return prevOrphans, ok } 删除孤块 func (o *OrphanManage) Delete(hash *bc.Hash) { o.mtx.Lock() defer o.mtx.Unlock() block, ok := o.orphan[*hash] if !ok { return } delete(o.orphan, *hash) prevOrphans, ok := o.prevOrphans[block.PreviousBlockHash] if !ok || len(prevOrphans) == 1 { delete(o.prevOrphans, block.PreviousBlockHash) return } for i, preOrphan := range prevOrphans { if preOrphan == hash { o.prevOrphans[block.PreviousBlockHash] = append(prevOrphans[:i], prevOrphans[i+1:]...) return } } } 删除孤块的过程中,同时删除父块 孤块处理逻辑 protocol/block.go func (c *Chain) processBlock(block *types.Block) (bool, error) { blockHash := block.Hash() if c.BlockExist(&blockHash) { log.WithFields(log.Fields{"hash": blockHash.String(), "height": block.Height}).Info("block has been processed") return c.orphanManage.BlockExist(&blockHash), nil } if parent := c.index.GetNode(&block.PreviousBlockHash); parent == nil { c.orphanManage.Add(block) return true, nil } if err := c.saveBlock(block); err != nil { return false, err } bestBlock := c.saveSubBlock(block) // ... } processBlock函数处理block块加入区块链上之前的过程。 c.BlockExist判断当前block块是否存在于区块链上或是否存在孤块缓存池中,如果存在则返回。 c.index.GetNode判断block块的父节点是否存在。如果在现有的主链中却未找到它的父区块则将block块添加到孤块缓存池。 c.saveBlock走到了这一步说明,block父节点是存在于区块链,则将block块存储到区块链。该函数会验证区块和交易有效性。 saveSubBlock 代码如下: func (c *Chain) saveSubBlock(block *types.Block) *types.Block { blockHash := block.Hash() prevOrphans, ok := c.orphanManage.GetPrevOrphans(&blockHash) if !ok { return block } bestBlock := block for _, prevOrphan := range prevOrphans { orphanBlock, ok := c.orphanManage.Get(prevOrphan) if !ok { log.WithFields(log.Fields{"hash": prevOrphan.String()}).Warning("saveSubBlock fail to get block from orphanManage") continue } if err := c.saveBlock(orphanBlock); err != nil { log.WithFields(log.Fields{"hash": prevOrphan.String(), "height": orphanBlock.Height}).Warning("saveSubBlock fail to save block") continue } if subBestBlock := c.saveSubBlock(orphanBlock); subBestBlock.Height > bestBlock.Height { bestBlock = subBestBlock } } return bestBlock } saveSubBlock 在孤块缓存池中查询是否存在当前区块的下一个区块。比如当前区块高度为100,则在孤块缓存池中查询是否有区块高度为101的区块。如果存在则将101区块存储到区块链并从孤块缓存池中删除该区块。 saveSubBlock是一个递归函数的实现。目的是为了寻找最深叶子节点的递归方式。比如当前区块高度为100的,递归查询出高度为99、98、97等高度的区块。
该部分主要针对用户使用bytom自带的账户模式发送交易 1、构建交易 API接口 build-transaction,代码api/transact.go#L120 以标准的非BTM资产转账交易为例,资产ID为全F表示BTM资产,在该示例中BTM资产仅作为手续费,该交易表示花费99个特定的资产到指定地址中。其中构建交易的输入请求json格式如下: { "base_transaction": null, "actions": [ { "account_id": "0ER7MEFGG0A02", "amount": 20000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "type": "spend_account" }, { "account_id": "0ER7MEFGG0A02", "amount": 99, "asset_id": "42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f", "type": "spend_account" }, { "amount": 99, "asset_id": "42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f", "address": "sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me", "type": "control_address" } ], "ttl": 0, "time_range": 0 } 对应源代码的请求对象如下: // BuildRequest is main struct when building transactions type BuildRequest struct { Tx *types.TxData `json:"base_transaction"` Actions []map[string]interface{} `json:"actions"` TTL json.Duration `json:"ttl"` TimeRange uint64 `json:"time_range"` } 结构字段说明如下: Tx 交易的TxData部分,该字段为预留字段,为空即可 TTL 构建交易的生存时间(单位为毫秒),意味着在该时间范围内,已经缓存的utxo不能用于再一次build交易,除非剩余的utxo足以构建一笔新的交易,否则会报错。当ttl为0时会被默认设置为600s,即5分钟 TimeRange 时间戳,意味着该交易将在该时间戳(区块高度)之后不会被提交上链,为了防止交易在网络中传输延迟而等待太久时间,如果交易没有在特定的时间范围内被打包,该交易便会自动失效 Actions 交易的actions结构,所有的交易都是由action构成的,map类型的interface{}保证了action类型的可扩展性。其中action中必须包含type字段,用于区分不同的action类型,action主要包含input和output两种类型,其详细介绍如下: input action 类型: issue 发行资产 spend_account 以账户的模式花费utxo spend_account_unspent_output 直接花费指定的utxo output action 类型: control_address 接收方式为地址模式 control_program 接收方式为(program)合约模式 retire 销毁资产 注意事项: 一个交易必须至少包含一个input和output(coinbase交易除外,因为coinbase交易是由系统产生,故不在此加以描述),否则交易将会报错。 除了BTM资产(所有交易都是以BTM资产作为手续费)之外,其他资产在构建input和output时,所有输入和输出的资产总和必须相等,否则交易会报出输入输出不平衡的错误信息。 交易的手续费: 所有inputs的BTM资产数量 - 所有outputs的BTM资产数量 交易中的资产amount都是neu为单位的,BTM的单位换算如下:1 BTM = 1000 mBTM = 100000000 neu action简介 下面对构建交易时用到的各种action类型进行详细说明: issue issueAction结构体源代码如下: type issueAction struct { assets *Registry bc.AssetAmount } type AssetAmount struct { AssetId *AssetID `protobuf:"bytes,1,opt,name=asset_id,json=assetId" json:"asset_id,omitempty"` Amount uint64 `protobuf:"varint,2,opt,name=amount" json:"amount,omitempty"` } 结构字段说明如下: assets 主要用于资产的管理,无需用户设置参数 AssetAmount 表示用户需要发行的资产ID和对应的资产数目,这里的AssetID需要通过create-asset创建,并且这里不能使用BTM的资产ID issueAction的json格式为: { "amount": 100000000, "asset_id": "3152a15da72be51b330e1c0f8e1c0db669269809da4f16443ff266e07cc43680", "type": "issue" } 例如发行一笔资产的交易示例如下:(该交易表示发行数量为900000000个assetID的42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f的资产到接收地址sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me中, 其中手续费为20000000neu的BTM资产) { "base_transaction": null, "actions": [ { "account_id": "0ER7MEFGG0A02", "amount": 20000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "type": "spend_account" }, { "amount": 900000000, "asset_id": "42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f", "type": "issue" }, { "amount": 900000000, "asset_id": "42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f", "address": "sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me", "type": "control_address" } ], "ttl": 0, "time_range": 0 } spend_account spendAction结构体源代码如下: type spendAction struct { accounts *Manager bc.AssetAmount AccountID string `json:"account_id"` ClientToken *string `json:"client_token"` } type AssetAmount struct { AssetId *AssetID `protobuf:"bytes,1,opt,name=asset_id,json=assetId" json:"asset_id,omitempty"` Amount uint64 `protobuf:"varint,2,opt,name=amount" json:"amount,omitempty"` } 结构字段说明如下: accounts 主要用于账户的管理,无需用户设置参数 AccountID 表示需要花费资产的账户ID AssetAmount 表示花费的资产ID和对应的资产数目 ClientToken 表示Reserve用户UTXO的限制条件,目前不填或为空即可 spendAction的json格式为: { "account_id": "0BF63M2U00A04", "amount": 2000000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "type": "spend_account" } 例如转账一笔资产的交易示例如下:(该交易表示通过账户的方式转账100000000neu的BTM资产到地址sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me中, 其中手续费20000000neu = 输入BTM资产数量 - 输出BTM资产数量) { "base_transaction": null, "actions": [ { "account_id": "0ER7MEFGG0A02", "amount": 120000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "type": "spend_account" }, { "amount": 100000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "address": "sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me", "type": "control_address" } ], "ttl": 0, "time_range": 0 } spend_account_unspent_output spendUTXOAction结构体源代码如下: type spendUTXOAction struct { accounts *Manager OutputID *bc.Hash `json:"output_id"` ClientToken *string `json:"client_token"` } 结构字段说明如下: accounts 主要用于账户的管理,无需用户设置参数 OutputID 表示需要花费的UTXO的ID,可以根据list-unspent-outputs查询可用的UTXO,其中OutputID对应该API返回结果的id字段 ClientToken 表示Reserve用户UTXO的限制条件,目前不填或为空即可 spendUTXOAction的json格式为: { "type": "spend_account_unspent_output", "output_id": "58f29f0f85f7bd2a91088bcbe536dee41cd0642dfb1480d3a88589bdbfd642d9" } 例如通过花费UTXO的方式转账一笔资产的交易示例如下:(该交易表示通过直接花费UTXO的方式转账100000000neu的BTM资产到地址sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me中, 其中手续费 = 输入BTM资产的UTXO值 - 输出BTM资产数量) { "base_transaction": null, "actions": [ { "output_id": "58f29f0f85f7bd2a91088bcbe536dee41cd0642dfb1480d3a88589bdbfd642d9", "type": "spend_account_unspent_output" }, { "amount": 100000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "address": "sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me", "type": "control_address" } ], "ttl": 0, "time_range": 0 } control_address controlAddressAction结构体源代码如下: type controlAddressAction struct { bc.AssetAmount Address string `json:"address"` } type AssetAmount struct { AssetId *AssetID `protobuf:"bytes,1,opt,name=asset_id,json=assetId" json:"asset_id,omitempty"` Amount uint64 `protobuf:"varint,2,opt,name=amount" json:"amount,omitempty"` } 结构字段说明如下: Address 表示接收资产的地址,可以根据 create-account-receiver API接口创建地址 AssetAmount 表示接收的资产ID和对应的资产数目 controlAddressAction的json格式为: { "amount": 100000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "address": "bm1q50u3z8empm5ke0g3ngl2t3sqtr6sd7cepd3z68", "type": "control_address" } 例如转账一笔资产的交易示例如下:(该交易表示通过账户的方式转账100000000neu的BTM资产到地址sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me中, 其中control_address类型表示以地址作为接收方式) { "base_transaction": null, "actions": [ { "account_id": "0ER7MEFGG0A02", "amount": 120000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "type": "spend_account" }, { "amount": 100000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "address": "sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me", "type": "control_address" } ], "ttl": 0, "time_range": 0 } control_program controlProgramAction结构体源代码如下: type controlProgramAction struct { bc.AssetAmount Program json.HexBytes `json:"control_program"` } type AssetAmount struct { AssetId *AssetID `protobuf:"bytes,1,opt,name=asset_id,json=assetId" json:"asset_id,omitempty"` Amount uint64 `protobuf:"varint,2,opt,name=amount" json:"amount,omitempty"` } 结构字段说明如下: Program 表示接收资产的合约脚本,可以根据 create-account-receiver API接口创建接收program(返回结果的 program 和 address 是一一对应的) AssetAmount 表示接收的资产ID和对应的资产数目 controlProgramAction的json格式为: { "amount": 100000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "control_program":"0014a3f9111f3b0ee96cbd119a3ea5c60058f506fb19", "type": "control_program" } 例如转账一笔资产的交易示例如下:(该交易表示通过账户的方式转账100000000neu的BTM资产到接收program(跟address是一一对应的)0014a3f9111f3b0ee96cbd119a3ea5c60058f506fb19中, 其中control_program类型表示以program作为接收方式) { "base_transaction": null, "actions": [ { "account_id": "0ER7MEFGG0A02", "amount": 120000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "type": "spend_account" }, { "amount": 100000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "control_program": "0014a3f9111f3b0ee96cbd119a3ea5c60058f506fb19", "type": "control_program" } ], "ttl": 0, "time_range": 0 } retire retireAction结构体源代码如下: type retireAction struct { bc.AssetAmount } type AssetAmount struct { AssetId *AssetID `protobuf:"bytes,1,opt,name=asset_id,json=assetId" json:"asset_id,omitempty"` Amount uint64 `protobuf:"varint,2,opt,name=amount" json:"amount,omitempty"` } 结构字段说明如下: AssetAmount 表示销毁的资产ID和对应的资产数目 retireAction的json格式为: { "amount": 900000000, "asset_id": "3152a15da72be51b330e1c0f8e1c0db669269809da4f16443ff266e07cc43680", "type": "retire" } 例如销毁一笔资产的交易示例如下:(该交易表示通过账户的方式将100000000neu的BTM资产销毁, retire表示销毁指定数量的资产) { "base_transaction": null, "actions": [ { "account_id": "0ER7MEFGG0A02", "amount": 120000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "type": "spend_account" }, { "amount": 100000000, "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "type": "retire" } ], "ttl": 0, "time_range": 0 } build-transaction的输入构造完成之后,便可以通过http的调用方式进行发送交易,构建交易请求成功之后返回的json结果如下: { "allow_additional_actions": false, "raw_transaction": "070100020161015f1190c60818b4aff485c865113c802942f29ce09088cae1b117fc4c8db2292212ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d599010001160014a86c83ee12e6d790fb388345cc2e2b87056a077301000161015fb018097c4040c8dd86d95611a13c24f90d4c9d9d06b25f5c9ed0556ac8abd73442275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f80a094a58d1d0101160014068840e56af74038571f223b1c99f1b60caaf456010003013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80bfffcb9901011600140b946646626c55a52a325c8bb48de792284d9b7200013e42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f9d9f94a58d1d01160014c8b4391bab4923a83b955170d24ee4ca5b6ec3fb00013942275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f6301160014366b275ed9b2266b645cf1b8be51009cc3b260e100", "signing_instructions": [ { "position": 0, "witness_components": [ { "keys": [ { "derivation_path": [ "010100000000000000", "0100000000000000" ], "xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8" } ], "quorum": 1, "signatures": null, "type": "raw_tx_signature" }, { "type": "data", "value": "d174db6506e35f2decb5be148c2984bfd0f6c67f043365bf642d1af387c04fd5" } ] }, { "position": 1, "witness_components": [ { "keys": [ { "derivation_path": [ "010100000000000000", "0800000000000000" ], "xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8" } ], "quorum": 1, "signatures": null, "type": "raw_tx_signature" }, { "type": "data", "value": "05cdbcc705f07ad87521835bbba226ad7b430cc24e5e3f008edbe61540535419" } ] } ] } 对应响应对象的源代码如下: // Template represents a partially- or fully-signed transaction. type Template struct { Transaction *types.Tx `json:"raw_transaction"` SigningInstructions []*SigningInstruction `json:"signing_instructions"` // AllowAdditional affects whether Sign commits to the tx sighash or // to individual details of the tx so far. When true, signatures // commit to tx details, and new details may be added but existing // ones cannot be changed. When false, signatures commit to the tx // as a whole, and any change to the tx invalidates the signature. AllowAdditional bool `json:"allow_additional_actions"` } 结构字段说明如下: Transaction 交易相关信息,该字段包含TxData和bc.Tx两个部分: TxData 表示给用户展示的交易数据部分,该部分对用户可见 Version 交易版本 SerializedSize 交易序列化之后的size TimeRange 交易提交上链的最大时间戳(区块高度)(主链区块高度到达该时间戳(区块高度)之后,如果交易没有被提交上链,该交易便会失效) Inputs 交易输入 Outputs 交易输出 bc.Tx 表示系统中处理交易用到的转换结构,该部分对用户不可见,故不做详细描述 SigningInstructions 交易的签名信息 Position 对input action签名的位置 WitnessComponents 对input action签名需要的数据信息,其中build交易的signatures为null,表示没有签名; 如果交易签名成功,则该字段会存在签名信息。该字段是一个interface接口,主要包含3种不同的类型: SignatureWitness 对交易模板Template中交易input action位置的合约program进行哈希,然后对hash值进行签名 signatures (数组类型)交易的签名,sign-transaction执行完成之后才会有值存在 keys (数组类型)包含主公钥xpub和派生路径derivation_path,通过它们可以在签名阶段找到对应的派生私钥child_xprv,然后使用派生私钥进行签名 quorum 账户key 的个数,必须和上面的keys的长度相等。如果quorum 等于1,则表示单签账户,否则为多签账户 program 签名的数据部分,program的hash值作为签名数据。如果program为空,则会根据当前交易ID和对应action位置的InputID两部分生成一个hash,然后把它们作为指令数据自动构造一个program RawTxSigWitness 对交易模板Template的交易ID和对应input action位置的InputID(该字段位于bc.Tx中)进行哈希,然后对hash值进行签名 signatures (数组类型)交易的签名,sign-transaction执行完成之后才会有值存在 keys (数组类型)包含主公钥xpub和派生路径derivation_path,通过它们可以在签名阶段找到对应的派生私钥child_xprv,然后使用派生私钥进行签名 quorum 账户key的个数,必须和上面的keys 的长度相等。如果quorum 等于1,则表示单签账户,否则为多签账户 DataWitness 该类型无需签名,验证合约program的附加数据 AllowAdditional 是否允许交易的附加数据,如果为true,则交易的附加数据会添加到交易中,但是不会影响交易的执行的program脚本,对签名结果不会造成影响; 如果为false,则整个交易作为一个整体进行签名,任何数据的改变将影响整个交易的签名 估算手续费 估算手续费接口estimate-transaction-gas是对build-transaction的结果进行手续费的预估,估算的结果需要重新加到build-transaction的结果中,然后对交易进行签名和提交。其主要流程如下: build - estimate - build - sign - submit 估算手续费的输入请求json格式如下: { "transaction_template": { "allow_additional_actions": false, "raw_transaction": "070100020161015f1190c60818b4aff485c865113c802942f29ce09088cae1b117fc4c8db2292212ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d599010001160014a86c83ee12e6d790fb388345cc2e2b87056a077301000161015fb018097c4040c8dd86d95611a13c24f90d4c9d9d06b25f5c9ed0556ac8abd73442275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f80a094a58d1d0101160014068840e56af74038571f223b1c99f1b60caaf456010003013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80bfffcb9901011600140b946646626c55a52a325c8bb48de792284d9b7200013e42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f9d9f94a58d1d01160014c8b4391bab4923a83b955170d24ee4ca5b6ec3fb00013942275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f6301160014366b275ed9b2266b645cf1b8be51009cc3b260e100", "signing_instructions": [ { "position": 0, "witness_components": [ { "keys": [ { "derivation_path": [ "010100000000000000", "0100000000000000" ], "xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8" } ], "quorum": 1, "signatures": null, "type": "raw_tx_signature" }, { "type": "data", "value": "d174db6506e35f2decb5be148c2984bfd0f6c67f043365bf642d1af387c04fd5" } ] }, { "position": 1, "witness_components": [ { "keys": [ { "derivation_path": [ "010100000000000000", "0800000000000000" ], "xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8" } ], "quorum": 1, "signatures": null, "type": "raw_tx_signature" }, { "type": "data", "value": "05cdbcc705f07ad87521835bbba226ad7b430cc24e5e3f008edbe61540535419" } ] } ] } } 对应响应对象的源代码如下: type request struct{ TxTemplate txbuilder.Template `json:"transaction_template"` } // Template represents a partially- or fully-signed transaction. type Template struct { Transaction *types.Tx `json:"raw_transaction"` SigningInstructions []*SigningInstruction `json:"signing_instructions"` // AllowAdditional affects whether Sign commits to the tx sighash or // to individual details of the tx so far. When true, signatures // commit to tx details, and new details may be added but existing // ones cannot be changed. When false, signatures commit to the tx // as a whole, and any change to the tx invalidates the signature. AllowAdditional bool `json:"allow_additional_actions"` } 其中TxTemplate相关字段的说明见build-transaction的结果描述 调用estimate-transaction-gas接口成功之后返回的json结果如下: { "total_neu": 5000000, "storage_neu": 3840000, "vm_neu": 1419000 } 对应响应对象的源代码如下: // EstimateTxGasResp estimate transaction consumed gas type EstimateTxGasResp struct { TotalNeu int64 `json:"total_neu"` StorageNeu int64 `json:"storage_neu"` VMNeu int64 `json:"vm_neu"` } 结构字段说明如下: TotalNeu 预估的总手续费(单位为neu),该值直接加到build-transaction的BTM资产输入action中即可 StorageNeu 存储交易的手续费 VMNeu 运行虚拟机的手续费 2、签名交易 API接口 sign-transaction,代码api/hsm.go#L53 签名交易的输入请求json格式如下: { "password": "123456", "transaction": { "allow_additional_actions": false, "raw_transaction": "070100020161015f1190c60818b4aff485c865113c802942f29ce09088cae1b117fc4c8db2292212ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d599010001160014a86c83ee12e6d790fb388345cc2e2b87056a077301000161015fb018097c4040c8dd86d95611a13c24f90d4c9d9d06b25f5c9ed0556ac8abd73442275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f80a094a58d1d0101160014068840e56af74038571f223b1c99f1b60caaf456010003013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80bfffcb9901011600140b946646626c55a52a325c8bb48de792284d9b7200013e42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f9d9f94a58d1d01160014c8b4391bab4923a83b955170d24ee4ca5b6ec3fb00013942275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f6301160014366b275ed9b2266b645cf1b8be51009cc3b260e100", "signing_instructions": [ { "position": 0, "witness_components": [ { "keys": [ { "derivation_path": [ "010100000000000000", "0100000000000000" ], "xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8" } ], "quorum": 1, "signatures": null, "type": "raw_tx_signature" }, { "type": "data", "value": "d174db6506e35f2decb5be148c2984bfd0f6c67f043365bf642d1af387c04fd5" } ] }, { "position": 1, "witness_components": [ { "keys": [ { "derivation_path": [ "010100000000000000", "0800000000000000" ], "xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8" } ], "quorum": 1, "signatures": null, "type": "raw_tx_signature" }, { "type": "data", "value": "05cdbcc705f07ad87521835bbba226ad7b430cc24e5e3f008edbe61540535419" } ] } ] } } 对应请求对象的源代码如下: type SignRequest struct { //function pseudohsmSignTemplates request Password string `json:"password"` Txs txbuilder.Template `json:"transaction"` } 结构字段说明如下: Password 签名的密码,根据密码可以从节点服务器上解析出用户的私钥,然后用私钥对交易进行签名 Txs 交易模板,build-transaction的返回结果,结构类型为 txbuilder.Template,相关字段的说明见build-transaction的结果描述 签名交易sign-transaction请求成功之后返回的json结果如下: { "sign_complete": true, "transaction": { "allow_additional_actions": false, "raw_transaction": "070100020161015f1190c60818b4aff485c865113c802942f29ce09088cae1b117fc4c8db2292212ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d599010001160014a86c83ee12e6d790fb388345cc2e2b87056a0773630240273d5fc4fb06909fbc2968ea91c411fd20f690c88e74284ce2732052400129948538562fe432afd6cf17e590e8645b80edf80b9d9581d0a980d5f9f859e3880620d174db6506e35f2decb5be148c2984bfd0f6c67f043365bf642d1af387c04fd50161015fb018097c4040c8dd86d95611a13c24f90d4c9d9d06b25f5c9ed0556ac8abd73442275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f80a094a58d1d0101160014068840e56af74038571f223b1c99f1b60caaf4566302400cf0beefceaf9fbf1efadedeff7aee5b38ee7a25a20d78b630b01613bc2f8c9230555a6e09aaa11a82ba68c0fc9e98a47c852dfe3de851d93f9b2b7ce256f90d2005cdbcc705f07ad87521835bbba226ad7b430cc24e5e3f008edbe6154053541903013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80bfffcb9901011600140b946646626c55a52a325c8bb48de792284d9b7200013e42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f9d9f94a58d1d01160014c8b4391bab4923a83b955170d24ee4ca5b6ec3fb00013942275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f6301160014366b275ed9b2266b645cf1b8be51009cc3b260e100", "signing_instructions": [ { "position": 0, "witness_components": [ { "keys": [ { "derivation_path": [ "010100000000000000", "0100000000000000" ], "xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8" } ], "quorum": 1, "signatures": [ "273d5fc4fb06909fbc2968ea91c411fd20f690c88e74284ce2732052400129948538562fe432afd6cf17e590e8645b80edf80b9d9581d0a980d5f9f859e38806" ], "type": "raw_tx_signature" }, { "type": "data", "value": "d174db6506e35f2decb5be148c2984bfd0f6c67f043365bf642d1af387c04fd5" } ] }, { "position": 1, "witness_components": [ { "keys": [ { "derivation_path": [ "010100000000000000", "0800000000000000" ], "xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8" } ], "quorum": 1, "signatures": [ "0cf0beefceaf9fbf1efadedeff7aee5b38ee7a25a20d78b630b01613bc2f8c9230555a6e09aaa11a82ba68c0fc9e98a47c852dfe3de851d93f9b2b7ce256f90d" ], "type": "raw_tx_signature" }, { "type": "data", "value": "05cdbcc705f07ad87521835bbba226ad7b430cc24e5e3f008edbe61540535419" } ] } ] } } 对应响应对象的源代码如下: type signResp struct { Tx *txbuilder.Template `json:"transaction"` SignComplete bool `json:"sign_complete"` } 结构字段说明如下: Tx 签名之后的交易模板txbuilder.Template,如果签名成功则signatures会由null变成签名的值,而raw_transaction的长度会变长,是因为bc.Tx部分添加了验证签名的参数信息 SignComplete 签名是否完成标志,如果为true表示签名完成,否则为false表示签名未完成,单签的话一般可能为签名密码错误; 而多签的话一般为还需要其他签名。签名失败只需将签名的交易数据用正确的密码重新签名即可,无需再次build-transaction构建交易 3、提交交易 API接口 submit-transaction,代码api/transact.go#L135 提交交易的输入请求json格式如下: { "raw_transaction": "070100020161015f1190c60818b4aff485c865113c802942f29ce09088cae1b117fc4c8db2292212ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d599010001160014a86c83ee12e6d790fb388345cc2e2b87056a0773630240273d5fc4fb06909fbc2968ea91c411fd20f690c88e74284ce2732052400129948538562fe432afd6cf17e590e8645b80edf80b9d9581d0a980d5f9f859e3880620d174db6506e35f2decb5be148c2984bfd0f6c67f043365bf642d1af387c04fd50161015fb018097c4040c8dd86d95611a13c24f90d4c9d9d06b25f5c9ed0556ac8abd73442275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f80a094a58d1d0101160014068840e56af74038571f223b1c99f1b60caaf4566302400cf0beefceaf9fbf1efadedeff7aee5b38ee7a25a20d78b630b01613bc2f8c9230555a6e09aaa11a82ba68c0fc9e98a47c852dfe3de851d93f9b2b7ce256f90d2005cdbcc705f07ad87521835bbba226ad7b430cc24e5e3f008edbe6154053541903013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80bfffcb9901011600140b946646626c55a52a325c8bb48de792284d9b7200013e42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f9d9f94a58d1d01160014c8b4391bab4923a83b955170d24ee4ca5b6ec3fb00013942275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f6301160014366b275ed9b2266b645cf1b8be51009cc3b260e100" } 对应源代码的请求对象如下: type SubmitRequest struct { //function submit request Tx types.Tx `json:"raw_transaction"` } 结构字段说明如下: Tx 签名完成之后的交易信息。这里需要注意该字段中的raw_transaction不是签名交易sign-transaction的全部返回结果,而是签名交易返回结果中transaction中的raw_transaction字段。 submit-transaction请求成功之后返回的json结果如下: { "tx_id": "2c0624a7d251c29d4d1ad14297c69919214e78d995affd57e73fbf84ece361cd" } 对应源代码的响应对象如下: type submitTxResp struct { TxID *bc.Hash `json:"tx_id"` } 结构字段说明如下: TxID 交易ID,当交易被提交到交易池之后会显示该信息,否则表示交易失败
比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 一、合约简述 equity是bytom的一种智能合约语言,是一门声明性谓词语言。详细说明请参考官方equity合约相关介绍。 二、锁定合约流程 1、合约编写 合约的编写可以通过参考合约模板来进行构造,如果需要更进一步的了解,可以阅读一下合约相关的说明文档。以典型的锁定publickey合约为例,其合约代码如下: contract LockWithPublicKey(publicKey: PublicKey) locks locked { clause unlockWithSig(sig: Signature) { verify checkTxSig(publicKey, sig) unlock locked } } 2、编译合约 编译合约的工具暂未提供,建议使用api调用的方式来编译合约。调用的方式可以使用 curl 命令行 或 postman 工具等。 curl 调用方式如下: curl -X POST http://localhost:9888/create-key -d '{"alias": "alice", "password": "123456"}' postman 需要到官网下载工具: https://www.getpostman.com/ 1)bytomd默认启动或开启auth认证,需要通过curl的方式调用API接口 获取access toekn curl -X POST http://localhost:9888/create-access-token -d '{"id":"token1"}' 返回结果如下: { "token": "token1:1fee70f537128a201338bd5f25a3adbf33dad02eae4f4c9ac43f336a069df8f3", "id": "token1", "created_at": "2018-03-20T18:56:01.043919771+08:00" } 2)选择 basic auth 认证方式,填入 access-token 的用户名和密码,即上述返回结果的 token1 和 1fee70f537128a201338bd5f25a3adbf33dad02eae4f4c9ac43f336a069df8f3 3)如果合约参数包含类型PublicKey,便需要获取合约参数 PublicKey, publicKey可通过API接口获取。这里需要将结果保存下来,以后解锁合约的时候需要用到 root_xpub 和 pubkey对应的 derivation_path。 4)编译合约,返回结果program 便是可锁定的合约。(直接编译合约产生的是合约执行步骤,如果部署这样的合约,用户可以直接构造对应的参数便能解锁) 3、构建锁定合约交易 下面表示锁定 1000 个 777e3586d2ec47c4974d262e0ff86fd3c1c063d242d32bdef71d6d16eed6763e 类型的资产到合约 204e925f0fcc2f2618d96b7a3dce2aad28e60ab3339377760d48aea8ae8169ae417403ae7cac00c0 之中。具体内容参考构建交易的wiki: https://github.com/Bytom/bytom/wiki/API-Reference#build-transaction 4、签名交易 具体内容参考签名交易的wiki:https://github.com/Bytom/bytom/wiki/API-Reference#sign-transaction 5、提交交易 具体方式参考提交交易的wiki:https://github.com/Bytom/bytom/wiki/API-Reference#submit-transaction 一旦合约交易打包成功,这样合约交易便发送成功了。 三、解锁合约流程 1、合约交易被区块打包成功之后,可以查看具体的合约交易内容,找到对应的outputID。 其中ID字段便表示 outputID 2、根据outputID通过list-unspent-outputs 接口API 查找合约的UTXO,查看合约锁定的资产类型和数量是否匹配 3、构建解锁合约交易,解锁clause unlockWithSig 的参数为 Signature, 但是签名结果只能在 sign-transaction 成功之后才能得到,所以我们只需构建完成签名参数即可。于之前构建的publicKey 为 4e925f0fcc2f2618d96b7a3dce2aad28e60ab3339377760d48aea8ae8169ae41,那么签名参数为root_xpub 和 derivation_path,对应的值分别为: "dc903a862a14966d47dc6bc935c687c6cccb7a8f9c70f13bb82a41a0fe2696596cb141ff1840f90c75f8f25099f0dc50e005e1e36817d184b2b1eb1354b61575" 和 [ "010400000000000000", "0100000000000000" ] 。 其结构如下:(具体字段可参考官方智能合约文档说明) 4、签名交易,具体内容参考签名交易的wiki:https://github.com/Bytom/bytom/wiki/API-Reference#sign-transaction 5、提交交易,具体方式参考提交交易的wiki:https://github.com/Bytom/bytom/wiki/API-Reference#submit-transaction 解锁合约交易打包成功之后,根据 outputID 通过list-unspent-outputs 接口API 查找原来的合约 UTXO,将显示为空,否则合约交易解锁失败。
用户模型是比原链在最初就需要确定的重要数据结构, 团队的选择还是聚焦在两种典型的模型系统中,Account模型和UTXO模型,和其他大多数区块链设计一样, 选择了模型就决定了协议层的重要实现,两种模型各有利弊,不同区块链针对想聚焦的场景自身会有判断。 UTXO 的起源(来自高明的中本聪) 中本聪对比特币的设计,让整个世界进入了数字货币时代。比特币起源于中本聪,UTXO出自比特币。自然,UTXO来自高明的中本聪。UTXO的优点: 在版本控制方面的考虑,svn 是中心化的数据库保持一份账本,这和区块链的设计自然是相违背的,git 是去中心化的数据库,但会保存太多冗余数据,对于分布式性能肯定是要大打折扣。UTXO数据库是抛弃了历史包袱的git, 只存储了最后一个版本。简易实用。 UTXO 具有天然的匿名效果,一个账户所对应的未花费交易是难以发现的,如门罗币就是采用混币的方式实现隐私的。 在性能方面,由于UTXO是独立的数据记录, 那么就存在极大的并行性可以提升区块链交易验证速度。 设计的易实现性 — 以太坊 弃UTXO用账户模型 以太坊黄皮书的设计者Gavin Wood 对UTXO的理解,十分深刻, 既然UTXO有这么多的优点,他为什么弃用UTXO了? 这时你应该提出个问题,以太坊的最大亮点是什么?你肯定会回答:智能合约。正是因为智能合约的考虑,Gavin Wood要基于UTXO去实现图灵完备的智能合约(功能多样性的超级电脑)是困难的。而账户模型是天然的面向对象的,对每一笔交易,都会在相对应账户上进行记录(nonce++)。为了易于管理账户,而引入了世界状态,每一笔交易都会改变这个世界状态。这和现实世界是相对应的,每一个微小的改变,都会改变这个世界。 追求更高的性能 以太坊的账户模型很容易的实现了超级电脑模型。然而,性能一直是一道难以逾越的坎。在性能方面,utxo天然的可以并行运行,而基于世界状态的以太坊难以扩展。Gavin Wood当然是认识到这一点的,但要去改变,很难。那到不如用带有函数式编程特点的rust 去重写以太坊,也算是一种折中方案。 比原链的思考 马克思哲学的否定之否定规律,事物的发展变化是螺旋式上升的。在区块链领域也是适合的,前进一步,也需要后退半步。基于UTXO模型去实现堆栈式虚拟机, 那还是会失去灵活性,用UTXO去结合以太坊EVM, 难度极大,也是不太实用的,这好比用haskell语言,去实现cpp风格的面向对象编程, 看不到有什么实际的意义。世界上没有银弹,比原链必须舍弃部分,妥协部分才能更好地适应场景。 我们在采用了比特币UTXO的易于并行运算的模型前提下,还做了针对性的改进,加了个资产号字段,使不同的资产可以在同一笔交易中处理转换,只要满足总输入等于总输出就可以。 但为了数据易于管理,易于编程, 我们引入以太坊的世界状态的概念,每一种资产都维持一个全局世界状态,该全局世界状态具有快速可查找,不可更改,简单易提供证明的特性。它的具体实现会参考以太坊的PAT树(一种扩展的基数树),比特币的merkle树,以及cosmos的IAVL树(一种不可更改的平衡二叉树)。每一种资产的所有outputs在一个全局的UTXO数据库中会有一个索引计数(每一个output的计数不能超过1,保持并行计算时,一个output最多能被一个BVM实例所使用,确保了数据一致性)。BVM是比原链实现的智能合约虚拟机模型, 每一笔交易的的执行,都会实例化一个BVM实例,只有在BVM实例中,各资产的世界状态才能在保持有效性,一致性的前提下更新状态。BVM可以并行创造多个”合约沙盒”实例, 在沙盒中合约的运行不受外界影响。 比原链创造的初衷是解决数字资产登记流转的问题, 对于公有链项目,保持简洁,保持高效,保持专注,就是保障安全, 新的扩展型UTXO模型正是基于这种场景实现的融合和改进。
一、引文 设计Bytom 数据结构,组合了许多技术点,如 patricia tree,utxo, bvm, account model,protobuf,sql,memcache 等。本文会对一些技术点做以下两点分析。 Bytom 为什么要采用该技术点? Bytom 如何应用该技术点? 最后介绍综合这些技术点如何实现Bytom。 [patricial tree](http://www.allisons.org/ll/AlgDS/Tree/PATRICIA/) 二、为什么要采用PAT树? •PAT树具有[基数树](https://en.wikipedia.org/wiki/Radix_tree) 的特点,内容可快速追踪。 •PAT树具有[merkle树](https://en.wikipedia.org/wiki/Merkle_tree) 的特点,数据可快速证明。 在分布式系统中,一致性和有效性是十分关键的点。bytom采用PAT树,其中的数据可快速证明,可以快速证明每一份状态机是否一致。内容可快速追踪,可以使bytom在每一个快照状态下,快速查找其数据,并检验数据的有效性。 Bytom 如何用PAT树? Ethereum的PAT树是16叉基数树,分两层,第一层管理的是所有的账户,第二层管理是各账户的存储内容。 Bytom 的PAT树与Ethereum 不同? Bytom 的PAT树是二叉基数树。 Bytom的PAT树是用来管理未花费的outputs。 三、UTXO 为什么要使用UTXO? UTXO诞生于比特币,和现实世界的RMB一样,从央行诞生的那一刻起,他流转无数人的账户,但他的价值面额始终和原来一样,这样以币为中心,而不是以人为中心,资产便于监管和统计。Bytom 就是用于资产的发布和管理的,所以,UTXO的这种以资产为中心的设计模式,是很适合Bytom上面的资产管理。 怎么使用UTXO? 相比比特币的UTXO, bytom的UTXO多了三个字段 assertid, 因为bytom是一个多资产发布与管理的平台,所以使用该字段来唯一确定各种资产。 accountid, 这是便于各账户对utxo的索引和管理,bytom 相比bitcoin,引入了账户模型,后面会介绍。 program, 拥有该utxo的账户可以用Ivy语言编写自己想要的程序放在该字段,以便在交易时,图灵完备的BVM会执行该程序。 四、BVM BVM是在状态机的转化过程被启动运行,也就是excute(transaction)这一步骤。 为什么需要使用BVM? bitcoin 中的非图灵完备栈式脚本语言,所表达的功能极少,很难实现一些稍微复杂的功能,如verify_spv(跨链锚定验证的功能,如btc_relay),再如简单的去实现multi_lock(M人加密,只要收集N人私钥就能解密,0 < N < M)功能。 ethereum中的evm能简单的用solidity语言编写程序实现这些功能,但EVM过于复杂,它号称超级世界计算机,对于bytom这种只对资产有兴趣的区块链是没必要的。因此bytom不如基于[Chain](https://chain.com/) 公司的能用[Ivy](https://chain.com/docs/1.2/ivy-playground/docs) 高级语言编程的CVM去做自己的扩展,灵活易用。 如何使用BVM? 用户在发送每一笔交易时,可以自己编写自己所需要的程序,等到交易打包进块时,BVM会去执行该代码,由于BVM是图灵完备的虚拟机,所以需要加入feed计价机制(feed 等同于ethereum的gas * gasprice)来解决停机问题。 五、账户模型 为什么要采用账户模型? 账户模型易于管理相关数据,是以人为中心,十分的直观。对于BVM来说,基于账户代码去执行也十分便捷。再者我们引入了资产模型,类似于账户模型,这样易于资产的监管和查询。 bytom怎么去实现账户模型? bytom 中的账户模型也分两类,但不同于ethereum中的个人帐户和合约账户, 它是指资产账户和个人账户。 资产账户: assetid 是全局唯一的资产识别id。 alias 是资产的别名,可便于记忆,如(gold, silver) 。 vmversion 是为了软分叉时,做到动态过度。 program 是指发布该资产时需要执行的程序。 initialblockhash 是指该资产是在哪个块高度被登记。 signer 管理公私钥对,以便用该资产的私钥签名,只有拥有该资产私钥的人才能发布该资产。 definition 对该资产的解释说明等。 个人账户: accountid 全局唯一可识别账户id。 alais 帐户名。 signer, 私钥对,用于发送交易。 *utxos 该账户所有的未花费交易的索引,便于快速管理该账户下资产。 program, 该账户发送交易时可插入需要的程序。 综述 UTXO的物理结构,用memcache 存着。UTXO的逻辑结构则是用二叉PAT树来管理。 个人账户 根据AccountId 能够快速的索引其相关的utxo。资产账户根据AssetId能够快速的管理相关的utxo。 上图是描述bytom主要数据结构的uml图。 Bytom用PAT树来组织utxo作为世界状态树。 账户模型分两种,资产账户和个人账户,账户可以索引管理其相关的utxo。 UTXO 池会采用如memcache的内存数据库,落盘数据库会选择关系型数据库,数据会选择protobuf来序列化。 在账户做交易时,每个账户都可以从世界状态树去查找选择自己的utxo,并编写自己所需的资产程序,将其做为TxInput。 在交易打包进块时,验证节点会去实例化BVM,并执行该交易中所有TxInput中的程序。
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 人们常说,“阅读源代码”是学习编程的一种重要方法。作为程序员,我们在平时的学习工作中,都应该阅读过不少源代码。但是对于大多数人来说,阅读的可能更多是一些代码片断、示例,或者在老师、同事的指导下,先对要阅读的项目代码有了整体的了解之后,再进行针对性的阅读。 但是如果我们面对的是一个像比原这样比较庞大的项目,身边又没有人指导,只能靠自己去看,这时应该怎么来阅读呢?也许每个人也都能找到自己的办法,或高效,或低效,或放弃。 我在这次阅读比原源代码的过程中,尝试的是这样一种方法:从外部入手,通过与比原节点进行数据交互,来一步步了解比原的内部原理。就像剥石榴一样,一点点小心翼翼的下手,最后才能吃到鲜美的果肉。 所以这个文章系列叫作“剥开比原看代码”。 说明 在系列中的每一章,我通常都会由一个或者几个相关的问题入手,然后通过对源代码进行分析,来说明比原的代码是如何实现的。对于与当前问题关系不大的代码,则会简单带过,等真正需要它们出场的时候再详细解说。 为了保证文章中引用代码的稳定性,我将基于比原的v1.0.1代码进行分析。随着时间推移,比原的代码也将快速更新,但是我觉得,只要把这个版本的代码理解了,再去看新的代码,应该是一件很容易的事情。 在文章中,将会有一些直接指向github上bytom源代码的链接。为了方便,我专门将bytom v1.0.1的代码放到了一个新的仓库中,这样就不容易与比原官方的最新代码混淆。该仓库地址为:https://github.com/freewind/bytom-v1.0.1 当然,你不必clone这个仓库(clone官方仓库http://github.com/Bytom/bytom就够了),然后在必要的时候,使用以下命令将代码切换到v1.0.1的tag,以便与本系列引用的代码一致: git fetch git checkout -b v1.0.1 不论采用哪种阅读方法,我想第一步都应该先在本地把比原节点跑起来,试试各种功能。 对于如何下载、配置和安装的问题,请直接参看官方文档https://github.com/Bytom/bytom/tree/v1.0.1(注意我这里给出的是v1.0.1的文档),这里不多说。 本篇问题 当我们本地使用make bytomd编译完比原后,我们可以使用下面的命令来进行初始化: ./bytomd init --chain_id testnet 这里指定了使用的chain是testnet(还有别的选项,如mainnet等等)。运行成功后,它将会在本地文件系统生成一些配置文件,供比原启动时使用。 所以我的问题是: 比原初始化时,产生了什么样的配置文件,放在了哪个目录下? 下面我将结合源代码,来回答这个问题。 目录位置 首先比原在本地会有一个目录专门用于放置各种数据,比如密钥、配置文件、数据库文件等。这个目录对应的代码位于config/config.go#L190-L205: func DefaultDataDir() string { // Try to place the data folder in the user's home dir home := homeDir() dataDir := "./.bytom" if home != "" { switch runtime.GOOS { case "darwin": dataDir = filepath.Join(home, "Library", "Bytom") case "windows": dataDir = filepath.Join(home, "AppData", "Roaming", "Bytom") default: dataDir = filepath.Join(home, ".bytom") } } return dataDir } 可以看到,在不同的操作系统上,数据目录的位置也不同: 苹果系统(darwin):~/Library/Bytom Windows(windows): ~/AppData/Roaming/Bytom 其它(如Linux):~/.bytom 配置文件内容 我们根据自己的操作系统打开相应的目录(我的是~/Library/Bytom),可以看到有一个config.toml,内容大约如下: $ cat config.toml # This is a TOML config file. # For more information, see https://github.com/toml-lang/toml fast_sync = true db_backend = "leveldb" api_addr = "0.0.0.0:9888" chain_id = "testnet" [p2p] laddr = "tcp://0.0.0.0:46656" seeds = "47.96.42.1:46656,172.104.224.219:46656,45.118.132.164:46656" 它已经把一些基本信息告诉我们了,比如: db_backend = "leveldb":说明比原内部使用了leveldb作为数据库(用来保存块数据、帐号、交易信息等) api_addr = "0.0.0.0:9888":我们可以在浏览器中打开http://localhost:9888来访问dashboard页面,进行查看与管理 chain_id = "testnet":当前连接的是testnet,即测试网,里面挖出来的比原币是不值钱的 laddr = "tcp://0.0.0.0:46656":本地监听46656端口,别的节点如果想连我,就需要访问我的46656端口 seeds = "47.96.42.1:46656,172.104.224.219:46656,45.118.132.164:46656":比原启动后,会主动连接这几个地址获取数据 内容模板 使用不同的chain_id去初始化时,会生成不同内容的配置文件,那么这些内容来自于哪里呢? 原来在config/toml.go#L22-L45,预定义了不同的模板内容: var defaultConfigTmpl = `# This is a TOML config file. # For more information, see https://github.com/toml-lang/toml fast_sync = true db_backend = "leveldb" api_addr = "0.0.0.0:9888" ` var mainNetConfigTmpl = `chain_id = "mainnet" [p2p] laddr = "tcp://0.0.0.0:46657" seeds = "45.79.213.28:46657,198.74.61.131:46657,212.111.41.245:46657,47.100.214.154:46657,47.100.109.199:46657,47.100.105.165:46657" ` var testNetConfigTmpl = `chain_id = "testnet" [p2p] laddr = "tcp://0.0.0.0:46656" seeds = "47.96.42.1:46656,172.104.224.219:46656,45.118.132.164:46656" ` var soloNetConfigTmpl = `chain_id = "solonet" [p2p] laddr = "tcp://0.0.0.0:46658" seeds = "" ` 可以看到,原来这些端口号和seed的地址,都是事先写好在模板里的。 而且,通过观察这些配置,我们可以发现,如果chain_id不同,则监听的端口和连接的种子都不同: mainnet(连接到主网): 46657,会主动连接6个种子 testnet(连接到测试网): 46656,会主动连接3个种子 solonet(本地单独节点): 46658,不会主动连接别人(也因此不会被别人连接上),适合单机研究 写入文件 这里我们需要快速的把bytomd init的执行流程过一遍,才能清楚配置文件的写入时机,也同时把前面的内容串在了一起。 首先,当我们运行bytomd init时,它对应的代码入口为cmd/bytomd/main.go#L54: func main() { cmd := cli.PrepareBaseCmd(commands.RootCmd, "TM", os.ExpandEnv(config.DefaultDataDir())) cmd.Execute() } 其中的config.DefaultDataDir()就对应于前面提到数据目录位置。 然后执行cmd.Execute(),将根据传入的参数init,选择下面的函数来执行:cmd/bytomd/commands/init.go#L25-L24 func initFiles(cmd *cobra.Command, args []string) { configFilePath := path.Join(config.RootDir, "config.toml") if _, err := os.Stat(configFilePath); !os.IsNotExist(err) { log.WithField("config", configFilePath).Info("Already exists config file.") return } if config.ChainID == "mainnet" { cfg.EnsureRoot(config.RootDir, "mainnet") } else if config.ChainID == "testnet" { cfg.EnsureRoot(config.RootDir, "testnet") } else { cfg.EnsureRoot(config.RootDir, "solonet") } log.WithField("config", configFilePath).Info("Initialized bytom") } 其中的configFilePath,就是config.toml的写入地址,即我们前面所说的数据目录下的config.toml文件。 cfg.EnsureRoot将用来确认数据目录是有效的,并且将根据传入的chain_id不同,来生成不同的内容写入到配置文件中。 它对应的代码是config/toml.go#L10 func EnsureRoot(rootDir string, network string) { cmn.EnsureDir(rootDir, 0700) cmn.EnsureDir(rootDir+"/data", 0700) configFilePath := path.Join(rootDir, "config.toml") // Write default config file if missing. if !cmn.FileExists(configFilePath) { cmn.MustWriteFile(configFilePath, []byte(selectNetwork(network)), 0644) } } 可以看到,它对数据目录进行了权限上的确认,并且发现当配置文件存在的时候,不会做任何更改。所以如果我们需要生成新的配置文件,就需要把旧的删除(或改名)。 其中的selectNetwork(network)函数,实现了根据chain_id的不同来组装不同的配置文件内容,它对应于master/config/toml.go#L48: func selectNetwork(network string) string { if network == "testnet" { return defaultConfigTmpl + testNetConfigTmpl } else if network == "mainnet" { return defaultConfigTmpl + mainNetConfigTmpl } else { return defaultConfigTmpl + soloNetConfigTmpl } } 果然就是一个简单的字符串拼接,其中的defaultConfigTmpl和*NetConfgTmpl在前面已经出现,这里不重复。 最后调用第三方函数cmn.MustWriteFile(configFilePath, []byte(selectNetwork(network)), 0644),把拼接出来的配置文件内容以权限0644写入到指定的文件地址。 到这里,我们这个问题就算回答完毕了。