以太坊1.0的設計依據
編者注:本文建議與《Vitalik:以太坊 Serenity 設計依據綜述》對照閱讀,mix 一下,效果更佳。
儘管以太坊借用了許多已經在諸如比特幣這樣的舊加密貨幣中試用並測試了五年的想法,但是以太坊中也有許多地方不同於當前處理特定協議特性的常見方式。此外,以太坊不得不開發一種全新的經濟方法,以提供其它現有系統無法提供的特性。本文件旨在詳細說明在構建以太坊協議的過程中所產生的細微甚至被忽視又或者是在某些情況下有爭議的決策,並揭示這些方法和潛在的替代方案所涉及的風險。
01
原則
以太坊協議的設計過程遵循以下幾個原則:
1、多層次複雜性模型(Sandwich complexity model):我們認為以太坊的底層架構應該儘可能簡單,並且以太坊的接合點(包括面向開發者的高階程式語言以及面向使用者的使用者介面)應該儘可能易於理解。如果複雜性不可避免,那麼它應該被置於協議的“中間層”。這裡的“中間層”不屬於核心共識的一部分,同時也無法被終端使用者的高階語言編譯器看見,包括引數序列化、反序列化指令碼、儲存資料結構模型、leveldb儲存介面和電報協議(wire protocol)等。然而,這種偏好並不是絕對的。
2 、自由:使用者可以使用以太坊協議做任何事情,我們不應根據其目的的性質來選擇優先支援或不支援某種型別的以太坊合約或交易。這類似於“網路中立”概念背後的指導原則。當前比特幣交易協議並不遵循這一原則,其不鼓勵使用者將區塊鏈用於“未標示用途(比如資料儲存、元協議等)”,並且在某些情況下限定明確的準協議變化(比如OP_RETURN限定為40個位元組)以應對作惡者通過“未授權”方式使用區塊鏈來攻擊應用程式的意圖。在以太坊中,我們強烈傾向於以與經濟激勵大致對等的方式來設定交易費用,從而使意圖阻塞或妨礙區塊鏈的使用者的活動成本內部化(即庇古稅收) 。
3、通用化:在以太坊中,其協議特性和操作碼所包含的概念應該儘可能偏底層,以便它們可以以任意方式組合,包括那些當下看來毫無用處但到後來又可能有用的組合。如此一來,我們便可以在不必要時剝離某些功能,從而可以提高這些底層概念的效率。遵循這一原則的一個例子是,我們選擇LOG操作碼作為向(尤其是輕客戶端)dapp提供資訊的方式,而不是像之前在內部提議的那樣僅僅簡單地記錄所有交易和訊息——“訊息”這一概念實際上是多個概念的集合,包括“函式呼叫”和“外部觀察者感興趣的事件”,我們認為將這兩者分開是很有必要的。
4 、我們沒有所謂的特色:通用化的必然結果是,我們拒絕將非常常見的高階用例構建為協議的內在部分。我們認為,如果人們真的想要這麼做,那麼他們可以在合約內部構件一個子協議(例如由以太幣支撐的子貨幣,比特幣/ 萊特幣 / 狗狗幣側鏈等)。一個關於這一方面的例子是以太坊內不存在類似於比特幣的“鎖定時間(locktime)”功能,因為這樣的功能可以通過某種協議進行模擬。在這種協議中,使用者傳送“已簽署的資料包”,並且這些資料包可以被送入專門用以處理這類資料包的合約中。如果在某種特定的合約層面上而言,這些資料包是有效的,那麼合約將執行相應的功能。
5、非風險厭惡:如果某種可能引入高度風險的變更能夠提供非常可觀的收益(比如通用的狀態轉換、出塊時間縮減50倍、共識效率等),那麼我們可以忍受這種風險。
這些原則對以太坊的開發具有相當的指導意義,但它們並非是絕對的。在一些情況中,由於我們希望減少開發時間或者不希望同時嘗試太多激進的東西,某些變更,甚至包括一些明顯有益的變更都被延遲到後續(例如在以太坊1.1)再行釋出。
02
區塊鏈層面的協議
本節介紹了在以太坊中所進行的一系列區塊鏈協議更改,包括區塊和交易的運作方式、資料的序列化和儲存方式以及帳戶模型背後的機制。
03
為什麼選擇帳戶模型
在比特幣及其衍生幣中,使用者餘額資料都儲存在基於未花費交易輸出(UTXO)的結構中:系統的全域性狀態包含一組“未花費輸出”(即“幣”)。如此一來,每一個幣都歸屬於一個所有者並擁有一個值,而每一筆交易在花費一個或多個幣的同時,又同時創造出一個或多個幣,但這個過程必須符合有效性限制:
-
每一個被引用的輸入必須有效且未被花費
-
該筆交易必須包含與每個輸入相匹配的所有者的簽名
-
輸入的總值必須等於或大於輸出的總值

因此,在比特幣系統中,使用者的“餘額”實際是使用者所擁有的能夠給出有效簽名的私鑰所對應的所有幣的總值。
以太坊捨棄了這種方案,並採用了一種更簡單的方法:讓狀態儲存一個帳戶列表(其中每個賬戶都有餘額)以及與以太坊相關的特定資料(程式碼和內部儲存)。如果交易傳送方的帳戶有足夠餘額用於支付款項的話,那麼這筆交易就是有效的。在這種情況下,傳送方及接收方兩方賬戶將遵循借貸記賬法進行價值轉移。如果接收方帳戶擁有程式碼,並且程式碼執行,那麼其內部儲存也可能被改變,或者該程式碼可能向其它帳戶建立額外的訊息,從而產生進一步的價值轉移。
使用UTXO模型的好處是:
1、更高的隱私程度:如果一個使用者每接收一筆交易就使用一個新的地址,那麼外人通常很難將這些帳戶相互聯絡。這一特點使得以太幣在很大程度上非常適合作為貨幣,但不太適用於所有dapp,因為dapp通常需要跟蹤與使用者相捆綁的複雜狀態,並且可能不存在像貨幣那麼簡單的使用者狀態分割槽方案。
2、潛在的可擴充套件性範例:在理論上,UTXO與某些型別的可擴充套件性範例更加相容,因為我們只能依賴特定的幣的所有者來維護一個關於所有權的默克爾證明。此外,即使包括所有者在內的所有人都決定遺忘這些資料,最終受害的也只有幣的所有者。在一個帳戶範例中,如果每個人都失去了與該帳戶相對應的默克爾樹的特定部分,那麼他們將無法以任何方式處理任何能夠影響該帳戶的訊息,包括給該賬戶傳送訊息。然而,現實中還存在有不依賴於UTXO的可擴充套件性範例(這句話不太好理解)。
使用帳戶模型的好處是:
1、節省大量空間:舉個例子,如果一個帳戶擁有5個UTXO,那麼它從UTXO模型切換到帳戶模型所需耗費的空間將會從 (20 + 32 + 8) * 5 = 300 位元組(地址耗費20位元組,txid耗費32位元組,值耗費8個位元組)減少到20 + 8 + 2 = 30個位元組(地址耗費20位元組,值耗費8個位元組,nonce值(即隨機數,下同)耗費2個位元組(詳見下文))。事實上,由於帳戶需要儲存在帕特里夏樹中(詳見下文),因此這一部分所節省的空間並沒有這麼大,但也不小。此外,交易所需的空間變得更小(比如,在以太坊中只需要100位元組,而在比特幣中需要200到250位元組),因為每筆交易只需建立一個引用以及一個簽名,然後產生一個輸出。
2、更好的可替代性:由於這裡不存在區塊鏈層面的概念來區別特定的幣集合的來源,因此,無論從技術上還是從法律上而言,根據幣的來源來區分出特定的幣,併為之制定紅名單/黑名單方案都是不可行的。
3、簡單性:更容易編碼和理解。尤其在涉及複雜指令碼的時候,這一特點更加明顯。儘管我們可以強行將任意去中心化應用程式套入UTXO範例中,比如讓指令碼能夠限制特定UTXO所允許花費的UTXO型別,並且要求花費行為包含該指令碼所評估的應用狀態根變化的默克爾樹證明,但相比起賬戶範例,UTXO範例要更加複雜、更加簡陋。
4、持續的輕客戶端引用:輕型客戶端可以通過向下掃描特定方向的狀態樹來隨時訪問與帳戶相關的所有資料。在UTXO範例中,這些引用資料會隨每一筆交易而發生變化,這對於嘗試使用上述UTXO狀態根傳播機制的長期執行的dapp來說,將是一個特別棘手的問題。
綜上所述,考慮到我們所要面對的dapp將包含任意狀態和程式碼,我們認為帳戶模型的好處遠大於其它替代方案。此外,根據“我們沒有所謂的特色”原則,如果人們確實很重視個人隱私,那麼我們可以通過合約內的已簽署資料包協議來構建混合器(mixer)以及混幣(coinjoin)方案。
帳戶範例有一個弱點,那就是為了防止重放攻擊,每一筆交易都必須擁有一個“nonce值”,以便帳戶能夠跟蹤已被使用的nonce值,並且僅接受繼上一個被使用的nonce值之後當前nonce值為1的交易。這意味著,即使從此不再會使用的帳戶也永遠無法從帳戶狀態中移除。一個解決此問題的簡單方案是要求交易包含一個區塊編號,從而使其在一定週期後不可重放,並在每個週期內重置一次nonce值。礦工或其他使用者需要通過去“ping”不再使用的帳戶才能將其從狀態中刪除,如果我們將整體掃描作為區塊鏈協議本身的一部分,那麼這個成本實在是太高昂了。為了加快以太坊1.0的開發速度,我們沒有采用這種機制;但以太坊1.1及後續的版本可能會使用這樣的機制。
04
默克爾帕特里夏樹
默克爾帕特里夏樹(簡稱MPT,又叫trie,為方便起見,如無特殊情況,下文中trie一律用“樹”指代),最早由Alan Reiner設想並在Ripple協議中實現,將作為以太坊的主要資料結構,並用於儲存所有帳戶狀態以及每個區塊中的交易和收據。MPT是默克爾樹[1]和帕特里夏樹[2]的結合體,通過吸納二者的元素來建立一個同時具有以下兩種屬性的結構:
-
每一個唯一的鍵/值對都唯一地對映為一個根雜湊,並且不可能通過欺騙手段成為樹的一部分(除非攻擊者擁有大約2^128算力)
-
我們可以在對數時間內對鍵/值對進行更改,新增或刪除操作
這一結構使得我們能夠提供一種高效且易於更新的全域性狀態樹“指紋”。關於以太坊MPT的正式闡述可以參閱:https://github.com/ethereum/wiki/wiki/Patricia-Tree
MPT中的具體設計決策包括:
1、擁有兩類節點,kv節點和發散節點(更多詳情請參閱MPT規範)。kv節點的存在提高了效率,因為如果在特定區域中,樹是稀疏的,那麼kv節點可以作為一條“捷徑”,從而使得我們無需儲存深度為64的樹。
2、讓離散節點以十六進位制而非二進位制進行運作:這樣做是為了提高查詢效率。我們現在認為這一選擇的效果並不理想,因為我們可以通過一批儲存節點在二進位制範例中模擬十六進位制樹的查詢效率。然而,由於這一二進位制樹結構很容易實現錯誤並最終導致狀態根不匹配,我們決定將這一重組方案推遲到1.1版本再議。
3、空值和非成員之間沒有區別:這是基於簡單性的考慮,這種方案與以太坊的預設設定相容得更好,這種預設設定是指:未設定的值(例如餘額)通常按零處置,而零則由空字串進行表示。然而,我們注意到這一做法會損害到一部分的通用性,因此效果並非最優。
4、終止節點和非終止節點之間的區別:從技術上講,“節點終止”標誌是不必要的,因為在以太坊中,所有樹都用於儲存靜態的金鑰長度。但我們還是加入這一屬性以提高以太坊的通用性,並希望以太坊MPT的實現能夠原封不動地用於其它加密協議。
5、使用 sha3(k) 作為“安全樹”中的金鑰(用於狀態和帳戶儲存樹):通過將處於不利地位的離散節點鏈條的深度最大值設定為64層,並不斷呼叫SLOAD和SSTORE,我們可以讓針對樹的DoS攻擊更加困難。需要注意的是,這一方案也會使對樹進行列舉變得更加困難。如果你想要客戶端擁有列舉功能,最簡單的方案就是去維護一個數據庫對映sha3(k) -> k。
05
RLP
RLP(“遞迴長度字首”)編碼是以太坊所使用的主要序列化格式,我們可以在任何地方看到,比如區塊、交易、帳戶狀態資料和電報協議訊息。 RLP的正式闡述請參閱: https://github.com/ethereum/wiki/wiki/RLP
RLP旨在成為高度簡約的序列化格式,它唯一的目的就是儲存巢狀位元組陣列。與protobuf [3],BSON [4]以及其它現有的解決方案不同,RLP不會嘗試定義任何特定的資料型別,比如布林型別、浮點型、雙精浮點型甚至整型。相反,它僅僅以巢狀陣列的形式來儲存結構,並將其留給協議來確定陣列的含義。這裡沒有明確支援鍵/值對映,如果你想要支援鍵/值對映,那麼半官方建議是將這些對映表示為[[k1, v1], [k2, v2], ...],其中k1,k2 ...依照字串的標準排序進行排序。
RLP的替代方案是使用現有演算法,例如 protobuf 或 BSON。但是,我們更傾向於RLP,因為它(1)實現簡單,(2)在位元組層面保證絕對完美的一致性。在很多語言中,鍵/值對映都沒有明確的排序,並且浮點格式有許多特殊情況,這些情況可能會導致相同的資料擁有不同的編碼結果,從而產生不同的雜湊。通過在內部開發協議,我們可以確信協議的設計遵循這些目標(這是一個通用的原則,也適用於程式碼的其它部分,例如VM)。需要注意的是,BitTorrent所使用的bencode為RLP提供了一個還不錯的替代方案——儘管它在長度方面使用了十進位制編碼,這使得它略次於二進位制RLP。
06
壓縮演算法
電報協議和資料庫都使用自定義的壓縮演算法來儲存資料。我們可以將這一演算法描述為“對零進行行程長度編碼,並使其它值保留原樣”。當然,這裡面存在一些關於常見值的特殊情況,比如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位元組)。然而,最近我們進行了一些更改,將所有這些特殊情況全部移除。這意味著,在預設情況下,以太坊的資料結構會更加龐大。此外,我們還通過將資料儲存功能置於電報協議中並將其無縫插入使用者的資料庫實現中來把資料儲存功能新增到區塊鏈協議之外的層中。這種方法增加了以太坊的模組化特性,簡化了共識層,並且還提高了持續升級到即將部署的壓縮演算法的便利性(例如通過網路協議版本更新來進行)。
07
樹的用法
警告:本節假設讀者已經瞭解布隆過濾器的工作原理。相關介紹請參閱http://en.wikipedia.org/wiki/Bloom_filter
在以太坊區塊鏈中,每個區塊頭都包含指向三個樹的指標:狀態樹,表示獲取特定區塊後的全域性狀態;交易樹,表示由索引鍵入的區塊中的所有交易(即key 0:將要執行的第一筆交易;key 1:第二筆交易等,依此類推)以及收據樹,表示與每筆交易相對應的“收據”。交易的收據是一個經過RLP編碼的資料結構:
[ medstate, gas_used, logbloom, logs ]
其中:
-
medstate是處理交易後的狀態樹根
-
gas_used是處理交易後所花費的 gas 數量
-
logs是在交易執行(包括主呼叫和子呼叫)期間由 LOG0 ... LOG4 操作碼所生成的專案列表,其形式為[address, [topic1, topic2...], data]。 address是生成日誌的合約的地址,它的主題最多為4個32位元組的值,並且它的資料是任意大小的位元組陣列。
-
logbloom是一個布隆過濾器,由交易中所有日誌的地址和主題組成
區塊頭中還有一個布隆過濾器,它是該區塊內的交易的所有布隆過濾器的邏輯或(OR)。使用這種結構的目的是讓以太坊協議在儘可能多的方面對輕客戶端友好。想了解更多關於以太坊輕客戶端及其用例的資訊,請參閱https://github.com/ethereum/wiki/wiki/Light-client-protocol#principles的原則部分。
參考註釋:
[1]默克爾樹:https://en.wikipedia.org/wiki/Merkle_tree
[2] 帕特里夏樹:https://en.wikipedia.org/wiki/Radix_tree
[3] protobuf:https://developers.google.com/protocol-buffers/docs/pythontutorial
[4] BSON:https://bsonspec.org/
[5] GHOST:https://eprint.iacr.org/2013/881.pdf
[6] Decker和Wattenhofer 2013年在蘇黎世所撰寫的論文見:
https://www.tik.ee.ethz.ch/file/49318d3f56c1d525aabf7fda78b23fc0/P2P2013_041.pdf
本文翻譯:喏唄爾
原文作者:Vitalik Buterin
原文連結:https://github.com/ethereum/wiki/wiki/Design-Rationale