區塊鏈入門文章--以太坊到底是如何運作的?


以太坊到底是如何運作的?

本文譯自這里,原作者 preethi,譯者博主本人。翻譯前已獲得原作者許可。

不管你知不知道以太坊區塊鏈 (Ethereum),你都有可能聽到過這個東西。最近以太坊經常出現在新聞里,有時還是一些主流雜志的封面新聞,但如果你對以太坊沒有基本常識,那這些文章讀起來就會覺得不知所雲。所以,這東西到底是什么?本質上,它是一個公共數據庫,用於永久性地記錄數字交易。關鍵是,這個數據庫不需要任何中央機構來維護或者保護它。相反地,它是一個“無需信任關系”的交易系統,也就是說在這個系統中,不必依靠一個可信的第三方或者其他類似機構,個人之間便可進行端到端的交易。

仍然感到很迷惑? 這就是寫這篇文章的來由了。我的目標是在技術層面上解釋以太坊是如何工作的,並避免使用復雜的數學或者讓人勸退的公式。不管你是不是程序員,我都希望你在看完文章后能更好地理解這一技術。如果某些內容涉及到太深的技術,很難完全理解,那也不用在意!真的不需要去理解每一個細節。我建議只需在一個寬泛的層面上理解這些東西即可。

本文涵蓋的許多議題,都是對以太坊黃皮書中所涉及概念的剖析。我添加了一些自己的見解和圖示使你更容易理解以太坊。敢於挑戰技術細節的讀者也可以直接去閱讀以太坊黃皮書。

現在讓我們開始吧!

區塊鏈定義

區塊鏈是一台“密碼學上安全的狀態開放共享的用於交易的單實例計算機”。是不是很繞口? 我們把它分解一下。

  • “密碼學上安全的”意味着在創建數字貨幣時,借助非常難以破解的復雜數學算法作為其保護措施。 概念上可以對比一下防火牆。這基本上就沒法去欺騙系統(如創建虛假的交易、刪除交易等)。
  • “用於交易的單實例計算機”意味着該計算機只有一個實例,其負責系統中所創建的所有交易。換而言之,每個人都信任同一個全局事實。
  • “狀態開放共享的”意味着這台計算機的狀態對每個人都是共享且開放的。

以太坊實現了這一區塊鏈模式。

解說以太坊區塊鏈模式

以太坊區塊鏈實際上是一台基於交易的狀態機。在計算機科學相關概念中,_狀態機_能讀取一系列的輸入,基於這些輸入其能轉換到新的狀態中。

image

以太坊狀態機是從“創世狀態”開始轉換的。這時網絡上還沒有進行任何交易,其就像一張白紙一樣。執行了若干交易后,就會從該創世狀態轉換到某種最終狀態中。在任何時候,當前以太坊的狀態都會是某種最終狀態。

image

以太坊的狀態由上百萬的交易構成。這些交易按照“區塊”分組。一個區塊包含一系列的交易,並且每個區塊與其前面一個區塊鏈接在一起。

image

有效的交易才能使狀態機從一個狀態轉換到下一個狀態。必須通過一個被稱為挖礦的流程來驗證交易是否有效。挖礦就是一組節點(即計算機)花費其計算資源來創建一個包含有效交易的區塊。

網絡上任何宣布自己是礦機的節點都可以嘗試去創建和驗證區塊。分布於全世界的大量礦機在同時做這件事。當將區塊提交到區塊鏈上時,每個礦機會提供一個數學上的“證明”,只要有這個證明,那對應的區塊就是有效的。

為將自己生成的區塊加入到主區塊鏈,礦機必須比其他與其競爭的礦機更快地得出證明。礦機提供數學證明來驗證每一個區塊這一過程被稱為“工作量證明”。

成功驗證一塊區塊的礦機將會獲得一定的獎勵。那獎勵是什么呢?以太坊區塊鏈使用一種內在的被稱為“Ether(以太)”的數字代幣。每當礦機證明一個區塊時,系統就生成一些 Ether 代幣並獎勵給它。

你可能會問是什么讓每個人都始終認可同一條區塊鏈?怎樣才能確保不會有一群礦機決定建立自己的區塊鏈?

在本文開頭處,我們將區塊鏈定義為一台狀態共享的用於交易的單實例計算機。在該定義下,我們可以明確區塊鏈的正確當前狀態是整個系統中存在唯一一個全局事實,每個用戶都必須認可這一點。當系統中存在多個狀態(或者鏈)時,由於沒法就哪一個才是正確的達成共識,所以會破壞掉整個系統。如果鏈出現分叉,你可能在這條鏈上有 10 代幣,那條鏈上有 20 個,另外一條鏈上還有 40 個。這種情況下,沒法確定哪條鏈是“最有效”的。

當產生多條路徑時,就會出現“分叉”。我們通常想避免分叉,因為這會使系統分裂,強迫人們選擇一條他們“信任”的鏈。

image

為確定哪條路徑是最有效的,並防止產生多條鏈,以太坊使用一種稱為“GHOST 協議”的機制。

“GHOST”= “Greedy Heaviest Observed Subtree”

簡單來說,GHOST 協議要求我們必須選擇那條已經完成了最多計算量的路徑。找出那條路徑的一種方式是使用最新區塊(“葉子區塊”)的區塊號,它表示當前這條路徑中的區塊總數(不計入創世區塊)。區塊號越大,說明從創世區塊到該葉子區塊的路徑越長,且用來挖礦的算力越多。我們可以利用這一推論就當前狀態的權威形式達成共識。

image

