이번에는 노드의 거래 내역과 채굴 결과 등 블록체인에 저장된 transaction을 투명하게 공개할 수 있는 블록 스캔 사이트인 pyBTC Block scan 페이지를 제작해보자.
1. 블록 스캔 사이트 만들기 (one_node_chainScan.ipynb)
1) 블록 스캔 사이트 Back-end 구축
from flask import Flask
from flask import render_template
import requests
import json
import os
import pandas as pd
app = Flask(__name__, template_folder=os.getcwd())
@app.route('/')
def index():
headers = {'Content-Type' : 'application/json; charset=utf-8'}
# 블록 체인 내 블록 정보를 제공하는 url(http://localhost:5000/chain) 에 request 방식으로 데이터를 요청
res = requests.get("http://localhost:5000/chain", headers=headers)
# 요청 결과 데이터(res.text)를 json으로 로드
status_json = json.loads(res.text)
# 결과 데이터를 pandas의 데이터프레임(df_scan)으로 정리
df_scan = pd.DataFrame(status_json['chain'])
# Front 구성 내용이 담길 html(one_node_scan.html) 파일에 데이터프레임 정보(df_scan)와 블록의 길이(block_len)를 제공
return render_template('/one_node_scan.html', df_scan = df_scan, block_len = len(df_scan))
app.run(port=8080)
블록 스캔 사이트를 제작하려면 블록체인의 블록 정보를 조회해야 한다.
이를 위해 운영 중인 노드의 API 주소에 request 방식으로 정보를 요청할 것 : flask, request, json 패키지 호출
여기에 front 역할을 할 html 파일의 디렉터리 지정을 위한 os 패키지도 호출한다.
해당 사이트에는 블록체인의 구성 요소에 대한 정보가 포함되어야 하며, 구성 요소 정보는 블록의 생성시간, previous_hash, nonce 값, 거래 내역으로 구성될 것임
이를 위해 블록체인의 블록 정보를 제공하는 URL(http://localhost:5000/chain)에 request의 GET 방식으로 데이터를 요청한 뒤 결과 데이터(res.text)를 json으로 로드한다.
이후 결과 데이터를 pandas의 dataframe(df_scan)으로 정리함
다음으로는 front 구성 내용이 담길 html(one_node_scan.html) 파일에 데이터프레임 정보(df_scan)와 블록의 길이(block_len)를 제공한다.
2) 블록 스캔 사이트 Front-end 구축
<h1><center> pyBTC Block Scan </center></h1>
<div>
<table border="1">
<tr>
<th>timestamp</th>
<th>previous_hash</th>
<th>nonce</th>
<th>transactions</th>
</tr>
{%for i in range(0, block_len)%}
<tr>
<td>{{df_scan.loc[i, 'timestamp']}}</td>
<td>{{df_scan.loc[i, 'previous_hash']}}</td>
<td>{{df_scan.loc[i, 'nonce']}}</td>
<td style="font-size:2px">{{df_scan.loc[i, 'transactions']}}</td>
</tr>
{%endfor%}
</table>
</div>
백엔드에서 제공된 정보들이 html 파일에서 렌더링 되면 이를 사용자가 보기 편하도록 front-end 작업이 진행되어야 한다.
따라서 위와 같이 백엔드에서 제공받은 데이터를 바탕으로 테이블을 생성해주고, 각 칸에 timestamp, previous_hash, nonce, transactions 값들이 입력되도록 하였음
실행해보면 위와 같이 pyBTC Block Scan 사이트에 접속되는 것을 볼 수 있다.
특히 거래 내역 컬럼에서는 test_from 에서 test_to, test_to2, test_to3로의 송금 내역 및 채굴 보상 지급 내역을 거래 시간과 함께 모두 확인할 수 있다.
2. Block Wallet 사이트 만들기 (one_node_Wallet.ipynb)
1) Block Wallet Back-end 구축
앞서 블록체인 노드를 운영하고, 노드에 여러 작업을 지시하며 해당 내역을 확인할 수 있는 블록 스캔 사이트 제작까지 마쳤으니, 이제는 계정별 pyBTC 잔액 조회와 송금 기능이 있는 지갑 사이트인 pyBTC Wallet 사이트를 제작해보자.
from flask import Flask
from datetime import datetime
from flask import render_template
from flask import request
from flask import url_for
from flask import redirect
import requests
import json
import os
import pandas as pd
# Flask app 선언
app = Flask(__name__, template_folder=os.getcwd())
# login 기능
@app.route('/', methods=['GET', 'POST'])
def login():
if request.method=='POST':
print("login 버튼을 누름")
input_value = request.form.to_dict(flat=False) ['wallet_id'][0]
print("login 지갑주소 : " , input_value)
### 기존 user 정보 확인
headers = {'Content-Type' : 'application/json; charset=utf-8'}
res = requests.get("http://localhost:5000/chain", headers=headers)
status_json = json.loads(res.text)
status_json['chain']
tx_amount_l = []
tx_sender_l = []
tx_reciv_l = []
tx_time_l = []
# 거래내역 정리 (df_tx)
for chain_index in range(len(status_json['chain'])):
chain_tx = status_json['chain'][chain_index]['transactions']
for each_tx in range(len(chain_tx)):
tx_amount_l.append(chain_tx[each_tx]['amount'])
tx_sender_l.append(chain_tx[each_tx]['sender'])
tx_reciv_l.append(chain_tx[each_tx]['recipient'])
tx_time_l.append(chain_tx[each_tx]['timestamp'])
df_tx = pd.DataFrame()
df_tx['timestamp'] = tx_time_l
df_tx['sender'] = tx_sender_l
df_tx['recipient'] = tx_reciv_l
df_tx['amount'] = tx_amount_l
df_tx
# pyBTC 잔고현황 정리 (df_status)
df_sended = pd.DataFrame(df_tx.groupby('sender')['amount'].sum()).reset_index()
df_sended.columns = ['user','sended_amount']
df_received= pd.DataFrame(df_tx.groupby('recipient')['amount'].sum()).reset_index()
df_received.columns = ['user','received_amount']
df_status = pd.merge(df_received,df_sended, on ='user', how= 'outer').fillna(0)
df_status['balance'] = df_status['received_amount'] - df_status['sended_amount']
df_status
# 결과값 랜더링
if (df_status['user']==input_value ).sum() == 1:
print("로그인성공")
return render_template("wallet.html", wallet_id = input_value,
wallet_value = df_status[df_status['user']== input_value]['balance'].iloc[0])
else:
return "잘못된 지갑 주소입니다!"
return render_template('login.html')
# 지갑 기능
@app.route('/wallet', methods=['GET', 'POST'])
def wallet():
if request.method=='POST':
send_value = int(request.form.to_dict(flat=False)['send_value'][0] )
send_target = request.form.to_dict(flat=False)['send_target'][0]
send_from = request.form.to_dict(flat=False)['send_from'][0]
print("Login Wallet ID : " ,send_from)
if send_value > 0:
print("Send Amout :", send_value)
## transaction 입력하기
headers = {'Content-Type' : 'application/json; charset=utf-8'}
data = {
"sender": send_from,
"recipient": send_target,
"amount": send_value,
}
requests.post("http://localhost:5000/transactions/new", headers=headers, data=json.dumps(data))
return "전송 완료!"
else:
return "0 pyBTC 이상 보내주세요!"
return render_template('wallet.html')
app.run(port=8081)
해당 지갑 사이트에서는 로그인, pyBTC 전송 등의 기능이 필요하기에 Flask 내의 url_for, redirect 모듈이 추가로 호출된다.
블록 스캔 사이트와 달리, 이 pyBTC Wallet은 로그인과 지갑, 2개의 화면으로 구성되어야 함
1. 로그인 페이지
- POST 방식이 아닌 단순 접속 방식으로 접근될 경우 login.html 페이지를 렌더링하여 로그인 페이지가 나타남
- 로그인 페이지에서 로그인 버튼을 클릭하면 POST 방식을 통하여 백엔드에 접속하게 되며, 이 때 POST 임을 감지하여 입력된 지갑 아이디가 input_value라는 변수에 저장된다.
- 다음으로는 블록체인의 블록 정보 조회 URL에 request의 GET 방식으로 접속하여 정보를 받아오며, 이후 pandas 작업을 통해 현 계정별 잔액을 조회하고 로그인 계정이 dataframe의 user 값과 동일할 경우 해당 계정의 잔고 값과 함께 로그인이 성공된 wallet.html 페이지로 렌더링해 줄 것이다.
2. 지갑 페이지
- 로그인이 성공되어 wallet.html 페이지가 렌더링될 경우 지갑 포기 페이지가 나타남
- 이 때 로그인한 사용자의 지갑 ID와 지갑 내 잔고 값을 리턴해준다.
- 이후 사용자가 보내고자 하는 USER ID와 보낼 금액을 입력한 뒤 보내기 버튼을 클릭하면 POST 방식을 통하여 백엔드의 wallet에 접속하게 됨
- 이 경우 POST 임을 감지한 뒤, 송금될 pyBTC의 금액과 송금 받을 지갑 아이디, 송금하는 지갑 아이디가 각각 send_value, send_target, send_from의 변수에 저장됨
- 보내는 금액이 정상적 (>0) 으로 확인된 후에, 블록체인의 pyBTC 송금 URL에 request의 POST 방식으로 송금 데이터를 업데이트해 줄 것이며 "송금 완료" 메시지를 띄우게 된다.
2) Block Wallet Front-end 구축
1. login.html
<h1><center>pyBTC Block Chain Network Wallet</center></h1>
<form action="/" method="POST">
<div class="form-group">
<input type="text" name="wallet_id" placeholder="지갑ID를 입력해주세요!" size="80">
</div>
<div class="form-group">
<input type="submit" value="로그인"/>
</div>
</form>
- login 화면에서 test_from이라는 지갑 ID로 로그인하면 백엔드에서 로그인한 지갑 주소 및 성공 로그를 확인할 수 있음
2. wallet.html
<h1> <center> pyBTC Block Chain Wallet</center></h1>
<h3>내 지갑 주소 : {{wallet_id}}</h3>
<br>
<h3>내 지갑 잔액 : {{wallet_value}}pyBTC</h3>
<form action="wallet" method="POST">
<div class="form-group">
<input type="text" name="send_from" value= '{{wallet_id}}' size="80" readonly>
</div>
<div class="form-group">
<input type="text" name="send_target" placeholder="보낼 지갑 주소를 입력해주세요!" size="80">
</div>
<div class="form-group">
<input type="text" name="send_value" placeholder="보낼 pyBTC 수량을 입력해주세요!" size="30">
</div>
<div class="form-group">
<input type="submit" value="보내기" />
</div>
</form>
- 보내기 버튼을 클릭하면 POST 액션으로 사용자가 입력한 받는 지갑 주소, pyBTC 금액에 백엔드로 전송될 수 있도록 from 형식으로 구성
- 이때 보내는 사람의 지갑 주소는 미리 백엔드에서 리턴되어 수정되면 안되기에 read-only 형식이어야 함
- 이제 여기서 test_to4라는 주소로 3개의 pyBTC를 보내보자.
이후 거래 내역이 블록에 저장되도록 앞서 작성했던 채굴 명령을 진행한 뒤 확인해보면 다음과 같이 지갑 사이트 및 블록 스캔 사이트에 거래 내역이 저장된 것을 확인할 수 있다!
3. 여러 개의 노드 연결하기
지금까지 생성한 파이썬 기반의 블록체인 네트워크는 분산된 데이터 저장인 '탈중앙화' 요소가 포함되어 있지 않다.
현재까지 구축된 블록체인은 one_node.ipynb 하나의 노드로 운영되기에 데이터가 분산되었다고 할 수 없기 때문
즉, 하나의 서버에 데이터가 저장된 중앙화된 방식으로 운영되고 있는 것이다.
현재의 상태에서는 one_node.ipynb의 데이터만 알맞게 수정된다면 과거의 거래 내역이 수정될 수 있고 이에 따라 블록체인 네트워크의 ‘변경 및 삭제가 불가능’하다는 장점이 적용될 수 없다.
이번에는 위와 같이 탈중앙화된 pyBTC 블록체인 네트워크를 위하여 노드를 추가할 것이다.
따라서 여러 노드가 채굴 보상을 위해 경쟁하며, 알맞은 nonce값을 찾아 블록을 완성하게 될 경우 주변의 노드와 소통하여 블록 생성을 알리고, 주변의 노드들은 해당 nonce 값이 알맞은 값인지를 검증하는 과정을 반복할 것이다.
이를 통하여 탈중앙화된 블록체인 네트워크를 완성해 보자.
1) 여러 노드 운영을 위한 추가 사항 (node_network_1.ipynb)
import hashlib
import json
from time import time
import random
import requests
from flask import Flask, request, jsonify
from urllib.parse import urlparse
# 블록체인 객체 선언
class Blockchain(object):
def __init__(self):
self.chain = [] # 블록을 연결하는 체인
self.current_transaction = [] # 블록 내에 기록되는 거래 내역 리스트
self.nodes = set() # 블록체인을 운영하는 노드들의 정보
self.new_block(previous_hash=1, proof=100) # 블록체인 첫 생성 시 자동으로 첫 블록을 생성하는 코드
@staticmethod
def hash(block):
block_string = json.dumps(block, sort_keys = True).encode()
return hashlib.sha256(block_string).hexdigest()
@property
def last_block(self):
return self.chain[-1]
@staticmethod
def valid_proof(last_proof, proof):
guess = str(last_proof + proof).encode()
guess_hash = hashlib.sha256(guess).hexdigest()
return guess_hash[:4] == "0000"
def pow(self, last_proof):
proof = random.randint(-1000000, 1000000)
while self.valid_proof(last_proof, proof) is False:
proof = random.randint(-1000000, 1000000)
return proof
def new_transaction(self, sender, recipient, amount):
self.current_transaction.append(
{
'sender' : sender, # 송신자
'recipient' : recipient, # 수신자
'amount' : amount, # 금액
'timestamp' : time()
}
)
return self.last_block['index'] + 1
def new_block(self, proof, previous_hash = None):
block = {
'index' : len(self.chain) + 1,
'timestamp' : time(),
'transactions' : self.current_transaction,
'nonce' : proof,
'previous_hash' : previous_hash or self.hash(self.chain[-1]),
}
self.current_transaction = []
self.chain.append(block)
return block
def valid_chain(self, chain):
last_block = chain[0]
current_index = 1
while current_index < len(chain):
block = chain[current_index]
print('%s' % last_block)
print('%s' % block)
print("\n--------\n")
if block['previous_hash'] != self.hash(last_block):
return False
last_block = block
current_index += 1
return True
### 1. 블록 객체 내용 추가
# 1-1. 노드 등록
def register_node(self, address):
parsed_url = urlparse(address)
self.nodes.add(parsed_url.netloc)
# 1-2. 노드의 블록 유효성 검증 (다른 블록과 비교하며 업데이트)
def resolve_conflicts(self):
neighbours = self .nodes # 구동되는 노드들을 저장
new_block = None
max_length = len(self.chain) # 내 블록의 길이 저장
for node in neighbours:
node_url = "http://" + str(node.replace("0.0.0.0","localhost")) + '/chain' # url을 받아서 request 통해 체인 정보 저장
response = requests.get(node_url)
if response.status_code == 200: # 웹페이지와 정상적으로 교류가 되면 그 정보 저장
length = response.json()['length']
chain = response.json()['chain']
## 다른 노드의 길이(length)가 내 노드의 길이(max_length)보다 길고 and 내 체인이 유효한 경우
if length > max_length and self.valid_chain(chain): # 간 체인을 비교 » 제일 간 블록이 인정된다
max_length = length
## 기존 노드의 정보보다 받은 정보가 최신이다. 전송받은 블록 정보를 new_block에 넣는다
new_block = chain
## 다른 노드의 길이(length)가 내 노드의 길이(max_length)보다 짧거나 내 체인이 유효하지 않은 경우
else:
1==1 # 별도 작업 불필요
if new_block != None:
self.chain = new_block # 기존 블록 정보가 잘못된 것을 인정하고 검증된 블록 정보로 바꾼다.
return True
return False
### 2. 블록 운영 함수의 변화
# 2-1. 노드 기본값 설정
blockchain = Blockchain()
my_ip = '127.0.0.1'
my_port = '5000' #혹은 '5001’ or '5002’
node_identifier = 'node_'+my_port
mine_owner = 'master'
mine_profit = 0.1
app = Flask(__name__)
@app.route('/', methods=['GET'])
def index():
return "Welcome to the Blockchain API. Use /chain to view the chain, /mine to mine a block, or /transactions/new to create a transaction.", 200
# 블록 정보 호출
@app.route('/chain', methods=['GET'])
def full_chain():
print("chain info requested!")
response = {
'chain' : blockchain.chain,
'length' : len(blockchain.chain),
}
return jsonify(response), 200
# 신규 거래 추가
@app.route('/transactions/new', methods=['POST'])
def new_transaction():
values = request.get_json()
print("transactions_new!!! : ", values)
required = ['sender', 'recipient', 'amount']
if not all(k in values for k in required):
return 'missing values', 400
index = blockchain.new_transaction(values['sender'], values['recipient'], values['amount'])
response = {'message' : 'Transaction will be added to Block {%s}' % index}
# 2-3. 거래 내역 추가 함수에 일부 내용 추가
## 본 노드에 받은 거래 내역 정보를 다른 노드들에 다같이 업데이트해 준다.
if "type" not in values: # 신규로 추가된 경우 type 이라는 정보가 포함되어 있지 않다.
for node in blockchain.nodes: # nodes에 저장된 모든 노드에 정보를 전달한다.
headers = {'Content-Type' : 'application/json; charset=utf-8'}
data = {
"sender": values['sender'],
"recipient": values['recipient'],
"amount": values['amount'],
"type" : "sharing" # 전파이기에 sharing이라는 typeO| 꼭 필요하다.
}
requests.post("http://" + node + "/transactions/new", headers=headers, data=json.dumps(data))
print("share transaction to » ","http://" + node )
return jsonify(response), 201
# 채굴
@app.route('/mine', methods = ['GET'])
def mine():
print("MINING STARTED")
last_block = blockchain.last_block
last_proof = last_block['nonce']
proof = blockchain.pow(last_proof)
blockchain.new_transaction(
sender = mine_owner,
recipient=node_identifier,
amount=mine_profit # coinbase transaction
)
previous_hash = blockchain.hash(last_block)
block = blockchain.new_block(proof, previous_hash)
print("MINING FINISHED")
### 노드 연결을 위해 추가
for node in blockchain.nodes: # nodes에 연결된 모든 노드에 작업 증명이 완료되었음을 전파
headers = {'Content=Type' : 'application/json; charset=utf-8'}
data = {
"miner_node" : 'http://' + my_ip + ":" + my_port,
'new_nonce' : blockchain.last_block['nonce']
}
alarm_res = requests.get("http://" + node + "/nodes/resolve", headers=headers, data = json.dumps(data))
if "ERROR" not in alarm_res.text:
# 정상 response
response = {
'message' : 'new block found',
'index' : block['index'],
'transactions' : block['transactions'],
'nonce' : block['nonce'],
'previous_hash' : block['previous_hash']
}
else:
block = blockchain.new_block(proof, previous_hash)
return jsonify(response), 200
### 2-2. 노드 연결을 위해 추가되는 함수 : 다른 Node 등록!
@app.route('/nodes/register', methods=['POST'])
def register_nodes():
values = request.get_json() # json 형태로 보내면 노드가 저장됨
print("register nodes !!! :", values)
registering_node = values.get('nodes')
if registering_node == None: # 요청된 node 값이 없다면!
return "Error: Please supply a valid list of nodes", 400
## 요청받은 노드가 이미 등록된 노드와 중복인지 검사
## 중복인 경우
if registering_node.split("//")[1] in blockchain.nodes:
print("Node already registered") # 이미 등록된 노드입니다.
response = {
'message' : 'Already Registered Node',
'total_nodes' : list(blockchain. nodes),
}
## 중복이 아니라면
else:
# 내 노드 리스트에 추가
blockchain.register_node(registering_node)
# # 이후 해당 노드에 내 정보 등록하기
headers = {'Content-Type' : 'application/json; charset=utf-8'}
data = {
"nodes": 'http://' + my_ip + ":" + my_port
}
print("MY NODE INFO " , 'http://' + my_ip + ":" + my_port)
requests.post(registering_node + "/nodes/register", headers=headers, data=json.dumps(data))
# 이후 주변 노드들에도 새로운 노드가 등장함을 전파
for add_node in blockchain.nodes:
if add_node != registering_node.split("//")[1]:
print('add_node : ', add_node)
## 노드 등록하기
headers = {'Content-Type' : 'application/json; charset=utf-8'}
data = {
"nodes": registering_node
}
requests.post('http://' + add_node + "/nodes/register", headers=headers, data=json.dumps(data))
response = {
'message' : 'New nodes have been added',
'total_nodes' : list(blockchain.nodes),
}
return jsonify(response), 201
## 타 노드에서 블록 생성 내용을 전파하였을 때 검증 작업을 진행한다.
@app.route('/nodes/resolve', methods=['GET'])
def resolve():
requester_node_info = request.get_json()
required = ['miner_node'] # 해당 데이터가 존재해야 함
# 데이터가 없으면 에러를 띄움
if not all(k in requester_node_info for k in required):
return 'missing values', 400
## 그전에 우선 previous에서 바뀐 것이 있는지 점검하자!!
my_previous_hash = blockchain.last_block['previous_hash']
my_previous_hash
last_proof = blockchain.last_block['nonce']
headers = {'Content-Type' : 'application/json; charset=utf-8'}
miner_chain_info = requests.get(requester_node_info['miner_node'] + "/chain", headers=headers)
# 초기 블럭은 과거 이력 변조 내역을 확인할 필요가 없다.
print("다른노드에서 요청이 온 블록, 검증 시작")
new_block_previous_hash = json.loads(miner_chain_info.text)['chain'][-2]['previous_hash']
# 내 노드의 전 해시와 새로 만든 노드의 전 해시가 같을 때!!! >> 정상
if my_previous_hash == new_block_previous_hash and \
hashlib.sha256(str(last_proof + int(requester_node_info['new_nonce'])).encode()).hexdigest()[:4] == "0000" :
# 정말 PoW의 조건을 만족시켰을까? 검증하기
print("다른노드에서 요청이 온 블록, 검증결과 정상!!!!!!")
replaced = blockchain.resolve_conflicts() # 결과값 : True Flase / True면 내 블록의 길이가 짧아 대체되어야 한다.
# 체인 변경 알림 메시지
if replaced == True:
## 내 체인이 깗아서 대체되어야 함
print("REPLACED length :",len(blockchain.chain))
response = {
'message' : 'Our chain was replaced >> ' + my_ip + my_port,
'new_chain' : blockchain.chain
}
else:
response = {
'message' : 'Our chain is authoritative',
'chain' : blockchain.chain
}
#아니면 무엇인가 과거 데이터가 바뀐 것이다!!
else:
print("다른 노드에서 요청이 온 블록, 검증결과 이상발생!!!!!!!!")
response = {
'message' : 'Our chain is authoritative» ' + my_ip + my_port,
'chain' : blockchain.chain
}
return jsonify(response), 200
if __name__ == '__main__':
app.run(host=my_ip, port=my_port)
이제 여러 개의 노드가 동시에 운영되며, 탈중앙화 블록체인 네트워크를 구성할 준비가 완료되었다.
'Blockchain' 카테고리의 다른 글
[Blockchain] 파이썬으로 만드는 비트코인 (PoW) - 노드 구축 및 운영해보기 (0) | 2024.09.29 |
---|---|
[Blockchain] 블록체인 네트워크 구축을 위한 준비 - SQLite, flask, JS (3) | 2024.09.21 |
[Blockchain] 블록체인이란 무엇일까? - 정의, 구성 요소, 암호 해시, LAYER (1) | 2024.09.20 |