1. 程式人生 > >關於以太坊智慧合約在專案實戰過程中的設計及經驗總結(1)

關於以太坊智慧合約在專案實戰過程中的設計及經驗總結(1)

此文已由作者蘇州授權網易雲社群釋出。

歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗


1.智慧合約的概述

近幾年,區塊鏈概念的大風吹遍了全球各地,有的人覺得這是一個大風口,有的人覺得他是個泡沫。眾所周知,比特幣是區塊鏈1.0,而以太坊被稱為了區塊鏈2.0,而區塊鏈1.0和2.0最主要的差別就在於以太坊擁有了智慧合約。其實,智慧合約在1994年就已出現,電腦科學家和密碼學家NickSzabo首次提出智慧合約概念。早於區塊鏈概念的誕生。Szabo描述了什麼是“以數字形式指定的一系列承諾,包括各方履行這些許諾”的協議。雖然有它好處,但智慧合約的想法一直未取得進展—主要是缺乏可以讓它發揮出作用的區塊鏈。

從技術角度理解,智慧合約其實是一個語法簡單、指令集精簡的圖靈完備的語言,就像簡化版的JavaScript。智慧合約和其他的語言的區別主要在於,一方面,智慧合約和代幣體系完美結合,能夠完成一系列價值轉移,另一方面,智慧合約會在所有節點統一執行,根據確定的輸入、確定的程式碼保證確定的輸出,也是所有節點狀態一致性的保證。最後是智慧合約都由有外部觸發呼叫,不存在什麼定時呼叫等。

廢話不多說,接下來,本人就從技術角度,來說說智慧合約方面的設計。


2.智慧合約的分層設計

2.1分層設計說明

智慧合約的分層設計模型主要是借鑑gitHub上的一篇名為《 淺談以太訪智慧合約的設計模式與升級方法》文章的中心思想,其作者也是基於其多年的Java實戰經驗提出的一些智慧合約設計思路。該文章有許多借鑑之處,但也存在許多坑點沒有仔細考慮。文章的分層設計思路主要如下:

“業務邏輯與外部解耦、業務邏輯與資料解耦”是Java設計模式的一種策略,也是其文章的主要思想。其實現方式主要將合約拆分為代理合約、業務控制合約、業務資料合約、命名控制器合約。其中代理合約是用於業務邏輯與外部Dapp的解耦,業務控制合約、業務資料合約和命名控制器合約是用於業務邏輯與資料的解耦。作者在設計時,也拆分了幾種不同的場景,詳見如下:

控制器合約與資料合約1—>1:

控制器合約與資料合約1—>N:

控制器合約與資料合約N—>1:

控制器合約與資料合約N—>N:

此類情況可以拆解為上面三種情況的組合。

2.2分層設計實現關鍵點

1)合約與合約之間的呼叫

合約呼叫合約的實現主要有兩種方式,第一種方式是可以通過 call、delegatecall、 callcode方法實現對其他合約的方法的呼叫,但是其弊端是使用存在安全性問題,而且不能獲知被呼叫合約的執行結果,不建議使用。第二種方式,是通過在合約中“外部引用”被呼叫的外部合約進行實現。

通過合約“外部引用”實現呼叫外部合約需要注意以下幾點:

  • 合約物件中需要定義被應用合約物件的方法,否則合約中無法識別被應用物件,編譯器會報錯;

  • 被引用物件需要通過合約物件的設定外部合約方法將合約物件進行引入,注意需要引入外部合約物件後。

2)合約與合約之間的轉賬

合約可以接收轉賬,需要顯示宣告回撥函式,並在回撥函式上加payable進行修飾。合約與合約之間進行轉賬時,需要在合約中顯示用send或者transfer進行合約之間的轉賬,合約與合約之間的轉賬將以內部交易的形式執行。另外,在顯示轉賬的方法中也需要加payable修飾。

pragma solidity ^0.4.2;

contract  Test{

function TTest(address contractAddress,uint amount)  payable {

   contractAddress.transfer(amount);

    }

    function()  payable {

    }

}

2.3分層設計的侷限與問題

1)被呼叫合約的方法的資料返回限制

被呼叫合約在返回string/bytes等不定長型別時會存在問題。這種限制需要在設計被呼叫合約時要注意,在實際專案中業務邏輯合約和資料合約都屬於被呼叫合約,故而其設計公共方法時需要規避string/bytes等不定長的限制問題。以下是一個呼叫失敗的反例:

pragma solidity ^0.4.2;

contract  Test{

function TTest(address contractAddress,uint amount) {

   A a=A(contractAddress);

  //編譯會報錯

   string temp=a.getString();

    }

}

2)被呼叫合約的方法的返回引數長度限制

被呼叫合約在返回定長的資料時,不能返回超過32位長度的資料,例如bytes33/uint33編譯器將會提示錯誤。

3)被呼叫合約結構體資料返回限制

Solidity語言中,在編譯器0.4.17版本之後,可以支援struct結構體的資料返回。在返回結構體的情況下,編碼需要注意新增“pragma experimental ABIEncoderV2;”,需要注意的是結構體中也不能包含string/bytes等不定長資料型別,但是返回struct這種形式還處於試驗階段,穩定性安全性有待論證。(在0.4.17版本之前不能使用因為以前編譯器沒有把struct作為一個真正的類,只是形式上的組合在一起)

pragma solidity ^0.4.17;

pragma experimental ABIEncoderV2;

contract  Test{

    struct MyStruct { int key; uint deleted; }

function TTest() returns() {

   return MyStruct({key:int(1),deleted:uint(1)});

    }

}

4)被呼叫合約返回合約型別的限制

被呼叫合約能夠返回合約型別的資料,編譯器將合約看成地址返回,而地址是定長的。

5)被呼叫合約事件監聽的問題

如果被呼叫合約需要觸發事件,可能會存在事件監聽的問題。如果通過web3j監聽區塊鏈的事件,被呼叫的合約事件資訊可能會被編碼,故而可能導致web3j無法監聽到被呼叫合約內部觸發的事件。問題原因為在定義的介面合約中沒有相關的事件宣告。(詳見附錄例項程式碼)例如以下是本人測試返回的事件資訊:

//被呼叫合約的事件監聽返回資料

{

"data": "0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000bbf289d846208c16edc8474705c748aff07732db000000000000000000000000000000000000000000000000000000000000000e5365727669636520446f2e2e2e2e000000000000000000000000000000000000",

"topics": [

"4d3a2e6362f7a2697702c4af6f5a55dbb398da05784a12752d3cb5e12dcbf965"

]

}

6)智慧合約的傳入引數大小限制

智慧合約在傳入引數方面存在著EVM虛擬機器棧的限制,預設情況下EVM虛擬機器棧的大小為1024*512bit,故而引數不能超過這個大小,否則會出現虛擬機器棧。如果涉及的業務資料不大的情況下,可以在鏈上儲存,若涉及的業務資料比較大,建議通過鏈外進行業務資料的互動。當下針對大業務資料比較好的一種解決方案是,通過IPFS檔案系統儲存線外資料。

(備註:IPFS(InterPlanetary File System,星際檔案系統)是一個旨在建立持久且分散式儲存和共享檔案的網路傳輸協議。它是一種內容可定址的對等超媒體分發協議。)

7)智慧合約方法中區域性變數的數量限制

在智慧合約程式設計中,Solidity編譯器不允許方法的超過16個“區域性變數”,否則編譯器將會報錯。其中方法“區域性變數”的計算規則自定義區域性變數算1個,每個傳入傳出引數算1個,外部合約呼叫算2個,總計不能超過16個,否則會出現“Stack Too Deep”的編譯錯誤。

8)合約依賴外部庫或者外部合約時部署限制

當某合約依賴外部庫函式或者外部合約時,其部署合約時需要先部署外部庫函式或者外部合約,將部署後得到的外部庫或者外部合約地址設定到該合約的abi檔案中。解決方式有兩種,一種通過在該函式中定義設定外部函式或者外部合約的地址的方法,手動設定;另一種是通過Truffle框架,其提供了依賴部署的方式,詳見《Truffle使用手冊》。

9)合約執行的出現Invalid Code的問題

針對使用assert斷言或者require的函式修飾器(例如許可權控制、啟停控制等)程式判斷不通過,將會執行revert語句,而revert語句在當前版本被認定為Invalid Code。

另外,說明下assert語句和require的區別:用了assert的話,則程式的gas limit會消耗完畢;而require的話,則只是消耗掉當前執行的gas。


3.智慧合約的資料遷移

資料遷移問題也是設計系統必須要考慮的問題,而區塊鏈的特點就是資料擁有不可竄改的特點,也就造就了智慧合約資料遷移的困難。

