V神設計理念公布,細數以太坊潛在的優缺點

作者: Vitalik;翻譯&校對: kim & 阿劍

從某些協議功能的處理方法上來說,以太坊與常見密碼學貨幣運行方式仍有許多不同。

編者註:本譯文的首個版本見於此處,本次再版已經過校對。在此對原譯者 kim 表示感謝。
原文的寫作時間不確定,但可將其視為一個原點,反思以太坊的設計理念以及以太坊在這幾年間的演化。既是反思其創新,也是反思其有欠考慮的地方。

儘管以太坊的許多理念在早先的密碼學貨幣(如比特幣)上已經運用並測試了5年之久,但從某些協議功能的處理方法上來說,以太坊與常見方式仍有許多不同。而且,以太坊可用於開發全新的經濟工具,因為它具有其他系統不具備的許多功能。本文會詳細描述以太坊所有的潛在優點,以及在構建以太坊協議過程中某些有爭議的地方。另外,也會指出我們的方案及替代方案的潛在風險。

原則

以太坊協議的設計遵循以下幾點原則:

  • 三明治複雜模型(亦可譯為 “複雜度分層模型” ):我們認為以太坊的底層協議應儘可能的簡單,接口設計應易於理解(不論是面向開發者的高級編程語言接口,還是面向用戶的使用接口)。那些不可避免的複雜部分應放入中間層。中間層不作文核心共識的一部分,且對最終用戶不可見,它包含:高級語言編譯器、參數序列化和反序列化腳本、存儲數據結構模型、leveldb 存儲接口以及聯網協議等。當然,區分的界線不是絕對明確的,有時候需要酌情調整。
  • 自由:不應限制用戶使用以太坊協議,也不應試圖優先支持或不支持某些以太坊合約或交易。這一點與 “網絡中立” 概念背後的指導原則相似。比特幣交易協議就 沒有 遵循這一原則:比特幣交易協議並不鼓勵區塊鏈的 “非常規用途(off-labal purpose)” (如,數據存儲,元協議)(校對註:off-labal 的原意為將藥物用在其經過批准的適應症之外的癥狀上,例如使用止咳藥來治療頭痛。此處意譯為 “非常規用途” );而且,有時候還有人用 准-協議層 的變更(例如將 OP_RETURN 字段的長度限制在 40 字節)來攻擊以 “未經授權” 的方式使用區塊鏈的應用(校對註:此處是在諷刺比特幣的社區有審查比特幣區塊鏈用法的傾向)。因此,在以太坊,我們堅定支持僅使用交易手續費來達成大體激勵相容的辦法 —— 用戶消耗整個網絡越多資源,需要付出的代價就越高,也即使其自己承擔成本(即庇古稅)。
  • 泛化:以太坊協議的特性和操作碼應最大限度地體現低層次的概念(就像基本粒子一樣),以便它們可以隨意組合,包括組合出今天看來沒什麼用、但未來可能有用的東西。而且,通過剝離那些不需要的功能,低層次的概念可以更加高效。遵循這一原則的例子是,我們選擇 LOG 操作碼作為向 dapp 提供信息的方式,而不是像之前那樣記錄下所有交易和消息。在早先,“消息(message)” 的概念完完全全是多種概念的集合,它包含 “函數調用(function call)” 和 “外在觀察者感興趣的事件信息(event)” ,而兩者是完全可以分離開來的。
  • 沒有特點就是最大的特點:為了遵循泛化原則,我們拒絕將那些高級用例內嵌為協議的一部分,哪怕是經常使用的用例,也絕不這麼做。如果人們真的想實現這些用例,可以在合約內創建子協議(如,基於以太坊的子貨幣,比特幣/萊特幣/狗幣的側鏈等)。比如,在以太坊中就缺少類似比特幣中的 “時間鎖” 功能。但是,通過以下協議可以模擬出這個功能:用戶發送簽名數據包到特定的合約中處理,如果數據包在特定合約中有效,則執行相應的函數。
  • 不厭惡風險:如果風險的增加帶來了可觀的好處,我們願意承擔更高的風險(例如,通用的狀態轉換,出塊時間減低 50 倍,共識效率,等等)。

這些原則指導着以太坊的開發,但它們並不是絕對的;某些情況下,為了減少開發時間或者不希望一次作出過多改變,也會使我們推遲作出某些修改,把它留到將來的版本中去修改。

區塊鏈層協議

本節對以太坊中區塊鏈層協議的改變進行了描述,包括區塊和交易是如何工作的、數據如何序列化及存儲、賬戶背後的機制。

賬戶 ,而非 UTXO 1

比特幣及其許多變種,都將用戶的餘額信息存儲在 UTXO 結構中,系統的整個狀態由一系列的 “未花費的輸出” 組成(可以將這些 “未花費的輸出” 想象成錢幣)(校對註:更好的一個比喻可能是 “支票”。)。每個 UTXO 都有擁有者和自身的價值屬性。一筆交易在消費若干個 UTXO 同時也會生成若干個新的 UTXO;而交易受到下列有效性要求的約束:

1.每個被引用的輸入必須有效,且未被使用過;2.交易的簽名必須與每筆輸入的所有者簽名匹配;3.輸入的總值必須等於或大於輸出的總值。
因此,比特幣系統中,用戶的 “餘額” 是該用戶的私鑰能夠有效簽名的所有 UTXO 的總和。下圖展示了比特幣系統中交易輸入輸出過程:

V神設計理念公布,細數以太坊潛在的優缺點

比特幣所用的三式記賬法

但是,以太坊拋棄了 UTXO 的方案,轉而使用更簡單的方法:採用狀態(state)的概念存儲一系列賬戶,每個賬戶都有自己的餘額,以及以太坊特有的數據(代碼和內部存儲器)。如果交易發起方的賬戶餘額足夠支付交易費用,則交易有效,那麼發起方賬戶會扣除相應金額,而接收賬戶則計入該金額。某些情況下,接收賬戶內有需要執行的代碼,則交易會觸發該代碼的執行,那麼賬戶的內部存儲器可能會發生變化,甚至可能會創建額外的消息發送給其他賬戶,從而導致新的交易發生。

儘管以太坊沒有採用 UTXO 的概念,但 UTXO 也不乏有一些優點:

  • 較高程度的隱私保護:如果用戶每次交易都使用一個新的地址,那麼賬戶之間的相互關聯就很困難。這樣做適用於對安全性要求高的貨幣系統,但不是對任何 dapp 都合適。因為 dapp 通常需要跟蹤用戶複雜的綁定狀態,而 dapp 的狀態並不能像貨幣系統中的狀態那樣簡單地劃分。
  • 潛在的可擴展性:理論上來說,UTXO 與某些類型的可擴展性方案(scalability paradigm)更契合,因為只需持幣者擁有能夠證明自己貨幣所有權的默克爾證明即可,即使所有的人(包括 TA 本人)都遺忘了這一數據,真正受損也這個人,其他人不受影響。在以太坊賬戶系統中,如果所有人都丟失了某個賬戶對應的默克爾樹部分,那麼該賬戶將無法處理任何能夠影響它的消息,包括發送給它的消息,它也無法處理。不過,並非只有 UTXO 能夠可擴展,也存在不依賴 UTXO 就能擴展的方式(此處沒有擴展開來講,譯者注)。

賬戶的好處有以下幾點:

  • 節省大量空間:如果一個賬戶有 5 個 UTXO,則從 UTXO 模式轉成賬戶模式,所需空間會從 300 字節降到 30 字節。具體計算如下:300 = (20+32+8)* 5 (20 是地址字節數,32 是 TX 的 id 字節數,8 是面額佔用的字節數); 30 = 20 + 8 + 2 (20 是地址字節數,8 是賬戶餘額值字節數,2 是 nonce 2 字節數);但實際節約並沒有這麼大,因為賬戶需要被存儲在帕特里夏樹中。另外以太坊中交易也比比特幣中的更小(以太坊中 100 字節,比特幣中 200-250 字節),因為每次交易只需要生成一次引用,一次簽名,以及一個輸出。
  • 可互換性更強:UTXO 結構並沒有區塊鏈層的概念,所以不管是在技術還是法律上,通過建立一個紅名單/黑名單,並依據的這些 “有效輸出” 的來源區分它們並不是很實際。
  • 簡單:以太坊編碼更簡單、更易於理解,尤其是在涉及到複雜腳本時。儘管任何去中心化應用都可以用 UTXO 方式來(勉強)實現,但這種方式實質上是賦予腳本限制給定的 UTXO 所能輸出的 UTXO 的種類及其使用條件(比如需要包含默克爾樹證明來幫助腳本所對應的應用更改狀態根)的能力。因此,UTXO 實現方式比以太坊使用賬戶的方式要複雜的多。
  • 輕客戶端:輕客戶端可以隨時通過沿指定方向掃描狀態樹來訪問與賬戶相關的所有數據。在 UTXO 範式中,每筆交易需要用到的引用都不同,這對於長時間運行並使用了上文提到的 UTXO 根狀態傳播機制的 dapp 應用來說,無疑是繁重的。

我們認為,賬戶的好處大大超過了其他方式,尤其是對於我們想要支持的、可包含任意狀態和代碼的 dapp 應用而言。另外,本着 “沒有特點就是最大的特點” 的指導原則,我們認為如果用戶真的關心私密性,則可以通過合約中的簽名數據包協議來建立一個加密 “混幣器(mixer and coinjoin)” 混淆支付路徑。

賬戶方式的一個弱點是:為了阻止重放攻擊(replay attack,指讓同一筆交易重複執行),每筆交易必須有一個 “nonce”(流水號)。因此,每個賬戶都要有一個實時更新的 nonce 值,每一筆新交易都在賬戶 nonce 值上遞增 1 作為自己的 nonce(並在交易處理之後按此值更新賬戶的 nonce 值)(校對註:在賬戶模式下,如果交易不附帶這種消耗性的標識符,交易就可被重複處理,這樣接收賬戶可以一遍又一遍地收賬且不用付出任何代價,而發賬的賬戶會被吸干;以太坊賬戶的 nonce 隨所發起的交易得到處理而遞增,就解決了這個問題)。這就意味着,即使不再使用的賬戶,也不能從賬戶狀態中移除。解決這個問題的一個簡單方法是讓交易包含一個區塊號,使它們在一段時間后就無法再被重放,並且每隔一段時間段重置 nonce。

若要在狀態中刪除某個賬戶(比如長期不使用的賬戶),就必須先 “ping” 出它們來,而完整掃描區塊鏈協議的開銷是非常大的。在1.0上我們沒有實現這個機制,1.1及以上版本可能會使用這個機制。

