/
content.json
1 lines (1 loc) · 868 KB
/
content.json
1
{"meta":{"title":"深入浅出区块链","subtitle":"系统学习区块链技术","description":"打造最好的区块链技术博客","author":"Tiny熊","url":"https://learnblockchain.cn"},"pages":[{"title":"","date":"2017-10-24T10:16:10.000Z","updated":"2019-03-27T08:30:09.044Z","comments":true,"path":"about/index.html","permalink":"https://learnblockchain.cn/about/index.html","excerpt":"","text":"深入浅出区块链博客深入浅出区块链是由一群区块链技术爱好者共同维护的一个秉承去中心化精神的区块链技术博客。 博客最初由Tiny熊发起,后HiBlock区块链社区、登链学院-区块链技术培训学院 等其他的组织及个人加入贡献内容。经过一年多的发展已经是国内内容质量最高、访问量最大的区块链技术博客站。 深入浅出区块链建站依赖一直秉承开放、协作、透明、链接、分享的价值观,致力于分享高质量的内容给开发者,帮助开发者成长。 我们也诚邀各位开发者(技术布道者)来贡献内容,做为作者,你将获得: 收获区块链技术博客第一站作者的荣耀,和一群最优秀的人一起交流; 享有HiBlock区块链社区数千开发者社区资源; 免费学习登链学院所有课程。 贡献如果你有意加入我们,称为我们的一员,请移步Github我们愿意和所有的技术社区建立联系与合作,一起推动区块链技术在中国的发展。我的微信: xlbxiong 部分作者介绍 Tiny熊写过代码,带过团队,抱过娃,北航硕士,登链创始人&CTO,精通以太坊智能合约开发作者。 BobHiBlock 社区发起人,敏捷开发培训专家。 虞是乎磨链社区共建者,Nerthus CTO。 盖盖英属哥伦比亚大学电子工程系博士(在读),专注于解决区块链扩容问题,关注最新区块链技术研究,探索区块链在身份认证、物联网、安全与隐私方面的应用。Solidity 中文文档译者之一。 杨尉先河系统CTO TopJohn区块链开源社区爱好者,磨链社区共建者,目前从事区块链研究相关工作,超级账本TWGC工作组active volunteer。 Magic_陈互联网行业老兵,有前后端开发经验,熟悉python、go、nodejs等多种语言,目前在国内某一线互联网大厂工作,主要从事devops以及分布式存储方面的工作,注重底层技术,同时对新技术保持敏感。 被打劫的强盗码农一枚,喜欢健身,国漫。目前在一家金融公司工作,个人全面拥抱区块链技术。"},{"title":"深入浅出区块链-文章分类","date":"2017-10-24T13:14:00.000Z","updated":"2017-11-03T05:31:44.000Z","comments":false,"path":"categories/index.html","permalink":"https://learnblockchain.cn/categories/index.html","excerpt":"","text":""},{"title":"友情链接","date":"2019-03-24T07:52:24.000Z","updated":"2019-03-24T08:27:14.716Z","comments":true,"path":"friends/index.html","permalink":"https://learnblockchain.cn/friends/index.html","excerpt":"","text":"排名不分先后 Tiny熊的博客 Tiny熊个人博客 HPB芯链 - HPB 公链 forecho 的博客 - forecho 个人博客 静轩之别苑 - 个人博客、前端开发博客 币兔 - 发现DAPP 无退社区 - 区块链社区 BlockFlow - 区块链论坛 区块链技术中文社区 友链申请"},{"title":"视频课程介绍及招募体验师","date":"2018-07-24T07:17:12.000Z","updated":"2018-11-28T09:46:53.338Z","comments":true,"path":"course/index.html","permalink":"https://learnblockchain.cn/course/index.html","excerpt":"","text":"区块链视频课程Tiny熊和朋友们成立了登链学院(公众号同名),为了更好的普及区块链技术,也为了这个事情能持续的做下去,所以我们选择用商业公司来运作,在推广普及区块链技术的同时,也希望能小有盈利以改善家人的生活。 我们现在推出的视频课程,有: 区块链入门-零基础搞懂区块链这是0基础入门课,课程介绍可点击上方链接。 深入详解以太坊智能合约语言Solidity这是市面上最深入的讲解智能合约语言Solidity的视频教程,还在更新中,预购从速,更新完之后提价。 通过代币学以太坊智能合约开发这门课程囊括发行代币(token),众筹的所有知识点,同时通过代币的开发,引申出了很多智能合约开发知识点,不管是想要发行代币还是学习智能合约都是不可多得好教程。这门课程课程会假定大家对区块链相关概念有了解,因此大家最好先学习上面两门课程。 区块链全栈-以太坊DAPP开发实战本门课从一个去中心化的概念讲起,让大家理解去中心化应用与传统应用的不同,以及开发一个完整去中心化应用需要掌握哪些知识,然后用一个个案例逐步深入介绍每一个知识点。 使用Python3 实现极简区块链如果你懂区块链原理,但是不知道如何使用代码实现区块链。那这门课程你一定不要错过。这门课程以python3实现区块链为例,教你如何打包区块,如何构建一个区块链,以及如何达成共识等等,让你获得亲自实现一个区块链的成就感。 以太坊去中心化网页钱包开发从如何创建账号开始,深入探索BIP32、BIP44、BIP39等提案,以及如何存储私钥、发送离线签名交易和Token。 更多课程请期待… 招募课程体验师先说一下我们为什么要招募课程体验师,原因是这样的,在有一些课程中,我自己觉的已经把问题讲的比较清楚,可是还是有学员在微信中问我问题,在经历好几个提问之后,我发现每个人知识结构的不一样,对问题的理解是不一样的,有一些我认为是基本知识点而没有讲解。 估计有更多的问题因为学员不愿意反馈或者反馈渠道不通畅而发现不了,而这样我们的课程就错过了很多改进的机会。 登链做为一家认认真真做区块链技术培训的学院,我们的使命是让所有程序员都懂区块链,因此我们需要一些真正愿意且积极学习区块链技术的小伙伴来一起参与帮助我们改进课程, 不管你现在的水平如何,只要愿意学,我们都欢迎你来参与。 如果你愿意帮助我们改进课程,你将免费学习所有的课程。有兴趣的参与的同学,请加课程助理的微信: upchainedu 申请, 暗号是:“课程体验师”。体验师参与的原则是: 体验师需要先自己购买学习,反馈建议,课程助理返学费。如果你负担不起课程,可以联系体验师索要优惠劵。 最后很期待大家的参与,也感谢要参与的同学,让我们一起做全国做好的区块链技术培训学院,让每个程序员都懂区块链。"},{"title":"深入浅出区快链-文章标签","date":"2017-10-24T13:11:50.000Z","updated":"2017-11-03T05:31:44.000Z","comments":false,"path":"tags/index.html","permalink":"https://learnblockchain.cn/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"以太坊DAO攻击解决方案代码解析","slug":"dao","date":"2019-04-07T11:41:25.000Z","updated":"2019-04-07T12:26:31.072Z","comments":true,"path":"2019/04/07/dao/","link":"","permalink":"https://learnblockchain.cn/2019/04/07/dao/","excerpt":"虽然 The DAO 攻击发生在2016年,但解除攻击的方案依然值得学习。","text":"虽然 The DAO 攻击发生在2016年,但解除攻击的方案依然值得学习。 区块链本是去中心化架构,在以太坊首次遭遇严重黑客攻击智能合约事件时,采用的解决方案却破坏了去中心化理念。 这里不讨论其是否违背区块链精神,本文重点介绍解决方案的技术实施细节。方案中涉及网络隔离技术和矿工共识投票技术。且只是从软件上处理,未破坏共识协议。解决方案的成功实施,为区块链分叉提供了实操经验,值得公链开发者学习。 什么是 The DAO 攻击简单地讲,在2016年4月30日开始,一个名为“The DAO”的初创团队,在以太坊上通过智能合约进行ICO众筹。28天时间,筹得1.5亿美元,成为历史上最大的众筹项目。 THE DAO创始人之一Stephan TualTual在6月12日宣布,他们发现了软件中存在的“递归调用漏洞”问题。 不幸的是,在程序员修复这一漏洞及其他问题的期间,一个不知名的黑客开始利用这一途径收集THE DAO代币销售中所得的以太币。6月18日,黑客成功挖到超过360万个以太币,并投入到一个DAO子组织中,这个组织和THE DAO有着同样的结构。 THE DAO持有近15%的以太币总数,因此THE DAO这次的问题对以太坊网络及其加密币都产生了负面影响。 6月17日,以太坊基金会的Vitalik Buterin更新一项重要报告,他表示,DAO正在遭到攻击,不过他已经研究出了解决方案: 现在提出了软件分叉解决方案,通过这种软件分叉,任何调用代码或委托调用的交易——借助代码hash0x7278d050619a624f84f51987149ddb439cdaadfba5966f7cfaea7ad44340a4ba(也就是DAO和子DAO)来减少账户余额——都会视为无效…… 最终因为社交的不同意见,最终以太坊分裂出支持继续维持原状的以太经典 ETC,同意软件分叉解决方案的在以太坊当前网络实施。 以上内容整理自文章The DAO 攻击。 解决方案因为投资者已经将以太币投入了 The DAO 合约或者其子合约中,在攻击后无法立刻撤回。需要让投资者快速撤回投资,且能封锁黑客转移资产。 V神公布的解决方案是,在程序中植入转移合约以太币代码,让矿工选择是否支持分叉。在分叉点到达时则将 The DAO 和其子合约中的以太币转移到一个新的安全的可取款合约中。全部转移后,原投资者则可以直接从取款合约中快速的拿回以太币。 取款合约在讨论方案时,已经部署到主网。合约地址是 0xbf4ed7b27f1d666546e30d74d50d173d20bca754。 取款合约代码如下: 1234567891011121314151617181920212223// Deployed on mainnet at 0xbf4ed7b27f1d666546e30d74d50d173d20bca754contract DAO { function balanceOf(address addr) returns (uint); function transferFrom(address from, address to, uint balance) returns (bool); uint public totalSupply;}contract WithdrawDAO { DAO constant public mainDAO = DAO(0xbb9bc244d798123fde783fcc1c72d3bb8c189413); address public trustee = 0xda4a4626d3e16e094de3225a751aab7128e96526; function withdraw(){ uint balance = mainDAO.balanceOf(msg.sender); if (!mainDAO.transferFrom(msg.sender, this, balance) || !msg.sender.send(balance)) throw; } function trusteeWithdraw() { trustee.send((this.balance + mainDAO.balanceOf(this)) - mainDAO.totalSupply()); }} 同时,为照顾两个阵营,软件提供硬分叉开关,选择权则交给社区。支持分叉的矿工会在X区块到X+9区块出块时,在区块extradata字段中写入0x64616f2d686172642d666f726b(“dao-hard-fork”的十六进制数)。 从分叉点开始,如果连续10个区块均有硬分叉投票,则表示硬分叉成功。 矿工投票与区块头校验首先,选择权交给社区。因此是否同意硬分叉,可通过参数进行选择。但是在当前版本中,社区已完成硬分叉,所以已移除开关类代码。 当前,主网已默认配置支持DAO分叉,并设定了开始硬分叉高度 1920000,代码如下: 12345// params/config.go:38MainnetChainConfig = &ChainConfig{ DAOForkBlock: big.NewInt(1920000), DAOForkSupport: true, } 如果矿工支持分叉,则需要在从高度 192000 到 192009,在区块头 extradata 写入指定信息 0x64616f2d686172642d666f726b ,以表示支持硬分叉。 12345//params/dao.go:28var DAOForkBlockExtra = common.FromHex(\"0x64616f2d686172642d666f726b\")// params/dao.go:32var DAOForkExtraRange = big.NewInt(10) 支持硬分叉时矿工写入固定的投票信息: 1234567891011121314// miner/worker.go:857if daoBlock := w.config.DAOForkBlock; daoBlock != nil { // 检查是否区块是否仍然属于分叉处理期间:[DAOForkBlock,DAOForkBlock+10) limit := new(big.Int).Add(daoBlock, params.DAOForkExtraRange) if header.Number.Cmp(daoBlock) >= 0 && header.Number.Cmp(limit) < 0 { // 如果支持分叉,则覆盖Extra,写入保留的投票信息 if w.config.DAOForkSupport { header.Extra = common.CopyBytes(params.DAOForkBlockExtra) } else if bytes.Equal(header.Extra, params.DAOForkBlockExtra) { // 如果矿工反对,则不能让其使用保留信息,覆盖它。 header.Extra = []byte{} } }} 需要连续10个区块的原因是为了防止矿工使用保留信息污染非分叉块和方便轻节点安全同步数据。同时,所有节点在校验区块头时,必须安全地校验特殊字段信息,校验区块是否属于正确的分叉上。 1234567891011121314151617181920212223242526// consensus/ethash/consensus.go:294 if err := misc.VerifyDAOHeaderExtraData(chain.Config(), header); err != nil { //❶ return err}// consensus/misc/dao.go:47 func VerifyDAOHeaderExtraData(config *params.ChainConfig, header *types.Header) error { if config.DAOForkBlock == nil {//❷ return nil } limit := new(big.Int).Add(config.DAOForkBlock, params.DAOForkExtraRange) //❸ if header.Number.Cmp(config.DAOForkBlock) < 0 || header.Number.Cmp(limit) >= 0 { return nil } if config.DAOForkSupport { if !bytes.Equal(header.Extra, params.DAOForkBlockExtra) { //❹ return ErrBadProDAOExtra } } else { if bytes.Equal(header.Extra, params.DAOForkBlockExtra) {//❺ return ErrBadNoDAOExtra } } // All ok, header has the same extra-data we expect return nil} ❶ 在校验区块头时增加 DAO 区块头识别校验。 ❷ 如果节点未设置分叉点,则不校验。 ❸ 确保只需在 DAO 分叉点的10个区块上校验。 ❹ 如果节点允许分叉,则要求区块头 Extra 必须符合要求。 ❺ 当然,如果节点不允许分叉,则也不能在区块头中加入非分叉链的 Extra 特殊信息。 这种 config.DAOForkBlock 开关,类似于互联网公司产品新功能灰度上线的功能开关。在区块链上,可以先实现功能代码逻辑。至于何时启用,则可以在社区、开发者讨论后,确定最终的开启时间。当然区块链上区块高度等价于时间戳,比如 DAO 分叉点 1920000 也是讨论后敲定。 如何分离网络?如果分叉后不能快速地分离网络,会导致节点出现奇奇怪怪的问题。长远来说,为针对以后可能出现的分叉,应设计一种通用解决方案,已降低代码噪音。否则,你会发现代码中到处充斥着一些各种梗。但时间又非常紧急,这次的 The DAO 分叉处理是通过特定代码拦截实现。 在我看来,区块链项目不同于其他传统软件,一旦发现严重BUG是非常致命的。在上线后的代码修改,应保持尽可能少和充分测试。非常同意 the dao 的代码处理方式。不必为以后可能的分叉,而做出觉得“很棒”的功能。务实地解决问题才是正道。 不应该让节点同时成为两个阵营的中继点,应分离出两个网络,以让其互不干预。The DAO 硬分叉的处理方式是:节点连接握手后,向对方请求分叉区块头信息。在15秒必须响应,否则断开连接。 代码实现是在eth/handler.go文件中,在消息层进行拦截处理。 节点握手后,开始15秒倒计时,一旦倒计时结束,则断开连接。 12345// eth/handler.go:300 p.forkDrop = time.AfterFunc(daoChallengeTimeout, func() { p.Log().Debug(\"Timed out DAO fork-check, dropping\") pm.removePeer(p.id) }) 在倒计时前,需要向对方索要区块头信息,以进行分叉校验。 12345678910111213141516171819202122232425// eth/handler.go:297 if err := p.RequestHeadersByNumber(daoBlock.Uint64(), 1, 0, false); err != nil { return err }``` 此时,对方在接收到请求时,如果存在此区块头则返回,否则忽略。```go// eth/handler.go:348 case msg.Code == GetBlockHeadersMsg: var query getBlockHeadersData if err := msg.Decode(&query); err != nil { return errResp(ErrDecode, \"%v: %v\", msg, err) } hashMode := query.Origin.Hash != (common.Hash{}) first := true maxNonCanonical := uint64(100) var ( bytes common.StorageSize headers []*types.Header unknown bool ) //省略一部分 ... return p.SendBlockHeaders(headers) 这样,有几种情况出现。根据不同情况分别处理: 有返回区块头: 如果返回的区块头不一致,则校验不通过,等待倒计时结束。如果区块头一致,则根据前面提到的校验分叉区块方式检查。校验失败,此直接断开连接,说明已经属于不同分叉。校验通过,则关闭倒计时,完成校验。 123456789101112// eth/handler.go:465if p.forkDrop != nil && pm.chainconfig.DAOForkBlock.Cmp(headers[0].Number) == 0 { p.forkDrop.Stop() p.forkDrop = nil if err := misc.VerifyDAOHeaderExtraData(pm.chainconfig, headers[0]); err != nil { p.Log().Debug(\"Verified to be on the other side of the DAO fork, dropping\") return err } p.Log().Debug(\"Verified to be on the same side of the DAO fork\") return nil } 没有返回区块头: 如果自己也没有到达分叉高度,则不校验,假定双方在同一个网络。但我自己已经到达分叉高度,则考虑对方的TD是否高于我的分叉块。如果是,则包容,暂时认为属于同一网络。否则,则校验失败。 12345678910111213141516// eth/handler.go:442 if len(headers) == 0 && p.forkDrop != nil { verifyDAO := true if daoHeader := pm.blockchain.GetHeaderByNumber(pm.chainconfig.DAOForkBlock.Uint64()); daoHeader != nil { if _, td := p.Head(); td.Cmp(pm.blockchain.GetTd(daoHeader.Hash(), daoHeader.Number.Uint64())) >= 0 { verifyDAO = false } } if verifyDAO { p.Log().Debug(\"Seems to be on the same side of the DAO fork\") p.forkDrop.Stop() p.forkDrop = nil return nil }} 转移资产上述所做的一切均为安全、稳定的硬分叉,隔离两个网络。硬分叉的目的是,以人为介入的方式拦截攻击者资产。 一旦到达分叉点,则立即激活资产转移操作。首先,矿工在挖到分叉点时,需执行转移操作: 12345678910// miner/worker.go:877func (w *worker) commitNewWork(interrupt *int32, noempty bool, timestamp int64) { // ...// Create the current work task and check any fork transitions needed env := w.current if w.config.DAOForkSupport && w.config.DAOForkBlock != nil && w.config.DAOForkBlock.Cmp(header.Number) == 0 { misc.ApplyDAOHardFork(env.state) } // ...} 其次,任何节点在接收区块,进行本地处理校验时同样需要在分叉点执行: 123456789// core/state_processor.go:66func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) { //... // Mutate the block and state according to any hard-fork specs if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 { misc.ApplyDAOHardFork(statedb) } //...} 转移资金也是通过取款合约处理。将The DAO 合约包括子合约的资金,全部转移到新合约中。 123456789101112func ApplyDAOHardFork(statedb *state.StateDB) { // Retrieve the contract to refund balances into if !statedb.Exist(params.DAORefundContract) { statedb.CreateAccount(params.DAORefundContract) } // Move every DAO account and extra-balance account funds into the refund contract for _, addr := range params.DAODrainList() { statedb.AddBalance(params.DAORefundContract, statedb.GetBalance(addr)) statedb.SetBalance(addr, new(big.Int)) }} 至此,合约资金已全部强制转移到新合约。 参考资料 EIP 779: Hardfork Meta: DAO Fork Hard Fork Specification PR#2814-finalize the DAO fork","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"攻击","slug":"攻击","permalink":"https://learnblockchain.cn/tags/攻击/"}]},{"title":"如何开发一款以太坊安卓钱包系列5 - 发送转账交易","slug":"eth-wallet-dev-5","date":"2019-04-04T06:34:14.000Z","updated":"2019-04-05T10:29:31.048Z","comments":true,"path":"2019/04/04/eth-wallet-dev-5/","link":"","permalink":"https://learnblockchain.cn/2019/04/04/eth-wallet-dev-5/","excerpt":"这是如何开发以太坊安卓钱包系列第5篇,利用钱包对交易进行本地签名,然后发送到以太坊网络。","text":"这是如何开发以太坊安卓钱包系列第5篇,利用钱包对交易进行本地签名,然后发送到以太坊网络。 预备知识发送一个交易, 逻辑上会包含三个步骤: 构造交易对象; 对交易进行签名; 把签名后的交易序列化后发送到网络节点。 第 2 3步,web3j 提供的API 几句代码就可以解决,关键第 1 步构造交易对象,我们来逐步分解。 一个交易长什么样一个交易的结构如下: 1234567891011 public class RawTransaction { private String to; private BigInteger value; private BigInteger gasLimit; private BigInteger gasPrice; private BigInteger nonce; private String data;} 发起交易的时候,就是需要填充每一个字段,构建这样一个交易结构,每个字段含义如下: to : 用户要转账的目标地址; value: 转账的金额; gasLimit: 表示用户愿意为交易准备的(计算和存储空间)工作量; gasPrice: 交易发起者愿意支付的单位工作量费用,矿工在选择交易的时候,是按照gasPrice进行排序,先服务高出价者,因此如果出价过低会导致交易迟迟不能打包确认,出价过高则费用较大; Gas是以太坊的工作计费机制,是交易者给矿工打包的一笔预算,预算= gasLimit gasPrice, 可以类比为请货车的运费:公里数 每公里单价。 nonce: 交易序列号, 它可以用来防止重放攻击,如果没有nonce的活,同一笔交易就可以多次广播。同样的道理,如果遇到一个交易很久没有打包,可以使用相同的交易nonce序列号, 用更高的gasPrice 重发这笔交易; data: 交易的附加的消息,对于代币Token转账,则data就是调用函数的ABI编码数据,参考:如何理解以太坊ABI 这个结构中没有from地址 ,是因为在对交易用私钥签名后,可以推倒出用户地址。 交易界面用户在App界面通过以下界面来发起一个交易: 这个界面对应的代码是SendActivity.java,构造交易目标地址和金额可以直接从界面获得。 设置 Gas如果 Gas 设置丢给用户,从体验上说有点说不过去,因此我们给用户一些推荐值。 Gas Price先说说gas price, gas price 是一个竞争值, 一个矿工能做的工作量基本是固定的,因此他总是会挑给价最高的,如果一个时间段内,提交的交易数量很多,价格也会随之水涨船高,如果交易少,价格就会下降。 那么设置一个合理的价格就显得很重要,怎么恰到好处设置一个不至于浪费又不用等待长时间的gas price呢? 幸运的是web3 提供了一个接口获取最近区块的gas price,因此可以这个作为推荐值。 也有一些第三方提供的预测gas price的接口,如:gasPriceOracle 、 ethgasAPI、 etherscan gastracker,大家可自行选择。 获取Gas设置,代码中提供了一个专门的类FetchGasSettingsInteract 1234567891011121314151617181920212223242526272829public class FetchGasSettingsInteract { private final MutableLiveData<BigInteger> gasPrice = new MutableLiveData<>(); private BigInteger cachedGasPrice; public FetchGasSettingsInteract( gasSettingsDisposable = Observable.interval(0, 60, TimeUnit.SECONDS) .doOnNext(l ->fetchGasPriceByWeb3() ).subscribe(); } private void fetchGasPriceByWeb3() { final Web3j web3j = Web3j.build(rpcServerUrl)); try { EthGasPrice price = web3j.ethGasPrice().send(); if (price.getGasPrice().compareTo(BalanceUtils.gweiToWei(BigDecimal.ONE)) >= 0) { cachedGasPrice = price.getGasPrice(); gasPrice.postValue(cachedGasPrice); } } catch (Exception ex) { // silently } } public MutableLiveData<BigInteger> gasPriceUpdate() { return gasPrice; }} FetchGasSettingsInteract 类中gasPrice是一个可以订阅的LiveData数据,fetchGasPriceByWeb3函数用于获取价格,在构造函数中使用了Observable.interval来开启一个间隔一分钟的循环任务,即每分钟去取一下最新的价格。 Gas LimitGas Limit用来确定工作量,不像Gas Price 谁时间的变化而浮动,工作量任务确定后,这个值就是固定的,如一个转账到普通的交易,工作量中是21000。 对于智能合约交易,gasLimit则根据执行的任务而变化,如果设定的不够,会发生out-of-gas 错误,交易就不会打包上链,如果设定的过高,多余的就会退回交易发起者,这也是为什么我把这个费用称为预算的原因。 有些人会认为直接设置高一点的值,反正会退回,但如果合约执行出错,就会吃掉所有的gas,对于ERC20转账,一般推荐设置的值为90000, 如果是运行非标准的智能合约,如使用DAPP,可以使用ethEstimateGas 函数进行预测。 在钱包中运行DAPP,也是钱包的一项重要功能,我会在小专栏进行介绍。 这里使用推荐默认值,在FetchGasSettingsInteract加入方法: 1234567891011121314public Single<GasSettings> fetch(ConfirmationType type) { return Single.fromCallable( () -> { BigInteger gasLimit; if (type == ConfirmationType.ETH) { gasLimit = new BigInteger(21000); } else if (type == ConfirmationType.ERC20) { gasLimit = new BigInteger(21000); } else { ... } return new GasSettings(cachedGasPrice, gasLimit); });} 为了避免 SendActivity(UI) 与数据的耦合使用了ConfirmationViewModel, ConfirmationViewModel 中保留了一个 FetchGasSettingsInteract 对象,界面提供推荐的gas的代码逻辑调用流程是这样: 其中虚线部分是数据订阅回调,在SendActivity拿到GasSettings就可以进行展示。 123456789Title: 获取Gas 过程SendActivity->ConfirmationViewModel: prepareConfirmationViewModel->FetchGasSettingsInteract: gasPriceUpdateNote right of FetchGasSettingsInteract: 定时请求FetchGasSettingsInteract-->>ConfirmationViewModel: onGasPriceConfirmationViewModel->FetchGasSettingsInteract: fetchFetchGasSettingsInteract->FetchGasSettingsInteract: fetchFetchGasSettingsInteract-->>ConfirmationViewModel: onGasSettingsConfirmationViewModel-->>SendActivity: onGasSettings流程图源码, hexo 无法渲染,使用 https://bramp.github.io/js-sequence-diagrams/ 代码调用代码逻辑,大家最好把代码https://github.com/xilibi2003/Upchain-wallet 克隆到本地跟一下。 确认交易数据用户在没有填写收款地址、发送金额以及调整好Gas(可选),在发送交易之前,一般需要用户再次确认下交易详情,使用下面这个对话框: 代码中使用的一个自定义的ConfirmTransactionView来展示这个信息,UI部分的代码就不贴了。 在用户确认无误之后,点击确认,用户输入密码之后,就可以正式发起交易了。 获取nonce细心的同学可能会发现,现在构造交易结构还差nonce,web3j提供了相应的API,获取的逻辑在EthereumNetworkRepository类中,代码如下: 123456789public Single<BigInteger> getLastTransactionNonce(Web3j web3j, String walletAddress){ return Single.fromCallable(() -> { EthGetTransactionCount ethGetTransactionCount = web3j .ethGetTransactionCount(walletAddress, DefaultBlockParameterName.PENDING) .send(); return ethGetTransactionCount.getTransactionCount(); });} 发起交易完整的交易流程调用序列图如下: 12345678910Title: 用户发起交易调用Note left of SendActivity: 用户点击发送SendActivity->ConfirmationViewModel: createTransactionConfirmationViewModel->CreateTransactionInteract: createEthTransactionCreateTransactionInteract->EthereumNetworkRepository: getLastTransactionNonceCreateTransactionInteract->CreateTransactionInteract: createRawTransactionCreateTransactionInteract->CreateTransactionInteract: signMessageCreateTransactionInteract->CreateTransactionInteract: ethSendRawTransactionCreateTransactionInteract-->>ConfirmationViewModel: onCreateTransactionConfirmationViewModel-->>SendActivity:onTransaction流程图源码, 因 hexo 无法渲染,使用 https://bramp.github.io/js-sequence-diagrams/ 交易主要在createEthTransaction函数完成,逻辑有: 获取交易nonce 使用nonce, gasPrice, gasLimit, to, amount 构造一个原始交易 使用 密码 + keystore 对原始交易进行签名 发送交易, 把txHash 封装为一个可回调的数据 createEthTransaction代码如下: 123456789101112131415161718192021public Single<String> createEthTransaction(ETHWallet from, String to, BigInteger amount, BigInteger gasPrice, BigInteger gasLimit, String password) { final Web3j web3j = Web3j.build(rpcServerUrl)); return networkRepository.getLastTransactionNonce(web3j, from.address) .flatMap(nonce -> Single.fromCallable( () -> { Credentials credentials = WalletUtils.loadCredentials(password, keystorePath); RawTransaction rawTransaction = RawTransaction.createEtherTransaction(nonce, gasPrice, gasLimit, to, amount); byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials); String hexValue = Numeric.toHexString(signedMessage); EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).send(); return ethSendTransaction.getTransactionHash(); } ).subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()));} Token 转账交易Token 转账交易部分,请订阅我的小专栏。 对于Token 转账交易,有两点需要注意:1. 交易的目标地址(即交易字段的to字段)其实不是用户填写的收款人钱包地址,目标地址是Token 合约地址。2. 需要把对转账函数transfer的调用转化为交易的附加数据data。转化为交易的附加数据的方法如下:123456789public String createTokenTransferData(String to, BigInteger tokenAmount) { List<Type> params = Arrays.<Type>asList(new Address(to), new Uint256(tokenAmount)); List<TypeReference<?>> returnTypes = Arrays.<TypeReference<?>>asList(new TypeReference<Bool>() { }); Function function = new Function(\"transfer\", params, returnTypes); return FunctionEncoder.encode(function);}ERC20转账函数createERC20Transfer 有一点点不同:得到调用函数附加数据之后,在构造交易对象时,加入附加数据,部分代码如下:123String callFuncData = createTokenTransferData(to, amount);rawTransaction = RawTransaction.createTransaction( nonce, gasPrice, gasLimit, contractAddress, callFuncData); 参考文档web3j 微信:xlbxiong 备注:钱包, 加入钱包开发的微信群。 加入知识星球,和一群优秀的区块链开发者一起学习。深入浅出区块链 - 系统学习区块链,学区块链的都在这里,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"钱包","slug":"钱包","permalink":"https://learnblockchain.cn/tags/钱包/"},{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"},{"name":"web3j","slug":"web3j","permalink":"https://learnblockchain.cn/tags/web3j/"}]},{"title":"DApp教程:用Truffle 开发一个链上记事本","slug":"dapp_noteOnChain","date":"2019-03-30T13:29:55.000Z","updated":"2019-04-05T10:29:31.030Z","comments":true,"path":"2019/03/30/dapp_noteOnChain/","link":"","permalink":"https://learnblockchain.cn/2019/03/30/dapp_noteOnChain/","excerpt":"以编写一个链上记事本为例,介绍如何开发DApp,一年多前写的开发、部署第一个DApp因为Truffle 、MetaMask、Solidity都有升级,也随手更新了。通过两个教程大家可以更好理解前端如何与合约进行交互, 本文也将介绍如何使用Truffle 把合约部署到以太坊正式网络上(貌似很多人遇到问题)。","text":"以编写一个链上记事本为例,介绍如何开发DApp,一年多前写的开发、部署第一个DApp因为Truffle 、MetaMask、Solidity都有升级,也随手更新了。通过两个教程大家可以更好理解前端如何与合约进行交互, 本文也将介绍如何使用Truffle 把合约部署到以太坊正式网络上(貌似很多人遇到问题)。 项目背景及效果链上记事本让事件永久上链,让事件成为无法修改的历史,从此再无删帖,之前有一个帖子,介绍如何MetaMask上链记事,现在我们通过这个DApp来完成。 链上记事本有两个功能: 添加一个新记事 查看之前(自己的)记事本 实现效果: 本合约也部署到以太坊官方测试网络Ropsten, 如Englist first Note 的交易记录可以在EtherScan查询。 项目准备创建项目文件夹:noteOnChain,然后在目录下,执行: 1truffle unbox pet-shop 使用Truffle 对项目初始化。 如果没有使用过truffle 可以阅读开发、部署第一个DApp。 Truffle 的Box,是一套套的开发模板, 它会帮助我们安装好相应的依赖,快速的启动应用开发。如果我们项目需要是使用到 JQuery, Bootstrap库,使用pet-shop这个Box 是不错的选择,官方还提供了React 、 Vue 项目相应的模板,所有的Box 可以在这里查询。 合约实现项目初始化会在noteOnChain目录下生成contracts目录来存放合约文件,在contracts目录下添加一个合约文件NoteContract.sol: 1234567891011121314151617181920pragma solidity ^0.5.0;contract NoteContract { mapping(address => string [] ) public notes; constructor() public { } event NewNote(address, string note);// 添加记事 function addNote( string memory note) public { notes[msg.sender].push(note); emit NewNote(msg.sender, note); } function getNotesLen(address own) public view returns (uint) { return notes[own].length; }} 合约关键是状态变量notes的定义,这是一个mapping, 保存着所有地址下所有的记事本。 修改记事本逻辑如果需要修改笔记功能,可以在合约中加入以下代码: 123456event ModifyNote(address, uint index);function modifyNote(address own, uint index, string memory note) public { notes[own][index] = note; emit ModifyNote(own, index);} 如果需要只有自己能修改笔记可以modifyNote的第一行加上:1require(own == msg.sender); 合约部署先为合约添加一个部署脚本: 12345var Note = artifacts.require("./NoteContract.sol");module.exports = function(deployer) { deployer.deploy(Note);}; truffle部署的命令是 1truffle migrate 默认情况下,会部署到本地的Ganache提供的测试网络,本文介绍下如何通过Truffle部署到太坊官方网络,这里以Ropsten为例介绍。 Ganache 的安装使用可阅读开发、部署第一个DApp Infura 节点服务注册 与 HDWalletProvider 安装大多数人应该都没有部署自己的节点,我们可以使用Infura 提供的节点服务。 有部分人可能不解 Infura 服务,其实 MetaMask 后面的节点服务就是Infura。 然后通过 HDWalletProvider 连接到Infura节点,并为我们签署交易,通过下面命令安装HDWalletProvider: 1npm install truffle-hdwallet-provider 在使用Infura之前,我们需要注册一个访问Infura服务的Token, 注册地址为:https://infura.io/register, 注册后创建一个 Project, 复制节点url: 为 truffle 配置一个新网络修改truffle.js 加入一个新网络. 首先引入 HDWalletProvider: 1var HDWalletProvider = require("truffle-hdwallet-provider"); 配置签名的钱包助记词: 1var mnemonic = "orange apple banana ... "; 助记词其实不应该明文配置保存,最好配置在一个隐私文件里,并被代码管理工具所忽略。 加入新网络,以Ropsten为例: 12345678910networks: { ropsten: { provider: function() { return new HDWalletProvider(mnemonic, \"https://ropsten.infura.io/xxx\") }, network_id: 3, gas: 7003605, gasPrice: 100000000000, } } HDWalletProvider 的第一个参数是助记词(确保账号有足够的余额),第二个参数是 上面复制的 Infura 节点服务地址,gas 和 gasPrice 分别配置部署时的Gas Limit 和 Gas Price。 Truffle 网络的配置可查阅链接。 部署通过以下命令来选择网络部署: 1truffle migrate --network ropsten 此过程大约需要等待半分钟,正常的话会输出像下面的提示: network 'ropsten'.123456789101112131415Running migration: 1_initial_migration.js Deploying Migrations... ... 0xd79bc3c5a7d338a7f85db9f86febbee738ebdec9494f49bda8f9f4c90b649db7 Migrations: 0x0c6c4fc8831755595eda4b5724a61ff989e2f8b9Saving successful migration to network... ... 0xc37320561d0004dc149ea42d839375c3fc53752bae5776e4e7543ad16c1b06f0Saving artifacts...Running migration: 2_deploy_contracts.js Deploying NoteContract... ... 0x7efbb3e4f028aa8834d0078293e0db7ff8aff88e72f33960fc806a618a6ce4d3 NoteContract: 0xda05d7bfa5b6af7feab7bd156e812b4e564ef2b1Saving successful migration to network... ... 0x6257dd237eb8b120c8038b066e257baee03b9c447c3ba43f843d1856de1fe132Saving artifacts... 我们可以用输出的交易Hash到https://ropsten.etherscan.io/ 查询。 前端界面Truffle Boxs为项目生成了html前端文件src/index.html,删除原来Boxs提供的宠物相关代码,加入一下html: 123456789 <div class=\"form-group\"> <div class=\"col-sm-8 col-sm-push-1 \"> <textarea class=\"form-control\" id=\"new_note\" ></textarea> </div> <button for=\"new_note\" class=\"\" id=\"add_new\">添加笔记</button> </div><div id=\"notes\" ></div> 以上html 定义了一个文本框textarea用来输入笔记,定义了一个button用来提交笔记上链。定义了一个id为 notes 的div, 用来加载已有笔记。初始内容为空,后通过web3读取到合约里笔记后,通过JQuery插入。 合约交互删除原来Boxs提供的加载宠物逻辑,逻辑分三个部分: 初始化 web3 及合约 获取笔记填充到前端页面 发布笔记上链 初始化 在initWeb3函数中,完成web3的初始化: 12345678910111213141516171819// 最新dapp 浏览器或MetaMask if (window.ethereum) { App.web3Provider = window.ethereum; try { // 请求账号授权 await window.ethereum.enable(); } catch (error) { // User denied account access... console.error(\"User denied account access\") }}// Legacy dapp browsers...else if (window.web3) { App.web3Provider = window.web3.currentProvider;}else { App.web3Provider = new Web3.providers.HttpProvider('http://localhost:9545');}web3 = new Web3(App.web3Provider); 完成initContract初始化合约: 123456789101112initContract: function() { $.getJSON('NoteContract.json', function(data) { App.contracts.noteContract = TruffleContract(data); App.contracts.noteContract.setProvider(App.web3Provider); App.contracts.noteContract.deployed().then(function(instance) { App.noteIntance = instance; return App.getNotes(); }); }); return App.bindEvents();} 获取笔记填充到前端页面initContract函数里, noteIntance保存了部署后的合约实例,getNotes用来获取当前账号的所有笔记: 123456789getNotes: function() { App.noteIntance.getNotesLen(App.account).then(function(len) { App.noteLength = len; if (len > 0) { App.loadNote( len - 1); } }).catch(function(err) { });} 目前solidity 还无法支持返回动态类型的数组,没有办法直接获取到如string 数组的内容,所有这里采用一个变通的方法,先获取到笔记的长度,然后通过loadNote来逐条获取笔记: 123456789101112loadNote: function(index) { App.noteIntance.notes(App.account, index).then(function(note) { $(\"#notes\").append( '<div > <textarea >' + note + '</textarea></div>' ; if (index -1 >= 0) { App.loadNote(index - 1); } } ).catch(function(err) { });} 发布笔记上链使用JQuery监听用户点击add_new按钮,然后调用合约的 addNote 函数把用户输入的笔记存储到智能合约。 1234567891011 bindEvents: function() { $("#add_new").on('click', function() { $("#loader").show(); App.noteIntance.addNote($("#new_note").val()).then(function(result) { return App.watchChange(); }).catch(function (err) { console.log(err.message); }); });} 运行DApp使用以下命令,启动DApp 服务: 1npm run dev 在浏览器打开http://localhost:3000 浏览器的MetaMask 也需要连接Ropsten网络,确保网络一致。 不知道如何设置MetaMask 可阅读开发、部署第一个去中心化应用(。 本文为保持主干清晰,代码有删减, 网站代码请订阅小专栏查看。 参考文档Truffle 官方文档 加我微信:xlbxiong 备注:DApp, 加入以太坊DApp开发微信群。 加入知识星球 成长比别人快一点。 深入浅出区块链 - 系统学习区块链,学区块链的都在这里,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Dapp","slug":"ethereum/Dapp","permalink":"https://learnblockchain.cn/categories/ethereum/Dapp/"}],"tags":[{"name":"Dapp","slug":"Dapp","permalink":"https://learnblockchain.cn/tags/Dapp/"},{"name":"Truffle","slug":"Truffle","permalink":"https://learnblockchain.cn/tags/Truffle/"}]},{"title":"如何开发一款以太坊安卓钱包系列4 - 获取以太及Token余额","slug":"eth-wallet-dev-4","date":"2019-03-26T13:40:50.000Z","updated":"2019-04-05T10:29:31.039Z","comments":true,"path":"2019/03/26/eth-wallet-dev-4/","link":"","permalink":"https://learnblockchain.cn/2019/03/26/eth-wallet-dev-4/","excerpt":"这是如何开发以太坊安卓钱包系列,接上一篇继续展示钱包账号资产信息,这篇来看看如何获取账号的以太余额及Token余额。","text":"这是如何开发以太坊安卓钱包系列,接上一篇继续展示钱包账号资产信息,这篇来看看如何获取账号的以太余额及Token余额。 回顾在上一篇中,为了避免 UI 与上面4个数据的耦合,使用了一个TokensViewModel,并且已经完成当前选中账号defaultWallet的获取,我们在回看一下TokensViewModel的定义: 1234567public class TokensViewModel extends ViewModel { private final MutableLiveData<ETHWallet> defaultWallet; private final MutableLiveData<NetworkInfo> defaultNetwork; private final MutableLiveData<Token[]> tokens; private final MutableLiveData<Ticker> prices;} 上面还有三个变量,一个是tokens, 当前账号下 所拥有的 Token 数组; 一个是defaultNetwork当前选中网络,还有一个prices我们下一遍介绍。 为什么需要 defaultNetwork 来保存网络信息呢? 这是因为同一个账号,他在不同的网络下,其余额是不同的,而登链钱包又可以支持多个不同的网络,所有我们在获取账号余额前,需要确定一下其网络。 网络以太坊网络这里补充下以太坊网络,当前以太坊在使用的网络有5个: Mainnet :主网,真正有价值的网络,当前Pow共识; Ropsten :测试网网络, 使用Pow,和当前的公有链环境一致; Kovan :测试网网络, 使用PoA共识,仅parity钱包支持; Rinkeby:测试网网络,使用PoA共识 仅geth钱包支持; Goerli:测试网网络,为Eth2.0 作准备启动的一个跨客户端的网络。 除此之外,登链钱包还支持本地开发网络。 NetworkInfo代码中使用 NetworkInfo类 来表示一个网络,其定义如下,大家看一下注释: 123456789public class NetworkInfo { public final String name; // 网络名称,如 mainnet, ropsten public final String symbol; // ETH public final String rpcServerUrl; // 节点提供的rpc 服务地址 public final String backendUrl; // 查询交易的列表的服务url public final String etherscanUrl; public final int chainId; public final boolean isMainNetwork;} 在EthereumNetworkRepository.java中用一个 NetworkInfo 数组 NETWORKS 列出了所有支持的网络,其中包含了一个本地开发网络,: 123456789101112private final NetworkInfo[] NETWORKS = new NetworkInfo[] { new NetworkInfo(\"Mainnet\",\"ETH\", \"https://mainnet.infura.io/llyrtzQ3YhkdESt2Fzrk\", \"https://api.trustwalletapp.com/\", \"https://etherscan.io/\",1, true), // ignore some ... new NetworkInfo(\"local_dev\",\"ETH\", \"http://192.168.8.100:8545\", \"http://192.168.8.100:8000/\", \"\",1337, false),}; NetworkInfo中节点及交易查询服务,我们可以选择自己搭建节点(使用Geth、Ganache 等工具),或使用第三方的服务。 测试网络如果是测试网络,就必须得自己搭建节点,如使用geth启动一个网络: 1geth --datadir my_datadir --dev --rpc --rpcaddr \"0.0.0.0\" console 特别要注意,需要对--rpcaddr 进行设置,表示哪一个地址能接受RPC请求,因为默认情况下,geth只接受来自 localhost 的请求,这样就无法接受到来自手机的客户端的请求。如果是Ganache,可以点击Ganache右上角的设置,进行配置。 确定当前网络在钱包有一个设置项,会把用户选中的网络的name保存到 SharedPreference, 如图: 确定网络的代码逻辑就简单了: 从SharedPreference读取到选中的网络名再对NETWORKS 做一个匹配,代码在EthereumNetworkRepository中,大家可对照查看。 Coin 还是 TokenCoin 指的是以太币,Token 是大家通常所说的代币 或 通证,以太余额何Token余额,他们的获取方式是不一样的,明白这一点很重要,有必要先介绍下以太坊账户模型。 以太坊账户模型以太币Eth是以太坊的原生代币,在以太坊的账户模型中,有一个字段balance存储着余额,例如账号的定义像下面: 123456class Account { nonce: '0x01', balance: '0x03e7', // wei stateRoot: '0x56abc....', codeHash: '0x56abc....', } 获取以太币的余额只需要调用web3j提供的RPC接口eth_getBalance。 而一个地址的Token余额,他记录在Token合约上,注意合约其实也是一个账户(合约账户),Token是指符合ERC20标准的合约, 每个地址的余额通常存储在一个Mapping类型的balanceOf变量中,获取地址的余额需要调用合约的balanceOf方法,并给他传递地址作为参数。 如果在合约地址上调用 eth_getBalance, 获取的是合约上所存的 eth余额。 Token & TokenInfo在登链代码里,每一种币及余额封装成了一个Token类,不论是以太币还是Token 都处理是一个Token实例。 这里Token 命名不是很严谨,以太币一般称为Coin,为了方便,Coin和Token 都统一作为Token处理,Coin 作为一个特殊的Token,了解这一点对后文阅读很重要。 Token的定义如下: 123456789101112public class Token { public final TokenInfo tokenInfo; public final String balance; // 币余额 public String value; // 币对应的法币价值}public class TokenInfo { public final String address; // 合约地址 public final String name; public final String symbol; // 代币符号 public final int decimals;} 账号所有资产资产包括以太币资产及Token资产。 关联 Token在获取账号余额之前,我们需要先知道有多少 Token 种类,然后再获取每种Token余额。在登链钱包中,每一账号在某个网络下所关联 Token种类,保存为一个 Realm文件,相关逻辑在RealmTokenSource类中。 Realm 是一个移动端数据库,是替代sqlite的一种解决方案。 在用户通过以下界面添加新资产,会调用RealmTokenSource类的put方法保存到.realm文件。 现在来看看如何获取账号所关联的 Token, 逻辑上比较简单,不过涉及了多个类,我把调用序列图梳理一下: 流程图源码12345Title: 获取账号Token种类TokensViewModel->FetchTokensInteract: fetchFetchTokensInteract->TokenRepository: fetchTokenRepository->TokenLocalSource: fetchTokenLocalSource-->>TokensViewModel: OnTokens 通过这个调用过程,最终通过TokensViewModel类的onTokens获取到Token种类。 123 private void onTokens(Token[] tokens) { this.tokens.postValue(tokens);} 在PropertyFragmeng界面中订阅收到数据之后,把它设置到界面的Adapter里,完成Token列表的显示。 Ethplorer-API 服务TokenRepository在执行fetch方法时,如果是在主网下,会调用代码中EthplorerTokenService类,从第三方服务Ethplorer-API获取到获取到某一个地址所关联的所有的Token种类。 Ethplorer-API提供的API更多,不过我们只需要getAddressInfo接口,请求接口如下: 1/getAddressInfo/0xaccount?apiKey=freekey Ethplorer-API 的免费接口是有请求限额,每2秒才能发起一个请求,需要注意访问频度。 余额 balance获取以太余额分为两步: 先构造出web3j 对象 web3j 调用 ethGetBalance 获取以太余额 web3j对象的构造方法如下:1web3j = Web3j.build(new HttpService(networkInfo.rpcServerUrl, httpClient, false)); web3j对象在TokenRepository初始化的时候完成,在TokenRepository获取到Token列表之后,如果是以太币会随即会调用getEthBalance 方法: 123456private BigDecimal getEthBalance(String walletAddress) throws Exception { return new BigDecimal(web3j .ethGetBalance(walletAddress, DefaultBlockParameterName.LATEST) .send() .getBalance());} 获取 Token 数量在TokenRepository获取到Token列表之后,如果是ERC20代币会随即会调用getBalance 方法。根据前面的介绍获取代币的余额需要调用合约的balanceOf方法,在以太坊上对合约方法的调用实际上会合约地址发起一个调用,调用的附加数据是函数及参数的ABI编码数据。 之前写过一篇文章:如何理解以太坊ABI, 大家可以读一下。 用以下方法构造出balanceOf的ABI函数类型: 123456private static org.web3j.abi.datatypes.Function balanceOf(String owner) { return new org.web3j.abi.datatypes.Function( \"balanceOf\", Collections.singletonList(new Address(owner)), Collections.singletonList(new TypeReference<Uint256>() {}));} 获取到balanceOf的ABI 之后,经过编码之后,使用 createEthCallTransaction来构造这样一个交易:交易的发起者是当前的账号,交易的目标地址是合约地址,附加数据是编码之后的数据,getBalance方法如下: 12345678910111213141516171819202122232425private BigDecimal getBalance(String walletAddress, TokenInfo tokenInfo) throws Exception { org.web3j.abi.datatypes.Function function = balanceOf(walletAddress); String responseValue = callSmartContractFunction(function, tokenInfo.address, walletAddress); List<Type> response = FunctionReturnDecoder.decode( responseValue, function.getOutputParameters()); if (response.size() == 1) { return new BigDecimal(((Uint256) response.get(0)).getValue()); } else { return null; }}private String callSmartContractFunction( org.web3j.abi.datatypes.Function function, String contractAddress, String walletAddress) throws Exception { String encodedFunction = FunctionEncoder.encode(function); EthCall response = web3j.ethCall( Transaction.createEthCallTransaction(walletAddress, contractAddress, encodedFunction), DefaultBlockParameterName.LATEST) .sendAsync().get(); return response.getValue();} 余额格式化上面获取到的余额,是以最小单位表示的一个数,如以太币余额用wei表示,而现示给用户的数据是ether,即大家说的以太。 注: 1 eth = 10^18 wei , 更多单位转换 转换方法如下: 12BigDecimal decimalDivisor = new BigDecimal(Math.pow(10, decimals));BigDecimal ethbalance = balance.divide(decimalDivisor); 对以太币而言 decimals 为 18,之后 ethbalance 会转化为一个保留4位小数点数的字符串保存到Token类型的balance变量,转换方法如下:1ethBalance.setScale(4, RoundingMode.CEILING).toPlainString() UI界面最终通过订阅 tokens 数组获取Token种类及余额,代码查阅PropertyFragment.java 。 参考文档web3jRealmEthplorer-API 加我微信:xlbxiong 备注:钱包, 加入钱包开发的微信群。 加入知识星球,和一群优秀的区块链从业者一起学习。深入浅出区块链 - 系统学习区块链,学区块链的都在这里,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"钱包","slug":"钱包","permalink":"https://learnblockchain.cn/tags/钱包/"},{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"},{"name":"web3j","slug":"web3j","permalink":"https://learnblockchain.cn/tags/web3j/"}]},{"title":"如何开发一款以太坊安卓钱包系列3 - 资产信息展示","slug":"eth_wallet_dev_3","date":"2019-03-24T13:59:44.000Z","updated":"2019-04-05T10:29:31.019Z","comments":true,"path":"2019/03/24/eth_wallet_dev_3/","link":"","permalink":"https://learnblockchain.cn/2019/03/24/eth_wallet_dev_3/","excerpt":"这是如何开发以太坊(安卓)钱包系列第3篇, 钱包账号资产信息展示,展示信息主要包括账号地址、eth余额及该账号所拥有的Token及余额。","text":"这是如何开发以太坊(安卓)钱包系列第3篇, 钱包账号资产信息展示,展示信息主要包括账号地址、eth余额及该账号所拥有的Token及余额。 预备知识 MVVM本文会涉及和UI界面的交互,提前理解下界面和数据如何交互是非常有必要的,如果你已经很熟悉MVVM,可跳过这一小节。 最早写Android的时候,数据和界面经常耦合在一起,一个Activity文件总是特别大,每当产品界面改版就非常痛苦,吐槽下,很多产品经理都喜欢对界面改来改去。 后来Google 推荐多个架构模式: MPV、 MVVM模式来解决数据和UI耦合的问题,登链钱包代码,使用的就是MVVM模式,所以对它做一个简单介绍,下面是MVVM的视图和数据的交互图: View 通常对应于Activity/Fragment/自定义ViewModel:则是和数据相关的模块。 View 与 Model 不直接发生联系, 而是通过ViewModel负责接收View层的事件以及获取并处理数据,ViewModel层的数据变化也会通知给View层进行相应的UI的更新,从而实现业务逻辑和Ui的隔离。 使用MVVM模式最大的优点就是解耦, 因为数据处理逻辑是独立于View, 在UI更改时,ViewModel 不用做太多改动。 我们使用了Google在I/O大会推出的一套遵循MVVM开发模式的LiveData和ViewModel组件架构。 ViewModel 和 LiveDataViewModel 会关注UI生命周期来存储和管理数据,在Activity发生变化(锁屏开屏、旋转)时,ViewModel 会自动保留之前的数据并给新的Activity或Fragment使用,当界面被系统销毁时,ViewModel也会进行资源清理,避免内存泄漏。 ViewModel 还可以用于不同界面间数据共享。 LiveData是一个可观察的数据持有者类。观察者可以方便我们以异步的方式获取数据,同时LiveData也是有生命周期感知的。如果其生命周期处于STARTED或RESUMED状态。LiveData会将观察者视为活动状态,并通知其数据的变化。LiveData未注册的观察对象以及非活动观察者是不会收到有关更新的通知。 了解更多,可自行以关键字: Lifecycle、ViewModel、LiveData 进行搜索。 账号信息展示展示信息主要包括账号地址、eth余额及该账号所拥有的Token及余额, 其界面效果如下: 这个界面应的是登链钱包的PropertyFragment,上图的UPT 是我自己发行的Token,所以没有显示价格 现在我们来思考一下, 怎么来展现上面的数据, 别着急往下看, 可以先想想。 先对问题做一个拆分,把数据拆分为4个部分: 显示当前选中的账号 显示当前账号 ETH 余额 显示当前账号下 Token 数量 显示对应的法币金额。 为了避免 UI 与上面4个数据的耦合,代码使用了一个TokensViewModel, 获取到的数据用 LiveData做了一个Wrap,以便UI可以订阅数据,TokensViewModel类像下面,代码有删减: 1234567public class TokensViewModel extends ViewModel { private final MutableLiveData<ETHWallet> defaultWallet; private final MutableLiveData<NetworkInfo> defaultNetwork; private final MutableLiveData<Token[]> tokens; private final MutableLiveData<Ticker> prices;} MutableLiveData 是前面提到的 LiveData的子类,在UI界面中就可以对数据进行订阅,下面我们逐一拆解下每个数据。 显示当前账号可以分为两个步骤: 从数据库中读取账号; 界面显示账号 TokensViewModel中定义了一个MutableLiveData<ETHWallet> defaultWallet ,从数据库中读取账号会保存在defaultWallet中,然后UI对 defaultWallet 进行观察显示。 注解: 登链钱包 里大量使用的这个方式,通过一个LiveData 做数据桥接。 在上一篇导入账号及账号管理,所有的账号使用greenDao 存储起来, 因此我们只需要把所有账号从加载出来,挑选出当前选中的那一个。 结合代码看一看: 1234567891011// WalletDaoUtils.java public static ETHWallet getCurrent() { List<ETHWallet> ethWallets = ethWalletDao.loadAll(); for (ETHWallet ethwallet : ethWallets) { if (ethwallet.isCurrent()) { ethwallet.setCurrent(true); return ethwallet; } } return null; } 上面代码先用 ETHWalletDao.loadAll 加载出所有的账号,返回当前选中的,上面的代码会被FetchWalletInteract 类的 findDefault方法调用,在ViewModle里,很多时候以数据进行交互的类,我们会命名为 xxxInteract,这也是一个习惯用法。 其代码如下: 123456789101112// FetchWalletInteract.java// 返回一个可订阅的Single<ETHWallet> 对象public Single<ETHWallet> findDefault() { return Single.fromCallable(() -> { return WalletDaoUtils.getCurrent(); }).subscribe(this::onDefaultWallet); } // 获取到默认钱包账号 设置到 defaultWallet 这个LiveData private void onDefaultWallet(ETHWallet wallet) { defaultWallet.setValue(wallet); } findDefault()返回一个可订阅的Single 对象,如果不熟悉可参考后面的文档。 之后,在UI界面PropertyFragment.java 中, 就可以对 defaultWallet 进行订阅: 1tokensViewModel.defaultWallet().observe(this, this::showWallet); 当获取到默认账号时,就会回调showWallet: 123456// UI 显示 public void showWallet(ETHWallet wallet) { tvWalletName.setText(wallet.getName()); tvWalletAddress.setText(wallet.getAddress()); } 这样, 界面的显示就完成了,下一篇继续介绍获取余额。 参考文档 lifecycle官方文档地址 RxAndroid 了解更多响应式编程 我创建了一个专门讨论钱包开发的微信群,加微信:xlbxiong 备注:钱包。 加入知识星球,和一群优秀的区块链从业者一起学习。深入浅出区块链 - 系统学习区块链,学区块链的都在这里,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"钱包","slug":"钱包","permalink":"https://learnblockchain.cn/tags/钱包/"},{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"},{"name":"web3j","slug":"web3j","permalink":"https://learnblockchain.cn/tags/web3j/"}]},{"title":"跨链技术的分析和思考","slug":"blockchain_interoperability","date":"2019-03-23T04:04:30.000Z","updated":"2019-04-05T10:29:31.003Z","comments":true,"path":"2019/03/23/blockchain_interoperability/","link":"","permalink":"https://learnblockchain.cn/2019/03/23/blockchain_interoperability/","excerpt":"当前的区块链底层技术平台百花齐放,不同的业务、不同的技术底层的区块链之间缺乏统一的互联互通的机制,这极大限制了区块链技术和应用生态的健康发展。跨链的需求由此而来,本文通过分析几种主流的跨链方案探讨跨链技术的本质及相应的解决思路。","text":"当前的区块链底层技术平台百花齐放,不同的业务、不同的技术底层的区块链之间缺乏统一的互联互通的机制,这极大限制了区块链技术和应用生态的健康发展。跨链的需求由此而来,本文通过分析几种主流的跨链方案探讨跨链技术的本质及相应的解决思路。 跨链的类型跨链交互根据所跨越的区块链底层技术平台的不同可以分为同构链跨链和异构链跨链:同构链之间安全机制、共识算法、网络拓扑、区块生成验证逻辑都一致,它们之间的跨链交互相对简单。而异构链的跨链交互相对复杂,比如比特币采用PoW算法而联盟链Fabric采用传统确定性共识算法,其区块的组成形式和确定性保证机制均有很大不同,直接跨链交互机制不易设计。异构链之间的跨链交互一般需要第三方辅助服务辅助跨链交互。 主流跨链机制概述截至目前,主流的区块链跨链技术方案按照其具体的实现方式主要分为三大类,分别是公证人机制、侧链/中继和哈希锁定: 公证人机制(Notary schemes): 公证人也称见证人机制,公证人机制本质上是一种中介的方式。具体而言,假设区块链A和B本身是不能直接进行互操作的,那么他们可以引入一个共同信任的第三方作为中介,由这个共同信任的中介进行跨链消息的验证和转发。公证人机制的优点在于能够灵活地支持各种不同结构的区块链(前提是公证人能够访问相关方的链上信息),缺点在于存在中心化风险。 哈希锁定(Hash-locking): 哈希锁定技术主要是支持跨链中的原子资产交换,最早起源自比特币的闪电网络。其典型实现是哈希时间锁定合约HTLC(Hashed TimeLock Contract)。哈希锁定的原理是通过时间差和影藏哈希值来达到资产的原子交换。哈希锁定只能做到交换而不能做到资产或者信息的转移,因此其使用场景有限。 侧链/中继链(Sidechains / Relays): 侧链是指完全拥有某链的功能的另一条区块链,侧链可以读取和验证主链上的信息。主链不知道侧链的存在,由侧链主动感知主链信息并进行相应的动作。而中继链则是侧链和公证人机制的结合体,中继链具有访问需要和验证进行互操作的链的关键信息并对两条链的跨链消息进行转移。从这个角度看中继链也是一种去中心的公证人机制。 下面就这几种跨链方式的典型实现方式进行详细分析: 典型跨链机制实现分析公证人机制最传统的公证人机制是基于中心化交易所得跨链资产交换,这种跨链的方式比较单一,只支持资产的交换,如下图演示了Alice通过交易所,用比特币和Bob交换ETH的过程。 Alice 通过交易所钱包将自己的比特币打入交易所地址; Alice 在交易所上挂上卖单1个BTC卖出20ETH价格; Bob需要将自己的ETH打入交易所的以太坊地址; Bob通过交易所挂出购买比特币的单子 20ETH买一个比特币; 交易所将Alice的卖单和Bob的卖单进行撮合; 交易所将Alice在交易所存储的1BTC 转移给Bob的比特币地址; 交易所将Bob在交易所存储的20ETH 转移给Alice的以太坊地址; 至此完成了Alice和Bob的BTC和ETH的交换(案例中省去了交易所的服务费)。通过该例子可以看出交易所的方式目前仅能够支持资产的交换,且资产交换的原子性、安全性完全由中心化的交易所保障存在较大的中心化风险。 除此之外还有一种著名的分布式账本技术Ripple,也是采用类似公证人的机制来解决全球金融机构之间的资产交换。Ripple的系统架构如上图所示,Ripple系统中交易通过网络中的验证者进行交易的验证,验证者验证的交易通过加密算法保护交易内容不能被验证着窥探从而保证交易的隐私性。 公证人机制的跨链技术实现简单,且能够比较灵活地支持不同类型的底层区块链体系。公证人机制的主要问题在于公证人机制的安全性保障完全由公证人系统保障。参与跨链的相关方需要对中间人给予较大的信任。 哈希锁定哈希时间锁定(HTLC)最早出现在比特币的闪电网络,跨链资产交换支持一定数量的A链资产和一定数量的B链资产进行原子交换。哈希时间锁定巧妙地采用了哈希锁和时间锁,迫使资产的接收方在deadline内确定收款并产生一种收款证明给打款人,否则资产会归还给打款人。收款证明能够被付款人用来获取接收人区块链上的等量价值的数量资产或触发其他事件。 如下图所示,我们用一个例子来阐述如何使用哈希时间锁定进行跨链的原子资产交换,假设Alice和Bob有资产交换的需求,Alice想用1个BTC和Bob换20个ETH. 那么首先需要在两条链上设置哈希时间锁定合约,然后执行如下步骤: Alice 随机构建一个字符串s,并计算出其哈希 h = hash(s); Alice 将h发送给Bob的合约; Alice锁定自己的1个BTC资产,并设置一个较长的锁定时间t1, 并设置了获取该BTC的一个条件:谁能够提供h的原始值s就可以得到该BTC; Bob观察到Alice 合约中锁定了一个BTC, 然后Bob锁定自己的20个ETH资产,并设置一个相对较短的锁定时间t2, t2 < t1, Bob也设置了同样获取条件(谁提供h的原始值s就可以获取20个ETH); Alice将自己最初生成的字符串s 发送到Bob的合约里取得了20个ETH; Bob观察到步骤5中Alice的s值,将其发送给Alice的合约成功获取1个BTC; 至此Alice和Bob完成了资产的交换。 从上述的过程我们可以看出哈希时间锁定合约有一些约束条件: 进行跨链资产交换的双方必须能够解析双方的合约内部数据,例如s,例如锁定资产的证明等; 哈希锁定的超时时间设置时需要保证存在时间差,这样在单方面作弊时另一方可以及时撤回自己的资产。 哈希锁定的思想运用在支付领域较多,例如闪电网络、雷电网络以及跨链资产转移协议Interledger等。但是哈希锁定目前看只适合偏资产或者关键数据的交换,甚至不支持转移因此其试用场景受限。 侧链/中继链侧链侧链是相对于主链而言的,最初的侧链提出是针对比特币做新特性的测试和研发。侧链相对主链而言能够验证和解析主链中的区块数据和账本数据。侧链实现的基础技术是双向锚定(Two-way Peg),通过双向锚定技术可以将数字资产在主链上进行锁定,同时将等价的资产在侧链中释放。相反当侧链中相关资产进行锁定时,主链上锚定的等价资产也可以被释放。 BTC-Relay是号称的史上第一个侧链,BTC-Relay是通过以太坊构建了一个比特币的侧面,运用以太坊的智能合约允许用户验证比特币的交易。这里我们仍然以Alice 1BTC和Bob的20ETH数字资产交换为例阐述相应原理: Bob将20ETH发送到BTCSwap的合约进行冻结;(该合约只要能够确认BTC网络上Bob接收到来自Alice 1BTC就自动将20ETH转给Alice) Alice 确认Bob冻结信息后,将1 BTC转给Bob比特币账户; BTC Relayer将比特币区块头推送到BTCSwap合约; Alice 接下来就可以调用relay tx; BTCSwap合约结合tx和BTC链的区块链进行SPV验证,验证通过则将20ETH转给Alice以太坊地址。 这种跨链的实现方式简单,但是BTC Relay需要额外的信任和维护成本,且智能合约内部的数据存储会有体积膨胀的问题。但是侧链的机制相对哈希锁定而言能够提供更多的跨链交互场景,侧链以及类SPV验证的思想适合所有跨链的场景。 中继链中继链本质上算是公证人机制和侧链机制的融合和扩展,目前社区内最活跃的两个跨链项目Cosmos 和 Polkadot 采用的都是基于中继链的多链多层架构,其中Cosmos目前支持的是跨链资产交互而Polkadot则宣称提供任意类型的跨链交互,具体实现还有待观察。 CosmosCosmos网络是一个多链混合的区块链网格结构,如下图所示,该网络中主要包括两种角色:Hub: 用于处理跨链交互的中继链;Zone: Cosmos中的平行链, Cosmos中平行链需要具备两个前提条件: 1. 快速确定性(fast finality), 这个特性由共识算法保障,也就是说Cosmos的跨链不直接支持PoW等概率确定模型的区块链; 2. 强监管性(Sovereignty):每个平行链都具有一组验证者能够决定其出块。 为了支持平行链之间的跨链互操作,Cosmos提出了一种跨链交互协议IBC(Inter-Blockchain Communication protocol), 并利用tendermint共识算法的即时确定性实现多个异构链之间的价值和数据传输。 首先我们以Chain A 到Chain B 转账10 token为例说明使用IBC的跨链交互: 1. 互相跟踪,也就是说如果A要和B进行跨链交易,那么A和B链需要分别运行相当于对方区块链的轻节点服务,这样互相可以实时接收到对方的区块头信息(方便后续执行类SPV验证); 2. A链上初始化IBC协议,冻结相关资产10 token, 并生成相应的证明发送给B区块链; 3. B链接收到相应的IBC消息,通过A链的区块头信息确定A确实进行相应的资产冻结,然后B链会生成等价值10 token的资产。 以上是使用IBC协议的两个平行链直接进行跨链的基本过程,如果区块链很多,那么这种方式的两两跨链复杂度会呈现组合级别增加。因此Cosmos网络又引入了一种Hub的中继链,所有的平行链都通过IBC连接到Hub,让Hub辅助跨链交易的验证和转移,目前Cosmos实现了一个官方的Hub称为Cosmos Hub(如前图所示)。 如下图所示是Cosmos 网络的详细架构图,Cosmos为方便平行链开发提供了基本服务CosmosSDK包括:共识、网络以及IBC协议等,这样基于Cosmos SDK开发的子链之间都能够方便地互相交互。此外对于非Cosmos SDK 开发的区块链需要使用Peg Zone进行桥接,如图中的Ethereum。 笔者认为Cosmos为跨链带来的最大贡献在于IBC协议的设计,IBC协议提供了一种通用的跨链协议标准。IBC的设计使得跨链交易可以在多个Hub之间进行安全路由和转发,类似目前互联网的TCP/IP 协议。但是遗憾的是目前的Cosmos设计也只能够支持资产的跨链,而且由于不同区块链的业务不同其共识速率的不一致也会影响跨链交易有效性的证明。 PolkadotPolkadot也是一种集成平行链和中继链的多层多链架构,Polkadot区块链的整体架构图如下图所示,主要包含三种角色链和四种参与方: 三种链角色: 中继链(Relay chain): 中继链位于Polkadot的体系的核心地位,主要是为整个系统提供统一的共识和安全性保障; 平行链(Parachain): 在Polkadot中平行链负责具体的业务场景,平行链自身不具备区块的共识,它们将共识的职责渡让给了中继链,所有平行链共享来自中继链的安全保障,中继链是Polkadot组成的一部分; 桥接链:桥接链指的是非Polkadot体系之外的区块链,如Bitcoin, Ethereum, 这些区块链有自身的共识算法,它们通过不同的Bridge与Polkadot连接在一起进行跨链交互。 四种参与方: 验证者(Validator): 验证者负责Polkadot的网络出块,会运行一个中继链的客户端,在每一轮区块产生中会对其提名的平行链出的块进行核验。当平行链的跨都被他们的子验证者集合确定好之后,验证者们会将所有平行链区块头组装到中继链的区块并进行共识。 核验人(Collator): 帮助验证者收集、验证和提交备选平行链区块,维护了一个平行链的全节点。 钓鱼人(Fisherman):钓鱼人主要靠检举非法交易或者区块以获取收益; 提名人(Nominator): 拥有stake的相关方,维护和负责验证者的安全性。 Polkadot的特性包括两个,一个是共享安全性,一个是不需信任的跨链交互。这里的不需信任的跨链交互其实是和第一个特点共享安全性密切相关的,而且Polkadot的不需信任的跨链交互也主要是只其内部的平行链之间。 在Polkadot中如果parachain A 需要发送一笔交易到parachain B的过程如下: A链将跨链交易放到自己的engress(每个平行链有一个消息输出队列engress 和一个消息输入队列ingress); A链的Collator收集A链的普通交易以及跨链交易并提交给A链的验证者集合; A链的验证者集合验证成功,将本次A链的区块头信息以及A链的engress内信息提交到中继链上; 中继链运行共识算法进行区块确认以及跨链交易路由,中继链上的验证者会将A链的相应交易从A链的engress queue中移动到B链的ingress queue中。 B链执行区块,将ingress queue中相应交易执行并修改自身账本。 以上便是Polkadot跨链交易的主要步骤,由于所有平行链的共识同步发生(中继链区块示意图如下),因此跨链交易不会有诸如双花等安全性问题。 Polkadot 的平行链之间的跨链交换的安全性保障主要来自共享安全性这个特点,共享安全性使得跨链交易和普通交易同步发生也就不存在其他跨链场景中的双花等跨链数据不一致问题。其次Polkadot中的引入的特殊状态验证方法方便中继链进行跨链等消息的有效性验证。 值得一提的是Polkadot项目目前还处在项目初期,对于parachain的设计、Collator的协作以及Validator的共识、工作效率等都未完善。这种共享安全性的方式是否也限制了平行链自身的性能都还有待考证。 关于跨链技术的几点思考综合以上的一些主流跨链场景和方案的分析,从跨链的概念以及需求上看跨链的本质其实就是 如何将A链上的消息M安全可信地转移到B链并在B链上产生预期效果。那么一个成功的跨链交互到底需要解决哪些问题呢?笔者认为主要有以下四个问题: 消息M的真实性证明,也就是说M是否确实是存在A链上的,也确实是A链发给B链的; 消息M的路由,如何让跨链消息安全跨系统路由; 消息M的有效性证明,这里的有效性是指来自A链的消息M如何让B链认可其抵达B链时状态仍然有效,比如转移的资产是否是冻结的,没有双花的,如果是状态那么是否在此期间未发生改变等; 消息M的执行结果证明,这个是指A链需要确认跨链操作是否成功,以及成功操作的相应回执。 那么针对这些关键本质问题,如何去处理呢?笔者设想未来的区块链应该在底层平台的设计之初就需要遵循统一的跨链协议标准,就像现在的操作系统对TCP/IP协议的支持一样。需要进行通用跨链的区块链至少要支持一下功能: 提供跨链消息的输入和输出口径,例如Cosmos和Polkadot的跨链队列; 提供跨链消息的真实性证明,区块链需要提供类似SPV的证明手段; 消息的有效路由需要构建跨链消息的统一格式,定义好消息的来源和去处以及消息内容,如Cosmos的IBC协议; 消息的有效性证明,区块链可能需要设计新的类似UTXO的可验证存储结构,方便做类SPV类验证,否则目前的基于KV的数据存储方式做有效性证明几乎不可能; 跨链执行结果证明,和有效性证明类似,需要全新的数据结构和运行算法支持。 除此之外,跨链系统的设计还需要考虑系统稳定性、可扩展性以及易升级性、容错等等,总而言之,真正的可信互联网建设艰辛蛮长,诸君共勉! 本文经作者授权转自BITKING 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"跨链","slug":"cross-chain","permalink":"https://learnblockchain.cn/categories/cross-chain/"}],"tags":[{"name":"侧链","slug":"侧链","permalink":"https://learnblockchain.cn/tags/侧链/"},{"name":"中继链","slug":"中继链","permalink":"https://learnblockchain.cn/tags/中继链/"}]},{"title":"寻找一种易于理解的一致性算法(扩展版)","slug":"easy_raft","date":"2019-03-22T13:27:59.000Z","updated":"2019-04-05T10:29:30.991Z","comments":true,"path":"2019/03/22/easy_raft/","link":"","permalink":"https://learnblockchain.cn/2019/03/22/easy_raft/","excerpt":"摘要 Raft 是一种为了管理复制日志的一致性算法。它提供了和 Paxos 算法相同的功能和性能,但是它的算法结构和 Paxos 不同,使得 Raft 算法更加容易理解并且更容易构建实际的系统。为了提升可理解性,Raft 将一致性算法分解成了几个关键模块,例如领导人选举、日志复制和安全性。同时它通过实施一个更强的一致性来减少需要考虑的状态的数量。从一个用户研究的结果可以证明,对于学生而言,Raft 算法比 Paxos 算法更加容易学习。Raft 算法还包括一个新的机制来允许集群成员的动态改变,它利用重叠的大多数来保证安全性。","text":"摘要 Raft 是一种为了管理复制日志的一致性算法。它提供了和 Paxos 算法相同的功能和性能,但是它的算法结构和 Paxos 不同,使得 Raft 算法更加容易理解并且更容易构建实际的系统。为了提升可理解性,Raft 将一致性算法分解成了几个关键模块,例如领导人选举、日志复制和安全性。同时它通过实施一个更强的一致性来减少需要考虑的状态的数量。从一个用户研究的结果可以证明,对于学生而言,Raft 算法比 Paxos 算法更加容易学习。Raft 算法还包括一个新的机制来允许集群成员的动态改变,它利用重叠的大多数来保证安全性。 1 介绍一致性算法允许一组机器像一个整体一样工作,即使其中一些机器出现故障也能够继续工作下去。正因为如此,一致性算法在构建可信赖的大规模软件系统中扮演着重要的角色。在过去的 10 年里,Paxos 算法统治着一致性算法这一领域:绝大多数的实现都是基于 Paxos 或者受其影响。同时 Paxos 也成为了教学领域里讲解一致性问题时的示例。 但是不幸的是,尽管有很多工作都在尝试降低它的复杂性,但是 Paxos 算法依然十分难以理解。并且,Paxos 自身的算法结构需要进行大幅的修改才能够应用到实际的系统中。这些都导致了工业界和学术界都对 Paxos 算法感到十分头疼。 和 Paxos 算法进行过努力之后,我们开始寻找一种新的一致性算法,可以为构建实际的系统和教学提供更好的基础。我们的做法是不寻常的,我们的首要目标是可理解性:我们是否可以在实际系统中定义一个一致性算法,并且能够比 Paxos 算法以一种更加容易的方式来学习。此外,我们希望该算法方便系统构建者的直觉的发展。不仅一个算法能够工作很重要,而且能够显而易见的知道为什么能工作也很重要。 Raft 一致性算法就是这些工作的结果。在设计 Raft 算法的时候,我们使用一些特别的技巧来提升它的可理解性,包括算法分解(Raft 主要被分成了领导人选举,日志复制和安全三个模块)和减少状态机的状态(相对于 Paxos,Raft 减少了非确定性和服务器互相处于非一致性的方式)。一份针对两所大学 43 个学生的研究表明 Raft 明显比 Paxos 算法更加容易理解。在这些学生同时学习了这两种算法之后,和 Paxos 比起来,其中 33 个学生能够回答有关于 Raft 的问题。 Raft 算法在许多方面和现有的一致性算法都很相似(主要是 Oki 和 Liskov 的 Viewstamped Replication),但是它也有一些独特的特性: 强领导者:和其他一致性算法相比,Raft 使用一种更强的领导能力形式。比如,日志条目只从领导者发送给其他的服务器。这种方式简化了对复制日志的管理并且使得 Raft 算法更加易于理解。 领导选举:Raft 算法使用一个随机计时器来选举领导者。这种方式只是在任何一致性算法都必须实现的心跳机制上增加了一点机制。在解决冲突的时候会更加简单快捷。 成员关系调整:Raft 使用一种共同一致的方法来处理集群成员变换的问题,在这种方法下,处于调整过程中的两种不同的配置集群中大多数机器会有重叠,这就使得集群在成员变换的时候依然可以继续工作。 我们相信,Raft 算法不论出于教学目的还是作为实践项目的基础都是要比 Paxos 或者其他一致性算法要优异的。它比其他算法更加简单,更加容易理解;它的算法描述足以实现一个现实的系统;它有好多开源的实现并且在很多公司里使用;它的安全性已经被证明;它的效率和其他算法比起来也不相上下。 接下来,这篇论文会介绍以下内容:复制状态机问题(第 2 节),讨论 Paxos 的优点和缺点(第 3 节),讨论我们为了可理解性而采取的方法(第 4 节),阐述 Raft 一致性算法(第 5-8 节),评价 Raft 算法(第 9 节),以及一些相关的工作(第 10 节)。 2 复制状态机一致性算法是从复制状态机的背景下提出的(参考英文原文引用37)。在这种方法中,一组服务器上的状态机产生相同状态的副本,并且在一些机器宕掉的情况下也可以继续运行。复制状态机在分布式系统中被用于解决很多容错的问题。例如,大规模的系统中通常都有一个集群领导者,像 GFS、HDFS 和 RAMCloud,典型应用就是一个独立的的复制状态机去管理领导选举和存储配置信息并且在领导人宕机的情况下也要存活下来。比如 Chubby 和 ZooKeeper。 图 1 :复制状态机的结构。一致性算法管理着来自客户端指令的复制日志。状态机从日志中处理相同顺序的相同指令,所以产生的结果也是相同的。 复制状态机通常都是基于复制日志实现的,如图 1。每一个服务器存储一个包含一系列指令的日志,并且按照日志的顺序进行执行。每一个日志都按照相同的顺序包含相同的指令,所以每一个服务器都执行相同的指令序列。因为每个状态机都是确定的,每一次执行操作都产生相同的状态和同样的序列。 保证复制日志相同就是一致性算法的工作了。在一台服务器上,一致性模块接收客户端发送来的指令然后增加到自己的日志中去。它和其他服务器上的一致性模块进行通信来保证每一个服务器上的日志最终都以相同的顺序包含相同的请求,尽管有些服务器会宕机。一旦指令被正确的复制,每一个服务器的状态机按照日志顺序处理他们,然后输出结果被返回给客户端。因此,服务器集群看起来形成一个高可靠的状态机。 实际系统中使用的一致性算法通常含有以下特性: 安全性保证(绝对不会返回一个错误的结果):在非拜占庭错误情况下,包括网络延迟、分区、丢包、冗余和乱序等错误都可以保证正确。 可用性:集群中只要有大多数的机器可运行并且能够相互通信、和客户端通信,就可以保证可用。因此,一个典型的包含 5 个节点的集群可以容忍两个节点的失败。服务器被停止就认为是失败。他们当有稳定的存储的时候可以从状态中恢复回来并重新加入集群。 不依赖时序来保证一致性:物理时钟错误或者极端的消息延迟只有在最坏情况下才会导致可用性问题。 通常情况下,一条指令可以尽可能快的在集群中大多数节点响应一轮远程过程调用时完成。小部分比较慢的节点不会影响系统整体的性能。 3 Paxos 算法的问题在过去的 10 年里,Leslie Lamport 的 Paxos 算法几乎已经成为一致性的代名词:Paxos 是在课程教学中最经常使用的算法,同时也是大多数一致性算法实现的起点。Paxos 首先定义了一个能够达成单一决策一致的协议,比如单条的复制日志项。我们把这一子集叫做单决策 Paxos。然后通过组合多个 Paxos 协议的实例来促进一系列决策的达成。Paxos 保证安全性和活性,同时也支持集群成员关系的变更。Paxos 的正确性已经被证明,在通常情况下也很高效。 不幸的是,Paxos 有两个明显的缺点。第一个缺点是 Paxos 算法特别的难以理解。完整的解释是出了名的不透明;通过极大的努力之后,也只有少数人成功理解了这个算法。因此,有了几次用更简单的术语来解释 Paxos 的尝试。尽管这些解释都只关注了单决策的子集问题,但依然很具有挑战性。在 2012 年 NSDI 的会议中的一次调查显示,很少有人对 Paxos 算法感到满意,甚至在经验老道的研究者中也是如此。我们自己也尝试去理解 Paxos;我们一直没能理解 Paxos 直到我们读了很多对 Paxos 的简化解释并且设计了我们自己的算法之后,这一过程花了近一年时间。 我们假设 Paxos 的不透明性来自它选择单决策问题作为它的基础。单决策 Paxos 是晦涩微妙的,它被划分成了两种没有简单直观解释和无法独立理解的情景。因此,这导致了很难建立起直观的感受为什么单决策 Paxos 算法能够工作。构成多决策 Paxos 增加了很多错综复杂的规则。我们相信,在多决策上达成一致性的问题(一份日志而不是单一的日志记录)能够被分解成其他的方式并且更加直接和明显。 Paxos算法的第二个问题就是它没有提供一个足够好的用来构建一个现实系统的基础。一个原因是还没有一种被广泛认同的多决策问题的算法。Lamport 的描述基本上都是关于单决策 Paxos 的;他简要描述了实施多决策 Paxos 的方法,但是缺乏很多细节。当然也有很多具体化 Paxos 的尝试,但是他们都互相不一样,和 Paxos 的概述也不同。例如 Chubby 这样的系统实现了一个类似于 Paxos 的算法,但是大多数的细节并没有被公开。 而且,Paxos 算法的结构也不是十分易于构建实践的系统;单决策分解也会产生其他的结果。例如,独立的选择一组日志条目然后合并成一个序列化的日志并没有带来太多的好处,仅仅增加了不少复杂性。围绕着日志来设计一个系统是更加简单高效的;新日志条目以严格限制的顺序增添到日志中去。另一个问题是,Paxos 使用了一种对等的点对点的方式作为它的核心(尽管它最终提议了一种弱领导人的方法来优化性能)。在只有一个决策会被制定的简化世界中是很有意义的,但是很少有现实的系统使用这种方式。如果有一系列的决策需要被制定,首先选择一个领导人,然后让他去协调所有的决议,会更加简单快速。 因此,实际的系统中很少有和 Paxos 相似的实践。每一种实现都是从 Paxos 开始研究,然后发现很多实现上的难题,再然后开发了一种和 Paxos 明显不一样的结构。这样是非常费时和容易出错的,并且理解 Paxos 的难度使得这个问题更加糟糕。Paxos 算法在理论上被证明是正确可行的,但是现实的系统和 Paxos 差别是如此的大,以至于这些证明没有什么太大的价值。下面来自 Chubby 实现非常典型: 在Paxos算法描述和实现现实系统中间有着巨大的鸿沟。最终的系统建立在一种没有经过证明的算法之上。 由于以上问题,我们认为 Paxos 算法既没有提供一个良好的基础给实践的系统,也没有给教学很好的帮助。基于一致性问题在大规模软件系统中的重要性,我们决定看看我们是否可以设计一个拥有更好特性的替代 Paxos 的一致性算法。Raft算法就是这次实验的结果。 4 为了可理解性的设计设计 Raft 算法我们有几个初衷:它必须提供一个完整的实际的系统实现基础,这样才能大大减少开发者的工作;它必须在任何情况下都是安全的并且在大多数的情况下都是可用的;并且它的大部分操作必须是高效的。但是我们最重要也是最大的挑战是可理解性。它必须保证对于普遍的人群都可以十分容易的去理解。另外,它必须能够让人形成直观的认识,这样系统的构建者才能够在现实中进行必然的扩展。 在设计 Raft 算法的时候,有很多的点需要我们在各种备选方案中进行选择。在这种情况下,我们评估备选方案基于可理解性原则:解释各个备选方案有多大的难度(例如,Raft 的状态空间有多复杂,是否有微妙的暗示)?对于一个读者而言,完全理解这个方案和暗示是否容易? 我们意识到对这种可理解性分析上具有高度的主观性;尽管如此,我们使用了两种通常适用的技术来解决这个问题。第一个技术就是众所周知的问题分解:只要有可能,我们就将问题分解成几个相对独立的,可被解决的、可解释的和可理解的子问题。例如,Raft 算法被我们分成领导人选举,日志复制,安全性和角色改变几个部分。 我们使用的第二个方法是通过减少状态的数量来简化需要考虑的状态空间,使得系统更加连贯并且在可能的时候消除不确定性。特别的,所有的日志是不允许有空洞的,并且 Raft 限制了日志之间变成不一致状态的可能。尽管在大多数情况下我们都试图去消除不确定性,但是也有一些情况下不确定性可以提升可理解性。尤其是,随机化方法增加了不确定性,但是他们有利于减少状态空间数量,通过处理所有可能选择时使用相似的方法。我们使用随机化去简化 Raft 中领导人选举算法。 5 Raft 一致性算法Raft 是一种用来管理章节 2 中描述的复制日志的算法。图 2 为了参考之用,总结这个算法的简略版本,图 3 列举了这个算法的一些关键特性。图中的这些元素会在剩下的章节逐一介绍。 Raft 通过选举一个高贵的领导人,然后给予他全部的管理复制日志的责任来实现一致性。领导人从客户端接收日志条目,把日志条目复制到其他服务器上,并且当保证安全性的时候告诉其他的服务器应用日志条目到他们的状态机中。拥有一个领导人大大简化了对复制日志的管理。例如,领导人可以决定新的日志条目需要放在日志中的什么位置而不需要和其他服务器商议,并且数据都从领导人流向其他服务器。一个领导人可以宕机,可以和其他服务器失去连接,这时一个新的领导人会被选举出来。 通过领导人的方式,Raft 将一致性问题分解成了三个相对独立的子问题,这些问题会在接下来的子章节中进行讨论: 领导选举:一个新的领导人需要被选举出来,当现存的领导人宕机的时候(章节 5.2) 日志复制:领导人必须从客户端接收日志然后复制到集群中的其他节点,并且强制要求其他节点的日志保持和自己相同。 安全性:在 Raft 中安全性的关键是在图 3 中展示的状态机安全:如果有任何的服务器节点已经应用了一个确定的日志条目到它的状态机中,那么其他服务器节点不能在同一个日志索引位置应用一个不同的指令。章节 5.4 阐述了 Raft 算法是如何保证这个特性的;这个解决方案涉及到一个额外的选举机制(5.2 节)上的限制。 在展示一致性算法之后,这一章节会讨论可用性的一些问题和计时在系统的作用。 状态: 状态 所有服务器上持久存在的 currentTerm 服务器最后一次知道的任期号(初始化为 0,持续递增) votedFor 在当前获得选票的候选人的 Id log[] 日志条目集;每一个条目包含一个用户状态机执行的指令,和收到时的任期号 状态 所有服务器上经常变的 commitIndex 已知的最大的已经被提交的日志条目的索引值 lastApplied 最后被应用到状态机的日志条目索引值(初始化为 0,持续递增) 状态 在领导人里经常改变的 (选举后重新初始化) nextIndex[] 对于每一个服务器,需要发送给他的下一个日志条目的索引值(初始化为领导人最后索引值加一) matchIndex[] 对于每一个服务器,已经复制给他的日志的最高索引值 附加日志 RPC: 由领导人负责调用来复制日志指令;也会用作heartbeat 参数 解释 term 领导人的任期号 leaderId 领导人的 Id,以便于跟随者重定向请求 prevLogIndex 新的日志条目紧随之前的索引值 prevLogTerm prevLogIndex 条目的任期号 entries[] 准备存储的日志条目(表示心跳时为空;一次性发送多个是为了提高效率) leaderCommit 领导人已经提交的日志的索引值 返回值 解释 term 当前的任期号,用于领导人去更新自己 success 跟随者包含了匹配上 prevLogIndex 和 prevLogTerm 的日志时为真 接收者实现: 如果 term < currentTerm 就返回 false (5.1 节) 如果日志在 prevLogIndex 位置处的日志条目的任期号和 prevLogTerm 不匹配,则返回 false (5.3 节) 如果已经存在的日志条目和新的产生冲突(索引值相同但是任期号不同),删除这一条和之后所有的 (5.3 节) 附加日志中尚未存在的任何新条目 如果 leaderCommit > commitIndex,令 commitIndex 等于 leaderCommit 和 新日志条目索引值中较小的一个 请求投票 RPC: 由候选人负责调用用来征集选票(5.2 节) 参数 解释 term 候选人的任期号 candidateId 请求选票的候选人的 Id lastLogIndex 候选人的最后日志条目的索引值 lastLogTerm 候选人最后日志条目的任期号 返回值 解释 term 当前任期号,以便于候选人去更新自己的任期号 voteGranted 候选人赢得了此张选票时为真 接收者实现: 如果term < currentTerm返回 false (5.2 节) 如果 votedFor 为空或者为 candidateId,并且候选人的日志至少和自己一样新,那么就投票给他(5.2 节,5.4 节) 所有服务器需遵守的规则: 所有服务器: 如果commitIndex > lastApplied,那么就 lastApplied 加一,并把log[lastApplied]应用到状态机中(5.3 节) 如果接收到的 RPC 请求或响应中,任期号T > currentTerm,那么就令 currentTerm 等于 T,并切换状态为跟随者(5.1 节) 跟随者(5.2 节): 响应来自候选人和领导者的请求 如果在超过选举超时时间的情况之前都没有收到领导人的心跳,或者是候选人请求投票的,就自己变成候选人 候选人(5.2 节): 在转变成候选人后就立即开始选举过程 自增当前的任期号(currentTerm) 给自己投票 重置选举超时计时器 发送请求投票的 RPC 给其他所有服务器 如果接收到大多数服务器的选票,那么就变成领导人 如果接收到来自新的领导人的附加日志 RPC,转变成跟随者 如果选举过程超时,再次发起一轮选举 领导人: 一旦成为领导人:发送空的附加日志 RPC(心跳)给其他所有的服务器;在一定的空余时间之后不停的重复发送,以阻止跟随者超时(5.2 节) 如果接收到来自客户端的请求:附加条目到本地日志中,在条目被应用到状态机后响应客户端(5.3 节) 如果对于一个跟随者,最后日志条目的索引值大于等于 nextIndex,那么:发送从 nextIndex 开始的所有日志条目: 如果成功:更新相应跟随者的 nextIndex 和 matchIndex 如果因为日志不一致而失败,减少 nextIndex 重试 如果存在一个满足N > commitIndex的 N,并且大多数的matchIndex[i] ≥ N成立,并且log[N].term == currentTerm成立,那么令 commitIndex 等于这个 N (5.3 和 5.4 节) 图 2:一个关于 Raft 一致性算法的浓缩总结(不包括成员变换和日志压缩)。 特性 解释 选举安全特性 对于一个给定的任期号,最多只会有一个领导人被选举出来(5.2 节) 领导人只附加原则 领导人绝对不会删除或者覆盖自己的日志,只会增加(5.3 节) 日志匹配原则 如果两个日志在相同的索引位置的日志条目的任期号相同,那么我们就认为这个日志从头到这个索引位置之间全部完全相同(5.3 节) 领导人完全特性 如果某个日志条目在某个任期号中已经被提交,那么这个条目必然出现在更大任期号的所有领导人中(5.4 节) 状态机安全特性 如果一个领导人已经在给定的索引值位置的日志条目应用到状态机中,那么其他任何的服务器在这个索引位置不会提交一个不同的日志(5.4.3 节) 图 3:Raft 在任何时候都保证以上的各个特性。 5.1 Raft 基础一个 Raft 集群包含若干个服务器节点;通常是 5 个,这允许整个系统容忍 2 个节点的失效。在任何时刻,每一个服务器节点都处于这三个状态之一:领导人、跟随者或者候选人。在通常情况下,系统中只有一个领导人并且其他的节点全部都是跟随者。跟随者都是被动的:他们不会发送任何请求,只是简单的响应来自领导者或者候选人的请求。领导人处理所有的客户端请求(如果一个客户端和跟随者联系,那么跟随者会把请求重定向给领导人)。第三种状态,候选人,是用来在 5.2 节描述的选举新领导人时使用。图 4 展示了这些状态和他们之间的转换关系;这些转换关系会在接下来进行讨论。 图 4:服务器状态。跟随者只响应来自其他服务器的请求。如果跟随者接收不到消息,那么他就会变成候选人并发起一次选举。获得集群中大多数选票的候选人将成为领导者。在一个任期内,领导人一直都会是领导人直到自己宕机了。 图 5:时间被划分成一个个的任期,每个任期开始都是一次选举。在选举成功后,领导人会管理整个集群直到任期结束。有时候选举会失败,那么这个任期就会没有领导人而结束。任期之间的切换可以在不同的时间不同的服务器上观察到。 Raft 把时间分割成任意长度的任期,如图 5。任期用连续的整数标记。每一段任期从一次选举开始,就像章节 5.2 描述的一样,一个或者多个候选人尝试成为领导者。如果一个候选人赢得选举,然后他就在接下来的任期内充当领导人的职责。在某些情况下,一次选举过程会造成选票的瓜分。在这种情况下,这一任期会以没有领导人结束;一个新的任期(和一次新的选举)会很快重新开始。Raft 保证了在一个给定的任期内,最多只有一个领导者。 不同的服务器节点可能多次观察到任期之间的转换,但在某些情况下,一个节点也可能观察不到任何一次选举或者整个任期全程。任期在 Raft 算法中充当逻辑时钟的作用,这会允许服务器节点查明一些过期的信息比如陈旧的领导者。每一个节点存储一个当前任期号,这一编号在整个时期内单调的增长。当服务器之间通信的时候会交换当前任期号;如果一个服务器的当前任期号比其他人小,那么他会更新自己的编号到较大的编号值。如果一个候选人或者领导者发现自己的任期号过期了,那么他会立即恢复成跟随者状态。如果一个节点接收到一个包含过期的任期号的请求,那么他会直接拒绝这个请求。 Raft 算法中服务器节点之间通信使用远程过程调用(RPCs),并且基本的一致性算法只需要两种类型的 RPCs。请求投票(RequestVote) RPCs 由候选人在选举期间发起(章节 5.2),然后附加条目(AppendEntries)RPCs 由领导人发起,用来复制日志和提供一种心跳机制(章节 5.3)。第 7 节为了在服务器之间传输快照增加了第三种 RPC。当服务器没有及时的收到 RPC 的响应时,会进行重试, 并且他们能够并行的发起 RPCs 来获得最佳的性能。 5.2 领导人选举Raft 使用一种心跳机制来触发领导人选举。当服务器程序启动时,他们都是跟随者身份。一个服务器节点继续保持着跟随者状态只要他从领导人或者候选者处接收到有效的 RPCs。领导者周期性的向所有跟随者发送心跳包(即不包含日志项内容的附加日志项 RPCs)来维持自己的权威。如果一个跟随者在一段时间里没有接收到任何消息,也就是选举超时,那么他就会认为系统中没有可用的领导者,并且发起选举以选出新的领导者。 要开始一次选举过程,跟随者先要增加自己的当前任期号并且转换到候选人状态。然后他会并行的向集群中的其他服务器节点发送请求投票的 RPCs 来给自己投票。候选人会继续保持着当前状态直到以下三件事情之一发生:(a) 他自己赢得了这次的选举,(b) 其他的服务器成为领导者,(c) 一段时间之后没有任何一个获胜的人。这些结果会分别的在下面的段落里进行讨论。 当一个候选人从整个集群的大多数服务器节点获得了针对同一个任期号的选票,那么他就赢得了这次选举并成为领导人。每一个服务器最多会对一个任期号投出一张选票,按照先来先服务的原则(注意:5.4 节在投票上增加了一点额外的限制)。要求大多数选票的规则确保了最多只会有一个候选人赢得此次选举(图 3 中的选举安全性)。一旦候选人赢得选举,他就立即成为领导人。然后他会向其他的服务器发送心跳消息来建立自己的权威并且阻止新的领导人的产生。 在等待投票的时候,候选人可能会从其他的服务器接收到声明它是领导人的附加日志项 RPC。如果这个领导人的任期号(包含在此次的 RPC中)不小于候选人当前的任期号,那么候选人会承认领导人合法并回到跟随者状态。 如果此次 RPC 中的任期号比自己小,那么候选人就会拒绝这次的 RPC 并且继续保持候选人状态。 第三种可能的结果是候选人既没有赢得选举也没有输:如果有多个跟随者同时成为候选人,那么选票可能会被瓜分以至于没有候选人可以赢得大多数人的支持。当这种情况发生的时候,每一个候选人都会超时,然后通过增加当前任期号来开始一轮新的选举。然而,没有其他机制的话,选票可能会被无限的重复瓜分。 Raft 算法使用随机选举超时时间的方法来确保很少会发生选票瓜分的情况,就算发生也能很快的解决。为了阻止选票起初就被瓜分,选举超时时间是从一个固定的区间(例如 150-300 毫秒)随机选择。这样可以把服务器都分散开以至于在大多数情况下只有一个服务器会选举超时;然后他赢得选举并在其他服务器超时之前发送心跳包。同样的机制被用在选票瓜分的情况下。每一个候选人在开始一次选举的时候会重置一个随机的选举超时时间,然后在超时时间内等待投票的结果;这样减少了在新的选举中另外的选票瓜分的可能性。9.3 节展示了这种方案能够快速的选出一个领导人。 领导人选举这个例子,体现了可理解性原则是如何指导我们进行方案设计的。起初我们计划使用一种排名系统:每一个候选人都被赋予一个唯一的排名,供候选人之间竞争时进行选择。如果一个候选人发现另一个候选人拥有更高的排名,那么他就会回到跟随者状态,这样高排名的候选人能够更加容易的赢得下一次选举。但是我们发现这种方法在可用性方面会有一点问题(如果高排名的服务器宕机了,那么低排名的服务器可能会超时并再次进入候选人状态。而且如果这个行为发生得足够快,则可能会导致整个选举过程都被重置掉)。我们针对算法进行了多次调整,但是每次调整之后都会有新的问题。最终我们认为随机重试的方法是更加明显和易于理解的。 5.3 日志复制一旦一个领导人被选举出来,他就开始为客户端提供服务。客户端的每一个请求都包含一条被复制状态机执行的指令。领导人把这条指令作为一条新的日志条目附加到日志中去,然后并行的发起附加条目 RPCs 给其他的服务器,让他们复制这条日志条目。当这条日志条目被安全的复制(下面会介绍),领导人会应用这条日志条目到它的状态机中然后把执行的结果返回给客户端。如果跟随者崩溃或者运行缓慢,再或者网络丢包,领导人会不断的重复尝试附加日志条目 RPCs (尽管已经回复了客户端)直到所有的跟随者都最终存储了所有的日志条目。 图 6:日志由有序序号标记的条目组成。每个条目都包含创建时的任期号(图中框中的数字),和一个状态机需要执行的指令。一个条目当可以安全的被应用到状态机中去的时候,就认为是可以提交了。 日志以图 6 展示的方式组织。每一个日志条目存储一条状态机指令和从领导人收到这条指令时的任期号。日志中的任期号用来检查是否出现不一致的情况,同时也用来保证图 3 中的某些性质。每一条日志条目同时也都有一个整数索引值来表明它在日志中的位置。 领导人来决定什么时候把日志条目应用到状态机中是安全的;这种日志条目被称为已提交。Raft 算法保证所有已提交的日志条目都是持久化的并且最终会被所有可用的状态机执行。在领导人将创建的日志条目复制到大多数的服务器上的时候,日志条目就会被提交(例如在图 6 中的条目 7)。同时,领导人的日志中之前的所有日志条目也都会被提交,包括由其他领导人创建的条目。5.4 节会讨论某些当在领导人改变之后应用这条规则的隐晦内容,同时他也展示了这种提交的定义是安全的。领导人跟踪了最大的将会被提交的日志项的索引,并且索引值会被包含在未来的所有附加日志 RPCs (包括心跳包),这样其他的服务器才能最终知道领导人的提交位置。一旦跟随者知道一条日志条目已经被提交,那么他也会将这个日志条目应用到本地的状态机中(按照日志的顺序)。 我们设计了 Raft 的日志机制来维护一个不同服务器的日志之间的高层次的一致性。这么做不仅简化了系统的行为也使得更加可预计,同时他也是安全性保证的一个重要组件。Raft 维护着以下的特性,这些同时也组成了图 3 中的日志匹配特性: 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令。 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也全部相同。 第一个特性来自这样的一个事实,领导人最多在一个任期里在指定的一个日志索引位置创建一条日志条目,同时日志条目在日志中的位置也从来不会改变。第二个特性由附加日志 RPC 的一个简单的一致性检查所保证。在发送附加日志 RPC 的时候,领导人会把新的日志条目紧接着之前的条目的索引位置和任期号包含在里面。如果跟随者在它的日志中找不到包含相同索引位置和任期号的条目,那么他就会拒绝接收新的日志条目。一致性检查就像一个归纳步骤:一开始空的日志状态肯定是满足日志匹配特性的,然后一致性检查保护了日志匹配特性当日志扩展的时候。因此,每当附加日志 RPC 返回成功时,领导人就知道跟随者的日志一定是和自己相同的了。 在正常的操作中,领导人和跟随者的日志保持一致性,所以附加日志 RPC 的一致性检查从来不会失败。然而,领导人崩溃的情况会使得日志处于不一致的状态(老的领导人可能还没有完全复制所有的日志条目)。这种不一致问题会在领导人和跟随者的一系列崩溃下加剧。图 7 展示了跟随者的日志可能和新的领导人不同的方式。跟随者可能会丢失一些在新的领导人中有的日志条目,他也可能拥有一些领导人没有的日志条目,或者两者都发生。丢失或者多出日志条目可能会持续多个任期。 图 7:当一个领导人成功当选时,跟随者可能是任何情况(a-f)。每一个盒子表示是一个日志条目;里面的数字表示任期号。跟随者可能会缺少一些日志条目(a-b),可能会有一些未被提交的日志条目(c-d),或者两种情况都存在(e-f)。例如,场景 f 可能会这样发生,某服务器在任期 2 的时候是领导人,已附加了一些日志条目到自己的日志中,但在提交之前就崩溃了;很快这个机器就被重启了,在任期 3 重新被选为领导人,并且又增加了一些日志条目到自己的日志中;在任期 2 和任期 3 的日志被提交之前,这个服务器又宕机了,并且在接下来的几个任期里一直处于宕机状态。 在 Raft 算法中,领导人处理不一致是通过强制跟随者直接复制自己的日志来解决了。这意味着在跟随者中的冲突的日志条目会被领导人的日志覆盖。5.4 节会阐述如何通过增加一些限制来使得这样的操作是安全的。 要使得跟随者的日志进入和自己一致的状态,领导人必须找到最后两者达成一致的地方,然后删除从那个点之后的所有日志条目,发送自己的日志给跟随者。所有的这些操作都在进行附加日志 RPCs 的一致性检查时完成。领导人针对每一个跟随者维护了一个 nextIndex,这表示下一个需要发送给跟随者的日志条目的索引地址。当一个领导人刚获得权力的时候,他初始化所有的 nextIndex 值为自己的最后一条日志的index加1(图 7 中的 11)。如果一个跟随者的日志和领导人不一致,那么在下一次的附加日志 RPC 时的一致性检查就会失败。在被跟随者拒绝之后,领导人就会减小 nextIndex 值并进行重试。最终 nextIndex 会在某个位置使得领导人和跟随者的日志达成一致。当这种情况发生,附加日志 RPC 就会成功,这时就会把跟随者冲突的日志条目全部删除并且加上领导人的日志。一旦附加日志 RPC 成功,那么跟随者的日志就会和领导人保持一致,并且在接下来的任期里一直继续保持。 如果需要的话,算法可以通过减少被拒绝的附加日志 RPCs 的次数来优化。例如,当附加日志 RPC 的请求被拒绝的时候,跟随者可以包含冲突的条目的任期号和自己存储的那个任期的最早的索引地址。借助这些信息,领导人可以减小 nextIndex 越过所有那个任期冲突的所有日志条目;这样就变成每个任期需要一次附加条目 RPC 而不是每个条目一次。在实践中,我们十分怀疑这种优化是否是必要的,因为失败是很少发生的并且也不大可能会有这么多不一致的日志。 通过这种机制,领导人在获得权力的时候就不需要任何特殊的操作来恢复一致性。他只需要进行正常的操作,然后日志就能自动的在回复附加日志 RPC 的一致性检查失败的时候自动趋于一致。领导人从来不会覆盖或者删除自己的日志(图 3 的领导人只附加特性)。 日志复制机制展示出了第 2 节中形容的一致性特性:Raft 能够接受,复制并应用新的日志条目只要大部分的机器是工作的;在通常的情况下,新的日志条目可以在一次 RPC 中被复制给集群中的大多数机器;并且单个的缓慢的跟随者不会影响整体的性能。 5.4 安全性前面的章节里描述了 Raft 算法是如何选举和复制日志的。然而,到目前为止描述的机制并不能充分的保证每一个状态机会按照相同的顺序执行相同的指令。例如,一个跟随者可能会进入不可用状态同时领导人已经提交了若干的日志条目,然后这个跟随者可能会被选举为领导人并且覆盖这些日志条目;因此,不同的状态机可能会执行不同的指令序列。 这一节通过在领导选举的时候增加一些限制来完善 Raft 算法。这一限制保证了任何的领导人对于给定的任期号,都拥有了之前任期的所有被提交的日志条目(图 3 中的领导人完整特性)。增加这一选举时的限制,我们对于提交时的规则也更加清晰。最终,我们将展示对于领导人完整特性的简要证明,并且说明领导人是如何领导复制状态机的做出正确行为的。 5.4.1 选举限制在任何基于领导人的一致性算法中,领导人都必须存储所有已经提交的日志条目。在某些一致性算法中,例如 Viewstamped Replication,某个节点即使是一开始并没有包含所有已经提交的日志条目,它也能被选为领导者。这些算法都包含一些额外的机制来识别丢失的日志条目并把他们传送给新的领导人,要么是在选举阶段要么在之后很快进行。不幸的是,这种方法会导致相当大的额外的机制和复杂性。Raft 使用了一种更加简单的方法,它可以保证所有之前的任期号中已经提交的日志条目在选举的时候都会出现在新的领导人中,不需要传送这些日志条目给领导人。这意味着日志条目的传送是单向的,只从领导人传给跟随者,并且领导人从不会覆盖自身本地日志中已经存在的条目。 Raft 使用投票的方式来阻止一个候选人赢得选举除非这个候选人包含了所有已经提交的日志条目。候选人为了赢得选举必须联系集群中的大部分节点,这意味着每一个已经提交的日志条目在这些服务器节点中肯定存在于至少一个节点上。如果候选人的日志至少和大多数的服务器节点一样新(这个新的定义会在下面讨论),那么他一定持有了所有已经提交的日志条目。请求投票 RPC 实现了这样的限制: RPC 中包含了候选人的日志信息,然后投票人会拒绝掉那些日志没有自己新的投票请求。 Raft 通过比较两份日志中最后一条日志条目的索引值和任期号定义谁的日志比较新。如果两份日志最后的条目的任期号不同,那么任期号大的日志更加新。如果两份日志最后的条目任期号相同,那么日志比较长的那个就更加新。 5.4.2 提交之前任期内的日志条目如同 5.3 节介绍的那样,领导人知道一条当前任期内的日志记录是可以被提交的,只要它被存储到了大多数的服务器上。如果一个领导人在提交日志条目之前崩溃了,未来后续的领导人会继续尝试复制这条日志记录。然而,一个领导人不能断定一个之前任期里的日志条目被保存到大多数服务器上的时候就一定已经提交了。图 8 展示了一种情况,一条已经被存储到大多数节点上的老日志条目,也依然有可能会被未来的领导人覆盖掉。 图 8:如图的时间序列展示了为什么领导人无法决定对老任期号的日志条目进行提交。在 (a) 中,S1 是领导者,部分的复制了索引位置 2 的日志条目。在 (b) 中,S1 崩溃了,然后 S5 在任期 3 里通过 S3、S4 和自己的选票赢得选举,然后从客户端接收了一条不一样的日志条目放在了索引 2 处。然后到 (c),S5 又崩溃了;S1 重新启动,选举成功,开始复制日志。在这时,来自任期 2 的那条日志已经被复制到了集群中的大多数机器上,但是还没有被提交。如果 S1 在 (d) 中又崩溃了,S5 可以重新被选举成功(通过来自 S2,S3 和 S4 的选票),然后覆盖了他们在索引 2 处的日志。反之,如果在崩溃之前,S1 把自己主导的新任期里产生的日志条目复制到了大多数机器上,就如 (e) 中那样,那么在后面任期里面这些新的日志条目就会被提交(因为S5 就不可能选举成功)。 这样在同一时刻就同时保证了,之前的所有老的日志条目就会被提交。 为了消除图 8 里描述的情况,Raft 永远不会通过计算副本数目的方式去提交一个之前任期内的日志条目。只有领导人当前任期里的日志条目通过计算副本数目可以被提交;一旦当前任期的日志条目以这种方式被提交,那么由于日志匹配特性,之前的日志条目也都会被间接的提交。在某些情况下,领导人可以安全的知道一个老的日志条目是否已经被提交(例如,该条目是否存储到所有服务器上),但是 Raft 为了简化问题使用一种更加保守的方法。 当领导人复制之前任期里的日志时,Raft 会为所有日志保留原始的任期号, 这在提交规则上产生了额外的复杂性。在其他的一致性算法中,如果一个新的领导人要重新复制之前的任期里的日志时,它必须使用当前新的任期号。Raft 使用的方法更加容易辨别出日志,因为它可以随着时间和日志的变化对日志维护着同一个任期编号。另外,和其他的算法相比,Raft 中的新领导人只需要发送更少日志条目(其他算法中必须在他们被提交之前发送更多的冗余日志条目来为他们重新编号)。 5.4.3 安全性论证在给定了完整的 Raft 算法之后,我们现在可以更加精确的讨论领导人完整性特性(这一讨论基于 9.2 节的安全性证明)。我们假设领导人完全性特性是不存在的,然后我们推出矛盾来。假设任期 T 的领导人(领导人 T)在任期内提交了一条日志条目,但是这条日志条目没有被存储到未来某个任期的领导人的日志中。设大于 T 的最小任期 U 的领导人 U 没有这条日志条目。 图 9:如果 S1 (任期 T 的领导者)提交了一条新的日志在它的任期里,然后 S5 在之后的任期 U 里被选举为领导人,然后至少会有一个机器,如 S3,既拥有来自 S1 的日志,也给 S5 投票了。 在领导人 U 选举的时候一定没有那条被提交的日志条目(领导人从不会删除或者覆盖任何条目)。 领导人 T 复制这条日志条目给集群中的大多数节点,同时,领导人U 从集群中的大多数节点赢得了选票。因此,至少有一个节点(投票者、选民)同时接受了来自领导人T 的日志条目,并且给领导人U 投票了,如图 9。这个投票者是产生这个矛盾的关键。 这个投票者必须在给领导人 U 投票之前先接受了从领导人 T 发来的已经被提交的日志条目;否则他就会拒绝来自领导人 T 的附加日志请求(因为此时他的任期号会比 T 大)。 投票者在给领导人 U 投票时依然保存有这条日志条目,因为任何中间的领导人都包含该日志条目(根据上述的假设),领导人从不会删除条目,并且跟随者只有在和领导人冲突的时候才会删除条目。 投票者把自己选票投给领导人 U 时,领导人 U 的日志必须和投票者自己一样新。这就导致了两者矛盾之一。 首先,如果投票者和领导人 U 的最后一条日志的任期号相同,那么领导人 U 的日志至少和投票者一样长,所以领导人 U 的日志一定包含所有投票者的日志。这是另一处矛盾,因为投票者包含了那条已经被提交的日志条目,但是在上述的假设里,领导人 U 是不包含的。 除此之外,领导人 U 的最后一条日志的任期号就必须比投票人大了。此外,他也比 T 大,因为投票人的最后一条日志的任期号至少和 T 一样大(他包含了来自任期 T 的已提交的日志)。创建了领导人 U 最后一条日志的之前领导人一定已经包含了那条被提交的日志(根据上述假设,领导人 U 是第一个不包含该日志条目的领导人)。所以,根据日志匹配特性,领导人 U 一定也包含那条被提交的日志,这里产生矛盾。 这里完成了矛盾。因此,所有比 T 大的领导人一定包含了所有来自 T 的已经被提交的日志。 日志匹配原则保证了未来的领导人也同时会包含被间接提交的条目,例如图 8 (d) 中的索引 2。 通过领导人完全特性,我们就能证明图 3 中的状态机安全特性,即如果服务器已经在某个给定的索引值应用了日志条目到自己的状态机里,那么其他的服务器不会应用一个不一样的日志到同一个索引值上。在一个服务器应用一条日志条目到他自己的状态机中时,他的日志必须和领导人的日志,在该条目和之前的条目上相同,并且已经被提交。现在我们来考虑在任何一个服务器应用一个指定索引位置的日志的最小任期;日志完全特性保证拥有更高任期号的领导人会存储相同的日志条目,所以之后的任期里应用某个索引位置的日志条目也会是相同的值。因此,状态机安全特性是成立的。 最后,Raft 要求服务器按照日志中索引位置顺序应用日志条目。和状态机安全特性结合起来看,这就意味着所有的服务器会应用相同的日志序列集到自己的状态机中,并且是按照相同的顺序。 5.5 跟随者和候选人崩溃到目前为止,我们都只关注了领导人崩溃的情况。跟随者和候选人崩溃后的处理方式比领导人要简单的多,并且他们的处理方式是相同的。如果跟随者或者候选人崩溃了,那么后续发送给他们的 RPCs 都会失败。Raft 中处理这种失败就是简单的通过无限的重试;如果崩溃的机器重启了,那么这些 RPC 就会完整的成功。如果一个服务器在完成了一个 RPC,但是还没有响应的时候崩溃了,那么在他重新启动之后就会再次收到同样的请求。Raft 的 RPCs 都是幂等的,所以这样重试不会造成任何问题。例如一个跟随者如果收到附加日志请求但是他已经包含了这一日志,那么他就会直接忽略这个新的请求。 5.6 时间和可用性Raft 的要求之一就是安全性不能依赖时间:整个系统不能因为某些事件运行的比预期快一点或者慢一点就产生了错误的结果。但是,可用性(系统可以及时的响应客户端)不可避免的要依赖于时间。例如,如果消息交换比服务器故障间隔时间长,候选人将没有足够长的时间来赢得选举;没有一个稳定的领导人,Raft 将无法工作。 领导人选举是 Raft 中对时间要求最为关键的方面。Raft 可以选举并维持一个稳定的领导人,只要系统满足下面的时间要求: 广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障间隔时间(MTBF) 在这个不等式中,广播时间指的是从一个服务器并行的发送 RPCs 给集群中的其他服务器并接收响应的平均时间;选举超时时间就是在 5.2 节中介绍的选举的超时时间限制;然后平均故障间隔时间就是对于一台服务器而言,两次故障之间的平均时间。广播时间必须比选举超时时间小一个量级,这样领导人才能够发送稳定的心跳消息来阻止跟随者开始进入选举状态;通过随机化选举超时时间的方法,这个不等式也使得选票瓜分的情况变得不可能。选举超时时间应该要比平均故障间隔时间小上几个数量级,这样整个系统才能稳定的运行。当领导人崩溃后,整个系统会大约相当于选举超时的时间里不可用;我们希望这种情况在整个系统的运行中很少出现。 广播时间和平均故障间隔时间是由系统决定的,但是选举超时时间是我们自己选择的。Raft 的 RPCs 需要接收方将信息持久化的保存到稳定存储中去,所以广播时间大约是 0.5 毫秒到 20 毫秒,取决于存储的技术。因此,选举超时时间可能需要在 10 毫秒到 500 毫秒之间。大多数的服务器的平均故障间隔时间都在几个月甚至更长,很容易满足时间的需求。 6 集群成员变化到目前为止,我们都假设集群的配置(加入到一致性算法的服务器集合)是固定不变的。但是在实践中,偶尔是会改变集群的配置的,例如替换那些宕机的机器或者改变复制级别。尽管可以通过暂停整个集群,更新所有配置,然后重启整个集群的方式来实现,但是在更改的时候集群会不可用。另外,如果存在手工操作步骤,那么就会有操作失误的风险。为了避免这样的问题,我们决定自动化配置改变并且将其纳入到 Raft 一致性算法中来。 为了让配置修改机制能够安全,那么在转换的过程中不能够存在任何时间点使得两个领导人同时被选举成功在同一个任期里。不幸的是,任何服务器直接从旧的配置直接转换到新的配置的方案都是不安全的。一次性自动的转换所有服务器是不可能的,所以在转换期间整个集群存在划分成两个独立的大多数群体的可能性(见图 10)。 图 10:直接从一种配置转到新的配置是十分不安全的,因为各个机器可能在任何的时候进行转换。在这个例子中,集群配额从 3 台机器变成了 5 台。不幸的是,存在这样的一个时间点,两个不同的领导人在同一个任期里都可以被选举成功。一个是通过旧的配置,一个通过新的配置。 为了保证安全性,配置更改必须使用两阶段方法。目前有很多种两阶段的实现。例如,有些系统在第一阶段停掉旧的配置所以集群就不能处理客户端请求;然后在第二阶段在启用新的配置。在 Raft 中,集群先切换到一个过渡的配置,我们称之为共同一致;一旦共同一致已经被提交了,那么系统就切换到新的配置上。共同一致是老配置和新配置的结合: 日志条目被复制给集群中新、老配置的所有服务器。 新、旧配置的服务器都可以成为领导人。 达成一致(针对选举和提交)需要分别在两种配置上获得大多数的支持。 共同一致允许独立的服务器在不影响安全性的前提下,在不同的时间进行配置转换过程。此外,共同一致可以让集群在配置转换的过程人依然响应客户端的请求。 集群配置在复制日志中以特殊的日志条目来存储和通信;图 11 展示了配置转换的过程。当一个领导人接收到一个改变配置从 C-old 到 C-new 的请求,他会为了共同一致存储配置(图中的 C-old,new),以前面描述的日志条目和副本的形式。一旦一个服务器将新的配置日志条目增加到它的日志中,他就会用这个配置来做出未来所有的决定(服务器总是使用最新的配置,无论他是否已经被提交)。这意味着领导人要使用 C-old,new 的规则来决定日志条目 C-old,new 什么时候需要被提交。如果领导人崩溃了,被选出来的新领导人可能是使用 C-old 配置也可能是 C-old,new 配置,这取决于赢得选举的候选人是否已经接收到了 C-old,new 配置。在任何情况下, C-new 配置在这一时期都不会单方面的做出决定。 一旦 C-old,new 被提交,那么无论是 C-old 还是 C-new,在没有经过他人批准的情况下都不可能做出决定,并且领导人完全特性保证了只有拥有 C-old,new 日志条目的服务器才有可能被选举为领导人。这个时候,领导人创建一条关于 C-new 配置的日志条目并复制给集群就是安全的了。再者,每个服务器在见到新的配置的时候就会立即生效。当新的配置在 C-new 的规则下被提交,旧的配置就变得无关紧要,同时不使用新的配置的服务器就可以被关闭了。如图 11,C-old 和 C-new 没有任何机会同时做出单方面的决定;这保证了安全性。 图 11:一个配置切换的时间线。虚线表示已经被创建但是还没有被提交的条目,实线表示最后被提交的日志条目。领导人首先创建了 C-old,new 的配置条目在自己的日志中,并提交到 C-old,new 中(C-old 的大多数和 C-new 的大多数)。然后他创建 C-new 条目并提交到 C-new 中的大多数。这样就不存在 C-new 和 C-old 可以同时做出决定的时间点。 在关于重新配置还有三个问题需要提出。第一个问题是,新的服务器可能初始化没有存储任何的日志条目。当这些服务器以这种状态加入到集群中,那么他们需要一段时间来更新追赶,这时还不能提交新的日志条目。为了避免这种可用性的间隔时间,Raft 在配置更新的时候使用了一种额外的阶段,在这个阶段,新的服务器以没有投票权身份加入到集群中来(领导人复制日志给他们,但是不考虑他们是大多数)。一旦新的服务器追赶上了集群中的其他机器,重新配置可以像上面描述的一样处理。 第二个问题是,集群的领导人可能不是新配置的一员。在这种情况下,领导人就会在提交了 C-new 日志之后退位(回到跟随者状态)。这意味着有这样的一段时间,领导人管理着集群,但是不包括他自己;他复制日志但是不把他自己算作是大多数之一。当 C-new 被提交时,会发生领导人过渡,因为这时是最早新的配置可以独立工作的时间点(将总是能够在 C-new 配置下选出新的领导人)。在此之前,可能只能从 C-old 中选出领导人。 第三个问题是,移除不在 C-new 中的服务器可能会扰乱集群。这些服务器将不会再接收到心跳,所以当选举超时,他们就会进行新的选举过程。他们会发送拥有新的任期号的请求投票 RPCs,这样会导致当前的领导人回退成跟随者状态。新的领导人最终会被选出来,但是被移除的服务器将会再次超时,然后这个过程会再次重复,导致整体可用性大幅降低。 为了避免这个问题,当服务器确认当前领导人存在时,服务器会忽略请求投票 RPCs。特别的,当服务器在当前最小选举超时时间内收到一个请求投票 RPC,他不会更新当前的任期号或者投出选票。这不会影响正常的选举,每个服务器在开始一次选举之前,至少等待一个最小选举超时时间。然而,这有利于避免被移除的服务器扰乱:如果领导人能够发送心跳给集群,那么他就不会被更大的任期号废黜。 7 日志压缩Raft 的日志在正常操作中不断的增长,但是在实际的系统中,日志不能无限制的增长。随着日志不断增长,他会占用越来越多的空间,花费越来越多的时间来重置。如果没有一定的机制去清除日志里积累的陈旧的信息,那么会带来可用性问题。 快照是最简单的压缩方法。在快照系统中,整个系统的状态都以快照的形式写入到稳定的持久化存储中,然后到那个时间点之前的日志全部丢弃。快照技术被使用在 Chubby 和 ZooKeeper 中,接下来的章节会介绍 Raft 中的快照技术。 增量压缩的方法,例如日志清理或者日志结构合并树,都是可行的。这些方法每次只对一小部分数据进行操作,这样就分散了压缩的负载压力。首先,他们先选择一个已经积累的大量已经被删除或者被覆盖对象的区域,然后重写那个区域还活跃的对象,之后释放那个区域。和简单操作整个数据集合的快照相比,需要增加复杂的机制来实现。状态机可以实现 LSM tree 使用和快照相同的接口,但是日志清除方法就需要修改 Raft 了。 图 12:一个服务器用新的快照替换了从 1 到 5 的条目,快照值存储了当前的状态。快照中包含了最后的索引位置和任期号。 图 12 展示了 Raft 中快照的基础思想。每个服务器独立的创建快照,只包括已经被提交的日志。主要的工作包括将状态机的状态写入到快照中。Raft 也包含一些少量的元数据到快照中:最后被包含索引指的是被快照取代的最后的条目在日志中的索引值(状态机最后应用的日志),最后被包含的任期指的是该条目的任期号。保留这些数据是为了支持快照后紧接着的第一个条目的附加日志请求时的一致性检查,因为这个条目需要前一日志条目的索引值和任期号。为了支持集群成员更新(第 6 节),快照中也将最后的一次配置作为最后一个条目存下来。一旦服务器完成一次快照,他就可以删除最后索引位置之前的所有日志和快照了。 尽管通常服务器都是独立的创建快照,但是领导人必须偶尔的发送快照给一些落后的跟随者。这通常发生在当领导人已经丢弃了下一条需要发送给跟随者的日志条目的时候。幸运的是这种情况不是常规操作:一个与领导人保持同步的跟随者通常都会有这个条目。然而一个运行非常缓慢的跟随者或者新加入集群的服务器(第 6 节)将不会有这个条目。这时让这个跟随者更新到最新的状态的方式就是通过网络把快照发送给他们。 安装快照 RPC: 由领导人调用以将快照的分块发送给跟随者。领导者总是按顺序发送分块。 参数 解释 term 领导人的任期号 leaderId 领导人的 Id,以便于跟随者重定向请求 lastIncludedIndex 快照中包含的最后日志条目的索引值 lastIncludedTerm 快照中包含的最后日志条目的任期号 offset 分块在快照中的字节偏移量 data[] 原始数据 done 如果这是最后一个分块则为 true 结果 解释 term 当前任期号(currentTerm),便于领导人更新自己 接收者实现: 如果term < currentTerm就立即回复 如果是第一个分块(offset 为 0)就创建一个新的快照 在指定偏移量写入数据 如果 done 是 false,则继续等待更多的数据 保存快照文件,丢弃具有较小索引的任何现有或部分快照 如果现存的日志条目与快照中最后包含的日志条目具有相同的索引值和任期号,则保留其后的日志条目并进行回复 丢弃整个日志 使用快照重置状态机(并加载快照的集群配置) 图 13:一个关于安装快照的简要概述。为了便于传输,快照都是被分成分块的;每个分块都给了跟随者生命的迹象,所以跟随者可以重置选举超时计时器。 在这种情况下领导人使用一种叫做安装快照的新的 RPC 来发送快照给太落后的跟随者;见图 13。当跟随者通过这种 RPC 接收到快照时,他必须自己决定对于已经存在的日志该如何处理。通常快照会包含没有在接收者日志中存在的信息。在这种情况下,跟随者丢弃其整个日志;它全部被快照取代,并且可能包含与快照冲突的未提交条目。如果接收到的快照是自己日志的前面部分(由于网络重传或者错误),那么被快照包含的条目将会被全部删除,但是快照后面的条目仍然有效,必须保留。 这种快照的方式背离了 Raft 的强领导人原则,因为跟随者可以在不知道领导人情况下创建快照。但是我们认为这种背离是值得的。领导人的存在,是为了解决在达成一致性的时候的冲突,但是在创建快照的时候,一致性已经达成,这时不存在冲突了,所以没有领导人也是可以的。数据依然是从领导人传给跟随者,只是跟随者可以重新组织他们的数据了。 我们考虑过一种替代的基于领导人的快照方案,即只有领导人创建快照,然后发送给所有的跟随者。但是这样做有两个缺点。第一,发送快照会浪费网络带宽并且延缓了快照处理的时间。每个跟随者都已经拥有了所有产生快照需要的信息,而且很显然,自己从本地的状态中创建快照比通过网络接收别人发来的要经济。第二,领导人的实现会更加复杂。例如,领导人需要发送快照的同时并行的将新的日志条目发送给跟随者,这样才不会阻塞新的客户端请求。 还有两个问题影响了快照的性能。首先,服务器必须决定什么时候应该创建快照。如果快照创建的过于频繁,那么就会浪费大量的磁盘带宽和其他资源;如果创建快照频率太低,他就要承受耗尽存储容量的风险,同时也增加了从日志重建的时间。一个简单的策略就是当日志大小达到一个固定大小的时候就创建一次快照。如果这个阈值设置的显著大于期望的快照的大小,那么快照对磁盘压力的影响就会很小了。 第二个影响性能的问题就是写入快照需要花费显著的一段时间,并且我们还不希望影响到正常操作。解决方案是通过写时复制的技术,这样新的更新就可以被接收而不影响到快照。例如,具有函数式数据结构的状态机天然支持这样的功能。另外,操作系统的写时复制技术的支持(如 Linux 上的 fork)可以被用来创建完整的状态机的内存快照(我们的实现就是这样的)。 8 客户端交互这一节将介绍客户端是如何和 Raft 进行交互的,包括客户端如何发现领导人和 Raft 是如何支持线性化语义的。这些问题对于所有基于一致性的系统都存在,并且 Raft 的解决方案和其他的也差不多。 Raft 中的客户端发送所有请求给领导人。当客户端启动的时候,他会随机挑选一个服务器进行通信。如果客户端第一次挑选的服务器不是领导人,那么那个服务器会拒绝客户端的请求并且提供他最近接收到的领导人的信息(附加条目请求包含了领导人的网络地址)。如果领导人已经崩溃了,那么客户端的请求就会超时;客户端之后会再次重试随机挑选服务器的过程。 我们 Raft 的目标是要实现线性化语义(每一次操作立即执行,只执行一次,在他调用和收到回复之间)。但是,如上述,Raft 是可以执行同一条命令多次的:例如,如果领导人在提交了这条日志之后,但是在响应客户端之前崩溃了,那么客户端会和新的领导人重试这条指令,导致这条命令就被再次执行了。解决方案就是客户端对于每一条指令都赋予一个唯一的序列号。然后,状态机跟踪每条指令最新的序列号和相应的响应。如果接收到一条指令,它的序列号已经被执行了,那么就立即返回结果,而不重新执行指令。 只读的操作可以直接处理而不需要记录日志。但是,在不增加任何限制的情况下,这么做可能会冒着返回脏数据的风险,因为领导人响应客户端请求时可能已经被新的领导人作废了,但是他还不知道。线性化的读操作必须不能返回脏数据,Raft 需要使用两个额外的措施在不使用日志的情况下保证这一点。首先,领导人必须有关于被提交日志的最新信息。领导人完全特性保证了领导人一定拥有所有已经被提交的日志条目,但是在他任期开始的时候,他可能不知道那些是已经被提交的。为了知道这些信息,他需要在他的任期里提交一条日志条目。Raft 中通过领导人在任期开始的时候提交一个空白的没有任何操作的日志条目到日志中去来实现。第二,领导人在处理只读的请求之前必须检查自己是否已经被废黜了(他自己的信息已经变脏了如果一个更新的领导人被选举出来)。Raft 中通过让领导人在响应只读请求之前,先和集群中的大多数节点交换一次心跳信息来处理这个问题。可选的,领导人可以依赖心跳机制来实现一种租约的机制,但是这种方法依赖时间来保证安全性(假设时间误差是有界的)。 9 算法实现和评估我们已经为 RAMCloud 实现了 Raft 算法作为存储配置信息的复制状态机的一部分,并且帮助 RAMCloud 协调故障转移。这个 Raft 实现包含大约 2000 行 C++ 代码,其中不包括测试、注释和空行。这些代码是开源的。同时也有大约 25 个其他独立的第三方的基于这篇论文草稿的开源实现,针对不同的开发场景。同时,很多公司已经部署了基于 Raft 的系统。 这一节会从三个方面来评估 Raft 算法:可理解性、正确性和性能。 9.1 可理解性为了和 Paxos 比较 Raft 算法的可理解能力,我们针对高层次的本科生和研究生,在斯坦福大学的高级操作系统课程和加州大学伯克利分校的分布式计算课程上,进行了一次学习的实验。我们分别拍了针对 Raft 和 Paxos 的视频课程,并准备了相应的小测验。Raft 的视频讲课覆盖了这篇论文的所有内容除了日志压缩;Paxos 讲课包含了足够的资料来创建一个等价的复制状态机,包括单决策 Paxos,多决策 Paxos,重新配置和一些实际系统需要的性能优化(例如领导人选举)。小测验测试一些对算法的基本理解和解释一些边角的示例。每个学生都是看完第一个视频,回答相应的测试,再看第二个视频,回答相应的测试。大约有一半的学生先进行 Paxos 部分,然后另一半先进行 Raft 部分,这是为了说明两者从第一部分的算法学习中获得的表现和经验的差异。我们计算参加人员的每一个小测验的得分来看参与者是否在 Raft 算法上更加容易理解。 我们尽可能的使得 Paxos 和 Raft 的比较更加公平。这个实验偏爱 Paxos 表现在两个方面:43 个参加者中有 15 个人在之前有一些 Paxos 的经验,并且 Paxos 的视频要长 14%。如表格 1 总结的那样,我们采取了一些措施来减轻这种潜在的偏见。我们所有的材料都可供审查。 关心 缓和偏见采取的手段 可供查看的材料 相同的讲课质量 两者使用同一个讲师。Paxos 使用的是现在很多大学里经常使用的。Paxos 会长 14%。 视频 相同的测验难度 问题以难度分组,在两个测验里成对出现。 小测验 公平评分 使用评价量规。随机顺序打分,两个测验交替进行。 评价量规(rubric) 表 1:考虑到可能会存在的偏见,对于每种情况的解决方法,和相应的材料。 参加者平均在 Raft 的测验中比 Paxos 高 4.9 分(总分 60,那么 Raft 的平均得分是 25.7,而 Paxos 是 20.8);图 14 展示了每个参与者的得分。配置t-检验(又称student‘s t-test)表明,在 95% 的可信度下,真实的 Raft 分数分布至少比 Paxos 高 2.5 分。 图 14:一个散点图表示了 43 个学生在 Paxos 和 Raft 的小测验中的成绩。在对角线之上的点表示在 Raft 获得了更高分数的学生。 我们也建立了一个线性回归模型来预测一个新的学生的测验成绩,基于以下三个因素:他们使用的是哪个小测验,之前对 Paxos 的经验,和学习算法的顺序。模型预测,对小测验的选择会产生 12.5 分的差别。这显著的高于之前的 4.9 分,因为很多学生在之前都已经有了对于 Paxos 的经验,这相当明显的帮助 Paxos,对 Raft 就没什么太大影响了。但是奇怪的是,模型预测对于先进行 Paxos 小测验的人而言,Raft的得分低了6.3分; 虽然我们不知道为什么,这似乎在统计上是有意义的。 我们同时也在测验之后调查了参与者,他们认为哪个算法更加容易实现和解释;这个的结果在图 15 上。压倒性的结果表明 Raft 算法更加容易实现和解释(41 人中的 33个)。但是,这种自己报告的结果不如参与者的成绩更加可信,并且参与者可能因为我们的 Raft 更加易于理解的假说而产生偏见。 图 15:通过一个 5 分制的问题,参与者(左边)被问哪个算法他们觉得在一个高效正确的系统里更容易实现,右边被问哪个更容易向学生解释。 关于 Raft 用户学习有一个更加详细的讨论。 9.2 正确性在第 5 节,我们已经制定了正式的规范,和对一致性机制的安全性证明。这个正式规范使用 TLA+ 规范语言使图 2 中总结的信息非常清晰。它长约400行,并作为证明的主题。同时对于任何想实现 Raft 的人也是十分有用的。我们通过 TLA 证明系统非常机械的证明了日志完全特性。然而,这个证明依赖的约束前提还没有被机械证明(例如,我们还没有证明规范的类型安全)。而且,我们已经写了一个非正式的证明关于状态机安全性是完备的,并且是相当清晰的(大约 3500 个词)。 9.3 性能Raft 和其他一致性算法例如 Paxos 有着差不多的性能。在性能方面,最重要的关注点是,当领导人被选举成功时,什么时候复制新的日志条目。Raft 通过很少数量的消息包(一轮从领导人到集群大多数机器的消息)就达成了这个目的。同时,进一步提升 Raft 的性能也是可行的。例如,很容易通过支持批量操作和管道操作来提高吞吐量和降低延迟。对于其他一致性算法已经提出过很多性能优化方案;其中有很多也可以应用到 Raft 中来,但是我们暂时把这个问题放到未来的工作中去。 我们使用我们自己的 Raft 实现来衡量 Raft 领导人选举的性能并且回答两个问题。首先,领导人选举的过程收敛是否快速?第二,在领导人宕机之后,最小的系统宕机时间是多久? 图 16:发现并替换一个已经崩溃的领导人的时间。上面的图考察了在选举超时时间上的随机化程度,下面的图考察了最小选举超时时间。每条线代表了 1000 次实验(除了 150-150 毫秒只试了 100 次),和相应的确定的选举超时时间。例如,150-155 毫秒意思是,选举超时时间从这个区间范围内随机选择并确定下来。这个实验在一个拥有 5 个节点的集群上进行,其广播时延大约是 15 毫秒。对于 9 个节点的集群,结果也差不多。 为了衡量领导人选举,我们反复的使一个拥有五个节点的服务器集群的领导人宕机,并计算需要多久才能发现领导人已经宕机并选出一个新的领导人(见图 16)。为了构建一个最坏的场景,在每一的尝试里,服务器都有不同长度的日志,意味着有些候选人是没有成为领导人的资格的。另外,为了促成选票瓜分的情况,我们的测试脚本在终止领导人之前同步的发送了一次心跳广播(这大约和领导人在崩溃前复制一个新的日志给其他机器很像)。领导人均匀的随机的在心跳间隔里宕机,也就是最小选举超时时间的一半。因此,最小宕机时间大约就是最小选举超时时间的一半。 图 16 中上面的图表明,只需要在选举超时时间上使用很少的随机化就可以大大避免选票被瓜分的情况。在没有随机化的情况下,在我们的测试里,选举过程往往都需要花费超过 10 秒钟由于太多的选票瓜分的情况。仅仅增加 5 毫秒的随机化时间,就大大的改善了选举过程,现在平均的宕机时间只有 287 毫秒。增加更多的随机化时间可以大大改善最坏情况:通过增加 50 毫秒的随机化时间,最坏的完成情况(1000 次尝试)只要 513 毫秒。 图 16 中下面的图显示,通过减少选举超时时间可以减少系统的宕机时间。在选举超时时间为 12-24 毫秒的情况下,只需要平均 35 毫秒就可以选举出新的领导人(最长的一次花费了 152 毫秒)。然而,进一步降低选举超时时间的话就会违反 Raft 的时间不等式需求:在选举新领导人之前,领导人就很难发送完心跳包。这会导致没有意义的领导人改变并降低了系统整体的可用性。我们建议使用更为保守的选举超时时间,比如 150-300 毫秒;这样的时间不大可能导致没有意义的领导人改变,而且依然提供不错的可用性。 10 相关工作已经有很多关于一致性算法的工作被发表出来,其中很多都可以归到下面的类别中: Lamport 关于 Paxos 的原始描述,和尝试描述的更清晰。 关于 Paxos 的更详尽的描述,补充遗漏的细节并修改算法,使得可以提供更加容易的实现基础。 实现一致性算法的系统,例如 Chubby,ZooKeeper 和 Spanner。对于 Chubby 和 Spanner 的算法并没有公开发表其技术细节,尽管他们都声称是基于 Paxos 的。ZooKeeper 的算法细节已经发表,但是和 Paxos 着实有着很大的差别。 Paxos 可以应用的性能优化。 Oki 和 Liskov 的 Viewstamped Replication(VR),一种和 Paxos 差不多的替代算法。原始的算法描述和分布式传输协议耦合在了一起,但是核心的一致性算法在最近的更新里被分离了出来。VR 使用了一种基于领导人的方法,和 Raft 有很多相似之处。 Raft 和 Paxos 最大的不同之处就在于 Raft 的强领导特性:Raft 使用领导人选举作为一致性协议里必不可少的部分,并且将尽可能多的功能集中到了领导人身上。这样就可以使得算法更加容易理解。例如,在 Paxos 中,领导人选举和基本的一致性协议是正交的:领导人选举仅仅是性能优化的手段,而且不是一致性所必须要求的。但是,这样就增加了多余的机制:Paxos 同时包含了针对基本一致性要求的两阶段提交协议和针对领导人选举的独立的机制。相比较而言,Raft 就直接将领导人选举纳入到一致性算法中,并作为两阶段一致性的第一步。这样就减少了很多机制。 像 Raft 一样,VR 和 ZooKeeper 也是基于领导人的,因此他们也拥有一些 Raft 的优点。但是,Raft 比 VR 和 ZooKeeper 拥有更少的机制因为 Raft 尽可能的减少了非领导人的功能。例如,Raft 中日志条目都遵循着从领导人发送给其他人这一个方向:附加条目 RPC 是向外发送的。在 VR 中,日志条目的流动是双向的(领导人可以在选举过程中接收日志);这就导致了额外的机制和复杂性。根据 ZooKeeper 公开的资料看,它的日志条目也是双向传输的,但是它的实现更像 Raft。 和上述我们提及的其他基于一致性的日志复制算法中,Raft 的消息类型更少。例如,我们数了一下 VR 和 ZooKeeper 使用的用来基本一致性需要和成员改变的消息数(排除了日志压缩和客户端交互,因为这些都比较独立且和算法关系不大)。VR 和 ZooKeeper 都分别定义了 10 中不同的消息类型,相对的,Raft 只有 4 中消息类型(两种 RPC 请求和对应的响应)。Raft 的消息都稍微比其他算法的要信息量大,但是都很简单。另外,VR 和 ZooKeeper 都在领导人改变时传输了整个日志;所以为了能够实践中使用,额外的消息类型就很必要了。 Raft 的强领导人模型简化了整个算法,但是同时也排斥了一些性能优化的方法。例如,平等主义 Paxos (EPaxos)在某些没有领导人的情况下可以达到很高的性能。平等主义 Paxos 充分发挥了在状态机指令中的交换性。任何服务器都可以在一轮通信下就提交指令,除非其他指令同时被提出了。然而,如果指令都是并发的被提出,并且互相之间不通信沟通,那么 EPaxos 就需要额外的一轮通信。因为任何服务器都可以提交指令,所以 EPaxos 在服务器之间的负载均衡做的很好,并且很容易在 WAN 网络环境下获得很低的延迟。但是,他在 Paxos 上增加了非常明显的复杂性。 一些集群成员变换的方法已经被提出或者在其他的工作中被实现,包括 Lamport 的原始的讨论,VR 和 SMART。我们选择使用共同一致的方法因为他对一致性协议的其他部分影响很小,这样我们只需要很少的一些机制就可以实现成员变换。Lamport 的基于 α 的方法之所以没有被 Raft 选择是因为它假设在没有领导人的情况下也可以达到一致性。和 VR 和 SMART 相比较,Raft 的重新配置算法可以在不限制正常请求处理的情况下进行;相比较的,VR 需要停止所有的处理过程,SMART 引入了一个和 α 类似的方法,限制了请求处理的数量。Raft 的方法同时也需要更少的额外机制来实现,和 VR、SMART 比较而言。 11 结论算法的设计通常会把正确性,效率或者简洁作为主要的目标。尽管这些都是很有意义的目标,但是我们相信,可理解性也是一样的重要。在开发者把算法应用到实际的系统中之前,这些目标没有一个会被实现,这些都会必然的偏离发表时的形式。除非开发人员对这个算法有着很深的理解并且有着直观的感觉,否则将会对他们而言很难在实现的时候保持原有期望的特性。 在这篇论文中,我们尝试解决分布式一致性问题,但是一个广为接受但是十分令人费解的算法 Paxos 已经困扰了无数学生和开发者很多年了。我们创造了一种新的算法 Raft,显而易见的比 Paxos 要容易理解。我们同时也相信,Raft 也可以为实际的实现提供坚实的基础。把可理解性作为设计的目标改变了我们设计 Raft 的方式;随着设计的进展,我们发现自己重复使用了一些技术,比如分解问题和简化状态空间。这些技术不仅提升了 Raft 的可理解性,同时也使我们坚信其正确性。 12 感谢这项研究必须感谢以下人员的支持:Ali Ghodsi,David Mazie`res,和伯克利 CS 294-91 课程、斯坦福 CS 240 课程的学生。Scott Klemmer 帮我们设计了用户调查,Nelson Ray 建议我们进行统计学的分析。在用户调查时使用的关于 Paxos 的幻灯片很大一部分是从 Lorenzo Alvisi 的幻灯片上借鉴过来的。特别的,非常感谢 DavidMazieres 和 Ezra Hoch,他们找到了 Raft 中一些难以发现的漏洞。许多人提供了关于这篇论文十分有用的反馈和用户调查材料,包括 Ed Bugnion,Michael Chan,Hugues Evrard,Daniel Giffin,Arjun Gopalan,Jon Howell,Vimalkumar Jeyakumar,Ankita Kejriwal,Aleksandar Kracun,Amit Levy,Joel Martin,Satoshi Matsushita,Oleg Pesok,David Ramos,Robbert van Renesse,Mendel Rosenblum,Nicolas Schiper,Deian Stefan,Andrew Stone,Ryan Stutsman,David Terei,Stephen Yang,Matei Zaharia 以及 24 位匿名的会议审查人员(可能有重复),并且特别感谢我们的领导人 Eddie Kohler。Werner Vogels 发了一条早期草稿链接的推特,给 Raft 带来了极大的关注。我们的工作由 Gigascale 系统研究中心和 Multiscale 系统研究中心给予支持,这两个研究中心由关注中心研究程序资金支持,一个是半导体研究公司的程序,由 STARnet 支持,一个半导体研究公司的程序由 MARCO 和 DARPA 支持,在国家科学基金会的 0963859 号批准,并且获得了来自 Facebook,Google,Mellanox,NEC,NetApp,SAP 和 Samsung 的支持。Diego Ongaro 由 Junglee 公司,斯坦福的毕业团体支持。 本文经TopJohn授权转自TopJohn’s Blog 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"共识","slug":"共识","permalink":"https://learnblockchain.cn/categories/共识/"}],"tags":[{"name":"一致性算法","slug":"一致性算法","permalink":"https://learnblockchain.cn/tags/一致性算法/"}]},{"title":"理解去中心化 稳定币 DAI","slug":"understand_dai","date":"2019-03-19T12:44:58.000Z","updated":"2019-04-05T10:29:31.022Z","comments":true,"path":"2019/03/19/understand_dai/","link":"","permalink":"https://learnblockchain.cn/2019/03/19/understand_dai/","excerpt":"随着摩根大通推出JPM Coin 稳定币,可以预见稳定币将成为区块链落地的一大助推器。坦白来讲,对于一个程序员的我来讲(不懂一点专业经济和金融),理解DAI的机制,真的有一点复杂。耐心看完,必有收获。","text":"随着摩根大通推出JPM Coin 稳定币,可以预见稳定币将成为区块链落地的一大助推器。坦白来讲,对于一个程序员的我来讲(不懂一点专业经济和金融),理解DAI的机制,真的有一点复杂。耐心看完,必有收获。 为什么需要稳定币如果一个货币其价值时刻在剧烈波动,就无法作为一个日常支付和交易的货币,谁也无法承担今天发的工资,第二天就跌掉了三分之一。 在币价高度不稳定时,在不退出加密货币市场的情况下,稳定币就可以提供价值保值。 通常发行稳定币的方式是通过资产担保来发行,像USDT、TUSD等就是通过美元资产来担保发行等额稳定币,如银行存款1亿美元就发行1亿USDT, 既通过锚定法币来实行稳定性。 USDT 因审计不公开,经常被质疑超发,如1亿美元担保发行1.5亿USDT,就会导致0.5亿USDT无法兑换美元。这也是为什么在监管下发行的稳定币,如TUSD、GUSD有逐步取代USDT的趋势。 本文的主角 DAI 同样是通过资产抵押发行, DAI 是通过抵押数字资产发行,去中心化发行。 注意加粗的两个关键字抵押数字资产和去中心化,它是用一套称之为Maker的智能合约发行的,其背后的团队为MakerDAO。 Maker目前只支持抵押ETH,后面可能会加入其它代币。DAO (Decentralized Autonomous Organization): 去中心化的自治组织 我们都知道数字资产的价值是有很大波动的, 那么Maker怎么来确保 1 DAI = 1 USD的呢? 稳定币 DAI的发行Maker体系中有一个实现了抵押贷款逻辑的智能合约(CDP), 当我们抵押(发送)ETH到智能合约,合约根据当时ETH的价值,计算一个折扣后,发行对应的DAI(符合ERC20标准的代币)。 以太价格获取Maker采用的是中心化方案,从各大交易所获取再加权平均。 为了方便理解,类比抵押屋产贷款,我们把房子作为抵押品向银行贷款,ETH就相当于房子,智能合约相当于银行,DAI 相当于贷款拿到的钱。银行给我们贷款时,银行也会对房子的价值打一个折扣。 这个折扣在Maker系统中称之为抵押率,这是一个很重要的概念,大家务必理解。 我们给他一个数学定义: 抵押率 = 抵押物的价值 / 放贷的价值。 如果房子价值200万,抵押率为200%, 银行就只能给我们贷款100万,这个大家应该能够理解。同样,假设以太币现在价值200美元,抵押率为200%,那么把1个以太币(200美元)发送到CDP智能合约,就可以获得发行的100个DAI。 在抵押ETH生成DAI的同时,合约会为我们生成一张CDP借贷凭证,它记录着借贷关系及金额,并且抵押ETH会一直锁定在合约里,在还清100个DAI时,ETH将归还我们。 就像银行扣押房子直到我们还清贷款一样。 到这里,DAI的发行应该明白了。 套现保值DAI的这种抵押贷款逻辑非常有意思 ,它生成的CDP借贷凭证提供给我们一个套现保值的手段。假如你有一大笔以太在手里, 而你又急需一笔资金怎么办? 那么抵押生成DAI是获得资金的一个绝佳选择。如果在交易所把币卖掉换成稳定币,会失去以太的所有权,币价上涨时就无法换回对应的以太。 例如:目前 ETH 价格约为 130 美元, 按200%的抵押率, 1000个以太可以抵押生成6.5万个DAI,即可以获得6.5万美元资金,假设一年之后,ETH价格涨到到500 美元,只需要偿还6.5万个DAI(美元)及一点利息就可以赎回1000个以太(价值50万美元)。 DAI是如何保持稳定的?依靠抵押美元发行的USDT、TUSD,能保持价值相对稳定很容易理解,靠抵押ETH的DAI如何保持稳定呢? 分两种情况:如果 ETH 升值, 意味着 DAI 有更足够的抵押(更高的抵押率,担保更充足),这不会有太大影响。如果DAI的交易价格超过1美元,Maker也会激励用户创造更多的DAI(目标利率反馈机制)。 目标利率反馈机制(TRFM):不过最重要的是以下几点:当DAI的交易价格超过1美元时,智能合约会激励人们生成DAI。当DAI的交易价格不到1美元时,智能合约会激励人们赎返DAI。 如果 ETH 价值下降则复杂一些,回到抵押屋产贷款的类比,如果我们的房子价值下降,银行会要求我们追加抵押物或及时还款,Maker也是一样,始终要求DAI是超额抵押的。 如果资产下跌到一定值(如抵押率150%),并且原抵押人没有追加抵押物或偿还(部分)DAI,合约会自动启动清算(liquidated),之前抵押的以太币被拍卖,直到从CDP合约借出的DAI被还清。 还是前面的类比,价值200万房子,抵押率200%,贷款了100万,在房子下跌到150万时,银行就会拍卖房子,清除这笔贷款。 Maker也是使用这种方式从市面上回购DAI用来偿还给CDP。 简单总结:Maker始终要求DAI是超额抵押的,当系统发现有部分资产存在风险时,就会对风险过高的资产进行清算,它会首先清算抵押率低于 150% 的CDP借贷凭证,而为了防止清算持有人必须往CDP借贷凭证存入更多ETH或偿还DAI来提高抵押率。 现在我们来看 MakerDao抵押借款的界面就清晰了,以下截图是抵押1 ETH 生成60个DAI: Collateralization ratio 抵押率为 228%, Liquidation price 清算价格为90 美金。 清算关于清算也许还有几点需要了解: 在发生清算后, 就再也无法通过偿还DAI来取回之前抵押的ETH了(CDP借贷凭证会关闭)。 清算发生时,会扣除一部分的罚金(13%的罚金)和手续费。 拍卖ETH得到的DAI 会被销毁, 就像用户偿还DAI 被销毁一样。 拍卖偿还DAI后, 剩余的资产用户可以拿回。 Maker系统中有一个专门负责清算的合约。 MKR 应对暴跌上面一有一个前提,不管如果 DAI 都是超额抵押, 如果以太价格急剧下跌,抵押品的价值达不到借出的DAI的价值时,这时启动清算,将由Mkr持有者负责回购。 Mkr 是Maker系统中的权益代币, Mkr持有者是系统的收益者,获取借款利息及罚金等。 还是前面的类比,如果价值200万房子, 突然跌倒100万以下, 这时候在公开市场拍卖,市场是没有买家出100万以上购买房子的,那么银行将启用自有资金回购。 相当于损失的价值转嫁到Mkr持有者,价格波动是没发消灭的,它只能转移,DAI的价格波动性实际由CDP 借贷凭证持有者和Mkr持有者共同承担。 一点拓展DAI 由于它的超额抵押借款机制,是一个很好的杠杆做多工具。 如果我们预期以太币会上涨,我们可以把前面1000个以太抵押生成6.5万个DAI,再此购买以太进行抵押,多次操作之后,可能获得数倍的增值。 为了写这边文章,拓展我不少金融领域知识,以前一直不理解做多做空(因为我不炒股、不炒币),现在把我的理解做一个记录,供参考: 做多做多就是看好其上涨而买入,杠杆做多则是借钱买入。上面就是借DAI(美元)买入以太,借来的6.5万个DAI(美元),按130美元一个以太,可以购买到500个以太,如果一个月后以太涨到200美元, 500个以太就是10万美元,还掉6.5万美元后,相当于凭空赚了3.5万美元。 做空做空就是认为其下跌而卖出,同样也可以借别人的卖出。现在130美元一个以太,我认为以太会下跌到100美元,于是我向交易所借了1000个币卖掉获得13万美元,如果真下跌到100美元,就用10万美元换1000个币还给交易所,这样我凭空赚了3万美元。 参考文章:DAI 白皮书DAI 稳定币的通俗解释 如果你对稳定币感兴趣,我们可以一起交流,我的微信:xlbxiong 备注:稳定币。 加入知识星球,和一群优秀的区块链从业者一起学习。深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"稳定币","slug":"稳定币","permalink":"https://learnblockchain.cn/tags/稳定币/"},{"name":"DAI","slug":"DAI","permalink":"https://learnblockchain.cn/tags/DAI/"}]},{"title":"如何开发一款以太坊(安卓)钱包系列2 - 导入账号及账号管理","slug":"eth-wallet-dev-2","date":"2019-03-18T03:27:50.000Z","updated":"2019-04-05T10:29:31.010Z","comments":true,"path":"2019/03/18/eth-wallet-dev-2/","link":"","permalink":"https://learnblockchain.cn/2019/03/18/eth-wallet-dev-2/","excerpt":"这是如何开发一款以太坊(安卓)钱包系列第2篇,如何导入账号。有时用户可能已经有一个账号,这篇文章接来介绍下,如何实现导入用户已经存在的账号。","text":"这是如何开发一款以太坊(安卓)钱包系列第2篇,如何导入账号。有时用户可能已经有一个账号,这篇文章接来介绍下,如何实现导入用户已经存在的账号。 导入账号预备知识从用户需求上来讲,导入用户已经存在的账号是有必要的。 不过从安全性考虑,当你之前使用的是一个非官方、非开源的钱包产品时(尤其是小众钱包),或者之前没有对私钥、助记词、Keysotre文件小心保存时。正确的做法是提示用户: 在新的钱包重新创建一个钱包账号、并安全备份(因为之前的可能已经不安全); 然后在老钱包里把所有的币转移到新账号。 导入账号有3种方式: 通过私钥导入 通过KeyStore 导入 通过助记词导入 通过私钥导入账号关键是用用户输入的私钥创建一个椭圆曲线秘钥对,然后用这个秘钥对创建钱包,代码如下:(代码在代码库中的app/src/pro/upchain/wallet/utils/ETHWalletUtils.java文件中) 12345public static ETHWallet loadWalletByPrivateKey(String privateKey, String pwd) { Credentials credentials = null; ECKeyPair ecKeyPair = ECKeyPair.create(Numeric.toBigInt(privateKey)); return generateWallet(generateNewWalletName(), pwd, ecKeyPair);} 返回语句中的 generateWallet(),在系列1-通过助记词创建账号 已经介绍过,通过椭圆曲线秘钥对创建钱包。 loadWalletByPrivateKey()中第2个参数密码pwd,在私钥生成账号这个过程并不需要pwd,它是用来加密保存私钥,即为了生成keystore文件。 通过KeyStore文件导入账号关于KeyStore文件,不了解的可以阅读下账号Keystore文件导入导出。 关键步骤: KeyStore 文本内容解析WalletFile实例; 使用密码 解码 WalletFile 生成椭圆曲线秘钥对创建钱包。 12345678910111213141516/** * @param keystore 原json文件内容 * @param pwd keystore解密密码 * @return */public static ETHWallet loadWalletByKeystore(String keystore, String pwd) { try { WalletFile walletFile = null; walletFile = objectMapper.readValue(keystore, WalletFile.class); return generateWallet(generateNewWalletName(), pwd, Wallet.decrypt(pwd, walletFile)); } catch (IOException e) { } catch (CipherException e) { } return null;} 通过助记词导入账号导入和上一篇中,创建非常相似,不同的是,种子由用户提供的助记词生成。 使用助记词导入账号时,还需要用户选择(或输入)一个推倒路径(参考BIP44),关键步骤是: 通过助记词创建随机数种子; 通过 种子 + 路径 派生生成私钥 创建钱包 ; 12345678910111213141516171819202122232425/** * 通过导入助记词,导入钱包 * * @param path bip44路径 * @param list 助记词 * @param pwd 密码 * @return */public static ETHWallet importMnemonic(String path, String mnemonic, String pwd) { List<String> list = Arrays.asList(mnemonic.split(\" \")); if (!path.startsWith(\"m\") && !path.startsWith(\"M\")) { //参数非法 return null; } String[] pathArray = path.split(\"/\"); if (pathArray.length <= 1) { //内容不对 return null; } String passphrase = \"\"; long creationTimeSeconds = System.currentTimeMillis() / 1000; DeterministicSeed ds = new DeterministicSeed(list, null, passphrase, creationTimeSeconds); return generateWalletByMnemonic(generateNewWalletName(), ds, pathArray, pwd);} generateWalletByMnemonic在上一篇中已经介绍过, 账号存储(保存到数据库)很多同学肯定已经注意到, 不管通过什么方式构造的账号,都会最终构造为一个ETHWallet 钱包对象,他的定义如下: 1234567891011121314@Entitypublic class ETHWallet { @Id(autoincrement = true) private Long id; public String address; private String name; private String password; // 经过加密后的pwd private String keystorePath; private String mnemonic; private boolean isCurrent; // 是否是当前选中的钱包 private boolean isBackup; // 是否备份过} 前面构造的ETHWallet是只存在于内容之中, 在应用程序退出之后,这个数据将丢失, 因此我们需要把它序列化到序列化数据库中存储起来,在下一次进入应用的时候加载数据库还原出账号。 greenDAOgreenDAO 是一个将对象映射到 SQLite 数据库中的轻量且快速的 ORM 解决方案,以下是一个greenDAO的作用示意图: 这里我们也使用了 greenDAO 来把ETHWallet对象映射到 SQLite 数据库, greenDAO的用法这里只简单说明,不详细阐述,大家可以跟随官方提供的introduction 和 how-to-get-started。 对象映射保存把ETHWallet映射的到数据库,需要给类加上@Entity注解,这样greenDAO会生成几个类:DaoMaster、DaoSession及 ETHWalletDao 帮我们完成构建数据库表等操作。 在使用ETHWalletDao插入到数据库之前需要先进行一个初始化,通常初始化放在应用程序入口中进行,如:pro.upchain.wallet.UpChainWalletApp的onCreate()中执行,初始化代码如下: 123456protected void init() { DaoMaster.DevOpenHelper mHelper = new DaoMaster.DevOpenHelper(this, \"wallet\", null); SQLiteDatabase db = mHelper.getWritableDatabase(); DaoSession daoSession = new DaoMaster(db).newSession(); ETHWalletDao ethWalletDao = daoSession.getETHWalletDao();} 有了greenDAO为我们生成的辅助类,插入到数据库就很简单了,一行代码: 1ethWalletDao.insert(ethWallet); // ethWallet为ETHWallet实例, 前面不管是新创建还是导入的账号都会构造这样一个实例。 多账号管理考虑到用户可能会创建多个账号,因此需要确定一个当前选定的账号,一般情况下,用户新创建的账号应该作为当前选中的的账号,同时其他账号应该取消选中, 我们完善下账号存储逻辑, 如下:(代码在代码库中的app/src/pro/upchain/wallet/utils/WalletDaoUtils.java文件中) 123456789101112131415161718192021222324252627282930313233/** * 插入新创建钱包 * * @param ethWallet 钱 */public static void insertNewWallet(ETHWallet ethWallet) { updateCurrent(-1); // 取消其他站账号选中状态 ethWallet.setCurrent(true); ethWalletDao.insert(ethWallet);}/** * 更新选中钱包 * * @param id 钱包ID */public static ETHWallet updateCurrent(long id) {// 加载所有钱包账号 List<ETHWallet> ethWallets = ethWalletDao.loadAll(); ETHWallet currentWallet = null; for (ETHWallet ethwallet : ethWallets) { if (id != -1 && ethwallet.getId() == id) { ethwallet.setCurrent(true); currentWallet = ethwallet; } else { ethwallet.setCurrent(false); } ethWalletDao.update(ethwallet); } return currentWallet;} 打通账号创建与保存以通过私钥导入账号进行保存为例,把创建账号和保存账号打通,这里我们使用响应式编程 ReactiveX,这部分作为订阅者福利,发表在我的小专栏,趁还未涨价,赶紧订阅吧,超值的! 下一篇 将继续介绍资产的显示。 学习资料 RxAndroid 了解更多响应式编程 introduction 和 how-to-get-started 了解greenDAO。 我创建了一个专门讨论钱包开发的微信群,加微信:xlbxiong 备注:钱包。 加入知识星球,和一群优秀的区块链从业者一起学习。深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"钱包","slug":"钱包","permalink":"https://learnblockchain.cn/tags/钱包/"},{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"}]},{"title":"技术工坊 第11期","slug":"dev_meeting_11","date":"2019-03-16T10:47:23.000Z","updated":"2019-04-05T11:37:02.812Z","comments":true,"path":"2019/03/16/dev_meeting_11/","link":"","permalink":"https://learnblockchain.cn/2019/03/16/dev_meeting_11/","excerpt":"","text":"分享嘉宾:杨尉 先河系统CTO 话题简介:Filecoin 基础及开发网络实战 话题大纲: Filecoin的定义及设计目的 Filecoin与IPFS的关系 Filecoin网络中的角色 Filecoin核心概念理解 Filecoin开发网络实战 点击下载PPT 或收听音频 分享嘉宾:温国兵 SuperONE COO 话题简介:EOS 钱包,DApp 超级入口 话题大纲: EOS 公链简介 EOS 钱包概览 EOS 钱包技术架构 DApp 如何接入 EOS 钱包 EOS DApp 生态 点击下载PPT或收听音频 分享嘉宾:谭长生 DLB数链产品经理 话题简介:大数据+区块链项目遇到的那些事儿话题大纲: 大数据+区块链的业态进化 目前大数据产业面临的挑战 DLB如何建立一个普惠新生态 点击下载PPT","categories":[{"name":"技术工坊","slug":"dev-meeting","permalink":"https://learnblockchain.cn/categories/dev-meeting/"}],"tags":[]},{"title":"如何开发一款以太坊(安卓)钱包系列1 - 通过助记词创建账号","slug":"eth_wallet_dev_1","date":"2019-03-13T08:56:50.000Z","updated":"2019-04-05T10:29:31.051Z","comments":true,"path":"2019/03/13/eth_wallet_dev_1/","link":"","permalink":"https://learnblockchain.cn/2019/03/13/eth_wallet_dev_1/","excerpt":"上周我开源了一款钱包,反映很好,一周时间不到已经快到100 Star。接下来我会几篇系列文章把开发以太坊钱包的核心要点写出来,也算是对代码的一个解读。","text":"上周我开源了一款钱包,反映很好,一周时间不到已经快到100 Star。接下来我会几篇系列文章把开发以太坊钱包的核心要点写出来,也算是对代码的一个解读。 写在前面本钱包是基于Android安卓平台开发,使用的是原生语言 Java 编写, 是基于Java 1.8 版本,其中使用了Java 1.8 中一些较新的语言特性,如 Lambda表达式等;另外还较多使用了ReactiveX/RxAndroid响应式编程用法。 在本系列文章中,重点是介绍以太坊钱包账号、交易等逻辑,有时可能会假定读者已经了解以太坊及Android开发等相关知识,因为这些内容不是文章的重点,因此不会过多介绍,请海涵。 \b钱包包含的功能通常一个钱包会包含以下功能: 支持通过生成助记词、Keystore文件、私钥 创建钱包账号。 支持导出钱包账号助记词、私钥、Keystore文件。 支持多个钱包账号管理 账户余额查询及转账功能(二维码扫描支持)。 支持ERC20 代币(余额显示、转账、代币币价显示) 支持用法币(美元和人民币)实时显示币价。 历史交易列表显示 创建账号预备知识我们先来介绍第一个功能:通过生成助记词、Keystore文件、私钥创建钱包账号。本系列中,钱包都是指分层确定性钱包,(HD钱包 Hierarchical Deterministic Wallets), 之前博客有一篇文章分层钱包进行了详细的介绍,还不熟悉的可以读一下。为了保持本文的完整,这里做一个总结性回顾:以太坊及比特币的地址是由随机生成的私钥经过椭圆曲线等算法单向推倒而来 ,BIP32及BIP44是为方便管理私钥提出的分层推倒方案,BIP39 定义助记词让分层种子的备份更方便。而KeyStore文件是用来解密以太坊保存私钥的一种方式,大家可以阅读下这篇文章: 账号Keystore文件导入导出了解更多。 实现完成的,界面如下图: 这是一张导入钱包账号的截图(导入和创建,其实原理一样),界面仿照ImToken,不过本文将不会介绍UI部分的编写。 Web3j & bitcoinj为了完成创建账号功能,我们需要使用到两个库:Web3j 和 bitcoinj Web3是一套和以太坊通信的封装库,Web3j是Java版本的实现,例如发起交易和智能合约进行交互,下图很好的表达了其作用。 不过本文中的功能,主要是使用了web3j中椭圆曲线加密及KeyStore文件的生成与解密。 bitcoinj 的功能和web3类似,它是比特币协议的Java实现,他实现了 BIP32、BIP44及BIP39 相关协议。 Android使用Gradle来构建,直接在app/build.gradle文件中加入:12implementation 'org.web3j:core:4.1.0-android'implementation 'org.bitcoinj:bitcoinj-core:0.14.7' 提示: 实践中遇到的一个问题,由于bitcoinj 中引入了 com.lambdaworks:scrypt加密库, 它包含的lib/x86_64/darwin/libscrypt.dylib文件,会导致在进行Android App Bundle 编译时会出现错误(好像也会导致某些机型没法安装),解决办法是在 build.gradle 加入一下语句,把这个文件在打包时排除掉。packagingOptions { exclude ‘lib/x86_64/darwin/libscrypt.dylib’} 创建账号实现通过助记词常见钱包账号这是目前钱包客户端,最常见的一种为用户常见账号的方式,这里会包含一下几个核心步骤: 生成一个随机数种子; 通过随机数种子得到助记词; 通过 种子 + 路径 派生生成私钥; 使用KeyStore保存私钥; 私钥推倒出账号地址。 大家可以在再次阅读分层钱包,理解为何这么做的原因。 理解了上面几点,那么代码就容易明白了,代码在代码库中的app/src/pro/upchain/wallet/utils/ETHWalletUtils.java中,关键代码逻辑如下: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091// 创建钱包对象入口函数public static ETHWallet generateMnemonic(String walletName, String pwd) { String[] pathArray = \"m/44'/60'/0'/0/0\".split(\"/\"); long creationTimeSeconds = System.currentTimeMillis() / 1000; SecureRandom secureRandom = SecureRandomUtils.secureRandom(); DeterministicSeed ds = new DeterministicSeed(secureRandom, 128, \"\", creationTimeSeconds); return generateWalletByMnemonic(walletName, ds, pathArray, pwd);}/** * @param walletName 钱包名称 * @param ds 助记词加密种子 * @param pathArray 助记词标准 * @param pwd 密码 * @return */@Nullablepublic static ETHWallet generateWalletByMnemonic(String walletName, DeterministicSeed ds, String[] pathArray, String pwd) { //种子 byte[] seedBytes = ds.getSeedBytes(); //助记词 List<String> mnemonic = ds.getMnemonicCode(); if (seedBytes == null) return null; // 衍生推倒key DeterministicKey dkKey = HDKeyDerivation.createMasterPrivateKey(seedBytes); for (int i = 1; i < pathArray.length; i++) { ChildNumber childNumber; if (pathArray[i].endsWith(\"'\")) { int number = Integer.parseInt(pathArray[i].substring(0, pathArray[i].length() - 1)); childNumber = new ChildNumber(number, true); } else { int number = Integer.parseInt(pathArray[i]); childNumber = new ChildNumber(number, false); } dkKey = HDKeyDerivation.deriveChildKey(dkKey, childNumber); } ECKeyPair keyPair = ECKeyPair.create(dkKey.getPrivKeyBytes()); ETHWallet ethWallet = generateWallet(walletName, pwd, keyPair); if (ethWallet != null) { ethWallet.setMnemonic(convertMnemonicList(mnemonic)); } return ethWallet;}// 通过椭圆曲线秘钥对创建钱包 @Nullableprivate static ETHWallet generateWallet(String walletName, String pwd, ECKeyPair ecKeyPair) { WalletFile keyStoreFile; try { //最后两个参数: n 是 CPU/Memory 开销值,越高的开销值,计算就越困难。r 表示块大小,p 表示并行度 keyStoreFile = Wallet.create(pwd, ecKeyPair, 1024, 1); // WalletUtils. .generateNewWalletFile(); } catch (Exception e) { e.printStackTrace(); return null; } BigInteger publicKey = ecKeyPair.getPublicKey(); String s = publicKey.toString(); String wallet_dir = AppFilePath.Wallet_DIR; String keystorePath = \"keystore_\" + walletName + \".json\"; File destination = new File(wallet_dir, \"keystore_\" + walletName + \".json\"); //目录不存在则创建目录,创建不了则报错 if (!createParentDir(destination)) { return null; } try { objectMapper.writeValue(destination, keyStoreFile); } catch (IOException e) { e.printStackTrace(); return null; } ETHWallet ethWallet = new ETHWallet(); ethWallet.setName(walletName); ethWallet.setAddress(Keys.toChecksumAddress(keyStoreFile.getAddress())); ethWallet.setKeystorePath(destination.getAbsolutePath()); ethWallet.setPassword(Md5Utils.md5(pwd)); return ethWallet;} 上述代码中,generateMnemonic()是入口函数,最终返回的是一个ETHWallet 自定义的钱包实体类,一个实例就对应一个钱包,ETHWallet保存了钱包相关的属性,后面会详细介绍,如果对它序列化保存钱包账号及多个钱包账号管理。 几个注意事项关于助记词及私钥的保存,有几点要特别注意,否则有可能和其他钱包无法兼容或导致私钥泄漏。 这部分作为订阅者福利,发表在我的小专栏,趁还未涨价,赶紧订阅吧,超值的! 参考文档 web3j API 文档 web3j GitHub bitcoinj 介绍及文档 我创建了一个专门讨论钱包开发的微信群,加微信:xlbxiong 备注:钱包。 加入最专业的区块链问答社区,和一群优秀的区块链从业者一起学习。深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"钱包","slug":"钱包","permalink":"https://learnblockchain.cn/tags/钱包/"},{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"},{"name":"助记词","slug":"助记词","permalink":"https://learnblockchain.cn/tags/助记词/"},{"name":"web3j","slug":"web3j","permalink":"https://learnblockchain.cn/tags/web3j/"}]},{"title":"filecoin技术架构分析系列文章 - 目录","slug":"filecoin-code-analysis-0","date":"2019-03-11T07:35:33.000Z","updated":"2019-04-05T10:29:30.992Z","comments":true,"path":"2019/03/11/filecoin-code-analysis-0/","link":"","permalink":"https://learnblockchain.cn/2019/03/11/filecoin-code-analysis-0/","excerpt":"我是杨尉,先河系统CTO,欢迎大加关注的的Github: waynewyang,因为工作需要,在FileCoin开源后,从源码层对filecoin的架构进行了一下分析,整理了一些文章,希望对大家有所帮助。","text":"我是杨尉,先河系统CTO,欢迎大加关注的的Github: waynewyang,因为工作需要,在FileCoin开源后,从源码层对filecoin的架构进行了一下分析,整理了一些文章,希望对大家有所帮助。 目录 1 filecoin概念 2 filecoin通用语言理解 3 filecoin开发网使用 4 filecoin源码顶层架构分析 5 filecoin源码协议层分析之心跳协议 6 filecoin源码协议层分析之hello握手协议 7 filecoin源码协议层分析之存储协议 8 filecoin源码协议层分析之检索协议 9 filecoin源码分析之支撑包分析(1) 10 filecoin源码分析之支撑包分析(2/2) 11 filecoin源码分析之内部接口层api包分析 12 filecoin源码分析之内部接口层plumbing&porcelain接口 13 filecoin源码分析之服务层actor及vm 14 filecoin源码分析之服务层链同步、共识协议及挖矿 15 filecoin源码分析之节点运行逻辑","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"filecoin技术架构分析之十五:filecoin源码分析之节点运行逻辑","slug":"filecoin-code-analysis-15","date":"2019-03-10T08:35:33.000Z","updated":"2019-04-05T10:29:31.007Z","comments":true,"path":"2019/03/10/filecoin-code-analysis-15/","link":"","permalink":"https://learnblockchain.cn/2019/03/10/filecoin-code-analysis-15/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第十五章源码分析之节点运行逻辑。","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第十五章源码分析之节点运行逻辑。 分析基于的源码版本:go-filecoin master a0598a54(2019年3月9日) 前提 我们在前面的章节已经经过了三个阶段的梳理分析 概念阶段,包括概念、通用语言理解、开发网络使用 顶层架构与概念的结合理解 具体源码的简析,包括协议层、支撑包、内部api层、服务层 源码部分的command部分比较容易理解,就不单独文章赘述了,基本与内部api层都可以对应起来 现在再来看节点的运行逻辑应该会更加清晰了 filecoin节点运行逻辑简析基本数据结构123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164▼ package node▶ imports▼ variables +ErrNoMinerAddress -filecoinDHTProtocol : dhtprotocol.ID -log // 创建具体的filecoin节点实例▼+Config : struct [fields] // 设置区块时间 +BlockTime : time.Duration // 配置节点是否转发 +IsRelay : bool // libp2p选项 +Libp2pOpts : []libp2p.Option // 在离线模式下,会关闭libp2p +OfflineMode : bool // 配置资源 +Repo : repo.Repo // 配置区块奖励方法 +Rewarder : consensus.BlockRewarder // 配置节点时空证明校验函数 +Verifier : proofs.Verifier [methods] // 创建node实例 +Build(ctx context.Context) : *Node, error -buildHost(ctx context.Context, makeDHT func(host host.Host) routing.IpfsRouting, error) : host.Host, error +ConfigOpt : func(*Config) error▼+Node : struct [fields] // 确认最新区块,本地持久化并广播 +AddNewlyMinedBlock : newBlockFunc // 订阅主题"/fil/blocks" +BlockSub : pubsub.Subscription // 块服务接口 +Blockstore : bstore.Blockstore // 维持相关节点连接 +Bootstrapper : *net.Bootstrapper // 读取区块信息 +ChainReader : chain.ReadStore // 同时协议 +Consensus : consensus.Protocol // 块交换,节点间的数据交换 +Exchange : exchange.Interface // new-head 主题 +HeaviestTipSetCh : chan interface{} // 新区块处理请求 +HeaviestTipSetHandled : func() // hello服务 +HelloSvc : *hello.Handler // 消息订阅 +MessageSub : pubsub.Subscription // 挖矿调度 +MiningScheduler : mining.Scheduler // 消息池操作 +MsgPool : *core.MessagePool // 离线模式 +OfflineMode : bool +OnlineStore : *hamt.CborIpldStore // 对应libp2p中的host +PeerHost : host.Host // libp2p中的ping service +Ping : *ping.PingService // 高层api +PorcelainAPI : *porcelain.API // 功率表 +PowerTable : consensus.PowerTableView // 配置资源 +Repo : repo.Repo // 检索客户端 +RetrievalClient : *retrieval.Client // 检索矿工 +RetrievalMiner : *retrieval.Miner // 路由,libp2p +Router : routing.IpfsRouting // 存储矿工 +StorageMiner : *storage.Miner // 存储客户 +StorageMinerClient : *storage.Client // 链同步 +Syncer : chain.Syncer // 钱包管理 +Wallet : *wallet.Wallet -blockTime : time.Duration -blockservice : bserv.BlockService -cancelMining : context.CancelFunc -cancelSubscriptionsCtx : context.CancelFunc -cborStore : *hamt.CborIpldStore -host : host.Host -lookup : lookup.PeerLookupService -mining -miningCtx : context.Context -miningDoneWg : *sync.WaitGroup -sectorBuilder : sectorbuilder.SectorBuilder [methods] +BlockHeight() : *types.BlockHeight, error +BlockService() : bserv.BlockService +CborStore() : *hamt.CborIpldStore +ChainReadStore() : chain.ReadStore // 创建矿工方法 +CreateMiner(ctx context.Context, accountAddr address.Address, gasPrice types.AttoFIL, gasLimit types.GasUnits, pledge uint64, pid libp2ppeer.ID, collateral *types.AttoFIL) : *address.Address, error +GetBlockTime() : time.Duration +Host() : host.Host // 节点查找方法 +Lookup() : lookup.PeerLookupService +MiningSignerAddress() : address.Address +MiningTimes() : time.Duration, time.Duration // 创建新的account地址,钱包地址 +NewAddress() : address.Address, error +SectorBuilder() : sectorbuilder.SectorBuilder +SetBlockTime(blockTime time.Duration) // 启动节点 +Start(ctx context.Context) : error // 启动挖矿 +StartMining(ctx context.Context) : error // 停止节点 +Stop(ctx context.Context) // 停止挖矿 +StopMining(ctx context.Context) -addNewlyMinedBlock(ctx context.Context, b *types.Block) -cancelSubscriptions() -getLastUsedSectorID(ctx context.Context, minerAddr address.Address) : uint64, error -getMinerActorPubKey() : []byte, error -handleNewHeaviestTipSet(ctx context.Context, head types.TipSet) -handleNewMiningOutput(miningOutCh chan mining.Output) -handleSubscription(ctx context.Context, f pubSubProcessorFunc, fname string, s pubsub.Subscription, sname string) -isMining() : bool -miningAddress() : address.Address, error -miningOwnerAddress(ctx context.Context, miningAddr address.Address) : address.Address, error -saveMinerConfig(minerAddr address.Address, signerAddr address.Address) : error -setIsMining(isMining bool) -setupHeartbeatServices(ctx context.Context) : error -setupMining(ctx context.Context) : error [functions] // 调用Build创建node实例 +New(ctx context.Context, opts ...ConfigOpt) : *Node, error▼-blankValidator : struct [methods] +Select(_ string, _ [][]byte) : int, error +Validate(_ string, _ []byte) : error -newBlockFunc : func(context.Context, *types.Block) -pubSubProcessorFunc : func(ctx context.Context, msg pubsub.Message) error▼ functions +BlockTime(blockTime time.Duration) : ConfigOpt +IsRelay() : ConfigOpt +Libp2pOptions(opts ...libp2p.Option) : ConfigOpt +OfflineMode(offlineMode bool) : ConfigOpt +RewarderConfigOption(rewarder consensus.BlockRewarder) : ConfigOpt +StartMining(ctx context.Context, node *Node) : error +VerifierConfigOption(verifier proofs.Verifier) : ConfigOpt -initSectorBuilderForNode(ctx context.Context, node *Node, sectorStoreType proofs.SectorStoreType) : sectorbuilder.SectorBuilder, error -initStorageMinerForNode(ctx context.Context, node *Node) : *storage.Miner, error -readGenesisCid(ds datastore.Datastore) : cid.Cid, error 创建filecoin节点实例 实例化filecoin节点,简析见如下添加的注释 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173// Build instantiates a filecoin Node from the settings specified in the config.func (nc *Config) Build(ctx context.Context) (*Node, error) { // 创建内存资源实例 if nc.Repo == nil { nc.Repo = repo.NewInMemoryRepo() } // 创建块服务实例 bs := bstore.NewBlockstore(nc.Repo.Datastore()) validator := blankValidator{} var peerHost host.Host var router routing.IpfsRouting // 带宽统计实例,加入libp2popts bandwidthTracker := p2pmetrics.NewBandwidthCounter() nc.Libp2pOpts = append(nc.Libp2pOpts, libp2p.BandwidthReporter(bandwidthTracker)) // 非离线模式才启用libp2p if !nc.OfflineMode { makeDHT := func(h host.Host) (routing.IpfsRouting, error) { r, err := dht.New( ctx, h, dhtopts.Datastore(nc.Repo.Datastore()), dhtopts.NamespacedValidator("v", validator), dhtopts.Protocols(filecoinDHTProtocol), ) if err != nil { return nil, errors.Wrap(err, "failed to setup routing") } router = r return r, err } var err error // 实例化非离线模式libp2p host peerHost, err = nc.buildHost(ctx, makeDHT) if err != nil { return nil, err } } else { // 离线模式处理 router = offroute.NewOfflineRouter(nc.Repo.Datastore(), validator) peerHost = rhost.Wrap(noopLibP2PHost{}, router) } // ping服务实例 // set up pinger pinger := ping.NewPingService(peerHost) // bitswap实例 // set up bitswap nwork := bsnet.NewFromIpfsHost(peerHost, router) //nwork := bsnet.NewFromIpfsHost(innerHost, router) bswap := bitswap.New(ctx, nwork, bs) bservice := bserv.New(bs, bswap) cstOnline := hamt.CborIpldStore{Blocks: bservice} cstOffline := hamt.CborIpldStore{Blocks: bserv.New(bs, offline.Exchange(bs))} // 获取创世块cid genCid, err := readGenesisCid(nc.Repo.Datastore()) if err != nil { return nil, err } // chain.Store实例以及功率表 var chainStore chain.Store = chain.NewDefaultStore(nc.Repo.ChainDatastore(), &cstOffline, genCid) powerTable := &consensus.MarketView{} // 共识协议processor实例 var processor consensus.Processor if nc.Rewarder == nil { processor = consensus.NewDefaultProcessor() } else { processor = consensus.NewConfiguredProcessor(consensus.NewDefaultMessageValidator(), nc.Rewarder) } // 共识协议实例 var nodeConsensus consensus.Protocol if nc.Verifier == nil { nodeConsensus = consensus.NewExpected(&cstOffline, bs, processor, powerTable, genCid, &proofs.RustVerifier{}) } else { nodeConsensus = consensus.NewExpected(&cstOffline, bs, processor, powerTable, genCid, nc.Verifier) } // 链同步,链读取,消息池实例 // only the syncer gets the storage which is online connected chainSyncer := chain.NewDefaultSyncer(&cstOnline, &cstOffline, nodeConsensus, chainStore) chainReader, ok := chainStore.(chain.ReadStore) if !ok { return nil, errors.New("failed to cast chain.Store to chain.ReadStore") } msgPool := core.NewMessagePool() // Set up libp2p pubsub fsub, err := libp2pps.NewFloodSub(ctx, peerHost) if err != nil { return nil, errors.Wrap(err, "failed to set up pubsub") } // 钱包服务实例 backend, err := wallet.NewDSBackend(nc.Repo.WalletDatastore()) if err != nil { return nil, errors.Wrap(err, "failed to set up wallet backend") } fcWallet := wallet.New(backend) // 实例化高层api PorcelainAPI := porcelain.New(plumbing.New(&plumbing.APIDeps{ Chain: chainReader, Config: cfg.NewConfig(nc.Repo), Deals: strgdls.New(nc.Repo.DealsDatastore()), MsgPool: msgPool, MsgPreviewer: msg.NewPreviewer(fcWallet, chainReader, &cstOffline, bs), MsgQueryer: msg.NewQueryer(nc.Repo, fcWallet, chainReader, &cstOffline, bs), MsgSender: msg.NewSender(fcWallet, chainReader, msgPool, consensus.NewOutboundMessageValidator(), fsub.Publish), MsgWaiter: msg.NewWaiter(chainReader, bs, &cstOffline), Network: net.New(peerHost, pubsub.NewPublisher(fsub), pubsub.NewSubscriber(fsub), net.NewRouter(router), bandwidthTracker), SigGetter: mthdsig.NewGetter(chainReader), Wallet: fcWallet, })) // 实例化node nd := &Node{ blockservice: bservice, Blockstore: bs, cborStore: &cstOffline, OnlineStore: &cstOnline, Consensus: nodeConsensus, ChainReader: chainReader, Syncer: chainSyncer, PowerTable: powerTable, PorcelainAPI: PorcelainAPI, Exchange: bswap, host: peerHost, MsgPool: msgPool, OfflineMode: nc.OfflineMode, PeerHost: peerHost, Ping: pinger, Repo: nc.Repo, Wallet: fcWallet, blockTime: nc.BlockTime, Router: router, } // Bootstrapping network peers. periodStr := nd.Repo.Config().Bootstrap.Period period, err := time.ParseDuration(periodStr) if err != nil { return nil, errors.Wrapf(err, "couldn't parse bootstrap period %s", periodStr) } // 实例化Bootstrapper,指定node的该方法 // Bootstrapper maintains connections to some subset of addresses ba := nd.Repo.Config().Bootstrap.Addresses bpi, err := net.PeerAddrsToPeerInfos(ba) if err != nil { return nil, errors.Wrapf(err, "couldn't parse bootstrap addresses [%s]", ba) } minPeerThreshold := nd.Repo.Config().Bootstrap.MinPeerThreshold nd.Bootstrapper = net.NewBootstrapper(bpi, nd.Host(), nd.Host().Network(), nd.Router, minPeerThreshold, period) // 实例化链查找服务,指定node的该方法 // On-chain lookup service defaultAddressGetter := func() (address.Address, error) { return nd.PorcelainAPI.GetAndMaybeSetDefaultSenderAddress() } nd.lookup = lookup.NewChainLookupService(nd.ChainReader, defaultAddressGetter, bs) return nd, nil} 启动及停止filecoin节点 启动filecoin节点的流程概览 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182// Start boots up the node.func (node *Node) Start(ctx context.Context) error { // 加载本地chain信息 if err := node.ChainReader.Load(ctx); err != nil { return err } // 如果存在存储矿工,配置挖矿功能 // Only set these up if there is a miner configured. if _, err := node.miningAddress(); err == nil { if err := node.setupMining(ctx); err != nil { log.Errorf("setup mining failed: %v", err) return err } } // 设置链同步回调函数 // Start up 'hello' handshake service syncCallBack := func(pid libp2ppeer.ID, cids []cid.Cid, height uint64) { // TODO it is possible the syncer interface should be modified to // make use of the additional context not used here (from addr + height). // To keep things simple for now this info is not used. err := node.Syncer.HandleNewBlocks(context.Background(), cids) if err != nil { log.Infof("error handling blocks: %s", types.NewSortedCidSet(cids...).String()) } } // 实例化hello握手协议 node.HelloSvc = hello.New(node.Host(), node.ChainReader.GenesisCid(), syncCallBack, node.ChainReader.Head) // 实例化存储矿工协议 cni := storage.NewClientNodeImpl(dag.NewDAGService(node.BlockService()), node.Host(), node.GetBlockTime()) var err error node.StorageMinerClient, err = storage.NewClient(cni, node.PorcelainAPI) if err != nil { return errors.Wrap(err, "Could not make new storage client") } // 实例化检索客户及检索矿工协议 node.RetrievalClient = retrieval.NewClient(node) node.RetrievalMiner = retrieval.NewMiner(node) // 订阅区块通知 // subscribe to block notifications blkSub, err := node.PorcelainAPI.PubSubSubscribe(BlockTopic) if err != nil { return errors.Wrap(err, "failed to subscribe to blocks topic") } node.BlockSub = blkSub // 订阅消息通知 // subscribe to message notifications msgSub, err := node.PorcelainAPI.PubSubSubscribe(msg.Topic) if err != nil { return errors.Wrap(err, "failed to subscribe to message topic") } node.MessageSub = msgSub cctx, cancel := context.WithCancel(context.Background()) node.cancelSubscriptionsCtx = cancel // 启用新线程订阅区块及消息主题,设置handle回调 go node.handleSubscription(cctx, node.processBlock, "processBlock", node.BlockSub, "BlockSub") go node.handleSubscription(cctx, node.processMessage, "processMessage", node.MessageSub, "MessageSub") // 启用新线程处理新的tipset事件 node.HeaviestTipSetHandled = func() {} node.HeaviestTipSetCh = node.ChainReader.HeadEvents().Sub(chain.NewHeadTopic) go node.handleNewHeaviestTipSet(cctx, node.ChainReader.Head()) // 非离线模式启动bootstapper服务 if !node.OfflineMode { node.Bootstrapper.Start(context.Background()) } // 启动心跳服务 if err := node.setupHeartbeatServices(ctx); err != nil { return errors.Wrap(err, "failed to start heartbeat services") } return nil} 停止filecoin节点的流程概览 释放资源,停止相关服务 12345678910111213141516171819202122232425262728293031323334// Stop initiates the shutdown of the node.func (node *Node) Stop(ctx context.Context) { node.ChainReader.HeadEvents().Unsub(node.HeaviestTipSetCh) // 停止挖矿 node.StopMining(ctx) // 取消订阅 node.cancelSubscriptions() // 停止链读取服务 node.ChainReader.Stop() // 停止密封服务 if node.SectorBuilder() != nil { if err := node.SectorBuilder().Close(); err != nil { fmt.Printf("error closing sector builder: %s\\n", err) } node.sectorBuilder = nil } // 关闭host实例 if err := node.Host().Close(); err != nil { fmt.Printf("error closing host: %s\\n", err) } // 关闭资源实例 if err := node.Repo.Close(); err != nil { fmt.Printf("error closing repo: %s\\n", err) } // 关闭bootstqpper实例 node.Bootstrapper.Stop() fmt.Println("stopping filecoin :(")} 启动及停止挖矿 启动挖矿 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160// StartMining causes the node to start feeding blocks to the mining worker and initializes// the SectorBuilder for the mining address.func (node *Node) StartMining(ctx context.Context) error { // 如果在挖矿中,退出 if node.isMining() { return errors.New("Node is already mining") } // 获取矿工地址 minerAddr, err := node.miningAddress() if err != nil { return errors.Wrap(err, "failed to get mining address") } // 确保密封服务实例存在 // ensure we have a sector builder if node.SectorBuilder() == nil { if err := node.setupMining(ctx); err != nil { return err } } // 获取地址 minerOwnerAddr, err := node.miningOwnerAddress(ctx, minerAddr) minerSigningAddress := node.MiningSignerAddress() if err != nil { return errors.Wrapf(err, "failed to get mining owner address for miner %s", minerAddr) } blockTime, mineDelay := node.MiningTimes() // 实例化挖矿调度服务 if node.MiningScheduler == nil { getStateFromKey := func(ctx context.Context, tsKey string) (state.Tree, error) { tsas, err := node.ChainReader.GetTipSetAndState(ctx, tsKey) if err != nil { return nil, err } return state.LoadStateTree(ctx, node.CborStore(), tsas.TipSetStateRoot, builtin.Actors) } getState := func(ctx context.Context, ts types.TipSet) (state.Tree, error) { return getStateFromKey(ctx, ts.String()) } getWeight := func(ctx context.Context, ts types.TipSet) (uint64, error) { parent, err := ts.Parents() if err != nil { return uint64(0), err } // TODO handle genesis cid more gracefully if parent.Len() == 0 { return node.Consensus.Weight(ctx, ts, nil) } pSt, err := getStateFromKey(ctx, parent.String()) if err != nil { return uint64(0), err } return node.Consensus.Weight(ctx, ts, pSt) } getAncestors := func(ctx context.Context, ts types.TipSet, newBlockHeight *types.BlockHeight) ([]types.TipSet, error) { return chain.GetRecentAncestors(ctx, ts, node.ChainReader, newBlockHeight, consensus.AncestorRoundsNeeded, consensus.LookBackParameter) } processor := consensus.NewDefaultProcessor() worker := mining.NewDefaultWorker(node.MsgPool, getState, getWeight, getAncestors, processor, node.PowerTable, node.Blockstore, node.CborStore(), minerAddr, minerOwnerAddr, minerSigningAddress, node.Wallet, blockTime) node.MiningScheduler = mining.NewScheduler(worker, mineDelay, node.ChainReader.Head) } // paranoid check // 启动挖矿服务 if !node.MiningScheduler.IsStarted() { node.miningCtx, node.cancelMining = context.WithCancel(context.Background()) outCh, doneWg := node.MiningScheduler.Start(node.miningCtx) node.miningDoneWg = doneWg node.AddNewlyMinedBlock = node.addNewlyMinedBlock node.miningDoneWg.Add(1) go node.handleNewMiningOutput(outCh) } // initialize a storage miner // 初始化存储矿工 storageMiner, err := initStorageMinerForNode(ctx, node) if err != nil { return errors.Wrap(err, "failed to initialize storage miner") } node.StorageMiner = storageMiner // loop, turning sealing-results into commitSector messages to be included // in the chain // 新开线程处理,1 密封完成处理;2 接受停止挖矿消息 go func() { for { select { // 密封完成处理 case result := <-node.SectorBuilder().SectorSealResults(): if result.SealingErr != nil { log.Errorf("failed to seal sector with id %d: %s", result.SectorID, result.SealingErr.Error()) } else if result.SealingResult != nil { // TODO: determine these algorithmically by simulating call and querying historical prices gasPrice := types.NewGasPrice(0) gasUnits := types.NewGasUnits(300) val := result.SealingResult // This call can fail due to, e.g. nonce collisions. Our miners existence depends on this. // We should deal with this, but MessageSendWithRetry is problematic. _, err := node.PorcelainAPI.MessageSend( node.miningCtx, minerOwnerAddr, minerAddr, nil, gasPrice, gasUnits, "commitSector", val.SectorID, val.CommD[:], val.CommR[:], val.CommRStar[:], val.Proof[:], ) if err != nil { log.Errorf("failed to send commitSector message from %s to %s for sector with id %d: %s", minerOwnerAddr, minerAddr, val.SectorID, err) continue } node.StorageMiner.OnCommitmentAddedToChain(val, nil) } // 挖矿取消 case <-node.miningCtx.Done(): return } } }() // schedules sealing of staged piece-data // 定时密封阶段性的碎片数据 if node.Repo.Config().Mining.AutoSealIntervalSeconds > 0 { go func() { for { select { // 取消 case <-node.miningCtx.Done(): return // 定时密封 case <-time.After(time.Duration(node.Repo.Config().Mining.AutoSealIntervalSeconds) * time.Second): log.Info("auto-seal has been triggered") if err := node.SectorBuilder().SealAllStagedSectors(node.miningCtx); err != nil { log.Errorf("scheduler received error from node.SectorBuilder.SealAllStagedSectors (%s) - exiting", err.Error()) return } } } }() } else { log.Debug("auto-seal is disabled") } // 设置微挖矿状态 node.setIsMining(true) return nil} 停止挖矿 12345678910111213141516// StopMining stops mining on new blocks.func (node *Node) StopMining(ctx context.Context) { node.setIsMining(false) // 取消挖矿 if node.cancelMining != nil { node.cancelMining() } // 等待执行中的挖矿任务完成后结束 if node.miningDoneWg != nil { node.miningDoneWg.Wait() } // TODO: stop node.StorageMiner} 阶段性分析结束说明 至此笔者针对go-filecoin部分的分析快告一个小的段落了 文章因为时间的关系,书面出来只是将关键部分书面表达出来,更多的像是笔者的一个分析笔记,但是我相信对于想分析源码的朋友有一定帮助 后面会抽空补充一章总结,笔者在第4章中有提到过,薄读->厚读->再薄读,我们还需要一次薄读,来加深我们对filecoin的认识。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"filecoin技术架构分析十四:filecoin源码分析之服务层链同步、共识协议及挖矿","slug":"filecoin-code-analysis-14","date":"2019-03-09T08:35:33.000Z","updated":"2019-04-05T10:29:31.017Z","comments":true,"path":"2019/03/09/filecoin-code-analysis-14/","link":"","permalink":"https://learnblockchain.cn/2019/03/09/filecoin-code-analysis-14/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第十四章源码分析之服务层链同步、共识协议及挖矿。","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第十四章源码分析之服务层链同步、共识协议及挖矿。 分析基于的源码版本:go-filecoin master a0598a54(2019年3月9日) chain同步基础结构 TipIndex 定义定义了tipset的基础结构及方法 12345678910111213141516171819202122232425262728293031▼ package chain▼+TipIndex : struct [fields] -mu : sync.Mutex // 根据id来获取tipset及其状态根 -tsasByID : tsasByTipSetID // 根据父块来获取tipset及其状态根 -tsasByParentsAndHeight : map[string]tsasByTipSetID [methods] // 根据id来获取tipset及其状态根 +Get(tsKey string) : *TipSetAndState, error // 根据父块来获取tipset及其状态根 +GetByParentsAndHeight(pKey string, h uint64) : []*TipSetAndState, error // 根据Id判断是否有此tipset +Has(tsKey string) : bool // 根据父块判断是否有此tipset +HasByParentsAndHeight(pKey string, h uint64) : bool // 设置tipset和状态根 +Put(tsas *TipSetAndState) : error [functions] +NewTipIndex() : *TipIndex▼+TipSetAndState : struct [fields] // tipset +TipSet : types.TipSet // 相当于区块的root cid +TipSetStateRoot : cid.Cid 链同步 chain同步的接口定义 1234567891011location: chain/syncer.go▼ package chain▼+Syncer : interface [methods] // 处理新区块的接口定义 +HandleNewBlocks(ctx context.Context, blkCids []cid.Cid) : error 具体接口实现在location: chain/defalut_syncer.go中 特殊情况的错误 12345location: chain/reorg.go // 如果当前区块头不包含在最新的区块头之上时候,会报此错误▼ functions +IsReorg(curHead types.TipSet, newChain []types.TipSet) : bool 链存储 其中 Readstore是一个通用接口 Store的设计基本是给ChainSync使用的 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748location: chain/store.go▼ package chain▼ constants // 用于发布新的区块头的主题"new-head" +NewHeadTopic▼ variables // 创世块的key +GenesisKey▼+ReadStore : interface [methods] // 获取历史区块,通过channel实现 +BlockHistory(ctx context.Context, tips types.TipSet) : chan interface{} // 获取创世区块cid +GenesisCid() : cid.Cid // 通过cid获取具体的block +GetBlock(ctx context.Context, id cid.Cid) : *types.Block, error // 通过cid获取具体的block +GetTipSetAndState(ctx context.Context, tsKey string) : *TipSetAndState, error // 获取最新区块 +Head() : types.TipSet // 最新区块变更事件 +HeadEvents() : *pubsub.PubSub // 最新合约状态 +LatestState(ctx context.Context) : state.Tree, error // 加载chain +Load(ctx context.Context) : error // 停止 +Stop() // 这个接口只是chain同步使用▼+Store : interface [embedded] +ReadStore [methods] +GetBlocks(ctx context.Context, ids types.SortedCidSet) : []*types.Block, error +GetTipSetAndStatesByParentsAndHeight(ctx context.Context, pTsKey string, h uint64) : []*TipSetAndState, error +HasAllBlocks(ctx context.Context, cs []cid.Cid) : bool +HasBlock(ctx context.Context, c cid.Cid) : bool +HasTipSetAndState(ctx context.Context, tsKey string) : bool +HasTipSetAndStatesWithParentsAndHeight(ctx context.Context, pTsKey string, h uint64) : bool // 存储并更新最新区块信息 +PutTipSetAndState(ctx context.Context, tsas *TipSetAndState) : error +SetHead(ctx context.Context, s types.TipSet) : error consensus 主要功能 提供创建选票方法,验证中奖选票方法,确定最终的tipset 将合法的tipset消息取出,生效actor状态 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364▼ package consensus▶ imports▼ constants +ECPrM : uint64 +ECV : uint64 +LookBackParameter▼ variables +AncestorRoundsNeeded +ErrInvalidBase +ErrStateRootMismatch +ErrUnorderedTipSets -log -ticketDomain : *big.Int // Expected实现EC共识▼+Expected : struct [fields] // 全局功率表 +PwrTableView : PowerTableView -bstore : blockstore.Blockstore -cstore : *hamt.CborIpldStore -genesisCid : cid.Cid -processor : Processor -verifier : proofs.Verifier [methods] // 比较两个tipset的权重 +IsHeavier(ctx context.Context, a, b types.TipSet, aSt, bSt state.Tree) : bool, error // 建立新的tipset +NewValidTipSet(ctx context.Context, blks []*types.Block) : types.TipSet, error // 运行状态转换 // 1 新区块到来的时候出发状态转换(chain sync逻辑) // 2 进入后判断tipset的有效性,包括验证选票是否中奖 // 3 逐一执行消息,切换状态 +RunStateTransition(ctx context.Context, ts types.TipSet, ancestors []types.TipSet, pSt state.Tree) : state.Tree, error // 计算tipset权重 +Weight(ctx context.Context, ts types.TipSet, pSt state.Tree) : uint64, error -runMessages(ctx context.Context, st state.Tree, vms vm.StorageMap, ts types.TipSet, ancestors []types.TipSet) : state.Tree, error -validateBlockStructure(ctx context.Context, b *types.Block) : error -validateMining(ctx context.Context, st state.Tree, ts types.TipSet, parentTs types.TipSet) : error▼+Processor : interface // 会被RunStateTransition间接掉用,进行状态切换(生效挖矿成功的tipset消息) [methods] // 从tipset中逐一取出block处理 +ProcessBlock(ctx context.Context, st state.Tree, vms vm.StorageMap, blk *types.Block, ancestors []types.TipSet) : []*ApplicationResult, error +ProcessTipSet(ctx context.Context, st state.Tree, vms vm.StorageMap, ts types.TipSet, ancestors []types.TipSet) : *ProcessTipSetResponse, error▼ functions // 与白皮书描述一致,按照存储功率出块,用以判断是否中奖 +CompareTicketPower(ticket types.Signature, minerPower uint64, totalPower uint64) : bool // 产生随机挑战种子,针对时空证明 +CreateChallengeSeed(parents types.TipSet, nullBlkCount uint64) : proofs.PoStChallengeSeed, error // 生成选票 // 用上一个区块的时空证明+矿工地址(目前直接用的矿工地址,issue1054讨论中) 生成256bit哈希 +CreateTicket(proof proofs.PoStProof, minerAddr address.Address) : []byte // 判断是否中奖,调用CompareTicketPower +IsWinningTicket(ctx context.Context, bs blockstore.Blockstore, ptv PowerTableView, st state.Tree, ticket types.Signature, miner address.Address) : bool, error // 实例化Expected +NewExpected(cs *hamt.CborIpldStore, bs blockstore.Blockstore, processor Processor, pt PowerTableView, gCid cid.Cid, verifier proofs.Verifier) : Protocol -init() mining挖矿的主要逻辑 1 不能将空块最为基准块 2 基于上一个Tipset信息(如果上一个为空块,必须找到空块之前高度最高的Tipset,并记录中间空块数据)和空块数母生成合法的时空证明挑战参数 3 生成时空证明 4 时空证明成功,调用共识协议创建奖票 5 如果奖票中奖,将未打包的消息打包区块 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980location: mining/working//这里是挖矿逻辑的真正入口// Mine implements the DefaultWorkers main mining function..// The returned bool indicates if this miner created a new block or not.func (w *DefaultWorker) Mine(ctx context.Context, base types.TipSet, nullBlkCount int, outCh chan<- Output) bool { log.Info("Worker.Mine") ctx = log.Start(ctx, "Worker.Mine") defer log.Finish(ctx) // 不能将空块作为基准块挖矿 if len(base) == 0 { log.Warning("Worker.Mine returning because it can't mine on an empty tipset") outCh <- Output{Err: errors.New("bad input tipset with no blocks sent to Mine()")} return false } st, err := w.getStateTree(ctx, base) if err != nil { log.Errorf("Worker.Mine couldn't get state tree for tipset: %s", err.Error()) outCh <- Output{Err: err} return false } log.Debugf("Mining on tipset: %s, with %d null blocks.", base.String(), nullBlkCount) if ctx.Err() != nil { log.Warningf("Worker.Mine returning with ctx error %s", ctx.Err().Error()) return false } // 基于上一个基准Tipset以及空块数目生成Post随机挑战参数 challenge, err := consensus.CreateChallengeSeed(base, uint64(nullBlkCount)) if err != nil { outCh <- Output{Err: err} return false } // 生成时空证明 prCh := createProof(challenge, w.createPoSTFunc) var proof proofs.PoStProof var ticket []byte select { case <-ctx.Done(): log.Infof("Mining run on base %s with %d null blocks canceled.", base.String(), nullBlkCount) return false case prChRead, more := <-prCh: if !more { log.Errorf("Worker.Mine got zero value from channel prChRead") return false } copy(proof[:], prChRead[:]) // 时空证明成功,调用共识协议创建奖票 ticket = consensus.CreateTicket(proof, w.minerAddr) } // TODO: Test the interplay of isWinningTicket() and createPoSTFunc() // https://github.com/filecoin-project/go-filecoin/issues/1791 // 调用共识协议确认是否中奖 weHaveAWinner, err := consensus.IsWinningTicket(ctx, w.blockstore, w.powerTable, st, ticket, w.minerAddr) if err != nil { log.Errorf("Worker.Mine couldn't compute ticket: %s", err.Error()) outCh <- Output{Err: err} return false } if weHaveAWinner { // 如果中奖将打包消息,生成区块 next, err := w.Generate(ctx, base, ticket, proof, uint64(nullBlkCount)) if err == nil { log.SetTag(ctx, "block", next) log.Debugf("Worker.Mine generates new winning block! %s", next.Cid().String()) } outCh <- NewOutput(next, err) return true } return false} 其他细节源码简析 消息队列(交易消息集)的处理 123456789101112131415161718192021222324252627282930313233location: mining/mqueue.go▼ package mining▶ imports▼+MessageQueue : struct [fields] -senderQueues : queueHeap [methods] // 取出消息切片,即多条消息 +Drain() : []*types.SignedMessage +Empty() : bool // 从队列取出一条消息 +Pop() : *types.SignedMessage, bool [functions] // 实例化消息队列 +NewMessageQueue(msgs []*types.SignedMessage) : MessageQueue -nonceQueue : []*types.SignedMessage // 一些队列的基本操作 // 1 长度、push、pop功能 // 2 Less主要是比较两条交易中的Gas价格,大家可以回头看看type中的消息定义,这里不赘述了 // 3 为什么要提供Less接口,留给大家思索一下,熟悉以太坊的可能一眼就看出了▼-queueHeap : []nonceQueue [methods] +Len() : int +Less(i, j int) : bool +Pop() : interface{} +Push(x interface{}) +Swap(i, j int) 调度器 入口 node实例会调用NewScheduler创建相关实例并启动挖矿 1234567891011121314151617181920212223242526272829303132▼ package mining▶ imports▼ constants +MineDelayConversionFactor▼-timingScheduler : struct [fields] -isStarted : bool -mineDelay : time.Duration // 查找权重最高的Tipset -pollHeadFunc : func() types.TipSet // 底层的挖矿逻辑,在下面会分析Worker -worker : Worker [methods] // 判断是否启动挖矿 +IsStarted() : bool // 启动挖矿 +Start(miningCtx context.Context) : chan Output, *sync.WaitGroup▼+Scheduler : interface [methods] +IsStarted() : bool +Start(miningCtx context.Context) : chan Output, *sync.WaitGroup▼ functions +MineOnce(ctx context.Context, w Worker, md time.Duration, ts types.TipSet) : Output, error // 实例化timingScheduler +NewScheduler(w Worker, md time.Duration, f func() types.TipSet) : Scheduler -nextNullBlkCount(prevNullBlkCount int, prevBase, currBase types.TipSet) : int 打包区块 具体见如下注释,可对应此查阅源码。 1234567891011121314location: mining/block_generate.go▼ package mining▶ imports▼ DefaultWorker* : ctype [methods] // 1 如果节点没有产生过有效存储,无法参与挖矿 // 2 计算区块高度= 基准Tipset高度+空块数目 // 3 取出未打包消息,调用vm执行,生成收据,并更新状态 // 4 打包区块信息,返回 +Generate(ctx context.Context, baseTipSet types.TipSet, ticket types.Signature, proof proofs.PoStProof, nullBlockCount uint64) : *types.Block, error 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"filecoin技术架构分析十三:filecoin源码分析之服务层actor及vm","slug":"filecoin-code-analysis-13","date":"2019-03-08T08:35:33.000Z","updated":"2019-04-05T10:29:31.035Z","comments":true,"path":"2019/03/08/filecoin-code-analysis-13/","link":"","permalink":"https://learnblockchain.cn/2019/03/08/filecoin-code-analysis-13/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第十三章源码分析之服务层actor及vm。","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第十三章源码分析之服务层actor及vm。 说明 分析源代码版本:master 2c87fd59(2019.3.7) 回头看第三章开发网使用中创建矿工,提交订单,支付等操作实际上都是actor的新增及状态改变 当前的实现vm还不具备通用abi数据的解释执行能力,未达到真正智能合约水平 exec(actor及vm的接口定义) 说明 提供可执行actor的最小接口要求 ExecutableActor (由actor及具体actor包实现) 提供actor键值存取接口定义 Lookup (由actor包实现) 提供状态临时存储的接口定义 Storage (由vm.Storage实现) actor的执行环境接口定义 VMContext (由vm.context实现) 具体源码注释如下 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283▼ package exec▶ imports▼ constants +ErrDanglingPointer +ErrDecode +ErrInsufficientGas +ErrStaleHead▼ variables +Errors▼+Error : string [methods] +Error() : string +ExportedFunc : func(ctx VMContext) []byte, uint8, error // actor符号集合▼+Exports : map[string]*FunctionSignature [methods] // 判断是否存在特定方法 +Has(method string) : bool // 对于单个函数的符号表 // todo中的事情:需要转换为非go类型▼+FunctionSignature : struct [fields] +Params : []abi.Type +Return : []abi.Type // 可执行合约接口,这是每一类型的合约必须实现的最小接口 // 包括account,miner,storagemarket,paymentbroker▼+ExecutableActor : interface [methods] +Exports() : Exports +InitializeState(storage Storage, initializerData interface{}) : error // 由actor.lookup实现键值存储 (actor/storage.go)▼+Lookup : interface [methods] +Commit(ctx context.Context) : cid.Cid, error +Delete(ctx context.Context, k string) : error +Find(ctx context.Context, k string) : interface{}, error +IsEmpty() : bool +Set(ctx context.Context, k string, v interface{}) : error +Values(ctx context.Context) : []*hamt.KV, error // 由vm.Storage实现 // 解决持久化的问题,有副本防止回滚机制 // 具体实现还有Flush持久化到datastore功能▼+Storage : interface [methods] // 提交最新actor Head +Commit(cid.Cid, cid.Cid) : error // 如下都为内存中操作 +Get(cid.Cid) : []byte, error +Head() : cid.Cid +Put(interface{}) : cid.Cid, error // actor的abi执行环境接口,由vm.context实现▼+VMContext : interface [methods] // 创建新的合约地址 +AddressForNewActor() : address.Address, error // 查询区块高度 +BlockHeight() : *types.BlockHeight // Gas收费 +Charge(cost types.GasUnits) : error // 创建合约 +CreateNewActor(addr address.Address, code cid.Cid, initalizationParams interface{}) : error // 判断是否为account类型的Actor +IsFromAccountActor() : bool // 合约中交易信息 +Message() : *types.Message // 执行合约函数 +Send(to address.Address, method string, value *types.AttoFIL, params []interface{}) : [][]byte, uint8, error +Storage() : Storage // 当Storage接口完成会删除如下两项 +ReadStorage() : []byte, error +WriteStorage(interface{}) : error actor的类型及源码分析 actor包定义及实现了基础actor,此外filecoin还定义了四种内置的actor类型 存储市场actor,此类actor整个网络只有一个实例,用于创建存储矿工、更新功率表、获取总存储容量 Miner actor,此类actor整个网络只有多个实例(随用户数增加而增加),用于执行矿工相关的操作 paymentbroker actor,此类actor整个网络只有一个实例,用于创建支付通道以及支付相关信息 account actor,账户actor,此类actor整个网络只有多个实例(随用户数增加而增加),只能用于基本的转账操作 ### 基础actor包 说明 定义了actor的基础结构,其中code如果使用内置的如上四种actor,他们的值都是固定的 提供了actor的基础操作方法 见笔者在代码中的注释 1234567891011121314151617181920212223242526272829303132333435363738394041424344location: actor/actor.go▼ package actor // Actor可以理解为合约或者账户,转账操作要检查code cid合法性▼+Actor : struct [fields] //余额 +Balance : *types.AttoFIL // 合约代码的cid,vm具体执行其对应的代码 // 1 具体代码的cid // 2 在go语言实现的四种特定合约,这个字段是常量,比如account,miner,storagemarket,paymentbroker +Code : cid.Cid // 合约状态的最新状态 +Head : cid.Cid // 防止重放攻击而设置的参数 +Nonce : types.Uint64 [methods] // 计算actor的cid +Cid() : cid.Cid, error // 打印合约信息 +Format(f fmt.State, c rune) // 增加Nonce+1方法 +IncNonce() // 编码 +Marshal() : []byte, error // 解码 +Unmarshal(b []byte) : error [functions] +NewActor(code cid.Cid, balance *types.AttoFIL) : *Actor▼ functions // 只有account类型的actor使用 +NextNonce(actor *Actor) : uint64, error -init() 12345678location: actor/export.go▼ functions // 返回某个actor的方法执行函数 +MakeTypedExport(actor exec.ExecutableActor, method string) : exec.ExportedFunc // 序列化成字节切片 +MarshalValue(val interface{}) : []byte, error storagemarket actor 主要功能 创建存储矿工 获取总存储量 更新功率 123456789101112131415161718192021222324252627282930313233343536373839▼ package storagemarket▶ imports▼ constants +ErrInsufficientCollateral +ErrPledgeTooLow +ErrUnknownMiner▼ variables +Errors +MinimumCollateralPerSector +MinimumPledge -storageMarketExports▼+Actor : struct [methods] // 创建存储矿工 // 会调用到miner actor的创建 +CreateMiner(vmctx exec.VMContext, pledge *big.Int, publicKey []byte, pid peer.ID) : address.Address, uint8, error +Exports() : exec.Exports // 获取总存储 +GetTotalStorage(vmctx exec.VMContext) : *big.Int, uint8, error +InitializeState(storage exec.Storage, _ interface{}) : error // 更新功率 +UpdatePower(vmctx exec.VMContext, delta *big.Int) : uint8, error▼+State : struct [fields] // miners合集的cid +Miners : cid.Cid +TotalCommittedStorage : *big.Int▼ functions +MinimumCollateral(sectors *big.Int) : *types.AttoFIL // 实例化存储市场 +NewActor() : *actor.Actor, error -init() miner actor 提供功能 有基本转账功能 提供如下功能 filecoin网络中存在多个Miner Actor 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677▼ package miner▶ imports▼ constants +ErrAskNotFound +ErrCallerUnauthorized +ErrInsufficientPledge +ErrInvalidPoSt +ErrInvalidSealProof +ErrInvalidSector +ErrPublicKeyTooBig +ErrSectorCommitted +ErrStoragemarketCallFailed +MaximumPublicKeySize▼ variables +Errors +GracePeriodBlocks +ProvingPeriodBlocks -minerExports▼+Actor : struct [fields] +Bootstrap : bool [methods] // 增加订单 +AddAsk(ctx exec.VMContext, price *types.AttoFIL, expiry *big.Int) : *big.Int, uint8, error // 抵押承诺 +CommitSector(ctx exec.VMContext, sectorID uint64, commD, commR, commRStar, proof []byte) : uint8, error +Exports() : exec.Exports // 获取存储矿工相关信息 +GetAsk(ctx exec.VMContext, askid *big.Int) : []byte, uint8, error +GetAsks(ctx exec.VMContext) : []uint64, uint8, error +GetKey(ctx exec.VMContext) : []byte, uint8, error +GetLastUsedSectorID(ctx exec.VMContext) : uint64, uint8, error +GetOwner(ctx exec.VMContext) : address.Address, uint8, error +GetPeerID(ctx exec.VMContext) : peer.ID, uint8, error +GetPledge(ctx exec.VMContext) : *big.Int, uint8, error +GetPower(ctx exec.VMContext) : *big.Int, uint8, error +GetProvingPeriodStart(ctx exec.VMContext) : *types.BlockHeight, uint8, error +GetSectorCommitments(ctx exec.VMContext) : map[string]types.Commitments, uint8, error +InitializeState(storage exec.Storage, initializerData interface{}) : error // 提交时空证明 +SubmitPoSt(ctx exec.VMContext, postProofs []proofs.PoStProof) : uint8, error // 更新节点Id +UpdatePeerID(ctx exec.VMContext, pid peer.ID) : uint8, error // 报价单:价格,时长,序号▼+Ask : struct [fields] +Expiry : *types.BlockHeight +ID : *big.Int +Price : *types.AttoFIL // 矿工Actor状态▼+State : struct [fields] +Asks : []*Ask +Collateral : *types.AttoFIL +LastPoSt : *types.BlockHeight +LastUsedSectorID : uint64 +NextAskID : *big.Int +Owner : address.Address +PeerID : peer.ID +PledgeSectors : *big.Int +Power : *big.Int +ProvingPeriodStart : *types.BlockHeight +PublicKey : []byte +SectorCommitments : map[string]types.Commitments [functions] +NewState(owner address.Address, key []byte, pledge *big.Int, pid peer.ID, collateral *types.AttoFIL) : *State▼ functions +NewActor() : *actor.Actor -init() paymentbroker actor 说明 全网只有一个paymentbroker 几个概念的关系简图 源码分析注释12345678910111213141516171819202122232425262728293031323334353637383940414243▼ package paymentbroker▼+Actor : struct [methods] // 关闭支付通道 +Close(vmctx exec.VMContext, payer address.Address, chid *types.ChannelID, amt *types.AttoFIL, validAt *types.BlockHeight, sig []byte) : uint8, error // 创建支付通道 +CreateChannel(vmctx exec.VMContext, target address.Address, eol *types.BlockHeight) : *types.ChannelID, uint8, error +Exports() : exec.Exports // 增加资金 +Extend(vmctx exec.VMContext, chid *types.ChannelID, eol *types.BlockHeight) : uint8, error +InitializeState(storage exec.Storage, initializerData interface{}) : error // 查询某个支付者的信息 +Ls(vmctx exec.VMContext, payer address.Address) : []byte, uint8, error // 撤回资金 +Reclaim(vmctx exec.VMContext, chid *types.ChannelID) : uint8, error // 赎回(或者收款)资金 +Redeem(vmctx exec.VMContext, payer address.Address, chid *types.ChannelID, amt *types.AttoFIL, validAt *types.BlockHeight, sig []byte) : uint8, error // 收据,指明在特定区块高度之前都是有效的 +Voucher(vmctx exec.VMContext, chid *types.ChannelID, amount *types.AttoFIL, validAt *types.BlockHeight) : []byte, uint8, error▼+PaymentChannel : struct [fields] // 支付通道内金额 +Amount : *types.AttoFIL // 已被赎回金额 +AmountRedeemed : *types.AttoFIL +Eol : *types.BlockHeight // 收款人地址 +Target : address.Address▼ functions // 收据的签名及校验 +SignVoucher(channelID *types.ChannelID, amount *types.AttoFIL, validAt *types.BlockHeight, addr address.Address, signer types.Signer) : types.Signature, error +VerifyVoucherSignature(payer address.Address, chid *types.ChannelID, amt *types.AttoFIL, validAt *types.BlockHeight, sig []byte) : bool -createVoucherSignatureData(channelID *types.ChannelID, amount *types.AttoFIL, validAt *types.BlockHeight) : []byte -findByChannelLookup(ctx context.Context, storage exec.Storage, byPayer exec.Lookup, payer address.Address) : exec.Lookup, error -init() -reclaim(ctx context.Context, vmctx exec.VMContext, byChannelID exec.Lookup, payer address.Address, chid *types.ChannelID, channel *PaymentChannel) : error -updateChannel(ctx exec.VMContext, target address.Address, channel *PaymentChannel, amt *types.AttoFIL, validAt *types.BlockHeight) : error -withPayerChannels(ctx context.Context, storage exec.Storage, payer address.Address, f func(exec.Lookup) error) : error -withPayerChannelsForReading(ctx context.Context, storage exec.Storage, payer address.Address, f func(exec.Lookup) error) : error account actor 说明 纯账户,记录nonce 只有转帐功能 filecoin网络中存在多个account Actor 123456789101112131415161718▼ package account▶ imports▼ variables -accountExports▼+Actor : struct [methods] +Exports() : exec.Exports +InitializeState(_ exec.Storage, _ interface{}) : error▼ functions // 实例化account actor 集成actor包中Actor所实现的所有方法 +NewActor(balance *types.AttoFIL) : *actor.Actor, error // 将其他actor类型转为account,保留余额 +UpgradeActor(act *actor.Actor) : error vm(虚拟机运行环境) 虚拟机执行函数 123456789101112131415▼ package vm▶ imports▼-sendDeps : struct [fields] -transfer : func(*actor.Actor, *actor.Actor, *types.AttoFIL) error▼ functions // 执行合约 +Send(ctx context.Context, vmCtx *Context) : [][]byte, uint8, error // 转账 +Transfer(fromActor, toActor *actor.Actor, value *types.AttoFIL) : error -send(ctx context.Context, deps sendDeps, vmCtx *Context) : [][]byte, uint8, error vm环境实现 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354▼+Context : struct [fields] -ancestors : []types.TipSet -blockHeight : *types.BlockHeight -deps : *deps -from : *actor.Actor -gasTracker : *GasTracker -lookBack : int -message : *types.Message -state : *state.CachedTree -storageMap : StorageMap -to : *actor.Actor [methods] // 实现上述VMContext 接口,注释见上 +AddressForNewActor() : address.Address, error +BlockHeight() : *types.BlockHeight +Charge(cost types.GasUnits) : error +CreateNewActor(addr address.Address, code cid.Cid, initializerData interface{}) : error +GasUnits() : types.GasUnits +IsFromAccountActor() : bool +Message() : *types.Message +Rand(sampleHeight *types.BlockHeight) : []byte, error +ReadStorage() : []byte, error +Send(to address.Address, method string, value *types.AttoFIL, params []interface{}) : [][]byte, uint8, error +Storage() : exec.Storage +WriteStorage(memory interface{}) : error [functions] +NewVMContext(params NewContextParams) : *Context▼+NewContextParams : struct [fields] +Ancestors : []types.TipSet +BlockHeight : *types.BlockHeight +From : *actor.Actor +GasTracker : *GasTracker +LookBack : int +Message : *types.Message +State : *state.CachedTree +StorageMap : StorageMap +To : *actor.Actor▼-deps : struct [fields] +EncodeValues : func([]*abi.Value) []byte, error +GetOrCreateActor : func(context.Context, address.Address, func() *actor.Actor, error) *actor.Actor, error +Send : func(context.Context, *Context) [][]byte, uint8, error +ToValues : func([]interface{}) []*abi.Value, error▼ deps* : ctype [functions] -makeDeps(st *state.CachedTree) : *deps▼ functions -computeActorAddress(creator address.Address, nonce uint64) : address.Address, error 合约状态存储 12345678910111213141516171819202122232425262728293031▼+Storage : struct [fields] -actor : *actor.Actor -blockstore : blockstore.Blockstore -chunks : map[cid.Cid]ipld.Node [methods] +Commit(newCid cid.Cid, oldCid cid.Cid) : error +Flush() : error +Get(cid cid.Cid) : []byte, error +Head() : cid.Cid +Prune() : error +Put(v interface{}) : cid.Cid, error -liveDescendantIds(id cid.Cid) : *cid.Set, error [functions] +NewStorage(bs blockstore.Blockstore, act *actor.Actor) : Storage▼-storageMap : struct [fields] -blockstore : blockstore.Blockstore -storageMap : map[address.Address]Storage [methods] +Flush() : error +NewStorage(addr address.Address, actor *actor.Actor) : Storage▼+StorageMap : interface [methods] +Flush() : error +NewStorage(addr address.Address, actor *actor.Actor) : Storage▼ functions +NewStorageMap(bs blockstore.Blockstore) : StorageMap state包(actor状态) 表征actor的状态 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"filecoin技术架构分析之十二:filecoin源码分析之内部接口层plumbing&porcelain接口","slug":"filecoin-code-analysis-12","date":"2019-03-07T10:35:33.000Z","updated":"2019-04-05T10:29:31.041Z","comments":true,"path":"2019/03/07/filecoin-code-analysis-12/","link":"","permalink":"https://learnblockchain.cn/2019/03/07/filecoin-code-analysis-12/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第十二章源码分析之内部接口层plumbing&porcelain接口。","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第十二章源码分析之内部接口层plumbing&porcelain接口。 说明 目前官方正在将api包解耦,往plumbing、porcelain中迁移 缘由: 原来的api包,依赖于node包,而node包应该属于api之上的,这导致代码耦合性大 node作为一个上帝对象,被api包依赖,对架构扩展性,其他类型节点扩展开发不利 就在笔者写这篇文章的同时,官方应该还在继续迁移,后面api包会逐步都迁移完 porcelain主要依赖于plumbing接口 上一章所述的api包将会被废除 plumbing&porcelain模式简述 该模式是借鉴git的思路,提供两种接口,porcelain偏高层面对用户更加友好方便;plumbing偏底层,友好度弱于porcelain porcelain是英文瓷器的意思,类似洗手盆之类;plumbing是水管装置的意思,类似下水管,用户当然直接用洗手盆省心,不用管水管的事情 用户级更偏向用porcelain,协议级更偏向使用plumbing, plumbing底层接口 说明 plumbing底层接口是为实现协议以及面向网络的必须最小实现 更应用级别的调用更多将会调用到porcelain高层接口 提供的具体功能接口 区块状态读取 配置信息 日志 消息池操作 消息预览,Gas计算 消息查询 消息发送 消息等待 网络操作 Chain状态获取(actor信息) 钱包底层操作 具体的方法如下 12345678910111213141516171819202122232425262728293031323334353637383940414243▼ package plumbing▶ imports▼+API : struct [fields] -chain : chain.ReadStore -config : *cfg.Config -logger : logging.EventLogger -msgPool : *core.MessagePool -msgPreviewer : *msg.Previewer -msgQueryer : *msg.Queryer -msgSender : *msg.Sender -msgWaiter : *msg.Waiter -network : *ntwk.Network -sigGetter : *mthdsig.Getter -wallet : *wallet.Wallet [methods] +ActorGet(ctx context.Context, addr address.Address) : *actor.Actor, error +ActorGetSignature(ctx context.Context, actorAddr address.Address, method string) : *exec.FunctionSignature, error +BlockGet(ctx context.Context, id cid.Cid) : *types.Block, error +ChainHead(ctx context.Context) : types.TipSet +ChainLs(ctx context.Context) : chan interface{} +ConfigGet(dottedPath string) : interface{}, error +ConfigSet(dottedPath string, paramJSON string) : error +MessagePoolGet(cid cid.Cid) : *types.SignedMessage, bool +MessagePoolPending() : []*types.SignedMessage +MessagePoolRemove(cid cid.Cid) +MessagePreview(ctx context.Context, from, to address.Address, method string, params ...interface{}) : types.GasUnits, error +MessageQuery(ctx context.Context, optFrom, to address.Address, method string, params ...interface{}) : [][]byte, *exec.FunctionSignature, error +MessageSend(ctx context.Context, from, to address.Address, value *types.AttoFIL, gasPrice types.AttoFIL, gasLimit types.GasUnits, method string, params ...interface{}) : cid.Cid, error +MessageWait(ctx context.Context, msgCid cid.Cid, cb func(*types.Block, *types.SignedMessage, *types.MessageReceipt) error) : error +NetworkFindProvidersAsync(ctx context.Context, key cid.Cid, count int) : chan pstore.PeerInfo +NetworkGetPeerID() : peer.ID +PubSubPublish(topic string, data []byte) : error +PubSubSubscribe(topic string) : pubsub.Subscription, error +SignBytes(data []byte, addr address.Address) : types.Signature, error +WalletAddresses() : []address.Address +WalletFind(address address.Address) : wallet.Backend, error +WalletNewAddress() : address.Address, error [functions] +New(deps *APIDeps) : *API porcelain高层接口 说明 porcelain主要依赖plumbing实现。 主要是面向用户级操作 提供功能 获取区块高度 建立支付通道/多支付通道 获取默认地址 消息池等待未被打包进区块的消息 采用默认地址发送消息 获取指定矿工报价单 获取矿工Owner地址 获取矿工节点ID 创建矿工,预览Gas消耗 矿工报价,预览Gas消耗 矿工报价 获取签名支付凭证 钱包余额查询 12345678910111213141516171819202122232425▼ package porcelain▶ imports▼+API : struct [embedded] +*plumbing.API : *plumbing.API [methods] +ChainBlockHeight(ctx context.Context) : *types.BlockHeight, error +CreatePayments(ctx context.Context, config CreatePaymentsParams) : *CreatePaymentsReturn, error +GetAndMaybeSetDefaultSenderAddress() : address.Address, error +MessagePoolWait(ctx context.Context, messageCount uint) : []*types.SignedMessage, error +MessageSendWithDefaultAddress(ctx context.Context, from, to address.Address, value *types.AttoFIL, gasPrice types.AttoFIL, gasLimit types.GasUnits, method string, params ...interface{}) : cid.Cid, error +MinerGetAsk(ctx context.Context, minerAddr address.Address, askID uint64) : minerActor.Ask, error +MinerGetOwnerAddress(ctx context.Context, minerAddr address.Address) : address.Address, error +MinerGetPeerID(ctx context.Context, minerAddr address.Address) : peer.ID, error +MinerPreviewCreate(ctx context.Context, fromAddr address.Address, pledge uint64, pid peer.ID, collateral *types.AttoFIL) : types.GasUnits, error +MinerPreviewSetPrice(ctx context.Context, from address.Address, miner address.Address, price *types.AttoFIL, expiry *big.Int) : types.GasUnits, error +MinerSetPrice(ctx context.Context, from address.Address, miner address.Address, gasPrice types.AttoFIL, gasLimit types.GasUnits, price *types.AttoFIL, expiry *big.Int) : MinerSetPriceResponse, error +PaymentChannelLs(ctx context.Context, fromAddr address.Address, payerAddr address.Address) : map[string]*paymentbroker.PaymentChannel, error +PaymentChannelVoucher(ctx context.Context, fromAddr address.Address, channel *types.ChannelID, amount *types.AttoFIL, validAt *types.BlockHeight) : *paymentbroker.PaymentVoucher, error +WalletBalance(ctx context.Context, address address.Address) : *types.AttoFIL, error [functions] +New(plumbing *plumbing.API) : *API 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"filecoin技术架构分析十一:filecoin源码分析之内部接口层api包分析","slug":"filecoin-code-analysis-11","date":"2019-03-07T09:35:33.000Z","updated":"2019-04-05T10:29:31.001Z","comments":true,"path":"2019/03/07/filecoin-code-analysis-11/","link":"","permalink":"https://learnblockchain.cn/2019/03/07/filecoin-code-analysis-11/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第十一章源码分析之内部接口层api包分析。","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第十一章源码分析之内部接口层api包分析。 api包提供内部接口,供协议层、command/REST使用 较大程度依赖node包 apiapi的接口定义 如下所示,包含了一系列子接口 12345678910111213141516type API interface { Actor() Actor Address() Address Client() Client Daemon() Daemon Dag() Dag ID() ID Log() Log Miner() Miner Mining() Mining Paych() Paych Ping() Ping RetrievalClient() RetrievalClient Swarm() Swarm Version() Version} api的接口实现123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263▼ package impl▶ imports// nodeAPI来实现其接口定义▼-nodeAPI : struct [fields] // 合约 -actor : *nodeActor // 地址 -address : *nodeAddress // 客户端 -client : *nodeClient // daemon -daemon : *nodeDaemon // dag -dag : *nodeDag // 节点ID -id : *nodeID // 日志 -log : *nodeLog // 日志 -logger : logging.EventLogger // 矿工 -miner : *nodeMiner // 挖矿 -mining : *nodeMining // 节点 -node : *node.Node // 支付通道 -paych : *nodePaych // ping -ping : *nodePing // 检索客户端 -retrievalClient : *nodeRetrievalClient // swarm -swarm : *nodeSwarm // 版本 -version : *nodeVersion [methods] // 如下为实现API接口 +Actor() : api.Actor +Address() : api.Address +Client() : api.Client +Daemon() : api.Daemon +Dag() : api.Dag +ID() : api.ID +Log() : api.Log +Miner() : api.Miner +Mining() : api.Mining +Paych() : api.Paych +Ping() : api.Ping +RetrievalClient() : api.RetrievalClient +Swarm() : api.Swarm +Version() : api.Version▼ functions // 实例化API // 1 获取高层API porcelainAPI 指针,miner与paych有用到 // 2 调用各子系统的实例化函数逐一实例化 +New(node *node.Node) : api.API actoractor的接口定义123456789101112131415161718192021222324252627282930313233343536▼ package api▶ imports▼+ActorView : struct [fields] // actor类型 +ActorType : string // actor地址 +Address : string // actor余额 +Balance : *types.AttoFIL // actor代码-CID +Code : cid.Cid // 导出符号集合 +Exports : ReadableExports // 表征actor实例的状态 +Head : cid.Cid // 消息计数器,仅为account actors与外部发生交互的时候计算 +Nonce : uint64 // 导出符号集合 +ReadableExports : map[string]*ReadableFunctionSignature▼+ReadableFunctionSignature : struct [fields] // 参数 +Params : []string // 返回 +Return : []string▼+Actor : interface // 目前接口只有查看功能,返回合约的具体信息 [methods] +Ls(ctx context.Context) : []*ActorView, error actor的接口实现1234567891011121314151617181920212223242526272829▼ package impl▶ imports// 使用nodeActor来实现Actor接口▼-nodeActor : struct [fields] -api : *nodeAPI [methods] // 调用ls方法实现查询功能 +Ls(ctx context.Context) : []*api.ActorView, error [functions] // 实例化nodeActor,由api实现代码中调用 -newNodeActor(api *nodeAPI) : *nodeActor▼ functions // 获取合约类型 // 1 account actor // 2 存储市场actor // 3 支付通道actor // 4 矿工actor // 4 BootstrapMiner actor -getActorType(actType exec.ExecutableActor) : string // 查询合约状态 -ls(ctx context.Context, fcn *node.Node, actorGetter state.GetAllActorsFunc) : []*api.ActorView, error -makeActorView(act *actor.Actor, addr string, actType exec.ExecutableActor) : *api.ActorView -makeReadable(f *exec.FunctionSignature) : *api.ReadableFunctionSignature -presentExports(e exec.Exports) : api.ReadableExports address 提供功能 地址显示方法 地址查找方法 创建地址方法 导出地址方法 导入地址方法 12345678910111213141516▼ package api▶ imports▼+Address : interface [methods] +Addrs() : Addrs +Export(ctx context.Context, addrs []address.Address) : []*types.KeyInfo, error +Import(ctx context.Context, f files.File) : []address.Address, error▼+Addrs : interface [methods] +Lookup(ctx context.Context, addr address.Address) : peer.ID, error +Ls(ctx context.Context) : []address.Address, error +New(ctx context.Context) : address.Address, error client 提供如下功能 查询piece数据(DAG格式) 导入数据(相当于ipfs add) 列出所有订单 支付 发起存储交易 查询存储交易 12345678910111213141516▼+Ask : struct [fields] +Error : error +Expiry : *types.BlockHeight +ID : uint64 +Miner : address.Address +Price : *types.AttoFIL▼+Client : interface [methods] +Cat(ctx context.Context, c cid.Cid) : uio.DagReader, error +ImportData(ctx context.Context, data io.Reader) : ipld.Node, error +ListAsks(ctx context.Context) : chan Ask, error +Payments(ctx context.Context, dealCid cid.Cid) : []*paymentbroker.PaymentVoucher, error +ProposeStorageDeal(ctx context.Context, data cid.Cid, miner address.Address, ask uint64, duration uint64, allowDuplicates bool) : *storage.DealResponse, error +QueryStorageDeal(ctx context.Context, prop cid.Cid) : *storage.DealResponse, error config 提供功能 Get配置 Set配置 daemon 提供功能 启动进程相关 具体的业务启动逻辑会调用到node包 1234567891011121314151617181920212223242526272829▼ package api▶ imports▼+DaemonInitConfig : struct [fields] // 如果配置,定期检查并密封staged扇区 +AutoSealIntervalSeconds : uint +DefaultAddress : address.Address // 指定网络 +DevnetNightly : bool +DevnetTest : bool +DevnetUser : bool // 创世文件 +GenesisFile : string +PeerKeyFile : string // repo目录 +RepoDir : string // 指定矿工 +WithMiner : address.Address +DaemonInitOpt : func(*DaemonInitConfig)▼+Daemon : interface [methods] +Init(ctx context.Context, opts ...DaemonInitOpt) : error +Start(ctx context.Context) : error +Stop(ctx context.Context) : error dag 提供功能 dag查询功能 类似ipfs block get id 提供功能 ID详细信息 如多地址、协议版本、导出公钥等 1234567891011121314151617▼+IDDetails : struct [fields] +Addresses : []ma.Multiaddr +AgentVersion : string +ID : peer.ID +ProtocolVersion : string +PublicKey : []byte [methods] +MarshalJSON() : []byte, error +UnmarshalJSON(data []byte) : error▼+ID : interface [methods] +Details() : *IDDetails, error▼ functions -decode(idd map[string]*json.RawMessage, key string, dest interface{}) : error log 提供日志功能 123▼+Log : interface [methods] +Tail(ctx context.Context) : io.Reader miner 创建矿工 123▼+Miner : interface [methods] +Create(ctx context.Context, fromAddr address.Address, gasPrice types.AttoFIL, gasLimit types.GasUnits, pledge uint64, pid peer.ID, collateral *types.AttoFIL) : address.Address, error mining 挖矿控制 启动 停止 12345▼+Mining : interface [methods] +Once(ctx context.Context) : *types.Block, error +Start(ctx context.Context) : error +Stop(ctx context.Context) : error ping 提供ping接口 123456789▼+PingResult : struct [fields] +Success : bool +Text : string +Time : time.Duration▼+Ping : interface [methods] +Ping(ctx context.Context, pid peer.ID, count uint, delay time.Duration) : chan *PingResult, error retrieval_client 提供检索接口 123▼+RetrievalClient : interface [methods] +RetrievePiece(ctx context.Context, pieceCID cid.Cid, minerAddr address.Address) : io.ReadCloser, error swarm 提供节点连接功能 显示连接节点 连接节点 查找节点 12345678910111213141516171819202122232425262728293031323334▼+SwarmConnInfo : struct [fields] +Addr : string +Latency : string +Muxer : string +Peer : string +Streams : []SwarmStreamInfo [methods] +Len() : int +Less(i, j int) : bool +Swap(i, j int)▼+SwarmConnInfos : struct [fields] +Peers : []SwarmConnInfo [methods] +Len() : int +Less(i, j int) : bool +Swap(i, j int)▼+SwarmConnectResult : struct [fields] +Peer : string +Success : bool▼+SwarmStreamInfo : struct [fields] +Protocol : string▼+Swarm : interface [methods] +Connect(ctx context.Context, addrs []string) : []SwarmConnectResult, error +FindPeer(ctx context.Context, peerID peer.ID) : peerstore.PeerInfo, error +Peers(ctx context.Context, verbose, latency, streams bool) : *SwarmConnInfos, error 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"filecoin技术架构分析十:filecoin源码分析之支撑包分析(2)","slug":"filecoin-code-analysis-10","date":"2019-03-07T08:35:33.000Z","updated":"2019-04-05T10:29:31.028Z","comments":true,"path":"2019/03/07/filecoin-code-analysis-10/","link":"","permalink":"https://learnblockchain.cn/2019/03/07/filecoin-code-analysis-10/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第十章源码分析之支撑包分析(2)。","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第十章源码分析之支撑包分析(2)。 本章续上一章的支撑包介绍,主要为便于后面章节的源码理解 repo 提供功能 实例化fs资源或者mem资源 提供读取、设置API地址方法 提供存储已被校验区块的方法 提供阶段密封数据存储方法 提供密封完成数据存储方法 提供读取配置方法 提供通用数据存储方法 提供交易数据存储方法 提供钱包信息存储方法 提供存储密钥方法 提供快照配置存储方法 提供版本号读取方法 12345678910111213141516171819202122232425262728293031323334353637383940414243▼ package repo▶ imports▼ constants // 当前为1,可以cat ~/.filecoin/version确认 +Version : uint▼+Datastore : interface [embedded] // 包含datastore的read、write、batch +datastore.Batching // Repo接口分别由fsrepo及memrepo实现▼+Repo : interface [methods] // 读取API地址 +APIAddr() : string, error // 存储已被校验过的区块数据 +ChainDatastore() : Datastore // 关闭 +Close() : error // 读取配置,对应上一章中的config +Config() : *config.Config // 存储通用数据 +Datastore() : Datastore // 交易数据存储 +DealsDatastore() : Datastore // 存储密钥相关 +Keystore() : keystore.Keystore // 存储倒数第二个配置 +ReplaceConfig(cfg *config.Config) : error // 存储密封扇区 +SealedDir() : string // 设置API地址 +SetAPIAddr(string) : error // 存储分段密封扇区 +StagingDir() : string // 读取版本号 +Version() : uint // 存储钱包信息 +WalletDatastore() : Datastore 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091location: repo/fsrepo.go▼ package repo▼ constants // api文件 +APIFile // chain目录:chain -chainDatastorePrefix // 配置文件名称,对应上一章中的config -configFilename // 交易目录:deals -dealsDatastorePrefix // 资源目录锁文件:repo.lock -lockFile // 快照文件前缀名 snapshot -snapshotFilenamePrefix // 快照目录;配置快照 -snapshotStorePrefix // 临时配置文件名称 -tempConfigFilename // version文件名称 -versionFilename // 钱包目录名称wallet -walletDatastorePrefix▼ variables -log▼+FSRepo : struct [fields] -cfg : *config.Config -chainDs : Datastore -dealsDs : Datastore -ds : Datastore -keystore : keystore.Keystore -lk : sync.RWMutex -lockfile : io.Closer // 资源目录路径 -path : string // 资源目录版本 -version : uint -walletDs : Datastore [methods] +APIAddr() : string, error +ChainDatastore() : Datastore +Close() : error +Config() : *config.Config +Datastore() : Datastore +DealsDatastore() : Datastore +Keystore() : keystore.Keystore +ReplaceConfig(cfg *config.Config) : error +SealedDir() : string +SetAPIAddr(maddr string) : error // 快照存储 +SnapshotConfig(cfg *config.Config) : error +StagingDir() : string +Version() : uint +WalletDatastore() : Datastore -loadConfig() : error -loadFromDisk() : error -loadVersion() : uint, error -openChainDatastore() : error -openDatastore() : error -openDealsDatastore() : error -openKeystore() : error -openWalletDatastore() : error -removeAPIFile() : error -removeFile(path string) : error [functions] // 打开已被初始化过的资源目录 +OpenFSRepo(p string) : *FSRepo, error▼+NoRepoError : struct [fields] +Path : string [methods] +Error() : string▼ functions // 从文件中读取api file +APIAddrFromFile(apiFilePath string) : string, error // 初始化资源目录 +InitFSRepo(p string, cfg *config.Config) : error -checkWritable(dir string) : error -fileExists(file string) : bool -genSnapshotFileName() : string -initConfig(p string, cfg *config.Config) : error -initVersion(p string, version uint) : error -isInitialized(p string) : bool, error 1234567891011121314151617181920212223242526272829303132333435363738▼ package repo▼ imports▼+MemRepo : struct [fields] +C : *config.Config +Chain : Datastore +D : Datastore +DealsDs : Datastore +Ks : keystore.Keystore +W : Datastore -apiAddress : string -lk : sync.RWMutex -sealedDir : string -stagingDir : string -version : uint [methods] +APIAddr() : string, error +ChainDatastore() : Datastore +CleanupSectorDirs() +Close() : error +Config() : *config.Config +Datastore() : Datastore +DealsDatastore() : Datastore +Keystore() : keystore.Keystore +ReplaceConfig(cfg *config.Config) : error +SealedDir() : string +SetAPIAddr(addr string) : error +StagingDir() : string +Version() : uint +WalletDatastore() : Datastore [functions] // 实例化内存资源接口,会调用NewInMemoryRepoWithSectorDirectories +NewInMemoryRepo() : *MemRepo // 实例化内存资源接口,指定阶段密封和最终密封目录 +NewInMemoryRepoWithSectorDirectories(staging, sealedDir string) : *MemRepo proofs和sectorbuilder proofs提供功能 校验时空证明的方法 校验密封证明的方法 更细节的注释见如下代码笔者增加的注释 rustverifier实现具体的方法 1234567891011121314151617181920212223242526272829303132location: proofs/types.go▼ package proofs▼ constants // merkle根长度 +CommitmentBytesLen : uint // 时空证明挑战参数长度:32bytes +PoStChallengeSeedBytesLen : uint // 密封复制证明长度:384bytes +SealBytesLen : uint // 时空证明长度:192bytes +SnarkBytesLen : uint // 原始数据的merkle根,由PoRep输出 +CommD : []byte // 副本数据的merkle根,由PoRep输出 +CommR : []byte // 中间层的merkle根,由PoRep输出 +CommRStar : []byte // 挑战随机参数,32bytes,256bits,PoSt的输入 +PoStChallengeSeed : []byte // 时空证明输出,192bytes +PoStProof : []byte // 密封复制证明,384bytes +SealProof : []byte 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950location: proofs/interface.go▼ package proofs▼ constants +Live +Test +SectorStoreType : int // 校验时空证明校验请求▼+VerifyPoSTRequest : struct [fields] // 挑战参数 +ChallengeSeed : PoStChallengeSeed +CommRs : []CommR +Faults : []uint64 +Proof : PoStProof +StoreType : SectorStoreType▼+VerifyPoSTResponse : struct [fields] +IsValid : bool // 向特定矿工&特定扇区发起密封校验请求▼+VerifySealRequest : struct [fields] // 来自于密封的返回参数 +CommD : CommD +CommR : CommR +CommRStar : CommRStar +Proof : SealProof // 矿工标识 +ProverID : [31]byte // 扇区ID +SectorID : [31]byte // 用于控制密封校验效率 +StoreType : SectorStoreType▼+VerifySealResponse : struct [fields] +IsValid : bool▼+Verifier : interface [methods] // 校验时空证明 +VerifyPoST(VerifyPoSTRequest) : VerifyPoSTResponse, error // 校验密封证明 +VerifySeal(VerifySealRequest) : VerifySealResponse, error 1234567891011121314151617181920location: proofs/rustverifier.go▼ package proofs▶ imports▼ variables -log // RustVerifier 实现VerifyPoST与VerifySeal接口▼+RustVerifier : struct [methods] +VerifyPoST(req VerifyPoSTRequest) : VerifyPoSTResponse, error +VerifySeal(req VerifySealRequest) : VerifySealResponse, error▼ functions +CSectorStoreType(cfg SectorStoreType) : *C.ConfiguredStore, error -cUint64s(src []uint64) : *C.uint64_t, C.size_t -elapsed(what string) : func() sectorbuilder 提供向unsealed扇区写入pieces的方法 提供生成时空证明的方法 提供从特定扇区读取特定pieces的方法 提供密封完成通知的方法 提供批量密封所有未完成的分段扇区 与rust-fil-proof交互,更深入的逻辑需要参见rust 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162location: proofs/sectorbuilder/interface.gopackage sectorbuilder▶ imports // 生成生成时空证明请求▼+GeneratePoSTRequest : struct [fields] +ChallengeSeed : proofs.PoStChallengeSeed +CommRs : []proofs.CommR // 生成生成时空证明响应▼+GeneratePoSTResponse : struct [fields] +Faults : []uint64 +Proof : proofs.PoStProof▼+PieceInfo : struct [fields] +Ref : cid.Cid +Size : uint64 // 密封元数据▼+SealedSectorMetadata : struct [fields] +CommD : proofs.CommD // 副本哈希后续将被删除 +CommR : proofs.CommR +CommRStar : proofs.CommRStar // Pieces后续将被删除 +Pieces : []*PieceInfo +Proof : proofs.SealProof +SectorID : uint64 // 密封结果▼+SectorSealResult : struct [fields] +SealingErr : error +SealingResult : *SealedSectorMetadata +SectorID : uint64 // SectorBuilder提供相关功能 // 1 写入、密封pieces至扇区 // 2 unseal、读取pieces▼+SectorBuilder : interface [methods] // 向unsealed扇区写入pieces +AddPiece(ctx context.Context, pi *PieceInfo) : uint64, error +Close() : error // 生成时空证明 +GeneratePoST(GeneratePoSTRequest) : GeneratePoSTResponse, error +GetMaxUserBytesPerStagedSector() : uint64, error // 从扇区中读取特定pieces +ReadPieceFromSealedSector(pieceCid cid.Cid) : io.Reader, error // 密封所有未完成的分段扇区 +SealAllStagedSectors(ctx context.Context) : error // 密封完成的通知 +SectorSealResults() : chan SectorSealResult▼ functions -init() 1234location: proofs/sectorbuilder/poller.go// 当pieces加入后,会进行FFI调用,定时执行密封const SealedSectorPollingInterval = 1 * time.Second type如下对一些主要结构进行简析 AttoFIL(10*-18 FIL) 提供AttoFIL的算数运算方法 提供AttoFIL的逻辑运算方法 Block 区块结构 1234567891011121314151617181920212223▼+Block : struct [fields] +Height : Uint64 +MessageReceipts : []*MessageReceipt +Messages : []*SignedMessage +Miner : address.Address +Nonce : Uint64 +ParentWeight : Uint64 +Parents : SortedCidSet +Proof : proofs.PoStProof +StateRoot : cid.Cid +Ticket : Signature -cachedBytes : []byte -cachedCid : cid.Cid [methods] +Cid() : cid.Cid +Equals(other *Block) : bool +IsParentOf(c Block) : bool +Score() : uint64 +String() : string +ToNode() : node.Node [functions] +DecodeBlock(b []byte) : *Block, error BlockHeight 区块高度相关操作方法 123456789101112131415161718▼+BlockHeight : struct [fields] -val : *big.Int [methods] +Add(y *BlockHeight) : *BlockHeight +AsBigInt() : *big.Int +Bytes() : []byte +Equal(y *BlockHeight) : bool +GreaterEqual(y *BlockHeight) : bool +GreaterThan(y *BlockHeight) : bool +LessEqual(y *BlockHeight) : bool +LessThan(y *BlockHeight) : bool +String() : string +Sub(y *BlockHeight) : *BlockHeight [functions] +NewBlockHeight(x uint64) : *BlockHeight +NewBlockHeightFromBytes(buf []byte) : *BlockHeight +NewBlockHeightFromString(s string, base int) : *BlockHeight, bool BytesAmount (*big.Int) 提供相关的算数逻辑运算 ChannelID(支付通道结构体) 12345678910111213▼+ChannelID : struct [fields] -val : *big.Int [methods] +Bytes() : []byte +Equal(y *ChannelID) : bool +Inc() : *ChannelID +KeyString() : string +String() : string [functions] +NewChannelID(x uint64) : *ChannelID +NewChannelIDFromBytes(buf []byte) : *ChannelID +NewChannelIDFromString(s string, base int) : *ChannelID, bool 一些变量定义 创建各类actor对象 123456789101112func init() { AccountActorCodeObj = dag.NewRawNode([]byte("accountactor")) AccountActorCodeCid = AccountActorCodeObj.Cid() StorageMarketActorCodeObj = dag.NewRawNode([]byte("storagemarket")) StorageMarketActorCodeCid = StorageMarketActorCodeObj.Cid() PaymentBrokerActorCodeObj = dag.NewRawNode([]byte("paymentbroker")) PaymentBrokerActorCodeCid = PaymentBrokerActorCodeObj.Cid() MinerActorCodeObj = dag.NewRawNode([]byte("mineractor")) MinerActorCodeCid = MinerActorCodeObj.Cid() BootstrapMinerActorCodeObj = dag.NewRawNode([]byte("bootstrapmineractor")) BootstrapMinerActorCodeCid = BootstrapMinerActorCodeObj.Cid()} Message相关 消息结构及方法 filecoin网络的交易由一些列的Message组成 123456789101112131415▼+Message : struct [fields] +From : address.Address +Method : string +Nonce : Uint64 +Params : []byte +To : address.Address +Value : *AttoFIL [methods] +Cid() : cid.Cid, error +Marshal() : []byte, error +String() : string +Unmarshal(b []byte) : error [functions] +NewMessage(from, to address.Address, nonce uint64, value *AttoFIL, method string, params []byte) : *Message 12345▼+MessageReceipt : struct [fields] +ExitCode : uint8 +GasAttoFIL : *AttoFIL +Return : [][]byte 1234567891011▼+MeteredMessage : struct [fields] +GasLimit : GasUnits +GasPrice : AttoFIL [embedded] +Message : Message [methods] +Marshal() : []byte, error +Unmarshal(b []byte) : error [functions] +NewMeteredMessage(msg Message, gasPrice AttoFIL, gasLimit GasUnits) : *MeteredMessage 1234567891011121314▼+SignedMessage : struct [fields] +Signature : Signature [embedded] +MeteredMessage : MeteredMessage [methods] +Cid() : cid.Cid, error +Marshal() : []byte, error +RecoverAddress(r Recoverer) : address.Address, error +String() : string +Unmarshal(b []byte) : error +VerifySignature() : bool [functions] +NewSignedMessage(msg Message, s Signer, gasPrice AttoFIL, gasLimit GasUnits) : *SignedMessage, error TipSet 区块集合 1234567891011121314 +Tip : Block▼+TipSet : map[cid.Cid]*Tip [methods] +AddBlock(b *Block) : error +Clone() : TipSet +Equals(ts2 TipSet) : bool +Height() : uint64, error +MinTicket() : Signature, error +ParentWeight() : uint64, error +Parents() : SortedCidSet, error +String() : string +ToSlice() : []*Block +ToSortedCidSet() : SortedCidSet abi abi 对filecoin中的各类数据定义数据类型 提供abi编解码操作方法 pubsub 提供功能 提供订阅实例化以及订阅方法 提供发布实例化以及发布方法 1234567891011121314151617181920212223242526272829▼ package pubsub▶ imports▼+Subscriber : struct [fields] -pubsub : *libp2p.PubSub [methods] +Subscribe(topic string) : Subscription, error [functions] +NewSubscriber(sub *libp2p.PubSub) : *Subscriber▼-subscriptionWrapper : struct [embedded] +*libp2p.Subscription : *libp2p.Subscription [methods] +Next(ctx context.Context) : Message, error▼+Message : interface [methods] +GetData() : []byte +GetFrom() : peer.ID▼+Subscription : interface [methods] +Cancel() +Next(ctx context.Context) : Message, error +Topic() : string 123456789101112▼ package pubsub▶ imports▼+Publisher : struct [fields] -pubsub : *pubsub.PubSub [methods] +Publish(topic string, data []byte) : error [functions] +NewPublisher(sub *pubsub.PubSub) : *Publisher 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"登链钱包-一款功能强大的完全开源以太坊钱包","slug":"wallet-annouce","date":"2019-03-07T02:34:57.000Z","updated":"2019-04-05T10:29:31.012Z","comments":true,"path":"2019/03/07/wallet-annouce/","link":"","permalink":"https://learnblockchain.cn/2019/03/07/wallet-annouce/","excerpt":"你是否和我前段时间一样,苦苦的寻找一款好用的开源以太坊钱包,你会发现可用都很少,因为很多钱包说开源,仅仅是开源部分代码,现在不需要再找了。 重要的事情说三遍:这是一个款完全开源,完全免费,功能强大支持DApp浏览器功能的钱包;这是一个款完全开源,完全免费,功能强大支持DApp浏览器功能的钱包;这是一个款完全开源,完全免费,功能强大支持DApp浏览器功能的钱包。 再也不用傻乎乎找人开发以太坊钱包了, 直接拿去用吧;再也不用担心私钥会被上传到别人的服务器上。","text":"你是否和我前段时间一样,苦苦的寻找一款好用的开源以太坊钱包,你会发现可用都很少,因为很多钱包说开源,仅仅是开源部分代码,现在不需要再找了。 重要的事情说三遍:这是一个款完全开源,完全免费,功能强大支持DApp浏览器功能的钱包;这是一个款完全开源,完全免费,功能强大支持DApp浏览器功能的钱包;这是一个款完全开源,完全免费,功能强大支持DApp浏览器功能的钱包。 再也不用傻乎乎找人开发以太坊钱包了, 直接拿去用吧;再也不用担心私钥会被上传到别人的服务器上。 写在前面区块链是开放的,很难想象一个封闭的项目如何产生信任,开源一直是区块链社区所倡导的行为准则。我们也希望开源能够降低行业的开发门槛,吸引更多的开发者和公司能够利用我们的代码,找到更多落地的应用场景,一起来推动行业的发展。同时我们也相信开源可以是产品更加的安全,我们也邀请专业的区块链安全团队零时科技来为钱包做安全审计。 效果演示先来看看钱包长什么样吧,我制作了一个gif图片: 19年4月更新:加入 DApp 浏览器 功能 DApp 浏览器,目前暂未开源,需要请加微信:xlbxiong。 Gif 图片比较简陋,见谅见谅,可以看的出来界面参考了现在的主流钱包,感谢imToken及ETHWallet, 大家可以戳链接下载APK体验,Google play 也已经上架,链接 功能介绍目前版本支持一下功能: 支持通过生成助记词、Keystore文件、私钥 创建钱包账号; 支持导出钱包账号助记词、私钥、Keystore文件; 账户余额查询及转账功能; 支持多个钱包账号管理; 支持ERC20 代币(余额显示、转账、代币币价显示); 历史交易列表显示; 支持DApp Browser 浏览器 二维码扫描,兼容imToken格式; 支持用法币(美元和人民币)实时显示币价; 支持以太坊官方测试网络(Infura Koven及Ropsten)及本地测试网络。 功能够全面吧,尤其是最后一个功能支持以太坊官方测试网络(Infura Koven及Ropsten)及本地测试网络,估计是开发者的最爱,做为开发者的我,懂你们的痛(可以获取到免费的以太币用于测试)。 代码的讲解和相应的课程,我们后面会陆续放出,在还没有放出之前,先提醒大家几个注意的点: 使用本地网络测试的时候注意Geth 或 Ganache 设置下可接收RPC连接的地址,因为默认情况下只支持本地连接,这样手机上就无法连接。 显示交易记录功能需要自己搭建一个服务器提供API接口,这个接口来自TrustWallet,为了和本应用保持版本一致,我Fork了一份,地址为trust-ray,这个库会解析区块,并把交易信息存到MongoDb数据库里,然后用API提供给客户端使用。 实时币价的显示其实也是使用trust-ray提供的接口,trust-ray 使用的是CoinMarketCap的数据,目前使用的是CoinMarketCap免费提供的数据,CoinMarketCap现在有一套新的付费接口,免费的数据可能在将来会停用,到时需要使用CoinMarketCap 的apikey来访问。 代码中ERC20_Contract目录提供了一个ERC20合约给大家部署测试Token功能。 其他的代码介绍及环境搭建大家就只有等我的文章了,大家也可以学习网页钱包开发课程,课程详细介绍了开发钱包必备的理论知识。 有什么需要的功能,可以提issue或加我微信留言。 对了本项目的GitHub地址为:Upchain-wallet, 点 Star 的同学都会发大财,哈哈哈~~~ 参考的开源项目本钱包在开发是站在巨人的肩膀上完成,特别感谢以下项目: web3j bitcoinj Trust-wallet ETHWallet BGAQRCode Trust-ray 再啰嗦几句本次开源也是受到区块链社区的影响,尤其是HiBlock区块链社区一些朋友坚持布道和开源的精神影响。 HiBlock区块链社区 是国内最大的区块链开发者社区,社区已经聚集了数千名区块链开发者。 登链钱包是由登链学院出品,希望大家知道登链学院不单出品优质课程,我们也为行业发展贡献一份力量,感谢大家转发。 想要加入开源钱包讨论群的的朋友,加微信:xlbxiong 备注:钱包 PS: 我们提供专业的钱包定制开发,欢迎咨询微信:xlbxiong 备注:定制开发 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"钱包","slug":"钱包","permalink":"https://learnblockchain.cn/tags/钱包/"},{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"}]},{"title":"filecoin技术架构分析九:filecoin源码分析之支撑包分析(1)","slug":"filecoin-code-analysis-9","date":"2019-03-06T08:35:33.000Z","updated":"2019-04-05T10:29:31.045Z","comments":true,"path":"2019/03/06/filecoin-code-analysis-9/","link":"","permalink":"https://learnblockchain.cn/2019/03/06/filecoin-code-analysis-9/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第九章filecoin源码分析之支撑包分析(1)。","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第九章filecoin源码分析之支撑包分析(1)。 目的 简析一些支撑包,便于后面分析的理解 编译相关 bin目录:主要为编译用shell脚本 bls-signatures: 通过cgo编译,导出库及头文件 build: 编译相关 util/version:版本检查 scripts:相关脚本 cborutil 对外提供功能 读取流消息 写入流消息 主要被协议层使用 123456789101112131415161718▼ package cborutil▶ imports▼ constants +MaxMessageSize▼ variables +ErrMessageTooLarge▼+MsgReader : struct [fields] -br : *bufio.Reader [methods] +ReadMsg(i interface{}) : error [functions] +NewMsgReader(r io.Reader) : *MsgReader 123456789101112▼ package cborutil▶ imports▼+MsgWriter : struct [fields] -w : *bufio.Writer [methods] +WriteMsg(i interface{}) : error [functions] +NewMsgWriter(w io.Writer) : *MsgWriter address 对外提供功能 地址相关操作功能 实例化铸币地址、存储市场地址、支付通道地址 实例化两个测试地址 提供主网地址、测试网地址创建接口 提供地址格式转换功能,包含22bytes与41bytes、切片字符串转换、打印。 提供地址的合法性检查功能 地址格式 要与id区分开,id用的是ipfs中的cid,而地址则是filecoin独立定义的。 22 bytes地址:包含1byte网络类型、1byte地址版本、20bytes哈希 41 bytes地址:包含2bytes网络类型、1byte地址版本、32bytes编码值、6bytes校验和 用命令显示的是41bytes格式的地址,address包提供了22bytes与41bytes地址的转换接口 1234567891011121314151617181920212223242526272829303132333435location: address/constants.go▼ package address▶ imports▼ constants // Base32编码的字符集 +Base32Charset // 地址的哈希部分,目前为20 bytes +HashLength, 20bytes,160bit // 地址长度,为HashLength+1+1= 22 bytes +Length // 地址格式的版本定义:当前为0 +Version : byte▼ variables // 基于Base32Charset的Base32实例,用于编解码 +Base32 // Base32 Reverse集合 +Base32CharsetReverse // 铸币地址,基于"filecoin"哈希生成 +NetworkAddress : Address // 支付通道地址 +PaymentBrokerAddress : Address // 存储市场地址 +StorageMarketAddress : Address // 测试地址 +TestAddress : Address // 测试地址 +TestAddress2 : Address▼ functions -init() 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283location: address/address.go▼ package address▶ imports▼ constants +Mainnet : Network +Testnet▼ variables // 错误提示 +ErrInvalidBytes +ErrUnknownNetwork +ErrUnknownVersion -generator // 配置输入哈希长度20bytes -hashConfig // Address为22字节字符串▼+Address : []byte [methods] // 转换为编码前地址切片输出 +Bytes() : []byte // 判断地址是否为空 +Empty() : bool // 打印地址信息 +Format(f fmt.State, c rune) // 输出地址中的20bytes哈希值 +Hash() : []byte // 转换为编码后地址切片输出 +MarshalText() : []byte, error // 输出地址的网络类型 +Network() : Network // 转换为41bytes的编码输出 // 2(网络类型)+1(地址版本)+32(base32编码)+6(base32校验位) +String() : string // 编码后地址切片输出转换为字符 +UnmarshalText(in []byte) : error // 获取地址版本号 +Version() : byte // 类型定义 +Network : byte▼ functions // 采用blake2b-160再次哈希 +Hash(input []byte) : []byte // 生成测试网络地址,输入为原始哈希,会执行blake2b-160再次哈希 +MakeTestAddress(input string) : Address // 通过字符串网络类型转换为byte网络类型 // fc:主网转化为0 // tf:测试网化为1 +NetworkFromString(input string) : Network, error // 通过byte网络类型转换为字符串网络类型 // 0:主网转化为fc // 1:测试网化为tf +NetworkToString(n Network) : string // 构建新地址:输入为原始20bytes哈希+网络类型+地址版本 +New(network Network, hash []byte) : Address // 构建新地址:输入为22bytes的原始切片 +NewFromBytes(raw []byte) : Address, error // 通过41bytes的字串串生成22bytes的原始地址 +NewFromString(s string) : Address, error // 构建新地址:输入为原始20bytes哈希,调用New +NewMainnet(hash []byte) : Address // 生成测试网络地址,输入为原始哈希再次哈希,被MakeTestAddress调用 +NewTestnet(hash []byte) : Address // 校验41bytes地址的合法性 +ParseError(addr string) : error // base32编码校验码生成,结果为6bytes -createChecksum(hrp string, data []byte) : []byte // 解码 -decode(addr string) : string, byte, []byte, error // 编码 -encode(hrp string, version byte, data []byte) : string, error -hrpExpand(hrp string) : []byte -init() -polymod(values []byte) : uint32 // 校验和验证 -verifyChecksum(hrp string, data []byte) : bool 123456789101112131415location: address/set.go▼ package address▶ imports▼ variables -addrSetEntry // 地址集合 +Set : map[Address]▼ functions -init() config 对外提供功能 提供对内存中配置的实例化操作 对具体实例的设置和读取 对配置文件的读写 包含API、启动、数据存储、网络连接、挖矿、钱包、心跳相关配置 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120▼ package config▶ imports▼ variables // 对特定参数的合法性校验规则集合 // 1 目前只是限定昵称为字符 +Validators▼+APIConfig : struct [fields] // 是否允许跨域请求 +AccessControlAllowCredentials : bool // 允许的方法列表 +AccessControlAllowMethods : []string // 允许的元列表 +AccessControlAllowOrigin : []string // 地址 +Address : string [functions] // 实例化APIconfig -newDefaultAPIConfig() : *APIConfig▼+BootstrapConfig : struct [fields] // 启动地址集合 +Addresses : []string // 最小节点阈值 +MinPeerThreshold : int // 启动时间阈值,目前为10s +Period : string [functions] // 实例化启动配置的接口 -newDefaultBootstrapConfig() : *BootstrapConfig // 存储在内存之中的filecoin配置▼+Config : struct [fields] // API相关 +API : *APIConfig // 启动相关 +Bootstrap : *BootstrapConfig // 数据存储相关 +Datastore : *DatastoreConfig // 心跳相关 +Heartbeat : *HeartbeatConfig // 挖矿相关 +Mining : *MiningConfig // 网络连接相关 +Swarm : *SwarmConfig // 钱包相关 +Wallet : *WalletConfig [methods] // 获取配置,参数为API的上述子结构 +Get(key string) : interface{}, error // 设置配置,参数为API的上述子结构 +Set(dottedKey string, jsonString string) : error // 写对应目录的配置文件 +WriteFile(file string) : error [functions] // 实例化配置,会调用各字节口的实例化 +NewDefaultConfig() : *Config // 读对应目录的配置文件 +ReadFile(file string) : *Config, error▼+DatastoreConfig : struct [fields] // 路径 +Path : string // 类型 +Type : string [functions] -newDefaultDatastoreConfig() : *DatastoreConfig▼+HeartbeatConfig : struct [fields] // 心跳周期 +BeatPeriod : string // 心跳目标 +BeatTarget : string // 昵称 +Nickname : string // 重连时间 +ReconnectPeriod : string [functions] -newDefaultHeartbeatConfig() : *HeartbeatConfig▼+MiningConfig : struct [fields] // 自动密封间隔周期 +AutoSealIntervalSeconds : uint // 区块签名地址 +BlockSignerAddress : address.Address // 矿工地址 +MinerAddress : address.Address // 存储报价 +StoragePrice : *types.AttoFIL [functions] -newDefaultMiningConfig() : *MiningConfig▼+SwarmConfig : struct [fields] // 地址 +Address : string // 转发地址 +PublicRelayAddress : string [functions] -newDefaultSwarmConfig() : *SwarmConfig▼+WalletConfig : struct [fields] // 默认钱包地址 +DefaultAddress : address.Address [functions] -newDefaultWalletConfig() : *WalletConfig▼ functions -validate(dottedKey string, jsonString string) : error -validateLettersOnly(key string, value string) : error crypto 对外提供功能 生成私钥接口 签名接口 私钥转公钥接口 从签名消息中提取公钥接口 验证消息合法性接口 主要用于地址生成、钱包相关 1234567891011121314151617181920212223242526▼ package crypto▶ imports▼ constants // 定义私钥长度32位 +PrivateKeyBytes // 定义公钥长度65位 +PublicKeyBytes▼ functions // 从签名消息中恢复公钥 +EcRecover(msg, signature []byte) : []byte, error // 比较私钥是否相同 +Equals(sk, other []byte) : bool // 生成私钥,调用GenerateKeyFromSeed +GenerateKey() : []byte, error // 生成私钥 +GenerateKeyFromSeed(seed io.Reader) : []byte, error // 由私钥得到公钥 +PublicKey(sk []byte) : []byte // 使用私钥签名 +Sign(sk, msg []byte) : []byte, error // 验证签名合法性 +Verify(pk, msg, signature []byte) : bool util/convert 提供功能 ToCid:转cid功能 functional-tests 测试脚本 flags 通过ldflags注入,表示git提交版本号 1var Commit string fixtures 提供功能 定义不同网络启动相关地址 预先分配初始网络状态,比如代币的预先分配 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354▼ package fixtures▶ imports▼ constants // 开发人员,开发网络启动相关地址 -nightlyFilecoinBootstrap0 : string -nightlyFilecoinBootstrap1 : string -nightlyFilecoinBootstrap2 : string -nightlyFilecoinBootstrap3 : string -nightlyFilecoinBootstrap4 : string // 测试网络启动相关地址 -testFilecoinBootstrap0 : string -testFilecoinBootstrap1 : string -testFilecoinBootstrap2 : string -testFilecoinBootstrap3 : string -testFilecoinBootstrap4 : string // 用户,开发网络启动相关地址 -userFilecoinBootstrap0 : string -userFilecoinBootstrap1 : string -userFilecoinBootstrap2 : string -userFilecoinBootstrap3 : string -userFilecoinBootstrap4 : string▼ variables // 开发人员,开发网络启动相关地址 +DevnetNightlyBootstrapAddrs // 测试网络启动相关地址 +DevnetTestBootstrapAddrs // 用户,开发网络启动相关地址 +DevnetUserBootstrapAddrs // 预生成测试网络地址集合 +TestAddresses : []string // 预生成测试矿工账户集合 +TestMiners : []string // 预生成地址的私钥 -testKeys : []string▼-detailsStruct : struct [fields] // 创世区块cid +GenesisCid : cid.Cid +Keys : []*types.KeyInfo +Miners : []▼ functions // 预生成的Key文件路径 +KeyFilePaths() : []string // 预生成信息 // 1 解析gen.json文件到detailsStruct结构体 // 2 追击Miners信息到TestMiners中 -init() 如下为gen.json文件,可据此预先给特定矿工分配代币 1234567891011121314{ "keys": 5, "preAlloc": [ "1000000000000", "1000000000000", "1000000000000", "1000000000000", "1000000000000" ], "miners": [{ "owner": 0, "power": 1 }]} filnet 提供功能 节点启动 定期检查连接节点,如果数量不够会链接随机节点 123456789101112location: filnet/address.go▼ package filnet▼ imports gx/ipfs/QmNTCey11oxhb1AxDnQBRHtdhap6Ctud872NjAYPYYXPuc/go-multiaddr gx/ipfs/QmRhFARzTHcFh8wUxwN5KvyTGq73FLC65EfFAhz8Ng7aGb/go-libp2p-peerstore▼ functions // 节点id转换为完整的节点信息,包括所有的多地址格式 +PeerAddrsToPeerInfos(addrs []string) : []pstore.PeerInfo, error 123456789101112131415161718192021222324252627282930313233343536373839404142location: filnet/bootstrap.go▼ package filnet▶ imports▼ variables -log▼+Bootstrapper : struct [fields] // 对应bootstrap +Bootstrap : func([]peer.ID) // 连接超时时间,用于连接随机节点 +ConnectionTimeout : time.Duration // 最小连接节点数量阈值 +MinPeerThreshold : int // 定时检查连接节点数量,小于阈值会处理 +Period : time.Duration // 随机节点切片 -bootstrapPeers : []pstore.PeerInfo -cancel : context.CancelFunc -ctx : context.Context -d : inet.Dialer -dhtBootStarted : bool -h : host.Host -r : routing.IpfsRouting -ticker : *time.Ticker [methods] // 定时调用Bootstrap 检查连接节点数量,小于阈值会处理 +Start(ctx context.Context) // 停止节点 +Stop() // 如果启动节点不够,将会尝试连接随机节点。 -bootstrap(currentPeers []peer.ID) [functions] // 实例化 +NewBootstrapper(bootstrapPeers []pstore.PeerInfo, h host.Host, d inet.Dialer, r routing.IpfsRouting, minPeer int, period time.Duration) : *Bootstrapper▼ functions -hasPID(pids []peer.ID, pid peer.ID) : bool 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"filecoin技术架构分析八:filecoin源码分析之协议层检索协议","slug":"filecoin-code-analysis-8","date":"2019-03-05T09:35:33.000Z","updated":"2019-04-05T10:29:31.037Z","comments":true,"path":"2019/03/05/filecoin-code-analysis-8/","link":"","permalink":"https://learnblockchain.cn/2019/03/05/filecoin-code-analysis-8/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第八章filecoin源码分析之协议层检索协议。","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第八章filecoin源码分析之协议层检索协议。 协议概览图 此概览图为当前的实现,整个检索的代码还没有完善 目前的逻辑比较简单,需要指定矿工、内容cid即可进行免费检索 源码信息 version master分支 619b0eb1(2019年3月2日) package retrieval location protocol/retrieval 源码分析检索矿工1234567891011121314151617181920212223242526272829303132333435363738▼ package retrieval▼ imports github.com/filecoin-project/go-filecoin/cborutil github.com/filecoin-project/go-filecoin/proofs/sectorbuilder gx/ipfs/QmTGxDz2CjBucFzPNTiWwzQmTWdrBnzqbqrMucDYMsjuPb/go-libp2p-net gx/ipfs/QmZNkThpqfVXs9GNbexPrfBbXSLNYeKrE7jwFM2oqHbyqN/go-libp2p-protocol gx/ipfs/QmbkT7eMTyXfpeyB3ZMxxcxg7XH8t6uXp49jqzz4HB7BGF/go-log gx/ipfs/Qmd52WKRSwrBK5gUaJKawryZQ5by6UbNB8KVW2Zy6JtbyW/go-libp2p-host io/ioutil▼ constants // 定义检索协议: "/fil/retrieval/free/0.0.0" -retrievalFreeProtocol▼ variables -log▼+Miner : struct [fields] // 矿工节点,参见minerNode -node : minerNode [methods] // 执行具体的检索服务 // 通过解析协议流数据,执行检索动作并返回 -handleRetrievePieceForFree(s inet.Stream) [functions] // 实例化检索矿工 // 设置处理免费检索的handle方法:handleRetrievePieceForFree +NewMiner(nd minerNode) : *Miner▼-minerNode : interface [methods] +Host() : host.Host +SectorBuilder() : sectorbuilder.SectorBuilder 检索客户12345678910111213141516171819202122232425▼ package retrieval▶ imports▼ constants // 检索内容大小限制 +RetrievePieceChunkSize▼+Client : struct [fields] -node : clientNode [methods] // 通过cid进行检索 // 通过协议流,发送检索请求以及接受检索回复和数据 +RetrievePiece(ctx context.Context, minerPeerID peer.ID, pieceCID cid.Cid) : io.ReadCloser, error [functions] // 实例化检索客户 +NewClient(nd clientNode) : *Client▼-clientNode : interface [methods] +Host() : host.Host 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"filecoin技术架构分析七:filecoin源码协议层分析之存储协议","slug":"filecoin-code-analysis-7","date":"2019-03-05T08:35:33.000Z","updated":"2019-04-05T10:29:31.069Z","comments":true,"path":"2019/03/05/filecoin-code-analysis-7/","link":"","permalink":"https://learnblockchain.cn/2019/03/05/filecoin-code-analysis-7/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第七章filecoin源码协议层分析之存储协议。","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第七章filecoin源码协议层分析之存储协议。 协议概览图 源码信息 version master分支 619b0eb1(2019年3月2日) package storage location protocol/storage 源码分析存储矿工123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202▼ package storage▶ imports▼ constants //等待密封数据前缀 -dealsAwatingSealDatastorePrefix // 存储交易协议名称:"/fil/storage/mk/1.0.0" -makeDealProtocol // 矿工数据存储前缀 -minerDatastorePrefix // 存储查询协议名称:"/fil/storage/qry/1.0.0" -queryDealProtocol // Gas及Gas限制 -submitPostGasLimit -submitPostGasPrice // 支付通道建立等待时间 -waitForPaymentChannelDuration▼ variables -log▼+Miner : struct [fields] // 交易集合 -deals : map[cid.Cid]*storageDeal // 等待密封结构体 -dealsAwaitingSeal : *dealsAwaitingSealStruct // 交易的资源对象 -dealsDs : repo.Datastore // 交易锁 -dealsLk : sync.Mutex // 存储矿工地址 -minerAddr : address.Address // 节点的Owner地址 -minerOwnerAddr : address.Address // 节点对象,有定义存储矿工必须实现的接口 -node : node // 存储矿工的高层API -porcelainAPI : minerPorcelain // 是否在生成时空证明中,以及对应的锁 -postInProcess : *types.BlockHeight -postInProcessLk : sync.Mutex // 接受交易以及拒绝交易 -proposalAcceptor : func(ctx context.Context, m *Miner, p *DealProposal) *DealResponse, error -proposalRejector : func(ctx context.Context, m *Miner, p *DealProposal, reason string) *DealResponse, error [methods] // 密封消息提交到区块链时候,所执行的回调函数,在node中执行 // 1 失败,则调用dealsAwaitingSeal.fail // 2 成功,则调用dealsAwaitingSeal.success // 3 成功之后,需要保存密封扇区信息,如果失败调用dealsAwaitingSeal.fail +OnCommitmentAddedToChain(sector *sectorbuilder.SealedSectorMetadata, err error) // 新区块产生的回调,由node调用,它将会触发新的存储证明 // 如果时空证明过期,将会在新的周期重新出发时空证明 +OnNewHeaviestTipSet(ts types.TipSet) // 由handleQueryDeal调用,返回查询结果 +Query(ctx context.Context, c cid.Cid) : *DealResponse // 生成时空证明 -generatePoSt(commRs []proofs.CommR, challenge proofs.PoStChallengeSeed) : proofs.PoStProof, []uint64, error // 获取支付通道信息 // 1 等待支付通道建立完成 // 2 获取支付通道信息并返回 // 3 支付信息包括:合约地址、支付者地址、通道信息、支付通道消息cid、支付凭证合集 -getPaymentChannel(ctx context.Context, p *DealProposal) : *paymentbroker.PaymentChannel, error // 获取新的时空证明时间 -getProvingPeriodStart() : *types.BlockHeight, error // 获取存储矿工的特定交易 -getStorageDeal(c cid.Cid) : *storageDeal // 获取存储矿工报价 -getStoragePrice() : *types.AttoFIL, error // 存储交易请求的入口方法,交易请求流的handle函数 // 1 读取流中交易请求信息 // 2 调用receiveStorageProposal处理交易请求 // 3 回复处理回复 -handleMakeDeal(s inet.Stream) //解析具体流信息,处理查询请求,会调用Query请求 -handleQueryDeal(s inet.Stream) // 从资源目录中加载交易信息到Miner实例中 -loadDeals() : error // 加载待密封的信息 -loadDealsAwaitingSeal() : error // 密封失败,更新响应信息 -onCommitFail(dealCid cid.Cid, message string) // 密封成功,更新响应信息 // 1 切换状态至Posted // 2 更新证明信息:扇区ID,副本信息,原始数据信息 -onCommitSuccess(dealCid cid.Cid, sector *sectorbuilder.SealedSectorMetadata) // 处理存储交易 // 1,获取存储交易信息 // 2,数据处理,密封 -processStorageDeal(c cid.Cid) // 处理交易请求 // 1 检查签名的正确性 // 2 检查支付信息正确性,调用validateDealPayment方法 // 3 不合法调用proposalRejector(rejectProposal)拒绝请求;合法调用proposalAcceptor(acceptProposal)回复 -receiveStorageProposal(ctx context.Context, sp *SignedDealProposal) : *DealResponse, error // 从Miner对象中存储交易信息到资源目录中 -saveDeal(proposalCid cid.Cid) : error // 存储待密封信息至资源目录 -saveDealsAwaitingSeal() : error // 提交时空证明 // 1 产生随机种子 // 2 根据时空证明输入长度,生成副本切片 // 3 随机种子+副本切片作为输入生成时空证明 // 4 调用高层接口发送消息 -submitPoSt(start, end *types.BlockHeight, inputs []generatePostInput) // 更新交易响应消息 -updateDealResponse(proposalCid cid.Cid, f func(*DealResponse)) : error // 检查支付信息的正确性 // 1 客户出价必须高于矿工报价 // 2 收款人必须为本节点矿工 // 3 支付通道总资金必须大于矿工报价 // 4 必须有交易凭证,且交易凭证总金额必须大于矿工报价 -validateDealPayment(ctx context.Context, p *DealProposal) : error [functions] // 实例化存储矿工 // 1 通过node传参赋值 // 2 指定密封成功失败的回调函数 // 3 设置交易请求以及交易查询的流handle方法 +NewMiner(ctx context.Context, minerAddr, minerOwnerAddr address.Address, nd node, dealsDs repo.Datastore, porcelainAPI minerPorcelain) : *Miner, error▼-dealsAwaitingSealStruct : struct [fields] // 从扇区id获取失败信息 +FailedSectors : map[uint64]string // 从扇区id获取交易的cid +SectorsToDeals : map[uint64][]cid.Cid // 从扇区id获取sector元数据 +SuccessfulSectors : map[uint64]*sectorbuilder.SealedSectorMetadata -l : sync.Mutex // 失败处理回调,在实例化Miner指向onCommitFail -onFail : func(dealCid cid.Cid, message string) // 成功处理回调,在实例化Miner指向onCommitSuccess -onSuccess : func(dealCid cid.Cid, sector *sectorbuilder.SealedSectorMetadata) [methods] // 对数据进行密封 -add(sectorID uint64, dealCid cid.Cid) // 密封失败处理dealsAwaitingSeal.onFail -fail(sectorID uint64, message string) // 密封成功处理dealsAwaitingSeal.onSuccess -success(sector *sectorbuilder.SealedSectorMetadata)▼-generatePostInput : struct [fields] // 副本merkle根 -commD : proofs.CommD // 原始数据merkle根 -commR : proofs.CommR // 中间数据merkle根 -commRStar : proofs.CommRStar // 扇区ID -sectorID : uint64▼-storageDeal : struct [fields] // 交易请求结构体 +Proposal : *DealProposal // 交易请求响应结构体 +Response : *DealResponse // 存储矿工高层API▼-minerPorcelain : interface [methods] // 区块高度 +ChainBlockHeight(ctx context.Context) : *types.BlockHeight, error // 获取配置 +ConfigGet(dottedPath string) : interface{}, error // 发送、查询、等待消息 +MessageQuery(ctx context.Context, optFrom, to address.Address, method string, params ...interface{}) : [][]byte, *exec.FunctionSignature, error +MessageSend(ctx context.Context, from, to address.Address, value *types.AttoFIL, gasPrice types.AttoFIL, gasLimit types.GasUnits, method string, params ...interface{}) : cid.Cid, error +MessageWait(ctx context.Context, msgCid cid.Cid, cb func(*types.Block, *types.SignedMessage, *types.MessageReceipt) error) : error▼-node : interface [methods] // 区块高度 +BlockHeight() : *types.BlockHeight, error // 区块服务,存储/查询服务 +BlockService() : bserv.BlockService // 区块时间 +GetBlockTime() : time.Duration // 主机信息 +Host() : host.Host // 扇区创建,具体包含 // 1 增加、读取piece; // 2 密封所有非空分期扇区 // 3 密封结果通过返回,通过通道channel的方式 // 4 获取扇区中最大的piece字节大小 // 5 生成时空证明 +SectorBuilder() : sectorbuilder.SectorBuilder▼ functions // 存储交易信息之后,调用processStorageDeal处理交易信息 -acceptProposal(ctx context.Context, sm *Miner, p *DealProposal) : *DealResponse, error // 获取具体文件大小 -getFileSize(ctx context.Context, c cid.Cid, dserv ipld.DAGService) : uint64, error -init() // 存储交易信息,更新响应消息,并返回 -rejectProposal(ctx context.Context, sm *Miner, p *DealProposal, reason string) : *DealResponse, error 存储客户123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128▼ package storage▼ imports▼ constants +ChannelExpiryInterval // Gas及Gas限制 +CreateChannelGasLimit +CreateChannelGasPrice +ErrDupicateDeal // 建立Voucher的周期 +VoucherInterval // 存储前缀 -clientDatastorePrefix▼ variables +Errors▼+Client : struct [fields] // 存储客户高层API -api : clientPorcelainAPI // 交易集合 -deals : map[cid.Cid]*clientDeal // 交易资源目录对象及锁 -dealsDs : repo.Datastore -dealsLk : sync.Mutex // 存储客户节点 -node : clientNode [methods] // 加载特定交易的凭证 +LoadVouchersForDeal(dealCid cid.Cid) : []*paymentbroker.PaymentVoucher, error // 发起存储交易 // 1 获取文件大小、矿工报价、区块高度、目的地址 // 2 建立支付通道 // 3 调用MakeProtocolRequest发起交易请求 // 4 检查交易响应 // 5 持久化交易响应并回复 +ProposeDeal(ctx context.Context, miner address.Address, data cid.Cid, askID uint64, duration uint64, allowDuplicates bool) : *DealResponse, error // 查询交易 // 1 获取矿工信息,地址、节点ID // 2 调用MakeProtocolRequest发起请求 +QueryDeal(ctx context.Context, proposalCid cid.Cid) : *DealResponse, error // 检查交易响应 -checkDealResponse(ctx context.Context, resp *DealResponse) : error // 判断是否为重复交易 -isMaybeDupDeal(p *DealProposal) : bool // 加载交易信息 -loadDeals() : error // 返回目标矿工地址 -minerForProposal(c cid.Cid) : address.Address, error // 持久化交易响应 -recordResponse(resp *DealResponse, miner address.Address, p *DealProposal) : error // 保存交易信息 -saveDeal(cid cid.Cid) : error [functions] // 实例化存储客户 +NewClient(nd clientNode, api clientPorcelainAPI, dealsDs repo.Datastore) : *Client, error▼+ClientNodeImpl : struct [fields] -blockTime : time.Duration -dserv : ipld.DAGService -host : host.Host [methods] //实现clientNode接口 +GetBlockTime() : time.Duration // 获取文件大小 +GetFileSize(ctx context.Context, c cid.Cid) : uint64, error // 发起协议请求 // 1 建立对应的存储交易或者请求的协议流 // 2 发起请求 +MakeProtocolRequest(ctx context.Context, protocol protocol.ID, peer peer.ID, request interface{}, response interface{}) : error [functions] // 实例化客户节点 +NewClientNodeImpl(ds ipld.DAGService, host host.Host, bt time.Duration) : *ClientNodeImpl▼-clientDeal : struct [fields] // 目标矿工,请求及响应 +Miner : address.Address +Proposal : *DealProposal +Response : *DealResponse▼-clientNode : interface // 由ClientNodeImpl实现 [methods] +GetBlockTime() : time.Duration +GetFileSize(context.Context, cid.Cid) : uint64, error +MakeProtocolRequest(ctx context.Context, protocol protocol.ID, peer peer.ID, request interface{}, response interface{}) : error▼-clientPorcelainAPI : interface [embedded] +types.Signer [methods] // 获取区块高度 +ChainBlockHeight(ctx context.Context) : *types.BlockHeight, error // 创建支付通道 // 包括源及目的地址,价格,时间,支付间隔,通道超时时间,Gas及限制 +CreatePayments(ctx context.Context, config porcelain.CreatePaymentsParams) : *porcelain.CreatePaymentsReturn, error // 获取目标地址 +GetAndMaybeSetDefaultSenderAddress() : address.Address, error // 获取矿工报价 +MinerGetAsk(ctx context.Context, minerAddr address.Address, askID uint64) : miner.Ask, error // 获取矿工Owner地址 +MinerGetOwnerAddress(ctx context.Context, minerAddr address.Address) : address.Address, error // 获取矿工节点ID +MinerGetPeerID(ctx context.Context, minerAddr address.Address) : peer.ID, error▼ functions -init() 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"filecoin技术架构分析六:filecoin源码协议层分析之hello握手协议","slug":"filecoin-code-analysis-6","date":"2019-03-04T09:35:33.000Z","updated":"2019-04-05T10:29:31.054Z","comments":true,"path":"2019/03/04/filecoin-code-analysis-6/","link":"","permalink":"https://learnblockchain.cn/2019/03/04/filecoin-code-analysis-6/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第六章filecoin源码协议层分析之hello握手协议.","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第六章filecoin源码协议层分析之hello握手协议. 目的 处理节点上线后的区块同步握手。 源码信息 version master分支 619b0eb1(2019年3月2日) package hello location protocol/hello node/node.go 源码分析数据结构 定义协议名称 12// Protocol is the libp2p protocol identifier for the hello protocol.const protocol = "/fil/hello/1.0.0" 定义hello协议消息体结构 TipSet切片 TipSet高度 创世区块cid 123456// Message is the data structure of a single message in the hello protocol.type Message struct { HeaviestTipSetCids []cid.Cid HeaviestTipSetHeight uint64 GenesisHash cid.Cid} 同步回调函数类型定义 1type syncCallback func(from peer.ID, cids []cid.Cid, height uint64) 获取Tipset函数类型定义 1type getTipSetFunc func() types.TipSet Handler结构体,当连接到其他节点的时候,其一,会发送包含本节点信息的hello 消息给对端节点; 其二, 对端也会回复一个包含对端节点信息的消息体过来。 host 对应libp2p上的主机 创世区块cid 区块同步回调函数 获取TipSet的函数 12345678910111213141516// Handler implements the 'Hello' protocol handler. Upon connecting to a new// node, we send them a message containing some information about the state of// our chain, and receive the same information from them. This is used to// initiate a chainsync and detect connections to forks.type Handler struct { host host.Host genesis cid.Cid // chainSyncCB is called when new peers tell us about their chain chainSyncCB syncCallback // getHeaviestTipSet is used to retrieve the current heaviest tipset // for filling out our hello messages. getHeaviestTipSet getTipSetFunc} 错误的创世区块 12// ErrBadGenesis is the error returned when a missmatch in genesis blocks happens.var ErrBadGenesis = fmt.Errorf("bad genesis block") 以上基本是作为hello客户端的一些定义,以下作为hello服务端的一些定义 12345// New peer connection notificationstype helloNotify Handler// 连接超时时间const helloTimeout = time.Second * 10 方法Handler 方法 流函数处理,接收远端节点的hello消息 12345678910111213141516171819202122232425func (h *Handler) handleNewStream(s net.Stream) { defer s.Close() // nolint: errcheck //获取远端节点实例 from := s.Conn().RemotePeer() var hello Message // 读取流信息到hello结构体中 if err := cbu.NewMsgReader(s).ReadMsg(&hello); err != nil { log.Warningf("bad hello message from peer %s: %s", from, err) return } // 调用processHelloMessage方法对接收到的消息进行处理 switch err := h.processHelloMessage(from, &hello); err { // 如果创世区块不一样,关闭流连接退出,不予处理 case ErrBadGenesis: log.Warningf("genesis cid: %s does not match: %s, disconnecting from peer: %s", &hello.GenesisHash, h.genesis, from) s.Conn().Close() // nolint: errcheck return case nil: // ok, noop default: log.Error(err) }} 处理hello消息 1234567891011func (h *Handler) processHelloMessage(from peer.ID, msg *Message) error { // 如果创世区块不一样,报错 if !msg.GenesisHash.Equals(h.genesis) { return ErrBadGenesis } // 调用区块同步方法 // 此回调函数实在node包实例化hello协议的时候中定义的 h.chainSyncCB(from, msg.HeaviestTipSetCids, msg.HeaviestTipSetHeight) return nil} 响应远端节点的连接,回复hello消息体 123456789101112131415161718192021222324252627func (h *Handler) getOurHelloMessage() *Message { heaviest := h.getHeaviestTipSet() height, err := heaviest.Height() if err != nil { panic("somehow heaviest tipset is empty") } return &Message{ GenesisHash: h.genesis, HeaviestTipSetCids: heaviest.ToSortedCidSet().ToSlice(), HeaviestTipSetHeight: height, }}func (h *Handler) sayHello(ctx context.Context, p peer.ID) error { s, err := h.host.NewStream(ctx, p, protocol) if err != nil { return err } defer s.Close() // nolint: errcheck //获取本节点的hello消息体 msg := h.getOurHelloMessage() //向远端节点发送消息体 return cbu.NewMsgWriter(s).WriteMsg(&msg)} helloNotify方法 hello方法,返回一个handler实例 123func (hn *helloNotify) hello() *Handler { return (*Handler)(hn)} helloNotify实现了libp2p-net/interface.go中的Notifiee接口 1234567891011121314151617func (hn *helloNotify) Connected(n net.Network, c net.Conn) { go func() { ctx, cancel := context.WithTimeout(context.Background(), helloTimeout) defer cancel() p := c.RemotePeer() // 有其他节点连接的时候调用sayHello,发送hello消息体 if err := hn.hello().sayHello(ctx, p); err != nil { log.Warningf("failed to send hello handshake to peer %s: %s", p, err) } }()}func (hn *helloNotify) Listen(n net.Network, a ma.Multiaddr) {}func (hn *helloNotify) ListenClose(n net.Network, a ma.Multiaddr) {}func (hn *helloNotify) Disconnected(n net.Network, c net.Conn) {}func (hn *helloNotify) OpenedStream(n net.Network, s net.Stream) {}func (hn *helloNotify) ClosedStream(n net.Network, s net.Stream) {} 函数 创建hello实例 12345678910111213141516171819202122232425262728293031323334353637// New creates a new instance of the hello protocol and registers it to// the given host, with the provided callbacks.func New(h host.Host, gen cid.Cid, syncCallback syncCallback, getHeaviestTipSet getTipSetFunc) *Handler { hello := &Handler{ host: h, genesis: gen, chainSyncCB: syncCallback, getHeaviestTipSet: getHeaviestTipSet, } //设置流处理回调函数 h.SetStreamHandler(protocol, hello.handleNewStream) //注册网络状态改变通知回调函数 // register for connection notifications h.Network().Notify((*helloNotify)(hello)) return hello}//上文中的helloNotify 实现了libp2p-net/interface.go中的Notifiee接口// Notifiee is an interface for an object wishing to receive// notifications from a Network.type Notifiee interface { Listen(Network, ma.Multiaddr) // called when network starts listening on an addr ListenClose(Network, ma.Multiaddr) // called when network stops listening on an addr Connected(Network, Conn) // called when a connection opened Disconnected(Network, Conn) // called when a connection closed OpenedStream(Network, Stream) // called when a stream opened ClosedStream(Network, Stream) // called when a stream closed // TODO // PeerConnected(Network, peer.ID) // called when a peer connected // PeerDisconnected(Network, peer.ID) // called when a peer disconnected} 实例化及业务逻辑 location: node/node.go Node节点中定义了hello服务 123456type Node struct { ...... HelloSvc *hello.Handler ......} 启动hello服务 123456789101112131415161718192021// Start boots up the node.func (node *Node) Start(ctx context.Context) error { ...... // Start up 'hello' handshake service // 定义区块同步的回调函数 syncCallBack := func(pid libp2ppeer.ID, cids []cid.Cid, height uint64) { // TODO it is possible the syncer interface should be modified to // make use of the additional context not used here (from addr + height). // To keep things simple for now this info is not used. // 触发调用会启动同步区块的动作 err := node.Syncer.HandleNewBlocks(context.Background(), cids) if err != nil { log.Infof("error handling blocks: %s", types.NewSortedCidSet(cids...).String()) } } //实例化hello服务 node.HelloSvc = hello.New(node.Host(), node.ChainReader.GenesisCid(), syncCallBack, node.ChainReader.Head) ......} 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"filecoin技术架构分析五:filecoin源码分析之协议层心跳协议","slug":"filecoin-code-analysis-5","date":"2019-03-04T08:35:33.000Z","updated":"2019-04-05T10:29:30.996Z","comments":true,"path":"2019/03/04/filecoin-code-analysis-5/","link":"","permalink":"https://learnblockchain.cn/2019/03/04/filecoin-code-analysis-5/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第五章filecoin源码分析之协议层心跳协议。","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第五章filecoin源码分析之协议层心跳协议。 源码信息 version master分支 619b0eb1(2019年3月2日) package metrics location metrics/heartbeat.go node/node.go 源码分析数据结构 定义心跳协议名称以及连接超时时间 123456// HeartbeatProtocol is the libp2p protocol used for the heartbeat serviceconst ( HeartbeatProtocol = "fil/heartbeat/1.0.0" // Minutes to wait before logging connection failure at ERROR level connectionFailureErrorLogPeriodMinutes = 10 * time.Minute) 定义心跳信息结构 节点的区块头 节点的区块高度 节点的昵称 是否在区块同步中(TODO) 矿工地址(如果没有挖矿,这里为零地址) 1234567891011121314151617// Heartbeat contains the information required to determine the current state of a node.// Heartbeats are used for aggregating information about nodes in a log aggregator// to support alerting and devnet visualization.type Heartbeat struct { // Head represents the heaviest tipset the nodes is mining on Head string // Height represents the current height of the Tipset Height uint64 // Nickname is the nickname given to the filecoin node by the user Nickname string // TODO: add when implemented // Syncing is `true` iff the node is currently syncing its chain with the network. // Syncing bool // Address of this node's active miner. Can be empty - will return the zero address MinerAddress address.Address} 心跳服务结构体 主机结构体:对应libp2p主机 心跳配置 区块头获取 挖矿地址获取 stream锁 stream 1234567891011121314// HeartbeatService is responsible for sending heartbeats.type HeartbeatService struct { Host host.Host Config *config.HeartbeatConfig // A function that returns the heaviest tipset HeadGetter func() types.TipSet // A function that returns the miner's address MinerAddressGetter func() address.Address streamMu sync.Mutex stream net.Stream} 定义心跳服务Option函数 函数入参为心跳服务结构体,主要用于对心跳服务结构体传参或者解析12// HeartbeatServiceOption is the type of the heartbeat service's functional options.type HeartbeatServiceOption func(service *HeartbeatService) 方法 获取心跳服务的stream实例 1234567// Stream returns the HeartbeatService stream. Safe for concurrent access.// Stream is a libp2p connection that heartbeat messages are sent over to an aggregator.func (hbs *HeartbeatService) Stream() net.Stream { hbs.streamMu.Lock() defer hbs.streamMu.Unlock() return hbs.stream} 设置心跳服务的stream实例 123456// SetStream sets the stream on the HeartbeatService. Safe for concurrent access.func (hbs *HeartbeatService) SetStream(s net.Stream) { hbs.streamMu.Lock() defer hbs.streamMu.Unlock() hbs.stream = s} 定时确认连接性,并调用运行心跳服务 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657// Start starts the heartbeat service by, starting the connection loop. The connection// loop will attempt to connected to the aggregator service, once a successful// connection is made with the aggregator service hearbeats will be sent to it.// If the connection is broken the heartbeat service will attempt to reconnect via// the connection loop. Start will not return until context `ctx` is 'Done'.func (hbs *HeartbeatService) Start(ctx context.Context) { log.Debug("starting heartbeat service") rd, err := time.ParseDuration(hbs.Config.ReconnectPeriod) if err != nil { log.Errorf("invalid heartbeat reconnectPeriod: %s", err) return } //启动重连定时器 reconTicker := time.NewTicker(rd) defer reconTicker.Stop() // Timestamp of the first connection failure since the last successful connection. // Zero initially and while connected. var failedAt time.Time // Timestamp of the last ERROR log (or of failure, before the first ERROR log). var erroredAt time.Time for { select { case <-ctx.Done(): return case <-reconTicker.C: //重连定时周期到,重新连接 if err := hbs.Connect(ctx); err != nil { // Logs once as a warning immediately on failure, then as error every 10 minutes. now := time.Now() logfn := log.Debugf if failedAt.IsZero() { // First failure since connection failedAt = now erroredAt = failedAt // Start the timer on raising to ERROR level logfn = log.Warningf } else if now.Sub(erroredAt) > connectionFailureErrorLogPeriodMinutes { logfn = log.Errorf erroredAt = now // Reset the timer } failureDuration := now.Sub(failedAt) logfn("Heartbeat service failed to connect for %s: %s", failureDuration, err) // failed to connect, continue reconnect loop continue } failedAt = time.Time{} // we connected, send heartbeats! // Run will block until it fails to send a heartbeat. //如果连接成功,运行心跳服务 if err := hbs.Run(ctx); err != nil { log.Warning("disconnecting from aggregator, failed to send heartbeat") continue } } }} 运行心跳服务 12345678910111213141516171819202122232425262728293031323334// Run is called once the heartbeat service connects to the aggregator. Run// send the actual heartbeat. Run will block until `ctx` is 'Done`. An error will// be returned if Run encounters an error when sending the heartbeat and the connection// to the aggregator will be closed.func (hbs *HeartbeatService) Run(ctx context.Context) error { bd, err := time.ParseDuration(hbs.Config.BeatPeriod) if err != nil { log.Errorf("invalid heartbeat beatPeriod: %s", err) return err } //启动心跳定时器 beatTicker := time.NewTicker(bd) defer beatTicker.Stop() //通过encoder进行流写入 // TODO use cbor instead of json encoder := json.NewEncoder(hbs.stream) for { select { case <-ctx.Done(): return nil case <-beatTicker.C: //心跳定时周期到,调用Beat方法获取心跳参数 hb := hbs.Beat() //写入流,发起心跳 if err := encoder.Encode(hb); err != nil { //发生错误会关闭流连接 hbs.stream.Conn().Close() // nolint: errcheck return err } } }} 获取心跳参数 1234567891011121314151617// Beat will create a heartbeat.func (hbs *HeartbeatService) Beat() Heartbeat { nick := hbs.Config.Nickname ts := hbs.HeadGetter() tipset := ts.ToSortedCidSet().String() height, err := ts.Height() if err != nil { log.Warningf("heartbeat service failed to get chain height: %s", err) } addr := hbs.MinerAddressGetter() return Heartbeat{ Head: tipset, Height: height, Nickname: nick, MinerAddress: addr, }} 心跳流连接1234567891011121314151617181920212223242526272829303132333435363738// Connect will connects to `hbs.Config.BeatTarget` or returns an errorfunc (hbs *HeartbeatService) Connect(ctx context.Context) error { log.Debugf("Heartbeat service attempting to connect, targetAddress: %s", hbs.Config.BeatTarget) targetMaddr, err := ma.NewMultiaddr(hbs.Config.BeatTarget) if err != nil { return err } pid, err := targetMaddr.ValueForProtocol(ma.P_P2P) if err != nil { return err } peerid, err := peer.IDB58Decode(pid) if err != nil { return err } // Decapsulate the /p2p/<peerID> part from the target // /ip4/<a.b.c.d>/p2p/<peer> becomes /ip4/<a.b.c.d> targetPeerAddr, _ := ma.NewMultiaddr( fmt.Sprintf("/p2p/%s", peer.IDB58Encode(peerid))) targetAddr := targetMaddr.Decapsulate(targetPeerAddr) hbs.Host.Peerstore().AddAddr(peerid, targetAddr, pstore.PermanentAddrTTL) // 建立心跳服务流 s, err := hbs.Host.NewStream(ctx, peerid, HeartbeatProtocol) if err != nil { log.Debugf("failed to open stream, peerID: %s, targetAddr: %s %s", peerid, targetAddr, err) return err } log.Infof("successfully to open stream, peerID: %s, targetAddr: %s", peerid, targetAddr) //设置流函数 hbs.SetStream(s) return nil} 函数 向心跳服务结构体传参,用于设置获取矿工地址函数 123456// WithMinerAddressGetter returns an option that can be used to set the miner address getter.func WithMinerAddressGetter(ag func() address.Address) HeartbeatServiceOption { return func(service *HeartbeatService) { service.MinerAddressGetter = ag }} 获取默认的矿工地址 123func defaultMinerAddressGetter() address.Address { return address.Address{}} 实例化心跳服务,具体的实例化在node包中实现。 12345678910111213141516// NewHeartbeatService returns a HeartbeatServicefunc NewHeartbeatService(h host.Host, hbc *config.HeartbeatConfig, hg func() types.TipSet, options ...HeartbeatServiceOption) *HeartbeatService { srv := &HeartbeatService{ Host: h, Config: hbc, HeadGetter: hg, MinerAddressGetter: defaultMinerAddressGetter, } // 设置心跳服务的获取矿工属性,这会覆盖到上面设置的默认矿工地址 for _, option := range options { option(srv) } return srv} 实例化及业务逻辑 主要由node调用,location:node/node.go,主要逻辑如下 在node的启动方法中,调用node.setupHeartbeatServices方法,建立心跳服务 12345678910// Start boots up the node.func (node *Node) Start(ctx context.Context) error { ...... if err := node.setupHeartbeatServices(ctx); err != nil { return errors.Wrap(err, "failed to start heartbeat services") } return nil} 建立心跳服务,具体见如下注释 1234567891011121314151617181920212223242526272829303132333435func (node *Node) setupHeartbeatServices(ctx context.Context) error { // 设置“矿工地址获取函数” mag := func() address.Address { addr, err := node.miningAddress() // the only error miningAddress() returns is ErrNoMinerAddress. // if there is no configured miner address, simply send a zero // address across the wire. if err != nil { return address.Address{} } return addr } // 存在心跳目标的时候,实例化心跳服务实例 // start the primary heartbeat service if len(node.Repo.Config().Heartbeat.BeatTarget) > 0 { //调用metrics包中的建立心跳服务实例、以及启动心跳服务实例方法 hbs := metrics.NewHeartbeatService(node.Host(), node.Repo.Config().Heartbeat, node.ChainReader.Head, metrics.WithMinerAddressGetter(mag)) go hbs.Start(ctx) } // 确认是否用户有通过环境变量配置额外的心跳告警服务(自定义指向其他节点),根据用户配置的数目,拉起对应的多线程心跳服务。 // check if we want to connect to an alert service. An alerting service is a heartbeat // service that can trigger alerts based on the contents of heatbeats. if alertTarget := os.Getenv("FIL_HEARTBEAT_ALERTS"); len(alertTarget) > 0 { ahbs := metrics.NewHeartbeatService(node.Host(), &config.HeartbeatConfig{ BeatTarget: alertTarget, BeatPeriod: "10s", ReconnectPeriod: "10s", Nickname: node.Repo.Config().Heartbeat.Nickname, }, node.ChainReader.Head, metrics.WithMinerAddressGetter(mag)) go ahbs.Start(ctx) } return nil} 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"filecoin技术架构分析四:filecoin源码顶层架构分析","slug":"filecoin-code-analysis-4","date":"2019-02-28T08:35:33.000Z","updated":"2019-04-05T10:29:30.994Z","comments":true,"path":"2019/02/28/filecoin-code-analysis-4/","link":"","permalink":"https://learnblockchain.cn/2019/02/28/filecoin-code-analysis-4/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第四章filecoin源码顶层架构分析。","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第四章filecoin源码顶层架构分析。 题外话——关于竞争力 网络技术的高速发展带领我们进入了知识大爆炸、技术快速跃迁的时代,5G已经开始走向商业落地,网络速率的再次跃迁给我们带来了无限的想象空间,全息投影、即时翻译、远程医疗、人工智能等等会更加成熟落地?路由器在个人家庭中的角色可能会发生变化?IOT万物互联的时代将会真正到来?区块链的TPS提升?高速网络下的云应用、大数据会出现什么新的玩法? 笔者想说的是,整个世界都在急速变化,在波涛汹涌的竞争浪潮之中,如何保持自己的竞争力。我偶尔会问同事、朋友,你与刚毕业的大学生相比,优势在哪里? 笔者认为如下两点才是在这个高速时代的真正竞争力,个人如此,公司团队亦如此。 高效的学习能力 高维的思维能力 以上为笔者观点,也欢迎大家探讨。在分析具体架构之前,笔者在4.2.1中分享自己的分析思路,我认为这也许也值得分享。 filecoin源码顶层架构概览及分析思路分析思路 终于进入到源码分析环节了,其实回顾一下前面三章,filecoin的概念及通用语言可以总结为filecoin的本质,分析源码的过程归根接底还是理解设计者的意图,第三章filecoin开发网络的实战使用对于笔者来说也是为了更清晰地对filecoin本质及设计意图进行深入理解。 分析总思路为:抓住本质分析,理解设计者意图 自上而下逐层分析,从抽象到具体 自下而上反向总结,从具体到抽象 分析过程分为三大步骤 第一步,理解filecoin本质及设计目的(前面三章) 第二步,理解filecoin的顶层架构设计(本章),反向加深对filecoin本质的理解 第三步,各层的具体源码分析(后面章节),反向加深对filecoin本质的理解 详细参见下图 在顶层源码中分为go-filecon和rust-fil-proofs。分别为主框架和存储证明部分,本文主要分析go-filecoin源码的顶层框架。 filecoin顶层架构概览架构图12345678910111213141516171819202122232425262728 ┌─────────────────────────────────────┐ │ │ Network │ network (gossipsub, bitswap, etc.) │ | | \\/ │ │ |_| /\\ └─────▲────────────▲────────────▲─────┘ │ │ │ ┌────────────────────────────┐ ┌─────▼────┐ ┌─────▼─────┐ ┌────▼─────┐ │ │ │ │ │ │ │ │ │ Commands / REST API │Protocols │ Storage │ │ Mining │ │Retrieval │ │ │ │ Protocol │ │ Protocol │ │ Protocol │ └────────────────────────────┘ │ │ │ │ │ │ │ └──────────┘ └───────────┘ └──────────┘ │ │ │ │ │ └──────────┬─┴─────────────┴───────────┐ │ ▼ ▼ ▼ ┌────────────────────────────────┐ ┌───────────────────┬─────────────────┐ Internal │ Core API │ │ Porcelain │ Plumbing │ API │ │ ├───────────────────┘ │ └────────────────────────────────┘ └─────────────────────────────────────┘ │ │ ┌─────────┴────┬──────────────┬──────────────┬─┴────────────┐ ▼ ▼ ▼ ▼ ▼ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ │ │ │ │ │ │ │ Core │ Message │ │ Chain │ │ Processor │ │ Block │ │ Wallet │ │ Pool │ │ Store │ │ │ │ Service │ │ │ │ │ │ │ │ │ │ │ │ │ └────────────┘ └────────────┘ └────────────┘ └────────────┘ └────────────┘ 官方给出的如上架构概览图是小于实际源码的,但是不影响理解。 官方的spec项目中,有较多文档说明已经滞后于源码,其引用的源码有些已经从go-filecoin源码中消失了,想深入分析的朋友建议可以结合源码和文档同步进行看。 本文后面的章节中,只会简述各个层的设计目的,每一层的具体源码分析,将放到后面章节分享给大家。 IPFS与filecoin在技术架构层面的关系 IPFS与filecoin同样采用IPLD结构,数据结构是互通的,简而言之,在IPFS之上存储的数据,filecoin可以读取。filecoin存储的未密封数据,IPFS也是可以读取的。 IPFS与filecoin网络部分均复用libp2p部分。 filecoin复用了大量IPFS组件,比如CID、IPLD、bitswap等等。 网络层 网络层的实现依赖协议实验室的libp2p项目,如果不熟悉的可以先简单记住如下要点,后面笔者考虑视情况补充IPFS/libp2p的相关分享。 libp2p的网络层实现了节点之间的联通性问题,包括节点发现、NAT穿透、pubsub、relay等。 libp2p的路由层的主要目的,包括节点路由、内容路由、DHT键值存储。 multistream需要理解,filecoin的协议层之协议定义就是基于mulitistream的。 filecoin网络层的目的 处理请求信息、回复响应信息,包括存储订单处理、检索请求处理、区块同步等等。 协议层协议层主要处理应用级的逻辑,状态切换等,具体会通过api层调用具体的core服务进行处理。 hello握手协议 协议名称: /fil/hello/1.0.0 目的: 本节点上线,向其他节点发起hello握手请求,进而进行区块同步。 响应其他新上线的节点hello握手请求,触发其进行区块同步。 存储协议存储矿工 协议名称:/fil/storage/mk/1.0.0、 /fil/storage/qry/1.0.0 目的: 接受客户发起的订单交易请求、查询订单请求,会提交对应处理状态到区块链上(包括清单处理成功或失败;密封成功或者失败等等)。 更新本地的密封或者订单状态。 存储客户 采用上述的/fil/storage/mk/1.0.0、 /fil/storage/qry/1.0.0协议,建立multistream,向矿工发起交易或者查询交易状态。 检索协议检索矿工 协议名称:/fil/retrieval/free/0.0.0 目的: 接受客户的检索请求,并响应处理 注意:目前仅仅支持free检索,白皮书所描述的完整检索功能尚未实现。 检索客户 采用上述的/fil/retrieval/free/0.0.0协议,建立multistream,向矿工发起检索请求。 心跳协议 协议名称:fil/heartbeat/1.0.0 目的: 启动之后向指定节点发起心跳。 如前面3.2.1章节中的设置Nick Name,以及激活极点,都属于心跳协议实现的。 REST/CMD 这个应该不用多解释,提供cmd或者REST接口供用户操作具体节点。第三章中的开发网络使用基本都是使用的CMD方式。 内部api层node对象 filecoin的node节点是一个上帝对象,从下面Node的结构可以看出,基本贯穿了filecoin的整个业务。 为了解决耦合性问题,尤其是后续轻节点的实现,官方已经开始将原有的api包,往plumbing包以及porcelain包迁移,这样做的目的是让系统具备更好的解耦性,以满足更灵活的需求。 plumbing和 porcelain模式也是借鉴git的思维。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273▼+Node : struct [fields] +AddNewlyMinedBlock : newBlockFunc +BlockSub : ps.Subscription +Blockstore : bstore.Blockstore +Bootstrapper : *filnet.Bootstrapper +ChainReader : chain.ReadStore +Consensus : consensus.Protocol +Exchange : exchange.Interface +HeaviestTipSetCh : chan interface{} +HeaviestTipSetHandled : func() +HelloSvc : *hello.Handler +MessageSub : ps.Subscription +MiningScheduler : mining.Scheduler +MsgPool : *core.MessagePool +OfflineMode : bool +OnlineStore : *hamt.CborIpldStore +PeerHost : host.Host +Ping : *ping.PingService +PorcelainAPI : *porcelain.API +PowerTable : consensus.PowerTableView +Repo : repo.Repo +RetrievalClient : *retrieval.Client +RetrievalMiner : *retrieval.Miner +Router : routing.IpfsRouting +StorageMiner : *storage.Miner +StorageMinerClient : *storage.Client +Syncer : chain.Syncer +Wallet : *wallet.Wallet -blockTime : time.Duration -blockservice : bserv.BlockService -cancelMining : context.CancelFunc -cancelSubscriptionsCtx : context.CancelFunc -cborStore : *hamt.CborIpldStore -host : host.Host -lookup : lookup.PeerLookupService -mining -miningCtx : context.Context -miningDoneWg : *sync.WaitGroup -sectorBuilder : sectorbuilder.SectorBuilder [methods] +BlockHeight() : *types.BlockHeight, error +BlockService() : bserv.BlockService +CborStore() : *hamt.CborIpldStore +ChainReadStore() : chain.ReadStore +CreateMiner(ctx context.Context, accountAddr address.Address, gasPrice types.AttoFIL, gasLimit types.GasUnits, pledge uint64, pid libp2ppeer.ID, collateral *types.AttoFIL) : *address.Address, error +GetBlockTime() : time.Duration +Host() : host.Host +Lookup() : lookup.PeerLookupService +MiningSignerAddress() : address.Address +MiningTimes() : time.Duration, time.Duration +NewAddress() : address.Address, error +SectorBuilder() : sectorbuilder.SectorBuilder +SetBlockTime(blockTime time.Duration) +Start(ctx context.Context) : error +StartMining(ctx context.Context) : error +Stop(ctx context.Context) +StopMining(ctx context.Context) -addNewlyMinedBlock(ctx context.Context, b *types.Block) -cancelSubscriptions() -getLastUsedSectorID(ctx context.Context, minerAddr address.Address) : uint64, error -getMinerActorPubKey() : []byte, error -handleNewHeaviestTipSet(ctx context.Context, head types.TipSet) -handleNewMiningOutput(miningOutCh chan mining.Output) -handleSubscription(ctx context.Context, f pubSubProcessorFunc, fname string, s ps.Subscription, sname string) -isMining() : bool -miningAddress() : address.Address, error -miningOwnerAddress(ctx context.Context, miningAddr address.Address) : address.Address, error -saveMinerConfig(minerAddr address.Address, signerAddr address.Address) : error -setIsMining(isMining bool) -setupMining(ctx context.Context) : error [functions] +New(ctx context.Context, opts ...ConfigOpt) : *Node, error api包 这基本上是早期实现的api接口,对应4.2.2.1中的Core API,严重依赖于Node,耦合性大。现在在逐步迁移。感兴趣可以参照如下源码逐层深入去看。 12345678910111213141516171819package: apilocation: api/api.gotype API interface { Actor() Actor Address() Address Client() Client Daemon() Daemon Dag() Dag ID() ID Log() Log Miner() Miner Mining() Mining Paych() Paych Ping() Ping RetrievalClient() RetrievalClient Swarm() Swarm Version() Version} plumbing和porcelain包 plumbing api简而言之,是实现底层的公共api,其不依赖于Node的实现。 而porcelain api则是在plumbing api之上,更偏应用级的调用,同样不依赖于Node实现。 1源码分别参见go-filecoin目录下,./plumbing和./porcelain。 core服务层 关于核心业务调度以及业务持久化的底层处理基本都在这一层,包含但不限于如下服务。 Message pool 消息池主要保存还未上链的消息。 Chain store 链存储主要持久化链信息,注意同步区块的逻辑是在协议层的hello协议所出发的。 Processor 处理事务消息如何驱动状态转换。 Block service 负责IPLD数据的内容寻址,包括区块链等。 Wallet 钱包管理。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"filecoin技术架构分析三:filecoin开发网络使用","slug":"filecoin-code-analysis-3","date":"2019-02-22T08:35:33.000Z","updated":"2019-04-05T10:29:31.062Z","comments":true,"path":"2019/02/22/filecoin-code-analysis-3/","link":"","permalink":"https://learnblockchain.cn/2019/02/22/filecoin-code-analysis-3/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第三章filecoin开发网络使用。","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第三章filecoin开发网络使用。 filecoin开发网络使用辅助资源 Filecoin状态: https://stats.kittyhawk.wtf 网络 存储实时价格 FIL/GB/Month 当前存储容量 GB 当前网络利用率 检索平均价格 激活节点以及分布图 存储平均价格曲线 best tipset 存储矿工 存储矿工数量变化曲线 存储矿工共识结果 近30天的矿工top图 检索矿工 平均检索价格 平均检索时间 平均检索容量 FIL指数 流通FIL及抵押FIL变化图 FIL地址总数 FIL总抵押数及对应存储空间 FIL总数上升曲线图 FIL区块奖励下降曲线图 Filecoin区块浏览器: http://user.kittyhawk.wtf:8000 Chain信息 BestBlock信息 Actor合约信息 获取FIL用于抵押或支付:http://user.kittyhawk.wtf:9797 获取mock FIL代币 Dashboard: http://user.kittyhawk.wtf:8010 Network概览,最新区块信息 区块浏览器链接 Genesis File: http://user.kittyhawk.wtf:8020/genesis.car 创始文件,用于初始化filecoin资源 Prometheus Endpoint: http://user.kittyhawk.wtf:9082/metrics 一些技术指标,比如内存、进程、线程等 Connected Nodes PeerID’s: http://user.kittyhawk.wtf:9082/nodes 连接的节点信息 使用接入filecoin开发网络 初始化filecoin资源目录 如果之前有运行过filecoin,想重新开始,需要删除filecoin资源,同时重新初始化是需要重新花时间同步开发网区块信息的。 1rm -rf ~/.filecoin 初始化资源目录,使用–devnet-user表示连接至开发网 123waynewyang:Downloads waynewyang$ go-filecoin init --devnet-user --genesisfile=http://user.kittyhawk.wtf:8020/genesis.carinitializing filecoin node at ~/.filecoinwaynewyang:Downloads waynewyang$ 启动filecoin进程,接入开发网 1234go-filecoin daemon//如果开发者,需要接入nightly devnet,请设置环境变量后启动filecoinenv FIL_USE_SMALL_SECTORS=true go-filecoin daemon 检查连接性 go-filecoin swarm peers 查看已经连接的节点 1234567891011waynewyang:filecoin waynewyang$ go-filecoin swarm peers/ip4/115.238.154.84/tcp/19109/ipfs/Qmb6ZYi7GLFAje3UekGZ2LZymck7RVHKSKb1bhPzzPTQkm/ip4/115.238.154.84/tcp/41187/ipfs/QmZ9UHdU2fwDN7emWW8AeaUdkF9fT7RwJrnbbdcQFUq9X6/ip4/123.134.67.81/tcp/6000/ipfs/QmccrEQsauwge4BZQeN1jBtFyd7dnTi4pSDvkikMWaFccw/ip4/123.134.67.82/tcp/6000/ipfs/QmWuA1AW4qDqztDrwo2pBgT2au67BJbGtEzWRufbc8isgn/ip4/123.134.67.83/tcp/6000/ipfs/QmbPCabGcngs3bCgMK8dC3w9pjoyPd1NFyDhbkgLyT2eJ7/ip4/123.134.67.85/tcp/6000/ipfs/QmUqSSZrwfSUU3vfw7D1UyKaLvEv1Ykcvx3ntvSXWaA7kj/ip4/123.134.67.86/tcp/6000/ipfs/QmPrz2z764AVaHivM7iX2JqRw5EdE3jcZTrjwVxS4VukyK/ip4/123.134.67.87/tcp/6000/ipfs/QmTxVFq3u7qPxsXFQdoyqPrdh6meW6JBGkSJ8HJXAiMUfh/ip4/123.134.67.88/tcp/6000/ipfs/QmXAVRPYu57XDwJHszn9U9x1KtTwPsJBaS1mTdNZzAQVyQ/ip4/123.134.67.89/tcp/6000/ipfs/Qmc5umx9R3bpD5VxvUmfyLoDz5wtVT43p5xjSEmTe26qTD go-filecoin ping peerID 确认连通性 1234567waynewyang:filecoin waynewyang$ go-filecoin ping QmW4Z8p7FCspLV1FeTRW6uCNApUXqkm8xYYw4yuBnqBGeBPING <peer.ID Qm*nqBGeB>Pong received: time=245.12 msPong received: time=245.61 msPong received: time=251.98 msPong received: time=245.69 msPong received: time=255.64 ms 给你的filecoin Node设置昵称 1234waynewyang:filecoin waynewyang$ go-filecoin config heartbeat.nickname "wwwarsyuncom""wwwarsyuncom"waynewyang:filecoin waynewyang$ go-filecoin config heartbeat.nickname"wwwarsyuncom" 激活节点 1go-filecoin config heartbeat.beatTarget "/dns4/stats-infra.kittyhawk.wtf/tcp/8080/ipfs/QmUWmZnpZb6xFryNDeNU7KcJ1Af5oHy7fB9npU67sseEjR" 在 https://stats.kittyhawk.wtf/ 查看filecoin网络,节点已经激活 获取Mock FIL用于测试 FIL用于矿工抵押;或者作为客户进行交易需要 注意:开发网目前运行的都是全节点,获取mock FIL需要建立在本地区块数据同步完成的基础上进行,必须同步完区块之后才能生效,根据个人机器配置情况,这需要较长一段时间。 go-filecoin message wait ${MESSAGE_CID} 本质上是转账交易,wiki上说明的是等待30s,但是这是在本地区块数据同步完成的基础上才行的。 笔者已提交建议给官方,在wiki上更为清晰地表述。 1234567891011121314151617181920212223242526272829303132333435waynewyang:filecoin waynewyang$ go-filecoin wallet addrs lsfcq09qtmrxgq5sdr95gs93tx79u9uymdwfdsaphpawaynewyang:filecoin waynewyang$ export WALLET_ADDR=`go-filecoin wallet addrs ls`waynewyang:filecoin waynewyang$ MESSAGE_CID=`curl -X POST -F "target=${WALLET_ADDR}" "http://user.kittyhawk.wtf:9797/tap" | cut -d" " -f4` % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed100 232 100 50 100 182 48 177 0:00:01 0:00:01 --:--:-- 177waynewyang:go-filecoin waynewyang$ go-filecoin message wait ${MESSAGE_CID}{ "meteredMessage": { "message": { "to": "fcqm0u932ja5thlsy4dgpz5urlapk8qhtd0clqv5e", "from": "fcq09sqhrd4gls86muuenzvqdc37mzscagapjveal", "nonce": "rQQ=", "value": "1000", "method": "", "params": null }, "gasPrice": "0", "gasLimit": "AA==" }, "signature": "WKA+eRY7XCQlSmallzoFu8Tps7NZ2AOAKLRFo21rTERFYJqXJT2qEWZ8sFvm6ZShR5syb7RSAJnDp4Am2Vzp0gE="}{ "exitCode": 0, "return": null, "gasAttoFIL": "0"}waynewyang:filecoin waynewyang$ go-filecoin wallet balance fcq09qtmrxgq5sdr95gs93tx79u9uymdwfdsaphpa1000waynewyang:filecoin waynewyang$ go-filecoin wallet balance ${WALLET_ADDR}1000 矿工操作存储矿工 创建存储矿工示例,需要等待1分钟左右 抵押10个扇区的存储空间(当前默认每个扇区256MiB) 支付100个FIL为担保 gas价格为0 限制gas消耗最大为1000个FIL 12waynewyang:filecoin waynewyang$ go-filecoin miner create 10 100 --price=0 --limit=1000 --peerid `go-filecoin id | jq -r '.ID'`fcqjge872spqrgtm8dhlndjgfhhuxzx0y3ujvxxsl //所返回的就是矿工地址minerAddress 启动挖矿 12waynewyang:filecoin waynewyang$ go-filecoin mining startStarted mining 收益之一: 启动挖矿之后就可以参与挖区块奖励 查询区块头 12waynewyang:go-filecoin waynewyang$ go-filecoin chain head[{"/":"zDPWYqFD2mBqLx7bwQNdeVoMxj6SC5HxzorZAoXpT6xjaythnENw"}] 查询具体区块信息 12345678go-filecoin show block <blockID>waynewyang:go-filecoin waynewyang$ go-filecoin show block zDPWYqFD2mBqLx7bwQNdeVoMxj6SC5HxzorZAoXpT6xjaythnENwBlock DetailsMiner: fcq0y72meekgwnvchwml0uzx759q25nk0rqc47retWeight: 293567.552Height: 10787Nonce: 0 收益之二:创建报价单ask 12345678910111213141516171819202122232425262728293031321) 获取矿工地址export MINER_ADDR=`go-filecoin config mining.minerAddress | tr -d \\"` 2) 设置矿机Ownerexport MINER_OWNER_ADDR=`go-filecoin miner owner $MINER_ADDR`3) 创建报价单,价格0.000000001 FIL/byte/block, 交易费0,gas限制1000,提供2880个block空间存储go-filecoin miner set-price --from=$MINER_OWNER_ADDR --miner=$MINER_ADDR --price=0 --limit=1000 0.000000001 2880 # output: CID of the ask发布报价单,需要打包进去区块waynewyang:filecoin waynewyang$ go-filecoin miner set-price --from=$MINER_OWNER_ADDR --miner=$MINER_ADDR --price=0 --limit=1000 0.000000001 15315Set price for miner fcqjge872spqrgtm8dhlndjgfhhuxzx0y3ujvxxsl to 0.000000001. Published ask, cid: zDPWYqFCxL3VW3xzmHhCBqPTvhoQa53pn6DzV3uY23jNL76za1Vt. Ask confirmed on chain in block: zDPWYqFD7wjnj74sdB9HqupDmWmpPPEvygB14Pbo6rQC7ho2687D.4) 查询区块信息(第三步中是zDPWYqFD7wjnj74sdB9HqupDmWmpPPEvygB14Pbo6rQC7ho2687D)可以找到对应报价单信息waynewyang:filecoin waynewyang$ go-filecoin show block zDPWYqFD7wjnj74sdB9HqupDmWmpPPEvygB14Pbo6rQC7ho2687D --enc=json{"miner":"fcqnam6n2qml2eyngws25srzvhcdf0t8gcgrsvnrk","ticket":"AM0p5IC9ph+o9dTwd/MXYdeOJW25PfDwhTgonNRkSP4=","parents":[{"/":"zDPWYqFCwNWHJXdeXcjx7ipUvRKFq5WhLbtSm6ESuNufkLuGiAgW"}],"parentWeight":"kujBrQE=","height":"2Hc=","nonce":"AA==","messages":[{"meteredMessage":{"message":{"to":"fcqp606qfk5gwmq6ac24g4mhv3cr8zzf67vqkpulh","from":"fcqr89lj0lvduj475zw002j6q5yrl30ks7uep2p5e","nonce":"Ag==","value":"215.6046624","method":"createChannel","params":"glYAABXMs4C0hXqcW94YGyVKxii6SLQhQ6HoAg=="},"gasPrice":"0","gasLimit":"rAI="},"signature":"vHzwO73TvM8MW1FKg8Qgfy/IP+wfJIQkEK0ExBB75gBbPMhv6GiU4aBq1T2Gb2OeMfrch8Zg3EFOJd0uUJltwAE="},{"meteredMessage":{"message":{"to":"fcqafmqgvzkzpvc6wjxecm7gsweuawjv8t6falk6r","from":"fcqr89lj0lvduj475zw002j6q5yrl30ks7uep2p5e","nonce":"Aw==","value":"100","method":"createMiner","params":"g0EKWEEEiA8ArEoyzhjWwijpTWYqDsOFfwxa2F0pUfOyRI/6yY28OD4QHcwUdb3a9omX9DNxVzdS2a8pWgiLNowe9wYVcFgiEiD3rKfg/NyDnrLF9IGxfp6U72jZxuniXlPcv5SG5OZHrA=="},"gasPrice":"0","gasLimit":"6Ac="},"signature":"/X/87zil8InOeLeQ6kqkqnpg7mP/e5jMaaVS4LRMIdYN84HTbABBpvt6quRqVQsadJnqOW7mn+6NA+2d9FDjPAA="},{"meteredMessage":{"message":{"to":"fcqp606qfk5gwmq6ac24g4mhv3cr8zzf67vqkpulh","from":"fcqmqr5f2a5qnwnvftpuzd6sjfy5tcq5dd0k24h85","nonce":"Ug==","value":"0.008596","method":"createChannel","params":"glYAAIVV3axUhfe7OGwwSH/IIONRcbinQ/OWAQ=="},"gasPrice":"0","gasLimit":"rAI="},"signature":"dxhSaVRvFBtdrbnEByza7a5JqLzm6n6rVZYGuFN6zegCTMDKbGGh++EvVmWo0WSbdcUo2vB/jFTgqzATh9+1NQA="},{"meteredMessage":{"message":{"to":"fcqugc6nql2eqglfwq0dw7ep7l9a07jacqgstely7","from":"fcq0nmdcq7updgwc3uh2lz2rnjms7gprdggcvxjqj","nonce":"BA==","value":null,"method":"commitSector","params":"hUECWCCFzRGqHyJ3VWk3GueHzfkcWF218hOqRGtLxsJ0oJ3pXVgg6qrxCGo6SSjyUSbJWVKPKGaY/wrymC21t5LSScCNpQBYIOeZkdzp7lPt8Fh/Sdl9YqJ8BCaJ7etWEnDnLzRnV/geWQGAlTMp95t1Hh61eFBmzy6Ex/Ee1cso7Cethz+Z2EHCfhi5UzOMeLqeA/Wfypcnrw15mF4OrYR8648RXx6jp8svbgZ6Jg9fP/q0RukszZ/SD9f0pCMg2N/xt5hVIPG7jowSCfkj//CpdRj1GRPLzXvzyWmW4SgKR4lNpJNnmuiXSe8nYLsZgY2v8xy4NB448e/slxh7D4NQPanCoN3WO10oBR42ZxeCZY6stq+JfwucGr5OajgXSK2rGwz/Sj+GYpMbtgpfxSd4Z+jZ6mnoY03NaIHvwnDKchRz797lFL3so6AQRRnctN3Pl7LSn52YA0EOkmhJLMev6DKBWEqSfjXTY4AJSJ7RmGq88BXoHzwGjndRj0QHFtSTjHIoxF9uN86zB6gfSS+A7ZviuTvfturtKee243b9OIojIf2ne2hF8+7PSIwCp5FPLXEqR/UtXnJ6pxjBKhF36k3FRdzsxo52DMImgPhluGWI3xRhpIMmFNDMNynOBQ9F6mnR8fRBdPKB"},"gasPrice":"0","gasLimit":"rAI="},"signature":"hEluqbFG8TTSeeJyfOs10fZD/gOsrnFV8QgRAb4mhSFlzYpcijT9ye1yUYam5hcsW1eq1MFRfVGHhqYYUZ4LYwA="},{"meteredMessage":{"message":{"to":"fcqjge872spqrgtm8dhlndjgfhhuxzx0y3ujvxxsl","from":"fcqm0u932ja5thlsy4dgpz5urlapk8qhtd0clqv5e","nonce":"AQ==","value":"0","method":"addAsk","params":"gkWAlOvcA0I70w=="},"gasPrice":"0","gasLimit":"6Ac="},"signature":"IGUvf7CZ8lKDSTyzbg5kyArIPv8TIoFEWpA3ihRC7I86NFvgebCdEKQ6PqYhTJj1GQK/+JF28kCinXFN/9G8FgE="},{"meteredMessage":{"message":{"to":"fcqp606qfk5gwmq6ac24g4mhv3cr8zzf67vqkpulh","from":"fcqsvmdzpy5mjc9m0cuh6uhmprr6gk5w2zcrnjy02","nonce":"Aw==","value":"0.0000301989888","method":"createChannel","params":"glYAAGsLRe4dsJwsvfE2+dkEh1DPX11jQ+OdAQ=="},"gasPrice":"0","gasLimit":"rAI="},"signature":"Zg5kUOtEZ9Um+mGKicHaqTCaORppGv5KAaSlTpN/qTcoTptRUP3ZbQ5YOL7zjTG6aF7Y4r0Ck0NsnG0J/i2B0AA="},{"meteredMessage":{"message":{"to":"fcqp606qfk5gwmq6ac24g4mhv3cr8zzf67vqkpulh","from":"fcqrqtfcug5hlx7gugvwj0f2dyx6j9cxdn0ynmpu3","nonce":"Ag==","value":"0","method":"createChannel","params":"glYAAJsQ6e5i/M0X04YZwzl3VWckPu4RQ62HAQ=="},"gasPrice":"0","gasLimit":"rAI="},"signature":"oisXV83sTFkJw7y5KbO5fLhx2oa48qZKAVwX+1fIvWUDgm5PQNDddPeCkklPg2L+fmp4wL2fLF9R2qPRciLUfAA="}],"stateRoot":{"/":"zdpuAvwpuqNR4J6PJDqGfF5GbyjWarD1BhujTUfyREMHSY1eF"},"messageReceipts":[{"exitCode":0,"return":["Ag=="],"gasAttoFIL":"0"},{"exitCode":0,"return":["AAA2Qrupiubk6ljOMnMUrnnHKaVXmQ=="],"gasAttoFIL":"0"},{"exitCode":0,"return":["0gA="],"gasAttoFIL":"0"},{"exitCode":0,"return":null,"gasAttoFIL":"0"},{"exitCode":0,"return":[""],"gasAttoFIL":"0"},{"exitCode":0,"return":["Aw=="],"gasAttoFIL":"0"},{"exitCode":0,"return":["Ag=="],"gasAttoFIL":"0"}],"proof":[177,165,90,219,1,18,240,190,113,56,243,22,167,201,232,75,124,152,130,111,74,132,5,192,33,191,102,220,102,9,99,109,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}//查询最新区块信息waynewyang:go-filecoin waynewyang$ go-filecoin show block `go-filecoin chain head --repodir=~/.filecoin2/ |jq -r '.[0]'|jq -r '.["/"]'`Block DetailsMiner: fcq973y2y7hvcce8zkwds7r2847xmfjvdecn98lwsWeight: 134386.836Height: 5013Nonce: 05) 获取所有矿工的报价信息go-filecoin client list-asks --enc=json | jq 现在默认是只要客户出价高于矿工报价,默认接受交易。 停止挖矿 12go-filecoin mining stoprm -rf ~/.filecoin //删除filecoin矿工实例,区块同步也被删除,再次实例化,需要再次同步区块 检索矿工 暂未发现支持,目前可以自己的供应商(具体矿工)处获取;后面通过更深入的分析之后另行补充。 修复矿工 修复矿工的概念是白皮书之后提出的,后面继续深入分析之后再另行补充。 客户操作存储客户 filecoin 与IPFS数据结构是兼容的 1234567891011121314151617181920//创建测试文件waynewyang:test waynewyang$ echo "Hi my name is $USER"> hello.txtwaynewyang:test waynewyang$ cat hello.txt Hi my name is waynewyang//导入filecoin本地资源库waynewyang:test waynewyang$ export CID=`go-filecoin client import ./hello.txt`waynewyang:test waynewyang$ echo $CIDQmchgh3N3kxWiaZ2cp9PbV93i77H3K8KtQCBTeVR5Q7wzs//这里会发现用IPFS上传得到的CID也是一样waynewyang:test waynewyang$ ipfs add hello.txt added Qmchgh3N3kxWiaZ2cp9PbV93i77H3K8KtQCBTeVR5Q7wzs hello.txt 25 B / 25 B [========================================================================] 100.00%//用go-filecoin或者IPFS命令获取数据,结果一致waynewyang:test waynewyang$ go-filecoin client cat $CIDHi my name is waynewyangwaynewyang:test waynewyang$ ipfs block get $CIDHi my name is waynewyang 导入测试数据 1234waynewyang:sample-data waynewyang$ export CID=`go-filecoin client import camel.jpg`waynewyang:sample-data waynewyang$ go-filecoin client cat $CID > image.png && open image.pngwaynewyang:sample-data waynewyang$ echo $CIDQmeubcGKFXpafFT4xRFGf3NqDRzJUVoAqe5sh1ugbRPZ7u 查询矿工的报价单 123456789101112131415161718192021222324252627282930waynewyang:sample-data waynewyang$ go-filecoin client list-asks --enc=json | jq{ "Miner": "fcqvnwlanfu7ecflnp3rc5gm0ecdamvxgvlawref4", "Price": "0.000000001", "Expiry": 7079, "ID": 0, "Error": null}{ "Miner": "fcqsmut6jnwchq0qlc3t6v44pzgf8l49lg6r8wl4a", "Price": "0.000000001", "Expiry": 16522, "ID": 0, "Error": null}{ "Miner": "fcqsmut6jnwchq0qlc3t6v44pzgf8l49lg6r8wl4a", "Price": "0.000000000000000001", "Expiry": 18753, "ID": 1, "Error": null}{ "Miner": "fcqghrce7vaf6czj54x5qke0mn2uzzg8ckvgvcjpe", "Price": "0.000000001", "Expiry": 14404, "ID": 0, "Error": null}...... 下单 123456go-filecoin client propose-storage-deal <miner> <data> <ask> <duration><miner> address of the miner from list-asks<data> CID of the imported data that you want to store<ask> ID of the ask, also from list-asks (usually 0)<duration> how long you want to store (in # of ~30sec blocks). For example, storing for 1 day (2 blocks/min * 60 min/hr * 24 hr/day) = 2880 blocks. 发送数据和支付 1234567891 支付1)支付到paych中2)定期向矿工付款2 数据1)未密封完的数据称之为暂存区2)密封完成后阶段性支付 检索客户 现在是指定所对应的存储矿工进行检索,暂未发现更多支持,在后面的深入分析中会继续跟进。 查询订单状态,必须是密封,posted交易结束后才能查询 1go-filecoin client query-storage-deal < dealID > 检索 1go-filecoin retrieval-client retrieve-piece < minerAddress > < CID > filecoin合约文件合约 其实现在的创建存储矿工,以及矿工创建报价、存储客户提交订单存储,这些笔者认为属于filecoin文件合约的范畴。 与以太坊类似,以太坊抽象出了代币合约以及通用智能合约; 而filecoin则是抽象出了文件合约和通用智能合约。 智能合约 暂未发现支持,在后面的深入分析中会继续跟进。 单机运行多个filecoin节点修改资源目录和服务端口的方式 go-filecoin init的时候,通过 ‘–repodir=所指定资源目录路径’ 命令进行初始化目录资源,后面的其他命令同样需要所指定资源目录路径进行操作。 修改资源目录下的config.json文件,将默认的端口予以修改,避免与另外的本机实例相冲突。 容器部署方式 可以打包成docker镜像,有兴趣的朋友可以自行尝试。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"filecoin技术架构分析二:filecoin通用语言理解","slug":"filecoin-code-analysis-2","date":"2019-02-20T08:35:33.000Z","updated":"2019-04-05T10:29:31.056Z","comments":true,"path":"2019/02/20/filecoin-code-analysis-2/","link":"","permalink":"https://learnblockchain.cn/2019/02/20/filecoin-code-analysis-2/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第二章filecoin通用语言理解。","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第二章filecoin通用语言理解。 为什么要把filecoin通用语言单独列为一讲笔者认为一位优秀的软件从业人员,必须具备两种必备能力 - 架构设计能力 - 架构剖析能力 这两者是相辅相成的,架构设计师所设计之架构不可能超过自己的认知范畴,故架构设计师必须有效高效地拓展自己的技术认知视图,以适应当代软件架构高速发展的现实。而拓展的途径,一方面就是相关理论体系的快速学习跟进;另一方面,就是实战,对有显著价值的优秀软件项目进行架构剖析。有理论、有实战方是王道。理解具体架构的通用语言就是分析他人架构设计思维的一条捷径。 理解具体技术架构的通用语言是分析架构的一条捷径 通用语言是架构设计人员为实现某个具体技术架构,所高度抽象出来的名词或者称谓,通过理解通用语言,可以快速理解架构设计人员的思维和设计目的。 与读书方法类似(薄读->厚读>再薄读),理解通用语言就是第一次的薄读过程,非常重要。 对业务的理解是非常有必要的,所以在第一章中,笔者铺垫了filecoin的一些基本概念,任何架构的设计不能脱离业务而行,业务驱动开发仍是非常实用的架构模式;filecoin 技术架构从业务来划分,可划分为两个大的范畴。 分布式存储解决方案 存储矿工 检索矿工 存储客户端 检索客户端 区块链项目 filecoin公链 filecoin actors 智能合约 filecoin核心通用业务组件 组件名称 目的 DSN 保障数据安全、包括故障容错、数据完整性、数据可恢复等 新型存储证明 证明矿工按照协议规范存储了客户指定的数据,数据有效性 可验证市场 对矿工与客户组成的交易市场进行了建模,保证交易的有效性 有效工作量证明 出块的共识机制,很重要,做到激励兼容 下面各节将会对filecoin技术架构中的核心通用语言进行解释。 存储证明Proof-of-Storage包含复制证明(PoR)和时空证明(PoSt),其作用主要有两点: 证明矿工做了有效存储 竞争区块打包出块,获取区块奖励 2.2.1 为什么使用存储证明 相对于PoW(Proof-of-Work)或者PoC PoW耗能严重;PoC以空间换时间,同样存在耗能严重问题 而filecoin网络的耗能必须远低于类似比特币的PoW,参见第一讲filecoin的对标对象,filecoin必须实现以更低的成对去应对商业竞争,同时提供相同级别的安全性,以及文件存储的效用 存储证明需要做要与实体经济挂钩,减少无谓浪费 相对于PoS(Proof-of-Stake)或者PoC Proof-of-Storage在定向领域(分布式存储)以更简单方式,协调激励,并驱使矿工以有竞争力的价格提供真实的新存储,它促使矿工积极保证filecoin网络的效用 当然Proof-of-Stake是区块链领域的热点研究问题 Proof-of-Storage阻止网络攻击 攻击类型 说明 阻止攻击原理 女巫攻击Sybil attack 作恶节点创造多个女巫身份,谎称存储了多个副本 每个节点的副本都是有签名的,想通过复制证明,就相当于真实做了有效存储 外包攻击outsourcing attacks 作恶节点快速从其他节点获取内容,谎称他们存储了比他们实际存储更多的内容 针对外包攻击,从其他节点获取的整个过程,满足不了证明人随机挑战的要求,依然需要重新生成副本(重新seal需要时间),从而阻止外包攻击 生成攻击generation attacks 作恶节点宣称将要存储超过其实际容量的内容但并未存储内容,以此增加出块的概率 宣称无用,存储证明一定要确认密封动作并能应对随机挑战才能OK,如果重新密封就来不及证明,每次挑战是有时间要求的 复制证明基础 复制证明本质上可以理解为一种零知识证明,既然是零知识证明,我们在后面需要理解filecoin复制证明的题目和答案 zk-SNARK zero knowledge Succinct Non-interactive ARgument of Knowledgezero knowledge:零知识,即在证明的过程中不透露任何内情succinct:简洁的,主要是指验证过程不涉及大量数据传输以及验证算法简单non-interactive:无交互。 生成证明的方法在filecoin架构中称之为seal密封 密封过程是需要时间的,Seal过程串行加密的过程,无法并行操作,seal密封过程是有意设计慢的,主要目的是为了防攻击。 filecoin复制证明的题目和答案 公开的信息 矿工的节点公钥、密封公钥、存储公钥、原始Data哈希、该矿工存储的副本根哈希 隐含因素理解: 特有节点的副本哈希是由哪些哈希组成(DAG),任意挑战者或者攻击者是不知情的 挑战随机参数,通过CRH(防碰撞的哈希散列Collision-resistant hashing)生成哈希之后传递给证明者,作用是确定特定的叶子节点的哈希,比如让证明者自行计算离H(c))最近的叶子节点哈希。 复制证明的题目与答案 挑战参数:副本哈希rt,挑战随机参数c -> H(c) 证明者输入(题目): H(c)(每一次挑战都会变) 隐含信息比喻:该叶子节点是与H(c)最近的节点 证明者输出(答案): H(c)对应的叶子节点 ——> rt的路径(攻击者是很难反推的) 时空证明 时空证明可以理解为矿工持续性地生成复制证明 挑战者输入一个随机参数c,后面的随机参数由证明者基于上一个的挑战答案去生成。(不用与挑战者持续交互) 下图中变量i会轮询生成新的时间变量产生随机挑战。 预期共识基础前提 filecoin基于存储证明(有效存储量)来作为矿工在整个网络中的power power属性 说明 公开 1 某一时刻,整个网络存储总量是公开的2 单个矿工某一时刻,有效存储总量是公开的 可公开验证的 对于每个存储任务,矿工都需要生成”时空证明“,证明持续提供服务。通过读取区块链,任何人都可以验证矿工的power声明是否是正确的。 变化 在任意时间点,矿工都可以通过增加新增扇区和扇区补充的抵押来增加新的存储。这样矿工就能变更他们能提供的power。 使用power达成共识 目的: 每一轮选举一个(或多个)矿工,使得赢得选举的概率与每个矿工分配的存储成比例 filecoin预期共识(Expected Consensus,EC) 预期共识的基本直觉是确定性的,不可预测的 预期的期望是每个周期内当选的Leader是1,但一些周期内可能有0个或者许多的Leader。 在每个周期,每个区块链被延伸一个或多个区块,见下图 区块线性扩展,但是数据结构是DAG EC是一个概率共识,每个周期都使得比前面的区块更加确定,最终达到了足够的确定性 选举方案 预期共识通过选举方案产生 选举方案属性 说明 公平 每个参与者每次选举只有一次试验,因为签名是确定性的,而且t和rand(t)是固定的。随机值rand(t)在时刻t之前是未知的 保密 由于有能力的攻击者不拥有Mi用来计算签名的秘钥 公开可验证 当选Leader i ∈ Lt 可以通过给出t,rand(t),H(i)/2L,来说服一个有效的验证者。鉴于前面的观点(复制证明与时间证明),有能力的攻击者在不拥有获胜秘密秘钥的情况下不能生成证明。 filecoin智能合约文件合约 允许用户对他们提供的存储服务进行条件编程,会形成一个多样化市场。 承包矿工:客户可以提前指定矿工提供服务而不参与市场 付款策略:客户可以为矿工设计不同的奖励策略,例如合约可以给矿工支付随着时间的推移越来高的费用 票务服务:合约可以允许矿工存放token和用于代表用户的存储/检索的支付 更复杂的操作:客户可以创建合约来运行数据更新 智能合约 用户可以将程序关联到其他系统(如以太坊)的交易上,他们不直接依赖存储的使用。 与其他系统的兼容 规格支持跨链交互,以便能将filecoin存储带入其他基于区块链的平台,同时也将其他平台的功能带入filecoin。 交易市场 存储需求和供给组成了两个Filecoin市场:存储市场和检索市场。这两个市场是两个去中心化交易所,简而言之,客户和矿工们通过向各自的市场提交订单来设定他们请求服务或者提供服务的订单的价格。交易所为客户和矿工们提供了一种方式来查看匹配出价并执行订单。如果服务请求被成功满足,通过运行管理协议,网络保证了矿工得到报酬,客户将被收取费用。 可以类比为淘宝商城 存储市场 交易数据会上链,包含于区块之中。 本质上也属于filecoin智能合约中的文件合约。 20190214上线的开发网络已支持 检索市场 交易数据不会上链,属于offchain的方式。 本质上也属于filecoin智能合约中的文件合约。 filecoin节点 filecoin节点相关 node id表示filecoin网络节点 account id并表示账号,默认与钱包地址一致 wallet addr表示钱包地址 miner id表示矿工id 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"filecoin技术架构分析一:filecoin概念","slug":"filecoin-code-analysis-1","date":"2019-02-18T08:35:33.000Z","updated":"2019-04-05T10:29:30.998Z","comments":true,"path":"2019/02/18/filecoin-code-analysis-1/","link":"","permalink":"https://learnblockchain.cn/2019/02/18/filecoin-code-analysis-1/","excerpt":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第一章介绍filecoin概念。","text":"我是先河系统CTO杨尉,欢迎大加关注的的Github: waynewyang,本文是filecoin技术架构分析系列文章第一章介绍filecoin概念。 filecoin的定义 Filecoin是一个去中心化的存储网络(DSN),是一个云存储的自由交易市场,通过Filecoin项目来实现其协议。矿工通过提供数据存储或检索来获得token(也称为“filecoin”)。相反,客户向矿工支付token以存储或分发数据并进行检索。 Filecoin的多重含义: 网络 市场 项目 协议 Token filecoin的设计目的 filecoin设计符合激励相容,每一个参与者的最有利可图的选择(包括目标客户,矿工,投资者和开发人员)将是采取行动提高网络服务质量,这也是他们的最优策略。 以超高竞争力的价格可靠地存储文件(低成本、高效率) 客户可以调整其存储策略以满足他们的需求,在冗余,检索速度和成本之间创建自定义平衡。全球的Filecoin存储和检索市场使供应商竞争以最优惠的价格为客户提供灵活的选择 filecoin与IPFS的关系filecoin与IPFS属性对比 类别 IPFS Filecoin 功能 基于内容寻址的分布式存储基础设施 IPFS网络之上的激励层,提供一个云存储领域的自由交易市场 对标对象 HTTP 大型集中式孤岛存储提供商,如国外的aws、国内的aliyun等 存储权限 对有所有权的IPFS节点具备存储权限 1 除对有所有权的IPFS节点具备存储权限外2 还可以通过支付的方式,在其供应商的节点之上具备存储权限 读取权限 ALL(只要知道内容cid) ALL(只要知道内容cid) 架构设计 另行文章补充分析 原则上需要无缝对接到IPFS1Filecoin将IPLD用于区块链数据结构2 Filecoin节点使用libp2p建立彼此的安全连接3 节点和Filecoin块传播之间的消息传递使用libp2p pubsub 使用场景 1 存储自己的节点数据,分享数据等,类似BT2 基于IPFS或其中部分组件构建企业自己的分布式云存储架构、区块链架构等 1 成为filecoin矿工,提供分布式检索及存储服务2 成为filecoin客户,支付费用享受filecoin网络的检索及存储服务3 基于filecoin,开发第三方管理系统 IPFS现在和将来都可以免费下载,运行和使用,并且将独立于Filecoin运行。一旦Filecoin正式网络启动,IPFS节点还可以免费或利润地在Filecoin检索市场上提供其文件的检索。 IPFS的对标对象 特点 HTTP IPFS 寻址方式 位置寻址一维寻址,低效、脆弱 内容寻址多维寻址,高效、稳定 效率 低效 高效 稳定性 脆弱 稳定 开放性 封闭、垄断 开放、共享 filecoin的对标对象 特点 传统云存储提供商(大型集中式孤岛存储网络) Filecoin 网络模式 集中式 DSN 加入门槛 高,从硬件底层基础设施、一直到软件、服务的提供,小企业很难插足 低、自由交易市场,Filecoin做好基础设施 宏观视野:闲置存储空间 高 低 价格 昂贵,垄断、可人为保持高水平 便宜,自由竞争市场 安全性 差,破坏隐私1 云存储上可查看用户隐私2 甚至许多密码鉴权信息都没有隐私可言3 单个提供商的故障影响大 强1 无第三方或者中心机构,文件加密安全得到保障2 单个云提供商的故障小 利益分配群体 巨头 All filecoin网络中的角色 角色 说明 主要影响因素 存储矿工 存储矿工通过为客户存储数据来获得Filecoin;获得区块奖励和交易费用的概率与矿工对Filecoin网络的存储量成正比 存储容量 检索矿工 检索矿工的带宽和交易的出价/响应时间(即延迟和与客户的接近度)将决定其在网络上关闭检索交易的能力 带宽 检索客户 支付filecoin获取检索服务 存储客户 支付filecoin获取存储服务 矿工收益方式类比理解 类比filecoin为一家股份公司,类比存储矿工为股东(股份出资人) 收益来源 类比分析 提供存储服务 存储矿工收益来自两部分1 工资(提供存储并收取服务费用)2 按照出资比例分红(区块奖励就是按照有效存储占比来实现的) 提供检索服务 检索矿工是offchain的,不参与挖矿,收益来自1 工资(提供检索并收取服务费用) 存储矿工存储两类数据,存储整个区块链所需的总存储量将远低于矿工为交易存储的密封数据。 密封客户的存储数据 blockchain数据的数据的副本 检索矿工 提供检索的途径 可以存储热门数据(非存储矿工),以便更优质提供服务 自己同时做存储矿工,或者从存储矿工处获取 不限于从filecoin网络获取,可以从免费的IPFS网络获取 检索效率的保证- 检索矿工是不运行在blockchain中的,是off blockchain的。 - 全球分布式 客户(检索客户和存储客户)选择filecoin的理由 企业客户愿意使用filecoin来支付数据存储和检索的理由 filecoin是一套激励相容的系统,filecoin的设计目标保证了每个参与者(包括客户,矿工,投资者和开发人员)的最有利可图的选择或者说是最优策略是采取行动来提高网络的服务质量。具备技术先进性。 数据更为安全 抵押机制促使矿工提供稳定安全服务,预计会出现声誉系统。矿工需要自行保证系统内的稳定性。 即便提供商出现故障,filecoin网络可以在多个存储提供商之间进行额外的修复。 客户可以根据数据安全等级选择副本数量。 价格更为廉价 内容寻址的本质决定了其全局冗余度低。 filecoin作为全球性的分布式存储系统,可以做全球性去重,从而降低整个网络存储成本。 个人客户选择使用filecoin的理由 预计filecoin将提供允许一方支付另一方来检索数据的结构 包括web 2.0网站的主要内容分发模型,在该模型中,网站所有者为基础设施服务付费,以免费向其用户提供数据,然后以其他方式通过内容获利。 filecoin的设计目标,让用户和内容创作者能够探索各种新的内容分发和经济模型。 例如版权问题的解决 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/categories/FileCoin/"}],"tags":[{"name":"FileCoin","slug":"FileCoin","permalink":"https://learnblockchain.cn/tags/FileCoin/"}]},{"title":"第10期 - 区块链安全的至暗时刻","slug":"dev_meeting_10","date":"2019-01-18T02:47:23.000Z","updated":"2019-04-05T11:36:18.196Z","comments":true,"path":"2019/01/18/dev_meeting_10/","link":"","permalink":"https://learnblockchain.cn/2019/01/18/dev_meeting_10/","excerpt":"","text":"分享嘉宾:邓永凯 零时科技 CEO话题简介:区块链安全的至暗时刻 活动大纲: 一、区块链安全现状二、交易平台安全三、智能合约安全四、共识安全五、区块链安全解决方案 点击下载PPT因可能涉及的某些安全问题可能对一些项目产生影响,本期的视频暂不公开。","categories":[{"name":"技术工坊","slug":"dev-meeting","permalink":"https://learnblockchain.cn/categories/dev-meeting/"}],"tags":[]},{"title":"第9期 - 漫谈区块图技术之XDAG和Conflux","slug":"dev_meeting_9","date":"2019-01-11T02:47:23.000Z","updated":"2019-04-05T11:32:49.398Z","comments":true,"path":"2019/01/11/dev_meeting_9/","link":"","permalink":"https://learnblockchain.cn/2019/01/11/dev_meeting_9/","excerpt":"","text":"分享嘉宾:以太零 高级工程师 苏显华话题:漫谈区块图技术之XDAG和Conflux 1、区块图技术重量级区块链项目-conflux2、图灵奖姚教授发明的Conflux是什么?3、XDAG的工作量证明和区块图算法4、XDAG现存问题和未来改进5、Conflux特性和发布时间 点击下载PPT 及 现场视频","categories":[{"name":"技术工坊","slug":"dev-meeting","permalink":"https://learnblockchain.cn/categories/dev-meeting/"}],"tags":[]},{"title":"第8期 - 大白话区块链共识机制算法","slug":"dev_meeting_8","date":"2019-01-04T02:47:23.000Z","updated":"2019-04-05T11:32:54.312Z","comments":true,"path":"2019/01/04/dev_meeting_8/","link":"","permalink":"https://learnblockchain.cn/2019/01/04/dev_meeting_8/","excerpt":"","text":"分享嘉宾:Nerthus CTO 虞双齐力求用最普通的话语讲解区块链共识机制。 话题大纲: 区块链史前文明 工作量证明(PoW)共识机制算法与分析、以及算力和区块难度等区块链概念。 权益证明(PoS)共识机制、各种优化版的Pos介绍与分析。 点击下载PPT 及 现场视频","categories":[{"name":"技术工坊","slug":"dev-meeting","permalink":"https://learnblockchain.cn/categories/dev-meeting/"}],"tags":[]},{"title":"第7期 - 选对赛道,穿越凛冬:区块链的设计思想和演化逻辑","slug":"dev_meeting_7","date":"2018-12-28T03:41:23.000Z","updated":"2019-04-05T11:33:38.569Z","comments":true,"path":"2018/12/28/dev_meeting_7/","link":"","permalink":"https://learnblockchain.cn/2018/12/28/dev_meeting_7/","excerpt":"","text":"讲师:AMT社区共建者 Jason分享大纲: 一、 伟大变革or旁氏泡沫:谁需要区块链,需要什么样的区块链? 二 、区块链对现实世界的依赖性:虚生于实,依于实。 三、 几大公链设计思想对比:面对不可能三角时各公链的取舍和结果 1)TPS:BTC,LTC,ETH的出块时间对比说明了什么 2) 去中心化: 如何面对用去中心化换TPS 3)安全:没有绝对,只有相对 四、 发展焦点: 1)协议共识 VS 社会共识 谁将获胜? 2)分裂是好是坏 :BTC分裂BCH, BCH分裂BSV 3)智能合约:新经济的核心 五、 下一跳去到哪里:TPS、新共识、跨链、应用落地(IBO STO等) 六 、匿名技术公链:最后的保留地 七 、前路展望:符合社会需要,百花迎春 点击下载PPT 及 现场视频","categories":[{"name":"技术工坊","slug":"dev-meeting","permalink":"https://learnblockchain.cn/categories/dev-meeting/"}],"tags":[]},{"title":"IPFS 使用入门","slug":"use-ipfs","date":"2018-12-25T11:16:27.000Z","updated":"2019-04-05T10:29:32.284Z","comments":true,"path":"2018/12/25/use-ipfs/","link":"","permalink":"https://learnblockchain.cn/2018/12/25/use-ipfs/","excerpt":"在上一篇文章介绍了IPFS要做什么, 本篇文章介绍下IPFS怎么用, 按照本站的风格,我不会仅仅把一个个命令列出来,同时会说明命令在后面为我们做了什么。","text":"在上一篇文章介绍了IPFS要做什么, 本篇文章介绍下IPFS怎么用, 按照本站的风格,我不会仅仅把一个个命令列出来,同时会说明命令在后面为我们做了什么。 IPFS 安装要使用IPFS, 第一步肯定是先把IPFS安装好,IPFS在Mac OS X 、Linux及Window平台均有提供, 可以通过这个链接下载对应平台可执行文件的压缩包。 对于Mac OS X 及 Linux 平台,使用一下命令进行安装: 123$ tar xvfz go-ipfs.tar.gz$ cd go-ipfs$ ./install.sh 上面先使用tar 对压缩包进行解压,然后执行install.sh 进行安装,安装脚本install.sh其实就是把可执行文件ipfs移动到$PATH目录下。安装完成之后,可以在命令行终端敲入ipfs试试看,如果显示一堆命令说明,则说明IPFS安装成功。 在Windows平台也是类似,把ipfs.exe移动到环境变量%PATH%指定的目录下。 IPFS 基本用法IPFS初始化安装完成之后,要使用IPFS第一步是要对IPFS进行初始化,使用ipfs init进行初始化 1234567> ipfs initinitializing ipfs node at /Users/Emmett/.ipfsgenerating 2048-bit RSA keypair...donepeer identity: QmYM36s4ut2TiufVvVUABSVWmx8VvmDU7xKUiVeswBuTvato get started, enter: ipfs cat /ipfs/QmS4ustL54uo8FzR9455qaxZwuMiUhyvMcX9Ba8nUH4uVv/readme 上面是执行命令即对应输出,在执行ipfs init进行初始化时,会有一下行为: 生成一个秘钥对并产生对应的节点id, 即命令提示:peer identity后面的hash值。 节点的id用来标识和连接一个节点,每个节点的id是独一无二的, 因此大家看到的提示也会和我的不一样。 在当前用户的主目录(~ 目录)下产生一个.ipfs 的隐藏目录,这个目录称之为库(repository)目录,ipfs 所有相关的数据都会放在这个目录下。如同步文件数据块放在.ipfs/blocks 目录,秘钥在.ipfs/keystore 目录,ipfs配置文件为:.ipfs/config。 IPFS 配置修改在IPFS初始化之后,可以根据需要修改配置(可选),修改方法如下: 123cd ~/.ipfsexport EDITOR=/usr/bin/vimipfs config edit 或者直接编辑 ~/.ipfs/config 文件。 上传文件到IPFS我们先创建一个upchain.pro.txt文件,可以使用如下方式: 1> echo "登链学院:区块链教育领先品牌" >> upchain.pro.txt ipfs 使用add 命令来添加内容到节点中, 在命令行输入: 123> ipfs add upchain.pro.txtadded QmQgMZKqHzyEdyJja5ioF8WaXrbUDVjqhJDoaUKDymgioi upchain.pro.txt 43 B / 43 B [=====================================================] 100% 当它文件添加到节点时,会为文件生成唯一的hash: QmQgMZKqHzyEdyJja5ioF8WaXrbUDVjqhJDoaUKDymgioi, 可以使用ipfs cat 查看文件的内容: 12> ipfs cat QmQgMZKqHzyEdyJja5ioF8WaXrbUDVjqhJDoaUKDymgioi登链学院:区块链教育领先品牌 注意,此时文件仅仅是上传在本地的IPFS节点中,如果需要把文件同步到网络,就需要开启 daemon 服务, 使用命令: 123456789101112131415161718192021222324> ipfs daemonInitializing daemon...go-ipfs version: 0.4.18-Repo version: 7System version: amd64/darwinGolang version: go1.11.1Swarm listening on /ip4/127.0.0.1/tcp/4001Swarm listening on /ip4/192.168.8.105/tcp/4001Swarm listening on /ip6/2408:84f3:82e:cfcd:409:fee2:e261:4dc3/tcp/4001Swarm listening on /ip6/2408:84f3:82e:cfcd:a9c6:116b:349f:8c2b/tcp/4001Swarm listening on /ip6/2408:84f3:82e:cfcd:ec89:145d:cf27:4/tcp/4001Swarm listening on /ip6/::1/tcp/4001Swarm listening on /ip6/fd1d:43b:e89b:eb9b:c405:56af:8f52:67df/tcp/4001Swarm listening on /p2p-circuitSwarm announcing /ip4/127.0.0.1/tcp/4001Swarm announcing /ip4/192.168.8.105/tcp/4001Swarm announcing /ip6/2408:84f3:82e:cfcd:409:fee2:e261:4dc3/tcp/4001Swarm announcing /ip6/2408:84f3:82e:cfcd:a9c6:116b:349f:8c2b/tcp/4001Swarm announcing /ip6/2408:84f3:82e:cfcd:ec89:145d:cf27:4/tcp/4001Swarm announcing /ip6/::1/tcp/4001Swarm announcing /ip6/fd1d:43b:e89b:eb9b:c405:56af:8f52:67df/tcp/4001API server listening on /ip4/127.0.0.1/tcp/5001Gateway (readonly) server listening on /ip4/127.0.0.1/tcp/8080Daemon is ready 开启 daemon 之后,Swarm 就会尝试连接其他的节点,同步数据,同时在本地还会开启两个服务:API服务及Web网关服务,下面分别介绍下: API服务,默认在5001端口,可以通过 http://localhost:5001/webui 进行访问,界面如: 这也是IPFS的一个Web版的管理控制台, 可以通过这个控制台添加文件,查看节点连接情况等等。 网关服务,默认在8080端口, 由于当前浏览器还不支持通过IPFS协议(ipfs://)来访问文件,如果我们要在浏览器里访问文件的话,就需要借助于IPFS 提供的网关服务,由浏览器先访问到网关,网关去获取IPFS网络杀过了的文件。 有了网关服务,就可以通过这个链接:http://localhost:8080/ipfs/QmQgMZKqHzyEdyJja5ioF8WaXrbUDVjqhJDoaUKDymgioi 来访问刚刚上传到ipfs 的文件。 ipfs 也提供了官方的网关服务:https://ipfs.io/, 因此也可以通过 https://ipfs.io/ipfs/QmQgMZKqHzyEdyJja5ioF8WaXrbUDVjqhJDoaUKDymgioi (需要翻墙)来访问刚刚上传到ipfs 的文件。 上传目录到IPFS我们先创建一个文件夹upchain, 并把之前的 upchain.pro.txt 放进目录。 12> mkdir upchain> mv upchain.pro.txt upchain 上传目录到IPFS 需要在使用 add 命令时加上 -r ,如下: 1234> ipfs add -r upchainadded QmQgMZKqHzyEdyJja5ioF8WaXrbUDVjqhJDoaUKDymgioi upchain/upchain.pro.txtadded QmQYpGRFBpHVzoShpwU5C3XgGAxJKqY83X8VXfMbyktdbP upchain 43 B / 43 B [===========================================================================] 100.00% 在上传时文件夹,文件夹也会生成一个对应的hash,可以通过hash后接文件名来进行访问, 如: 12>ipfs cat QmQYpGRFBpHVzoShpwU5C3XgGAxJKqY83X8VXfMbyktdbP/upchain.pro.txt登链学院:区块链教育领先品牌 在浏览器可以链接:http://127.0.0.1:8080/ipfs/QmQYpGRFBpHVzoShpwU5C3XgGAxJKqY83X8VXfMbyktdbP/upchain.pro.txt 来访问。 通过上传目录的方式,可以把整个静态网站的根目录上传到IPFS网络,这样就可以省去托管服务器,例如可以直接通过以下链接访问深入浅出区块链博客:https://ipfs.io/ipfs/QmaFWgfpRNzeLgfDrH33BuBdiauRTejnF3Yw9AuCphq2ua/index.html 使用IPNS解决文件更新问题因为IPFS在IPFS中,对一个文件的内容修改后(如升级),会生成一个完全不同的新Hash,使用IPNS就可以利用同一个链接总是指向更新的内容,其实使用也很简单,只需要每次在内容更新之后使用ipfs name publish hash 发布到节点。 例如把upchain.pro.txt发布到节点,使用下面的命令:12> ipfs name publish QmQgMZKqHzyEdyJja5ioF8WaXrbUDVjqhJDoaUKDymgioiPublished to QmYM36s4ut2TiufVvVUABSVWmx8VvmDU7xKUiVeswBuTva: /ipfs/QmQgMZKqHzyEdyJja5ioF8WaXrbUDVjqhJDoaUKDymgioi 命令中的QmQgMZKqHzyEdyJja5ioF8WaXrbUDVjqhJDoaUKDymgioi是upchain.pro.txt的hash, 命令提示中的QmYM36s4ut2TiufVvVUABSVWmx8VvmDU7xKUiVeswBuTva是当前节点id(大家可以回看一个前面ipfs init 的输出)。 发布之后就可以使用http://127.0.0.1:8080/ipns/QmYM36s4ut2TiufVvVUABSVWmx8VvmDU7xKUiVeswBuTva 或 https://ipfs.io/ipns/QmYM36s4ut2TiufVvVUABSVWmx8VvmDU7xKUiVeswBuTva 来访问upchain.pro.txt的内容,如图: 其实理想下是使用 ipns://QmYM36s4ut2TiufVvVUABSVWmx8VvmDU7xKUiVeswBuTva 来访问,通过网站还是前面提到的浏览器暂不支持ipfs协议。 现在我们来更新一下upchain.pro.txt 加入文字:”创办人:Tiny熊” 1234> echo "创办人:Tiny熊" >> upchain.pro.txt> ipfs add upchain.pro.txtadded QmUUiDN6tWtj89xmUw1iCK2NczBqE6m3zH9QnbhHoMvZ5S upchain.pro.txt 63 B / 63 B [=============================================================] 100.00% 重新发布一下: 12> ipfs name publish QmUUiDN6tWtj89xmUw1iCK2NczBqE6m3zH9QnbhHoMvZ5SPublished to QmYM36s4ut2TiufVvVUABSVWmx8VvmDU7xKUiVeswBuTva: /ipfs/QmUUiDN6tWtj89xmUw1iCK2NczBqE6m3zH9QnbhHoMvZ5S 再次访问 http://127.0.0.1:8080/ipns/QmYM36s4ut2TiufVvVUABSVWmx8VvmDU7xKUiVeswBuTva (这个链接和上面的链接一样)可以看到内容更新了。 如果我们要查询 节点id 指向的hash 可以使用 ipfs name resolve 进行查询:12> ipfs name resolve/ipfs/QmUUiDN6tWtj89xmUw1iCK2NczBqE6m3zH9QnbhHoMvZ5 有一点值得大家注意: 节点id其实是公钥的hash,它的关联信息是需要经过私钥签名才可以发布,因此只有我们自己才可以更新节点的指向。 如果我们有多个站点需要更新,可以新产生一个秘钥对,使用新的key 发布,如: 123> ipfs key gen --type=rsa --size=2048 mykeyQmVZvdYEsdfHSR43Qm1fY8eDFrhB3UNZ2oVyEuVUH3VHrg> ipfs name publish --key=mykey hashxxx PinningPinning 在IPFS里是一个很重要的概念,当我们每次请求一个网络上的内容的时候,IPFS总是会把内容先同步的本地提供服务,而为了防止 IPFS 存储空间不停增长,实际上使用cache 机制来处理文件, 如果文件在一段时间内没有被使用,文件会被”回收“。 Pinning 的作用就是把文件”钉“住,确保文件在本地不被”回收“。 如果是重要的文件,就可以使用 Pinning 防止文件被删除。 当我们使用ipfs add 添加文件时,默认会进行Pinning(使用其他命令获取的文件不会进行pinning), IPFS 提供了pin命令进行Pinning操作, 比如我们查询下某一个hash 是否被pin: 12345> ipfs pin ls QmUUiDN6tWtj89xmUw1iCK2NczBqE6m3zH9QnbhHoMvZ5SQmUUiDN6tWtj89xmUw1iCK2NczBqE6m3zH9QnbhHoMvZ5S recursive> ipfs pin ls QmWnrAEKyDVUQ1jh9vDtQhtBSNEgUnQhAJyMmo3JjwJZK7Error: path 'QmWnrAEKyDVUQ1jh9vDtQhtBSNEgUnQhAJyMmo3JjwJZK7' is not pinned 可以使用 pin add 手动钉住一个文件,如: 12> ipfs pin add QmWnrAEKyDVUQ1jh9vDtQhtBSNEgUnQhAJyMmo3JjwJZK7pinned QmWnrAEKyDVUQ1jh9vDtQhtBSNEgUnQhAJyMmo3JjwJZK7 recursively 如果要删除pin的状态,使用pin rm , 如: 12> ipfs pin rm -r QmWnrAEKyDVUQ1jh9vDtQhtBSNEgUnQhAJyMmo3JjwJZK7unpinned QmWnrAEKyDVUQ1jh9vDtQhtBSNEgUnQhAJyMmo3JjwJZK7 pin rm 的参数 -r 表示递归的删除pin 状态,对于没有pin住的文件, 如果执行GC操作 ipfs repo gc 文件会被删除。 欢迎来知识星球提问,星球内已经聚集了300多位区块链技术爱好者。深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"IPFS","slug":"IPFS","permalink":"https://learnblockchain.cn/categories/IPFS/"}],"tags":[{"name":"IPFS","slug":"IPFS","permalink":"https://learnblockchain.cn/tags/IPFS/"}]},{"title":"第六期 - DAG技术中的比特币XDAG","slug":"dev_meeting_6","date":"2018-12-21T03:41:23.000Z","updated":"2019-04-05T11:33:32.991Z","comments":true,"path":"2018/12/21/dev_meeting_6/","link":"","permalink":"https://learnblockchain.cn/2018/12/21/dev_meeting_6/","excerpt":"","text":"讲师:以太零 苏显华分享大纲: 区块链技术现状 DAG公链的发展现状 DAG技术的比特币XDAG XDAG的技术实践 未来的挑战 点击下载PPT 及 现场视频","categories":[{"name":"技术工坊","slug":"dev-meeting","permalink":"https://learnblockchain.cn/categories/dev-meeting/"}],"tags":[]},{"title":"第五期 - 搞明白以太坊DAPP开发","slug":"dev_meeting_5","date":"2018-12-14T03:41:23.000Z","updated":"2019-04-05T11:33:28.601Z","comments":true,"path":"2018/12/14/dev_meeting_5/","link":"","permalink":"https://learnblockchain.cn/2018/12/14/dev_meeting_5/","excerpt":"","text":"讲师:登链学院 Tiny熊分享大纲: DAPP与APP 的区别 智能合约开发简介 web3.js 与 合约交互 MetaMask, Remix , Truffle,Ganache 使用 点击下载PPT 及 现场视频","categories":[{"name":"技术工坊","slug":"dev-meeting","permalink":"https://learnblockchain.cn/categories/dev-meeting/"}],"tags":[]},{"title":"第四期 - 以太坊零手续费及其安全防御的实现","slug":"dev_meeting_4","date":"2018-12-13T03:41:23.000Z","updated":"2019-04-05T11:33:25.800Z","comments":true,"path":"2018/12/13/dev_meeting_4/","link":"","permalink":"https://learnblockchain.cn/2018/12/13/dev_meeting_4/","excerpt":"","text":"讲师:以太零CTO 钟瑞仙主要内容: 以太坊⼿手续费简介 零⼿手续费的必要性 零⼿手续费的实现 零⼿手续费带来的安全问题及其解决⽅方案 零⼿手续费的副作⽤ 点击下载PPT及完整课程视频","categories":[{"name":"技术工坊","slug":"dev-meeting","permalink":"https://learnblockchain.cn/categories/dev-meeting/"}],"tags":[]},{"title":"第三期 - 高TPS与去中心化存储带来的机遇","slug":"dev_meeting_3","date":"2018-12-13T03:27:23.000Z","updated":"2019-04-05T11:33:22.910Z","comments":true,"path":"2018/12/13/dev_meeting_3/","link":"","permalink":"https://learnblockchain.cn/2018/12/13/dev_meeting_3/","excerpt":"","text":"讲师:星际区块(深圳)CEO 谢建怀主要内容: 高TPS能让我们做更多有意思的东西 第三代区块链技术能落地的思考 去中心化存储能在工程上带来哪些应用 区块链应用的项目探索(基于EOS、FIBOS和IPFS 应用) 点击下载PPT及完整课程视频","categories":[{"name":"技术工坊","slug":"dev-meeting","permalink":"https://learnblockchain.cn/categories/dev-meeting/"}],"tags":[]},{"title":"第二期 - 深度探索以太坊智能合约","slug":"dev_meeting_2","date":"2018-12-13T03:26:23.000Z","updated":"2019-04-05T11:33:18.543Z","comments":true,"path":"2018/12/13/dev_meeting_2/","link":"","permalink":"https://learnblockchain.cn/2018/12/13/dev_meeting_2/","excerpt":"","text":"讲师:以太零CTO 钟瑞仙主要内容包含: 以太坊账户介绍 交易数据⾥里data字段的编码规则 智能合约属性的索引和存储 预编译合约介绍及汇编调⽤ 点击下载PPT及完整课程视频","categories":[{"name":"技术工坊","slug":"dev-meeting","permalink":"https://learnblockchain.cn/categories/dev-meeting/"}],"tags":[]},{"title":"第一期 - 以太坊钱包开发","slug":"dev_meeting_1","date":"2018-12-13T03:25:59.000Z","updated":"2019-04-05T11:33:12.568Z","comments":true,"path":"2018/12/13/dev_meeting_1/","link":"","permalink":"https://learnblockchain.cn/2018/12/13/dev_meeting_1/","excerpt":"","text":"讲师:登链学院 熊丽兵分享大纲: 私钥 地址 及账号 什么是HD钱包(分层确定性钱包) 助记词及私钥保存 如何测量gasLimit及设定gasPrice 如何发送签名交易及转移Token 点击下载PPT及完整课程视频","categories":[{"name":"技术工坊","slug":"dev-meeting","permalink":"https://learnblockchain.cn/categories/dev-meeting/"}],"tags":[]},{"title":"站在Web3.0 理解IPFS是什么","slug":"what-is-ipfs","date":"2018-12-12T08:35:33.000Z","updated":"2019-04-05T10:29:31.084Z","comments":true,"path":"2018/12/12/what-is-ipfs/","link":"","permalink":"https://learnblockchain.cn/2018/12/12/what-is-ipfs/","excerpt":"尽管网络上,已经有不少文章讨论IPFS,不过真正讲明白IPFS想做什么的很少,文本尝试站在未来Web3.0的高度来看看IPFS究竟用来解决什么问题。","text":"尽管网络上,已经有不少文章讨论IPFS,不过真正讲明白IPFS想做什么的很少,文本尝试站在未来Web3.0的高度来看看IPFS究竟用来解决什么问题。 DApp 的缺陷对区块链有所了解的同学,知道区块链维护的是一个中立的(去中心)、共同信任、难以篡改的数据库、智能合约创造的是一个完全透明(不被干扰)的运行规则,因此可以解决信任问题。 一切看起来很美好,我们可以开发去中心化应用DApp 解决信任问题,由此也确实产生了很多的博彩类DApp游戏。 不熟悉DApp的同学可以看我另一篇文章程序员如何切入区块链去中心化应用开发. 细心的同学,也许会发现一个问题,虽然DApp的后台逻辑(智能合约)是在无中心的节点上运行的透明的规则,但是我们看到内容却来自于一台无信任的中心化服务器。 这是由当前互联网规则-超文本媒体传输协议(HTTP)决定的,简单来讲,在这个协议下,当我们在浏览器输入一个网址时,总是会先找到这个网址(域名)对应的服务器IP地址,然后请求服务器,并把服务器的响应显示在浏览器。 这种方式下文件能否访问,完全取决于服务器,服务器也许会关闭、内容获取被篡改或删除,对用户都无法保证。我自己看到好内容把网页收藏的习惯,经常会出现过一段时间再去访问的时候,页面已经不存在了。 IPFS想要做什么IPFS - InterPlanetary File System 星际文件系统,多数人谈到IPFS都只讲到它的去中心化存储,其实IPFS想要做的远不只存储,其目标是取代HTTP,成为Web3.0时代的基础协议。我们从其官网对IPFS的定义就可以看到其雄心。 尽管Web3.0目前没有明确定义,从2014年以太坊联合创始人Gavin Wood提出分布式网络的Web3.0概念开始,业界普遍认为Web3.0 特征应该是 分布式、可信任。 在官网的有这样两个描述: IPFS is the Distributed WebA peer-to-peer hypermedia protocol to make the web faster, safer, and more open. IPFS aims to replace HTTP and build a better web for all of us. 翻译过来就是: 1. IPFS是分布式Web,是点对点的超媒体协议,以构建更快、更安全、更开放的网络。 2. IPFS旨在取代HTTP,为我们构建一个更好的web。 当然,要完全取代HTTP还有一段路要走,最大的坎是怎样让主流的浏览器支持IPFS协议,现在是通过HTTP网关的方式访问IPFS网上面存在的文件。未来IPFS能取代Http的话?就是通过网络浏览器里直接输入 ipfs://文件hash 访问内容,目前这种方式访问IPFS 必须依靠浏览器插件ipfs 伴侣, 并且这个插件的使用不广泛。 注意,这里提到的浏览器,只是沿用这个名词,Web3.0的浏览器也许不叫浏览器, 它更可能是数字钱包和浏览器的组合体,现在浏览器上发起交易也同样需要依靠钱包插件进行签名。所以这样一个形态的产品也是大家的机会,这是一个全新的超大入口级产品。 即未来期望的访问方式是这样的:ipfs://Qme2qNy61yLj9hzDm4VN6HDEkCmksycgSEM33k4eHCgaVu 而现在通过网关访问是这样的:http://127.0.0.1:8080/ipfs/Qme2qNy61yLj9hzDm4VN6HDEkCmksycgSEM33k4eHCgaVuhttps://ipfs.io/ipfs/Qme2qNy61yLj9hzDm4VN6HDEkCmksycgSEM33k4eHCgaVu IPFS是怎么做的IPFS是一种内容可寻址、版本化、点对点超媒体的分布式存储、传输协议。 我们知道在现在的网络服务里,内容是基于位置(IP)寻址的,就是在查找内容的时候,需要先找到内容所在的服务器(根据IP),然后再在服务器上找对应的内容。而在IPFS的网络里,是根据内容寻址,每一个上传到IPFS上面去的文件、文件夹,都是以Qm为开头字母的哈希值,无需知道文件存储在哪里,通过哈希值就能够找到这个文件,这种方式叫内容寻址。 工作原理在IPFS系统中,内容会分块存放(如果内容很小就会直接存在DHT中),并分散存储在IPFS网络中的节点上(不过目前的IPFS实现,一个节点会完整保存内容的所有区块)。系统会给内容的每一个块计算哈希值,然后把所有块的哈希值拼凑起来,再计算一次哈希值,从而得到最终的哈希值。同时每个节点会维护一张DHT(分布式哈希表),包含数据块与目标节点的映射关系。 在IPFS中是通过哈希去请求文件的,它就会使用这个分布式哈希表找到文件所在的节点,取回文件根据哈希重新组合文件(同样也会验证文件)。 IPFS的特点根据前面的原理,我们可以推倒出IPFS的几个特点: 当我们知道一个文件的哈希值之后,可以确保文件不被修改, 即可以确保访问的文件是没有被篡改的。因为根据哈希的特点,哪怕源文件有一丁点的更改,对应的哈希值也会完全不同。 (理论上) 如果IPFS得以普及,节点数达到一定规模,内容将永久保存,就算部分节点离线,也不会影响文件的读取,不像现在的收藏会失效。 由于IPFS是一个统一的网络,只要文件在网络中被存储过,除了必要的冗余备份,文件不会被重复存储,对比现有互联网,信息孤岛,各中心间不共享数据,数据不的不重复存储,IPFS一定意义上节约了空间,使得整个网络带宽消耗更低,网络更加高效。 相对于中心化存储的容易遭受DDOS攻击,IPFS采用分布式存储网络,文件被存储在不同的网络节点,天然避免了DDOS攻击,同时一个文件可以同时从多个节点同时下载,通信的效率也会更高。 IPNS在IPFS中,一个文件的哈希值完全取决于其内容,修改它的内容,其相应的Hash值也会发生改变。这样有一个优点是保证文件的不可篡改,提高数据的安全性。但同时我们在开发应用(如网站)时,经常需要更新内容发布新版本,如果每次都让用户每次在浏览器中输入不同的IPFS地址来访问更新后内容的网页,这个体验肯定是无法接受的。 IPFS提供了一个解决方案IPNS(Inter-Planetary Naming System),他提供了一个被私钥限定的IPNS哈希ID(通常是PeerID),其用来指向具体IPFS文件哈希,当有新的内容更新时,就可以更新IPNS哈希ID的指向。 为了方便大家理解,做一个类比,和DNS类似, DNS记录了域名指向的IP地址, 如果服务器更改,我们可以更改DNS域名指向,保证域名指向最新的服务器。IPNS则是用一个哈希ID指向一个真实内容文件的Hash,文件更新这更改哈希ID的指向,当然更新指向需要有哈希ID对应的私钥。 通过IPNS访问文件的方式如下: 利用插件访问:ipns://QmYM36s4ut2TiufVvVUABSVWmx8VvmDU7xKUiVeswBuTva利用网关访问: http://127.0.0.1:8080/ipns/QmYM36s4ut2TiufVvVUABSVWmx8VvmDU7xKUiVeswBuTva IPNS同样兼容DNS,使用DNS TXT记录域名对应的IPNS哈希ID,就可以域名来替换IPNS哈希ID来进行访问。从而实现更容易读写和记忆。 例如使用以下方式简化访问:ipns://ipfs.iohttps://ipfs.io/ipns/ipfs.io/ IPFS/IPNS 如果使用,将在后面的文章进一步介绍。 小结IPFS是一项非常激动人心的技术,尽管它仍在发展的早期(区块链也是),还有很多问题需要我们一起解决,如NAT穿透问题,浏览器支持问题,内容存储激励问题,存储数据安全与隐私保护问题。但是通过 IPFS + 区块链将真正创建Web3.0时代的应用,这是一个完全可信的、自运转(不停机)的应用,它可以做什么我不知道,我对未来充满期待。 欢迎来知识星球提问,星球内已经聚集了300多位区块链技术爱好者。深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"IPFS","slug":"IPFS","permalink":"https://learnblockchain.cn/categories/IPFS/"}],"tags":[{"name":"IPFS","slug":"IPFS","permalink":"https://learnblockchain.cn/tags/IPFS/"},{"name":"Web3","slug":"Web3","permalink":"https://learnblockchain.cn/tags/Web3/"}]},{"title":"区块链技术工坊","slug":"dev_meeting","date":"2018-12-12T03:25:59.000Z","updated":"2019-04-05T10:29:31.083Z","comments":true,"path":"2018/12/12/dev_meeting/","link":"","permalink":"https://learnblockchain.cn/2018/12/12/dev_meeting/","excerpt":"","text":"我们是谁、我们的目标区块链技术工坊主要由HiBlock社区牵头,在全国各主要城市由一群热爱区块链技术开发者组织的技术分享活动。深圳地区由登链Tiny熊组织,我们每月邀请2到3位技术大咖进行分享。 由于区块链技术涉及面广、发展快速,我们希望通过这个分享大家能够一起学习交流、共同成长。 如何加入很简单,在活动行报名即可,活动行地址。如果你也有意参与分享,欢迎联系Tiny熊,微信:xlbxiong 致谢一个活动,单靠几个爱好的的兴趣是不够的,因此要感谢中琛源提供场地支持,ATM社区、零时科技对活动的大力赞助。如果你有兴趣赞助,请联系Tiny熊了解权益和义务。","categories":[{"name":"技术工坊","slug":"dev-meeting","permalink":"https://learnblockchain.cn/categories/dev-meeting/"}],"tags":[]},{"title":"Fabric 网络环境启动过程详解","slug":"fabric_startup_process","date":"2018-11-21T09:30:27.000Z","updated":"2019-04-05T10:29:32.302Z","comments":true,"path":"2018/11/21/fabric_startup_process/","link":"","permalink":"https://learnblockchain.cn/2018/11/21/fabric_startup_process/","excerpt":"这篇文章对Fabric的网络环境启动过程进行讲解,也就是我们上节讲到的启动测试Fabric网络环境时运行network_setup.sh这个文件的执行流程","text":"这篇文章对Fabric的网络环境启动过程进行讲解,也就是我们上节讲到的启动测试Fabric网络环境时运行network_setup.sh这个文件的执行流程 Fabric网络环境启动过程详解上一节我们讲到 fabric网络环境的启动测试,主要是使用 ./network_setup.sh up 这个命令,所以fabric网络环境启动的重点就在network_setup.sh这个文件中。接下来我们就分析一下network_setup.sh这个文件。network_setup.sh其中包括两个部分,一个是利用generateArtifacts.sh脚本文件配置组织关系和颁发证书、公/私钥、通道证书等,另一个是docker-compose-cli.yaml用于根据配置启动集群并测试chaincode的示例代码。下面是具体的流程图介绍: 首先看下generateArtifacts.sh脚本文件,它包含三个函数,分别是: 123456781.generateCerts: 该函数使用cryptogen工具根据crypto-config.yaml来生成公私钥和证书信息等。2.replacePrivateKey: 将docker-compose-e2e-template.yaml文档中的ca私钥替换成具体的私钥。3.generateChannelArtifacts: 使用configtxgen工具根据configtx.yaml文件来生成创世区块和通道相关信息,更新锚节点。 接着是docker-compose-cli.yaml文件 docker-compose-cli.yaml文件根据组织关系启动docker集群,并在cli容器中执行command命令运行./scripts/script.sh脚本文件。 那./scripts/script.sh脚本具体做了什么呢? 1234561. createChannel:创建channel。2. joinChannel:将每个peer节点加入channel。3. updateAnchorPeers:更新锚节点4. installChaincode:部署chaincode。5. instantiateChaincode:初始化chaincode。6. chaincodeQuery:chaincode查询 另外docker-compose-cli.yaml这个文件还有一个配置项是需要注意的地方,那就是: 1file: base/docker-compose-base.yaml 这里的docker-compose-base.yaml其实就是Orderer和peer的基础配置文件,包括指定端口等。 几个重要的配置文件1.crypto-config.yaml基于crypto-config.yaml(此文件在../fabric/examples/e2e_cli中)生成公、私钥和证书信息,并保存在crypto-config文件夹中。另外crypto-config.yaml还定义了组织成员以及组织下的peer节点个数。 crypto-config.yaml文件讲解: 字段Name和Domain就是关于这个组织的名字和域名,这主要是用于生成证书的时候,证书内会包含该信息。而Template.Count=2是说我们要生成2套公私钥和证书,一套是peer0.org1的,还有一套是peer1.org1的(也就指定了org中存在peer0和peer1两个节点)。最后Users.Count=1是说每个Template下面会有几个普通User(注意,Admin是Admin,不包含在这个计数中),这里配置了1,也就是说我们只需要一个普通用户User1@org1.example.com 我们可以根据实际需要调整这个配置文件,增删Org Users等。文件内容如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172 # --------------------------------------------------------------------------- # Orderer # --------------------------------------------------------------------------- - Name: Orderer Domain: example.com # --------------------------------------------------------------------------- # "Specs" - See PeerOrgs below for complete description # --------------------------------------------------------------------------- Specs: - Hostname: orderer# ---------------------------------------------------------------------------# "PeerOrgs" - Definition of organizations managing peer nodes# ---------------------------------------------------------------------------PeerOrgs: # --------------------------------------------------------------------------- # Org1 # --------------------------------------------------------------------------- - Name: Org1 Domain: org1.example.com # --------------------------------------------------------------------------- # "Specs" # --------------------------------------------------------------------------- # Uncomment this section to enable the explicit definition of hosts in your # configuration. Most users will want to use Template, below # # Specs is an array of Spec entries. Each Spec entry consists of two fields: # - Hostname: (Required) The desired hostname, sans the domain. # - CommonName: (Optional) Specifies the template or explicit override for # the CN. By default, this is the template: # # "{{.Hostname}}.{{.Domain}}" # # which obtains its values from the Spec.Hostname and # Org.Domain, respectively. # --------------------------------------------------------------------------- # Specs: # - Hostname: foo # implicitly "foo.org1.example.com" # CommonName: foo27.org5.example.com # overrides Hostname-based FQDN set above # - Hostname: bar # - Hostname: baz # --------------------------------------------------------------------------- # "Template" # --------------------------------------------------------------------------- # Allows for the definition of 1 or more hosts that are created sequentially # from a template. By default, this looks like "peer%d" from 0 to Count-1. # You may override the number of nodes (Count), the starting index (Start) # or the template used to construct the name (Hostname). # # Note: Template and Specs are not mutually exclusive. You may define both # sections and the aggregate nodes will be created for you. Take care with # name collisions # --------------------------------------------------------------------------- Template: Count: 2 # Start: 5 # Hostname: {{.Prefix}}{{.Index}} # default # --------------------------------------------------------------------------- # "Users" # --------------------------------------------------------------------------- # Count: The number of user accounts _in addition_ to Admin # --------------------------------------------------------------------------- Users: Count: 1 # --------------------------------------------------------------------------- # Org2: See "Org1" for full specification # --------------------------------------------------------------------------- - Name: Org2 Domain: org2.example.com Template: Count: 2 Users: Count: 1 注:peer:Fabric 网络中的节点,表现为一个运行着的docker容器。可以与网络中的其他peer进行通信,每个peer都在本地保留一份ledger的副本。它是org下的组织成员。org:一个组织,它可以由一个或多个peer组成。Orderer :联盟成员共享的中心化节点。用来对交易进行排序,是 Fabric 共识机制的重要组成部分。 2.configtx.yaml基于configtx.yaml(此文件在../fabric/examples/e2e_cli中)生成创世区块和通道相关信息,并保存在channel-artifacts文件夹。还可以指定背书策略。 configtx.yaml文件讲解: 官方提供的examples/e2e_cli/configtx.yaml这个文件里面配置了由2个Org参与的Orderer共识配置TwoOrgsOrdererGenesis,以及由2个Org参与的Channel配置:TwoOrgsChannel。 另外我们可以在此文件的Orderer部分设置共识的算法是Solo还是Kafka,以及共识时区块大小,超时时间等,我们使用默认值即可,不用更改。而Peer节点的配置包含了MSP的配置,锚节点的配置。如果我们有更多的Org,或者有更多的Channel,那么就可以根据模板进行对应的修改。 Policies配置也要特别注意,该配置项定义了不同角色的权限,Reader,Writer以及Admin分别对应读,写,以及admin权限,读权限角色只能从别的peer节点同步账本而不能发起交易,只有writer定义项下的角色才拥有发起交易的也就是调用chaincode的invoke方法的权限(不一定都是invoke方案,只要涉及到chaincode中状态修改的方法,都只有拥有writer权限或admin权限的角色才能调用)。以该配置的Organizations配置下的Org1配置为例,”OR(‘Org1MSP.admin’, ‘Org1MSP.client’)”,表示org1的msp服务中的admin或者client角色拥有发起交易的权限。文件内容如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149# Copyright IBM Corp. All Rights Reserved.## SPDX-License-Identifier: Apache-2.0#---################################################################################## Profile## - Different configuration profiles may be encoded here to be specified# as parameters to the configtxgen tool#################################################################################Profiles: TwoOrgsOrdererGenesis: Orderer: <<: *OrdererDefaults Organizations: - *OrdererOrg Consortiums: SampleConsortium: Organizations: - *Org1 - *Org2 TwoOrgsChannel: Consortium: SampleConsortium Application: <<: *ApplicationDefaults Organizations: - *Org1 - *Org2################################################################################## Section: Organizations## - This section defines the different organizational identities which will# be referenced later in the configuration.#################################################################################Organizations: # SampleOrg defines an MSP using the sampleconfig. It should never be used # in production but may be used as a template for other definitions - &OrdererOrg # DefaultOrg defines the organization which is used in the sampleconfig # of the fabric.git development environment Name: OrdererOrg # ID to load the MSP definition as ID: OrdererMSP # MSPDir is the filesystem path which contains the MSP configuration MSPDir: crypto-config/ordererOrganizations/example.com/msp - &Org1 # DefaultOrg defines the organization which is used in the sampleconfig # of the fabric.git development environment Name: Org1MSP # ID to load the MSP definition as ID: Org1MSP MSPDir: crypto-config/peerOrganizations/org1.example.com/msp AnchorPeers: # AnchorPeers defines the location of peers which can be used # for cross org gossip communication. Note, this value is only # encoded in the genesis block in the Application section context - Host: peer0.org1.example.com Port: 7051 - &Org2 # DefaultOrg defines the organization which is used in the sampleconfig # of the fabric.git development environment Name: Org2MSP # ID to load the MSP definition as ID: Org2MSP MSPDir: crypto-config/peerOrganizations/org2.example.com/msp AnchorPeers: # AnchorPeers defines the location of peers which can be used # for cross org gossip communication. Note, this value is only # encoded in the genesis block in the Application section context - Host: peer0.org2.example.com Port: 7051################################################################################## SECTION: Orderer## - This section defines the values to encode into a config transaction or# genesis block for orderer related parameters#################################################################################Orderer: &OrdererDefaults # Orderer Type: The orderer implementation to start # Available types are "solo" and "kafka" OrdererType: solo Addresses: - orderer.example.com:7050 # Batch Timeout: The amount of time to wait before creating a batch BatchTimeout: 2s # Batch Size: Controls the number of messages batched into a block BatchSize: # Max Message Count: The maximum number of messages to permit in a batch MaxMessageCount: 10 # Absolute Max Bytes: The absolute maximum number of bytes allowed for # the serialized messages in a batch. AbsoluteMaxBytes: 98 MB # Preferred Max Bytes: The preferred maximum number of bytes allowed for # the serialized messages in a batch. A message larger than the preferred # max bytes will result in a batch larger than preferred max bytes. PreferredMaxBytes: 512 KB Kafka: # Brokers: A list of Kafka brokers to which the orderer connects # NOTE: Use IP:port notation Brokers: - 127.0.0.1:9092 # Organizations is the list of orgs which are defined as participants on # the orderer side of the network Organizations:################################################################################## SECTION: Application## - This section defines the values to encode into a config transaction or# genesis block for application related parameters#################################################################################Application: &ApplicationDefaults # Organizations is the list of orgs which are defined as participants on # the application side of the network Organizations: 本文的作者是lgy 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"Fabric","slug":"Fabric","permalink":"https://learnblockchain.cn/categories/Fabric/"},{"name":"联盟链","slug":"Fabric/联盟链","permalink":"https://learnblockchain.cn/categories/Fabric/联盟链/"}],"tags":[{"name":"Fabric","slug":"Fabric","permalink":"https://learnblockchain.cn/tags/Fabric/"}]},{"title":"Fabric1.0 交易流程","slug":"fabric_transaction_process","date":"2018-11-21T09:30:27.000Z","updated":"2019-04-05T10:29:32.299Z","comments":true,"path":"2018/11/21/fabric_transaction_process/","link":"","permalink":"https://learnblockchain.cn/2018/11/21/fabric_transaction_process/","excerpt":"这篇文章详细介绍fabric的交易流程,以图片加文字的形式呈现。","text":"这篇文章详细介绍fabric的交易流程,以图片加文字的形式呈现。 Fabric 1.0交易流程Fabric中的所有交易都是通过chaincode执行 应用程序客户端通过SDK调用证书服务(CA)服务,进行注册和登记,并获取身份证书。 应用程序客户端通过SDK创建好交易提案(Proposal),交易提案把带有本次交易要调用的合约标识、合约方法和参数信息以及客户端签名等信息发送给背书(Endorser)节点。 背书(Endorser)节点收到交易提案(Proposal)后,开始进行验证,验证的内容如下: 交易预案是完好的 该预案以前没有提交过(防止重放攻击) 携带的签名是合法的 交易发起者是否满足区块链写策略, 即ACL 权限检查 满足以上要求后,背书节点把’交易预案’作为输入参数,调用chaincode中的函数,chaincode根据当前的账本状态计算出一个’交易结果’,该结果包括返回值,读写集。此时,区块链账本并不会被更新。’交易结果’在被签名后与一个是/否的背书结果一同返回,称之为’预案回复’。 应用程序客户端收到背书(Endorser)节点返回的信息后,判断提案结果是否一致,以及是否收到足够多的背书节点返回的结果(参照指定的背书策略执行),如果没有足够的背书,则中止处理,这个交易就会被舍弃。否则,将交易提案、模拟交易结果和背书信息打包组成一个交易并签名发给Orderer节点(一个排序服务)。 Orderer节点对来自客户端(SDK)的交易信息进行共识排序,分通道对’交易消息’按时间排序,并按通道将交易打包成块,发送给提交(Committer)节点。 提交(Committer)节点收到区块后,会对区块中的每笔交易进行校验,检查交易依赖的输入输出是否符合当前区块链的状态,验证背书策略是否满足,验证完成后将区块追加到本地的区块链,更新账本,并修改世界状态。具体过程如下: 运行验证逻辑(VSCC检查背书策略) 在区块中指明哪些交易是有效和无效的。 在内存或文件系统上把区块加入区块链 将区块内的有效交易写入状态数据库。 发出Event消息,使得客户端通过SDK监听知道哪些交易是有效的或无效的。 本文的作者是lgy 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"Fabric","slug":"Fabric","permalink":"https://learnblockchain.cn/categories/Fabric/"},{"name":"联盟链","slug":"Fabric/联盟链","permalink":"https://learnblockchain.cn/categories/Fabric/联盟链/"}],"tags":[{"name":"Fabric","slug":"Fabric","permalink":"https://learnblockchain.cn/tags/Fabric/"}]},{"title":"联盟链初识以及Fabric环境搭建流程","slug":"fabric_introduction","date":"2018-11-21T09:30:27.000Z","updated":"2019-04-05T10:29:32.239Z","comments":true,"path":"2018/11/21/fabric_introduction/","link":"","permalink":"https://learnblockchain.cn/2018/11/21/fabric_introduction/","excerpt":"这篇文章首先简单介绍了联盟链是什么,再详细的介绍了Fabric环境搭建的整个流程。","text":"这篇文章首先简单介绍了联盟链是什么,再详细的介绍了Fabric环境搭建的整个流程。 区块链分类以参与方式分类,区块链可以分为:公有链、联盟链和私有链。 定义我们知道区块链就是一个分布式的,去中心化的公共数据库(或称公共账本)。而联盟链是区块链的一个分支,所以它本身也是一个分布式的,去中心化的公共数据库,跟其他链的区别就是它是针对特定群体的成员和有限的第三方,其内部指定多个预选节点为记账人,其共识过程受到预选节点控制的区块链 本质联盟链本质仍然是一种私有链,只不过它要比单个小组织开发的私有链更大,但是却没有公有链这么大的规模,可以理解为它是介于公有链和私有链的一种区块链。 联盟链的特点 交易速度快 我们知道对于公有链来说,要想达成共识,必须得由区块链中的所有节点来决定,本身公有链的节点数量就非常庞大,所以处理速度很慢。但对于联盟链来说,由于其节点不多的原因,而且只要当网络上2/3的节点达成共识,就可以完成交易,交易速度自然也就快很多。 数据默认不会公开 不同于公有链,联盟链上的信息并不是所有有访问条件的人就可以访问的,联盟链的数据只限于联盟里的机构及其用户才有权限进行访问。 部分去中心化与公有链不同,联盟链某种程度上只属于联盟内部的所有成员所有,且很容易达成共识,因为其节点数毕竟是有限的。 联盟链项目R3:由40多加银行参与的区块链联盟R3,包括世界著名的银行(如摩根大通、高盛、瑞信、伯克莱、汇丰银行等),IT巨头(如IBM、微软)。 超级账本(Hyperledger):由 Linux基金会在2015年12月主导发起该项目, 成员包括金融,银行,物联网,供应链,制造和科技行业的领头羊。 Fabric介绍我们知道智能合约比较成功的就是以太坊了。以太坊主要是公有链,其实对企业应用来说并不是特别合适,而且本身并没有权限控制功能,面向企业的,主要还是HyperLedger Fabric,当然还有R3的Corda。这里我们主要是讲Fabric。Fabric是一个面向企业应用的区块链框架,基于Fabric的开发可以粗略分为几个层面: 1. 参与Fabric的底层开发,这主要是fabric,fabric-ca和sdk等核心组件。2. 参与Fabric周边生态的开发,如支持如支持fabric的工具explorer, composer等。3. 利用fabric平台开发应用,这就是利用fabirc提供的各种sdk来为应用服务(应用开发) 大部分企业会参与2-3的内容,以3为主来服务应用场景,以2为辅。因为现在除了区块链核心功能尚未完善外,对区块链的管理,运维,监控,测试,优化,调试等工具非常匮乏。企业将不得不面对自己开发一些工作。 Fabric环境依赖Fabric官方推荐的开发环境是基于docker搭建的,使用docker搭建需要一下前置条件: docker一一Docker version 17.06.2-ce 或以上版本 Docker Compose一一1.14或以上版本 Go一一1.10或以上版本, Node.js一一8.9.x或以上版本 Python一一主要是python-pip Fabric环境搭建具体步骤这里使用的是Ubuntu 16.04.4版本 1.安装go及环境变量配置(1)下载最新版本的go二进制文件 1$ wget https://dl.google.com/go/go1.9.2.linux-amd64.tar.gz (2)解压文件 1$ sudo tar -C /usr/local -xzf go1.9.2.linux-amd64.tar.gz (3)配置环境变量 1vim ~/.profile 添加以下内容: 1234export PATH=$PATH:/usr/local/go/binexport GOROOT=/usr/local/goexport GOPATH=$HOME/goexport PATH=$PATH:$HOME/go/bin 编辑保存并退出vi后,记得使这些环境变量生效 1source ~/.profile 2.安装DockerFabric的chaincode是运行在docker里的。 (1) 由于apt官方库里的docker版本可能比较旧,所以先卸载可能存在的旧版本: 1sudo apt-get remove docker docker-engine docker-ce docker.io (2) 更新apt包索引: 1sudo apt-get update (3) 安装以下包以使apt可以通过HTTPS使用存储库(repository): 1sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common (4) 添加Docker官方的GPG密钥: 1234curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -备注:可验证秘钥指纹 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88使用如下命令验证:sudo apt-key fingerprint 0EBFCD88 (5) 使用下面的命令来设置stable存储库: 1sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" (6) 再更新一下apt包索引: 1sudo apt-get update (7) 安装最新版本的Docker CE: 12345sudo apt-get install -y docker-ce注意:在生产系统上,可能会需要应该安装一个特定版本的Docker CE,而不是总是使用最新版本:列出可用的版本:apt-cache madison docker-ce选择要安装的特定版本,第二列是版本字符串,第三列是存储库名称,它指示包来自哪个存储库,以及扩展它的稳定性级别。要安装一个特定的版本,将版本字符串附加到包名中,并通过等号(=)分隔它们:sudo apt-get install docker-ce=<VERSION> (8) 测试是否安装成功 1docker --version (9) 使用阿里提供的镜像,否则后面下载Fabric镜像会非常慢cd到/etc/docker目录下,创建文件daemon.json,输入下面的内容: 123{ "registry-mirrors": ["https://obou6wyb.mirror.aliyuncs.com"]} 保存并退出,接着执行: 12sudo systemctl daemon-reloadsudo systemctl restart docker (10) 查看docker服务是否启动: 1systemctl status docker (11) 若未启动,则启动docker服务: 1sudo service docker start或者sudo systemctl start docker 3.安装最新版本的Docker-compose(1) Docker-compose是支持通过模板脚本批量创建Docker容器的一个组件。在安装Docker-Compose之前,需要安装Python-pip,运行脚本: 1sudo apt-get install python-pip (2) 安装Docker-compose: 1pip install docker-compose (3) 验证是否成功: 1sudo docker-compose --version 安装Docker还可以参考此篇文章 4.Fabric源码下载(1) 新建存放测试、部署代码的目录。 1mkdir -p ~/go/src/github.com/hyperledger/ (2) cd到刚创建的目录 1cd ~/go/src/github.com/hyperledger (3) 下载Fabric,这里使用使用git命令下载源码: 1git clone https://github.com/hyperledger/fabric.git 特别注意这里:直接使用上面的git clone下载会非常慢,因为github.global.ssl.fastly.Net域名被限制了。只要找到这个域名对应的ip地址,然后在hosts文件中加上ip–>域名的映射,刷新DNS缓存就可以了。解决办法:步骤【1】:查询域名global-ssl.fastly.Net和 github.com 公网地址可以使用https://www.ipaddress.com/ 这个查。分别查找下面这两个域名的ip地址: 12github.global.ssl.fastly.netgithub.com 步骤【2】:将ip地址添加到hosts文件 1sudo vim /etc/hosts 在文件下方输入下面内容并保存,前面两个ip就是我们刚才上面查找到的ip: 12151.101.185.194 github.global.ssl.fastly.net192.30.253.113 github.com 步骤【3】:修改完hosts还不会立即生效,你需要刷新DNS缓存,告诉电脑我的hosts文件已经修改了。输入指令:sudo /etc/init.d/networking restart 即可,如果不行也可以尝试重启一下电脑。接下来再去git clone就快很多了。 (4) 由于Fabric一直在更新,新版本的并不稳定,所有我们并不需要最新的源码,需要切换到v1.0.0版本的源码: 1git checkout v1.0.0 5.下载Fabric Docker镜像(1) 前面步骤4下载完成后,我们可以看到当前工作目录(~/go/src/github.com/hyperledger/)下多了一个fabric的文件夹,接下来我们cd到~/go/src/github.com/hyperledger/fabric/examples/e2e_cli目录下执行: 1source download-dockerimages.sh -c x86_64-1.0.0 -f x86_64-1.0.0 (注:一定要下载完所有镜像并且镜像版本要和Fabric版本一致如何没有下载问继续执行source download-dockerimages.sh命令直到在完如图所有镜像),执行完所有会用到的Fabric docker镜像都会下载下来了。运行以下命令检查下载的镜像列表: 1docker images 注意:如果下载时遇到权限问题,需要切换到root用户下:su root(2) 重启Docker 1service docker restart 6.测试Fabric环境是否成功在~/go/src/github.com/hyperledger/fabric/examples/e2e_cli下执行如下命令启动测试 1./network_setup.sh up 这个指令具体进行了如下操作: 编译生成Fabric公私钥、证书的程序,程序在目录:fabric/release/linux-amd64/bin 基于configtx.yaml生成创世区块和通道相关信息,并保存在channel-artifacts文件夹。基于configtx.yaml生成创世区块和通道相关信息,并保存在channel-artifacts文件夹。 基于crypto-config.yaml生成公私钥和证书信息,并保存在crypto-config文件夹中。基于crypto-config.yaml生成公私钥和证书信息,并保存在crypto-config文件夹中。 基于docker-compose-cli.yaml启动1Orderer+4Peer+1CLI的Fabric容器。基于docker-compose-cli.yaml启动1Orderer+4Peer+1CLI的Fabric容器。在CLI启动的时候,会运行scripts/script.sh文件,这个脚本文件包含了创建Channel,加入Channel,安装Example02,运行Example02等功能。 运行完如果出现下图所示,说明整个Fabric网络已经通了。 这里记录本人测试Fabric环境是否成功时遇到的问题1. 如果发现运行 ./network_setup.sh up命令 后提示在…fabric/release/linux-amd64/bin文件夹下找不到指定文件解决办法:可以在~/go/src/github.com/hyperledger/fabric/scripts文件下找到 bootstrap.1.0.0.sh文件,手动运行它 ./bootstrap.1.0.0.sh, 此时可以在当前文件夹生成一个bin文件夹,bin里面的文件就是我们需要的,将它拷贝到前面的…fabric/release/linux-amd64/bin文件夹下 2. 如果出现:Error on outputBlock: Error writing genesis block: open ./channel-artifacts/genesis.block: is a directory不能生成创世块的错误。解决办法:可以在~/go/src/github.com/hyperledger/fabric/examples/e2e_cli/channel-artifacts目录下,将genesis.block这个目录删除,rm -rf genesis.block/ 3. 如果出现:.ERROR: for orderer.example.com Cannot start service orderer.example.com: b’OCI runtime create failed: container_linux.go:348: starting container process caused “process_linux.go:402: container init caused \\“rootfs_linux.go:58:解决办法:执行./network_setup.sh down 清除网络后再启动即可 测试Fabric网络接下来我们手动测试下Fabric网络,Fabric提供了SDK和CLI两种交互方式,这里我们使用的是CLI。这里我们使用官方提供的小例子进行测试,在官方例子中,channel名字是mychannel,链码(智能合约)的名字是mycc。首先要登录到CLI这个容器中,才能执行Fabric的CLI命令: 1docker exec -it cli bash 这时用户名变为root@caa22f87a5bf,当前目录变为/opt/go/src/github.com/hyperledger/fabric/peer#,接着可执行peer命令,体验区块链的命令行使用方式。 1.查看a账户的余额 1peer chaincode query -C mychannel -n mycc -c '{"Args":["query","a"]}' 此时我们可以看到控制台输出有: 1Query Result: 90 这里90就是a账户的余额 2.调用链码,转账 这里我们让b账户向a账户转账10: 1peer chaincode invoke -o orderer.example.com:7050 --tls true --cafile /opt/go/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n mycc -c '{"Args":["invoke","b","a","10"]}' 转账成功后,我们可以看到有输出如下提示: 1DEBU 009 ESCC invoke result: version:1 response:<status:200 message:"OK" 接下来我们使用前面的命令继续查看a账户的余额,输出结果如下: 1Query Result: 100 很明显我们已经转账成功了。 退出cli容器: 直接执行 1exit 最后如果我们要关闭Fabric网络,cd到~/go/src/github.com/hyperledger/fabric/examples/e2e_cli下(注意这里的路径按自己前面创建的,不一定要和我一样),执行: 1./network_setup.sh down 参考文章https://blog.csdn.net/github_34965845/article/details/80610060https://www.cnblogs.com/preminem/p/7729497.htmlhttps://www.cnblogs.com/gao90/p/8692642.htmlhttps://blog.csdn.net/so5418418/article/details/78355868https://blog.csdn.net/iflow/article/details/77951610https://blog.csdn.net/vivian_ll/article/details/79966210 本文的作者是lgy 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。 深入浅出区块链知识星球最专业技术问答社区,加入社区还可以在微信群里和300多位区块链技术爱好者一起交流。","categories":[{"name":"Fabric","slug":"Fabric","permalink":"https://learnblockchain.cn/categories/Fabric/"},{"name":"联盟链","slug":"Fabric/联盟链","permalink":"https://learnblockchain.cn/categories/Fabric/联盟链/"}],"tags":[{"name":"Fabric","slug":"Fabric","permalink":"https://learnblockchain.cn/tags/Fabric/"}]},{"title":"深入理解Plasma(四)Plasma Cash","slug":"plasma-cash","date":"2018-11-16T04:44:17.000Z","updated":"2019-04-05T10:29:32.232Z","comments":true,"path":"2018/11/16/plasma-cash/","link":"","permalink":"https://learnblockchain.cn/2018/11/16/plasma-cash/","excerpt":"这一系列文章将围绕以太坊的二层扩容框架 Plasma,介绍其基本运行原理,具体操作细节,安全性讨论以及未来研究方向等。本篇文章主要介绍在 Plasma 框架下的项目 Plasma Cash。","text":"这一系列文章将围绕以太坊的二层扩容框架 Plasma,介绍其基本运行原理,具体操作细节,安全性讨论以及未来研究方向等。本篇文章主要介绍在 Plasma 框架下的项目 Plasma Cash。 在上一篇文章中我们已经理解了 Plasma 的最小实现 Plasma MVP 如何使用 UTXO 模型实现 Plasma 链下扩容的核心思想。但由于 Plasma MVP 本身过于简单,并不能用于实际的生产环境中。2018 年 3 月,在巴黎举行的以太坊开发者大会上,Vitalik 发布了 Plasma Cash 模型[1],可以视为对 Plasma MVP 的改进。Plasma Cash 与 Plasma MVP 的主要区别是每次存款操作都会产生一个唯一的 coin ID 对应转移到侧链上的资产,并使用一种称为稀疏梅克尔树(Sparse Merkle Tree)的数据结构存储交易历史。由此带来的好处是用户不需要关注子链上的每个动态,只需要关注跟自己的 token 有关的动态。在下文中将介绍具体细节。 存款(Deposits)Plasma Cash 中的每次存款操作都会对应产生一个 NFT(non-fungible token)[2]。NFT 可以简单理解为“不可互换的 token”,即每个 token 都是独一无二的,由唯一的 ID 标记。以太坊官方为 NFT 提供了 ERC721 标准[3],在之前火爆到阻塞以太坊的 CryptoKitties 就是由 ERC721 合约实现的。 在 Plasma Cash 中,当用户向 Plasma 合约发送存款交易时,合约会生成一个与存款等值的 token,并给这个 token 分配一个唯一的 ID。如果一个用户分别执行两次存款操作,且每次存款都是 5 ETH,那么他将得到相等价值的两个完全不同的 token。和 Plasma MVP 一样,每次存款操作都会使得 Plasma 合约产生一个只包含这个存款交易的区块。 Plasma Cash 区块Plasma Cash 中的每个 token 都被分配唯一的 ID,因此可以按 ID 的顺序存储每个 token 的交易历史。Plasma Cash 的区块按 token ID 的顺序给每个 token 分配了一个插槽(slot),每个插槽会记录这个 token 是否被交易的信息。例如在下图(来源[4])的区块中,包含 4 个 token,id 分别是 #1,#2,#3,#4。其中 #1,#2,#3 被标记为没有被花费,而 #4 由用户 A 发送给用户 B。 从上面这个例子中我们可以看到,每个插槽记录了其所对应的 token 在当前区块中的交易状态,所有存储了某个 token 的交易状态的区块按时间顺序连在一起就构成了这个 token 的全部交易历史。每当一个 token 被分配了一个 id,之后的所有交易状态都会被保存在每个区块相同的插槽中,也不会被其它 token 取代。因此,用户只需要关注每个区块中存储属于自己的 token 的状态,完全不用关心别的插槽存储的内容。 交易与验证由于 Plasma Cash 中的节点只追踪属于自己的 token 的交易历史,因此当有交易发生时,token 的发送者要向接收者提供关于这个 token 所有的交易历史(从存款交易开始)以便接收者验证。从下图(来源[4])的例子中可以看到 4 个区块中所记录的 4 个 token 的交易历史。 截止到区块 #4,可以看到token #1 和 token #3 始终没有被交易。token #2 在区块 #2 被 E 发送给了 F,在区块 #4 被 F 发送给了 G,在其它区块没有发生交易,token #2 的最终所有权归 G。token #4 在区块 #1 被 A 发送给了 B,在区块 #3 被 B 发送给了 C,在其它区块没有发生交易,token #4 的最终所有权归 C。F 为了向 G 证明 token #2 的合法性,需要向 G 提供 token #2 在前 4 个区块中的所有交易历史,也就是说不仅需要包括区块 #2 中 E => F 的交易证明、区块 #4中 F => G 的交易证明,还要包括在区块 #1 和 #3 中没有被交易的证明。到这里可能感觉有点奇怪,为什么还要包括没有被交易的证明?这是为了防止双花,因为 G 并不知道在区块 #1 和 #3 中 token #2 是否被交易给了其它人。假如 F 在区块 #3 中将 token #2 发送给了 H,并且对 G 隐瞒了这个交易,那么发生在区块 #4 中的 F => G 就是非法(双花)的。因此,在 Plasma Cash 中,完整且合法的交易历史是一个 token 被安全交易的前提。 稀疏梅克尔树(Sparse Merkle Tree)在上文中我们已经了解到一个交易的成功的前提是需要发送方提供关于一个 token 的完整交易历史。完整的交易历史既包括这个 token 在哪些区块被交易的信息,也包括这个 token 在哪些区块没有被交易的信息。我们都知道,在区块链中,使用梅克尔树(Merkle Tree,MT)构造梅克尔证明(Merkel Proof, MP)可以在 O(logN)的时间复杂度验证一个交易是否存在一个区块中。但想要证明一个交易没有存在一个区块中,使用标准的梅克尔树却没那么容易。因此,Plasma Cash 中使用了一种称为稀疏梅克尔树(Sparse Merkle Tree,SMT)的数据结构存储交易数据,能够在O(logN)的时间复杂度验证一个交易不存在。 SMT 实际上一点都不复杂,它的叶子节点是按数据集中的元素序号顺序排列的。如果某个叶子节点对应的元素为空,那么该叶子节点将存储一个特定的值(例如 0 的哈希值)。一个简单的 SMT 示例如下图(来源[5])所示。 扩展到 Plasma Cash 中,SMT 的叶子节点对应了区块中给每个 token 分配的插槽,按照每个 token 的 ID 排序。每个叶子节点存储对应的 token 的交易信息,如果 token 在这个区块中没有被交易,则相应的叶子节点存储的值为 null。 以上图为例,如果需要证明交易 A 存在,就像在标准的 MT 中一样,需要构造 MP:H(null) 和 H(H(null) + H(D))。如果需要证明 B 不存在,同样很简单,我们已经知道 B 的位置是第二个叶子节点,如果 B 不存在,那么该节点存储的值应该为 null。因此就像在标准的 MT 中证明存在的 MP 一样,只不过需要加上 H(null) 作为 MP 的一部分,即 MP:H(null)、H(A)和 H(H(null)+H(D))。 取款/退出(Withdrawl/Exit)Plasma Cash 中的取款操作在流程上跟 Plasma MVP 大体相同,都要从提交取款申请开始,经历争议期之后才能完成。由于 Plasma Cash 中采用的数据结构不同,在取款时需要提交的 token 所有权证明不同,因此当争议发生时需要提交的争议证明也不同。 提交取款申请在向 Plasma 合约提交关于某个 token 的取款申请时,需要提供关于这个 token 最近的两次交易证明。例如,在上图中,假如 G 想要取走 token #2 到主链,那么他需要提交关于 F => G 以及 E => F 的 Merkle Proof。 提交争议取款者在提交了取款申请之后同样需要支付一定的保证金,并等待一段时间的争议期。在这期间如果有其它节点提交了有效的争议证明,那么取款者不但无法完成取款操作,也会损失全部或部分的保证金。 目前 Plasma Cash 支持三种争议证明,分别应对三种不同的攻击场景(具体会在后文分析): 已花费证明。如果能证明正在取款的 token 已经被花费,那么取款立即被取消; 双花证明。如果能证明取款申请中提供的两次交易证明中间还有别的交易,即发生了双花,那么取款立即被取消; 非法交易历史证明。用户还可以对正在取款的 token 的其它交易历史提出争议。这种争议不会立刻阻断取款,而是强制取款者提交其它交易证明来反驳争议,如果没有在规定时间内反驳,则取款被取消。 攻击场景在这一节将讨论已有的 3 种攻击场景以及如何构造争议分别应对这些攻击[6]。在这里假设 Plasma Cash 中存在不可信的 operator 接收所有的交易并构造区块。 发送交易后立即退出如下图(来源[7])所示,假设攻击者 Alice 向 Bob 发送了一个 token A,且 Bob 已经验证了 A 的交易历史没有问题,交易在区块 N+X 得到确认。在这之后,Alice 立即提交取款申请,企图将 token A 取回主链,并提交 A 在区块 N 以及之前的交易证明。为了应对这种情况,Bob 必须及时发现 Alice 的取款行为,并且在争议期结束前提交在区块 N+X 中 token A 被 Alice 发送给 Bob 的证明。这里需要注意的是,如果 Bob 在区块 N+Y 将 token A 发送给 Charlie 的交易是不能被当做争议证明的,只有最接近被争议的交易的下一个交易证明有效。 双花攻击双花攻击需要 operator 配合,将含有已经被花费的 token 的交易打包入下一个区块中。如下图所示(来源[7]),攻击者 Alice 和 Charlie 是同谋,Alice 向 Bob 发送一个 token A 在区块 N+X 被确认,之后 Alice 又将 token A 发送给 Charlie,并在区块 N+Y 被确认。这时在主链看来,Bob 和 Charlie 都是 token A 的合法拥有者。接下来,Charlie 立即提交取款申请,企图取走 token A。Bob 为了防止自己的 token 被盗,可以在争议期内提交在区块 N+X 被确认的交易,表明自己在 Charlie 之前已经拥有了 token A。 取款包含非法交易历史这种攻击需要联合比较多的同谋者。如下图所示,Alice 在区块 N 拥有 token A。Bob 联合 operator、Charlie 以及 Dylan 企图盗走 Alice 的 token。首先,operator 伪造 Alice 将 token A 发送给 Bob 的交易,并在区块 N+X 得到确认,之后 Bob 将 token 发送给 Charlie,在区块 N+Y 确认。同样地,Charlie 接着将 token 发送给 Dylan,在区块 N+Z 确认。这是,Dylan 提出取款申请,企图取走 token A。Dylan 用于取款申请的两个交易证明 Charlie => Dylan 和 Bob => Charlie 都是合法的,但 token A 的交易历史中有一部分是伪造的。Alice 为了证明自己是 token A 的最新合法拥有者,可以提出争议,要求 Dylan 提供 Alice => Bob 的交易证明,同时 Alice 需要提交一部分保证金(否则任何人都可以随便提出争议)。Dylan 必须在一定的时间内提供合法的交易证明,否则取款失效。 相关项目 Talk is cheap, show me your code. 目前已经有许多机构和公司已经实现了 Plasma Cash,但实现的语言和细节有所不同: Loom Network [8] Omisego [9] Wolk [10] Lucidity [11] 总结本篇介绍了 Plasma 框架下的基于 NFT 的项目 Plasma Cash。Plasma Cash 给每个新转移的 token 分配一个唯一的 token ID,并且用稀疏梅克尔树存储交易,使得用户可以只关注跟自己的 token 有关的动态,而不需要关注其它 token。Plasma Cash 可以被看作 Plasma 逐渐迈向成熟的一步,已经有很多公司使用 Plasma Cash 搭建自己的平台和应用,例如 Loomnetwork 公司搭建了自己的 Plasma Cash 子链并且编写了 SDK 支撑开发者在上面开发新的应用。然而 Plasma Cash 本身仍然存在较多的问题,例如 token 无法被分隔合并、需要提交的证明过长等。在接下来的文章中还会继续跟进 Plasma 最新的进展。 相关资源 https://ethresear.ch/t/plasma-cash-plasma-with-much-less-per-user-data-checking/1298 https://en.wikipedia.org/wiki/Non-fungible_token http://erc721.org/ https://github.com/ethsociety/learn-plasma https://medium.com/@kelvinfichter/whats-a-sparse-merkle-tree-acda70aeb837 https://karl.tech/plasma-cash-simple-spec/ https://github.com/loomnetwork/plasma-paper/blob/master/plasma_cash.pdf https://github.com/loomnetwork/plasma-cash https://github.com/omisego/plasma-cash https://github.com/wolkdb/deepblockchains/tree/master/Plasmacash https://github.com/luciditytech/lucidity-plasma-cash 本文的作者是盖盖,他的微信公众号: chainlab 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Plasma","slug":"ethereum/Plasma","permalink":"https://learnblockchain.cn/categories/ethereum/Plasma/"}],"tags":[{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"},{"name":"Plasma","slug":"Plasma","permalink":"https://learnblockchain.cn/tags/Plasma/"},{"name":"扩容","slug":"扩容","permalink":"https://learnblockchain.cn/tags/扩容/"}]},{"title":"深入理解Plasma(三)Plasma MVP","slug":"plasma-mvp","date":"2018-11-03T08:54:17.000Z","updated":"2019-04-05T10:29:32.254Z","comments":true,"path":"2018/11/03/plasma-mvp/","link":"","permalink":"https://learnblockchain.cn/2018/11/03/plasma-mvp/","excerpt":"这一系列文章将围绕以太坊的二层扩容框架 Plasma,介绍其基本运行原理,具体操作细节,安全性讨论以及未来研究方向等。本篇文章主要介绍 Plasma 的一个最小实现 Plasma MVP(Minima Viable Plasma)。","text":"这一系列文章将围绕以太坊的二层扩容框架 Plasma,介绍其基本运行原理,具体操作细节,安全性讨论以及未来研究方向等。本篇文章主要介绍 Plasma 的一个最小实现 Plasma MVP(Minima Viable Plasma)。 在上一篇文章中我们已经理解了 Plasma 中的一些关键操作,但是 Plasma 是一套框架,如果脱离了实际的应用,仍然很难彻底理解它。因此本篇将详细介绍 Plama 的第一个项目 Plasma MVP(Minimal Viable Plasma),即在 Plasma 框架下的最基础的实现。Plasma MVP 是 Vitalic 和他的团队在 2018 年初提出的基于 UTXO 模型实现的 Plasma 设计标准[1],它以最简单的方式实现了链下交易,但无法支持复杂的计算,例如脚本(Script)和智能合约。在阅读下面的内容之前,请确保已经理解了这个系列之前的文章。 整个 Plasma MVP 的生命周期可以通过下面这幅图表现出来: Plasma 合约首先需要将 Plasma 合约部署到主链(以太坊)上作为主链和子链沟通的媒介。Plasma 合约会处理由子链提交的区块,并且将区块的哈希值存在主链上。除此之外,还会处理用户的存款(deposit)、取款(withdrawal/exit)以及争议(challenge)操作。 Plasma 合约中主要包括的数据结构有: Owner:合约的拥有者(即部署合约交易的发送者)的地址,即部署合约交易的发送者; Plasma 区块列表:每个 Plasma 区块中存储了(1)区块的 Merkle root(2)区块提交的时间; 退出列表:即提交了退出申请的列表,每个退出申请存储了(1)申请者的地址(2)申请退出的 UTXO 的位置。 Plasma 合约中主要包括的函数有: submitBlock(bytes32 root):向主链提交一个区块,仅仅提交区块中所有交易的 Merkle root; deposit():生成一个只包含一个交易的区块,这个交易中包含与 msg.value 值相等的 UTXO; startExit():执行给定 UTXO 的退出操作; challengeExit():向某个正在执行的退出提出争议。 Operator在前面的文章中我们已经知道 Plasma 子链是一个独立的区块链,那么也就有独立的共识机制。在 Plasma MVP 中采用的共识机制就是 PoA(Proof of Authority),即参与共识的只有唯一一个矿工,称为 Operator。Operator 负责处理所有子链上发生的交易,将其打包成区块存储在子链上,并且周期性地向 Plasma 合约提交区块,将子链上的状态(区块的哈希值)提交到主链共识。那么,既然 Operator 是唯一的矿工,这不就意味着 Plasma 违背了去中心化的初衷了吗?其实,这是去中心化向执行效率的妥协。在之前的文章中也提到过,Plasma 的安全基础依赖于底层的区块链,只要底层的区块链能够保证安全,那么在 Plasma 子链上发生的最差结果也只是迫使用户退出子链,而不会造成资产损失。 Operator 可以采用最简单的 REST API 方式实现,子链中的用户可以通过调用简单的 API 获取到子链中区块的数据。 存款(deposit)用户 Alice 通过存款(deposit)操作向 Plasma 合约发送带有一定数额的以太币或 ERC20 token 加入 Plasma Chain,这时 Plasma 合约会执行 deposit() 函数,生成一个只包含一个交易的区块,这个交易的 UTXO 记录了 Alice 从主链转移到子链的数额。当这个区块被主链确认后,Alice 就可以使用新生成的 UTXO 向其它用户发送交易了。 交易(transaction)在 Plasma MVP 中,所有用户发送的交易都是直接发送给 Operator,当积累了一定数量的交易后,由 Operator 将交易打包成区块。这里需要注意的是,由于 Plasma MVP 采用的是 UTXO 模型,所以即使交易的收款方不存在,交易也是成立的。 在子链上 Alice 向 Bob 发送一个交易的流程如下: Alice 首先需要得到 Bob 在子链上的地址; Alice 将一个或多个 UTXO 作为输入构造交易发送到 Bob 的地址,并对交易签名; 等待该交易被打包到区块中; Alice 向 Bob 发送确认消息,并且使用相同的私钥签名。 生成区块在 Plasma MVP 中,一个 Plasma 区块产生的情况只有两种:一种是 Operator 打包生成区块,另外一种是当用户执行 deposit 操作时,由 Plasma 合约直接生成一个只包含一个交易的区块。 监视子链为了保证子链上资产的安全,用户需要周期性地检查子链上的数据,保证没有恶意交易产生。用户需要运行一种自动化的软件(例如钱包),每隔一段时间下载子链中的区块数据,检查每个区块中的交易,如果有恶意交易产生,立即退出子链。 取款/退出(withdrawal/exit)当 Alice 想要退出子链时,需要向 Plasma 合约发送一个 exit 交易,申请中需要包含(1)所要退出的 UTXO 的位置,包括区块号(blknum)、区块内交易号(txindex)以及交易内输出号(outindex)(2)包含该 UTXO 的交易(3)该交易的 Merkle proof(4)用于生成该 UTXO 所涉及的之前一系列交易的确认签名。除此之外,exit 交易中还要包含“退出押金(exit bond)”。如果这个 exit 被 challenge 成功,那么取款的操作将被取消,而且退出押金将被发送给提出 challenge 的用户。 之后这个申请会被放入一个优先队列中,通过这个公式计算优先级: Priority = blknum 1000000000 + txindex 10000 + oindex 之所以采用这种优先队列的方式处理取款顺序的原因是保证旧的 UTXO 总能优先于新的 UTXO 被取出。也就是说,当有恶意交易(例如双花等)产生时,所有在恶意交易发生之前的交易都可以被优先取出。那么如何解决在恶意交易之后被确认的交易的取款问题呢?Plasma MVP 采用了“确认签名(Confirmation Signatures)”的机制,在下一小节我们将介绍这一机制是如何工作的。 确认签名(Confirmation Signatures)在 Plasma MVP 中,用户的退出顺序以所要退出的 UTXO 所在的交易的位置为准。假如 operator 作恶,在一个合法的交易之前插入一个非法的交易,那么当用户执行取款时,由于非法交易可以先被取出,因此当执行到该用户的交易时,可能 Plasma 合约中的资产已经被取空。为了解决这个问题,Plasma MVP 采用了“确认签名”机制,例如当 Alice 产生一个交易时,她首先会对交易签名。当该交易被打包入区块后,Alice 还需要对该交易进行一次签名,即“确认签名”。 引入确认签名机制后,当 Alice 发现在一个区块中自己的合法交易之前存在非法交易时,可以拒绝对自己的交易进行“确认签名”,同时申请取款。这样可以使得当前的交易失效,保证自己之前“确认签名”后的交易可以优先于非法交易之前取出。 这种确认签名机制极大地破坏了用户体验,用户每产生一个交易都要经历签名->等待确认->确认签名。而且由于确认签名也需要占据 Plasma 区块的空间,因此也降低了子链的可扩展性。为了解决这个问题,Plasma 的研究人员提出了扩展版本 More Viable Plasma 移除了确认签名的要求[2]。 争议(Challenge)每个取款操作都会经历一个争议期。例如在 Alice 的某个 UTXO 退出子链的过程中,如果 Bob 在争议期内发现有恶意行为发生,他可以提出一个争议(challenge)。一个争议需要给出针对的 UTXO 的位置,以及该 UTXO 被花费的证明,即该 UTXO 已经存在于某个交易中,且这个交易已经被打包到区块。 合约通过调用 challengeExit() 函数执行一个争议,争议成功后会取消正在执行的取款操作,并将提交取款申请所冻结的押金发送给 Bob。 攻击场景在 Plasma 子链中主要存在两种攻击场景: Alice 试图忽视在子链中转移给 Bob 的资产,使用最初加入 Plasma 子链时的交易证明向主链提出取款申请。 Operator 生成一个恶意交易,占有其他用户的资产,并且尝试退出子链。 下面对这两个攻击场景进行分析,观察 Plasma MVP 如何保证资产的安全性: 场景1 Alice 使用最初加入子链时生成的交易作为证据向主链提出取款申请; Bob(或者其他任意用户)拥有 Alice 申请退出的 UTXO 被花费的交易证明,并将此作为证据向主链提出一个争议; 争议生效,Alice 的退出申请被驳回,同时将 Alice 申请退出的押金发送给 Bob; Alice 的攻击失效。 场景2 Operator 创建了一个非法交易,并且将其打包生成区块之后在主链得到确认; Operator 提交取款申请,打算将 Alice 的资产取走; 在争议期内,Alice 发现了 Operator 的恶意行为,立即提出取款申请,退出子链; 由于 Alice 的申请优先级较高,因此会在 Operator 之前退出; Operator 的攻击失效。 相关项目 Talk is cheap, show me your code. 目前已经有许多机构和公司已经实现了 Plasma MVP,但实现的语言和细节有所不同: FourthState Lab[3] Omisego[4] Kyokan[5] 总结本文介绍了 Plasma 的最小实现版本 Plasma MVP,虽然采用最简单的 UTXO 模型,但已经足够体现出 Plasma 的核心思想。在 Plasma MVP 中,用户资产的安全主要依赖于用户及时发现恶意行为,并退出子链。接下来的文章将会介绍另外一个稍微复杂一点的项目,Plasma Cash。 相关资源 https://ethresear.ch/t/minimal-viable-plasma/426 https://ethresear.ch/t/more-viable-plasma/2160 https://github.com/fourthstate https://github.com/omisego/plasma-mvp https://github.com/kyokan/plasma 本文的作者是盖盖,他的微信公众号: chainlab 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Plasma","slug":"ethereum/Plasma","permalink":"https://learnblockchain.cn/categories/ethereum/Plasma/"}],"tags":[{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"},{"name":"Plasma","slug":"Plasma","permalink":"https://learnblockchain.cn/tags/Plasma/"},{"name":"扩容","slug":"扩容","permalink":"https://learnblockchain.cn/tags/扩容/"}]},{"title":"以太坊钱包开发系列4 - 发送Token(代币)","slug":"eth-web-wallet_4","date":"2018-10-26T09:34:44.000Z","updated":"2019-04-05T10:29:32.235Z","comments":true,"path":"2018/10/26/eth-web-wallet_4/","link":"","permalink":"https://learnblockchain.cn/2018/10/26/eth-web-wallet_4/","excerpt":"以太坊去中心化网页钱包开发系列,点链接观看视频课程,将从零开始开发出一个可以实际使用的钱包,本系列文章是理论与实战相结合,一共有四篇:创建钱包账号、账号Keystore文件导入导出、展示钱包信息及发起签名交易、发送Token(代币),本文是第四篇,Token(代币、通证)是以太坊的一大特色,既然开发钱包,则发送Token 功能必不可少。","text":"以太坊去中心化网页钱包开发系列,点链接观看视频课程,将从零开始开发出一个可以实际使用的钱包,本系列文章是理论与实战相结合,一共有四篇:创建钱包账号、账号Keystore文件导入导出、展示钱包信息及发起签名交易、发送Token(代币),本文是第四篇,Token(代币、通证)是以太坊的一大特色,既然开发钱包,则发送Token 功能必不可少。 合约 ABI 信息首先我们需要明白,进行Token转账的时候,其实是在调用合约的转账函数,而要调用一个合约的函数,需要知道合约的 ABI 信息。 其次 通常我们所说的Token, 其实指的是符合 ERC20 标准接口的合约, ERC20 接口定义如下: 1234567891011121314151617181920contract ERC20Interface { string public constant name = \"Token Name\"; string public constant symbol = \"SYM\"; uint8 public constant decimals = 0; function totalSupply() public constant returns (uint); function balanceOf(address tokenOwner) public constant returns (uint balance); function allowance(address tokenOwner, address spender) public constant returns (uint remaining); function approve(address spender, uint tokens) public returns (bool success); function transfer(address to, uint tokens) public returns (bool success); function transferFrom(address from, address to, uint tokens) public returns (bool success); event Transfer(address indexed from, address indexed to, uint tokens); event Approval(address indexed tokenOwner, address indexed spender, uint tokens);} ABI 全称是 Application Binary Interface,它就是合约接口的描述,因此有了合约的接口定义,就可以很容易通过编译拿到ABI 信息,比如像下图在Remix 的编译选项卡就可以直接复制ABI。 生成的 ABI 描述大概长这样: 12345678910111213141516171819202122232425262728293031323334353637[... { \"constant\": true, \"inputs\": [], \"name\": \"totalSupply\", \"outputs\": [ { \"name\": \"\", \"type\": \"uint256\" } ], \"payable\": false, \"stateMutability\": \"view\", \"type\": \"function\" }, { \"constant\": true, \"inputs\": [ { \"name\": \"tokenOwner\", \"type\": \"address\" } ], \"name\": \"balanceOf\", \"outputs\": [ { \"name\": \"balance\", \"type\": \"uint256\" } ], \"payable\": false, \"stateMutability\": \"view\", \"type\": \"function\" }, ...] 它是一个JSON形式的数组,数组里每一个元素,都是对函数接口的描述,在外部调用合约的时候就需要遵从这个接口,以上面的接口为例,通常一个接口描述包含下述几个字段: name: 函数会事件的名称 type: 可取值有function,constructor,fallback,event inputs: 函数的输入参数,每个参数对象包含下述属性: name: 参数名称 type: 参数的规范型(Canonical Type)。 outputs: 一系列的类似inputs的对象,如果无返回值时,可以省略。 constant: true表示函数声明自己不会改变状态变量的值。 payable: true表示函数可以接收ether,否则表示不能。 接下来在构造合约对象就需要是使用ABI。 构造合约对象ethers.js 构造合约对象很简单,仅需要提供三个参数给ethers.Contract构造函数,代码如下: 123var abi = [...];var addr = \"0x...\";var contract = new ethers.Contract(address, abi, provider); 合约的地址在合约部署之后,可以获得,关于Token合约部署及ERC20相关的概念,这里不展开讲,不熟悉的同学,可以参考我另一篇文章创建自己的数字货币。 只有就可以是使用 contract 对象来调用Token合约的函数。 获取Token余额及转移Token获取Token余额结合UI来实现以下获取Token余额,UI如下: 在HTML里,定义的标签如下: 123456<tr> <th>TT Token:</th> <td> <input type=\"text\" readonly=\"readonly\" class=\"readonly\" id=\"wallet-token-balance\" value=\"0.0\" /></div> </td></tr> 对应的逻辑代码也很简单: 12345 var tokenBalance = $('#wallet-token-balance'); // 直接调用合约方法contract.balanceOf(activeWallet.address).then(function(balance){ tokenBalance.val(balance);}); 转移Token转移Token的UI效果如下: 界面的HTML代码如下: 1234567891011121314151617<h3>转移代币:</h3><table> <tr> <th>发送至:</th> <td><input type=\"text\" placeholder=\"(target address)\" id=\"wallet-token-send-target-address\" /></td> </tr> <tr> <th>金额:</th> <td><input type=\"text\" placeholder=\"(amount)\" id=\"wallet-token-send-amount\" /></td> </tr> <tr> <td> </td> <td> <div id=\"wallet-token-submit-send\" class=\"submit disable\">发送</div> </td> </tr></table> 上面定义了两个文本输入框及一个“发送“按钮,在逻辑处理部分,转移Token代币尽管和获取余额类似,同样是调用合约的方法,不过转移代币需要发起一个交易,因此需要测量gas 消耗。点击发送时运行一下(关键)代码: 1234567891011121314151617181920212223242526var inputTargetAddress = $('#wallet-token-send-target-address');var inputAmount = $('#wallet-token-send-amount');var submit = $('#wallet-token-submit-send');var targetAddress = ethers.utils.getAddress(inputTargetAddress.val());var amount = inputAmount.val();submit.click(function() {// 先计算transfer 需要的gas 消耗量,这一步有默认值,非必须。 contract.estimate.transfer(targetAddress, amount) .then(function(gas) { // 必须关联一个有过签名钱包对象 let contractWithSigner = contract.connect(activeWallet); // 发起交易,前面2个参数是函数的参数,第3个是交易参数 contractWithSigner.transfer(targetAddress, amount, { gasLimit: gas, // 偷懒,直接是用 2gwei gasPrice: ethers.utils.parseUnits(\"2\", \"gwei\"), }).then(function(tx) { console.log(tx); // 介绍刷新上面的Token余额,重置输入框 }); });} 上述有一个地方都要注意一下,在合约调用 transfer 之前, 需要连接一个signer,因为发起交易的时候需要用它来进行签名,在ethers.js API里 Wallet 是 signer(抽象类)的实现类。 所有会更改区块链数据的函数都需要关联签名器,如果是视图函数则只需要连接provider。 ethers.js 的 Contract 提供了一个非常方便方法:contract.estimate.functionName 来计算预测交易的gasLimit。 在发起交易的时候,可以提供一个可选的Overrides参数,在这个参数里可以指定如交易的 gasLimit 、 gasPrice,如果我们不指定这个参数时,会默认使用 contract.estimate 获得的值作为 gasLimit,以及 provider.getGasPrice() 的值来指定 gasPrice。 哈哈,恭喜大家,到这里这里就完整的实现了一个基于以太坊去中心化网页钱包。 这是一条硬广,欢迎订阅深入浅出区块链技术小专栏,目前仅需69元, 订阅就可以查看完整源码,还有其他惊喜哦~。戳链接收看详细的视频课程讲解。 参考文档 Ethers.js 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。 深入浅出区块链知识星球最专业技术问答社区,加入社区还可以在微信群里和300多位区块链技术爱好者一起交流。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"比特币","slug":"比特币","permalink":"https://learnblockchain.cn/tags/比特币/"},{"name":"钱包","slug":"钱包","permalink":"https://learnblockchain.cn/tags/钱包/"},{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"}]},{"title":"以太坊钱包开发系列3 - 展示钱包信息及发起签名交易","slug":"eth-web-wallet_3","date":"2018-10-26T02:31:44.000Z","updated":"2019-04-05T10:29:32.278Z","comments":true,"path":"2018/10/26/eth-web-wallet_3/","link":"","permalink":"https://learnblockchain.cn/2018/10/26/eth-web-wallet_3/","excerpt":"以太坊去中心化网页钱包开发系列,点链接观看视频课程,将从零开始开发出一个可以实际使用的钱包,本系列文章是理论与实战相结合,一共有四篇:创建钱包账号、账号Keystore文件导入导出、展示钱包信息及发起签名交易、发送Token(代币),这是第三篇介绍使用ethers.js的钱包对象获取相关信息及发起你离线交易。","text":"以太坊去中心化网页钱包开发系列,点链接观看视频课程,将从零开始开发出一个可以实际使用的钱包,本系列文章是理论与实战相结合,一共有四篇:创建钱包账号、账号Keystore文件导入导出、展示钱包信息及发起签名交易、发送Token(代币),这是第三篇介绍使用ethers.js的钱包对象获取相关信息及发起你离线交易。 使用 Provider 连接以太坊网络我们前面两篇文章介绍创建(或导入)钱包账号的过程都是是离线的,即不需要依赖以太坊网络即可创建钱包账号,但如果想获取钱包账号的相关信息,比如余额、交易记录,发起交易的话,就需要让钱包连上以太坊的网络。 不管是在 Web3 中,还是Ethers.js 都是使用 Provider 来进行网络连接的,Ethers.js 提供了集成多种 Provider 的方式: Web3Provider: 使用一个已有的web3 兼容的Provider,如有MetaMask 或 Mist提供。 EtherscanProvider 及 InfuraProvider: 如果没有自己的节点,可以使用Etherscan 及 Infura 的Provider,他们都是以太坊的基础设施服务提供商,Ethers.js 还提供了一种更简单的方式:使用一个默认的provider, 他会自动帮我们连接Etherscan 及 Infura。 1let defaultProvider = ethers.getDefaultProvider('ropsten'); 连接Provider, 通常有一个参数network网络名称,取值有: homestead, rinkeby, ropsten, kovan, 关于Provider的更多用法,可以参考Ethers.js Provider。 JsonRpcProvider 及 IpcProvider: 如果有自己的节点可以使用,可以连接主网,测试网络,私有网络或Ganache,这也是本系列文章使用的方式。 使用钱包连接Provider的方法如下: 12345// 连接本地的geth 节点,8545是geth 的端口var provider = new ethers.providers.JsonRpcProvider(\"http://127.0.0.1:8545\");// wallet 为前两篇文章中生成的钱包对象, activeWallet就是后面可以用来请求余额发送交易的对象var activeWallet = wallet.connect(App.provider); 启动geth的需要注意一下,需要使用 --rpc --rpccorsdomain 开启 RPC通信及跨域, 展示钱包详情:查询余额及Nonce连接到以太坊网络之后,就可以向网络请求余额以及获取账号交易数量,使用一下API: 12345activeWallet.getBalance().then(function(balance) {});activeWallet.getTransactionCount().then(function(transactionCount) {}); activeWallet就是后面可以用来请求发送交易的对象 1234567891011121314151617181920212223<h3>钱包详情:</h3><table> <tr><th>地址:</th> <td> <input type=\"text\" readonly=\"readonly\" class=\"readonly\" id=\"wallet-address\" value=\"\" /></div> </td> </tr> <tr><th>余额:</th> <td> <input type=\"text\" readonly=\"readonly\" class=\"readonly\" id=\"wallet-balance\" value=\"0.0\" /></div> </td> </tr> <tr><th>Nonce:</th> <td> <input type=\"text\" readonly=\"readonly\" class=\"readonly\" id=\"wallet-transaction-count\" value=\"0\" /></div> </td> </tr> <tr><td> </td> <td> <div id=\"wallet-submit-refresh\" class=\"submit\">刷新</div> </td> </tr></table> js处理的逻辑就是获取信息之后,填充相应的控件,代码如下: 1234567891011121314151617181920var inputBalance = $('#wallet-balance');var inputTransactionCount = $('#wallet-transaction-count');$(\"#wallet-submit-refresh\").click(function() {// 获取余额时, 包含当前正在打包的区块 activeWallet.getBalance('pending').then(function(balance) { // 单位转换 wei -> ether inputBalance.val(ethers.utils.formatEther(balance, { commify: true })); }, function(error) { }); activeWallet.getTransactionCount('pending').then(function(transactionCount) { inputTransactionCount.val(transactionCount); }, function(error) { });});// 模拟一次点击获取数据$(\"#wallet-submit-refresh\").click(); 发送签名交易之前我们有一篇文章:如何使用Web3.js API 在页面中进行转账介绍过发起交易,不过当时的签名是利用MetaMask来完成的,现在我们要完成一个钱包,必须要发送一个签名交易,签名交易也称为离线交易(因为这个过程可以离线进行:在离线状态下对交易进行签名,然后把签名后的交易进行广播)。 尽管 Ethers.js 提供了非常简洁的API来发送签名交易,但是探究下简洁API背后的细节依然会对我们有帮助,这个过程大致可分为三步: 构造交易 交易签名 发送(广播)交易 构造交易先来看看一个交易长什么样子: 12345678910const txParams = { nonce: '0x00', gasPrice: '0x09184e72a000', gasLimit: '0x2710', to: '0x0000000000000000000000000000000000000000', value: '0x00', data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057', // EIP 155 chainId - mainnet: 1, ropsten: 3 chainId: 3} 发起交易的时候,就是需要填充每一个字段,构建这样一个交易结构。to 和 value: 很好理解,就是用户要转账的目标及金额。data: 是交易时附加的消息,如果是对合约地址发起交易,这会转化为对合约函数的执行,可参考:如何理解以太坊ABInonce: 交易序列号chainId: 链id,用来去区分不同的链(分叉链)id可在EIP-55查询。 nonce 和 chainId 有一个重要的作用就是防止重放攻击,如果没有nonce的活,收款人可能把这笔签名过的交易再次进行广播,没有chainId的话,以太坊上的交易可以拿到以太经典上再次进行广播。 gasPrice和gasLimit: Gas是以太坊的工作计费机制,是由交易发起者给矿工打包的费用。上面几个参数的设置比较固定,Gas的设置(尤其是gasPrice)则灵活的多。 gasLimit 表示预计的指令和存储空间的工作量,如果工作量没有用完,会退回交易发起者,如果不够会发生out-of-gas 错误。一个普通转账的交易,工作量是固定的,gasLimit为21000,合约执行gasLimit则是变化的,也许有一些人会认为直接设置为高一点,反正会退回,但如果合约执行出错,就会吃掉所有的gas。幸运的是web3 和 ethers.js 都提供了测算Gas Limit的方法,下一遍发送代币 gasPrice是交易发起者是愿意为工作量支付的单位费用,矿工在选择交易的时候,是按照gasPrice进行排序,先服务高出价者,因此如果出价过低会导致交易迟迟不能打包确认,出价过高对发起者又比较亏。 web3 和 ethers.js 提供一个方法 getGasPrice() 用来获取最近几个历史区块gas price的中位数,也有一些第三方提供预测gas price的接口,如:gasPriceOracle 、 ethgasAPI、 etherscan gastracker,这些服务通常还会参考当前交易池内交易数量及价格,可参考性更强, 常规的一个做法是利用这些接口给用户一个参考值,然后用户可以根据参考值进行微调。 交易签名在构建交易之后,就是用私钥对其签名,代码如下: 123const tx = new EthereumTx(txParams)tx.sign(privateKey)const serializedTx = tx.serialize() 代码参考ethereumjs-tx 发送(广播)交易然后就是发送(广播)交易,代码如下: 1234web3.eth.sendRawTransaction(serializedTx, function (err, transactionHash) { console.log(err); console.log(transactionHash);}); 通过这三步就完成了发送签名交易的过程,ethers.js 里提供了一个简洁的接口,来完成所有这三步操作(强调一下,签名已经在接口里帮我们完成了),接口如下: 1234567activeWallet.sendTransaction({ to: targetAddress, value: amountWei, gasPrice: activeWallet.provider.getGasPrice(), gasLimit: 21000, }).then(function(tx) { }); 用ethers.js 实现发送交易先来看看发送交易的UI界面: 1234567891011121314<h3>以太转账:</h3><table> <tr> <th>发送至:</th> <td><input type=\"text\" placeholder=\"(target address)\" id=\"wallet-send-target-address\" /></td> </tr> <tr> <th>金额:</th> <td><input type=\"text\" placeholder=\"(amount)\" id=\"wallet-send-amount\" /></td> </tr> <tr> <td> </td> <td> <div id=\"wallet-submit-send\" class=\"submit disable\">发送</div> </td> </tr></table> 上面主要定义了两个文本输入框及一个“发送“按钮,在点击发送时运行一下(关键)代码: 123456789101112131415161718var inputTargetAddress = $('#wallet-send-target-address');var inputAmount = $('#wallet-send-amount');var submit = $('#wallet-submit-send');submit.click(function() {// 得到一个checksum 地址 var targetAddress = ethers.utils.getAddress(inputTargetAddress.val());// ether -> wei var amountWei = ethers.utils.parseEther(inputAmount.val()); activeWallet.sendTransaction({ to: targetAddress, value: amountWei, // gasPrice: activeWallet.provider.getGasPrice(), (可用默认值) // gasLimit: 21000, }).then(function(tx) { console.log(tx); });}) 哈哈哈~, 干活介绍到这里,现在夹带一点私货,又到了推广时间了,完整源码请订阅深入浅出区块链技术小专栏查看,赶紧订阅吧,走过路过,不容错过。戳链接收看详细的视频课程讲解。 参考文档 ethereum-tx EIP-55 Ethers.js 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。 深入浅出区块链知识星球最专业技术问答社区,加入社区还可以在微信群里和300多位区块链技术爱好者一起交流。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"比特币","slug":"比特币","permalink":"https://learnblockchain.cn/tags/比特币/"},{"name":"钱包","slug":"钱包","permalink":"https://learnblockchain.cn/tags/钱包/"},{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"}]},{"title":"以太坊钱包开发系列2 - 账号Keystore文件导入导出","slug":"eth-web-wallet_2","date":"2018-10-25T12:34:44.000Z","updated":"2019-04-05T10:29:32.270Z","comments":true,"path":"2018/10/25/eth-web-wallet_2/","link":"","permalink":"https://learnblockchain.cn/2018/10/25/eth-web-wallet_2/","excerpt":"以太坊去中心化网页钱包开发系列,点链接观看视频课程,将从零开始开发出一个可以实际使用的钱包,本系列文章是理论与实战相结合,一共有四篇:创建钱包账号、账号Keystore文件导入导出、展示钱包信息及发起签名交易、发送Token(代币),这是第二篇,主要介绍钱包账号导出与导入,将对Keystore文件的生成的原理进行介绍。","text":"以太坊去中心化网页钱包开发系列,点链接观看视频课程,将从零开始开发出一个可以实际使用的钱包,本系列文章是理论与实战相结合,一共有四篇:创建钱包账号、账号Keystore文件导入导出、展示钱包信息及发起签名交易、发送Token(代币),这是第二篇,主要介绍钱包账号导出与导入,将对Keystore文件的生成的原理进行介绍。 如何导入Geth创建的账号?在上一篇文章,介绍了如何使用私钥及助记词来创建账号,如果是使用已有的私钥及助记词,这其实也是账号导入的过程。 有一些同学会问,我的账号是Geth生成的,如何导入到钱包呢?使用Geth的同学,应该知道Geth在创建账号时会生成一个对应keystore JSON文件,Keystore文件存储加密后的私钥信息,因此我们需要做的就是导入这个Keystore文件,这个文件通常在同步区块数据的目录下的keystore文件夹(如: ~/.ethereum/keystore)里。 尽管在ethers.js 中,简单的使用一个函数就可以完成keystore文件的导入,不过理解Keystore 文件的作用及原理还是非常有必要的,当然如果你是在没有兴趣,可以直接跳到本文最后一节:使用ethers.js 实现账号导出导入。 详细解读 Keystore 文件为什么需要 Keystore 文件通过这篇文章理解开发HD 钱包涉及的 BIP32、BIP44、BIP39,私钥其实就代表了一个账号,最简单的保管账号的方式就是直接把私钥保存起来,如果私钥文件被人盗取,我们的数字资产将洗劫一空。 Keystore 文件就是一种以加密的方式存储密钥的文件,这样的发起交易的时候,先从Keystore 文件是使用密码解密出私钥,然后进行签名交易。这样做之后就会安全的多,因为只有黑客同时盗取 keystore 文件和密码才能盗取我们的数字资产。 Keystore 文件如何生成的 以太坊是使用对称加密算法来加密私钥生成Keystore文件,因此对称加密秘钥(注意它其实也是发起交易时需要的解密秘钥)的选择就非常关键,这个秘钥是使用KDF算法推导派生而出。因此在完整介绍Keystore 文件如何生成前,有必要先介绍一下KDF。 使用 KDF 生成秘钥密码学KDF(key derivation functions),其作用是通过一个密码派生出一个或多个秘钥,即从 password 生成加密用的 key。 其实在理解开发HD 钱包涉及的 BIP32、BIP44、BIP39中介绍助记词推导出种子的PBKDF2算法就是一种KDF函数,其原理是加盐以及增加哈希迭代次数。 而在Keystore中,是用的是Scrypt算法,用一个公式来表示的话,派生的Key生成方程为: 1DK = Scrypt(salt, dk_len, n, r, p) 其中的 salt 是一段随机的盐,dk_len 是输出的哈希值的长度。n 是 CPU/Memory 开销值,越高的开销值,计算就越困难。r 表示块大小,p 表示并行度。 Litecoin 就使用 scrypt 作为它的 POW 算法 实际使用中,还会加上一个密码进行计算,用一张图来表示这个过程就是: 对私钥进行对称加密上面已经用KDF算法生成了一个秘钥,这个秘钥就是接着进行对称加密的秘钥,这里使用的对称加密算法是 aes-128-ctr,aes-128-ctr 加密算法还需要用到一个参数初始化向量 iv。 Keystore文件好了,我们现在结合具体 Keystore文件的内容,就很容易理解了Keystore 文件怎么产生的了。 123456789101112131415161718192021{ \"address\":\"856e604698f79cef417aab...\", \"crypto\":{ \"cipher\":\"aes-128-ctr\", \"ciphertext\":\"13a3ad2135bef1ff228e399dfc8d7757eb4bb1a81d1b31....\", \"cipherparams\":{ \"iv\":\"92e7468e8625653f85322fb3c...\" }, \"kdf\":\"scrypt\", \"kdfparams\":{ \"dklen\":32, \"n\":262144, \"p\":1, \"r\":8, \"salt\":\"3ca198ce53513ce01bd651aee54b16b6a....\" }, \"mac\":\"10423d837830594c18a91097d09b7f2316...\" }, \"id\":\"5346bac5-0a6f-4ac6-baba-e2f3ad464f3f\", \"version\":3} 来解读一下各个字段: address: 账号地址 version: Keystore文件的版本,目前为第3版,也称为V3 KeyStore。 id : uuid crypto: 加密推倒的相关配置. cipher 是用于加密以太坊私钥的对称加密算法。用的是 aes-128-ctr 。 cipherparams 是 aes-128-ctr 加密算法需要的参数。在这里,用到的唯一的参数 iv。 ciphertext 是加密算法输出的密文,也是将来解密时的需要的输入。 kdf: 指定使用哪一个算法,这里使用的是 scrypt。 kdfparams: scrypt函数需要的参数 mac: 用来校验密码的正确性, mac= sha3(DK[16:32], ciphertext) 下面一个小节单独分析。 我们来完整梳理一下 Keystore 文件的产生: 使用scrypt函数 (根据密码 和 相应的参数) 生成秘钥 使用上一步生成的秘钥 + 账号私钥 + 参数 进行对称加密。 把相关的参数 和 输出的密文 保存为以上格式的 JSON 文件 如何确保密码是对的?当我们在使用Keystore文件来还原私钥时,依然是使用kdf生成一个秘钥,然后用秘钥对ciphertext进行解密,其过程如下: 此时细心的同学会发现,无论使用说明密码,来进行这个操作,都会生成一个私钥,但是最终计算的以太坊私钥到底是不是正确的,却不得而知。 这就是 keystore 文件中 mac 值的作用。mac 值是 kdf输出 和 ciphertext 密文进行SHA3-256运算的结果,显然密码不同,计算的mac 值也不同,因此可以用来检验密码的正确性。检验过程用图表示如下: 现在我们以解密的角度完整的梳理下流程,就可以得到以下图: 用ethers.js 实现账号导出导入ethers.js 直接提供了加载keystore JSON来创建钱包对象以及加密生成keystore文件的方法,方法如下: 123456789// 导入keystore Json ethers.Wallet.fromEncryptedJson(json, password, [progressCallback]).then(function(wallet) { // wallet }); // 使用钱包对象 导出keystore Json wallet.encrypt(pwd, [progressCallback].then(function(json) { // 保存json }); 现在结合界面来完整的实现账号导出及导入,先看看导出,UI图如下: HTML 代码如下: 1234567891011121314<h3>KeyStore 导出:</h3><table> <tr> <th>密码:</th> <td><input type=\"text\" placeholder=\"(password)\" id=\"save-keystore-file-pwd\" /></td> </tr> <tr> <td> </td> <td> <div id=\"save-keystore\" class=\"submit\">导出</div> </td> </tr></table> 上面主要定义了一个密码输入框和一个导出按钮,点击“导出”后,处理逻辑代码如下: 12345678910111213141516// \"导出\" 按钮,执行exportKeystore函数 $('#save-keystore').click(exportKeystore); exportKeystore: function() { // 获取密码 var pwd = $('#save-keystore-file-pwd'); // wallet 是上一篇文章中生成的钱包对象 wallet.encrypt(pwd.val()).then(function(json) { var blob = new Blob([json], {type: \"text/plain;charset=utf-8\"}); // 使用了FileSaver.js 进行文件保存 saveAs(blob, \"keystore.json\"); }); } FileSaver.js 是可以用来在页面保存文件的一个库。 再来看看导入keystore 文件, UI图如下: 1234567891011121314151617 <h2>加载账号Keystore文件</h2><table> <tr> <th>Keystore:</th> <td><div class=\"file\" id=\"select-wallet-drop\">把Json文件拖动到这里</div><input type=\"file\" id=\"select-wallet-file\" /></td> </tr> <tr> <th>密码:</th> <td><input type=\"password\" placeholder=\"(password)\" id=\"select-wallet-password\" /></td> </tr> <tr> <td> </td> <td> <div id=\"select-submit-wallet\" class=\"submit disable\">解密</div> </td> </tr></table> 上面主要定义了一个文件输入框、一个密码输入框及一个“解密“按钮,因此处理逻辑包含两部分,一是读取文件,二是解析加载账号,关键代码如下: 123456789101112131415 // 使用FileReader读取文件,var fileReader = new FileReader(); fileReader.onload = function(e) { var json = e.target.result; // 从加载 ethers.Wallet.fromEncryptedJson(json, password).then(function(wallet) { }, function(error) { }); };fileReader.readAsText(inputFile.files[0]); 哈哈哈,有到了推广时间了,完整源码请订阅深入浅出区块链技术小专栏查看,赶紧订阅吧,走过路过,不容错过。 戳链接收看详细的视频课程讲解。 参考文档what-is-an-ethereum-keystore-file 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。 深入浅出区块链知识星球最专业技术问答社区,加入社区还可以在微信群里和300多位区块链技术爱好者一起交流。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"比特币","slug":"比特币","permalink":"https://learnblockchain.cn/tags/比特币/"},{"name":"钱包","slug":"钱包","permalink":"https://learnblockchain.cn/tags/钱包/"},{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"}]},{"title":"以太坊钱包开发系列1 - 创建钱包账号","slug":"eth-web-wallet_1","date":"2018-10-25T10:34:44.000Z","updated":"2019-04-05T10:29:32.251Z","comments":true,"path":"2018/10/25/eth-web-wallet_1/","link":"","permalink":"https://learnblockchain.cn/2018/10/25/eth-web-wallet_1/","excerpt":"以太坊去中心化网页钱包开发系列,详细的视频课程讲解直接戳链接,本系列将从零开始开发出一个可以实际使用的钱包,本系列是理论与实战相结合,文章一共有四篇:创建钱包账号、账号Keystore文件导入导出、展示钱包信息及发起签名交易、发送Token(代币),这是第一篇,主要介绍钱包将实现哪些功能及怎么创建钱包账号,本钱包是基于ethers.js 进行开发。","text":"以太坊去中心化网页钱包开发系列,详细的视频课程讲解直接戳链接,本系列将从零开始开发出一个可以实际使用的钱包,本系列是理论与实战相结合,文章一共有四篇:创建钱包账号、账号Keystore文件导入导出、展示钱包信息及发起签名交易、发送Token(代币),这是第一篇,主要介绍钱包将实现哪些功能及怎么创建钱包账号,本钱包是基于ethers.js 进行开发。 去中心化网页钱包先明确一下定义,什么是去中心化钱包,账号秘钥的管理,交易的签名,都是在客户端完成, 即私钥相关的信息都是在用户手中,钱包的开发者接触不到私钥信息。 对应的中心化钱包则是私钥由中心服务器托管,如交易所的钱包就是这种。 网页钱包,或者叫web钱包,是指钱包以网页的形式展现,去中心化网页钱包则交易的签名等操作是在浏览器里完成。其他形式的钱包,如Android钱包或iOS钱包其开发思路和web钱包一样,因此文本对开发其他平台的钱包也有参考意义,不过本系列文章主要侧重在钱包功能的实现,并未过多考虑用户体验。 钱包功能一个钱包通常主要包含的功能有: 账号管理(主要是私钥的管理):创建账号、账号导入导出 账号信息展示:如以太币余额、Token(代币)余额。 转账功能:发送以太币及发送Token(代币) 这些功能将基于 ethers.js 进行开发, ethers.js 和web3.js 一样,也是一套和以太坊区块链进行交互的库,不仅如此,ethers.js 还对BIP 39等相关的提案进行了实现,可以在这个链接阅读其文档。 这些功能主要表现为钱包的两个界面,一个界面是:账号管理,一个界面是进行账号信息展示及转账。下面逐个进行介绍 创建钱包账号读过上一篇文章理解开发HD 钱包涉及的 BIP32、BIP44、BIP39的同学,会知道创建账号,可以有两种方式: 直接生成32个字节的数当成私钥 通过助记词进行确定性推导出私钥 使用随机数作为私钥创建钱包账号即方式一,可以使用ethers.utils.randomBytes生成一个随机数,然后使用这个随机数来创建钱包,如代码: 123var privateKey = ethers.utils.randomBytes(32);var wallet = new ethers.Wallet(privateKey);console.log(\"账号地址: \" + wallet.address); 上面代码的 wallet 是 ethers 中的一个钱包对象,它除了有代码中出现的.address 属性之外,还有如 获取余额、发送交易等方法,在后面的文章会进行介绍。 注意ethers.utils.randomBytes 生成的是一个字节数组,如果想用十六进制数显示出来表示,需要转化为BigNumber代码如下: 12let keyNumber = ethers.utils.bigNumberify(privateKey);console.log(randomNumber._hex); 现在我们结合界面,完整的实现创建账号,其效果图如下,加载私钥时创建账号。 界面代码(HTML)代码如下(主要是在表格中定义个一个输入框及一个按钮): 123456789101112<table> <tr> <th>私钥:</th> <td><input type=\"text\" placeholder=\"(private key)\" id=\"select-privatekey\" /></td> </tr> <tr> <td> </td> <td> <div id=\"select-submit-privatekey\" class=\"submit\">加载私钥</div> </td> </tr></table> 对应的逻辑代码(JavaScript)如下: 123456789101112131415// 使用JQuery获取两个UI标签 var inputPrivatekey = $('#select-privatekey'); var submit = $('#select-submit-privatekey');// 生成一个默认的私钥 let randomNumber = ethers.utils.bigNumberify(ethers.utils.randomBytes(32)); inputPrivatekey.val(randomNumber._hex);// 点击“加载私钥”时, 创建对应的钱包 submit.click(function() { var privateKey = inputPrivatekey.val(); if (privateKey.substring(0, 2) !== '0x') { privateKey = '0x' + privateKey; } var wallet = new ethers.Wallet(privateKey)); }); 如果用户提供一个已有账号的私钥,则会导入其原有账号。 通过助记词方式创建钱包账号这是目前主流常见钱包的方式,关于助记词推导过程请阅读理解开发HD 钱包涉及的 BIP32、BIP44、BIP39。 我们需要先生成一个随机数,然后用随机数生成助记词,随后用助记词创建钱包账号,设计到的API有: 12345678910var rand = ethers.utils.randomBytes(16);// 生成助记词var mnemonic = ethers.utils.HDNode.entropyToMnemonic(rand);var path = \"m/44'/60'/0'/0/0\";// 通过助记词创建钱包ethers.Wallet.fromMnemonic(mnemonic, path); 现在我们结合界面来实现一下通过助记词方式创建钱包账号,其效果图如下: 界面代码(HTML)代码如下(主要是在表格中定义个两个输入框及一个按钮): 12345678910111213141516<table> <tr> <th>助记词:</th> <td><input type=\"text\" placeholder=\"(mnemonic phrase)\" id=\"select-mnemonic-phrase\" /></td> </tr> <tr> <th>Path:</th> <td><input type=\"text\" placeholder=\"(path)\" id=\"select-mnemonic-path\" value=\"m/44'/60'/0'/0/0\" /></td> </tr> <tr> <td> </td> <td> <div id=\"select-submit-mnemonic\" class=\"submit\">推倒</div> </td> </tr></table> 对应的逻辑代码(JavaScript)如下: 1234567891011121314151617 var inputPhrase = $('#select-mnemonic-phrase'); var inputPath = $('#select-mnemonic-path'); var submit = $('#select-submit-mnemonic');// 生成助记词 var mnemonic = ethers.utils.HDNode.entropyToMnemonic(ethers.utils.randomBytes(16)); inputPhrase.val(mnemonic); submit.click(function() { // 检查助记词是否有效。 if (!ethers.utils.HDNode.isValidMnemonic(inputPhrase.val())) { return; }// 通过助记词创建钱包对象 var wallet = ethers.Wallet.fromMnemonic(inputPhrase.val(), inputPath.val()); }); 同样用户可以提供一个其保存的助记词来导入其钱包,有一些遗憾的是,ethers.js 暂时不支持通过添加密码作为Salt来保护种子(也可能是我没有找到,如果知道的同学,希望反馈下),如果需要此功能可以引入bip39 和 ethereumjs-wallet 库来实现,代码可参考理解开发HD 钱包涉及的 BIP32、BIP44、BIP39。 小结其实 ethers 还提供了一个更简单的方法来创建钱包: 12// 直接创建一个随机钱包ethers.Wallet.createRandom(); 完整源码请订阅深入浅出区块链技术小专栏查看, 哈哈,是不是有一点鸡贼,创作不易呀。戳链接收看详细的视频课程讲解。 参考文档:ethers.js 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。 深入浅出区块链知识星球最专业技术问答社区,加入社区还可以在微信群里和300多位区块链技术爱好者一起交流。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"比特币","slug":"比特币","permalink":"https://learnblockchain.cn/tags/比特币/"},{"name":"钱包","slug":"钱包","permalink":"https://learnblockchain.cn/tags/钱包/"},{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"}]},{"title":"深入理解Plasma(二)Plasma 细节","slug":"plasma-in-detail","date":"2018-10-24T12:54:17.000Z","updated":"2019-04-05T10:29:32.258Z","comments":true,"path":"2018/10/24/plasma-in-detail/","link":"","permalink":"https://learnblockchain.cn/2018/10/24/plasma-in-detail/","excerpt":"这一系列文章将围绕以太坊的二层扩容框架,介绍其基本运行原理,具体操作细节,安全性讨论以及未来研究方向等。本篇文章主要对 Plasma 一些关键操作的细节进行剖析。","text":"这一系列文章将围绕以太坊的二层扩容框架,介绍其基本运行原理,具体操作细节,安全性讨论以及未来研究方向等。本篇文章主要对 Plasma 一些关键操作的细节进行剖析。 在上一篇文章中我们已经理解了什么是 Plasma 框架以及它是如何运行的,这一篇文章将对其运行过程中的一些关键部分,包括 Plasma 提交区块的过程,当有恶意行为发生时如何构建防伪证明以及如何退出 Plasma 子链等进行剖析。需要注意的是,由于 Plasma 是一套框架,因此本文只剖析 Plasma 项目的共性,每一部分的实现细则还是需要参考实际的项目,例如 Plasma MVP(Minimal-Viable-Plasma)和 Plasma Cash 等。 存款(Deposit)Plasma 的主要思想就是将大部分计算过程都转移到链下进行,用户只有在进入和退出 Plasma Chain 的时候需要跟主链上的智能合约交互,这也是所有 Plasma 应用的标准流程。 用户在将主链的资产(如以太币或者其它 ERC20 合约发布的 token)转移到 Plasma Chain 的过程称为存款(Deposit),具体做法是直接向主链上的 Plasma 合约发送以太币或 token。Plasma 合约收到 Deposit 交易后会在子链上创建跟 Deposit 数额一致的交易,并将其打包进区块中,作为存款确认的证明。这个过程如下图所示(来源自[1])。 当用户看到子链上自己之前存款的交易被确认后,就可以在子链上使用这笔资产(给子链上的其他用户发送交易或者退出子链等)。 状态确认(State Commitment)当大部分都转移到链下进行时,需要某种机制确保链下状态的更新得到确认,这样才能保证当有恶意行为发生时,主链可以保证用户不会受到损失。这就是为什么需要状态确认(State Commitment),即子链周期性地将状态更新情况提交到主链进行共识。 然而,将子链中的所有交易都同步到主链显然违反了 Plasma 的初衷,在 Plasma 中,实际向主链提交的是 Merkle Tree 的根哈希。因此子链中的实际交易情况被隐藏,在主链上只能看到子链区块的哈希值。 当有恶意行为发生时,子链网络中的所有用户都可以向主链提交防伪证明,证明成立后,含有恶意交易的区块将被回滚。 防伪证明(Fraud Proof)Plasma 的一个关键设计之一就是允许用户构造防伪证明(Fraud Proof)。防伪证明的意义在于只要发布区块的节点构造了含有恶意交易的区块,那么就要承担被惩罚的风险。每当新的区块被提交到主链上时,会留出一段时间给用户提交防伪证明,如果在这段时间内没有证明被提交,则认为新的区块被验证合法。如果有防伪证明检测到区块中存在恶意交易,则该区块将被舍弃,回滚到上一个被验证合法的区块。Plasma 中的防伪证明主要有以下(但不限于)几种: 资产可花费证明 交易签名有效性证明 存取款证明 至于每种防伪证明的具体形式,则依赖于实际 Plasma 应用的实现细则。 如下图所示(来源自[1]),子链中每个节点都存有 1-4 个区块的数据。假设区块 1-3 已经被验证合法,而区块 4 中存在恶意交易,那么每个节点都可以使用 1-4 个区块中的数据构造防伪证明提交到主链,主链验证后将子链中的状态回滚到区块 1-3。 防伪证明还可以使用零知识证明(zk-SNARKs 或者 STARKs)来构造,但由于目前通过零知识证明生成证明的时间和空间还有待优化,目前设计的 Plasma 并不依赖零知识证明。零知识证明在 Plasma 中的应用是一个很有前景的研究方向,感兴趣的读者可以参考以太坊研究团队关于这方面的研究[2])。 取款(Withdrawal)取款(Withdrawal),顾名思义,就是从子链中的资产取回到主链上,因此取款也被称为退出(Exit)。取款操作可以说是 Plasma 框架中最重要的一步,因为它确保用户可以安全地将子链上的资产转移到主链上。之前的存款以及状态确认步骤已经产生了一些交易数据,并在主链上得到同步,用户可以利用这些数据构造资产证明,之后执行简单取款(Simple Withdrawal)操作退出子链。当有扣留(Withholding)攻击发生(即子链上的矿工恶意扣留区块,意图双花攻击等)时,用户可能无法成功获取数据构造资产证明,这时需要执行批量取款(Mass Withdrawal)操作退出子链。 需要注意的是,当子链中有取款操作发生时,跟这个取款操作相关的账号或者交易都将被禁止。 简单取款(Simple Withdrawal)执行简单取款的条件是所要取回的资产已经在主链和子链上确认。 一个简单取款的执行主要有以下几个步骤: 向主链上的 Plasma 智能合约发送已签名的取款交易。取款的数额可以包含多个输出(UTXO模型),但所有输出必须在同一个子链当中,而且每个输出的余额必须全部取出,不能只取出一部分。取款数额的一部分还需要被当作押金,作为恶意行为的惩罚。 当取款请求发送后,需要等待一段“争议期(Challenge Period)”,这期间其它节点可以构造证据证明该取款中的部分或全部数额已经被花费。如果争议期内有节点提供证明成功,那么取款被取消,而且押金被扣除。 接下来可能还要等待一段时间,直到所有区块高度较低的取款操作都完成。这是为了保证所有取款操作按照“先来后到”的顺序执行。 当争议期结束后,如果没有争议被提出,则认为取款操作成立,取款者将子链资产成功取回到主链。 快速取款(Fast Withdrawal)快速取款(Fast Withdrawal)跟简单取款相比的差别主要是引入一个中间人,白皮书上称为 Liquidity Provider,这里简称为 LP。如果一个用户不想等待很长的争议期(目前的实现至少要一周),那么它可以选择从 LP 这里直接取款,只需要消耗一个交易确认的时间,代价是需要支付给 LP 一定的费用。由于 Plasma 白皮书上关于快速取款的描述太过晦涩,这里主要参考 kfichter 提出的 Simple Fast Withdrawal[3] 来介绍快速取款是如何实现的。 为了实现快速取款,取款方和 LP 可以利用一个流动合约(liquidity contract)。假设取款方是 Alice,她想要执行快速取款将 10 以太币从子链转移到主链。她首先向流动合约发送 10 以太币,注意这里的交易是在子链上进行的。当这个交易被子链打包成区块后,Alice 可以调用合约中的某个退出函数,这时 Alice 将获取一个代表她这笔资产的 token。Bob 作为 LP,他检查了子链上数据之后证明 Alice 的取款没有问题之后愿意以 9 以太币的价格购买这个 token。Alice 将 token 卖给 Bob,获得了 9 以太币,Bob 赚取了 1 以太币。 需要注意的是,实现快速取款的前提条件是没有拜占庭行为发生,即没有扣留区块攻击发生,因为 LP 需要验证取款方的交易历史。 批量取款(Mass Withdrawal)当子链中有拜占庭行为(例如,区块扣留攻击)发生时,将会影响以后生成防伪证明,因此网络中的每个用户都有责任快速退出子链。虽然批量取款(Mass Withdrawal)操作不是必要选择,但当大量用户执行取款时很可能会造成主链拥塞,也会消耗更多的 gas,因此批量取款是当子链受到攻击时更好的选择。 批量取款操作由于所采用的模型(UTXO 模型或者账户模型)不同会有较大的差别,而且目前关于批量取款的操作细节也正在研讨当中,因此这里只对批量取款做简单介绍,想要了解目前研究状态可以参考[4]。 当子链中有拜占庭行为发生时,用户之间可以共同协作执行批量取款。这时会有一个节点扮演取款处理人(Exit Processor)的角色,简称为 EP,负责当前某个批量操作(可以同时有多个批量取款操作发生,但同一个取款申请不能存在于多个批量取款),并且可以收取服务费作为报酬。EP 将构造一个位图(bitmap,即一串0/1)记录哪些资产要执行取款。之后 EP 将利用现有的区块数据检查每个取款是否合法,之后将构造一个批量退出初始化交易(Mass Exit Initiation Transaction,MEIT),并将其发送到主链上。在 MEIT 被主链确认之前,每个用户都可以对这个交易提出异议。当争议期结束,MEIT 被主链确认,批量取款成功。 总结本文主要对 Plasma 框架中一些关键操作进行了比较详细的介绍,但如果不依托于某个实际的 Plasma 项目,对一些细节还是很难理解。因此在后面的文章中将会介绍 Plasma MVP 以及 Plasma Cash。 相关资源 https://plasma.io/ https://ethresear.ch/t/plasma-is-plasma/2195 https://ethresear.ch/t/simple-fast-withdrawals/2128 https://ethresear.ch/t/basic-mass-exits-for-plasma-mvp/3316 本文的作者是盖盖,他的微信公众号: chainlab 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Plasma","slug":"ethereum/Plasma","permalink":"https://learnblockchain.cn/categories/ethereum/Plasma/"}],"tags":[{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"},{"name":"Plasma","slug":"Plasma","permalink":"https://learnblockchain.cn/tags/Plasma/"},{"name":"扩容","slug":"扩容","permalink":"https://learnblockchain.cn/tags/扩容/"}]},{"title":"深入理解Plasma(一)Plasma 框架","slug":"plasma-framework","date":"2018-10-20T07:54:17.000Z","updated":"2019-04-05T10:29:32.275Z","comments":true,"path":"2018/10/20/plasma-framework/","link":"","permalink":"https://learnblockchain.cn/2018/10/20/plasma-framework/","excerpt":"这一系列文章将围绕以太坊的二层扩容框架,介绍其基本运行原理,具体操作细节,安全性讨论以及未来研究方向等。本篇文章作为开篇,主要目的是理解 Plasma 框架。","text":"这一系列文章将围绕以太坊的二层扩容框架,介绍其基本运行原理,具体操作细节,安全性讨论以及未来研究方向等。本篇文章作为开篇,主要目的是理解 Plasma 框架。 Plasma 作为以太坊的二层扩容框架,自从 2017 年被 Joseph Poon(Lightning Network 创始人)和 Vitalik Buterin (Ethereum 创始人)提出以来[1],一直是区块链从业人员关注的焦点[2]。首先需要明确的是,Plasma 实质上是一套框架,而不是一个单独的项目,它为各种不同的项目实际项目提供链下(off-chain)解决方案。这也是为什么很多人对 Plasma 感到疑惑的一个重要原因,因为在缺乏实际应用场景的情况下很难将 Plasma 解释清楚。因此,理解 Plasma 是一套框架是理解 Plasma 的关键。 从区块链扩容谈起在介绍 Plasma 之前,不得不先介绍区块链扩容。我们都知道,比特币(Bitcoin)和以太坊(Ethereum)作为目前最广泛使用的区块链平台,面临的最大问题就是可扩展性(Scalability)。这里需要注意的是,区块链中的可扩展性问题并不是单独特指某个问题,而是区块链想要实现 Web3.0[3] 的愿景,为亿万用户提供去中心化服务所要克服的一系列挑战。虽然以太坊号称是“世界计算机”,但这台“计算机”却是单线程的,每秒钟只能处理大约 15 条交易,与目前主流的 Visa 和 MasterCard 动辄每秒上万的吞吐量相比实在相形见绌。因此如何在保证区块链安全性的情况下,提高可扩展性是目前区块链发展亟待解决的问题之一。 目前关于区块链扩容的解决方案无外乎两个方向:一层(Layer 1)扩容和二层(Layer 2)扩容[4]。一层扩容也被称为链上(on-chain)扩容,顾名思义,这类扩容方案需要更改区块链底层协议。但同时也意味着需要将区块链硬分叉。这类扩容方案就像将原来的单核 CPU 改装成多核 CPU,从而可以多线程处理计算任务,提高整个网络的吞吐量。 目前最典型的一层扩容方案是 Vitalik 和他的研究团队提出的“Sharding(分片)”,也就是说将区块链划分成不同的部分(shards),每个部分独立处理交易。想要了解更多关于 Sharding 的信息,可以参考以太坊官方的 Wiki[5]。 二层扩容也称链下(off-chain)扩容,同样非常好理解,这种扩容方案不需要修改区块链底层协议,而是通过将大量、频繁的计算工作转移到“链下”完成,并定期或在需要时将链下的计算结果提交到“链上”保证其最终性(finality)。二层扩容的核心思想是将底层区块链作为共识基础,使用智能合约或者其它手段作为链下和链上沟通的桥梁,当有欺诈行为发生时链下的用户仍然可以回到链上的某一状态。虽然将计算转移到链下会在一段时间内损失最终性,但这个代价是值得的,因为这样做不止可以极大提高区块链的灵活性和可扩展性,也极大降低了用户进行交易所需要的代价。将计算转移到链下也并不意味着完全放弃安全性,因为最终的安全性还是由底层所依赖的区块链来保证,因此二层扩容主要关注的问题就在于如何保证链上链下切换过程的安全性。这种思想最早被用在闪电网络(Lightning Network)当中作为比特币的其中一个扩容方案,并取得了很好的效果。 本文所要介绍的 Plasma 就属于基于以太坊二层扩容方案,类似的解决方案还有 State Channels 和 Trubit。这些方案虽然面向的问题有所区别,但基本思想都是将复杂的计算转移到链下进行。那么,接下来我们将进入 Plasma 的世界,一窥究竟! 理解 Plasma在前文中我们已经明白 Plasma 是一种二层扩容框架,那么该如何进一步理解 Plasma 是什么?它区别于其它二层扩容方案的地方在哪呢? Plasma 也被称为“链中链(blockchains in blockchains)”。任何人都可以在底层区块链之上创建不同的 Plasma 支持不同的业务需求,例如分布式交易所、社交网络、游戏等。 这里可以举一个例子来理解 Plasma。假如企鹅公司创建了一个 Plasma Chain 叫作 Game Chain。用户通过向 Game Chain 发送一些以太币换取 Token,用于购买皮肤等游戏内的增值商品。加入 Game Chain 的操作是在链上进行的,以太坊区块链将这部分资产锁定,转移到 Game Chain 上。之后每次我们购买虚拟商品的交易就会转移到链下进行,由企鹅公司记账。这种方式几乎跟我们现实生活中游戏内购的体验一样,不仅结算迅速,而且手续费低廉(相比于以太坊之上需要给矿工支付的手续费)。那么问题来了,如果企鹅公司从中作祟,修改账本,恶意占有我们的资产怎么办?这时我们就可以提交之前每次交易的凭证到以太坊区块链上,如果确实是企鹅恶意篡改了账本,那么我们不但能够成功取回自己的资产,还能获得之前企鹅公司创建 Game Chain 存入的部分或全部押金。 通过上面这个例子是不是已经明白 Plasma 大致是如何工作的了?但上面这个例子还是过于简单,只涉及用户自己和企鹅公司。下面我们使用区块链的语言对上面的例子进行解析。 首先,企鹅公司在以太坊主链之上创建了一系列智能合约作为主链和子链 Game Chain 通信的媒介。这些智能合约中不仅规定了子链中的一些基本的状态转换规则(例如如何惩罚作恶的节点),也记录了子链中的状态(子链中区块的哈希值)。之后企鹅公司可以搭建自己的子链(可以用以太坊搭建一套私链)。子链实际上是一个完全独立的区块链,可以拥有专门的矿工,使用不同于主链的共识算法,例如 PoS(Proof of Stake)等。 当子链创建完毕后,企鹅公司可以使用 ERC721 合约创建 token 作为游戏内的商品(就像 Cryptokitty)。但这里需要注意的是,所有数字资产必须在以太坊主链上创建,并通过 Plasma 子链的智能合约转移到子链中。用户也需要在主链上购买数字资产后转移到子链上。在上面这个例子中,Game Chain 的智能合约将主链上的资产锁定,之后在子链上生成等值的资产。之后用户就可以完全脱离主链,在子链上进行交易。企鹅公司在子链上扮演 operator 的角色,如果一切运行正常,子链中的矿工会正常打包区块,并在需要时由 operator 将区块的哈希值提交到主链作为子链的状态更新证明。在这个过程中,用户完全不需要和主链交互。 我们可以看到,将复杂的计算操作转移到链下确实使得整个交易过程变得简单。但没有强大的共识算法和庞大的参与者,资产在子链上是很不安全的。Plasma 给了我们一种避险机制,即使 operator 作恶,我们也能取回属于自己的资产。下图(来源自[1])简单说明了这个过程。图中,在第 4 个区块中的交易被篡改。由于 Alice 本地保存有 Plasma Chain 中所有的区块数据,因此她可以向主链提交一个含有“防伪证明(Fraud Proof)”的交易。如果证明生效,那么主链将状态从 4 号区块回滚到 3 号区块,一切恢复正常。Plasmas Chain 中的参与者也可以随时提交资产证明,返回到主链。 到这里我们应该已经理解了,Plasma 所要做的工作并不是保护子链的安全,而是当有安全事故发生时,保证用户可以安全地取回自己的资产,并返回到主链上。并且采用一系列经济激励的方式减少作恶情况的发生。 下一篇文章将对 Plasma 运行过程的细节进行剖析。 相关资源 https://plasma.io/ https://ethresear.ch/c/plasma https://medium.com/l4-media/making-sense-of-web-3-c1a9e74dcae https://blog.ethereum.org/2018/01/02/ethereum-scalability-research-development-subsidy-programs/ https://github.com/ethereum/wiki/wiki/Sharding-FAQs https://medium.com/l4-media/making-sense-of-ethereums-layer-2-scaling-solutions-state-channels-plasma-and-truebit-22cb40dcc2f4 https://medium.com/@argongroup/ethereum-plasma-explained-608720d3c60e 本文的作者是盖盖,他的微信公众号: chainlab 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Plasma","slug":"ethereum/Plasma","permalink":"https://learnblockchain.cn/categories/ethereum/Plasma/"}],"tags":[{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"},{"name":"Plasma","slug":"Plasma","permalink":"https://learnblockchain.cn/tags/Plasma/"},{"name":"扩容","slug":"扩容","permalink":"https://learnblockchain.cn/tags/扩容/"}]},{"title":"理解开发HD 钱包涉及的 BIP32、BIP44、BIP39","slug":"hdwallet","date":"2018-09-28T08:17:41.000Z","updated":"2019-04-05T10:29:32.227Z","comments":true,"path":"2018/09/28/hdwallet/","link":"","permalink":"https://learnblockchain.cn/2018/09/28/hdwallet/","excerpt":"如果你还在被HD钱包(分层确定性钱包)、BIP32、BIP44、BIP39搞的一头雾水,来看看这边文章吧。","text":"如果你还在被HD钱包(分层确定性钱包)、BIP32、BIP44、BIP39搞的一头雾水,来看看这边文章吧。 数字钱包概念钱包用来存钱的,在区块链中,我们的数字资产都会对应到一个账户地址上, 只有拥有账户的钥匙(私钥)才可以对资产进行消费(用私钥对消费交易签名)。私钥和地址的关系如下:(图来自精通比特币)一句话概括下就是:私钥通过椭圆曲线生成公钥, 公钥通过哈希函数生成地址,这两个过程都是单向的。 因此实际上,数字钱包实际是一个管理私钥(生成、存储、签名)的工具,注意钱包并不保存资产,资产是在链上的。 如何创建账号创建账号关键是生成一个私钥, 私钥是一个32个字节的数, 生成一个私钥在本质上在1到2^256之间选一个数字。因此生成密钥的第一步也是最重要的一步,是要找到足够安全的熵源,即随机性来源,只要选取的结果是不可预测或不可重复的,那么选取数字的具体方法并不重要。 比如可以掷硬币256次,用纸和笔记录正反面并转换为0和1,随机得到的256位二进制数字可作为钱包的私钥。 从编程的角度来看,一般是通过在一个密码学安全的随机源(不建议大家自己去写一个随机数)中取出一长串随机字节,对其使用SHA256哈希算法进行运算,这样就可以方便地产生一个256位的数字。 实际过程需要比较下是否小于n-1(n = 1.158 * 10^77, 略小于2^256),我们就有了一个合适的私钥。否则,我们就用另一个随机数再重复一次。这样得到的私钥就可以根据上面的方法进一步生成公钥及地址。 BIP32钱包也是一个私钥的容器,按照上面的方法,我们可以生成一堆私钥(一个人也有很多账号的需求,可以更好保护隐私),而每个私钥都需要备份就特别麻烦的。 最早期的比特币钱包就是就是这样,还有一个昵称:“Just a Bunch Of Keys(一堆私钥)“ 为了解决这种麻烦,就有了BIP32 提议: 根据一个随机数种子通过分层确定性推导的方式得到n个私钥,这样保存的时候,只需要保存一个种子就可以,私钥可以推导出来,如图: (图来自精通比特币)上图中的孙秘钥就可以用来签发交易。 补充说明下 BIP: Bitcoin Improvement Proposals 比特币改进建议, bip32是第32个改进建议。BIP32提案的名字是:Hierarchical Deterministic Wallets, 就是我们所说的HD钱包。 来分析下这个分层推导的过程,第一步推导主秘钥的过程: 根种子输入到HMAC-SHA512算法中就可以得到一个可用来创造主私钥(m) 和 一个主链编码( a master chain code)这一步生成的秘钥(由私钥或公钥)及主链编码再加上一个索引号,将作为HMAC-SHA512算法的输入继续衍生出下一层的私钥及链编码,如下图: 衍生推导的方案其实有两个:一个用父私钥推导(称为强化衍生方程),一个用父公钥推导。同时为了区分这两种不同的衍生,在索引号也进行了区分,索引号小于2^31用于常规衍生,而2^31到2^32-1之间用于强化衍生,为了方便表示索引号i’,表示2^31+i。 因此增加索引(水平扩展)及 通过子秘钥向下一层(深度扩展)可以无限生成私钥。 注意, 这个推导过程是确定(相同的输入,总是有相同的输出)也是单向的,子密钥不能推导出同层级的兄弟密钥,也不能推出父密钥。如果没有子链码也不能推导出孙密钥。现在我们已经对分层推导有了认识。 一句话概括下BIP32就是:为了避免管理一堆私钥的麻烦提出的分层推导方案。 秘钥路径及BIP44通过这种分层(树状结构)推导出来的秘钥,通常用路径来表示,每个级别之间用斜杠 / 来表示,由主私钥衍生出的私钥起始以“m”打头。因此,第一个母密钥生成的子私钥是m/0。第一个公共钥匙是M/0。第一个子密钥的子密钥就是m/0/1,以此类推。 BIP44则是为这个路径约定了一个规范的含义(也扩展了对多币种的支持),BIP0044指定了包含5个预定义树状层级的结构:m / purpose' / coin' / account' / change / address_indexm是固定的, Purpose也是固定的,值为44(或者 0x8000002C)Coin type这个代表的是币种,0代表比特币,1代表比特币测试链,60代表以太坊完整的币种列表地址:https://github.com/satoshilabs/slips/blob/master/slip-0044.mdAccount代表这个币的账户索引,从0开始Change常量0用于外部链,常量1用于内部链(也称为更改地址)。外部链用于在钱包外可见的地址(例如,用于接收付款)。内部链用于在钱包外部不可见的地址,用于返回交易变更。 (所以一般使用0)address_index这就是地址索引,从0开始,代表生成第几个地址,官方建议,每个account下的address_index不要超过20 根据 EIP85提议的讨论以太坊钱包也遵循BIP44标准,确定路径是m/44'/60'/a'/0/na 表示帐号,n 是第 n 生成的地址,60 是在 SLIP44 提案中确定的以太坊的编码。所以我们要开发以太坊钱包同样需要对比特币的钱包提案BIP32、BIP39有所了解。 一句话概括下BIP44就是:给BIP32的分层路径定义规范 BIP39BIP32 提案可以让我们保存一个随机数种子(通常16进制数表示),而不是一堆秘钥,确实方便一些,不过用户使用起来(比如冷备份)也比较繁琐,这就出现了BIP39,它是使用助记词的方式,生成种子的,这样用户只需要记住12(或24)个单词,单词序列通过 PBKDF2 与 HMAC-SHA512 函数创建出随机种子作为 BIP32 的种子。 可以简单的做一个对比,下面那一种备份起来更友好:1234// 随机数种子090ABCB3A6e1400e9345bC60c78a8BE7 // 助记词种子candy maple cake sugar pudding cream honey rich smooth crumble sweet treat 使用助记词作为种子其实包含2个部分:助记词生成及助记词推导出随机种子,下面分析下这个过程。 生成助记词助记词生成的过程是这样的:先生成一个128位随机数,再加上对随机数做的校验4位,得到132位的一个数,然后按每11位做切分,这样就有了12个二进制数,然后用每个数去查BIP39定义的单词表,这样就得到12个助记词,这个过程图示如下: (图来源于网络) 下面是使用bip39生成生成助记词的一段代码: 1234var bip39 = require('bip39')// 生成助记词var mnemonic = bip39.generateMnemonic()console.log(mnemonic) 助记词推导出种子这个过程使用密钥拉伸(Key stretching)函数,被用来增强弱密钥的安全性,PBKDF2是常用的密钥拉伸算法中的一种。PBKDF2基本原理是通过一个为随机函数(例如 HMAC 函数),把助记词明文和盐值作为输入参数,然后重复进行运算最终产生生成一个更长的(512 位)密钥种子。这个种子再构建一个确定性钱包并派生出它的密钥。 密钥拉伸函数需要两个参数:助记词和盐。盐可以提高暴力破解的难度。 盐由常量字符串 “mnemonic” 及一个可选的密码组成,注意使用不同密码,则拉伸函数在使用同一个助记词的情况下会产生一个不同的种子,这个过程图示图下: (图来源于网络) 同样代码来表示一下: 123456789101112var hdkey = require('ethereumjs-wallet/hdkey')var util = require('ethereumjs-util')var seed = bip39.mnemonicToSeed(mnemonic, \"pwd\");var hdWallet = hdkey.fromMasterSeed(seed);var key1 = hdWallet.derivePath(\"m/44'/60'/0'/0/0\");console.log(\"私钥:\"+util.bufferToHex(key1._hdkey._privateKey));var address1 = util.pubToAddress(key1._hdkey._publicKey, true);console.log(\"地址:\"+util.bufferToHex(address1));console.log(\"校验和地址:\"+ util.toChecksumAddress(address1.toString('hex'))); 校验和地址是EIP-55中定义的对大小写有要求的一种地址形式。 密码可以作为一个额外的安全因子来保护种子,即使助记词的备份被窃取,也可以保证钱包的安全(也要求密码拥有足够的复杂度和长度),不过另外一方面,如果我们忘记密码,那么将无法恢复我们的数字资产。 一句话概括下BIP39就是:通过定义助记词让种子的备份更友好 我为大家录制了一个视频:以太坊去中心化网页钱包开发,从如何创建账号开始,深入探索BIP32、BIP44、BIP39等提案,以及如何存储私钥、发送离线签名交易和Token。 小结HD钱包(Hierarchical Deterministic Wallets)是在BIP32中提出的为了避免管理一堆私钥的麻烦提出的分层推导方案。而BIP44是给BIP32的分层增强了路径定义规范,同时增加了对多币种的支持。BIP39则通过定义助记词让种子的备份更友好。 目前我们的市面上单到的以太币、比特币钱包基本都遵循这些标准。 最后推荐一个助记词秘钥生成器网站 欢迎来知识星球提问,星球内已经聚集了300多位区块链技术爱好者。深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"比特币","slug":"比特币","permalink":"https://learnblockchain.cn/tags/比特币/"},{"name":"钱包","slug":"钱包","permalink":"https://learnblockchain.cn/tags/钱包/"},{"name":"BIP32","slug":"BIP32","permalink":"https://learnblockchain.cn/tags/BIP32/"},{"name":"HD钱包","slug":"HD钱包","permalink":"https://learnblockchain.cn/tags/HD钱包/"},{"name":"BIP44","slug":"BIP44","permalink":"https://learnblockchain.cn/tags/BIP44/"},{"name":"BIP39","slug":"BIP39","permalink":"https://learnblockchain.cn/tags/BIP39/"}]},{"title":"如何使用Web3.js API 在页面中进行转账","slug":"web3-sendeth","date":"2018-09-12T09:22:34.000Z","updated":"2019-04-05T10:29:32.245Z","comments":true,"path":"2018/09/12/web3-sendeth/","link":"","permalink":"https://learnblockchain.cn/2018/09/12/web3-sendeth/","excerpt":"本文介绍如何使用Web3.js API 在页面中进行转账,是我翻译的文档Web3.js 0.2x 中文版 及 区块链全栈-以太坊DAPP开发实战 中Demo的文章说明。","text":"本文介绍如何使用Web3.js API 在页面中进行转账,是我翻译的文档Web3.js 0.2x 中文版 及 区块链全栈-以太坊DAPP开发实战 中Demo的文章说明。 写在前面阅读本文前,你应该对以太坊、智能合约、钱包的概念有所了解,如果你还不了解,建议你先看以太坊是什么除此之外,你最好还了解一些HTML及JavaScript知识。 转账UI 页面的编写转账UI主体的界面如图: 实现这个界面很简单,这里就不代码了。大家可以打开Demo,右击查看页面源码。 用户环境检查既然需要使用Web3.js API 在页面中进行转账, 首先应该检查在浏览器环境有没有安装好钱包,并且钱包应该是解锁状态。 先检查是否安装了MetaMask钱包: 123456789101112window.addEventListener('load', function() { if (typeof web3 !== 'undefined') { web3 = new Web3(web3.currentProvider); if (web3.currentProvider.isMetaMask == true) { // \"MetaMask可用\" } else { // \"非MetaMask环境\" } } else { $(\"#env\").html(\"No web3? 需要安装<a href='https://metamask.io/'>MetaMask</a>!\"); }} MetaMask推荐在window加载时,进行MetaMask的检查,当然在没有安装MetaMask时,也可以指定一个节点Provider来创建web3,可以参考Web3.js 文档引入web3 检查是否钱包已经解锁:我们在发送交易之前应该先首先检查一下当前钱包的一个状态,检查钱包是否解锁(是否输入了密码进入了MetaMask),通常使用eth下面的getAccounts来进行检查,getAccounts是会返回账号的一个列表,如果当前账号列表里面有数据的话,说明钱包已经解锁可以获得到账号,如果账号拿到的列表是空的话,那么说明钱包没有解锁。 可以把下面的代码加到上面的监听函数中: 12345web3.eth.getAccounts(function (err, accounts) { if (accounts.length == 0) { $(\"#account\").html(\"请检查钱包是否解锁\"); } }); 发送交易如果MetaMask钱包是解锁的,我们就可以来发送交易,发送交易使用sendtransaction这个方法。 1web3.eth.sendTransaction(transactionObject [, callback]) 第二个参数是回调函数用来获得发送交易的Hash值。 第一个参数是一个交易对象,交易对象里面有几个字段: from : 就是从哪个账号发送金额 to : 发动到到哪个账号 value 是发送的金额 gas: 设置gas limit gasPrice: 设置gas 价格 如果from没有的话,他就会用当前的默认账号, 如果是转账to和value是必选的两个字段。在发送交易的时候弹出来MetaMask的一个授权的窗口,如果我们gas和gasPrice没有设置的话,就可以在MetaMask里面去设置。如果这两个gas和gas Price设置了的话,MetaMask就会使用我们设置的gas。 因此在发送交易的时候,关键是构造这样一个交易对象,JavaScrpt代码如下: 1234567891011121314151617181920// 这里使用Metamask 给的gas Limit 及 gas 价var fromAccount = $('#fromAccount').val();var toAccount = $('#toAccount').val();var amount = $('#amount').val();// 对输入的数字做一个检查if (web3.isAddress(fromAccount) && web3.isAddress(toAccount) && amount != null && amount.length > 0) { var message = {from: fromAccount, to:toAccount, value: web3.toWei(amount, 'ether')}; web3.eth.sendTransaction(message, (err, res) => { var output = \"\"; if (!err) { output += res; } else { output = \"Error\"; } }} 补充说明:$('#fromAccount').val()是使用JQuery用来获取用户输入内容,其次应该在实际构造发送交易之前对输入的参数做一个判断,web3.isAddress用来检查字符串是不是地址。另外对于一个向普通外部地址账号的转账,消耗的gas 是固定的21000。 运行测试需要注意一点的是,由于安全原因,MetaMask只支持站点方式访问的页面,即通过http:// 来访问页面,在浏览器中通过file:// + 文件地址的方式是不行的。因此需要把编写的代码放置到web服务器的目录下,自己试验下。 线上的Demo地址为https://web3.learnblockchain.cn/transDemo.html 想好好系统学习以太坊DApp开发,这门视频课程以太坊DAPP开发实战不容错过。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。深入浅出区块链知识星球,最专业的区块链问题技术社区,欢迎加入,作为星友福利,星友还可以加入我创建优质区块链技术群,群内聚集了300多位区块链技术大牛和爱好者。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Dapp","slug":"ethereum/Dapp","permalink":"https://learnblockchain.cn/categories/ethereum/Dapp/"}],"tags":[{"name":"Web3.js","slug":"Web3-js","permalink":"https://learnblockchain.cn/tags/Web3-js/"},{"name":"Dapp入门","slug":"Dapp入门","permalink":"https://learnblockchain.cn/tags/Dapp入门/"}]},{"title":"程序员如何切入区块链去中心化应用开发","slug":"devDapp","date":"2018-08-31T03:30:55.000Z","updated":"2019-04-05T10:29:32.291Z","comments":true,"path":"2018/08/31/devDapp/","link":"","permalink":"https://learnblockchain.cn/2018/08/31/devDapp/","excerpt":"前段时间一个以太坊游戏应用:Fomo3D异常火爆,在短短的几天内就吸引了几万的以太币投入游戏,第一轮游戏一个“黑客”用了一个非常巧妙的利用以太坊规则成为了最终赢家,拿走了1万多以太币奖金。 区块链应用的价值由这个游戏反映的淋漓尽致,Fomo3D游戏能够成功核心所依赖的是以太坊提供的一个可信、不可篡改平台。当游戏的规则确定之后,一切都按规则运行,无人可干预。今天这篇就来介绍一下程序员如何切入去中心化应用开发。","text":"前段时间一个以太坊游戏应用:Fomo3D异常火爆,在短短的几天内就吸引了几万的以太币投入游戏,第一轮游戏一个“黑客”用了一个非常巧妙的利用以太坊规则成为了最终赢家,拿走了1万多以太币奖金。 区块链应用的价值由这个游戏反映的淋漓尽致,Fomo3D游戏能够成功核心所依赖的是以太坊提供的一个可信、不可篡改平台。当游戏的规则确定之后,一切都按规则运行,无人可干预。今天这篇就来介绍一下程序员如何切入去中心化应用开发。 中心化应用作为对比,先来看看中心化应用,其实就是现有的互联网应用,为什么它是中心化应用,看看它的架构图: 平时我们接触的是应用的前端(或称客户端),前端可以是HTML5的web页面、 小程序、APP, 在前端展现的内容通常发送一个请求到服务器,服务器返回相应的内容给前端。在前端的动作同样也会转化请求发送到服务器,服务器处理之后返回数据到前端。也就是说我们所有看到的内容或者操作都是中心化的服务器控制,因此说是中心化应用。 去中心化应用DAPP而去中心化应用有什么不同呢? 看看它的架构图:前端的表现上是一样的, 还是H5页面、 小程序、APP,DAPP和传统App关键是后端部分不同,是后端不再是一个中心化的服务器,而是分布式网络上任意节点,注意可以是 任意一个节点,在应用中给节点发送的请求通常称为 交易,交易和中心化下的请求有几个很大的不同是:交易的数据经过用户个人签名之后发送到节点,节点收到交易请求之后,会把 请求广播到整个网络,交易在网络达成共识之后,才算是真正的执行(真正其作用的执行不一是连接的后端节点,尽管后端也会执行)。以及中心化下的请求大多数都是同步的(及时拿到结果), 而交易大多数是异步的,这也是在开发去中心应用时需要注意的地方, 从节点上获得数据状态(比如交易的结果),一般是通过事件回调来获得。 如何开发在开发中心化应用最重要两部分是 客户端UI表现和 后端服务程序, UI表现通过HTTP请求连接到后端服务程序,后端服务程序运行在服务器上,比如Nginx Apached等等。 开发一个去中心化应用最重要也是两部分: 客户端UI表现及 智能合约,智能合约的作用就像后端服务程序,智能合约是运行在节点的EVM上, 客户端调用智能合约,是通过向节点发起RPC请求完成。 下面是一个对比: 客户端UI <=> 客户端UI HTTP <=> RPC 后端服务程序 <=> 智能合约 Nginx/Apache <=> 节点 因此对于去中心化应用来说,程序员可以从两个方面切入: 一个是 去中心化应用的客户端开发, 熟悉已经熟悉客户端软件(如Web\\APP等)开发的同学,只需要了解一下客户端跟区块链节点通信的API接口,如果是在当前应用最广泛的区块链平台以太坊上开发去中心化应用,那么需要了解Web3这个库,Web3对节点暴露出来的JSON-RPC接口进行了封装,比如Web3提供的功能有:获取节点状态,获取账号信息,调用合约、监听合约事件等等。 目前的主流语言都有Web3的实现,列举一些实现给大家参考: JavaScript Web3.js Python Web3.py Haskell hs-web3 Java web3j Scala web3j-scala Purescript purescript-web3 PHP web3.php PHP ethereum-php 另一个切入点是 智能合约的开发,在以太坊现在推荐的语言是Solidity,有一些同学对新学一门语言有一些畏惧,Solidity的语法其实很简洁,有过一两门其他语言基础(开发经验)的同学三五天就可以学会,我也录制了一个视频课程:深入详解以太坊智能合约语言Solidity。 下面用一个Hello合约,体会下Solidity的语法: 12345contract Hello { function hello() public returns(string) { return \"Hello World\"; }} 如果把上面的contract关键字更改为class,就和其他语言定义一个类一样。 有兴趣的同学可以进一步学习一下这个DApp开发案例Web3与智能合约交互实战, 在DAPP的开发过程中,一些开发工具可以帮助我们事半功倍,如:Truffle开发框架以及Ganache工具来模拟节点等,这篇文章一步步教你开发、部署第一个去中心化应用 补充对于想切入到去中心化应用开发的同学,对区块链运行的原理了解肯定会是加分项,尤其是各类共识机制(POW,POS,DPOS等)的理解,P2P网络的理解,以及各类加密和Hash算法的运用。有一些同学想做区块链底层开发,对区块链运行的原理则是必须项。 欢迎来知识星球提问,星球内已经聚集了300多位区块链技术爱好者。深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Dapp","slug":"ethereum/Dapp","permalink":"https://learnblockchain.cn/categories/ethereum/Dapp/"}],"tags":[{"name":"以太坊概念","slug":"以太坊概念","permalink":"https://learnblockchain.cn/tags/以太坊概念/"},{"name":"Dapp入门","slug":"Dapp入门","permalink":"https://learnblockchain.cn/tags/Dapp入门/"}]},{"title":"如何理解以太坊ABI - 应用程序二进制接口","slug":"understand-abi","date":"2018-08-09T09:08:39.000Z","updated":"2019-04-05T10:29:32.247Z","comments":true,"path":"2018/08/09/understand-abi/","link":"","permalink":"https://learnblockchain.cn/2018/08/09/understand-abi/","excerpt":"很多同学不是很明白以太坊ABI是什么,他的作用是什么,读完本文就明白了。","text":"很多同学不是很明白以太坊ABI是什么,他的作用是什么,读完本文就明白了。 写在前面阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么,也可以观看我们的视频:零基础搞懂区块链和深入详解以太坊智能合约语言Solidity, 可以系统全面学习理解以太坊、智能合约。 ABI 是什么ABI 全称是 Application Binary Interface,翻译过来就是:应用程序二进制接口,简单来说就是 以太坊的调用合约时的接口说明。还不是很理解,没关系。 调用合约函数发生了什么从外部施加给以太坊的行为都称之为向以太坊网络提交了一个交易, 调用合约函数其实是向合约地址(账户)提交了一个交易,这个交易有一个附加数据,这个附加的数据就是ABI的编码数据。 比特币的交易也可以附加数据,以太坊革命性的地方就是能把附加数据转化为都函数的执行。 因此要想和合约交互,就离不开ABI数据。 演示调用函数以下面以个最简单的合约为例,我们看看用参数 1 调用set(uint x),这个交易附带的数据是什么。 1234567891011121314pragma solidity ^0.4.0;contract SimpleStorage { uint storedData; function set(uint x) public { storedData = x; } function get() public constant returns (uint) { return storedData; }} 当然第一步需要先把合约部署到以太坊网络(其实部署也是一个)上,然后用 “1” 作为参数调用set,如下图: 然后我们打开etherscan查看交易详情数据, 可以看到其附加数据如下图: 这个数据就是ABI的编码数据:10x60fe47b10000000000000000000000000000000000000000000000000000000000000001 ABI 编码分析我把上面交易的附加数据拷贝出来分析一下,这个数据可以分成两个子部分: 函数选择器(4字节)0x60fe47b1 第一个参数(32字节)00000000000000000000000000000000000000000000000000000000000000001 函数选择器值 实际是对函数签名字符串进行sha3(keccak256)哈希运算之后,取前4个字节,用代码表示就是: 1bytes4(sha3(“set(uint256)”)) == 0x60fe47b1 参数部分则是使用对应的16进制数。 现在就好理解 附加数据怎么转化为对应的函数调用。 ABI 编码函数那么怎么获得函数对应的ABI 数据呢, 有两种方法: Solidity ABI 编码函数一个是 solidity 提供了ABI的相关API, 用来直接得到ABI编码信息,这些函数有: abi.encode(…) returns (bytes):计算参数的ABI编码。 abi.encodePacked(…) returns (bytes):计算参数的紧密打包编码 abi. encodeWithSelector(bytes4 selector, …) returns (bytes): 计算函数选择器和参数的ABI编码 abi.encodeWithSignature(string signature, …) returns (bytes): 等价于* abi.encodeWithSelector(bytes4(keccak256(signature), …) 通过ABI编码函数可以在不用调用函数的情况下,获得ABI编码值,下面通过一段代码来看看这些方法的使用: 1234567891011121314pragma solidity ^0.4.24;contract testABI { uint storedData; function set(uint x) public { storedData = x; } function abiEncode() public constant returns (bytes) { abi.encode(1); // 计算1的ABI编码 return abi.encodeWithSignature(\"set(uint256)\", 1); //计算函数set(uint256) 及参数1 的ABI 编码 }} 大家可以运行运行下abiEncode函数,它的输出其实就是前面调用的附加数据。 Web3 ABI 编码函数另一个web3提供相应的API,例如使用web3计算函数选择器的方式如下: 1web3.eth.abi.encodeFunctionSignature('myMethod(uint256,string)'); 其完整的文档在这里,这里不一一演示。 如果你想学习以太坊DApp开发,这门视频课程以太坊DAPP开发实战是你不错的选择。 欢迎来知识星球提问,星球内已经聚集了300多位区块链技术爱好者。深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"ABI","slug":"ABI","permalink":"https://learnblockchain.cn/tags/ABI/"}]},{"title":"智能合约语言 Solidity 教程系列13 - 函数调用","slug":"solidity-callfun","date":"2018-08-09T03:17:17.000Z","updated":"2019-04-05T10:29:31.076Z","comments":true,"path":"2018/08/09/solidity-callfun/","link":"","permalink":"https://learnblockchain.cn/2018/08/09/solidity-callfun/","excerpt":"这是Solidity教程系列文章第13篇介绍函数调用, 本文会介绍函数使用元组返回多个值,通过命名方式进行参数调用以及如何省略函数参数名称。 Solidity 系列完整的文章列表请查看分类-Solidity。","text":"这是Solidity教程系列文章第13篇介绍函数调用, 本文会介绍函数使用元组返回多个值,通过命名方式进行参数调用以及如何省略函数参数名称。 Solidity 系列完整的文章列表请查看分类-Solidity。 写在前面Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 欢迎订阅区块链技术专栏阅读更全面的分析文章。 函数调用及参数在函数类型一节中,我们介绍过Solidity 中有两种函数调用方式:内部函数调用和外部函数调用,这一节我们进一步介绍。 内部函数调用(Internal Function Calls)内部调用,不会创建一个EVM消息调用。而是直接调用当前合约的函数,也可以递归调用。如下面这个的例子: 1234567891011pragma solidity ^0.4.16;contract C { function g(uint a) public pure returns (uint ret) { return f(); // 直接调用 } function f() internal pure returns (uint ret) { return g(7) + f(); // 直接调用及递归调用 }} 这些函数调用被转换为EVM内部的简单指令跳转(jumps)。 这样带来的一个好处是,当前的内存不会被回收。在一个内部调用时传递一个内存型引用效率将非常高的。当然,仅仅是同一个合约的函数之间才可通过内部的方式进行调用。 外部函数调用(External Function Calls)外部调用,会创建EVM消息调用。表达式this.g(8);和c.g(2)(这里的c是一个合约实例)是外部调用函数的方式,它会发起一个消息调用,而不是EVM的指令跳转。需要注意的是,在合约的构造器中,不能使用this调用函数,因为当前合约还没有创建完成。 其它合约的函数必须通过外部的方式调用。对于一个外部调用,所有函数的参数必须要拷贝到内存中。 当调用其它合约的函数时,可以通过选项.value(),和.gas()来分别指定要发送的以太币(以wei为单位)和gas值,如: 1234567891011121314151617pragma solidity ^0.4.0;contract InfoFeed { function info() public payable returns (uint ret) { return 42; }}contract Consumer { InfoFeed feed; function setFeed(address addr) public { feed = InfoFeed(addr); } function callFeed() public { feed.info.value(10).gas(800)(); // 附加以太币及gas来调用info }} info()函数,必须使用payable关键字,否则不能通过value()来接收以太币。 表达式InfoFeed(addr)进行了一个显示的类型转换,表示给定的地址是合约InfoFeed类型,这里并不会执行构造器的初始化。在进行显式的类型强制转换时需要非常小心,不要调用一个我们不知道类型的合约函数。 我们也可以使用function setFeed(InfoFeed _feed) { feed = _feed; }来直接进行赋值。注意feed.info.value(10).gas(800)仅仅是对发送的以太币和gas值进行了设置,真正的调用是后面的括号()。调用callFeed时,需要预先存入一定量的以太币,要不能会因余额不足报错。 如果我们不知道被调用的合约源代码,和它们交互会有潜在的风险,即便被调用的合约继承自一个已知的父合约(继承仅仅要求正确实现接口,而不关注实现的内容)。因为和他们交互,相当于把自己控制权交给被调用的合约,对方几乎可以利用它做任何事。此外, 被调用的合约可以改变调用合约的状态变量,在编写函数时需要注意可重入性漏洞问题(可查看安全建议)。 函数参数与其他语言一样,函数可以提供参数作为输入(函数类型本身也可以作为参数); 与Javascript和C不同的是,solidity还可以返回任意数量的参数作为输出。 输入参数输入参数的声明方式与变量相同, 未使用的参数可以省略变量名称。假设我们希望合约接受一种带有两个整数参数的外部调用,可以这样写: 1234567pragma solidity ^0.4.16;contract Simple { function taker(uint _a, uint _b) public pure { // 使用 _a _b }} 输出参数输出参数的声明和输入参数一样,只不过它接在returns 之后,假设我们希望返回两个结果:两个给定整数的和及积,可以这样写: 123456789101112pragma solidity ^0.4.16;contract Simple { function arithmetics(uint _a, uint _b) public pure returns (uint o_sum, uint o_product) { o_sum = _a + _b; o_product = _a * _b; }} 可以省略输出参数的名称,也可以使用return语句指定输出值,return可以返回多个值(见下文)。返回一个没有赋值的参数,则默认为0。 输入参数和输出参数可以在函数内表达式中使用,也可以作为被赋值的对象, 如: 1234567contract Simple { function taker(uint _a, uint _b) public pure returns (uint _c) { _a = 1; _b = 2; _c = 3; }} 返回多个值当一个函数有多个输出参数时, 可以使用元组(tuple)来返回多个值。元组(tuple)是一个数量固定,类型可以不同的元素组成的一个列表(用小括号表示),使用return (v0, v1, …, vn) 语句,就可以返回多个值,返回值的数量需要和输出参数声明的数量一致。 123456789101112function f() public pure returns (uint, bool, uint) { // 使用元组返回多个值 return (7, true, 2);}function callf() public { uint x; bool y; uint z; // 使用元组给多个变量赋值 (x, y , z) = f();} 补充关于元组的介绍上面的代码中,使用了元组返回多个值及使用元组给多个变量赋值,给多个变量赋值通常也称为解构(解构的概念在函数式语言中较为常见),再来看看元组的一些用法,比如元组可以交换变量值,如: 1(x, y) = (y, x); 元组支持省略一些元素, 如: 1(x, y, ) = (1, 2, 4); 开头的元素也可以省略,如: 1(, y, ) = (1, 2, 4); 注意 (1,) 是一个一个元素的元组, (1) 只是1。 使用命名参数调用函数调用的参数,可以通过指定名称的方式调用,使用花括号{} 包起来,参数顺序任意,但参数的类型和数量要与定义一致。如: 1234567891011pragma solidity ^0.4.0;contract C { function f(uint key, uint value) public { // ... } function g() public { f({value: 2, key: 3}); // 命名参数 }} 省略函数参数名称没有使用的参数名称可以省略(一般常见于返回值)。这些参数依然在栈(stack)上存在,但不可访问。 12345678pragma solidity ^0.4.16;contract C { // omitted name for parameter function func(uint k, uint) public pure returns(uint) { return k; }} 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。如果想与我有更密切的交流可以选择加入我的知识星球(星球成员可加入微信技术交流群)","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Solidity","slug":"ethereum/Solidity","permalink":"https://learnblockchain.cn/categories/ethereum/Solidity/"}],"tags":[{"name":"Solidity手册","slug":"Solidity手册","permalink":"https://learnblockchain.cn/tags/Solidity手册/"}]},{"title":"智能合约语言 Solidity 教程系列12 - 库的使用","slug":"solidity-library","date":"2018-08-09T02:40:56.000Z","updated":"2019-04-05T10:29:32.266Z","comments":true,"path":"2018/08/09/solidity-library/","link":"","permalink":"https://learnblockchain.cn/2018/08/09/solidity-library/","excerpt":"这是Solidity教程系列文章第12篇介绍库的使用:库与合约的不同,使用库的正姿势。 Solidity 系列完整的文章列表请查看分类-Solidity。","text":"这是Solidity教程系列文章第12篇介绍库的使用:库与合约的不同,使用库的正姿势。 Solidity 系列完整的文章列表请查看分类-Solidity。 写在前面Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 欢迎订阅区块链技术专栏阅读更全面的分析文章。 库库与合约类似,它也部署在一个指定的地址上(仅被部署一次,当代码在不同的合约可反复使用),然后通过EVM的特性DELEGATECALL (Homestead之前是用CALLCODE)来复用代码。库函数在被调用时,库代码是在发起合约(下文称主调合约:主动发起DELEGATECALL调用的合约)的上下文中执行的,使用this将会指向到主调合约,而且库代码可以访问主调合约的存储(storage)。 因为库合约是一个独立的代码,它仅可以访问主调合约明确提供的状态变量,否则,没办法法去知道这些状态变量。 对比普通合约来说,库存在以下的限制(这些限制将来也可能在将来的版本被解除): 无状态变量(state variables)。 不能继承或被继承 不能接收以太币 不能销毁一个库 不会修改状态变量(例如被声明view或pure)库函数只能通过直接调用(如不用DELEGATECALL),是因为其被认为是状态无关的。 库有许多使用场景。两个主要的场景如下: 如果有许多合约,它们有一些共同代码,则可以把共同代码部署成一个库。这将节省gas,因为gas也依赖于合约的规模。因此,可以把库想象成使用其合约的父合约。使用父合约(而非库)切分共同代码不会节省gas,因为在Solidity中,继承通过复制代码工作。 库可用于给数据类型添加成员函数。(参见下一节Using for) 由于库被当作隐式的父合约(不过它们不会显式的出现在继承关系中,但调用库函数和调用父合约的方式是非常类似的,如库L有函数f(),使用L.f()即可访问)。库里面的内部(internal)函数被复制给使用它的合约;同样按调用内部函数的调用方式,这意味着所有内部类型可以传进去,memory类型则通过引用传递,而不是拷贝的方式。 同样库里面的结构体structs和枚举enums也会被复制给使用它的合约。因此,如果一个库里只包含内部函数或结构体或枚举,则不需要部署库,因为库里面的所有内容都被复制给使用它的合约。 下面的例子展示了如何使用库。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546pragma solidity ^0.4.16;library Set { // 定义了一个结构体,保存主调函数的数据(本身并未实际存储的数据)。 struct Data { mapping(uint => bool) flags; } // self是一个存储类型的引用(传入的会是一个引用,而不是拷贝的值),这是库函数的特点。 // 参数名定为self 也是一个惯例,就像调用一个对象的方法一样. function insert(Data storage self, uint value) public returns (bool) { if (self.flags[value]) return false; // 已存在 self.flags[value] = true; return true; } function remove(Data storage self, uint value) public returns (bool) { if (!self.flags[value]) return false; self.flags[value] = false; return true; } function contains(Data storage self, uint value) public view returns (bool) { return self.flags[value]; }}contract C { Set.Data knownValues; function register(uint value) public { // 库函数不需要实例化就可以调用,因为实例就是当前的合约 require(Set.insert(knownValues, value)); } // 在这个合约中,如果需要的话可以直接访问knownValues.flags,} 当然,我们也可以不按上面的方式来使用库函数,可以不定义结构体,可以不使用storage类型引用的参数,还可以在任何位置有多个storage的引用类型的参数。 调用Set.contains,Set.remove,Set.insert都会编译为以DELEGATECALL的方式调用外部合约和库。如果使用库,需要注意的是一个真实的外部函数调用发生了。尽管msg.sender,msg.value,this还会保持它们在主调合约中的值(在Homestead之前,由于实际使用的是CALLCODE,msg.sender,msg.value会变化)。 下面的例子演示了在库中如何使用memory类型和内部函数(inernal function)来实现一个自定义类型,而不会用到外部函数调用(external function)。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152pragma solidity ^0.4.16;library BigInt { struct bigint { uint[] limbs; } function fromUint(uint x) internal pure returns (bigint r) { r.limbs = new uint[](1); r.limbs[0] = x; } function add(bigint _a, bigint _b) internal pure returns (bigint r) { r.limbs = new uint[](max(_a.limbs.length, _b.limbs.length)); uint carry = 0; for (uint i = 0; i < r.limbs.length; ++i) { uint a = limb(_a, i); uint b = limb(_b, i); r.limbs[i] = a + b + carry; if (a + b < a || (a + b == uint(-1) && carry > 0)) carry = 1; else carry = 0; } if (carry > 0) { // too bad, we have to add a limb uint[] memory newLimbs = new uint[](r.limbs.length + 1); for (i = 0; i < r.limbs.length; ++i) newLimbs[i] = r.limbs[i]; newLimbs[i] = carry; r.limbs = newLimbs; } } function limb(bigint _a, uint _limb) internal pure returns (uint) { return _limb < _a.limbs.length ? _a.limbs[_limb] : 0; } function max(uint a, uint b) private pure returns (uint) { return a > b ? a : b; }}contract C { using BigInt for BigInt.bigint; function f() public pure { var x = BigInt.fromUint(7); var y = BigInt.fromUint(uint(-1)); var z = x.add(y); }} 合约的源码中不能添加库地址,它是在编译时向编译器以参数形式提供的(这些地址须由链接器(linker)填进最终的字节码中,使用命令行编译器来进行联接 TODO)。如果地址没有以参数的方式正确给到编译器,编译后的字节码将会仍包含一个这样格式的占们符Set__(其中Set是库的名称)。可以通过手动将所有的40个符号替换为库的十六进制地址。 Using for 指令指令using A for B;用来把库函数(从库A)关联到类型B。这些函数将会把调用函数的实例作为第一个参数。语法类似,python中的self变量一样。例如:A库有函数 add(B b1, B b2),则使用Using A for B指令后,如果有B b1就可以使用b1.add(b2)。 using A for * 表示库A中的函数可以关联到任意的类型上。 在这两种情形中,所有函数,即使第一个参数的类型与调用函数的对象类型不匹配的,也会被关联上。类型检查是在函数被调用时执行,以及函数重载是也会执行检查。 using A for B; 指令仅在当前的作用域有效,且暂时仅仅支持当前的合约这个作用域,后续也非常有可能解除这个限制,允许作用到全局范围。如果能作用到全局范围,通过引入一些模块(module),数据类型将能通过库函数扩展功能,而不需要每个地方都得写一遍类似的代码了。 下面我们使用Using for 指令方式重写上一节Set的例子: 123456789101112131415161718192021222324252627282930313233343536373839404142434445 pragma solidity ^0.4.16;// 库合约代码和上一节一样library Set { struct Data { mapping(uint => bool) flags; } function insert(Data storage self, uint value) public returns (bool) { if (self.flags[value]) return false; // already there self.flags[value] = true; return true; } function remove(Data storage self, uint value) public returns (bool) { if (!self.flags[value]) return false; // not there self.flags[value] = false; return true; } function contains(Data storage self, uint value) public view returns (bool) { return self.flags[value]; }}contract C { using Set for Set.Data; // 这是一个关键的变化 Set.Data knownValues; function register(uint value) public { // 现在 Set.Data都对应的成员方法 // 效果和Set.insert(knownValues, value)相同 require(knownValues.insert(value)); }} 同样可以使用Using for的方式来对基本类型(elementary types)进行扩展: 12345678910111213141516171819202122232425262728293031pragma solidity ^0.4.16;library Search { function indexOf(uint[] storage self, uint value) public view returns (uint) { for (uint i = 0; i < self.length; i++) if (self[i] == value) return i; return uint(-1); }}contract C { using Search for uint[]; uint[] data; function append(uint value) public { data.push(value); } function replace(uint _old, uint _new) public { // 进行库调用 uint index = data.indexOf(_old); if (index == uint(-1)) data.push(_new); else data[index] = _new; }} 需要注意的是所有库调用都实际上是EVM函数调用。这意味着,如果传的是memory类型的,或者是值类型,那么进行一次拷贝,即使是self变量,解决方法是使用存储(storage)类型的引用来避免拷贝内容。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。如果想与我有更密切的交流可以选择加入我的知识星球(星球成员可加入微信技术交流群)","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Solidity","slug":"ethereum/Solidity","permalink":"https://learnblockchain.cn/categories/ethereum/Solidity/"}],"tags":[{"name":"Solidity手册","slug":"Solidity手册","permalink":"https://learnblockchain.cn/tags/Solidity手册/"}]},{"title":"Python实现一条基于POS算法的区块链","slug":"python-blockchain-with-pos","date":"2018-08-07T12:44:00.000Z","updated":"2019-04-05T10:29:32.281Z","comments":true,"path":"2018/08/07/python-blockchain-with-pos/","link":"","permalink":"https://learnblockchain.cn/2018/08/07/python-blockchain-with-pos/","excerpt":"区块链中的共识算法在比特币公链架构解析中,就曾提到过为了实现去中介化的设计,比特币设计了一套共识协议,并通过此协议来保证系统的稳定性和防攻击性。 \b并且我们知道,截止目前使用最广泛,也是最被大家接受的共识算法,\b是我们\b先前介绍过的POW(proof of work)工作量证明算法。目前市值排名前二的比特币和以太坊也是采用的此算法。","text":"区块链中的共识算法在比特币公链架构解析中,就曾提到过为了实现去中介化的设计,比特币设计了一套共识协议,并通过此协议来保证系统的稳定性和防攻击性。 \b并且我们知道,截止目前使用最广泛,也是最被大家接受的共识算法,\b是我们\b先前介绍过的POW(proof of work)工作量证明算法。目前市值排名前二的比特币和以太坊也是采用的此算法。 虽然POW共识算法取得了巨大的成功,但对它的质疑也从来未曾停止过。 其中最主要的一个原因就是电力消耗。据不完全统计,基于POW的挖矿机制所消耗的电量是非常巨大的,甚至比绝大多数国家耗电量还要多。这对我们的资源造成了极大的浪费,此外随着\b比特大陆等公司的\b强势崛起,造成了算力的高度集中。\b 基于以上种种原因,更多的共识算法被提出来 POS、DPOS、BPFT等等。 今天我们就来认识POS(proof of stake)算法。 \bProof of stake,译为权益证明。你可能已经猜到了,权益证明简单理解就是拥有更多token的人,有更大的概率获得记账权利,然后获得奖励。 这个概率具体有多大呢? 下面我们在代码实现中会展示,分析也放在后面。 当然,\bPOS是会比POW更好吗? 会更去中心化吗? \b现在看来未必,所以我们这里也不去对比谁优谁劣。 我们站在中立的角度,单纯的来讨论讨论POS这种算法。 代码实战生成一个Block既然要实现POS算法,那么就难免要生成一条链,链又是由一个个Block生成的,所以下面我们首先来看看如何生成Block,当然在前面的内容里面,关于如何生成Block,以及交易、UTXO等等都已经介绍过了。由于今天我们的核心是实现POS,所以关于Block的生成,我们就用最简单的实现方式,好让大家把目光聚焦在核心\b的内容上面。 我们用三个方法来实现生成一个合法的区块 calculate_hash 计算区块的hash值 is_block_valid 校验区块是否合法 generate_block 生成一个区块 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253from hashlib import sha256from datetime import datetimedef generate_block(oldblock, bpm, address): \"\"\" :param oldblock: :param bpm: :param address: :return: \"\"\" newblock = { \"Index\": oldblock[\"Index\"] + 1, \"BPM\": bpm, \"Timestamp\": str(datetime.now()), \"PrevHash\": oldblock[\"Hash\"], \"Validator\": address } newblock[\"Hash\"] = calculate_hash(newblock) return newblockdef calculate_hash(block): record = \"\".join([ str(block[\"Index\"]), str(block[\"BPM\"]), block[\"Timestamp\"], block[\"PrevHash\"] ]) return sha256(record.encode()).hexdigest()def is_block_valid(newblock, oldblock): \"\"\" :param newblock: :param oldblock: :return: \"\"\" if oldblock[\"Index\"] + 1 != newblock[\"Index\"]: return False if oldblock[\"Hash\"] != newblock[\"PrevHash\"]: return False if calculate_hash(newblock) != newblock[\"Hash\"]: return False return True 这里为了更灵活,我们没有用类的实现方式,直接采用函数来实现了\bBlock生成,相信很容易看懂。 创建一个TCP服务器由于我们需要用权益证明算法来选择记账人,所以需要从很多Node(节点)中选择记账人,也就是需要一个server让节点链接上来,同时要同步信息给节点。因此需要一个TCP长链接。 123456from socketserver import BaseRequestHandler, ThreadingTCPServerdef run(): # start a tcp server serv = ThreadingTCPServer(('', 9090), HandleConn) serv.serve_forever() 在这里我们用了python内库socketserver来创建了一个TCPServer。 需要注意的是,这里我们是采用的多线程的创建方式,这样可以保证有多个客户端\b同时连接上来,而不至于被阻塞。当然,这里这个server也是存在问题的,那就是有多少个客户端连接,就会创建多少个线程,\b更好的方式是创建一个线程池。由于这里是测试,所以就采用更简单的方式了。 相信大家已经看到了,在我们创建TCPServer的时候,\b使用到了HandleConn,但是我们还没有定义,所以接下来我们就来定义一个HandleConn ##消息处理器下面我们来实现Handler函数,\bHandler函数在跟Client Node通信的时候,需要我们的Node实现下面的功能 Node可以输入balance(token数量) 也就是股权数目 Node需要能够接收广播,方便Server同步区块以及记账人信息 添加自己到候选人名单 (候选人为持有token的人) 输入BPM生成Block 验证一个区块的合法性 感觉任务还是蛮多的,接下来我们看代码实现 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556import threadingfrom queue import Queue, Empty# 定义变量block_chain = []temp_blocks = []candidate_blocks = Queue() # 创建队列,用于线程间通信announcements = Queue()validators = {}My_Lock = threading.Lock()class HandleConn(BaseRequestHandler): def handle(self): print(\"Got connection from\", self.client_address) # validator address self.request.send(b\"Enter token balance:\") balance = self.request.recv(8192) try: balance = int(balance) except Exception as e: print(e) t = str(datetime.now()) address = sha256(t.encode()).hexdigest() validators[address] = balance print(validators) while True: announce_winner_t = threading.Thread(target=annouce_winner, args=(announcements, self.request,), daemon=True) announce_winner_t.start() self.request.send(b\"\\nEnter a new BPM:\") bpm = self.request.recv(8192) try: bpm = int(bpm) except Exception as e: print(e) del validators[address] break # with My_Lock: last_block = block_chain[-1] new_block = generate_block(last_block, bpm, address) if is_block_valid(new_block, last_block): print(\"new block is valid!\") candidate_blocks.put(new_block) self.request.send(b\"\\nEnter a new BPM:\\n\") annouce_blockchain_t = threading.Thread(target=annouce_blockchain, args=(self.request,), daemon=True) annouce_blockchain_t.start() 这段代码,可能对大多数同学来说是有难度的,在这里我们采用了多线程的方式,同时为了能够让消息在线程间通信,\b我们使用了队列。 这里使用队列,也是为了我们的系统可以更好的拓展,后面如果可能,这一节的程序很容易拓展为分布式系统。 将多线程里面处理的任务拆分出去成独立的服务,然后用消息队列进行通信,就是一个\b简单的分布式系统啦。(是不是很激动?) 由于这里有难度,所以代码还是讲一讲吧 123456789101112# validator address self.request.send(b\"Enter token balance:\") balance = self.request.recv(8192) try: balance = int(balance) except Exception as e: print(e) t = str(datetime.now()) address = sha256(t.encode()).hexdigest() validators[address] = balance print(validators) 这一段就是我们提到的Node \b客户端添加自己到候选人的代码,\b每链接一个客户端,就会添加一个候选人。 这里我们用添加的时间戳的hash来记录候选人。 当然也可以用其他的方式,比如我们代码里面的client_address 1234567891011121314151617181920announce_winner_t = threading.Thread(target=annouce_winner, args=(announcements, self.request,), daemon=True) announce_winner_t.start()def annouce_winner(announcements, request): \"\"\" :param announcements: :param request: :return: \"\"\" while True: try: msg = announcements.get(block=False) request.send(msg.encode()) request.send(b'\\n') except Empty: time.sleep(3) continue 然后接下来我们起了一个线程去广播获得记账权的节点信息到所有节点。 1234567891011121314151617self.request.send(b\"\\nEnter a new BPM:\") bpm = self.request.recv(8192) try: bpm = int(bpm) except Exception as e: print(e) del validators[address] break # with My_Lock: last_block = block_chain[-1] new_block = generate_block(last_block, bpm, address) if is_block_valid(new_block, last_block): print(\"new block is valid!\") candidate_blocks.put(new_block) 根据节点输入的BPM值生成一个区块,并校验区块的有效性。 将有效的区块放到候选区块当中,等待记账人将区块添加到链上。 123456789101112131415161718annouce_blockchain_t = threading.Thread(target=annouce_blockchain, args=(self.request,), daemon=True) annouce_blockchain_t.start()def annouce_blockchain(request): \"\"\" :param request: :return: \"\"\" while True: time.sleep(30) with My_Lock: output = json.dumps(block_chain) try: request.send(output.encode()) request.send(b'\\n') except OSError: pass 最后起一个线程,同步区块链到所有节点。 看完了,节点跟Server交互的部分,接下来是最重要的部分, POS算法实现123456789101112131415161718192021222324252627282930313233343536373839def pick_winner(announcements): \"\"\" 选择记账人 :param announcements: :return: \"\"\" time.sleep(10) while True: with My_Lock: temp = temp_blocks lottery_pool = [] # if temp: for block in temp: if block[\"Validator\"] not in lottery_pool: set_validators = validators k = set_validators.get(block[\"Validator\"]) if k: for i in range(k): lottery_pool.append(block[\"Validator\"]) lottery_winner = choice(lottery_pool) print(lottery_winner) # add block of winner to blockchain and let all the other nodes known for block in temp: if block[\"Validator\"] == lottery_winner: with My_Lock: block_chain.append(block) # write message in queue. msg = \"\\n{0} 赢得了记账权利\\n\".format(lottery_winner) announcements.put(msg) break with My_Lock: temp_blocks.clear() 这里我们用pick_winner 来选择记账权利,我们根据token数量构造了一个列表。 一个人获得记账权利的概率为:1p = mount['NodeA']/mount['All'] 文字描述就是\b\b其token数目在总数中的占比。 比如总数有100个,他有10个,那么其获得记账权的概率就是0.1, 到这里核心的部分就写的差不多了,接下来,我们来添加节点,开始测试吧 测试POS的记账方式在测试之前,起始还有一部分工作要做,前面我们的run方法需要完善下,代码如下: 1234567891011121314151617181920212223242526272829303132333435363738394041def run(): # create a genesis block t = str(datetime.now()) genesis_block = { \"Index\": 0, \"Timestamp\": t, \"BPM\": 0, \"PrevHash\": \"\", \"Validator\": \"\" } genesis_block[\"Hash\"] = calculate_hash(genesis_block) print(genesis_block) block_chain.append(genesis_block) thread_canditate = threading.Thread(target=candidate, args=(candidate_blocks,), daemon=True) thread_pick = threading.Thread(target=pick_winner, args=(announcements,), daemon=True) thread_canditate.start() thread_pick.start() # start a tcp server serv = ThreadingTCPServer(('', 9090), HandleConn) serv.serve_forever()def candidate(candidate_blocks): \"\"\" :param candidate_blocks: :return: \"\"\" while True: try: candi = candidate_blocks.get(block=False) except Empty: time.sleep(5) continue temp_blocks.append(candi)if __name__ == '__main__': run() 添加节点连接到TCPServer为了充分减少程序的复杂性,tcp client我们这里就不实现了,可以放在后面拓展部分。 毕竟我们这个系统是很容易扩展的,后面我们拆分了多线程的部分,在实现tcp client就是一个完整的分布式系统了。 所以,我们这里用linux自带的命令 nc,\b不知道nc怎么用的\b同学可以google或者\b man nc 启动服务 运行 python pos.py 打开3个终端 分别\b输入下面命令 nc localhost 9090 终端如果\b输出1Enter token balance: 说明你client已经链接服务器ok啦. 测试POS的记账方式接下来依次按照提示操作。 balance可以按心情来操作,因为这里是测试,我们输入100,紧接着会提示输入BPM,我们前面提到过,输入BPM是为了生成Block,那么就输入吧,随便输入个9. ok, 接下来就稍等片刻,等待记账。输出如同所示依次在不同的终端,根据提示输入数字,等待消息同步。 生成区块链下面是我这边获得的3个block信息。 总结在上面的代码中,我们实现了一个完整的\b基于POS算法记账的链,当然这里有许多值得扩展与改进的地方。 python中多线程开销比较大,可以改成协程的方式 TCP\b建立的长链接是基于TCPServer,是中心化的方式,可以改成P2P对等网络 链的信息不够完整 系统可以拓展成分布式,让其更健壮 大概列了以上几点,其他还有很多可以\b拓展的地方,感兴趣的朋友可以先玩玩, 后者等到我们后面的教程。 (广告打的措手不及,哈哈) 当然了,语言不是重点,所以在这里,我也实现了go语言的版本源码地址 go语言的实现感觉要更好理解一点,也显得要优雅一点。这也是为什么go语言在分布式领域要更抢手的原因之一吧! 项目地址 https://github.com/csunny/py-bitcoin/ \b参考 https://medium.com/@mycoralhealth/code-your-own-proof-of-stake-blockchain-in-go-610cd99aa658 我的专栏专注区块链底层技术开发,P2P网络、加密技术、MerkleTree、DAG、DHT等等,另外对于分布式系统的学习也很有帮助。欢迎大家交流。","categories":[{"name":"比特币","slug":"bitcoin","permalink":"https://learnblockchain.cn/categories/bitcoin/"}],"tags":[{"name":"区块链","slug":"区块链","permalink":"https://learnblockchain.cn/tags/区块链/"},{"name":"权益证明","slug":"权益证明","permalink":"https://learnblockchain.cn/tags/权益证明/"},{"name":"Python","slug":"Python","permalink":"https://learnblockchain.cn/tags/Python/"}]},{"title":"什么是EOS(柚子)","slug":"whatiseos","date":"2018-07-17T06:25:44.000Z","updated":"2019-04-05T10:29:31.086Z","comments":true,"path":"2018/07/17/whatiseos/","link":"","permalink":"https://learnblockchain.cn/2018/07/17/whatiseos/","excerpt":"是时候给写写EOS了,现在EOS主网已经上线,尽管我个人不是很喜欢EOS项目(不过也一直在关注EOS),但是不可否认EOS这个争议性很大的项目给区块链世界带来的变化。","text":"是时候给写写EOS了,现在EOS主网已经上线,尽管我个人不是很喜欢EOS项目(不过也一直在关注EOS),但是不可否认EOS这个争议性很大的项目给区块链世界带来的变化。 写在前面阅读本文前,如果了解过比特币及以太坊,可以更好的理解本文。欢迎订阅专栏:区块链技术指引你从头开始学区块链技术。 本文出现EOS是指EOS.io公链项目,不是指以太坊上的EOS Token。 EOS 简介EOS: Enterprise Operation System 中文意思为:商业级区块链操作系统。 尽管以太坊创造性引入智能合约概念,极大的简化了区块链应用的开发,但以太坊平台依然有一个很大的限制,就是交易确认时间及交易吞吐量比较小,从而严重影响了以太坊进行商业应用。 交易吞吐量有一个专门的词:TPS (transaction per second 每秒的交易量) 比特币的TPS 是大概7,并且最少几十分钟交易才能被确认,以太坊的TPS大概是20左右,交易的确认一般需要几分钟的时间。不过比特币以太坊也在不断进化以提高TPS,比如比特币的闪电网络,以太坊的Sharding技术(分片)以及Plasma技术(分层)。 EOS 项目的目标是建立可以承载商业级智能合约与应用的区块链基础设施,成为区块链世界的“底层操作系统”。EOS通过石墨烯技术解决延迟和数据吞吐量问题,TPS可达到数千,交易的确认时间也只有数秒。同时声称未来使用并行链的方式,最高可以达到数百万TPS。 EOS 设计了一套账户权限管理系统,EOS不再使用的地址作为账户,可以直接使用字符作为账户名,并设计了一套的账户权限体系。 此外,在 EOS 上转账交易及运行智能合约不需要消耗 EOS代币。而是EOS 系统当中,抵押代币获取对应的资源,来执行相应交易,在EOS运行程序完全免费的说是不准确的。 值的一提的是EOS项目其ICO也是基于以太坊ERC20 Token进行的,其ICO 时间长达355天,作为一个当时还未上线的项目,融资额达到40亿美元是前所未有。 充满争议的技术天才BMEOS的主要开发者为丹尼尔·拉瑞莫(Daniel Larimer), 绰号BM(GitHub的昵称:ByteMaster), 它是EOS的项目方,BlockOne公司的CTO。和V神一样,也是一个神奇的人物,网络上两人因理念不合有多次论战。BM有一句牛B 轰轰的话:我终生的使命,是致力于找到一些加密经济的解决方案,给所有人的财产、自由、平等带来保障。 BM成功创立过三个区块链项目:BitShares、Steem 以及EOS,是一个技术天才,也是一个多变的人。2009年的BM也准备的数字货币一展身手,在其研究比特币之后,2010年BM提出了一些比特币的问题,并想要改进,结果比特币的创始人中本聪(Satoshi Nakamoto)怼会了他“If you don’t believe me or don’t get it, I don’t have time to try to convince you, sorry.”(懂不懂随你,我可没时间理你)。于是BM开始着手创建自己的区块链项目,这就是2013年发布的 BitShares 比特股,世界上第一个数字货币去中心化交易所。 BitShares在2014年上线时,是当时的明星项目,也由于bug太多、糟糕的体验以及BM在进行个别版本升级的时候都不提供向下兼容,用户逐渐流失,更要命的是,BM利用自己超过1/3的记账节点,在没有达成社区共识的情况下,强行分叉增发了BitShares发行总量。尽管BM在技术提供了改进,发布了石墨烯工具集,不过最终社区投票决定让BM离开了BitShares。 离开BitShares的BM,于2016年创立了区块链项目Steem,去中心化社交网站Steemit就是基于Steem创建,在Steemit的运营期间,BM和Steemit的CEO Ned有过多次口水战。在2017年,BM离开了自己创建的Steem项目(也许除了BM自己,没有人能知道他离开Steem的真实原因),选择与布鲁默联合创办了BlockOne公司打造EOS项目。 石墨烯(Graphene)与 DPOS和BitShares、Steem 一样,EOS底层使用的也是石墨烯技术,石墨烯是一个开源的区块链底层库,也出自BM之手,它采用的是 DPOS(Delegated Proof-of-Stake 股份授权证明机制 )的共识机制。在比特币及以太坊网络中,任何人都可以参与记账,而DPOS为了提高出块速度TPS,限制了参与记账了人数,在DPOS中,记账者不在称为矿工,而是改称为见证人 Witness,现在EOS中,又有一个新词:Block Producer,简称BP,大家翻译为超级节点(本文中依旧会使用见证人这个词,超级节点更像是一个市场营销用词)。 在EOS中,见证人的个数是21个,BitShares中是101个,BitShares的出块时间打开是 1.5秒,在EOS中,出块时间提高到了0.5秒。 和Pow及Pos共识机制矿工可以自由选择参与挖矿不同,DPOS下节点需要参与见证人选举,只有赢得选举的节点才能负责出块,在EOS中,赢得选举21个节点见证人轮流出块。另外还有100个备用见证人(候选节点),在21个见证人出现问题后做替补。EOS的发行总量是10亿, 见证人在完成打包交易区块后,可以领取到区块的奖励,区块的奖励来自对发行量的通胀增发,通胀率每年接近5%。 BM特色的去中心化我个人理解的区块链,它最大的革命性就是他的中立性,其运行不应该受到任何人的干扰,在POW共识中,矿工、项目方(开发者)以及交易方他们是相互独立的存在。 在EOS中,BM本人拥有巨量的选票,他可以在一定程度上左右见证人的选举,同时BM还为EOS制定了宪法,要求所有的见证人必须遵照宪法。因此BM某种程度上可以左右EOS系统的运行。 本文是个人对EOS的理解,受我自己视野局限也许理解有偏差,欢迎大家批准指正,我的微信: xlbxiong。 EOS相关资料: EOS开发者资源 官方网站 Github 代码 我们为区块链爱者这提供了系统的区块链视频教程,觉得文章学习不过瘾的同学可以戳区块链视频教程。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"EOS","slug":"EOS","permalink":"https://learnblockchain.cn/categories/EOS/"}],"tags":[{"name":"EOS入门","slug":"EOS入门","permalink":"https://learnblockchain.cn/tags/EOS入门/"},{"name":"柚子","slug":"柚子","permalink":"https://learnblockchain.cn/tags/柚子/"}]},{"title":"搭建智能合约开发环境Remix IDE及使用","slug":"remix-ide","date":"2018-06-07T02:56:04.000Z","updated":"2019-04-05T10:29:31.073Z","comments":true,"path":"2018/06/07/remix-ide/","link":"","permalink":"https://learnblockchain.cn/2018/06/07/remix-ide/","excerpt":"目前开发智能的IDE, 首推还是Remix, 而Remix官网, 总是由于各种各样的(网络)原因无法使用,本文就来介绍一下如何在本地搭建智能合约开发环境remix-ide并介绍Remix的使用。","text":"目前开发智能的IDE, 首推还是Remix, 而Remix官网, 总是由于各种各样的(网络)原因无法使用,本文就来介绍一下如何在本地搭建智能合约开发环境remix-ide并介绍Remix的使用。 写在前面Remix 是以太坊智能合约编程语言Solidity IDE,阅读本文前,你应该对以太坊、智能合约有所了解,如果还不了解,建议先看以太坊是什么。 Remix IDE 介绍Remix IDE 是一款基于浏览器的IDE,跟有些开发聊的时候,发现有一些同学对浏览器的IDE,有一些偏见,其实Atom编辑器就是基于web技术开发的一款编辑器(Atom可以看做一个没有地址栏的浏览器),其实基于浏览器的IDE,有一个很大的好处就是不用安装,打开即用。 Remix IDE的功能全面(传统IDE有的功能这里都有),比如: 代码提示补全,代码高亮 代码警告、错误提示 运行日志输出 代码调试 … Remix IDE 安装 更新: Remix 现在提供了一个APP, 叫Remix APP, 如果是Mac 电脑,可以直接使用其提供的发布包,地址为:https://github.com/horizon-games/remix-app/releases 如果你有很好的网络环境,可以直接访问Remix官网。要不能还是还是像我一样老老实实把Remix IDE安装在本地,我发现要想成功安装选择对应的版本很关键,具体的版本要求如下: 123456$ node --versionv7.10.1$ npm --version4.2.0$ nvm --version0.33.11 nvm 安装nvm 是一个node 版本工具,我们可以使用nvm来安装不同版本的node。nvm 官方安装方法如下: 命令行中输入: 1curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash 在当前用户profile文件,如(~/.bash_profile, ~/.zshrc, ~/.profile, or ~/.bashrc)添加加载nvm的脚本: 12export NVM_DIR=\"$HOME/.nvm\"[ -s \"$NVM_DIR/nvm.sh\" ] && \\. \"$NVM_DIR/nvm.sh\" 重启下命令行,输入nvm 试试,应该可以看到 nvm 命令的帮助 使用nvm 安装node因为Remix IDE 要求使用node 7.10.1, 命令行输入一下命令进行安装: 1nvm install 7 安装完成之后,使用node –version 和 npm –version检查下版本号,是否和刚刚列出版本要求一致,在版本一值的qing 命令行安装Remix ide方法1直接使用npm安装,这也是我安装使用的方法。12npm install remix-ide -gremix-ide 如果出现错误:Error: EACCES: permission denied, access ‘/usr/local/lib/node_modules’可以尝试用以下方法解决:1sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share} 如果我们使用的是正确的node 和npm 版本的话,应该都可以安装成功,安装成功之后,remix-ide命令用来启动ide. 方法2remix-ide的github 上还提供了另一个方法进行安装,通过clone 代码来安装,方法如下: 12345git clone https://github.com/ethereum/remix-ide.gitcd remix-idenpm installnpm run setupremix # this will clone https://github.com/ethereum/remix for you and link it to remix-idenpm start Remix ide 使用Remix IDE 默认是使用8080端口启动的,启动之后在浏览器打开:http://localhost:8080/, 如图: 和大多数IDE一样,最左边是文件浏览,中间是代码编辑区域,右边是功能区域,下边是日志区域。在右侧的功能区域,常用的是Compile、Run及Debuger几个标签页(Tab)。 在Compile页,会动态的显示当前编辑区域合约的编译信息,如显示错误和警告。编译的直接码信息及ABI接口可以通过点击Details查看到。在这篇文章里 也有截图说明。在Run页,可以部署合约,以及调用合约函数等,使用非常简单,我们前面也有多篇文章讲解。Debuger页在下面调试一节单独讲解。 Remix ide 加载本地磁盘文件这是一个非常用的功能,但发现使用的人非常少,通过加载本地磁盘文件,就可以方便代码管理工具(如 git)管理我们的合约代码。我详细介绍下如何这个功能怎么使用? 使用在线版本的Remix可以使用这个功能, 不过需要安装一下remixd, 安装使用命令npm install -g remixd。 在需要的本地合约代码的目录下启动remix-ide, Remix IDE 会自动把当前目录做为共享目录。如果是使用在线的Remix,需要使用命令remixd -s shared-folder 来指定共享目录。 加载共享目录,在文件浏览区域上有,有这样一个图标,他用来加载本地共享目录,如图: 调试在合约编写过程中,合约调试是必不可少的一部分,为了模拟调试的过程,我故意在代码中加入一ge错误的逻辑代码如下: 1234567891011121314pragma solidity ^0.4.0;contract SimpleStorage { uint storedData; function set(uint x) public { storedData += x; // 错误的,多加了一个加号 } function get() public constant returns (uint) { return storedData; }} 加入了错误的逻辑之后,我第2次调用set函数,合约状态变量的值,可能会出错(如果第一次不是用参数0去调用的话)。注意如果需要调试合约,在部署合约的环境应该选择:JavaScript VM。 开始调试在我们每次执行一个交易(不管是方式调用还是函数执行)的时候,在日志都会输出一条记录,如图: 点击上图中的“Debug”按钮,在Remix右侧的功能区域会切换到调试面板,如下图:调试过程过程中,有下面几项需要重点关注: Transactions: 可以查看交易及交易的执行过程,并且提供了7个调试的按钮,如下图: 为了方便介绍,我为每个按钮编了号,每个按钮的含义是: 后退一步(不进入函数内部) 后退一步(进入函数内部) 前进一步(进入函数内部) 前进一步(不进入函数内部) 跳到上一个断点 跳出当前调用 跳到下一个断点 Solidity Locals:显示当前上下文的局部变量的值, 如图: Solidity State: 显示当前执行合约的状态变量,如下图: 在本例中,我们跟踪运行步骤的时候,可以看到局部变量的值为2,赋值给状态变量之后,状态变量的值更改为了3,所以可以判断运行当前语句的时候出错了。 Step detail: 显示当前步骤的gas详情等,如下图: 设置断点这部分为小专栏读者准备,欢迎订阅小专栏区块链技术查看。 参考链接 remix-ide github Remix Document 我们还为区块链技术爱好者提供了系统的区块链视频教程,觉得文章学习不过瘾的同学可以戳入门视频教程及以太坊智能合约开发。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。如果你学习区块链中遇到问题,欢迎加入知识星球深入浅出区块链问答社区,作为星友福利,星友可加入我创建的区块链技术群,群内已经聚集了300多位区块链技术牛人和爱好者。 !--- 在本例中,我们的代码比较简单,执行的不多,可以不用设置断点,如果代码比较多,这可以设置断点,利用上面介绍的第7个按钮快速的调转到断点处,设置断点的方法很简单,在编辑区域,点击代码的行号,就可以在当前行设置一个断点,如下图: ![](https://img.learnblockchain.cn/2018/remix-debug-break.png!wl) 上图在第8行出设置了一个断点,成功设置断点会在行号处,加上一个色块来标示断点。取消断点的方式是,再次点击断点处。 有一点需要注意一下,如果在声明变量的地方设置断点,这断点可能会触发两次,第一次是初始化为0,第二次是赋实际的值。 --","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"智能合约","slug":"ethereum/智能合约","permalink":"https://learnblockchain.cn/categories/ethereum/智能合约/"}],"tags":[{"name":"IDE","slug":"IDE","permalink":"https://learnblockchain.cn/tags/IDE/"}]},{"title":"Solidity 教程系列11 - 视图函数、虚函数讲解","slug":"solidity-functions","date":"2018-05-17T14:16:29.000Z","updated":"2019-04-05T10:29:32.285Z","comments":true,"path":"2018/05/17/solidity-functions/","link":"","permalink":"https://learnblockchain.cn/2018/05/17/solidity-functions/","excerpt":"Solidity 教程系列第11篇 - Solidity 视图函数、虚函数讲解。Solidity 系列完整的文章列表请查看分类-Solidity。","text":"Solidity 教程系列第11篇 - Solidity 视图函数、虚函数讲解。Solidity 系列完整的文章列表请查看分类-Solidity。 写在前面Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 欢迎订阅区块链技术专栏阅读更全面的分析文章。 视图函数(View Functions)一个函数如果它不修改状态变量,应该声明为view函数,不过下面几种情况认为是修改了状态: 写状态变量 触发事件(events) 创建其他的合约 call调用附加了以太币 调用了任何没有view或pure修饰的函数 使用了低级别的调用(low-level calls) 使用了包含特定操作符的内联汇编 看一个例子: 1234567891011121314pragma solidity ^0.4.16;contract C { uint public data = 0; function f(uint a, uint b) public view returns (uint) { return a * (b + 42) + now; } // 错误做法,虽然可以编译通过 function df(uint a) public view { data = a; }} 有几个地方需要注意一下: 声明为view 和声明为constant是等价的,constant是view的别名,constant在计划Solidity 0.5.0版本之后会弃用(constant这个词有歧义,view 也更能表达返回值可视)。 访问函数都被标记为view。 当前编译器并未强制要求声明为view,但建议大家对于不会修改状态的函数的标记为view。 纯函数(Pure Functions)函数可以声明为view,表示它即不读取状态,也不修改状态,除了上一节介绍的几种修改状态的情况,以下几种情况被认为是读取了状态: 读状态变量 访问了 this.balance 或 \\.balance 访问了block, tx, msg 的成员 (msg.sig 和 msg.data除外). 调用了任何没有pure修饰的函数 使用了包含特定操作符的内联汇编 看一个例子: 1234567pragma solidity ^0.4.16;contract C { function f(uint a, uint b) public pure returns (uint) { return a * (b + 42); }} 尽管view 和 pure 修饰符编译器并未强制要求使用,view 和 pure 修饰也不会带来gas 消耗的改变,但是更好的编码习惯让我们跟容易发现智能合约中的错误。 参考文献官方文档-函数 欢迎来知识星球提问,星球内已经聚集了300多位区块链技术爱好者。深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"智能合约","slug":"ethereum/智能合约","permalink":"https://learnblockchain.cn/categories/ethereum/智能合约/"},{"name":"Solidity","slug":"ethereum/智能合约/Solidity","permalink":"https://learnblockchain.cn/categories/ethereum/智能合约/Solidity/"}],"tags":[{"name":"Solidity","slug":"Solidity","permalink":"https://learnblockchain.cn/tags/Solidity/"},{"name":"智能合约","slug":"智能合约","permalink":"https://learnblockchain.cn/tags/智能合约/"}]},{"title":"详解 Solidity 事件Event - 完全搞懂事件的使用","slug":"solidity-event","date":"2018-05-09T12:37:03.000Z","updated":"2019-04-05T10:29:32.241Z","comments":true,"path":"2018/05/09/solidity-event/","link":"","permalink":"https://learnblockchain.cn/2018/05/09/solidity-event/","excerpt":"很多同学对Solidity 中的Event有疑问,这篇文章就来详细的看看Solidity 中Event到底有什么用?","text":"很多同学对Solidity 中的Event有疑问,这篇文章就来详细的看看Solidity 中Event到底有什么用? 写在前面Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么,另外本文在监听合约事件是对上一篇Web3与智能合约交互实战进行补充,如果阅读了上一篇可以更好的理解本文。 什么是事件Evnet事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。当被发送事件(调用)时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并记录到区块链中.来捋这个关系:区块链是打包一系列交易的区块组成的链条,每一个交易“收据”会包含0到多个日志记录,日志代表着智能合约所触发的事件。 在DAPP的应用中,如果监听了某事件,当事件发生时,会进行回调。不过要注意:日志和事件在合约内是无法被访问的,即使是创建日志的合约。 在Solidity 代码中,使用event 关键字来定义一个事件,如: 1event EventName(address bidder, uint amount); 这个用法和定义函数式一样的,并且事件在合约中同样可以被继承。触发一个事件使用emit(说明,之前的版本里并不需要使用emit),如: 1emit EventName(msg.sender, msg.value); 触发事件可以在任何函数中调用,如: 12345function testEvent() public { // 触发一个事件 emit EventName(msg.sender, msg.value); } 监听事件通过上面的介绍,可能大家还是不清楚事件有什么作用,如果你跟过Web3与智能合约交互实战这篇文章,你会发现点击”Updata Info”按钮之后,虽然调用智能合约成功,但是当前的界面并没有得到更新。使用事件监听,就可以很好的解决这个问题,让看看如何实现。 修改合约,定义事件及触发事件先回顾一下合约代码: 12345678910111213141516pragma solidity ^0.4.21;contract InfoContract { string fName; uint age; function setInfo(string _fName, uint _age) public { fName = _fName; age = _age; } function getInfo() public constant returns (string, uint) { return (fName, age); } } 首先,需要定义一个事件: 1234event Instructor( string name, uint age ); 这个事件中,会接受两个参数:name 和 age , 也就是需要跟踪的两个信息。 然后,需要在setInfo函数中,触发Instructor事件,如: 12345function setInfo(string _fName, uint _age) public { fName = _fName; age = _age; emit Instructor(_fName, _age);} 在Web3与智能合约交互实战, 点击”Updata Info”按钮之后,会调用setInfo函数,函数时触发Instructor事件。 使用Web3监听事件,刷新UI现在需要使用Web3监听事件,刷新UI。先回顾下之前的使用Web3和智能合约交互的代码: 1234567891011121314151617181920212223242526272829<script> if (typeof web3 !== 'undefined') { web3 = new Web3(web3.currentProvider); } else { // set the provider you want from Web3.providers web3 = new Web3(new Web3.providers.HttpProvider(\"http://localhost:7545\")); } web3.eth.defaultAccount = web3.eth.accounts[0]; var infoContract = web3.eth.contract(ABI INFO); var info = infoContract.at('CONTRACT ADDRESS'); info.getInfo(function(error, result){ if(!error) { $(\"#info\").html(result[0]+' ('+result[1]+' years old)'); console.log(result); } else console.error(error); }); $(\"#button\").click(function() { info.setInfo($(\"#name\").val(), $(\"#age\").val()); });</script> 现在可以不需要 info.getInfo()来获取信息,而改用监听事件获取信息,先定义一个变量引用事件: 1var instructorEvent = info.Instructor(); 然后使用.watch()方法来添加一个回调函数: 12345678instructorEvent.watch(function(error, result) { if (!error) { $(\"#info\").html(result.args.name + ' (' + result.args.age + ' years old)'); } else { console.log(error); } }); 代码更新之后,可以在浏览器查看效果,这是点击”Updata Info”按钮之后,会及时更新界面,如图: 完整的代码请订阅小专栏区块链技术查看。 事件高级用法-过滤器有时我们会有这样的需求:获取当前所有姓名及年龄记录,或者是,要过滤出年龄28岁的记录,应该如何做呢?以及另外一个常见的场景:想要获取到代币合约中所有的转账记录,也同样需要使用事件过滤器功能,这部分内容请大家订阅小专栏区块链技术阅读。 如果你想学习以太坊DApp开发,这门视频课程以太坊DAPP开发实战是你不错的选择。 参考文章https://coursetro.com/posts/code/100/Solidity-Events-Tutorial---Using-Web3.js-to-Listen-for-Smart-Contract-Eventshttps://github.com/ethereum/wiki/wiki/JavaScript-API#contract-events 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。如果你想和我有密切的联系,欢迎加入知识星球深入浅出区块链,我会在星球为大家解答技术问题,作为星友福利,星友可加入我创建的区块链技术群,群内已经聚集了300多位区块链技术牛人和爱好者。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"智能合约","slug":"ethereum/智能合约","permalink":"https://learnblockchain.cn/categories/ethereum/智能合约/"}],"tags":[{"name":"Solidity","slug":"Solidity","permalink":"https://learnblockchain.cn/tags/Solidity/"},{"name":"智能合约","slug":"智能合约","permalink":"https://learnblockchain.cn/tags/智能合约/"},{"name":"Event","slug":"Event","permalink":"https://learnblockchain.cn/tags/Event/"},{"name":"web3","slug":"web3","permalink":"https://learnblockchain.cn/tags/web3/"}]},{"title":"智能合约最佳实践 之 Solidity 编码规范","slug":"solidity-style-guide","date":"2018-05-04T02:22:08.000Z","updated":"2019-04-05T10:29:32.262Z","comments":true,"path":"2018/05/04/solidity-style-guide/","link":"","permalink":"https://learnblockchain.cn/2018/05/04/solidity-style-guide/","excerpt":"每一门语言都有其相应的编码规范, Solidity 也一样, 下面官方推荐的规范及我的总结,供大家参考,希望可以帮助大家写出更好规范的智能合约。","text":"每一门语言都有其相应的编码规范, Solidity 也一样, 下面官方推荐的规范及我的总结,供大家参考,希望可以帮助大家写出更好规范的智能合约。 命名规范避免使用小写的l,大写的I,大写的O 应该避免在命名中单独出现,因为很容易产生混淆。 合约、库、事件、枚举及结构体命名 合约、库、事件及结构体命名应该使用单词首字母大写的方式,这个方式也称为:帕斯卡命名法或大驼峰式命名法,比如:SimpleToken, SmartBank, CertificateHashRepository,Player。 函数、参数、变量及修饰器函数、参数、变量及修饰器应该使用首单词小写后面单词大写的方式,这个方式也称为:(小)驼峰式命名法,是一种混合大小写的方式,如: 函数名应该如:getBalance,transfer,verifyOwner,addMember。 参数和变量应该如:initialSupply,senderAddress,account,isPreSale。 修饰器应该如:onlyAfter,onlyOwner。 代码格式相关缩进使用空格(spaces)而不是Tab, 缩进应该是4个空格 空行合约之间应该有空行,例如: 12345678910111213contract A { ...} contract B { ...} contract C { ...} 而不是使用: 12345678910contract A { ...}contract B { ...} contract C { ...} 函数之间应该有空行,例如: 123456789contract A { function spam() public { ... } function ham() public { ... }} 没有实现的话,空行可以省去,如: 1234contract A { function spam() public; function ham() public;} 而不是: 12345678contract A { function spam() public { ... } function ham() public { ... }} 左括号应该跟定义在一行定义包括合约定义、函数定义、库定义、结构体定义等等,例如推荐使用: 123456 contract Coin { struct Bank { address owner; uint balance; }} 而不是: 1234567contract Coin{ struct Bank { address owner; uint balance; }} 左括号应该跟条件控制在一行在使用if, else, while, for 时,推荐的写法是: 1234567if (...) { ...}for (...) { ...} 而不是: 12345678910if (...){ ...}while(...){}for (...) { ...;} 如果控制语句内只有一行,括号可省略,如: 12if (x < 10) x += 1; 但像下面一个语句有多方就不能省略,如: 12345if (x < 10) someArray.push(Coin({ name: 'spam', value: 42 })); 表达式内的空格 一个单行的表达里,在小括号、中括号、大括号里应该避免不必要的空格,例如推荐使用: 1spam(ham[1], Coin({name: \"ham\"})); 而不是: 1spam( ham[ 1 ], Coin( { name: \"ham\" } ) ); 有一种例外是,结尾的括号跟在结束的分号后面, 应该加一个空格,如下面的方式也是推荐的: 1function singleLine() public { spam(); } 分号;前不应该有空格,例如推荐使用: 1function spam(uint i, Coin coin) public; 而不是: 1function spam(uint i , Coin coin) public ; 不要为对齐添加不必要的空格,例如推荐使用: 123x = 1;y = 2;long_variable = 3; 而不是: 123x = 1;y = 2;long_variable = 3; 回退函数不应该有空格,例如推荐使用: 123456789101112function() public { ...}``` 而不是:```jsfunction () public { ...} 控制每一行长度每行不应该太长,最好在79(或99)个字符以内,函数的参数应该是单独的行,且只有一个缩进,例如推荐的方式是: 12345thisFunctionCallIsReallyLong( longArgument1, longArgument2, longArgument3); 而不是: 12345678910111213141516171819202122232425thisFunctionCallIsReallyLong(longArgument1, longArgument2, longArgument3);thisFunctionCallIsReallyLong(longArgument1, longArgument2, longArgument3);thisFunctionCallIsReallyLong( longArgument1, longArgument2, longArgument3);thisFunctionCallIsReallyLong(longArgument1,longArgument2,longArgument3);thisFunctionCallIsReallyLong( longArgument1, longArgument2, longArgument3); 对应的赋值语句应该是这样写: 123456 thisIsALongNestedMapping[being][set][to_some_value] = someFunction( argument1, argument2, argument3, argument4); 而不是: 1234thisIsALongNestedMapping[being][set][to_some_value] = someFunction(argument1, argument2, argument3, argument4); 事件定义也应该遵循同样的原则,例如应该使用: 123456789101112131415event LongAndLotsOfArgs( adress sender, adress recipient, uint256 publicKey, uint256 amount, bytes32[] options);LongAndLotsOfArgs( sender, recipient, publicKey, amount, options); 而不是: 1234567891011event LongAndLotsOfArgs(adress sender, adress recipient, uint256 publicKey, uint256 amount, bytes32[] options);LongAndLotsOfArgs(sender, recipient, publicKey, amount, options); 文件编码格式推荐使用utf-8 及 ASCII 编码 引入文件应该在最上方建议使用: 1234567891011import \"owned\";contract A { ...}contract B is owned { ...} 而不是: 1234567891011contract A { ...}import \"owned\";contract B is owned { ...} 函数编写规范函数的顺序在编写函数的时候,应该让大家容易找到构造函数,回退函数,官方推荐的的函数顺序是: 构造函数 回退函数 (如果有) 外部函数(external) 公有函数(public) 内部函数(internal) 私有函数(private) 同一类函数时,constant函数放在后面, 例如推荐方式为: 1234567891011121314151617181920212223242526 contract A { // 构造函数 function A() public { ... } // 回退函数 function() public { ... } // 外部函数 // ... // 带有constant 外部函数 // ... // 公有函数 // ... // 内部函数 // ... // 私有函数 // ...} 而不是下面的函数顺序: 1234567891011121314151617181920212223 contract A { // 外部函数 // ... // 公有函数 // ... // 内部函数 // ... function A() public { ... } function() public { ... } // 私有函数 // ...} 明确函数的可见性所有的函数(包括构造函数)应该在定义的时候明确函数的可见性,例如应该使用: 123function explicitlyPublic(uint val) public { doSomething();} 而不是 123function implicitlyPublic(uint val) { doSomething();} 可见性应该在修饰符前面函数的可见性应该写在自定义的函数修饰符前面,例如: 123function kill() public onlyowner { selfdestruct(owner);} 而不是 123function kill() onlyowner public { selfdestruct(owner);} 区分函数和事件为了防止函数和事件(Event)产生混淆,声明一个事件使用大写并加入前缀(可使用LOG)。对于函数, 始终以小写字母开头,构造函数除外。 1234567// 不建议event Transfer() {}function transfer() {}// 建议event LogTransfer() {}function transfer() external {} 常量常量应该使用全大写及下划线分割大词的方式,如:MAX_BLOCKS,TOKEN_NAME, CONTRACT_VERSION。 参考文献Solidity style-guide 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。如果你想和我有密切的联系,欢迎加入知识星球深入浅出区块链,我会在星球为大家解答技术问题,作为星友福利,星友可加入我创建的区块链技术群,群内已经聚集了200多位区块链技术牛人和爱好者。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Solidity","slug":"ethereum/Solidity","permalink":"https://learnblockchain.cn/categories/ethereum/Solidity/"}],"tags":[{"name":"Solidity手册","slug":"Solidity手册","permalink":"https://learnblockchain.cn/tags/Solidity手册/"}]},{"title":"美链BEC合约漏洞技术分析","slug":"bec-overflow","date":"2018-04-25T02:13:07.000Z","updated":"2019-04-05T10:29:32.298Z","comments":true,"path":"2018/04/25/bec-overflow/","link":"","permalink":"https://learnblockchain.cn/2018/04/25/bec-overflow/","excerpt":"这两天币圈链圈被美链BEC智能合约的漏洞导致代币价值几乎归零的事件刷遍朋友圈。这篇文章就来分析下BEC智能合约的漏洞","text":"这两天币圈链圈被美链BEC智能合约的漏洞导致代币价值几乎归零的事件刷遍朋友圈。这篇文章就来分析下BEC智能合约的漏洞 漏洞攻击交易我们先来还原下攻击交易,这个交易可以在这个链接查询到。我截图给大家看一下: 攻击者向两个账号转移57896044618…000.792003956564819968个BEC,相当于BEC凭空进行了一个巨大的增发,几乎导致BEC价格瞬间归零。下面我们来分析下这个攻击过程。 合约漏洞分析我们先来看看BEC智能合约的代码,BEC在合约中加入一个批量转账的函数,它的实现如下: 123456789101112function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) { uint cnt = _receivers.length; uint256 amount = uint256(cnt) * _value; require(cnt > 0 && cnt <= 20); require(_value > 0 && balances[msg.sender] >= amount); balances[msg.sender] = balances[msg.sender].sub(amount); for (uint i = 0; i < cnt; i++) { balances[_receivers[i]] = balances[_receivers[i]].add(_value); Transfer(msg.sender, _receivers[i], _value); } return true; 这个函数的作用是,调用者传入若干个地址和转账金额,在经过一些条件检查之后,对msg.sender的余额进行减操作,对每一个对每一个传入的地址进行加操作,以实现BEC的转移。问题出在 uint256 amount = uint256(cnt) * _value; 这句代码,当传入值_value过大时(接近uint256的取值范围的最大值),uint256 amount = uint256(cnt) * _value计算时会发生溢出,导致amount实际的值是一个非常小的数(此时amount不再是cnt * _value的实际值),amount很小,也使得后面对调用者余额校验可正常通过(即require(_value > 0 && balances[msg.sender] >= amount)语句通过)。 我们来结合实际攻击交易使用的参数来分析一下: batchTransfer的参数_value值为16进制的800000000000000000000...,参数_receivers数组的大小为2,相乘之后刚好可超过uint256所能表示的整数大小上限,引发溢出问题amount实际的值为0,后面的转账操作实际上msg.sender的余额减0, 而对两个账号进行了加16进制的800000000000000000000...,最终的结果是相当于增发了2 * 16进制的800000000000000000000...。 实际上对于这种整数溢出漏洞,最简单的方法是采用 SafeMath 数学计算库来避免。有趣的是BEC智能合约代码中,其实其他的都使用了SafeMath, 而关键的uint256 amount = uint256(cnt) * _value却没有使用。心痛程序员,也心痛韭菜。这句代码改为uint256 amount = _value.mul(uint256(cnt));就可以防止溢出问题 所以在做加减乘除的时候请记得一定使用:SafeMath,代码在这里 溢出补充说明溢出补充说明为小专栏订阅用户福利,小专栏的文章内介绍了什么时候会发生上溢,什么时候会发生下溢,并且给出了代码事例。大家可请前往我的小专栏阅读。 知识星球深入浅出区块链做好的区块链技术问答社区,欢迎来提问,作为星球成员福利,成员可加入区块链技术付费交流群。深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"智能合约","slug":"ethereum/智能合约","permalink":"https://learnblockchain.cn/categories/ethereum/智能合约/"}],"tags":[{"name":"智能合约","slug":"智能合约","permalink":"https://learnblockchain.cn/tags/智能合约/"}]},{"title":"Web3与智能合约交互实战","slug":"web3-html","date":"2018-04-15T13:24:16.000Z","updated":"2019-04-05T10:29:32.293Z","comments":true,"path":"2018/04/15/web3-html/","link":"","permalink":"https://learnblockchain.cn/2018/04/15/web3-html/","excerpt":"Web3与智能合约交互实战","text":"Web3与智能合约交互实战 写在前面在最初学习以太坊的时候,很多人都是自己创建以太坊节点后,使用geth与之交互。这种使用命令行交互的方法虽然让很多程序员感到兴奋(黑客帝国的既视感?),但不可能指望普通用户通过命令行使用Dapp。因此,我们需要一种友好的方式(比如一个web页面)来与智能合约交互,于是问题的答案就是web3.js。 Web3.jsWeb3.js是以太坊官方的Javascript API,可以帮助智能合约开发者使用HTTP或者IPC与本地的或者远程的以太坊节点交互。实际上就是一个库的集合,主要包括下面几个库: web3-eth用来与以太坊区块链和智能合约交互 web3-shh用来控制whisper协议与p2p通信以及广播 web3-bzz用来与swarm协议交互 web3-utils包含了一些Dapp开发有用的功能 Web3与geth通信使用的是 JSON-RPC ,这是一种轻量级的RPC(Remote Procedure Call)协议,整个通信的模型可以抽象为下图。 搭建测试链在开发初期,我们并没有必要使用真实的公链,为了开发效率,一般选择在本地搭建测试链。在本文我们选择的Ganache(在此之前使用的是testrpc,Ganache属于它的升级版),一个图形化测试软件(也有命令行版本),可以一键在本地搭建以太坊区块链测试环境,并且将区块链的状态通过图形界面显示出来,Ganache的运行界面如下图所示。 从图中可以看到Ganache会默认创建10个账户,监听地址是http://127.0.0.1:7545,可以实时看到Current Block、Gas Price、Gas Limit等信息。 创建智能合约目前以太坊官方全力支持的智能合约开发环境是Remix IDE,我们在合约编辑页面编写如下代码: 12345678910111213141516pragma solidity ^0.4.21;contract InfoContract { string fName; uint age; function setInfo(string _fName, uint _age) public { fName = _fName; age = _age; } function getInfo() public constant returns (string, uint) { return (fName, age); } } 代码很简单,就是简单的给name和age变量赋值与读取,接下来切换到 run 的 tab 下,将Environment切换成Web3 Provider,并输入我们的测试链的地址http://127.0.0.1:7545,这里对这三个选项做一简单说明: Javascript VM:简单的Javascript虚拟机环境,纯粹练习智能合约编写的时候可以选择 Injected Web3:连接到嵌入到页面的Web3,比如连接到MetaMask Web3 Provider:连接到自定义的节点,如私有的测试网络。 如果连接成功,那么在下面的Account的选项会默认选择 Ganache 创建的第一个账户地址。接下来我们点击Create就会将我们的智能合约部署到我们的测试网中。接下来 Remix 的页面不要关闭,在后面编写前端代码时还要用到合约的地址以及ABI信息。 安装Web3在这之前,先在终端创建我们的项目: 12> mkdir info> cd info 接下来使用 node.js 的包管理工具 npm 初始化项目,创建package.json 文件,其中保存了项目需要的相关依赖环境。 1> npm init 一路按回车直到项目创建完成。最后,运行下面命令安装web.js: 1> npm install web3 注意: 在实际安装过程中我发现web3在安装完成后并没有 /node_modules/web3/dist/we3.min.js 文件,这个问题在 issue#1041中有体现,但官方好像一直没解决。不过可以在这里下载所需的文件,解压后将dist文件夹的内容拷贝到 /node_modules/web3路径下。 创建 UI在项目目录下创建index.html,在这里我们将创建基础的 UI,功能包括name和age的输入框,以及一个按钮,这些将通过 jQuery 实现: 12345678910111213141516171819202122232425262728293031323334353637383940<!DOCTYPE html><html lang=\"en\"><head> <meta charset=\"UTF-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"> <title>Document</title>permalink: web3-html <link rel=\"stylesheet\" type=\"text/css\" href=\"main.css\"> <script src=\"./node_modules/web3/dist/web3.min.js\"></script></head><body> <div class=\"container\"> <h1>Info Contract</h1> <h2 id=\"info\"></h2> <label for=\"name\" class=\"col-lg-2 control-label\">Name</label> <input id=\"name\" type=\"text\"> <label for=\"name\" class=\"col-lg-2 control-label\">Age</label> <input id=\"age\" type=\"text\"> <button id=\"button\">Update Info</button> </div> <script src=\"https://code.jquery.com/jquery-3.2.1.slim.min.js\"></script> <script> // Our future code here.. </script></body></html> 接下来需要编写main.css文件设定基本的样式: 1234567891011121314151617181920212223242526272829body { background-color:#F0F0F0; padding: 2em; font-family: 'Raleway','Source Sans Pro', 'Arial';}.container { width: 50%; margin: 0 auto;}label { display:block; margin-bottom:10px;}input { padding:10px; width: 50%; margin-bottom: 1em;}button { margin: 2em 0; padding: 1em 4em; display:block;}#info { padding:1em; background-color:#fff; margin: 1em 0;} ##使用Web3与智能合约交互UI 创建好之后,在<script>标签中间编写web.js的代码与智能合约交互。首先创建web3实例,并与我们的测试环境连接: 12345678<script> if (typeof web3 !== 'undefined') { web3 = new Web3(web3.currentProvider); } else { // set the provider you want from Web3.providers web3 = new Web3(new Web3.providers.HttpProvider(\"http://localhost:7545\")); }</script> 这段代码是web3.js Github提供的样例,意思是如果web3已经被定义,那么就可以直接当作我们的 provider 使用。如果没有定义,则我们手动指定 provider。 这里可能会存在疑问:为什么 web3 会被事先定义呢?实际上,如果你使用类似 MetaMask(一个 Chrome 上的插件,迷你型以太坊钱包)这样的软件,provider 就会被自动植入。 在上面代码的基础上,接下来设置默认的以太坊账户: 1web3.eth.defaultAccount = web3.eth.accounts[0]; 在上文中我们使用 Ganache 已经创建了 10 个账户了,这里我们选择第一个账户当作默认账户。 接下来需要让我们的web3知道我们的合约是什么样的,这里需要用到合约的 ABI(Application Binary Interface)。ABI可以使我们调用合约的函数,并且从合约中获取数据。 在上文中我们已经在 Remix 中创建了我们的合约,这时重新回到 Remix,在 Compile 的 tab 下我们点击Details 出现的页面中我们可以拷贝合约的ABI,如下图所示。将其复制到代码中: 1var infoContract = web3.eth.contract(PASTE ABI HERE!); 接下来转到 run 的tab,拷贝合约的地址,将其复制到下面的代码中: 1var info = InfoContract.at('PASTE CONTRACT ADDRESS HERE'); 完成这些我们就可以调用合约中的函数了,下面我们使用 jQuery 与我们的合约进行交互: 12345678910111213info.getInfo(function(error, result){ if(!error) { $(\"#info\").html(result[0]+' ('+result[1]+' years old)'); console.log(result); } else console.error(error);});$(\"#button\").click(function() { info.setInfo($(\"#name\").val(), $(\"#age\").val());}); 以上的代码就简单地实现了对合约中两个函数的调用,分别读取和显示name和age变量。 到此我们就完成了全部的代码,完整代码可以在 InfoContract 中找到。在浏览器中打开index.html测试效果如下图(输入名字和年龄后刷新)。 本文中点击”Updata Info”按钮之后,虽然调用智能合约成功,但是当前的界面并没有得到更新,下一篇文章会介绍Web3监听合约事件更新界面。 本文的作者是盖盖,他的微信公众号: chainlab 如果你想学习以太坊DApp开发,这门视频课程以太坊DAPP开发实战是你不错的选择。 参考文献 Interacting with a Smart Contract through Web3.js (Tutorial)) 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。如果你想和我有密切的联系,欢迎加入知识星球深入浅出区块链,我会在星球为大家解答技术问题,作为星友福利,星友可加入我创建的区块链技术群,群内已经聚集了300多位区块链技术牛人和爱好者。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"},{"name":"智能合约","slug":"智能合约","permalink":"https://learnblockchain.cn/tags/智能合约/"},{"name":"Web3","slug":"Web3","permalink":"https://learnblockchain.cn/tags/Web3/"}]},{"title":"智能合约语言 Solidity 教程系列10 - 完全理解函数修改器","slug":"solidity-modify","date":"2018-04-09T12:35:47.000Z","updated":"2019-04-05T10:29:32.287Z","comments":true,"path":"2018/04/09/solidity-modify/","link":"","permalink":"https://learnblockchain.cn/2018/04/09/solidity-modify/","excerpt":"这是Solidity教程系列文章第10篇,带大家完全理解Solidity的函数修改器。Solidity系列完整的文章列表请查看分类-Solidity。","text":"这是Solidity教程系列文章第10篇,带大家完全理解Solidity的函数修改器。Solidity系列完整的文章列表请查看分类-Solidity。 写在前面Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 欢迎订阅区块链技术专栏阅读更全面的分析文章。 函数修改器(Function Modifiers)函数修改器(Modifiers)可以用来改变一个函数的行为。比如用于在函数执行前检查某种前置条件。 如果熟悉Python的同学,会发现函数修改器的作用和Python的装饰器很相似。 修改器是一种可被继承合约属性,同时还可被继承的合约重写(override)。下面我们来看一段示例代码: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546pragma solidity ^0.4.11;contract owned { function owned() public { owner = msg.sender; } address owner; // 定义了一个函数修改器,可被继承 // 修饰时,函数体被插入到 “_;” 处 // 不符合条件时,将抛出异常 modifier onlyOwner { require(msg.sender == owner); _; }}contract mortal is owned { // 使用继承的`onlyOwner` function close() public onlyOwner { selfdestruct(owner); }}contract priced { // 函数修改器可接收参数 modifier costs(uint price) { if (msg.value >= price) { _; } }}contract Register is priced, owned { mapping (address => bool) registeredAddresses; uint price; function Register(uint initialPrice) public { price = initialPrice; } // 需要提供payable 以接受以太 function register() public payable costs(price) { registeredAddresses[msg.sender] = true; } function changePrice(uint _price) public onlyOwner { price = _price; }} 上面onlyOwner就是定义的一个函数修改器,当用这个修改器区修饰一个函数时,则函数必须满足onlyOwner的条件才能运行,这里的条件是:必须是合约的创建这才能调用函数,否则抛出异常。我们在实现一个可管理、增发、兑换、冻结等高级功能的代币文章中就使用了这个函数修改器。 多个修改器如果同一个函数有多个修改器,他们之间以空格隔开,修饰器会依次检查执行。 在修改器中或函数内的显式的return语句,仅仅跳出当前的修改器或函数。返回的变量会被赋值,但执行流会在前一个修改器后面定义的”_”后继续执行, 如: 12345678910111213141516contract Mutex { bool locked; modifier noReentrancy() { require(!locked); locked = true; _; locked = false; } // 防止递归调用 // return 7 之后,locked = false 依然会执行 function f() public noReentrancy returns (uint) { require(msg.sender.call()); return 7; }} 修改器的参数可以是任意表达式。在此上下文中,所有的函数中引入的符号,在修改器中均可见。但修改器中引入的符号在函数中不可见,因为它们有可能被重写。 深入理解修改器的执行次序再来看一个复杂一点的例子,来深入理解修改器: 1234567891011121314151617181920212223242526272829303132333435pragma solidity ^0.4.11;contract modifysample { uint a = 10; modifier mf1 (uint b) { uint c = b; _; c = a; a = 11; } modifier mf2 () { uint c = a; _; } modifier mf3() { a = 12; return ; _; a = 13; } function test1() mf1(a) mf2 mf3 public { a = 1; } function test2() public constant returns (uint) { return a; } } 上面的智能合约运行test1()之后,状态变量a的值是多少, 是1, 11, 12,还是13呢?答案是 11, 大家可以运行下test2获取下a值。 我们来分析一下 test1, 它扩展之后是这样的: 12345678uint c = b; uint c = a; a = 12; return ; _; a = 13;c = a;a = 11; 这个时候就一目了然了,最后a 为11, 注意第5及第6行是不是执行的。 参考文献官方文档-Function Modifiers 如果你想和认识我,和我建立联系,欢迎加入知识星球深入浅出区块链,我会在星球为大家解答技术问题,作为星友福利,星友可加入我创建的区块链技术群,群内已经聚集了200多位区块链技术爱好者。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Solidity","slug":"ethereum/Solidity","permalink":"https://learnblockchain.cn/categories/ethereum/Solidity/"}],"tags":[{"name":"Solidity手册","slug":"Solidity手册","permalink":"https://learnblockchain.cn/tags/Solidity手册/"}]},{"title":"智能合约语言 Solidity 教程系列9 - 错误处理","slug":"solidity-errorhandler","date":"2018-04-07T12:35:47.000Z","updated":"2019-04-05T10:29:32.295Z","comments":true,"path":"2018/04/07/solidity-errorhandler/","link":"","permalink":"https://learnblockchain.cn/2018/04/07/solidity-errorhandler/","excerpt":"这是Solidity教程系列文章第9篇介绍Solidity 错误处理。Solidity系列完整的文章列表请查看分类-Solidity。","text":"这是Solidity教程系列文章第9篇介绍Solidity 错误处理。Solidity系列完整的文章列表请查看分类-Solidity。 写在前面Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 欢迎订阅区块链技术专栏阅读更全面的分析文章。 什么是错误处理错误处理是指在程序发生错误时的处理方式,Solidity处理错误和我们常见的语言不一样,Solidity是通过回退状态的方式来处理错误。发生异常时会撤消当前调用(及其所有子调用)所改变的状态,同时给调用者返回一个错误标识。注意捕捉异常是不可能的,因此没有try … catch…。 为什么Solidity处理错误要这样设计呢?我们可以把区块链理解为是全球共享的分布式事务性数据库。全球共享意味着参与这个网络的每一个人都可以读写其中的记录。如果想修改这个数据库中的内容,就必须创建一个事务,事务意味着要做的修改(假如我们想同时修改两个值)只能被完全的应用或者一点都没有进行。学习过数据库的同学,应该理解事务的含义,如果你对事务一词不是很理解,建议你搜索一下“数据库事务“。Solidity错误处理就是要保证每次调用都是事务性的。 如何处理Solidity提供了两个函数assert和require来进行条件检查,如果条件不满足则抛出异常。assert函数通常用来检查(测试)内部错误,而require函数来检查输入变量或合同状态变量是否满足条件以及验证调用外部合约返回值。另外,如果我们正确使用assert,有一个Solidity分析工具就可以帮我们分析出智能合约中的错误,帮助我们发现合约中有逻辑错误的bug。 除了可以两个函数assert和require来进行条件检查,另外还有两种方式来触发异常: revert函数可以用来标记错误并回退当前调用 使用throw关键字抛出异常(从0.4.13版本,throw关键字已被弃用,将来会被淘汰。) 当子调用中发生异常时,异常会自动向上“冒泡”。 不过也有一些例外:send,和底层的函数调用call, delegatecall,callcode,当发生异常时,这些函数返回false。 注意:在一个不存在的地址上调用底层的函数call,delegatecall,callcode 也会返回成功,所以我们在进行调用时,应该总是优先进行函数存在性检查。 在下面通过一个示例来说明如何使用require来检查输入条件,以及assert用于内部错误检查: 1234567891011pragma solidity ^0.4.0;contract Sharer { function sendHalf(address addr) public payable returns (uint balance) { require(msg.value % 2 == 0); // 仅允许偶数 uint balanceBeforeTransfer = this.balance; addr.transfer(msg.value / 2); // 如果失败,会抛出异常,下面的代码就不是执行 assert(this.balance == balanceBeforeTransfer - msg.value / 2); return this.balance; }} 我们实际运行下,看看异常是如何发生的: 首先打开Remix,贴入代码,点击创建合约。如下图: 运行测试1:附加1wei (奇数)去调用sendHalf,这时会发生异常,如下图: 运行测试2:附加2wei 去调用sendHalf,运行正常。 运行测试3:附加2wei以及sendHalf参数为当前合约本身,在转账是发生异常,因为合约无法接收转账,错误提示上图类似。 assert类型异常在下述场景中自动产生assert类型的异常: 如果越界,或负的序号值访问数组,如i >= x.length 或 i < 0时访问x[i] 如果序号越界,或负的序号值时访问一个定长的bytesN。 被除数为0, 如5/0 或 23 % 0。 对一个二进制移动一个负的值。如:5<<i; i为-1时。 整数进行可以显式转换为枚举时,如果将过大值,负值转为枚举类型则抛出异常 如果调用未初始化内部函数类型的变量。 如果调用assert的参数为false require类型异常在下述场景中自动产生require类型的异常: 调用throw 如果调用require的参数为false 如果你通过消息调用一个函数,但在调用的过程中,并没有正确结束(gas不足,没有匹配到对应的函数,或被调用的函数出现异常)。底层操作如call,send,delegatecall或callcode除外,它们不会抛出异常,但它们会通过返回false来表示失败。 如果在使用new创建一个新合约时出现第3条的原因没有正常完成。 如果调用外部函数调用时,被调用的对象不包含代码。 如果合约没有payable修饰符的public的函数在接收以太币时(包括构造函数,和回退函数)。 如果合约通过一个public的getter函数(public getter funciton)接收以太币。 如果.transfer()执行失败 当发生require类型的异常时,Solidity会执行一个回退操作(指令0xfd)。当发生assert类型的异常时,Solidity会执行一个无效操作(指令0xfe)。在上述的两种情况下,EVM都会撤回所有的状态改变。是因为期望的结果没有发生,就没法继续安全执行。必须保证交易的原子性(一致性,要么全部执行,要么一点改变都没有,不能只改变一部分),所以需要撤销所有操作,让整个交易没有任何影响。 注意assert类型的异常会消耗掉所有的gas, 而require从大都会版本(Metropolis, 即目前主网所在的版本)起不会消耗gas。 参考文献 Solidity 错误处理 欢迎来我的知识星球深入浅出区块链讨论区块链技术,同时我也会为大家提供区块链技术解答,作为星友福利,星友可加入区块链技术付费交流群。深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Solidity","slug":"ethereum/Solidity","permalink":"https://learnblockchain.cn/categories/ethereum/Solidity/"}],"tags":[{"name":"Solidity手册","slug":"Solidity手册","permalink":"https://learnblockchain.cn/tags/Solidity手册/"}]},{"title":"剖析非同质化代币ERC721-全面解析ERC721标准","slug":"token-erc721","date":"2018-03-23T13:54:50.000Z","updated":"2019-04-05T10:29:32.273Z","comments":true,"path":"2018/03/23/token-erc721/","link":"","permalink":"https://learnblockchain.cn/2018/03/23/token-erc721/","excerpt":"什么是ERC-721?现在我们看到的各种加密猫猫狗狗都是基于ERC-721创造出来的,每只都是一个独一无二的ERC-721代币,不过ERC-721在区块链世界远不止猫猫狗狗,它更大的想象空间在于将物理世界的资产映射到区块链上。本文就来剖析下什么是ERC721.","text":"什么是ERC-721?现在我们看到的各种加密猫猫狗狗都是基于ERC-721创造出来的,每只都是一个独一无二的ERC-721代币,不过ERC-721在区块链世界远不止猫猫狗狗,它更大的想象空间在于将物理世界的资产映射到区块链上。本文就来剖析下什么是ERC721. ERC721是什么在创建代币一篇,我们讲到过ERC20代币,和ERC20一样,ERC721同样是一个代币标准,ERC721官方简要解释是Non-Fungible Tokens,简写为NFTs,多翻译为非同质代币。 ERC721 是由Dieter Shirley 在2017年9月提出。Dieter Shirley 正是谜恋猫CryptoKitties背后的公司Axiom Zen的技术总监。因此谜恋猫也是第一个实现了ERC721 标准的去中心化应用。ERC721号提议已经被以太坊作为标准接受,但该标准仍处于草稿阶段。本文介绍的ERC721标准基于最新(2018/03/23官方提议。 那怎么理解非同质代币呢? 非同质代表独一无二,谜恋猫为例,每只猫都被赋予拥有基因,是独一无二的(一只猫就是一个NFTs),猫之间是不能置换的。这种独特性使得某些稀有猫具有收藏价值,也因此受到追捧。 ERC20代币是可置换的,且可细分为N份(1 = 10 * 0.1), 而ERC721的Token最小的单位为1,无法再分割。 如果同一个集合的两个物品具有不同的特征,这两个物品是非同质的,而同质是某个部分或数量可以被另一个同等部分或数量所代替。 非同质性其实广泛存在于我们的生活中,如图书馆的每一本,宠物商店的每一只宠物,歌手所演唱的歌曲,花店里不同的花等等,因此ERC721合约必定有广泛的应用场景。通过这样一个标准,也可建立跨功能的NFTs管理和销售平台(就像有支持ERC20的交易所和钱包一样),使生态更加强大。 ERC721标准ERC721最为一个合约标准,提供了在实现ERC721代币时必须要遵守的协议,要求每个ERC721标准合约需要实现ERC721及ERC165接口,接口定义如下: 1234567891011121314151617181920pragma solidity ^0.4.20;interface ERC721 /* is ERC165 */ { event Transfer(address indexed _from, address indexed _to, uint256 _tokenId); event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId); event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); function balanceOf(address _owner) external view returns (uint256); function ownerOf(uint256 _tokenId) external view returns (address); function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable; function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; function transferFrom(address _from, address _to, uint256 _tokenId) external payable; function approve(address _approved, uint256 _tokenId) external payable; function setApprovalForAll(address _operator, bool _approved) external; function getApproved(uint256 _tokenId) external view returns (address); function isApprovedForAll(address _owner, address _operator) external view returns (bool);} 接口说明: balanceOf(): 返回由_owner 持有的NFTs的数量。 ownerOf(): 返回tokenId代币持有者的地址。 approve(): 授予地址_to具有_tokenId的控制权,方法成功后需触发Approval 事件。 setApprovalForAll(): 授予地址_operator具有所有NFTs的控制权,成功后需触发ApprovalForAll事件。 getApproved()、isApprovedForAll(): 用来查询授权。 safeTransferFrom(): 转移NFT所有权,一次成功的转移操作必须发起 Transer 事件。函数的实现需要做一下几种检查: 调用者msg.sender应该是当前tokenId的所有者或被授权的地址 _from 必须是 _tokenId的所有者 _tokenId 应该是当前合约正在监测的NFTs 中的任何一个 _to 地址不应该为 0 如果_to 是一个合约应该调用其onERC721Received方法, 并且检查其返回值,如果返回值不为bytes4(keccak256("onERC721Received(address,uint256,bytes)"))抛出异常。一个可接收NFT的合约必须实现ERC721TokenReceiver接口:1234interface ERC721TokenReceiver { /// @return `bytes4(keccak256(\"onERC721Received(address,uint256,bytes)\"))` function onERC721Received(address _from, uint256 _tokenId, bytes data) external returns(bytes4);} transferFrom(): 用来转移NFTs, 方法成功后需触发Transfer事件。调用者自己确认_to地址能正常接收NFT,否则将丢失此NFT。此函数实现时需要检查上面条件的前4条。 ERC165 标准ERC721标准同时要求必须符合ERC165标准 ,其接口如下:123interface ERC165 { function supportsInterface(bytes4 interfaceID) external view returns (bool);} ERC165同样是一个合约标准,这个标准要求合约提供其实现了哪些接口,这样再与合约进行交互的时候可以先调用此接口进行查询。interfaceID为函数选择器,计算方式有两种,如:bytes4(keccak256('supportsInterface(bytes4)'));或ERC165.supportsInterface.selector,多个函数的接口ID为函数选择器的异或值。关于ERC165,这里不深入介绍,有兴趣的同学可以阅读官方提案。 可选实现接口:ERC721MetadataERC721Metadata 接口用于提供合约的元数据:name , symbol 及 URI(NFT所对应的资源)。其接口定义如下:12345interface ERC721Metadata /* is ERC721 */ { function name() external pure returns (string _name); function symbol() external pure returns (string _symbol); function tokenURI(uint256 _tokenId) external view returns (string);} 接口说明: name(): 返回合约名字,尽管是可选,但强烈建议实现,即便是返回空字符串。 symbol(): 返回合约代币符号,尽管是可选,但强烈建议实现,即便是返回空字符串。 tokenURI(): 返回_tokenId所对应的外部资源文件的URI(通常是IPFS或HTTP(S)路径)。外部资源文件需要包含名字、描述、图片,其格式的要求如下:12345678910111213141516171819{ \"title\": \"Asset Metadata\",permalink: token-erc721 \"type\": \"object\", \"properties\": { \"name\": { \"type\": \"string\", \"description\": \"Identifies the asset to which this NFT represents\", }, \"description\": { \"type\": \"string\", \"description\": \"Describes the asset to which this NFT represents\", }, \"image\": { \"type\": \"string\", \"description\": \"A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive.\", } }} tokenURI通常是被web3调用,以便在应用层做相应的查询和展示。 可选实现接口:ERC721EnumerableERC721Enumerable的主要目的是提高合约中NTF的可访问性,其接口定义如下:12345interface ERC721Enumerable /* is ERC721 */ { function totalSupply() external view returns (uint256); function tokenByIndex(uint256 _index) external view returns (uint256); function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);} 接口说明: totalSupply(): 返回NFT总量 tokenByIndex(): 通过索引返回对应的tokenId。 tokenOfOwnerByIndex(): 所有者可以一次拥有多个的NFT, 此函数返回_owner拥有的NFT列表中对应索引的tokenId。 补充说明NTF IDsNTF ID,即tokenId,在合约中用唯一的uint265进行标识,每个NFT的ID在智能合约的生命周期内不允许改变。推荐的实现方式有: 从0开始,每新加一个NFT,NTF ID加1 使用sha3后uuid 转换为 NTF ID 与ERC-20的兼容性ERC721标准尽可能遵循 ERC-20 的语义,但由于同质代币与非同质代币之间的根本差异,并不能完全兼容ERC-20。 交易、挖矿、销毁在实现transter相关接口时除了满足上面的的条件外,我们可以根据需要添加自己的逻辑,如加入黑名单等。同时挖矿、销毁尽管不是标准的一部分,我们可以根据需要实现。 参考实现参考实现为订阅用户专有福利,请订阅我的小专栏:区块链技术查看。 参考文献 EIPS-165 EIPS-721 欢迎来我的知识星球深入浅出区块链讨论区块链,作为星友福利,星友可加入区块链技术付费交流群。深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"智能合约","slug":"ethereum/智能合约","permalink":"https://learnblockchain.cn/categories/ethereum/智能合约/"}],"tags":[{"name":"智能合约","slug":"智能合约","permalink":"https://learnblockchain.cn/tags/智能合约/"},{"name":"Token","slug":"Token","permalink":"https://learnblockchain.cn/tags/Token/"},{"name":"ERC721","slug":"ERC721","permalink":"https://learnblockchain.cn/tags/ERC721/"}]},{"title":"如何搭建以太坊私有链","slug":"create_private_blockchain","date":"2018-03-18T12:05:59.000Z","updated":"2019-04-05T10:29:31.081Z","comments":true,"path":"2018/03/18/create_private_blockchain/","link":"","permalink":"https://learnblockchain.cn/2018/03/18/create_private_blockchain/","excerpt":"在开发以太坊时,很多时候需要搭建一条以太坊私有链,通过本文一起看看如何在Mac上进行搭建。","text":"在开发以太坊时,很多时候需要搭建一条以太坊私有链,通过本文一起看看如何在Mac上进行搭建。 写在前面阅读本文前,你应该对以太坊语言有所了解,如果你还不了解,建议你先看以太坊是什么 go-ethereum客户端安装Go-ethereum客户端通常被称为Geth,它是个命令行界面,执行在Go上实现的完整以太坊节点。Geth得益于Go语言的多平台特性,支持在多个平台上使用(比如Windows、Linux、Mac)。Geth是以太坊协议的具体落地实现,通过Geth,你可以实现以太坊的各种功能,如账户的新建编辑删除,开启挖矿,ether币的转移,智能合约的部署和执行等等。所以,我们选择geth工具来进行开发。由于本人是mac,所以优先使用mac进行开发啦。mac中geth安装如下: 12brew tap ethereum/ethereumbrew install ethereum 检查是否安装成功 1geth --help 如果输出一些帮助提示命令,则说明安装成功。其他平台可参考Geth 安装 搭建私有链以太坊支持自定义创世区块,要运行私有链,我们就需要定义自己的创世区块,创世区块信息写在一个json格式的配置文件中。首先将下面的内容保存到一个json文件中,例如genesis.json。json文件内容如下: 1234567891011121314151617{ \"config\": { \"chainId\": 10, \"homesteadBlock\": 0, \"eip155Block\": 0, \"eip158Block\": 0 }, \"alloc\" : {}, \"coinbase\" : \"0x0000000000000000000000000000000000000000\", \"difficulty\" : \"0x20000\", \"extraData\" : \"\", \"gasLimit\" : \"0x2fefd8\", \"nonce\" : \"0x0000000000000042\", \"mixhash\" : \"0x0000000000000000000000000000000000000000000000000000000000000000\", \"parentHash\" : \"0x0000000000000000000000000000000000000000000000000000000000000000\", \"timestamp\" : \"0x00\"} 初始化:写入创世区块准备好创世区块json配置文件后,需要初始化区块链,将上面的创世区块信息写入到区块链中。首先要新建一个目录data0用来存放区块链数据(其实,这个目录data0就相当于一个根节点。当我们基于genesis.json生成根节点后,其他人就可以来连接此根节点,从而能进行交易)。data0目录结构如图所示: 接下来进入privatechain目录中,执行初始化命令: 12cd privatechaingeth --datadir data0 init genesis.json 上面的命令的主体是 geth init,表示初始化区块链,命令可以带有选项和参数,其中–datadir选项后面跟一个目录名,这里为 data0,表示指定数据存放目录为 data0, genesis.json是init命令的参数。 运行上面的命令,会读取genesis.json文件,根据其中的内容,将创世区块写入到区块链中。如果看到log信息中含有Successfully wrote genesis state字样,说明初始化成功。 初始化成功后的目录如下:其中geth/chaindata中存放的是区块数据,keystore中存放的是账户数据。 启动私有链节点初始化完成后,就有了一条自己的私有链,之后就可以启动自己的私有链节点并做一些操作,在终端中输入以下命令即可启动节点: 1geth --datadir data0 --networkid 1108 console 上面命令的主体是geth console,表示启动节点并进入交互式控制台,–datadir选项指定使用data0作为数据目录,–networkid选项后面跟一个数字,这里是1108,表示指定这个私有链的网络id为1108。网络id在连接到其他节点的时候会用到,以太坊公网的网络id是1,为了不与公有链网络冲突,运行私有链节点的时候要指定自己的网络id(上面命令可能会运行失败,我直接重启mac,再进入到privateChain目录中,简单粗暴)。 运行上面的命令后,就启动了区块链节点并进入了Javascript Console:这是一个交互式的Javascript执行环境,在这里面可以执行Javascript代码,其中>是命令提示符。在这个环境里也内置了一些用来操作以太坊的Javascript对象,可以直接使用这些对象。这些对象主要包括: eth:包含一些跟操作区块链相关的方法net:包含以下查看p2p网络状态的方法admin:包含一些与管理节点相关的方法miner:包含启动&停止挖矿的一些方法personal:主要包含一些管理账户的方法txpool:包含一些查看交易内存池的方法web3:包含了以上对象,还包含一些单位换算的方法 玩转Javascript Console进入以太坊Javascript Console后,就可以使用里面的内置对象做一些操作,这些内置对象提供的功能很丰富,比如查看区块和交易、创建账户、挖矿、发送交易、部署智能合约等。接下来介绍几个常用功能,下面的操作中,前面带>的表示在Javascript Console中执行的命令。 创建账户前面只是搭建了私有链,并没有自己的账户,可以在js console中输入eth.accounts来验证: 12> eth.accounts[] 此时没有账户,接下来使用personal对象来创建一个账户: 1234> personal.newAccount()> Passphrase:> Repeat passphrase:"0x4a3b0216e1644c1bbabda527a6da7fc5d178b58f" Passphrase其实就是密码的意思,输入两次密码后,就创建了一个账户。再次执行命令: 1234> personal.newAccount()> Passphrase:> Repeat passphrase:"0x46b24d04105551498587e3c6ce2c3341d5988938" 这时候再去看账户,就有两个了。 12> eth.accounts["0x4a3b0216e1644c1bbabda527a6da7fc5d178b58f", "0x46b24d04105551498587e3c6ce2c3341d5988938"] 账户默认会保存在数据目录的keystore文件夹中。查看目录结构,发现data0/keystore中多了两个文件,这两个文件就对应刚才创建的两个账户,这是json格式的文本文件,可以打开查看,里面存的是私钥经过密码加密后的信息。 json文件中信息格式如下: 123456789101112131415161718192021{ "address": "4a3b0216e1644c1bbabda527a6da7fc5d178b58f", "crypto": { "cipher": "aes-128-ctr", "ciphertext": "238d6d48126b762c8f13e84622b1bbb7713f7244c2f24555c99b76396fae8355", "cipherparams": { "iv": "d0f5a3d3e6c1eeec77bf631bc938725d" }, "kdf": "scrypt", "kdfparams": { "dklen": 32, "n": 262144, "p": 1, "r": 8, "salt": "70dc72c4eb63bea50f7637d9ff85bb53f6ca8ace17f4245feae9c0bc9abaad82" }, "mac": "bd7fc0c937c39f1cbbf1ca654c33b53d7f9c644c6dacfeefe1641d2f3decea04" }, "id": "57803d82-0cd4-4a78-9c29-9f9252fdcf60", "version": 3} 查看账户余额eth对象提供了查看账户余额的方法: 1234> eth.getBalance(eth.accounts[0])0> eth.getBalance(eth.accounts[1])0 目前两个账户的以太币余额都是0,要使账户有余额,可以从其他账户转账过来,或者通过挖矿来获得以太币奖励。 启动&停止挖矿通过miner.start()来启动挖矿: 1> miner.start(10) 其中start的参数表示挖矿使用的线程数。第一次启动挖矿会先生成挖矿所需的DAG文件,这个过程有点慢,等进度达到100%后,就会开始挖矿,此时屏幕会被挖矿信息刷屏。 如果想停止挖矿,并且进度已经达到100%之后,可以在js console中输入 1miner.stop(): 注意:输入的字符会被挖矿刷屏信息冲掉,没有关系,只要输入完整的miner.stop()之后回车,即可停止挖矿。 挖到一个区块会奖励5个以太币,挖矿所得的奖励会进入矿工的账户,这个账户叫做coinbase,默认情况下coinbase是本地账户中的第一个账户: 12> eth.coinbase"0x4a3b0216e1644c1bbabda527a6da7fc5d178b58f" 现在的coinbase是账户0,要想使挖矿奖励进入其他账户,通过miner.setEtherbase()将其他账户设置成coinbase即可: 1234> miner.setEtherbase(eth.accounts[1])true> eth.coinbase"0x46b24d04105551498587e3c6ce2c3341d5988938" 挖到区块以后,账户0里面应该就有余额了: 12> eth.getBalance(eth.accounts[0])2.31e+21 getBalance()返回值的单位是wei,wei是以太币的最小单位,1个以太币=10的18次方个wei。要查看有多少个以太币,可以用web3.fromWei()将返回值换算成以太币: 12> web3.fromWei(eth.getBalance(eth.accounts[0]),'ether')2310 发送交易截止目前,账户一的余额还是0: 12> eth.getBalance(eth.accounts[1])0 可以通过发送一笔交易,从账户0转移10个以太币到账户1: 12345678> amount = web3.toWei(10,'ether')"10000000000000000000"> eth.sendTransaction({from:eth.accounts[0],to:eth.accounts[1],value:amount})Error: authentication needed: password or unlock at web3.js:3143:20 at web3.js:6347:15 at web3.js:5081:36 at <anonymous>:1:1 这里报错了,原因是账户每隔一段时间就会被锁住,要发送交易,必须先解锁账户,由于我们要从账户0发送交易,所以要解锁账户0: 1234> personal.unlockAccount(eth.accounts[0])Unlock account 0x4a3b0216e1644c1bbabda527a6da7fc5d178b58fPassphrase: true 输入创建账户时设置的密码,就可以成功解锁账户。然后再发送交易: 12345> amount = web3.toWei(10,'ether')"10000000000000000000"> eth.sendTransaction({from:eth.accounts[0],to:eth.accounts[1],value:amount})INFO [03-07|11:13:11] Submitted transaction fullhash=0x1b21bba16dd79b659c83594b0c41de42debb2738b447f6b24e133d51149ae2a6 recipient=0x46B24d04105551498587e3C6CE2c3341d5988938"0x1b21bba16dd79b659c83594b0c41de42debb2738b447f6b24e133d51149ae2a6" 我们去查看账户1中的余额: 12> eth.getBalance(eth.accounts[1])0 发现还没转过去,此时交易已经提交到区块链,但还未被处理,这可以通过查看txpool来验证: 12345> txpool.status{ pending: 1, queued: 0} 其中有一条pending的交易,pending表示已提交但还未被处理的交易。 要使交易被处理,必须要挖矿。这里我们启动挖矿,然后等待挖到一个区块之后就停止挖矿: 1> miner.start(1);admin.sleepBlocks(1);miner.stop(); 当miner.stop()返回true后,txpool中pending的交易数量应该为0了,说明交易已经被处理了,而账户1应该收到币了: 12> web3.fromWei(eth.getBalance(eth.accounts[1]),'ether')10 查看交易和区块eth对象封装了查看交易和区块信息的方法。 查看当前区块总数: 12> eth.blockNumber463 通过区块号查看区块: 1234567891011121314151617181920212223> eth.getBlock(66){ difficulty: 135266, extraData: "0xd783010802846765746886676f312e31308664617277696e", gasLimit: 3350537, gasUsed: 0, hash: "0x265dfcc0649bf6240812256b2b9b4e3ae48d51fd8e43e25329ac111556eacdc8", logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", miner: "0x4a3b0216e1644c1bbabda527a6da7fc5d178b58f", mixHash: "0xaf755722f62cac9b483d3437dbc795f2d3a02e28ec03d39d8ecbb6012906263c", nonce: "0x3cd80f6ec5c2f3e9", number: 66, parentHash: "0x099776a52223b892d13266bb3aec3cc04c455dc797185f0b3300d39f9fc0a8ec", receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", size: 535, stateRoot: "0x0c9feec5a201c8c98618331aecbfd2d4d93da1c6064abd0c41ae649fc08d8d06", timestamp: 1520391527, totalDifficulty: 8919666, transactions: [], transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", uncles: []} 通过交易hash查看交易: 1234567891011121314151617> eth.getTransaction("0x1b21bba16dd79b659c83594b0c41de42debb2738b447f6b24e133d51149ae2a6"){ blockHash: "0x1cb368a27cc23c786ff5cdf7cd4351d48f4c8e8aea2e084a5e9d7c480449c79a", blockNumber: 463, from: "0x4a3b0216e1644c1bbabda527a6da7fc5d178b58f", gas: 90000, gasPrice: 18000000000, hash: "0x1b21bba16dd79b659c83594b0c41de42debb2738b447f6b24e133d51149ae2a6", input: "0x", nonce: 0, r: "0x31d22686e0d408a16497becf6d47fbfdffe6692d91727e5b7ed3d73ede9e66ea", s: "0x7ff7c14a20991e2dfdb813c2237b08a5611c8c8cb3c2dcb03a55ed282ce4d9c3", to: "0x46b24d04105551498587e3c6ce2c3341d5988938", transactionIndex: 0, v: "0x38", value: 10000000000000000000} 如果你想学习以太坊DApp开发,这门视频课程以太坊DAPP开发实战是你不错的选择。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"私有链","slug":"ethereum/私有链","permalink":"https://learnblockchain.cn/categories/ethereum/私有链/"}],"tags":[{"name":"以太坊","slug":"以太坊","permalink":"https://learnblockchain.cn/tags/以太坊/"},{"name":"私有链","slug":"私有链","permalink":"https://learnblockchain.cn/tags/私有链/"}]},{"title":"如何编写一个可升级的智能合约","slug":"contract-upgrade","date":"2018-03-15T08:14:01.000Z","updated":"2019-04-05T10:29:32.243Z","comments":true,"path":"2018/03/15/contract-upgrade/","link":"","permalink":"https://learnblockchain.cn/2018/03/15/contract-upgrade/","excerpt":"区块链信任基础的数据不可修改的特性,让它传统应用程序有一个很大的不同的地方是一经发布于区块链上就无法修改(不能直接在原有的合约上直接修改再重新发布)。","text":"区块链信任基础的数据不可修改的特性,让它传统应用程序有一个很大的不同的地方是一经发布于区块链上就无法修改(不能直接在原有的合约上直接修改再重新发布)。 写在前面阅读本文前,你应该对以太坊、智能合约及Solidity语言有所了解,如果你还不了解,建议你先看以太坊是什么 当智能合约出现bug一方面正式由于智能合约的不可修改的特性,因为只要规则确定之后,没人能够修改它,大家才能够信任它。但另一方面,如果规则的实现有Bug, 可能会造成代币被盗,或是调用消耗大量的gas。这时就需要我们去修复错误。 我们知道一个智能合约包含两部分: 代码逻辑和数据,而代码逻辑又是最容易出问题的部分, 如在实现如下合约时,由于手抖在写addTen()时,10写成了11。 12345678910111213pragma solidity ^0.4.18;contract MyContract { mapping (address => uint256) public balanceOf; function setBlance(address _address,uint256 v) public { balanceOf[_address] = v; } function addTen(address addr) public returns (uint){ return balanceOf[addr] + 11; }} 假如我们在部署之后发现了这个问题,想要修复这个bug的话,只好重新部署合约,可是这时会有一个尴尬的问题,原来的合约已经有很多人使用,如果部署新的合约,老合约的数据将会丢失。 数据合约及控制合约那么如何解决上面的问题了,一个解决方案是分离合约中的数据,使用一个单独的合约来存储数据(下文称数据合约),使用一个单独的合约写业务逻辑(下文称控制合约)。我们来看看代码如何实现。 12345678910111213141516171819202122pragma solidity ^0.4.18;contract DataContract { mapping (address => uint256) public balanceOf; function setBlance(address _address,uint256 v) public { balanceOf[_address] = v; }}contract ControlContract { DataContract dataContract; function ControlContract(address _dataContractAddr) public { dataContract = DataContract(_dataContractAddr); } function addTen(address addr) public returns (uint){ return dataContract.balanceOf(addr) + 11; }} 现在我们有两个合约DataContract 专门用来存数据,ControlContract用来处理逻辑,并利用DataContract来读写数据。通过这样的设计,可以在更新控制合约后保持数据合约不变,这样就不会丢失数据,也不用迁移数据。 读写控制通过DataContract我们可以单独更新合约逻辑,不过你也许发现了一个新的问题,DataContract的数据不仅仅可以被ControlContract读写,还可以被其他的合约读写,因此需要对DataContract添加读写控制。我们给DataContract添加一个mapping, 用来控制哪些地址可以访问数据,同时添加了修饰器及设置访问的方法,代码如下: 1234567891011121314151617181920212223242526272829pragma solidity ^0.4.18;contract DataContract { mapping (address => uint256) public balanceOf; mapping (address => bool) accessAllowed; function DataContract() public { accessAllowed[msg.sender] = true; } function setBlance(address _address,uint256 v) public { balanceOf[_address] = v; } modifier platform() { require(accessAllowed[msg.sender] == true); _; } function allowAccess(address _addr) platform public { accessAllowed[_addr] = true; } function denyAccess(address _addr) platform public { accessAllowed[_addr] = false; }}... 订阅我的小专栏可参看合约的完整代码。 部署方法如下: 先部署DataContract合约 使用DataContract合约地址作为部署ControlContract合约的参数 用ControlContract合约地址作为参数调用DataContract合约的allowAccess方法。如果需要更新控制合约(如修复了addTen)则重新执行第2-3步,同时对老的控制合约执行denyAccess()。 更多当我们在实现数据合约时,它包含的逻辑应该越少越好,并且应该是严格测试过的,因为一旦数据合约部署之后,就没法更改。大多数情况下,和用户交互的是DApp, 因此当控制合约升级之后,需要升级DApp,使之关联新的控制合约。 尽管合约可以通过本文的方式升级,但我们依然要谨慎升级,因为升级表示你可以重写逻辑,会降低用户对你的信任度。本文介绍升级方法更多的是一种思路,实际项目中可能会对应多个控制合约及数据合约。 欢迎来我的知识星球讨论区块链技术。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"智能合约","slug":"ethereum/智能合约","permalink":"https://learnblockchain.cn/categories/ethereum/智能合约/"}],"tags":[{"name":"智能合约","slug":"智能合约","permalink":"https://learnblockchain.cn/tags/智能合约/"}]},{"title":"智能合约语言 Solidity 教程系列8 - Solidity API","slug":"solidity-api","date":"2018-03-14T15:04:43.000Z","updated":"2019-04-05T10:29:32.289Z","comments":true,"path":"2018/03/14/solidity-api/","link":"","permalink":"https://learnblockchain.cn/2018/03/14/solidity-api/","excerpt":"这是Solidity教程系列文章第8篇介绍Solidity API,它们主要表现为内置的特殊的变量及函数,存在于全局命名空间里。 Solidity 系列完整的文章列表请查看分类-Solidity。","text":"这是Solidity教程系列文章第8篇介绍Solidity API,它们主要表现为内置的特殊的变量及函数,存在于全局命名空间里。 Solidity 系列完整的文章列表请查看分类-Solidity。 写在前面Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 欢迎订阅区块链技术专栏阅读更全面的分析文章。 Solidity API 主要表现为Solidity 内置的特殊的变量及函数,他们存在于全局命名空间里,主要分为以下几类: 有关区块和交易的属性 ABI编码函数 有关错误处理 有关数学及加密功能 地址相关 合约相关 下面详细讲解下 区块和交易的属性(Block And Transaction Properties)用来提供一些区块链当前的信息。 blockhash(uint blockNumber) returns (bytes32):返回给定区块号的哈希值,只支持最近256个区块,且不包含当前区块。 block.coinbase (address): 当前块矿工的地址。 block.difficulty (uint):当前块的难度。 block.gaslimit (uint):当前块的gaslimit。 block.number (uint):当前区块的块号。 block.timestamp (uint): 当前块的Unix时间戳(从1970/1/1 00:00:00 UTC开始所经过的秒数) gasleft() (uint256): 获取剩余gas。 msg.data (bytes): 完整的调用数据(calldata)。 msg.gas (uint): 当前还剩的gas(弃用)。 msg.sender (address): 当前调用发起人的地址。 msg.sig (bytes4):调用数据(calldata)的前四个字节(例如为:函数标识符)。 msg.value (uint): 这个消息所附带的以太币,单位为wei。 now (uint): 当前块的时间戳(block.timestamp的别名) tx.gasprice (uint) : 交易的gas价格。 tx.origin (address): 交易的发送者(全调用链) 注意:msg的所有成员值,如msg.sender,msg.value的值可以因为每一次外部函数调用,或库函数调用发生变化(因为msg就是和调用相关的全局变量)。 不应该依据 block.timestamp, now 和 block.blockhash来产生一个随机数(除非你确实需要这样做),这几个值在一定程度上被矿工影响(比如在赌博合约里,不诚实的矿工可能会重试去选择一个对自己有利的hash)。 对于同一个链上连续的区块来说,当前区块的时间戳(timestamp)总是会大于上一个区块的时间戳。 为了可扩展性的原因,你只能查最近256个块,所有其它的将返回0. ABI编码函数Solidity 提供了一下函数,用来直接得到ABI编码信息,这些函数有: * abi.encode(...) returns (bytes):计算参数的ABI编码。 * abi.encodePacked(...) returns (bytes):计算参数的紧密打包编码 * abi. encodeWithSelector(bytes4 selector, ...) returns (bytes): 计算函数选择器和参数的ABI编码 * abi.encodeWithSignature(string signature, ...) returns (bytes): 等价于* abi.encodeWithSelector(bytes4(keccak256(signature), ...) 通过ABI编码函数可以在不用调用函数的情况下,获得ABI编码值,下面通过一段代码来看看这些方式的使用: 12345678pragma solidity ^0.4.24;contract testABI { function abiEncode() public constant returns (bytes) { abi.encode(1); // 计算 1 的ABI编码 return abi.encodeWithSignature(\"set(uint256)\", 1); //计算函数set(uint256) 及参数1 的ABI 编码 }} 错误处理 assert(bool condition)用于判断内部错误,条件不满足时抛出异常 require(bool condition):用于判断输入或外部组件错误,条件不满足时抛出异常 require(bool condition, string message)同上,多了一个错误信息。 revert():终止执行并还原改变的状态 revert(string reason)同上,提供一个错误信息。 之前老的错误处理方式用throw 及 if … throw,这种方式会消耗掉所有剩余的gas。目前throw 的方式已经被弃用。 数学及加密功能 addmod(uint x, uint y, uint k) returns (uint):计算(x + y) % k,加法支持任意的精度且不会在2**256处溢出,从0.5.0版本开始断言k != 0。 mulmod(uint x, uint y, uint k) returns (uint):计算 (x y) % k, 乘法支持任意的精度且不会在2*256处溢出, 从0.5.0版本开始断言k != 0。 keccak256(…) returns (bytes32):使用以太坊的(Keccak-256)计算HASH值。紧密打包参数。 sha256(…) returns (bytes32):使用SHA-256计算hash值,紧密打包参数。 sha3(…) returns (bytes32):keccak256的别名 ripemd160(…) returns (bytes20):使用RIPEMD-160计算HASH值。紧密打包参数。 ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address):通过椭圆曲线签名来恢复与公钥关联的地址,或者在错误时返回零。可用于签名数据的校验,如果返回结果是签名者的公匙地址,那么说明数据是正确的。 ecrecover函数需要四个参数,需要被签名数据的哈希结果值,r,s,v分别来自签名结果串。r = signature[0:64]s = signature[64:128]v = signature[128:130]其中v取出来的值或者是00或01。要使用时,我们先要将其转为整型,再加上27,所以我们将得到27或28。在调用函数时v将填入27或28。 用javascript表达如下:1234567var msg = '0x8CbaC5e4d803bE2A3A5cd3DbE7174504c6DD0c1C'var hash = web3.sha3(msg)var sig = web3.eth.sign(address, h).slice(2)var r = `0x${sig.slice(0, 64)}`var s = `0x${sig.slice(64, 128)}`var v = web3.toDecimal(sig.slice(128, 130)) + 27 订阅区块链技术专栏可以参考到完整的使用例子。 紧密打包参数(tightly packed)意思是说参数不会补位,是直接连接在一起的,下面几个是相等的。 123456keccak256("ab", "c")keccak256("abc")keccak256(0x616263) // hexkeccak256(6382179)keccak256(97, 98, 99) //ascii 如果需要填充,可以使用显式类型转换:keccak256(“\\x00\\x12”) 与keccak256(uint16(0x12))相同。 注意,常量将使用存储它们所需的最少字节数来打包,例如keccak256(0) == keccak256(uint8(0))和keccak256(0x12345678) == keccak256(uint32(0x12345678)) 在私链(private blockchain)上运行sha256,ripemd160或ecrecover可能会出现Out-Of-Gas报错。因为私链实现了一种预编译合约,合约要在收到第一个消息后才会真正存在(虽然他们的合约代码是硬编码的)。而向一个不存在的合约发送消息,所以才会导致Out-Of-Gas的问题。一种解决办法(workaround)是每个在你真正使用它们之前先发送1 wei到这些合约上来完成初始化。在官方和测试链上没有这个问题。 地址相关 .balance (uint256):Address的余额,以wei为单位。 .transfer(uint256 amount):发送给定数量的ether到某个地址,以wei为单位。失败时抛出异常。 .send(uint256 amount) returns (bool):发送给定数量的ether到某个地址,以wei为单位, 失败时返回false。 .call(…) returns (bool):发起底层的call调用。失败时返回false。 .callcode(…) returns (bool):发起底层的callcode调用,失败时返回false。不鼓励使用,未来可能会移除。 .delegatecall(…) returns (bool):发起底层的delegatecall调用,失败时返回false 更多信息参考地址篇。 警告:send() 执行有一些风险:如果调用栈的深度超过1024或gas耗光,交易都会失败。因此,为了保证安全,必须检查send的返回值,如果交易失败,会回退以太币。如果用transfer会更好。 合约相关 this(当前合约的类型):表示当前合约,可以显式的转换为Address selfdestruct(address recipient):销毁当前合约,并把它所有资金发送到给定的地址。 suicide(address recipient):selfdestruct的别名 另外,当前合约里的所有函数均可支持调用,包括当前函数本身。 参考文档 Special Variables and Functions 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。如果想与我有更密切的交流可以选择加入我的知识星球(星球成员可加入微信技术交流群)","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Solidity","slug":"ethereum/Solidity","permalink":"https://learnblockchain.cn/categories/ethereum/Solidity/"}],"tags":[{"name":"Solidity手册","slug":"Solidity手册","permalink":"https://learnblockchain.cn/tags/Solidity手册/"}]},{"title":"如何通过以太坊智能合约来进行众筹(ICO)","slug":"ico-crowdsale","date":"2018-02-28T12:30:42.000Z","updated":"2019-04-05T10:29:32.237Z","comments":true,"path":"2018/02/28/ico-crowdsale/","link":"","permalink":"https://learnblockchain.cn/2018/02/28/ico-crowdsale/","excerpt":"前面我们有两遍文章写了如何发行代币,今天我们讲一下如何使用代币来公开募资,即编写一个募资合约。","text":"前面我们有两遍文章写了如何发行代币,今天我们讲一下如何使用代币来公开募资,即编写一个募资合约。 写在前面本文所讲的代币是使用以太坊智能合约创建,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 众筹先简单说下众筹的概念:一般是这样的,我一个非常好的想法,但是我没有钱来做这事,于是我把这个想法发给大家看,说:我做这件事需要5百万,大家有没有兴趣投些钱,如果大家在30天内投够了5百万我就开始做,到时大家都是原始股东,如果募资额不到5百万,大家投的钱就还给大家。 现在ICO众筹已经被各路大佬拿来割韭菜而被玩坏了(不管有无达标,都把钱卷走)。 其实区块链技术本事非常适合解决众筹的信任问题,借助于智能合约,可以实现当募资额完成时,募资款自动打到指定账户,当募资额未完成时,可退款。这个过程不需要看众筹大佬的人品,不用依靠第三方平台信用担保。 代币传统的众筹在参与之后通常不容易交易(参与之后无法转给其他人),而通过用代币来参与众筹,则很容易进行交易,众筹的参与人可随时进行买卖,待众筹项目实施完成的时候,完全根据代币持有量进行回馈。 举个例子说明下,大家会更容易理解,有这一个众筹:A有技术做一个能监测健康的指环,为此向公众募资200百万,募资时100块对应一个代币,约定在指环上市之后,代币的持有人可以用一个代币来兑换一个指环。而指环的研发周期是一年,因此在指环还未上市的一年里,众筹的参与人可以随时交易所持有的代币。 众筹智能合约代码接下来就看看如何实现一个众筹智能合约。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104pragma solidity ^0.4.16;interface token { function transfer(address receiver, uint amount);}contract Crowdsale { address public beneficiary; // 募资成功后的收款方 uint public fundingGoal; // 募资额度 uint public amountRaised; // 参与数量 uint public deadline; // 募资截止期 uint public price; // token 与以太坊的汇率 , token卖多少钱 token public tokenReward; // 要卖的token mapping(address => uint256) public balanceOf; bool fundingGoalReached = false; // 众筹是否达到目标 bool crowdsaleClosed = false; // 众筹是否结束 /** * 事件可以用来跟踪信息 **/ event GoalReached(address recipient, uint totalAmountRaised); event FundTransfer(address backer, uint amount, bool isContribution); /** * 构造函数, 设置相关属性 */ function Crowdsale( address ifSuccessfulSendTo, uint fundingGoalInEthers, uint durationInMinutes, uint finneyCostOfEachToken, address addressOfTokenUsedAsReward) { beneficiary = ifSuccessfulSendTo; fundingGoal = fundingGoalInEthers * 1 ether; deadline = now + durationInMinutes * 1 minutes; price = finneyCostOfEachToken * 1 finney; tokenReward = token(addressOfTokenUsedAsReward); // 传入已发布的 token 合约的地址来创建实例 } /** * 无函数名的Fallback函数, * 在向合约转账时,这个函数会被调用 */ function () payable { require(!crowdsaleClosed); uint amount = msg.value; balanceOf[msg.sender] += amount; amountRaised += amount; tokenReward.transfer(msg.sender, amount / price); FundTransfer(msg.sender, amount, true); } /** * 定义函数修改器modifier(作用和Python的装饰器很相似) * 用于在函数执行前检查某种前置条件(判断通过之后才会继续执行该方法) * _ 表示继续执行之后的代码 **/ modifier afterDeadline() { if (now >= deadline) _; } /** * 判断众筹是否完成融资目标, 这个方法使用了afterDeadline函数修改器 * */ function checkGoalReached() afterDeadline { if (amountRaised >= fundingGoal) { fundingGoalReached = true; GoalReached(beneficiary, amountRaised); } crowdsaleClosed = true; } /** * 完成融资目标时,融资款发送到收款方 * 未完成融资目标时,执行退款 * */ function safeWithdrawal() afterDeadline { if (!fundingGoalReached) { uint amount = balanceOf[msg.sender]; balanceOf[msg.sender] = 0; if (amount > 0) { if (msg.sender.send(amount)) { FundTransfer(msg.sender, amount, false); } else { balanceOf[msg.sender] = amount; } } } if (fundingGoalReached && beneficiary == msg.sender) { if (beneficiary.send(amountRaised)) { FundTransfer(beneficiary, amountRaised, false); } else { //If we fail to send the funds to beneficiary, unlock funders balance fundingGoalReached = false; } } }} 部署及说明在部署这个合约之前,我们需要先部署一个代币合约,请参考一步步教你创建自己的数字货币。 创建众筹合约我们需要提供一下几个参数:ifSuccessfulSendTo: 募资成功后的收款方(其实这里可以默认为合约创建者)fundingGoalInEthers: 募资额度, 为了方便我们仅募3个etherdurationInMinutes: 募资时间finneyCostOfEachToken 每个代币的价格, 这里为了方便使用了单位finney及值为:1 (1 ether = 1000 finney)addressOfTokenUsedAsReward: 代币合约地址。如:本文使用的参数为: 1"0xc6f9ea59d424733e8e1902c7837ea75e20abfb49",3, 100, 1,"0xad8972e2b583f580fc52f737b98327eb65d08f8c" 参与人投资的时候实际购买众筹合约代币,所以需要先向合约预存代币,代币的数量为:募资额度 / 代币的价格 , 这里为:3 * 1000/1 = 3000 (当能也可以大于3000)。向合约预存代币可以使用myetherwallet钱包,或在remix中重新加载代币合约,执行代币合约tranfer()函数进行代币转账,转账的地址就是我们创建合约的地址。如使用myetherwallet转账如图: 投资人向众筹合约转账(发送以太币)即是参与众筹行为,转账时,会执行Fallback回退函数(即无名函数)向其账户打回相应的代币。 safeWithdrawl() 可以被参与人或收益人调用,如果融资不达标参与人可收回之前投资款,如果融资达标收益人可以拿到所有的融资款。 扩展上面是一个很正规的募资合约。接下来讲两个募资合约的扩展,如何实现无限募资合约及割韭菜合约。这部分内容独家发布在我的小专栏区块链技术 如何创建代币发行代币,现在也录制了对应的视频教程:通过代币学以太坊智能合约开发,目前我们也在招募体验师,可以点击链接了解。 如果你在学习中遇到问题,欢迎到我的知识星球提问,作为星球成员福利,成员可加入区块链技术付费交流群。 参考文档 Create a crowdsale 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"智能合约","slug":"ethereum/智能合约","permalink":"https://learnblockchain.cn/categories/ethereum/智能合约/"}],"tags":[{"name":"智能合约","slug":"智能合约","permalink":"https://learnblockchain.cn/tags/智能合约/"},{"name":"Token","slug":"Token","permalink":"https://learnblockchain.cn/tags/Token/"},{"name":"ICO","slug":"ICO","permalink":"https://learnblockchain.cn/tags/ICO/"}]},{"title":"什么是拜占庭将军问题","slug":"bitcoin-byzantine","date":"2018-02-04T16:00:00.000Z","updated":"2019-04-05T10:29:31.077Z","comments":true,"path":"2018/02/05/bitcoin-byzantine/","link":"","permalink":"https://learnblockchain.cn/2018/02/05/bitcoin-byzantine/","excerpt":"接触区块链的同学,多少都听说过拜占庭将军问题,经常看到或听到某某区块链使用某某算法解决了拜占庭将军问题,那么究竟什么是拜占庭将军问题呢?","text":"接触区块链的同学,多少都听说过拜占庭将军问题,经常看到或听到某某区块链使用某某算法解决了拜占庭将军问题,那么究竟什么是拜占庭将军问题呢? 什么是拜占庭将军问题也被称为“拜占庭容错”、“拜占庭将军问题”。拜占庭将军问题是Leslie Lamport(2013年的图灵讲得主)用来为描述分布式系统一致性问题(Distributed Consensus)在论文中抽象出来一个著名的例子。 这个例子大意是这样的: 拜占庭帝国想要进攻一个强大的敌人,为此派出了10支军队去包围这个敌人。这个敌人虽不比拜占庭帝国,但也足以抵御5支常规拜占庭军队的同时袭击。这10支军队在分开的包围状态下同时攻击。他们任一支军队单独进攻都毫无胜算,除非有至少6支军队(一半以上)同时袭击才能攻下敌国。他们分散在敌国的四周,依靠通信兵骑马相互通信来协商进攻意向及进攻时间。困扰这些将军的问题是,他们不确定他们中是否有叛徒,叛徒可能擅自变更进攻意向或者进攻时间。在这种状态下,拜占庭将军们才能保证有多于6支军队在同一时间一起发起进攻,从而赢取战斗? 拜占庭将军问题中并不去考虑通信兵是否会被截获或无法传达信息等问题,即消息传递的信道绝无问题。Lamport已经证明了在消息可能丢失的不可靠信道上试图通过消息传递的方式达到一致性是不可能的。所以,在研究拜占庭将军问题的时候,已经假定了信道是没有问题的. 问题分析单从上面的说明可能无法理解这个问题的复杂性,我们来简单分析一下: 先看在没有叛徒情况下,假如一个将军A提一个进攻提议(如:明日下午1点进攻,你愿意加入吗?)由通信兵通信分别告诉其他的将军,如果幸运中的幸运,他收到了其他6位将军以上的同意,发起进攻。如果不幸,其他的将军也在此时发出不同的进攻提议(如:明日下午2点、3点进攻,你愿意加入吗?),由于时间上的差异,不同的将军收到(并认可)的进攻提议可能是不一样的,这是可能出现A提议有3个支持者,B提议有4个支持者,C提议有2个支持者等等。 再加一点复杂性,在有叛徒情况下,一个叛徒会向不同的将军发出不同的进攻提议(通知A明日下午1点进攻, 通知B明日下午2点进攻等等),一个叛徒也会可能同意多个进攻提议(即同意下午1点进攻又同意下午2点进攻)。 叛徒发送前后不一致的进攻提议,被称为“拜占庭错误”,而能够处理拜占庭错误的这种容错性称为「Byzantine fault tolerance」,简称为BFT。 相信大家已经可以明白这个问题的复杂性了。 中本聪的解决方案在出现比特币之前,解决分布式系统一致性问题主要是Lamport提出的Paxos算法或其衍生算法。Paxos类算法仅适用于中心化的分布式系统,这样的系统的没有不诚实的节点(不会发送虚假错误消息,但允许出现网络不通或宕机出现的消息延迟)。 中本聪在比特币中创造性的引入了“工作量证明(POW : Proof of Work)”来解决这个问题,有兴趣可进一步阅读工作量证明。通过工作量证明就增加了发送信息的成本,降低节点发送消息速率,这样就以保证在一个时间只有一个节点(或是很少)在进行广播,同时在广播时会附上自己的签名。这个过程就像一位将军A在向其他的将军(B、C、D…)发起一个进攻提议一样,将军B、C、D…看到将军A签过名的进攻提议书,如果是诚实的将军就会立刻同意进攻提议,而不会发起自己新的进攻提议。 以上就是比特币网络中是单个区块(账本)达成共识的方法(取得一致性)。 理解了单个区块取得一致性的方法,那么整个区块链(总账本)如果达成一致也好理解。我们稍微把将军问题改一下:假设攻下一个城堡需要多次的进攻,每次进攻的提议必须基于之前最多次数的胜利进攻下提出的(只有这样敌方已有损失最大,我方进攻胜利的可能性就更大),这样约定之后,将军A在收到进攻提议时,就会检查一下这个提议是不是基于最多的胜利提出的,如果不是(基于最多的胜利)将军A就不会同意这样的提议,如果是的,将军A就会把这次提议记下来。 这就是比特币网络最长链选择。 经济学分析工作量证明其实相当于提高了做叛徒(发布虚假区块)的成本,在工作量证明下,只有第一个完成证明的节点才能广播区块,竞争难度非常大,需要很高的算力,如果不成功其算力就白白的耗费了(算力是需要成本的),如果有这样的算力作为诚实的节点,同样也可以获得很大的收益(这就是矿工所作的工作),这也实际就不会有做叛徒的动机,整个系统也因此而更稳定。 很多人批评工作量证明造成巨大的电力浪费,促使人们去探索新的解决一致性(共识)问题的机制:权益证明机制(POS: Proof of Stake)是一个代表。在拜占庭将军问题的角度来看,它同样提高了做叛徒的成本,因为账户需要首先持有大量余额才能有更多的几率广播区块,POS不是本文重点,以后在讲。 共识算法的核心就是解决拜占庭将军问题(分布式网络一致性问题)。 扩展阅读The Byzantine Generals Problem 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。如果想与我有更密切的交流可以选择加入我的知识星球(星球成员可加入微信技术交流群)","categories":[{"name":"比特币","slug":"bitcoin","permalink":"https://learnblockchain.cn/categories/bitcoin/"}],"tags":[{"name":"比特币","slug":"比特币","permalink":"https://learnblockchain.cn/tags/比特币/"},{"name":"共识协议","slug":"共识协议","permalink":"https://learnblockchain.cn/tags/共识协议/"}]},{"title":"智能合约语言 Solidity 教程系列7 - 以太单位及时间单位","slug":"solidity-unit","date":"2018-02-02T11:51:03.000Z","updated":"2019-04-05T10:29:32.255Z","comments":true,"path":"2018/02/02/solidity-unit/","link":"","permalink":"https://learnblockchain.cn/2018/02/02/solidity-unit/","excerpt":"这是Solidity教程系列文章第7篇介绍以太单位及时间单位,系列带你全面深入理解Solidity语言。Solidity 系列完整的文章列表请查看分类-Solidity。","text":"这是Solidity教程系列文章第7篇介绍以太单位及时间单位,系列带你全面深入理解Solidity语言。Solidity 系列完整的文章列表请查看分类-Solidity。 写在前面Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 欢迎订阅区块链技术专栏阅读更全面的分析文章。 货币单位(Ether Units)一个数字常量(字面量)后面跟随一个后缀wei, finney,szabo或ether,这个后缀就是货币单位。不同的单位可以转换。不含任何后缀的默认单位是wei。不同的以太币单位转换关系如下: 1 ether == 10^3 finney == 1000 finney 1 ether == 10^6 szabo 1 ether == 10^18 wei 插曲:以太币单位其实是密码学家的名字,是以太坊创始人为了纪念他们在数字货币的领域的贡献。他们分别是:wei: Wei Dai 戴伟 密码学家 ,发表 B-moneyfinney: Hal Finney 芬尼 密码学家、工作量证明机制(POW)提出szabo: Nick Szabo 尼克萨博 密码学家、智能合约的提出者 我们可以使用一下代码验证一个转换关系:123456789101112131415161718192021222324pragma solidity ^0.4.16;contract testUnit { function tf() public pure returns (bool) { if (1 ether == 1000 finney){ return true; } return false; } function ts() public pure returns (bool) { if (1 ether == 1000000 szabo){ return true; } return false; } function tgw() public pure returns (bool) { if (1 ether == 1000000000000000000 wei){ return true; } return false; }} 时间单位(Time Units)时间单位: seconds, minutes, hours, days, weeks, years均可做为后缀,并进行相互转换,规则如下: 1 == 1 seconds (默认是seconds为单位) 1 minutes == 60 seconds 1 hours == 60 minutes 1 days == 24 hours 1 weeks = 7 days 1 years = 365 days 使用这些单位进行日期计算需要特别小心,因为不是每年都是365天,且并不是每天都有24小时,因为还有闰秒。由于无法预测闰秒,必须由外部的预言(oracle)来更新从而得到一个精确的日历库。 这些后缀不能用于变量。如果想对输入的变量说明其不同的单位,可以使用下面的方式:1234567891011121314pragma solidity ^0.4.16;contract testTUnit { function currTimeInSeconds() public pure returns (uint256){ return now; } function f(uint start, uint daysAfter) public { if (now >= start + daysAfter * 1 days) { // ... } }} 参考文档 units 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。如果想与我有更密切的交流可以选择加入我的知识星球(星球成员可加入微信技术交流群)","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Solidity","slug":"ethereum/Solidity","permalink":"https://learnblockchain.cn/categories/ethereum/Solidity/"}],"tags":[{"name":"Solidity手册","slug":"Solidity手册","permalink":"https://learnblockchain.cn/tags/Solidity手册/"}]},{"title":"实现一个可管理、增发、兑换、冻结等高级功能的代币","slug":"create-token2","date":"2018-01-27T09:04:13.000Z","updated":"2019-04-05T10:29:32.249Z","comments":true,"path":"2018/01/27/create-token2/","link":"","permalink":"https://learnblockchain.cn/2018/01/27/create-token2/","excerpt":"本文主要介绍代币高级功能的实现: 代币管理、代币增发、代币兑换、资产冻结、Gas自动补充。","text":"本文主要介绍代币高级功能的实现: 代币管理、代币增发、代币兑换、资产冻结、Gas自动补充。 写在前面在上一篇:一步步教你创建自己的数字货币(代币)进行ICO中我们实现一个最基本功能的代币,本文将在上一遍文章的基础上,讲解如果添加更多的高级功能。 实现代币的管理者虽然区块链是去中心化的,但是实现对代币(合约)的管理,也在许多应用中有需求,为了对代币进行管理,首先需要给合约添加一个管理者。 我们来看看如果实现,先创建一个owned合约。 1234567891011121314151617contract owned { address public owner; function owned() { owner = msg.sender; } modifier onlyOwner { require(msg.sender == owner); _; } // 实现所有权转移 function transferOwnership(address newOwner) onlyOwner { owner = newOwner; }} 这个合约重要的是加入了一个函数修改器(Function Modifiers)onlyOwner,函数修改器是一个合约属性,可以被继承,还能被重写。它用于在函数执行前检查某种前置条件。关于函数修改器可进一步阅读Solidity 教程系列10 - 完全理解函数修改器 如果熟悉Python的同学,会发现函数修改器的作用和Python的装饰器很相似。 然后让代币合约继承owned以拥有onlyOwner修改器,代码如下:1234567891011contract MyToken is owned { function MyToken( uint256 initialSupply, string tokenName, uint8 decimalUnits, string tokenSymbol, address centralMinter ) { if(centralMinter != 0 ) owner = centralMinter; }} 代币增发实现代币增发,代币增发就如同央行印钞票一样,想必很多人都需要这样的功能。 给合约添加以下的方法:123456function mintToken(address target, uint256 mintedAmount) onlyOwner { balanceOf[target] += mintedAmount; totalSupply += mintedAmount; Transfer(0, owner, mintedAmount); Transfer(owner, target, mintedAmount); } 注意onlyOwner修改器添加在函数末尾,这表示只有ower才能调用这用函数。他的功能很简单,就是给指定的账户增加代币,同时增加总供应量。 资产冻结有时为了监管的需要,需要实现冻结某些账户,冻结后,其资产仍在账户,但是不允许交易,之道解除冻结。给合约添加以下的变量和方法(可以添加到合约的任何地方,但是建议把mapping加到和其他mapping一起,event也是如此):1234567mapping (address => bool) public frozenAccount;event FrozenFunds(address target, bool frozen);function freezeAccount(address target, bool freeze) onlyOwner { frozenAccount[target] = freeze; FrozenFunds(target, freeze);} 单单以上的代码还无法冻结,需要把他加入到transfer函数中才能真正生效,因此修改transfer函数1234function transfer(address _to, uint256 _value) { require(!frozenAccount[msg.sender]); ...} 这样在转账前,对发起交易的账号做一次检查,只有不是被冻结的账号才能转账。 代币买卖(兑换)可以自己的货币中实现代币与其他数字货币(ether 或其他tokens)的兑换机制。有了这个功能,我们的合约就可以在一买一卖中赚利润了。 先来设置下买卖价格1234567uint256 public sellPrice;uint256 public buyPrice;function setPrices(uint256 newSellPrice, uint256 newBuyPrice) onlyOwner { sellPrice = newSellPrice; buyPrice = newBuyPrice;} setPrices()添加了onlyOwner修改器,注意买卖的价格单位是wei(最小的货币单位: 1 eth = 1000000000000000000 wei) 添加来添加买卖函数:123456789101112131415161718function buy() payable returns (uint amount){ amount = msg.value / buyPrice; // calculates the amount require(balanceOf[this] >= amount); // checks if it has enough to sell balanceOf[msg.sender] += amount; // adds the amount to buyer's balance balanceOf[this] -= amount; // subtracts amount from seller's balance Transfer(this, msg.sender, amount); // execute an event reflecting the change return amount; // ends function and returns}function sell(uint amount) returns (uint revenue){ require(balanceOf[msg.sender] >= amount); // checks if the sender has enough to sell balanceOf[this] += amount; // adds the amount to owner's balance balanceOf[msg.sender] -= amount; // subtracts the amount from seller's balance revenue = amount * sellPrice; msg.sender.transfer(revenue); // sends ether to the seller: it's important to do this last to prevent recursion attacks Transfer(msg.sender, this, amount); // executes an event reflecting on the change return revenue; // ends function and returns} 加入了买卖功能后,要求我们在创建合约时发送足够的以太币,以便合约有能力回购市面上的代币,否则合约将破产,用户没法先合约卖代币。 实现Gas的自动补充以太坊中的交易时需要gas(支付给矿工的费用,费用以ether来支付)。而如果用户没有以太币,只有代币的情况(或者我们想向用户隐藏以太坊的细节),就需要自动补充gas的功能。这个功能将使我们代币更加好用。 自动补充的逻辑是这样了,在执行交易之前,我们判断用户的余额(用来支付矿工的费用),如果用户的余额非常少(低于某个阈值时)可能影响到交易进行,合约自动售出一部分代币来补充余额,以帮助用户顺利完成交易。 先来设定余额阈值:12345uint minBalanceForAccounts; function setMinBalance(uint minimumBalanceInFinney) onlyOwner { minBalanceForAccounts = minimumBalanceInFinney * 1 finney; } finney 是货币单位 1 finney = 0.001eth然后交易中加入对用户的余额的判断。1234567function transfer(address _to, uint256 _value) { ... if(msg.sender.balance < minBalanceForAccounts) sell((minBalanceForAccounts - msg.sender.balance) / sellPrice); if(_to.balance<minBalanceForAccounts) // 可选,让接受者也补充余额,以便接受者使用代币。 _to.send(sell((minBalanceForAccounts - _to.balance) / sellPrice));} 代码部署高级功能完整代码请前往我的小专栏, 项目的完整的部署方法参考上一篇,不同的是创建合约时需要预存余额,如图: 专栏已经有多篇文章介绍Remix Solidity IDE的使用,这里就不一一截图演示了,请大家自己测试验证。 如何创建代币发行代币,现在也录制了对应的视频教程:通过代币学以太坊智能合约开发,现在我们在招募体验师,可以点击链接了解详情。 如果你在创建代币的过程中遇到问题,欢迎到我的知识星球提问,作为星球成员福利,成员可加入区块链技术付费交流群。 参考文档 Create your own crypto-currency with ethereum 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"智能合约","slug":"ethereum/智能合约","permalink":"https://learnblockchain.cn/categories/ethereum/智能合约/"}],"tags":[{"name":"智能合约","slug":"智能合约","permalink":"https://learnblockchain.cn/tags/智能合约/"},{"name":"Token","slug":"Token","permalink":"https://learnblockchain.cn/tags/Token/"}]},{"title":"Truffle 教程:教你开发、部署第一个去中心化应用(Dapp) - 宠物商店","slug":"first-dapp","date":"2018-01-12T14:36:39.000Z","updated":"2019-04-05T10:29:32.231Z","comments":true,"path":"2018/01/12/first-dapp/","link":"","permalink":"https://learnblockchain.cn/2018/01/12/first-dapp/","excerpt":"今天我们来编写一个完整的去中心化(区块链)应用(Dapps), 本文可以和编写智能合约结合起来看。2019/03/30更新: 适配 Truffle v5.0.0; 更新Solidity 代码,适配solidity 0.5.0以上版本; 适配MetaMask 更新, 适配支持window.ethereum 对象;","text":"今天我们来编写一个完整的去中心化(区块链)应用(Dapps), 本文可以和编写智能合约结合起来看。2019/03/30更新: 适配 Truffle v5.0.0; 更新Solidity 代码,适配solidity 0.5.0以上版本; 适配MetaMask 更新, 适配支持window.ethereum 对象; 写在前面阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么除此之外,你最好还了解一些HTML及JavaScript知识。 本文通过实例教大家来开发去中心化应用,应用效果如图: 从本文,你可以学习到: 搭建智能合约开发环境 创建Truffle项目 编写智能合约 编译和部署智能合约到区块链 如何通过Web3和智能合约交互 MetaMask 的使用 小专栏用户在教程结尾处可以下载完整的Dapp代码。 项目背景Pete有一个宠物店,有16只宠物,他想开发一个去中心化应用,让大家来领养宠物。在truffle box中,已经提供了pet-shop的网站部分的代码,我们只需要编写合约及交互部分。 环境搭建 安装Node 安装 Truffle :npm install -g truffle 安装Ganache Ganache(或Ganache CLI)已经取代了 testrpc。 创建项目 建立项目目录并进入 12> mkdir pet-shop-tutorial> cd pet-shop-tutorial 使用truffle unbox 创建项目 123456789101112 > truffle unbox pet-shop Downloading... Unpacking... Setting up... Unbox successful. Sweet!Commands: Compile: truffle compile Migrate: truffle migrate Test contracts: truffle test Run dev server: npm run dev 这一步需要等待一会 也可以使用truffle init 来创建一个全新的项目。 项目目录结构contracts/ 智能合约的文件夹,所有的智能合约文件都放置在这里,里面包含一个重要的合约Migrations.sol(稍后再讲)migrations/ 用来处理部署(迁移)智能合约 ,迁移是一个额外特别的合约用来保存合约的变化。test/ 智能合约测试用例文件夹truffle.js/ 配置文件 其他代码可以暂时不用管 编写智能合约智能合约承担着分布式应用的后台逻辑和存储。智能合约使用solidity编写,可阅读solidity系列文章 在contracts目录下,添加合约文件Adoption.sol1234567891011121314151617181920pragma solidity ^0.5.0;contract Adoption { address[16] public adopters; // 保存领养者的地址 // 领养宠物 function adopt(uint petId) public returns (uint) { require(petId >= 0 && petId <= 15); // 确保id在数组长度内 adopters[petId] = msg.sender; // 保存调用这地址 return petId; } // 返回领养者 function getAdopters() public view returns (address[16] memory) { return adopters; }} 编译部署智能合约Truffle集成了一个开发者控制台,可用来生成一个开发链用来测试和部署智能合约。 编译Solidity是编译型语言,需要把可读的Solidity代码编译为EVM字节码才能运行。dapp的根目录pet-shop-tutorial下 1> truffle compile 输出 12Compiling ./contracts/Adoption.sol...Writing artifacts to ./build/contracts 部署编译之后,就可以部署到区块链上。在migrations文件夹下已经有一个1_initial_migration.js部署脚本,用来部署Migrations.sol合约。Migrations.sol 用来确保不会部署相同的合约。 现在我们来创建一个自己的部署脚本2_deploy_contracts.js 12345var Adoption = artifacts.require("Adoption");module.exports = function(deployer) { deployer.deploy(Adoption);}; 在执行部署之前,需要确保有一个区块链运行, 可以使用Ganache来开启一个私链来进行开发测试,默认会在7545端口上运行一个开发链。Ganache 启动之后是这样: 接下来执行部署命令: 1> truffle migrate 执行后,有一下类似的输出, 12345678910111213141516Using network 'develop'.Running migration: 1_initial_migration.js Deploying Migrations... ... 0x3076b7dac65afc44ec51508bf6f2b6894f833f0f9560ecad2d6d41ed98a4679f Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0Saving successful migration to network... ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956Saving artifacts...Running migration: 2_deploy_contracts.js Deploying Adoption... ... 0x2c6ab4471c225b5473f2079ee42ca1356007e51d5bb57eb80bfeb406acc35cd4 Adoption: 0x345ca3e014aaf5dca488057592ee47305d9b3e10Saving successful migration to network... ... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0Saving artifacts... 在打开的Ganache里可以看到区块链状态的变化,现在产生了4个区块。这时说明已经智能合约已经部署好了。 测试现在我们来测试一下智能合约,测试用例可以用 JavaScript 或 Solidity来编写,这里使用Solidity。 在test目录下新建一个TestAdoption.sol,编写测试合约 12345678910111213141516171819202122232425262728293031323334pragma solidity ^0.5.0;import \"truffle/Assert.sol\"; // 引入的断言import \"truffle/DeployedAddresses.sol\"; // 用来获取被测试合约的地址import \"../contracts/Adoption.sol\"; // 被测试合约contract TestAdoption { Adoption adoption = Adoption(DeployedAddresses.Adoption()); // 领养测试用例 function testUserCanAdoptPet() public { uint returnedId = adoption.adopt(8); uint expected = 8; Assert.equal(returnedId, expected, \"Adoption of pet ID 8 should be recorded.\"); } // 宠物所有者测试用例 function testGetAdopterAddressByPetId() public { // 期望领养者的地址就是本合约地址,因为交易是由测试合约发起交易, address expected = this; address adopter = adoption.adopters(8); Assert.equal(adopter, expected, \"Owner of pet ID 8 should be recorded.\"); } // 测试所有领养者 function testGetAdopterAddressByPetIdInArray() public { // 领养者的地址就是本合约地址 address expected = this; address[16] memory adopters = adoption.getAdopters(); Assert.equal(adopters[8], expected, \"Owner of pet ID 8 should be recorded.\"); }} 提示:Assert.sol 及 DeployedAddresses.sol是Truffle框架提供,在test目录下并不提供truffle目录。 TestAdoption合约中添加adopt的测试用例。 运行测试用例在终端中,执行 1truffle test 如果测试通过,则终端输出: 123456789101112131415Using network 'develop'.Compiling ./contracts/Adoption.sol...Compiling ./test/TestAdoption.sol...Compiling truffle/Assert.sol...Compiling truffle/DeployedAddresses.sol... TestAdoption ✓ testUserCanAdoptPet (62ms) ✓ testGetAdopterAddressByPetId (53ms) ✓ testGetAdopterAddressByPetIdInArray (73ms) 3 passing (554ms) 创建用户接口和智能合约交互我们已经编写和部署及测试好了我们的合约,接下我们为合约编写UI,让合约真正可以用起来。 在Truffle Box pet-shop里,已经包含了应用的前端代码,代码在src/文件夹下。 在编辑器中打开src/js/app.js 可以看到用来管理整个应用的App对象,init函数加载宠物信息,就初始化web3.web3是一个实现了与以太坊节点通信的库,我们利用web3来和合约进行交互。 初始化web3、接下来,我们来编辑app.js修改initWeb3():删除注释,修改为: 12345678910111213141516171819202122232425initWeb3: function() { // Modern dapp browsers... if (window.ethereum) { App.web3Provider = window.ethereum; try { // Request account access await window.ethereum.enable(); } catch (error) { // User denied account access... console.error(\"User denied account access\") } } // Legacy dapp browsers... else if (window.web3) { App.web3Provider = window.web3.currentProvider; } // If no injected web3 instance is detected, fall back to Ganache else { App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545'); } web3 = new Web3(App.web3Provider); return App.initContract();} 新的Dapp浏览器或MetaMask的新版本,注入了一个ethereum 对象到window对象里, 应该优先使用ethereum来构造web3, 同时使用ethereum.enable()来请求用户授权访问链接账号。 代码中优先使用Mist 或 MetaMask提供的web3实例,如果没有则从本地环境创建一个。 实例化合约使用truffle-contract会帮我们保存合约部署的信息,就不需要我们手动修改合约地址,修改initContract()代码如下: 123456789101112131415initContract: function() { // 加载Adoption.json,保存了Adoption的ABI(接口说明)信息及部署后的网络(地址)信息,它在编译合约的时候生成ABI,在部署的时候追加网络信息 $.getJSON('Adoption.json', function(data) { // 用Adoption.json数据创建一个可交互的TruffleContract合约实例。 var AdoptionArtifact = data; App.contracts.Adoption = TruffleContract(AdoptionArtifact); // Set the provider for our contract App.contracts.Adoption.setProvider(App.web3Provider); // Use our contract to retrieve and mark the adopted pets return App.markAdopted(); }); return App.bindEvents();} 处理领养修改markAdopted()代码: 123456789101112131415161718markAdopted: function(adopters, account) { var adoptionInstance; App.contracts.Adoption.deployed().then(function(instance) { adoptionInstance = instance; // 调用合约的getAdopters(), 用call读取信息不用消耗gas return adoptionInstance.getAdopters.call(); }).then(function(adopters) { for (i = 0; i < adopters.length; i++) { if (adopters[i] !== '0x0000000000000000000000000000000000000000') { $('.panel-pet').eq(i).find('button').text('Success').attr('disabled', true); } } }).catch(function(err) { console.log(err.message); });} 修改handleAdopt()代码: 123456789101112131415161718192021222324252627handleAdopt: function(event) { event.preventDefault(); var petId = parseInt($(event.target).data('id')); var adoptionInstance; // 获取用户账号 web3.eth.getAccounts(function(error, accounts) { if (error) { console.log(error); } var account = accounts[0]; App.contracts.Adoption.deployed().then(function(instance) { adoptionInstance = instance; // 发送交易领养宠物 return adoptionInstance.adopt(petId, {from: account}); }).then(function(result) { return App.markAdopted(); }).catch(function(err) { console.log(err.message); }); });} 在浏览器中运行安装 MetaMaskMetaMask 是一款插件形式的以太坊轻客户端,开发过程中使用MetaMask和我们的dapp进行交互是个很好的选择,通过此链接安装,安装完成后,浏览器工具条会显示一个小狐狸图标。 配置钱包在接受隐私说明后,会出现页面如下: 这里我们通过还原一个Ganache为我们创建好的钱包,作为我们的开发测试钱包。点击页面的 Import Existing DEN,输入Ganache显示的助记词。 1candy maple cake sugar pudding cream honey rich smooth crumble sweet treat 然后自己想要的密码,点击OK。如图: 连接开发区块链网络默认连接的是以太坊主网(左上角显示),选择Custom RPC,添加一个网络:http://127.0.0.1:7545,点返回后,显示如下:这是左上角显示为Private Network,账号是Ganache中默认的第一个账号。 至此MetaMask的安装,配置已经完成。 安装和配置lite-server接下来需要本地的web 服务器提供服务的访问, Truffle Box pet-shop里提供了一个lite-server可以直接使用,我们看看它是如何工作的。bs-config.json指示了lite-server的工作目录。 12345{ \"server\": { \"baseDir\": [\"./src\", \"./build/contracts\"] }} ./src 是网站文件目录./build/contracts 是合约输出目录 以此同时,在package.json文件的scripts中添加了dev命令:1234\"scripts\": { \"dev\": \"lite-server\", \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"}, 当运行npm run dev的时候,就会启动lite-server 启动服务1> npm run dev 会自动打开浏览器显示我们的dapp,如本文的第一张图。现在领养一直宠物看看,当我们点击Adopt时,MetaMask会提示我们交易的确认,如图: 点击Submit确认后,就可以看到成功领养了这次宠物。 在MetaMask中,也可以看到交易的清单: 好了,恭喜你,即将成为一名去中心化式应用开发者的你已经成为迈出了坚实的一步。还可以阅读另一篇开发链上记事本 进一步巩固DApp 开发。 加我微信:xlbxiong 备注:DApp, 加入以太坊DApp开发微信群。 如果学习中遇到问题,欢迎加入知识星球交流。 想全面学习去中心化应用的同学可以学习我们的视频课程区块链全栈-以太坊DAPP开发实战。 参考文档 Truffle手册 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Dapp","slug":"ethereum/Dapp","permalink":"https://learnblockchain.cn/categories/ethereum/Dapp/"}],"tags":[{"name":"以太坊概念","slug":"以太坊概念","permalink":"https://learnblockchain.cn/tags/以太坊概念/"},{"name":"Truffle","slug":"Truffle","permalink":"https://learnblockchain.cn/tags/Truffle/"},{"name":"Dapp入门","slug":"Dapp入门","permalink":"https://learnblockchain.cn/tags/Dapp入门/"}]},{"title":"一步步教你创建自己的数字货币(代币)进行ICO","slug":"create_token","date":"2018-01-12T14:36:39.000Z","updated":"2019-04-05T10:29:32.297Z","comments":true,"path":"2018/01/12/create_token/","link":"","permalink":"https://learnblockchain.cn/2018/01/12/create_token/","excerpt":"本文从技术角度详细介绍如何基于以太坊ERC20创建代币的流程.","text":"本文从技术角度详细介绍如何基于以太坊ERC20创建代币的流程. 写在前面本文所讲的代币是使用以太坊智能合约创建,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 代币Token如果不那么追求精确的定义,代币就是数字货币,比特币、以太币就是一个代币。利用以太坊的智能合约可以轻松编写出属于自己的代币,代币可以代表任何可以交易的东西,如:积分、财产、证书等等。因此不管是出于商业,还是学习很多人想创建一个自己的代币,先贴一个图看看创建的代币是什么样子。 今天我们就来详细讲一讲怎样创建一个这样的代币。 ERC20 Token也许你经常看到ERC20和代币一同出现, ERC20是以太坊定义的一个代币标准。要求我们在实现代币的时候必须要遵守的协议,如指定代币名称、总量、实现代币交易函数等,只有支持了协议才能被以太坊钱包支持。其接口如下: 12345678910111213141516contract ERC20Interface { string public constant name = \"Token Name\"; string public constant symbol = \"SYM\"; uint8 public constant decimals = 18; // 18 is the most common number of decimal places function totalSupply() public constant returns (uint); function balanceOf(address tokenOwner) public constant returns (uint balance); function allowance(address tokenOwner, address spender) public constant returns (uint remaining); function transfer(address to, uint tokens) public returns (bool success); function approve(address spender, uint tokens) public returns (bool success); function transferFrom(address from, address to, uint tokens) public returns (bool success); event Transfer(address indexed from, address indexed to, uint tokens); event Approval(address indexed tokenOwner, address indexed spender, uint tokens);} 简单说明一下:name : 代币名称symbol: 代币符号decimals: 代币小数点位数,代币的最小单位, 18表示我们可以拥有 .0000000000000000001单位个代币。totalSupply() : 发行代币总量。balanceOf(): 查看对应账号的代币余额。transfer(): 实现代币交易,用于给用户发送代币(从我们的账户里)。transferFrom(): 实现代币用户之间的交易。allowance(): 控制代币的交易,如可交易账号及资产。approve(): 允许用户可花费的代币数。 编写代币合约代码代币合约代码: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980pragma solidity ^0.4.16;interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public; }contract TokenERC20 { string public name; string public symbol; uint8 public decimals = 18; // 18 是建议的默认值 uint256 public totalSupply; mapping (address => uint256) public balanceOf; // mapping (address => mapping (address => uint256)) public allowance; event Transfer(address indexed from, address indexed to, uint256 value); event Burn(address indexed from, uint256 value); function TokenERC20(uint256 initialSupply, string tokenName, string tokenSymbol) public { totalSupply = initialSupply * 10 ** uint256(decimals); balanceOf[msg.sender] = totalSupply; name = tokenName; symbol = tokenSymbol; } function _transfer(address _from, address _to, uint _value) internal { require(_to != 0x0); require(balanceOf[_from] >= _value); require(balanceOf[_to] + _value > balanceOf[_to]); uint previousBalances = balanceOf[_from] + balanceOf[_to]; balanceOf[_from] -= _value; balanceOf[_to] += _value; Transfer(_from, _to, _value); assert(balanceOf[_from] + balanceOf[_to] == previousBalances); } function transfer(address _to, uint256 _value) public { _transfer(msg.sender, _to, _value); } function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) { require(_value <= allowance[_from][msg.sender]); // Check allowance allowance[_from][msg.sender] -= _value; _transfer(_from, _to, _value); return true; } function approve(address _spender, uint256 _value) public returns (bool success) { allowance[msg.sender][_spender] = _value; return true; } function approveAndCall(address _spender, uint256 _value, bytes _extraData) public returns (bool success) { tokenRecipient spender = tokenRecipient(_spender); if (approve(_spender, _value)) { spender.receiveApproval(msg.sender, _value, this, _extraData); return true; } } function burn(uint256 _value) public returns (bool success) { require(balanceOf[msg.sender] >= _value); balanceOf[msg.sender] -= _value; totalSupply -= _value; Burn(msg.sender, _value); return true; } function burnFrom(address _from, uint256 _value) public returns (bool success) { require(balanceOf[_from] >= _value); require(_value <= allowance[_from][msg.sender]); balanceOf[_from] -= _value; allowance[_from][msg.sender] -= _value; totalSupply -= _value; Burn(_from, _value); return true; }} 代码的详细解读,请订阅我的小专栏。 部署在开发测试智能合约时,MetaMask和Remix Solidity IDE是两个非常好用的工具,今天就用他们来完成部署。 安装和配置MetaMask请参考开发、部署第一个去中心化应用,不同的上本文选择了以太坊的测试网络Ropsten,如果你没有余额请点击购买buy,进入的网站可以送一些测试以太币给你,配置好之后,界面应该如下: 浏览器打开Remix Solidity IDE,\b复制以上源码粘贴上,在右侧选项参考如图的设置:注意Environment和Account和MetaMask保持一致,然后选择合约TokenERC20,填入你想要的发行量,名称及代号,就可以创建合约了。这时MetaMask会弹出一个交易确认框,点SUBMIT。待合约部署交易确认之后,复制合约地址。 打开Metamask界面,切换到TOKENS,点添加合约,出现如下对话框:填入刚刚复制的地址,点ADD,这时你就可以看到你创建的代币了,如图: 哈哈,你已经完成了代币的创建和部署(正式网络和测试网络部署方法一样),可以在Etherscan查询到我们刚刚部署的代币。可以用它进行ICO了,从此走上人生巅峰(玩笑话,不鼓励大家发行无意义的代币)。 代币交易由于MetaMask插件没有提供代币交易功能,同时考虑到很多人并没有以太坊钱包或是被以太坊钱包网络同步问题折磨,今天我用网页钱包来讲解代币交易。 进入网页钱包地址, 第一次进入有一些安全提示需要用户确认。 进入之后,按照下图进行设置: 连接上之后,如图需要添加代币,填入代币合约地址。 进行代币转账交易在接下来的交易确认也,点击确认即可。 交易完成后,可以看到MetaMask中代币余额减少了,如图: 代币交易是不是很简单,只要明白了交易流程,使用其他的钱包也是一样的道理。 如何创建代币发行代币,现在也录制了对应的视频教程:通过代币学以太坊智能合约开发,现在我们在招募体验师可以点击链接了解。如果你在创建代币的过程中遇到问题,我的知识星球可为大家解答问题,作为星球成员福利,成员还可加入区块链技术付费交流群。 参考文档 代币标准 Create your own crypto-currency with ethereum 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"智能合约","slug":"ethereum/智能合约","permalink":"https://learnblockchain.cn/categories/ethereum/智能合约/"}],"tags":[{"name":"智能合约","slug":"智能合约","permalink":"https://learnblockchain.cn/tags/智能合约/"},{"name":"Token","slug":"Token","permalink":"https://learnblockchain.cn/tags/Token/"},{"name":"ERC20","slug":"ERC20","permalink":"https://learnblockchain.cn/tags/ERC20/"}]},{"title":"区块链技术学习指引","slug":"guide","date":"2018-01-11T07:03:36.000Z","updated":"2019-04-05T10:29:32.272Z","comments":true,"path":"2018/01/11/guide/","link":"","permalink":"https://learnblockchain.cn/2018/01/11/guide/","excerpt":"本章的文章越来越多,本文是一个索引帖,方便找到自己感兴趣的文章,你也可以使用左侧的分类、标签及搜索功能。有新文章时会更新本文,建议大家加入收藏夹中,如果你觉得本站不错,欢迎你转发给朋友。","text":"本章的文章越来越多,本文是一个索引帖,方便找到自己感兴趣的文章,你也可以使用左侧的分类、标签及搜索功能。有新文章时会更新本文,建议大家加入收藏夹中,如果你觉得本站不错,欢迎你转发给朋友。 引言给迷失在如何学习区块链技术的同学一个指引,区块链技术是随比特币诞生,因此要搞明白区块链技术,应该先了解下比特币。但区块链技术不单应用于比特币,还有非常多的现实应用场景,想做区块链应用开发,可进一步阅读以太坊系列。 比特币如果你是还不知比特币是什么,那就看看比特币是什么 基础入门接下来可以通过下面这几篇文章了解比特币大概的运行原理: 区块链记账原理 通过这篇可以了解到区块链是一个怎样的结构 比特币所有权及隐私问题 通过这篇可以了解到地址私钥 非对称加密应用 等概念 比特币如何挖矿 通过这篇了解工作量证明 比特币如何达成共识 - 最长链的选择 通过这篇可以了解共识机制。 补充阅读 什么是拜占庭将军问题 进阶在基础入门之后,可以进一步阅读以下几篇,理解分布式网络,交易验证。 分析比特币网络:一种去中心化、点对点的网络架构 比特币区块结构 Merkle 树及简单支付验证分析 比特币脚本及交易分析 - 智能合约雏形 看完上面这些,区块链应该理解差不多了,就可以尝试实现一个简单的区块链了。参考这篇用Python从零开始创建区块链。 以太坊一个技术要落地还得靠应用, 以太坊就这样一个建立在区块链技术之上,去中心化的应用平台。可以阅读几下几篇,这部分以开发为主,需要大家多发时间实践。 以太坊开发入门 智能合约开发环境搭建及Hello World合约 搭建智能合约开发环境Remix IDE及使用 以太坊客户端Geth命令用法-参数详解 Geth控制台使用实战及Web3.js使用 如何搭建以太坊私有链 智能合约及应用开发 程序员如何切入区块链去中心化应用开发 一步步教你开发、部署第一个Dapp应用 一步步教你创建自己的数字货币(代币)进行ICO 实现一个可管理、增发、兑换、冻结等高级功能的代币 如何通过以太坊智能合约来进行众筹(ICO) 剖析非同质化代币ERC721–全面解析ERC721标准 Web3与智能合约交互实战 Web3监听合约事件 如何编写一个可升级的智能合约 美链BEC合约漏洞技术分析 Solidity语言教程全面学习Solidity语言可以购买图书:精通以太坊智能合约 Solidity 教程系列1 - 类型介绍 Solidity 教程系列2 - 地址类型介绍 Solidity 教程系列3 - 函数类型介绍 Solidity 教程系列4 - 数据存储位置分析 Solidity 教程系列5 - 数组介绍 Solidity 教程系列6 - 结构体与映射 Solidity 教程系列7 - 以太单位及时间单位 Solidity 教程系列8 - Solidity API Solidity 教程系列9 - 错误处理 Solidity 教程系列10 - 完全理解函数修改器 Solidity 教程系列11 - 视图函数、虚函数讲解 Solidity 教程系列12 - 库的使用 Solidity 教程系列13 - 函数调用 智能合约最佳实践 之 Solidity 编码规范 如何理解以太坊ABI - 应用程序二进制接口 以太扩容 深入理解Plasma(一)Plasma 框架 深入理解Plasma(二)Plasma 细节 深入理解Plasma(三)Plasma MVP 深入理解Plasma(四)Plasma Cash 钱包开发系列 理解开发HD 钱包涉及的 BIP32、BIP44、BIP39 以太坊钱包开发系列1 - 创建钱包账号 以太坊钱包开发系列1 - 账号Keystore文件导入导出 以太坊钱包开发系列2 - 展示钱包信息及发起签名交易 以太坊钱包开发系列3 - 发送Token(代币) 如可开发一款以太安卓钱包 登链钱包(一款功能强大的以太坊钱包)完全开源 如何开发一款以太坊(安卓)钱包系列1 - 通过助记词创建账号 如何开发一款以太坊(安卓)钱包系列2 - 导入账号及账号管理 柚子EOS 什么是EOS 跨链研究 跨链技术的分析和思考 IPFS 站在Web3.0 理解IPFS是什么 IPFS 使用入门 FileCoin 1 filecoin概念 2 filecoin通用语言理解 3 filecoin开发网使用 4 filecoin源码顶层架构分析 5 filecoin源码协议层分析之心跳协议 6 filecoin源码协议层分析之hello握手协议 7 filecoin源码协议层分析之存储协议 8 filecoin源码协议层分析之检索协议 9 filecoin源码分析之支撑包分析(1) 10 filecoin源码分析之支撑包分析(2) 11 filecoin源码分析之内部接口层api包分析 12 filecoin源码分析之内部接口层plumbing&porcelain接口 13 filecoin源码分析之服务层actor及vm 14 filecoin源码分析之服务层链同步、共识协议及挖矿 15 filecoin源码分析之节点运行逻辑 推荐深入浅出只给大家推荐优质内容,大家也需要理解很多优质内容是需要付费的,这样作者才可持续输出优质内容。 推荐大家订阅深入浅出区块链技术小专栏,目前仅需69元(不定时涨价哦), 部分源码和进阶内容仅在小专栏开放,订阅小专栏还有其他惊喜哦~。 通过视频课程学习区块链技术,推荐大家关注登链学院, 公众号: edupchain, 成为课程体验师还可以免费学习课程。 如果在学习过程中遇到问题,可以关注最专业的区块链问答社区:《知识星球:深入浅出区块链》,跟随星主一起学习,可以让你无论从广度还是深度上都提升一个层次。加星主微信:xlbxiong, 还进入专属的微信交流群,同时链接数千区块链开发者。 如果你有开发钱包、DAPP、交易所、公链、人才招聘等需求,也欢迎勾搭Tiny熊(微信:xlbxiong) 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"目录","slug":"目录","permalink":"https://learnblockchain.cn/categories/目录/"}],"tags":[{"name":"如何学习","slug":"如何学习","permalink":"https://learnblockchain.cn/tags/如何学习/"},{"name":"目录","slug":"目录","permalink":"https://learnblockchain.cn/tags/目录/"}]},{"title":"智能合约语言 Solidity 教程系列6 - 结构体与映射","slug":"solidity-structs","date":"2017-12-27T03:55:26.000Z","updated":"2019-04-05T10:29:30.637Z","comments":true,"path":"2017/12/27/solidity-structs/","link":"","permalink":"https://learnblockchain.cn/2017/12/27/solidity-structs/","excerpt":"Solidity 教程系列第6篇 - Solidity 结构体与映射。Solidity 系列完整的文章列表请查看分类-Solidity。","text":"Solidity 教程系列第6篇 - Solidity 结构体与映射。Solidity 系列完整的文章列表请查看分类-Solidity。 写在前面Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 本系列文章一部分是参考Solidity官方文档(当前最新版本:0.4.20)进行翻译,另一部分是Solidity深入分析,这部分请订阅区块链技术专栏阅读。 结构体(Structs)Solidity提供struct来定义自定义类型,自定义的类型是引用类型。我们看看下面的例子:1234567891011121314151617181920212223242526272829303132333435363738394041424344pragma solidity ^0.4.11;contract CrowdFunding { // 定义一个包含两个成员的新类型 struct Funder { address addr; uint amount; } struct Campaign { address beneficiary; uint fundingGoal; uint numFunders; uint amount; mapping (uint => Funder) funders; } uint numCampaigns; mapping (uint => Campaign) campaigns; function newCampaign(address beneficiary, uint goal) public returns (uint campaignID) { campaignID = numCampaigns++; // campaignID 作为一个变量返回 // 创建一个结构体实例,存储在storage ,放入mapping里 campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0); } function contribute(uint campaignID) public payable { Campaign storage c = campaigns[campaignID]; // 用mapping对应项创建一个结构体引用 // 也可以用 Funder(msg.sender, msg.value) 来初始化. c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value}); c.amount += msg.value; } function checkGoalReached(uint campaignID) public returns (bool reached) { Campaign storage c = campaigns[campaignID]; if (c.amount < c.fundingGoal) return false; uint amount = c.amount; c.amount = 0; c.beneficiary.transfer(amount); return true; }} 上面是一个简化版的众筹合约,但它可以让我们理解structs的基础概念,struct可以用于映射和数组中作为元素。其本身也可以包含映射和数组等类型。 不能声明一个struct同时将自身struct作为成员,这个限制是基于结构体的大小必须是有限的。但struct可以作为mapping的值类型成员。 注意在函数中,将一个struct赋值给一个局部变量(默认是storage类型),实际是拷贝的引用,所以修改局部变量值的同时,会影响到原变量。 当然,也可以直接通过访问成员修改值,而不用一定赋值给一个局部变量,如campaigns[campaignID].amount = 0 映射(Mappings)映射类型,一种键值对的映射关系存储结构。定义方式为mapping(_KeyType => _KeyValue)。键类型允许除映射、变长数组、合约、枚举、结构体外的几乎所有类型()。值类型没有任何限制,可以为任何类型包括映射类型。 映射可以被视作为一个哈希表,所有可能的键会被虚拟化的创建,映射到一个类型的默认值(二进制的全零表示)。在映射表中,并不存储键的数据,仅仅存储它的keccak256哈希值,这个哈希值在查找值时需要用到。正因为此,映射是没有长度的,也没有键集合或值集合的概念。 映射类型,仅能用来作为状态变量,或在内部函数中作为storage类型的引用。 可以通过将映射标记为public,来让Solidity创建一个访问器。通过提供一个键值做为参数来访问它,将返回对应的值。映射的值类型也可以是映射,使用访问器访问时,要提供这个映射值所对应的键,不断重复这个过程。来看一个例子: 1234567891011121314151617pragma solidity ^0.4.0;contract MappingExample { mapping(address => uint) public balances; function update(uint newBalance) public { balances[msg.sender] = newBalance; }}contract MappingUser { function f() public returns (uint) { MappingExample m = new MappingExample(); m.update(100); return m.balances(this); }} 注意:映射并未提供迭代输出的方法,可以自行实现一个这样的数据结构。参考iterable mapping 参考文档Solidity官方文档 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Solidity","slug":"ethereum/Solidity","permalink":"https://learnblockchain.cn/categories/ethereum/Solidity/"}],"tags":[{"name":"Solidity手册","slug":"Solidity手册","permalink":"https://learnblockchain.cn/tags/Solidity手册/"}]},{"title":"智能合约语言 Solidity 教程系列5 - 数组介绍","slug":"solidity-arrays","date":"2017-12-21T03:55:18.000Z","updated":"2019-04-05T10:29:30.963Z","comments":true,"path":"2017/12/21/solidity-arrays/","link":"","permalink":"https://learnblockchain.cn/2017/12/21/solidity-arrays/","excerpt":"Solidity 教程系列第5篇 - Solidity 数组介绍。Solidity 系列完整的文章列表请查看分类-Solidity。","text":"Solidity 教程系列第5篇 - Solidity 数组介绍。Solidity 系列完整的文章列表请查看分类-Solidity。 写在前面Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 本文前半部分是参考Solidity官方文档(当前最新版本:0.4.20)进行翻译,后半部分对官方文档中没有提供代码的知识点补充代码说明(订阅专栏阅读)。 数组(Arrays)数组可以声明时指定长度,也可以是动态变长。对storage存储的数组来说,元素类型可以是任意的,类型可以是数组,映射类型,结构体等。但对于memory的数组来说。如果作为public函数的参数,它不能是映射类型的数组,只能是支持ABI的类型。 一个元素类型为T,固定长度为k的数组,可以声明为T[k],而一个动态大小(变长)的数组则声明为T[]。还可以声明一个多维数组,如声明一个类型为uint的数组长度为5的变长数组(5个元素都是变长数组),可以声明为uint[][5]。(注意,相比非区块链语言,多维数组的长度声明是反的。) 要访问第三个动态数组的第二个元素,使用x[2][1]。数组的序号是从0开始的,序号顺序与定义相反。 bytes和string是一种特殊的数组。bytes类似byte[],但在外部函数作为参数调用中,bytes会进行压缩打包。string类似bytes,但不提供长度和按序号的访问方式(目前)。所以应该尽量使用bytes而不是byte[]。 可以将字符串s通过bytes(s)转为一个bytes,可以通过bytes(s).length获取长度,bytes(s)[n]获取对应的UTF-8编码。通过下标访问获取到的不是对应字符,而是UTF-8编码,比如中文编码是多字节,变长的,所以下标访问到的只是其中的一个编码。类型为数组的状态变量,可以标记为public,从而让Solidity创建一个访问器,如果要访问数组的某个元素,指定数字下标就好了。(稍后代码事例) 创建内存数组可使用new关键字创建一个memory的数组。与stroage数组不同的是,你不能通过.length的长度来修改数组大小属性。我们来看看下面的例子:123456789101112pragma solidity ^0.4.16;contract C { function f(uint len) public pure { uint[] memory a = new uint[](7); //a.length = 100; // 错误 bytes memory b = new bytes(len); // Here we have a.length == 7 and b.length == len a[6] = 8; }} 数组常量及内联数组数组常量,是一个数组表达式(还没有赋值到变量)。下面是一个简单的例子:12345678910pragma solidity ^0.4.16;contract C { function f() public pure { g([uint(1), 2, 3]); } function g(uint[3] _data) public pure { // ... }} 通过数组常量,创建的数组是memory的,同时还是定长的。元素类型则是使用刚好能存储的元素的能用类型,比如[1, 2, 3],只需要uint8即可存储,它的类型是uint8[3] memory。 由于g()方法的参数需要的是uint(默认的uint表示的其实是uint256),所以需要对第一个元素进行类型转换,使用uint(1)来进行这个转换。 还需注意的一点是,定长数组,不能与变长数组相互赋值,我们来看下面的代码:12345678910// 无法编译pragma solidity ^0.4.0;contract C { function f() public { // The next line creates a type error because uint[3] memory // cannot be converted to uint[] memory. uint[] x = [uint(1), 3, 4]; }} 已经计划在未来移除这样的限制。当前因为ABI传递数组还有些问题。 成员length属性数组有一个.length属性,表示当前的数组长度。storage的变长数组,可以通过给.length赋值调整数组长度。memory的变长数组不支持。不能通过访问超出当前数组的长度的方式,来自动实现改变数组长度。memory数组虽然可以通过参数,灵活指定大小,但一旦创建,大小不可调整。 push方法storage的变长数组和bytes都有一个push方法(string没有),用于附加新元素到数据末端,返回值为新的长度。 限制情况当前在external函数中,不能使用多维数组。 另外,基于EVM的限制,不能通过外部函数返回动态的内容。123contract C { function f() returns (uint[]) { ... } } 在这个的例子中,如果通过web.js调用能返回数据,但从Solidity中调用不能返回数据。一种绕过这个问题的办法是使用一个非常大的静态数组。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556pragma solidity ^0.4.16;contract ArrayContract { uint[2**20] m_aLotOfIntegers; // 这里不是两个动态数组的数组,而是一个动态数组里,每个元素是长度为二的数组。 bool[2][] m_pairsOfFlags; // newPairs 存在 memory里,因为是函数参数 function setAllFlagPairs(bool[2][] newPairs) public { m_pairsOfFlags = newPairs; } function setFlagPair(uint index, bool flagA, bool flagB) public { // 访问不存在的index会抛出异常 m_pairsOfFlags[index][0] = flagA; m_pairsOfFlags[index][1] = flagB; } function changeFlagArraySize(uint newSize) public { // 如果新size更小, 移除的元素会被销毁 m_pairsOfFlags.length = newSize; } function clear() public { // 销毁 delete m_pairsOfFlags; delete m_aLotOfIntegers; // 同销毁一样的效果 m_pairsOfFlags.length = 0; } bytes m_byteData; function byteArrays(bytes data) public { // byte arrays (\"bytes\") are different as they are stored without padding, // but can be treated identical to \"uint8[]\" m_byteData = data; m_byteData.length += 7; m_byteData[3] = byte(8); delete m_byteData[2]; } function addFlag(bool[2] flag) public returns (uint) { return m_pairsOfFlags.push(flag); } function createMemoryArray(uint size) public pure returns (bytes) { // Dynamic memory arrays are created using `new`: uint[2][] memory arrayOfPairs = new uint[2][](size); // Create a dynamic byte array: bytes memory b = new bytes(200); for (uint i = 0; i < b.length; i++) b[i] = byte(i); return b; }} 补充事例说明事例代码及讲解,请订阅区块链技术查看。 参考文档Solidity官方文档-数组 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Solidity","slug":"ethereum/Solidity","permalink":"https://learnblockchain.cn/categories/ethereum/Solidity/"}],"tags":[{"name":"Solidity手册","slug":"Solidity手册","permalink":"https://learnblockchain.cn/tags/Solidity手册/"}]},{"title":"智能合约语言 Solidity 教程系列4 - 数据存储位置分析","slug":"solidity_reftype_datalocation","date":"2017-12-21T03:51:02.000Z","updated":"2019-04-05T10:29:30.638Z","comments":true,"path":"2017/12/21/solidity_reftype_datalocation/","link":"","permalink":"https://learnblockchain.cn/2017/12/21/solidity_reftype_datalocation/","excerpt":"Solidity教程系列第4篇 - Solidity数据位置分析。Solidity 系列完整的文章列表请查看分类-Solidity。","text":"Solidity教程系列第4篇 - Solidity数据位置分析。Solidity 系列完整的文章列表请查看分类-Solidity。 写在前面Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 这部分的内容官方英文文档讲的不是很透,因此我在参考Solidity官方文档(当前最新版本:0.4.20)的同时加入了深入分析部分,欢迎订阅专栏。 数据位置(Data location)在系列第一篇,我们提到 Solidity 类型分为两类:值类型(Value Type) 及 引用类型(Reference Types),前面我们已经介绍完了值类型,接下来会介绍引用类型。 引用类型是一个复杂类型,占用的空间通常超过256位, 拷贝时开销很大,因此我们需要考虑将它们存储在什么位置,是memory(内存中,数据不是永久存在)还是storage(永久存储在区块链中)所有的复杂类型如数组(arrays)和结构体(struct)有一个额外的属性:数据的存储位置(data location)。可为memory和storage。 根据上下文的不同,大多数时候数据位置有默认值,也通过指定关键字storage和memory修改它。 函数参数(包含返回的参数)默认是memory。局部复杂类型变量(local variables)和 状态变量(state variables) 默认是storage。 局部变量:局部作用域(越过作用域即不可被访问,等待被回收)的变量,如函数内的变量。状态变量:合约内声明的公有变量 还有一个存储位置是:calldata,用来存储函数参数,是只读的,不会永久存储的一个数据位置。外部函数的参数(不包括返回参数)被强制指定为calldata。效果与memory差不多。 数据位置指定非常重要,因为他们影响着赋值行为。在memory和storage之间或与状态变量之间相互赋值,总是会创建一个完全独立的拷贝。而将一个storage的状态变量,赋值给一个storage的局部变量,是通过引用传递。所以对于局部变量的修改,同时修改关联的状态变量。另一方面,将一个memory的引用类型赋值给另一个memory的引用,不会创建拷贝(即:memory之间是引用传递)。 注意:不能将memory赋值给局部变量。 对于值类型,总是会进行拷贝。 下面看一段代码:1234567891011121314151617181920212223242526pragma solidity ^0.4.0;contract C { uint[] x; // x的存储位置是storage // memoryArray的存储位置是 memory function f(uint[] memoryArray) public { x = memoryArray; // 从 memory 复制到 storage var y = x; // storage 引用传递局部变量y(y 是一个 storage 引用) y[7]; // 返回第8个元素 y.length = 2; // x同样会被修改 delete x; // y同样会被修改 // 错误, 不能将memory赋值给局部变量 // y = memoryArray; // 错误,不能通过引用销毁storage // delete y; g(x); // 引用传递, g可以改变x的内容 h(x); // 拷贝到memory, h无法改变x的内容 } function g(uint[] storage storageArray) internal {} function h(uint[] memoryArray) public {}} 总结强制的数据位置(Forced data location) 外部函数(External function)的参数(不包括返回参数)强制为:calldata 状态变量(State variables)强制为: storage 默认数据位置(Default data location) 函数参数及返回参数:memory 复杂类型的局部变量:storage 深入分析storage 存储结构是在合约创建的时候就确定好了的,它取决于合约所声明状态变量。但是内容可以被(交易)调用改变。 Solidity 称这个为状态改变,这也是合约级变量称为状态变量的原因。也可以更好的理解为什么状态变量都是storage存储。 memory 只能用于函数内部,memory 声明用来告知EVM在运行时创建一块(固定大小)内存区域给变量使用。 storage 在区块链中是用key/value的形式存储,而memory则表现为字节数组 关于栈(stack)EVM是一个基于栈的语言,栈实际是在内存(memory)的一个数据结构,每个栈元素占为256位,栈最大长度为1024。值类型的局部变量是存储在栈上。 不同存储的消耗(gas消耗) storage 会永久保存合约状态变量,开销最大 memory 仅保存临时变量,函数调用之后释放,开销很小 stack 保存很小的局部变量,几乎免费使用,但有数量限制。 参考资料Solidity官方文档-类型 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Solidity","slug":"ethereum/Solidity","permalink":"https://learnblockchain.cn/categories/ethereum/Solidity/"}],"tags":[{"name":"Solidity手册","slug":"Solidity手册","permalink":"https://learnblockchain.cn/tags/Solidity手册/"}]},{"title":"智能合约语言 Solidity 教程系列3 - 函数类型","slug":"solidity_func","date":"2017-12-12T07:25:59.000Z","updated":"2019-04-05T10:29:30.978Z","comments":true,"path":"2017/12/12/solidity_func/","link":"","permalink":"https://learnblockchain.cn/2017/12/12/solidity_func/","excerpt":"Solidity 教程系列第三篇 - Solidity 函数类型介绍。Solidity 系列完整的文章列表请查看分类-Solidity。","text":"Solidity 教程系列第三篇 - Solidity 函数类型介绍。Solidity 系列完整的文章列表请查看分类-Solidity。 写在前面Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 本文前半部分是参考Solidity 官方文档(当前最新版本:0.4.20)进行翻译,后半部分函数可见性( public, external, internal, privite )深度分析(仅针对专栏订阅用户)。 函数类型(Function Types)函数也是一种类型,且属于值类型。可以将一个函数赋值给一个函数类型的变量。还可以将一个函数作为参数进行传递。也可以在函数调用中返回一个函数。函数类型有两类:内部(internal)和外部(external)函数 内部(internal)函数只能在当前合约内被调用(在当前的代码块内,包括内部库函数,和继承的函数中)。外部(external)函数由地址和函数方法签名两部分组成,可作为外部函数调用的参数,或返回值。 函数类型定义如下:1function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)] 如果函数不需要返回,则省去returns ()函数类型默认是internal, 因此internal可以省去。但以此相反,合约中函数本身默认是public的, 仅仅是当作类型名使用时默认是internal的。 有两个方式访问函数,一种是直接用函数名f, 一种是this.f, 前者用于内部函数,后者用于外部函数。 如果一个函数变量没有初始化,直接调用它将会产生异常。如果delete了一个函数后调用,也会发生同样的异常。 如果外部函数类型在Solidity的上下文环境以外的地方使用,他们会被视为function类型。它会编码为20字节的函数所在地址,和在它之前的4字节的函数方法签名一起作为bytes24类型。合约中的public的函数,可以使用internal和external两种方式来调用。internal访问形式为f, external访问形式为this.f 成员: 属性 selector public (或 external) 函数有一个特殊的成员selector, 它对应一个ABI 函数选择器。 1234567pragma solidity ^0.4.16;contract Selector {function f() public view returns (bytes4) { return this.f.selector;}} 下面的代码显示内部(internal)函数类型的使用: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748pragma solidity ^0.4.16;library ArrayUtils { // internal functions can be used in internal library functions because // they will be part of the same code context function map(uint[] memory self, function (uint) pure returns (uint) f) internal pure returns (uint[] memory r) { r = new uint[](self.length); for (uint i = 0; i < self.length; i++) { r[i] = f(self[i]); } } function reduce( uint[] memory self, function (uint, uint) pure returns (uint) f ) internal pure returns (uint r) { r = self[0]; for (uint i = 1; i < self.length; i++) { r = f(r, self[i]); } } function range(uint length) internal pure returns (uint[] memory r) { r = new uint[](length); for (uint i = 0; i < r.length; i++) { r[i] = i; } }}contract Pyramid { using ArrayUtils for *; function pyramid(uint l) public pure returns (uint) { return ArrayUtils.range(l).map(square).reduce(sum); } function square(uint x) internal pure returns (uint) { return x * x; } function sum(uint x, uint y) internal pure returns (uint) { return x + y; }} 下面的代码显示外部(external)函数类型的使用:1234567891011121314151617181920212223242526272829pragma solidity ^0.4.11;contract Oracle { struct Request { bytes data; function(bytes memory) external callback; } Request[] requests; event NewRequest(uint); function query(bytes data, function(bytes memory) external callback) public { requests.push(Request(data, callback)); NewRequest(requests.length - 1); } function reply(uint requestID, bytes response) public { // Here goes the check that the reply comes from a trusted source requests[requestID].callback(response); }}contract OracleUser { Oracle constant oracle = Oracle(0x1234567); // known contract function buySomething() { oracle.query(\"USD\", this.oracleResponse); } function oracleResponse(bytes response) public { require(msg.sender == address(oracle)); // Use the data }} 函数可见性分析 public - 任意访问 private - 仅当前合约内 internal - 仅当前合约及所继承的合约 external - 仅外部访问(在内部也只能用外部访问方式访问) public 还是 external 最佳实践请订阅区块链技术查看。 参考文档Solidity官方文档-类型 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Solidity","slug":"ethereum/Solidity","permalink":"https://learnblockchain.cn/categories/ethereum/Solidity/"}],"tags":[{"name":"Solidity","slug":"Solidity","permalink":"https://learnblockchain.cn/tags/Solidity/"}]},{"title":"智能合约语言 Solidity 教程系列2 - 地址类型介绍","slug":"solidity2","date":"2017-12-12T07:25:59.000Z","updated":"2019-04-05T10:29:30.983Z","comments":true,"path":"2017/12/12/solidity2/","link":"","permalink":"https://learnblockchain.cn/2017/12/12/solidity2/","excerpt":"Solidity教程系列第二篇 - Solidity地址类型介绍.Solidity 系列完整的文章列表请查看分类-Solidity。","text":"Solidity教程系列第二篇 - Solidity地址类型介绍.Solidity 系列完整的文章列表请查看分类-Solidity。 写在前面Solidity是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么 本文前半部分是参考Solidity官方文档(当前最新版本:0.4.20)进行翻译,后半部分是结合实际合约代码实例说明类型的使用(仅针对专栏订阅用户)。 地址类型(Address)地址类型address是一个值类型, 地址: 20字节(一个以太坊地址的长度),地址类型也有成员,地址是所有合约的基础支持的运算符: <=, <, ==, !=, >= 和 > 注意:从0.5.0开始,合约不再继承自地址类型,但仍然可以显式转换为地址。 地址类型的成员 balance 属性及transfer() 函数这里是地址类型相关成员的快速索引 balance用来查询账户余额,transfer()用来发送以太币(以wei为单位)。 如: 123address x = 0x123;address myAddress = this;if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10); 注解:如果x是合约地址,合约的回退函数(fallback 函数)会随transfer调用一起执行(这个是EVM特性),如果因gas耗光或其他原因失败,转移交易会还原并且合约会抛异常停止。 关于回退函数(fallback 函数),简单来说它是合约中无函数名函数,下面代码事例中,进进一步讲解回退函数(fallback) 的使用。 send() 函数 send 与transfer对应,但更底层。如果执行失败,transfer不会因异常停止,而send会返回false。 警告:send() 执行有一些风险:如果调用栈的深度超过1024或gas耗光,交易都会失败。因此,为了保证安全,必须检查send的返回值,如果交易失败,会回退以太币。如果用transfer会更好。 call(), callcode() 和 delegatecall() 函数 为了和非ABI协议的合约进行交互,可以使用call() 函数, 它用来向另一个合约发送原始数据,支持任何类型任意数量的参数,每个参数会按规则(ABI协议)打包成32字节并一一拼接到一起。一个例外是:如果第一个参数恰好4个字节,在这种情况下,会被认为根据ABI协议定义的函数器指定的函数签名而直接使用。如果仅想发送消息体,需要避免第一个参数是4个字节。如下面的例子: 123address nameReg = 0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2;nameReg.call("register", "MyName");nameReg.call(bytes4(keccak256("fun(uint256)")), a); call函数返回一个bool值,以表明执行成功与否。正常结束返回true,异常终止返回false。但无法获取到结果数据,因为需要提前知道返回的数据的编码和数据大小(因不知道对方使用的协议格式,所以也不会知道返回的结果如何解析)。 还可以提供.gas()修饰器进行调用: 1namReg.call.gas(1000000)("register", "MyName"); 类似还可以提供附带以太币: 1nameReg.call.value(1 ether)("register", "MyName"); 修饰器可以混合使用,修饰器调用顺序无所谓。 1nameReg.call.gas(1000000).value(1 ether)("register", "MyName"); 注解:目前还不能在重载函数上使用gas或value修饰符,A workaround is to introduce a special case for gas and value and just re-check whether they are present at the point of overload resolution.(这句我怕翻译的不准确,引用原文) 同样我们也可以使用delegatecall(),它与call方法的区别在于,仅仅是代码会执行,而其它方面,如(存储,余额等)都是用的当前的合约的数据。delegatecall()方法的目的是用来执行另一个合约中的库代码。所以开发者需要保证两个合约中的存储变量能兼容,来保证delegatecall()能顺利执行。在homestead阶段之前,仅有一个受限的callcode()方法可用,但callcode未提供对msg.sender,msg.value的访问权限。 上面的这三个方法call(),delegatecall(),callcode()都是底层的消息传递调用,最好仅在万不得已才进行使用,因为他们破坏了Solidity的类型安全。 .gas() 在call(), callcode() 和 delegatecall() 函数下都可以使用, delegatecall()不支持.value() 注解:所有合约都继承了address的成员,因此可以使用this.balance查询余额。callcode不鼓励使用,以后会移除。 警告:上述的函数都是底层的函数,使用时要异常小心。当调用一个未知的,可能是恶意的合约时,当你把控制权交给它,它可能回调回你的合约,所以要准备好在调用返回时,应对你的状态变量可能被恶意篡改的情况。 地址常量(Address Literals)一个能通过地址合法性检查(address checksum test)十六进制常量就会被认为是地址,如0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF。而不能通过地址合法性检查的39到41位长的十六进制常量,会提示一个警告,被视为普通的有理数常量。 地址合法性检查定义在EIP-55 合约事例讲解合约事例代码123456789101112131415161718192021222324252627282930313233343536373839404142434445pragma solidity ^0.4.0;contract AddrTest{ event logdata(bytes data); function() payable { logdata(msg.data); } function getBalance() returns (uint) { return this.balance; } uint score = 0; function setScore(uint s) public { score = s; } function getScore() returns ( uint){ return score; }}contract CallTest{ function deposit() payable { } event logSendEvent(address to, uint value); function transferEther(address towho) payable { towho.transfer(10); logSendEvent(towho, 10); } function callNoFunc(address addr) returns (bool){ return addr.call(\"tinyxiong\", 1234); } function callfunc(address addr) returns (bool){ bytes4 methodId = bytes4(keccak256(\"setScore(uint256)\")); return addr.call(methodId, 100); } function getBalance() returns (uint) { return this.balance; } } 代码运行及讲解代码运行及讲解,请订阅区块链技术查看。 本文现在有对应的视频教程,如果文章没看明白,可以选择观看视频学习,戳视频介绍。 参考文档Solidity官方文档-类型 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Solidity","slug":"ethereum/Solidity","permalink":"https://learnblockchain.cn/categories/ethereum/Solidity/"}],"tags":[{"name":"Solidity","slug":"Solidity","permalink":"https://learnblockchain.cn/tags/Solidity/"}]},{"title":"比特币如何达成共识 - 最长链的选择","slug":"bitcoin-sonsensus","date":"2017-12-07T08:12:29.000Z","updated":"2019-04-05T10:29:30.986Z","comments":true,"path":"2017/12/07/bitcoin-sonsensus/","link":"","permalink":"https://learnblockchain.cn/2017/12/07/bitcoin-sonsensus/","excerpt":"比特币没有中心机构,几乎所有的完整节点都有一份公共总帐本,那么大家如何达成共识:确认哪一份才是公认权威的总账本呢?","text":"比特币没有中心机构,几乎所有的完整节点都有一份公共总帐本,那么大家如何达成共识:确认哪一份才是公认权威的总账本呢? 为什么要遵守协议这其实是一个经济问题,在经济活动中的每个人都是自私自利的,追求的是利益的最大化,一个节点工作量只有在其他的节点认同其是有效的(打包的新区块,其他的节点只有验证通过才会加入到区块链中,并在网络上传播),才能够过得收益,而只有遵守规则才会得到其他的节点认同。因此,基于逐利,节点就会自发的遵守协议。共识就是数以万计的独立节点遵守了简单的规则(通过异步交互)自发形成的。 共识:共同遵守的协议规范 去中心化共识在工作量证明一篇,我们了解通过工作量证明来竞争记账,权威的总帐本是怎么达到共识的,没有完全说清楚,今天补上,实际上,比特币的共识由所有节点的4个独立过程相互作用而产生: 每个节点(挖矿节点)依据标准对每个交易进行独立验证 挖矿节点通过完成工作量证明,将交易记录独立打包进新区块 每个节点独立的对新区块进行校验并组装进区块链 每个节点对区块链进行独立选择,在工作量证明机制下选择累计工作量最大的区块链 共识最终目的是保证比特币不停的在工作量最大的区块链上运转,工作量最大的区块链就是权威的公共总帐本。 第1 2 3步在比特币如何挖矿-工作量证明一篇有提到过,下面着重讲第4步。 最长链的选择先来一个定义,把累计了最多难度的区块链。在一般情况下,也是包含最多区块的那个链称为主链每一个(挖矿)节点总是选择并尝试延长主链。 分叉当有两名矿工在几乎在相同的时间内,各自都算得了工作量证明解,便立即传播自己的“获胜”区块到网络中,先是传播给邻近的节点而后传播到整个网络。每个收到有效区块的节点都会将其并入并延长区块链。当这个两个区块传播时,一些节点首先收到#3458A, 一些节点首先收到#3458B,这两个候选区块(通常这两个候选区块会包含几乎相同的交易)都是主链的延伸,分叉就会产生,这时分叉出有竞争关系的两条链,如图:两个块都收到的节点,会把其中有更多工作量的一条会继续作为主链,另一条作为备用链保存(保存是因为备用链将来可能会超过主链难度称为新主链)。 分叉解决收到#3458A的(挖矿)节点,会立刻以这个区块为父区块来产生新的候选区块,并尝试寻找这个候选区块的工作量证明解。同样地,接受#3458B区块的节点会以这个区块为链的顶点开始生成新块,延长这个链(下面称为B链)。这时总会有一方抢先发现工作量证明解并将其传播出去,假设以#3458B为父区块的工作量证明首先解出,如图: 当原本以#3458A为父区块求解的节点在收到#3458B, #3459B之后,会立刻将B链作为主链(因为#3458A为顶点的链已经不是最长链了)继续挖矿。 节点也有可能先收到#3459B,再收到#3458B,收到#3459B时,会被认为是“孤块“(因为还找不到#3459B的父块#3458B)保存在孤块池中,一旦收到父块#3458B时,节点就会将孤块从孤块池中取出,并且连接到它的父区块,让它作为区块链的一部分。 比特币将区块间隔设计为10分钟,是在更快速的交易确认和更低的分叉概率间作出的妥协。更短的区块产生间隔会让交易确认更快地完成,也会导致更加频繁地区块链分叉。与之相对地,长的间隔会减少分叉数量,却会导致更长的确认时间。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"比特币","slug":"bitcoin","permalink":"https://learnblockchain.cn/categories/bitcoin/"}],"tags":[{"name":"比特币","slug":"比特币","permalink":"https://learnblockchain.cn/tags/比特币/"},{"name":"共识协议","slug":"共识协议","permalink":"https://learnblockchain.cn/tags/共识协议/"}]},{"title":"智能合约语言 Solidity 教程系列1 - 类型介绍","slug":"solidity1","date":"2017-12-05T07:25:59.000Z","updated":"2019-04-05T10:29:30.629Z","comments":true,"path":"2017/12/05/solidity1/","link":"","permalink":"https://learnblockchain.cn/2017/12/05/solidity1/","excerpt":"现在的Solidity中文文档,要么翻译的太烂,要么太旧,决定重新翻译下。尤其点名批评极客学院名为《Solidity官方文档中文版》的翻译,机器翻译的都比它好,大家还是别看了。","text":"现在的Solidity中文文档,要么翻译的太烂,要么太旧,决定重新翻译下。尤其点名批评极客学院名为《Solidity官方文档中文版》的翻译,机器翻译的都比它好,大家还是别看了。 写在前面Solidity是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,如果你还不了解,建议你先看以太坊是什么Solidity教程会是一系列文章,本文是第一篇:介绍Solidity的变量类型。Solidity 系列完整的文章列表请查看分类-Solidity。 本文前半部分是参考Solidity官方文档(当前最新版本:0.4.24)进行翻译,后半部分是结合实际合约代码实例说明类型的使用(仅针对专栏订阅用户)。 类型Solidity是一种静态类型语言,意味着每个变量(本地或状态变量)需要在编译时指定变量的类型(或至少可以推倒出类型)。Solidity提供了一些基本类型可以用来组合成复杂类型。 Solidity类型分为两类: 值类型(Value Type) - 变量在赋值或传参时,总是进行值拷贝。 引用类型(Reference Types) 值类型(Value Type)值类型包含: 布尔类型(Booleans) 整型(Integers) 定长浮点型(Fixed Point Numbers) 定长字节数组(Fixed-size byte arrays) 有理数和整型常量(Rational and Integer Literals) 字符串常量(String literals) 十六进制常量(Hexadecimal literals) 枚举(Enums) 函数类型(Function Types) 地址类型(Address) 地址常量(Address Literals) 函数类型及地址类型(Address)有单独的博文,请点击查看。 布尔类型(Booleans)布尔(bool):可能的取值为常量值true和false。 布尔类型支持的运算符有: !逻辑非 && 逻辑与 || 逻辑或 == 等于 != 不等于 注意:运算符&&和||是短路运算符,如f(x)||g(y),当f(x)为真时,则不会继续执行g(y)。 整型(Integers)int/uint: 表示有符号和无符号不同位数整数。支持关键字uint8 到 uint256 (以8步进),uint 和 int 默认对应的是 uint256 和 int256。 支持的运算符: 比较运算符: <=, < , ==, !=, >=, > (返回布尔值:true 或 false) 位操作符: &,|,^(异或),~(位取反) 算术操作符:+,-,一元运算-,一元运算+,,/, %(取余数), **(幂), << (左移位), >>(右移位) 说明: 整数除法总是截断的,但如果运算符是字面量(字面量稍后讲),则不会截断。 整数除0会抛异常。 移位运算的结果的正负取决于操作符左边的数。x << y 和 x 2**y 是相等, x >> y 和 x / 2**y 是相等的。 不能进行负移位,即操作符右边的数不可以为负数,否则会抛出运行时异常。 注意:Solidity中,右移位是和除等价的,因此右移位一个负数,向下取整时会为0,而不像其他语言里为无限负小数。 定长浮点型(Fixed Point Numbers)注意:定长浮点型 Solidity(发文时)还不完全支持,它可以用来声明变量,但不可以用来赋值。 fixed/ufixed: 表示有符号和无符号的固定位浮点数。关键字为ufixedMxN 和 ufixedMxN。M表示这个类型要占用的位数,以8步进,可为8到256位。N表示小数点的个数,可为0到80之间 支持的运算符: 比较运算符: <=, < , ==, !=, >=, > (返回布尔值:true 或 false) 算术操作符:+,-,一元运算-,一元运算+,,/, %(取余数)注意:它和大多数语言的float和double不一样,*M是表示整个数占用的固定位数,包含整数部分和小数部分。因此用一个小位数(M较小)来表示一个浮点数时,小数部分会几乎占用整个空间。 定长字节数组(Fixed-size byte arrays)关键字有:bytes1, bytes2, bytes3, …, bytes32。(以步长1递增)byte代表bytes1。 支持的运算符: 比较符: <=, <, ==, !=, >=, > (返回bool) 位操作符: &, |, ^ (按位异或),~(按位取反), << (左移位), >> (右移位) 索引(下标)访问: 如果x是bytesI,当0 <= k < I ,则x[k]返回第k个字节(只读)。 移位运算和整数类似,移位运算的结果的正负取决于操作符左边的数,且不能进行负移位。如可以-5<<1, 不可以5<<-1 成员变量:.length:表示这个字节数组的长度(只读)。 补充小技巧: 如何在Remix中把内容传递给函数的定长字节数组参数,答案是使用16进制形式(0x1122)传递。如果参数是定长字节数组的数组 bytes32[],则在Remix中参数内容传递形式为:[“0x00”,”0x0a”]。 变长(动态分配大小)字节数组(Dynamically-sized byte array) bytes:动态分配大小字节数组, 参见Arrays,不是值类型! string:动态分配大小UTF8编码的字符类型,参看Arrays。不是值类型! 根据经验:bytes用来存储任意长度的字节数据,string用来存储任意长度的(UTF-8编码)的字符串数据。如果长度可以确定,尽量使用定长的如byte1到byte32中的一个,因为这样更省空间。 有理数和整型常量(Rational and Integer Literals) 也有人把Literals翻译为字面量 整型常量是有一系列0-9的数字组成,10进制表示,比如:8进制是不存在的,前置0在Solidity中是无效的。 10进制小数常量(Decimal fraction literals)带了一个., 在.的两边至少有一个数字,有效的表示如:1., .1 和 1.3. 科学符号也支持,基数可以是小数,指数必须是整数, 有效的表示如: 2e10, -2e10, 2e-10, 2.5e1。 数字常量表达式本身支持任意精度,也就是可以不会运算溢出,或除法截断。但当它被转换成对应的非常量类型,或者将他们与非常量进行运算,则不能保证精度了。如:(2*800 + 1) - 2*800的结果为1(uint8整类) ,尽管中间结果已经超过计算机字长。另外:.5 * 8的结果是4,尽管有非整形参与了运算。 只要操作数是整形,整型支持的运算符都适用于整型常量表达式。如果两个操作数是小数,则不允许进行位运算,指数也不能是小数。 注意:Solidity对每一个有理数都有一个数值常量类型。整数常量和有理数常量从属于数字常量。所有的数字常表达式的结果都属于数字常量。所以1 + 2和2 + 1都属于同样的有理数的数字常量3 警告:整数常量除法,在早期的版本中是被截断的,但现在可以被转为有理数了,如5/2的值为 2.5 注意:数字常量表达式,一旦其中含有常量表达式,它就会被转为一个非常量类型。下面代码中表达式的结果将会被认为是一个有理数:12uint128 a = 1;uint128 b = 2.5 + a + 0.5; 上述代码编译不能通过,因为b会被编译器认为是小数型。 字符串常量字符串常量是指由单引号,或双引号引起来的字符串 (“foo” or ‘bar’)。字符串并不像C语言,包含结束符,”foo”这个字符串大小仅为三个字节。和整数常量一样,字符串的长度类型可以是变长的。字符串可以隐式的转换为byte1,…byte32 如果适合,也会转为bytes或string。 字符串常量支持转义字符,比如\\n,\\xNN,\\uNNNN。其中\\xNN表示16进制值,最终转换合适的字节。而\\uNNNN表示Unicode编码值,最终会转换为UTF8的序列。 十六进制常量(Hexadecimal literals)十六进制常量,以关键字hex打头,后面紧跟用单或双引号包裹的字符串,内容是十六进制字符串,如hex”001122ff”。它的值会用二进制来表示。 十六进制常量和字符串常量类似,也可以转换为字节数组。 枚举(Enums)在Solidity中,枚举可以用来自定义类型。它可以显示的转换与整数进行转换,但不能进行隐式转换。显示的转换会在运行时检查数值范围,如果不匹配,将会引起异常。枚举类型应至少有一名成员。下面是一个枚举的例子:123456789101112131415161718192021222324pragma solidity ^0.4.0;contract test { enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill } ActionChoices choice; ActionChoices constant defaultChoice = ActionChoices.GoStraight; function setGoStraight() { choice = ActionChoices.GoStraight; } // Since enum types are not part of the ABI, the signature of "getChoice" // will automatically be changed to "getChoice() returns (uint8)" // for all matters external to Solidity. The integer type used is just // large enough to hold all enum values, i.e. if you have more values, // `uint16` will be used and so on. function getChoice() returns (ActionChoices) { return choice; } function getDefaultChoice() returns (uint) { return uint(defaultChoice); }} 代码实例通过合约代码实例说明类型的使用,请订阅区块链技术查看。 本文现在有对应的视频教程,如果文章没看明白,可以选择观看视频学习。 参考文档Solidity官方文档-类型 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"Solidity","slug":"ethereum/Solidity","permalink":"https://learnblockchain.cn/categories/ethereum/Solidity/"}],"tags":[{"name":"Solidity手册","slug":"Solidity手册","permalink":"https://learnblockchain.cn/tags/Solidity手册/"}]},{"title":"Geth 控制台使用及 Web3.js 使用实战","slug":"geth_cmd_short","date":"2017-12-01T11:41:53.000Z","updated":"2019-04-05T10:29:30.979Z","comments":true,"path":"2017/12/01/geth_cmd_short/","link":"","permalink":"https://learnblockchain.cn/2017/12/01/geth_cmd_short/","excerpt":"在开发以太坊去中心化应用,免不了和以太坊进行交互,那就离不开Web3。Geth 控制台(REPL)实现了所有的web3 API及Admin API,使用好 Geth 就是必修课。结合Geth命令用法阅读效果更佳。","text":"在开发以太坊去中心化应用,免不了和以太坊进行交互,那就离不开Web3。Geth 控制台(REPL)实现了所有的web3 API及Admin API,使用好 Geth 就是必修课。结合Geth命令用法阅读效果更佳。 写在前面阅读本文之前,你需要对以太坊(区块链)有初步的了解,如果你不知道以太坊是什么,请先阅读以太坊是什么。如果你在我的小专栏之外的地方阅读到本文,你可能只能阅读本文的节选,阅读完整全文请订阅小专栏区块链技术 geth控制台初探 - 启动、退出安装参考智能合约开发环境搭建最简单启动方式如下:1$ geth console geth控制台启动成功之后,可以看到>提示符。退出输入exit geth 日志控制重定向日志到文件使用geth console启动是,会在当前的交互界面下时不时出现日志。可以使用以下方式把日志输出到文件。1$ geth console 2>>geth.log 可以新开一个命令行终端输入以下命令查看日志:1$ tail -f geth.log 重定向另一个终端也可以把日志重定向到另一个终端,先在想要看日志的终端输入:1$ tty 就可以获取到终端编号,如:/dev/ttys003然后另一个终端使用:1$ geth console 2>> /dev/ttys003 启动geth, 这是日志就输出到另一个终端。如果不想看到日志还可以重定向到空终端:1$ geth console 2>> /dev/null 日志级别控制使用–verbosity可以控制日志级别,如不想看到日志还可以使用:1$ geth --verbosity 0 console 启动一个开发模式测试节点1geth --datadir /home/xxx/testNet --dev console 技巧:如果我们经常使用一个方式来启动,可以把命令存为一个bash脚本。~/bin你可以放一些常用的脚本,并把~/bin加入到环境变量PATH里。 连接geth节点另外一个启动geth的方法是连接到一个geth节点:123$ geth attach ipc:/some/custom/path$ geth attach http://191.168.1.1:8545$ geth attach ws://191.168.1.1:8546 如连接刚刚打开的开发模式节点使用:1geth attach ipc:testNet/geth.ipc 更多内容请前往区块链技术小专栏查看全文链接。 如果你想学习以太坊DApp开发,这门视频课程以太坊DAPP开发实战是你不错的选择。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"geth","slug":"ethereum/geth","permalink":"https://learnblockchain.cn/categories/ethereum/geth/"}],"tags":[{"name":"Geth使用","slug":"Geth使用","permalink":"https://learnblockchain.cn/tags/Geth使用/"},{"name":"Web3.js","slug":"Web3-js","permalink":"https://learnblockchain.cn/tags/Web3-js/"}]},{"title":"以太坊客户端Geth命令用法-参数详解","slug":"geth_cmd_options","date":"2017-11-29T03:51:52.000Z","updated":"2019-04-05T10:29:30.981Z","comments":true,"path":"2017/11/29/geth_cmd_options/","link":"","permalink":"https://learnblockchain.cn/2017/11/29/geth_cmd_options/","excerpt":"Geth在以太坊智能合约开发中最常用的工具(必备开发工具),一个多用途的命令行工具。熟悉Geth可以让我们有更好的效率,大家可收藏起来作为Geth命令用法手册。 本文主要是对geth help的翻译,基于最新的geth 1.7.3-stable版本。","text":"Geth在以太坊智能合约开发中最常用的工具(必备开发工具),一个多用途的命令行工具。熟悉Geth可以让我们有更好的效率,大家可收藏起来作为Geth命令用法手册。 本文主要是对geth help的翻译,基于最新的geth 1.7.3-stable版本。 如果你还不知道geth是什么,请先阅读入门篇:以太坊是什么。更多geth实战使用方法请参考Geth控制台使用实战及Web3.js使用以下开始正文。 命令用法geth [选项] 命令 [命令选项] [参数…] 版本:1.7.3-stable 命令:account 管理账户 attach 启动交互式JavaScript环境(连接到节点) bug 上报bug Issues console 启动交互式JavaScript环境 copydb 从文件夹创建本地链 dump Dump(分析)一个特定的块存储 dumpconfig 显示配置值 export 导出区块链到文件 import 导入一个区块链文件 init 启动并初始化一个新的创世纪块 js 执行指定的JavaScript文件(多个) license 显示许可信息 makecache 生成ethash验证缓存(用于测试) makedag 生成ethash 挖矿DAG(用于测试) monitor 监控和可视化节点指标 removedb 删除区块链和状态数据库 version 打印版本号 wallet 管理Ethereum预售钱包 help,h 显示一个命令或帮助一个命令列表 ETHEREUM选项:--config value TOML 配置文件 --datadir “xxx” 数据库和keystore密钥的数据目录 --keystore keystore存放目录(默认在datadir内) --nousb 禁用监控和管理USB硬件钱包 --networkid value 网络标识符(整型, 1=Frontier, 2=Morden (弃用), 3=Ropsten, 4=Rinkeby) (默认: 1) --testnet Ropsten网络:预先配置的POW(proof-of-work)测试网络 --rinkeby Rinkeby网络: 预先配置的POA(proof-of-authority)测试网络 --syncmode "fast" 同步模式 ("fast", "full", or "light") --ethstats value 上报ethstats service URL (nodename:secret@host:port) --identity value 自定义节点名 --lightserv value 允许LES请求时间最大百分比(0 – 90)(默认值:0) --lightpeers value 最大LES client peers数量(默认值:20) --lightkdf 在KDF强度消费时降低key-derivation RAM&CPU使用 开发者(模式)选项:--dev 使用POA共识网络,默认预分配一个开发者账户并且会自动开启挖矿。 --dev.period value 开发者模式下挖矿周期 (0 = 仅在交易时) (默认: 0) ETHASH 选项:--ethash.cachedir ethash验证缓存目录(默认 = datadir目录内) --ethash.cachesinmem value 在内存保存的最近的ethash缓存个数 (每个缓存16MB ) (默认: 2) --ethash.cachesondisk value 在磁盘保存的最近的ethash缓存个数 (每个缓存16MB) (默认: 3) --ethash.dagdir "" 存ethash DAGs目录 (默认 = 用户hom目录) --ethash.dagsinmem value 在内存保存的最近的ethash DAGs 个数 (每个1GB以上) (默认: 1) --ethash.dagsondisk value 在磁盘保存的最近的ethash DAGs 个数 (每个1GB以上) (默认: 2) 交易池选项:--txpool.nolocals 为本地提交交易禁用价格豁免 --txpool.journal value 本地交易的磁盘日志:用于节点重启 (默认: "transactions.rlp") --txpool.rejournal value 重新生成本地交易日志的时间间隔 (默认: 1小时) --txpool.pricelimit value 加入交易池的最小的gas价格限制(默认: 1) --txpool.pricebump value 价格波动百分比(相对之前已有交易) (默认: 10) --txpool.accountslots value 每个帐户保证可执行的最少交易槽数量 (默认: 16) --txpool.globalslots value 所有帐户可执行的最大交易槽数量 (默认: 4096) --txpool.accountqueue value 每个帐户允许的最多非可执行交易槽数量 (默认: 64) --txpool.globalqueue value 所有帐户非可执行交易最大槽数量 (默认: 1024) --txpool.lifetime value 非可执行交易最大入队时间(默认: 3小时) 性能调优的选项:--cache value 分配给内部缓存的内存MB数量,缓存值(最低16 mb /数据库强制要求)(默认:128) --trie-cache-gens value 保持在内存中产生的trie node数量(默认:120) 帐户选项:--unlock value 需解锁账户用逗号分隔 --password value 用于非交互式密码输入的密码文件 API和控制台选项:--rpc 启用HTTP-RPC服务器 --rpcaddr value HTTP-RPC服务器接口地址(默认值:“localhost”) --rpcport value HTTP-RPC服务器监听端口(默认值:8545) --rpcapi value 基于HTTP-RPC接口提供的API --ws 启用WS-RPC服务器 --wsaddr value WS-RPC服务器监听接口地址(默认值:“localhost”) --wsport value WS-RPC服务器监听端口(默认值:8546) --wsapi value 基于WS-RPC的接口提供的API --wsorigins value websockets请求允许的源 --ipcdisable 禁用IPC-RPC服务器 --ipcpath 包含在datadir里的IPC socket/pipe文件名(转义过的显式路径) --rpccorsdomain value 允许跨域请求的域名列表(逗号分隔)(浏览器强制) --jspath loadScript JavaScript加载脚本的根路径(默认值:“.”) --exec value 执行JavaScript语句(只能结合console/attach使用) --preload value 预加载到控制台的JavaScript文件列表(逗号分隔) 网络选项:--bootnodes value 用于P2P发现引导的enode urls(逗号分隔)(对于light servers用v4+v5代替) --bootnodesv4 value 用于P2P v4发现引导的enode urls(逗号分隔) (light server, 全节点) --bootnodesv5 value 用于P2P v5发现引导的enode urls(逗号分隔) (light server, 轻节点) --port value 网卡监听端口(默认值:30303) --maxpeers value 最大的网络节点数量(如果设置为0,网络将被禁用)(默认值:25) --maxpendpeers value 最大尝试连接的数量(如果设置为0,则将使用默认值)(默认值:0) --nat value NAT端口映射机制 (any|none|upnp|pmp|extip:<IP>) (默认: “any”) --nodiscover 禁用节点发现机制(手动添加节点) --v5disc 启用实验性的RLPx V5(Topic发现)机制 --nodekey value P2P节点密钥文件 --nodekeyhex value 十六进制的P2P节点密钥(用于测试) 矿工选项:--mine 打开挖矿 --minerthreads value 挖矿使用的CPU线程数量(默认值:8) --etherbase value 挖矿奖励地址(默认=第一个创建的帐户)(默认值:“0”) --targetgaslimit value 目标gas限制:设置最低gas限制(低于这个不会被挖?) (默认值:“4712388”) --gasprice value 挖矿接受交易的最低gas价格 --extradata value 矿工设置的额外块数据(默认=client version) GAS价格选项:--gpoblocks value 用于检查gas价格的最近块的个数 (默认: 10) --gpopercentile value 建议gas价参考最近交易的gas价的百分位数,(默认: 50) 虚拟机的选项:--vmdebug 记录VM及合约调试信息 日志和调试选项:--metrics 启用metrics收集和报告 --fakepow 禁用proof-of-work验证 --verbosity value 日志详细度:0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=detail (default: 3) --vmodule value 每个模块详细度:以 <pattern>=<level>的逗号分隔列表 (比如 eth/*=6,p2p=5) --backtrace value 请求特定日志记录堆栈跟踪 (比如 “block.go:271”) --debug 突出显示调用位置日志(文件名及行号) --pprof 启用pprof HTTP服务器 --pprofaddr value pprof HTTP服务器监听接口(默认值:127.0.0.1) --pprofport value pprof HTTP服务器监听端口(默认值:6060) --memprofilerate value 按指定频率打开memory profiling (默认:524288) --blockprofilerate value 按指定频率打开block profiling (默认值:0) --cpuprofile value 将CPU profile写入指定文件 --trace value 将execution trace写入指定文件 WHISPER实验选项:--shh 启用Whisper --shh.maxmessagesize value 可接受的最大的消息大小 (默认值: 1048576) --shh.pow value 可接受的最小的POW (默认值: 0.2) 弃用选项:--fast 开启快速同步 --light 启用轻客户端模式 其他选项:–help, -h 显示帮助 版权:Copyright 2013-2017 The go-ethereum Authors 翻译说明 有些参数翻译可能有不准确的地方,请大家指正。 原文会尽量随geth升级保持更新,原始链接:https://learnblockchain.cn/2017/11/29/geth_cmd_options/ 如果你想学习以太坊DApp开发,这门视频课程以太坊DAPP开发实战是你不错的选择。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"},{"name":"geth","slug":"ethereum/geth","permalink":"https://learnblockchain.cn/categories/ethereum/geth/"}],"tags":[{"name":"Geth命令用法","slug":"Geth命令用法","permalink":"https://learnblockchain.cn/tags/Geth命令用法/"},{"name":"Geth命令参数详解","slug":"Geth命令参数详解","permalink":"https://learnblockchain.cn/tags/Geth命令参数详解/"},{"name":"Geth手册","slug":"Geth手册","permalink":"https://learnblockchain.cn/tags/Geth手册/"}]},{"title":"智能合约开发环境搭建及Hello World合约","slug":"init-env","date":"2017-11-24T11:41:53.000Z","updated":"2019-04-05T10:29:30.632Z","comments":true,"path":"2017/11/24/init-env/","link":"","permalink":"https://learnblockchain.cn/2017/11/24/init-env/","excerpt":"如果你对于以太坊智能合约开发还没有概念(本文会假设你已经知道这些概念),建议先阅读入门篇。就先学习任何编程语言一样,入门的第一个程序都是Hello World。今天我们来一步一步从搭建以太坊智能合约开发环境开始,讲解智能合约的Hello World如何编写。","text":"如果你对于以太坊智能合约开发还没有概念(本文会假设你已经知道这些概念),建议先阅读入门篇。就先学习任何编程语言一样,入门的第一个程序都是Hello World。今天我们来一步一步从搭建以太坊智能合约开发环境开始,讲解智能合约的Hello World如何编写。 开发环境搭建Solidity安装强烈建议新手使用Remix -Solidity IDE来进行开发。Remix 是一个基于浏览器的Solidity,就可以不用安装Solidity,本文的Hello World教程也将基于Remix Solidity IDE来进行。 如果你想自己安装请参考Solidity安装指引。 更新,开发环境搭建还可以看另一篇文章: 搭建智能合约开发环境Remix IDE及使用。 geth 安装Mac下安装命令如下:其他平台参考:geth官方安装指引12brew tap ethereum/ethereumbrew install ethereum brew 是 Mac 下的包管理工具,和Ubuntu里的apt-get类似 安装完以后,就是把geth控制台启动。 启动环境在入门篇讲过,geth是一个以太坊客户端,现在利用geth\b启动一个以太坊(开发者)网络节点。 1geth --datadir testNet --dev console 2>> test.log 执行命名后,会进入geth控制台,这时光标停在一个向右的箭头处,像这样: 命令参数说明(更多命令详解可阅读Geth命令用法-参数详解篇):–dev 启用开发者网络(模式),开发者网络会使用POA共识,默认预分配一个开发者账户并且会自动开启挖矿。–datadir 后面的参数是区块数据及秘钥存放目录。第一次输入命令后,它会放在当前目录下新建一个testNet目录来存放数据。console 进入控制台2>> test.log 表示把控制台日志输出到test.log文件 为了更好的理解,建议新开一个命令行终端,实时显示日志:1tail -f test.log 准备账户部署智能合约需要一个外部账户,我们先来看看分配的开发者账户,在控制台使用以下命令查看账户:1> eth.accounts 回车后,返回一个账户数组,里面有一个默认账户,如: 也可以使用personal.listAccounts查看账户, 再来看一下账户里的余额,使用一下命令:1> eth.getBalance(eth.accounts[0]) eth.accounts[0]表示账户列表第一个账户回车后,可以看到大量的余额,如:1.15792089237316195423570985008687907853269… e+77 开发者账户因余额太多,如果用这个账户来部署合约时会无法看到余额变化,为了更好的体验完整的过程,这里选择创建一个新的账户。 创建账户使用以下命令创建账户:1> personal.newAccount("TinyXiong") TinyXiong为新账户的密码,回车后,返回一个新账户。 这时我们查看账户列表:1> eth.accounts 可以看到账户数组你包含两个账户,新账户在第二个(索引为1)位置。 现在看看账户的余额:12> eth.getBalance(eth.accounts[1])0 回车后,返回的是0,新账户是0。结果如: 给新账户转账我们知道没有余额的账户是没法部署合约的,那我们就从默认账户转1以太币给新账户,使用以下命令(请使用你自己eth.accounts对应输出的账户):1eth.sendTransaction({from: '0xb0ebe17ef0e96b5c525709c0a1ede347c66bd391', to: '0xf280facfd60d61f6fd3f88c9dee4fb90d0e11dfc', value: web3.toWei(1, "ether")}) 在打开的tail -f test.log日志终端里,可以同时看到挖矿记录再次查看新账户余额,可以新账户有1个以太币 解锁账户在部署合约前需要先解锁账户(就像银行转账要输入密码一样),使用以下命令:1personal.unlockAccount(eth.accounts[1],"TinyXiong"); “TinyXiong” 是之前创建账户时的密码解锁成功后,账户就准备完毕啦,接下来就是编写合约代码。 编写合约代码现在我们来开始编写第一个智能合约代码,solidity代码如下: 123456789101112pragma solidity ^0.4.18;contract hello { string greeting; function hello(string _greeting) public { greeting = _greeting; } function say() constant public returns (string) { return greeting; }} 简单解释下,我们定义了一个名为hello的合约,在合约初始化时保存了一个字符串(我们会传入hello world),每次调用say返回字符串。把这段代码写(拷贝)到Browser-Solidity,如果没有错误,点击Details获取部署代码,如: 在弹出的对话框中找到WEB3DEPLOY部分,点拷贝,粘贴到编辑器后,修改初始化字符串为hello world。 solidity在博文写作时(2017/11/24),版本为0.4.18,solidity发展非常快,solidity版本之间有可能不能兼容,这是你可以在Browser-Solidity的Settings里选择对应的编译器版本。Browser-Solidity也不停的更新中,截图可能和你看到的界面不一样。 部署合约Browser-Solidity生成的代码,拷贝到编辑器里修改后的代码如下: 1234567891011121314var _greeting = \"Hello World\" ;var helloContract = web3.eth.contract([{\"constant\":true,\"inputs\":[],\"name\":\"say\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"name\":\"_greeting\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"}]);var hello = helloContract.new( _greeting, { from: web3.eth.accounts[1], data: '0x6060604052341561000f57600080fd5b6040516102b83803806102b8833981016040528080518201919050508060009080519060200190610041929190610048565b50506100ed565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061008957805160ff19168380011785556100b7565b828001600101855582156100b7579182015b828111156100b657825182559160200191906001019061009b565b5b5090506100c491906100c8565b5090565b6100ea91905b808211156100e65760008160009055506001016100ce565b5090565b90565b6101bc806100fc6000396000f300606060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063954ab4b214610046575b600080fd5b341561005157600080fd5b6100596100d4565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561009957808201518184015260208101905061007e565b50505050905090810190601f1680156100c65780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6100dc61017c565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101725780601f1061014757610100808354040283529160200191610172565b820191906000526020600020905b81548152906001019060200180831161015557829003601f168201915b5050505050905090565b6020604051908101604052806000815250905600a165627a7a723058204a5577bb3ad30e02f7a3bdd90eedcc682700d67fc8ed6604d38bb739c0655df90029', gas: '4700000' }, function (e, contract){ console.log(e, contract); if (typeof contract.address !== 'undefined') { console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash); } }); 第1行:修改字符串为Hello World第2行:修改合约变量名第3行:修改合约实例变量名,之后可以直接用实例调用函数。第6行:修改部署账户为新账户索引,即使用新账户来部署合约。第8行:准备付的gas费用,IDE已经帮我们预估好了。第9行:设置部署回调函数。 拷贝回geth控制台里,回车后,看到输出如:1Contract mined! address: 0x79544078dcd9d560ec3f6eff0af42a9fc84c7d19 transactionHash: 0xe2caab22102e93434888a0b8013a7ae7e804b132e4a8bfd2318356f6cf0480b3 说明合约已经部署成功。 在打开的tail -f test.log日志终端里,可以同时看到挖矿记录 现在我们查看下新账户的余额:1> eth.getBalance(eth.accounts[1]) 是不是比之前转账的余额少呀! 运行合约12> hello.say()"Hello World" 输出Hello World,我们第一个合约Hello World,成功运行了。 运行截图如下: 本文会随geth,solidity语言版本升级保持更新,查看本文原始链接:https://learnblockchain.cn/2017/11/24/init-env/ 第一个合约的意义更重要的是体验智能合约开发流程,对于初学者一些可以选择先放弃一些细节,开发流程打通之后,可以增强信心进行下一步的学习。如果你想学习以太坊DApp开发,这门视频课程以太坊DAPP开发实战是你不错的选择。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"环境","slug":"环境","permalink":"https://learnblockchain.cn/tags/环境/"},{"name":"geth安装","slug":"geth安装","permalink":"https://learnblockchain.cn/tags/geth安装/"},{"name":"第一个智能合约","slug":"第一个智能合约","permalink":"https://learnblockchain.cn/tags/第一个智能合约/"}]},{"title":"以太坊是什么 - 以太坊开发入门指南","slug":"whatiseth","date":"2017-11-20T03:51:52.000Z","updated":"2019-04-05T10:29:30.958Z","comments":true,"path":"2017/11/20/whatiseth/","link":"","permalink":"https://learnblockchain.cn/2017/11/20/whatiseth/","excerpt":"很多同学已经跃跃欲试投入到区块链开发队伍当中来,可是又感觉无从下手,本文将基于以太坊平台,以通俗的方式介绍以太坊开发中涉及的各晦涩的概念,轻松带大家入门。","text":"很多同学已经跃跃欲试投入到区块链开发队伍当中来,可是又感觉无从下手,本文将基于以太坊平台,以通俗的方式介绍以太坊开发中涉及的各晦涩的概念,轻松带大家入门。 写在前面阅读本文前,你应该大概了解区块链是什么,如果你还不了解,欢迎订阅专栏:区块链技术指引你从头开始学区块链技术。 以太坊是什么以太坊(Ethereum)是一个建立在区块链技术之上, 去中心化应用平台。它允许任何人在平台中建立和使用通过区块链技术运行的去中心化应用。 对这句话不理解的同学,姑且可以理解为以太坊是区块链里的Android,它是一个开发平台,让我们就可以像基于Android Framework一样基于区块链技术写应用。 在没有以太坊之前,写区块链应用是这样的:拷贝一份比特币代码,然后去改底层代码如加密算法,共识机制,网络协议等等(很多山寨币就是这样,改改就出来一个新币)。以太坊平台对底层区块链技术进行了封装,让区块链应用开发者可以直接基于以太坊平台进行开发,开发者只要专注于应用本身的开发,从而大大降低了难度。 目前围绕以太坊已经形成了一个较为完善的开发生态圈:有社区的支持,有很多开发框架、工具可以选择。 智能合约什么是智能合约以太坊上的程序称之为智能合约, 它是代码和数据(状态)的集合。 智能合约可以理解为在区块链上可以自动执行的(由消息驱动的)、以代码形式编写的合同(特殊的交易)。 智能合约英文是Smart Contract,和人工智能( AI:Artificial Intelligence )的智能没有关系,最早尼克萨博在95年就提出智能合约的概念,它的概念很简单,就是将法律条文写成可执行代码。当时并没有区块链,不过智能合约与区块链最配,我们知道合同都是要一式两份、三或四份,不能控制在某一方手中,这也就是去中心化。 在比特币脚本中,我们讲到过比特币的交易是可以编程的,但是比特币脚本有很多的限制,能够编写的程序也有限,而以太坊则更加完备(在计算机科学术语中,称它为是“图灵完备的”),让我们就像使用任何高级语言一样来编写几乎可以做任何事情的程序(智能合约)。 智能合约非常适合对信任、安全和持久性要求较高的应用场景,比如:数字货币、数字资产、投票、保险、金融应用、预测市场、产权所有权管理、物联网、点对点交易等等。目前除数字货币之外,真正落地的应用还不多(就像移动平台刚开始出来一样),相信1到3年内,各种杀手级会慢慢出现。 编程语言:Solidity智能合约的官方推荐的编程语言是Solidity,文件扩展名以.sol结尾。Solidity语言和JavaScript很相似,用它来开发合约并编译成以太坊虚拟机字节代码。 还有Viper,Serpent,LLL及Bamboo,建议大家还是使用Solidity。更新:Serpent官方已经不再推荐,建议Serpent的用户转换到Viper,他们都是类Python语言。 Browser-Solidity是一个浏览器的Solidity IDE, 大家可以点进去看看,以后我们更多文章介绍Solidity这个语言。 运行环境:EVMEVM(Ethereum Virtual Machine)以太坊虚拟机是以太坊中智能合约的运行环境。 Solidity之于EVM,就像之于跟JVM的关系一样,这样大家就容易理解了。以太坊虚拟机是一个隔离的环境,外部无法接触到在EVM内部运行的代码。 而EVM运行在以太坊节点上,当我们把合约部署到以太坊网络上之后,合约就可以在以太坊网络中运行了。 合约的编译以太坊虚拟机上运行的是合约的字节码形式,需要我们在部署之前先对合约进行编译,可以选择Browser-Solidity Web IDE或solc编译器。 合约的部署在以太坊上开发应用时,常常要使用到以太坊客户端(钱包)。平时我们在开发中,一般不接触到客户端或钱包的概念,它是什么呢? 以太坊客户端(钱包)以太坊客户端,其实我们可以把它理解为一个开发者工具,它提供账户管理、挖矿、转账、智能合约的部署和执行等等功能。 EVM是由以太坊客户端提供的 Geth是典型的开发以太坊时使用的客户端,基于Go语言开发。 Geth提供了一个交互式命令控制台,通过命令控制台中包含了以太坊的各种功能(API)。Geth的使用我们之后会有文章介绍,这里大家先有个概念。 \bGeth控制台和Chrome浏览器开发者工具里的面的控制台是类似的,不过Geth控制台是跑在终端里。相对于Geth,Mist则是图形化操作界面的以太坊客户端。 如何部署智能合约的部署是指把合约字节码发布到区块链上,并使用一个特定的地址来标示这个合约,这个地址称为合约账户。 以太坊中有两类账户: 外部账户该类账户被私钥控制(由人控制),没有关联任何代码。 合约账户该类账户被它们的合约代码控制且有代码与之关联。 和比特币使用UTXO的设计不一样,以太坊使用更为简单的账户概念。两类账户对于EVM来说是一样的。 外部账户与合约账户的区别和关系是这样的:一个外部账户可以通过创建和用自己的私钥来对交易进行签名,来发送消息给另一个外部账户或合约账户。在两个外部账户之间传送消息是价值转移的过程。但从外部账户到合约账户的消息会激活合约账户的代码,允许它执行各种动作(比如转移代币,写入内部存储,挖出一个新代币,执行一些运算,创建一个新的合约等等)。只有当外部账户发出指令时,合同账户才会执行相应的操作。 合约部署就是将编译好的合约字节码通过外部账号发送交易的形式部署到以太坊区块链上(由实际矿工出块之后,才真正部署成功)。 运行合约部署之后,当需要调用这个智能合约的方法时只需要向这个合约账户发送消息(交易)即可,通过消息触发后智能合约的代码就会在EVM中执行了。 Gas和云计算相似,占用区块链的资源(不管是简单的转账交易,还是合约的部署和执行)同样需要付出相应的费用(天下没有免费的午餐对不对!)。以太坊上用Gas机制来计费,Gas也可以认为是一个工作量单位,智能合约越复杂(计算步骤的数量和类型,占用的内存等),用来完成运行就需要越多Gas。任何特定的合约所需的运行合约的Gas数量是固定的,由合约的复杂度决定。而Gas价格由运行合约的人在提交运行合约请求的时候规定,以确定他愿意为这次交易愿意付出的费用:Gas价格(用以太币计价) * Gas数量。 Gas的目的是限制执行交易所需的工作量,同时为执行支付费用。当EVM执行交易时,Gas将按照特定规则被逐渐消耗,无论执行到什么位置,一旦Gas被耗尽,将会触发异常。当前调用帧所做的所有状态修改都将被回滚, 如果执行结束还有Gas剩余,这些Gas将被返还给发送账户。 如果没有这个限制,就会有人写出无法停止(如:死循环)的合约来阻塞网络。 因此实际上(把前面的内容串起来),我们需要一个有以太币余额的外部账户,来发起一个交易(普通交易或部署、运行一个合约),运行时,矿工收取相应的工作量费用。 以太坊网络有些着急的同学要问了,没有以太币,要怎么进行智能合约的开发?可以选择以下方式: 选择以太坊官网测试网络Testnet测试网络中,我们可以很容易获得免费的以太币,缺点是需要发很长时间初始化节点。 使用私有链创建自己的以太币私有测试网络,通常也称为私有链,我们可以用它来作为一个测试环境来开发、调试和测试智能合约。通过上面提到的Geth很容易就可以创建一个属于自己的测试网络,以太币想挖多少挖多少,也免去了同步正式网络的整个区块链数据。 使用开发者网络(模式)相比私有链,开发者网络(模式)下,会自动分配一个有大量余额的开发者账户给我们使用。 使用模拟环境另一个创建测试网络的方法是使用testrpc,testrpc是在本地使用内存模拟的一个以太坊环境,对于开发调试来说,更方便快捷。而且testrpc可以在启动时帮我们创建10个存有资金的测试账户。进行合约开发时,可以在testrpc中测试通过后,再部署到Geth节点中去。 更新:testrpc 现在已经并入到Truffle 开发框架中,现在名字是Ganache CLI。 Dapp:去中心化的应用程序以太坊社区把基于智能合约的应用称为去中心化的应用程序(Decentralized App)。如果我们把区块链理解为一个不可篡改的数据库,智能合约理解为和数据库打交道的程序,那就很容易理解Dapp了,一个Dapp不单单有智能合约,比如还需要有一个友好的用户界面和其他的东西。 TruffleTruffle是Dapp开发框架,他可以帮我们处理掉大量无关紧要的小事情,让我们可以迅速开始写代码-编译-部署-测试-打包DApp这个流程。 总结我们现在来总结一下,以太坊是平台,它让我们方便的使用区块链技术开发去中心化的应用,在这个应用中,使用Solidity来编写和区块链交互的智能合约,合约编写好后之后,我们需要用以太坊客户端用一个有余额的账户去部署及运行合约(使用Truffle框架可以更好的帮助我们做这些事情了)。为了开发方便,我们可以用Geth或testrpc来搭建一个测试网络。 注:本文中为了方便大家理解,对一些概念做了类比,有些严格来不是准确,不过我也认为对于初学者,也没有必要把每一个概念掌握的很细致和准确,学习是一个逐步深入的过程,很多时候我们会发现,过一段后,我们会对同一个东西有不一样的理解。 本文完,这些概念你都明白了么?现在你可以开始开发了,看看智能合约开发环境搭建及Hello World合约 我们还为区块链技术爱好者提供了系统的区块链视频教程,觉得文章学习不过瘾的同学可以戳入门视频教程及以太坊智能合约开发。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论,作为星友福利,星友可加入区块链技术交流群,群内已经聚集了300多位区块链技术牛人和爱好者。","categories":[{"name":"以太坊","slug":"ethereum","permalink":"https://learnblockchain.cn/categories/ethereum/"}],"tags":[{"name":"以太坊入门","slug":"以太坊入门","permalink":"https://learnblockchain.cn/tags/以太坊入门/"},{"name":"ethereum","slug":"ethereum","permalink":"https://learnblockchain.cn/tags/ethereum/"},{"name":"以太坊概念","slug":"以太坊概念","permalink":"https://learnblockchain.cn/tags/以太坊概念/"}]},{"title":"非对称加密技术- RSA算法数学原理分析","slug":"asy-encryption","date":"2017-11-15T02:53:27.000Z","updated":"2019-04-05T10:29:30.956Z","comments":true,"path":"2017/11/15/asy-encryption/","link":"","permalink":"https://learnblockchain.cn/2017/11/15/asy-encryption/","excerpt":"非对称加密技术,在现在网络中,有非常广泛应用。加密技术更是数字货币的基础。 所谓非对称,就是指该算法需要一对密钥,使用其中一个(公钥)加密,则需要用另一个(私钥)才能解密。但是对于其原理大部分同学应该都是一知半解,今天就来分析下经典的非对称加密算法 - RSA算法。通过本文的分析,可以更好的理解非对称加密原理,可以让我们更好的使用非对称加密技术。","text":"非对称加密技术,在现在网络中,有非常广泛应用。加密技术更是数字货币的基础。 所谓非对称,就是指该算法需要一对密钥,使用其中一个(公钥)加密,则需要用另一个(私钥)才能解密。但是对于其原理大部分同学应该都是一知半解,今天就来分析下经典的非对称加密算法 - RSA算法。通过本文的分析,可以更好的理解非对称加密原理,可以让我们更好的使用非对称加密技术。 题外话:本博客一直有打算写一系列文章通俗的密码学,昨天给站点上https, 因其中使用了RSA算法,就查了一下,发现现在网上介绍RSA算法的文章都写的太难理解了,反正也准备写密码学,就先写RSA算法吧,下面开始正文。 RSA算法原理RSA算法的基于这样的数学事实:两个大质数相乘得到的大数难以被因式分解。如:有很大质数p跟q,很容易算出N,使得 N = p * q,但给出N, 比较难找p q(没有很好的方式, 只有不停的尝试) 这其实也是单向函数的概念 下面来看看数学演算过程: 选取两个大质数p,q,计算N = p q 及 φ ( N ) = φ (p) φ (q) = (p-1) * (q-1) 三个数学概念:质数(prime numbe):又称素数,为在大于1的自然数中,除了1和它本身以外不再有其他因数。互质关系:如果两个正整数,除了1以外,没有其他公因子,我们就称这两个数是互质关系(coprime)。φ(N):叫做欧拉函数,是指任意给定正整数N,在小于等于N的正整数之中,有多少个与N构成互质关系。 如果n是质数,则 φ(n)=n-1。如果n可以分解成两个互质的整数之积, φ(n) = φ(p1p2) = φ(p1)φ(p2)。即积的欧拉函数等于各个因子的欧拉函数之积。 选择一个大于1 小于φ(N)的数e,使得 e 和 φ(N)互质 e其实是1和φ(N)之前的一个质数 计算d,使得de=1 mod φ(N) 等价于方程式 ed-1 = k φ(N) 求一组解。 d 称为e的模反元素,e 和 φ(N)互质就肯定存在d。 模反元素是指如果两个正整数a和n互质,那么一定可以找到整数b,使得ab被n除的余数是1,则b称为a的模反元素。可根据欧拉定理证明模反元素存在,欧拉定理是指若n,a互质,则:a^φ(n) ≡ 1(mod n) 及 a^φ(n) = a * a^(φ(n) - 1), 可得a的 φ(n)-1 次方,就是a的模反元素。 (N, e)封装成公钥,(N, d)封装成私钥。假设m为明文,加密就是算出密文c: m^e mod N = c (明文m用公钥e加密并和随机数N取余得到密文c)解密则是: c^d mod N = m (密文c用密钥解密并和随机数N取余得到明文m) 私钥解密这个是可以证明的,这里不展开了。 加解密步骤具体还是来看看步骤,举个例子,假设Alice和Bob又要相互通信。 Alice 随机取大质数P1=53,P2=59,那N=53*59=3127,φ(N)=3016 取一个e=3,计算出d=2011。 只将N=3127,e=3 作为公钥传给Bob(公钥公开) 假设Bob需要加密的明文m=89,c = 89^3 mod 3127=1394,于是Bob传回c=1394。 (公钥加密过程) Alice使用c^d mod N = 1394^2011 mod 3127,就能得到明文m=89。 (私钥解密过程) 假如攻击者能截取到公钥n=3127,e=3及密文c=1394,是仍然无法不通过d来进行密文解密的。 安全性分析那么,有无可能在已知n和e的情况下,推导出d?123 1. ed≡1 (mod φ(n))。只有知道e和φ(n),才能算出d。 2. φ(n)=(p-1)(q-1)。只有知道p和q,才能算出φ(n)。 3. n=pq。只有将n因数分解,才能算出p和q。 如果n可以被因数分解,d就可以算出,因此RSA安全性建立在N的因式分解上。大整数的因数分解,是一件非常困难的事情。只要密钥长度足够长,用RSA加密的信息实际上是不能被解破的。 补充模运算规则 模运算加减法: (a + b) mod p = (a mod p + b mod p) mod p (a - b) mod p = (a mod p - b mod p) mod p 模运算乘法: (a b) mod p = (a mod p b mod p) mod p 模运算幂 a ^ b mod p = ((a mod p)^b) mod p 深入浅出区块链星球为各位解答区块链技术问题,欢迎加入讨论。深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。","categories":[{"name":"其他","slug":"other","permalink":"https://learnblockchain.cn/categories/other/"}],"tags":[{"name":"非对称加密","slug":"非对称加密","permalink":"https://learnblockchain.cn/tags/非对称加密/"},{"name":"数学","slug":"数学","permalink":"https://learnblockchain.cn/tags/数学/"},{"name":"RSA算法","slug":"RSA算法","permalink":"https://learnblockchain.cn/tags/RSA算法/"}]},{"title":"比特币脚本及交易分析 - 智能合约雏形","slug":"bitcoin-script","date":"2017-11-10T09:05:33.000Z","updated":"2019-04-05T10:29:30.966Z","comments":true,"path":"2017/11/10/bitcoin-script/","link":"","permalink":"https://learnblockchain.cn/2017/11/10/bitcoin-script/","excerpt":"大家都有转过账,每笔交易是这样的:张三账上减¥200,李四账上加¥200。在比特币区块链中,交易不是这么简单,\b\b交易实际是通过脚本来完成,以承载更多的功能个,这也是为什么比特币被称为是一种“可编程的货币”。本文就来分析一下交易是如何实现可编程的。","text":"大家都有转过账,每笔交易是这样的:张三账上减¥200,李四账上加¥200。在比特币区块链中,交易不是这么简单,\b\b交易实际是通过脚本来完成,以承载更多的功能个,这也是为什么比特币被称为是一种“可编程的货币”。本文就来分析一下交易是如何实现可编程的。 未花费的交易输出(UTXO)先引入一个概念:未花费的交易输出——UTXO(Unspent Transaction Output) 其实比特币的交易都是基于UTXO上的,即交易的输入是之前交易未花费的输出,这笔交易的输出可以被当做下一笔新交易的输入。 挖矿奖励属于一个特殊的交易(称为coinbase交易),可以没有输入。UTXO是交易的基本单元,不能再分割。在比特币没有余额概念,只有分散到区块链里的UTXO 随着钱从一个地址被移动到另一个地址的同时形成了一条所有权链,像这样: 比特币脚本比特币交易是首先要提供一个用于解锁UTXO(用私钥去匹配锁定脚本)的脚本(常称为解锁脚本:Signature script),这也叫交易输入,交易的输出则是指向一个脚本(称为锁定脚本:PubKey script),这个脚本表达了:谁的签名(签名是常见形式,\b并不一定必须是签名)能匹配这个输出地址,钱就支付给谁。 每一个比特币节点会通过同时执行这解锁和锁定脚本(不是当前的锁定脚本,是指上一个交易的锁定脚本)来验证一笔交易,脚本组合结果为真,则为有效交易。 当解锁版脚本与锁定版脚本的设定条件相匹配时,执行组合有效脚本时才会显示结果为真 如最为常见类型的比特币交易脚本(支付到公钥哈希:P2PKH(Pay-to-Public-Key-Hash))组合是这样: 常见交易脚本验证过程比特币交易脚本语言是一种基于逆波兰表示法的基于栈的执行语言(不知道逆波兰和栈的同学去翻大学数据结构课本,你也可跳过这个部分)。 比特币脚本语言包含基本算数计算、基本逻辑(比如if…then)、报错以及返回结果和一些加密指令,不支持循环。想了解更多语言细节可参考:比特币脚本 脚本语言通过从左至右地处理每个项目的方式执行脚本。 下面用两个图说明下常见类型的比特币交易脚本验证执行过程:上图为解锁脚本运行过程(主要是入栈)上图为锁定脚本运行过程(主要是出栈),最后的结果为真,说明交易有效。 交易分析实际上比特币的交易被设计为可以纳入多个输入和输出。 交易结构我们来看看完整的交易结构, 交易的锁定时间定义了能被加到区块链里的最早的交易时间。在大多数交易里,它被设置成0,用来表示立即执行。如果锁定时间不是0并且小于5亿,就被视为区块高度,意指在这个指定的区块高度之前,该交易不会被包含在区块链里。如果锁定时间大于5亿,则它被当作是一个Unix纪元时间戳(从1970年1月1日以来的秒数),并且在这个指定时间之前,该交易不会被包含在区块链里。 交易的数据结构没有交易费的字段,交易费通过所有输入的总和,以及所有输出的总和之间的差来表示,即: 交易费 = 求和(所有输入) - 求和(所有输出) 交易输入结构刚刚我们提过输入需要提供一个解锁脚本,现在来看看一个交易的输入结构: 我们结合整个交易的结构里看输入结构就是这样子: 交易输出结构刚刚我们提过输出是指向一个解锁脚本,具体交易的输出结构为:我们结合整个交易的结构里看输出结构就是这样子: 交易哈希计算在比特币区块结构Merkle 树及简单支付验证分析 讲到区块结构,区块结构包含多个交易的哈希。那么交易哈希是怎么计算的呢? 交易结构各字段序列化为字节数组 把字节数组拼接为支付串 对支付串计算两次SHA256 得到交易hash 了解详情可进一步参考如何计算交易Hash?及如何创建Hash? 现在是不是对完整的交易到区块有了更清晰的认识。 智能合约雏形 - 应用场景说明由于交易是通过脚本来实现,脚本语言可以表达出无数的条件变种。 比特币的脚本目前常用的主要分为两种,一种是常见的P2PKH(支付给公钥哈希),另一种是P2SH(Pay-to-Script-Hash支付脚本哈希)。P2SH支付中,锁定脚本被密码学哈希所取代,当一笔交易试图支付UTXO时,要解锁支付脚本,它必须含有与哈希相匹配的脚本。 这里不展开技术细节,下面说明一些应用场景,以便大家有更直观的认识。 多重签名应用合伙经营中,如只有一半以上的的股东同意签名就可以进行支付,可为公司治理提供管控便利,同时也能有效防范盗窃、挪用和遗失。 用于担保和争端调解,一个买家想和他不认识或不信任的某人交易,在一般情况交易正常进行时,买家不想任何第三方参与。那交易双方可以发起支付,但如果交易出现问题时,那第三方就可以根据裁定,使用自己的签名和裁定认可的一方共同签名来兑现这笔交易。 保证合同保证合同是建造公众商品时的集资办法,公众商品是指一旦建成,任何人都可以免费享受到好处。标准的例子是灯塔,所有人都认同应该建造一个,但是对于个人航海者来说灯塔太贵了,灯塔同时也会方便其他航海者。一个解决方案是向所有人集资,只有当筹集的资金超过所需的建造成本时,每个人才真正付钱,如果集资款不足,则谁都不用付钱。 依靠预言假如老人想让他孙子继承遗产,继承时间是在他死后或者在孙子年满18岁时(也是一个带锁定时间交易),无论哪个条件先满足,他的孙子都可以得到遗产。因为比特币节点可依靠预言对死亡条件进行判断,预言是指具有密钥对的服务器,当用户自定义的表达式被证明是真的,它能按照要求对交易签名。 相信随着区块链的普及,会对未来的交易模式和商业结构带来巨大的影响。不过由于比特币的脚本语言不是图灵完备的,交易模式依旧有限,以太坊就是为解决这一问题而出现,后面我们会有大量介绍以太坊的文章。 参考文献 & 补充阅读 精通比特币 廖雪峰的深入理解比特币交易的脚本 比特币合同 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"比特币","slug":"bitcoin","permalink":"https://learnblockchain.cn/categories/bitcoin/"}],"tags":[{"name":"比特币脚本","slug":"比特币脚本","permalink":"https://learnblockchain.cn/tags/比特币脚本/"},{"name":"交易结构","slug":"交易结构","permalink":"https://learnblockchain.cn/tags/交易结构/"}]},{"title":"比特币区块结构Merkle树及简单支付验证分析","slug":"merkle","date":"2017-11-09T02:03:36.000Z","updated":"2019-04-05T10:29:30.985Z","comments":true,"path":"2017/11/09/merkle/","link":"","permalink":"https://learnblockchain.cn/2017/11/09/merkle/","excerpt":"在比特币网络中,不是每个节点都有能力储存完整的区块链数据,受限于存储空间的的限制,很多节点是以SPV(Simplified Payment Verification简单支付验证)钱包接入比特币网络,通过简单支付验证可以在不必存储完整区块链下对交易进行验证,本文将分析区块结构Merkle树及如何进行交易验证。","text":"在比特币网络中,不是每个节点都有能力储存完整的区块链数据,受限于存储空间的的限制,很多节点是以SPV(Simplified Payment Verification简单支付验证)钱包接入比特币网络,通过简单支付验证可以在不必存储完整区块链下对交易进行验证,本文将分析区块结构Merkle树及如何进行交易验证。 区块结构在工作量证明中出现过一个区块信息截图: 细心的同学一定已经在里面发现了很多未讲的其他信息,如:时间戳,版本号,交易次数,二进制哈希树根(Merkle根)等。 我们来看看一个区块结构到底是怎样的: 如上图(下文称:区块结构图)所示:每个数据区块包含区块头和区块体。区块头封装了当前版本号、前一区块哈希值、当前区块PoW要求的随机数(Nonce)、时间戳、以及Merkle根信息。区块体则包括当前区块经过验证的、 区块创建过程中生成的所有交易记录。这些记录通过 Merkle树的哈希过程生成唯一的Merkle根并记入区块头. 区块哈希值实际上并不包含在区块的数据结构里,其实区块打包时只有区块头被用于计算哈希(从网络被接收时由每个节点计算出来),常说的区块哈希值实际是区块头哈希值,它可以用来唯一、明确地标识一个区块。 区块头是80字节,而平均每个交易至少是250字节,而且平均每个区块包含2000个交易。因此,包含完整交易的区块比区块头的4千倍还要大。SPV节点只下载区块头,不下载包含在每个区块中的交易信息。这样的不含交易信息的区块链,大小只有完整区块链的几千分之1,那SPV节点是如何验证交易的呢? 哈希验证上面先留一个引子,先来回顾下哈希函数,记账原理我们知道原始信息任何微小的变化都会哈希完全不同的哈希值。 简单文件验证我们通常用哈希来检验下载的文件是否完整,我经常看到这样的下载页面:可以看到下载链接后面提供了一个MD5(MD5也是一种Hash算法),这样我们可以在下载之后对文件计算MD5,如果MD5与提供的MD5相等,说明文件有没有被损坏,这个验证过程相信大家都能理解。 多点文件验证(哈希列表)现在复杂度提高一点,在P2P网络中下载时,会把大文件切成小文件,同时从多个机器上下载数据,这个时候怎么验证数据呢? 以BT下载为例,在下载真正的数据之前,我们会先下载一个哈希列表的(每个下小块计算出一个哈希),如果有一个小块数据在传输过程中损坏了,那我只要重新下载这一个数据块就行了,这时有一个问题就出现了,那么多的哈希,怎么保证它们本身(哈希列表中的哈希值)都是正确地呢? 答案是把每个小块数据的哈希值拼到一起,然后对这个长字符串在作一次哈希运算,得到哈希列表的根哈希。只要根哈希校对比一样就说明验哈希列表是正确的,再通过哈希列表校验小数据块,如果所有的小数据块验证通过则说明大文件没有被损坏。 Merkle\b树验证交易的过程和文件验证很相似,可以人为每个交易是一个小数据块,但比特币使用Merkle\b树的方式进行验证,相对于哈希列表,Merkle树是一种哈希二叉树,它的明显的一个好处是可以单独拿出一个分支来(作为一个小树)对部分数据进行校验,更加高效。 我们回看下上面的区块结构图,区块体就包含这样一个Merkle\b树,Merkle树被用来归纳一个区块中的所有交易。 每个叶子节点是每个交易信息的哈希,往上对相邻的两个哈希合并成字符串再哈希,继续类似的操作直到只剩下顶部的一个节点,即Merkle根,存入区块头。 因为Merkle树是二叉树,所以它需要偶数个叶子节点。如果仅有奇数个交易需要归纳,那最后的交易就会被复制一份以构成偶数个叶子节点,这种偶数个叶子节点的树也被称为平衡树。 简化支付验证SPV节点不保存所有交易也不会下载整个区块,仅仅保存区块头,我们来看看它是如何对交易数据进行验证的。 假如要验证区块结构图中交易6,SPV节点会通过向相邻节点索要(通过Merkleblock消息)包括从交易6哈希值沿Merkle树上溯至区块头根哈希处的哈希序列 (即哈希节点6, 5, 56, 78, 5678, 1234 1~8 - 称为认证路径) 来确认交易的存在性和正确性。(在N个交易组成的区块中确认任一交易只需要计算log2(N)个字节的哈希值,非常快速高效) 大家明白了吗? 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"比特币","slug":"bitcoin","permalink":"https://learnblockchain.cn/categories/bitcoin/"}],"tags":[{"name":"比特币","slug":"比特币","permalink":"https://learnblockchain.cn/tags/比特币/"},{"name":"区块结构","slug":"区块结构","permalink":"https://learnblockchain.cn/tags/区块结构/"},{"name":"Merkle树","slug":"Merkle树","permalink":"https://learnblockchain.cn/tags/Merkle树/"},{"name":"SPV简单支付验证","slug":"SPV简单支付验证","permalink":"https://learnblockchain.cn/tags/SPV简单支付验证/"}]},{"title":"分析比特币网络:一种去中心化、点对点的网络架构","slug":"bitcoin-p2p","date":"2017-11-07T03:25:30.000Z","updated":"2019-04-05T10:29:30.626Z","comments":true,"path":"2017/11/07/bitcoin-p2p/","link":"","permalink":"https://learnblockchain.cn/2017/11/07/bitcoin-p2p/","excerpt":"比特币采用了基于互联网的点对点(P2P:peer-to-peer)分布式网络架构。比特币网络可以认为是按照比特币P2P协议运行的一系列节点的集合。本文来分析下比特币网络,了解它跟传统中心化网络的区别,以及比特币网络是如何发现相邻节点的。","text":"比特币采用了基于互联网的点对点(P2P:peer-to-peer)分布式网络架构。比特币网络可以认为是按照比特币P2P协议运行的一系列节点的集合。本文来分析下比特币网络,了解它跟传统中心化网络的区别,以及比特币网络是如何发现相邻节点的。 中心化网络为了更好的理解P2P网络,我们先来看看传统的中心化模型: 这是一种典型的星型(“中心化”)结构,我们常见B/S及C/S网络架构就是这种模型,C1 、C2 、C3等之间没法直接的连接,C节点如果要连接必须要通过中心化S节点做为桥梁。中心化节点充当服务者、中介作用,比如我们没有办法把资金直接从一个人转移给另一个人,必须通过银行这个中介。 P2P网络P2P网络是指位于同一网络中的每台计算机都彼此对等,各个节点共同提供网络服务,不存在任何“特殊”节点,每个网络节点以扁平(flat)的拓扑结构相互连通。 对比中心化网络,在P2P网络中不存在任何服务端(server)、中央化的服务。P2P网络的节点之间交互连接、协同,每个节点在对外提供服务的同时也使用网络中其他节点所提供的服务,每个节点即是服务端又是客户端。P2P网络模型除应用于比特币网络,使用广泛的BT下载就是基于P2P网络。 P2P网络不仅仅去除了中心化带来的风险(中心化可能作恶),还可以提高传输的效率。(中心化网络当能也有优点) 如何发现节点既然每个网络节点都是平等的(是指在网络层面上节点是平等的,但各节点在功能上可以有不同的分工, 如钱包节点、挖矿节点等),不存在任何“特殊”中心节点,那么当新的网络节点启动后,它是如何跟其他的节点建立连接,从而加入到比特币网络呢? 在中心化网络中,新加入的节点只要连接“特殊”的中心节点就可以加入网络。 为了能够加入到比特币网络,比特币客户端会做一下几件事情: 节点会记住它最近成功连接的网络节点,当重新启动后它可以迅速与先前的对等节点网络重新建立连接。 节点会在失去已有连接时尝试发现新节点。 当建立一个或多个连接后,节点将一条包含自身IP地址消息发送给其相邻节点。相邻节点再将此消息依次转发给它们各自的相邻节点,从而保证节点信息被多个节点所接收、保证连接更稳定。 新接入的节点可以向它的相邻节点发送获取地址getaddr消息,要求它们返回其已知对等节点的IP地址列表。节点可以找到需连接到的对等节点。 在节点启动时,可以给节点指定一个正活跃节点IP, 如果没有,客户端也维持一个列表,列出了那些长期稳定运行的节点。这样的节点也被称为种子节点(其实和BT下载的种子文件道理是一样的),就可以通过种子节点来快速发现网络中的其他节点。 节点通信简述比特币节点通常采用TCP协议、使用8333端口与相邻节点建立连接, 建立连接时也会有认证“握手”的通信过程,用来确定协议版本,软件版本,节点IP,区块高度等。 当节点连接到相邻节点后,接着就开始跟相邻节点同步区块链数据(轻量级钱包应用其实不会同步所有区块数据),节点们会交换一个getblocks消息,它包含本地区块链最顶端的哈希值。如果某个节点识别出它接收到的哈希值并不属于顶端区块,而是属于一个非顶端区块的旧区块,就说其自身的本地区块链比其他节点的区块链更长,并告诉其他节点需要补充区块,其他节点发送getdata消息来请求区块,验证后更新到本地区块链中。 我们还为初学者提供了系统的区块链视频教程,觉得文章学习不过瘾的同学可以戳入门视频教程。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"比特币","slug":"bitcoin","permalink":"https://learnblockchain.cn/categories/bitcoin/"}],"tags":[{"name":"p2p","slug":"p2p","permalink":"https://learnblockchain.cn/tags/p2p/"},{"name":"去中心化","slug":"去中心化","permalink":"https://learnblockchain.cn/tags/去中心化/"},{"name":"比特币网络","slug":"比特币网络","permalink":"https://learnblockchain.cn/tags/比特币网络/"}]},{"title":"比特币如何挖矿(挖矿原理)-工作量证明","slug":"bitcoin-pow","date":"2017-11-04T08:12:29.000Z","updated":"2019-04-05T10:29:30.634Z","comments":true,"path":"2017/11/04/bitcoin-pow/","link":"","permalink":"https://learnblockchain.cn/2017/11/04/bitcoin-pow/","excerpt":"在区块链记账原理 一篇,我们了解到记账是把交易记录、交易时间、账本序号、上一个Hash值等信息计算Hash打包的过程。我们知道所有的计算和存贮是需要消耗计算机资源的,既然要付出成本,那节点为什么还要参与记账呢?在中本聪(比特币之父)的设计里,完成记账的节点可以获得系统给与的一定数量的比特币奖励,这个奖励的过程也就是比特币的发行过程,因此大家形象的把记账称为“挖矿”,本文将详细讨论这个过程。","text":"在区块链记账原理 一篇,我们了解到记账是把交易记录、交易时间、账本序号、上一个Hash值等信息计算Hash打包的过程。我们知道所有的计算和存贮是需要消耗计算机资源的,既然要付出成本,那节点为什么还要参与记账呢?在中本聪(比特币之父)的设计里,完成记账的节点可以获得系统给与的一定数量的比特币奖励,这个奖励的过程也就是比特币的发行过程,因此大家形象的把记账称为“挖矿”,本文将详细讨论这个过程。 记账工作由于记账是有奖励的,每次记账都可以给自己凭空增加一定数量的个比特币(当前是12.5比特币,博文写作时每个比特币是4万人民币以上,大家可以算算多少钱),因此就出现大家争相记账,大家一起记账就会引起问题:出现记账不一致的问题,比特币系统引入工作量证明来解决这个问题,规则如下: 一段时间内(10分钟左右,具体时间会与密码学难题难度相互影响)只有一人可以记账成功 通过解决密码学难题(即工作量证明)竞争获得唯一记账权 其他节点复制记账结果 不过在进行工作量证明之前,记账节点会做进行如下准备工作: 收集广播中还没有被记录账本的原始交易信息 检查每个交易信息中付款地址有没有足够的余额 验证交易是否有正确的签名 把验证通过的交易信息进行打包记录 添加一个奖励交易:给自己的地址增加12.5比特币 如果节点争夺记账权成功的话,就可以得到12.5比特币的奖励。 工作量证明区块链记账原理我们了解到,每次记账的时候会把上一个块的Hash值和当前的账页信息一起作为原始信息进行Hash。如果仅仅是这样,显然每个人都可以很轻松的完成记账。为了保证10分钟左右只有一个人可以记账,就必须要提高记账的难度,使得Hash的结果必须以若干个0开头。同是为了满足这个条件,在进行Hash时引入一个随机数变量。用伪代码表示一下:1Hash(上一个Hash值,交易记录集) = 456635BCD 1Hash(上一个Hash值,交易记录集,随机数) = 0000aFD635BCD 我们知道改变Hash的原始信息的任何一部分,Hash值也会随之不断的变化,因此在运算Hash时,不断的改变随机数的值,总可以找的一个随机数使的Hash的结果以若干个0开头(下文把这个过程称为猜谜),率先找到随机数的节点就获得此次记账的唯一记账权。 计算量分析(这部分可选阅读)我们简单分析下记账难度有多大,Hash值是由数字和大小写字母构成的字符串,每一位有62种可能性(可能为26个大写字母、26个小写字母,10个数字中任一个),假设任何一个字符出现的概率是均等的,那么第一位为0的概率是1/62(其他位出现什么字符先不管),理论上需要尝试62次Hash运算才会出现一次第一位为0的情况,如果前两2位为0,就得尝试62的平方次Hash运算,以n个0开头就需要尝试62的n次方次运算。我们结合当前实际区块#493050信息来看看: 注:数据来源于https://blockchain.info我们可以看到Hash值以18个0开头,理论上需要尝试62的18次方次,这个数是非常非常巨大的,我已经算不清楚了,应该是亿亿级别以上了。如此大的计算量需要投入大量的计算设备、电力等,目前应该没有单矿工独立参与挖矿了,基本都是由矿工联合起来组成矿池进行挖矿(矿池里的矿工按算力百分比来分收益)。 从经济的角度讲,只有挖矿还有收益(比特币价格不断上涨也让收益变大),就会有新的矿工加入,从而加剧竞争,提高算力难度,挖矿就需要耗费更多的运算和电力,相互作用引起最终成本会接近收益。 题外话:国内由于电力成本较低,相对收益更高,中国的算力占整个网络的一半以上 验证在节点成功找到满足的Hash值之后,会马上对全网进行广播打包区块,网络的节点收到广播打包区块,会立刻对其进行验证。 如果验证通过,则表明已经有节点成功解迷,自己就不再竞争当前区块打包,而是选择接受这个区块,记录到自己的账本中,然后进行下一个区块的竞争猜谜。网络中只有最快解谜的区块,才会添加的账本中,其他的节点进行复制,这样就保证了整个账本的唯一性。 假如节点有任何的作弊行为,都会导致网络的节点验证不通过,直接丢弃其打包的区块,这个区块就无法记录到总账本中,作弊的节点耗费的成本就白费了,因此在巨大的挖矿成本下,也使得矿工自觉自愿的遵守比特币系统的共识协议,也就确保了整个系统的安全。 进阶阅读比特币区块结构Merkle树及简单支付验证分析,可以详细了解区块结构如何验证交易。 说明矿工的收益其实不仅仅包含新发行的12.5比特币奖励,同时还有交易费收益(本文忽略一些细节是为了让主干更清晰)。 有兴趣的同学可以看看图中区块都包含了那些信息,红箭头标示出的是本文涉及的信息。 本文中有提到共识协议,比特币共识协议主要是由工作量证明和最长链机制 两部分组成,请阅读比特币如何达成共识 - 最长链的选择。 我们还为初学者提供了系统的区块链视频教程,觉得文章学习不过瘾的同学可以戳入门视频教程。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"比特币","slug":"bitcoin","permalink":"https://learnblockchain.cn/categories/bitcoin/"}],"tags":[{"name":"比特币","slug":"比特币","permalink":"https://learnblockchain.cn/tags/比特币/"},{"name":"挖矿","slug":"挖矿","permalink":"https://learnblockchain.cn/tags/挖矿/"},{"name":"工作量证明","slug":"工作量证明","permalink":"https://learnblockchain.cn/tags/工作量证明/"},{"name":"共识机制","slug":"共识机制","permalink":"https://learnblockchain.cn/tags/共识机制/"}]},{"title":"比特币所有权及隐私问题-非对称加密应用","slug":"bitcoin-own","date":"2017-11-02T09:19:29.000Z","updated":"2019-04-05T10:29:30.975Z","comments":true,"path":"2017/11/02/bitcoin-own/","link":"","permalink":"https://learnblockchain.cn/2017/11/02/bitcoin-own/","excerpt":"比特币系统是如何确定某个账户的比特币是属于谁的?谁可以支付这个账户比特币? 如果你对这个问题还不是很明白,那就一起来看看吧。","text":"比特币系统是如何确定某个账户的比特币是属于谁的?谁可以支付这个账户比特币? 如果你对这个问题还不是很明白,那就一起来看看吧。 银行系统我们先来回顾下现实的银行系统: 首先我们需要把我们的个人信息(如身份证)给银行,银行给我们开立相对应的账户,银行在开户的时候确立了对账户的所有权。 进行支付的时候,银行对交易双方完成转账(银行在开户的时候已经知道我们对应的账户)。 同时银行会对账户信息进行保密(这点其实不能保证)。 匿名账本那么比特币如何在没有第三方银行的参与下,在确保隐私的同时如何确定账户所有权的呢? 实际上比特币的账户是用地址来表示,账本上不显示个人信息,转账是把比特币从一个地址转移到另一个地址。转账记录如这样:12345{ \"付款地址\":\"2A39CBa2390FDe\" \"收款地址\":\"AAC9CBa239aFcc\" \"金额\":\"0.2btc\"} 接下来问题就变为了 谁有权用某个地址进行付款。 支付和所有权 实际是同一个问题,如果此比特币只有我可以用来支付,那么说明我拥有所有权 地址与私钥比特币的解决方案是,谁拥有某个地址的私钥(如果完全没有加密概念的人,可以简单的把私钥当作密码),谁就能用这个地址进行支付。(所以私钥一定保管好,如果私钥泄漏,比特币就可能丢失) 比特币地址和私钥是一个非对称的关系,私钥经过一系列运算(其中有两次Hash)之后,可以得到地址, 但是无法从地址反推得到私钥。1234地址: 2A39CBa2390FDe私钥: sdgHsdniNIhdsgaKIhkgnakgaihNKHIskdgalHash(Hash(fun(sdgHsdniNIhdsgaKIhkgnakgaihNKHIskdgal))) -> 2A39CBa2390FDe 银行系统银行账号和密码是完全独立的,无法互相推导,转出时需要同时验证账号和密码 还是上面交易的例子:12345{ \"付款地址\":\"2A39CBa2390FDe\", \"收款地址\":\"AAC9CBa239aFcc\", \"金额\":\"0.2btc\"} 只有拥有地址2A39CBa2390FDe的私钥才能进行支付。 非对称加密技术这个时候问题就变为了,如何证明你拥有某个地址的私钥(在不泄漏私钥的情况下)。 对交易信息进行签名实际在签名之前,会先对交易信息进行Hash运算得到摘要信息,然后对摘要信息进行签名。过程大概是这样:1.对交易进行hash, 得到一个摘要信息(Hash值) 12345hash(' {"付款地址":"2A39CBa2390FDe", "收款地址":"AAC9CBa239aFcc", "金额":"0.2btc" }') -> 8aDB23CDEA6 2.用私钥对交易摘要进行签名(付款方在安全的环境下进行,以避免私钥泄密), 用代码表示大概是这样。1234#参数1为交易摘要#参数2为私钥#返回签名信息sign(\"8aDB23CDEA6\", \"J78sknJhidhLIqdngalket\") -> \"3cdferdadgadg\" 广播在签名运算之后,付款节点就开始在全网进行广播:我支付了0.2btc到AAC9CBa239aFcc,签名信息是3cdferdadgadg,你们来确认一下吧。 广播过程实际上是发信息到相连的其它节点,其它节点在验证通过后再转发到与之相连的节点,这样的扩散过程。 广播的信息包含了交易原始信息和签名信息 验证其它节点在收到广播信息之后,会验证签名信息是不是付款方用私钥对交易原始信息签名产生的,如果验证通过说明确实是付款方本人发出的交易,说明交易有效,才会记录到账本中去。(实际还会验证付款账号有没有足够的余额,我们暂时忽略这点)验证过程实际是签名过程的逆运算,用代码表示大概过程是这样的: 1234#参数1为签名信息#参数2为付款方地址#返回交易摘要verify(\"3cdferdadgadg\", \"2A39CBa2390FDe\") -> \"8aDB23CDEA6\" 如果验证输出的信息和原始交易信息的hash一致,则验证通过,记录账本,用代码表示大概是这样: 12345678if(verify(\"3cdferdadgadg\", \"2A39CBa2390FDe\") == hash('{\"付款地址\":\"2A39CBa2390FDe\", \"收款地址\":\"AAC9CBa239aFcc\", \"金额\":\"0.2btc\"}')) : # \b\b写入账本 # 广播else: # donothing 大家可以理解为付款地址为公钥,签名过程即为用私钥对交易摘要的加密过程,验证过程为用公钥解密的过程(为方便大家理解,严格来讲是不准确的)。 补充说明上面为了更好的理解,我对一些信息进行了简化。 比特币系统使用了椭圆曲线签名算法,算法的私钥由32个字节随机数组成,通过私钥可以计算出公钥,公钥经过一序列哈希算法和编码算法得到比特币地址,地址也可以理解为公钥的摘要。 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"比特币","slug":"bitcoin","permalink":"https://learnblockchain.cn/categories/bitcoin/"}],"tags":[{"name":"比特币","slug":"比特币","permalink":"https://learnblockchain.cn/tags/比特币/"},{"name":"所有权问题","slug":"所有权问题","permalink":"https://learnblockchain.cn/tags/所有权问题/"},{"name":"隐私问题","slug":"隐私问题","permalink":"https://learnblockchain.cn/tags/隐私问题/"},{"name":"非对称加密","slug":"非对称加密","permalink":"https://learnblockchain.cn/tags/非对称加密/"}]},{"title":"用Python从零开始创建区块链","slug":"build_blockchain_by_python","date":"2017-10-27T02:15:28.000Z","updated":"2019-04-05T10:29:30.972Z","comments":true,"path":"2017/10/27/build_blockchain_by_python/","link":"","permalink":"https://learnblockchain.cn/2017/10/27/build_blockchain_by_python/","excerpt":"本文主要内容翻译自Learn Blockchains by Building One\b本文原始链接,转载请注明出处。作者认为最快的学习区块链的方式是自己创建一个,本文就跟随作者用Python来创建一个区块链。","text":"本文主要内容翻译自Learn Blockchains by Building One\b本文原始链接,转载请注明出处。作者认为最快的学习区块链的方式是自己创建一个,本文就跟随作者用Python来创建一个区块链。 对数字货币的崛起感到新奇的我们,并且想知道其背后的技术——区块链是怎样实现的。 但是完全搞懂区块链并非易事,我喜欢在实践中学习,通过写代码来学习技术会掌握得更牢固。通过构建一个区块链可以加深对区块链的理解。 准备工作本文要求读者对Python有基本的理解,能读写基本的Python,并且需要对HTTP请求有基本的了解。 我们知道区块链是由区块的记录构成的不可变、有序的链结构,记录可以是交易、文件或任何你想要的数据,重要的是它们是通过哈希值(hashes)链接起来的。 如果你还不是很了解哈希,可以查看这篇文章 环境准备环境准备,确保已经安装Python3.6+, pip , Flask, requests安装方法:1pip install Flask==0.12.2 requests==2.18.4 同时还需要一个HTTP客户端,比如Postman,cURL或其它客户端。 参考源代码(原代码在我翻译的时候,无法运行,我fork了一份,修复了其中的错误,并添加了翻译,感谢star) 开始创建Blockchain新建一个文件 blockchain.py,本文所有的代码都写在这一个文件中,可以随时参考源代码 Blockchain类首先创建一个Blockchain类,在构造函数中创建了两个列表,一个用于储存区块链,一个用于储存交易。 以下是Blockchain类的框架:12345678910111213141516171819202122class Blockchain(object): def __init__(self): self.chain = [] self.current_transactions = [] def new_block(self): # Creates a new Block and adds it to the chain pass def new_transaction(self): # Adds a new transaction to the list of transactions pass @staticmethod def hash(block): # Hashes a Block pass @property def last_block(self): # Returns the last Block in the chain pass Blockchain类用来管理链条,它能存储交易,加入新块等,下面我们来进一步完善这些方法。 块结构每个区块包含属性:索引(index),Unix时间戳(timestamp),交易列表(transactions),工作量证明(稍后解释)以及前一个区块的Hash值。 以下是一个区块的结构:12345678910111213block = { 'index': 1, 'timestamp': 1506057125.900785, 'transactions': [ { 'sender': \"8527147fe1f5426f9dd545de4b27ee00\", 'recipient': \"a77f5cdfa2934df3954a5c7c7da5df1f\", 'amount': 5, } ], 'proof': 324984774000, 'previous_hash': \"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\"} 到这里,区块链的概念就清楚了,每个新的区块都包含上一个区块的Hash,这是关键的一点,它保障了区块链不可变性。如果攻击者破坏了前面的某个区块,那么后面所有区块的Hash都会变得不正确。不理解的话,慢慢消化,可参考区块链记账原理 加入交易接下来我们需要添加一个交易,来完善下new_transaction方法12345678910111213141516171819class Blockchain(object): ... def new_transaction(self, sender, recipient, amount): \"\"\" 生成新交易信息,信息将加入到下一个待挖的区块中 :param sender: <str> Address of the Sender :param recipient: <str> Address of the Recipient :param amount: <int> Amount :return: <int> The index of the Block that will hold this transaction \"\"\" self.current_transactions.append({ 'sender': sender, 'recipient': recipient, 'amount': amount, }) return self.last_block['index'] + 1 方法向列表中添加一个交易记录,并返回该记录将被添加到的区块(下一个待挖掘的区块)的索引,等下在用户提交交易时会有用。 创建新块当Blockchain实例化后,我们需要构造一个创世块(没有前区块的第一个区块),并且给它加上一个工作量证明。每个区块都需要经过工作量证明,俗称挖矿,稍后会继续讲解。 为了构造创世块,我们还需要完善new_block(), new_transaction() 和hash() 方法: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566import hashlibimport jsonfrom time import timeclass Blockchain(object): def __init__(self): self.current_transactions = [] self.chain = [] # Create the genesis block self.new_block(previous_hash=1, proof=100) def new_block(self, proof, previous_hash=None): \"\"\" 生成新块 :param proof: <int> The proof given by the Proof of Work algorithm :param previous_hash: (Optional) <str> Hash of previous Block :return: <dict> New Block \"\"\" block = { 'index': len(self.chain) + 1, 'timestamp': time(), 'transactions': self.current_transactions, 'proof': proof, 'previous_hash': previous_hash or self.hash(self.chain[-1]), } # Reset the current list of transactions self.current_transactions = [] self.chain.append(block) return block def new_transaction(self, sender, recipient, amount): \"\"\" 生成新交易信息,信息将加入到下一个待挖的区块中 :param sender: <str> Address of the Sender :param recipient: <str> Address of the Recipient :param amount: <int> Amount :return: <int> The index of the Block that will hold this transaction \"\"\" self.current_transactions.append({ 'sender': sender, 'recipient': recipient, 'amount': amount, }) return self.last_block['index'] + 1 @property def last_block(self): return self.chain[-1] @staticmethod def hash(block): \"\"\" 生成块的 SHA-256 hash值 :param block: <dict> Block :return: <str> \"\"\" # We must make sure that the Dictionary is Ordered, or we'll have inconsistent hashes block_string = json.dumps(block, sort_keys=True).encode() return hashlib.sha256(block_string).hexdigest() 通过上面的代码和注释可以对区块链有直观的了解,接下来我们看看区块是怎么挖出来的。 理解工作量证明新的区块依赖工作量证明算法(PoW)来构造。PoW的目标是找出一个符合特定条件的数字,这个数字很难计算出来,但容易验证。这就是工作量证明的核心思想。 为了方便理解,举个例子: 假设一个整数 x 乘以另一个整数 y 的积的 Hash 值必须以 0 结尾,即 hash(x * y) = ac23dc…0。设变量 x = 5,求 y 的值? 用Python实现如下: 123456from hashlib import sha256x = 5y = 0 # y未知while sha256(f'{x*y}'.encode()).hexdigest()[-1] != \"0\": y += 1print(f'The solution is y = {y}') 结果是y=21. 因为:1hash(5 * 21) = 1253e9373e...5e3600155e860 在比特币中,使用称为Hashcash的工作量证明算法,它和上面的问题很类似。矿工们为了争夺创建区块的权利而争相计算结果。通常,计算难度与目标字符串需要满足的特定字符的数量成正比,矿工算出结果后,会获得比特币奖励。当然,在网络上非常容易验证这个结果。 实现工作量证明让我们来实现一个相似PoW算法,规则是:寻找一个数 p,使得它与前一个区块的 proof 拼接成的字符串的 Hash 值以 4 个零开头。 12345678910111213141516171819202122232425262728293031323334353637import hashlibimport jsonfrom time import timefrom uuid import uuid4class Blockchain(object): ... def proof_of_work(self, last_proof): \"\"\" 简单的工作量证明: - 查找一个 p' 使得 hash(pp') 以4个0开头 - p 是上一个块的证明, p' 是当前的证明 :param last_proof: <int> :return: <int> \"\"\" proof = 0 while self.valid_proof(last_proof, proof) is False: proof += 1 return proof @staticmethod def valid_proof(last_proof, proof): \"\"\" 验证证明: 是否hash(last_proof, proof)以4个0开头? :param last_proof: <int> Previous Proof :param proof: <int> Current Proof :return: <bool> True if correct, False if not. \"\"\" guess = f'{last_proof}{proof}'.encode() guess_hash = hashlib.sha256(guess).hexdigest() return guess_hash[:4] == \"0000\" 衡量算法复杂度的办法是修改零开头的个数。使用4个来用于演示,你会发现多一个零都会大大增加计算出结果所需的时间。 现在Blockchain类基本已经完成了,接下来使用HTTP requests来进行交互。 Blockchain作为API接口我们将使用Python Flask框架,这是一个轻量Web应用框架,它方便将网络请求映射到 Python函数,现在我们来让Blockchain运行在基于Flask web上。 我们将创建三个接口: /transactions/new 创建一个交易并添加到区块 /mine 告诉服务器去挖掘新的区块 /chain 返回整个区块链 创建节点我们的“Flask服务器”将扮演区块链网络中的一个节点。我们先添加一些框架代码: 1234567891011121314151617181920212223242526272829303132333435363738394041import hashlibimport jsonfrom textwrap import dedentfrom time import timefrom uuid import uuid4from flask import Flaskclass Blockchain(object): ...# Instantiate our Nodeapp = Flask(__name__)# Generate a globally unique address for this nodenode_identifier = str(uuid4()).replace('-', '')# Instantiate the Blockchainblockchain = Blockchain()@app.route('/mine', methods=['GET'])def mine(): return \"We'll mine a new Block\" @app.route('/transactions/new', methods=['POST'])def new_transaction(): return \"We'll add a new transaction\"@app.route('/chain', methods=['GET'])def full_chain(): response = { 'chain': blockchain.chain, 'length': len(blockchain.chain), } return jsonify(response), 200if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) 简单的说明一下以上代码: 第15行: 创建一个节点. 第18行: 为节点创建一个随机的名字. 第21行: 实例Blockchain类. 第24–26行: 创建/mine GET接口。 第28–30行: 创建/transactions/new POST接口,可以给接口发送交易数据. 第32–38行: 创建 /chain 接口, 返回整个区块链。 第40–41行: 服务运行在端口5000上. 发送交易发送到节点的交易数据结构如下:12345{ \"sender\": \"my address\", \"recipient\": \"someone else's address\", \"amount\": 5} 之前已经有添加交易的方法,基于接口来添加交易就很简单了 123456789101112131415161718192021222324import hashlibimport jsonfrom textwrap import dedentfrom time import timefrom uuid import uuid4from flask import Flask, jsonify, request...@app.route('/transactions/new', methods=['POST'])def new_transaction(): values = request.get_json() # Check that the required fields are in the POST'ed data required = ['sender', 'recipient', 'amount'] if not all(k in values for k in required): return 'Missing values', 400 # Create a new Transaction index = blockchain.new_transaction(values['sender'], values['recipient'], values['amount']) response = {'message': f'Transaction will be added to Block {index}'} return jsonify(response), 201 挖矿挖矿正是神奇所在,它很简单,做了一下三件事: 计算工作量证明PoW 通过新增一个交易授予矿工(自己)一个币 构造新区块并将其添加到链中 123456789101112131415161718192021222324252627282930313233343536import hashlibimport jsonfrom time import timefrom uuid import uuid4from flask import Flask, jsonify, request...@app.route('/mine', methods=['GET'])def mine(): # We run the proof of work algorithm to get the next proof... last_block = blockchain.last_block last_proof = last_block['proof'] proof = blockchain.proof_of_work(last_proof) # 给工作量证明的节点提供奖励. # 发送者为 \"0\" 表明是新挖出的币 blockchain.new_transaction( sender=\"0\", recipient=node_identifier, amount=1, ) # Forge the new Block by adding it to the chain block = blockchain.new_block(proof) response = { 'message': \"New Block Forged\", 'index': block['index'], 'transactions': block['transactions'], 'proof': block['proof'], 'previous_hash': block['previous_hash'], } return jsonify(response), 200 注意交易的接收者是我们自己的服务器节点,我们做的大部分工作都只是围绕Blockchain类方法进行交互。到此,我们的区块链就算完成了,我们来实际运行下 运行区块链你可以使用cURL 或Postman 去和API进行交互 启动server:12$ python blockchain.py* Runing on http://127.0.0.1:5000/ (Press CTRL+C to quit) 让我们通过请求 http://localhost:5000/mine 来进行挖矿 通过post请求,添加一个新交易 如果不是使用Postman,则用一下的cURL语句也是一样的:12345$ curl -X POST -H \"Content-Type: application/json\" -d '{ \"sender\": \"d4ee26eee15148ee92c6cd394edd974e\", \"recipient\": \"someone-other-address\", \"amount\": 5}' \"http://localhost:5000/transactions/new\" 在挖了两次矿之后,就有3个块了,通过请求 http://localhost:5000/chain 可以得到所有的块信息。 1234567891011121314151617181920212223242526272829303132333435363738{ \"chain\": [ { \"index\": 1, \"previous_hash\": 1, \"proof\": 100, \"timestamp\": 1506280650.770839, \"transactions\": [] }, { \"index\": 2, \"previous_hash\": \"c099bc...bfb7\", \"proof\": 35293, \"timestamp\": 1506280664.717925, \"transactions\": [ { \"amount\": 1, \"recipient\": \"8bbcb347e0634905b0cac7955bae152b\", \"sender\": \"0\" } ] }, { \"index\": 3, \"previous_hash\": \"eff91a...10f2\", \"proof\": 35089, \"timestamp\": 1506280666.1086972, \"transactions\": [ { \"amount\": 1, \"recipient\": \"8bbcb347e0634905b0cac7955bae152b\", \"sender\": \"0\" } ] } ], \"length\": 3} 一致性(共识)我们已经有了一个基本的区块链可以接受交易和挖矿。但是区块链系统应该是分布式的。既然是分布式的,那么我们究竟拿什么保证所有节点有同样的链呢?这就是一致性问题,我们要想在网络上有多个节点,就必须实现一个一致性的算法。 注册节点在实现一致性算法之前,我们需要找到一种方式让一个节点知道它相邻的节点。每个节点都需要保存一份包含网络中其它节点的记录。因此让我们新增几个接口: /nodes/register 接收URL形式的新节点列表 /nodes/resolve 执行一致性算法,解决任何冲突,确保节点拥有正确的链 我们修改下Blockchain的init函数并提供一个注册节点方法:1234567891011121314151617181920...from urllib.parse import urlparse...class Blockchain(object): def __init__(self): ... self.nodes = set() ... def register_node(self, address): \"\"\" Add a new node to the list of nodes :param address: <str> Address of node. Eg. 'http://192.168.0.5:5000' :return: None \"\"\" parsed_url = urlparse(address) self.nodes.add(parsed_url.netloc) 我们用 set 来储存节点,这是一种避免重复添加节点的简单方法。 实现共识算法前面提到,冲突是指不同的节点拥有不同的链,为了解决这个问题,规定最长的、有效的链才是最终的链,换句话说,网络中有效最长链才是实际的链。 我们使用一下的算法,来达到网络中的共识12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667...import requestsclass Blockchain(object) ... def valid_chain(self, chain): \"\"\" Determine if a given blockchain is valid :param chain: <list> A blockchain :return: <bool> True if valid, False if not \"\"\" last_block = chain[0] current_index = 1 while current_index < len(chain): block = chain[current_index] print(f'{last_block}') print(f'{block}') print(\"\\n-----------\\n\") # Check that the hash of the block is correct if block['previous_hash'] != self.hash(last_block): return False # Check that the Proof of Work is correct if not self.valid_proof(last_block['proof'], block['proof']): return False last_block = block current_index += 1 return True def resolve_conflicts(self): \"\"\" 共识算法解决冲突 使用网络中最长的链. :return: <bool> True 如果链被取代, 否则为False \"\"\" neighbours = self.nodes new_chain = None # We're only looking for chains longer than ours max_length = len(self.chain) # Grab and verify the chains from all the nodes in our network for node in neighbours: response = requests.get(f'http://{node}/chain') if response.status_code == 200: length = response.json()['length'] chain = response.json()['chain'] # Check if the length is longer and the chain is valid if length > max_length and self.valid_chain(chain): max_length = length new_chain = chain # Replace our chain if we discovered a new, valid chain longer than ours if new_chain: self.chain = new_chain return True return False 第一个方法 valid_chain() 用来检查是否是有效链,遍历每个块验证hash和proof. 第2个方法 resolve_conflicts() 用来解决冲突,遍历所有的邻居节点,并用上一个方法检查链的有效性, 如果发现有效更长链,就替换掉自己的链 让我们添加两个路由,一个用来注册节点,一个用来解决冲突。12345678910111213141516171819202122232425262728293031323334@app.route('/nodes/register', methods=['POST'])def register_nodes(): values = request.get_json() nodes = values.get('nodes') if nodes is None: return \"Error: Please supply a valid list of nodes\", 400 for node in nodes: blockchain.register_node(node) response = { 'message': 'New nodes have been added', 'total_nodes': list(blockchain.nodes), } return jsonify(response), 201@app.route('/nodes/resolve', methods=['GET'])def consensus(): replaced = blockchain.resolve_conflicts() if replaced: response = { 'message': 'Our chain was replaced', 'new_chain': blockchain.chain } else: response = { 'message': 'Our chain is authoritative', 'chain': blockchain.chain } return jsonify(response), 200 你可以在不同的机器运行节点,或在一台机机开启不同的网络端口来模拟多节点的网络,这里在同一台机器开启不同的端口演示,在不同的终端运行一下命令,就启动了两个节点:http://localhost:5000 和 http://localhost:5001 12pipenv run python blockchain.pypipenv run python blockchain.py -p 5001 然后在节点2上挖两个块,确保是更长的链,然后在节点1上访问接口/nodes/resolve ,这时节点1的链会通过共识算法被节点2的链取代。 好啦,你可以邀请朋友们一起来测试你的区块链 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"其他","slug":"other","permalink":"https://learnblockchain.cn/categories/other/"}],"tags":[{"name":"python","slug":"python","permalink":"https://learnblockchain.cn/tags/python/"},{"name":"创建区块链","slug":"创建区块链","permalink":"https://learnblockchain.cn/tags/创建区块链/"},{"name":"翻译","slug":"翻译","permalink":"https://learnblockchain.cn/tags/翻译/"}]},{"title":"区块链记账原理","slug":"whatbc","date":"2017-10-25T15:26:04.000Z","updated":"2019-04-05T10:29:30.959Z","comments":true,"path":"2017/10/25/whatbc/","link":"","permalink":"https://learnblockchain.cn/2017/10/25/whatbc/","excerpt":"区块链(1.0)是一个基于密码学安全的分布式账本,是一个方便验证,不可篡改的账本。 通常认为与智能合约相结合的区块链为区块链2.0, 如以太坊是典型的区块链2.0 很多人只了解过比特币,不知道区块链,比特币实际是一个使用了区块链技术的应用,只是比特币当前太热,把区块链技术的光芒给掩盖了。区块链才是未来,期望各位开发人员少关心币价,多关心技术。 本文将讲解区块链1.0技术是如何实现的。","text":"区块链(1.0)是一个基于密码学安全的分布式账本,是一个方便验证,不可篡改的账本。 通常认为与智能合约相结合的区块链为区块链2.0, 如以太坊是典型的区块链2.0 很多人只了解过比特币,不知道区块链,比特币实际是一个使用了区块链技术的应用,只是比特币当前太热,把区块链技术的光芒给掩盖了。区块链才是未来,期望各位开发人员少关心币价,多关心技术。 本文将讲解区块链1.0技术是如何实现的。 哈希函数在讲区块链记账之前,先说明一下哈希函数。哈希函数:Hash(原始信息) = 摘要信息原始信息可以是任意的信息, hash之后会得到一个简短的摘要信息 哈希函数有几个特点: 同样的原始信息用同一个哈希函数总能得到相同的摘要信息 原始信息任何微小的变化都会哈希出面目全非的摘要信息 从摘要信息无法逆向推算出原始信息 举例说明:Hash(张三借给李四100万,利息1%,1年后还本息 …..) = AC4635D34DEF账本上记录了AC4635D34DEF这样一条记录。 可以看出哈希函数有4个作用: 简化信息很好理解,哈希后的信息变短了。 标识信息可以使用AC4635D34DEF来标识原始信息,摘要信息也称为原始信息的id。 隐匿信息账本是AC4635D34DEF这样一条记录,原始信息被隐匿。 验证信息假如李四在还款时欺骗说,张三只借给李四10万,双方可以用AC4635D34DEF来验证原始信息 哈希函数的这4个作用在区块链技术里有广泛的运用。(哈希函数是一组函数或算法,以后会发文章专门介绍哈希) 区块链记账方法假设有一个账页序号为0的账页交易记录如下: 账号 入账 出账 余额 备注说明 王二 100 190 收到xxx货款 张三 100 30 xxxx 李四 120 90 170 xxxx 记账时间为:2017-10-22 10:22:02 区块链在记账是会把账页信息(包含序号、记账时间、交易记录)作为原始信息进行Hash, 得到一个Hash值,如:787635ACD, 用函数表示为:1Hash(序号0、记账时间、交易记录) = 787635ACD 账页信息和Hash值组合在一起就构成了第一个区块。 比特币系统里约10分钟记一次账,即每个区块生成时间大概间隔10分钟 在记第2个账页的时候,会把上一个块的Hash值和当前的账页信息一起作为原始信息进行Hash,即: 1Hash(上一个Hash值、序号1、记账时间、交易记录) = 456635BCD 这样第2个区块不仅包含了本账页信息,还间接的包含了第一个区块的信息。依次按照此方法继续记账,则最新的区块总是间接包含了所有之前的账页信息。 所有这些区块组合起来就形成了区块链,这样的区块链就构成了一个便于验证(只要验证最后一个区块的Hash值就相当于验证了整个账本),不可更改(任何一个交易信息的更改,会让所有之后的区块的Hash值发生变化,这样在验证时就无法通过)的总账本。 记账有成本,想了解节点为什么要记账,请看这篇:在比特币如何挖矿-工作量证明 我们还为区块链技术爱好者提供了系统的区块链视频教程,觉得文章学习不过瘾的同学可以戳入门视频教程及以太坊智能合约开发。 深入浅出区块链系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"比特币","slug":"bitcoin","permalink":"https://learnblockchain.cn/categories/bitcoin/"}],"tags":[{"name":"区块链","slug":"区块链","permalink":"https://learnblockchain.cn/tags/区块链/"},{"name":"原理","slug":"原理","permalink":"https://learnblockchain.cn/tags/原理/"},{"name":"哈希","slug":"哈希","permalink":"https://learnblockchain.cn/tags/哈希/"},{"name":"如何记账","slug":"如何记账","permalink":"https://learnblockchain.cn/tags/如何记账/"}]},{"title":"比特币是什么","slug":"whatisbitcoin","date":"2017-10-23T14:36:05.000Z","updated":"2019-04-05T10:29:30.630Z","comments":true,"path":"2017/10/23/whatisbitcoin/","link":"","permalink":"https://learnblockchain.cn/2017/10/23/whatisbitcoin/","excerpt":"对于比特币也许一千个人有一千种理解。本文作为入门篇(写给完全没有了解过比特币概念的新手,老手可忽略),我尽量用简单易懂的语言来介绍比特币。 到底什么是比特币,它到底是怎么运行的呢。","text":"对于比特币也许一千个人有一千种理解。本文作为入门篇(写给完全没有了解过比特币概念的新手,老手可忽略),我尽量用简单易懂的语言来介绍比特币。 到底什么是比特币,它到底是怎么运行的呢。 比特币是什么 比特币是一种基于分布式网络的数字货币。比特币系统(广义的比特币)则是用来构建这种数字货币的网络系统,是一个分布式的点对点网络系统。 本文主要讲解狭义的比特币概念。 数字货币是什么凯恩斯在《货币论》上讲,货币可以承载债务,价格的一般等价物。货币的本质是等价物,它可以是任何东西,如:一张纸,一个数字,只要人们认可它的价值。人民币,美元等作为国家信用货币,其价值由国家主权背书。而数字货币是一种不依赖信用和实物的新型货币,它的价值由大家的共识决定。比特币就是一种数字货币。(我们在网银,微信,支付宝的金额,准确来讲,它是信用货币的数字化,不是数字货币,不过央行也在研究比特币,准备发行数字货币) 运行原理大家知道,在银行系统的数据库里记录着跟我们身份id对应的财产,下文称这样的记录为账本,如张三的卡10月1日转入1w, 余额10w。比特币系统也同样有这样的账本,不同银行由单一的组织负责记录,比特币的记账由所有运行系统的人(即节点,可以简单理解为一台电脑)共同参与记录,每个节点都保存(同步)一份完整的账本。同时使用简单多数原则,来保证账本的一致性。举个例子:如果有人在自己电脑上把自己的余额从1万改为1百万,他这个账本和大多数人的账本不一致,就会被比特币系统认为是无效的。 比特币使用区块链技术来支撑整个系统的运行,有兴趣的同学,可以详细阅读下这几篇博文: 区块链记账原理 比特币所有权问题 比特币如何挖矿 还可进阶阅读:分析比特币网络:一种去中心化、点对点的网络架构,可以详细了解比特币网络。比特币区块结构Merkle树及简单支付验证分析,可以详细了解区块结构如何验证交易。 我们还为初学者提供了系统的区块链视频教程,觉得文章学习不过瘾的同学可以戳入门视频教程. 深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。我的知识星球为各位解答区块链技术问题,欢迎加入讨论。","categories":[{"name":"比特币","slug":"bitcoin","permalink":"https://learnblockchain.cn/categories/bitcoin/"}],"tags":[{"name":"比特币","slug":"比特币","permalink":"https://learnblockchain.cn/tags/比特币/"}]},{"title":"前言-如何学习区块链","slug":"前言","date":"2017-10-20T07:03:36.000Z","updated":"2019-04-05T10:29:30.961Z","comments":true,"path":"2017/10/20/前言/","link":"","permalink":"https://learnblockchain.cn/2017/10/20/前言/","excerpt":"区块链未来3到5年应该会出现行业井喷式发展,相应所需的人才必定水涨船高,每一个开发人员都不应该错过这样的机会。区块链涉及的技术很多,很多开发人员看了一些资料后,感觉好像懂了,又好像没懂。如何系统的学习区块链技术,是很多想从事区块链开发的程序员的问题,我们来一起讨论下,希望可以帮助更多的人掌握区块链开发技术。","text":"区块链未来3到5年应该会出现行业井喷式发展,相应所需的人才必定水涨船高,每一个开发人员都不应该错过这样的机会。区块链涉及的技术很多,很多开发人员看了一些资料后,感觉好像懂了,又好像没懂。如何系统的学习区块链技术,是很多想从事区块链开发的程序员的问题,我们来一起讨论下,希望可以帮助更多的人掌握区块链开发技术。 确定方向从事区块链开发也有很多方向,如:区块链应用开发人员、区块链架构师、底层核心开发、共识算法研究等等。 方向不同,需要学习的内容就不一样,如果做基于区块链应用开发,只需要了解一门编程语言(nodejs, Go, Python, C++ 等), 大概了解区块链的原理,不一定要深入,当能理解越深入开发应用就越顺。如果做区块链基础开发,就需要了解加密算法,P2P通信,共识算法等等。 投入时间学习-动手实践由于区块链涉及的技术很多,可以相对各个技术有一个概念了解,再逐步深入原理。 当你在学习了解概念的时候,必定会产生很多疑问, 例如我们经常可以看到一句: 比特币的共识机制是通过工作量证明(POW)来实现的。就有了新疑问:什么是工作量证明,进一步了解,它是通过验证的一个特定结果,就能确认参与者完成了相应的工作量(不理解没关系,可以简单为,张三考试考了100分,就确认张三肯定好好学习了)。这时又有了新的疑问,比特币在验证什么样的结果,这时你又需要了解密码学和Hash。 逐步深入的过程也是解答疑问的过程,需要我们善用Google搜索。 如果觉得已进理解一个概念或原理时,可以尝试动手实现它,如在理解挖矿后,可以写代码模拟挖矿过程。 学习是一个长期的过期,没有捷径,必须得多读书,读代码,写代码。 学习资源介绍下面是一些学习资源的介绍,相信对大家有帮助 比特币:一种点对点的电子现金系统-英文 比特币:一种点对点的电子现金系统-中文 以太坊白皮书-英文 以太坊白皮书-中文 区块链技术指南-电子书 区块链开发指南-纸书 比特币 - 官网 以太坊 - github 超级账本Hyperledger ETHFANS - 社区 深入浅出区块链","categories":[{"name":"其他","slug":"other","permalink":"https://learnblockchain.cn/categories/other/"}],"tags":[{"name":"如何学习","slug":"如何学习","permalink":"https://learnblockchain.cn/tags/如何学习/"}]}]}