저는 블록체인이 멋진 것이라고 생각합니다. 왜냐하면 블록체인은 오픈소스 소프트웨어 개발을 오픈소스 + 국가로 확장하기 때문입니다. 이는 컴퓨팅 패러다임의 흥미로운 혁신처럼 보입니다. 우리는 단순히 코드를 공유하는 것이 아니라, 작동하는 컴퓨터를 공유할 수 있으며, 누구나, 어디에서나, 허가 없이 공개적으로 사용할 수 있습니다. 이 혁명의 씨앗은 비트코인에서 시작되었을 수도 있으므로, 비트코인이 어떻게 작동하는지 직관적으로 이해하기 위해 더 자세히 알아보고 싶었습니다. 그리고 “내가 만들 수 없는 건 이해할 수 없다”는 정신에 따라 비트코인을 처음부터 구현하는 것보다 더 나은 게 있을까요?우리는 순수 파이썬을 사용하여 아무런 종속성 없이 비트코인 거래를 만들고, 디지털 서명하고, 브로드캐스트할 것입니다. 이 과정에서 우리는 비트코인이 가치를 나타내는 방식에 대해 조금 알게 될 것입니다. 시도해 봅시다.(그런데 이 글의 시각적 형식이 거슬린다면 동일한 내용이 담긴
1단계: 암호화 엔티티 생성
우선, 우리는 공개 키와 개인 키라는 두 개의 키로만 구성된 완전히 새로운 암호화 개체를 만들고 싶습니다. 비트코인은 RSA와 같은 일반적인 암호화 방식 대신
타원 곡선 암호화( ECC)를 사용하여 거래를 보호합니다. 여기서는 ECC에 대해 자세히 설명하지 않을 것입니다. 다른 사람들이 훨씬 더 나은 작업을 했기 때문입니다. 예를 들어,
Andrea Corbellini의 블로그 게시물 시리즈는 매우 유용한 자료 입니다 . 여기에서는 코드만 작성하겠지만, 이것이 수학적으로 어떻게 작동하는지 이해하려면 해당 게시물을 읽어야 합니다.
secp256k1 곡선을 사용합니다 . 이 분야의 초보자로서 저는 이 부분이 매우 흥미로웠습니다. 선택할 수 있는 다양한 곡선의 라이브러리가 있고, 각각 장단점과 기타 속성이 있습니다. NIST는 사용할 곡선에 대한 권장 사항을 발표하지만, 사람들은 백도어가 내장될 가능성이 낮은 다른 곡선(예: secp256k1)을 사용하는 것을 선호합니다. 그러나 타원 곡선은 차원이 상대적으로 낮은 수학적 객체로, 이를 정의하는 데는 3개의 정수만 필요합니다.
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
)
곡선 외에도 생성기 지점을 정의합니다. 이는 곡선 루프에서 고정된 “시작점”으로, 곡선을 따라 “무작위 이동”을 시작하는 데 사용됩니다. 발전기는 잘 알려지고 합의된 상수입니다.
@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
마지막으로 생성 지점 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,
)
아직 실제로 아무것도 한 것은 없습니다. 단지 일부 데이터 구조를 정의하고 비트코인에서 사용되는 타원 곡선과 관련된 잘 알려진 상수로 채우는 것뿐입니다. 우리가 개인 키를 생성할 준비가 되면 이는 곧 바뀔 것입니다. 개인 키(또는 지금부터 “비밀 키”라고 부르겠습니다)는 1 <= 키 <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
이것이 우리의 개인 키입니다. 아주 작은 정수이긴 하지만, 이것을 아는 사람이라면 누구나 이에 연결된 비트코인 체인에서 여러분이 소유한 모든 자금을 통제할 수 있습니다. 비트코인의 가장 간단하고 일반적인 사용 사례는 귀하의 계정을 제어하는 단일 “비밀번호”입니다. 물론, 제가 위에서 한 것처럼 다른 Andrew가 직접 개인 키를 생성하는 극히 드문 경우가 발생하면 해당 개인 키와 연결된 지갑의 비트코인 잔액은 0일 가능성이 높습니다 :).이제 공개 키를 생성할 텐데, 여기서 흥미로운 일이 시작됩니다. 공개 키는 생성기 지점을 자기 자신의 secret_key 횟수만큼 더한 곡선상의 지점입니다. 저것들. 우리는 다음과 같습니다: public_key = G + G + G + (비밀 키 크기) + G = secret_key * G. 여기서 ‘+’ (더하기)와 ‘*’ (곱하기) 기호는 모두 매우 특별하고 약간 혼란스럽습니다. 비밀 키는 정수이지만 생성자 지점 G는 곡선 위의 지점인 튜플 (x, y)이고, 그 결과 튜플 (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를 자기 자신에게 여러 번 더해야 할 것입니다. 그래서 결과는 정확할 것이지만, 작업은 매우 느릴 것입니다. 대신, 반복되는 덧셈의 속도를 크게 높이기 위해 “두 배로 더하기” 알고리즘을 구현해 보겠습니다. 이것이 효과가 있는 이유는 위의 게시물을 참조하세요. 자세한 내용은 다음과 같습니다.
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
개인/공개 키 쌍을 사용하여 암호화 엔티티를 생성했습니다. 이제 연관된 비트코인 지갑 주소를 가져올 시간입니다. 지갑 주소는 공개 키 그 자체일 뿐만 아니라, 공개 키에 기초하여 결정적일 수 있으며 여러 가지 추가 이점(예: 내장 체크섬)이 있습니다. 주소를 생성하기 전에 몇 가지 해시 함수를 정의해야 합니다. 비트코인은 널리 쓰이는 SHA-256과 RIPEMD-160을 사용합니다. Python 구현을 사용하여 바로 플러그 앤 플레이할 수도 있지만
hashlib
, 그러려면 종속성이 없는 구현이어야 하므로
import hashlib
부정행위에 해당합니다. 그럼 먼저, 비교적 읽기 쉬운
NIST FIPS PUB 180-4 문서에 따라 순수 Python으로 작성한 SHA256 구현을 소개합니다 .
def gen_sha256_with_variable_scope_protector_to_not_pollute_global_namespace():
"""
SHA256 implementation.
Follows the FIPS PUB 180-4 description for calculating SHA-256 hash function
https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf
Noone in their right mind should use this for any serious reason. This was written
purely for educational purposes.
"""
import math
from itertools import count, islice
# -----------------------------------------------------------------------------
# SHA-256 Functions, defined in Section 4
def rotr(x, n, size=32):
return (x >> n) | (x << 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은 해시할 메시지의 몇 바이트를 가져와서 먼저 메시지를 패딩한 다음, 메시지를 여러 조각으로 나누고 이 조각들을 섹션 3에 정의된 “비트 믹서”에 입력합니다. 여기에는 여러 비트 이동과 이진 연산이 포함되어 있는데 솔직히 저는 그 방식을 이해할 수 없지만, 이를 통해 SHA256이 제공하는 훌륭한 속성이 탄생하게 됩니다. 특히, 이 방법은 암호화가 취소 불가능하고 동일한 다이제스트에 해시된 다른 메시지를 생성하는 것이 계산적으로 불가능한, 가변 크기의 원본 메시지에 대해 혼란스러워 보이는 짧은 고정 크기 다이제스트를 생성합니다.비트코인은 해시를 생성하기 위해 전반적으로 SHA256을 사용하는데, 이는 비트코인 작업 증명의 핵심 요소입니다. 작업 증명의 목적은 거래 블록을 변경하여 전체가 충분히 낮은 숫자(다이제스트 바이트가 숫자로 해석되는 수준)로 해시될 때까지 기다리는 것입니다. 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("<Q", ctx.count)
padlen = 64 - ((ctx.count // 8) % 64)
if padlen < 1 + 8:
padlen += 64
RMD160Update(ctx, PADDING, padlen-8)
RMD160Update(ctx, size, 8)
return struct.pack("<5L", *ctx.state)
# -----------------------------------------------------------------------------
K0 = 0x00000000
K1 = 0x5A827999
K2 = 0x6ED9EBA1
K3 = 0x8F1BBCDC
K4 = 0xA953FD4E
KK0 = 0x50A28BE6
KK1 = 0x5C4DD124
KK2 = 0x6D703EF3
KK3 = 0x7A6D76E9
KK4 = 0x00000000
PADDING = [0x80] + [0]*63
def ROL(n, x):
return ((x << n) & 0xffffffff) | (x >> (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
. 이 클래스 역시 곡선 상의 한 지점일 뿐이지만, 비트코인 공개 키에 대한 추가적인 의미론과 해석, 그리고 비트코인 프로토콜에서 통신하기 위해 키를 바이트로 인코딩/디코딩하는 몇 가지 방법을 갖고 있습니다.
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
. 이는 모호성이 매우 낮은 58자리 알파벳 문자를 기반으로 하는 비트코인 전용 바이트 인코딩입니다. 예를 들어, “O”와 “0”은 종이 위에서 망가지기 쉽기 때문에 사용하지 않습니다. 따라서 우리는 비트코인 주소(원시 형태로 25바이트)를 가져와서 58진법으로 변환한 다음, 문자를 출력해야 합니다. 주소의 25바이트 원시 파일에는 버전에 대한 1바이트(비트코인 “메인넷”은
b'\x00'
, 비트코인 ”테스트넷”은
b'\x6f'
)와 해시에 대한 20바이트가 포함되어 있습니다. 다이제스트와 마지막으로 체크섬에 4바이트를 추가하여
1–1/2** 4 = 93,75%
사용자가 텍스트 필드에 비트코인 주소를 잘못 입력하는 경우 확률적으로 오류를 발생시킬 수 있습니다. 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
좋습니다. 이제 블록 탐색기 웹사이트를 확인해서 이 주소가 이전에 실행된 적이 없는지 확인해 보겠습니다:
www.blockchain.com/btc-testnet/address/mnNcaVkC35ezZSgvn8fhXEa9QTHSUtPfzQ . 이 가이드를 끝까지 읽고 나면 이런 일은 일어나지 않겠지만, 이 가이드를 작성할 당시에는 이 주소가 “깨끗”하다는 것을 확인했기 때문에 위에서 한 것처럼 테스트넷에서 비밀 키를 생성하여 사용하는 사람은 없었습니다. 이는 비트코인을 가지고 장난치는 “안드레이”라는 나쁜 유머 감각을 가진 사람이 또 있었을 것이기 때문에 당연한 일입니다. 하지만 과거에 사람들이 사용했을 것으로 예상되는 매우 비밀스러운 개인 키도 확인할 수 있습니다. 예를 들어, 가장 작은 유효한 비밀 키에 속하는 주소가 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부: 시드 펀드 획득 + 비트코인의 내부 구조 소개
- 3부: 거래 작성하기
꼬리표:
바퀴통: