區塊鏈這個技術在2017年是比較火的,基於區塊鏈技術的比特幣的價格也是高的驚人,於是我就想對區塊鏈技術做個深入瞭解。在網上看了大量文章後,發現大多數文章要麼只講理論,要麼就只貼程式碼,都不太滿意。於是我就寫了這篇文章,程式碼結合理論一起講解。
定義
我有一個習慣,就是查詢一個概念或一個專有名詞的時候就喜歡去維基百科上找,而不喜歡去百度上面搜。維基百科對區塊鏈有這樣一段描述:
it as an open, distributed ledger that can record transactions between two parties efficiently and in a verifiable and permanent way
簡單說區塊鏈就是開放的分散式賬本,並且可以以一種可核查的(verifiable)和永久的(permanent)方式記錄雙方的交易。這裡有幾個關鍵詞(已經加重標出了),我們依次來講解一下這幾個詞。開放的,與開放的相對的是封閉的(或者說加密的),我們平常使用的資料庫(如MySQL),在訪問資料庫之前都要先輸入密碼,密碼錯了則不能訪問,這個是封閉的的做法。而開放的則不是這樣,任何人都可以免費的得到這份賬本,無需輸入密碼。分散式說明無中心節點,每個節點都是平等的,並且都儲存著一份完整的賬本。賬本說明區塊鏈可以儲存、讀取資訊,我們可以大膽的認為它就是一個資料庫,只不過與我們傳統的資料庫不太一樣而已。可核查的,我們知道既然每個節點都是平等的,那麼每個節點都有權向賬本中寫入資料(向區塊鏈的末尾新增一個區塊),其他節點就要判斷你新增的這個資料是否是合法的,只有合法的資料才會被認可,並且把這個資料也新增到自己的賬本。
區塊
一個區塊需要包含以下資訊:
- id,用於唯一表示這個區塊
- prev_hash,前一個區塊的hash
- nonce,工作量證明相關,用於工作量證明演算法的計數器
- data,儲存的資料,我們這個是很簡單的區塊鏈,所以存放的是簡單的字串
- timestamp,挖到這個區塊的時間,用unix時間戳表示
- hash,256位(二進位制,每一位都是0或1),每個區塊的hash都不同,同id,也可用於唯一表示這個區塊
上面只是對每個屬性做了個簡單的介紹,具體的介紹放到對應的小節講,如nonce會在講解工作量證明那節會詳細講到。我們按照上面的定義可以很快寫出下面的類:
class Block(object):
def _calc_hash(self, data, nonce, prev_hash):
return hashlib.sha256(data + str(nonce) + prev_hash).hexdigest()
def __init__(self, **kwargs):
self.id = kwargs['id']
self.prev_hash = kwargs['prev_hash']
self.nonce = kwargs['nonce']
self.data = kwargs['data']
self.timestamp = kwargs['timestamp']
self.hash = self._calc_hash(self.data, self.nonce, self.prev_hash)
複製程式碼
這裡我假設輸入是合法的,如data和prev_hash均有值且都是str型別,nonce有值且可用str()內建函式轉成str型別。如果把這段程式碼用到生產環境中是非常不明智的,最好先對使用者的輸入做一系列的合法性判斷,如果不合法該怎麼處理,因為本篇主要講解原理,所以程式碼追求儘可能的簡單。
這裡用到了hashlib這個庫來計算hash值,關於hash有以下幾個重要推論:
- 源不一樣則hash出來的結果一定不一樣
- 源一樣則hash出來的結果也一樣
- 源一旦有任何改變都會導致hash出來的結果變得面目全非,實際上這點可以通過前面兩點推導而來
- 根據結果無法反向推匯出源,即不可逆
- 用同一個hash函式(或hash演算法)得到的結果的長度都是一樣長的,不管源的長度如何,
本程式中用到了sha256演算法,該演算法得出的結果長度恆為256位。 大家可能注意到了,我是說以上幾點都是推論,至於為什麼說是推論,因為理論上並不成立。比如說第1點,我們可以試想以下,因為hash是256位,每一位都要麼是0要麼是1,那麼總共有2的256次方種可能, 如果我們有2的256次方加1次不同的輸入,那麼必然會有至少兩次hash出來的結果是一樣的,但是要計算2的256次方加1次hash所需要的時間很長很長(感興趣的讀者可以自己計算一下,設每次hash所需時間為T,那麼2的256次方乘以T就是需要的時間),長到兩次hash的結果完全一樣的概率無限趨近於0,這一點只要知道就好。
基於以上幾點,我們就知道,除非某兩個區塊的data + str(nonce) + prev_hash
的結果完全一樣,否則任意兩個區塊的hash值都不一樣且是唯一的。而兩個區塊的data + str(nonce) + prev_hash
的結果完全一樣幾乎是不可能的,所以使用hash來唯一表示一個區塊也是可行的。
建立創世區塊
創世區塊是區塊鏈的第一個區塊,一般都是預先定義好,不儲存任何有價值的資訊。我們定義一個建立創世區塊的函式,程式碼如下:
def create_genesis_block():
params = {
'id': 0,
'data': '',
'nonce': 0,
'prev_hash': '',
'timestamp': int(time.time())
}
return Block(**params)
複製程式碼
這段程式碼很好理解,就不詳細說了。需要注意的是,因為創世區塊是區塊鏈的第一個區塊,所以它的prev_hash為空字串,prev_hash是表示前一個區塊的hash。
工作量證明
前面說過區塊鏈是可核查的,即區塊鏈要有一種方法來證明你這個區塊是合法的,是合法的區塊才能append到區塊鏈的末尾,本篇就來講述如何判斷一個區塊是否是合法的。 工作量證明的核心是難於計算但是易於驗證。換句話說就是,客戶端通過大量的計算才能得到一個結果,但是要驗證這個結果是否正確則是非常容易的。要驗證一個區塊 是否合法,我們要先定義一個規則,任何遵守這個規則的區塊我們都認為是合法的。比如我們規定一個區塊的hash必須以4個0(十六進位制,二進位制這是16個0)開頭,如以下是合法的hash
0000ea046ff88074619b8c984ad64416d7b38a7cd2601339bc31718c0c1faad7
0000a19c1565f45229616010a136680a1540a6a4f580a6acbb84d0bf65f04b60
複製程式碼
以下則是不合法的hash
12deea046ff88074619b8c984ad64416d7b38a7cd2601339bc31718c0c1faad7
ef23a19c1565f45229616010a136680a1540a6a4f580a6acbb84d0bf65f04b60
複製程式碼
這個是很好驗證的,符合工作量證明的核心易於驗證。我們再來討論得到這個hash值是否是難於計算。轉換成二進位制後,hash的前16位必須是0,根據概率學,那麼大概需要2的16次方(65536)次計算才能得到一個合法的hash值,讀者可以自己寫程式碼驗證一下,如:
import hashlib
def calc_hash(data, nonce, prev_hash):
return hashlib.sha256(data + str(nonce) + prev_hash).hexdigest()
def main():
nonce = 0
data = 'hello, blockchaindfewdffbv'
prev_hash = ''
while True:
hash = calc_hash(data, nonce, prev_hash)
if hash[:4] == '0000':
break
nonce += 1
print 'Got you:'
print nonce
print hash
複製程式碼
根據概率學,跑的次數越多,平均值就會越接近65536,理論上是這樣,實際上應該也是,有興趣的讀者可以自己測一下。所以平均要經過65536次計算才能得到一個合法的hash,如果改變一下規則,要求hash值(十六進位制)的前6位必須是0,那麼平均要經過2的24次方(16777216)才能得到一個合法的hash值,因此符合工作量證明的核心難於計算。 從理論上這個規則是OK的嗎,我們就來寫程式碼來實現它吧。
NUM_ZEROS = 4
class ProofOfWork(object):
def __init__(self, block):
self.block = block
def is_block_valid(self):
return self.block and self.block.hash and self.block.hash.startswith('0' * NUM_ZEROS)
複製程式碼
程式碼很簡單,就不解釋了。NUM_ZEROS越大,難度越高,需要的計算次數也越多。但是不管NUM_ZEROS設的多大,都是易於驗證的。
這個ProofOfWork類基本可以工作,但是有個問題,Block有那麼多個屬性,我們僅僅是驗證了它的其中一個屬性(hash),而忽略了其他屬性。沒錯,我們還要驗證id和prev_hash,id 必須比前一個區塊的id多1,pre_hash必須等於前一個區塊的hash,我們來完善程式碼。
NUM_ZEROS = 4
class ProofOfWork(object):
def __init__(self, block, prev_block):
self.block = block
self.prev_block = prev_block
def is_block_valid(self):
return self.block and self.block.hash and self.block.hash.startswith('0' * NUM_ZEROS) \
and self.block.id == self.prev_block.id + 1 \
and self.block.prev_hash == self.prev_block.hash
複製程式碼
挖礦
現在我們只有一個區塊,也就是創世區塊,我們自然而然就想得到更多的塊,這個就是挖礦了。挖礦的目的是要根據blockchain中的最後一個block,找到一個符合條件的新的block。 符合條件是指符合工作量證明,只有通過工作量證明的區塊才能被其他節點認可和接受。挖礦的原理很簡單,就是不斷的遞增nonce值,得到一個對應的區塊,然後去驗證這個區塊是否滿足工作量證明,滿足則說明找到了一個符合條件的礦,沒找到則繼續遞增nonce。程式碼如下:
def mine(prev_block):
nonce = 0
while True:
data = 'block ' + str(prev_block.id + 1)
params = {
'id': prev_block.id + 1,
'data': data,
'nonce': nonce,
'prev_hash': prev_block.hash,
'timestamp': int(time.time())
}
block = Block(**params)
if ProofOfWork(block, prev_block).is_block_valid():
return block
nonce += 1
複製程式碼
因為挖礦需要做大量的計算,所以挖礦是很耗費能源的(電力)。
區塊鏈
區塊鏈,顧名思義,就是區塊形成的鏈。上節講了挖礦,就算挖得再多,不把它們鏈起來,依然沒用。話不多說,直接上程式碼:
class BlockChain(object):
def __init__(self):
self.blocks = []
def add_block(self, block):
if not ProofOfWork(block, self.get_last_block()).is_block_valid():
return False
self.blocks.append(block)
return True
def get_last_block(self):
return self.blocks[-1]
def is_chain_valid(self):
prev_block = self.blocks[0]
for block in self.blocks[1:]:
if not ProofOfWork(block, prev_block).is_block_valid():
return False
prev_block = block
return True
複製程式碼
我們把區塊鏈定義成了一個list,並新增了幾個方法:
- add_block: 向區塊鏈的末尾新增一個區塊,只有經過工作量證明的區塊才能新增
- get_last_block:返回區塊鏈中的最後一個區塊
- is_chain_valid: 判斷該區塊鏈是否合法
程式碼很簡單,就不解釋了。
後記
本文在講解區塊鏈的原理的基礎上,也實現了一個簡單的區塊鏈。當然由於篇幅所限,只講了區塊鏈的部分,還有很多沒講,比如既然區塊鏈是一個資料庫的話,我們並沒有講到區塊鏈的儲存,以及各個節點的同步等。這個有機會放到下篇再講(要準備年會了,我也不知道下篇是什麼時候^_^)。實際上儲存以及同步都已經實現了,程式碼在github上,希望讀者可以star一下, 另希望也能關注一下本人的技術公眾號。