以太坊智能合約的原理和使用方法

一、智能合約概述

1.1 智能合約是什麼

智能合約本質上是運行在區塊鏈上的一段代碼,代碼的邏輯定義了合約的內容。
智能合約賬戶保存了合約當前的運行狀態,包括當前餘額(balance)、交易次數(nonce)、合約代碼(code)、存儲(storage 數據結構是MPT,合約的執行數據保存在這裏)。

1.2 solidity語言

智能合約的最常用的語言是Solidity,語法上與JavaScript很接近。如下圖所示:
在這裏插入圖片描述
“pragma solidity ^0.4.21”聲明solidity版本號,不同版本在語法上有一些差別;
contract相當於“class(類)”,裏面定義了一些狀態變量。solidity是強類型編程語言,大部分類型與常用編程語言類似,address類型是solidity特有的類型;
mapping哈希表不支持遍歷,所以需要單獨創建一個數組,用於遍歷key值,例如上圖的哈希表bids和數組bidders;
event(事件)用於記錄日誌,使用emit調用該日誌函數;
constructor是構造函數,僅在合約創建時調用一次;
接下來3個成員函數都是public,可以被外部賬戶或合約賬戶調用。

二、外部賬戶調用智能合約

如果外部賬戶是轉賬給另一個外部賬戶,那麼與比特幣的轉賬幾乎相同;如果轉賬給合約賬戶,那麼就是發起這個賬戶的合約調用,調用的函數及參數在“TX DATA”域中說明,如下圖所示:
在這裏插入圖片描述
其它域的說明如下:
“SENDER ADDRESS”爲發起轉賬的地址;
“TO CONTRACT ADDRESS”爲接收的合約賬戶;
“VALUE”爲轉賬金額(金額爲0說明僅調用函數,沒有轉賬);
“GAS USED”是該交易花費的汽油量;
“GAS PRICE”是單位汽油的價格;
“GAS LIMIT”爲發起人最多願意花費的汽油量。

三、一個合約調用另一個合約

一個合約可以調用另一個合約,但是合約賬戶不能主動調用另一個合約,必須由外部賬戶發起。

3.1 直接調用

給出另一個合約的地址,直接調用。如下圖所示,callAFooDirectly的參數是一個合約地址,將地址轉換爲合約實例,然後就可以調用該合約的foo函數:
在這裏插入圖片描述
如果在執行a.foo()過程中拋出錯誤,則callAFooDirectly也拋出錯誤,本次調用全部回滾。ua爲執行a.foo(“call foo directly”)的返回值。
另外可以通過.gas() 和 .value() 調整提供的gas數量或提供一些ETH。

3.2 使用address類型的call()函數

通過address類型call函數調用智能合約。下面這個例子相當於A(addr).foo(“call foo by func call”),如下圖所示:
在這裏插入圖片描述
call函數的第一個參數被編碼成4個字節,表示要調用的函數的簽名。其它參數會被擴展到32字節,表示要調用函數的參數。
返回一個布爾值表明了被調用的函數已經執行完畢(true)或者引發 了一個EVM異常(false),無法獲取函數返回值。而發起調用的函數並不會產生異常,繼續執行。
與直接調用類似,也可以通過.gas() 和 .value() 調整提供的gas數量或提供一些ETH。

3.3 代理調用delegatecall()函數

代理調用delegatecall()函數使用方法與call()相同,只是不能使用.value()。
另外call()會切換到被調用的智能合約上下文中,delegatecall()只使用給定地址的代碼,其它屬性(存儲,餘額等)都取自當前合約。delegatecall 的目的是使用存儲在另外一個合約中的庫代碼。

3.4 payable

如果合約賬戶接收外部轉賬(“VALUE”域不爲0),被調用的函數必須標註payable。例如下圖所示,bid函數接收拍賣出價,以太幣存儲在合約內,可以防止惡意出價:
在這裏插入圖片描述

3.5 fallback()函數

如果轉賬交易的“TX DATA”域中沒有說明函數名,或域裏的函數名不存在,則默認調用fallback函數,如果合約中沒有定義fallback函數(合約中不一定存在fallback函數),那麼將調用失敗。函數定義如下所示:
function() public [payable]{
……
}

  • 匿名函數,沒有參數也沒有返回值。
  • 如果轉賬金額(“VALUE”域)不是0,同樣需要聲明payable,否則會拋出異常。

四、智能合約的創建和運行

4.1 創建與運行

智能合約的代碼寫完後,要編譯成bytecode。創建合約時,外部帳戶發起一個轉賬交易到0x0的地址,轉賬金額(“VALUE”域)是0,但是要支付汽油費,合約的代碼放在data域裏。

智能合約運行在EVM(Ethereum Virtual Machine)上,EVM的尋找空間爲256位。以太坊是一個交易驅動的狀態機,調用智能合約的交易發佈到區塊鏈上後,每個礦工都會執行這個交易,從當前狀態確定性地轉移到下一個狀態。

4.2 汽油費(gas fee)

