在上一篇關(guān)于以太坊1.x的文章中,我們對(duì)Eth 1.x研究的起源、利害因素以及一些可能的解決方案做了簡(jiǎn)要回顧。在上篇文章的結(jié)尾我們提到了“無狀態(tài)”以太坊的概念,而在本文中我們將進(jìn)一步詳細(xì)闡釋無狀態(tài)客戶端。
無狀態(tài)(stateless)是Eth 1.x研究的新方向,因此我們將進(jìn)行一次相對(duì)深入的探析,以便對(duì)未來可能面臨的挑戰(zhàn)和可能性了然于胸。如果讀者有興趣進(jìn)一步了解,我會(huì)盡量提供相關(guān)資源的鏈接。
什么是“狀態(tài)”?
要解釋無狀態(tài)以太坊,我們首先需要理解“狀態(tài)”(state)的概念。當(dāng)我們提到“狀態(tài)”時(shí),一般是指“事務(wù)的狀態(tài)”。
以太坊的完整“狀態(tài)”描述了所有賬戶和余額的當(dāng)前狀態(tài),以及在EVM中部署和運(yùn)行的所有智能合約的集體歷史。鏈上每個(gè)最終確定的區(qū)塊,都有且只有一個(gè)狀態(tài),這是由網(wǎng)絡(luò)中的所有參與者共同確認(rèn)的。每當(dāng)有新的區(qū)塊被添加到鏈上,狀態(tài)都會(huì)隨之改變且更新。
在Eth 1.x研究語境中,我們不僅要知道狀態(tài)是什么,還要知道它在協(xié)議(據(jù)黃皮書中的定義)和大多數(shù)客戶端實(shí)現(xiàn)(如geth、parity、trinity、besu等)中是如何表現(xiàn)的。
什么是Trie?
以太坊所使用的數(shù)據(jù)結(jié)構(gòu)叫作Merkle Patricia Trie。有趣的是,‘Trie’最初截取自‘retrieval’一詞,但大多數(shù)人會(huì)將其發(fā)音為‘try’,以區(qū)別于‘tree’?;氐秸},關(guān)于MPT數(shù)據(jù)結(jié)構(gòu),我們需要了解:
在trie的一端,是描述狀態(tài)(值節(jié)點(diǎn))的所有特定數(shù)據(jù)片段。數(shù)據(jù)可以是特定帳戶的余額,也可以是存儲(chǔ)在智能合約中的變量(例如某種ERC-20通證的總供應(yīng)量)。Trie的中間則是分支節(jié)點(diǎn),通過哈希運(yùn)算將所有值串聯(lián)在一起。分支節(jié)點(diǎn)是包含其子節(jié)點(diǎn)哈希的數(shù)組(array),每個(gè)分支節(jié)點(diǎn)隨后再次經(jīng)過哈希并歸入其父節(jié)點(diǎn)的數(shù)組中。這一連串的哈希最終會(huì)到達(dá)trie另一端的一個(gè)狀態(tài)根節(jié)點(diǎn)。
在上面的簡(jiǎn)化圖示中,我們可以看到一些數(shù)值,以及得到這些值的路徑。例如,為了得到V-2,我們經(jīng)歷了1,3,3,4的路徑。同理,V-3可通過路徑3,2,3,3來獲取。需要注意的是,本例中的路徑長(zhǎng)度始終為4個(gè)字符,并且要獲取某個(gè)值只有一條可用路徑。
該結(jié)構(gòu)具有確定性和可加密驗(yàn)證的重要特性:生成狀態(tài)根的唯一方法就是通過計(jì)算狀態(tài)的每個(gè)單獨(dú)數(shù)據(jù)段,如此一來,通過比對(duì)根哈希和前序哈希(Merkle證明),就可以輕松證明兩個(gè)狀態(tài)是相同的。反之,我們也不能用相同的根哈希創(chuàng)建兩個(gè)不同的狀態(tài),任何使用不同值修改狀態(tài)的嘗試都將導(dǎo)致不同的狀態(tài)根哈希。
以太坊通過引入新節(jié)點(diǎn)類型,擴(kuò)展節(jié)點(diǎn)(extension nodes)和葉節(jié)點(diǎn)(leaf nodes)來提升效率,優(yōu)化trie結(jié)構(gòu)。通過將路徑的一些部分編碼為節(jié)點(diǎn),如此一來trie就會(huì)更加緊湊。
在這種優(yōu)化后的MPT結(jié)構(gòu)中,每個(gè)節(jié)點(diǎn)都需要在多個(gè)后續(xù)節(jié)點(diǎn)共享的路徑壓縮部分或值(若有必要,由路徑的其他部分前綴)之間進(jìn)行選擇。其實(shí)是相同的數(shù)據(jù)和組織,但是這個(gè)trie結(jié)構(gòu)只需要9個(gè)節(jié)點(diǎn)而非18個(gè)節(jié)點(diǎn)。看起來似乎更有效率,但事后看來,實(shí)際上這并不是最理想的。我們將在下一節(jié)討論原因。
要獲取狀態(tài)的特定部分(例如賬戶當(dāng)前的ETH余額),需要從狀態(tài)根開始,沿著trie的路徑從一個(gè)節(jié)點(diǎn)到另一個(gè)節(jié)點(diǎn),直到達(dá)到所需的值。在每個(gè)節(jié)點(diǎn)上,路徑中的字符用來決定下一個(gè)目的節(jié)點(diǎn),就像是一個(gè)用于導(dǎo)航哈希數(shù)據(jù)結(jié)構(gòu)的探測(cè)棒。
而在以太坊真正使用的版本中,路徑是長(zhǎng)度為64個(gè)字符(256位)的地址哈希,值是RLP編碼數(shù)據(jù)[1]。分支節(jié)點(diǎn)是包含17個(gè)元素的數(shù)組(其中有16個(gè)是每個(gè)可能的十六進(jìn)制字符,剩余一個(gè)則為值),而葉節(jié)點(diǎn)和擴(kuò)展節(jié)點(diǎn)包含2個(gè)元素(一個(gè)是部分路徑,另一個(gè)是下一個(gè)子節(jié)點(diǎn)的值或哈希)。要了解更多細(xì)節(jié),可以瀏覽以太坊的wiki頁面[2],或者,如果你喜歡親自鉆研,那么這篇文章提供了一個(gè)很棒的Python DIY trie練習(xí)[3](不幸的是這篇文章已經(jīng)過時(shí)了)。
在數(shù)據(jù)庫中使用Trie
讀到這里我們應(yīng)該提醒自己,trie結(jié)構(gòu)只是一個(gè)抽象的概念。這是一種將以太坊狀態(tài)的整體打包成統(tǒng)一結(jié)構(gòu)的方法。該結(jié)構(gòu)需要在客戶端的代碼中實(shí)現(xiàn),并存儲(chǔ)在磁盤上(或者分布在全球的數(shù)千個(gè)磁盤中)。這意味著要采用多維trie結(jié)構(gòu)并將其嵌入到一個(gè)普通的只理解[key,value]對(duì)的數(shù)據(jù)庫中。
在大多數(shù)以太坊客戶端(turbo-geth除外)中,MPT是通過為每個(gè)節(jié)點(diǎn)創(chuàng)建不同的[key, value]對(duì)來實(shí)現(xiàn)的,其中value是節(jié)點(diǎn)本身,key是該節(jié)點(diǎn)的哈希。
因此穿越trie的過程,或多或少與之前描述的理論上的過程相同。要查找?guī)粲囝~,我們將從根哈希開始,并在數(shù)據(jù)庫中查找其值以獲取第一個(gè)分支節(jié)點(diǎn);使用哈希地址的第一個(gè)字符,可以找到第一個(gè)節(jié)點(diǎn)的哈希;在數(shù)據(jù)庫中查找哈希,然后得到第二個(gè)節(jié)點(diǎn);使用哈希地址的下一個(gè)字符,我們可以找到第三個(gè)節(jié)點(diǎn)的哈希。如果運(yùn)氣好的話,我們沿途可能會(huì)碰到一個(gè)擴(kuò)展節(jié)點(diǎn)或葉節(jié)點(diǎn),也就不需要檢查全部的64個(gè)半字節(jié)。無論如何,我們最后會(huì)找到所需的帳戶,并從數(shù)據(jù)庫中檢索其余額。
這個(gè)過程與計(jì)算每個(gè)新區(qū)塊的哈希在很大程度上是相似的,但是是反過來的:從所有邊緣節(jié)點(diǎn)(賬戶)開始,通過連續(xù)的哈希來構(gòu)建trie,直到最后一個(gè)新的根哈希,再與鏈中最新確認(rèn)的區(qū)塊進(jìn)行比較。
這就是狀態(tài)trie明顯的效率優(yōu)勢(shì)所能夠施展的地方:在磁盤上重構(gòu)整個(gè)trie的強(qiáng)度是非常大的,以太坊使用的優(yōu)化版MPT結(jié)構(gòu)通過折衷實(shí)現(xiàn)效率來提高協(xié)議效率。
這些額外的節(jié)點(diǎn)類型(葉節(jié)點(diǎn)和擴(kuò)展節(jié)點(diǎn))理論上節(jié)省了存儲(chǔ)trie所需的內(nèi)存,但它們會(huì)使得用于修改常規(guī)數(shù)據(jù)庫中狀態(tài)的算法更加復(fù)雜。不過一臺(tái)功能強(qiáng)大的計(jì)算機(jī)設(shè)備能夠極速執(zhí)行該過程。然而,純粹的處理能力也只能起這么大的效用了。
節(jié)點(diǎn)同步
到目前為止,我們的討論還局限在運(yùn)行以太坊客戶端(如geth)的個(gè)體計(jì)算機(jī)范圍中。而以太坊是一個(gè)網(wǎng)絡(luò),所有這一切的關(guān)鍵是要在全球數(shù)千臺(tái)計(jì)算機(jī)以及不同客戶端之間達(dá)到統(tǒng)一的狀態(tài)共識(shí)。
不斷洗牌的#Defi、cryptokitty拍賣或cheeze巫師大戰(zhàn)的token,以及常規(guī)的ETH交易都會(huì)碰撞在一起,給以太坊客戶端創(chuàng)建一個(gè)極速變化的狀態(tài)。而以太坊客戶端需要保持狀態(tài)同步,隨著以太坊越來越廣泛的應(yīng)用,同步狀態(tài)就會(huì)愈難,狀態(tài)trie的結(jié)構(gòu)也會(huì)愈深。
“Turbo-geth是抽薪止沸的一種實(shí)現(xiàn):其將trie數(shù)據(jù)庫扁平化,并使用節(jié)點(diǎn)的路徑(而非其哈希)作為[key, value]對(duì)。這有效地使得檢索操作無關(guān)樹的深度,并帶來了各種酷炫的功能,使得運(yùn)行全節(jié)點(diǎn)時(shí)性能提升且減少磁盤負(fù)載?!?/p>
以太坊的狀態(tài)非常大,并且隨每一個(gè)區(qū)塊而變化。那么狀態(tài)和改變究竟有多大?我們可以將整個(gè)以太坊的當(dāng)前狀態(tài)大致定位在狀態(tài)trie的約4億個(gè)節(jié)點(diǎn)上。其中,大約每15秒需要添加或修改約3000個(gè)(甚至多達(dá)6000個(gè))節(jié)點(diǎn)。與以太坊區(qū)塊鏈保持同步,實(shí)質(zhì)上就是不斷有效地構(gòu)建新的狀態(tài)trie。
“這種狀態(tài)trie數(shù)據(jù)庫操作的多步驟過程,正是以太坊客戶端占用如此龐大的磁盤I/O和內(nèi)存的原因,即便是“快速同步”(fast sync)模式可能也需要長(zhǎng)達(dá)6個(gè)多小時(shí)才能完成。而要運(yùn)行一個(gè)以太坊全節(jié)點(diǎn),快速SSD(而非便宜但可靠的HDD)必不可少,因?yàn)樘幚頎顟B(tài)更改對(duì)磁盤讀/寫的要求非常高?!?/p>
其中需要注意的關(guān)鍵點(diǎn)在于,建立一個(gè)新節(jié)點(diǎn)進(jìn)行同步,與保持現(xiàn)有節(jié)點(diǎn)同步,這兩者之間相去甚遠(yuǎn)。而當(dāng)我們實(shí)現(xiàn)無狀態(tài)以太坊之后,它們之間的區(qū)別將會(huì)模糊化(希望如此)。
最直接的同步節(jié)點(diǎn)的方法是使用“full sync”(完全同步)方式:從創(chuàng)始區(qū)塊開始,將每個(gè)區(qū)塊中的每筆交易恢復(fù)成列表,并構(gòu)建狀態(tài)trie。后續(xù)區(qū)塊一旦產(chǎn)生,狀態(tài)trie就會(huì)被修改,隨著區(qū)塊鏈完整歷史的重現(xiàn)對(duì)節(jié)點(diǎn)進(jìn)行添加或修改。而從創(chuàng)世區(qū)塊開始下載并執(zhí)行每個(gè)區(qū)塊的狀態(tài)更改,需要花費(fèi)整整一個(gè)星期的時(shí)間。如果你同步時(shí)不亟待進(jìn)行新的交易,那就只是時(shí)間問題。
另一種同步方式“fast-sync”(快速同步)名副其實(shí),其同步速度更快,但也更復(fù)雜:新客戶端可以從最近受信任的“檢查點(diǎn)”(checkpoint)區(qū)塊請(qǐng)求狀態(tài)條目,而不再需要從創(chuàng)世區(qū)塊開始。該方式需要下載的信息量要少得多,但仍然有許多信息需要處理??焖偻侥壳安皇軒捪拗?,而是受磁盤性能的限制。
實(shí)質(zhì)上,進(jìn)行快速同步的節(jié)點(diǎn)是在與鏈的末端進(jìn)行賽跑。節(jié)點(diǎn)需要在狀態(tài)陳腐(stale)并且全節(jié)點(diǎn)不再提供該狀態(tài)之前從“檢查點(diǎn)”(checkpoint)中獲取所有狀態(tài)(如果發(fā)生這種情況,節(jié)點(diǎn)可以輾轉(zhuǎn)至新的檢查點(diǎn))。一旦快速同步節(jié)點(diǎn)克服了這種障礙,并使其狀態(tài)完全與檢查點(diǎn)(checkpoint)同步,就可以切換為完全同步,即從每個(gè)區(qū)塊中包含的交易生成并更新自己的狀態(tài)副本。
什么是區(qū)塊見證(witness)?
講到這里,我們可以開始探索無狀態(tài)以太坊的概念。無狀態(tài)以太坊的主要目標(biāo)之一就是減少新節(jié)點(diǎn)同步過程的痛苦??紤]到只有0.1%的狀態(tài)是隨區(qū)塊變化的,所以似乎應(yīng)該有一種方式可以減少切換為完全同步之前需要下載的所有額外信息。
但這也是以太坊采用加密安全數(shù)據(jù)結(jié)構(gòu)所帶來的挑戰(zhàn)之一:在trie結(jié)構(gòu)中,僅更改一個(gè)值就會(huì)導(dǎo)致全然不同的根哈希。這是一種特性,不是漏洞。這使得每個(gè)人都能確保自己和網(wǎng)絡(luò)中的其他節(jié)點(diǎn)處于同一狀態(tài)。
如果要走捷徑的話,我們需要一條關(guān)于狀態(tài)的新信息:即區(qū)塊見證(block witness)。假設(shè)此trie結(jié)構(gòu)中只有一個(gè)值最近產(chǎn)生了改變(下圖綠色部分):
同步狀態(tài)(包括該筆交易)的全節(jié)點(diǎn)將采用傳統(tǒng)的方式:獲取所有狀態(tài)片段,并對(duì)它們?nèi)窟M(jìn)行哈希運(yùn)算,以創(chuàng)建新的根哈希。如此就可以輕松驗(yàn)證自己的狀態(tài)是否與其他所有人的狀態(tài)相同(因?yàn)楣?jié)點(diǎn)掌握了相同的哈希以及相同的交易歷史)。
那對(duì)于新加入進(jìn)來的節(jié)點(diǎn)呢?新節(jié)點(diǎn)要進(jìn)行驗(yàn)證所需的最小信息量是多少,以保證至少在其觀察時(shí)段內(nèi)的觀察結(jié)果與其它節(jié)點(diǎn)是一致的?
一個(gè)新的節(jié)點(diǎn)需要更早的全節(jié)點(diǎn)提供證明,證明所觀測(cè)到的交易符合迄今為止的狀態(tài)。
用抽象的術(shù)語來說,一個(gè)區(qū)塊見證(witness)證明提供了狀態(tài)trie中所有丟失的哈希,并結(jié)合了一些位置“結(jié)構(gòu)”信息,表示這些哈希在狀態(tài)trie中位于何處。這使得新節(jié)點(diǎn)能夠?qū)⑿陆灰装谄錉顟B(tài)中,并在本地計(jì)算新的根哈希,而不需要下載狀態(tài)trie的完整副本。
簡(jiǎn)言之,這就是beam同步( beam sync)[4]蘊(yùn)含的原理。Beam同步方案不再收集檢查點(diǎn)trie中的每個(gè)節(jié)點(diǎn),而是開始監(jiān)測(cè)并嘗試在交易發(fā)生時(shí)就執(zhí)行交易,從全節(jié)點(diǎn)請(qǐng)求每個(gè)區(qū)塊的見證(witness)內(nèi)容,以獲取沒有掌握的信息。隨著越來越多的狀態(tài)被新交易影響,通過beam同步逐漸填充信息,直到最終切換到完全同步,客戶端可以更信任自己的狀態(tài)副本。
不同程度的“無狀態(tài)”
隨著區(qū)塊見證(witness)的引入,“完全無狀態(tài)”概念的定義逐漸清晰。與此同時(shí),這也是我們開始遇到瓶頸和問題的地方,并且沒有明顯可行的解決方案。
與beam同步方案不同,真正的無狀態(tài)客戶端自始至終不會(huì)保留狀態(tài)副本,而是只與區(qū)塊見證(witness)一同獲取最新的交易,只需要包含執(zhí)行下一個(gè)區(qū)塊所需的一切信息。
所以我們幾乎可以預(yù)見如果整個(gè)網(wǎng)絡(luò)都是無狀態(tài)的,那么實(shí)際上是可以達(dá)到永動(dòng)狀態(tài)的。新區(qū)塊的見證從上一個(gè)區(qū)塊產(chǎn)生,然后依次傳遞見證!至少可以持續(xù)到最后確認(rèn)的“事務(wù)狀態(tài)”,以及該狀態(tài)產(chǎn)生的第一個(gè)見證。而這對(duì)于以太坊來說將是一個(gè)富有戲劇性的巨變,所以不太可能會(huì)獲得廣泛的支持。
有一種不那么激進(jìn)的方法:適應(yīng)不同程度的“無狀態(tài)”。在這種網(wǎng)絡(luò)中,會(huì)有一些節(jié)點(diǎn)保存完整的狀態(tài)副本,也能為其它所有節(jié)點(diǎn)提供最新的見證(witness)。
· 完整狀態(tài)節(jié)點(diǎn)會(huì)像往常一樣工作,但需要額外計(jì)算一個(gè)見證(witness)并將其添加到新區(qū)塊,或通過輔助網(wǎng)絡(luò)子協(xié)議廣播;
· 部分狀態(tài)節(jié)點(diǎn)可以只在少量區(qū)塊生成期間保留完整的狀態(tài),或者只“監(jiān)測(cè)”其相關(guān)的狀態(tài)部分,然后從見證中獲取驗(yàn)證區(qū)塊所需的其余數(shù)據(jù)。這對(duì)需要運(yùn)行基礎(chǔ)設(shè)施的DApp開發(fā)者而言大有裨益;
· 根據(jù)定義,零狀態(tài)節(jié)點(diǎn)想要在運(yùn)行客戶端時(shí)盡可能輕巧,可以完全依賴區(qū)塊見證來驗(yàn)證新的區(qū)塊。
要啟用這個(gè)方案,可能需要類似于bittorrent采用的分塊(chunking)和群集(swarming)行為,其中見證(witness)片段根據(jù)其需要進(jìn)行廣播,并與具有(互補(bǔ))部分狀態(tài)的其他節(jié)點(diǎn)建立最佳連接?;蛘?,這可能需要制定一個(gè)狀態(tài)trie的替代實(shí)現(xiàn)方案,使得更適宜見證(witness)的生成。這都是我們需要研究和實(shí)驗(yàn)的內(nèi)容!
如若想要更深入地探析狀態(tài)節(jié)點(diǎn)與無狀態(tài)節(jié)點(diǎn)之間的權(quán)衡,可參見Alexey Akhunov的《The shades of statefulness》[5]。
這種半-無狀態(tài)方式的一個(gè)重要特點(diǎn)在于這些改變不一定要訴諸硬分叉形式。通過微小的并且可測(cè)試的漸進(jìn)方式,就可以將以太坊的無狀態(tài)組件構(gòu)建成一個(gè)輔助型子協(xié)議,或者劃分為一系列不具爭(zhēng)議的EIPs,而無需進(jìn)行大型“信念飛躍式”的升級(jí)。
無狀態(tài)客戶端路線圖
我們?cè)谘芯恐杏龅降囊粋€(gè)明顯問題就是區(qū)塊見證(witness)的大小。普通區(qū)塊包含一個(gè)區(qū)塊頭和交易列表,其大小約為100 kB。相對(duì)于網(wǎng)絡(luò)延遲及15秒的區(qū)塊時(shí)間,這種大小可以使得區(qū)塊廣播速度較快。
然而,見證(witness)需要包含狀態(tài)trie的邊緣和深層節(jié)點(diǎn)的哈希。這意味著其大小要大得多:早期數(shù)據(jù)顯示大約為1 MB。因此,與網(wǎng)絡(luò)延遲和區(qū)塊時(shí)間相比,同步見證要慢得多,這可能會(huì)是一個(gè)障礙。
這種兩難境地類似于電影下載和流媒體之間的區(qū)別:如果網(wǎng)絡(luò)過慢導(dǎo)致流媒體無法順暢加載,那么下載完整電影就是唯一可行的選擇。如果網(wǎng)絡(luò)速度快,那就可以流暢播放電影。如果實(shí)際情況是網(wǎng)絡(luò)速度不上不下,那么我們就需要更多的數(shù)據(jù)來作出決定。那些標(biāo)準(zhǔn)之下的互聯(lián)網(wǎng)服務(wù)提供商,會(huì)在需求過高時(shí)認(rèn)識(shí)到低速網(wǎng)絡(luò)的局限性。
很大程度上,這就是Eth 1.x工作組正在著手解決的為具體問題。目前,我們對(duì)假想見證網(wǎng)絡(luò)的了解,還不足以確定其是否能正?;蚶硐脒\(yùn)行,難點(diǎn)就在于細(xì)節(jié)(以及數(shù)據(jù))。
一種方法是通過改變trie本身的結(jié)構(gòu)(如二進(jìn)制trie),考慮如何壓縮和減少見證(witness)數(shù)量,以使其在實(shí)現(xiàn)時(shí)更高效。另一種方法則是原型化網(wǎng)絡(luò)原語(例如bittorrent的swarming),使得見證能夠有效地在網(wǎng)絡(luò)中不同節(jié)點(diǎn)之間傳遞。而這兩個(gè)方案,都能受益于形式化見證規(guī)范(目前該規(guī)范還不存在)。
責(zé)任編輯;zl
評(píng)論