我很喜歡前諾貝爾物理獎得主 Richard Phillips Feynman 曾說過的一句話:
「What I cannot create, I do not understand.」
當初為了搞懂區塊鏈的基本原理,便自己用 Python 刻出了一個簡單但具有基本功能的區塊鏈,並且當作去年 it邦幫忙鐵人賽的部分內容,這幾天趁著比特幣再創歷史新高,也把過去寫過的文章整理一下,但因為文章較長,共分成四集如下:
- 定義基本架構、功能與格式(本文)
- 利用非對稱加密簽署並發送交易
- 製作節點與用戶端程式
- 節點間的廣播與同步
區塊鏈的願景:Be your own bank
2008年的金融風暴讓人們開始認知到銀行與政府不一定可以被信任,而且在過去120年來貨幣的購買力下跌了30倍,截至今天也還在下跌中。
貨幣購買力下降的主因除了經濟成長外,也與政府不斷發行新貨幣進入市場有相關,就像是去年因為肺炎疫情重創經濟,美國政府帶頭進行了無上限的QE,所以實際上貨幣流通量增加的速度實際上遠遠超出你我的想像,下圖是美金流通量的歷史。
你要怎麼信任手中的貨幣在未來是有價值的?
除了政府與銀行外,又有誰能發行可信的貨幣?
怎能保證發行者在未來不會繼續超發?
Bitcoin在一開始便是為了解決誰可以是有公信力的發行者、誰又可以是公正的保管者的問題。比特幣的發明者─中本聰(Satoshi Nakamoto)提供的解決方案就是把貨幣的發行與詮釋權發還給大眾,並搭配密碼學來確保大眾手上的資料與紀錄難以被竄改,這也是為什麼有時候虛擬貨幣會被叫成加密貨幣的原因。
換言之,中本聰的理想就是人人手上都有帳本、每個人都是銀行,藉此來保證了共識的公正與執行,也因此Bitcoin的最初願景便是”Be your own bank.”。
交易的組成
先來看區塊鏈的架構與裏頭具備哪些要點,首先我們先把一些基本的交易與區塊的格式跟內容定義清楚。
Transaction
就像我們平常習慣用的銀行轉帳一樣,每筆交易都會產生一筆交易明細,詳細記錄這筆交易的發送人、接收者、金額、手續費與備註,這裡的每一筆交易明細我們先稱之為Transaction
。
Blocks of transactions
所有的Transaction
會根據時間順序被放置到一個個區塊(Block
)內,當有新的區塊正在產出,新生成的所有交易紀錄都會被放置在該區塊之下。
交易格式
根據最上面的交易明細,一筆交易裏頭應該要有這些資訊:
- 發送方(
sender
):交易的發起方,同時要確認是發送方底下餘額否足夠 - 收款方(
receiver
):交易的收受方,通常無須使用者同意即可收款 - 金額大小(
amounts
):這筆交易的金額數目大小 - 手續費(
fee
):交易時所需支付的手續費多寡 - 訊息(
message
):如同轉帳的備註一般留下資訊,通常是給收款方看
區塊格式
每一個區塊包含了許多筆交易(Transaction
),就像是帳本的內頁儲存了許多交易紀錄,另外為了加密的需求會記錄前後一個區塊的哈希(hash
)值,也就是每一塊區塊之間的哈希值是環環相扣的。
可以把哈希值看做是每個區塊上的鎖頭,而礦工挖掘出的nonce
則代表了能夠匹配這個鎖頭的鑰匙(或另一把鎖),而且下一個區塊的哈希值又根據這個nonce
值而產生,如此一來只要其中任何一個交易紀錄、區塊被竄改,則整個鍊上的nonce
跟hash
都需要修正,並且需要在新的區塊產生前計算/修正完畢,這需要擁有異常龐大的計算量,也因此竄改區塊鏈是幾近不可能的事情。
因此,一個區塊裡頭至少需要有這些資訊:
- 前個區塊的哈希值(
previous_hash
):為了加密需要我們會使用到前一個區塊的哈希值,藉此保障區塊鏈上的資料安全 - 此次區塊的哈希值(
hash
):目前區塊計算後的哈希值 nonce
:礦工找到能夠解開上個區塊鎖的鑰匙- 當前難度(
difficulty
):挖出這個區塊時所使用的困難度 - 區塊產生時的時間戳(
timestamp
):紀錄了這是該區塊是在何時產生,在調整挖礦難度會使用到 - 交易紀錄(
transactions
):紀錄了這個區塊中所有的交易紀錄 - 挖掘礦工(
miner
):紀錄了這個區塊中是由誰挖掘出來的 - 礦工獎勵(
miner_rewards
):紀錄了這個區塊產出時分給礦工的獎勵
區塊鏈架構
為了維持使用上的穩定,也避免使用者等待時間的高低起伏,因此區塊產出的時間必須盡量保持固定,但因為礦工的數目往往無法預測,挖掘區塊的難度就必須跟著礦工數目與預算力的多寡進行自動調整,所以在區塊鏈裡都會記錄事先設定好的出塊時間,並透過動態調整難度讓實際的出塊時間與實際的出塊時間不至於落差太大。
另外,區塊鏈裡頭也會設定每一區塊中所能存放的交易上限,避免無上限時造成資料傳遞上的堵塞,同時也會將目前區塊產出時會給予礦工的獎勵數目寫在區塊鏈裡頭。
- 難度調節區塊數(
adjust_difficulty_blocks
):每多少個區塊調節一次難度 - 目前難度(
difficulty
):希望每個區塊的產出時間盡量保持一致,也因此隨著挖掘nonce的機器數目與效能變動,挖掘難度也必須隨之調整以讓產出時間維持在動態平衡上,這個欄位代表了區塊鏈當下的難度 - 出塊時間(
block_time
):理想上多久能夠出一個區塊,當實際出塊時間塊於設定的理想值時,代表運算效能優於實際需要,因此必須將難度做相對應的提升,反之亦然,詳情在之後的教學中會有進一步的說明。 - 挖礦獎勵(
miner_rewards
):獎勵挖礦者的金額多寡,挖出新區塊的礦工可以得到獎勵,藉此鼓勵礦工參與區塊鏈營運 - 區塊容量(
block_limitation
):每一個區塊能夠容納的交易上限,上限的存在是因為當礦工挖掘出新的nonce
時,他需要把所有被接受的交易連同區塊資料一併廣播給其他人知悉,因此如果容量過大會導致傳播過慢或是讓礦工需要的網速增加到不符合經濟效益的地步。 - 區塊鏈(
chain
):目前區塊鏈中儲存的所有區塊 - 等待中的交易(
pending_pranscations
):當使用者發送交易時,因為區塊鏈能夠吞吐的交易量有限,交易會先處在pending的狀況,當交易量過大時,礦工會首先選擇手續費高的交易先處理。
產生哈希數(Hash)
哈希函數可以想做是一種單向轉換方式,可以把任意長度的輸入轉換成固定長度的輸出,以SHA-1為例,它能夠把不定長度的輸入值轉換成固定20個位元組的輸出。
在定義上,哈希函數(hash function
)必須同時滿足兩個條件:
- 同樣的輸入值必會得到相同的輸出值
- 輸出的哈希數無法反推回原本的資料
以下面為例,Hello World!
的字串能夠透過SHA-1
的哈希函數轉換成:
2ef7bde608ce5404e97d5f042f95f89f1c232871
但此時產生的2ef7bde608ce5404e97d5f042f95f89f1c232871
無法反推回原本的Hello World!
。
由於輸入資料的不同,往往我們可以把哈希數視作幾近隨機的位元組所構成(但仍然會因為哈希函數的不同而有所變異)
在這裡我們先把下面這些資料連接後作為哈希函數的輸入:
- 前一個區塊的哈希值(
previous_hash
) - 區塊產生當下的時間戳(
timestamp
) - 所有的交易明細(
transactions
) - 挖掘中的
nonce
值
下面是轉換哈希數的程式碼,其中transaction_to_string
負責把交易明細轉換成字串、get_transactions_string
負責把區塊紀錄的所有交易明細轉換成一個字串、get_hash
負責依據這四筆資料產生相對應的哈希數。
產生創世塊(genesis block)
創世塊是開始部署區塊鏈時所產生的第一個區塊,通常具有劃時代、開天闢地的意涵在內,雖然以第一個區塊的角度而言它不需要帶有任何交易紀錄,是個無任何資料的空區塊,但創造並執行區塊鏈的人可以把心中的精神或具象徵性的意義寫入創世塊中藉此提醒後人。
以比特幣來說,比特幣的創世塊裡頭隱含了下面這句話。
The Times 03/Jan/2009 Chancellor on brink of second bailout for banks.
這句話被比特幣創始人中本聰寫入創世塊中,來源是是2009/01/03英國《泰晤士報》的頭版標題,那時候的世界還陷在2008金融風暴的危機中尚未復甦,這篇報導則敘述了當時的英國正考慮對銀行進行財務紓困,或許中本聰只是單純想證明這區塊確實是當天寫入的,又或許想透過《泰晤士報》的頭版標題又對政府與中心化金融機構進行一次諷刺。
由於這是我們的第一個區塊鏈,所以我們就在previous_hash
的欄位給...........Hello World!
藉此紀念一下 ,並且難度與挖礦獎勵設定成區塊鏈的預設值,礦工這裡就直接填入我們的姓名,產生創世塊後就直接把創世塊加入到chain
之中
放置交易紀錄至新區塊中
區塊過大會導致在網路傳播上的不易與耗時,想像一下如果單一區塊有1GB那麼大,那每次產出區塊後光同步下載就要花費多少時間?為了避免這種狀況產生,每個區塊的承載量是設有容量上限的,既然有上限,礦工勢必得做出選擇,那礦工如何選擇哪幾筆交易應該被優先處理呢?
通常礦工會根據自身的利益最大化而選擇手續費高的交易優先處理,因此在這裡我們選擇手續費最高的幾筆交易優先加入區塊中。
挖掘新區塊
接著我們就可以來挖掘新區塊了,挖掘的步驟是透過改變nonce
值(從0,1,2,3....直到找到符合的nonce
)而得到新的哈希數,在這裡我們把難度定義為:
產生的哈希數的”開頭有幾個0"
也就是每次改變nonce
、產生一個新的hash
後來確認有沒有符合要求(開頭有幾個0),如果符合就代表我們找到一個合規nonce
值了!
但如果沒有,就只好持續的往下找了。也因為運算量越大能夠找到合規的nonce
值的機率也越大,也又有權力驗證與廣播區塊,因此這個方法又被稱為工作量證明Proof of Work(POW)
。
但透過這個方式區塊的產生時間會非常地不穩定,你可以到bitcoin的區塊瀏覽器看看產出的時間,bitcoin預設是每十分鐘應該要產出一個區塊,但也可以發現實際上每個區塊的產生時間會跟十分鐘有點落差,這是POW的必然結果。
在這裡的實作中,我們生成一個區塊後不停計算不一樣的nonce
值,直到我們能夠找到合規的nonce
為止,在發現(挖掘)合規的nonce
之後,就可以把挖出來的區塊置入鏈裡頭。
調整挖掘難度
由於每區塊裏頭都記錄著區塊被挖掘出的當下時間戳(timestamp
),因此我們可以知道每個區塊的產出時間(也就是找出符合的nonce
所耗費的時間),如果難度是固定的,那麼參與挖礦的運算力如果成長十倍,區塊的平均產出時間也會連帶變成十分之一,因此順應運算力的多寡而調整難度對區塊鏈的長久運行是很重要的。
那怎麼去評估區塊的產生時間呢?如果單純採用前一個區塊的產出時間很明顯的是不可行,因為POW
的核心精神是利用隨機數去猜到可能可以符合的nonce
,因此每一個區塊的產出時間會變動相當大:
根據上圖看到我們的區塊鏈在難度5的狀況下,連續十塊的出塊時間從0.47秒到39.44秒都有可能,因此根據單個區塊的出塊時間決定難度是萬萬不可行的,取而代之的方法便是取多個區塊的出塊時間再取平均,這其實就像是訊號處理中的均值濾波器(mean filter)。
我們設定如果平均出塊時間小於設定的出塊時間,就把難度加1,如果平均出塊時間大於設定的出塊時間,就把難度減1。
這裡難度的定義是挖到的nonce
值必須要滿足讓Hash的頭幾個Bytes為0,因此難度每加1,實際上的運算量會增加16倍(位元組是兩兩16進位構成的),也因為調整幅度太大,所以其實這裡設計的並不是很好的難度調整算法。
計算帳戶餘額
除了難度調整,在發起交易當下也必須檢查匯款人的餘額是否足夠,同時也限制不能匯出超過自己帳戶的餘額,而區塊鏈裡的帳戶餘額總共只有三種來源:
- 區塊獎勵:挖出區塊的礦工能得到該區塊的出塊獎勵
- 手續費:挖出區塊的礦工能得到該比區塊內所有交易的手續費
- 匯款收入:收到別人匯款的款項
因此我們寫一個簡單的函式,從第一個區塊的第一筆交易開始檢查,一路檢查到最後一筆後便可以得到該帳戶的餘額。
確認哈希值是否正確
為了避免我們的資料被竄改,也必須時常檢查資料的正確性。還記得我們每個區塊的哈希數都是環環相扣的吧?
在昨天每個哈希數都由下面這四筆資料計算出來:
- 前一個區塊的hash(
previous_hash
) - 區塊產生的時間戳
- 所有的交易紀錄
nonce
所以檢查的方式就是從第一個區塊的哈希數一路算到最後一個,一旦中間開始的某個哈希數算完之後對不起來,那麼就代表其中的某筆交易紀錄被竄改過。
如果我們在其中一個區塊插入了一筆偽造的交易,那麼透過一連串哈希的確認與計算,我們便可以發現hash數是對不起來的!
如果某特定區塊被更改,那麼之後的所有區塊都必須重新被計算哈希數,否則會完全對不起來而被發現資料被竄改過!當然攻擊者可以選擇更改後重新計算所有的哈希數,但當主鏈夠長、礦工夠多時,重新計算所有的哈希數所需要的運算量與成本非常可怕,也因此保障了區塊鏈的不可竄改性。
更何況在重新計算時,正常的塊也在不停的被一般的礦工產出,要跟所有的礦工競爭幾近天方夜譚。
文章列表:
- 定義基本架構、功能與格式
http://bit.ly/3rPRrtE - 利用非對稱加密簽署並發送交易
http://bit.ly/3og5jLF - 製作節點與用戶端程式
http://bit.ly/2KVXBb2 - 節點間的廣播與同步
http://bit.ly/394aurx
工商服務
我在台大資工訓練班有開設相關區塊鏈課程,有興趣的可以參考。
https://train.csie.ntu.edu.tw/train/course.php?id=3174