如何用Solidity寫一個能夠升級(改版)的智慧合約

買賣虛擬貨幣
概要Solidity(或者更概略的說,EVM),在軟體工程師的生產效率和語言表達能力都還有好長一段路需要走。如果你曾經在以太坊上開發智慧合約,到現在你應該已經察覺,Solidity真是個綁手綁腳的語言。特別是在你是從Swift或Javascript轉行過來開發Solidity。用Solidity開發程式,這個語言允許軟體工程師能做的事情,還有整個語言的表達能力,都讓人有種倒退走的感覺。這種感覺有時候真的會讓人發飆。但為什麼它是受限的語言Solidity和其他能夠被編譯成能夠在EVM(以太坊虛擬機器)上運作的bytecode(位組碼),都會受到某些限制,因為:
· 當你要執行你的程式的時候,你的程式會在整個系統上的每一臺節點上運作。當一個節點收到新的區塊的時候,這個節點就會驗證這個區塊的完整性。在以太坊上這些驗證包含了,這個區塊所包含的所有計算是正確的,而且合約的每個狀態都是計算正確的。· 這造成了,即便EVM是圖靈完備,大量的計算會十分昂貴(甚至會因為超過燃料(gas)上限而直接不被允許),因為每個節點都需要把這些計算計算一遍。也就因此把整個乙太網絡拖慢。· 標準函式庫(standard library)還沒被開發完成。尤其是陣列和字串都很難用。我本人都還曾經自己實作過操作字串的函式庫,就為了一些很基本,我們之前都習以為常的功能。· 你所寫的合約沒辦法從外界(EVM之外)獲得任何資料,除非透過交易(Oracle)。而且當一筆交易被佈署到網路上,他就沒把辦法再升級改版(除非是透過migration或是純儲存合約(pure storage contract))。這些限制有些是以太坊必須一定存在的限制(就像是你永遠不可能會有辦法儲存你Google Photos上照片的備份在鏈上,或者單純透過鏈上的計算資源去做影象辨識,但這也沒什麼關係啦)。但其他限制會存在,只是因為這是一個十分新的科技(雖然真的進步的超級快),但這個技術也將會一直改善。好的所以我到底要怎麼解決這個問題?
當我們在開發專案的時候,我們可能在未來會對合約做更改。我們可以透過橫跨不同合約間資訊傳遞的方式去間接解決這個問題。在進入可升級智慧合約(Upgradable Smart Contracts)的實作之前,讓我們先來了解它的限制。什麼是函式庫,為什麼我們需要函式庫?在Solidity,函式庫(library)是一種特殊的合約,這種合約不會儲存任何資料(storage),並且也不能持有任何以太幣。有時候我們可以試著把函式庫當成一種以太坊虛擬機器(EVM)的單例(Singleton)就好。這個單例是一段可以被其他合約呼叫的程式碼,而且這段程式碼在被呼叫的時候不需要重新佈署。這個特性解決了一些我們所面對的大問題,比如說:· 佈署所需的燃料(gas)花費:因為同樣的程式碼不需要一而再再而三地被佈署,所以一個很明顯的優勢就是能夠節省大量的燃料。並且不同的合約可以都倚賴同一個已經被佈署出去的函式庫。· 繁冗的程式碼在區塊鏈重複出現:這明顯的是上面那點所伴隨來的好處,佈署的次數比較少,區塊鏈上的紀錄也就比較少。· 程式碼升級:在之前的狀況是,如果需要修改程式錯誤或者幫合約改版,就需要重新佈署一個新的,因而與之前合約獨立的合約(甚至更慘的狀況是像之前一樣要對以太方進行硬分岔)。這個問題因此被解決了
有沒有開始覺得函式庫是一個很厲害的東西了阿?不幸的是,函式庫也有一些限制,下面是幾點是關於函式庫我們必須知道的重要資訊:· 沒有儲存(storage)的能力· 函式庫能夠操縱其他合約的儲存(storage)· 函式庫不能有任何payable的函式(function)。· 函式庫不能有任何fallback的函式(function)。· 函式庫不能有事件日誌(event log)。
· Libraries can be used to fire event logs for the contract which uses it.· 函式庫是不能被繼承的· 雖然函式庫不能直接被繼承,但是函式庫可以跟其他函式庫接在一起,就能像一個一般合約一樣的使用被接上的函式庫,單這樣使用,函式庫本身的限制依然還會在。這幾點可能在一開始會讓人聽起來很混亂,但是別灰心,這邊有一個非常棒的資源可以協助你瞭解函式庫。但至少接下來,我們只會用上一些,因為了解、實作可升級智慧合約所必須瞭解的部分就好了。函式庫是如何運作的?

函式庫是一種特殊的合約,這種合約不能有任何payable函式,而且也不能有任何fallback函式(這些限制在編譯期間就被強行限制了,因此可以讓函式庫這種合約絕無可能持有任何資金)。函式庫是透過函式庫關鍵字(library L{})去定義的,就像一個合約會透過(contract C{})去定義一樣。

library L{
    function a() returns (address) {
        return address(this);
    }
}

contract C{
    function a() constant returns (address) {
    //This will behave as if the library code was written within this contract
        return L.a();
    }
}
要呼叫一個函式庫裡面的函式必須用特別的指令(DELEGATECALL),這個指令會傳遞呼叫函式的資訊(calling context)到函式庫,因此就幾乎像是一個都在同個合約裡面,合約自己處理自己的指令。我真的蠻喜歡在Solidity檔案裡面闡述這件事情的角度。函式庫可以被視為給其他智慧合約使用的固有基礎合約從上面的的程式碼可以知道,如果合約C的函式a()被呼叫,a()會回傳合約C的地址而非函式庫L的地址。同樣這點也適用所有msg的性質:msg.sender,msg.value,msg.sig,msg.data以及msg.gas(Solidity檔案裡面所寫的與這相反,但是在做了一些實驗之後,似乎我這樣解釋才是正確的)。一件我們在這邊會注意到的事情是,我們還是不太清楚型別C(class C)和函式庫L(library L)是怎麼被連在一起的,因此接下來我們就會試著瞭解這件事情。
函式庫們是如何被連線起來的?與顯式繼承不同的是,一個倚賴函式庫合約(contract C is B {}),他與函式庫之間的連線(link)方式是沒有那麼清楚的。在上述例子,合約C在他自己的函式a()中用到函式庫L。但是在上面這短短的程式碼中,我們都沒有提到函式庫要使用什麼地址,而且函式庫L不會出現在合約C編譯完的位組碼(byecode)中。函式庫的連線是發生在位組碼等級。當合約C被編譯完成後,合約C會替函式庫的地址給一個像是下面這種形式的預留位置0073__L_____________________________________630dbe671f(0dbe671f是a()的function signature),如果我們完全不更動合約C就佈署合約C,這樣就會因為位組碼是不合法的導致佈署失敗。簡單解釋就是,函式庫連線就是簡單的把所有在合約位組碼裡面預留給函式庫的位置,全部都用函式庫的地址填上就完成了。這樣填完的位組碼就可以順利的佈署到區塊鏈上。好的我們現在介紹了函式庫的基本概念後,我們就可以瞭解怎麼用函式庫去開發可升級智慧合約了。函式庫本身是沒辦法升級的
對的函式庫沒辦法升級,而且合約也沒辦法升級。就像我們本篇文章前面所提到的,對函式庫的參考(reference)是位組碼(bytecode)等級的事情,而不是儲存(storage)等級的事情。在合約一佈署後,就無法修改合約的位組碼。因此對於函式庫的參考就會被隨著合約永存。所以我們必須問這個問題「為什麼還是有人提出可升級這個特點呢?」最後,他到底怎麼運作的?這邊我們會用到一些小技巧,讓我們一起仔細瞭解。

我們沒有直接將給使用者使用的主合約C和佈署所需的函式庫連線,而是將給使用者使用的主合約C跟一個排程員(Dispatcher)合約連線起來。在編譯時期和佈署時期,因為排程員合約沒有實作函式庫內任何函式,所以一切都不會出現問題。這也就意味著,排程員合約並沒有用到任何函式庫內的程式碼,排程員合約僅僅是位組碼(就像是我們在上面看到合約C的位組碼一樣),在排程員的位組碼中,並不需要引用(include)函式庫的地址。我們就沒有將任何地址寫死(hardcode)在位元組碼等級,也因此我們就可以隨時把函式庫替換成任何我們所需的函式庫。

然而,如果我們沒有在排程員合約中用任何函式庫的程式碼,這樣我們要如何執行函式庫中的函式?

當一筆交易進來的時候,主合約(Token contract,代幣合約)發現這筆交易需要從主合約所連線的函式庫(TokenLib1)呼叫delegatecall,然而呼叫delegatecall這件事情不會直接請函式庫直接回傳,而是會先透過排程員合約呼叫delegatecall。

接下來的事情就會變得很有趣了。一當排程員合約在他自己的fallback function中接收到delegatecall,排程員合約會在fallback function中判斷所需的正確版本函式庫是哪個,然後將呼叫delegatecall的要求導向正確的函式庫。當函式庫回傳資料後,資料就會一路回到主合約。

雖然這個解法運作起來還不錯,但是他還是有些限制。

限制

排程員合約必須知道呼叫函式庫,函式庫回傳值的記憶體大小。現在這個問題可以透過mapping解決,會以function signature去取得回傳值大小。為了簡化說明,所以我們故意避免對這方面多做解釋。

上述方法在以太坊虛擬機器中可以成功執行,但是我們只能在不同合約間儲存足跡(storage footprint)都相同的狀況下才能使用。因為函式庫沒有任何儲存,所以我們必須讓排程員合約裡面也沒有任何儲存。這也就是為什麼需要有另外一個分離的Dispatcher Storage(見上圖下面方塊)去儲存排程員合約所需的資料。還有,Dispatcher Storage的地址必須寫死在排程員合約的位組碼裡面。

可以發現,使用者所會面使用到的合約(Token contract)其實沒有使用到任何特別的技術,唯一不一樣的地方就是,不能像之前一樣連線到某特定版本的函式庫,而是連線到排程員合約。

更多區塊鏈資訊:www.qukuaiwang.com.cn/news

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;