作者: 天宸@蚂蚁安全实验室
原文链接: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》6.4 小节

2.1.3 脚本执行

把解锁脚本和锁定脚本拼接到一起,解锁脚本在前,锁定脚本在后。脚本语言通过从左至右地处理每一个项目的方式来执行脚本。

数字(常数)被推送至堆栈,操作符向堆栈推送或移除一个或者多个参数,对它们进行处理。执行过程如下。

图片

图片来自《精通比特币2》6.4 小节

我们稍微做一些解释 。带'<>' 表示值,值要入栈,不带尖括号的表示操作符,操作符操作栈顶数据,不入栈。

那么上图的执行就是:

1.sig入栈

2.PubK入栈

3.DUP 是操作符,表示把栈顶值复制一份,此时栈里有 2 个 PubK。

图片

图片来自《精通比特币2》6.4 小节

有了前面的基础,接下来的执行就比较显而易见了。如果执行成功,栈顶最后会显示 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 官网案例

目前 solidity 语言已经从 0.1.x 更新到 0.8.x,有了很多安全性的提升。此合约是 0.5.x 版本的合约,可以看到 0.5.x 相较于之前的版本对合约的语法做了很多的限制,如数据存储的位置必须要显示的指定,否则就会导致编译错误。这一限制很好的防御了“影子变量漏洞”(见下一篇文章),提高了合约的安全性。关于版本的更多的改进,可参阅Solidity 官方网站 的 ADDITIONAL MATERIAL 部分。

2.2.2 编译和部署合约

部署一个新的智能合约或者说 DApp 其实总共只需要两个步骤,首先要将已经编写好的合约代码编译成字节代码,然后将字节码和构造参数打包成交易发送到网络中,等待当前交易被矿工打包进区块链。

图片

图片来源 Draveness 博客

编译 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 官网

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安全、密码学等重点领域的安全防御技术难题。

扫码关注蚂蚁安全实验室微信公众号,干货不断!


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1546/