現在你已經大概了解了區塊鏈,讓我們深入一點看看以太坊所包含的主要概念:

  • 帳戶
  • 狀態
  • gas 和費用
  • 交易
  • 區塊
  • 交易執行
  • 挖礦
  • 工作量證明

開始探討前請注意:當我提及 X 的“哈希”時,我是指用以太坊的 KECCAK-256 算法計算出的哈希值。

帳戶

以太坊的全局“共享狀態”由許多小對象(帳戶)組成,它們能通過消息傳遞框架進行交互。每個帳戶擁有一個關聯的狀態和一個 20 字節的地址。以太坊中的地址是一個 160 比特的標識符,可以用來標識任何帳戶。

帳戶有兩種類型:

  • 外部帳戶(或者說外部持有帳戶),由私鑰所控制並且沒有關聯的代碼。
  • 合約帳戶,由對應合約代碼所控制並且有關聯的代碼。

image

外部帳戶 vs 合約帳戶

外部帳戶和合約帳戶之間有一個重要的本質區別需要了解。外部帳戶通過其私鑰創建並簽署交易后,從而向其他外部帳戶或者合約帳戶發送消息。在外部帳戶之間傳遞消息就是傳遞價值 (Ether)。但是當外部帳戶發送消息到合約帳戶時,就會激活合約帳戶的代碼進而執行各種操作(例如轉移代幣、寫入內部存儲、鑄造新代幣、做些計算以及創建新合約等等)。

合約帳戶不同於外部帳戶,它不能主動發起新的交易。只有在響應所接收到的交易(來自於外部帳戶或者其他合約帳戶)時,合約帳戶才能發起交易。我們將在“交易與消息”一節中進一步了解在合約之間發生的調用。

image

因此,外部帳戶發起的交易觸發了以太坊區塊鏈上的所有操作。

image

帳戶狀態

不管是哪種類型帳戶,其帳戶狀態都包括四個部分:

  • nonce:若是外部帳戶,其表示從該帳戶地址已發起的交易數量。而若是合約帳戶,則其表示該帳戶已創建的合約數量。
  • balance:該地址的余額,以 Wei 為單位。每個 Ether 為 1e+18 Wei。
  • storageRoot:Merkle Patricia 樹根節點的哈希(我們稍后解釋什么是 Merkle 樹)。這棵樹編碼了該帳戶相關存儲內容的哈希,默認情況下是空的。
  • codeHash:該帳戶相關的 EVM 代碼的哈希。EVM 指 Ethereum Virtual Machine,后文有進一步講述。對於合約帳戶,codeHash 就是該代碼的哈希。對於外部帳戶,codeHash 為空字符串的哈希。

image

全局狀態(世界狀態)

現在我們知道以太坊的全局狀態包括了在帳戶地址和帳戶狀態之間形成的映射關系。這一映射關系存儲在 Merkle Patricia 樹這種數據結構中。

Merkle 樹是一種二叉樹(譯者注:1. 以太坊采用一種修改過的 Merkle Patricia 樹,其結合了 Merkle 樹和前綴樹的優點;2. Merkle 樹不一定要是二叉樹,但二叉樹最常見),包含這些節點:

  • 在樹的底部有大量包含底層數據的葉節點
  • 一些中間節點,每個節點的值都是其兩個子節點值的哈希
  • 一個根節點,位於樹頂端,其值也是兩個子節點值的哈希

image

將待存儲的數據分解成一個個_塊_,再將這些塊分成若干_桶(bucket)_,計算出每桶的哈希,然后根據這些哈希算出上一層節點的哈希。不斷重復這個過程,直到只剩下一個哈希:根哈希

image

在這棵樹中存儲的每一個值都需要有一個鍵。從根節點開始,通過鍵來找到下一個中間節點,重復這個過程直至找到目標葉節點,其保存有關聯的數據。在表示以太坊全局狀態的樹結構中,鍵/值映射關系是指地址映射到其對應帳戶狀態(包括 balance,nonce,codeHash 以及 storageRoot,其中 storageRoot 本身也指向一棵樹)。

image

來源:以太坊白皮書

在保存交易和回執時也用了同樣的數據結構。具體來說,每一個區塊都有一個“頭部”,其中保存有三個根節點的哈希,對應於三棵不同的 Merkle 樹,包括:

  1. 狀態樹
  2. 交易樹
  3. 回執樹

image

在以太坊中,對於“輕量級客戶端”或者“輕量級節點”來說,能通過 Merkle 樹高效存儲所有這些信息是非常有幫助的。要記得區塊鏈是由一堆節點共同維護的。寬泛地說,節點有兩種類型:完整節點和輕量級節點。

建立完整歸檔節點時,需要下載從創世區塊到當前頭部區塊這一整條鏈,並執行其中所有交易,以保持與區塊鏈同步。通常,為進行挖礦,礦機需要存儲完整歸檔節點。也可以在下載完整節點同時只執行部分交易。但不管怎樣,只要是完整節點,都會包含整條鏈。

除非節點需要執行所有交易或者很輕松地查找歷史數據,否則真的沒有必要存儲整條鏈。所以就提出了輕量級節點這一概念。創建輕量級節點時,只用下載從創世區塊到當前頭部區塊這一條鏈上的所有區塊頭部,無需下載存儲整條鏈、執行任何交易以及檢索任何相關狀態。由於輕量級節點能訪問區塊頭部中的三棵樹根節點的哈希值,所以為交易、事件和余額等相關操作接收和創建可驗證的應答也還是很容易的。

這一點可行是因為 Merkle 樹中的哈希會向上傳播:如果惡意用戶將 Merkle 樹底部的一筆交易換為虛假交易,那么這一修改將導致上層節點哈希發生變化,從而又導致上層節點的上層節點哈希發生變化,最終將使樹的根節點哈希發生變化。

