關於以太坊智能合約在項目實戰過程當中的設計及經驗總結(1)

此文已由做者蘇州受權網易雲社區發佈。html

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


1.智能合約的概述github

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

從技術角度理解,智能合約實際上是一個語法簡單、指令集精簡的圖靈完備的語言,就像簡化版的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 如何工做:渲染引擎和性能優化技巧

相關文章
相關標籤/搜索