从Solidity到EOS合约开发

之前一直在以太坊上开发智能合约,最近开始转到EOS上,感受到有很多不同之处,决定整理记录下来,给其他想入门的兄弟们一些参考。

1.合约的编译部署

Solidity EOS
编译合约 solcjs --abi --bin hello.sol eosio-cpp -o hello.wasm hello.cpp --abigen
部署合约 hello=(web3.eth.contract([…])).new({…}) cleos set contract hello DIR/hello -p hello@active
调用合约 hello.hi.sendTransaction(…) cleos push action hello hi ‘[“bob”]’ -p bob@active

以太坊智能合约一般采用Solidity编写,然后通过solc编译成EVM字节码和对应的ABI。不过我相信大多数人都不是直接通过命令编译合约的,因为有remix或者truffle这些非常方便的工具和框架。

EOS智能合约是用C++编写的,然后编译成WASM字节码和对应的ABI。编译工具叫eosio-cpp,如果要生成ABI需要加上–abigen选项。

再谈谈部署,以太坊中有外部账户和合约账户的概念,合约账户的地址是部署的时候自动生成的,并且合约一旦部署就永远无法更改。EOS则不区分这两类账户,任何一个账户都可以部署合约,并且合约可以随时更新。EOS上的账户名是一个12位长度的字符串,只能包含a~z和1~5这31种字符,因为底层其实是通过一个uint64_t的类型存储的,每5位代表一个字符。和以太坊消耗gas类似,EOS上部署合约需要消耗RAM,RAM是需要花钱买的。

具体到部署过程,以太坊是在geth控制台中根据ABI创建contract对象,然后调用new()方法发起合约创建交易(参数包含字节码)。EOS上则是通过客户端工具cleos的set contract命令,指定字节码和ABI所在的目录即可。

最后说说合约调用,在以太坊中有两种调用方式:call方式只是本地执行,不产生交易也不消耗gas,sendTransaction()方式则会产生交易并消耗gas,交易被矿工执行并打包进区块,同时修改账户状态。在EOS中交易是免费的,因此没有call方式,所有调用都会发送交易(被称为action)。交易不会消耗EOS,但是需要消耗CPU,CPU是通过抵押EOS获得的,消耗掉的部分在24小时后会自动恢复,通过这种方式来限制用户每天能够发起调用的次数。

2.合约代码结构

作为对比,我们在以太坊和EOS分别写一个hello合约,看看有什么异同。

Solidity合约代码:

pragma solidity ^0.5.0;
  
contract Hello {
    address owner;
    event hello(address);

    constructor() public {
        owner = msg.sender;
    }

    function hi(address user) public {
        require(msg.sender == user);
        emit hello(user);
    }
}

EOS合约代码:

#include 
#include 

using namespace eosio;

class hello : public contract {
  public:
      using contract::contract;

      [[eosio::action]]
      void hi( name user ) {
         require_auth(user);
         print("Hello, ", name{user});
      }
};
EOSIO_DISPATCH(hello, (hi))

可以看到,基本还是很类似的,有一些小小的不同:

  • Solidity有contract关键字,EOS上则是继承contract类
  • EOS中需要在函数前面通过[[eosio::action]]声明这是一个action(C++11的属性语法)
  • Solidity中有全局的msg对象,可以拿到发送方的信息,EOS中没有,需要作为参数显式提供,然后通过require_auth()判断是否有权限
  • Solidity中想要打印日志需要通过发送event的方式(会存储到StateDB中),EOS中可以直接通过print打印日志(仅打印,不存储)
  • EOS合约最后一行需要包含一个EOSIO_DISPATCH的宏,声明合约中的所有action

3.数据持久化

在Solidity中所有被声明为storage类型的变量都会被持久化(写入StateDB),所有的成员变量默认都是storage类型(比如上面的owner)。

EOS中的数据持久化存储是利用Boost库提供的multi_index_table(多索引表),最多支持16个索引。从这点上看,比以太坊要强大很多,相当于给智能合约加上了数据库的支持!不过也别高兴得太早,实际提供的功能还是很有限的,主键必须是uint64_t类型,副键只能是下面这几种类型:

  • uint64_t
  • uint128_t
  • uint256_t
  • double
  • long double

根据我的实际测试,目前不支持uint256_t类型(实际上编译都通不过)。那些想用string或者checksum256作为键值的需求就别想了。。

要创建多索引表,首先需要定义表中item的结构,比如:

     struct record {
       uint64_t    primary;
       uint64_t    secondary_1;
       uint128_t   secondary_2;
       double      secondary_3;
       long double secondary_4;
       uint64_t primary_key() const { return primary; }
       uint64_t get_secondary_1() const { return secondary_1; }
       uint128_t get_secondary_2() const { return secondary_2; }
       double get_secondary_3() const { return secondary_3; }
       long double get_secondary_4() const { return secondary_4; }
     };