image

任何節點都可以通過“Merkle proof”來驗證一組數據。Merkle proof 包括以下幾點:

  1. 一塊待驗證的數據以及其哈希
  2. 樹的根節點哈希
  3. 相關“分支”(從這塊數據到根節點這一路徑上所經過的節點及相鄰節點)

image

任何人在閱讀證明時,都可以驗證是否那一分支上從底層節點到根節點間的哈希始終與樹上對應哈希保持一致,從而確定所驗證的數據塊在樹上的位置是否正確。

總之,使用 Merkle Patricia 樹的好處是,從密碼學角度來說,這一結構的根節點依賴於樹中存儲的數據,因此根節點的哈希可以用於標識這一數據是否安全。由於區塊頭部包含了狀態樹、交易樹、回執樹的根節點哈希,那當每個節點驗證以太坊中一小部分狀態時,只需一小部分數據即可,而不用存儲大小上可能無限增長的完整狀態。

Gas 和支付

以太坊中一個非常重要的概念是費用。在以太坊網絡上執行交易時產生的每一次計算都需要支付一筆費用(沒有免費的午餐!)。這類費用是按照“gas”這種面額來支付的。

Gas 是用於衡量特定計算所需費用的單位。Gas 單價是你願意為每單位 gas 支付的 Ether 數量,以“gwei”為單位。“Wei”是 Ether 的最小單位,10¹⁸ Wei 等同於 1 Ether。1 gwei 等同於 1,000,000,000 Wei。

在每筆交易中,發送方設定一個 gas 額度gas 單價。發送方為執行一筆交易,最多支付等同於 gas 單價gas 額度之積的費用,以 Wei 為單位。

例如,如果發送方設定 gas 額度為 50,000,gas 單價為 20 gwei。這表示發送方為執行這筆交易,最多願意支付 50,000 x 20 gwei = 1,000,000,000,000,000 Wei = 0.001 Ether。

image

要注意,gas 額度表示發送方願意為最多多少 gas 而付費。如果他們帳戶余額中有足夠的 Ether 來承擔這一額度的開銷,那就沒有任何問題。當交易結束時,所有尚未用完的 gas 都將按照交易發起時的費率換算為 Wei,並返還給發送方。

image

如果在執行交易時,發送方沒有 提供足夠的 gas,那么該交易會“耗盡 gas”,導致系統判定該交易無效。在這種情況下,將終止交易執行,並且因這一交易而改變的所有以太坊狀態都將恢復到執行該交易前的初始狀態。此外,關於此次交易失敗的記錄將被存儲下來,其中記錄了該交易嘗試做什么以及在哪里失敗了。並且由於系統在用完 gas 之前已經消耗了一些資源來做計算,因此,不會將 gas 退還給發送方。

image

那在 gas 上花費的錢去了哪里呢?發送方購買 gas 時花的所有錢都轉移到了“受益人”所在地址,通常就是礦機所在地址。因為礦機在執行計算和驗證交易時消耗了資源,所以會獲得對應 gas 費用以作為獎勵。

image

通常,如果發送方願意為 gas 支付更高單價,那么礦機就能從交易中獲得更多利潤。因此,礦機就越可能選擇這筆交易。這樣,礦機就能自由地選擇驗證或者忽略哪些交易。為了向發送方提供 gas 參考單價,礦機可以將執行交易時他們所接受的最低 gas 單價公布出來。

存儲數據也需要費用

不僅要為計算支付 gas,存儲數據也需要支付 gas。存儲所需費用與所存儲數據的大小呈正比,計算數據大小時以 32 字節為單位,不到 32 字節的部分忽略不計。

存儲數據相關費用有一些微妙的特性。例如,由於存儲數據會導致_所有_節點上的以太坊狀態數據庫同時增大,所以就需要將存儲的數據量保持在一個盡可能小的規模上。因此,如果交易中包含了一項操作來清除存儲空間中的條目,那么執行該操作就是免費的,所免除的費用將退還回來。

收費的目的是什么?

以太坊工作方式的一個重要特征是,網絡上執行的每個操作同時作用於每個完整節點。然而,以太坊虛擬機所執行的計算性操作非常昂貴。因此,以太坊智能條約最好用於執行簡單的任務,比如運行簡單的商業邏輯或者驗證簽名和其他加密對象,而不是用在更加復雜的用途上,比如文件存儲、電子郵件或者機器學習,這會對網絡造成壓力。收費能防止用戶濫用網絡資源。

以太坊的語言是圖靈完備的。(簡單地說,圖靈機可以模擬任何計算機算法,如果你不熟悉圖靈機,看看這里還有這里)。所以程序中可以執行循環操作,使以太坊容易受停機問題的影響,這個問題是說你沒法確定一個程序會不會永遠執行下去。如果不用付費,惡意用戶就能通過在交易中執行一個無限循環來破壞網絡的運作,卻不會受到任何懲罰。因此,收費能防止網絡被蓄意攻擊。

你可能會想,“為什么我們存儲數據時也需要付費?”唔,就如計算一樣,在以太坊網絡上存儲數據時整個網絡都需要承擔對應的開銷。

交易與消息

我們先前有提到以太坊是一台基於交易的狀態機。換而言之,不同帳戶之間進行的交易推動了以太坊全局狀態的變化。

從最基本的角度來看,交易是一條由外部帳戶所創建的加密簽名過的指令,被序列化並提交到區塊鏈上。

交易有兩種類型:消息調用合約創建(即用於創建新的以太坊合約的交易)。

不管是什么類型的交易,都包含以下信息:

  • nonce:發送方已發送的交易數量。
  • gasPrice:為執行這一交易,發送方願意為此支付的 gas 單價。
  • gasLimit:為執行這一交易,發送方願意為此支付的最大 gas 額度。在進行相關計算前,需要提前設定並支付這一數量的 gas。
  • to:接收方的地址。在合約創建類型的交易中,合約帳戶還不存在,所以使用一個空地址來代替。
  • value:發送方轉給接收方的金額,以 Wei 為單位。在合約創建類型的交易中,該金額作為新建的合約帳戶中的初始余額。
  • v, r, s:用於產生一個標識該交易發送方的簽名。
  • init(只存在於合約創建類型的交易中):用於初始化新合約帳戶的 EVM 代碼片段。init 只運行一次,然后就被丟棄掉。當 init 第一次運行時,它返回與該合約帳戶永久關聯的代碼。
  • data (可選字段,且僅存在於消息調用這一類型的交易中):消息調用所需要的輸入數據(即參數)。例如,如果一個智能合約提供域名注冊服務,為調用該合約就需要一些輸入數據,比如域名和 IP 地址。

image

我們在“帳戶”一節中了解到了交易(不管是消息調用還是合約創建)總是由外部帳戶所創建並提交到區塊鏈上。如果站在另一個角度來理解這一點,可以說交易是外部世界信息通往以太坊內部的橋梁。

image

但這不是說合約之間不能通信。在以太坊狀態中全局作用域內的合約可以與在相同作用域內的其他合約通信。它們向其他合約傳遞“消息”或者“內部交易”來實現通信。我們可以認為消息或者內部交易類似於交易,主要的區別在於它們不是由外部帳戶所創建的。反而它們是由合約所創建的。它們是虛擬對象,不同於交易的是它們不會被序列化並且只存在於以太坊執行環境中。

當一個合約向另一個合約發送一次內部交易時,將激活接收方合約帳戶的關聯代碼。

image

有個重點需要注意,內部交易或消息中不包含 gasLimit 這一信息。這是因為 gas 額度是由最初那條交易的外部創建人(即某個外部帳戶)所設定的。外部帳戶設定的 gas 額度必須能足以執行此交易,以及由此交易衍生出來的任何操作,例如合約之間的消息傳遞。在連續有序地執行交易和消息時,如果某個消息執行時用完了 gas,那么將撤銷執行該消息以及因執行該消息而觸發的任何后續消息。但是,不需要撤銷上一級的執行。

區塊

所有交易都按照一個個“區塊”組織起來。一條區塊鏈包含了一系列這樣的被鏈接起來的區塊。

在以太坊中,一個區塊包含了:

  • 區塊頭部
  • 這一區塊中的交易集的相關信息
  • 當前區塊的叔塊 (ommer) 頭部集。

叔塊解釋

叔塊是什么鬼東西?如果一個區塊的父塊等同於當前區塊的父塊的父塊,就說這個區塊是當前區塊的叔塊(譯者注:還可以將叔塊概念再往上推一些,在 6 代以內都算叔塊)。我們來快速了解一下叔塊是做什么的,以及為什么區塊中要包含叔塊頭部。

由於以太坊的構建方式有些不太一樣,所以相比於其他類型區塊鏈,其出塊時間縮短至大概 15 秒,像比特幣需要大概 10 分鍾。因此以太坊處理交易起來就更快。然而,更短的出塊時間帶來的一種負面影響是,會有更多礦機找到有效的區塊,但只有一個區塊能夠上鏈,因而也產生了更多上鏈失敗的區塊。這些上鏈失敗的區塊也被稱為“孤塊”(即開采出來的區塊沒法與主鏈連接在一起)。

叔塊存在的目的是為了給那些引用了這些孤塊的礦機一些回報。礦機引用的叔塊必須是“有效”的,也就是說,叔塊與當前區塊間隔不能超過六代。超出六代以后,舊孤塊就不能再被引用了(因為引入比其更早的交易會讓事情變得有些復雜)。

引用叔塊比采到新區塊所獲的獎勵要少一些。盡管如此,某種程度上,仍然能鼓勵礦機去引用這些孤塊來獲取獎勵。

區塊頭部

我們返回來探討下區塊。我們先前提到每個區塊都有一個區塊“頭部”,但它具體是什么呢?

區塊頭部是區塊的一個部分,包括:

  • parentHash:父塊頭部的哈希(這一字段使這些區塊構成了一條“鏈”)
  • ommersHash:該區塊所引用的叔塊列表的哈希
  • beneficiary:帳戶地址,開采該區塊所獲得的回報會轉入到該地址中
  • stateRoot:狀態樹根節點的哈希(回想下我們對狀態樹的理解,它是怎樣存儲在頭部中的,以使得輕量級客戶端能輕松驗證與狀態相關的所有信息)
  • transactionsRoot:交易樹根節點的哈希,該樹記錄了這個區塊中的所有交易
  • receiptsRoot:回執樹根節點的哈希,該樹記錄了這個區塊中所有交易的回執
  • logsBloom:一個記錄了日志信息的布隆過濾器(數據結構)
  • difficulty:開采這一區塊的難度
  • number:當前區塊的編號(創世區塊的區塊號為 0,其后每個區塊的編號按 1 遞增)
  • gasLimit:當前每個區塊的 gas 額度
  • gasUsed:該區塊中的交易所使用的 gas 總額
  • timestamp:該區塊創建時的 unix 時間戳
  • extraData:與該區塊相關的其他數據
  • mixHash:哈希值,和 nonce 一起使用可以證明在這個區塊上已經執行了足夠多的計算
  • nonce:哈希值,和 mixHash 一起使用可以證明這個區塊執行了足夠多的計算

image

要注意每個區塊頭部是怎樣為這些數據保存三個樹形數據結構的:

  • 狀態(stateRoot
  • 交易(transactionsRoot
  • 回執(receiptsRoot

這些樹形數據結構就是我們之前討論過的 Merkle Patricia 樹。

此外,在上面的描述中,有一些術語值得去說明一下。我們去看一下。

日志

我們可以通過通過日志來跟蹤以太坊中的各種交易和消息。合約可以通過定義它希望記錄的“事件”,來顯式地產生日志。

一項日志條目包括:

  • 日志記錄器的帳戶地址
  • 一組主題 (topic),表示該交易產生的各種事件,以及
  • 與這些事件相關的所有數據

日志由布隆過濾器記錄下來,該結構能有效地管理源源不絕的日志數據。

交易回執

頭部中的日志來自於交易回執中所包含的日志信息。就像你在商店購物時會收到一份回執單一樣,以太坊每次交易都生成一份回執。如你所想的那樣,回執中總會包含有關交易的一些信息。該回執包含的項目類似這樣的:

  • 該交易所在區塊的編號
  • 該交易所在區塊的哈希
  • 該交易的哈希
  • 該交易所使用的 gas 數額
  • 該交易執行后,其所在區塊累計消耗的 gas 數額
  • 該交易執行時產生的日志
  • ...等等

區塊難度

區塊的難度是用來保證我們以穩定的速率去驗證區塊。創世區塊的難度為 131,072,對於在其后的區塊,使用一個特殊的公式來計算對應難度。如果某個區塊的驗證速度比前面的區塊更快,以太坊協議就會增加下一個區塊的難度。

開采區塊必須要計算出 nonce 這個哈希值,而區塊的難度會通過工作量證明算法影響 nonce 的計算難度。

區塊的難度nonce 的關系在數學上可以形式化為:

image

這里, Hd 表示難度。

尋找滿足難度要求的 nonce 的唯一方法是,通過工作量證明算法來枚舉所有可能的解。尋找答案時所需的期望時長與難度成正比:難度越高,越難找到 nonce,從而就越難去驗證區塊,結果就是耗費了更長時間去驗證新的區塊。因此,協議可以通過調整區塊難度來調整區塊的驗證時長。

從另一方面來講,如果驗證時長變長了,協議就會降低難度。通過這種方式,系統就會自行調整驗證時間,以保持恆定的速率:平均情況下每 15 秒出一個區塊。

交易執行

現在我們來了解以太坊協議中的一個最復雜的部分:交易的執行。假設你將一個待處理的交易發送到以太坊網絡中。為接收你的交易,以太坊的狀態會發生怎樣的變化?

image

首先,為能執行交易,交易本身必須滿足一系列的初步要求。包括:

  • 這個交易必須按照 RLP 編碼做了正確的格式化。“RLP”表示“Recursive Length Prefix(遞歸長度前綴編碼)”,是一種用於格式化由二進制數據組成的嵌入數組的數據編碼。以太坊使用 RLP 編碼來序列化對象。
  • 有效的交易簽名。
  • 有效的交易 nonce。回想一下,nonce 表示該帳戶已發送的交易數量。交易中的 nonce 若要有效,其必須等同於發送方帳戶的 nonce。
  • 交易的 gas 額度必須大於或等於其所消耗的固有的 gas 數額。固有的 gas 數額包含:
  • 一筆 21,000 gas 的手續費,用於執行該交易
  • 一筆用於在交易中傳輸額外信息的 gas 費用(不管是表示數據還是代碼,對於值為 0x0 的字節,收取 4 gas 每字節,否則收取 68 gas 每字節)
  • 如果該交易是用於創建合約的,還需額外 32,000 gas

image

  • 發送方在帳戶中要有充足余額來支付所必需的 gas“預付”費用。Gas 預付費用的計算很簡單:首先,將交易的 gas 額度乘以該交易的 gas 單價得出最大 gas 成本。然后,將這個最大成本與發送方轉給接收方的金額加總起來,這一總額就是 gas 預付費用。

image

如果這個交易滿足上述所有要求,是一個有效交易,那我們就繼續來看。

首先,我們從發送方的余額中減去預付的 gas 費用,並且發送方帳戶的 nonce 需要加 1 以計入當前交易。這時,我們可以將總 gas 額度減去固有的 gas 數額,算出剩余的 gas 數額。

image

接下來,這個交易就開始執行了。在執行該交易的過程中,以太坊一直維護着一個“子狀態”。通過子狀態,以太坊可以記錄在交易執行期間產生的信息,並且在交易執行完成后馬上就需要用上這些信息。它具體包括這些內容:

  • 自毀帳戶集:在交易執行完成后將被銷毀的一組帳戶(如果有的話)。
  • 日志集:EVM 虛擬機執行代碼時的檢查點,已歸檔並且可索引。
  • 返還余額:交易執行結束后,返還到發送方帳戶的金額。還記得我們先前所說的,在以太坊中存儲數據需要付費,而發送方清空數據時可以獲得一些退款嗎?以太坊使用退款計數器維護這一信息。退款計數器從零開始,每次合約刪除一些存儲內容時其值都會增加。

然后,執行該交易中的各類計算。

當順利處理完該交易中的所有必需步驟后,明確需要返還給發送方的 gas 余額,讓該交易的關聯狀態最終確定下來。除了返還未使用的 gas 外,還會從我們先前提到的“返還余額”中取出部分作為津貼,補償給發送方。

當發送方獲得返還后:

  • 向礦機支付 gas 費用
  • 將該交易消耗的 gas 數額加總到其所在區塊的 gas 累計值上(該值記錄了在該區塊中所有交易所消耗的 gas 總額,用於驗證區塊)
  • 刪除自毀賬戶集中的所有帳戶(如果有的話)

最終,我們創建了一個新的狀態,並保留了該交易生成的日志。

我們已經了解了交易執行的基礎內容,現在我們去看看用於創建合約的交易與用於調用消息的交易之間有什么不同。

合約創建

回想一下,以太坊中有兩種類型的帳戶:合約帳戶和外部帳戶。當一個交易被稱作是“用於創建合約的”,我們是指這個交易的目的是創建一個新的合約帳戶。

為創建一個新的合約帳戶,首先我們用一個特殊的計算公式算出新帳戶的地址。然后我們按照下面這些步驟初始化新帳戶相關數據:

  • 將 nonce 設置為零
  • 如果發送方通過交易中的 value 字段轉移了一些 Ether 過去,則將新帳戶的余額設定為該值
  • 從發送方帳戶的余額中減去已轉給新賬戶的數額
  • 清空新賬戶存儲空間
  • 將該合約的 codeHash 字段設定為空字符串的哈希

當我們初始化完成后,就能通過交易中的 init 代碼真正地建立帳戶(回顧“交易與消息”一節,去復習一下 init 代碼這一概念)。不同場景下,init 代碼實現的功能並不相同。它可能會更新該帳戶的存儲內容,創建其他合約帳戶,發起其他消息調用,等等,這取決於該合約的構造器是如何實現的。

初始化該帳戶時會消耗 gas。不允許該交易超額使用 gas。如果超額了,執行時就會遇到 gas 耗盡異常(out-of-gas exception,OOG exception),然后就會退出執行。如果因遇到 gas 耗盡異常導致該交易終止,那么系統就會恢復到剛准備執行該交易時的狀態。先前執行時已經消耗的 gas _不會_退還給發送方。

啊這!

但是,如果發送方在交易中傳遞了一些 Ether,即便沒能成功創建這個合約,這些 Ether 還是會退還回來的。還好還好!

如果初始化代碼執行成功,那最后還需要為合約創建付一筆費用。這筆費用用於存儲,其與新建合約的代碼大小成正比(再說一次,沒有免費的午餐!)。如果 gas 余額不足以支付最后這筆費用,那該交易會再次遇到 gas 耗盡異常,並終止執行。

如果一切進展順利,那些尚未用完的 gas 余額會退還給最初發起該交易的發送方,現在可以持久地保存更新后的狀態了。

歡呼吧!

消息調用

除了一點點差別外,調用消息與創建合約是類似的。

因為不用創建新賬戶,所以消息調用中不會包含任何 init 代碼。但是,它可以包含交易發送方提供的輸入數據。在執行時,消息調用還有一個包含了輸出數據的部分,后續執行中可以使用這一數據。

就如合約創建一樣,如果執行消息調用時,gas 不足或者交易無效(例如棧溢出、無效的跳轉目標或者無效的地址),導致執行中斷,已經消耗掉的 gas 不會返還給最初的調用方。相反,所有未使用的 gas 余額都會被用掉,並且系統狀態將回到剛准備轉帳時的那一刻。

在目前最新的以太坊版本以前(譯者注:在拜占庭升級以前),只有消耗掉所有你提供的 gas,系統才能停止或撤銷交易的執行。假如說,你寫了一個合約,在調用該合約時,如果調用者沒有權限執行某一交易,那么它會拋出一個錯誤。在以前的以太坊版本中,如果合約拋出了錯誤,剩余的 gas 都會被用掉,而不會返還給發送方。但是拜占庭升級引入了一個新的操作碼“revert”,使得合約可以停止運行並撤銷對系統狀態的更改,不需要消耗剩余的 gas,而且在交易執行失敗時可以返回對應的原因。如果交易因撤銷操作而結束,那么尚未使用的 gas 將返還給發送方。

執行模型

到目前為止,我們已經了解了交易從開始執行到結束所必需進行的一系列步驟。現在,我們來看看在虛擬機中是怎樣實際執行交易的。

協議中實際執行交易的組件被稱為以太坊虛擬機(EVM),是以太坊自建的。

就如先前定義的那樣,EVM 是一種圖靈完備的虛擬機。相比於典型的圖靈完備的機器,EVM 的唯一限制是它實質上受 gas 的制約。因此,所提供的 gas 數額實質上限定了所能做的計算量。

image

來源:CMU

此外,EVM 的架構是基於棧的。基於堆棧的計算機使用先進、后出的棧來保存臨時數據。

EVM 中,棧中的每個數據項為 256 bit,最多能容納 1024 個數據項。

EVM 具備內存,其中數據內容存儲為按字尋址的字節數組形式。內存中數據是易失的,也就是說它不是永久的。

EVM 也有存儲設備。不同於內存,存儲設備中的數據是非易失的,這些數據作為整個系統狀態的一部分,在維護時都會考慮進來。EVM 將程序代碼單獨存儲在一個虛擬的 ROM 中,只能通過特殊指令來訪問。在這方面,EVM 不同於典型的馮諾依曼架構(在這種架構中,程序代碼在內存或者存儲設備中都可以保存)。

image

EVM 也有自己的語言:“EVM 字節碼”。當像你我這樣的程序員編寫在以太坊上運行的智能合約時,我們通常會通過比如 Solidity 這種處於更高層次的語言來寫程序。隨后我們能將其編譯為處於更低層次的 EVM 能夠理解的字節碼。

好了,現在來執行一下。

在執行特定計算前,處理器需要確保下面這些信息存在且有效:

  • 系統狀態
  • 用於計算的 gas 余額
  • 帳戶地址,這個帳戶擁有當前正在執行的代碼
  • 發送方地址,該發送方發起了一個交易,是導致此代碼被執行的根源
  • 帳戶地址,這個帳戶觸發了該代碼的執行(可以不同於最初的那個發送方)
  • 交易的 gas 單價,該交易是導致此代碼被執行的根源
  • 這次執行的輸入數據
  • 在這次執行中,轉給該帳戶的金額(以 Wei 為單位)
  • 待執行的機器碼
  • 當前區塊的頭部
  • 當前調用消息或創建合約時所使用的棧的深度

剛開始執行時,內存和棧都是空的,並且程序計數器為零。

PC: 0 STACK: [] MEM: [], STORAGE: {}

隨后 EVM 循環地執行這個交易,每個循環都需要計算系統狀態機器狀態。系統狀態就是指以太坊的全局狀態。而機器狀態包括:

  • 可用的 gas 額度
  • 程序計數器
  • 內存中的內容
  • 內存中當前活躍的字數
  • 棧中的內容

從最左邊添加或者刪除棧中的數據項。

每次循環都會從 gas 余額中減去適當數額,同時增加程序計數器的值。

在每個循環結束時,有三種可能:

  1. 機器進入了異常狀態(比如 gas 不足、無效指令、棧中缺失項目、存儲棧項目時超出了 1024 這一限制、無效的 JUMP/JUMPI 目標等等),因此必須停機,並撤銷所有的狀態更改。
  2. 機器繼續執行下一個循環
  3. 機器進入受控停機狀態(執行結束)

如果執行過程中沒有遇到異常,進入了“受控”或正常停機狀態,機器將生成最終的狀態數據,算出此次執行后的 gas 余額,生成所積累的子狀態數據以及最終的輸出。

呼,我們了解完了以太坊最復雜的一個部分。即便你沒有完全理解這個部分也是沒有關系的。除非你在工作中需要接觸很底層的內容,否則你_真的_不需要這么深入地理解其執行細節。

怎樣確定一個區塊

最后,我們來看看由大量交易組成的區塊是怎樣確定下來的。

我們所說的“確定”有兩種含義,其理解取決於這個區塊是新的還是現存的。如果是一個新區塊,我們是指開采該區塊的過程。如果是一個現存的區塊,那我們就是指驗證該區塊的過程。不管哪種情況,為“確定”一個區塊,都要滿足四個要求:

1) 驗證(對於開采新區塊來說,是確定)叔塊
該區塊頭部中的叔塊字段必須有效,並且必須離該區塊距離不超過六代。

2) 驗證(對於開采新區塊來說,是確定)交易
該區塊中的 gasUsed 字段必須要等於該區塊中包含的所有交易總共消耗的 gas 數額。(回想一下,執行交易時,我們會跟蹤其所在區塊的 gas 累計值,其記錄了該區塊中所有交易總共消耗的 gas 數額。)

3) 給予獎勵(只針對開采新區塊)
為獎勵挖出這個區塊,系統轉 5 Ether 到該區塊的受益人地址上。(根據以太坊提議 EIP-649,這 5 ETH 的獎勵不久將縮減為 3 ETH。)此外,對於當前區塊引用的每個叔塊,受益人會獲得當前區塊獎勵的 1/32 作為額外獎勵。最后,叔塊的受益人也會獲得一些獎勵(有一個特殊公式用來計算對應數額)。

4) 驗證(對於開采新區塊來說,是計算出一個有效的)狀態和 nonce
確保應用了所有交易以及對應的狀態更改,並將新區塊的最終狀態定義為,當其中最后一個交易完成了獎勵頒發之后的狀態。驗證時,檢查對比這一最終狀態與頭部中保存的狀態樹即可。

工作量的采礦證明

_“區塊”_那一小節簡要地解釋了區塊難度的概念。區塊難度的實質是工作量證明算法(PoW)。

以太坊的工作量證明算法叫做“Ethash”(以前叫做 Dagger-Hashimoto 算法)。

形式化定義該算法為:

image

這里 m 表示 mixHashn 表示 nonceHn 表示新區塊頭部(除去必須要算出來的 nonce mixHash 字段),Hn 表示區塊頭部的 nonce,以及 d 表示 DAG(一個大型數據集)。

在“區塊”一節中,我們討論了區塊頭部中包含的各個字段。其中有兩個字段分別為 mixHashnonce。就如你可能回想起的那樣:

  • mixHash 是一個哈希值,和 nonce 一起使用可以證明在這個區塊上已經執行了足夠多的計算
  • nonce 是一個哈希值,和 mixHash 一起使用可以證明在這個區塊上已經執行了足夠多的計算

PoW 函數用於計算這兩個字段。

