newsmode
search
Меню
arrow_back Назад

A from-scratch tour of Bitcoin in Python

auto_awesomeКраткое саммари

Андрей Карпаты реализует Bitcoin с нуля на чистом Python без единой зависимости, чтобы интуитивно понять, как работает блокчейн. Он последовательно создаёт криптографическую личность на основе эллиптической кривой secp256k1: генерирует секретный ключ (случайное целое число), выводит из него публичный ключ через скалярное умножение точки-генератора, а затем — биткойн-адрес, реализуя по пути хеш-функции SHA-256 и RIPEMD-160 и кодирование Base58Check. Далее в тестовой сети (testnet) он получает монеты из крана и вручную конструирует транзакцию: входы и выходы (UTXO), блокирующие и разблокирующие скрипты (OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG) и цифровую подпись по алгоритму ECDSA в DER-кодировании. Транзакция сериализуется в байты, рассылается через сервис Blockstream и попадает в блок (например, Block 2005515), а в конце две оставшиеся суммы (47 500 и 50 000 сатоши) консолидируются в третий кошелёк. По сути Bitcoin — это DAG из UTXO, у каждого из которых есть сумма и блокирующий Script, а децентрализация достигается через proof of work на базе SHA-256.

Блокчейн кажется мне захватывающим, потому что он расширяет разработку программного обеспечения с открытым исходным кодом до открытого исходного кода + состояния. Это, похоже, действительно интересная инновация в вычислительных парадигмах; мы получаем возможность делиться не просто кодом — мы делимся работающим компьютером, и кто угодно и где угодно может пользоваться им открыто и без необходимости разрешений. Семена этой революции, можно сказать, были посеяны вместе с Bitcoin, поэтому мне стало любопытно копнуть поглубже и интуитивно разобраться, как он устроен. И в духе принципа «чего я не могу создать, того я не понимаю» — что может быть лучше, чем реализовать всё с нуля?

Мы собираемся создать, поставить цифровую подпись и разослать в сеть транзакцию Bitcoin на чистом Python, с нуля и без единой зависимости. По ходу дела мы немало узнаем о том, как Bitcoin представляет ценность. Поехали.

(кстати, если визуальный формат этого поста вас раздражает, посмотрите версию в виде jupyter notebook — содержание там идентичное).

Шаг 1: генерируем криптографическую личность

Сначала мы хотим сгенерировать совершенно новую криптографическую личность, которая представляет собой просто пару из приватного и публичного ключа. Bitcoin для защиты транзакций использует криптографию на эллиптических кривых, а не что-то более распространённое вроде RSA. Я не буду делать здесь полноценное введение в ECC, потому что другие справились с этим значительно лучше, например, я нашёл серию постов в блоге Andrea Corbellini исключительным ресурсом. Здесь мы просто напишем код, но чтобы понять, почему он работает математически, вам нужно будет проштудировать эту серию.

Итак, Bitcoin использует кривую secp256k1. Будучи новичком в этой области, я нашёл эту часть захватывающей — существуют целые библиотеки разных кривых, из которых можно выбирать и которые предлагают разные плюсы/минусы и свойства. NIST публикует рекомендации, какие из них использовать, но люди предпочитают другие кривые (вроде secp256k1), в которые менее вероятно встроены бэкдоры. В любом случае, эллиптическая кривая — это довольно низкоразмерный математический объект, и для её задания достаточно всего трёх целых чисел:

from __future__ import annotations # PEP 563: Postponed Evaluation of Annotations from dataclasses import dataclass # https://docs.python.org/3/library/dataclasses.html I like these a lot @dataclass class 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 )

Помимо самой кривой мы задаём точку-генератор (Generator point) — некую фиксированную «начальную точку» на цикле кривой, которая используется, чтобы запустить «случайное блуждание» по кривой. Генератор — это публично известная и согласованная константа:

@dataclass class Point: """ An integer point (x,y) on a Curve """ curve: Curve x: int y: int G = 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_ likely import random random.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)

Generator IS on the curve: True Totally random point is not: False

Наконец, известен порядок (order) порождающей точки G — это, по сути, «размер множества», с которым мы работаем, в терминах целочисленных кортежей (x,y) на цикле вокруг кривой. Мне нравится свести эту информацию ещё в одну структуру данных, которую я назову Generator:

@dataclass class 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 = INF bitcoin_gen = Generator( G = G, # the order of G is known and can be mathematically derived n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141, )

Заметьте, что пока мы по сути ничего не сделали — всё это лишь определение нескольких структур данных и заполнение их публично известными константами, связанными с эллиптическими кривыми, которые используются в Bitcoin. Это вот-вот изменится, ведь мы готовы сгенерировать наш приватный ключ. Приватный ключ (или «секретный ключ», как я буду называть его дальше) — это просто случайное целое число, удовлетворяющее условию 1 <= key < n (напомню, n — порядок G):

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

22265090479312778178772228083027296664144

Это наш секретный ключ — довольно невзрачное целое число, но любой, кто его знает, может распоряжаться всеми средствами, которыми вы владеете в блокчейне Bitcoin и которые с ним связаны. В простейшем, самом распространённом базовом сценарии использования Bitcoin это единственный «пароль», управляющий вашим счётом. Разумеется, в крайне маловероятном случае, если какой-то другой Андрей вручную сгенерировал свой секретный ключ так же, как я выше, кошелёк, связанный с этим секретным ключом, скорее всего имеет нулевой баланс :). Если бы это было не так, нам бы и впрямь очень повезло.

Теперь мы сгенерируем публичный ключ — вот тут и начинается самое интересное. Публичный ключ — это точка на кривой, которая получается, если прибавить точку-генератор к самой себе secret_key раз. То есть у нас: public_key = G + G + G + (secret key раз) + G = secret_key * G. Заметьте, что и символ ‘+’ (сложение), и символ ‘*’ (умножение) здесь весьма особенные и немного сбивают с толку. Секретный ключ — это целое число, а точка-генератор G — это кортеж (x,y), являющийся точкой (Point) на кривой (Curve), и в результате получается кортеж (x,y) публичного ключа, снова точка на кривой. Именно здесь нам приходится по-настоящему определить операцию сложения на эллиптической кривой. У неё есть очень специфическое определение и геометрическая интерпретация (см. пост Andrea выше), но сама реализация относительно проста:

INF = Point(None, None, None) # special point at "infinity", kind of like a zero def extended_euclidean_algorithm(a, b): """ Returns (gcd, x, y) s.t. a * x + b * y == gcd This function implements the extended Euclidean algorithm and runs in O(log b) in the worst case, taken from Wikipedia. """ old_r, r = a, b old_s, s = 1, 0 old_t, t = 0, 1 while r != 0: quotient = old_r // r old_r, r = r, old_r - quotient * r old_s, s = s, old_s - quotient * s old_t, t = t, old_t - quotient * t return old_r, old_s, old_t def 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-variable return x % p def elliptic_curve_addition(self, other: Point) -> Point: # handle special case of P + 0 = 0 + P = 0 if self == INF: return other if other == INF: return self # handle special case of P + (-P) = 0 if 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 point rx = (m**2 - self.x - other.x) % self.curve.p ry = (-(m*(rx - self.x) + self.y)) % self.curve.p return 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 integer 1, then our public key would just be G: sk = 1 pk = G print(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 = 2 pk = G + G print(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 = 3 pk = G + G + G print(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)

secret key: 1 public key: (55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424) Verify the public key is on the curve: True secret key: 2 public key: (89565891926547004231252920425935692360644145829622209833684329913297188986597, 12158399299693830322967808612713398636155367887041628176798871954788371653930) Verify the public key is on the curve: True secret key: 3 public key: (112711660439710606056748659173929673102114977341539408544630613555209775888121, 25583027980570883691656905877401976406448868254816295069919888960541586679410) Verify the public key is on the curve: True

Хорошо, итак, у нас есть несколько пар ключей выше, но нам нужен публичный ключ, соответствующий нашему случайно сгенерированному секретному ключу. Используя только приведённый выше код, нам пришлось бы прибавлять G к самому себе очень большое число раз, потому что секретный ключ — большое целое число. Результат был бы верным, но работал бы очень медленно. Вместо этого давайте реализуем алгоритм «удвоить и прибавить» (double and add), чтобы кардинально ускорить повторное сложение. Опять же, почему он работает — см. пост выше, а вот и он:

def double_and_add(self, k: int) -> 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 convenience Point.__rmul__ = double_and_add # "verify" correctness print(G == 1*G) print(G + G == 2*G) print(G + G + G == 3*G)

True True True

# efficiently calculate our actual public key! public_key = secret_key * G print(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)

x: 83998262154709529558614902604110599582969848537757180553516367057821848015989 y: 37676469766173670826348691885774454391218658108212372128812329274086400588247 Verify the public key is on the curve: True

Имея пару приватный/публичный ключ, мы сгенерировали нашу криптографическую личность. Теперь пора вывести связанный с ней биткойн-адрес кошелька. Адрес кошелька — это не сам публичный ключ, но его можно детерминированно из него вывести, и он содержит несколько дополнительных полезностей (например, встроенную контрольную сумму). Однако прежде чем сгенерировать адрес, нам нужно определить пару хеш-функций. Bitcoin использует повсеместно распространённую SHA-256, а также RIPEMD-160. Мы могли бы просто взять и использовать реализации из питоновского hashlib, но это должна быть реализация без зависимостей, так что import hashlib — это жульничество. Поэтому сначала вот реализация SHA256, которую я написал на чистом Python, следуя (относительно читаемому) документу NIST FIPS PUB 180-4:

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 << size - n) & (2**size - 1) def shr(x, n): return 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 <= 15: # the first 16 words are just a copy of the block W.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**32 W.append(i2b(total)) # 2. Initialize the 8 working variables a,b,c,d,e,f,g,h with prev hash value a, 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**32 T2 = (capsig0(a) + maj(a, b, c)) % 2**32 h = g g = f f = e e = (d + T1) % 2**32 d = c c = b b = a a = (T1 + T2) % 2**32 # 4. Compute the i-th intermediate hash value H^i delta = [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 sha256 sha256 = gen_sha256_with_variable_scope_protector_to_not_pollute_global_namespace() print("verify empty hash:", sha256(b'').hex()) # should be e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 print(sha256(b'here is a random bytes message, cool right?').hex()) print("number of bytes in a sha256 digest: ", len(sha256(b'')))

verify empty hash: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 69b9779edaa573a509999cbae415d3408c30544bad09727a1d64eff353c95b89 number of bytes in a sha256 digest: 32

Хорошо, причина, по которой я хотел реализовать это с нуля и вставить сюда, в том, что я хочу, чтобы вы снова отметили: внутри не происходит ничего слишком страшного. SHA256 берёт некоторое сообщение в байтах, которое нужно захешировать, сначала дополняет (pad) его, затем разбивает на блоки и пропускает эти блоки через то, что лучше всего описать как навороченный «битовый смеситель», описанный в разделе 3, который содержит ряд битовых сдвигов и двоичных операций, оркестрованных так, что это, честно говоря, выше моего понимания, но что приводит к тем прекрасным свойствам, которые предлагает SHA256. В частности, он создаёт короткий дайджест фиксированного размера, выглядящий случайным, из любого исходного сообщения переменного размера так, что перемешивание необратимо, а ещё практически вычислительно невозможно построить другое сообщение, которое хешировалось бы в заданный дайджест.

Bitcoin использует SHA256 повсюду для создания хешей, и, конечно же, это ключевой элемент в Proof of Work Bitcoin, где цель — изменять блок транзакций до тех пор, пока всё это не захешируется в достаточно малое число (когда байты дайджеста интерпретируются как число). Что, благодаря приятным свойствам SHA256, можно сделать только перебором. Так что все ASIC, спроектированные для эффективного майнинга, — это просто невероятно оптимизированные, близкие к «железу» реализации в точности приведённого выше кода.

В любом случае, прежде чем мы сможем сгенерировать наш адрес, нам также нужна хеш-функция RIPEMD160, которую я нашёл в интернете, сократил и почистил:

def gen_ripemd160_with_variable_scope_protector_to_not_pollute_global_namespace(): import sys import struct # ----------------------------------------------------------------------------- # public interface def 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] # 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 <= inplen: RMD160Transform(ctx.state, inp[off:]) off += 64 if off < inplen: for i in range(inplen - off): ctx.buffer[have+i] = inp[off+i] def RMD160Final(ctx): size = struct.pack("> (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('<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 = a bb = b cc = c dd = d ee = e a = 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) d, a = R(d, e, a, b, c, F4, KK0, 5, 4, x) c, e = R(c, d, e, a, b, F4, KK0, 7, 13, x) b, d = R(b, c, d, e, a, F4, KK0, 7, 6, x) a, c = R(a, b, c, d, e, F4, KK0, 8, 15, x) e, b = R(e, a, b, c, d, F4, KK0, 11, 8, x) d, a = R(d, e, a, b, c, F4, KK0, 14, 1, x) c, e = R(c, d, e, a, b, F4, KK0, 14, 10, x) b, d = R(b, c, d, e, a, F4, KK0, 12, 3, x) a, c = R(a, b, c, d, e, F4, KK0, 6, 12, x) #/* #15 */ #/* Parallel round 2 */ e, b = R(e, a, b, c, d, F3, KK1, 9, 6, x) d, a = R(d, e, a, b, c, F3, KK1, 13, 11, x) c, e = R(c, d, e, a, b, F3, KK1, 15, 3, x) b, d = R(b, c, d, e, a, F3, KK1, 7, 7, x) a, c = R(a, b, c, d, e, F3, KK1, 12, 0, x) e, b = R(e, a, b, c, d, F3, KK1, 8, 13, x) d, a = R(d, e, a, b, c, F3, KK1, 9, 5, x) c, e = R(c, d, e, a, b, F3, KK1, 11, 10, x) b, d = R(b, c, d, e, a, F3, KK1, 7, 14, x) a, c = R(a, b, c, d, e, F3, KK1, 7, 15, x) e, b = R(e, a, b, c, d, F3, KK1, 12, 8, x) d, a = R(d, e, a, b, c, F3, KK1, 7, 12, x) c, e = R(c, d, e, a, b, F3, KK1, 6, 4, x) b, d = R(b, c, d, e, a, F3, KK1, 15, 9, x) a, c = R(a, b, c, d, e, F3, KK1, 13, 1, x) e, b = R(e, a, b, c, d, F3, KK1, 11, 2, x) #/* #31 */ #/* Parallel round 3 */ d, a = R(d, e, a, b, c, F2, KK2, 9, 15, x) c, e = R(c, d, e, a, b, F2, KK2, 7, 5, x) b, d = R(b, c, d, e, a, F2, KK2, 15, 1, x) a, c = R(a, b, c, d, e, F2, KK2, 11, 3, x) e, b = R(e, a, b, c, d, F2, KK2, 8, 7, x) d, a = R(d, e, a, b, c, F2, KK2, 6, 14, x) c, e = R(c, d, e, a, b, F2, KK2, 6, 6, x) b, d = R(b, c, d, e, a, F2, KK2, 14, 9, x) a, c = R(a, b, c, d, e, F2, KK2, 12, 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) % 0x100000000 state[1] = (state[2] + dd + e) % 0x100000000 state[2] = (state[3] + ee + a) % 0x100000000 state[3] = (state[4] + aa + b) % 0x100000000 state[4] = (state[0] + bb + c) % 0x100000000 state[0] = t % 0x100000000 return ripemd160 ripemd160 = 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'')))

