EOSIO Smart Contract

介绍EOSIO智能合约

编写智能合约需要的必备技能

C / C++ 相关

基于EOSIO的块链使用的是WebAssembly (WASM)来执行用户编写的智能合约。WASM是一种新兴的Web标准,广泛支持于谷歌、微软、苹果等。对编写WASM标准的智能合约来说使用clang/llvm和它的C/C++编译器是目前最为成熟的编译工具链。

其他的第三方工具链在开发中,包括:Rust, Python, and Solidity。虽然这些语言可能看起来相对简单,但它们可能会影响您所编写的智能性能。我们认为,对于开发高性能和安全的智能合约,C++是最好的语言,将来eos的智能合约也还会继续支持C++。

Linux / Mac OS Experience

EOSIO 支持下面的操作系统: - Amazon 2017.09 and higher - Centos 7 - Fedora 25 and higher (Fedora 27 推荐使用) - Mint 18 - Ubuntu 16.04 (Ubuntu 16.10 推荐使用) - MacOS Darwin 10.12 and higher (MacOS 10.13.x 推荐使用)

命令行相关

EOSIO提供了一些工具,您可以通过这些工具与eos进行交互。

EOSIO的基础知识

通信模式

EOSIO智能合约以action和访问共享内存数据库(shared memory database access)的形式相互通信,例如,合约可以用异步感应(async vibe)读取另一个合约数据库的状态,只要它包含在同一个事务的读取范围内。

The async communication may result in spam which the resource limiting algorithm will resolve. 异步通信可能导致资源限制算法将解决的垃圾邮件。

在合约中可以定义两种通信模式:

  • Inline. 被保证在当前的transaction或unwind中执行;结果无论成功或失败,都不会通知任何通知。Inline操作与original transaction具有相同的范围和权限。

  • Deferred. Defer将被BP节点安排在之后执行,有可能会通知通信的结果或者超时。Deferred可以带着调用者的授权延伸到不同的scopes。

Action vs Transaction

Action表示单个操作,而transaction是一个或多个action的集合。Action是合约和账户之间进行通信的方式。Action可以单独执行,或者组合组合起来作为一个整体执行。

仅有一个action的transaction.

{
  "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的transaction, 这些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": "...",
      "name": "...",
      "authorization": [{
          "actor": "...",
          "permission": "..."
        }
      ],
      "data": "..."
    }
  ],
  "signatures": [
    ""
  ],
  "context_free_data": []
}

Action名字约束

Action的类型是 base32被编码为64-bit整数. 这意味着它的字符集长度是12,并且只能包含a-z,1-5,和'.'。 如果长度超过12个,他会自动截取前12个符合规则的字符作为action的名字(原文是:If there is a 13th character then it is restricted to the first 16 characters ('.' and a-p).,应该是写错了)

Transaction 确认 收到一个transaction并不意味着这个transaction已经被确认,它仅仅说明这个transaction被一个BP节点接受并且没有错误,当然也意味着很有可能这个transaction被其他bp接受了。

当一个transaction被包含在一个block当中的时候,它才是可以被确认执行的。

智能合约文件

从简单易用的角度出发,我们编写了一个工具eosiocpp ,它可以创建一个新的智能合约。eosiocpp也可以创建3个合约文件,它们仅仅包含了合约的框架。

$ eosiocpp -n ${contract}

上面的命令会在./${project}目录下创建一个空的项目,它包含3个文件

${contract}.abi ${contract}.hpp ${contract}.cpp

hpp

${contract}.hpp 这是合约的头文件,可以包含一些变量,常量和函数的声明。

cpp

The ${contract}.cpp 这是合约的源码文件,包含合约的具体实现。

如果你用eosiocpp生成了一个 .cpp, 那它的内容大概类似如下:

#include <${contract}.hpp>

/**
 *  The init() and apply() methods must have C calling convention so that the blockchain can lookup and
 *  call these methods.
 */
extern "C" {

    /**
     *  This method is called once when the contract is published or updated.
     */
    void init()  {
       eosio::print( "Init World!\n" ); // Replace with actual code
    }

    /// The apply method implements the dispatch of actions to this contract
    void apply( uint64_t code, uint64_t action ) {
       eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
    }

} // extern "C"

在这个例子里,我们可以看到两个函数,initapply。它们会打印log并且不做任何检查。任何人都可以在任何时刻执行BP允许的所有action。在不需要任何签名的情况下,合约将被计入带宽消耗。(Absent any required signatures, the contract will be billed for the bandwidth consumed.)

init

init 仅当合约第一次被部署的时候执行。 在这个函数里可以初始化变量, 比如,在currency合约中总体的token的供应量。

apply

apply 是一个中转函数, 他监听所有传入的action,并且根据action调用合约相应的函数。apply函数需要两个参数, code 和 action

code filter

这个参数是为了对action做出回应,比如下面的apply函数,你可以构造一个通用响应去忽略code。 (In order to respond to a particular action, structure the apply function as follows. You may also construct a response to general actions by omitting the code filter.)

if (code == N(${contract_name}) {
    // your handler to respond to particular action
}

当然你也可以为每个action构造各自的一个响应。

action filter

为了响应每一个action,比如构造比如下面的apply函数。通常和code filter一起使用

if (action == N(${action_name}) {
    //your handler to respond to a particular action
}

wast

任何合约程序想要部署到EOSIO的区块链网络中都必须编译成WASM格式。这是EOS的支持唯一个的格式。

一旦你的CPP文件写好了,有就可以用eosiocpp把它编译成WASM (.wast)文件了

$ eosiocpp -o ${contract}.wast ${contract}.cpp

abi

ABI( Application Binary Interface)文件是一个JSON格式的描述文件,说明了如何在他们的JSON和二进制之间转化用户的action。ABI文件也同时说明了如何转换数据库的状态。一旦你用了ABI描述了你的合约,开发人员就和用户就可以和你的合约通过JSON进行交互。

ABI可以通过.hpp文件用eosiocpp生成。

$ eosiocpp -g ${contract}.abi ${contract}.hpp

下面这个例子展示了一个ABI文件的框架:

{
  "types": [{
      "new_type_name": "account_name",
      "type": "name"
    }
  ],
  "structs": [{
      "name": "transfer",
      "base": "",
      "fields": {
        "from": "account_name",
        "to": "account_name",
        "quantity": "uint64"
      }
    },{
      "name": "account",
      "base": "",
      "fields": {
        "account": "name",
        "balance": "uint64"
      }
    }
  ],
  "actions": [{
      "action": "transfer",
      "type": "transfer"
    }
  ],
  "tables": [{
      "table": "account",
      "type": "account",
      "index_type": "i64",
      "key_names" : ["account"],
      "key_types" : ["name"]
    }
  ]
}

你会注意到这个ABI定义了一个actoin名字是transfer,类型是transfer。这就是告诉EOSIO,当调用的action是transfer时,它的格式是transfer,定义如下:

...
  "structs": [{
      "name": "transfer",
      "base": "",
      "fields": {
        "from": "account_name",
        "to": "account_name",
        "quantity": "uint64"
      }
    },{
...

ABI文件有很多的部分组成,比如from,toquantity。每个部分都有自己的类型,比如account_nameuint64account_name是一个内建类型用base32字符串表示为uint64。想要看到更多的内建类型可以点击这里

{
  "types": [{
      "new_type_name": "account_name",
      "type": "name"
    }
  ],
...

在上面types 数组里,我们为已经存在的account_name类型定义了一个别名name

调试智能合约

为了能够调试智能合约,你需要在你的本地环境中启动一个nodeos。这个本地的nodeos可以是一个EOS私有的测试网络或者是公网的测试网络。

当你第一次创建智能合约的时候,推荐你最好在你自己的私有测试网络中调试好,因为你对你自己的私有测试网络有完全的掌控权。这可以让你无限制的使用EOS(币)也可以随时复位它的状态。当合约调试完毕,就可以部署到公共测试网络了,本地先运行一个连接到公共测试网络的nodeos,然后连接到这个节点就可以获得log输出了。

步骤是一样的,所以下面这个手册也适用于私有测试网络中的测试。

如果你还没有一个本地的nodeos环境,可以参考这个连接。默认情况下,你的本地nodes会运行在一个私有网络中,除非你修改了config.ini文件,让他去连接公共测试网络,如何修改可以参考这里

方法

调试最主要的方法就是用Caveman Debugging,我们增强了printing的功能,他可以去输出变量的值并且检查合约的流程。Printing可以通过下面API被合约使用: 这是c 这是 C++). C++的API是对C的封装,所以大多数我们使用C++的API。

Print

Print C API 支持如下数据类型: - prints - a null terminated char array (string) - prints_l - any char array (string) with given size - printi - 64-bit unsigned integer - printi128 - 128-bit unsigned integer - printd - double encoded as 64-bit unsigned integer - printn - base32 string encoded as 64-bit unsigned integer - printhex - hex given binary of data and its size

同时 Print C++ API 对上面的C API进行了封装,所以用户不需要指定应该使用哪种类型的Print。Print C++ API 支持 - a null terminated char array (string) - integer (128-bit unsigned, 64-bit unsigned, 32-bit unsigned, signed, unsigned) - base32 string encoded as 64-bit unsigned integer - struct that has print() method

Example

Let's write a new contract as example for debugging - debug.hpp

#include <eoslib/eos.hpp>
#include <eoslib/db.hpp>

namespace debug {
    struct foo {
        account_name from;
        account_name to;
        uint64_t amount;
        void print() const {
            eosio::print("Foo from ", eosio::name(from), " to ",eosio::name(to), " with amount ", amount, "\n");
        }
    };
}
  • debug.cpp
#include <debug.hpp>

extern "C" {

    void init()  {
    }

    void apply( uint64_t code, uint64_t action ) {
        if (code == N(debug)) {
            eosio::print("Code is debug\n");
            if (action == N(foo)) {
                 eosio::print("Action is foo\n");
                debug::foo f = eosio::current_message<debug::foo>();
                if (f.amount >= 100) {
                    eosio::print("Amount is larger or equal than 100\n");
                } else {
                    eosio::print("Amount is smaller than 100\n");
                    eosio::print("Increase amount by 10\n");
                    f.amount += 10;
                    eosio::print(f);
                }
            }
        }
    }
} // extern "C"
  • debug.abi
{
  "structs": [{
      "name": "foo",
      "base": "",
      "fields": {
        "from": "account_name",
        "to": "account_name",
        "amount": "uint64"
      }
    }
  ],
  "actions": [{
      "action_name": "foo",
      "type": "foo"
    }
  ]
}

现在我们可以部署这个合约并且调用它,假设你已经创建了debug账户并且在钱包中导入了它的密钥。

$ eosiocpp -o debug.wast debug.cpp
$ cleos set contract debug debug.wast debug.abi
$ cleos push message debug foo '{"from":"inita", "to":"initb", "amount":10}' --scope debug

当你检查你的nodeoslog的时候,就可以看到下面的信息:

Code is debug
Action is foo
Amount is smaller than 100
Increase amount by 10
Foo from inita to initb with amount 20

这里,你可以清楚的看到,合约按照你的预期被执行了,并且余额是正确的。你也许会看到上面的信息两次,因为每次的transaction都会被验证,生成块和块合约 There, you can confirm that your message is going to the right control flow and the amount is updated correctly. You might see the above message at least 2 times and that's normal because each transaction is being applied during verification, block generation, and block application.