智能合約是個Turing-complete Programming Model。理論上可證明,不存在任何算法可判斷出任一程序是否會停機。
以太坊中使用汽油費機制,防止出現死循環。合約中的指令在執行時要收取汽油費,由發起交易的人來支付。EVM中不同指令消耗的汽油費是不一樣的,簡單的指令很便宜,複雜的或者需要存儲狀態的指令就很貴讀取。讀取公共數據時,則不需要汽油費。汽油費在txdata結構體中,代碼如下所示:
在這裏插入圖片描述
AccountNonce是交易序號,用於防止重放攻擊,Price是單位汽油價格,GasLimit是願意支付的最大汽油量,Recipient是收款人地址,Amount是轉賬金額,Payload是合約函數和參數,即前面章節所述的txDATA域。
實際系統中,節點收到一個智能合約調用時,先按照GasLimit從發起的賬戶里扣掉Gas fee,增加自己賬戶餘額,然後根據實際執行結果算出花費的汽油費,多出的汽油費會退還。如果執行中發現汽油費不足,會引起回滾,但是執行中扣掉的汽油費不會退還;

GasLimit:
區塊頭結構體中的GasUsed指的所有交易用的汽油總和;GasLimit指的是區塊內所有交易能夠消耗的汽油上限,與txData結構體中的GasLimit沒有關係,用於限制區塊內消耗的資源(比特幣中使用區塊小於1M來限制)。每個礦工在打包區塊時,都可以在父區塊的GasLimit基礎上調整±1/1024。

合約執行異常的汽油費
合約執行過程中出現任何異常,都會回滾,同時扣掉對應汽油費,所以出錯的交易也會包含在區塊中。合約執行結果體現在交易收據中,即Receipt結構體中的Status域。
其他節點收到區塊後會扔掉自己的執行結果,同時驗證區塊內的交易,包括驗證汽油費的扣除是否合法。然後更新本地數據結構,繼續挖礦。

4.3 錯誤處理

智能合約的執行具有原子性,一旦遇到異常(比如汽油費不夠),除特殊情況外,執行操作全部回滾,不會只執行一部分。智能合約中不存在自定義的try-catch結構,不可以捕獲異常。
可以拋出錯誤的語句如下:

  • assert(bool condition):如果條件不滿足就拋出—用於內部錯誤。
  • require(bool condition):如果條件不滿足就拋出—用於函數輸入或者外部組件引起的錯誤。
  • revert():無條件地拋出異常,終止運行並回滾狀態變動。

4.4 嵌套調用

嵌套調用是指一個合約調用另一個合約中的函數。如果被調用的合約執行過程中發生異常,有些調用方法會導致發起調用的這個合約也跟着一起回滾,而call()函數調用則不會連環回滾,只會使得當前調用失敗,得到false返回值。

一個合約直接向一個合約帳戶裏轉賬,沒有指明調用哪個函數,仍然會引起嵌套調用,這時調用的是fallback函數。

五、智能合約可以獲取的信息

智能合約不支持任何造成執行結果不確定的操作,比如多線程下對內存的訪問、真隨機數產生。
另外智能合約無法獲取與系統環境相關的信息,因爲每個系統的環境都是不一樣的,所以只能獲得一些固定信息。

5.1 區塊信息

智能合約可以獲得的區塊信息如下:

  • block.blockhash(uint blockNumber) returns (bytes32):獲得指定區塊的哈希,僅對最近256個區塊有效而不包括當前區塊
  • block.coinbase (address):挖出當前區塊的礦工地址
  • block.difficulty (uint):當前區塊難度
  • block.gaslimit (uint):當前區塊gas限額
  • block.number (uint):當前區塊號
  • block.timestamp (uint):自unix epoch起始當前區塊以秒計的時間戳

5.2 合約調用信息

智能合約可以獲得的合約調用信息如下:

  • msg.data (bytes):完整的calldata
  • msg.gas (uint):剩餘gas
  • msg.sender (address):消息發送者(當前調用)
  • msg.sig (bytes4):calldata的前4字節(也就是函數標識符)
  • msg.value (uint):隨消息發送的wei的數量
  • now (uint):目前區塊時間戳(block.timestamp)
  • tx.gasprice (uint):交易的gas價格
  • tx.origin (address):交易發起者(完全的調用鏈)

六、合約地址類型

6.1 合約地址調用

所有智能合約均可顯式地轉換成地址類型,當前智能合約得到另一個合約的address,便可通過address調用對應合約的成員變量和函數,如下所示:

  • <address>.balance (uint256):
    balance爲成員變量,類型是uint256,以Wei爲單位的地址類型的餘額;
  • <address>.call(…) returns (bool):
    發出底層call,失敗時返回false,發送所有可用gas,不可調節;
  • <address>.callcode(…) returns (bool):
    發出底層callcode,失敗時返回false,發送所有可用gas,不可調節;
  • <address>.delegatecall(…) returns (bool):
    發出底層delegatecall,失敗時返回false,發送所有可用gas,不可調節。

6.2 轉賬給合約

轉賬給合約地址方式如下所示:

  • <address>.transfer(uint256 amount):
    會觸發調用fallback函數,向地址類型發送數量爲amount的Wei,失敗時拋出異常,同時發送2300gas的礦工費,不可調節;
  • <address>.send(uint256 amount) returns (bool):
    會觸發調用fallback函數,向地址類型發送數量爲amount的Wei,失敗時返回fasle,同時發送2300gas的礦工費用,不可調節;
  • <address>.call.value(uint256 amount)():
    會觸發調用fallback函數,向地址類型發送數量爲amount的Wei,失敗時返回fasle,發送所有可用gas,多餘的gas再返回。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章