行业知识
区块链课程:python实现交易
原���:
步骤 1:生成加密身份
首先,我们需要生成一个全新的加密身份,这只是一个私钥和公钥对。比特币使用椭圆曲线加密算法来保护交易,而不是像RSA这样的更常见的算法。在这里,我不会做一个完整的椭圆曲线介绍,因为其他人已经做得非常好,例如,我发现的Andrea Corbellini的博客文章系列是一个出色的资源。在这里,我们只需要编写代码,但要理解它为什么在数学上有效,你需要阅读这个系列。
好吧,比特币使用secp256k1曲线。作为这个领域的新手,我发现这部分非常吸引人——你可以选择不同的曲线库,它们提供不同的优缺点和特性。NIST发布了关于使用哪些曲线的建议,但人们更喜欢使用其他曲线(如secp256k1),因为它们不太可能有后门。无论如何,椭圆曲线是一个相当低维的数学对象,只需要用3个整数来定义:
from __future__ import annotations # PEP 563: Postponed Evaluation of Annotationsfrom dataclasses import dataclass # https://docs.python.org/3/library/dataclasses.html I like these a lot@dataclassclass Curve: """ Elliptic Curve over the field of integers modulo a prime. Points on the curve satisfy y^2 = x^3 + a*x + b (mod p). """ p: int # the prime modulus of the finite field a: int b: int# secp256k1 uses a = 0, b = 7, so we're dealing with the curve y^2 = x^3 + 7 (mod p)bitcoin_curve = Curve( p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F, a = 0x0000000000000000000000000000000000000000000000000000000000000000, # a = 0 b = 0x0000000000000000000000000000000000000000000000000000000000000007, # b = 7
除了实际的曲线之外,我们定义一个生成器点,这只是一个曲线周期上的固定“起始点”,用于启动围绕曲线的“随机行走”。生成器是一个公开已知且公认的常数:
@dataclassclass Point: """ An integer point (x,y) on a Curve """ curve: Curve x: int y: intG = Point( bitcoin_curve, x = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, y = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8,)# we can verify that the generator point is indeed on the curve, i.e. y^2 = x^3 + 7 (mod p)print("Generator IS on the curve: ", (G.y**2 - G.x**3 - 7) % bitcoin_curve.p == 0)# some other totally random point will of course not be on the curve, _MOST_ likelyimport randomrandom.seed(1337)x = random.randrange(0, bitcoin_curve.p)y = random.randrange(0, bitcoin_curve.p)print("Totally random point is not: ", (y**2 - x**3 - 7) % bitcoin_curve.p == 0) 最后,生成点的阶是已知的,它实际上是我们在曲线周期的(x,y)整数元组集合中工作的“集合大小”。我喜欢将这个信息组织成另一个数据结构,我将称之为生成点:
@dataclassclass Generator: """ A generator over a curve: an initial point and the (pre-computed) order """ G: Point # a generator point on the curve n: int # the order of the generating point, so 0*G = n*G = INFbitcoin_gen = Generator( G = G, # the order of G is known and can be mathematically derived n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,)
请注意,到目前为止,我们实际上并没有做任何事情,一切都是关于定义一些数据结构,并将与比特币中使用的椭圆曲线相关的公开已知常数填充到这些结构中。现在情况即将改变,因为我们准备生成我们的私钥。私钥(或者我接下来会称之为“秘密密钥”)只是一个满足 1 Point: assert isinstance(k, int) and k >= 0 result = INF append = self while k: if k & 1: result += append append += append k >>= 1 return result# monkey patch double and add into the Point class for conveniencePoint.__rmul__ = double_and_add# "verify" correctnessprint(G == 1*G)print(G + G == 2*G)print(G + G + G == 3*G)
快速计算:
# efficiently calculate our actual public key!public_key = secret_key * Gprint(f"x: {public_key.x}\ny: {public_key.y}")print("Verify the public key is on the curve: ", (public_key.y**2 - public_key.x**3 - 7) % bitcoin_curve.p == 0) 通过私钥/公钥对,我们现在生成了我们的加密身份。现在该是提取与之关联的比特币钱包地址的时候了。钱包地址不仅仅是公钥本身,它可以从公钥确定性地派生出来,并有一些额外的优点(例如内置的校验和)。在我们能够生成地址之前,我们需要定义一些哈希函数。比特币使用无处不在的SHA-256,还有RIPEMD-160。我们本可以简单地使用Python的hashlib中提供的实现,但这是一个零依赖的实现,所以导入hashlib是作弊。所以首先,这里是我在纯Python中根据(相对易读的)NIST FIPS PUB 180-4文档编写的SHA256实现:
def gen_sha256_with_variable_scope_protector_to_not_pollute_global_namespace(): """ SHA256 implementation. Follows the FIPS PUB 180-4 description for calculating SHA-256 hash function https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf Noone in their right mind should use this for any serious reason. This was written purely for educational purposes. """ import math from itertools import count, islice # ----------------------------------------------------------------------------- # SHA-256 Functions, defined in Section 4 def rotr(x, n, size=32): return (x >> n) | (x > n def sig0(x): return rotr(x, 7) ^ rotr(x, 18) ^ shr(x, 3) def sig1(x): return rotr(x, 17) ^ rotr(x, 19) ^ shr(x, 10) def capsig0(x): return rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22) def capsig1(x): return rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25) def ch(x, y, z): return (x & y)^ (~x & z) def maj(x, y, z): return (x & y) ^ (x & z) ^ (y & z) def b2i(b): return int.from_bytes(b, 'big') def i2b(i): return i.to_bytes(4, 'big') # ----------------------------------------------------------------------------- # SHA-256 Constants def is_prime(n): return not any(f for f in range(2,int(math.sqrt(n))+1) if n%f == 0) def first_n_primes(n): return islice(filter(is_prime, count(start=2)), n) def frac_bin(f, n=32): """ return the first n bits of fractional part of float f """ f -= math.floor(f) # get only the fractional part f *= 2**n # shift left f = int(f) # truncate the rest of the fractional content return f def genK(): """ Follows Section 4.2.2 to generate K The first 32 bits of the fractional parts of the cube roots of the first 64 prime numbers: 428a2f98 71374491 b5c0fbcf e9b5dba5 3956c25b 59f111f1 923f82a4 ab1c5ed5 d807aa98 12835b01 243185be 550c7dc3 72be5d74 80deb1fe 9bdc06a7 c19bf174 e49b69c1 efbe4786 0fc19dc6 240ca1cc 2de92c6f 4a7484aa 5cb0a9dc 76f988da 983e5152 a831c66d b00327c8 bf597fc7 c6e00bf3 d5a79147 06ca6351 14292967 27b70a85 2e1b2138 4d2c6dfc 53380d13 650a7354 766a0abb 81c2c92e 92722c85 a2bfe8a1 a81a664b c24b8b70 c76c51a3 d192e819 d6990624 f40e3585 106aa070 19a4c116 1e376c08 2748774c 34b0bcb5 391c0cb3 4ed8aa4a 5b9cca4f 682e6ff3 748f82ee 78a5636f 84c87814 8cc70208 90befffa a4506ceb bef9a3f7 c67178f2 """ return [frac_bin(p ** (1/3.0)) for p in first_n_primes(64)] def genH(): """ Follows Section 5.3.3 to generate the initial hash value H^0 The first 32 bits of the fractional parts of the square roots of the first 8 prime numbers. 6a09e667 bb67ae85 3c6ef372 a54ff53a 9b05688c 510e527f 1f83d9ab 5be0cd19 """ return [frac_bin(p ** (1/2.0)) for p in first_n_primes(8)] # ----------------------------------------------------------------------------- def pad(b): """ Follows Section 5.1: Padding the message """ b = bytearray(b) # convert to a mutable equivalent l = len(b) * 8 # note: len returns number of bytes not bits # append but "1" to the end of the message b.append(0b10000000) # appending 10000000 in binary (=128 in decimal) # follow by k zero bits, where k is the smallest non-negative solution to # l + 1 + k = 448 mod 512 # i.e. pad with zeros until we reach 448 (mod 512) while (len(b)*8) % 512 != 448: b.append(0x00) # the last 64-bit block is the length l of the original message # expressed in binary (big endian) b.extend(l.to_bytes(8, 'big')) return b def sha256(b: bytes) -> bytes: # Section 4.2 K = genK() # Section 5: Preprocessing # Section 5.1: Pad the message b = pad(b) # Section 5.2: Separate the message into blocks of 512 bits (64 bytes) blocks = [b[i:i+64] for i in range(0, len(b), 64)] # for each message block M^1 ... M^N H = genH() # Section 5.3 # Section 6 for M in blocks: # each block is a 64-entry array of 8-bit bytes # 1. Prepare the message schedule, a 64-entry array of 32-bit words W = [] for t in range(64): if t bytes: """ simple wrapper for a simpler API to this hash function, just bytes to bytes """ ctx = RMDContext() RMD160Update(ctx, b, len(b)) digest = RMD160Final(ctx) return digest # ----------------------------------------------------------------------------- class RMDContext: def __init__(self): self.state = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0] # uint32 self.count = 0 # uint64 self.buffer = [0]*64 # uchar def RMD160Update(ctx, inp, inplen): have = int((ctx.count // 8) % 64) inplen = int(inplen) need = 64 - have ctx.count += 8 * inplen off = 0 if inplen >= need: if have: for i in range(need): ctx.buffer[have+i] = inp[i] RMD160Transform(ctx.state, ctx.buffer) off = need have = 0 while off + 64 (32 - n)) def F0(x, y, z): return x ^ y ^ z def F1(x, y, z): return (x & y) | (((~x) % 0x100000000) & z) def F2(x, y, z): return (x | ((~y) % 0x100000000)) ^ z def F3(x, y, z): return (x & z) | (((~z) % 0x100000000) & y) def F4(x, y, z): return x ^ (y | ((~z) % 0x100000000)) def R(a, b, c, d, e, Fj, Kj, sj, rj, X): a = ROL(sj, (a + Fj(b, c, d) + X[rj] + Kj) % 0x100000000) + e c = ROL(10, c) return a % 0x100000000, c def RMD160Transform(state, block): #uint32 state[5], uchar block[64] x = [0]*16 assert sys.byteorder == 'little', "Only little endian is supported atm for RIPEMD160" x = struct.unpack(' bytes: """ Encode this transaction as bytes. If sig_index is given then return the modified transaction encoding of this tx with respect to the single input index. This result then constitutes the "message" that gets signed by the aspiring transactor of this input. """ out = [] # encode metadata out += [encode_int(self.version, 4)] # encode inputs out += [encode_varint(len(self.tx_ins))] if sig_index == -1: # we are just serializing a fully formed transaction out += [tx_in.encode() for tx_in in self.tx_ins] else: # used when crafting digital signature for a specific input index out += [tx_in.encode(script_override=(sig_index == i)) for i, tx_in in enumerate(self.tx_ins)] # encode outputs out += [encode_varint(len(self.tx_outs))] out += [tx_out.encode() for tx_out in self.tx_outs] # encode... other metadata out += [encode_int(self.locktime, 4)] out += [encode_int(1, 4) if sig_index != -1 else b''] # 1 = SIGHASH_ALL return b''.join(out)# we also need to know how to encode TxIn. This is just serialization protocol.def txin_encode(self, script_override=None): out = [] out += [self.prev_tx[::-1]] # little endian vs big endian encodings... sigh out += [encode_int(self.prev_index, 4)] if script_override is None: # None = just use the actual script out += [self.script_sig.encode()] elif script_override is True: # True = override the script with the script_pubkey of the associated input out += [self.prev_tx_script_pubkey.encode()] elif script_override is False: # False = override with an empty script out += [Script([]).encode()] else: raise ValueError("script_override must be one of None|True|False") out += [encode_int(self.sequence, 4)] return b''.join(out)TxIn.encode = txin_encode # monkey patch into the class# and TxOut as welldef txout_encode(self): out = [] out += [encode_int(self.amount, 8)] out += [self.script_pubkey.encode()] return b''.join(out)TxOut.encode = txout_encode # monkey patch into the classtx = Tx( version = 1, tx_ins = [tx_in], tx_outs = [tx_out1, tx_out2],) 在我们可以调用我们交易对象的`.encode()`方法并将其内容作为字节序列以便签名之前,我们需要满足比特币的一个规则,即我们用这个输入所指向的交易输出的 script_pubkey 替换 script_sig 的编码(我们没有,因为我们正在尝试生成它…)。以下是原始交易的链接再次。我们试图花费它的输出索引1,而 script_pubkey 是,再次,
OP_DUPOP_HASH1604b3518229b0d3554fe7cd3796ade632aff3069d8OP_EQUALVERIFYOP_CHECKSIG
这个特定的区块浏览器网站不支持我们以原始(字节)形式获取数据,因此我们将重新创建这个数据结构作为一个脚本(Script):
ource_script = Script([118, 169, out2_pkb_hash, 136, 172]) # OP_DUP, OP_HASH160, , OP_EQUALVERIFY, OP_CHECKSIGprint("recall out2_pkb_hash is just raw bytes of the hash of public_key: ", out2_pkb_hash.hex())print(source_script.encode().hex()) # we can get the bytes of the script_pubkey now# monkey patch this into the input of the transaction we are trying sign and constructtx_in.prev_tx_script_pubkey = source_script# get the "message" we need to digitally sign!!message = tx.encode(sig_index = 0)message.hex() 好的,让我们暂停一下。我们已经将交易编码成字节,以创建一个“消息”,在数字签名术语中。想想上面的字节编码了什么,以及我们即将签署的是什么。我们通过引用特定先前交易的输出(在这里,当然只有一个输入)来标识这个交易的 exact inputs。我们还标识了这个交易的 exact outputs(即将被铸造的 UTXOs,的说法),以及它们 script_pubkey 字段,在最常见的情况下,这些字段通过脚本封装了每个输出的所有者的公钥哈希。特别是,当我们签署一个特定的输入时,我们当然不包括其他输入的 script_sig(你可以看到 txin_encode 函数将它们设置为空脚本)。实际上,在完全一般(尽管不常见)的情况下,我们甚至可能没有它们。所以这个消息实际上编码的只是输入和新输出,它们的金额,以及它们的所有者(通过锁定脚本指定每个所有者的公钥哈希)。
我们现在准备用我们的私钥对消息进行数字签名。实际的签名是一个整数对的元组(r, s)。与椭圆曲线密码学(ECC)一样,我不会涵盖椭圆曲线数字签名算法(ECDSA)的完整数学细节。相反,只是提供代码,并展示这并不是一件可怕的事情:
@dataclassclass Signature: r: int s: intdef sign(secret_key: int, message: bytes) -> Signature: # the order of the elliptic curve used in bitcoin n = bitcoin_gen.n # double hash the message and convert to integer z = int.from_bytes(sha256(sha256(message)), 'big') # generate a new secret/public key pair at random sk = random.randrange(1, n) P = sk * bitcoin_gen.G # calculate the signature r = P.x s = inv(sk, n) * (z + secret_key * r) % n if s > n / 2: s = n - s sig = Signature(r, s) return sigdef verify(public_key: Point, message: bytes, sig: Signature) -> bool: # just a stub for reference on how a signature would be verified in terms of the API # we don't need to verify any signatures to craft a transaction, but we would if we were mining passrandom.seed(int.from_bytes(sha256(message), 'big')) # see note belowsig = sign(secret_key, message)sig
在上面的内容中,你会注意到一个经常被提及(并且是非常有道理的)的微妙之处:在这种天真形式中,我们在签名过程中生成 sk 时会产生一个随机数。这意味着我们的签名每次签名时都会改变,这对于许多原因来说是不希望的,包括这个练习的可重复性。而且,顺便说一下,情况会迅速变得更糟:如果你用同一个 sk 签署两个不同的消息,攻击者可以恢复秘密密钥,可怕。问问 Playstation 3 的人就知道了。有一个特定的标准(称为 RFC 6979),它推荐了一个确定性地生成 sk 的特定方法,但在这里为了简洁起见,我们省略了。相反,我这里实现了一个简陋的版本,其中我将 rng 种子为一个消息的哈希。请不要在任何接近生产环境的地方使用这个。
现在让我们实现一个 Signature 的 encode 函数,这样我们就可以通过比特币协议广播它。为此,我们使用 DER(Distinguished Encoding Rules)编码:
def signature_encode(self) -> bytes: """ return the DER encoding of this signature """ def dern(n): nb = n.to_bytes(32, byteorder='big') nb = nb.lstrip(b'\x00') # strip leading zeros nb = (b'\x00' if nb[0] >= 0x80 else b'') + nb # preprend 0x00 if first byte >= 0x80 return nb rb = dern(self.r) sb = dern(self.s) content = b''.join([bytes([0x02, len(rb)]), rb, bytes([0x02, len(sb)]), sb]) frame = b''.join([bytes([0x30, len(content)]), content]) return frameSignature.encode = signature_encode # monkey patch into the classsig_bytes = sig.encode()sig_bytes.hex()
我们终于准备好为我们的交易中的单个输入生成 script_sig 了。出于即将阐明的原因,它将包含恰好两个元素:1)签名和2)公钥,两者都作为字节编码:
# Append 1 (= SIGHASH_ALL), indicating this DER signature we created encoded "ALL" of the tx (by far most common)sig_bytes_and_type = sig_bytes + b'\x01'# Encode the public key into bytes. Notice we use hash160=False so we are revealing the full public key to Blockchainpubkey_bytes = PublicKey.from_point(public_key).encode(compressed=True, hash160=False)# Create a lightweight Script that just encodes those two things!script_sig = Script([sig_bytes_and_type, pubkey_bytes])tx_in.script_sig = script_sig
好的,既然我们已经创建了锁定脚本(script_pubkey)和解锁脚本(script_sig),我们可以简要地回顾一下这两个脚本在比特币脚本环境中的相互作用。在高层次上,在挖矿过程中的交易验证过程中,每个交易输入的这两个脚本会被连接成一个脚本,然后在这个“比特币虚拟机”(Bitcoin VM)中运行。我们现在可以看到,连接这两个脚本看起来会像这样:
OP_DUPOP_HASH160OP_EQUALVERIFYOP_CHECKSIG
这个脚本是从上到下执行的,使用典型的基于栈的推送/弹出方案,其中任何字节都会被推入栈中,任何操作都会消耗一些输入并推送一些输出。所以这里我们把签名和公钥推入栈中,然后公钥被复制(OP_DUP),被哈希(OP_HASH160),哈希值与 pubkey_hash_bytes 进行比较(OP_EQUALVERIFY),最后验证数字签名的完整性,确保它是由关联的私钥签发的。
我们已经完成了所有必要的步骤!让我们再次查看我们完全构建的交易的代表性表示。tx确实非常轻量级,不是吗?比特币交易的内容并不多。让我们将它编码成字节并以十六进制显示:
txtx.encode().hex()print("Transaction size in bytes: ", len(tx.encode())) 最后,我们来算交易的id
def tx_id(self) -> str: return sha256(sha256(self.encode()))[::-1].hex() # little/big endian conventions require byte order swapTx.id = tx_id # monkey patch into the classtx.id() # once this transaction goes through, this will be its id
现在我们准备将交易广播到世界各地的比特币节点。我们实际上是在向比特币网络发送定义我们交易的225字节(嵌入在标准的比特币协议网络封套中)。比特币节点将解码它,验证它,并在他们可能随时挖掘的下一个区块中包含它。用英语来说,这225字节是在说:“你好,比特币网络,你好吗?很好。我想创建一个新的交易,取 transaction 46325085c89fb98a4b7ceee44eac9b955f09e1ddc86d8dad3dfdcba46b4d36b2 在索引 1 的输出(UTXO),我想将其金额分成两个输出,一个输出到地址 mrFF91kpuRbivucowsY512fDnYt6BWrvx9,金额为50,000 sat,另一个输出到地址 mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ,金额为47,500 sat。(理解为剩余的2,500 sat将归入任何包含此交易的矿工的块中。)这里是证明我可以花费这个UTXO的两份文件:我的公钥,以及由相关私钥生成的数字签名,证明了我上述的意图。Kkthx!”
我们准备将这个信息广播到网络上,看看它是否能够成功。我们可以在这里包含一个简单的客户端,它通过套接字与节点通信,使用比特币协议——我们首先进行握手(发送版本信息来交换),然后使用tx消息广播上面提到的交易字节。然而,代码相当长,并不是非常有趣(它遵循比特币协议中描述的具体消息格式进行序列化),因此为了避免进一步膨胀这个笔记本,我将使用blockstream的方便的tx/push端点来广播交易。它只是一个大型文本框,我们精确地复制粘贴上面的原始交易十六进制,然后点击“广播”。如果你想要手动使用原始比特币协议来做这个,你会想要查看我的SimpleNode实现,并使用它通过套接字与节点通信。
import time; time.sleep(1.0) # now we wait :p, for the network to execute the transaction and include it in a block
交易在这里!我们可以看到,我们的原始字节被正确解析,并且交易被判断为有效,被包含在区块 2005515 中。我们的交易是这个区块中包含的 31 个交易之一,矿工 claiming our fee 作为感谢。
总结一下:再创建一个合并交易
现在让我们把所有东西结合起来,创建最后一个身份,并将我们在这个钱包中剩余的所有资金合并在一起。
secret_key3 = int.from_bytes(b"Andrej's Super Secret 3rd Wallet", 'big') # or just random.randrange(1, bitcoin_gen.n)assert 1
上一篇:手机下载路径修改指南
下一篇:ark服务器怎么建立?