校對註:這就是以太坊日後面臨的 “狀態爆炸” 問題的技術原因:所有狀態數據必須完整保存,無法合理地刪除賬戶。作為一種區塊鏈協議,以太坊的節點不僅要對事務(交易)的順序達成共識,還要對全局狀態達成共識(表現形式就是區塊頭裡需要包括狀態根。因此,若要刪除狀態,也需要全網的共識,否則會陷入分裂。
校對註:這種以 nonce 來標記賬戶交易順序的做法,也使得用戶的交易必須順序執行,如果一筆交易無法得到處理,使用後續 nonce 的交易也無法得到處理。關於 “加速” 已發出的交易的上鏈進度,見這篇文章。

默克爾帕特里夏樹(MPT)

默克爾帕特里夏樹(Merkle Patricia tree/trie),由 Alan Reiner 提出設想,並在瑞波協議中得到實現,是以太坊的主要數據結構,用於存儲所有賬戶狀態,以及每個區塊中的交易和收據數據。MPT 是默克爾樹和帕特里夏樹的結合,結合這兩種樹創建的結構具有以下屬性:

  • 任一組 鍵-值對 所對應的根哈希值都是唯一的,想要謊稱某個 鍵值對 存在於某棵樹上是一定會被識破的(除非攻擊者擁有約 2^128 的算力)。
  • 增、刪、改 一個鍵值對的時間複雜度是對數級別。

MPT為我們提供了一個高效、易更新、且代表整個狀態樹的 “指紋” 。關於MPT更詳細描述:https://github.com/ethereum/wiki/wiki/Patricia-Tree。

MPT的具體設計決策如下:

  • 有兩類節點:KV 節點和離散節點。KV節點的存在提高了效率,因為如果在特定區域樹是稀疏的,KV節點可作為一個 “捷徑” 來壓縮樹的高度(閱讀 MPT 的詳述可了解更多細節)。
  • 離散節點是十六進制,不是二進制:這樣讓查找更有效率,我們現在認識到這種選擇並不理想,因為十六進制樹的查找效率在二進制中可以通過批次存儲節點來模擬。但是,MPT 樹結構的實現是非常容易出錯的,最終至少會造成狀態根不匹配,所以我們決定擱置變更,等到 1.1 版本再說。
  • 空值(empty value)與非成員(non-membership)之間沒有區別:這樣做是為了簡化邏輯,以太坊中未啟用的賬戶的值(餘額)默認為 0,空字符串也用 0 表示。然而,需要強調的是,這樣做犧牲了一些通用性,因而也不是最優的。
  • 終節點(terminating)和非終節點的區別:技術上,標識一個節點 “是否是終節點” 是沒必要的,因為以太坊中所有的樹都被用於存儲固定長度(即鍵的長度)的數據,但為了增加通用性,我們還是會添加這個標識,以期望以太坊的 MPT 的實現方式能夠被其他密碼學貨幣原樣採納。
  • 在 “安全樹”(狀態樹和賬戶存儲樹)中採用 SHA3(k) 作為鍵:使用 SHA3(k),想要通過生成許多的賬戶(賬戶最多可讓狀態樹高達 64 層!)並重複調用 SLOAD 和 SSTORE 操作碼來 DoS 攻擊的難度會大大提高。注意,這也讓枚舉樹變得更困難;如果要使你的客戶端具備枚舉的功能,最簡單的方法就是維護一個映射 sha3(k) -> k 的數據庫。

校對註:這裡的意思是,如果使用 k 作為默克爾樹存儲數據的鍵,其分佈可能很稀疏,而攻擊者可以容易地規劃出需要很深的樹路徑來存儲的賬戶,並對這些賬戶重複調用狀態訪問操作,以此造成網絡中的節點超負荷運行,但是,哈希函數的結果是隨機分佈的,以 sha3(k) 作為鍵可以使鍵的分佈較為均勻,樹高也會較矮)。
這種特性也是有得有失,這一方面意味着 DoS 攻擊會變得更困難,另一方面,也使得一個區塊中的交易的狀態樹訪問路徑,很少有重合的,因此每次搜索都是複雜度最差的情形。
此外,這也使得 MPT 不宜實現 “無狀態性”(區塊自身攜帶驗證所需的數據、驗證者無需具有全局狀態),因為狀態訪問的路徑不重合,證據的空間效率也是最差情形。當然,也可以說,默克爾樹證據的空間效率本身也不夠高

RLP

RLP(recursive length prefix):遞歸長度前綴。

RLP 編碼是以太坊中主要的序列化格式,它的使用無處不在:區塊、交易、賬戶狀態以及網絡協議消息。詳見 RLP 正式描述: https://github.com/ethereum/wiki/wiki/RLP

RLP 旨在成為高度簡化的序列化格式,它唯一的目的是存儲嵌套的字節數組 3。不同於 protobuf、BSON 等現有的解決方案,RLP並不定義任何指定的數據類型,如 Boolean(布爾值)、float(浮點數)、double 或者 integer(整數)。它僅僅是以嵌套數組的形式存儲結構體,由協議來確定數組的含義。RLP 也沒有顯式支持 map 集合,半官方的建議是採用 [[k1, v1], [k2, v2], …] 的嵌套數組來表示鍵值對集合,k1,k2 … 按照字符串的標準排序。

與 RLP 具有相同功能的方案是 protobuf 或 BSON,它們是一直被使用的算法。然而,以太坊中,我們更偏向於使用 RLP,因為:(1)它易於實現;(2)絕對保證字節的一致性。

許多語言的鍵值對集合沒有明確的排序,並且浮點格式有很多特殊情況,這可能造成相同數據卻產生不同編碼和不同哈希值。通過內部開發協議,我們能確保它是帶着這些目標設計的(這是一般原則,也適用於代碼的其他部分,如虛擬機)。BitTorrent 使用的編碼方式 bencode 也許可以替代 RLP。不過它採用的是十進制的編碼方式,與採用二進制的 RLP 相比,稍微遜色了點。

壓縮算法

網絡協議和數據庫都採用了一個自定義的壓縮算法來存儲數據。該算法可描述為:對 0 使用行程編碼 4 並同時保留其他值(除了一些特殊情況如 sha3(' ') ),舉例如下:

壓縮算法存在之前,以太坊協議的許多地方都有一些特殊情況,例如,sha3 經常被重定義使得 sha3(' ')=' ',這樣不需要在賬戶中存儲代碼,可以節省 64 字節。然而,最近所有這些使得以太坊數據結構變得臃腫的特殊情況都被刪除了,取而代之的是將數據保存函數添加到區塊鏈協議之外的層,也就是將其放入網絡協議以及將其插入用戶數據庫實現。這樣增加了模塊化能力,簡化了共識層,使得對壓縮算法的持續更新部署起來相對簡單(例如:可通過網絡協議的版本號來區別、部署)。

樹(trie)的使用

提醒:理解這部分的知識需要讀者了解布隆過濾器 5 的原理。簡介可見:http://en.wikipedia.org/wiki/Bloom_filter

以太坊區塊鏈中每個區塊頭都包含指向三個樹的指針:狀態樹、交易樹、收據樹。

  • 狀態樹代表處理完該區塊后的整個狀態;
  • 交易樹代表區塊中所有交易,這些交易由 index 索引作為key;(例如,k0:第一個執行的交易,k1:第二個執行的交易)
  • 收據樹代表每筆交易相應的收據。

交易的收據是一個 RLP 編碼的數據結構:

  • 其中:
  • medstate:交易處理后,狀態樹的根;
  • gas_used:交易處理后,gas 的使用量;
  • logs:是許多 [address, [topic1, topic2…], data] 元素的列表。這些元素由交易執行期間調用的操作碼 LOG0 … LOG4 生成(包含主調用和子調用);address 是生成日誌的合約的地址;topics 是最多 4 個 32 字節的值;data 是任意大小的字節數組;
  • logbloom:交易中所有 logs 的 address 和 topics 組成的布隆過濾器。

區塊頭中也存在一個布隆過濾器,它是區塊中交易的所有布隆過濾器的或運算(OR)結果。這樣的構造使得以太坊協議對輕客戶端友好得無以復加。

註釋:

  • UTXO:unspent transaction outputs,字面理解是:未花費的交易輸出,也即未被任何交易引用為輸入的交易輸出。它是比特幣協議中用於存儲價值(所有權)信息的數據結構。—— 校對注
  • Nonce,Number used once 或 Number once 的縮寫,在密碼學中 Nonce 是一個只被使用一次的任意或非重複的隨機數值,在加密技術中的初始向量和加密哈希函數都發揮着重要作用,在各類驗證協議的通信應用中確保驗證信息不被重複使用以對抗重放攻擊(Replay Attack)。—— 譯者注
  • 嵌套數組:創建一個數組,並使用其他數組填充該數組。如數組 pets:
    var cats : String[] = ["Cat","Beansprout", "Pumpkin", "Max"];
    var dogs : String[] = ["Dog","Oly","Sib"];
    var pets : String = [cats, dogs];

    —— 譯者注

  • 行程編碼(run-length-encoding):一種統計編碼。主要技術是檢測重複的比特或字符序列,並用它們的出現次數取而代之。(百度百科)—— 譯者注
  • 布隆過濾器:由 Howard Bloom 在 1970 年提出的二進制向量數據結構,它具有很好的空間和時間效率,被用來檢測一個元素是不是集合中的一個成員。(百度百科)—— 譯者注

叔塊(uncle blocks)獎勵

GHOST 協議是一項不起的創新,由 Yonatan Sompolinsky 和 Aviv Zohar 在 2013 年 10 月首次提出的。它是解決快速出塊伴生問題的第一個認真嘗試。

GHOST 的用意是解決這樣一個難題:更短的出塊時間(因此確認速度會更快)會導致有更多區塊 “過時” 因而安全性會下降 —— 因為區塊在網絡中傳播需要一定時間,如果礦工 A 挖到一個區塊並向全網廣播,在廣播的路上,B 也挖出了區塊,那麼 B 的區塊是過時的,且 B 的本次挖礦對網絡的安全沒有貢獻。

此外,還有一個中心化問題:如果 A 是一個礦池,有 30% 的算力,B 有 10% 的算力。A有 70% 的時間產生過時的區塊(因為另外的 30% 時間會產生最新區塊,可認為 TA “立即” 得到了最新塊的數據而無需等待區塊傳播),而 B 有 90% 的時間產生過時區塊。如果區塊的產出時間間隔很短,那麼過時率就會變高,則 A 憑藉其更大的算力使挖礦效率也更高。所以,區塊生成過快,容易導致網絡算力大的礦池在事實上壟斷挖礦過程。

根據 Sompolinsky 和 Zohar的描述,GHOST 解決了在計算哪個鏈是最長的鏈的過程中,因產生過時區塊而造成的網絡安全性下降的問題。也就是說,不僅是父區塊和更早的區塊,同時過時的旁支區塊(在以太坊中,我們稱之為 “叔塊”)也被添加到計算哪個塊具有最大的總工作量證明中去。

為了解決第二個問題:中心化問題,我們採用了另一種策略:對過時區塊也提供區塊獎勵:挖到過時區塊的獎勵是該區塊基礎獎勵的 7/8;而包含過時區塊的侄子區塊將收到 1/32 的基礎獎勵作為賞金。但是,交易費不會獎勵給叔塊和侄塊。

在以太坊中,過時區塊只能被其兄弟區塊的 7 代以內的直系後代區塊包含為叔塊。之所以這樣限制是因為,首先,GHOST 協議若不限制過時區塊的代際距離,將會花費大量開銷在計算過時區塊的有效性上;其次,無限制的過時區塊激勵政策會讓礦工失去在主鏈上挖礦的熱情;最後,計算表明,過時區塊獎勵政策限制在 7 層內提供了大部分所需的效果,而且不會帶來負面效應。

  • 度量中心化風險的一個模擬器可見此處:https://github.com/ethereum/economic-modeling/blob/master/ghost.py
  • 一個更高層次的討論可見此處:https://blog.ethereum.org/2014/07/11/toward-a-12-second-block-time/

