作者: 天宸@螞蟻安全實驗室
原文鏈接:https://mp.weixin.qq.com/s/yDTx2-Ia8-b1PLz8oDzDfg
區塊鏈安全是區塊鏈的命門。如果沒有安全的1,后面跟再多0都沒有意義。螞蟻安全實驗室全新推出「區塊鏈安全專欄」,持續更新有關智能合約安全分析、鏈平臺、密碼學等最新技術思考和實踐。
作為智能合約安全系列文章的首篇,本文將圍繞合約運行平臺的運行機制展開分享。歡迎持續關注!
專家點評
西安電子科技大學區塊鏈應用與測評中心副主任衛佳在閱讀了本文后表示,文章從“智能合約”概念的起源輕松過渡到區塊鏈語境,用簡潔的語言描述了區塊鏈視角下智能合約的關鍵特性:運行環境可信、規則公開透明。全文脈絡清晰可見,對智能合約的早期雛形把握準確,以不長的篇幅全面介紹了智能合約演進的路線、具體的開發方法和其后可能的發展方向。既能為初學者提供便捷高效的入門參考,又能為合格開發者提供知識回顧和建立完整視野的機會。
01 引 言
智能合約是 1996 年由Nick Szabo 尼克薩博提出的理念。當時,他對智能合約定義是:智能合約是一組以數字形式指定的承諾,包括各方在其中履行這些承諾的協議。(A smart contract is a set of promises, specified in digital form, including protocols within which the parties perform on these promises.)。
由于缺少可信的執行環境,智能合約并沒有被應用到實際產業中,自比特幣誕生后,人們認識到比特幣的底層技術區塊鏈能為智能合約提供不可篡改的存儲和確定性的運行機制,智能合約有了可落地的基礎。以太坊首先看到了區塊鏈和智能合約的契合,發布了白皮書《以太坊:下一代智能合約和去中心化應用平臺》。借著以太坊的發展,智能合約的概念得以普及。
在加密貨幣領域,幣安將智能合約定義為在區塊鏈上運行的應用或程序。通常情況下,它們為一組具有特定規則的數字化協議,且該協議能夠被強制執行。這些規則由計算機源代碼預先定義,所有網絡節點會復制和執行這些計算機源碼。區塊鏈可以看作智能合約的執行平臺,在不同的平臺上,智能合約的執行方式不同。
02 智 能 合 約 平 臺
智能合約的執行要依托于區塊鏈平臺。目前主流的區塊鏈平臺有:以比特幣為代表的區塊鏈 1.0,以以太坊為代表的區塊鏈 2.0,以 EOS 為代表的區塊鏈 3.0,以及眾多的聯盟鏈平臺。智能合約在每一種平臺上都有不同的演進。
以比特幣和其他加密貨幣為代表的區塊鏈技術被稱為區塊鏈 1.0,它具有去中心化,防篡改,匿名和可審計性的典型特征。但是,由于比特幣腳本語言的局限性,無法使用復雜的邏輯編寫合約(比特幣腳本語言只有256條指令,其中15條當前被禁用,75條被保留)。由于功能有限,比特幣只能被視為智能合約的原型。
以太坊等新興的區塊鏈平臺包含在區塊鏈上運行用戶定義程序的想法,從而借助圖靈完備的編程語言創建了富有表現力的定制智能合約。以太坊智能合約的代碼以基于堆棧的字節碼語言編寫,并在以太坊虛擬機(EVM)中執行。幾種高級語言(例如Solidity 和 Vyper)可用于編寫以太坊智能合約。然后可以將這些語言的代碼編譯為 EVM 字節碼以運行。以太坊目前是開發智能合約最流行的平臺,因此被稱為區塊鏈 2.0。
盡管以太坊創造性引入智能合約概念,極大的簡化了區塊鏈應用的開發,但以太坊平臺依然有一個很大的限制,就是交易確認時間長及交易吞吐量比較小,從而嚴重影響了以太坊進行商業應用。EOS 項目的目標是建立可以承載商業級智能合約與應用的區塊鏈基礎設施,成為區塊鏈世界的 “底層操作系統”。也被稱為區塊鏈 3.0。
在區塊鏈的世界觀里,一直有公有鏈和聯盟鏈的分別。以上 3 種平臺都是公有鏈,公有鏈的傳播范圍最廣,也最為人們熟知。但是,由于性能及商業機密等問題,B2B 的業務很難遷移到公有鏈上,相較之下,聯盟鏈是最為合適的選擇。目前已有多種聯盟鏈平臺,例如,由 IBM 帶頭發起的 Hyperledger Fabric,由摩根大通開發的企業級區塊鏈平臺 Quorum,由金鏈盟維護的 FISCO BCOS,以及由螞蟻自研的 Mychain。
2.1 比特幣中的智能合約
比特幣是第一代區塊鏈技術,在比特幣平臺尚沒有引入圖靈完備的智能合約機制,但是其有一套比特幣的腳本(Script)。比特幣腳本是有智能合約表達能力的,可以把比特幣的腳本理解成是一種智能合約。
2.1.1 比特幣腳本系統簡介
比特幣交易腳本系統,也稱為腳本,是一種基于逆波蘭表示法的基于堆棧的執行語言。腳本是一種功能簡單的編程語言,被設計成在有限的硬件上執行。
在比特幣腳本語言中,包含了許多的特性,但都特定設定了一種重要的方式--除了條件流程控制之外,沒有循環或復雜的流程控制功能。施加的這些限制確保該語言不被用于創造無限循環或其它類型的邏輯炸彈,這樣的炸彈可以植入在一筆交易中,通過引起拒絕服務的方式攻擊比特幣網絡。
2.1.2 腳本構建
比特幣的交易驗證引擎依賴于兩類腳本來驗證比特幣交易:一個鎖定腳本 locking script 和一個解鎖腳本 unlocking script。
鎖定腳本是一個放置在一個輸出值上的花費條件,它明確了今后花費這筆輸出的條件。由于鎖定腳本往往含有一個公鑰或者比特幣地址(即公鑰的哈希),它也曾被稱作 scriptPubKey。
解鎖腳本是一個“解決”或滿足鎖定腳本設置的花費條件的腳本,它將允許輸出被消費。解鎖腳本是每一筆比特幣交易輸入的一部分。通常情況下,解鎖腳本含有一個用戶的私鑰簽發的數字簽名,因此它曾被稱作 ScriptSig。但是并非所有的解鎖腳本都會包含簽名。
轉賬給公鑰的哈希 P2PKH 是最常見的比特幣交易類型。以 P2PKH 為例,來看如何使用解鎖腳本和鎖定腳本。

2.1.3 腳本執行
把解鎖腳本和鎖定腳本拼接到一起,解鎖腳本在前,鎖定腳本在后。腳本語言通過從左至右地處理每一個項目的方式來執行腳本。
數字(常數)被推送至堆棧,操作符向堆棧推送或移除一個或者多個參數,對它們進行處理。執行過程如下。

我們稍微做一些解釋 。帶'<>' 表示值,值要入棧,不帶尖括號的表示操作符,操作符操作棧頂數據,不入棧。
那么上圖的執行就是:
1.sig入棧
2.PubK入棧
3.DUP 是操作符,表示把棧頂值復制一份,此時棧里有 2 個 PubK。

有了前面的基礎,接下來的執行就比較顯而易見了。如果執行成功,棧頂最后會顯示 TRUE。
2.2 以太坊上的智能合約
以太坊作為第二代區塊鏈技術的代表,提供了圖靈完備的智能合約運行平臺。智能合約運行在以太坊虛擬機(Ethereum Virtual Machine EVM) 上。 以太坊上有多種智能合約開發語言如 Solidity,Vyper,本文主要關注 Solidity。
以太坊上運行智能合約要遵循以下步驟:首先開發人員編寫 Solidity 合約;然后使用客戶端工具編譯成 EVM 字節碼,并部署到以太坊上;后續可以通過發送交易來觸發智能合約執行,真正的執行由 EVM 負責。
2.2.1 開發 Solidity 合約
Solidity 是一門面向合約的、為實現智能合約而創建的高級編程語言。這門語言受到了 C++,Python 和 Javascript 語言的影響,設計的目的是能在以太坊虛擬機(EVM)上運行。
Solidity 是靜態類型語言,支持繼承、庫和復雜的用戶定義類型等特性。更多的關于使用 Solidity 語言開發智能合約的介紹請參考Solidity 語言開發文檔。
一個簡單的智能合約示例如下:
pragma solidity >=0.5.0 <0.7.0;
contract OtherContract {
uint x;
function f(uint y) external {
x = y;
}
function() payable external {}
}
contract New {
OtherContract other;
uint myNumber;
// Function mutability must be specified.
function someInteger() internal pure returns (uint) { return 2; }
// Function visibility must be specified.
// Function mutability must be specified.
function f(uint x) public returns (bytes memory) {
// The type must now be explicitly given.
uint z = someInteger();
x += z;
// Throw is now disallowed.
require(x > 100);
int y = -3 >> 1;
// y == -2 (correct)
do {
x += 1;
if (x > 10) continue;
// 'Continue' jumps to the condition below.
} while (x < 11);
// Call returns (bool, bytes).
// Data location must be specified.
(bool success, bytes memory data) = address(other).call("f");
if (!success)
revert();
return data;
}
}
目前 solidity 語言已經從 0.1.x 更新到 0.8.x,有了很多安全性的提升。此合約是 0.5.x 版本的合約,可以看到 0.5.x 相較于之前的版本對合約的語法做了很多的限制,如數據存儲的位置必須要顯示的指定,否則就會導致編譯錯誤。這一限制很好的防御了“影子變量漏洞”(見下一篇文章),提高了合約的安全性。關于版本的更多的改進,可參閱Solidity 官方網站 的 ADDITIONAL MATERIAL 部分。
2.2.2 編譯和部署合約
部署一個新的智能合約或者說 DApp 其實總共只需要兩個步驟,首先要將已經編寫好的合約代碼編譯成字節代碼,然后將字節碼和構造參數打包成交易發送到網絡中,等待當前交易被礦工打包進區塊鏈。

編譯 Solidity 代碼需要 solidity 編譯器參與工作,編譯器的使用也非常簡單,我們可以直接使用如下的命令將合約編譯成二進制:
solc --bin contract.sol
除了官方提供的命令行工具,也可以選擇其他的客戶端工具,如 Remix,IntelliJ IDEA plugin 等。如果使用 Remix 工具,那么開發,編譯,部署合約都可以輕松完成。

點擊圖中的 compile 可以編譯合約

點擊圖中的 deploy 可以部署合約
客戶端工具讓編譯和部署合約變的更加簡單。更多客戶端選擇可以參照安裝 Solidity 編譯器。
客戶端工具隱藏了合約部署交易的細節。具體的,一個合約部署交易包涵以下部分:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"blockHash": "0xfb508342b89066fe2efa45d7dbb9a3ae241486eee66103c03049e2228a159ee8",
"blockNumber": "0x208c0a",
"from": "0xe118559d65f87aaa8caa4383b112ff679a21223a",
"gas": "0x2935a",
"gasPrice": "0x9502f9000",
"hash": "0xe74c796a041bad60469f2ee023c87e087847a6603b27972839d0c0de2e852315",
"input": "0x6080604052348015600f57600080fd5b50603580601d6000396000f3006080604052600080fd00a165627a7a72305820d9b24bc33db482b29de2352889cc2dfeb66029c28b0daf251aad5a5c4788774a0029",
"nonce": "0x2",
"to": null,
"transactionIndex": "0x5",
"value": "0x0",
"v": "0x2c",
"r": "0xa5516d78a7d486d111f818b6b16eef19989ccf46f44981ed119f12d5578022db",
"s": "0x7125e271468e256c1577b1d7a40d26e2841ff6f0ebcc4da073610ab8d76c19d5"
}
}
在這個用于創建合約的特殊交易中,我們可以看到目標地址 to 的值為空,input 的值就是合約的二進制代碼。這筆交易被打包寫入區塊鏈之后,我們就能在 Etherscan 上根據交易的 hash 看到這筆交易成功的創建了一個合約。
在以太坊上部署合約的過程其實與交易發送的過程基本相似,唯一的區別就是用于創建合約的交易目前地址為空,并且 data 字段中的內容就是合約的二進制代碼,也就是合約的部署由兩部分組成:編譯合約和發送消息。
2.2.3 EVM 虛擬機執行合約
以太坊虛擬機 EVM 提供了 Solidity 智能合約的運行環境。它不僅是沙盒封裝的,而且是完全隔離的,也就是說在 EVM 中運行代碼是無法訪問網絡、文件系統和其他進程的。甚至智能合約之間的訪問也是受限的。
EVM 虛擬機一種基于棧的虛擬機。在基于棧的虛擬機中,有個重要的概念:操作數棧,數據存取為后進先出。所有的操作都是直接與操作數棧直接交互,例如:取數據、存數據、執行操作等。這樣有一個好處:可以無視具體的物理機器架構,特別是寄存器,但是缺點也很明顯,速度慢,無論什么操作都需要經過操作數棧。
以太坊有三種類型的空間可以用于存儲操作數據,EVM 虛擬機可以直接操作這些類型。
· 堆棧:一種后進先出的容器,執行完畢后數據就會被清除。
· 內存:一種可以無限擴展的字節數組,執行完畢后數據就會被清除。
· 合約的持久化存儲:一種鍵-值對,它區別于堆棧和內存,它存儲的內容會長期保存。
來看一個具體的示例,方便理解 evm 虛擬機的執行過程。
pragma solidity ^0.5.0;
contract simple {
uint num = 0;
constructor() public {
num = 123;
}
function add(uint i) public returns(uint){
uint m = 111;
num =num * i+m;
return num;
}
}
編譯后的內容過長,節選函數實現部分。
JUMPDEST function add(uint i) public re...
//這下面就是函數的代碼了
PUSH 0 uint //局部變量在棧里面
DUP1 uint m
PUSH 6F 111
SWAP1 uint m = 111
POP uint m = 111 //從push0到這里實現了定義局部變量并賦值
DUP1 m
DUP4 i //獲取參數
PUSH 0 num
SLOAD num //上面那句和這句實現了讀取成員變量
MUL num * i //乘
ADD num * i+m //加
PUSH 0 num
DUP2 num =num * i+m
SWAP1 num =num * i+m //這三句賦值
SSTORE num =num * i+m //成員變量存儲
POP num =num * i+m
//下面幾句實現return
PUSH 0 num
SLOAD num
SWAP2 return num
POP return num
POP function add(uint i) public re...
SWAP2 function add(uint i) public re...
SWAP1 function add(uint i) public re...
POP function add(uint i) public re...
JUMP [out] function add(uint i) public re...
棧的變化如下圖所示,原圖有一些錯誤,已用紅色更正。

部分指令操作如下:
· POP指令:從棧頂彈出一個元素。
· PUSHx:PUSH系列指令把緊跟在指令后面的N(1 ~ 32)字節元素推入棧頂。
· DUPx: DUP系列指令復制從棧頂開始數的第N(1 ~ 16)個元素,并把復制后的元素推入棧頂。
· SWAPx:SWAP系列指令把棧頂元素和從棧頂開始數的第N(1 ~ 16)+ 1 個元素進行交換。
· SSTORE:從棧頂彈出 2 個元素,棧頂元素為 key,次頂元素為 value,存儲到 storage 空間。
· SLOAD:先取出棧頂元素x,然后在storage中取以x為鍵的值(storage[x])存入棧頂。
更多指令的解讀可以參考Ethereum Virtual Machine Opcodes。
Ethereum WebAssembly (ewasm)
除了 EVM,以太坊社區還在積極開發 eWASM 虛擬機。eWASM 使用了 WebAssembly 的一個子集。使用WebAssembly作為智能合約的格式可獲得多種好處,下面列出了其中的一些:
· 達到近乎本地的執行速度
· 可以使用許多傳統編程語言(例如C,C ++和Rust)
· 能利用龐大的開發人員社區和WebAssembly周圍的工具鏈
更多內容留給讀者自己探索。
2.3 EOS 平臺的智能合約
EOS(Enterprise Operation System),企業操作系統,是為企業級分布式應用設計的一款區塊鏈操作系統。相比于比特幣、以太坊平臺性能低、開發難度大以及手續費高等問題,EOS擁有高性能處理能力、易于開發以及用戶免費等優勢,極大的滿足企業級的應用需求,被譽為繼比特幣、以太坊之后區塊鏈 3.0 技術。
為什么EOS性能高?這要得益于他的共識算法的設計。想知道他的共識算法?歡迎關注后續文章。
要了解如何部署和運行 EOS 合約需要先了解 EOS 系統的組成部分。如下圖所示:

EOS 系統主要由以下幾個部分組成:
· cleos(cli+eos=Cleos):本地的命令行工具,用戶通過命令行與節點(nodeos)的 REST 接口通信。是用戶或者開發者與節點進程交互的橋梁。
· keosd(key + eos = Keosd):本地錢包工具。非節點用戶存儲錢包的進程,可以管理多個含有私鑰的錢包并加密。
· nodeos(Node + eos=Nodes): EOS 系統的核心進程,也就是所謂的“節點”。主要是生產節點,一般用戶可以不用啟動,運行時可以配置插件。本地節點啟動時,配置的插件情況如下:
nodeos -e -p eosio
--plugin eosio::producer_plugin
--plugin eosio::chain_api_plugin
--plugin eosio::http_plugin
--plugin eosio::history_plugin
--plugin eosio::history_api_plugin
--filter-on="*"
--access-control-allow-origin='*'
--contracts-console
--http-validate-host=false
--verbose-http-errors
上述命令所使用的插件有:
· producer_plugin(生產節點插件):生產節點必須使用這個插件,普通節點不需要。
· chain_api_plugin(區塊鏈接口插件):提供區塊鏈數據接口。
· http_plugin(http 插件):提供 http 接口。
· history_plugin :可以獲取歷史數據。
· history_api_plugin :給 history_plugin 插件提供接口。
· wallet_plugin(錢包插件):使用這個插件就可以省去 keosd 錢包工具。
· wallet_api_plugin(錢包接口插件):給錢包插件提供接口。
更多命令行參數請參考EOSIO 開發者文檔。
2.3.1 開發智能合約
EOS 平臺目前主要的合約開發語言是 C/C, 盡管可以用C開發,但是社區在主推 EOS.IO C API,它提供了更強大的類型安全性,且更易于閱讀。
一個簡單的 hello world 合約如下:
#include <eosio/eosio.hpp>
using namespace eosio;
class [[eosio::contract]] hello : public contract {
public:
using contract::contract;
[[eosio::action]]
void hi( name user ) {
require_auth(user);
print( "Hello, ", user);
}
};
EOSIO智能合約由一組 Action 和類型定義組成。 Action 指定并實現合約的行為。 類型定義指定所需的內容和結構。開發合約時要對每一個action 實現對應的 action handler,如示例中 hi 函數就是 hi action 的 handler。action handler 的參數指定了接收的參數類型和數量。當向此合約發送 action 時,要發送滿足要求的參數。
2.3.1.1 Action
EOSIO Action 主要在基于消息的通信體系結構中運行。 客戶端可以使用 cleos 命令,將消息發送(推送)到 nodeos 來調用 Action。也可以使用 EOSIO send 方法(例如eosio :: action :: send)來調用 Action。 nodeos 將 Action 請求分發給合約的 WASM 代碼。 該代碼完整地運行完,然后繼續處理下一個 Action。
Transactions VS. Actions
Action 代表單個操作,交易是一個或多個 Action 的集合。合約和賬戶以 Action 的形式通信,Action 可以單獨發送,即一個交易只有一個 Action,也可以捆綁在一起發送,即一個交易包涵一組 Action。
包含一個 Action 的交易結構如下:
{
"expiration": "2018-04-01T15:20:44",
"region": 0,
"ref_block_num": 42580,
"ref_block_prefix": 3987474256,
"net_usage_words": 21,
"kcpu_usage": 1000,
"delay_sec": 0,
"context_free_actions": [],
"actions": [{
"account": "eosio.token",
"name": "issue",
"authorization": [{
"actor": "eosio",
"permission": "active"
}
],
"data": "00000000007015d640420f000000000004454f5300000000046d656d6f"
}
],
"signatures": [
""
],
"context_free_data": []
}
包含一組 Action 的交易結構如下,這組 Action 必須都要執行成功,否則整個交易被回滾。
{
"expiration": "...",
"region": 0,
"ref_block_num": ...,
"ref_block_prefix": ...,
"net_usage_words": ..,
"kcpu_usage": ..,
"delay_sec": 0,
"context_free_actions": [],
"actions": [{
"account": "...",
"name": "...",
"authorization": [{
"actor": "...",
"permission": "..."
}
],
"data": "..."
}, {
"account": "eosio",
"name": "voteproducer",
"authorization": [
{
"actor": "gu4dgmjxgyge",
"permission": "active"
}
],
"data": {
"voter": "gu4dgmjxgyge",
"proxy": "",
"producers": [
"bitfinexeos1",
"eosisgravity"
]
},
"hex_data": "a09867fd499688660000000000000000021030555d4db7b23be0b3dbe632ec3055"
}
],
"signatures": [
""
],
"context_free_data": []
}
其中一些字段的含義如下:
· delay_sec : 延遲時間,交易被打包到塊中之后,延遲指定的時間執行。 在這段時間內,交易都可以被用戶取消。
· action:
· account: Action 所在的合約名稱
· name:所調用的 Action 的名字
· authorization:此次調用所需要的權限
· actor:操作者,如 gu4dgmjxgyge
· permission:權限名稱,如active
· data: 調用所需要的參數
· hex_data: data數據的十六進制形式
· expiration:交易過期時間,超過這個時間,交易就失效,不能再被寫入區塊
· ref_block_num:參考區塊,在最新的 2^16 個區塊中選擇一個
· ref_block_prefix:參考區塊的前綴
[注]
ref_block_num(參考區塊號), ref_block_prefix(參考區塊的前綴)和 expiration(過期時間)三者是用作TaPOS(Transaction as Proof of Stake, 交易作為權益證明)算法,是為了確保一筆交易在所引用的區塊之后和交易過期日期之前能夠發生。
這樣做有什么作用呢?
假設現在有2個用戶 A 和 B, B 叫 A 說你轉 2 個 EOS 給我, 我就送你 100 個 LIVE,A 說好啊。 然后 A 就轉 2 個 EOS 給 B 了, 這個時候 A 的區塊 a 還不是不可逆狀態, 如果此時 B 轉給 A 100 個 LIVE, 要是 區塊 a 被回滾掉了怎么辦,那么 B 就白白給了 A 100 個 LIVE 了。 這時候 ref-block 的作用就體現了,如果區塊 a 被回滾了,那么 B 轉給 A 100 個 LIVE 的區塊 b 也會被丟棄掉。 所以 當區塊 b ref-block 是 區塊 a 的時候,只有 區塊 a 被成功打包了, 區塊 b 才會被成功打包。
所以很顯然, 這兩個參數是為了讓鏈更穩固,也讓用戶交易更安全。但是,有的開發者使用這兩個參數作為隨機數的種子,這是非常不安全的做法,容易遭受隨機數預測攻擊。
2.3.1.2 通信模型
EOS體系是以通訊為基本的,Action 就是EOS上通訊的載體。EOSIO 支持兩種基本通信模型:內聯(inline)通信,如在當前交易中處理 Action,和延遲(defer)通信,如觸發一筆將來的交易。
Inline通信
Inline 通信是指調用 Action 和被調用 Action 都要執行成功(否則會一起回滾)。
Inline communication takes the form of requesting other actions that need to be executed as part of the calling action.
Inline 通信使用原始交易相同的 scope 和權限作為執行上下文,并保證與當前 action 一起執行。可以被認為是 transaction 中的嵌套 transaction。如果 transaction 的任何部分失敗,Inline 動作將和其他 transaction 一起回滾。無論成功或失敗,Inline 都不會在 transaction 范圍外生成任何通知。
重要的是要記住內聯操作是作為調用操作的一部分執行的。因此,它們與原始交易的范圍和權限相同。這是他們將被執行的保證。如果其中一個操作失敗,則整個交易將失敗。
Deferred通信
Deferred 通信在概念上等同于發送一個 transaction 給一個賬戶。這個 transaction 的執行是 eos 出快節點自主判斷進行的,Deferrd 通信無法保證消息一定成功或者失敗。
如前所述,Deferred 通信將在稍后由出快節點自行決定,從原始 transaction(即創建 Deferred 通信的 transaction)的角度來看,它只能確定創建請求是成功提交還是失敗(如果失敗,transaction 將立即失敗)。
在開發智能合約時,要區分兩種通信方式,并斟酌要使用的方式,否則合約將會遭受攻擊,如回滾攻擊。
2.3.2 編譯和部署合約
2.3.2.1 編譯
EOSIO 智能合約開發完成之后,需要先編譯成 WASM 字節碼,然后部署到鏈上。
EOS 平臺 CDT 套件中提供了 eosio-cpp 工具編譯合約,使用以下命令就可以把合約編譯成 wasm 文件。
eosio-cpp -o overflow.token.wasm overflow.token.cpp --abigen
--abigen 參數表示要同時生成 abi 文件。ABI描述文件對智能合約的每一個 action handler 進行了描述,根據這些描述就可以知道action handler接收的參數類型和數量,從而可以發起action調用 handler。
和以太坊一樣,業界也有很多 IDE 環境可以幫助開發者開發、編譯、部署合約,如 EOS Studio,成都鏈安也提供了一個在線的 IDE https://beosin.com/BEOSIN-IDE/index.html#/。
2.3.2.2 部署
部署合約可以使用 cleos 命令完成。
Adas-Macbook-Pro:random ada$ cleos set contract alice .
Reading WASM from /Users/ada/Blockchain/eos/eosio.cdt-1.4.1/eosio.contracts/random/random.wasm...
Publishing contract...
executed transaction: 97d33fc2143a84ab23e9f975983a036efb266aa1a9e77ae76273b7df2a2a03bc 8024 bytes 10800 us
# eosio <= eosio::setcode "0000000000855c340000fb82010061736d0100000001bd011c60047f7e7f7f0060000060047f7f7f7f0060037f7f7f017f6...
# eosio <= eosio::setabi "0000000000855c34570e656f73696f3a3a6162692f312e31000102686900030269640675696e7436340c626c6f636b5f707...
warning: transaction executed locally, but may not be confirmed by the network yet ]
這段log很明顯的說明了部署合約等價于調用 eosio 智能合約的 setcode 和 setabi 函數:
\>$ cleos push action eosio setcode '[eosio.bios.wasm]' -p eosio
\>$ cleos push action eosio setabi eosio '[eosio.bios.abi] -p eosio
來看一下部署合約的交易的具體內容:
Adas-Macbook-Pro:random ada$ cleos get transaction 97d33fc2143a84ab23e9f975983a036efb266aa1a9e77ae76273b7df2a2a03bc
{
"id": "97d33fc2143a84ab23e9f975983a036efb266aa1a9e77ae76273b7df2a2a03bc",
"trx": {
"receipt": {
"status": "executed",
"cpu_usage_us": 10800,
"net_usage_words": 1003,
"trx": [
1,{
"signatures": [
"SIG_K1_K12rdVM7JiVVZsZ24dxeQF2ehKc7Nymom4zm3QAZijRtCESQneXnEyCXA3wwYpT98JEhr2HcnXcE4bu3crYG1PoNY8fZTz"
],
"compression": "zlib",
"packed_context_free_data": "",
"packed_trx": "78dad57c7b8c5cd779df39f7358f3b... "
}
]
},
"trx": {
"expiration": "2019-12-26T03:25:48",
"ref_block_num": 3263,
"ref_block_prefix": 1594014232,
"max_net_usage_words": 0,
"max_cpu_usage_ms": 0,
"delay_sec": 0,
"context_free_actions": [],
"actions": [{
"account": "eosio",
"name": "setcode",
"authorization": [{
"actor": "alice",
"permission": "active"
}
],
"data": "0000000000855c340000fb82... "
},{
"account": "eosio",
"name": "setabi",
"authorization": [{
"actor": "alice",
"permission": "active"
}
],
"data": "0000000000855c34570e656f73696f3a3a6162692f312e31000102686900030269640675696e7436340c626c6f636b5f7072656669780675696e74333209626c6f636b5f6e756d0675696e74333201000000000000806b026869000000000000"
}
],
"transaction_extensions": [],
"signatures": [
"SIG_K1_K12rdVM7JiVVZsZ24dxeQF2ehKc7Nymom4zm3QAZijRtCESQneXnEyCXA3wwYpT98JEhr2HcnXcE4bu3crYG1PoNY8fZTz"
],
"context_free_data": []
}
},
"block_time": "2019-12-26T03:25:18.500",
"block_num": 1379521,
"last_irreversible_block": 1379663,
"traces": [{
... 省略若干內容
}
]
}
以上可以看出,合約部署交易有 2 個 Action:setcode 和 setabi。setcode 的 data 字段是編譯的 wasm 二進制字節碼,setabi 的 data 字段是合約代碼所對應的 abi 文件。
細心的讀者可能已經發現,EOS 的交易結構沒有 from 和 to 字段。這是因為 EOS 和以太坊的賬戶模型和權限模型都非常不同。因為賬戶權限模型已經超出了本文的范圍,這里不多做討論。讀者能夠了解在 EOS平臺上,部署合約本質上也是發送一筆交易即可。
2.3.3 WASM 虛擬機運行合約
和以太坊一樣,EOS 的智能合約也需要運行在虛擬機上。EOS 采用了 Web Assembly 又名 WASM 虛擬機。WASM是一個已嶄露頭角的 web 標準,受到 Google, Microsoft, Apple 及其他公司的廣泛支持。
EOS在技術白皮書中指明并不提供具體的虛擬機實現,任何滿足沙盒機制的虛擬機都可以運行在 EOSIO 中。EOS 官方虛擬機代碼實現來自WAVM,Primary repo: https://github.com/AndrewScheidecker/WAVM。
WAVM 也是基于棧的虛擬機,主要有以下 2 個特點:
1.棧是后進先出的,大多數 WAVM 指令都假定操作數將從棧頂中取出,并將結果放回棧頂中。
2.程序計數器控制程序執行,控制指令可以修改計數器的內容,如果沒有控制指令,則自增。
WASM 虛擬機能夠操作的存儲空間主要包括三部分:
· 棧 Wasm是基于棧的虛擬機,并且執行的是字節碼,這一點和JVM、EVM等虛擬機類似。和其他基于棧的虛擬機一樣,Wasm指令集里的很大一部分指令都是直接對棧進行操作,比如 i32.const、 i32.add、 i32.sub、 drop等。
· 內存 Wasm 虛擬機可以操作一個按字節尋址的線性內存空間。內存可以由 Wasm 虛擬機自己分配,也可以從外部引入(import),但是在MVP階段最多只能有一塊內存。不管 Wasm 內存來自于哪兒,都可以按頁進行擴展,一頁是64KiB。下面是內存操作相關的一些指令:
· memory.grow 使可訪問內存增加一頁
· memory.size 把當前內存字節數推入棧頂
· load系列指令(比如i32.load)把內存數據載入棧頂
· store 系列指令(比如i32.store)把棧頂數據寫回內存
· 全局變量 Wasm模塊可以從外部引入全局變量,也可以在內部自己定義全局變量,這些全局變量使用同一個索引空間。有兩條指令可以操作全局變量:
· get_global 獲取指定索引處的全局變量值,并推入棧頂
· set_global 從棧頂彈出一個值,并用它設置指定索引處的全局變量
來看一個具體的示例。下面的內容是 helloword 對應的 wast 代碼的一部分。
(module
(type (;0;) (func (result i32)))
(type (;1;) (func (param i32 i32)))
(type (;2;) (func (param i32 i32 i32) (result i32)))
(type (;3;) (func (param i32 i32) (result i32)))
(type (;4;) (func (param i64)))
(type (;5;) (func (param i32)))
(type (;6;) (func (param i32 i64)))
(type (;7;) (func))
(type (;8;) (func (param i64 i64 i64)))
(type (;9;) (func (param i32) (result i32)))
(type (;10;) (func (param i64 i64)))
(import "env" "action_data_size" (func (;0;) (type 0)))
(import "env" "eosio_assert" (func (;1;) (type 1)))
(import "env" "memset" (func (;2;) (type 2)))
(import "env" "read_action_data" (func (;3;) (type 3)))
(import "env" "memcpy" (func (;4;) (type 2)))
(import "env" "require_auth" (func (;5;) (type 4)))
(import "env" "prints" (func (;6;) (type 5)))
(import "env" "printn" (func (;7;) (type 4)))
(import "env" "eosio_assert_code" (func (;8;) (type 6)))
(func (;9;) (type 7)
call 12)
(func (;10;) (type 8) (param i64 i64 i64)
call 9
get_local 0
get_local 1
...
這里我們可以看到 11 個 function signatures 和他們對應的索引,function signature 就像函數原型,定義了預期的函數輸入和輸出。還有一些 import 的函數,表示從 external c++ 引入的函數。
對于一個函數,編譯后 WASM 字節碼如下:
(func (;12;) (type 7)
(local i32)
get_global 0
i32.const 16
i32.sub
tee_local 0
i32.const 0
i32.store offset=12
i32.const 0
get_local 0
i32.load offset=12
i32.load
i32.const 7
i32.add
i32.const -8
i32.and
tee_local 0
i32.store offset=8196
i32.const 0
get_local 0
i32.store offset=8192
i32.const 0
memory.size
i32.store offset=8204)
部分指令定義如下:
· i32.load8_s: 加載1字節, 將8位整數零擴展為32位整數
· i32.load8_u: 加載1字節, 將8位整數零擴展為32位整數
· i32.load16_s: 加載2字節, 將16位整數符號擴展為32位整數
· i32.load16_u: 加載2字節, 將16位整數零擴展為32位整數
· i32.load: 加載4字節,轉換為32位整數
[注]
符號擴展: 二進制中的有符號數,符號位總是位于數的第一位,如果向方位較大的數據類型進行擴展,符號位也應該位于第一位才對,所以當一個負數被擴展時,其擴展的高位全被置位為1;對于整數,因為符號位是0,所以其擴展的位仍然是0
零擴展: 不管要轉換成什么整型類型,不要最初值的符號位是什么,擴展的高位都被置位0.
見《深入理解計算機系統》 原書第3版 第2章 信息的表示和處理 2.2.6節 擴展一個數字的位表示。
更多關于 wasm 指令的具體含義,請參考:
· http://webassembly.org.cn/docs/semantics/
· https://webassembly.github.io/spec/core/syntax/instructions.html
03 小 結
本文主要講解了目前主流的智能合約運行平臺:以比特幣為首的區塊鏈 1.0 平臺,以以太坊為首的區塊鏈 2.0 平臺,以 EOS 為首的區塊鏈 3.0 平臺,以及在這些平臺上智能合約是如何開發,編譯,運行的。從本文中,我們看到了合約運行平臺的演進:從比特幣僅提供非圖靈完備的平臺,演進到以太坊提供圖靈完備的平臺;從以太坊僅提供低吞吐量的運行平臺,演進到 EOS 提供高吞吐量的運行平臺。這些演進滿足了各式各樣的需求,也推進了區塊鏈技術的持續創新。
在智能合約運行平臺蓬勃發展的同時,智能合約面臨的安全威脅也伴隨而來。本文希望讀者對智能合約的開發,編譯,部署,運行有個大致的了解。接下來,我們后續的文章會更具體的分析智能合約面臨的安全威脅。
參考文獻
1.https://docs.soliditylang.org/en/v0.8.1/index.html#
2.https://eos.readthedocs.io/zh_CN/latest/
3.https://eos.io/for-business/training-certification/
4.https://www.bookstack.cn/read/MasterBitcoin2CN/spilt.4.ch06.md
5.https://draveness.me/smart-contract-deploy/
6.https://cnodejs.org/topic/5aeecba802591040485bab2a

螞蟻安全天宸實驗室:
隸屬于螞蟻安全實驗室,致力于研究并落地下一代核電級安全防御和密碼學基礎設施,攻克業界系統安全、移動安全、IoT安全、密碼學等重點領域的安全防御技術難題。
掃碼關注螞蟻安全實驗室微信公眾號,干貨不斷!

本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1546/
暫無評論