파이썬으로 비트코인 구현하기

Junu Moon·2021년 7월 11일
0
post-thumbnail

비트코인이 열풍이다. 일론 머스크가 쏘아올린 도지코인이 급등하는 모습을 보면 암호화폐는 그저 투기종목 같아 보인다. 그러나 암호화폐는 달러, 원화와 같은 기존 통화와 근본적으로 다르다.

암호화폐의 핵심은 정부, 은행과 같은 제 3의 기관의 통제 없이 개인 대 개인(Peer 2 Peer)사이에서 자유로운 거래가 가능하도록 하는 시스템이다.
신뢰 기반의 기존 통화 시스템과는 다르게 암호화폐는 Don't Trust. Verify를 말한다.

파이썬으로 비트코인의 백서를 구현해보며 이것이 어떻게 가능한지를 알아보도록 하자.

Overview

파이썬으로 비트코인 백서에서 설명하는 암호화폐를 구현하고 핵심 기술을 이해해보자!

Goals

  • 파이썬으로 구현하는 simple blockchain cryptocurrency
  • 암호화폐에서 쓰이는 기술 이해 및 파이썬 라이브러리 사용

What we don't aim

  • 파이썬 기본 문법
  • 암호학에 대한 깊은 이해
  • 네트워크

Dependencies

  • python > 3.6
  • rsa

암호 화폐

  • 신뢰 대신 암호학적 증명(cryptographic proof)에 기반한 전자 화폐 시스템.
  • 거래 의사가 있는 두 당사자가 제 3자의 중개(계약) 없이 직접 거래를 할 수 있음.

구현 내용

  • class Node: 거래의 주체
    • def new_transaction: Node는 제 3자의 개입 없이 Transaction을 만들 수 있다.
  • class Transaction: 보내는 이, 받는 이, 총량, 거래 발생 시간 정보를 담은 거래 클래스
from datetime import datetime

class Transaction:
    
    def __init__(self, sender: 'Node', recipient: 'Node', amount: int):
        self.sender = sender
        self.recipient = recipient
        self.amount = amount
        self.timestamp = datetime.now()
        
    def __repr__(self):
        return f"{self.sender} -> {self.recipient} : {self.amount} coins"
    
class Node:
    
    def __init__(self):
        self.ledger = list() # 각 Node 마다 거래 리스트를 갖는다. 
    
    def new_transaction(self, recipient: 'Node', amount: int) -> Transaction:
        tx = Transaction(sender=self, recipient=recipient, amount=amount)
        self.ledger.append(tx)
        return tx
    
    def __repr__(self):
        return f"Node_{id(self)}"

호정 = Node()
준우 = Node()

tx1 = 준우.new_transaction(호정, 3)
print(tx1)

> excute

Node_4510831232 -> Node_4510832240 : 3 coins

거래 암호화

signature

  • 기존 화폐 시스템에서 거래를 검증하는 것은 정부, 은행 등의 써드파티다. 제 3자가 개입하지 않는 암호화폐에서 거래 검증은 암호화된 서명을 통해 이뤄진다.

비대칭 공개키 암호화(Asymmetric Encryption)

  • 비대칭 공개키 암호화에서 Node는 개인키(priviate key)와 공개키(public key)를 갖는다.
  • 보내는 사람은 개인키로 서명을 남기고, 받는 사람은 보내는 사람의 공개키로 서명을 검증할 수 있다.

구현내용

  • 비대칭 공개키 암호화에서 흔히 쓰이는 RSA를 사용한다.
  • Node 객체가 생성될 때 개인키/공개키 쌍을 갖는다.
  • 개인키로 거래에 서명하고, 공개키로 거래를 검증한다.
  • class Node:
    • def sign_transaction: Node는 각자의 서명을 갖고 거래를 만들 때 서명을 남긴다.
    • def verify_transaction: 거래의 수신 Node는 발신 Node의 서명을 확인함으로써 거래를 검증한다.
import rsa

class Node:
    
    def __init__(self):
        self.ledger = list()
        (self.public_key, self.secret_key) = rsa.newkeys(512)
        
    def sign_transaction(self, transaction) -> bytes:
        signature = rsa.sign(str(transaction).encode(),
                             self.secret_key,
                             'SHA-256')
        transaction.signature = signature
    
    def new_transaction(self, recipient: 'Node', amount: int) -> Transaction:
        tx = Transaction(sender=self, recipient=recipient, amount=amount)
        tx = self.sign_transaction(tx)
        self.ledger.append(tx)
        return tx
    
    @staticmethod
    def verify_transaction(transaction, sender_public_key):
        try:
            rsa.verify(str(transaction).encode(), transaction.signature,
                        sender_public_key)
            print("Transaction has valid signature")
        except Exception as e:
            print("Transaction has invalid signature!")
    
    def __repr__(self):
        return f"Node_{id(self)}"

호정 = Node()
준우 = Node()
성용 = Node()

tx1 = 준우.new_transaction(호정, 3)
print(tx1)

호정.ledger.append(tx1) # 자신의 장부에 거래 등록
호정.verify_transaction(tx1, 준우.public_key) # 거래 검증
호정.verify_transaction(tx1, 성용.public_key)

> result

Node_4514578832 -> Node_4512942352 : 3 coins
Transaction has valid signature
Transaction has invalid signature!

Hash Chaining

  • 이미 사용된 코인을 재사용하는 이중지불을 방지하면서 제 3자의 중앙관리 없이 실현하기 위한 유일한 방법은 거래마다 모든 거래를 인식하는 것이다.
  • 암호화폐에서는 거래에 유일한 hash id를 부여하고 새로운 거래의 hash id가 직전 거래의 hash id를 참조하도록 한다.
  • 결과적으로 새로운 거래는 거래 사슬(chain) 전체를 참조하게 된다.

![image-20210711093604359](/Users/junumoon/Library/Application Support/typora-user-images/image-20210711093604359.png)

Hash

  • 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하는 함수
  • 거래에 유일한 id를 부여하는 역할을 한다.

특성

  • 입력이 동일하다면 해쉬값은 항상 같다.
  • 입력이 아주 조금만 변해도 해쉬값은 완전히 달라진다.
  • 해쉬값으로 입력값을 계산하는 것이 매우 어렵다.

구현

  • Hash 알고리즘 중 가장 일반적으로 사용되는 sha-256을 사용한다.

  • class Transaction:

    • property previous_hash: 직전 거래 hash_id
    • property hash_id: 거래 정보와 직전 거래의 hash_id를 hash 함수에 넣고 갈아만든 배.
  • class Node:

    • def sign_transation: Transaction의 hash_id에 자신의 개인키를 넣고 서명
class Transaction:
    
    def __init__(self, preivous_hash: str, sender: 'Node', recipient: 'Node', amount: int):
        self.previous_hash = previous_hash
        # ...
        
    @property
    def hash_id(self):
        return rsa.compute_hash(message=str(self).encode(), method_name='SHA-256')
        
class Node:
    
    def new_transaction(self, recipient: 'Node', amount: int) -> Transaction:
        if not self.ledger: # 최초 거래의 previous_hash는 임의 지정
           previous_hash = 'first_transaction'
        else:
           previous_hash = self.ledger[-1].hash_id # 장부에 등록된 마지막 거래의 hash_id
        tx = Transaction(previous_hash=previous_hash, sender=self, recipient=recipient, amount=amount)
        tx = self.sign_transaction(tx)
        self.ledger.append(tx)
        return tx

호정 = Node()
준우 = Node()
성용 = Node()

tx_list = []
tx1 = 준우.new_transaction(호정, 5)
호정.ledger.append(tx1)
tx2 = 호정.new_transaction(성용, 3)
성용.ledger.append(tx2)

print('tx1 hash_id:', tx1.hash_id.hex())
print('tx2 hash_id:', tx2.hash_id.hex())

> result

tx1 hash_id: 6ef97048c38298fede63d5beda5ac4e31523d8199fe17027891153516b235e65
tx2 hash_id: f5255f6984c94f4b030c9adefa7926d3e23779645ac209f4469431a0b361dd53

작업증명(Proof-Of-Work)

  • 해쉬 사슬로 이루어진 암호화폐가 이중거래 및 위조를 방지하기 위해서는 작업증명이 필요하다.
  • 작업증명이란 거래에 부여되는 해쉬 아이디에 rule을 추가하여 해쉬 연산을 어렵게 만드는 것이다.
    • e.g) 해쉬 아이디의 초기값 6개는 0이어야 한다.
    • hash_id[:6] == "0" * 6
  • 거래에 임시값(nonce)를 더하여 산출된 해쉬 아이디가 조건을 만족하기 위해서는 임시값을 무작위로 대입하는 방법밖에 없기 때문에 큰 컴퓨팅 파워가 필요하다.

구현

  • def proof_of_work: transaction에 nonce를 추가하여 초기값이 6개가 될 때까지 nonce를 증가시키며 해쉬값을 계산한다.
def proof_of_work(transaction):
    nonce = 0 # 임시값
    
    while True:
        input_data = (str(transaction) + str(nonce)).encode()
        computed_hash = rsa.compute_hash(input_data, 'SHA-256').hex()
        if computed_hash[:6] == "0" * 6:
            break
        else:
            nonce += 1
        return computed_hash, nonce
  • 작업증명으로 인해 공격자 노드의 거래 위조가 어려워진다.
  • 거래를 위조하려면 해당 거래 이후 발생하는 모든 거래의 해쉬값을 계산해야 하는데, 이는 엄청나게 큰 컴퓨팅 파워를 요구하기 때문이다.

과거 블록을 변경하려면 공격자는 그 블록과 그 뒤를 잇는 모든 블록의 작업증명을 재수행해야 하고 그러면서 가장 정직 한 노드들의 작업을 따라잡아 앞질러야 한다. 이어지는 블록이 추가될수록 더 느린 공격자가 따 라잡을 확률이 지수적으로 감소한다. _ 비트코인 백서 - 작업증명

블록 Block

  • 그러나 거래가 발생할 때 마다 작업증명을 하는 것은 거래 자체가 어렵게 된다.
  • 따라서 k개의 거래를 담을 수 있는 묶음을 Block이라 하고 작업증명은 Block에 한해서 실시한다.
  • 새로운 거래가 암호화폐 체인에 등록되기 위해서는 Block에 먼저 등록되어야 한다.
  • 새로운 Block의 작업증명을 완수한 Node에는 보상을 준다.
  • 이렇게 작업증명으로 새로운 Block을 찾는 것을 일반적으로 mining이라고 한다.

구현

  • class Block: k개의 transaction을 담는 컨테이너
class Block:

    MINE_DIFFICULTY = 4
    FIRST_NONCE = 100

    def __init__(self, previous_hash, nonce, miner, transactions):
        self.preivous_hash = previous_hash
        self.nonce = nonce
        self.miner = miner
        self.timestamp = datetime.now()
        self.transactions = transactions

    @classmethod
    def proof_of_work(cls, block):
        nonce = 0
        while True:
            _hash = custom_hash(block, nonce)
            if _hash[:cls.MINE_DIFFICULTY] == "0"*cls.MINE_DIFFICULTY:
                break
            else:
                nonce += 1
        return _hash, nonce
profile
Machine Learning Engineer who is obeying the TDD Goat!

0개의 댓글