前面的章节带领大家一步步完成了以太坊的基本操作和代币的发放,本篇为本系列课程最后一篇内容了,也是重头戏,将带大家聊聊中心化存储钱包的设计以及研发过程中的坑和业务处理。
以太坊钱包的分类在第01课的时候就已经介绍过了,还举例说明了一下,我们知道其实无论 Geth 节点或 Ethereum Wallet 都可以称作钱包,因为它们都可以存储以太币、都可以发送交易指令等。但这里讲的钱包分类是另外一个维度,基于 Geth 节点上层再次开发出来的移动端 App 钱包。根据私钥存储的位置可分为两类:
中心化私钥存储的钱包,比如火币的钱包;
去中心化钱包,私钥存储在用户端,比如 imtoken 钱包。
去中心化钱包不是本节要讲的重点,这里只给大家简单介绍一下。无论是中心化的钱包或去中心化的钱包,在 App 层面都是很轻量级的,App 内是不会内置一个 Geth 节点,交易的查询或发送都是通过服务器来进行操作,不同点是交易签名的私钥由谁来保管和负责。
去中心化的钱包有个关键词:助记词。可以用下面的表达式来形容助记词的作用:
私钥 = 助记词 = keystore+密码
通过上面的公式可以看出助记词的重要作用,也是去中心钱包功能的一个亮点。当在这类钱包中创建一个账户之后,钱包会根据生成的私钥文件,生成一套助记词,可为英文可为中文,通过助记词可以反向计算出 keystore + 密码。助记词由用户手抄存放在安全的地方,当进行交易时,输入助记词对交易进行签名,发送交易。当助记词丢失,也就意味着失去了私钥,而钱包一般不会保存用户的私钥信息,资产将永久丢失。
去中心化钱包的好处是不用担心平台从中作梗,不用担心平台被黑客攻击而导致资产损失,但要求个人有一定的私钥保存能力。
所谓中心化钱包就是将所有的私钥文件存储在钱包服务商的服务器内,由服务商来保管这些私钥文件,也就是说资产属于你,但私钥不由你保管。这样做的好处是用户根本不用记住私钥,只用记住在平台所开设的账户、登录密码和支付密码即可。即使忘记了密码,还是可以通过平台提供的忘记密码进行找回,当然,这样就不具有去中心化钱包的优点了。
下面就带大家了解一下开发这样一个去中心化钱包的幕后技术工作和思路,后面所提到的钱包如果未做特殊说明,均指中心化钱包。
钱包的核心功能
钱包对外呈现可能有不同的功能,充值、提现、转账等,但从本质上来说只有一个功能,那就是转账。区块链本质上就是一个账本,记录着一笔笔交易,钱包当然离不开这个本质。
充值是钱包的外部账户向钱包的地址转账;
提现是钱包的账户向钱包之外的地址转账;
转账功能包括钱包内地址互转和向钱包外地址转账;
在这个过程当中也对应着钱包账户资金的增加与减少。
用户地址如何分配
用户在使用钱包的时候肯定需要有一个属于当前账户对应在区块链上的地址,这个地址如何生成呢?在技术交流群中,不少开发人员是这样实现的:每次当用户注册时就调用 Geth 节点的personal_newAccount方法生成一个地址,并将地址存放在 Geth 节点默认的位置。这种方式可以实现,但从技术上和安全上来讲是不可取的。
性能瓶颈之一,调用 Geth 节点生成地址非常耗时,特别是当节点在处理一些同步或高消耗的工作时。
性能瓶颈之二,当 Geth 节点下的私钥越来越多,Geth 启动会变得漫长。
安全问题,Geth 节点对外要广播交易,又要保存敏感的私钥信息,安全问题巨大。
优化之后的做法是事先批量生成地址,当用户注册时只用把地址分配给用户即可。这样做的好处是:
私钥的存储与 Geth 节点相隔离,确保私钥与外网的隔离性,从而确保私钥的安全;
性能的保障,当用户注册时只是将数据库的数据建立了一个关联,而不用去执行费时的加密算法来生成私钥和文件。
此种方法生成 Web3j 提供了相应的创建方法,可在无 Geth 节点的情况下通过代码直接生成符合私钥规则的公私钥。当然,如果有开发能力也可以通过 Geth 的源代码中的私钥生成方法抽离出一个单独的与网络无关的生产私钥程序。
充值交易
在比特币的钱包中,有子账户的概念,只需要在一个总账户下创建 N 多子账号,用户充值到此子账号的比特币同样的会显示在钱包上,同时又提供了查询一个地址所有交易的方法。遗憾的是以太币并没有提供此类接口,只能通过遍历区块交易的方法来判断是否有对应账户的充值交易。
相关操作:
查询区块高度,比对是否是新生成的区块,eth_blockNumber;
查看区块内容及详细交易,eth_gethBlockByNumber;
比对交易的 toAddress 是否为本钱包的地址,如果是则记录此笔交易到数据库,并记录交易状态(pending、确认次数等);
保证入库和记账的幂等性,因为会多次查询到同一笔交易。
确认次数
并不是我们查询区块链中的交易就说明这边交易已经成功了,比特币是默认确认12此之后,此交易几乎不会被篡改,以太坊默认6次,那么怎么计算确认次数呢?
确认次数 = 当前区块高度 - 交易所在区块高度 + 1
此处注意事项:交易有可能会被孤立,在执行此公式时需要验证一下区块中此交易是否还是在那个区块上,是否已经被回滚。同样的,要做好幂等性保障。
提现交易
提现交易同样涉及到上提到的知识点,同时它又有需要额外注意的事项。
提现地址的合法性检查,可参考源代码中的校验,此合法性检查可以避免后续很多问题的出现,比如 nonce 值的维护。
交易的金额检查,nonce 值检查(nonce 值会遇到的问题前面已经提到过),特别是私钥与 Geth 节点分离之后自己来维护私钥时 nonce 值会是一个很大的问题,比如前一笔交易失败,nonce 值需要回退,此时后一笔交易已经发出,因为前一笔 nonce 没有被补齐,后一笔迟迟不会被交易。这些都需要业务进行特殊判断和处理。
查询一个地址 nonce,eth_getTransactionCount。
提现与转账
提现与转账都是发起一笔交易,在以太坊的 json-rpc 中已经提到可以通过eth_sendTransaction和personal_sendTransaction直接进行转账,这是 Geth 节点所支持的。转账前可以通过 unlock 方法先将账户解锁,这些之前章节都有提到过。
但针对私钥单独存储的情况,上面的方式并不适用,可通过将交易先签名再广播的模式:
签名交易(可自主开发,可利用节点本身),eth_sign。
广播交易,可通过eth_sendRawTransaction进行广播。
钱包的内部转账只不过是 from 和 to 地址都是钱包的地址而已,业务层进行适当的处理。
通过这种模式,节点与外界打交道,仅有的功能就是广播交易,在此之前的所有操作都可以通过内网进行操作,极大的确保的私钥和交易的安全性。
转账手续费
转账的手续费算是常识性的内容,可给用户一些参考值,让用户选择愿意支付的手续费,也可通过节点提供的eth_estimateGas来进行预估。
最笨拙而又有效的方法是定期观察一下区块链上交易成功的交易的 gasPrice 的大概范围,动态的调整一下价格,而 gasLimit 在不影响交易(太大账户余额检查时不足)的情况下,尽可能稍微多一些,因为此部分如果未使用还会退换到交易发起账户中。
节点孤立
在发起或检查以太坊交易是否成功不仅仅要检查确认次数,还需要检查交易是否被孤立。孤立的情况用下面的图来展示分析一下:
节点孤立是什么情况?在上面图中,第1、2区块成功打包记入区块链,当到第3块时,有两个节点同时挖出第3块,整个区块链中有一部分认同了上面的链,有一部分认同了下面的链,此时因为都只有3块,没办法确认哪个是主链。但此时区块已经出现了分叉情况。当更多的块被挖出,在某一时间节点,上面的链的长度比下面的长了,此时所有的节点都会认为最长的为正确的链,下面的第3、4块打包的交易将会被回滚,等待重新打包。此时,第3、4块的状况叫做被孤立。
套入签名的业务逻辑,我们的节点处于被孤立的链上时,我们之前扫描到的交易所在区块高度是可变的,在那个区块上已经没有我们的交易了,如果此时对账户进行增减记账,会发生资产的不一致。
那么如何检测孤立区块呢?
监控区块的高度变化,并记录在本地数据库,同时,每收到一笔新的交易,都对比一下此交易记录的前一块交易 hash 是否一致,如果不一致说明区别已经被孤立,递归判断找到被孤立的前一块,然后从那块继续扫描,重新整理判断交易情况。
此处特别注意确保幂等性。
区块被孤立同时需要更新本地数据中此笔交易状态为被孤立状态。
测试环境模拟孤立
备份数据,发送签名(eth_sign)之后的交易;
挖矿3个区块;
恢复备份数据;
再挖5个块;
查询业务是否正常处理孤立情况。
由于每个节点同步到的数据进度差距太大,没办法像中心化的业务一样做负载均衡,只能通过热备的形式来保证当一个节点出现故障时能够快速切换到另外一个节点。因为区块打包本身就比较耗时,因此此处的时效性要求还是可以容忍的。
之前很多朋友因为将 Geth 节点公网开放而导致资产损失,正是因为没有正确的认识到 json-rpc 不同权限的接口的问题。在前面的章节中我们已经介绍了不同的节点的权限。这里再次声明一下,以下节点慎重对外往开放:
personal
net
txpool
miner
admin
等我们不需要的
其实最安全的模式就是 geth 节点只对外进行广播交易。
上面已经穿插着讲了此块内容,将私钥单独存放甚至进行二次加密。
钱包可以通过此种架构来达到高可用和安全性兼顾。