有区块链开发相关工作经验的同学知道,要开发智能合同的应用,首先需要通过geth同步以太坊主网络意味着您需要从其他节点下载大量数据。此外,当使用区块链技术,如支付和接收数字货币时,钱包应用程序需要向对方发送一系列数据,当我们需要通过网络收发数据时,我们需要序列化数据。
我们了解了很多数据结构,如有限组、椭圆曲线、公钥、私钥等,相关数据需要通过网络传输,因此相关数据结构需要序列化。对于椭圆曲线上的一个点,一种称为非压缩SEC(standard for efficient cryptography)的数据格式,其步骤为: 一、开头加0x04作为标志 2.放置点的x坐标值有32字节 3.有32字节的y坐标值。
相应的序列化代码如下:
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坐标值 因此,相应的实现代码是:
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。对于比特币使用的椭圆曲线,p值得满足p % 4 = 3.这样,我们就把开方运算转化为求指数运算,因此有限群元素开方运算的实现代码是:
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格式数据,其实现方法为:
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代码如下:
''' 给出以下私钥,要求公钥压缩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))
上述代码运行后的结果如下:
0357a4f368868a8a6d572991e484e664810ff14c05c0fa023275251151fe0e53d1 02933ec2d2b111b92737ec12f1c5d20f3233a0ad21cd8b36d0bca7a0cfa5cb8701 0296be5b1292f6c856b3c5654e886fc13511462059089cdf9c479623bfcbe77690
此外,需要序列化的结构是签名。他有两个值需要处理,即s和r,这两个值没有逻辑关联,不能像上面那样压缩。区块链中序列化签名的格式称为DER(Distinguished Encoding sinatures)。DER格式如下: 1,以0x30字节开头 2,添加s和r通常是(0x44和0x45)。 3,添加0x02作为分隔符 4.添加r的长度(一字节)将r转换为大端字节,如果它开头的字节>=0x80,先加0x然后添加r的内容, 5.添加s的长度(1字节),将s转换为大端格式,如果是首字节>=0x然后先添加080x后面跟着s的内容。 因为r是256位数值,所以它最多有32字节,如果32个字节>=0x然后我们需要在它前面添加0x所以r最多有33字节。s同理可以推断。让我们看看代码实现:
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'\0x00')
if rbin[0] & 0x80: # 如果头字节>=0x80,在前头添加0x00
rbin = '\0x00' + rbin
#以0x2开头,接着是r的长度,最后是r的内容
result = bytes([2, len(rbin)]) + rbin
sbin = self.s.to_bytes(32, byteorder='big')
sbin.lstrip(b'\0x00')
if sbin[0] & 0x80:
sbin = b'0x00' + sbin
result += bytes([2, len(sbin)]) + sbin
'''
以0x30开头,跟着是r和s的总长度,然后是r和s的编码
'''
print(f"der total bytes:{len(result)}")
return bytes([0x30, len(result)]) + result
我们运行上面编码试试:
sig = Signature(0x37206a0610995c58074999cb9767b87af4c4978db68c06e8e6e81d282047a7c6,
0x8ca63759c1157ebeaec0d03cecca119fc9a75bf8e6d0fa65c841c8e2738cdaec)
print(f"signature der format:{sig.der().hex()}")
代码运行后结果如下:
signature der format:3048022037206a0610995c58074999cb9767b87af4c4978db68c06e8e6e81d282047a7c60224307830308ca63759c1157ebeaec0d03cecca119fc9a75bf8e6d0fa65c841c8e2738cdaec
由于目前所有编码都是二进制格式,这种格式有个问题就是不利于人的阅读理解。因此比特币后来采用base58来将二进制数据再次进行编码,之所以不用base64是因为后者有限字符容易令人混淆,例如数字0和字母O,小写的l和大写的I,于是使用base58能避免这些问题,我们看看base58编码实现:
'''
base58 的编码字符没有小写的l和大写的I,以及大写的字母O
'''
BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
def encode_base58(s):
'''
先统计要编码的数据以多少个0开头
'''
count = 0
for c in s:
if c == 0:
count += 1
else:
break
num = int.from_bytes(s, 'big')
prefix = '1' * count
result = ''
while num > 0:
num, mod = divmod(num, 58)
result = BASE58_ALPHABET[mod] + result
return prefix + result
我们试试上面代码:
'''
测试base58编码
'''
encode = encode_base58(bytes.fromhex('7c076ff316692a3d7eb3c3bb0f8b1488cf72e1afcd929e29307032997a838a3d'))
print(f"base58 encode:{encode}")
上面代码运行后得到的编码内容为:
9MA8fRQrT4u8Zj8ZRd6MAiiyaxb2Y1CMpvVkHQu5hVM6
base58编码本身有很多毛病,在市场上已经很少用,只是它某些特定特点正好能在区块链或比特币上用到。在以太坊或比特币应用上,数字货币在转账时需要有对应的接收地址,而这个地址的编码就使用到了base58,我们看看具体流程: 1,如果地址来自主网,那么以0x00开头,如果来自测试网络则以0x6f开头。 2,拿到SEC编码(可以是压缩也可以非压缩),对其进行sha256哈希,然后把结果再次进行ripemd160哈希,这个过程叫hash160操作。 3,把第一和第二步所得结果前后连接起来 4,将第3步结果进行sha256哈希,取开头4个字节 5,将第3和第4步所得结果结合起来,进行base58编码 第5步所得结果也叫校验和,假设我们已经有了第3步的结果,然后看看如何实现第4,5两步,代码如下:
'''
实现地址编码的第4,5两步
'''
def encode_base58_checksum(b):
return encode_base58(b + hashlib.sha256(b).digest()[:4])
上面提到的hash160操作其实可以直接调用hashlib接口来实现:
def hash160(s):
return hashlib.new('ripemd160', hashlib.sha256(s).digest()).digest()
'''
在比特币应用中,哈希256都会连续执行两次以增强安全性
'''
def hash256(s):
return hashlib.sha256(hashlib.sha256(s).digest()).digest()
我们把这些步骤正好到椭圆曲线的点中:
class BitcoinEllipticPoint(EllipticPoint):
....
def hash160(self, compressed=True):
return hash160(self.sec(compressed))
def address(self, compressed=True, testnet=False):
h160 = self.hash160(compressed)
if testnet:
prefix = b'\x6f'
else:
prefix = b'\x00'
return encode_base58_checksum(prefix + h160)
我们运行上面实现的逻辑看看:
'''
测试椭圆曲线点的地址编码
'''
priv = PrivateKey(5002)
print(f"address for uncompressed SEC on testnet:{priv.point.address(compressed = False, testnet = True)}")
代码运行后结果如下:
address for uncompressed SEC on testnet:mmTPbXQFxboEtNRkwfh6K51jvdtHLxGeMA
还有一个数据结构需要序列化,那就是私钥。这个东西由于非常敏感,一旦你的私钥丢失,你所有的货币资产就会被别人窃取,因此我们通常不会在网络上传输私钥,但极个别时刻需要这么做,因此我们也需要对私钥进行序列化,它对应的格式叫WIF(Wallet Import Format),它的序列化步骤如下: 1,如果是主网私钥,以0x80开头,如果是测试网以0xef开头 2,将私钥转换为大端字节数组进行编码 3,如果公钥使用压缩SEC格式,那么在末尾添加0x01 4,将1,2,3三个步骤所得结果结合起来 5,将第四步进行sha256(也就是连续两轮256哈希)运算,取结果的前4个字节 6,将步骤4和5结合,使用base58进行编码 我们看看相应的代码实现:
class PrivateKey:
....
def wif(self, compressed = True, testnet = False):
#先将秘钥转换为大端字节序
secret_bytes = self.secret.to_bytes(32, 'big')
if testnet: #根据主网还是测试网添加前缀
prefix = b'\xef'
else:
prefix = b'\x80'
if compressed: # 根据压缩形态添加后缀
suffix = b'\x01'
else:
suffix = b'\01'
return encode_base58_checksum(prefix + secret_bytes + suffix)
我们实验一下秘钥的序列化功能:
''
检验秘钥的序列化功能
'''
priv = PrivateKey(5003)
print(f"secret key wif:{priv.wif(True, True)}")
上面代码运行后结果为:
secret key wif:cMahea7zqjxrtgAbB7LSGbcQUr1uX1ojuat9jZodMN8rFTv2sfUK
比特币应用的代码很混乱,中本聪混淆着使用大端字节序和小端字节序,这是创始人的特权,就像鲁迅写错别字就成为了“通假字”,因此我们还需要了解小端编码的实现:
'''
我们使用一段文字进行256哈希形成秘钥,然后设置秘钥的地址格式
'''
secret_text = "this is my secret key"
secret = little_endian_to_int(hash256(secret_text.encode('utf-8')))
priv = PrivateKey(secret)
print(f"secret key address:{priv.point.address(testnet=True)}")
上面代码运行后所得结果为:
secret key address:mtg8uRgVQHjPiX15mmt7FVZeqSUbkUn3h3
我们这节内容很多,好在逻辑并不复杂,主要就是比较繁琐。代码下载路径:https://github.com/wycl16514/blockchain-serialization.git