原文:

步骤 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 fielda: intb: 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 = 0b = 0x0000000000000000000000000000000000000000000000000000000000000007, # b = 7

除了实际的曲线之外,我们定义一个生成器点,这只是一个曲线周期上的固定“起始点”,用于启动围绕曲线的“随机行走”。生成器是一个公开已知且公认的常数:

@dataclassclass Point:""" An integer point (x,y) on a Curve """curve: Curvex: inty: 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 curven: 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 derivedn = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,)

请注意,到目前为止,我们实际上并没有做任何事情,一切都是关于定义一些数据结构,并将与比特币中使用的椭圆曲线相关的公开已知常数填充到这些结构中。现在情况即将改变,因为我们准备生成我们的私钥。私钥(或者我接下来会称之为“秘密密钥”)只是一个满足 1 <= key < n 的随机整数(回想一下 n 是 G 的阶):

# secret_key = random.randrange(1, bitcoin_gen.n) # this is how you _would_ do itsecret_key = int.from_bytes(b'Andrej is cool :P', 'big') # this is how I will do it for reproducibilityassert 1 <= secret_key < bitcoin_gen.nprint(secret_key)

这是我们的秘密密钥——它是一个相当不起眼的整数,但任何知道它的人都可以控制你在比特币区块链上拥有的所有资金,以及与之关联的资金。在比特币最简单、最普遍的用法案例中,它是控制你账户的单个“密码”。当然,在极其不可能的情况下,如果另一个人(比如安德烈)像我上面那样手动生成了他们的秘密密钥,与这个秘密密钥关联的钱包很可能比特币余额为零:)。如果不是这样,我们确实非常幸运。

现在我们要生成公钥,事情开始变得有趣。公钥是曲线上的一个点,通过将生成点对自己secret_key次相加得到。即:public_key = G + G + G + (secret key times) + G = secret_key * G。注意这里的’+‘(加)和’*’(乘)符号都非常特殊且有些混淆。秘密密钥是一个整数,但生成点G是一个(x,y)元组,它是曲线上的一个点,结果得到一个(x,y)元组的公钥,再次是一个曲线上的点。这就是我们必须实际上定义椭圆曲线上的加法运算符的地方。它有一个非常具体的定义和几何解释(参见Andrea上面的帖子),但实际实现相对简单:

INF = Point(None, None, None) # special point at "infinity", kind of like a zerodef extended_euclidean_algorithm(a, b):"""Returns (gcd, x, y) s.t. a * x + b * y == gcdThis function implements the extended Euclideanalgorithm and runs in O(log b) in the worst case,taken from Wikipedia."""old_r, r = a, bold_s, s = 1, 0old_t, t = 0, 1while r != 0:quotient = old_r // rold_r, r = r, old_r - quotient * rold_s, s = s, old_s - quotient * sold_t, t = t, old_t - quotient * treturn old_r, old_s, old_tdef inv(n, p):""" returns modular multiplicate inverse m s.t. (n * m) % p == 1 """gcd, x, y = extended_euclidean_algorithm(n, p) # pylint: disable=unused-variablereturn x % pdef elliptic_curve_addition(self, other: Point) -> Point:# handle special case of P + 0 = 0 + P = 0if self == INF:return otherif other == INF:return self# handle special case of P + (-P) = 0if self.x == other.x and self.y != other.y:return INF# compute the "slope"if self.x == other.x: # (self.y = other.y is guaranteed too per above check)m = (3 * self.x**2 + self.curve.a) * inv(2 * self.y, self.curve.p)else:m = (self.y - other.y) * inv(self.x - other.x, self.curve.p)# compute the new pointrx = (m**2 - self.x - other.x) % self.curve.pry = (-(m*(rx - self.x) + self.y)) % self.curve.preturn Point(self.curve, rx, ry)Point.__add__ = elliptic_curve_addition # monkey patch addition into the Point class

我承认这可能看起来有点吓人,理解和重新推导上面的内容花了我大约半天的时间。大部分复杂性都来自于所有数学运算都是用模算术进行的。所以即使是简单的运算,比如除法’/’,也突然需要像模乘法逆元inv这样的算法。但重要的是要注意,一切都是只是一堆在(x,y)元组上的加法/乘法,中间到处都是模p。让我们试着通过生成一些简单的(私钥,公钥)密钥对来深入了解:

# if our secret key was the interger 1, then our public key would just be G:sk = 1pk = Gprint(f" secret key: {sk}\n public key: {(pk.x, pk.y)}")print("Verify the public key is on the curve: ", (pk.y**2 - pk.x**3 - 7) % bitcoin_curve.p == 0)# if it was 2, the public key is G + G:sk = 2pk = G + Gprint(f" secret key: {sk}\n public key: {(pk.x, pk.y)}")print("Verify the public key is on the curve: ", (pk.y**2 - pk.x**3 - 7) % bitcoin_curve.p == 0)# etc.:sk = 3pk = G + G + Gprint(f" secret key: {sk}\n public key: {(pk.x, pk.y)}")print("Verify the public key is on the curve: ", (pk.y**2 - pk.x**3 - 7) % bitcoin_curve.p == 0)

好的,所以我们有一些密钥对,但我们需要与上面随机生成的秘密密钥关联的公钥。仅使用上面的代码,我们不得不将G添加到自身很多次,因为秘密密钥是一个大整数。所以结果会是正确的,但运行速度会很慢。相反,让我们实现一个“双倍加法”算法来显著加快重复加法的速度。再次,请参阅上面的帖子了解它为什么有效,但这里是算法:

