自從看到了這篇文章,我便一直很想深入了解以太坊的無(wú)狀態(tài)客戶端。當(dāng)然,經(jīng)過(guò)15個(gè)月的摸索,我對(duì)以太坊中的狀態(tài)、軟件、網(wǎng)絡(luò)的認(rèn)知都發(fā)生了很大的變化,比如說(shuō),我現(xiàn)在認(rèn)為要引入無(wú)狀態(tài)客戶端,則硬分叉在所難免;這與我之前的想法不同。但即便如此,我還是很高興能分享一些學(xué)習(xí)上的收獲,并給出后續(xù)發(fā)展建議。
無(wú)狀態(tài)客戶端的思想
在闡述無(wú)狀態(tài)客戶端之前,我們先來(lái)看看在以太坊客戶端軟件中一般的交易處理過(guò)程:
在區(qū)塊中執(zhí)行交易需要交易數(shù)據(jù)(藍(lán)色矩形所示),及當(dāng)前狀態(tài)(黃色矩形所示);執(zhí)行結(jié)束后,原本的當(dāng)前狀態(tài)變?yōu)闅v史狀態(tài),同時(shí)產(chǎn)生新的當(dāng)前狀態(tài)。處理交易可能還需要一些別的數(shù)據(jù)(如區(qū)塊時(shí)間戳,或是前個(gè)區(qū)塊哈希值),但我們先忽略這些大小固定且無(wú)關(guān)緊要的部分。執(zhí)行交易還會(huì)產(chǎn)生明細(xì)(receipt)(綠色部分),但在討論無(wú)狀態(tài)客戶端時(shí)也可暫時(shí)忽略。
無(wú)狀態(tài)客戶端的核心思想是:在區(qū)塊中執(zhí)行交易過(guò)程時(shí),不訪問(wèn)整個(gè)狀態(tài)。區(qū)塊創(chuàng)建者以分離的數(shù)據(jù)結(jié)構(gòu)作為區(qū)塊補(bǔ)充,里面提取了執(zhí)行交易所需的所有狀態(tài);為了讓執(zhí)行交易的人相信這些額外數(shù)據(jù)的確來(lái)自當(dāng)前狀態(tài),還要附上默克爾證明。這是可以做到的,因?yàn)槊總€(gè)區(qū)塊頭都帶有 “狀態(tài)根”,這個(gè)默克爾樹根凝結(jié)了區(qū)塊內(nèi)交易執(zhí)行完成后所有的狀態(tài)值。有了這些證明,我們能夠以下面這種形式執(zhí)行交易:
值得一提的是,這些區(qū)塊信息(我們稱之為 “區(qū)塊證明”)對(duì)于有狀態(tài)客戶端(那些想要保有全部狀態(tài)和歷史數(shù)據(jù)的客戶端)來(lái)說(shuō)也是非常有用的。當(dāng)前情況下,所謂的 “全節(jié)點(diǎn)” (在接收到區(qū)塊時(shí))會(huì)依憑本地保存的狀態(tài)執(zhí)行區(qū)塊交易,計(jì)算新?tīng)顟B(tài)的默克爾根并驗(yàn)證是否與區(qū)塊頭的默克爾樹根相同(譯者注:以此驗(yàn)證新區(qū)塊的合法性)。計(jì)算默克爾樹根需要大量的計(jì)算資源,包括大量?jī)?nèi)存以及大量 I/O 讀寫,詳細(xì)的描述見(jiàn)此。如果擁有前面的補(bǔ)充信息,全節(jié)點(diǎn)驗(yàn)證執(zhí)行過(guò)程的正確性會(huì)變得非常容易,大部分情況下將不需要涉及讀寫操作,就能夠基于執(zhí)行結(jié)果更新當(dāng)前和歷史的狀態(tài)數(shù)據(jù)庫(kù);全節(jié)點(diǎn)無(wú)需緩存狀態(tài)樹,與當(dāng)前狀態(tài)和歷史狀態(tài)數(shù)據(jù)庫(kù)的交互轉(zhuǎn)為以寫入為主。具體過(guò)程如下:
區(qū)塊證明占多大空間?
無(wú)狀態(tài)客戶端最大的缺點(diǎn)是——除了區(qū)塊,還需要在網(wǎng)路中傳輸一些額外的數(shù)據(jù)(區(qū)塊證明)。究竟這些額外的數(shù)據(jù)有多少呢?為了計(jì)算區(qū)塊證明的大小,我基于 Turbo-Geth 創(chuàng)建了一個(gè)無(wú)狀態(tài)客戶端原型,以下是它做的事情:
1. 從以太坊主網(wǎng)逐一收集區(qū)塊
2. 對(duì)每一個(gè)區(qū)塊分別抽取出交易需要訪問(wèn)的狀態(tài),以及被交易訪問(wèn)的智能合約字節(jié)碼
3. (通過(guò)計(jì)算狀態(tài)默克爾樹根)選取出足以驗(yàn)證所提取狀態(tài)確實(shí)屬于狀態(tài)的最少哈希值集合
4. 添加一些結(jié)構(gòu)信息,將上述證明(哈希值集合)和狀態(tài)抽取物編碼為樹型結(jié)構(gòu)??赡懿捎玫木幋a方式不是最好的,不過(guò)結(jié)果表明編譯出來(lái)的輸出遠(yuǎn)小于其他部分,所以可接受。
5. 將區(qū)塊證明編碼為字節(jié)數(shù)組。
6. 將字節(jié)數(shù)組解碼出區(qū)塊證明(即那個(gè)樹型結(jié)構(gòu)的數(shù)據(jù),一些節(jié)點(diǎn)來(lái)自于狀態(tài),一些則是無(wú)關(guān)狀態(tài)部分的哈希值)。
7. 再次執(zhí)行區(qū)塊,不過(guò)這回不訪問(wèn)當(dāng)前狀態(tài),而是直接與區(qū)塊證明進(jìn)行交互。
8. 驗(yàn)證狀態(tài)根和所有存儲(chǔ)根的正確性。
整個(gè)流程花了不少時(shí)間,但我希望得到的數(shù)據(jù)足夠精確。如我所料,雖然這個(gè)原型已經(jīng)能處理許多瑣碎的問(wèn)題(因?yàn)?hexary 的默克爾帕特里夏樹包含葉節(jié)點(diǎn)、擴(kuò)展節(jié)點(diǎn)、嵌入節(jié)點(diǎn)等等復(fù)雜的結(jié)構(gòu)),還是有一些罕見(jiàn)情況導(dǎo)致我的原型報(bào)錯(cuò)(在區(qū)塊 5340939、5361803、5480357、5480507、5480722、5632299、5707052、5769636 。..。.. );不過(guò)考慮到 670 萬(wàn)個(gè)區(qū)塊中只出現(xiàn)數(shù)十個(gè)報(bào)錯(cuò),我有信心這些小問(wèn)題不會(huì)影響數(shù)據(jù)分析的結(jié)果(當(dāng)然,我會(huì)盡快 debug)。
目前我的數(shù)據(jù)只采集至第6757045 個(gè)區(qū)塊,但我想很快就能超過(guò)這個(gè)數(shù)字。
第一張圖表表示區(qū)塊證明的總體量(注:所有圖表都經(jīng)過(guò)窗口 = 1024 的移動(dòng)平均計(jì)算)。
在偽龍硬分叉(區(qū)塊 2675000)之前,少量的 gas 消耗也可能產(chǎn)生非常大的區(qū)塊證明, 這個(gè)缺陷已經(jīng)在 2016 年秋天被修復(fù)。
下面我們只看偽龍硬分叉后至今的數(shù)據(jù)表現(xiàn):
為了清除余額和 nounce 值為零的賬戶狀態(tài),偽龍硬分叉后進(jìn)行 “狀態(tài)清理”,導(dǎo)致圖表最左側(cè) “峰值” 的出現(xiàn)。但 2017 年下半年,以及 2018 年的 “區(qū)塊證明” 的規(guī)模已經(jīng)超過(guò)了當(dāng)時(shí)的水平。
分解
首先,我們將區(qū)塊證明分解為三個(gè)部分:1)所有與 “主” 狀態(tài)樹有關(guān)的部分;2)所有與合約存儲(chǔ)樹有關(guān)的部分;3)智能合約字節(jié)碼。
可能從圖上有點(diǎn)看不清楚,不過(guò)可以看到 2016 年發(fā)生的垃圾攻擊造成當(dāng)時(shí)的字節(jié)碼(紅線)和 “主” 狀態(tài)樹的區(qū)塊證明(黃線)激升。有些人還記得,當(dāng)時(shí)對(duì)于智能合約的字節(jié)碼大小并沒(méi)有限制(現(xiàn)在限制小于 24k),造成當(dāng)時(shí)能夠部署超大的智能合約,并通過(guò) EXTCODESIZE 之類的函數(shù)進(jìn)行查詢。
實(shí)施偽龍硬分叉后的圖表表明,主默克爾樹的區(qū)塊證明在過(guò)去大部分時(shí)候體量占比較大,不過(guò)合約存儲(chǔ)證明(藍(lán)線)和字節(jié)碼隨后開始趕上。
分解狀態(tài)樹區(qū)塊證明
這里我們會(huì)進(jìn)一步將狀態(tài)樹區(qū)塊證明分解為四個(gè)部分。前兩部分是交易需要讀取的鍵值對(duì),或者是為了滿足默克爾帕特里夏樹的一些特殊需求所需的數(shù)據(jù)。
看起來(lái)狀態(tài)清除操作對(duì)于讀取鍵值對(duì)還是造成很大的影響。我們會(huì)發(fā)現(xiàn)鍵值對(duì)中值的大?。ㄒ话闶?80 字節(jié))通常比鍵要大得多( key 一般小于 64 字節(jié),而我剛剛才意識(shí)到鍵可以被壓縮,因?yàn)槲野衙總€(gè)十六進(jìn)制數(shù)都算為一個(gè)字節(jié))。
接下來(lái),我們看看用來(lái)建構(gòu)默克爾證明的哈希值,以及那些我稱之為 “mask(掩碼)” 的結(jié)構(gòu)化信息:
可以輕易發(fā)現(xiàn),相比于哈希值,結(jié)構(gòu)化信息的大小是可以忽略不計(jì)的;也可以進(jìn)一步說(shuō)明哈希值占區(qū)塊證明大小的主要組成部分(對(duì)于合約存儲(chǔ)證明來(lái)說(shuō)也是如此)。
分解合約存儲(chǔ)證明
與前面相同,首先是鍵值對(duì):
有意思的是,合約存儲(chǔ)證明中鍵值對(duì)的值(最多占 32 字節(jié))通常遠(yuǎn)小于鍵。
接著是哈希值和結(jié)構(gòu)化信息:
可以看到,與狀態(tài)樹區(qū)塊證明的表現(xiàn)相似。
與狀態(tài)費(fèi)用研究的相關(guān)性
我搞這個(gè)研究的其中一個(gè)目的是想弄清楚使用無(wú)狀態(tài)客戶端是否能規(guī)避某些類型的租金(譯者注:所指應(yīng)是 “狀態(tài)存儲(chǔ)租金”)。因?yàn)楫?dāng)前最大的挑戰(zhàn)在于:合約存儲(chǔ)租金的存在,可能會(huì)導(dǎo)致許多現(xiàn)有的合約遭受 griefing 攻擊(譯者注:大意為盡管不能讓攻擊者受益,但會(huì)讓受害者感覺(jué)很苦惱的攻擊形式)。為了保護(hù)智能合約避免遭此類攻擊,“狀態(tài)費(fèi)用” 協(xié)議第三版提出了費(fèi)用預(yù)付(押金)的概念,能暫時(shí)保護(hù)合約免受攻擊,給已部署合約提供一些緩沖時(shí)間,能夠升級(jí)為不受影響的代碼。
無(wú)狀態(tài)客戶端的構(gòu)想,至少在讓智能合約存儲(chǔ)免受 griefing 攻擊方面(如上文所示),提出了一種成本雖高但永久有效的預(yù)防方法。
不過(guò)如果真的引入某種無(wú)狀態(tài)客戶端,交易發(fā)送方無(wú)可避免的必須針對(duì)交易產(chǎn)生的區(qū)塊證明,按比例支付額外的 gas,這樣一來(lái)合約存儲(chǔ)操作(SLOAD、SSTORE)的成本可能比現(xiàn)在高昂。附帶一提,Martin Swende 最近的分析表明 SLOAD 費(fèi)用似乎被嚴(yán)重的低估。
我們可以將無(wú)狀態(tài)客戶端用于已有的智能合約,并讓新的智能合約自主選擇是否使用存儲(chǔ)租金。我估計(jì)這個(gè)租金會(huì)比使用 “區(qū)塊證明” 的成本便宜許多,但總有一天這個(gè)臨時(shí)費(fèi)用會(huì)消失(因?yàn)榇蠹叶际褂脽o(wú)狀態(tài)客戶端啦)。我們不會(huì)強(qiáng)迫已有的智能合約在某個(gè)時(shí)間點(diǎn)必須轉(zhuǎn)變?yōu)榧嫒菘蛇x費(fèi)用的部署形式,相反地我們希望以激勵(lì)手段鼓勵(lì)大家進(jìn)行遷移。
后續(xù)的建議
顯然,我要增加數(shù)據(jù)收集至當(dāng)前區(qū)塊,并修復(fù)數(shù)據(jù)分析中存在的 bug 。
即使我們?nèi)サ襞c合約儲(chǔ)存相關(guān)的部分,區(qū)塊證明的大小仍是個(gè)值得重視問(wèn)題。我認(rèn)為有兩種方案能降低它的大?。?/p>
1. 在無(wú)狀態(tài)客戶端中引入多一點(diǎn)的 “富狀態(tài)性”。如果大多數(shù)客戶端都持有最新的 N 個(gè)區(qū)塊證明(N = 1, 2, 3 。..),現(xiàn)在你(以太坊節(jié)點(diǎn)之一)知道你的對(duì)等節(jié)點(diǎn)保有許多以前的區(qū)塊證明,那么你可能會(huì)選擇只發(fā)送后 N 個(gè)區(qū)塊證明中不包含的部分。因?yàn)槟阒拦?jié)點(diǎn)有能力重構(gòu)完整的狀態(tài)證明。這個(gè)方案還需要經(jīng)過(guò)更多分析驗(yàn)證。
2. 使用 SNARK 證明,將現(xiàn)在區(qū)塊證明中可變的 “哈希值” 部分壓縮為固定大?。ň臀宜?,目前 SNARK 產(chǎn)生的證明大小約 60k 左右)。這個(gè)方案還需要投入更多的研究和開發(fā)工作,比如圍繞 Keccak256 算法創(chuàng)建 SNARK 證明,要考慮證明需要付出多少成本,是否需要用 CPU 加速證明過(guò)程、保證出塊速度足夠快等等。
評(píng)論