區塊鏈現在有多火看看比特幣的價格也就知道了,不過作爲一個有逼格的程序員,我們不應只關注到幣的價值(爲啥我沒去買比特幣,T-T),而應該去關注技術的本質,這個號稱“第四次工業革命”的區塊鏈技術。不過很多人估計對這個技術不太瞭解,包括我自己。既然不懂不如自己動手擼一個,實踐出真知。在翻閱文章的時候剛好找到了這篇文章,下面讓我們自己動手搭建一個簡單的區塊鏈。
簡單的說,區塊鏈就是一個不可變、有序的鏈(chain)結構,鏈中保存着稱之爲塊(block)的記錄,這些記錄可以是交易,文件或是任意你想要的數據。其中重要的是它們通過哈希鏈接在一起。
如果你不懂哈希,可以查看一下這裏,不過作爲開發者應該都懂吧。。。
準備
這裏我們會以python實現一個區塊鏈,因此需要首先安裝python,這裏我是用的是python2.7版本,需要安裝flask以及Requests庫。
pip install Flask requests
同時還需要一個http客戶端,比如postman或curl。
1、創建區塊鏈
新建一個blockchain.py文件,我們這裏只使用這一個文件。
區塊鏈表示
我們首先創建一個Blockchain類,並且在構造函數中創建兩個空的list,一個用於儲存我們的區塊鏈,另一個用於儲存交易。
Blockchain類的定義如下:
class Blockchain(object):
def __init__(self):
self.chain = []
self.current_transactions = []
def new_block(self):
# 創建一個新的塊,並添加到鏈中
pass
def new_transaction(self):
# 添加一筆新的交易到transactions中
pass
@staticmethod
def hash(block):
# 對一個塊做hash
pass
@property
def last_block(self):】
# 返回鏈中的最後一個塊
pass
我們的Blockchain類用來管理鏈,它會存儲交易信息,以及一些添加新的塊到鏈中的輔助方法,讓我們開始實現這些方法。
塊(Block)長啥樣?
每個塊都包含如下屬性:索引(index),時間戳(Unix時間),交易列表(transactions),工作量證明(proof,稍後解釋)以及前一個塊的hash值,下面是一個區塊的例子:
block = {
'index': 1,
'timestamp': 1506057125.900785,
'transactions': [
{
'sender': "8527147fe1f5426f9dd545de4b27ee00",
'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f",
'amount': 5,
}
],
'proof': 324984774000,
'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}
現在,鏈的概念應該很清楚了--每個新的區塊都包含了上一個區塊的hash值。這很重要,因爲它保障了整個區塊鏈的不可變性:如果攻擊者毀壞了一個之前的塊,那麼後續所有的塊的hash值都將錯誤。沒有理解?停下來細想一下,這是區塊鏈背後的核心思想
添加交易到區塊
我們需要一個添加交易到區塊的地方,實現一下new_transaction方法:
class Blockchain(object):
...
def new_transaction(self, sender, recipient, amount):
"""
添加一筆新的交易到transactions中
:param sender: <str> 發送者地址
:param recipient: <str> 接收者地址
:param amount: <int> 數量
:return: <int> 包含該交易記錄的塊的索引
"""
self.current_transactions.append({
'sender': sender,
'recipient': recipient,
'amount': amount,
})
return self.last_block['index'] + 1
在new_transaction方法向列表中添加一個交易記錄後,它返回該記錄將被添加到的區塊的索引--下一個待挖掘的區塊。這在後面用戶提交交易記錄的時候會有用。
創建一個新的區塊
當我們的Blockchain實例化之後,我們需要爲它創建一個創世塊(第一個區塊,沒有前區塊,類似於鏈表的head),並且給該創世塊添加一個工作量證明(proof of work),工作量證明是挖礦的結果。我們後面在來討論挖礦。
除了在我們的構造函數中創建一個創世塊之外,我們也要實現new_block(), new_transaction() 和 hash()方法:
import hashlib
import json
from time import time
class Blockchain(object):
def __init__(self):
self.chain = []
self.current_transactions = []
# 創建創世塊
self.new_block(previous_hash=1, proof=100)
def new_block(self, proof, previous_hash=None):
"""
創建一個新的塊,並添加到鏈中
:param proof: <int> 證明
:param previous_hash: (Optional) <str> 前一個塊的hash值
:return: <dict> 新的區塊
"""
block = {
'index': len(self.chain) + 1,
'timestamp': time(),
'transactions': self.current_transactions,
'proof': proof,
'previous_hash': previous_hash or self.hash(self.chain[-1]),
}
# 重置存儲交易信息的list
self.current_transactions = []
self.chain.append(block)
return block
def new_transaction(self, sender, recipient, amount)):
"""
添加一筆新的交易到transactions中
:param sender: <str> 發送者地址
:param recipient: <str> 接收者地址
:param amount: <int> 數量
:return: <int> 包含該交易記錄的塊的索引
"""
self.current_transactions.append({
'sender': sender,
'recipient': recipient,
'amount': amount,
})
return self.last_block['index'] + 1
@property
def last_block(self):
# 返回鏈中的最後一個塊
return self.chain[-1]
@staticmethod
def hash(block):
"""
創建區塊的SHA-256哈希值
:param block: <dict> Block
:return: <str>
"""
# We must make sure that the Dictionary is Ordered, or we'll have inconsistent hashes
block_string = json.dumps(block, sort_keys=True).encode()
return hashlib.sha256(block_string).hexdigest()
上面的代碼應該比較直觀,我們差不多完成了區塊鏈的表示了。不過你應該比較好奇新的區塊是怎麼創建或者挖出來的。
理解工作量證明(proof of work,pow)
工作量證明(POW),簡單的說就是一份證明,該證明可以用來確認你做過一定量的工作。工作量證明的目標就是找到一個能解決一個問題的數字,這個數字不好找但是很容易證明(該數字就是問題的答案),這就是工作量證明背後的核心思想(俗稱挖礦)。
我們舉一個簡單的例子,方便理解:
假設有一個問題,一個整數x乘以整數y,對積做hash,hash值必須以0結尾,即hash(x * y) = ac23dc…0。我們假設X的值爲5,求y的值大小?用Python實現如下:
from hashlib import sha256
x = 5
y = 0 # y是未知數
while sha256(str(x*y).encode()).hexdigest()[-1] != "0":
y += 1
print('The solution is y = ' + str(y))
可以看到如下的結果:
The solution is y = 21
在比特幣中使用的工作量證明算法叫做Hashcash,它和我們上面的簡單例子相差不大。礦工們爲了獲得能夠創建新的區塊權利,需要使用該算法參與計算競爭。通常,問題的難度取決於需要在字符串中查找的字符個數,礦工算出結果後,會獲得相應的獎勵幣。
tips:比特幣網絡中任何一個節點,如果想生成一個新的區塊並寫入區塊鏈,必須能夠解出比特幣網絡給出的工作量證明問題。這道題關鍵的三個要素是工作量證明函數、區塊及難度值。工作量證明函數是這道題的計算方法,區塊決定了這道題的輸入數據,難度值決定了這道題的所需要的計算量,誰最先解出問題,誰就能生成新的區塊並寫入區塊鏈。
工作量證明簡單實現
爲我們的區塊鏈實現一個相似的算法,規則跟上述的例子差不多:
找到一個數字p,該數字與前一個塊的proof值的hash值以4個0開頭
import hashlib
import json
from time import time
from uuid import uuid4
class Blockchain(object):
...
def proof_of_work(self, last_proof):
"""
簡單工作量證明(POW)算法:
- 找到一個數字p',使得hash(pp')值的開頭包含4個0, p是上一個塊的proof, p'是新的proof
:param last_proof: <int>
:return: <int>
"""
proof = 0
while self.valid_proof(last_proof, proof) is False:
proof += 1
return proof
@staticmethod
def valid_proof(last_proof, proof):
"""
驗證Proof: hash(last_proof, proof)值開頭是否包含4個0?
:param last_proof: <int> 上一個Proof
:param proof: <int> 當前Proof
:return: <bool>
"""
guess = (str(last_proof)+str(proof)).encode()
guess_hash = hashlib.sha256(guess).hexdigest()
return guess_hash[:4] == "0000"
爲了調整算法的難度,我們可以修改需要匹配的0的個數。但4已經足夠了,你會發現增加一個數字會大大增加計算的時間。
我們的Blockchain類差不多完成了,接下來可以開始用http請求進行交互了。
2、Blockchain作爲api
我們將使用Python Flask框架,這是一個輕量級的Web應用框架,它能方便的將請求映射到 Python函數,這允許我們通過http請求跟區塊鏈進行交互。
我們會創建3個方法:
/transactions/new 創建一個新的交易,並添加到區塊中
/mine 告訴服務器服挖掘一個新的塊
/chain 返回整個區塊鏈
設置flask
我們的服務器會作爲區塊鏈網絡中的一個節點,讓我們添加一下代碼:
import hashlib
import json
from textwrap import dedent
from time import time
from uuid import uuid4
from flask import Flask
class Blockchain(object):
...
# 實例化我們的節點
app = Flask(__name__)
# 爲這個節點生成一個全局的唯一地址
node_identifier = str(uuid4()).replace('-', '')
# 實例化區塊鏈
blockchain = Blockchain()
@app.route('/mine', methods=['GET'])
def mine():
return "We'll mine a new Block"
@app.route('/transactions/new', methods=['POST'])
def new_transaction():
return "We'll add a new transaction"
@app.route('/chain', methods=['GET'])
def full_chain():
response = {
'chain': blockchain.chain,
'length': len(blockchain.chain),
}
return jsonify(response), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
代碼相對簡單,就不做說明了。
交易請求
用戶發給服務器的交易請求如下:
{
"sender": "my address",
"recipient": "someone else's address",
"amount": 5
}
我們在Blockchain類中已經定義了添加交易的方法,因此接下來的事情比較簡單:
import hashlib
import json
from textwrap import dedent
from time import time
from uuid import uuid4
from flask import Flask, jsonify, request
...
@app.route('/transactions/new', methods=['POST'])
def new_transaction():
values = request.get_json()
# 檢查需要的字段是不是都有
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': f'Transaction will be added to Block {index}'}
return jsonify(response), 200
挖礦
挖礦是裏面的神奇所在,而且很簡單,只需要做如下三件事:
1 計算工作量證明
2 增加一個交易,授予礦工(自己)一個幣
3 構造新區塊並將其添加到鏈中
import hashlib
import json
from time import time
from uuid import uuid4
from flask import Flask, jsonify, request
...
@app.route('/mine', methods=['GET'])
def mine():
# 運行工作量證明算法,獲取下一個proof
last_block = blockchain.last_block
last_proof = last_block['proof']
proof = blockchain.proof_of_work(last_proof)
# 由於找到了proof,我們獲得一筆獎勵
# 發送者爲"0", 表明是該節點挖出來的新幣
blockchain.new_transaction(
sender="0",
recipient=node_identifier,
amount=1,
)
# 創建新的區塊,並添加到鏈中
previous_hash = blockchain.hash(last_block)
block = blockchain.new_block(proof, previous_hash)
response = {
'message': "New Block Forged",
'index': block['index'],
'transactions': block['transactions'],
'proof': block['proof'],
'previous_hash': block['previous_hash'],
}
return jsonify(response), 200
注意挖出來的新區塊的接收者就是我們服務器節點的地址,我們做的大部分工作都只是調用Blockchain類的一些方法。到此,我們可以開始跟我們的區塊鏈交互了。
3、與我們的區塊鏈交互
你可以通過cURL或Postman去跟API交互。
啓動服務器:
$ python blockchain.py
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
讓我們試着通過Get請求http://localhost:5000/mine來挖一個塊:
讓我們通過POST請求http://localhost:5000/transactions/new創建一筆新的交易,post body包含交易的結構信息:
我重啓服務器然後挖了兩個塊之後,現在總共有三個塊(包含創世塊),我們可以通過http://localhost:5000/chain請求獲取所有的塊信息。
4、一致性
我們已經有了一個可以接受交易以及挖礦的基礎區塊鏈。但是區塊鏈系統應該是去中心化的,既然區塊鏈是去中心化,我們該如何確保所有的節點都有同樣的鏈呢?這就是一致性問題,如果想要在我們的區塊鏈網絡中運行多個節點,我們就需要實現一個一致性算法。
註冊新節點
在實現一致性算法之前,我們需要找到一種方法讓一個節點知道它相鄰的節點。我們網絡中的每個節點都需要保存一份其它節點的記錄信息。因此讓我們增加一些接口:
1、/nodes/register 接收以url表示的新節點列表
2、/nodes/resolve 實現一致性算法,解決衝突,確保每個節點都有正確的鏈信息
我們需要修改Blockchain的構造函數,添加一個註冊節點的方法:
...
from urlparse import urlparse
...
class Blockchain(object):
def __init__(self):
...
self.nodes = set()
...
def register_node(self, address):
"""
添加一個新的節點到節點列表中
:param address: <str> 節點地址:比如'http://192.168.0.5:5000'
:return: None
"""
parsed_url = urlparse(address)
self.nodes.add(parsed_url.netloc)
我們通過set()來儲存節點列表,這是一種簡單方法讓我們避免添加重複節點。
實現一致性算法
之前提到,衝突發生在節點之間有不同的鏈信息。爲了解決衝突,我們定義一條規則:最長的、有效的鏈纔是權威的鏈,換句話說,網絡中最長的鏈纔是實際的鏈。通過這個算法,我們讓網絡中的所有節點保持一致性。
..
import requests
class Blockchain(object)
...
def valid_chain(self, chain):
"""
確定一個給定的區塊鏈是否有效
:param chain: <list> 區塊鏈
:return: <bool> True 有效, False 無效
"""
last_block = chain[0]
current_index = 1
while current_index < len(chain):
block = chain[current_index]
# 檢查block的hash值是否正確
if block['previous_hash'] != self.hash(last_block):
return False
# 檢查工作量證明是否正確
if not self.valid_proof(last_block['proof'], block['proof']):
return False
last_block = block
current_index += 1
return True
def resolve_conflicts(self):
"""
一致性算法,通過將我們的鏈替換成網絡中最長的鏈來解決衝突
:return: <bool> True 我們的鏈被取代, 否則爲False
"""
neighbours = self.nodes
new_chain = None
# 我們只查看比我們鏈長的節點
max_length = len(self.chain)
# Grab and verify the chains from all the nodes in our network
for node in neighbours:
response = requests.get('http://'+node+'/chain')
if response.status_code == 200:
length = response.json()['length']
chain = response.json()['chain']
# 檢查該節點的鏈是否比我們節點的鏈長,以及該鏈是否有效
if length > max_length and self.valid_chain(chain):
max_length = length
new_chain = chain
# 如果找到比我們長且有效的鏈,則替換我們原來的鏈
if new_chain:
self.chain = new_chain
return True
return False
valid_chain()方法主要負責檢查鏈是否有效,歷遍鏈中的所有塊,檢查塊的hash值以及工作量證明是否正確。
resolve_conflicts()負責解決衝突,通過歷遍附近節點,通過valid_chain方法驗證該節點的鏈是否正確,如果該節點鏈正確且該鏈比我們節點的鏈長度長,則用該鏈替換我們的鏈。
讓我們添加兩個方法,一個用來註冊附近節點,一個用來解決衝突:
@app.route('/nodes/register', methods=['POST'])
def register_nodes():
values = request.get_json()
nodes = values.get('nodes')
if nodes is None:
return "Error: Please supply a valid list of nodes", 400
for node in nodes:
blockchain.register_node(node)
response = {
'message': 'New nodes have been added',
'total_nodes': list(blockchain.nodes),
}
return jsonify(response), 201
@app.route('/nodes/resolve', methods=['GET'])
def consensus():
replaced = blockchain.resolve_conflicts()
if replaced:
response = {
'message': 'Our chain was replaced',
'new_chain': blockchain.chain
}
else:
response = {
'message': 'Our chain is authoritative',
'chain': blockchain.chain
}
return jsonify(response), 200
你可以在不同的機器上運行節點,這裏我們在一臺機器上啓動不同端口來代表不同的節點。假設我們有兩個節點:
http://localhost:5000 和 http://localhost:5001。
運行兩個節點,並將第二節點註冊到第一個節點中:
然後在第二個節點(5001端口)挖了一些塊,確保該節點的鏈長度比第一個節點長。然後在第一個節點調用GET /nodes/resolve請求,可以看到節點1的鏈通過一致性算法被替換掉了。
嗯。。現在可以邀請你的朋友來測試你的區塊鏈了。可以到這裏查看Blockhain.py