校對註:此處的 “包含” 在技術上的形式是:侄塊在區塊頭中引用叔塊的區塊哈希值,然後把叔塊的區塊頭包含在區塊體內。

區塊時間算法的設計決策包括:

  • 區塊時間 12s:選擇 12 秒是因為這已經是長於網絡延遲的最短時間間隔了。在 2013 年的一份關於測量比特幣網絡延遲的論文中,確定了 12.6 秒是新產生的區塊傳播到 95% 節點的時間;然而,該論文還指出傳播時間與區塊大小成比例,因此在更快的貨幣中,我們可以期待傳播時間大大減少。傳播間隔時間是恆定的,約為 2 秒。然而,為了安全起見,在我們的分析中,我們假定區塊的傳播需要 12 秒
  • 7 代祖先以內的限制:這樣設計的目的是希望只保留少量區塊,而將更早之前的區塊清除。已經證明 7 代的可引用範圍就可以提供大部分所需的效果。
  • 1 代後裔的限制:(例如,設 c = child 且 p = parent,則 c(c(p(p(p(head))))) 是無效的):這也是出於簡潔性的設計目標,而且上述的模擬器顯示這不會帶來很大的中心化風險。(校對註:此句難解;一種可能的意思是:叔塊的後代不能作為叔塊,即只有主鏈的一代旁支能作為叔塊。)
  • 叔塊必須是有效的 :叔塊必須是有效的 header,而不是有效的區塊。這樣做也是為了簡化,將區塊鏈模型保持為線性數據結構(而不會變成 DAG)。不過,要求叔塊是有效的區塊也是有效的方法。
  • 獎金分配:7/8 的挖礦基礎獎勵分配給叔塊,1/32 分給侄塊,它們交易費用都是 0%。如果費用占多數,從中心化的角度看,這會使叔塊激勵機制無效;然而,這也是為什麼只要我們繼續使用 PoW,以太坊就會不斷發行以太幣的原因。

難度更新算法

目前以太坊通過以下規則進行難度更新:

難度更新規則的設計目標如下:

  • 快速更新:區塊間的時間應該隨着 hash 算力的增減而快速調整;
  • 低波動性:如果挖礦算力恆定,那麼難度不應劇烈波動;
  • 簡單:算法的實現應相對簡單;
  • 低內存:算法不應依賴於過多的歷史區塊,要儘可能少的使用 “內存變量”。假設有最新的十個區塊,將存儲在這十個區塊頭部的內存變量相加,這些區塊都可用於算法的計算;
  • 不可爆破:算法不應讓礦工有過多篡改時間戳或者礦池反覆添加或刪除算力的激勵

我們當前的算法在低波動性和抗爆破性上並不理想。最近,我們計劃把時間戳參數改為與父區塊和祖父區塊比較,所以礦工只有在連續挖 2 個區塊時,才有動力去修改時間戳。另一個更強大的模擬公式:https://github.com/ethereum/economic-modeling/blob/master/diffadjust/blkdiff.py

Gas 和費用

比特幣中所有交易大體相同,因此它們的網絡成本用單一一種單位來模擬。以太坊中的交易要更複雜,所以交易費用需要考慮到賬戶的許多方面,包括網絡帶寬費用、存儲費用和計算費用。尤其重要的是,以太坊編程語言是圖靈完備的,所以交易會使用任意數量的寬帶、存儲和計算成本;而最終會使用多少數量是無法可靠預測的(因為所謂的 “圖靈停機問題”)(校對註:即不存在一個可靠的辦法,能夠斷言任意可在圖靈機上執行的程序會不會在有限步內終止)。防止有人使用無限循環來實施拒絕服務式攻擊是我們的一個關鍵目標。

以太坊交易費用的基本機制如下:

  • 每筆交易必須指明自身願意消耗的 gas 數量(即指定 startgas 的值),以及願意為每單元 gas 支付的費用(即 gasprice ),在交易執行開始時,startgas * gasprice 價值的以太幣會從發送者賬戶中扣除;(校對註:此處的 startgas 就是我們現在慣用的 gaslimit 。)
  • 交易執行期間的所有操作,包括讀寫數據庫、發送消息以及每一步的計算都會消耗一定數量的 gas;
  • 如果交易執行完畢,消耗的 gas 值小於指定的限制值,則交易執行正常,並將剩餘的 gas 值賦予變量 gas_rem ; 在交易完成後,發送者會收到返回的 gas_rem * gasprice 價值的以太幣,而給礦工的獎勵是(startgas – gas_rem)* gasprice 價值的以太幣;
  • 如果交易執行中,gas消耗殆盡,則所有的執行恢復原樣,但交易仍然有效,只是交易的唯一結果是將 startgas * gasprice 價值的以太幣支付給礦工,其他不變;
  • 當一個合約發送消息給另一個合約,可以對這個消息引起的子執行設置一個 gas 限制。如果子執行耗盡了 gas,則子執行恢復原樣,但 gas 仍然消耗。(校對註:截至本文校對之時(2021 年 7 月 9 日),這一點還未改變,但它在未來有可能會改變。見《值得考慮刪除的 EVM 功能》)