通過 PoW 函數准確計算出 mixHashnonce 的過程有些復雜,深入解釋的話足夠我們開一篇新帖子了。但是如果站在一個較高的層面來看,它的方式差不多是這樣的:

對於每個區塊都會計算出一個“種子”。不同紀元(每個“紀元”跨度為 30,000 個區塊)的種子值都不一樣。對於第一個紀元,種子值為哈希計算一個長為 32 個字節且值均為零的字節串的結果。而后續每個紀元的種子都是從前一個紀元的種子哈希計算后得來。通過這類種子節點能計算出一個偽隨機的“緩存”。

這個緩存對我們構建“輕量級節點”(在本帖前面有討論過)非常有幫助。輕量級節點不用耗費資源來存儲整個區塊鏈數據集,就能有效地驗證交易。因為通過這一緩存可以重新生成需要驗證的那個區塊,所以僅僅依靠這一緩存就足夠了。

節點通過這一緩存可以生成 DAG“數據集”,在該數據集中,每個項目僅依賴於從這一緩存中偽隨機選擇的少量項目。要想搭建礦機,你必須生成這一完整的數據集;所有完整客戶端和礦機都要存儲這個完整的數據集,並且數據集規模會隨着時間線性增長。

隨后礦機可以從該數據集中隨機選取一部分,並通過一個數學函數將它們一起進行哈希計算,得出“mixHash”這一值。礦機將會重復這一生成 mixHash 的過程,直到其小於目標值 nonce。當輸出值滿足要求時,該 nonce 值就是有效的,就可以將這個區塊添加到鏈上。

