诗盗·草

《#诗盗#·草》:心怀蒙古大草原,脚踩都市小草团。心旷神怡思飞扬,每天冒死来上班。

注解

没错,“小草青青,脚下留情”这是反人类的标语,人类自古就喜欢踩草地。

可是人人都踩,草地就坏了鸭!草草这么可爱你怎么可以踩?

草,之所以被踩坏,是因为很多人焦虑总是走同一条最短路线。而稣不走寻常路,每天走不同曲线,踩不同的位置。草草这么口爱,稣就是要踩,只要不被人捅死就可以愉快地去上班。

程序员鼓励师系列:EOSIO 智能合约开发从入门到入定

作者: UMU @ MEET.ONE 实验室

常规入门流程

经典三步:

  1. 了解区块链基本概念,了解 EOS 基本情况;

  2. 官方开发者文档

  3. 开始愉快地写代码。

但是,有个很大的问题:开发语言居然是 C++!所以,鼓励师出场了……

这不是 C++,这不是 C++,这真是不是 C++

不信?我们就来试一试:

1
2
3
error: cannot use 'try' with exceptions disabled
try {
^

智能合约的编译目标是 WASM 文件,最终要在 WASM 的 VM 里运行,比如 wabt,这和常规情况下使用原生 C++ 开发可执行程序、静态库、动态库等,有很大不同。

受限部分包括:

  • 语言特性。比如上面举例的 try。

  • 可调用外部函数。比如 CRT 的 rand 函数,再比如您想用 socket 自由通信……没门。

  • 内存访问。这个比较难解释,后面再说。

如果您学过 Golang、Python、nodejs、Java 或其它相近语言,转到智能合约开发,可以说是轻而易举。理由如下:

  1. 智能合约关注业务逻辑,和大部分脚本语言类似。

  2. 智能合约有很强的约束范围,API 很有限,不会要求记忆大量知识。举个例子,它可以使用 boost,但也只是子集,无法使用完全的 boost。

  3. 智能合约有很强的套路,代码是满满的既定格式。熟悉 Hello world,会用基本的命令行工具进行测试,最多只需要 2 天,就会发出“原来这么简单”的感叹。

【高级话题】关于 WASM

1. 多语言支持

如果您学过 Golang、Rust 可能会注意到,它们可以编译成 WASM 文件。比如 Golang 的编译命令为:

1
GOOS=js GOARCH=wasm go build -o hello.wasm

但是找个 Hello world 编译一下,您可能会哭,产生的 WASM 文件有 2.3MB,就获得一个打印信息……(EOS 基本概念:RAM 挺贵的。)

虽然现状是 C++ 一枝独秀,但未来可能会有人开发专门的编译器支持 Golang、Rust 等语言开发 EOS 智能合约。

2. WASM 逆向

VSCode 安装插件后可以直接打开 WASM 文件,显示 WAST 代码,比如我们随便打开一个 hello.wasm,滚动到末尾,可能会看到以下两行:

1
2
(data (i32.const 8192) "read\00")
(data (i32.const 8197) "get\00malloc_from_freed was designed to only be called after _heap was completely allocated\00"))

下面我们写个 C++ 代码:

1
2
const char* p = reinterpret_cast<const char*>(8192);
eosio::print(p);

以上代码,打印出 read。如果把 8192 改为 8197,则打印 get;改为 8201,打印 malloc_from_freed was designed to only be called after _heap was completely allocated

这个例子可能吓倒大家,特别交代下,一般开发中,较难遇到逆向……只是想说明 WASM 的内存管理和常规 C++ 开发的可执行程序是不同的,后者把指针指向 8192,是 Process Working Set 的地址,通常来说去读这么低的地址,后果极可能是读异常,挂掉。

划重点:虽然你用 C++ 写代码,但编译后是 WASM 二进制编码,运行时使用 VM,受控性很强,降低了开发难度,也杜绝很多安全问题。

3. 性能问题

为了讨好 Python 程序员,下面用 Python 来写个开平方运算,有这样的:

1
2
3
import math

print(math.sqrt(2.0))

也有这样的:

1
2
3
import numpy

print(numpy.sqrt(2.0))

他们有个共同点——很快……相对 C++ 写的!!有点难以理解?

Python 的 sqrt 函数,其实都是用 C 语言实现的,最终都是调用解释器里的本地代码,速度很快。

原生 C++ 写的本地程序,几乎肯定是比 Python 快的,但我们前面说过:智能合约的 C++ 不是常规的 C++,当它被编译成 WASM 后,我们去看 WAST 代码,会发现 sqrt 的实现整个被塞进 WASM 里,它最终要用 VM 来执行,当然没有 Python 解释器快了!

4. 设计原则

打开任意 WASM 文件,可以看到里面很多 (import 开头的行,这些都是原生 C++ 实现的 API,它们的执行速度就是本地代码的速度,对应官网 API 文档里的 API。

有前面的性能问题,我们不禁要问 EOS 为什么不多做点 API 来提高性能?这是因为维护少量 API 代价比较可控,数量一多就有版本问题,各节点可能因为版本不同步而无法达成共识。

另外,目前的 wabt 功能强大,性能也过得去,对于 sqrt 此类可能并不常用的数学函数,即使用原生 C++ 实现了,性能提升带来的好处,也无法平衡多版本可能带来的风险。

原则上,BP 之间快速达成共识,提升 TPS 才是更值得做的。

EOSIO MongoDB 插件系列:管理技巧

作者: UMU @ MEET.ONE 实验室

总结同步主网数据到 MongoDB 时的常用操作,大部分以 transaction_traces 表为例。

1. nodeos 配置优化

1
2
3
4
5
read-mode = read-only
validation-mode = light

mongodb-queue-size = 2048
abi-serializer-max-time-ms = 15000

2. 首次启动 nodeos

https://eosnode.tools/blocks 下载最新 blocks data,以减少网络同步时间。

首次启动,应使用 --replay-blockchain 参数。

3. 守护 nodeos 进程

目前 nodeos 1.5+ 版本如果优雅退出,下次启动可以无需痛苦的 replay 过程,所以可以监控 nodeos 进程,如果退出就调用。

启动脚本 /home/ubuntu/shell/continue.sh:

1
nohup /usr/local/eosio/bin/nodeos --config-dir /home/ubuntu/nodeos/config-dir --data-dir /home/ubuntu/nodeos/data-dir > /home/ubuntu/shell/`date +%Y-%m-%d_%H-%M`.log 2>&1 &

守护脚本 /home/ubuntu/shell/autorun.sh:

1
ps -C nodeos || /home/ubuntu/shell/continue.sh

添加到计划任务,运行 sudo crontab -e,输入下行并保存、退出:

1
* * * * * /home/ubuntu/shell/autorun.sh

4. 读写用户分离

nodeos 需要写入,使用有写入权限的 EOS 用户,其余情况使用只读权限的 EOSReader 用户,数据库安装之后就尽量不使用管理员用户。

1
2
3
use EOS
db.createUser({"user" : "EOS", "pwd" : "Password", "roles" : [{role : "readWrite", "db" : "EOS"},"dbOwner"]});
db.createUser({"user" : "EOSReader", "pwd" : "password", "roles" : [{role : "read", "db" : "EOS"}]});

5. 查询同步进度

1
2
use EOS
db.transaction_traces.find({}, {"block_num" : 1, "block_time" : 1}, -1).sort({$natural:-1}).pretty()

6. 修复丢失数据

参考《EOSIO MongoDB 插件系列:从 log 中找回丢

EOSIO MongoDB 插件系列:从 log 中找回丢失的插入记录

作者: UMU @ MEET.ONE 实验室

问题

当 MongoDB 因不可抗力故障,nodeos 重启后会丢失上次故障时正在插入的记录。

解决

nodeos 会将插入语句连同错误原因等信息一起写入 log,这给了我们手动修复丢失的机会。下面以 transaction_traces 为例,介绍修复流程。

1. 找出所有失败记录

1
grep 'mongo exception, trans_traces insert:' *.log > lost.txt

2. 从 log 生成 mongo script

1
2
3
4
5
6
echo 'print("++++");
var eos = db.getSiblingDB("EOS");' > lost.js

cat lost.txt | sed -n 's/.*, trans_traces insert: \(.*\), line 920, code.*/eos.transaction_traces.insert(\1)/p' >> lost.js

echo 'print("----");' >> lost.js

3. 导入 MongoDB

1
nohup mongo mongodb://$user:$password@127.0.0.1:$port/admin lost.js > lost.log

纯数字 EOS 账号

作者: UMU @ MEET.ONE 实验室

问题

2018 年最后一个工作日,智能合约开发小哥哥遇到一个奇怪的现象:某个账号给我们合约转账,在 EOS 浏览器上都可以找到记录,但用 cleos get table 在合约的 RAM 里找却找不到!

现象

了解具体情况后,注意到两个事实:

  1. 只有某个特定账号有问题,其它账号很正常。

  2. 那个有问题的账号是纯数字的。

这是 EOS 账号解析的问题,UMU 曾经给 EOS 提过一个相关的 issue:get_table_by_scope parameter lower_bound is NOT properly converted, cause enumeration dead loop #5824,里面有问题产生原因和解决方案。

分析

eosio::name 本质是一个 uint64_t 数字的 base32 编码,编码形式是为了方便人类记忆。举个例子:

shengxiaokai 本质上是 14075216089888066784 (0xc3553675c6a40ce0)

cleos get table 在解析账号时,兼容了这两种表达形式,所以 14075216089888066784shengxiaokai 是等价的。

但本身是纯数字的账号可就有歧义了,比如 313131313131 是当成一个 uint64_t 解释,还是当成 base32?很不巧,解析代码是优先当成 uint64_t 解释的。

解决方案

给纯数字 EOS 账号加上个空格后缀,比如 111122223333 可以改为 "111122223333 "

EOSIO 找出谁为我质押

作者: UMU @ MEET.ONE 实验室

问题

很多 EOS 浏览器都只能显示别人给我抵押了多少 EOS,但不能看到是哪个账号帮我抵押的。

分析

1. 看抵押的实现代码

eosio.contracts/eosio.system/src/delegate_bandwidth.cppdelegatebw 函数开始分析。

它调用了 changebw,其中的查表操作是这样的:

1
2
del_bandwidth_table     del_tbl( _self, from.value );
auto itr = del_tbl.find( receiver.value );

scope 是 from,而 from 就是要求的未知项,直接粉碎我们用这路线继续求解的可能。

2. 找交易记录

MEET.ONE 之前发布过几篇关于 MongoDB 插件的文章,这些积累为我们继续求解提供了很大便利。

直接在 Mongo Shell 里尝试:

1
2
use EOS
db.transaction_traces.findOne({"action_traces.act.account" : "eosio", "action_traces.act.name" : "delegatebw", "action_traces.act.data.receiver" : "shengxiaokai"})

执行之后,找到一条 trx_id 为 9bd50c0fd6f0e1d0ed4c6f5c6f873a33976955ff9dae2ac3eb16cb7e9a44d106 的交易记录,显示 1freeaccountshengxiaokai 抵押:

1
2
3
4
5
6
7
{
"from" : "1freeaccount",
"receiver" : "shengxiaokai",
"stake_net_quantity" : "0.0000 EOS",
"stake_cpu_quantity" : "0.5000 EOS",
"transfer" : 0
}