有过区块链开发相关工作经验的同学知道,要开发智能合约的应用,你首先需要通过geth同步以太坊主网,这意味着你需要从其他节点下载很多数据。另外在使用区块链技术时,例如支付,接收数字货币时,“钱包”应用需要发送一系列数据给对方,当我们需要通过网络收发数据时就需要对数据进行序列化。
前面我们了解了很多数据结构,例如有限群,椭圆曲线,公钥,私钥等,相关数据在应用时都需要通过网络进行数据传输,因此相关的数据结构需要进行序列化。对于椭圆曲线上一个点,在序列化时通常采用一种叫做非压缩SEC(standard for efficient cryptography)的数据格式,其步骤为: 1,在开头添加0x04作为标志 2,放置点的x坐标数值,它有32字节 3,放置点的y坐标数值,他有32字节。
相应的序列化代码如下:
代码语言:javascript复制 def sec(self):
'''
sec压缩在开头写入04,然后跟着32字节的x坐标值,最后跟着32字节的y坐标值
'''
return b'x04' self.x.num.to_bytes(32, 'big') self.y.num.to_bytes(32, 'big')
既然有非压缩那就有对应的压缩形态SEC,由于椭圆曲线关于x轴对称,因此给定一个x坐标,它最多对应两个y坐标,这两个y互为相反数。但由于我们作用在椭圆曲线上的点都是有限群里面的元素,对于含有p个元素的群而言(注意到p是素数),如果y是群里面的元素,那么”-y”就是p-y,如果y是偶数,那么p-y就是奇数,反之如果y是奇数,那么p-y就是偶数。利用这个特点我们可以压缩y坐标对应的内容。
于是给定椭圆曲线上一点(x,y),压缩形态SEC生成的步骤为: 1,如果y是奇数,那么以0x03开头,如果是偶数则以0x02开头 2,添加32字节的x坐标值 于是相应实现代码就是:
代码语言:javascript复制 def sec(self, compressed = True):
if compressed:
if self.y.num % 2 == 0:
return b'x02' self.x.num.to_bytes(32, 'big')
else:
return b'x03' self.x.num.to_bytes(32, 'big')
'''
sec压缩在开头写入04,然后跟着32字节的x坐标值,最后跟着32字节的y坐标值
'''
return b'x04' self.x.num.to_bytes(32, 'big') self.y.num.to_bytes(32, 'big')
相对于非压缩形态,压缩形态的序列化就节省了32字节。既然节省掉y,那么接收方收到数据后就需要还原它,还记得椭圆曲线的格式为 y ^ 2 = x^3 a * x b,我们知道了x的值,那意味着知道了y平方的值,现在我们需要计算y的值。
假设w, v为有限群的元素,并且有w^2 = v,其中群元素总共有p个。现在我们知道v,我们需要计算w。如果p % 4 = 3,那么我们有好的算法能快速计算w。由于p % 4 = 3, 于是有(p 1) % 4 = 0。也就是(p 1) 能整除4,也就是(p 1) / 4 是一个整数。根据费马小定理 w ^ (p-1) % p = 1, 于是有w ^ 2 = w ^ 2 1 = w ^ 2 (W ^ (p-1) ) = w ^ (p 1),由于p是奇数,因此(p 1)就能整除2,也就是(p 1)/2 是一个整数,于是就有 w = w ^ ((p 1)/2)。
上面我们提到(p 1)/4是一个整数,于是就有 w = w ^ ((p 1)/2) = w ^ (2 * (p 1)/2) = (w ^ 2) ^ ((p 1)/4) = v ^ ((p 1)/4),于是如果 w ^ 2 = v, 并且有 p % 4 = 3, 那么 w = v ^ ((p 1)/4)。对于比特币使用的椭圆曲线,p值恰好能满足p % 4 = 3。这样一来,我们就将开方运算转为求指数运算,由此有限群元素开方运算的实现代码为:
代码语言:javascript复制P = 2 ** 256 - 2 ** 32 - 977
class BitcoinFieldElement(FieldElemet): #S256Field
def __init__(self, num, prime = None):
super().__init__(num, P)
def __repr__(self):
return "{:x}".format(self.num).zfill(64) # 填满64个数字
def sqrt(self):
return self ** ((P 1) // 4)
我们看看如何解析压缩形态的SEC格式数据,其实现方法为:
代码语言:javascript复制 def parse(self, sec_bin): #解析sec压缩数据
if sec_bin[0] == 4: #非压缩sec格式
x = int.from_bytes(sec_bin[1:33], 'big')
y = int.from_bytes(sec_bin[33:65], 'big')
return BitcoinEllipticPoint(x = x, y = y)
#在压缩sec的格式下,先获取x,然后计算y的平方,最后使用开方算法获得y的值
is_even = sec_bin[0] == 2
x = BitcoinEllipticPoint(int.from_bytes(sec_bin[1:], 'big'))
#y ^ 2 = x ^3 7
alpha = x ** 3 BitcoinEllipticPoint(B)
beta = alpha.sqrt()
'''
在实数域中,y会对应一正一负两个值,在有限域中同理,如果y和(p-y)互为正和负.
也就是在有限群中,如果y满足椭圆方程,那么p-y同样满足椭圆方程。由于比特币对应有限群中元素个数P为素数,
因此如果y 是偶数,那么p-y就是奇数,如果y是奇数,那么p-y是偶数。于是在压缩SEC格式中,如果开头标志位为0x02,
那么如果计算出来的y是奇数,那么就要采用P-y
'''
if beta.num % 2 == 0: #y是偶数,P-y是奇数
even_beta = beta
odd_beta = BitcoinFieldElement(P - beta.num)
else: #y是奇数,P-y是偶数
even_beta = BitcoinFieldElement(P - beta.num)
odd_beta = beta
if is_even:
return BitcoinEllipticPoint(x, even_beta)
else:
return BitcoinEllipticPoint(x, odd_beta)
接下来,我们给几个私钥e,通过e * G 获得对应公钥,然后看看公钥对应的SEC压缩格式数据,代码如下:
代码语言:javascript复制'''
给定如下私钥,求它公钥的压缩sec个数:
5001, 2019 ^ 5, 0xdeadbeef54321
'''
priv = PrivateKey(5001)
print(priv.point.sec(True))
priv = PrivateKey(2019 ** 5)
print(priv.point.sec(True))
priv = PrivateKey(0xdeadbeef54321)
print(priv.point.sec(True))
上面代码运行后结果如下:
代码语言:javascript复制0357a4f368868a8a6d572991e484e664810ff14c05c0fa023275251151fe0e53d1
02933ec2d2b111b92737ec12f1c5d20f3233a0ad21cd8b36d0bca7a0cfa5cb8701
0296be5b1292f6c856b3c5654e886fc13511462059089cdf9c479623bfcbe77690
另外还需要序列化的结构就是签名。他有两个数值需要处理,分别是s和r,这两个值没有逻辑上的关联,因此不能像上面那样压缩。在区块链中用于序列化签名的格式叫DER(Distinguished Encoding sinatures)。DER格式如下: 1,以0x30字节开头 2,添加s和r的长度之和,通常情况下是(0x44和0x45)。 3,添加0x02作为分隔符 4,添加r的长度(一字节)将r转换为大端字节形式,如果它开头的字节>=0x80,那么先添加一个0x00然后添加r的内容, 5,添加s的长度(1字节),将s转为大端格式,如果它首字节>=0x80那么先添加一个0x00,后面才跟着s的内容。 由于r是256位数值,因此它最多32字节,如果它首字节>=0x80,那么我们需要在其前面添加0x00,因此r最多有33字节。s同理可以推断。我们看看代码实现:
代码语言:javascript复制class Signature:
def __init__(self, r, s):
self.r = r
self.s = s
def __repr__(self):
return f"Signature({hex(self.r)}, {hex(self.s)})"
def der(self):
#现将r转换为大端格式
rbin = self.r.to_bytes(32, byteorder='big')
#去掉开始的0x00内容
rbin = rbin.lstrip(b'