上述提到的幾點都是必須滿足的,例如:

  • 如果交易不需要指定 gas 限制,那麼惡意用戶就會發送一個有數十億步循環的交易。沒有人能夠處理這樣的交易,因為處理這樣的交易花的時間可能很長很長;但是誰也無法預先告知網絡上的礦工,這就會導致拒絕服務的漏洞產生。
  • 一種替代嚴格 gas 計數的方法是時間限制,但它不可能有用,因為它們太主觀了(某些計算機比別人的更快,即使大家的計算機都一樣也仍然有可能出現差池)。
  • startgas * gasprice 的整個值,在開始時就應該設置好,這樣不至於在交易執行中造成該賬戶 “破產”、無力繼續支付 gas 費用。一邊執行一邊檢查餘額也不行,因為賬戶可以把餘額放到別的地方。
  • 如果在 gas 不夠的情況下,交易執行不會完全復原(回滾),合約就必須採用強有力的安全措施來防止合約發生變化。
  • 如果子限制不存在,則惡意賬戶可以對其他合約實施拒絕服務攻擊。攻擊者可以先與受害合約達成一致意見,然後在計算過程開始時插入一個無限循環,那麼發送消息給受害合約或者受害合約的任何補救嘗試,都會使整個交易死鎖。(校對註:此句亦難解。)
  • 要求交易發送者而不是合約來支付 gas,這樣大大增加了開發人員的可操作性。以太坊早期的版本是由合約來支付gas的,這導致了一個相當嚴重的問題:每個合約必須實現 “門衛” 代碼,確保每個傳入的消息為合約提供了足夠的以太幣供其消耗。

gas 消耗計算有以下特點:

  • 對於任何交易,都將收取 21000 gas 的基本費用。這些費用可用於支付運行橢圓曲線算法所需的費用(該算法旨在從簽名中恢複發送者的地址)以及存儲交易所花費的硬盤和帶寬空間。
  • 交易可以包括無限量的 “數據” 。虛擬機中的某些操作碼,可以讓收到這樣交易的合約訪問這些數據。數據的 “固定消耗量” 規則是:每個零字節 4 gas,非零字節 68 gas。這個公式的產生是因為用戶向合約發送的交易中,大部分的交易數據由一系列的 32 字節的參數組成,其中多數參數具有許多前導零字節。該結構看起來似乎效率不高,但由於壓縮算法的存在,實際上還是很有效率的。我們希望此結構能夠代替其他更複雜的機制:這些機制根據預期字節數嚴格包裝參數,從而導致編譯階段複雜性大增。這是三明治複雜模型的一個例外,但由於成本效益比,這也是合理的模型。
  • 用於設置賬戶存儲項的操作碼 SSTORE 的消耗是:1)將零值改為非零值時,消耗 20000 gas;2)將零值變成零值,或非零值變非零值,消耗 5000 gas;3)將非零值變成零值,消耗 5000 gas;此外,交易執行成功(即未耗盡 gas 交易就執行完了)後會退回 15000 gas。退款金額上限是交易消耗 gas 總額的 50%。這給了人們小小激勵去清除存儲項。我們注意到,正因為缺乏這樣的激勵,許多合約的存儲空間沒有被有效使用,從而導致了存儲數據的快速膨脹。這一設計既能提供 “為存儲項持續收取租金” 模式的大部分好處,又不會失去合約一旦確立就可以永久存在的保證。延遲退款機制是必要的,因為可以阻止拒絕服務攻擊:攻擊者可以發送一筆含有少量 gas 的交易,循環清除大量的存儲項,直到用光 gas,這樣消耗了大量的驗證算力,但實際並沒有真正清除存儲,也不需要付出很多 gas。50% 的上限的是為了確保打包交易的礦工依然能夠確定執行交易的計算時間的上限。
  • (校對註:首先,SSTORE 等狀態訪問操作碼的 gas 消耗量已經隨着以太坊的硬分叉而多次更改。截至 2021 年 7 月,最新的數值可見《柏林升級內容概覽》;在可預見的未來,這個操作碼的數值還會繼續變化;其次,這裡的 gas refund 機制,事後證明並沒有啟動緩解狀態數據的膨脹問題,反而惡化了該問題,因為人們可以在 gas price 較低時寫入大量垃圾數據,在 gas price 較高時清除這些數據來獲得 gas,這就是 “GasToken” 的原理。當前已確定,在 “倫敦” 分叉中會改變 gas refund 機制,見《以太坊 “倫敦” 升級預覽》。)
  • 合約提供的消息數據是沒有成本的。因為在消息調用期間不需要實質複製任何數據,調用數據(call data)可以簡單地視為指向父合約 memory 的指針,該指針在子進程執行時不會改變。
  • Memory 是一個可以無限擴展的數組,然而,每擴展 32 字節的 memory 就會消耗 1 gas 的成本,不足 32 字節以 32 字節計。(校對註:memory 一般譯為內存,但在以太坊的語境下,它是 EVM 由於存儲數據的三種類型之一,因此都不譯,以示其特殊性。)
  • 某些操作碼的計算時間極度依賴參數,gas 開銷計算是動態變化的。例如,EXP 的的開銷是指數級別的(10 gas + 10 gas/字節,即,x^0 = 1 gas、x^1 … x^255 = 2 gas、x^256 … x^65535 = 3 gas,等等)。複製操作碼(如:CALLDATACOPY, CODECOPY, EXTCODECOPY)的開銷是 1 gas + 1 gas/32 字節(四捨五入;LOG 操作碼的規則也類似)。Memory 擴展的開銷不包含在這裡。如若包含,會變成一個平方攻擊向量(50000 次的 CALLDATACOPY,每次消耗 50000 gas,則其計算量應是 50000^2,但如果不使用動態收費規則,就只需付出 ~50000 gas)。
  • 如果值不是零,操作碼 CALL(以及 CALLCODE)會額外消耗 9000 gas。這是因為任何值傳輸都會引起歸檔節點的歷史存儲顯著增大。請注意,操作的 實際消耗 是 6700;但是此基礎上,我們強制增加了一個自動給予接收者的 gas 值,這個值最小 2300。這樣做是為了讓接受交易的錢包至少有足夠的 gas 來生成 log。(校對註:見《值得考慮刪除的 EVM 功能》)

Gas 機制的另一個重要部分是 gas 價格本身體現出的經濟學原理。比特幣中,默認的方法是採取純粹自願的收費方式,礦工扮演守門人的角色並且動態設置收費的最小值。以太坊中允許交易發送者設置任意數目的 gas。這種方式在比特幣社區非常受歡迎,因為它是 “市場經濟” 的體現:允許礦工和交易者之間依據供需關係來決定價格。然而,這種方式的問題是,交易處理並不遵循市場原則。儘管可以將交易處理看作是礦工向發送者提供的服務(這聽起來很直觀),但實際上礦工所處理的每個交易都必須由網絡中的每個節點處理,所以交易處理的大部分成本都由第三方機構承擔,而不是決定是否處理它的礦工。因此,“公地悲劇” 問題很有可能發生。

當前,因為缺乏礦工在實際中的行為的明確信息,所以我們將採取一個非常簡單公平的方法:投票系統,來設定單個區塊可消耗的 gas 總額。礦工有權將在最新區塊的 gas 上限基礎上變更 0.0975% (1/1024),作為當前區塊的 gas 上限。所以最終的 gas 上限應該是礦工們設置的中間值。我們希望將來能夠採用軟分叉的方法來使用更加精確的算法。

虛擬機

以太坊虛擬機是執行交易代碼的引擎,也是以太坊與其他系統的核心區別。請注意,虛擬機應該同 “合約與消息模型” 分開考慮。例如,SIGNEXTEND 操作碼是虛擬機的一個功能,但實際上 “某個合約可以調用其他合約並指定子調用的 gas 限定值” 是 “合約與消息模型” 的一部分。

EVM的設計目標如下:

  • 簡單:操作碼儘可能的少並且低級;數據類型儘可能少;虛擬機的結構儘可能少;
  • 結果明確:在 VM 規範中,沒有任何可能產生歧義的空間,結果應該是完全確定的。此外,計算步驟應該是精確的,以便可以測量 gas 的消耗量;
  • 節約空間:EVM 組件應儘可能緊湊;
  • 為預期用途而特化:在 VM 上構建的應用應能處理 20 字節的地址,以及 32 位的自定義加密值,擁有用於自定義加密的模數運算、讀取區塊和交易數據與狀態交互等能力;
  • 簡單安全:為了讓 VM 不被利用,應該能夠容易地讓建立一套 gas 消耗成本模型的操作;
  • 優化友好:應該易於優化,以便即時編譯(JIT)和 VM 的加速版本能夠構建出來。

同時 EVM 也有如下特殊設計:

  • 臨時/永久存儲的區別:我們先來看看什麼是臨時存儲和永久存儲。臨時存儲:存在於 VM 的每個實例中,並在 VM 執行結束后消失。永久存儲:存在於區塊鏈狀態層。假設執行下面的樹(S 代表永久存儲,M 代表臨時存儲):
  • A調用 B;
  • B 設置 B.S[0]=5,B.M[0]=9 ;
  • B 調用 C;
  • C 調用 B。
    此時,如果B試圖讀取 B.S[0] ,它將得到B前面存入的數據,也就是 5;但如果 B 試圖讀取 B.M[0] ,它將得到 0,因為 B.M 是臨時存儲,讀取它的時候是虛擬機的一個新的實例。在一個內部調用(inner call)中,如果設置 B.M[0] = 13 和 B.S[0] = 17 ,然後內部調用和 C 的調用都終止、回到了 B 的外部調用(outer call),此時讀取 M,將會看到 B.M[0] = 9 (此值是在上一次同一 VM 執行實例中設置的), B.S[0] = 17 。如果 B 的外部調用結束,然後 A 再次調用 B,將看到 B.M[0] = 0,B.S[0] = 17 。這個區別的目的是:1.每個執行實例都分配有內存空間,不會因為循環調用而減損,這讓安全編程更加容易。2.提供一個能夠快速操作的內存形式:因為需要修改樹,所以存儲更新必然很慢。
  • 棧/memory 模式:早期,計算狀態(除了指向下一個指令的程序計數器)有三種:棧(stack,一個 32 字節標準的 LIFO 棧),內存(memory,可無限延長的臨時字節數組),存儲項(storage,永久存儲)。在臨時存儲端,棧和內存的替代方案是 memory-only 範式,或者是寄存器和內存的混合體(兩者區別不大,寄存器本質上也是一種內存)。在這種情況下,每個指令都有三個參數,例如: ADD R1 R2 R3: M[R1] = M[R2] + M[R3] 。選擇棧範式的原因很明顯,它使代碼縮小了 4 倍。
  • 單詞大小 32 字節:在大多數結構中,如比特幣,單詞大小是 4 或 8 字節。4 或 8 字節對存儲地址和加密計算來說局限性太大了。而不對大小作限制又很難建立相應安全的 gas 模型。32 字節是一個理想大小,因為它足夠存儲下許多密碼算法所需要的大數值以及地址,又不會因為太大而導致效率低下。
    我們有自己的虛擬機:我們的虛擬機使用 java、Lisp 和 Lua 等語言開發。我們認為開發一款專業的虛擬機是值得的,因為:1)我們的 VM 規範比其他許多虛擬機簡單的多,因為其他虛擬機為複雜性付出的代價更小,也就是說它們更容易變得複雜;然而,在我們的方案中每額外增加一點複雜性,都會給集約化發展帶來障礙,並帶來潛在的安全缺陷,比如共識錯誤,這就讓我們的複雜性成本很高;2)我們的 VM 更加專業化,如支持 32 字節;3)我們不會有複雜的外部依賴,複雜的外部依賴會導致我們安裝失敗;4)完善的審查機制,可以具體到特殊的安全需求;即使使用外部 VM,也無法節省太多工作量。
    使用了可變、可擴展的 memory 大小:固定 memory 的大小是不必要的限制,太小或太大都不合適。如果內存大小是固定的,每次訪問內存都需要檢查訪問是否超出邊界,顯然這樣的效率並不高。
    1024 調用深度限制:許多編程語言在內存還沒有溢出時,就因為調用深度太深而崩潰了。所以僅使用區塊 gas 上限一種限制是不夠的。
    無類型:只是為了簡潔。不過,DIV、SDIV、MOD、SMOD 會使用有符號(signed)或無符號的操作碼(事實證明,對於操作碼 ADD 和 MUL,有符號和無符號是對等的);轉換成定點運算在所有情況下都很簡單,例如,在 32 位長度下,a * b -> (a * b) / 2^32, a / b -> a * 2^32 / b ,+、- 和 * 在整數下不變。
    校對註:在原譯本中還有如下一段,但其對應段落在當前版本的原文中已經刪除了: 棧大小沒有限制:沒什麼特別理由!許多情況下,該設計不是絕對必要的;因為,gas 的開銷和區塊 gas 上限總是會充當每種資源消耗的上限。