1)繼承式資料遷移法

新版本的資料合約中儲存一個指向舊版本資料合約的合約地址,新版本資料合約儲存的是增量的資料內容。該方法要求合約能夠分層設計,將資料部分的合約獨立出來。

2)日誌式資料回放法

注意我們不能新建一條鏈,併發所有的交易進行重放。此處指的是,在合約中通過event事件、結構體記錄資料的狀態及變化,必要時,能夠新建一個合約並重新初始化同樣的資料。


4.智慧合約補救策略設計

編寫以太訪智慧合約難免可能存在一些漏洞,假如系統遭受攻擊形成資金損失,可以通過如下處理方式:

1)合約暫停或者銷燬

在合約編碼的時候,一定要給每一個合約加上停止或者銷燬的方法,便於在第一時間發現合約出現錯誤或者漏洞時損失的“停滯”。在暫停和銷燬方法選擇方面,本人建議都使用暫停方式,因為方法被暫停後呼叫方能夠得到明確的異常通知,但是合約被銷燬後,合約就不存在了,這時呼叫方繼續呼叫會得不到異常反饋,且傳送的資金也將永遠不能被追回。

2)通過硬分叉

強制硬分叉,或者強制進行塊資料回滾適用於聯盟鏈的角度;並重新發布新合約;

3)合約資料遷移,重新發布新合約

在實踐專案中,建議設定方法的啟停開關,在出現異常情況下,可以及時停止合約方法,避免合約問題擴散。通過合約資料的遷移方式,重新發布問題合約。如果是代幣,就重新發行新的代幣,適用於公鏈或者聯盟鏈。


5.智慧合約安全性問題規避

1)合約之間的轉賬send方法使用問題

在合約之間進行轉賬操作時,如果使用<address>.send(value)方法時,該方法需要進行返回結果的判斷,如果返回結果為false需要人工丟擲異常,然後阻止後續流程,否則轉賬異常後返回false還是會繼續執行後續流程,這種方式也能避免call deep合約攻擊。

pragma solidity ^0.4.2;

contract  Test{

function TTest(address contractAddress,uint amount)  payable {

   if(!contractAddress.send(amount)){throw;}

    }

}

2)合約的許可權控制問題

合約的分層設計中,需要對依賴的外部合約進行手動注入,故而需要注意在合約的關鍵方法上進行許可權控制,規避其他人能改變合約的呼叫關係,從而系統被攻擊。

3)call、delegatecall、callcode方法使用問題

不建議在合約中使用call、delegatecall、callcode方法,因為這些方法能夠呼叫程式碼未知,從而導致風險未知。

4)呼叫外部合約的順序問題

在實現合約呼叫合約的模式中,需要注意的是,優先完成內部交易邏輯,將外部呼叫放在後面進行操作,這樣可以避免call deep攻擊。例如:Solidity官網文件中提到的Withdrawal模式。

5)交易執行順序問題

交易順序依賴就是智慧合約的執行隨著當前交易處理的順序不同而產生差異。在智慧合約設計時需要考慮,交易的順序性以及如何串聯交易流程,例如通過設定全域性業務的唯一標識。

6)問題合約的防範策略

每個智慧合約都不是百分百的完美,可能會存在一些漏洞或者Bug,針對有問題的合約,我們需要第一時間能進行對合約的控制。比如在合約中增加“銷燬函式”,第一時間銷燬有問題合約,不過這種方式比較粗暴。另一種方式,在合約方法中加入“啟停”控制,當發現問題時,第一時間將合約的方法停止,然後儘快升級新合約,避免問題的蔓延。

7)被呼叫合約方法訪問的約束策略

因為每個呼叫的合約一般是有明確的呼叫的物件的,比如代理合約呼叫業務合約,那麼就應該業務合約智慧被代理合約呼叫,否則其他人只要知道了業務合約的地址,其也可直接發起呼叫,對合約的安全性存在影響。


6.智慧合約實戰問題記錄

除了以上關於一些限制性的問題和安全漏洞方面的問題,在專案實戰過程中還遇到了一些其他問題,此處不再分類,一併記錄:

1)在用web3j呼叫的合約中含有自定義外部library的函式應用時,函式的監聽無效,或者函式呼叫失敗?

問題原因:是因為當前合約在部署時需要依賴library部署後的地址,而用web3j部署合約時並未依賴library的地址,從而導致當前合約中的library無法呼叫,從而引發在引入library的函式中事件及方法都呼叫失敗。

解決方式:1.通過增加設定library地址的函式,手動設定;2.通過Truffle等框架的依賴部署功能部署函式。

2)根據Solidity編譯後的abi檔案能夠反編譯為Solidity原始碼?

關於反編譯Solidity程式碼的問題,現在是沒有Solidity反編譯器的,需要付出極大的努力才能使其看起來與原始原始碼相似,只能通過看位元組碼反編譯操作碼,看程式的執行邏輯。

3)EVM在執行智慧合約時,事物的回滾和提交的觸發條件?

EVM在執行是能合約時在以下情況會進行丟擲異常,進行回滾;因為EVM首先在快照(默克爾樹)中執行程式碼,如果出現異常回滾將當前的快照回滾至原先的狀態,回滾也會包括已經執行的金額退回給原賬戶,但是需要注意的是事物回滾還是會扣取執行交易消耗的gas費用,事物回滾異常如下:

  • Gas不夠,丟擲OutOfGasException,細分為以下三種;

                 -notEnoughOpGas
                 -notEnoughSpendingGas
                 -gasOverflow

  • 指令非法,丟擲IllegalOperationException;

  • 定址錯誤,丟擲BadJumpDestinationException;

  • 棧太小,丟擲StackTooSmallException;

  • 棧太大,丟擲StackTooLargeException。

EVM在正確執行完以下指令,才能進行事物提交:

  •  執行完STOP執令;

  •  執行完RETURN執令;

  •  執行完SUICIDE指令。

4)關於合約自毀後合約地址上的資金問題?

合約在進行自毀操作後,需要提供一個資金轉向的地址,合約上的資金會轉入該地址當中。另外,如果有賬戶向銷燬後的合約地址傳送資金,將導致該筆資金被“凍結”且無法被追回的情況。

5)呼叫智慧合約一個不存在的方法的不報錯?

當呼叫一個外部合約時,且呼叫的方法不存在,包括方法名和方法引數沒有匹配上時,Solidity會預設執行回撥函式,回撥函式如果不顯示宣告的情況下為一個沒有方法名和返回引數的函式。

6)Remix無法連線EthereumJ測試鏈的問題?

首先在EthereumJ實現RPC的前提下(預設github原始碼是沒有實現的),如果發現EthereumJ不能連線Remix是因為Remix先會發OPTIONS的請求“探測”下測試鏈,“探測”通過後在發net_listening的Post請求,所以在實現RPC請求時需要也實現OPTIONS請求方法,另外需要同時在Remix介面中開啟listen on network。

如果Remix無法建立賬戶,請在Remix的Setting中勾選“Always use Ethereum VM at load”和“Enable Personal Mode”。

7)智慧合約中是否存在隨機函式,或者不同的機器獲取的now時間不一致導致程式結果不一致?

在Solidity語言中規避了隨機函式的存在,其設計的思想也是通過保障在同樣的輸入條件、程式程式碼的情況下能得到一樣的結果,這也是每個以太訪節點的資料一致性的保證。在獲取時間這個點上,now函式不是獲取的系統的預設時間,而是取至block塊的時間戳,從而每個節點在收到網路中傳播的塊時,其獲取到的now時間都是一樣的。

8)EthereumJ中指定監聽合約地址無效,還是能監聽到其他合約地址觸發的事件?

因為在建立事件監聽的時候,("address":["0x41bd05db83ed0645fac0995b11e8b734d7711b5c"]),地址被封裝為List物件,EthereumJ會匹配address引數物件型別,List因為不能被匹配所以address引數無法被設定,導致建立的監聽能監聽其他地址的事件。

9)關於取消nonce導致釋出的合約地址不變的問題?

因為在實際專案中對Ethereumj的版本進行調整,取消了nonce的限制,然而該智慧合約在釋出的過程中會根據傳送者的地址和傳送者擁有的nonce生成合約地址,所以在傳送者地址一致的和nonce一致的情況下,釋出的合約的地址都是同一個,新發布的合約會覆蓋久的合約,導致程式釋出錯誤。


免費領取驗證碼、內容安全、簡訊傳送、直播點播體驗包及雲伺服器等套餐

更多網易技術、產品、運營經驗分享請點選


相關文章:
【推薦】 SQL On Streaming
【推薦】 JavaScript 如何工作:渲染引擎和效能優化技巧