《Mastering Ethereum》读书笔记

根据领域的需求买了一本《精通以太坊》,看的过程中顺便整理一下之前不知道的知识。

以太坊背后的密码学

私钥是用户于本地随机生成的256位随机数字,私钥对应着以太坊账户的控制权和所有权,因此需要严格保密。私钥的生成更依赖于密码学安全的伪随机数生成器(如CSPRNG),在以太坊中私钥可以是\([1, n-1]\)中的任何数字,其中\(n\)代表了以太坊使用的椭圆曲线的阶,为\(1.158 * 10^{77}\)

以太坊使用椭圆曲线保证非对称密码的安全性,采用secp256k1的标准,对应椭圆曲线为\(y^{2} \equiv (x^{3} + 7) \ mod \ p\),其中\(p=2^{256} - 2^{32} - 2^{9} - 2^{8} - 2^{7} - 2^{6} - 2^{4} - 1\)是一个大素数。椭圆曲线上的两点\(p1, p2\),定义\(p1+p2=p3\),其中\(p3\)为点\(p1, p2\)连线与椭圆曲线的唯一交点,特别地若\(p1=p2\),那么\(p3\)则为过点\(p1\)的切线与椭圆曲线的唯一交点。根据加法在椭圆曲线上的定义,定义正整数乘法\(k * p=p+p+....+p\),得到的仍然是椭圆曲线上的唯一点。

公钥则是由私钥计算产生的椭圆曲线上的一个点,记私钥为\(k\),公钥为\(K\),那么则有\(K = k * G\),其中\(G\)是一个椭圆曲线上的常量点。由于椭圆曲线的定义,椭圆曲线上的乘法是难以进行逆运算的,因此保证了私钥的安全性。根据SECG发布的一种序列化编码方式,用四种可能的前缀来表示椭圆曲线上的点位,如下表所示,以太坊使用未压缩点编码公钥,即0x04 + x + y的方式编码。

前缀 含义 长度(字节)
0x00 无穷远点(0,0) 1
0x04 未压缩点,保存\(x, y\)坐标 65
0x01 偶数\(y\)压缩点 33
0x03 奇数\(y\)压缩点 33

以太坊的地址则使用哈希算法对公钥进行加密(特别注意此处的公钥应带有04前缀),以太坊中使用的哈希加密算法为keccak-256算法,具体的细节在另一篇博文中讨论。通过\(keccak(K)\)我们可以得到256位结果,选出大端序中的末20字节(160位)作为账户的地址

为了避免转账时可能发生的地址输入错误,社区提出了EIP-55提案,对十六进制编码的地址进行大写检验。对于十六进制小写编码的地址\(addr\)进行哈希运算,得到大端序下的高20字节逐位检验,如果十六进制编码的哈希结果大于等于8,那么则把对应的十六进制编码的地址修改为大写(如果为字母的话)。采用大小写混合的地址后,一旦发生了地址输入错误情况,则可以通过哈希值反向对地址的合法性进行核验,由于哈希函数对于输入的变化极为敏感,即使发生了一位的错误也会使哈希结果发生巨大的变化,EIP-55可以有效地防止可能发生的地址输入错误。

钱包

如果用户只采用同一个地址进行转账交易的话,很容易通过地址进行交易的追踪,为了实现在去中心化网络上的匿名性,用户可以通过钱包技术管理多个派生出的账户,从而避免通过地址进行的追踪。目前的钱包大多采用了确定性钱包,即通过一个种子密钥派生出多个可用的子钥,并利用多个可用的子地址进行交易。

简要介绍一下助记词标准(BIP-39)。在用户创建钱包时,钱包会生成一个密码学安全意义上的随机数,记为\(S\)(长度一般为128位或者256位),取出\(SHA-256(S)\)的前\(S/32\)比特作为校验值附加在\(S\)的末尾,按照11比特一组分为\(3S/32\)组,在预先选取出的2048个简单英文单词中进行映射,形成助记词组。若用户忘记了钱包的密码,可以通过助记词可以恢复出钱包生成的随机数。分层确定性钱包(HD)由一个512比特的种子密钥开始生成,使用助记词组作为第一个参数,可选的盐(默认为"mnemonic",可以在后附加用户指定的密码)作为第二个参数,使用密钥扩展算法PBKDF2进行2048轮HMAC-SHA512哈希运算得到。需要特别注意的是此处的密码与设立钱包时要求用户填写的密码不同,若设立时的密码丢失,可以通过助记词进行恢复,这也说明了这个密码仅作钱包的登录使用,并不参与到后续种子密钥的生成过程。

有了钱包的种子密钥后就可以根据曾几时确定性钱包标准(BIP-32)进行密钥的派生了,其中种子的左256位为根私钥,右256位为根链码,扩展派生出的密钥形成树形结构,根据扩展时使用的索引进行编码,不同的层级间用/隔开,如下图所示。

比较直观的派生过程如下图所示,将父公钥,父链码和索引作为哈希函数的输入,得到的512位哈希输出分为左右两部分,分别记为l, rl与父私钥p相加得到子私钥c=l+pr作为子链码用于下一级密钥的派生。为了方便钱包间的导入导出,将密钥与对应的链码结合起来称为扩展密钥,使用Base58Check进行编码,其中扩展私钥以xpriv开头,扩展公钥以xpub开头。

为了在不可信赖环境下进行部署,利用椭圆曲线运算上的结合律,可以不经过父私钥,只通过父扩展公钥生成子公钥。因为生成方法为c=l+p,根据公钥生成方式有C=G*c, P=G*p,因此有C=G*(l+p)=G(l)+P,将生成点与哈希结果的左半部分相乘,再加上父公钥即可得到派生出的子公钥,这样就避免了私钥的部署。

考虑如果某一个子私钥发生了泄露,在父扩展公钥暴露的情况下,可以对索引进行暴力破解,从而根据c=l+p的逆运算得到父私钥,使得由该父节点派生的所有密钥发生泄漏。为此引入了增强派生算法,由父私钥,父链码和索引生成子一级的私钥和链码,由于私钥不会被部署也就隔断了子一级向上推断的可能,当然缺点是私钥无法像公钥一样部署在服务器上,因此通常在最根一级使用增强派生算法,或在需要隔绝父子间推导关系的场景使用,以增强钱包的安全性。

为了区别两种派生方法,规定索引最高位为0代表普通派生方法,为1代表增强派生算法,特别地在表示时,m/x/y为密钥m派生的第x个子密钥的第y个子密钥,m/x'/y表示密钥m派生的第x+0x80000000个增强密钥的第y个子密钥。

根据BIP-44标准,定义树型层级的含义为m / purpose' / coin_type' / account' / change / address_index。其中purpose'总是设为44'coin-type'用以区分不同的币种,以太坊是60'account'用以区分不同的逻辑账户,change用以区分区块链的找零地址,address_index则是同一个逻辑账户下的不同地址。

参考链接:

  1. The math behind BIP-32 child key derivation
  2. 比特币区块链(六) | 钱包的技术细节之从种子创建HD钱包

交易

交易是唯一能够触发以太坊区块链状态发生改变的行为(矿工挖矿除外),交易包含以下内容:

  • nonce:序列编号,表示该次交易是发起者账户的第几次交易,用于抵御重放攻击
  • gas price:交易发起方愿意支付的gas价格,通常来说越高的价格对应着越快被矿工打包进区块
  • gas limit:交易发起方愿意支付的gas上限
  • recipient:目标以太坊地址
  • value:交易发送给的以太币数量
  • data:交易中附加的可变长数据
  • v, r, s:交易附加的数字签名部分,用于验证交易安全性

交易在以太坊中采用RLP(递归长度前缀)标准编码,打包为大端的,8比特倍数长度的二进制数据。

nonce满足了用户按序执行交易的需求,假设用户发出多笔交易,希望可以按照顺序依次执行,但按序发出的交易数据在区块链网络中被确认的顺序只取决于它们沿着P2P网络到达确认节点的顺序,因此通过nonce可以保证用户的交易按序执行。如果节点收到了不符合历史nonce的交易,则会放入待确认交易列表,直至空缺的nonce都有对应的交易被确认后再做执行。同时nonce也作为计数器抵御了重放攻击,避免同一笔交易被恶意多次广播。但是nonce的这一设计也为并行化构造交易带来了难题,目前只能采用串行的方法进行多笔交易的生成。

交易的目标地址可以是外部账户或者合约账户,可以通过data段对合约账户的函数进行调用,调用时按32字节对齐,以目标函数原型定义(例如withdraw(uint256))的Keccak-256哈希值的前4个字节开头,表示调用的函数,以参数作为结尾。特殊地,向0x0地址发送交易会将data段中的数据创建为一个合约,向0x00...00dEaD发送以太币是销毁以太币的含义,当然发送到一个任意地址也可能是在销毁以太币。

为了确保交易的安全性,以太坊采用了椭圆曲线数字签名算法(ECDSA)给交易附加上了数字签名,数字签名提供了身份验证,不可否认和数据完整性的支持。基于数据签名算法的性能考虑,数字签名一般只作用于待发送消息的哈希结果,即Sig=F_{sig}(Keccak(message), k),其中k是用于签名的私钥,message则是经过打包的数据。

为了保证用户使用私钥对多笔交易进行签名不会导致安全性的降低,在每次签名时,除了用户本身的私钥k,还会采用一个随机生成的私钥q共同加密,得到\(s \equiv q^{-1}(Keccak(m) + r*k) \ mod \ p\),其中p是椭圆曲线上的素数阶,r是私钥q对应的公钥Q的横坐标,生成的签名Sig=(r, s)

在验证数字签名时可以通过r对信息进行核验,如果签名有效则有\(q \equiv s(Keccak(m) + rk)\),私钥不可能公开,于是同乘生成点G得到\(Q = Keccak(m)*sG + rK\),即可通过Qr进行验证数字签名的有效性,保证是从公钥K对应账户中发出的交易。

但是注意到交易中并没有附加from项,我们需要从签名中先恢复出交易发起方的公钥并确定其账户,这也是数字签名设计的比较巧妙的地方:引入了随机生成的密钥对,并与账户持有的密钥对建立了对应的联系。加密时的\(q^{-1}\)等价于两侧都乘上一个私钥,这样在不泄露私钥的前提下,同乘上生成点G就可以转换为公钥间的联系,即\(sQ = rK + Keccak(m)*G\),即可算出交易发起的账户公钥为\(K=r^{-1}(sQ - Keccak(m) * G)\)

不过基于椭圆曲线关于x轴对称的性质,从横坐标r恢复公钥Q时会对应两个点,可以通过s反过来选择公钥K。为了提高效率,在数字签名中使用(v, r, s),约定v使用0, 1表示究竟采用曲线上的哪个点,在较老的链上使用的是27或28(27是比特币采用的随机数),经过SpuriousDragon分叉后采用35或是36。

EIP-155提案又为交易增加了chainID, r(=0), s(=0)的结构成员,其中chainID是为了抵御不同以太坊网络间的重放攻击。

目前以太坊没有原生的多签名交易支持,需要智能合约进行实现。交易的隐秘性通过P2P的传播来保护,任何一个消息的传递都可能来自发起者或是网络中的传播节点,攻击者需要掌握大多数节点才能确认交易发起者的网络地址。