原文標題:《Design Rationale》
原文作者: Vitalik Buterin
原文編譯: kim & 阿劍,以太坊愛好者
儘管以太坊的許多理念在早先的密碼學貨幣(如比特幣)上已經運用並測試了 5 年之久,但從某些協議功能的處理方法上來說,以太坊與常見方式仍有許多不同。而且,以太坊可用於開發全新的經濟工具,因爲它具有其他系統不具備的許多功能。本文會詳細描述以太坊所有的潛在優點,以及在構建以太坊協議過程中某些有爭議的地方。另外,也會指出我們的方案及替代方案的潛在風險。
以太坊協議的設計遵循以下幾點原則:
2.自由:不應限制用戶使用以太坊協議,也不應試圖優先支持或不支持某些以太坊合約或交易。這一點與「網絡中立」概念背後的指導原則相似。比特幣交易協議就 沒有 遵循這一原則:比特幣交易協議並不鼓勵區塊鏈的「非常規用途(off-labal purpose)」(如,數據存儲,元協議)(校對注:off-labal 的原意爲將藥物用在其經過批准的適應症之外的症狀上,例如使用止咳藥來治療頭痛。此處意譯爲「非常規用途」);而且,有時候還有人用 準-協議層 的變更(例如將 OP_RETURN 字段的長度限制在 40 字節)來攻擊以「未經授權」的方式使用區塊鏈的應用(校對注:此處是在諷刺比特幣的社區有審查比特幣區塊鏈用法的傾向)。因此,在以太坊,我們堅定支持僅使用交易手續費來達成大體激勵相容的辦法——用戶消耗整個網絡越多資源,需要付出的代價就越高,也即使其自己承擔成本(即庇古稅)。
本節對以太坊中區塊鏈層協議的改變進行了描述,包括區塊和交易是如何工作的、數據如何序列化及存儲、賬戶背後的機制。
比特幣及其許多變種,都將用戶的餘額信息存儲在 UTXO 結構中,系統的整個狀態由一系列的「未花費的輸出」組成(可以將這些「未花費的輸出」想象成錢幣)(校對注:更好的一個比喻可能是「支票」。)。每個 UTXO 都有擁有者和自身的價值屬性。一筆交易在消費若干個 UTXO 同時也會生成若干個新的 UTXO;而交易受到下列有效性要求的約束:
1. 每個被引用的輸入必須有效,且未被使用過;
2. 交易的簽名必須與每筆輸入的所有者簽名匹配;
3. 輸入的總值必須等於或大於輸出的總值。
因此,比特幣系統中,用戶的「餘額」是該用戶的私鑰能夠有效簽名的所有 UTXO 的總和。下圖展示了比特幣系統中交易輸入輸出過程:

- 比特幣所用的三式記賬法 -
但是,以太坊拋棄了 UTXO 的方案,轉而使用更簡單的方法:採用狀態(state)的概念存儲一系列賬戶,每個賬戶都有自己的餘額,以及以太坊特有的數據(代碼和內部存儲器)。如果交易發起方的賬戶餘額足夠支付交易費用,則交易有效,那麼發起方賬戶會扣除相應金額,而接收賬戶則計入該金額。某些情況下,接收賬戶內有需要執行的代碼,則交易會觸發該代碼的執行,那麼賬戶的內部存儲器可能會發生變化,甚至可能會創建額外的消息發送給其他賬戶,從而導致新的交易發生。
儘管以太坊沒有采用 UTXO 的概念,但 UTXO 也不乏有一些優點:
1.較高程度的隱私保護:如果用戶每次交易都使用一個新的地址,那麼賬戶之間的相互關聯就很困難。這樣做適用於對安全性要求高的貨幣系統,但不是對任何 dapp 都合適。因爲 dapp 通常需要跟蹤用戶複雜的綁定狀態,而 dapp 的狀態並不能像貨幣系統中的狀態那樣簡單地劃分。
默克爾帕特里夏樹(Merkle Patricia tree/trie),由 Alan Reiner 提出設想,並在瑞波協議中得到實現,是以太坊的主要數據結構,用於存儲所有賬戶狀態,以及每個區塊中的交易和收據數據。MPT 是默克爾樹和帕特里夏樹的結合,結合這兩種樹創建的結構具有以下屬性:
1.任一組 鍵-值對 所對應的根哈希值都是唯一的,想要謊稱某個 鍵值對 存在於某棵樹上是一定會被識破的(除非攻擊者擁有約 2^128 的算力)。
2.增、刪、改 一個鍵值對的時間複雜度是對數級別。
MPT 爲我們提供了一個高效、易更新、且代表整個狀態樹的「指紋」。關於 MPT 更詳細描述:https://github.com/ethereum/wiki/wiki/Patricia-Tree。
MPT 的具體設計決策如下:
1.有兩類節點:KV 節點和離散節點。KV 節點的存在提高了效率,因爲如果在特定區域樹是稀疏的,KV 節點可作爲一個「捷徑」來壓縮樹的高度(閱讀 MPT 的詳述可瞭解更多細節)。
2.離散節點是十六進制,不是二進制:這樣讓查找更有效率,我們現在認識到這種選擇並不理想,因爲十六進制樹的查找效率在二進制中可以通過批次存儲節點來模擬。但是,MPT 樹結構的實現是非常容易出錯的,最終至少會造成狀態根不匹配,所以我們決定擱置變更,等到 1.1 版本再說。
3.空值(empty value)與非成員(non-membership)之間沒有區別:這樣做是爲了簡化邏輯,以太坊中未啓用的賬戶的值(餘額)默認爲 0,空字符串也用 0 表示。然而,需要強調的是,這樣做犧牲了一些通用性,因而也不是最優的。
4.終節點(terminating)和非終節點的區別:技術上,標識一個節點「是否是終節點」是沒必要的,因爲以太坊中所有的樹都被用於存儲固定長度(即鍵的長度)的數據,但爲了增加通用性,我們還是會添加這個標識,以期望以太坊的 MPT 的實現方式能夠被其他密碼學貨幣原樣採納。
5.在「安全樹」(狀態樹和賬戶存儲樹)中採用 SHA3(k) 作爲鍵:使用 SHA3(k),想要通過生成許多的賬戶(賬戶最多可讓狀態樹高達 64 層!)並重復調用 SLOAD 和 SSTORE 操作碼來 DoS 攻擊的難度會大大提高。注意,這也讓枚舉樹變得更困難;如果要使你的客戶端具備枚舉的功能,最簡單的方法就是維護一個映射 sha3(k) -> k 的數據庫。
校對注:這裏的意思是,如果使用 k 作爲默克爾樹存儲數據的鍵,其分佈可能很稀疏,而攻擊者可以容易地規劃出需要很深的樹路徑來存儲的賬戶,並對這些賬戶重複調用狀態訪問操作,以此造成網絡中的節點超負荷運行,但是,哈希函數的結果是隨機分佈的,以 sha3(k) 作爲鍵可以使鍵的分佈較爲均勻,樹高也會較矮)。
這種特性也是有得有失,這一方面意味着 DoS 攻擊會變得更困難,另一方面,也使得一個區塊中的交易的狀態樹訪問路徑,很少有重合的,因此每次搜索都是複雜度最差的情形。
此外,這也使得 MPT 不宜實現「無狀態性」(區塊自身攜帶驗證所需的數據、驗證者無需具有全局狀態),因爲狀態訪問的路徑不重合,證據的空間效率也是最差情形。當然,也可以說,默克爾樹證據的空間效率本身也不夠高
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(' ') ),舉例如下:
>>> compress('horse')'horse'
>>> compress('donkey dragon 1231231243')'donkey dragon 1231231243'
>>> compress('\xf8\xaf\xf8\xab\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe{b\xd5\xcd\x8d\x87\x97')'\xf8\xaf\xf8\xab\xa0\xfe\x9e\xbe{b\xd5\xcd\x8d\x87\x97'>>> compress("\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p")『\xfe\x01'
壓縮算法存在之前,以太坊協議的許多地方都有一些特殊情況,例如,sha3 經常被重定義使得 sha3(' ')=' ',這樣不需要在賬戶中存儲代碼,可以節省 64 字節。然而,最近所有這些使得以太坊數據結構變得臃腫的特殊情況都被刪除了,取而代之的是將數據保存函數添加到區塊鏈協議之外的層,也就是將其放入網絡協議以及將其插入用戶數據庫實現。這樣增加了模塊化能力,簡化了共識層,使得對壓縮算法的持續更新部署起來相對簡單(例如:可通過網絡協議的版本號來區別、部署)。
提醒:理解這部分的知識需要讀者瞭解布隆過濾器 5 的原理。簡介可見:
http://en.wikipedia.org/wiki/Bloom_filter
以太坊區塊鏈中每個區塊頭都包含指向三個樹的指針:狀態樹、交易樹、收據樹。
• 狀態樹代表處理完該區塊後的整個狀態;
• 交易樹代表區塊中所有交易,這些交易由 index 索引作爲 key;(例如,k0:第一個執行的交易,k1:第二個執行的交易)
• 收據樹代表每筆交易相應的收據。
交易的收據是一個 RLP 編碼的數據結構:
[ medstate, gas_used, logbloom, logs ]其中:
• medstate:交易處理後,狀態樹的根;
• gas_used:交易處理後,gas 的使用量;
• logs:是許多 [address, [topic1, topic2...], data] 元素的列表。這些元素由交易執行期間調用的操作碼 LOG0 ... LOG4 生成(包含主調用和子調用);address 是生成日誌的合約的地址;topics 是最多 4 個 32 字節的值;data 是任意大小的字節數組;
• logbloom:交易中所有 logs 的 address 和 topics 組成的布隆過濾器。
區塊頭中也存在一個布隆過濾器,它是區塊中交易的所有布隆過濾器的或運算(OR)結果。這樣的構造使得以太坊協議對輕客戶端友好得無以復加。
1.UTXO:unspent transaction outputs,字面理解是:未花費的交易輸出,也即未被任何交易引用爲輸入的交易輸出。它是比特幣協議中用於存儲價值(所有權)信息的數據結構。——校對注
2.Nonce,Number used once 或 Number once 的縮寫,在密碼學中 Nonce 是一個只被使用一次的任意或非重複的隨機數值,在加密技術中的初始向量和加密哈希函數都發揮着重要作用,在各類驗證協議的通信應用中確保驗證信息不被重複使用以對抗重放攻擊(Replay Attack)。——譯者注
3.嵌套數組:創建一個數組,並使用其他數組填充該數組。如數組 pets:
var cats : String[] = ["Cat","Beansprout", "Pumpkin", "Max"];var dogs : String[] = ["Dog","Oly","Sib"];
var pets : String = [cats, dogs];
——譯者注
4.行程編碼(run-length-encoding):一種統計編碼。主要技術是檢測重複的比特或字符序列,並用它們的出現次數取而代之。(百度百科)——譯者注
5.布隆過濾器:由 Howard Bloom 在 1970 年提出的二進制向量數據結構,它具有很好的空間和時間效率,被用來檢測一個元素是不是集合中的一個成員。(百度百科)——譯者注
原文鏈接
歡迎加入律動 BlockBeats 官方社群:
Telegram 訂閱群:https://t.me/theblockbeats
Telegram 交流群:https://t.me/BlockBeats_App
Twitter 官方帳號:https://twitter.com/BlockBeatsAsia