然后声明表类型,通过indexed_by关键字声明副键,并指定取键值的函数:

typedef multi_index<"mytable"_n, record,
 indexed_by<"bysecondary1"_n, const_mem_fun>,
 indexed_by<"bysecondary2"_n, const_mem_fun>,
 indexed_by<"bysecondary3"_n, const_mem_fun>,
 indexed_by<"bysecondary4"_n, const_mem_fun>
> mytable;

往表中插入或者修改一条记录是需要消耗RAM的,因此需要指定payer,也就是由谁付费。比如下面的代码就会往表中插入一条数据,第一个参数是付费人,第二个参数是一个Lambda表达式(C++11语法),也就是匿名函数,为表中的记录赋值:

mytable table(_code, _code.value); // 指定合约的code & scope
addresses.emplace(payer, [&]( auto& row ) {
  row.primary = primary;
  row.secondary_1 = secondary_1;
  row.secondary_2 = secondary_2;
  row.secondary_3 = secondary_3;
  row.secondary_4 = secondary_4;
});

修改表中的内容也是类似的,但是我们要先找到需要修改的那条记录对应的迭代器:

auto iterator = addresses.find(key);
if( iterator != addresses.end() ) {
  table.modify(iterator, payer, [&]( auto& row ) {
    row.primary = primary;
    row.secondary_1 = secondary_1;
    row.secondary_2 = secondary_2;
    row.secondary_3 = secondary_3;
    row.secondary_4 = secondary_4;
  });
}

删除数据不需要付费,实际上还会把RAM返还给你:

auto iterator = addresses.find(key);
if ( iterator != addresses.end() ) {
  table.erase(key);
}

4.合约调用合约

以太坊中合约调用合约可以call、callcode、delegatecall这几种方式,之前的文章里也有讲到过。

EOS中是通过action调用合约的,可以指定发起人,但是必须持有发起人的eosio.code权限。举个例子:如果合约contract11111想以用户user11111111的身份发起对合约hello1111111的调用:

action(
  permission_level{"user11111111"_n, "active"_n}, //permission
  "hello1111111"_n, //code
  "hi"_n,           //action
  std::make_tuple("user11111111"_n) //data
).send();

那么,user11111111必须通过下面的命令把eosio.code的权限授予contract11111,否则执行会失败:

cleos set account permission \
user11111111 active \
'{"threshold": 1, "keys":[{"key":"EOS53GFkSuJx5NdM9XjNQjkVrzj37e2DrfZgFZFuHrzLAKW4Xk9br", "weight":1}] , "accounts":[{"permission":{"actor":"contract11111","permission":"eosio.code"},"weight":1}], "waits":[] }' \
-p user11111111@owner

当然,这样是非常危险的,这意味着合约可以以你的身份做任何事情,包括把你的EOS转走。。。所以上面只是一个示例,实际情况下都是以自己的身份调用合约的(通过get_self())。

EOS中的action有两种使用方式:

  • inline action
  • deferred action

inline action虽然翻译成“内联”,但是跟C++的内联函数完全不同,它是异步执行的。举个例子:

print("begin");
action(
  permission_level{get_self(), "active"_n}, //permission
  "hello1111111"_n, //code
  "hi"_n,           //action
  std::make_tuple("user11111111"_n) //data
).send();
print("end");

最终你会看到下面的打印:

begin
end
Hello, user11111111

需要注意的是,inline action和原有的action是在同一个交易中执行的,一旦inline action执行失败,将会导致整个交易的回滚。之前很多菠菜游戏的“回滚攻击”就是利用了这个原理:首先调用开奖合约,在这之后再发起一个inline action来检测自己的余额有没有发生变化,如果没有变化说明没中奖,这时就可以通过eosio_assert()让这个inline action执行失败,从而导致整个交易的回滚。交易一旦回滚,下注的钱也会被原路退回,黑客就不会有任何损失。

这时,你就需要用到deferred action了。所谓deferred action,就是延迟执行的action,需要指定一个延迟时间。另外,deferred action跟原有action不在同一个交易中,即使执行失败对原来的交易也不会产生任何影响。那么怎么在合约里创建deferred action呢?参考下面代码:

transaction tx{};
tx.actions.emplace_back(
  action(
    permission_level{get_self(), "active"_n},
    "hello1111111"_n,
    "hi"_n,
    std::make_tuple("user11111111"_n)
  )
);
tx.delay_sec = 1;
tx.send(now(), get_self());

今天就讲到这里,后面会通过一个具体的例子实战演练一下,敬请期待~

更多文章欢迎关注“鑫鑫点灯”专栏:https://blog.csdn.net/turkeycock
或关注飞久微信公众号:
从Solidity到EOS合约开发_第1张图片

你可能感兴趣的:(EOS)