블록체인은 잘 모르지만 믿고 따라해보는 골빈해커님이 블록체인 g-coin 소스를 올려주셨기에 한 번 탐독해봤습니다. 무엇보다 가장 놀란 건 소스코드가 너무 간결하네요. 소스코드 한줄 한줄에 내공이 느껴집니다. 오랜만에 이런 힐링되는 소스코드를 보게되는 바람에 급 탐독하게 되었습니다. 이론적인 배경이 없는 상태에서 소스코드를 분석하면서 이해하는 방식이라 ‘파이썬을 조금알고’, ‘저와 같이 이론 배경이 없으면서’, ‘퍼즐 맞추기를 좋아하시는 분’이 읽으시면 재미있을 것 같네요.

img


소스코드

골빈해커님의 깃허브에서 소스코드를 다운로드 받으실 수 있습니다.

gcoin 폴더에 들어가시면 아래 파일을 보실 수 있습니다.

  • block.py
  • blockchain.py
  • book.py
  • miner.py
  • node.py
  • proof.py
  • transaction.py

기초적인 개념을 코드와 같이 익혀볼 예정입니다. 자 따라가보시죠.


거래정보(transaction.py) 알아보기

먼저 거래에 대해서 알아보겠습니다. ‘transaction.py’을 열어봅니다. 거래 정보에는 ‘보내는 이’, ‘받는 이’, ‘거래량’이 포함되어 있군요.

class Transaction:
    def __init__(self, sender, recipient, amount):
        """Transaction

        Args:
            sender (str): 보내는 이
            recipient (str): 받는 이
            amount (int): 거래량 (양수)
        """
        self.sender = sender
        self.recipient = recipient
        self.amount = amount

        if amount < 1:
            raise Exception('Amount have to be positive number.')

    def dump(self): # 거래 정보를 반환합니다.
        return {
            'sender': self.sender,
            'recipient': self.recipient,
            'amount': self.amount
        }

    @classmethod
    def init_from_json(cls, data): # JSON으로부터 거래 정보를 로딩합니다.
        return cls(data['sender'],
                   data['recipient'],
                   data['amount'])

이제 함수를 살펴볼까요?

  • init(): 입력되는 보내는 이, 받는 이, 거래량으로 거래정보를 초기화 합니다.
  • dump(): 거래 정보를 반환하는 함수인데, 어딘가 쓰이겠죠?
  • init_from_json(): JSON으로부터 거래정보를 초기화하는 함수입니다. 정보를 JSON으로 주고 받나 봅니다.

가볍게 코드리딩하기에 부담없는 시작이네요.


블록(block.py) 알아보기

다음은 블록에 대해서 알아보겠습니다. 블록은 아래 항목들의 정보가 포함되어 있네요.

  • transactions (list): 거래정보 목록
  • proof (int): ???
  • previous_hash (str): ???
  • timestamp (float): 블록이 생성된 시각

일단 블록이 아까본 거래정보가 리스트로 되어 있어 여러 개를 가지고 있을 수 있습니다. timestamp에는 블록이 생성되는 시각이 담겨 있네요. ‘proof’와 ‘previous_hash’는 모르는 개념이지만 ‘proof’는 10으로, ‘previouse_hash’는 문자열로 타입입니다.

import json
import hashlib
from time import time

from gcoin.transaction import Transaction

class Block:
    def __init__(self, transactions, proof=0,
                 previous_hash=None, timestamp=0):
        """Block

        Args:
            transactions (list): list of Transaction object
            proof (int):
            previous_hash (str):
            timestamp (float):
        """
        self.transactions = transactions
        self.proof = proof if proof else 10
        self.timestamp = timestamp if timestamp else time()
        self.previous_hash = previous_hash if previous_hash else 'g'

    def hash(self):
        """Make hash of current block"""
        block_dump = json.dumps(self.dump(), sort_keys=True).encode()
        block_hash = hashlib.sha256(block_dump).hexdigest()

        return block_hash

    def dump(self):
        return {
            'transactions': [t.dump() for t in self.transactions],
            'previous_hash': self.previous_hash,
            'proof': self.proof,
            'timestamp': self.timestamp
        }

    @classmethod
    def init_from_json(cls, data):
        transactions = [Transaction.init_from_json(t)
                        for t in data['transactions']]

        return cls(transactions,
                   data['proof'],
                   data['previous_hash'],
                   data['timestamp'])

이제 함수를 보겠습니다.

  • init(): 블록정보를 초기화합니다.
  • dump(): 블록정보를 반환합니다. 여기서 아까본 거래정보에서 Transaction::dump() 함수가 쓰이네요. 리스트로 되어 있기에 하나씩 덤프뜨면서 리스트로 넣습니다. (덤프뜨다: 내용을 복사해서 가지고 오다.)
  • init_from_json(): JSON 데이터를 받아서 블록정보를 초기화합니다. dump()와 같이 거래정보는 여러개이기 때문에 이를 위한 처리가 포함되어 있네요.
  • hash(): 끼약~ 몬가 어려워 보이는 함수들이 많네요. 해쉬개념도 필요해보입니다.

hash()

hash() 함수를 조금 더 살펴보겠습니다.

  1. 블록정보를 덤프뜬다. ‘self.dump()’
  2. 1번에서 뜬 블록정보를 json 형식으로 덤프뜬다. ‘json.dumps()’
  3. 2번에서 뜬 덤프를 block_dump에 저장한다.
  4. block_dump에 해당하는 해쉬값을 계산하여 반환한다. sha256와 hexdigest가 보이니 대충 256비트의 헥사(16진수)값으로 변환되나 봅니다.

해쉬에 대한 개념은 모르나 블록정보를 256비트의 어떤 값으로 바꾸는 것 정도라고만 알아도 무방할 듯 하네요. 여기까지가 ‘거래정보’와 그 거래정보 여러개를 담고 있는 ‘블록정보’에 대해서 알아봤습니다. ‘채굴, 채굴’ 그러는 데, 다음은 블록을 어떻게 채굴하는 지 궁금해집니다.


채굴자(miner.py) 알아보기

‘채굴자’라는 의미같은 ‘Miner’는 account_id 정보만 받네요. 음 채굴하려면 계정이 있어야 되나 봅니다.

  • ‘GENESIS_ACCOUNT_ID’은 몬가 신 같은 존재의 ID가 있네요. ID 정보는 ‘0’입니다. 기억해두죠.
  • AMOUNT_OF_REWARD : 보상의 양이란 뜻인데, 채굴에 성공하면 받는 양 같습니다. 한 번 채굴할 때 하나씩 보상받습니다.
import gcoin.proof as proof
from gcoin.transaction import Transaction

GENESIS_ACCOUNT_ID = '0'
AMOUNT_OF_REWARD = 1

class Miner:
    def __init__(self, account_id):
        self.account_id = account_id

    def __call__(self, blockchain):
        last_block = blockchain.last_block()

        # Proof of Work
        new_proof = proof.find_proof(last_block.proof)

        # Adding mining rewards
        transaction = Transaction(GENESIS_ACCOUNT_ID,
                                  self.account_id, AMOUNT_OF_REWARD)
        blockchain.add_transaction(transaction)

        # Make new block with new proof,
        #   transactions and hash of last block
        block = blockchain.new_block(new_proof)

        return block

함수는 두 개뿐이네요.

  • init(): 주어진 계좌로 채굴자를 초기화 합니다.
  • call(): 특수 함수인 것 같은데 이건 어떻게 호출되죠? 문법은 조금 있다가 알아보겠습니다.

call()

일단 call() 함수 안을 살펴보겠습니다.

  1. 드디어 blockchain(블록체인)이란 녀석이 나왔네요. 블록체인에 대해서는 아직 살펴보지 않았지만, “blockchain.last_block()”을 통해서 가장 마지막에 있는 블록을 가져옵니다.
  2. proof(증명자)이란 녀석도 나왔습니다. 이 개념도 잘 모르지만 마지막 블록의 proof 값으로 증명을 찾아서 어떤 값을 반환하네요.
  3. 오~ 우리가 알고 있는 거래정보(Transaction) 객체가 나왔습니다. 거래정보와 입력값을 매칭해보죠. 신적인 존재로부터 채굴자의 계좌에 ‘1’만큼 보상을 하라는 거래정보가 생겼네요. 벌써부터 부자가 된 느낌인데요?
    • sender <- GENESIS_ACCOUNT_ID
    • recipient <- self.account_id
    • amount <- AMOUNT_OF_REWARD
  4. blockchain에 add_transaction()함수를 호출해서 거래를 추가하네요.
  5. 그리고는 새로운 증명과 함께 blockchain으로부터 블록을 하나 생성합니다.

아직 ‘proof(증명)’과 ‘blockchain(블록체인)’에 대해서는 모르지만 정리해해보겠습니다.

  • 채굴자는 블록체인의 마지막 블록을 가지고 와서 새로운 증명을 찾은 다음,
  • 신으로부터 보상을 받는 거래 정보를 생성한 뒤 블록체인에게 추가해달라고 하고,
  • 찾은 새로운 증명으로 블록체인에 새로운 블록 생성 요청한 후
  • 새로 생긴 블록을 반환합니다.

    채굴자는 증명찾기의 대가로 블록체인으로부터 보상도 받고 새로운 블록도 받나봅니다.

다 다음은 blockchain과 proof을 봐야겠죠? proof가 더 간단해보이니 이것부터 보겠습니다.


증명찾기(proof.py) 알아보기

증명찾기라는 용어자체가 생소하지만 구글검색 찬스는 나중에 하기로 하고, 일단 코드에 집중해보겠습니다.

import hashlib

DIFFICULTY = 1  # number of digits is difficulty
VALID_DIGITS = '0' * DIFFICULTY

def valid_proof(last_proof, proof):
    """ Validates proof

    last digits of hash(last_proof, proof)
        == VALID_DIGITS

    Args:
        last_proof (int): previous proof
        proof (int): proof to validate

    Returns:
        bool:
    """
    proof_seed = '{0}{1}'.format(last_proof, proof).encode()
    proof_hash = hashlib.sha256(proof_seed).hexdigest()

    return proof_hash[:DIFFICULTY] == VALID_DIGITS

def find_proof(last_proof):
    """proof of work

    Args:
        last_proof (int):

    Returns:
        int: proof
    """
    proof = 0

    while valid_proof(last_proof, proof) is False:
        proof += 1

    return proof

숫자의 갯수로 어려운 정도(DIFFICULTY)를 나타냅니다. 1이면 1만큼 어렵다는 뜻같네요. 유효한 수(VALID_DIGITS)가 있는데, 이것이 어려운 정도랑 관련이 있네요.

  • 1만큼 어려우면 유효한 수는 ‘0’
  • 5만큼 어려우면 유효한 수는 ‘00000’
  • 10만큼 어려우면 유효한 수는 ‘0000000000’ 이렇게 되는 것 같습니다.

역시 함수는 두 개뿐이네요. (골빈해커님 이런 간결한 코드 감사합니다)

  • valid_proof(): ‘증명검증’라는 정도로 이해해보죠.
  • find_proof(): 드디어 ‘증명찾기’라는 함수입니다.

valid_proof()

먼저 ‘증명검증(valid_proof)’입니다. 증명씨앗(proof_seed)와 증명해쉬(proof_hash) 두가지 개념이 나오네요.

  1. 마지막 증명(last_proof)와 현재 증명(proof)을 이용해서 증명씨앗(proof_seed)을 만들 뒤,
  2. 이 증명씨앗(proof_seed)을 이용해서 증명해쉬(proof_hash)을 계산합니다. 이것까지는 크게 어려운 것은 없네요. 블록정보안에 proof라는 숫자를 가지고 있으니 넘겨받은 두 개의 proof을 이용해서 씨앗과 해당하는 해쉬라는 걸 만드는 것 같습니다.

    proof_hash[:DIFFICULTY] == VALID_DIGITS

이게 주요 핵심 중 하나인 것 같네요. 증명해쉬(proof_hash)는 해쉬값이라는 정체불명의 숫자일텐데, 어려운 정도만큼 문자열을 가지고와서 유효한 숫자랑 같은 지 비교합니다. 예를 들어보겠습니다.

[가정1]

  • 증명해쉬(proof_hash) : ‘93A34B1’
  • 어려운 정도(DIFFICULTY) : 1
  • 유효한 숫자수(VALID_DIGITS) : ‘0’

따라서 proof_hash[:DIFFICULTY] == VALID_DIGITS proof_hash[:1] == ‘0’ ‘9’ == ‘0’ 결과는 FALSE 이네요.

이번엔 TRUE가 나오는 가정을 해볼까요?

[가정2]

  • 증명해쉬(proof_hash) : ‘03A34B1’
  • 어려운 정도(DIFFICULTY) : 1
  • 유효한 숫자수(VALID_DIGITS) : ‘0’

따라서 proof_hash[:DIFFICULTY] == VALID_DIGITS proof_hash[:1] == ‘0’ ‘0’ == ‘0’ 결과는 TRUE 이네요.

어려운 정도를 올려봅시다.

[가정3]

  • 증명해쉬(proof_hash) : ‘03A34B1’
  • 어려운 정도(DIFFICULTY) : 3
  • 유효한 숫자수(VALID_DIGITS) : ‘000’

따라서 proof_hash[:DIFFICULTY] == VALID_DIGITS proof_hash[:3] == ‘000’ ‘03A’ == ‘000’ 결과는 FALSE 이네요.

여러운 정도를 높이니깐 정체불명의 해쉬값 시작이 어려운 만큼 ‘0’으로 채워져야 하네요.

find_proof()

다음 함수인 ‘증명찾기(find_proof)’를 보겠습니다. 어디서 호출되는 지 모르겠지만 (블록체인에서 호출이 되겠죠?) 함수 내용은 간단하네요.

  • 함수에서 인자로 받은 마지막 증명(last_proof)과 현재 증명(proof, 초기값은 0)으로 ‘증명검증’을 해봅니다.
  • 증명검증이 이루어지면 바로 반환합니다.
  • 증명검증에 실패하면, proof를 하나 올리고, 다시 증명검증을 하네요. 증명검증이 될때까지 반복하네요. 반복하면서 proof는 계속 올라갑니다. proof는 증명씨앗을 만들때 사용되므로 proof 값이 올라가면 증명씨앗값이 바뀌고 그러면 증명해쉬도 바뀌게 되겠네요.
  • 무한반복하다보면 유효한 수에 만족하여 증명을 마칩니다. 그전까지는 무한반복…
  • 증명검증에 성공하면 proof 값은 엄청 높아져 있겠네요.
  • 난이도에 따라 만족해야할 유효한 수가 늘어나기 때문에 반복 수도 늘겠네요. 사람들이 블록체인 얘기할 때 의문스러운 점이 있었는 데, 이제 이해가 되는군요.

    난이도에 따라 채굴의 속도가 느려진다.

저는 무언가 의미있는 수학문제를 풀어서 보상을 받는 형식인 줄 알았는데, 유효한 수를 만족하는 해쉬값을 찾는 것이 핵심인 듯 합니다.

무언가 많은 것이 이해되는 느낌입니다. 블록체인(blockchain.py)에서 모든 퍼즐조각이 맞춰질 것 같네요. 잠시 쉬고 다시 시작합시다. 

통장(book.py) 알아보기

쉬지도 않고 보시는 군요. 계속 달려가보겠습니다. 이제 남은 건 ‘book.py’, ‘node.py’, ‘blockchain.py’ 세 개 있습니다. ‘blockchain.py’을 잠깐 살펴보니 ‘통장(book)’를 사용하고 있어 먼저 알아보겠습니다. book.py에는 계정(Account)와 통장(Book) 두 개의 클래스가 있네요.

"""account book"""

class Account:
    def __init__(self):
        self.target = []  # sender or recipient
        self.amount = []  # - / + amount

    def sum(self):
        return sum(self.amount)

    def add(self, target, amount):
        self.target.append(target)
        self.amount.append(amount)

계정는 ‘타겟’과 ‘양’ 정보의 리스크로 되어 있습니다. 타겟은 ‘보내는 이’ 또는 ‘받는 이’가 될 수 있고, ‘양’도 +, -로 지정할 수 있네요.

만약 보냈다면 하면

  • 타켓에서는 받는 이
  • 양은 -

으로 기록될 것같고, 받았다면

  • 타켓에는 보내는 이
  • 양은 +

으로 될 것 같네요. (‘이 코드로 이걸로 가계부를 만들 수 있을 것 같습니다.’)

sum()이라는 함수는 주고받은 양을 모두 합한 합계를 반환합니다. 어디에 사용되는 지는 모르겠으나 코드 자체는 어렵지 않으니 넘어가겠습니다.

class Book:
    def __init__(self):
        self.account = {}

    def check_balance(self, transaction):
        """Check sender's balance
        TODO: check balance in transactions in next blocks

        Args:
            transaction (obj): Transaction object

        Returns:
            bool:
        """
        if transaction.sender == '0':  # for mining rewards
            return True
        if transaction.sender in self.account:
            account = self.account[transaction.sender]
            return account.sum() - transaction.amount >= 0
        else:
            return False

    def get_account(self, account_id):
        if account_id not in self.account:
            self.account[account_id] = Account()

        return self.account[account_id]

    def apply(self, transactions):
        """Add new transactions to book in new block

        Args:
            transactions (obj): Transaction object
        """
        for t in transactions:
            sender = self.get_account(t.sender)
            recipient = self.get_account(t.recipient)

            sender.add(recipient, -t.amount)
            recipient.add(sender, t.amount)

함수를 세 개입니다.

  • init() : 계정를 리스트로 관리하나 봅니다.
  • check_balance() : 복잡을 것 같으니 좀 있다 보고요.
  • get_account() : 계정 ID로 계정정보를 얻는데, 기존에 있는 계정 ID면 해당하는 계정정보를 반환하고, 그렇지 않으면 신규로 발행해줍니다. 은행가서 계정을 만들어주는 것 같네요.
  • apply(): 거래정보들을 받아서 기록합니다.

apply()

어디서 호출되는 지 모르겠지만 (블록체인에서 호출되겠죠?) apply() 먼저 살펴보겠습니다.

  • 인자로 거래정보 목록을 받습니다.
  • 거래정보에는 ‘받는 이’, ‘보내는 이’, ‘거래량’이 기록되어 있는데요,
  • 계정 목록 중에 ‘받는 이’를 찾아서 ‘보내는 이’와 ‘거래량’을 마이너스로 기록해주고,
  • 계정 목록 중에 ‘보내는 이’를 찾아서 ‘받는 이’와 ‘거래량’을 플러스로 기록해줍니다.
  • 마치 우리가 송금을 하면, 받는 사람 통장과 보내는 사람 통장에 둘 다 찍히는 것과 동일하군요.

check_balance()

함수 이름부터 어려워 보이는 check_balance()이 하나 남았습니다. 이 또한 거래정보를 인자로 받네요.

  • 거래정보에서 보내는 이가 ‘0’님이라면 무조건 통과입니다. 이 ‘0’이 GENESIS_ACCOUNT_ID 이네요. 마치 미국에서 달러는 막 찍어내는 것과 동일한 느낌입니다.
  • 거래정보에서 보내는 이가 관리하고 있는 계정에 없다면 FALSE를 반환합니다. 신이 아닌 이상 정체모를 자금이 유입되버리면 경제가 무너지겠죠?
  • 만약 관리되고 있는 계정이라면 해당 계정의 잔고에 송금할 금액이 남아있는 지 보는 군요.

정리하면 다음과 같습니다.

거래정보가 올 때 신(GENESIS_ACCOUNT_ID) 계정이면 무한 남발 가능하나 존재하고 있지 않는 계정이거나 그 계정에 송금할 금액보다 잔고가 적을 경우 거래가 되지 않는다.

블록체인(blockchain.py) 알아보기

드디어 끝판왕이군요. 노드(node.py)가 남았지만 쉬지않는 당신을 위해 노드는 다음 편에서 볼 예정입니다. 블록체이는 함수가 많아서 하나씩 살펴보겠습니다.

init()와 init_chain()

import gcoin.proof
from gcoin.book import Book
from gcoin.block import Block

class BlockChain:
    def __init__(self, chain=None):
        """init chain with existing chain
        or make this new blockchain

        Args:
            chain: list of dictionary of Block, see load_chain
        """
        self.chain = []
        self.book = Book()
        self.transactions = []

        if chain:
            self.load_chain(chain)
        else:
            self.init_chain()
            
    def init_chain(self):
        """Init genesis chain"""
        block = Block([])
        self.add_block(block)            

블록체인은 체인(chain), 통장(book), 거래들(transactions)으로 구성되어 있네요.

  • self.chain = [] : 제가 알고있는 체인이라곤 자건거 체인 밖에 모르지만, 그냥 그거라도 생각해도 무방할 듯 합니다. 블록 정보를 죽 연결해놓은 것 같습니다.
  • self.book = Book() : 통장은 블록체인에 하나만 존재하는가 봅니다.
  • self.transactions = [] : 거래는 계속 일어날 것이고, 그 거래를 리스트로 관리하려나 봅니다.

초기화할 때 체인(chain)이 인자로 넘어오는 경우에는 이 체인의 정보를 읽어들어 체인을 구성합니다. 만약 아무 인자도 없다면 빈 블록을 하나 만들어서 추가합니다. ‘block = Block([])’을 조금 더 살펴보면, 블록의 생성함수가 다음과 같이 정의되어 있으므로 빈 거래 정보를 담고 있는 빈 블록이 생기겠죠?

def __init__(self, transactions, proof=0,
             previous_hash=None, timestamp=0):

정리하면 다음과 같습니다.

기존에 있는 체인을 받을 경우 이 체인 정보로 블록체인을 초기화하고 그렇지 않은 경우 빈 블록하나로 새로운 체인을 하나 만들어낸다.

add_transaction()

    def add_transaction(self, transaction):
        """Add new transaction
        It will only add amount

        Args:
            transaction (obj): Transaction object

        Returns:
            int: index of next block of chain
                return -1 if it's not correct transaction
        """
        if self.book.check_balance(transaction):
            self.transactions.append(transaction)
            return len(self.chain) + 1
        else:
            raise Exception('Transaction is wrong.')

우리가 잘 아는 거래(transaction)가 나왔네요. 거래는 많이 살펴봤으니 퍼즐을 맞춰보죠.

  • 먼저 거래가 오면 통장(book)에서 유효한지 검사합니다. 여기서 질문하나 거래에서 보내는 이가 ‘신’이라면 검사할까요?
  • 유효한 거래면 거래정보 리스트에 하나 추가하고 1이 증가된 체인길이를 반환하네요.
  • 유효하지 않는 거래면 메시지를 띄웁니다.

new_block()과 add_block()

    def new_block(self, proof):
        last_block = self.chain[-1]

        block = Block(self.transactions, proof,
                      previous_hash=last_block.hash())

        self.add_block(block)

        self.transactions = []

        return block
    
    def add_block(self, block):
        self.chain.append(block)
        self.book.apply(block.transactions)

new_block() 함수가 어디서 호출되었는 지 기억나시나요? 스크롤을 올려봅시다. 퍼즐을 맞추는 단계라 스크롤을 많이 움직이어야 할 것 같습니다.

‘드르륵 드르륵’

맞습니다. ‘채굴자(miner.py)’의 call()함수에서 호출됩니다. 채굴자만이 블록을 만들 수 있나 봅니다. 그럼 또 질문입니다. 채굴자는 proof 인자로 무엇을 넘길까요?

‘드르륵 드르륵’

맞습니다. 증명검증을 완료한 후 찾은 신규 proof값이 넘어오네요. 갑자기 의문이 듭니다. 이 proof 값도 계속 증가되지는 못할텐데 중복되먼 어떻하죠? 일단 넘어가겠습니다.

last_block = self.chain[-1]

체인의 마지막에 있는 블록(last_block)을 꺼냅니다.

block = Block(self.transactions, proof, previous_hash=last_block.hash())

블록을 하나 생성할 때, 가지고 있는 거래정보들과 넘겨받은 신규 proof, 가지고 온 마지막 블록의 해쉬값을 이전 해쉬로 넘기네요. 엇? self.transactions을 넘긴다구요? 블록체인에 수많은 거래가 있을텐데 이걸 블록 하나 만들때마다 넘긴다구요??? 일단 이 놀람은 뒤로 하고 계속 보겠습니다.

self.add_block(block)

가지고 있는 체인 끝에 하나 추가한 후, 통장(book)에 블록이 가지고 있는 거래정보를 기입합니다. 이 때 통장에선 어떤일이 일어났죠?

‘드르륵 드르륵’

맞습니다. 통장에선 넘겨받은 거래정보를 이용해서 보내는 이, 받는 이의 계정을 찾아서 기록합니다. 어어? 그럼 블록으로 넘기지 않은 거래정보들은 통장에 기록되지 않는 것 같습니다.

self.transactions = []

오~~ 거래정보 초기화? 먼가 번쩍합니다. 여러분도 눈치채셨나요? ‘유레카!!’ 여기서 중요한 개념이 나올 것 같습니다. 아직 저는 블록체인을 1도 모르는 상황이기에 코드로만 유추해보겠습니다.

여기까지 눈에 보이는 과정은 다음과 같습니다.

  1. 블록체인에서 거래정보가 들어오면 (add_transaction) 블록체인의 거래정리 리스트(self.transactions)에 추가합니다.
  2. 거래정보가 계속 들어오면 계속 추가하겠죠?
  3. 추가할 때 통장 잔고를 보면서 유효한 지 체크합니다.
  4. 이를 계속 반복합니다.

그러다 채굴자(minar.py)가 call() 함수에 의해 어렵게 어렵게 증명찾기에 성공하는 순간 다음과 같은 일이 벌어집니다. call()이란 함수는 어딘가에서 계속 호출되는가 봅니다.

  1. 증명찾기에 성공하면 새로운 증명값을 받습니다.
  2. 이 증명값으로 새로운 블록을 하나 만들고 지금까지 거래정보 리스트를 넘깁니다. 이 때 체인의 마지막 블록의 해쉬 정보도 같이 넘기네요.
  3. 블록체인에 새로 만든 블록을 추가합니다.
  4. 그 다음에는 통장에 거래정보들을 기록하네요. 지금까지는 거래정보 리스트를 블록체인에서 임시적으로만 가지고 있있으며 아직 통장엔 기록되지 않았으니 아직 사용할 수는 없었고, 이 시점부터 통장에 기록되니 사용이 가능합니다. (골빈해커님이 채굴해야 거래가 기록되요~라는 말이 이제야 이해됩니다.)
  5. 그리고는 체인에서 가지고 있는 거래정보는 초기화합니다.

자 몇가지 정리해볼까요?

  • 블록체인에서 이루어지는 거래정보들은 잠시 블록체인에서 가지고 있는다.
  • 이 거래정보는 임시적인 것으로 아직 통장에 기록되지 않았기 때문에 계정이 반영되지는 않는다.
  • 새로운 블록이 채굴되면 그 블록에 거래정보를 저장한다.
  • 새 블록 생성 시에 체인의 마지막 블록의 해쉽값을 넘겨서 연결고리를 만들어준다.
  • 새 블록 생성 조건은 채굴자는 새로운 증명찾기를 계속 시도하다가 어렵게 증명찾기에 성공할 때다.
  • 그 때 채굴자는 보상을 받는다.

아마도 이 보상을 코인이라고 부르는 것 같습니다. 지금까지 블록이 코인인 줄 알았는데, 전혀 상관이 없네요. 코인은 순수하게 채굴에 대한 보상일 뿐이고, 블록은 거래정보들을 담고 있지만 블록끼리 연결고리 정보도 담고 있는 정도로 생각하시면 될 것 같습니다. 또 의문이 하나 생기네요. 채굴만 계속하고 거래가 이뤄지지 않으면 블록들만 생길텐데, 이 블록들에는 거래정보가 없겠네요?

블록은 채굴자만이 만들 수 있고, 코인은 채굴에 성공한 채굴자에게만 보상으로 지급된다. 단 '신'은 코인을 남발할 수 있다.

valid()

    def valid(self):
        """Valid chain"""
        index = 1

        while index < len(self):
            prev_block = self.chain[index-1]
            curr_block = self.chain[index]

            # Check hash with previous hash
            if curr_block.previous_hash != prev_block.hash():
                return False

            # Check proof with previous proof
            if not gcoin.proof.valid_proof(prev_block.proof,
                                           curr_block.proof):
                return False

            index += 1

        return True

이 함수는 어디서 호출되는 지는 모르겠지만, 검증을 해보는 것 같습니다. 두가지를 검증하는데요,

  1. 가지고 있는 블록을 죽~ 돌면서 현재 블록에서 가지고 있는 이전 블록의 해쉬정보와 블록체인에서 가지고 있는 이전 블록의 해쉬가 일치하는 지 확인합니다.
  2. 이전 블록의 검증값과 현재 블록의 검증값으로 증명검증을 해보네요. 증명검증은 증명찾기 함수에서 호출되는 그 함수입니다. (‘드르륵 드르륵’)

어랏? 여기서 의문입니다. 지금까지 알고있는 증명검증은 난이도에 따라 조건에 맞는 해쉬값을 찾는 과정이 있는데요. 난이도가 변경되지 않는다면 별 문제가 되지 않겠지만, 만약 중간에 난이도가 바뀐다면 이 조건에 의해서 모두 검증이 실패날 것 같네요. 어렴풋이 블록체인은 난이도가 점점 어려워진다는 얘기를 들은 기억이 있어 걱정이 앞서네요. 의문은 골빈해커님한테 물어보기로 하고 일단 넘어가죠.

load_chain()

    def load_chain(self, chain):
        """load chain from list of dictionary
        from existing blockchain

        Args:
            chain (list):
                [{
                    transactions: [{
                        sender: 'dsf9s9f0ad'
                        recipient: 'dfsad90fasf'
                        amount: 12
                    }]
                    proof: 318832940000
                    previous_hash: 'fj9afje9ajf9sef0s0f'
                    timestamp: 1506057125.900785
                }]
        """
        for block in chain:
            block = Block.init_from_json(block)
            self.add_block(block)

앞서 호출되었던 체인 로딩하는 함수입니다. 주어진 체인 정보로 현재 블록체인에서 블록을 생성하여 체인을 재구성합니다.

기타

    def last_block(self):
        return self.chain[-1]

    def dump(self):
        return [block.dump() for block in self.chain]

    def __len__(self):
        return len(self.chain)

마지막 블록을 가지고 온다거나 블록체인을 덤프뜨거나 체인의 길이를 반환하는 함수들입니다. 코어 쪽은 노드(node.py) 하나가 남았고 실제 어플리케이션(app.py) 소스코드 분석이 남았네요.


요약

몇가지 의문들이 남아있지만 구글검색과 소스코드 원제작자인 골빈해커님이 해결해주실 것이라 믿고 이만 정리해보겠습니다. 블록체인의 개념을 전혀 모른 상태에서 따라해보는 것도 나쁘지 않네요. 주어들은 얘기들과 소스코드와 얇팍한 추론으로 퍼즐을 맞추는 즐거움이랄까요? 그래도 많은 수확이 있었던 것 같습니다.

  • 블록체인은 블록이 연결된 리스트를 얘기한다.
  • 채굴자만이 새 블록을 만들 수 있다. 난이도가 높을 수록 만들기 힘들다.
  • 새 블록을 만드면 보상(코인)을 받는다.
  • 새 블록에 그간 블록체인에서 이루어진 거래정보들을 저장한다.
  • 모든 코인 거래는 유효한지 검사된다.
  • 블록체인 자체도 유효한지 검사된다. 두 가지로 말이죠 (‘드르륵 드르륵’)
  • ‘신’은 코인을 막 만들 수 있다.

정리하면서 또 의문이 하나 드네요. 이 코인을 돈으로 어떻게 사죠? 채굴자는 보상으로 얻는다고 치고, 채굴자한테 돈을 주면 채굴자에게 코인을 받는 식이 되겠죠?

비행기에서 한 숨도 못 잤지만 너무 즐거운 시간을 보낸 것 같습니다. 골빈해커님 감사합니다~


같이 보기