学区块链做的笔记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_的有限域中,这也可以表示为,其中的是一个非常大的素数。

时,椭圆曲线的图为

椭圆曲线p=17

列入下面的Q点对应的(x,y)坐标是secp256k1的一个点位:

椭圆曲线上的运算

椭圆曲线上的加法并不是做数字之间的加法,而是把曲线上的两个点相加。乘法类似于重复进行加法运算。

椭圆曲线上加法运算的定义就是给定椭圆曲线上的两个点,椭圆曲线上存在第三个点,满足

从几何学的意义上来说,第三个点的计算其实是在之间画一条线。这条线会与椭圆曲线存在唯一的相交点,这个点称为,对应着在x轴我们就可以得到

v2-4c026e4bbeab0ee1505707093e1856ef_1440w

如果是同一点,那么之间的这条线就应该是椭圆曲线上)点的切线。

在椭圆曲线的数学运算中,存在一个“无限远点”,这个点类似常规数学中的零。在计算机中,它有时被表述为(这并不满足椭圆曲线的方程,但这是一个很容易验证的例子)。有一些特殊的案例可以用来解释为什么我们需要这个无限远点。

在一些情况下(比如有相同的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
2
x = 6e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b
y = 83b5c38e5e2b0c8529d7fa3f64d46daa1ece2d9ac14cab9477d042c84c32ccd0

python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# pip install cryptography -i https://pypi.tuna.tsinghua.edu.cn/simple
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.backends import default_backend
import binascii

# 给定私钥的16进制表示
private_key_hex = 'f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315'

# 将私钥的16进制转换为字节表示
private_key_bytes = binascii.unhexlify(private_key_hex)

# 将私钥字节表示加载到私钥对象
private_key = ec.derive_private_key(
int.from_bytes(private_key_bytes, byteorder='big'),
ec.SECP256K1(),
default_backend()
)

# 从私钥生成公钥
public_key = private_key.public_key()

# 获取公钥的坐标
public_key_coordinates = public_key.public_numbers()
x = public_key_coordinates.x
y = public_key_coordinates.y

# 打印公钥的坐标
print('x =', hex(x))
print('y =', hex(y))

在以太坊协议中,你可能会看到采用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
2
eth_encode = '04' + hex(x)[2:] + hex(y)[2:]
print(eth_encode)

椭圆曲线程序库:

  • OpenSSL
  • libsecp256k1

哈希函数

哈希函数在安全领域有着广泛的应用

  • 数据指纹
  • 数据一致性(错误侦测)
  • 工作量证明
  • 身份认证(密码哈希和密钥延申)
  • 伪随机数生成器
  • 消息承诺(commit-reveal机制)
  • 唯一标识符

以太坊协议中多处用到了名为Keccak-256的密码学哈希函数。

在以太坊的代码或者文档中看到大量“SHA-3”的字样,多数指原始版本的Keccak-256,而不是经过SHA-3标准化FIPS-202

如果两个哈希都叫“SHA-3”,我们如何辨别程序库用的是FIPS-202 SHA-3还是Keccak-256?

最简单的办法是采用测试矢量,用一个给定输入的已知结果来判断。

1
2
Keccak256("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
SHA3("") = a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a

以太坊地址

以太坊地址是唯一标识符,从公钥或者合约通过单向哈希函数Keccak-256计算而来。

我们从私钥开始获得了一个公钥。

1
2
3
4
# 私钥k
k = 'f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315'
# 公钥K(x和y坐标组合后以16进制的方式显示)
K = '6e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b83b5c38e5e2b0c8529d7fa3f64d46daa1ece2d9ac14cab9477d042c84c32ccd0'

在进行以太坊地址的运算时,公钥的前缀必须是十六进制的04.

我们使用Keccak-256来计算公钥的哈希值:

1
Keccak-256(K) = 2a5bc342ed616b5ba5732269001d3f1ef827552ae1114027bd3ecf1f086ba0f9

然后只保留后20位(大端序中最末的字节),这就是我们以太坊的地址:

1
001d3f1ef827552ae1114027bd3ecf1f086ba0f9
1
2
3
K_encode = '2a5bc342ed616b5ba5732269001d3f1ef827552ae1114027bd3ecf1f086ba0f9'
address = binascii.unhexlify(K_encode)[-20:].hex()
print(address)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# hashlib库也能提供keccak,但是实际上是FIPS-202 SHA-3
# 所以这里用Crypto里的keccak
from Crypto.Hash import keccak

# keccak256加密
# 这里传入的是16进制的字符串
def keccak256(data):
keccak_hash = keccak.new(digest_bits=256)
# keccak_hash.update(data.encode('utf-8'))
keccak_hash.update(binascii.unhexlify(data))
return keccak_hash.hexdigest()

K_encode = keccak256(K)
address = binascii.unhexlify(K_encode)[-20:].hex()
print('adress =', address)

以太坊地址的校验

不同于比特币,比特币地址中包含了一个内置的校验器用于防止可能的错误地址输入,以太坊的地址是原生的十六进制数据,没有任何校验信息

ICAP协议:

交换客户端地址协议(ICAP)是一种与国际银行账号(IBAN)编码部分兼容的以太坊地址编码形式,为以太坊地址提供通用、经校验且可互操作的编码。

目前阶段:只有少量的钱包支持ICAP地址格式。

EIP-55协议:十六进制编码地址的大写校验

由于ICAP和域名服务器推进的速度缓慢,有人提出了一个新的编码标准方案(EIP-55)(ERCs/ERCS/erc-55.md at master · ethereum/ERCs (github.com)).

以太坊地址并不区分大小写,通过修改地址中的大小写,我们可以获得一种校验,用于保护地址的完整性,避免地址输入时的人为错误。


MetaMask就支持这种校验方式。

用之前faucet合约的地址做实验,复制合约地址。

image-20231213170309910

1
0xAf9524D09866394B26F647aBbA518A08F3289f1D

这里可以看到合约地址里有大写有小写,然后给这个地址转账。

image-20231213170601524

这里是可以转账的,把第一个A改成小写试试。

image-20231213170710951

提示地址无效。


EIP-55的实现非常简单。我们对全小写的十六进制地址计算Keccak-256哈希。这个哈希被视为地址的数字指纹。接着,把这个哈希校验信息通过大写修改的方式融合到地址中。

  1. 针对全小写的地址计算一次哈希,不包括0x前缀:

    1
    Keccak256('001d3f1ef827552ae1114027bd3ecf1f086ba0f9') = 23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9695d9a19d8f673ca991deae1
  2. 注意检查地址中的字母,如果在哈希中对应的十六进制大于或等于8,就把这个字母改为大写。如果我们把地址和它的十六进制哈希值并列在一起,就很容易看出端倪:

    1
    2
    Address: 001d3f1ef827552ae1114027bd3ecf1f086ba0f9
    Hash: 23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9695d9a19d8f673ca991deae1

在我们的地址中,第四位有一个小写的d,哈希中对应位数的数值是6,因为它小于8,所以我们保持d的小写格式不变。地址中下一个字母是f,位于第六位。哈希值中对应位置的数字是c,它比8要大,因此我们把地址中的f改为大写的F。以此类推,只用哈希的前20位。

1
2
Address: 001d3F1ef827552Ae1114027BD3ECF1f086bA0F9
Hash: 23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9695d9a19d8f673ca991deae1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# EIP55编码
def EIP55(address):
address = address.lower()
checksum_address = ''
address_hash = keccak256(address.encode().hex())
print('address =', address)
print('address_hash =', address_hash)
for i in range(len(address)):
if address[i].isalpha() and address_hash[i] >= '8':
checksum_address += address[i].upper()
else:
checksum_address += address[i]
return checksum_address

checksum_address = EIP55(address)
print('checksum_address =', checksum_address)

用EIP-55编码检测错误

1
2
3
4
5
fake_address = '001d3F1ef827552Ae1114027BD3ECF1f086bA0E9'
check = EIP55(fake_address)

print(fake_address) #001d3F1ef827552Ae1114027BD3ECF1f086bA0E9
print(check) #001D3f1Ef827552Ae1114027BD3EcF1f086Ba0E9

这种方法可以防止手写输入地址出现错误