def double_and_add(self, k: int) -> Point:assert isinstance(k, int) and k >= 0result = INFappend = selfwhile k:if k & 1:result += appendappend += appendk >>= 1return 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 functionhttps://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdfNoone in their right mind should use this for any serious reason. This was writtenpurely for educational purposes."""import mathfrom itertools import count, islice# -----------------------------------------------------------------------------# SHA-256 Functions, defined in Section 4def rotr(x, n, size=32):return (x >> n) | (x <> ndef 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 Constantsdef 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 partf *= 2**n # shift leftf = int(f) # truncate the rest of the fractional contentreturn fdef genK():"""Follows Section 4.2.2 to generate KThe first 32 bits of the fractional parts of the cube roots of the first64 prime numbers:428a2f98 71374491 b5c0fbcf e9b5dba5 3956c25b 59f111f1 923f82a4 ab1c5ed5d807aa98 12835b01 243185be 550c7dc3 72be5d74 80deb1fe 9bdc06a7 c19bf174e49b69c1 efbe4786 0fc19dc6 240ca1cc 2de92c6f 4a7484aa 5cb0a9dc 76f988da983e5152 a831c66d b00327c8 bf597fc7 c6e00bf3 d5a79147 06ca6351 1429296727b70a85 2e1b2138 4d2c6dfc 53380d13 650a7354 766a0abb 81c2c92e 92722c85a2bfe8a1 a81a664b c24b8b70 c76c51a3 d192e819 d6990624 f40e3585 106aa07019a4c116 1e376c08 2748774c 34b0bcb5 391c0cb3 4ed8aa4a 5b9cca4f 682e6ff3748f82ee 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^0The first 32 bits of the fractional parts of the square roots ofthe 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 equivalentl = len(b) * 8 # note: len returns number of bytes not bits# append but "1" to the end of the messageb.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 bdef sha256(b: bytes) -> bytes:# Section 4.2K = genK()# Section 5: Preprocessing# Section 5.1: Pad the messageb = 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^NH = genH() # Section 5.3# Section 6for 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 wordsW = []for t in range(64):if t <= 15:# the first 16 words are just a copy of the blockW.append(bytes(M[t*4:t*4+4]))else:term1 = sig1(b2i(W[t-2]))term2 = b2i(W[t-7])term3 = sig0(b2i(W[t-15]))term4 = b2i(W[t-16])total = (term1 + term2 + term3 + term4) % 2**32W.append(i2b(total))# 2. Initialize the 8 working variables a,b,c,d,e,f,g,h with prev hash valuea, b, c, d, e, f, g, h = H# 3.for t in range(64):T1 = (h + capsig1(e) + ch(e, f, g) + K[t] + b2i(W[t])) % 2**32T2 = (capsig0(a) + maj(a, b, c)) % 2**32h = gg = ff = ee = (d + T1) % 2**32d = cc = bb = aa = (T1 + T2) % 2**32# 4. Compute the i-th intermediate hash value H^idelta = [a, b, c, d, e, f, g, h]H = [(i1 + i2) % 2**32 for i1, i2 in zip(H, delta)]return b''.join(i2b(i) for i in H)return sha256sha256 = gen_sha256_with_variable_scope_protector_to_not_pollute_global_namespace()print("verify empty hash:", sha256(b'').hex()) # should be e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855print(sha256(b'here is a random bytes message, cool right?').hex())print("number of bytes in a sha256 digest: ", len(sha256(b'')))

我之所以想从零开始实现这个算法并贴在这里,是因为我想让你再次注意,里面并没有发生什么太可怕的事情。SHA256接收一些待哈希的字节消息,首先填充消息,然后将其分成块,并将这些块传递给一个可以最好地描述为“位混合器”的东西,该混合器定义在第3节中,其中包含一些位移和二进制操作,这些操作对我来说坦白说超出了我的理解范围,但结果是产生了SHA256所提供的美丽属性。特别是,它创建了一个固定大小的、看起来随机的短摘要,对于任何可变大小的原始消息,使得混淆是不可逆的,而且基本上不可能构建一个不同的消息,其哈希值与给定的摘要相同。

比特币到处都在使用SHA256来创建哈希值,当然,它是比特币工作量证明(Proof of Work)的核心元素,目标是修改交易块,直到整个东西的哈希值足够小(当摘要的字节被解释为一个数字时)。由于SHA256的美好属性,只能通过暴力搜索来完成。所以所有为了高效挖矿而设计的ASIC硬件,都只是上面代码的极其优化、接近硬件的实现。

不管怎样,在我们可以生成我们的地址之前,我们还需要RIPEMD160哈希函数,我在互联网上找到了它,并进行了缩短和清理:

def gen_ripemd160_with_variable_scope_protector_to_not_pollute_global_namespace():import sysimport struct# -----------------------------------------------------------------------------# public interfacedef ripemd160(b: bytes) -> 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] # uint32self.count = 0 # uint64self.buffer = [0]*64 # uchardef RMD160Update(ctx, inp, inplen):have = int((ctx.count // 8) % 64)inplen = int(inplen)need = 64 - havectx.count += 8 * inplenoff = 0if inplen >= need:if have:for i in range(need):ctx.buffer[have+i] = inp[i]RMD160Transform(ctx.state, ctx.buffer)off = needhave = 0while off + 64 <= inplen:RMD160Transform(ctx.state, inp[off:])off += 64if off < inplen:for i in range(inplen - off):ctx.buffer[have+i] = inp[off+i]def RMD160Final(ctx):size = struct.pack("<Q", ctx.count)padlen = 64 - ((ctx.count // 8) % 64)if padlen < 1 + 8:padlen += 64RMD160Update(ctx, PADDING, padlen-8)RMD160Update(ctx, size, 8)return struct.pack("<5L", *ctx.state)# -----------------------------------------------------------------------------K0 = 0x00000000K1 = 0x5A827999K2 = 0x6ED9EBA1K3 = 0x8F1BBCDCK4 = 0xA953FD4EKK0 = 0x50A28BE6KK1 = 0x5C4DD124KK2 = 0x6D703EF3KK3 = 0x7A6D76E9KK4 = 0x00000000PADDING = [0x80] + [0]*63def ROL(n, x):return ((x <> (32 - n))def F0(x, y, z):return x ^ y ^ zdef F1(x, y, z):return (x & y) | (((~x) % 0x100000000) & z)def F2(x, y, z):return (x | ((~y) % 0x100000000)) ^ zdef 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) + ec = ROL(10, c)return a % 0x100000000, cdef RMD160Transform(state, block): #uint32 state[5], uchar block[64]x = [0]*16assert sys.byteorder == 'little', "Only little endian is supported atm for RIPEMD160"x = struct.unpack('<16L', bytes(block[0:64]))a = state[0]b = state[1]c = state[2]d = state[3]e = state[4]#/* Round 1 */a, c = R(a, b, c, d, e, F0, K0, 11,0, x)e, b = R(e, a, b, c, d, F0, K0, 14,1, x)d, a = R(d, e, a, b, c, F0, K0, 15,2, x)c, e = R(c, d, e, a, b, F0, K0, 12,3, x)b, d = R(b, c, d, e, a, F0, K0,5,4, x)a, c = R(a, b, c, d, e, F0, K0,8,5, x)e, b = R(e, a, b, c, d, F0, K0,7,6, x)d, a = R(d, e, a, b, c, F0, K0,9,7, x)c, e = R(c, d, e, a, b, F0, K0, 11,8, x)b, d = R(b, c, d, e, a, F0, K0, 13,9, x)a, c = R(a, b, c, d, e, F0, K0, 14, 10, x)e, b = R(e, a, b, c, d, F0, K0, 15, 11, x)d, a = R(d, e, a, b, c, F0, K0,6, 12, x)c, e = R(c, d, e, a, b, F0, K0,7, 13, x)b, d = R(b, c, d, e, a, F0, K0,9, 14, x)a, c = R(a, b, c, d, e, F0, K0,8, 15, x) #/* #15 */#/* Round 2 */e, b = R(e, a, b, c, d, F1, K1,7,7, x)d, a = R(d, e, a, b, c, F1, K1,6,4, x)c, e = R(c, d, e, a, b, F1, K1,8, 13, x)b, d = R(b, c, d, e, a, F1, K1, 13,1, x)a, c = R(a, b, c, d, e, F1, K1, 11, 10, x)e, b = R(e, a, b, c, d, F1, K1,9,6, x)d, a = R(d, e, a, b, c, F1, K1,7, 15, x)c, e = R(c, d, e, a, b, F1, K1, 15,3, x)b, d = R(b, c, d, e, a, F1, K1,7, 12, x)a, c = R(a, b, c, d, e, F1, K1, 12,0, x)e, b = R(e, a, b, c, d, F1, K1, 15,9, x)d, a = R(d, e, a, b, c, F1, K1,9,5, x)c, e = R(c, d, e, a, b, F1, K1, 11,2, x)b, d = R(b, c, d, e, a, F1, K1,7, 14, x)a, c = R(a, b, c, d, e, F1, K1, 13, 11, x)e, b = R(e, a, b, c, d, F1, K1, 12,8, x) #/* #31 */#/* Round 3 */d, a = R(d, e, a, b, c, F2, K2, 11,3, x)c, e = R(c, d, e, a, b, F2, K2, 13, 10, x)b, d = R(b, c, d, e, a, F2, K2,6, 14, x)a, c = R(a, b, c, d, e, F2, K2,7,4, x)e, b = R(e, a, b, c, d, F2, K2, 14,9, x)d, a = R(d, e, a, b, c, F2, K2,9, 15, x)c, e = R(c, d, e, a, b, F2, K2, 13,8, x)b, d = R(b, c, d, e, a, F2, K2, 15,1, x)a, c = R(a, b, c, d, e, F2, K2, 14,2, x)e, b = R(e, a, b, c, d, F2, K2,8,7, x)d, a = R(d, e, a, b, c, F2, K2, 13,0, x)c, e = R(c, d, e, a, b, F2, K2,6,6, x)b, d = R(b, c, d, e, a, F2, K2,5, 13, x)a, c = R(a, b, c, d, e, F2, K2, 12, 11, x)e, b = R(e, a, b, c, d, F2, K2,7,5, x)d, a = R(d, e, a, b, c, F2, K2,5, 12, x) #/* #47 */#/* Round 4 */c, e = R(c, d, e, a, b, F3, K3, 11,1, x)b, d = R(b, c, d, e, a, F3, K3, 12,9, x)a, c = R(a, b, c, d, e, F3, K3, 14, 11, x)e, b = R(e, a, b, c, d, F3, K3, 15, 10, x)d, a = R(d, e, a, b, c, F3, K3, 14,0, x)c, e = R(c, d, e, a, b, F3, K3, 15,8, x)b, d = R(b, c, d, e, a, F3, K3,9, 12, x)a, c = R(a, b, c, d, e, F3, K3,8,4, x)e, b = R(e, a, b, c, d, F3, K3,9, 13, x)d, a = R(d, e, a, b, c, F3, K3, 14,3, x)c, e = R(c, d, e, a, b, F3, K3,5,7, x)b, d = R(b, c, d, e, a, F3, K3,6, 15, x)a, c = R(a, b, c, d, e, F3, K3,8, 14, x)e, b = R(e, a, b, c, d, F3, K3,6,5, x)d, a = R(d, e, a, b, c, F3, K3,5,6, x)c, e = R(c, d, e, a, b, F3, K3, 12,2, x) #/* #63 */#/* Round 5 */ b, d = R(b, c, d, e, a, F4, K4,9,4, x)a, c = R(a, b, c, d, e, F4, K4, 15,0, x)e, b = R(e, a, b, c, d, F4, K4,5,5, x)d, a = R(d, e, a, b, c, F4, K4, 11,9, x)c, e = R(c, d, e, a, b, F4, K4,6,7, x)b, d = R(b, c, d, e, a, F4, K4,8, 12, x)a, c = R(a, b, c, d, e, F4, K4, 13,2, x)e, b = R(e, a, b, c, d, F4, K4, 12, 10, x)d, a = R(d, e, a, b, c, F4, K4,5, 14, x)c, e = R(c, d, e, a, b, F4, K4, 12,1, x)b, d = R(b, c, d, e, a, F4, K4, 13,3, x)a, c = R(a, b, c, d, e, F4, K4, 14,8, x)e, b = R(e, a, b, c, d, F4, K4, 11, 11, x)d, a = R(d, e, a, b, c, F4, K4,8,6, x)c, e = R(c, d, e, a, b, F4, K4,5, 15, x)b, d = R(b, c, d, e, a, F4, K4,6, 13, x) #/* #79 */aa = abb = bcc = cdd = dee = ea = state[0]b = state[1]c = state[2]d = state[3]e = state[4]#/* Parallel round 1 */a, c = R(a, b, c, d, e, F4, KK0,8,5, x)e, b = R(e, a, b, c, d, F4, KK0,9, 14, x)d, a = R(d, e, a, b, c, F4, KK0,9,7, x)c, e = R(c, d, e, a, b, F4, KK0, 11,0, x)b, d = R(b, c, d, e, a, F4, KK0, 13,9, x)a, c = R(a, b, c, d, e, F4, KK0, 15,2, x)e, b = R(e, a, b, c, d, F4, KK0, 15, 11, x) b, d = R(b, c, d, e, a, F4, K4,9,4, x)a, c = R(a, b, c, d, e, F4, K4, 15,0, x)e, b = R(e, a, b, c, d, F4, K4,5,5, x)d, a = R(d, e, a, b, c, F4, K4, 11,9, x)c, e = R(c, d, e, a, b, F4, K4,6,7, x)b, d = R(b, c, d, e, a, F4, K4,8, 12, x)a, c = R(a, b, c, d, e, F4, K4, 13,2, x)e, b = R(e, a, b, c, d, F4, K4, 12, 10, x)d, a = R(d, e, a, b, c, F4, K4,5, 14, x)c, e = R(c, d, e, a, b, F4, K4, 12,1, x)b, d = R(b, c, d, e, a, F4, K4, 13,3, x)a, c = R(a, b, c, d, e, F4, K4, 14,8, x)e, b = R(e, a, b, c, d, F4, K4, 11, 11, x)d, a = R(d, e, a, b, c, F4, K4,8,6, x)c, e = R(c, d, e, a, b, F4, K4,5, 15, x)b, d = R(b, c, d, e, a, F4, K4,6, 13, x) #/* #79 */aa = abb = bcc = cdd = dee = ea = state[0]b = state[1]c = state[2]d = state[3]e = state[4]#/* Parallel round 1 */a, c = R(a, b, c, d, e, F4, KK0,8,5, x)e, b = R(e, a, b, c, d, F4, KK0,9, 14, x)d, a = R(d, e, a, b, c, F4, KK0,9,7, x)c, e = R(c, d, e, a, b, F4, KK0, 11,0, x)b, d = R(b, c, d, e, a, F4, KK0, 13,9, x)a, c = R(a, b, c, d, e, F4, KK0, 15,2, x)e, b = R(e, a, b, c, d, F4, KK0, 15, 11, x) e, b = R(e, a, b, c, d, F2, KK2, 13,8, x)d, a = R(d, e, a, b, c, F2, KK2,5, 12, x)c, e = R(c, d, e, a, b, F2, KK2, 14,2, x)b, d = R(b, c, d, e, a, F2, KK2, 13, 10, x)a, c = R(a, b, c, d, e, F2, KK2, 13,0, x)e, b = R(e, a, b, c, d, F2, KK2,7,4, x)d, a = R(d, e, a, b, c, F2, KK2,5, 13, x) #/* #47 */#/* Parallel round 4 */c, e = R(c, d, e, a, b, F1, KK3, 15,8, x)b, d = R(b, c, d, e, a, F1, KK3,5,6, x)a, c = R(a, b, c, d, e, F1, KK3,8,4, x)e, b = R(e, a, b, c, d, F1, KK3, 11,1, x)d, a = R(d, e, a, b, c, F1, KK3, 14,3, x)c, e = R(c, d, e, a, b, F1, KK3, 14, 11, x)b, d = R(b, c, d, e, a, F1, KK3,6, 15, x)a, c = R(a, b, c, d, e, F1, KK3, 14,0, x)e, b = R(e, a, b, c, d, F1, KK3,6,5, x)d, a = R(d, e, a, b, c, F1, KK3,9, 12, x)c, e = R(c, d, e, a, b, F1, KK3, 12,2, x)b, d = R(b, c, d, e, a, F1, KK3,9, 13, x)a, c = R(a, b, c, d, e, F1, KK3, 12,9, x)e, b = R(e, a, b, c, d, F1, KK3,5,7, x)d, a = R(d, e, a, b, c, F1, KK3, 15, 10, x)c, e = R(c, d, e, a, b, F1, KK3,8, 14, x) #/* #63 */#/* Parallel round 5 */b, d = R(b, c, d, e, a, F0, KK4,8, 12, x)a, c = R(a, b, c, d, e, F0, KK4,5, 15, x)e, b = R(e, a, b, c, d, F0, KK4, 12, 10, x)d, a = R(d, e, a, b, c, F0, KK4,9,4, x)c, e = R(c, d, e, a, b, F0, KK4, 12,1, x)b, d = R(b, c, d, e, a, F0, KK4,5,5, x)a, c = R(a, b, c, d, e, F0, KK4, 14,8, x)e, b = R(e, a, b, c, d, F0, KK4,6,7, x)d, a = R(d, e, a, b, c, F0, KK4,8,6, x)c, e = R(c, d, e, a, b, F0, KK4, 13,2, x)b, d = R(b, c, d, e, a, F0, KK4,6, 13, x)a, c = R(a, b, c, d, e, F0, KK4,5, 14, x) e, b = R(e, a, b, c, d, F0, KK4, 15,0, x)d, a = R(d, e, a, b, c, F0, KK4, 13,3, x)c, e = R(c, d, e, a, b, F0, KK4, 11,9, x)b, d = R(b, c, d, e, a, F0, KK4, 11, 11, x) #/* #79 */t = (state[1] + cc + d) % 0x100000000state[1] = (state[2] + dd + e) % 0x100000000state[2] = (state[3] + ee + a) % 0x100000000state[3] = (state[4] + aa + b) % 0x100000000state[4] = (state[0] + bb + c) % 0x100000000state[0] = t % 0x100000000return ripemd160ripemd160 = gen_ripemd160_with_variable_scope_protector_to_not_pollute_global_namespace()print(ripemd160(b'hello this is a test').hex())print("number of bytes in a RIPEMD-160 digest: ", len(ripemd160(b'')))

与上面的SHA256一样,我们再次看到一个由许多二进制操作组成的“位混乱器”。非常酷。

好的,我们现在终于准备好获取我们的比特币地址了。我们将通过创建一个点类的子类来使其变得漂亮,这个子类叫做公钥(PublicKey),它再次是曲线上的一个点,但现在有一些额外的语义和解释,代表比特币公钥,以及一些将密钥编码/解码为字节以便在比特币协议中进行通信的方法。

class PublicKey(Point):"""The public key is just a Point on a Curve, but has some additional specificencoding / decoding functionality that this class implements."""@classmethoddef from_point(cls, pt: Point):""" promote a Point to be a PublicKey """return cls(pt.curve, pt.x, pt.y)def encode(self, compressed, hash160=False):""" return the SEC bytes encoding of the public key Point """# calculate the bytesif compressed:# (x,y) is very redundant. Because y^2 = x^3 + 7,# we can just encode x, and then y = +/- sqrt(x^3 + 7),# so we need one more bit to encode whether it was the + or the -# but because this is modular arithmetic there is no +/-, instead# it can be shown that one y will always be even and the other odd.prefix = b'\x02' if self.y % 2 == 0 else b'\x03'pkb = prefix + self.x.to_bytes(32, 'big')else:pkb = b'\x04' + self.x.to_bytes(32, 'big') + self.y.to_bytes(32, 'big')# hash if desiredreturn ripemd160(sha256(pkb)) if hash160 else pkbdef address(self, net: str, compressed: bool) -> str:""" return the associated bitcoin address for this public key as string """# encode the public key into bytes and hash to get the payloadpkb_hash = self.encode(compressed=compressed, hash160=True)# add version byte (0x00 for Main Network, or 0x6f for Test Network)version = {'main': b'\x00', 'test': b'\x6f'}ver_pkb_hash = version[net] + pkb_hash# calculate the checksumchecksum = sha256(sha256(ver_pkb_hash))[:4]# append to form the full 25-byte binary Bitcoin Addressbyte_address = ver_pkb_hash + checksum# finally b58 encode the resultb58check_address = b58encode(byte_address)return b58check_address

我们还没有准备好对这个类进行测试,因为你可能会注意到这里还有一个必要的依赖,那就是base58编码函数b58encode。这是一种比特币特定的字节编码,使用base58,即字母表中的非常不模糊的字符。例如,它不使用’O’和’0’,因为它们在纸上很容易混淆。所以我们必须将我们的比特币地址(在其原始形式中是25个字节)转换为base58,并打印出字符。尽管如此,我们地址的原始25个字节包含1个字节版本(比特币“主网”使用b’\x00’,而比特币“测试网”使用b’\x6f’),然后是来自哈希摘要的20个字节,最后是4个字节的校验和,这样我们就可以在用户在文本框中输入他们的比特币地址时,以93.75%的概率抛出错误。所以这里是base58编码:

# base58 encoding / decoding utilities# reference: https://en.bitcoin.it/wiki/Base58Check_encodingalphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'def b58encode(b: bytes) -> str:assert len(b) == 25 # version is 1 byte, pkb_hash 20 bytes, checksum 4 bytesn = int.from_bytes(b, 'big')chars = []while n:n, i = divmod(n, 58)chars.append(alphabet[i])# special case handle the leading 0 bytes... ¯\_(ツ)_/¯num_leading_zeros = len(b) - len(b.lstrip(b'\x00'))res = num_leading_zeros * alphabet[0] + ''.join(reversed(chars))return res

现在可以打印地址:

# we are going to use the develop's Bitcoin parallel universe "test net" for this demo, so net='test'address = PublicKey.from_point(public_key).address(net='test', compressed=True)print(address)

很好,现在我们可以查看一些区块链浏览器网站,以验证这个地址以前是否从未进行过交易:https://www.blockchain.com/btc-testnet/address/mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ。

到本教程结束时,情况将不再如此,但在撰写本文时,我确实看到这个地址是“干净”的,这意味着到目前为止,还没有人像我们上面那样在测试网上生成和使用私钥。这是有道理的,因为必须还有其他具有糟糕幽默感的“安德里亚”在玩比特币。但我们可以查看一些超级非秘密的私钥,我们预计过去有人使用过。例如,我们可以检查属于最低有效私钥1的地址,其中公钥正好是生成点 . 下面是我们获取它的方法:

lol_secret_key = 1lol_public_key = lol_secret_key * Glol_address = PublicKey.from_point(lol_public_key).address(net='test', compressed=True)lol_address

确实,正如我们在区块链浏览器上看到的那样,在撰写本文时,这个地址已经进行了1,812次交易,并且比特币余额为0.00美元。这是有道理的,因为如果它有任何余额(在幼稚的情况下,模一些我们将要探讨的脚本语言的细微差别),那么任何人都可以花费它,因为他们知道私钥(1)并可以使用它来数字签名花费它的交易。我们很快就会看到这是如何工作的。

第一部分:到目前为止的总结

我们能够生成一个由我们自己唯一知道的私钥(一个随机整数)组成的密码身份,并通过在比特币椭圆曲线上使用生成点的标量乘法跳跃来生成派生的公钥。我们还生成了相关的比特币地址,我们可以将其与他人分享以请求资金,这样做涉及引入了两个哈希函数(SHA256和RIPEMD160)。以下是三个重要量的总结,并再次打印出来:

(请注意,由于原文中提到的“私钥(1)”可能是一个错误,因为在比特币椭圆曲线上,私钥通常是一个介于1和n-1之间的随机数,其中n是曲线的生成器的次数。因此,这里的“1”可能应该是一个随机的私钥值。)

print("Our first Bitcoin identity:")print("1. secret key: ", secret_key)print("2. public key: ", (public_key.x, public_key.y))print("3. Bitcoin address: ", address)

第二部分:获得种子资金 + 深入了解比特币底层原理

现在该创建一个交易了。我们将要把一些BTC从上面生成的地址(mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ)发送到我们控制的另一个钱包。现在让我们创建这个第二个“目标”钱包:

secret_key2 = int.from_bytes(b"Andrej's Super Secret 2nd Wallet", 'big') # or just random.randrange(1, bitcoin_gen.n)assert 1 <= secret_key2 < bitcoin_gen.n # check it's validpublic_key2 = secret_key2 * Gaddress2 = PublicKey.from_point(public_key2).address(net='test', compressed=True)print("Our second Bitcoin identity:")print("1. secret key: ", secret_key2)print("2. public key: ", (public_key2.x, public_key2.y))print("3. Bitcoin address: ", address2)

好的,我们的目标是将一些BTC从mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ发送到mrFF91kpuRbivucowsY512fDnYt6BWrvx9。首先,因为我们刚刚从头开始生成了这些身份,所以第一个地址上没有比特币。由于我们正在使用“平行宇宙”开发者意图的比特币测试网络,我们可以使用多个可用的水龙头之一来非常客气地请求一些BTC。我通过搜索“bitcoin testnet faucet”,点击第一个链接,并请求水龙头将一些比特币发送到我们的源地址mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ。几分钟后,我们可以回到区块链浏览器并看到我们收到了硬币,在这种情况下是0.001 BTC。水龙头可用于测试网络,但当然你不会在主网络上找到它们:) 你必须例如打开一个Coinbase账户(该账户生成一个钱包)并用美元购买一些BTC。在本教程中,我们将使用测试网络,但我们在测试网络上做的一切在主网络上也会完全没问题。

现在如果我们点击确切的交易ID,我们可以看到一大堆额外信息,这些信息触及了比特币的核心以及如何在其中表示货币。

交易ID。首先要注意的是,每个交易都有一个独特的ID/散列。在这种情况下,水龙头交易具有ID 46325085c89fb98a4b7ceee44eac9b955f09e1ddc86d8dad3dfdcba46b4d36b2。正如我们将看到的,这只是一个SHA256双哈希(哈希的哈希)交易的Data Structure(我们很快就会看到)序列化为字节。双SHA256哈希通常用于比特币中的单哈希,以增加安全性,缓解单轮SHA256的一些不足和与之相关的一些攻击,这些攻击是在较旧版本的SHA(SHA-1)上发现的。

输入和输出。我们看到水龙头交易有1个输入和2个输出。1个输入来自地址2MwjXCY7RRpo8MYjtsJtP5erNirzFB9MtnH,价值0.17394181 BTC。有2个输出。第二个输出是我们的地址,我们确切地收到了0.001 BTC。第一个输出是一个不同的未知地址2NCorZJ6XfdimrFQuwWjcJhQJDxPqjNgLzG,收到了0.17294013 BTC,这可能是由水龙头所有者控制的。注意输入并不完全等于输出。确实我们有0.17394181 – (0.001 + 0.17294013) = 0.00000168。这个“找零”金额被称为费用,矿工可以将这个交易包含在他们的区块中,从而获得这笔费用,这个区块是区块2005500。你可以看到这个区块有48笔交易,水龙头交易是其中的一个!现在,费用作为矿工包含交易的财务激励,因为他们可以保留找零。矿工获得的费用越高,交易被包含在区块中的可能性就越大,速度也越快。高费用我们期望会被矿工热心接受,并包含在下一个区块中。如果费用低,交易可能永远不会被包含,因为有大量其他交易在网络上广播,他们愿意支付更高的费用。所以如果你是个矿工,你的区块中有限的空间——为什么还要费心呢?

当我们自己创建交易时,我们必须确保为矿工包含这个小费,并支付“市场费率”,我们将查找。在这个区块的情况下,我们可以看到矿工通过特殊“Coinbase”交易获得了0.09765625 BTC,每个矿工都可以从空输入发送给自己,然后通过这个块中所有47个非Coinbase交易累计获得了0.00316119 BTC作为总费用奖励。

大小。此外,注意到这个交易(序列化)是249字节。对于这样一个简单的交易来说,这是一个相当平均的大小。

Pkscript。最后,注意到第二个输出(我们的0.001 BTC)当你滚动到其详细信息时,有一个“Pkscript”字段,显示:

OP_DUPOP_HASH1604b3518229b0d3554fe7cd3796ade632aff3069d8OP_EQUALVERIFYOP_CHECKSIG

到这里,比特币的事情就变得有点疯狂了。它有一个完整的基于堆的脚本语言,但除非你正在做疯狂的多重签名智能合约三角托管翻转(?),绝大多数交易使用非常少数的简单“特殊情况”脚本,就像这里的这个一样。到现在为止,我已经对这种标准的简单事物视而不见了。这个“Pkscript”是这个特定输出的“锁定脚本”,其中包含了0.001 BTC。我们将想要花费这个输出,并将其变成我们即将创建的交易的一个输入。为了解锁这个输出,我们必须满足这个锁定脚本的条件。用英语来说,这个脚本说的是,任何想要花费这个输出的交易必须满足两个条件。1)他们的公钥哈希必须等于4b3518229b0d3554fe7cd3796ade632aff3069d8。2)有志于交易的数字签名必须通过验证,证明是由这个公钥关联的私钥生成的。只有私钥的拥有者才能同时满足1)提供完整的公钥,这将进行检查以确保哈希正确,以及2)创建数字签名,如我们将要看到的。

顺便说一下,我们可以验证我们的公钥当然哈希是正确的,所以我们将能够将其包括在我们即将创建的交易中,并且所有的挖矿节点都能够验证条件(1)。非常早期的比特币交易锁定脚本直接包含公钥(而不是其哈希)后跟OP_CHECKSIG,但这样做稍微复杂一点可以保护背后的确切公钥,直到所有者想要花费资金,只有那时他们才会透露公钥。(如果你想要了解更多,请查阅p2pk对p2pkh交易)。

PublicKey.from_point(public_key).encode(compressed=True, hash160=True).hex()

第三部分:创建我们的交易

好的,现在我们实际上要开始创建我们的交易了。假设我们想要将我们的资金发送到第二个钱包。也就是说,我们目前有一个包含0.001 BTC的钱包,并且我们想要将0.0005 BTC发送到我们的第二个钱包。为了实现这一点,我们的交易将恰好有一个输入(=水龙头交易的第二个输出),并且恰好有两个输出。一个输出将发送到我们的第二个地址,其余的我们将发送回自己的地址!

这里是一个关键点,需要理解。这有点奇怪。比特币交易的每一个输入/输出都必须完全花费。所以,如果我们拥有0.001 BTC并想将其一半发送到别处,我们实际上必须发送一半到那里,另一半回到我们自己的地址。

如果所有输出的总和小于所有输入的总和(所以我们没有发行货币),那么这笔交易将被认为是有效的。剩余的部分将是“找零”(费用),将由成功解决工作量证明并将其交易包含在新挖出的区块中的矿工声称。

让我们从交易输入数据结构开始:

@dataclassclass TxIn:prev_tx: bytes # prev transaction ID: hash256 of prev tx contentsprev_index: int # UTXO output index in the transactionscript_sig: Script = None # unlocking script, Script class coming a bit later belowsequence: int = 0xffffffff # originally intended for "high frequency trades", with locktimetx_in = TxIn(prev_tx = bytes.fromhex('46325085c89fb98a4b7ceee44eac9b955f09e1ddc86d8dad3dfdcba46b4d36b2'),prev_index = 1,script_sig = None, # this field will have the digital signature, to be inserted later)

首先,我们来理解一下交易输入数据结构。

prev_tx 和 prev_index 变量标识我们要花费的具体输出。注意,我们并没有指定我们要花费输出的多少部分。我们必须完整地花费输出(在比特币中通常称为“未花费交易输出”或UTXO)。一旦我们完全消耗了这个UTXO,我们就可以将其价值“分割”成我们喜欢的任何多个输出,并且可以选择将其中一些发送回我们的地址。无论如何,在这个例子中,我们标识了发送给我们比特币的交易,并说明我们打算花费的输出在该交易的第一个索引位置。第一个索引位置的输出发送到了另一个未知地址,这个地址由水龙头控制,我们无法花费,因为我们不控制它(我们没有私钥,也无法创建数字签名)。

script_sig 字段我们稍后回来重新访问。这是数字签名将去的地方,通过我们的私钥使用加密技术签署我们想要的交易,实际上是在说:“我批准这笔交易,作为拥有私钥的人,该私钥的公钥哈希为4b3518229b0d3554fe7cd3796ade632aff3069d8”。

sequence 字段在比特币的原始实现中存在,旨在提供一种“高频交易”功能,但今天它的用途非常有限,我们大部分时间可以忽略它。

计算费用。好的,上面的数据结构引用了我们交易的输入(这里是1个输入)。现在让我们为交易的两个输出创建数据结构。为了了解当前的交易费用“市场率”,有几种方法可供选择,比如查看一些最近区块中的交易,或者直接在一些网站上获取信息。一些最近的交易(包括上面提到的那笔)甚至在一个区块中以小于1 satoshi/byte(satoshi是比特币的1e-8)的价格打包。所以让我们尝试使用一个非常慷慨的费用,比如10 sat/B,或者总交易费用为0.0000001。在这种情况下,我们的输入是0.001 BTC = 100,000 sat,费用将是2,500 sat(因为我们的交易大约是250字节),我们将向目标钱包发送50,000 sat,剩余的(100,000 – 2,500 – 50,000 = 47,500)发送回我们的地址。

@dataclassclass TxOut:amount: int # in units of satoshi (1e-8 of a bitcoin)script_pubkey: Script = None # locking scripttx_out1 = TxOut(amount = 50000 # we will send this 50,000 sat to our target wallet)tx_out2 = TxOut(amount = 47500 # back to us)# the fee of 2500 does not need to be manually specified, the miner will claim it

填充锁定脚本。现在我们要为这两个输出填充script_pubkey“锁定脚本”。本质上,我们希望指定每个输出在未来的交易中被花费的条件。正如提到的,比特币有一个丰富的脚本语言,有近100个指令可以序列化为各种锁定/解锁脚本,但在这里我们将使用上面已经看到的非常标准和普遍的脚本,水龙头也使用它来支付我们。为了指示这两个输出的所有权,我们基本上想要指定能够花费输出的公钥哈希。但是,我们必须用“丰富的脚本语言”填充来装饰它。好的,让我们开始吧。

回想一下,当我们在比特币区块浏览器中查看水龙头交易时,锁定脚本具有这样的形式。输出所有者的公钥哈希被几个比特币脚本语言的操作码所夹:

“`

OP_DUP

OP_HASH160

公钥哈希

OP_EQUALVERIFY

OP_CHECKSIG

“`

这些操作码构成了一个简单的锁定脚本,它指示了交易输入的所有权。当我们接收比特币时,水龙头交易使用这个脚本来确保我们拥有与给定公钥关联的私钥。现在,我们将在我们的输出中使用相同的脚本来确保未来的交易只能由拥有相应私钥的人花费。

在这个脚本中:

– `OP_DUP` 复制了交易输入的第一个参数,通常是为了在脚本中使用。

– `OP_HASH160` 对公钥哈希进行哈希处理,得到一个160位的哈希值。

– `公钥哈希` 是水龙头交易中使用的同一个公钥哈希,它是输出所有者的公钥的哈希值。

– `OP_EQUALVERIFY` 检查哈希后的结果是否等于公钥哈希,如果等于,则执行下一操作码。

– `OP_CHECKSIG` 验证签名是否有效,这通常需要私钥来创建签名。

这个脚本确保了只有拥有正确私钥的人才能创建一个有效的签名,从而花费这个输出。当我们创建交易时,我们将使用我们的私钥来签署交易,从而生成一个数字签名,这个签名将满足锁定脚本的要求。这样,我们的输出就被安全地锁定,直到我们或者拥有私钥的人选择花费它们。

OP_DUPOP_HASH1604b3518229b0d3554fe7cd3796ade632aff3069d8OP_EQUALVERIFYOP_CHECKSIG

我们需要创建相同的结构并将其编码为字节,但是我们要用新所有者的哈希值替换公钥哈希。操作码(如OP_DUP等)都通过一个固定的schema编码为整数。以下是编码后的脚本:

“`

0x76a914

0x27f4312b

0x3a9830

0x483ada7b

0x2c9326a0

0x451e42

“`

这个序列 of hexadecimal values 代表了我们的锁定脚本,其中:

– `0x76a914` 代表了 `OP_DUP` 操作码的编码形式。

– `0x27f4312b` 是 `OP_HASH160` 操作码的编码形式。

– `0x3a9830` 是 `OP_EQUALVERIFY` 操作码的编码形式。

– `0x483ada7b` 是 `OP_CHECKSIG` 操作码的编码形式。

– `0x2c9326a0` 和 `0x451e42` 是 160位公钥哈希的前两部分,它们被 `OP_HASH160` 操作码处理。

请注意,这里的 `0x27f4312b` 是假设的公钥哈希值,实际上你应该使用你的钱包的公钥哈希值来替换它。同样,`0x483ada7b` 也应该替换为你的钱包地址的哈希值。这个脚本指示了只有拥有与这个公钥哈希相对应的私钥的人才能花费这个输出。

在实际创建交易时,你需要使用你的钱包软件或者比特币核心节点来生成这个脚本,并且确保使用正确的公钥哈希值。这个脚本将被包含在交易输出部分,确保了输出只能由指定的所有者花费。

def encode_int(i, nbytes, encoding='little'):""" encode integer i into nbytes bytes using a given byte ordering """return i.to_bytes(nbytes, encoding)def encode_varint(i):""" encode a (possibly but rarely large) integer into bytes with a super simple compression scheme """if i < 0xfd:return bytes([i])elif i < 0x10000:return b'\xfd' + encode_int(i, 2)elif i < 0x100000000:return b'\xfe' + encode_int(i, 4)elif i < 0x10000000000000000:return b'\xff' + encode_int(i, 8)else:raise ValueError("integer too large: %d" % (i, ))@dataclassclass Script:cmds: List[Union[int, bytes]]def encode(self):out = []for cmd in self.cmds:if isinstance(cmd, int):# an int is just an opcode, encode as a single byteout += [encode_int(cmd, 1)]elif isinstance(cmd, bytes):# bytes represent an element, encode its length and then contentlength = len(cmd)assert length < 75 # any longer than this requires a bit of tedious handling that we'll skip hereout += [encode_int(length, 1), cmd]ret = b''.join(out)return encode_varint(len(ret)) + ret# the first output will go to our 2nd walletout1_pkb_hash = PublicKey.from_point(public_key2).encode(compressed=True, hash160=True)out1_script = Script([118, 169, out1_pkb_hash, 136, 172]) # OP_DUP, OP_HASH160, , OP_EQUALVERIFY, OP_CHECKSIGprint(out1_script.encode().hex())# the second output will go back to usout2_pkb_hash = PublicKey.from_point(public_key).encode(compressed=True, hash160=True)out2_script = Script([118, 169, out2_pkb_hash, 136, 172])print(out2_script.encode().hex())

好的,现在我们通过指定填充有脚本操作码的公钥哈希来实际声明我们交易的两个输出的所有者。稍后当我们创建输入的解锁脚本时,我们将详细看到这些锁定脚本是如何工作的。现在,重要的是要理解,通过识别特定的公钥哈希,我们实际上是在声明每个输出UTXO的所有者。 with上述锁定的脚本,只有拥有原始公钥(及其关联的秘密键)的人才能花费这个UTXO。

这意味着,当我们创建交易时,我们将在输出部分指定这个锁定脚本,这样就确保了只有我们知道的私钥的所有者才能将来花费这些输出。这是通过比特币脚本语言实现的一种安全机制,它允许我们以一种透明和不可篡改的方式控制我们的资产。

在实际操作中,我们会使用我们的钱包软件或者比特币核心节点来生成这些锁定脚本,并且确保使用正确的公钥哈希值。这些脚本将被包含在交易输出部分,确保了输出只能由指定的所有者花费。这样的设计保证了比特币网络中的交易的安全性和不可篡改性。

tx_out1.script_pubkey = out1_scripttx_out2.script_pubkey = out2_script

数字签名。现在我们来到了 transaction input tx_in 的 script_sig 部分,这是我们之前跳过的部分。特别是,我们将创建一个数字签名,它有效地表示:“我,与引用交易的输出的锁定脚本上指定的公钥哈希相关联的私钥的所有者,批准将这个 UTXO 作为此交易的输入花费。”不幸的是,比特币在这里又变得相当复杂,因为你实际上只能签署交易的某些部分,而且可以从多个方组装出多个签名,并以各种方式组合它们。正如我们之前所做的那样,我们只覆盖了(到目前为止)最常见的情况,即签署整个交易,并构造一个解锁脚本, specifically to only satisfy the locking script of the exact form above (OP_DUP, OP_HASH160, , OP_EQUALVERIFY, OP_CHECKSIG).

首先,我们需要创建一个纯字节“消息”,我们将对其进行数字签名。在这个案例中,消息是整个交易的编码。所以这有点尴尬——整个交易还不能被编码成字节,因为我们还没有完成它!它仍然缺少我们的签名,我们正在尝试构建它。

相反,当我们序列化我们想要签名的交易输入时,规则是将 script_sig 的编码(我们没有,因为我们正在尝试生产它…)替换为指向的交易输出的 script_pubkey。所有其他交易输入的 script_sig 也用空脚本替换,因为那些输入可以属于许多其他所有者,他们可以独立地贡献他们自己的签名。我现在不确定这是否说得通。所以让我们看看代码。

我们需要最终的数据结构,实际的Transaction,这样我们才能将其序列化为字节消息。它主要是一个薄的容器,用于存储输入列表和输出列表。然后我们实现新Tx类的序列化,以及TxIn和TxOut类的序列化,这样我们就可以将整个交易序列化为字节。

在比特币中,数字签名是通过签署交易的特定部分来创建的,这个部分被称为“解锁脚本”(unlocking script),它与“锁定脚本”(locking script)相对应。锁定脚本定义了哪些条件必须满足才能花费输出,而解锁脚本则是用来满足这些条件的实际签名。在实践中,解锁脚本通常包含一个公钥和一个数字签名,该签名证明了签名者是该公钥的所有者,并且他们批准了交易。

为了创建数字签名,我们需要执行以下步骤:

1. 准备交易的所有输入和输出。

2. 选择要签署的交易输入。

3. 创建一个消息,通常是交易的编码版本,但不包括我们想要签名的输入的 script_sig 部分。

4. 使用私钥对消息进行签名。

5. 将签名添加到交易的相应输入中,作为解锁脚本的一部分。

在比特币的交易中,签名是由交易的发送者创建的,它允许接收者验证交易的真实性和发送者的身份。这个过程是比特币安全性的关键组成部分,确保了交易的可追溯性和不可篡改性。

@dataclassclass Tx:version: inttx_ins: List[TxIn]tx_outs: List[TxOut]locktime: int = 0def encode(self, sig_index=-1) -> bytes:"""Encode this transaction as bytes.If sig_index is given then return the modified transactionencoding of this tx with respect to the single input index.This result then constitutes the "message" that gets signedby the aspiring transactor of this input."""out = []# encode metadataout += [encode_int(self.version, 4)]# encode inputsout += [encode_varint(len(self.tx_ins))]if sig_index == -1:# we are just serializing a fully formed transactionout += [tx_in.encode() for tx_in in self.tx_ins]else:# used when crafting digital signature for a specific input indexout += [tx_in.encode(script_override=(sig_index == i))for i, tx_in in enumerate(self.tx_ins)]# encode outputsout += [encode_varint(len(self.tx_outs))]out += [tx_out.encode() for tx_out in self.tx_outs]# encode... other metadataout += [encode_int(self.locktime, 4)]out += [encode_int(1, 4) if sig_index != -1 else b''] # 1 = SIGHASH_ALLreturn 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... sighout += [encode_int(self.prev_index, 4)]if script_override is None:# None = just use the actual scriptout += [self.script_sig.encode()]elif script_override is True:# True = override the script with the script_pubkey of the associated inputout += [self.prev_tx_script_pubkey.encode()]elif script_override is False:# False = override with an empty scriptout += [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: ints: intdef sign(secret_key: int, message: bytes) -> Signature:# the order of the elliptic curve used in bitcoinn = bitcoin_gen.n# double hash the message and convert to integerz = int.from_bytes(sha256(sha256(message)), 'big')# generate a new secret/public key pair at randomsk = random.randrange(1, n)P = sk * bitcoin_gen.G# calculate the signaturer = P.xs = inv(sk, n) * (z + secret_key * r) % nif s > n / 2:s = n - ssig = 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 miningpassrandom.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 zerosnb = (b'\x00' if nb[0] >= 0x80 else b'') + nb # preprend 0x00 if first byte >= 0x80return nbrb = 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 <= secret_key3 < bitcoin_gen.n # check it's validpublic_key3 = secret_key3 * Gaddress3 = PublicKey.from_point(public_key3).address(net='test', compressed=True)print("Our third Bitcoin identity:")print("1. secret key: ", secret_key3)print("2. public key: ", (public_key3.x, public_key3.y))print("3. Bitcoin address: ", address3)

让我们来创建交易。目前我们第一个钱包 mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ 有 47,500 sat,第二个钱包 mrFF91kpuRbivucowsY512fDnYt6BWrvx9 有 50,000 sat。我们将用这两个作为输入创建一个交易,并且将唯一的输出到第三个钱包 mgh4VjZx5MpkHRis9mDsF2ZcKLdXoP3oQ4。像之前一样,我们将支付 2500 sat 作为手续费,所以我们发送给自己 50,000 + 47,500 – 2500 = 95,000 sat。

# ----------------------------# first input of the transactiontx_in1 = TxIn(prev_tx = bytes.fromhex('245e2d1f87415836cbb7b0bc84e40f4ca1d2a812be0eda381f02fb2224b4ad69'),prev_index = 0,script_sig = None, # digital signature to be inserted later)# reconstruct the script_pubkey locking this UTXO (note: it's the first output index in the # referenced transaction, but the owner is the second identity/wallet!)# recall this information is "swapped in" when we digitally sign the spend of this UTXO a bit laterpkb_hash = PublicKey.from_point(public_key2).encode(compressed=True, hash160=True)tx_in1.prev_tx_script_pubkey = Script([118, 169, pkb_hash, 136, 172]) # OP_DUP, OP_HASH160, , OP_EQUALVERIFY, OP_CHECKSIG# ----------------------------# second input of the transactiontx_in2 = TxIn(prev_tx = bytes.fromhex('245e2d1f87415836cbb7b0bc84e40f4ca1d2a812be0eda381f02fb2224b4ad69'),prev_index = 1,script_sig = None, # digital signature to be inserted later)pkb_hash = PublicKey.from_point(public_key).encode(compressed=True, hash160=True)tx_in2.prev_tx_script_pubkey = Script([118, 169, pkb_hash, 136, 172]) # OP_DUP, OP_HASH160, , OP_EQUALVERIFY, OP_CHECKSIG# ----------------------------# define the (single) outputtx_out = TxOut(amount = 95000,script_pubkey = None, # locking script, inserted separately right below)# declare the owner as identity 3 above, by inserting the public key hash into the Script "padding"out_pkb_hash = PublicKey.from_point(public_key3).encode(compressed=True, hash160=True)out_script = Script([118, 169, out_pkb_hash, 136, 172]) # OP_DUP, OP_HASH160, , OP_EQUALVERIFY, OP_CHECKSIGtx_out.script_pubkey = out_script# ----------------------------# create the aspiring transaction objecttx = Tx(version = 1,tx_ins = [tx_in1, tx_in2], # 2 inputs this time!tx_outs = [tx_out], # ...and a single output)# ----------------------------# digitally sign the spend of the first input of this transaction# note that index 0 of the input transaction is our second identity! so it must sign heremessage1 = tx.encode(sig_index = 0)random.seed(int.from_bytes(sha256(message1), 'big'))sig1 = sign(secret_key2, message1) # identity 2 signssig_bytes_and_type1 = sig1.encode() + b'\x01' # DER signature + SIGHASH_ALLpubkey_bytes = PublicKey.from_point(public_key2).encode(compressed=True, hash160=False)script_sig1 = Script([sig_bytes_and_type1, pubkey_bytes])tx_in1.script_sig = script_sig1# ----------------------------# digitally sign the spend of the second input of this transaction# note that index 1 of the input transaction is our first identity, so it signs heremessage2 = tx.encode(sig_index = 1)random.seed(int.from_bytes(sha256(message2), 'big'))sig2 = sign(secret_key, message2) # identity 1 signssig_bytes_and_type2 = sig2.encode() + b'\x01' # DER signature + SIGHASH_ALLpubkey_bytes = PublicKey.from_point(public_key).encode(compressed=True, hash160=False)script_sig2 = Script([sig_bytes_and_type2, pubkey_bytes])tx_in2.script_sig = script_sig2# and that should be it!print(tx.id())print(tx)print(tx.encode().hex())

让我们来创建交易。目前我们第一个钱包 mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ 有 47,500 sat,第二个钱包 mrFF91kpuRbivucowsY512fDnYt6BWrvx9 有 50,000 sat。我们将用这两个作为输入创建一个交易,并且将唯一的输出到第三个钱包 mgh4VjZx5MpkHRis9mDsF2ZcKLdXoP3oQ4。像之前一样,我们将支付 2500 sat 作为手续费,所以我们发送给自己 50,000 + 47,500 – 2500 = 95,000 sat。

再次,我们前往Blockstream的tx/push端点,复制粘贴上面提供的交易十六进制,然后等待:)

import time; time.sleep(1.0)# in Bitcoin main net a block will take about 10 minutes to mine# (Proof of Work difficulty is dynamically adjusted to make it so)

交易在这里,它最终出现了,作为区块 2005671 的一部分,还有其他 25 个交易。

以上就是“区块链课程:python实现交易”的全部内容,希望对你有所帮助。

关于Python技术储备

学好 Python 不论是就业还是做副业赚钱都不错,但要学会 Python 还是要有一个学习规划。最后大家分享一份全套的 Python 学习资料,给那些想学习 Python 的小伙伴们一点帮助!

一、Python所有方向的学习路线

Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。

二、Python必备开发工具

三、Python视频合集

观看零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。

四、实战案例

光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。

五、Python练习题

检查学习结果。

六、面试资料

我们学习Python必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。

最后祝大家天天进步!!

上面这份完整版的Python全套学习资料已经上传至CSDN官方,朋友如果需要可以直接微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】。