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 層內提供了大部分所需的效果,而且不會帶來負面效應。
·
1 代後裔的限制:(例如,設c = child 且p = parent,則 c(c(p(p(p(head))))) 是無效的):這也是出於簡潔性的設計目標,而且上述的模擬器顯示這不會帶來很大的中心化風險。 (註:此句難解;一種可能的意思是:叔塊的後代不能作為叔塊,即只有主鏈的一代旁支能作為叔塊。)
·
叔塊必須是有效的 :叔塊必須是有效的 header,而不是有效的區塊。這樣做也是為了簡化,將區塊鏈模型保持為線性資料結構(而不會變成 DAG)。不過,要求叔塊是有效的區塊也是有效的方法。
·
獎金分配:7/8 的挖礦基礎獎勵分配給叔塊,1/32 分給侄塊,它們交易費用都是 0%。如果費用佔多數,從中心化的角度來看,這會使叔塊激勵機制無效;然而,這也是為什麼只要我們繼續使用 PoW,以太坊就會不斷發行以太幣的原因。
難度更新演算法
目前以太坊透過以下規則進行難度更新: div>
難度更新規則的設計目標如下:
快速更新:區塊間的時間應該隨著hash 算力的增減而快速調整;
低波動:如果挖礦算力恆定,那麼難度不應劇烈波動;
簡單:演算法的實作應相對簡單;
低記憶體:演算法不應依賴過多的歷史區塊,要盡可能少的使用「記憶體變數」。假設有最新的十個區塊,將儲存在這十個區塊頭部的記憶體變數相加,這些區塊都可用於演算法的計算;
不可爆破:演算法不應讓礦工有過多篡改時間戳或礦池反覆添加或刪除算力的激勵我們當前的算法在低波動性和抗爆破性上並不理想。最近,我們計劃把時間戳參數改為與父區塊和祖父區塊比較,所以礦工只有在連續挖 2 個區塊時,才有動力去修改時間戳記。
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 元件應盡可能緊湊;
為預期用途而特化: span>在VM 上建構的應用應能處理20 位元組的位址,以及32 位元的自訂加密值,擁有用於自訂加密的模數運算、讀取區塊和交易資料與狀態互動等能力;
簡單安全:為了讓VM 不被利用,應該能夠輕鬆地讓建立一套gas 消耗成本模型的操作;
優化友善:應該要容易最佳化,以便即時編譯(JIT)和VM 的加速版本能夠建構出來。
同時EVM 也有以下特殊設計:
暫時/永久儲存的差異: h4>
我們先來看看什麼是暫存和永久儲存。暫存:存在於 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:主要的作用是讓合約依據模板檢查其他合約的程式碼,甚至是在與其他合約互動前,模擬它們。 見
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 可以創建獨立程式碼的位置(可複製貼上到其他合約的編譯函數,如果它們以不同索引結束,不會被打斷)。
歡迎加入律動 BlockBeats 官方社群:
Telegram 訂閱群:https://t.me/theblockbeats
Telegram 交流群:https://t.me/BlockBeats_App
Twitter 官方帳號:https://twitter.com/BlockBeatsAsia