以太坊區塊鏈中的資料結構
本文重點介紹以太坊中區塊鏈相關的基礎概念,適用於入門級別的同學觀看,對以太坊中的基礎知識有一個大概的瞭解。
本文目的是使得大家看完之後對以太坊有一個直觀的瞭解,因此對以太坊中的具體細節,不會深入闡述,以免新手們陷入冗餘細節中而不能一覽全貌。
以太坊
以太坊是一個基於交易的狀態機,其區塊鏈中的每個區塊就對應一個狀態,每產生一個區塊,以太坊中的狀態就會轉換到下一個狀態。通過狀態轉換使得執行以太坊中的所有節點保持資料的一致性。以太坊中的區塊鏈分散式存在,但與傳統的分散式卻截然不同。
從創世塊開始,不斷產生的交易持續刷新系統當前狀態,每當產生一個區塊,系統狀態就發生一次轉換。
以太坊中的雜湊
無論是比特幣還是以太坊中,都採用了SHA(Security Hash Algorithm)雜湊函式進行加密,在比特幣中採用了SHA256的雜湊函式,而在以太坊中,使用SHA3函式(Keccak函式)。它們的區別在於,SHA256屬於SHA-2,即第2代雜湊函式,而SHA3屬於第3代雜湊函式,你已經想到,是不是還有第1代SHA函式呢?
是的,確實存在第1代雜湊函式,但是第1代雜湊函式已經被人們找到認為製造碰撞的方法,已經不再適用於加密了。包括我們之前使用的MD5加密函式,也已經被發現可以製造碰撞,已經廢棄了。2015年8月5日,美國標準技術協會(NIST)正式釋出了SHA3,以其作為最新的一代標準加密函式。值得說明的是,比特幣中的SHA256目前也沒有被發現可以人為製造碰撞的方法。經過SHA256加密後可以得到長度為256bits的雜湊值,比特幣中一個使用者的賬戶地址,就是將其公鑰輸入到SHA256演算法中得到256bits的輸出得到。而以太坊中,經過SHA3加密後得到160bits的輸出。具體可以參見
以太坊中的序列化方法RLP
RLP(Recursive Length Prefix)可以將任意的資料編碼稱二進位制byte的陣列,即[]byte的形式。同時已知資料的RLP編碼結果,可以求出其原來的形式。RLP在以太坊中作用主要有如下幾個:
1.對結構體資料進行編碼
2.將特殊的資料型別(string,floats等)編碼為更高階的協議
RLP是以太坊中對資料進行編碼的主要手段。以太坊要用到SHA3函式的地方,首先對該資料進行RLP編碼,隨後對RLP編碼後的資料進行SHA3運算。因此以太坊中的SHA3計算是下列方式。更加詳細的內容可以參見[RLP]。(
區塊的組成
以太坊中,區塊主要由三部分組成:區塊頭(Block Header),叔塊(Uncle),交易列表(tx_List)。這三個部分也是礦工在網路中釋出的區塊的內容,如下圖所示。
區塊頭由15個欄位組成。
叔塊其實就是孤塊,因以太坊出塊速度很快平均十幾秒就會打包生成一個塊,所以礦工挖礦的競爭性很高,可能同時產出幾個都合法的區塊,以太坊為了一些安全性起見,允許競爭塊也掛在到主鏈上,同時給與挖出這些孤塊的礦工們少許獎勵增加工作的公平性。具體參見以太坊中的Ghost協議。
交易列表,儲存的是本區塊中所有的交易內容。
區塊頭結構
以太坊中每個區塊的結構和比特幣中每個區塊的結構卻不同,以太坊中每個區塊的結構比比特幣種區塊的結構更加複雜,太坊中區塊的區塊頭(Block Header)結構定義如下圖所示。
區塊頭中每個欄位的意義如下:
名稱 | 型別 | 意義 |
---|---|---|
parentHash | common.Hash | 父區塊的雜湊值 |
UncleHash | common.Hash | 叔父區塊列表的雜湊值 |
Coinbase | common.Address | 打包該區塊的礦工的地址,用於接收礦工費 |
Root | common.Hash | 狀態樹的根雜湊值 |
TxHash | common.Hash | 交易樹的根雜湊值 |
ReceiptHash | common.Hash | 收據樹的根雜湊值 |
Bloom | Bloom | 交易收據日誌組成的Bloom過濾器 |
Difficulty | *Big.Int | 本區塊的難度 |
Number | *Big.Int | 本區塊塊號,區塊號從0號開始算起 |
GasLimit | uint64 | 本區塊中所有交易消耗的Gas上限,這個數值不等於所有交易的中Gas limit欄位的和 |
GasUsed | uint64 | 本區塊中所有交易使用的Gas之和 |
Time | *big.Int | 區塊產生的unix時間戳,一般是打包區塊的時間,這個欄位不是出塊的時間戳 |
Extra | []byte | 區塊的附加資料 |
MixDigest | common.Hash | 雜湊值,與Nonce的組合用於工作量計算 |
Nonce | BlockNonce | 區塊產生時的隨機值 |
和比特幣中的區塊鏈一樣,以太坊中的區塊鏈仍然使用了parentHash連線前一個區塊,通過這個欄位,所有的區塊最終都能回溯到創始塊。
值得一提的是,區塊的時間戳並不是出塊的時間,礦工在挖礦的時候區塊的時間戳一般是確定的,所以這個區塊的時間戳,是礦工開始挖這個區塊的時間。
Gas Limit是用於限制區塊中的交易個數用的。具體由每個礦工自己設定,有些礦工覺得本區塊的Gas Limit太大,可以在挖下一個區塊時間Gas Limit下調,覺得Gas Limit太小的時候,就會上調。整個區塊鏈中區塊的大小,就是所有礦工微調的平均值。以太坊中正是通過Gas Limit來限制區塊的大小。
區塊頭中比較重要的三個欄位是Root、TxHash和ReceiptHash三個雜湊值。其中Root表示以太坊網路中全部有過轉賬交易的賬戶形成的Merkle partial Tree(MPT)的根雜湊值。為什麼是全部有個轉賬交易的賬戶呢,因為我們可以隨便建立一個以太坊賬戶,但是沒有發生交易的話,以太坊節點也不會知道這個賬戶的存在。TxHash是本區塊中所有交易形成的merkle tree的根雜湊值,ReceiptHash是本區塊中所有收據資訊構成的merkle tree 的雜湊值。這其中涉及到的Merkle tree的相關概念可以參考比特幣區塊鏈中的資料結構,裡面有merkle tree資料結構的介紹。接下來重點介紹Merkle Partial Tree。
Merkle Patricia Tree(默克爾-帕特里夏樹)
比特幣是基於交易的分散式賬本,而以太坊是基於賬戶的分散式賬本,相比於比特幣,以太坊設計的更加複雜。以太坊中會對所有有過轉賬交易的賬戶都會建立一個使用者狀態樹,每個執行以太坊客戶端的節點都會在本地維護一個使用者狀態樹,當有新的區塊打包到區塊鏈上之後,所有的以太坊節點(挖出區塊的節點除外)的就會執行最新區塊中的交易,對本地使用者的狀態樹進行維護,得到新的使用者狀態樹。使用者狀態樹的資訊並不儲存在區塊中,區塊中僅僅儲存了最新的狀態樹組成的根雜湊值。
關於MPT的介紹,網上資料眾多,但我認為比較容易理解的文章,可參見以太坊MPT原理,和這篇Understand ethereum’s trie,這兩篇是網上對於MPT的解釋比較到位的文章。
但是需要強調的是,以太坊中狀態樹之間的指標式雜湊指標,這樣從底層的value建立雜湊指標,再對其父節點建立雜湊指標,以此類推,直到最終的狀態樹merkle root。
每次釋出新的區塊鏈時,每個存有狀態樹的節點就會根據區塊中的交易更新對應的賬戶樹中的內容,每個節點根據區塊上的交易獨立的修改狀態樹,最後生成狀態樹的根雜湊值,隨後用本地生成的狀態樹雜湊值和新發布區塊中的狀態樹的Root欄位進行比較,如果相同,表示賬戶狀態和釋出區塊的節點的賬戶狀態保持了一致。以太坊中每個節點獨立的執行每個區塊上的交易,隨後驗證區塊,一個合法的區塊,所有節點執行過後其賬戶狀態就能保持一致。這就是以太坊中每個節點獨立執行卻又能維持區塊鏈資料的一致性的原因。
狀態樹的變化可以用下圖所示。每個狀態發生改變時,都會新建一個節點,但是原有的狀態樹仍然會儲存,儲存原有狀態的原因是因為以太坊出塊時間是十幾秒,在十幾秒鐘很容易出現多個合法的區塊,就會出現分叉,分叉的區塊最終合併之後,有一些分叉鏈上的節點的狀態就需要回退,儲存了歷史記錄就是方便隨時對狀態的回退。
區塊中的交易
以下程式碼部分來自於以太坊客戶端的c++原始碼(ethcore\TransactionBase.h)
class TransactionBase
{
protected:
Type m_type = NullTransaction; // 交易型別,表明該交易是合約建立交易還是一般的message-call 交易
u256 m_nonce; // 訊息傳送方的交易次數
u256 m_value; // 要交易的以太幣數量,若是建立合約的交易,稱為'endowment'
Address m_receiveAddress; // 交易接收方地址。如果要建立合約,則將地址寫為0x00
u256 m_gasPrice; // 本次交易的gas的單價
u256 m_gas; // 本次交易最多消耗的gas數量
bytes m_data; // 與交易相關的資料。或者是建立合約的初始化的一些引數。
boost::optional<SignatureStruct> m_vrs; // 交易的簽名,對交易發起方進行編碼
int m_chainId = -4; // EIP155計算的雜湊值,見https://github.com/ethereum/EIPs/issues/155
mutable h256 m_hashWith; // < Cached hash of transaction with signature.
mutable Address m_sender; // < Cached sender, determined from signature.
// 交易型別
enum Type
{
NullTransaction, //< 空交易.
ContractCreation, //< 建立合約的交易,直接忽略地址。
MessageCall //< message-call交易
};
};
可以看出一個交易中明確的給出了傳送地址,傳送以太幣數值,交易費等資訊,而以交易發起方的地址資訊,則以簽名的形式給出。和比特幣類似相同,以太坊中對每個區塊中的交易,經過雜湊後生成Merkle Patricia Root,這對應了區塊頭中的TxHash欄位。
區塊中的收據
以太坊中的每一筆交易都有對應的收據,收據樹的資訊是為了方便一些相關賬戶的查詢。與前面的狀態樹不同的是,交易樹和收據樹的形成都是隻關於本區塊中所有的交易。交易樹和收據樹其中一個作用是,可以用來做Merkle proof,以證明區塊中某筆交易的存在。除此以外,以太坊中如果需要查詢過去某段日期內和某個智慧合約的相關交易,如果沒有收據樹,只能遍歷過去所有的區塊以查詢符合特定需求的操作,而如果查詢的賬戶不存在,則需要遍歷到創始塊,這樣的代價實在太大,於是,以太坊中引入了bloom Filter。
Bloom Filter有什麼用呢?以太坊中的每一筆交易執行完畢就會生成一個收據,收據裡面包含了一個Bloom Filter的日誌,記錄了交易型別和地址等資訊。區塊的塊頭中也有一個Bloom Filter,裡面是所有交易生成的收據中的Bloom Filter的並集。當我們需要查詢和某個賬戶相關的交易時,我們可以利用區塊頭中的Bloom Filter進行查詢,查詢該區塊中是否存在關於某個賬戶的交易,如果塊頭的Bloom Filter中沒有找到,這說明該區塊不存在和某個賬戶相關的交易;如果查詢存在,我們還需要進一步在交易中尋找是否存在相關交易。
區塊中的收據資訊的相關程式碼定義如下:
using LogEntries = std::vector<LogEntry>;
class TransactionReceipt{
private:
boost::variant<uint8_t,h256> m_statusCodeOrStateRoot; // 狀態碼或者狀態樹根
u256 m_gasUsed; // 消耗的汽油,u256是256位的無符號整數
LogBloom m_bloom; // bloom日誌
LogEntries m_log; // 日誌
};
上述中的 LogEntries是LogEntry的vector集合,LogEntry的資料結構如下所示。是一個結構體,裡面包含一個地址、topics的256位的雜湊數的vector,以及位元組陣列組成的data。下述程式碼中的using語句並不存在於原始碼中,我新增到struct中方便理解每個型別的定義。每個LogEntry中包括了一個收款人的地址和其他相關資訊,而在收據資訊中包括了日誌、bloom過濾器和這筆交易消耗的汽油。
struct LogEntry
{
using bytes = std::vector<byte>;// byte的vector,原始碼struct中沒有這句。
using h256s = std::vector<h256>;// 256位雜湊值的vector,原始碼struct中沒有這句。
using Address = h160; // 160位的雜湊地址,原始碼struct中沒有這句。
Address address;
h256s topics;
bytes data;
};
至此,以太坊中一些重要的資料結構已經介紹完畢,通過這些可以對以太坊的區塊鏈有一個比較大致的瞭解。
參考閱讀:
以太坊的資料結構
以太坊MPT原理