关于智能合约

翻译:古千峰@BTCMedia

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

C / C++ 相关

基于EOSIO的块链使用的是WebAssembly(http://webassembly.org/) (WASM)来执行用户编写的智能合约。WASM是一种新兴的Web标准,广泛支持于谷歌、微软、苹果等。对编写WASM标准的智能合约来说使用clang/llvm(https://clang.llvm.org/) 和它的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)的形式相互通信,例如,合约可以用异步感应读取另一个合约数据库的状态,只要它包含在同一个事务的读取范围内。

异步通信可能导致资源限制算法将解决的垃圾邮件。

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

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

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

Action vs Transaction

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

以下是包含一个action的transaction:

  1. {
  2. "expiration": "2018-04-01T15:20:44",
  3. "region": 0,
  4. "ref_block_num": 42580,
  5. "ref_block_prefix": 3987474256,
  6. "net_usage_words": 21,
  7. "kcpu_usage": 1000,
  8. "delay_sec": 0,
  9. "context_free_actions": [],
  10. "actions": [{
  11. "account": "eosio.token",
  12. "name": "issue",
  13. "authorization": [{
  14. "actor": "eosio",
  15. "permission": "active"
  16. }
  17. ],
  18. "data": "00000000007015d640420f000000000004454f5300000000046d656d6f"
  19. }
  20. ],
  21. "signatures": [
  22. ""
  23. ],
  24. "context_free_data": []
  25. }

以下是包含多个action的transaction, 这些action要么全部成功要么全部失败.

  1. {
  2. "expiration": "...",
  3. "region": 0,
  4. "ref_block_num": ...,
  5. "ref_block_prefix": ...,
  6. "net_usage_words": ..,
  7. "kcpu_usage": ..,
  8. "delay_sec": 0,
  9. "context_free_actions": [],
  10. "actions": [{
  11. "account": "...",
  12. "name": "...",
  13. "authorization": [{
  14. "actor": "...",
  15. "permission": "..."
  16. }
  17. ],
  18. "data": "..."
  19. }, {
  20. "account": "...",
  21. "name": "...",
  22. "authorization": [{
  23. "actor": "...",
  24. "permission": "..."
  25. }
  26. ],
  27. "data": "..."
  28. }
  29. ],
  30. "signatures": [
  31. ""
  32. ],
  33. "context_free_data": []
  34. }

Action名字约束

Action的类型是 base32被编码为64-bit整数. 这意味着它的字符集长度是12,并且只能包含a-z,1-5,和’.’。 如果长度超过12个,他会自动截取前12个符合规则的字符作为action的名字

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

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

智能合约文件

从简单易用的角度出发,我们编写了一个工具eosiocpp(https://github.com/EOSIO/eos/wiki/Programs-&-Tools#eosiocpp),它可以创建一个新的智能合约。eosiocpp也可以创建3个合约文件,它们仅仅包含了合约的框架。

  1. $ eosiocpp -n ${contract}

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

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

hpp

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

cpp

${contract}.cpp 这是合约的源码文件,包含合约的具体实现。 如果你用eosiocpp生成了一个 .cpp, 那它的内容大概类似如下:

  1. #include <${contract}.hpp>
  2. /**
  3. * The init() and apply() methods must have C calling convention so that the blockchain can lookup and
  4. * call these methods.
  5. */
  6. extern "C" {
  7. /**
  8. * This method is called once when the contract is published or updated.
  9. */
  10. void init() {
  11. eosio::print( "Init World!\n" ); // Replace with actual code
  12. }
  13. /// The apply method implements the dispatch of actions to this contract
  14. void apply( uint64_t code, uint64_t action ) {
  15. eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
  16. }
  17. } // 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函数需要两个参数, codeaction

code filter

这个参数是为了对action做出回应,比如下面的apply函数,你可以构造一个通用响应去忽略code

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

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

action filter

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

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

wast

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

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

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

abi

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

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

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

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

  1. {
  2. "types": [{
  3. "new_type_name": "account_name",
  4. "type": "name"
  5. }
  6. ],
  7. "structs": [{
  8. "name": "transfer",
  9. "base": "",
  10. "fields": {
  11. "from": "account_name",
  12. "to": "account_name",
  13. "quantity": "uint64"
  14. }
  15. },{
  16. "name": "account",
  17. "base": "",
  18. "fields": {
  19. "account": "name",
  20. "balance": "uint64"
  21. }
  22. }
  23. ],
  24. "actions": [{
  25. "action": "transfer",
  26. "type": "transfer"
  27. }
  28. ],
  29. "tables": [{
  30. "table": "account",
  31. "type": "account",
  32. "index_type": "i64",
  33. "key_names" : ["account"],
  34. "key_types" : ["name"]
  35. }
  36. ]
  37. }

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

  1. "structs": [{
  2. "name": "transfer",
  3. "base": "",
  4. "fields": {
  5. "from": "account_name",
  6. "to": "account_name",
  7. "quantity": "uint64"
  8. }
  9. },{

ABI文件有很多的部分组成,比如from,toquantity。每个部分都有自己的类型,比如account_nameuint64account_name是一个内建类型用base32字符串表示为uint64。想要看到更多的内建类型可以点击: https://github.com/EOSIO/eos/blob/master/libraries/chain/contracts/abi_serializer.cpp

  1. {
  2. "types": [{
  3. "new_type_name": "account_name",
  4. "type": "name"
  5. }
  6. ],
  7. ...

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

调试智能合约

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

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

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

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

方法

  1. 调试最主要的方法就是用**Caveman Debugging**,我们增强了printing的功能,他可以去输出变量的值并且检查合约的流程。Printing可以通过下面API被合约使用:
  2. 这是c([https://github.com/EOSIO/eos/blob/master/contracts/eoslib/print.h](https://github.com/EOSIO/eos/blob/master/contracts/eoslib/print.h))
  3. 这是 C++([https://github.com/EOSIO/eos/blob/master/contracts/eoslib/print.hpp](https://github.com/EOSIO/eos/blob/master/contracts/eoslib/print.hpp)) . 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

实例

下面是一个用print实现debug的智能合约。

  • debug.hpp ```

    include

extern “C” {

  1. void init() {
  2. }
  3. void apply( uint64_t code, uint64_t action ) {
  4. if (code == N(debug)) {
  5. eosio::print("Code is debug\n");
  6. if (action == N(foo)) {
  7. eosio::print("Action is foo\n");
  8. debug::foo f = eosio::current_message<debug::foo>();
  9. if (f.amount >= 100) {
  10. eosio::print("Amount is larger or equal than 100\n");
  11. } else {
  12. eosio::print("Amount is smaller than 100\n");
  13. eosio::print("Increase amount by 10\n");
  14. f.amount += 10;
  15. eosio::print(f);
  16. }
  17. }
  18. }
  19. }

} // extern “C”

  1. * debug.abi

{ “structs”: [{ “name”: “foo”, “base”: “”, “fields”: { “from”: “account_name”, “to”: “account_name”, “amount”: “uint64” } } ], “actions”: [{ “action_name”: “foo”, “type”: “foo” } ] }

  1. 现在我们可以部署这个合约并且调用它,假设你已经创建了`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

  1. 当你检查你的`nodeos`log的时候,就可以看到下面的信息:

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

这里,你可以清楚的看到,合约按照你的预期被执行了,并且余额是正确的。你也许会看到上面的信息两次,因为每次的transaction都会被验证,生成块和块合约。