挖礦是一種安全機制

整體上來說,PoW 的目標是,通過一種密碼學上安全的方式,來證明礦機為得到一些輸出內容(即 nonce),已經花費了某一數量的算力。這是因為除了枚舉所有的可能情況外,沒有更好的方法來尋找低於目標門限值的 nonce。重復應用哈希函數所獲得的輸出服從一個均勻分布,所以我們得以保證,在平均情況下尋找這樣一個 nonce 所需的時間依賴於難度門檻值。難度越高,找到這種 nonce 就要越久。這樣,PoW 算法使得難度這一概念有了實際意義,因而就能用來增強區塊鏈的安全性。

我們所說的區塊鏈安全性是什么意思?很簡單:我們希望創建一條人人都信任的區塊鏈。就如我們先前在帖子里提及地,如果存在有多條鏈,那么將失去用戶的信任,因為他們沒法合理地判定哪條鏈才是“有效”的。為讓用戶群體接受在鏈上保存的狀態,我們需要且只要一個大家都信任的官方區塊鏈。

這就是 PoW 算法的作用:它保證了不管在什么時候某條區塊鏈一直是權威的,這樣攻擊者就很難創建新的區塊去覆蓋某部分歷史記錄(比如通過刪除交易或者創建虛假交易)或者維持一條分叉。攻擊者為搶先驗證自己的區塊,他們必須總是比網絡中其他所有節點更快地找到 nonce,使得網絡上所有節點都相信他們那條鏈是占最大權重的(基於我們先前提及的 GHOST 協議中的原理)。這是不可能的,除非攻擊者占據了一半多的全網挖礦算力,這種場景被稱為 51% 攻擊

image

挖礦是一種財富分配機制

PoW 算法不僅為區塊鏈提供了安全保障,還是一種財富分配方式,它將財富分配給了那些付出了自己的算力來保護區塊鏈的人們。回憶下,礦機開采出區塊時會獲得如下獎勵:

  • 當開采到“獲勝”區塊時(成功使區塊連上主鏈),有 5 Ether 的_固定獎勵_(很快就要改為 3 Ether 了)
  • 包含在該區塊中的所有交易所消耗的 gas 費用
  • 讓該區塊引用叔塊會獲得額外的獎勵

為確保用於保障安全和分配財富的 PoW 共識機制能長期堅持實行下去,以太坊努力發揚這兩點:

  • 讓盡可能多的人能夠使用它。換而言之,人們不需要用特殊或者不常見的硬件來執行這個算法。其目的是讓財富分配模型盡可能地開放,使得每個人在獲得 Ether 的同時能提供些算力作為報答,不管是多少算力。
  • 使某單一節點(或者一小撮節點)難以獲取不合理的利潤份額。如果有節點能獲取不合理的利潤份額,就意味着在確定權威的區塊鏈時,該節點有巨大的影響力。這令人討厭,因為它破壞了網絡的安全。

在比特幣區塊鏈網絡中出現的一個與上面兩個特性有關的問題是,其 PoW 算法是一個 SHA256 哈希函數。這種類型的函數其弱點是,通過特殊硬件(也稱為 ASIC),相比於其他普通硬件,能更有效地計算出其函數值。

為了緩解這一問題,以太坊選擇使其 PoW 算法(Ethhash)變的 sequentially memory-hard。這就是說這個算法經過了調整,使得在計算 nonce 時需要大量的內存和帶寬。由於需要大內存,所以計算機就很難並行地使用內存去同時找出多個 nonce,同時由於還需要大帶寬,就使得甚至是對於一台超快的計算機,都很難同時找出多個 nonce。這就減少了中心化的風險,同時為那些做驗證工作的節點創建了一個更加公平公正的競爭環境。

有一點需要提醒一下,以太坊正在從 PoW 共識機制遷移到“權益證明 (proof-of-stake)”共識機制上。這個話題本身很麻煩了,希望我們能在將來某個帖子中探討這個話題。☺️

總結

……呼!你把這篇帖子看完了。我想應該是的吧?

我知道,這篇帖子里有很多需要消化一下的內容。如果你為了能完全理解這里的內容,反復讀了好幾遍,那也沒什么關系。在能夠深刻理解這些東西前,我自己就讀了好幾次以太坊黃皮書、白皮書還有官方代碼庫的許多部分。

然而,我還是希望這篇概述能幫到你。如果你找到了任何不對的地方,我希望你能寫到自己的筆記上或者直接在評論中發出來。所有發上來的評論我都會看的,我保證 😉

要記得,我是人類(對,你沒看錯),所以我也會犯錯。我花時間寫這篇帖子是因為這對社區有幫助,這是免費的。所以拜托在反饋時提些有用的建議,而不是在那里抬杠亂噴。

想發些評論?點這里,然后拉到頁面底部就好。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM