之前一直在以太坊上开发智能合约,最近开始转到EOS上,感受到有很多不同之处,决定整理记录下来,给其他想入门的兄弟们一些参考。
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小时后会自动恢复,通过这种方式来限制用户每天能够发起调用的次数。
作为对比,我们在以太坊和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中所有被声明为storage类型的变量都会被持久化(写入StateDB),所有的成员变量默认都是storage类型(比如上面的owner)。
EOS中的数据持久化存储是利用Boost库提供的multi_index_table(多索引表),最多支持16个索引。从这点上看,比以太坊要强大很多,相当于给智能合约加上了数据库的支持!不过也别高兴得太早,实际提供的功能还是很有限的,主键必须是uint64_t类型,副键只能是下面这几种类型:
根据我的实际测试,目前不支持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);
}
以太坊中合约调用合约可以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虽然翻译成“内联”,但是跟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
或关注飞久微信公众号: