안녕하세요.
저는 공주대학교 컴퓨터공학부 컴퓨터소프트웨어전공 17학번 문승현입니다.
현재 블록체인 기술에 대해 공부를 하여 공부한 내용을 정리하고자 게시글을 작성하였습니다.
미리 읽어주시는 분들께 감사하며 잘못된 지식이 있을 경우 마음 편하게 피드백주시면 감사하겠습니다.
먼저 제가 깃허브를 돌아다니다 괜찮은 자료를 발견하여 분석을 시작한 코인의 깃허브 주소입니다.
https://github.com/golbin/g-coin
먼저 다운받게 되면 위와 같이 파일들이 있습니다.
golbin
- app.py
- gcoin
- blockchain.py
- block.py
- book.py
- config.py
- miner.py
- node.py
- proof.py
- __pycache__
- requirements.txt
- transaction.py
패키지
requirements.txt를 열어보게 되면 패키지의 버전은 아래와 같음을 알 수 있습니다.
Flask==0.12.2
requests==2.18.4
그리고 깃허브를 보면 파이썬의 버전은 3.6 이상의 버전임을 알 수 있습니다.
정리하자면 아래와 같습니다.
Python >= 3.6
Flask >= 0.12
Requests >= 2.18
패키지 설치
pip3 install -r ./requirements.txt
분석
app.py
hubeen@SeunghyunSeverx64 >>> ~/study/block/golbin >>> cat app.py
from argparse import ArgumentParser
from flask import Flask, jsonify, request
from gcoin.node import Node
from gcoin.miner import Miner
from gcoin.blockchain import BlockChain
from gcoin.transaction import Transaction
app = Flask('g-coin')
app.node = Node()
app.miner = Miner(app.node.id)
app.blockchain = BlockChain()
@app.route('/transaction', methods=['POST'])
def add_transaction():
# data.sender(str)
# data.recipient(str)
# data.amount(int)
data = request.get_json()
transaction = Transaction.init_from_json(data)
try:
next_index = app.blockchain.add_transaction(transaction)
except Exception as e:
return jsonify({'message': str(e)}), 403
response = {'message': 'Transaction will be added to {next_index}th block.'}
return jsonify(response), 201
@app.route('/transaction', methods=['GET'])
def get_pending_transactions():
transactions = [t.dump() for t in app.blockchain.transactions]
return jsonify(transactions), 201
@app.route('/mine', methods=['POST'])
def mine():
"""Mining
Have to make a standalone process
But it's just a prototype
"""
block = app.miner(app.blockchain)
response = {
'message': "New block is mined!",
'block': block.dump()
}
return jsonify(response), 200
@app.route('/chain', methods=['GET'])
def get_full_chain():
response = {
'chain': app.blockchain.dump(),
'length': len(app.blockchain)
}
return jsonify(response), 200
@app.route('/node', methods=['GET'])
def get_all_nodes():
response = {
'nodes': list(app.node.neighbor),
'total': len(app.node)
}
return jsonify(response), 201
@app.route('/node', methods=['POST'])
def add_node():
# data.address(str)
data = request.get_json()
app.node.add(data['address'])
response = {
'message': 'New app.node is added.',
'total': len(app.node)
}
return jsonify(response), 201
@app.route('/chain/valid', methods=['GET'])
def valid_chain():
valid = app.blockchain.valid()
response = {'result': valid}
return jsonify(response), 200
@app.route('/consensus', methods=['POST'])
def consensus():
new_blockchain = app.node.consensus_with_neighbor(app.blockchain)
if new_blockchain:
app.blockchain = new_blockchain
response = {'message': 'Our chain was replaced.'}
else:
response = {'message': 'I\'m King of the World.'}
return jsonify(response), 200
if __name__ == '__main__':
parser = ArgumentParser()
parser.add_argument('--port', default=5000, type=int)
args = parser.parse_args()
app.run(host='0.0.0.0',port=args.port)
보게 되면 --port 로 받은 인자를 int 형으로 플라스크의 포트로 설정하는 것을 볼 수 있습니다.
네, app.py는 그냥 플라스크를 실행하는 것을 알 수 있습니다.
아, 참 깃허브에서 받은 코드가 다른 부분이 있습니다.
app.run(host='0.0.0.0',port=args.port)
바로 이 부분인데요.
아마 host='0.0.0.0'이 없으실겁니다.
저는 외부 접속을 허용하기 위해 이 부분에 코드를 추가하였습니다.
config.py
# for the first chain
GENESIS_HASH = 'g'
# for mining rewards
GENESIS_ACCOUNT_ID = '0'
AMOUNT_OF_REWARD = 1
# for proof of work
DIFFICULTY = 1 # number of digits is difficulty
PROOF_DIGITS = '0' # Finding PROOF_DIGITS * DIFFICULTY ('00', '000', ..) is proof of work
config.py부터 보도록 하겠습니다.
GENESIS_HASH = 'g'
첫번째의 체인의 해쉬를 설정하는 부분입니다.
GENESIS_ACCOUNT_ID = '0'
AMOUNT_OF_REWARD = 1
채굴을 할 때에 나누어줄 갯수와 초기의 계정의 아이디를 설정하는 부분입니다.
쉽게 말하면 '신', '관리자'의 계정을 설정하는 부분입니다.
DIFFICULTY = 1 # number of digits is difficulty
PROOF_DIGITS = '0' # Finding PROOF_DIGITS * DIFFICULTY ('00', '000', ..) is proof of work
채굴에 대해 난이도를 설정하는 부분입니다.
현재의 코드는 난이도가 1로 설정이 되어있어 PROOF_DIGITS * 1로 '0' 되어있습니다.
이 난이도가 2로 설정이 되면 PROOF_DIGITS * 2가 되어 '00' 으로 동작한다는 것을 주석을 통해 알 수 있었습니다.
block.py
import json
import hashlib
from time import time
import gcoin.config as cfg
from gcoin.transaction import Transaction
class BlockHeader:
def __init__(self, previous_hash=None, timestamp=0,
difficulty=0, proof=0):
"""Block
Args:
previous_hash (str):
timestamp (float):
difficulty (int):
proof (int):
"""
self.previous_hash = previous_hash
self.timestamp = timestamp if timestamp else time()
self.difficulty = difficulty if difficulty else cfg.DIFFICULTY
self.proof = proof
def hash(self):
"""Make hash of a header of current block for finding proof
Don't use 'proof' because there is no 'proof' in new block at the first time.
"""
header_dump = self.dump()
header_dump.pop('proof', None)
header_dump = json.dumps(header_dump, sort_keys=True).encode()
header_hash = hashlib.sha256(header_dump).hexdigest()
return header_hash
def dump(self, proof=True):
data = {
'previous_hash': self.previous_hash,
'timestamp': self.timestamp,
'difficulty': self.difficulty
}
if proof:
data['proof'] = self.proof
return data
@classmethod
def init_from_json(cls, data):
return cls(data['previous_hash'],
data['timestamp'],
data['difficulty'],
data['proof'])
class Block:
def __init__(self, transactions, previous_hash=None):
"""Block
Args:
transactions (list): list of Transaction object
previous_hash (str):
"""
self.transactions = transactions
self.header = BlockHeader(previous_hash)
@property
def previous_hash(self):
return self.header.previous_hash
@property
def difficulty(self):
return self.header.difficulty
@property
def proof(self):
return self.header.proof
@proof.setter
def proof(self, value):
self.header.proof = value
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 {
'header': self.header.dump(),
'transactions': [t.dump() for t in self.transactions]
}
@classmethod
def init_from_json(cls, data):
transactions = [Transaction.init_from_json(t)
for t in data['transactions']]
header = BlockHeader.init_from_json(data['header'])
self = cls(transactions)
self.header = header
return self
블록의 구조를 볼 수 있는 코드입니다.
transactions : 거래 정보들
previous_hash : 이전 해쉬
proof : 보상?
timestamp : 블록이 생성된 시간
hash : 자기 블록의 해쉬
difficulty : 어려움?
import gcoin.config as cfg
일단 아까 저희가 처음에 본 config.py를 임포트하는 것을 볼 수 있습니다.
self.difficulty = difficulty if difficulty else cfg.DIFFICULTY
난이도를 설정한 것을 불러와 대입을 하는 것을 볼 수 있습니다.
class BlockHeader:
def __init__(self, previous_hash=None, timestamp=0,
difficulty=0, proof=0):
"""Block
Args:
previous_hash (str):
timestamp (float):
difficulty (int):
proof (int):
"""
self.previous_hash = previous_hash
self.timestamp = timestamp if timestamp else time()
self.difficulty = difficulty if difficulty else cfg.DIFFICULTY
self.proof = proof
def hash(self):
"""Make hash of a header of current block for finding proof
Don't use 'proof' because there is no 'proof' in new block at the first time.
"""
header_dump = self.dump()
header_dump.pop('proof', None)
header_dump = json.dumps(header_dump, sort_keys=True).encode()
header_hash = hashlib.sha256(header_dump).hexdigest()
return header_hash
def dump(self, proof=True):
data = {
'previous_hash': self.previous_hash,
'timestamp': self.timestamp,
'difficulty': self.difficulty
}
if proof:
data['proof'] = self.proof
return data
@classmethod
def init_from_json(cls, data):
return cls(data['previous_hash'],
data['timestamp'],
data['difficulty'],
data['proof'])
인자는 4개로 previous_hash, timestamp, difficulty, proof가 있습니다.
previous_hash : string 타입으로 이전 블록의 해쉬 값입니다.
timestamp : float 타입으로 블록의 생성 시간입니다.
difficulty : int 타입으로 난이도 결정의 값입니다.
proof : int 타입으로 아직까지 무슨 역할을 하는 아이인진 모르겠네요.
인자를 받으며 에러를 위해 초기 값들은 None, 0, 0, 0으로 설정하는 것을 볼 수 있습니다.
현재 자기 자신의 값에 받은 값을 대입하는 것을 볼 수 있습니다.
hash()
header_dump = self.dump()
header_dump.pop('proof', None)
header_dump = json.dumps(header_dump, sort_keys=True).encode()
header_hash = hashlib.sha256(header_dump).hexdigest()
블록 정보를 덤프를 뜨며 덤프를 뜬 내용을 json형식으로 만든 뒤 대입을 하는 것을 볼 수 있습니다.
그 뒤에 블록 해쉬에 sha256 알고리즘으로 암호화하여 hex형식으로 대입을 하네요.
init_from_json()
def init_from_json(cls, data):
return cls(data['previous_hash'],
data['timestamp'],
data['difficulty'],
data['proof'])
JSON 데이터를 받아 블록정보를 초기화합니다.
dump()
def dump(self, proof=True):
data = {
'previous_hash': self.previous_hash,
'timestamp': self.timestamp,
'difficulty': self.difficulty
}
if proof:
data['proof'] = self.proof
return data
블록의 정보를 반환해주는 함수입니다.
class Block:
def __init__(self, transactions, previous_hash=None):
"""Block
Args:
transactions (list): list of Transaction object
previous_hash (str):
"""
self.transactions = transactions
self.header = BlockHeader(previous_hash)
@property
def previous_hash(self):
return self.header.previous_hash
@property
def difficulty(self):
return self.header.difficulty
@property
def proof(self):
return self.header.proof
@proof.setter
def proof(self, value):
self.header.proof = value
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 {
'header': self.header.dump(),
'transactions': [t.dump() for t in self.transactions]
}
@classmethod
def init_from_json(cls, data):
transactions = [Transaction.init_from_json(t)
for t in data['transactions']]
header = BlockHeader.init_from_json(data['header'])
self = cls(transactions)
self.header = header
return self
transactions : list 타입으로 거래 정보들이 들어있는 곳입니다.
previous_hash : string 타입으로 이전 해쉬 값이 들어있는 곳입니다.
그리고 함수들의 역할은 위에서 설명한 함수들과 같은 역할을 하는 아이들이라 딱히 큰 설명은 없이 넘어가도 될 것 같습니다.
transaction.py
class Transaction:
def __init__(self, sender, recipient, amount):
"""Transaction
Args:
sender (str):
recipient (str):
amount (int): positive number
"""
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):
return cls(data['sender'],
data['recipient'],
data['amount'])
파일 명을 보고 딱 알아차릴 수 있겠네요!
거래정보를 관련하는 아이일 것이라 예상할 수 있었습니다.
인자는 3개로 sender, recipient, amount 입니다.
sender : string 타입으로 보내는 이의 정보가 담기는 변수입니다.
recipient : string 타입으로 받는 이의 정보가 담기는 변수입니다.
amount : int 타입으로 보낼 갯수가 담기는 변수입니다.
if amount < 1:
raise Exception('Amount have to be positive number.')
amount가 1보다 작을 수 없습니다.
그야 당연하죠 !
0개를 보낼 순 없잖아요? (웃음)
dump()
def dump(self):
return {
'sender': self.sender,
'recipient': self.recipient,
'amount': self.amount
}
거래 정보를 반환해주는 함수입니다.
init_from_json()
def init_from_json(cls, data):
return cls(data['sender'],
data['recipient'],
data['amount'])
json 값을 초기화해주는 함수입니다.
book.py
"""account book"""
import gcoin.config as cfg
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)
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 == cfg.GENESIS_ACCOUNT_ID: # 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)
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()
말 그대로 갯수를 더해주는 함수입니다.
갯수를 -, +로 빼고 더하고를 할 수 있겠네요.
add()
타겟에 타겟을 추가하고 갯수에 갯수를 추가합니다.
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 == cfg.GENESIS_ACCOUNT_ID: # 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)
현재 계정을 초기화하네요.
dicinary 타입으로 계정마다 갯수를 정하게 하는 것 같습니다.
check_balance()
transaction : 거래 내용
리턴 타입은 True, False 라고 합니다.
if transaction.sender == cfg.GENESIS_ACCOUNT_ID: # for mining rewards
return True
만약 보내는 이가 '신', '관리자' 계정이면 무조건 True가 되네요.
이에 대한 코드는 보안에 대한 취약점이 되지 않을까 싶습니다.
if transaction.sender in self.account:
account = self.account[transaction.sender]
return account.sum() - transaction.amount >= 0
else:
return False
거래 내용에 보내는 이가 계정 안에 있으면 계정에 대한 갯수를 보내거나 지불하는 것을 볼 수 있으며 계정 안에 없을 경우에는 False를 리턴하는 것을 볼 수 있습니다.
get_account()
account_id : 계정의 아이디입니다.
account이 없을 경우 계정을 생성하네요.
그 외에는 그 계정을 반환해주는 함수입니다.
apply()
transactions : 거래 기록들을 담는 변수입니다.
거래 기록들중에 계정 정보를 받아 보내는 이와 받는 이를 변수에 대입합니다.
그리고 보내는 이는 그 만큼의 코인을 - 하며 받는 이는 그 만큼의 코인을 더합니다.
남은 blockchain.py, miner.py, node.py, proof.py 는 다음 게시글에서 정리하도록 하겠습니다.
읽어주셔서 감사합니다.