程序员视角的钱包创建到交易签名 - Kai | Jeth 第二期

1,789 阅读14分钟

编者按:本文系 imToken 首席架构师 Kai 讲师,在由掘金技术社区主办,以太坊社区基金会、以太坊爱好者与 ConsenSys 协办的《开发者的以太坊入门指南 | Jeth 第二期 - 杭州场》 活动上的分享整理。Jeth 围绕以太坊技术开发主题的系列线下活动。每期 Jeth 会邀请以太坊开发领域的优秀技术团队和工程师在线下分享技术干货。旨在为开发者提供线下技术交流互动机会,帮助开发者成长。

本场分享视频回放链接(B 站)

我是 imToken 的首席架构师 Kai。开篇我先给大家抛出这次分享的核心要点: 在以太坊上,你的私钥就是你的账户。

生成随机数

一开始你什么都没有,你需要生成一个随机数,随机数我觉得程序员都很熟悉,某个事情随机发生需要找一个结果,要产生随机数。

私钥生成

随机数会生成私钥,这时你有两个选择:

1. 直接随机生成 32 bytes

直接随机生成 32 bytes 的二进制内容,把这个作为自己的私钥。假如你打开任何一个以太坊的钱包,你并入到私钥的界面,你随便输出一个东西,只要位数符合要求,它都可以作为钱包导进去。但这里面有一个安全性的问题,大家想到我这样可以看到别人的钱包,但是自己可以回去算下,32个字节可以生成多少个私钥。

2. 确定性推导

这个做法在钱包里很常见。钱包生成一个随机数,然后再拿这个随机数查字典,得到12个单词左右的助记词,再通过路径拿到你的私钥。

GitHub 链接:https://github.com/bitcoinjs/bip39

助记词的生成过程也很有趣,你把随机数拆分11个 bits,就有了11个二进制的字符串,社区里面有一个叫做 bip39 的二进制字符串的对应规范,里面有一个字典,你根据拿到的字符串查字典,查到11个字符串对应的11个单词。最后对前面这11个词做一个 Checksum ,你的钱包得到12个助记词,最终可以推出你的私钥,进而推出你的钱包账户。

Hierarchical Deterministic Wallet 分层-确定性-钱包

这里有两个概念,一个叫做分层,另一个叫做确定性

什么是分层,一开始随机数 seed 推导出了一把 Master key,这把 Master key 在下面会继续推导 Child keys,再往下推能得到私钥。这就是前面所提到的确定性推导。我知道一份助记词,这个助记词无论如何,按照固定的路径可以推出一个可以预期的私钥,叫做确定性;分层你有很多把私钥,很多把私钥里面从一个助记词里面一层一层倒出来,所以我们在这里叫做分层决定性钱包。

BIP44

助记词有了,我们要推导私钥,私钥的推导路径就是由这样一串字符格式:m/purpose/coin/account/change/address_index, 一层一层推导出来的,我们叫它 BIP44。不同的链有不同的推导路径,比如说,以太坊是60。你有一个助记词之后,你可以根据确定性的路径推导出不同的私钥,不同的私钥就是在不同的这些区块链上面的一个身份的一个钱包。

存储私钥

随机数变成助记词,助记词变成私钥,私钥如 STR 何保存下来,毕竟你的钱包不可能使用一次,比如说你有一个朋友谈到众筹,1.8元买到以太坊,他转给你2个,这2个以太坊你要存下来,你存下来之后,你的私钥需要落地存储。但存储不是简单地存储即可。网络空间不安全,每天都有人被盗 QQ 号,区块链世界里面当你的账户资产被盗是不可逆的,这时你需要做安全加密的的存储。

最终你的私钥会变成 V3 KeyStore 这样的长字符串。

  • 这里 KDF 的算法是给一个密钥通过 hash 的方式,计算 N 遍,比如说这里面有一个 N 值是26万次,这里面给出的密码会被这个 KDF 计算26万次之后输出所对应的密钥,因此别人想要破解的话,他需要计算的次数也需要26万次,这相当耗费 CPU 资源如果你这个钱包利用很高的值,这是加密的过程。
  • mac 值就是用作校验的。通过刚刚这个算法,把你的密钥保存之后每次输入密码解开,解开之后怎么样通过验证这个密码是正确的,我把这个密码走前面的这个部分,出来的结果是否对得上,你每次输入密码解开钱包签名里面,这个时候会把你的密钥按照同样的过程走一遍,做一个 hash 对比一下这个 mac 值对应得上,如果对得上,说明加密的这个结果就是你用这个密码加密的。

私钥如何转换到账户地址

私钥有了,也落地存储了,而你的私钥要转换到账户地址,这里有另外的一个推导过程。

这里用到了前面提的不对称加密算法,先做一个椭圆曲线给你一把密钥,还有一把公钥,就是他把私钥和公钥,把公钥投入到 hash ,再去拿第一位的一部分作为一个地址,你的钱包就有一个地址。

这是一个比较复杂的数学过程,作为我们做工程的人来讲,大 K 的最终的结果是一把私钥,小 k 是一把公钥;公钥跟私钥中间的关系是固定的,只能单向推导——从私钥推到公钥;这个椭圆曲线是比特币最早实现时所选的椭圆曲线,是相对而言效率最高的椭圆曲线。

转账

刚刚这些事情做完之后,你就可以去转账,但是转账之前,你的账户需要有一些资产。

在以太坊中转账的数据结构如上图所示,包括一个账户交易计数 Nonce,你要转给谁,你要转多少钱,以及在最下面有一个签名。你拿你的私钥去对这个交易做一个签名。这里面有很多种参数,我们一个一个来看一下。

Gas

首先是Gas,Gas 是以太坊代币的机消耗机制,你发起一个以太坊交易的时候,你必须消耗一定的以太币并算到 Gas 中,Gas 最终会流到矿工手里。Gas 的机制是先扣再退——发起交易时,你最初付出的 Gas 要写得大一点,跑完一条交易以后,最终的结果是消耗的 Gas 比填写的要少,系统会把剩下的退还。

Gas 就是让矿工打包需要付出的费用,你自己可以定义需要多少个 Gas,每个 Gas 值多少以太币,并告诉矿工说,我愿意为这笔交易付出多少钱、这笔钱转给谁和转多少。这里面 Gas 的计算最终在以太坊的虚拟机中按一条一条的指令扣费,你的这些转账里面,按照指令条数和存储空间计费,最终把你的手续费扣下来。

重放保护

Nonce 是账户交易计数,比方说你的第一笔交易 Nonce 是0,然后1、2、3以此类推加上去。

ChainID 是分叉链区分。以太坊和比特币都有自己的测试网络,假如说你在测试网络上面签署一笔交易,别人把它放到主网中运行,这时会发生转账。ChainID 会在当前跑的区块链里面,记录下你的 ID,防止你测试时引发交易。别人拿到这笔交易到主网上面重新运行时,会有一个负责所有交易的计数器,可以防止交易的二次发生。

一些有趣的事情

  • 不指定转账人:首先转账的时候没有指定说是谁转账?它只有 to,但没有 from。
  • 交易签名时就确定了 TxHash:交易签名的时候,交易的参数最终会算出来一个 hash,这个 hash 会让你到网络上交易之前,告知你 Gas 的费用。
  • Out of Gas:当你付出的矿工费 Gas 不够的时候,系统就会扣除你的矿工费,并告诉你由于 Gas 不足导致交易失败,但是照样把你这一条交易打包进去。

私钥签名

得到签名之后,就会有椭圆曲线还原出你的公钥,公钥可以算出来你的地址,所以这是刚刚所说的以太坊的交易里面没有 from。因为有椭圆曲线这个非对称加密的存在,它拿到你的公钥后,可以反推出这个地址是否为发出人的地址。这也就意味着,如果大家的 nonce 和交易计数是一致的话,无论这个交易发给谁签名,我只要拿签名后的数据就可以上传并执行这笔交易。

交易广播

交易签名完之后你要广播并上链,上图是以太坊矿工打包的记录。

交易上链的过程

交易上链的过程分为四点,首先发送交易到指定节点,或前面提到的某一个授信节点。对于钱包来说,它们会运行自己的节点,像以太坊会有官方的客户端 Gas。我们做钱包需要自己运维提供服务的节点池,用户把这一条交易发到节点上面,节点之间互相通讯,把这笔交易的信息扩散到全网,然后矿工节点(即专门做打包交易的节点)有一个打包池。以太坊交易的活跃程度比你当前打包要快,特别是网络拥堵的时候,放到这个池子之后,矿工在池子里面挑选一些对于他们有利的,谁愿意花钱打包,被矿工选中打包到区块里面。

矿工打包策略

矿工打包策略很简单,那就是向钱看。网络搜集全网搜集到各种各样的交易上传到 TxPool 即交易池当中。矿工总是在寻找找到开出更高 Gas,也就是说愿意付出更多手续费的交易,并打包进来。因为最终的前30名的 Gas 费用最终落入矿工,所以他们就会由高到低排序,并先服务 Gas 开价更高的富人。在打包的过程中有可能出现空块,出空块的意义很大,因为你打包一块出来的同时,还可以拿到以太坊网络本身给你的奖励。

Gas 的技巧

  • 交易比较堵的时候,你所发起的交易 Gas 比较低,仍在交易池中待打包。这时你可以再发一笔交易,用高 GasPrice 替换掉 TxPool 中的交易。
  • 想让矿工更快的打包时,可以在把这些交易替换掉,若要加速交易的确认,也可以给出比较高的 GasPrice。
  • 通过网络情况计算最优 GasPrice,因为每个时间节点网络上面发生的 GasPrice 不一样,我们按照网络情况自动给你一个最优的,以一个较低的 GasPrice 尽快打包。

矿工现状

一条链上数目庞大的节点,能保证你的矿工足够的分散。所有的打包的矿工都是在同一个大楼里面,我控制这个大楼就是控制这个网络。目前来说主链上只有矿池,没有孤儿矿工。因为矿池是全数据的节点,矿池找了很多矿工(也就是矿机),把交易派发给矿工,让它们算出来比较合理的块,并尽快地把块打包。现在来说矿机几乎是不知道区块链,它不会理解这一串长链有什么意义。另外,跟矿机和矿池强大的计算能力相比,你的电脑计算能力很弱,挖矿的效率跟不上。所以目前来看,矿池才是金主,即网络的实际控制人。

讲了这么多,大家应该理解到我文章开头下的结论————你的账户就是一把私钥,而且你的账户有了一把私钥之后,拿你的私钥做了签名。这时就会发一笔交易,你可以转账,你有私钥地址后可以接收别人转过来的资产。接下来我们往下继续探究账户的结构,从底层思考以太坊。

更底层去看账户

状态机模型

你可以把以太坊账户想象成状态机模型的结构。如图, alice 和 Bob 发生一笔交易并发生签名(Nonce = 1, Balance = 3)然后这笔交易被区块打包。打包之前,这个 alice 有42个,打包完成之后变成39个且 Nonce +1。

Ethereum Account (https://github.com/ethereumjs/ethereumjs-account)

这里 nonce 和 balbance 不必赘述,当 Stateroot 不为空时,这个账号是一个合约,即合约的状态就存在该字段中。 CodeHash 是你的合约账户里面的具体代码,但不完全是在这两个字段里面,这两个字段引用到底下的时候,只是一个指引你去找到对应东西的索引。

合约账户

  • 只包含代码
  • 可存储状态

合约账户跟普通账户不一样,普通用户的状态是空的,不能够存储状态。以太坊用户的钱包状态是每个人有多少钱。

一些账户有趣的事情

  • Ether 余额记录在账户本身
  • Token 余额记录对应在合约账户上

为什么要讲这个呢?大家知道以太坊去年因为很多项目发众筹而变得很火。对于众筹来说,每个 Token 即每个代币都是一个合约,这个合约的数据记录在合约账户上,而像以太坊的余额是记录在账户本身。

在这里举标准的以太坊 ERC-20 代币规范作为例子。decimal 是余额中小数点的位数。当用户用这个方法查代币余额时,记录在这个部署的合约中。剩下的提供给我们更加常用的方法,包括授权,而在授权之后你可以做一些很有趣的事情,比如去中心化交易。去中心化交易依赖于标准,把你的代币授权给合约的接口。需要留意以太坊本身的 Gas 是不可变更的,这与 imToken 不同。

刚刚说有合约,你要去调用这个合约,调用合约的时候读、写。读合约的时候可以通过 JSON-RPC 的接口;写合约就是发交易,把调用合约的方法写在前面的交易的 data 字段,两种调用都需要进行 solidity-ABI 编码,里面有调取合约的方法,每次发这个交易的时候,你要根据这个编码编出来再调用。

编码的过程大概是这样的:你这里有一个 balance ,发起交易时,首先要把 ID 找出来,把你的参数再编码,加起来拼接在一起,最终你做调用的时候读取合约,写一下的 data 参数。

One More Thing

如何安全保存钱包

  1. 离线存储
  2. 硬件钱包
  3. 多签钱包
  • 对于我们做钱包来说,我们接每一条链,我们弄清楚在这一条链上发生了上面,这个链如何上传交易。对于以太坊本身的节点实现,我们也有一定的了解;
  • 要搜集用户还没有的发生交易;
  • 矿池要足够大,要容纳下我们自己的用户的交易。

想了解更多,我推荐阅读 Ethereumjs 源码。

Ethereumjs 在我的分享中,讲到代码相关的内容都附带了 GitHub 链接,源码或者里面的例子都来自于对应的仓库。若想快速了解一下以太坊如何运作,不妨去看看 Ethereumjs 的源码。

GitHub: kaichen(github.com/kaichen)

Twitter: _kaichen

以上就是我今天分享的内容,谢谢大家。