這個 VM 中某些操作碼的功能和用意很容易理解,但也有一些不太好理解,以下是一些特殊的原因:

  • ADDMOD, MULMOD:大多數情況下, mulmod(a, b, c) = a * b % c ,但在橢圓曲線算法中,使用的是 32 字節模數運算,直接執行 a * b % c 實際上是在執行 ((a * b) % 2^256) % c ,會得到完全不同的結果。在 32 字節的空間中執行 32 字節數值的 a * b % c 計算的共識非常困難且繁瑣。
  • SIGNEXTEND:SIGNEXTEND操作碼的作用是為了方便從大的有符號整數到小的有符號整數的類型轉換。小的有符號整數是很有用的,因為未來的即時編譯虛擬機也許有能力檢測主要處理 32 字節整數又長時間運行的代碼塊,小的有符號整數能加快處理。
  • SHA3:在以太坊代碼中,SHA3 作為安全的、高強度的、不定長數據哈希映射方法,應用非常廣泛。通常,在使用存儲器時,需要使用 Hash 函數來防止惡意衝突,在驗證默克爾樹和類似的以太坊數據結構時也需要使用到 Hash 函數。重要的是,與 SHA3 的相似的哈希函數,如 SHA256、ECRECVOR、RIPEM160,不是以操作碼的形式包含在裡面,而是以偽合約的形式。這樣做的目的是將它們放在一個單獨的類別中,如果當我們以後提出適當的 “原生插件” 系統時,可以添加更多這樣的合約,而不需要擴展操作碼。
  • ORIGIN:ORIGIN 操作碼由交易的發送者提供,主要的作用是允許合約退回支付的 gas。
  • COINBASE:COINBASE 的主要作用是:1)允許子貨幣對網絡安全作出貢獻;2)使礦工能夠作為一個去中心化的經濟體,來設置基於子共識的應用,如 Schellingcoin。
  • PREVHASH:PREVHASH 可用作一個半安全的隨機來源。此外,允許合約求值(evalute)上一個區塊的默克爾樹狀態證明,而不需要高度複雜的 “以太坊輕客戶端” 遞歸結構 。
  • EXTCODESIZE, EXTCODECOPY:主要的作用是讓合約依據模板檢查其他合約的代碼,甚至是在與其他合約交互前,模擬它們。見:https://lesswrong.com/lw/aq9/decision_theories_a_less_wrong_primer/
  • JUMPDEST:當跳轉(jump)目的地限制在幾個索引時(尤其是,動態目的跳轉的計算複雜度是 O(log(有效挑戰目的數量)),而靜態跳轉總是恆定的),JIT 虛擬機實現起來更簡單。於是,我們需要:1)對有效變量跳轉目的地做限制;2)激勵使用靜態而不是動態跳轉。為了達到這兩個目標,我們定下了以下規則:1)緊接着 push 后的跳轉可以跳到任何地方,而不僅是另一個 jump;2)其他的 jump 只能跳轉到 JUMPDEST。對跳轉的限制是必須的,這樣就可通過查看代碼中的前一個操作來確定當前是一個靜態跳轉還是動態跳轉。缺乏對靜態跳轉的需求是激勵使用它們的原因。禁止跳轉進入 push 數據也會加快 JIT 虛擬機的編譯和執行。
  • LOG:LOG是事件的日誌。
  • CALLCODE:該操作碼允許合約使用自己的存儲項,在單獨的棧空間和 memory 中調用其他合約的 “函數” 。這樣可以在區塊鏈上靈活實現標準庫代碼。
  • SELFDESTRUCT:允許合約刪除它自己,前提是它已經不需要存在了。SELFDESTRUCT 並非立即執行,而是在交易執行完之後執行。這是因為如果允許 SELFDESTRUCT 在執行之後回滾,將會極大地提高緩存的複雜度,不利於高效的 VM 實現。
  • PC:儘管理論上不需要 PC 操作碼,因為所有 PC 操作碼的實例都可以根據將 push 操作的索引加入實際程序計數器來代替實現,但使用 PC 可以創建獨立代碼的位置(可複製粘貼到其他合約的編譯函數,如果它們以不同索引結束,不會被打斷)。

本文鏈接:https://www.8btc.com/article/6659227

轉載請註明文章出處

(0)
上一篇 2021-07-10 13:58
下一篇 2021-07-10 15:01

相关推荐