f51960af7dd4813a587ab26388ddab3b28d1f7b4 number of bytes in a RIPEMD-160 digest: 20

Как и в случае с SHA256 выше, мы снова видим «битовый смеситель» из множества двоичных операций. Довольно круто.

Хорошо, мы наконец готовы получить наш биткойн-адрес. Мы сделаем это красиво, создав подкласс Point под названием PublicKey, который, опять же, есть просто точка (Point) на кривой (Curve), но теперь обладает дополнительной семантикой и интерпретацией биткойн-публичного ключа, вместе с некоторыми методами кодирования/декодирования ключа в байты для обмена в протоколе Bitcoin.

class PublicKey(Point): """ The public key is just a Point on a Curve, but has some additional specific encoding / decoding functionality that this class implements. """ @classmethod def 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 bytes if 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 desired return ripemd160(sha256(pkb)) if hash160 else pkb def 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 payload pkb_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 checksum checksum = sha256(sha256(ver_pkb_hash))[:4] # append to form the full 25-byte binary Bitcoin Address byte_address = ver_pkb_hash + checksum # finally b58 encode the result b58check_address = b58encode(byte_address) return b58check_address

Мы пока не готовы опробовать этот класс, потому что, как вы заметите, тут есть ещё одна необходимая зависимость — функция b58-кодирования b58encode. Это просто специфичное для Bitcoin кодирование байтов, использующее основание 58 из символов алфавита, которые очень однозначны. Например, оно не использует ‘O’ и ‘0’, потому что их очень легко перепутать на бумаге. Так что нам нужно взять наш биткойн-адрес (который в сыром виде занимает 25 байт), преобразовать его в основание 58 и распечатать символы. При этом сырые 25 байт нашего адреса содержат 1 байт для версии (Bitcoin «main net» — это b'\x00', а Bitcoin «test net» использует b'\x6f'), затем 20 байт из хеш-дайджеста и, наконец, 4 байта контрольной суммы, чтобы мы могли с вероятностью 1 - 1/2**32 = 99.99999998% выдать ошибку на случай, если пользователь ошибётся, вводя свой биткойн-адрес в какое-нибудь поле. Итак, вот b58-кодирование:

# base58 encoding / decoding utilities # reference: https://en.bitcoin.it/wiki/Base58Check_encoding alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' def b58encode(b: bytes) -> str: assert len(b) == 25 # version is 1 byte, pkb_hash 20 bytes, checksum 4 bytes n = 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)

mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ

Круто, теперь мы можем проверить на каком-нибудь сайте-обозревателе блоков (block explorer), что с этого адреса ещё ни разу не было транзакций: https://www.blockchain.com/btc-testnet/address/mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ. К концу этого руководства это будет уже не так, но на момент написания я действительно видел, что этот адрес «чистый», то есть пока никто не сгенерировал и не использовал этот секретный ключ в testnet так, как сделали мы выше. Что логично, ведь для этого должен был бы найтись какой-то другой «Андрей» с дурным чувством юмора, тоже ковыряющийся в Bitcoin. Но мы можем также проверить какие-нибудь совсем не-секретные секретные ключи, которые, как мы ожидаем, использовались людьми в прошлом. Например, мы можем проверить адрес, принадлежащий наименьшему допустимому секретному ключу — 1, где публичный ключ в точности равен точке-генератору :). Вот как мы его получаем:

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

'mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r'

Действительно, как мы видим в обозревателе блокчейна, этот адрес участвовал в транзакциях 1 812 раз на момент написания и имеет баланс $0.00 BTC. Это логично, потому что если бы на нём был какой-то баланс (в наивном случае, с точностью до некоторых тонкостей со скриптовым языком, в которые мы углубимся), то любой смог бы просто потратить его, ведь все знают секретный ключ (1) и могут использовать его, чтобы цифровой подписью подписывать транзакции, которые его тратят. Скоро мы увидим, как это работает.

Часть 1: краткое резюме на текущий момент

Мы можем сгенерировать криптографическую личность, которая состоит из секретного ключа (случайного целого числа), известного только нам, и выведенного из него публичного ключа — путём «прыжков» по эллиптической кривой с помощью скалярного умножения порождающей точки на биткойн-эллиптической кривой. Затем мы также вывели связанный биткойн-адрес, которым можем поделиться с другими, чтобы попросить денежек, и для этого нам потребовалось ввести две хеш-функции (SHA256 и RIPEMD160). Вот три важные величины, снова сведённые вместе и распечатанные:

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)

Our first Bitcoin identity: 1. secret key: 22265090479312778178772228083027296664144 2. public key: (83998262154709529558614902604110599582969848537757180553516367057821848015989, 37676469766173670826348691885774454391218658108212372128812329274086400588247) 3. Bitcoin address: mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ

Часть 2: получаем стартовые средства + знакомство с Bitcoin «под капотом»

Теперь пора создать транзакцию. Мы собираемся отправить немного 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 valid public_key2 = secret_key2 * G address2 = 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)

Our second Bitcoin identity: 1. secret key: 29595381593786747354608258168471648998894101022644411052850960746671046944116 2. public key: (70010837237584666034852528437623689803658776589997047576978119215393051139210, 35910266550486169026860404782843121421687961955681935571785539885177648410329) 3. Bitcoin address: mrFF91kpuRbivucowsY512fDnYt6BWrvx9

Отлично, итак, наша цель — отправить немного BTC с mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ на mrFF91kpuRbivucowsY512fDnYt6BWrvx9. Во-первых, поскольку мы только что сгенерировали эти личности с нуля, на первом адресе нет биткойнов. Поскольку мы используем «параллельную вселенную» — задуманную для разработчиков тестовую сеть Bitcoin (test network), — мы можем воспользоваться одним из нескольких доступных кранов (faucets), чтобы вежливо попросить немного BTC. Я сделал это, погуглив «bitcoin testnet faucet», перейдя по первой ссылке и попросив кран отправить немного биткойнов на наш исходный адрес mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ. Спустя несколько минут мы можем вернуться в обозреватель блокчейна и увидеть, что мы получили монеты, в данном случае 0.001 BTC. Краны доступны для тестовой сети, но в основной сети (main net) вы их, конечно, не найдёте :) Там вам пришлось бы, например, открыть счёт на Coinbase (что создаёт кошелёк) и купить немного BTC за USD. В этом руководстве мы будем работать в тестовой сети, но всё, что мы делаем, прекрасно сработало бы и в основной сети.

Теперь, если мы кликнем по конкретному идентификатору транзакции, мы увидим кучу дополнительной информации, которая добирается до самой сути Bitcoin и того, как в нём представлены деньги.

Идентификатор транзакции. Прежде всего отметим, что у каждой транзакции есть отдельный id / хеш. В данном случае у транзакции крана id 46325085c89fb98a4b7ceee44eac9b955f09e1ddc86d8dad3dfdcba46b4d36b2. Как мы увидим, это просто двойной SHA256-хеш (хеш от хеша) структуры данных транзакции, которую мы вскоре увидим сериализованной в байты. Двойные SHA256-хеши в Bitcoin часто используются вместо одного хеша для дополнительной безопасности — чтобы смягчить несколько недостатков одного раунда 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. Эта «сдача» называется комиссией (fee), и эту комиссию имеет право забрать тот биткойн-майнер, который включил эту транзакцию в свой блок, — в данном случае это Block 2005500. Видно, что в этом блоке было 48 транзакций, и транзакция крана — одна из них! Так вот, комиссия выступает финансовым стимулом для майнеров включать транзакцию в свой блок, потому что они оставляют сдачу себе. Чем выше комиссия майнеру, тем вероятнее и быстрее транзакция попадёт в блокчейн. С высокой комиссией мы бы ожидали, что её охотно подхватят майнеры и включат в ближайший же блок. С низкой комиссией транзакция может вообще никогда не попасть в блок, потому что в сети рассылается множество других транзакций, готовых заплатить более высокую комиссию. Так что если вы майнер и у вас ограниченное место в блоке — зачем напрягаться?

Когда мы будем делать собственную транзакцию, нам придётся не забыть включить эти чаевые майнеру и заплатить «рыночную ставку», которую мы посмотрим. В случае этого блока видно, что общая сумма BTC, заработанная майнером этого блока, составила 0.09765625 BTC от специальной транзакции «Coinbase», которую каждому майнеру разрешено отправить с нулевого (null) входа самому себе, плюс в сумме 0.00316119 BTC составила суммарная награда из комиссий, сложенная по всем 47 не-Coinbase транзакциям в этом блоке.

Размер. Также отметим, что эта транзакция (в сериализованном виде) занимала 249 байт. Это вполне средний размер для простой транзакции вроде этой.

Pkscript. Наконец, отметим, что у второго выхода (наши 0.001 BTC), если прокрутить вниз к его деталям, есть поле «Pkscript», которое показывает:

OP_DUP OP_HASH160 4b3518229b0d3554fe7cd3796ade632aff3069d8 OP_EQUALVERIFY OP_CHECKSIG

Вот здесь с Bitcoin начинается небольшое безумие. У него есть целый стековый скриптовый язык, но если только вы не занимаетесь сумасшедшими multisig-смарт-контрактами с тройным эскроу и сальто (?), подавляющее большинство транзакций используют один из очень немногих простых скриптов «частного случая», ровно как этот. К этому моменту мой взгляд просто скользит по нему как по стандартной простой штуке. Этот «Pkscript» — «блокирующий скрипт» (locking script) для этого конкретного выхода, в котором лежит 0.001 BTC. Мы захотим потратить этот выход и превратить его во вход в нашей предстоящей транзакции. Чтобы разблокировать этот выход, нам придётся удовлетворить условия этого блокирующего скрипта. По-русски этот скрипт говорит, что любая транзакция, желающая потратить этот выход, должна удовлетворить два условия. 1) их публичный ключ должен хешироваться в 4b3518229b0d3554fe7cd3796ade632aff3069d8. И 2) цифровая подпись для желающей потратить транзакции должна пройти проверку как сгенерированная приватным ключом, связанным с этим публичным ключом. Только владелец секретного ключа сможет одновременно 1) предоставить полный публичный ключ, который проверят на корректность хеша, и 2) создать цифровую подпись, как мы вскоре увидим.

Кстати, мы можем убедиться, что наш публичный ключ, конечно же, хешируется правильно, так что мы сможем включить его в нашу предстоящую транзакцию, и все майнинговые узлы смогут проверить условие (1). Очень ранние транзакции Bitcoin имели блокирующие скрипты, которые напрямую содержали публичный ключ (а не его хеш), за которым следовал OP_CHECKSIG, но если делать это чуть более сложным способом, точный публичный ключ защищён за хешем до тех пор, пока владелец не захочет потратить средства, и только тогда он раскрывает публичный ключ. (Если хотите узнать больше, поищите транзакции p2pk против p2pkh.)

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

'4b3518229b0d3554fe7cd3796ade632aff3069d8'

Часть 3: конструируем нашу транзакцию

Хорошо, теперь мы на самом деле сконструируем нашу транзакцию. Допустим, мы хотим отправить половину наших средств на наш второй кошелёк. То есть сейчас у нас кошелёк с 0.001 BTC, и мы хотели бы отправить 0.0005 BTC на наш второй кошелёк. Чтобы этого добиться, у нашей транзакции будет ровно один вход (= 2-й выход транзакции крана) и ровно 2 выхода. Один выход пойдёт на наш 2-й адрес, а остальное мы отправим обратно на наш собственный адрес!

Вот это критически важная часть для понимания. Она немного странная. Каждый вход/выход любой биткойн-транзакции всегда должен быть потрачен полностью. Так что если у нас есть 0.001 BTC и мы хотим отправить половину куда-то ещё, нам фактически приходится отправить одну половину туда, а другую половину — обратно себе.

Транзакция будет считаться действительной, если сумма всех выходов меньше суммы всех входов (чтобы мы не печатали деньги). Остаток будет «сдачей» (комиссией), которую заберёт победивший майнер, которому повезёт с proof of work и который включит нашу транзакцию в свой свежедобытый блок.

Давайте начнём со структуры данных входа транзакции:

@dataclass class TxIn: prev_tx: bytes # prev transaction ID: hash256 of prev tx contents prev_index: int # UTXO output index in the transaction script_sig: Script = None # unlocking script, Script class coming a bit later below sequence: int = 0xffffffff # originally intended for "high frequency trades", with locktime tx_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) идентифицируют конкретный выход (Output), который мы собираемся потратить. Снова отметим, что нигде мы не указываем, какую часть выхода хотим потратить. Мы должны потратить выход (или «UTXO», как его часто называют, сокращение от Unspent Transaction Output — непотраченный выход транзакции) целиком. Как только мы израсходуем этот UTXO целиком, мы вольны «нарезать» его сумму на сколько угодно выходов и при желании отправить часть этих кусков обратно на наш собственный адрес. В любом случае, в данном случае мы идентифицируем транзакцию, которая прислала нам биткойны, и говорим, что выход, который мы намерены потратить, находится у неё под индексом 1. 0-й индекс ушёл на некий другой неизвестный адрес, контролируемый краном, который мы не сможем потратить, потому что не контролируем его (у нас нет приватного ключа, и мы не сможем создать цифровую подпись).

Поле script_sig мы вернёмся рассмотреть позже. Именно сюда пойдёт цифровая подпись, криптографически подписывающая желаемую транзакцию нашим приватным ключом и фактически говорящая: «Я одобряю эту транзакцию как обладатель приватного ключа, чей публичный ключ хешируется в 4b3518229b0d3554fe7cd3796ade632aff3069d8».

sequence было в исходной реализации Bitcoin от Сатоши и предназначалось для своего рода функциональности «высокочастотной торговли», но сегодня имеет очень ограниченное применение, и мы в основном будем его игнорировать.

Расчёт комиссии. Отлично, итак, приведённая выше структура данных ссылается на входы нашей транзакции (здесь 1 вход). Давайте теперь создадим структуры данных для двух выходов нашей транзакции. Чтобы получить представление о текущей «рыночной ставке» комиссий за транзакции, есть ряд доступных сайтов, или мы можем просто пролистать какие-нибудь транзакции в недавнем блоке, чтобы прикинуть. Ряд недавних транзакций (включая ту, что выше) были упакованы в блок даже при <1 сатоши/байт (сатоши — это 1e-8 биткойна). Так что давайте попробуем взять очень щедрую комиссию, скажем, 10 сат/Б, или суммарную комиссию за транзакцию 0.0000001. В таком случае мы берём наш вход 0.001 BTC = 100 000 сат, комиссия составит 2 500 сат (потому что наша транзакция будет примерно 250 байт), мы отправим 50 000 сат на наш целевой кошелёк, а остальное (100,000 - 2,500 - 50,000 = 47,500) обратно нам.

@dataclass class TxOut: amount: int # in units of satoshi (1e-8 of a bitcoin) script_pubkey: Script = None # locking script tx_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 для обоих этих выходов. По сути, мы хотим задать условия, при которых каждый выход может быть потрачен какой-то будущей транзакцией. Как уже упоминалось, у Bitcoin богатый скриптовый язык с почти 100 инструкциями, которые можно выстраивать в различные блокирующие/разблокирующие скрипты, но здесь мы используем тот самый сверхстандартный и повсеместный скрипт, который уже видели выше и который кран использовал, чтобы заплатить нам. Чтобы обозначить владение обоими этими выходами, мы по сути хотим указать хеш публичного ключа того, кто может потратить выход. Только нам надо приодеть это в «обвес» из богатого скриптового языка. Ну, поехали.

Напомним, что блокирующий скрипт в транзакции крана имел такой вид, когда мы смотрели на него в обозревателе блоков Bitcoin. Хеш публичного ключа владельца выхода зажат между несколькими опкодами скриптового языка Bitcoin, которые мы вскоре разберём:

OP_DUP OP_HASH160 4b3518229b0d3554fe7cd3796ade632aff3069d8 OP_EQUALVERIFY OP_CHECKSIG

Нам нужно создать ту же самую структуру и закодировать её в байты, но мы хотим заменить хеш публичного ключа на хеши нового владельца. Все опкоды (вроде OP_DUP и т.д.) кодируются как целые числа по фиксированной схеме. Вот она:

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, )) @dataclass class 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 byte out += [encode_int(cmd, 1)] elif isinstance(cmd, bytes): # bytes represent an element, encode its length and then content length = len(cmd) assert length < 75 # any longer than this requires a bit of tedious handling that we'll skip here out += [encode_int(length, 1), cmd] ret = b''.join(out) return encode_varint(len(ret)) + ret # the first output will go to our 2nd wallet out1_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_CHECKSIG print(out1_script.encode().hex()) # the second output will go back to us out2_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())

1976a91475b0c9fc784ba2ea0839e3cdf2669495cac6707388ac 1976a9144b3518229b0d3554fe7cd3796ade632aff3069d888ac

Хорошо, теперь мы фактически объявим владельцев обоих выходов нашей транзакции, указав хеши публичных ключей (дополненные опкодами Script). Чуть позже мы увидим, как именно работают эти блокирующие скрипты для выходов, когда будем создавать разблокирующий скрипт для входа. Пока что важно понять, что мы фактически объявляем владельца каждого выхода-UTXO, идентифицируя конкретный хеш публичного ключа. С блокирующим скриптом, заданным как выше, потратить этот UTXO сможет только тот, у кого есть исходный публичный ключ (и связанный с ним секретный ключ).

tx_out1.script_pubkey = out1_script tx_out2.script_pubkey = out2_script

Цифровая подпись

Теперь самое важное — мы возвращаемся к указанию script_sig входа транзакции tx_in, который мы пропустили выше. В частности, мы сконструируем цифровую подпись, которая фактически говорит: «Я, владелец приватного ключа, связанного с хешем публичного ключа в блокирующем скрипте выхода упомянутой транзакции, одобряю трату этого UTXO как входа этой транзакции». К сожалению, тут Bitcoin снова становится довольно навороченным, потому что подписывать можно лишь части транзакций, и ряд подписей может быть собран от ряда сторон и скомбинирован разными способами. Как и выше, мы рассмотрим лишь (намного) самый распространённый сценарий — подписание всей транзакции целиком — и построим разблокирующий скрипт специально так, чтобы удовлетворить только блокирующий скрипт ровно того вида, что выше (OP_DUP, OP_HASH160, , OP_EQUALVERIFY, OP_CHECKSIG).

Сначала нам нужно создать чистое «сообщение» (message) в байтах, которое мы будем цифрово подписывать. В данном случае сообщение — это кодировка всей транзакции. И вот тут неловкость — всю транзакцию пока нельзя закодировать в байты, потому что мы её ещё не закончили! В ней по-прежнему не хватает нашей подписи, которую мы как раз и пытаемся сконструировать.

Вместо этого, когда мы сериализуем вход транзакции, который хотим подписать, правило таково: заменить кодировку script_sig (которой у нас нет, потому что мы как раз и пытаемся её получить…) на script_pubkey того выхода транзакции, на который этот вход ссылается. script_sig всех остальных входов транзакции также заменяется пустым скриптом, потому что эти входы могут принадлежать множеству других владельцев, которые могут по отдельности и независимо вносить свои собственные подписи. Ладно, не уверен, что сейчас это вообще понятно. Так что давайте просто посмотрим это в коде.

Нам нужна финальная структура данных — собственно Transaction (транзакция), чтобы мы могли сериализовать её в байтовое сообщение. Это в основном тонкий контейнер для списка TxIn-ов и списка TxOut-ов: входов и выходов. Затем мы реализуем сериализацию для нового класса Tx, а также сериализацию для классов TxIn и TxOut, чтобы мы могли сериализовать всю транзакцию в байты.

@dataclass class Tx: version: int tx_ins: List[TxIn] tx_outs: List[TxOut] locktime: int = 0 def encode(self, sig_index=-1) -> 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 well def 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 class tx = Tx( version = 1, tx_ins = [tx_in], tx_outs = [tx_out1, tx_out2], )

Прежде чем мы сможем вызвать .encode у нашего объекта Transaction и получить его содержимое в виде байтов, чтобы подписать его, нам нужно удовлетворить правило Bitcoin, по которому мы заменяем кодировку script_sig (которой у нас нет, потому что мы как раз пытаемся её получить…) на script_pubkey того выхода транзакции, на который этот вход ссылается. Вот снова ссылка на исходную транзакцию. Мы пытаемся потратить её выход под индексом 1, и script_pubkey, опять же:

OP_DUP OP_HASH160 4b3518229b0d3554fe7cd3796ade632aff3069d8 OP_EQUALVERIFY OP_CHECKSIG

Этот конкретный сайт-обозреватель блоков не позволяет нам получить это в сыром (байтовом) виде, поэтому мы заново воссоздадим структуру данных как Script:

source_script = Script([118, 169, out2_pkb_hash, 136, 172]) # OP_DUP, OP_HASH160, , OP_EQUALVERIFY, OP_CHECKSIG print("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

recall out2_pkb_hash is just raw bytes of the hash of public_key: 4b3518229b0d3554fe7cd3796ade632aff3069d8 1976a9144b3518229b0d3554fe7cd3796ade632aff3069d888ac

# monkey patch this into the input of the transaction we are trying sign and construct tx_in.prev_tx_script_pubkey = source_script # get the "message" we need to digitally sign!! message = tx.encode(sig_index = 0) message.hex()

'0100000001b2364d6ba4cbfd3dad8d6dc8dde1095f959bac4ee4ee7c4b8ab99fc885503246010000001976a9144b3518229b0d3554fe7cd3796ade632aff3069d888acffffffff0250c30000000000001976a91475b0c9fc784ba2ea0839e3cdf2669495cac6707388ac8cb90000000000001976a9144b3518229b0d3554fe7cd3796ade632aff3069d888ac0000000001000000'

Хорошо, давайте на минутку остановимся. Мы закодировали транзакцию в байты, чтобы создать «сообщение» на жаргоне цифровых подписей. Подумайте о том, что кодируют приведённые выше байты и что именно мы собираемся подписать. Мы идентифицируем точные входы этой транзакции, ссылаясь на выходы конкретных предыдущих транзакций (здесь, конечно, всего 1 вход). Мы также идентифицируем точные выходы этой транзакции (новые, готовые быть «отчеканенными» UTXO, так сказать) вместе с их полями script_pubkey, которые в самом распространённом случае объявляют владельца каждого выхода через хеш его публичного ключа, обёрнутый в Script. В частности, мы, конечно же, не включаем script_sig ни одного из других входов, когда подписываем конкретный вход (вы можете видеть, что функция txin_encode устанавливает их в пустые скрипты). На самом деле в полностью общем (хотя и редком) случае у нас их может даже не быть. Так что на самом деле это сообщение кодирует лишь входы и новые выходы, их суммы и их владельцев (через блокирующие скрипты, указывающие хеш публичного ключа каждого владельца).

Теперь мы готовы цифрово подписать сообщение нашим приватным ключом. Сама подпись — это кортеж из двух целых чисел (r, s). Как и с эллиптической криптографией (ECC) выше, я не буду раскрывать все математические детали алгоритма цифровой подписи на эллиптических кривых (ECDSA). Вместо этого просто приведу код и покажу, что он не такой уж и страшный:

@dataclass class Signature: r: int s: int def 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 sig def 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 pass random.seed(int.from_bytes(sha256(message), 'big')) # see note below sig = sign(secret_key, message) sig

Signature(r=47256385045018612897921731322704225983926443696060225906633967860304940939048, s=24798952842859654103158450705258206127588200130910777589265114945580848358502)

В приведённом выше коде вы заметите очень часто (и совершенно справедливо) комментируемую тонкость: в этой наивной форме мы генерируем случайное число внутри процесса подписания, когда генерируем sk. Это означает, что наша подпись менялась бы при каждом подписании, что нежелательно по множеству причин, включая воспроизводимость этого упражнения. Кстати, всё становится намного хуже и очень быстро: если вы подпишете два разных сообщения одним и тем же sk, атакующий сможет восстановить секретный ключ, ой. Спросите хотя бы у ребят с Playstation 3. Есть специальный стандарт (под названием RFC 6979), который рекомендует конкретный способ детерминированно генерировать sk, но мы здесь его опустим ради краткости. Вместо этого я реализую самопальную версию, где я инициализирую ГСЧ хешем сообщения. Пожалуйста, не используйте это нигде близко к тому, что касается продакшена.

Давайте теперь реализуем функцию encode для Signature, чтобы мы могли разослать её по протоколу Bitcoin. Для этого мы используем DER-кодирование:

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 frame Signature.encode = signature_encode # monkey patch into the class sig_bytes = sig.encode() sig_bytes.hex()

'30440220687a2a84aeaf387d8c6e9752fb8448f369c0f5da9fe695ff2eceb7fd6db8b728022036d3b5bc2746c20b32634a1a2d8f3b03f9ead38440b3f41451010f61e89ba466'

Мы наконец готовы сгенерировать 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 Blockchain pubkey_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. На высоком уровне, в процессе валидации транзакции во время майнинга, для каждого входа транзакции два скрипта конкатенируются в единый скрипт, который затем выполняется в «Bitcoin VM» (?). Теперь мы видим, что конкатенация двух скриптов будет выглядеть так:

OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG

Это затем выполняется сверху вниз по типичной стековой схеме push/pop, где любые байты заталкиваются в стек, а любые операции потребляют какие-то входные значения и заталкивают какие-то выходные. Итак, здесь мы заталкиваем в стек подпись и публичный ключ, затем публичный ключ дублируется (OP_DUP), он хешируется (OP_HASH160), хеш сравнивается с pubkey_hash_bytes (OP_EQUALVERIFY), и наконец проверяется целостность цифровой подписи как подписанной связанным приватным ключом.

Мы теперь выполнили все необходимые шаги! Давайте снова взглянем на repr нашей полностью сконструированной транзакции:

tx

Tx(version=1, tx_ins=[TxIn(prev_tx=b'F2P\x85\xc8\x9f\xb9\x8aK|\xee\xe4N\xac\x9b\x95_\t\xe1\xdd\xc8m\x8d\xad=\xfd\xcb\xa4kM6\xb2', prev_index=1, script_sig=Script(cmds=[b"0D\x02 hz*\x84\xae\xaf8}\x8cn\x97R\xfb\x84H\xf3i\xc0\xf5\xda\x9f\xe6\x95\xff.\xce\xb7\xfdm\xb8\xb7(\x02 6\xd3\xb5\xbc'F\xc2\x0b2cJ\x1a-\x8f;\x03\xf9\xea\xd3\x84@\xb3\xf4\x14Q\x01\x0fa\xe8\x9b\xa4f\x01", b'\x03\xb9\xb5T\xe2P"\xc2\xaeT\x9b\x0c0\xc1\x8d\xf0\xa8\xe0IR#\xf6\'\xae8\xdf\t\x92\xef\xb4w\x94u']), sequence=4294967295)], tx_outs=[TxOut(amount=50000, script_pubkey=Script(cmds=[118, 169, b'u\xb0\xc9\xfcxK\xa2\xea\x089\xe3\xcd\xf2f\x94\x95\xca\xc6ps', 136, 172])), TxOut(amount=47500, script_pubkey=Script(cmds=[118, 169, b'K5\x18"\x9b\r5T\xfe|\xd3yj\xdec*\xff0i\xd8', 136, 172]))], locktime=0)

Довольно легковесно, не правда ли? В биткойн-транзакции не так уж много всего. Давайте закодируем её в байты и покажем в hex:

tx.encode().hex()

'0100000001b2364d6ba4cbfd3dad8d6dc8dde1095f959bac4ee4ee7c4b8ab99fc885503246010000006a4730440220687a2a84aeaf387d8c6e9752fb8448f369c0f5da9fe695ff2eceb7fd6db8b728022036d3b5bc2746c20b32634a1a2d8f3b03f9ead38440b3f41451010f61e89ba466012103b9b554e25022c2ae549b0c30c18df0a8e0495223f627ae38df0992efb4779475ffffffff0250c30000000000001976a91475b0c9fc784ba2ea0839e3cdf2669495cac6707388ac8cb90000000000001976a9144b3518229b0d3554fe7cd3796ade632aff3069d888ac00000000'

print("Transaction size in bytes: ", len(tx.encode()))

Transaction size in bytes: 225

Наконец, давайте вычислим id нашей готовой транзакции:

def tx_id(self) -> str: return sha256(sha256(self.encode()))[::-1].hex() # little/big endian conventions require byte order swap Tx.id = tx_id # monkey patch into the class tx.id() # once this transaction goes through, this will be its id

'245e2d1f87415836cbb7b0bc84e40f4ca1d2a812be0eda381f02fb2224b4ad69'

Мы теперь готовы разослать транзакцию биткойн-узлам по всему миру. Мы буквально вышвыриваем в эфир 225 байт (вложенных в стандартный сетевой конверт протокола Bitcoin), которые задают нашу транзакцию. Биткойн-узлы декодируют её, провалидируют и включат в следующий блок, который могут добыть в любую секунду (если комиссия достаточно высока). По-русски эти 225 байт говорят: «Привет, сеть Bitcoin, как дела? Отлично. Я хотел бы создать новую транзакцию, которая берёт выход (UTXO) транзакции 46325085c89fb98a4b7ceee44eac9b955f09e1ddc86d8dad3dfdcba46b4d36b2 под индексом 1, и я хотел бы нарезать её сумму на два выхода: один пойдёт на адрес mrFF91kpuRbivucowsY512fDnYt6BWrvx9 на сумму 50 000 сат, а другой — на адрес mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ на сумму 47 500 сат. (Подразумевается, что остаток в 2 500 сат достанется любому майнеру, который включит эту транзакцию в свой блок.) Вот два документа, доказывающих, что я могу потратить этот UTXO: мой публичный ключ и цифровая подпись, сгенерированная связанным приватным ключом, под вышеизложенным письмом о намерениях. Спасибки!»

Мы собираемся разослать это в сеть и посмотреть, приживётся ли оно! Мы могли бы включить сюда простой клиент, который говорит на протоколе Bitcoin через socket для связи с узлами — сначала мы бы сделали рукопожатие (обмениваясь версиями туда-сюда), а затем разослали бы байты транзакции выше с помощью сообщения tx. Однако код довольно длинный и не сильно увлекательный (это много сериализации согласно конкретным форматам сообщений, описанным в протоколе Bitcoin), так что вместо того, чтобы и дальше раздувать этот ноутбук, я воспользуюсь полезным эндпоинтом tx/push от Blockstream, чтобы разослать транзакцию. Это просто большое текстовое поле, куда мы копипастим сырой hex транзакции в точности как выше, и жмём «Broadcast». Если вы хотите сделать это вручную через сырой протокол Bitcoin, вам стоит заглянуть в мою реализацию SimpleNode и использовать её для связи с узлом через сокет.

import time; time.sleep(1.0) # now we wait :p, for the network to execute the transaction and include it in a block

А вот и транзакция! Мы видим, что наши сырые байты были корректно разобраны, транзакция была признана действительной и включена в Block 2005515. Наша транзакция была одной из 31 транзакции, включённой в этот блок, и майнер забрал нашу комиссию в знак благодарности.

Собираем всё вместе: ещё одна, консолидирующая транзакция

Давайте теперь соберём всё вместе, создадим последнюю личность и консолидируем все наши оставшиеся средства в этом одном кошельке.

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 valid public_key3 = secret_key3 * G address3 = 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)

Our third Bitcoin identity: 1. secret key: 29595381593786747354608258168471648998894101022644411057647114205835530364276 2. public key: (10431688308521398859068831048649547920603040245302637088532768399600614938636, 74559974378244821290907538448690356815087741133062157870433812445804889333467) 3. Bitcoin address: mgh4VjZx5MpkHRis9mDsF2ZcKLdXoP3oQ4

И давайте выкуем транзакцию. Сейчас у нас 47 500 сат в первом кошельке mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ и 50 000 сат во втором кошельке mrFF91kpuRbivucowsY512fDnYt6BWrvx9. Мы создадим транзакцию с этими двумя в качестве входов и единственным выходом в третий кошелёк mgh4VjZx5MpkHRis9mDsF2ZcKLdXoP3oQ4. Как и раньше, мы заплатим 2500 сат комиссии, так что отправляем себе 50 000 + 47 500 - 2500 = 95 000 сат.

# ---------------------------- # first input of the transaction tx_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 later pkb_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 transaction tx_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) output tx_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_CHECKSIG tx_out.script_pubkey = out_script # ---------------------------- # create the aspiring transaction object tx = 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 here message1 = tx.encode(sig_index = 0) random.seed(int.from_bytes(sha256(message1), 'big')) sig1 = sign(secret_key2, message1) # identity 2 signs sig_bytes_and_type1 = sig1.encode() + b'\x01' # DER signature + SIGHASH_ALL pubkey_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 here message2 = tx.encode(sig_index = 1) random.seed(int.from_bytes(sha256(message2), 'big')) sig2 = sign(secret_key, message2) # identity 1 signs sig_bytes_and_type2 = sig2.encode() + b'\x01' # DER signature + SIGHASH_ALL pubkey_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())

361fbb9de4ef5bfa8c1cbd5eff818ed9273f6e1f74b41a7f9a9e8427c9008b93 Tx(version=1, tx_ins=[TxIn(prev_tx=b'$^-\x1f\x87AX6\xcb\xb7\xb0\xbc\x84\xe4\x0fL\xa1\xd2\xa8\x12\xbe\x0e\xda8\x1f\x02\xfb"$\xb4\xadi', prev_index=0, script_sig=Script(cmds=[b'0D\x02 \x19\x9aj\xa5c\x06\xce\xbc\xda\xcd\x1e\xba&\xb5^\xafo\x92\xebF\xeb\x90\xd1\xb7\xe7rK\xac\xbe\x1d\x19\x14\x02 \x10\x1c\rF\xe036\x1c`Ski\x89\xef\xddo\xa6\x92&_\xcd\xa1dgn/I\x88Xq\x03\x8a\x01', b'\x03\x9a\xc8\xba\xc8\xf6\xd9\x16\xb8\xa8[E\x8e\x08~\x0c\xd0~jv\xa6\xbf\xdd\xe9\xbbvk\x17\x08m\x9a\\\x8a']), sequence=4294967295), TxIn(prev_tx=b'$^-\x1f\x87AX6\xcb\xb7\xb0\xbc\x84\xe4\x0fL\xa1\xd2\xa8\x12\xbe\x0e\xda8\x1f\x02\xfb"$\xb4\xadi', prev_index=1, script_sig=Script(cmds=[b'0E\x02!\x00\x84\xecC#\xed\x07\xdaJ\xf6F \x91\xb4gbP\xc3wRs0\x19\x1a?\xf3\xf5Y\xa8\x8b\xea\xe2\xe2\x02 w%\x13\x92\xec/R2|\xb7)k\xe8\x9c\xc0\x01Qn@9\xba\xdd*\xd7\xbb\xc9P\xc4\xc1\xb6\xd7\xcc\x01', b'\x03\xb9\xb5T\xe2P"\xc2\xaeT\x9b\x0c0\xc1\x8d\xf0\xa8\xe0IR#\xf6\'\xae8\xdf\t\x92\xef\xb4w\x94u']), sequence=4294967295)], tx_outs=[TxOut(amount=95000, script_pubkey=Script(cmds=[118, 169, b'\x0c\xe1vI\xc10l)\x1c\xa9\xe5\x87\xf8y;[\x06V<\xea', 136, 172]))], locktime=0) 010000000269adb42422fb021f38da0ebe12a8d2a14c0fe484bcb0b7cb365841871f2d5e24000000006a4730440220199a6aa56306cebcdacd1eba26b55eaf6f92eb46eb90d1b7e7724bacbe1d19140220101c0d46e033361c60536b6989efdd6fa692265fcda164676e2f49885871038a0121039ac8bac8f6d916b8a85b458e087e0cd07e6a76a6bfdde9bb766b17086d9a5c8affffffff69adb42422fb021f38da0ebe12a8d2a14c0fe484bcb0b7cb365841871f2d5e24010000006b48304502210084ec4323ed07da4af6462091b4676250c377527330191a3ff3f559a88beae2e2022077251392ec2f52327cb7296be89cc001516e4039badd2ad7bbc950c4c1b6d7cc012103b9b554e25022c2ae549b0c30c18df0a8e0495223f627ae38df0992efb4779475ffffffff0118730100000000001976a9140ce17649c1306c291ca9e587f8793b5b06563cea88ac00000000

Снова направляемся к эндпоинту Blockstream tx/push, копипастим hex транзакции выше и ждём :)

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)

И вот транзакция, как она в итоге появилась, в составе Block 2005671, вместе с 25 другими транзакциями.

Упражнение для читателя: украдите мои биткойны из моего 3-го кошелька-личности (mgh4VjZx5MpkHRis9mDsF2ZcKLdXoP3oQ4) на свой собственный кошелёк ;) Если это удастся, у 3-го кошелька «Итоговый баланс» (Final Balance) станет 0. На момент написания это 0.00095000 BTC, как мы и задумывали и ожидали.

И на этом мы закругляемся! Это, конечно, лишь самая базовая демонстрация Bitcoin, использующая ныне несколько устаревший формат транзакций P2PKH (без более свежих новшеств, включая P2SH, Segwit, bech32 и т.д.), и, конечно, мы не затронули никакую валидацию транзакций/блоков, майнинг и так далее. Тем не менее, надеюсь, это послужит хорошим введением в ключевые концепции того, как в Bitcoin представлена ценность и как криптография используется для защиты транзакций.

По сути, у нас есть DAG (ориентированный ациклический граф) из UTXO, у каждого из которых есть определённая amount (сумма) и блокирующий Script; транзакции полностью потребляют и создают UTXO, и они упаковываются в блоки майнерами каждые 10 минут. Затем экономика используется для достижения децентрализации через proof of work: вероятность того, что какая-либо сущность сможет добавить новый блок в цепочку, пропорциональна её доле в общей вычислительной мощности хеширования SHA256 в сети.

Пока я писал свою библиотеку karpathy/cryptos, было интересно поразмышлять, куда уходит весь код. Большая часть криптографической сложности приходится на ECC, ECDSA и SHA256, которые относительно стандартны в индустрии и которые вы никогда не захотели бы реализовывать сами («не катай свою криптографию»). Помимо этого, ключевые структуры данных транзакций, блоков и т.д. довольно просты, но вокруг протокола Bitcoin и сериализации/десериализации всех структур данных в байты и обратно есть множество негламурных деталей. Помимо этого, Bitcoin — это живая, дышащая, развивающаяся кодовая база, которая движется вперёд с новыми функциями, чтобы продолжать масштабироваться, ещё больше укреплять свою безопасность — и всё это при сохранении полной обратной совместимости, чтобы избежать хардфорков. Иногда соблюдение этих ограничений приводит к довольно корявым конструкциям, например, Segwit показался мне, мягко говоря, не очень эстетичным. В других случаях есть большой объём сложности (например, со скриптовым языком и всеми его опкодами), который редко используется в большинстве базовых одноранговых транзакций.

Наконец, мне очень понравились разные исторические аспекты Bitcoin. Например, меня крайне позабавило, что некоторые из исходных багов Сатоши до сих пор живы, например, в том, как корректируется сложность майнинга (там есть ошибка на единицу — расчёт основан на 2015 блоках вместо 2016), или как некоторые опкоды содержат баги (например, исходный multisig). Или как некоторые из первозданных идей Сатоши вокруг высокочастотной торговли (locktime / sequence) до сих пор существуют, но находят лишь ограниченное применение, причём, вероятно, не совсем так, как задумывалось. Bitcoin — это кодовая база со всеми тяготами любого другого программного проекта, но без возможности ломать унаследованную функциональность (это потребовало бы хардфорка).

Если хотите копнуть глубже, я нашёл Mastering Bitcoin и Programming Bitcoin очень полезными справочниками. Я также реализовал намного более чистую, разделённую, протестированную и более обширную версию всего вышеизложенного в своём репозитории karpathy/cryptos, если вы захотите использовать её как образец в своём собственном путешествии по блокчейну. Я также обязательно загружу этот ноутбук туда. А ещё найдите меня в Twitter.

Надеюсь, вы что-то узнали и что это было весело!