以太坊虚拟机EVM的缺陷与不足
那么接下來讓我們直接進入主題。首先,EVM的設計初衷是什么?它為什么被設計成目前我們看的樣子呢?根據以太坊官方提供的設計原理說明,EVM的設計目標主要針對以下方面:
讓我們通過對比x86匯編碼來看看它的表現。
?
首先是兩個32bit整數相加的x86匯編碼(也就是大多數PC的處理器采用的):
mov eax, dword [number1]
add eax, dword [number2]
然后是2個64bit整數相加,這里假設采用64位處理器:
mov rax, qword [number1]
add rax, qword [number2]
接下來是在32位x86計算機上兩個256bit整數相加:
mov eax, dword [number]add dword [number2], eaxmov eax, dword [number1+4]
adc dword [number2+4], eax
mov eax, dword [number1+8]
adc dword [number2+8], eax
mov eax, dword [number1+12]
adc dword [number2+12], eax
mov eax, dword [number1+16]
adc dword [number2+16], eax
mov eax, dword [number1+20]
adc dword [number2+20], eax
mov eax, dword [number1+24]
adc dword [number2+24], eax
mov eax, dword [number1+28]
adc dword [number2+28], eax
當然還有在64位x86計算機上兩個256bit整數相加:mov rax, qword [number]add qword [number2], raxmov rax, qword [number1+8]
adc qword [number2+8], rax
mov rax, qword [number1+16]
adc qword [number2+16], rax
mov rax, qword [number1+24]
adc qword [number2+24], rax
通過以上比較足以說明采用256bit整數遠比采用處理器原生支持的整數長度要復雜。EVM之所以選擇這種設計,主要是因為僅支持256bit整數會比增加額外的用于處理其他位寬整數的opcodes來的簡單得多。僅有的非256bit操作是一系列的push操作,用于從memory中獲取1-32字節的數據,以及一些專門針對8bit整數的操作。?
那么對于所有操作都采用這種低效的整數位寬的設計初衷是什么呢?
“4字節或8字節字長限制了更大的內存尋址和復雜的密碼學運算,同時無限制的值將很難實現安全的gas模型”
關于地址,我必須承認,能夠僅用單個操作實現兩個地址的比較確實很酷。但是,在x86機器上采用32bit整數實現相同功能也并不復雜(無SSE和任何其他優化):
mov esi, [address1]mov edi, [address2]mov ecx, 32 / 4
repe cmpsd
jne not_equal
; if reach here, then they’re equal
假設address1和address2 都是確定的地址,僅需要6+5+5=16字節的opcodes,而如果地址都在棧上,則僅需要6+3+3=12字節的opcode。關于另一個理由“復雜的密碼學運算”,筆者從幾個月前第一次看到這個理由,直到現在都沒有看到過一個不涉及地址或哈希值比較的256bit整數的應用實例。密碼學運算如果在區塊鏈上運行顯然過于昂貴了。筆者在github上搜索了一個多小時,試圖找到一個在solidity合約中用到密碼學運算的實例,結果卻一無所獲。幾乎所有的密碼學運算對于目前的計算機來說都是復雜的,所以在以太坊公有鏈上進行這種運算是非常昂貴的(必須消耗大量的gas,更不用說把密碼學算法用solidity實現所需要的工作量)。當然,如果是一條私有鏈,gas消耗可能不是問題。但如果你是這條鏈的擁有者,你應該也不會選擇用低效的EVM智能合約來實現密碼學運算,而會選擇采用C++,Go,或者其他一些編程語言實現。綜上所述,EVM僅支持256bit整數的理由完全不成立。筆者認為這是EVM最根本也是最明顯的問題,除此之外,EVM還有不少問題,下面我們一一道來。
EVM的內存分配模型
EVM中主要有3個用于存儲數據的地方:
除此之外,分配內存所需要花費的gas并不是線性的。比如你分配了100字長的內存,之后又分配1字長內存,這最后1字長內存的花費將明顯高于你一開始就只分配1字長內存的花費。這又大大增加了保證內存安全所需的花費。
?
既然如此,那為什么非要使用內存呢?為什么不使用棧?實際上EVM中棧有明顯的限制。 EVM中的棧
EVM是一個基于棧的虛擬機。這就意味著對于大多數操作都使用棧,而不是寄存器。基于棧的機器往往比較簡單,且易于優化,但其缺點就是比起基于寄存器的機器所需要的opcode更多。
所以EVM有許多特有的操作,大多數都只在棧上使用。比如SWAP和DUP系列操作等,具體請參見EVM文檔。現在我們試著編譯如下合約:
pragma solidity ^0.4.13;contract Something{
?
function foo(address a1, address a2, address a3, address a4, address a5, address a6){
address a7;
address a8;
address a9;
address a10;
address a11;
address a12;
address a13;
address a14;
address a15;
address a16;
address a17;
}
}
你將看到如下錯誤:CompilerError: Stack too deep, try removing local variables.這個錯誤是因為當棧深超過16時發生了溢出。官方的“解決方案”是建議開發者減少變量的使用,并使函數盡量小。當然還有其他幾種變通方法,比如把變量封裝到struct或數組中,或是采用關鍵字memory(不知道出于何種原因,無法用于普通變量)。既然如此,讓我們試一試這個采用struct的解決方案:pragma solidity ^0.4.13;contract Something{
struct meh{
address x;
}
function foo(address a1, address a2, address a3, address a4, address a5, address a6){
address a7;
address a8;
address a9;
address a10;
address a11;
address a12;
address a13;
meh memory a14;
meh memory a15;
meh memory a16;
meh memory a17;
}
}
結果呢?CompilerError: Stack too deep, try removing local variables.我們明明采用了memory關鍵字,為什么還是有問題呢?關鍵在于,雖然這次我們沒有在棧上存放17個256bit整數,但我們試圖存放13個整數和4個256bit內存地址。這當中包含一些Solidity本身的問題,但主要問題還是EVM無法對棧進行隨機訪問。據我所知,其他一些虛擬機往往采用以下兩種方法之一來解決這個問題:
然而,在EVM中,棧是唯一免費的存放數據的區域,其他區域都需要支付gas。因此,這相當于鼓勵盡量使用棧,因為其他區域都要收費。正因為如此,我們才會遇到上文所述的基本的語言實現問題。
?
bytecode大小
在EVM設計文檔中,設計者聲稱他們的目標是使得EVM的bytecode既簡單又高度壓縮。然而,這就像是試圖寫出既詳盡又簡潔的代碼一樣,實際上兩者是存在一定矛盾。要實現一個簡單的指令集就需要盡量限制操作的種類,并保持每種操作的盡量簡單;然而,要實現高度壓縮的bytecode則需要引入擁有豐富操作的指令集。
即使是“高度壓縮的bytecode”這一目標也沒有在EVM中實現,他們更加側重于實現易于生成gas模型的指令集。我并不是說這是錯的,只是想表明作為官方聲明的EVM最重要的目標之一最終并沒有實現這一事實。同時,EVM設計文檔中給出了一個數據:C語言實現的“Hello World”簡單程序生成4000字節的bytecode。這一結果并不正確,很大程度取決于編譯環境以及優化程度。在他們所述的C程序中,應該同時包含了ELF數據,relocation數據以及alignment優化等。筆者嘗試編譯了一個非常簡單的C程序(只有一個程序骨架),只需要46字節的x86機器碼;同時還用C語言寫了一個簡單的greeter type程序(Solidity示例程序),最終生成大約700字節bytecode,而同樣的Solidity示例程序則需要1000字節bytecode。
我當然明白簡化指令集是出于某些安全性因素考慮,但這顯然會導致區塊鏈更加臃腫。如果EVM智能合約的bytecode盡可能小的話確實是有害的。我們完全可以通過增加標準庫或是支持可以批處理某些基本操作的opcode來減小bytecode。
?
256bit整數(補充)
256bit整數確實令人頭疼,所以這里再做一些補充。最令人費解的是256bit整數被用到了一些根本沒必要的地方。比如,我們根本不可能在合約中使用超過4B(32bit)單位的gas,那么你猜在EVM中采用什么長度的整數來作為gas的計量呢?沒錯,當然是256bit。內存使用也非常昂貴,那內存大小的計量呢?自然也是256bit,當你的合約需要用到比宇宙中原子數量還多的地址時這個數字或許真的能派上用場。雖然我不認同在尋址或是永久內存的變量中使用256bit整數,但不得不說它使得計算某些數據的hash時能夠避免沖突,因此這還能勉強接受。但對于任一個instance,本可以采用任何整數長度,EVM還是使用了256bit。甚至JUMP也使用256bit,但他們限制了最大的JUMP地址為0x7FFFFFFFFFFFFFFF,相當于限制在64bit整數范圍內。最后,以太坊中的幣值當然也采用了256bit數來計算。ETH的最小單位是wei,所以總的幣的數量(單位為wei)為1000000000000000000 * 200000000 (200M只是估計值,目前僅有約92M)。而2^256約為1.157920892373162e+77,這足以表示所有已存在的所有ETH外加比全宇宙原子數還多的wei……歸根結底,256bit整數在EVM所設計的大多數應用中都沒有必要。
?
缺少標準庫
如果你曾經開發過Solidity智能合約的話,你應該也會碰到這個問題,因為Solidity中根本就沒有標準庫。如果你想比較兩個字符串,Solidity中根本就沒有類似strcmp或memcmp的標準庫函數供你調用,你必須自己用代碼實現或在網上拷貝代碼來實現。Zeppin項目使這一情況得到一定改善,他們提供了一個可供合約使用的標準庫(通過將代碼包含在合約中或是調用外部合約)。然而,這種方式的限制也很明顯,主要是在gas消耗方面。比如判斷字符串是否相等,進行兩次SHA3操作然后比較hash值顯然要比循環比較每個字符所要花費的gas要少。如果存在預編譯好的標準庫,并設定合理的gas價格,這將更加有利于整個智能合約生態的發展。目前的情況是,人們只能不斷的從一些開源軟件中復制黏貼代碼,首先這些代碼的安全性無法保證,再加上人們會為了更小的gas消耗而不斷修改代碼,這就有可能對他們的合約引入更嚴重的安全性問題。
?
gas經濟模型中的博弈論
我打算寫一篇新的博客單獨闡述這個主題。EVM不僅使寫出好的代碼變得很困難,還令其變得非常昂貴。比如,在區塊鏈上存儲數據需要耗費大量的gas。這意味著在智能合約中緩存數據的代價會非常大,因此往往在每次合約運行時重新計算數據。隨著合約被不斷執行,越來越多的gas和時間都被花在了重復計算完全相同的數據上。實際上單純通過交易在區塊鏈上存儲數據并不會消耗太多的gas,因為這并不會直接增加區塊的大小(不管以太坊還是Qtum都是如此)。真正花費比較大的其實是那些發送給合約的數據,因為這將直接增加區塊的大小。在以太坊中,通過交易在區塊鏈上記錄32byte的數據比在合約中存儲相同的數據消耗的gas要少一些,而如果是64byte的數據,則消耗的數據就少得多了(29,704 gas v.s. 80,000gas)。在合約中儲存數據會有“virtual”的花費,但比大多數人想象的要少得多。基本上就是遍歷區塊鏈上數據庫的花費。Qtum和以太坊采用的RLP和LevelDB數據庫系統在這方面非常高效,但持續的成本并不是線性的。
EVM鼓勵這種低效率的代碼的另一原因就是其不支持直接調用智能合約中某個具體的函數。這當然是出于安全性考慮,如果允許直接調用在ERC20代幣合約中的withdraw函數,結果確實會是災難性的。但是這在標準庫調用中將會非常高效。目前EVM中要么執行智能合約的所有代碼,要么一點也不執行,完全不可能只執行其中部分代碼。程序總是從頭開始運行,無法跳過Solidity ABI引導代碼。所以這導致的結果就是一些小函數被不斷復制(因為通過外部調用將更加昂貴),并且鼓勵開發者在同一個合約中包含盡量多的函數。調用一個100bytes的合約并不比調用10000bytes的合約昂貴,盡管所有代碼都必須加載到內存中。
最后一點,就是EVM中無法直接獲取合約中存儲的數據。合約代碼必須先被完全加載并執行,并且包含你所請求的數據,最終通過合約調用返回值的形式返回數據(還得保證沒有多個返回值)。同時,當你不確定你需要的是哪個數據,需要來來回回地調用合約時,第二次調用合約所需要的gas并沒有任何折扣(不過至少合約還在緩存中,對節點來說第二次調用稍微便宜一些)。實際上完全可以在不加載整個外部合約的基礎上訪問其數據,這其實和獲取當前合約的存儲數據沒什么兩樣,為什么偏要采用如此昂貴且低效的方式呢?
?
難以調試和測試
這個問題不僅僅是由于EVM的設計缺陷,也和其實現方式有關。當然,有一些項目正在做相關工作使整個過程變得簡單,比如Truffle項目。然而EVM的設計又使這些工作變得很困難。EVM唯一能拋出的異常就是“OutOfGas”,并且沒有調試日志,也無法調用外部代碼(比如test helpers和mock數據),同時以太坊區塊鏈本身很難生成一條測試網絡的私鏈,即使成功,私鏈的參數和行為也與公鏈不同。Qtum至少還有regtest模式可用,而在EVM中使用mock數據等進行測試則真的非常困難。據我所知目前還沒有任何針對Solidity的調試器,雖然有一款我知道的EVM assembly調試器,但其使用體驗極差。EVM和Solidity都沒有創建用于調試的符號格式或是數據格式,并且目前沒有任何一個EIP提出要建立像DWARF一樣標準的調試數格式。
?
不支持浮點數
對于那些支持EVM不需要浮點數的人來說,最常用的理由就是“沒有人會在貨幣中采用浮點數”。這其實是非常狹隘的想法。浮點數有很多應用實例,比如風險建模,科學計算,以及其他一些范圍和近似值比準確值更加重要的情況。這種認為智能合約只是用于處理貨幣相關問題的想法是非常局限的。
?
不可修改的代碼
智能合約在設計時需要考慮的重要問題之一就是是可升級性,因為合約的升級是必然的。在EVM中代碼是完全不可修改的,并且由于其采用哈佛計算機結構,也就不可能將代碼在內存中加載并執行,代碼和數據是被完全分離的。目前只能夠通過部署新的合約來達到升級的目的,這可能需要復制原合約中的所有代碼,并將老的合約重定向到新的合約地址。給合約打補丁或是部分升級合約代碼在EVM中是完全不可能的。
?
小結
不可否認,EVM作為第一個區塊鏈虛擬機存在諸多問題,這和絕大多數新生事物一樣(比如Javascript)。并且由于它的設計比較非主流,我認為不會有主流的編程語言能夠移植到EVM上。這種設計可以說對于近50年來的大多數編程范例來說都不太友好。比如JUMPDEST使得jump table優化更加困難,不支持尾遞歸,詭異且不靈活的內存模型,棧的限制,當然還有256bit整數等等。這種種問題都使得移植主流編程語言的代碼變得困難重重。我想這就是目前EVM只能支持專門定制的開發語言的原因。這是在是件令人遺憾的事。
?
筆者寫這篇文章并不是想要攻擊EVM的設計者,只是就事論事的討論。事后諸葛亮總是看上去很容易,實際上我知道EVM設計者們已經意識到某些方面的不足,并因此感到懊悔。我并不想指責他們(雖然看起來我就是在吐槽),我真正的目的是想通過指出這些問題來引起整個區塊鏈開發者社區的重視,從而我們不會重蹈覆轍,同時相信也能解答諸如“為什么我在Solidity中不能實現blabla功能”等問題。EVM的設計比較復雜,我們都還在學習這種設計帶來的好處以及弊端。我們可以確信的是,目前的智能合約還遠沒有達到我們對它的期望,在未來它的功能會變得更加強大。EVM是這個領域的開拓者,通過它我們可以不斷加深對智能合約的認識,并從中總結出最合理的設計。路漫漫其修遠兮,吾將上下而求索。
原文地址:http://www.8btc.com/evm-qtum-0817
總結
以上是生活随笔為你收集整理的以太坊虚拟机EVM的缺陷与不足的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Truffle3.0集成NodeJS并完
- 下一篇: King of the Ether