区块链学习笔记Day3:以太坊背后的密码学
学区块链做的笔记Day3,大部分内容来自《精通以太坊》。
PS:这里的代码基本上都是用crypto来写的,但是从官方的文档里可以看到ERCs/ERCS/erc-55.md at master · ethereum/ERCs (github.com),官方提供了
eth_utils
的库。
1 pip install eth-utils -i https://pypi.tuna.tsinghua.edu.cn/simple
椭圆曲线密码学的基本概念
以太坊使用跟比特币系统相同的椭圆曲线算法,称为secp256k1
,由NIST设定。
椭圆曲线密码学是基于离散对数问题的非对称密码学(也成为公钥密码学),它是基于椭圆曲线上点位的加法和乘法的不可逆特性。
secp256k1曲线由下列函数定义,这些函数生成了椭圆曲线:
或
mod _p_ (素数的模)表示曲线位于素数阶_p_的有限域中,这也可以表示为,其中的是一个非常大的素数。
当时,椭圆曲线的图为
列入下面的Q点对应的(x,y)坐标是secp256k1
的一个点位:
椭圆曲线上的运算
椭圆曲线上的加法并不是做数字之间的加法,而是把曲线上的两个点相加。乘法类似于重复进行加法运算。
椭圆曲线上加法运算的定义就是给定椭圆曲线上的两个点和,椭圆曲线上存在第三个点,满足。
从几何学的意义上来说,第三个点的计算其实是在和之间画一条线。这条线会与椭圆曲线存在唯一的相交点,这个点称为,对应着在x轴我们就可以得到。
如果和是同一点,那么和之间的这条线就应该是椭圆曲线上()点的切线。
在椭圆曲线的数学运算中,存在一个“无限远点”,这个点类似常规数学中的零。在计算机中,它有时被表述为(这并不满足椭圆曲线的方程,但这是一个很容易验证的例子)。有一些特殊的案例可以用来解释为什么我们需要这个无限远点。
在一些情况下(比如和有相同的x值,却有不同的y值),那么两点的连线就是一条垂直的直线,这样的情况下,就是无限远点。
如果是无限远点,那么。这个例子展示了“无限远点”如何扮演着普通数学中零的作用。
这意味着,加法是满足结合律的,也就是说。
我们已经定义了加法,现在可以借此延伸出乘法的定义。对于椭圆曲线上的P点,如果k是一个整数,那么(相加k次)。注意,有时候k会被称为“指数”,容易令人混淆。
生成公钥
我们从随机得来的私钥k开始,使用椭圆曲线上预先定义好的名为生成点的G点来产生另一个位于椭圆曲线上的点,这就是对应的公钥K。
生成点由secp256k1
椭圆曲线标准定义,在所有的secp256k1
实现中,这个点保持不变,所有从这个曲线产生的公钥都是经过相同的生成点计算而来的。因为对于所有的以太坊用户而言,生成点始终保持不变,所以使用一个私钥k与生成点G计算之后,总是会得出相同的公钥K。k和K之间的关系是固定的,但是只能从一个方向进行计算,也就是通过k算出K。这也是为什么一个以太坊地址(从公钥K而来)可以被公开分享,而不用担心对应的私钥(k)可能会被人反向算出。
如同我们在上一节中提到的,的乘法运算相当于是重复多次的加法运算,也就是,重复k次。概括而言,为了从私钥k计算出公钥K,我们需要把生成点G反复相加k次。
私钥:私钥就是一组随机获取的数字
现在,让我们把这项计算用在之前“私钥”一节中生成的那个私钥,并通过这个私钥来计算得出公钥:
1 | K = f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315 * G |
一些密码学类库可以帮助我们使用椭圆曲线加法来计算K。计算所得的公钥K定义为一个点:
其中:
1 | x = 6e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b |
python代码:
1 | # pip install cryptography -i https://pypi.tuna.tsinghua.edu.cn/simple |
在以太坊协议中,你可能会看到采用130个十六进制字符(65字节)表示的公钥。这是由SECG所发布的行业标准的一种序列化编码方式,在高效密码学标准(SECI)中有文献记载(http://www.secg.org/sec1-v2.pdf)这个标准定义了四种可能的前缀用来标示椭圆曲线上的点位
序列化EC公钥前缀表:
前缀 | 含义 | 长度(以字节记) |
---|---|---|
0x00 | 无穷远点 | 1 |
0x04 | 未压缩点 | 65 |
0x01 | 偶数y压缩的点 | 33 |
0x03 | 奇数y压缩的点 | 33 |
以太坊只是用未压缩的公钥,因此唯一相关的前缀就是0x04。包括x和y坐标的公钥经过编码后的形态如下:
因此,我们在上文计算得出的公钥,经过编码后的形态为:
1 | 046e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b83b5c38e5e2b0c8529d7fa3f64d46daa1ece2d9ac14cab9477d042c84c32ccd0 |
(就是单纯的04+x+y)
1 | eth_encode = '04' + hex(x)[2:] + hex(y)[2:] |
椭圆曲线程序库:
- OpenSSL
- libsecp256k1
哈希函数
哈希函数在安全领域有着广泛的应用
- 数据指纹
- 数据一致性(错误侦测)
- 工作量证明
- 身份认证(密码哈希和密钥延申)
- 伪随机数生成器
- 消息承诺(commit-reveal机制)
- 唯一标识符
以太坊协议中多处用到了名为Keccak-256
的密码学哈希函数。
在以太坊的代码或者文档中看到大量“
SHA-3
”的字样,多数指原始版本的Keccak-256
,而不是经过SHA-3标准化
的FIPS-202
。
如果两个哈希都叫“SHA-3
”,我们如何辨别程序库用的是FIPS-202 SHA-3
还是Keccak-256
?
最简单的办法是采用测试矢量,用一个给定输入的已知结果来判断。
1 | Keccak256("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 |
以太坊地址
以太坊地址是唯一标识符,从公钥或者合约通过单向哈希函数Keccak-256
计算而来。
我们从私钥开始获得了一个公钥。
1 | # 私钥k |
在进行以太坊地址的运算时,公钥的前缀必须是十六进制的04.
我们使用Keccak-256
来计算公钥的哈希值:
1 | Keccak-256(K) = 2a5bc342ed616b5ba5732269001d3f1ef827552ae1114027bd3ecf1f086ba0f9 |
然后只保留后20位(大端序中最末的字节),这就是我们以太坊的地址:
1 | 001d3f1ef827552ae1114027bd3ecf1f086ba0f9 |
1 | K_encode = '2a5bc342ed616b5ba5732269001d3f1ef827552ae1114027bd3ecf1f086ba0f9' |
1 | # hashlib库也能提供keccak,但是实际上是FIPS-202 SHA-3 |
以太坊地址的校验
不同于比特币,比特币地址中包含了一个内置的校验器用于防止可能的错误地址输入,以太坊的地址是原生的十六进制数据,没有任何校验信息。
ICAP协议:
交换客户端地址协议(ICAP)是一种与国际银行账号(IBAN)编码部分兼容的以太坊地址编码形式,为以太坊地址提供通用、经校验且可互操作的编码。
目前阶段:只有少量的钱包支持ICAP地址格式。
EIP-55协议:十六进制编码地址的大写校验
由于ICAP和域名服务器推进的速度缓慢,有人提出了一个新的编码标准方案(EIP-55)(ERCs/ERCS/erc-55.md at master · ethereum/ERCs (github.com)).
以太坊地址并不区分大小写,通过修改地址中的大小写,我们可以获得一种校验,用于保护地址的完整性,避免地址输入时的人为错误。
MetaMask就支持这种校验方式。
用之前faucet合约的地址做实验,复制合约地址。
1 | 0xAf9524D09866394B26F647aBbA518A08F3289f1D |
这里可以看到合约地址里有大写有小写,然后给这个地址转账。
这里是可以转账的,把第一个A改成小写试试。
提示地址无效。
EIP-55的实现非常简单。我们对全小写的十六进制地址计算Keccak-256哈希。这个哈希被视为地址的数字指纹。接着,把这个哈希校验信息通过大写修改的方式融合到地址中。
针对全小写的地址计算一次哈希,不包括
0x
前缀:1
Keccak256('001d3f1ef827552ae1114027bd3ecf1f086ba0f9') = 23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9695d9a19d8f673ca991deae1
注意检查地址中的字母,如果在哈希中对应的十六进制大于或等于8,就把这个字母改为大写。如果我们把地址和它的十六进制哈希值并列在一起,就很容易看出端倪:
1
2Address: 001d3f1ef827552ae1114027bd3ecf1f086ba0f9
Hash: 23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9695d9a19d8f673ca991deae1
在我们的地址中,第四位有一个小写的d,哈希中对应位数的数值是6,因为它小于8,所以我们保持d的小写格式不变。地址中下一个字母是f,位于第六位。哈希值中对应位置的数字是c,它比8要大,因此我们把地址中的f改为大写的F。以此类推,只用哈希的前20位。
1 | Address: 001d3F1ef827552Ae1114027BD3ECF1f086bA0F9 |
1 | # EIP55编码 |
用EIP-55编码检测错误
1 | fake_address = '001d3F1ef827552Ae1114027BD3ECF1f086bA0E9' |
这种方法可以防止手写输入地址出现错误