EOSIO 数据同步

作者: UMU @ MEET.ONE 实验

最近三个月尝试各种方案把 EOSIO 链上交易数据同步到数据库中,踩了不少坑,现总结一下经验。

1. 使用 MongoDB 插件同步 transaction_traces 和 action_traces

原始需求是要链上交易数据,所以先是把 transaction_traces 和 action_traces 都同步。

踩坑:无奈地发现速度跟不上,服务器的时间成本比较高,只能舍弃。

2. 使用 MongoDB 插件同步 transaction_traces

研究插件代码,发现 action_traces 是从 transaction_traces 拆出来的,是重复的,所以把 action_traces 去掉,这次成功追上主网区块高度。

踩坑:transaction_traces 在查询 actions 时不太方便,因为 actions 是放到 transaction_traces 内部的一个数组,要查询具体一个 action 就得分两步走,先在 MongoDB 查询出某个 trx,然后再 actions 数组里遍历。数据库使用端的工程师觉得这样太麻烦,无奈继续放弃这到手的肥肉。

3. 使用 MongoDB 插件同步 action_traces

明确 action_traces 才是客户端想要的后,就只同步 action_traces。

踩坑:action_traces 条数比 transaction_traces 多了三倍以上,又出现追不上区块的问题……

4. 使用 MongoDB 插件同步 action_traces,但只要 transfer 数据

客户端最关心的是 transfer 数据,既然跟不上,就舍弃其它数据。

踩坑:舍弃的数据后期不好补。

5. 考虑 kafka_plugin

有人说 kafka_plugin 同步数据很快,可以追上主网区块。

踩坑:从 kafka_plugin 代码就能看出它没有处理 action_traces,如果还要去后端再拿出来处理,再插入到 MongoDB 里,那开发成本和服务器成本一样又上去了。

6. 从 2019 年的区块开始同步

从 35058781 块开始,插入数据库。之前的区块(1 - 35058780)处理后,仅插入数据量相对很小的 account_controls、accounts、pub_keys,其它数据量大的表不插入。

做这个尝试很重要,因为发现重要的线索:

  • 大约在 2200 万块开始,nodeos 的处理速度下降很多,平均每块要 2-3ms,所以同步慢的原因在于跑 nodeos 的服务器的性能。

  • 在 1 开始的早期区块阶段,同时插入 transaction_traces 和 action_traces,并不能看出比只插入 action_traces 慢,说明 MongoDB 端压力很小。

7. 结论

  • 要追上主网区块高度,nodeos 机器性能要好,2.5GHz CPU 不够用。之前听闻 BOS 要求 BP 使用 4.0GHz 的 CPU,现在看起来也是有道理的……以性能成本换取时间。

  • MongoDB 集群,按之前的文章《为 EOSIO MongoDB 插件搭建高可用集群》的配置,插入阶段毫无压力。

  • 插件代码有些问题,需要优化,最明显的就是 queue_size 的设计不合理,打印处理时间太长的提示也不合理。

    • queue 函数是个模板,所有的 queue 都调用它,但 max_queue_size 和 queue_sleep_time 缺只有一份,这可能导致一个 queue 导致的 queue_sleep_time 加大,影响到其它 queue,即整体的休眠时间会无用地加大。

    • 打印处理时间没有按照 max_queue_size 变化,当 max_queue_size 设置大时,打印就很频繁,带来延迟。

  • 建议:一定要注意成本问题!如果查询量不是很巨大,找点友商的数据源用用就好了,自己搭建的成本好高……但如果查询量太大,或者友商卖太贵,以上经验就是很好的参考了。

Nodejs 实现监控告警

作者: UMU @ MEET.ONE 实验室

钉钉

  1. 选择要接受通知的群,群设置 - 群机器人 - 添加机器人;

  2. 复制 webhook URL,记为 webhook_url;

  3. 发送通知的代码:

1
const fetch = require('node-fetch')

fetch(webhook_url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "msgtype": "text",
    "text": {"content": text}
  })
}).then(function(res) {
  if (res.ok) {
    console.log('Dingtalk message sent!')
  } else {
   console.log('status = ' + res.status)
  }
})

Telegram

  1. 添加 @BotFather,发送 /newbot 命令,随提示逐步建立一个机器人,得到这个机器人的 token,记为 bot_token。

  2. 选择要接受通知的 Group 或 Channel,按以下任一方式取得 chat_id:

(1) 转发 Group 或 Channel 内的消息到 @getidsbot

(2) 通过 Web 版查看 Group 或 Channel 的 URL 中,p 参数 的值。

  • 如果是 Group,chat_id 为把 g 前缀替换为负号的负整数。比如 p=g268787210,则 chat_id = '-268787210'

  • 如果是 Channel,chatid 为 前部分,并把 c 前缀替换为 -100 的负整数。比如 p=c1383705039_968667419389618100,则 chat_id = '-1001383705039'

  1. 发送通知的代码:
1
2
3
4
const TelegramBot = require('node-telegram-bot-api')
const bot = new TelegramBot(bot_token, {polling: false})

bot.sendMessage(chat_id, text)

程序员鼓励师系列: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

多语言支持

如果您学过 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 智能合约。

WASM 逆向

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

1
(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,受控性很强,降低了开发难度,也杜绝很多安全问题。

性能问题

为了讨好 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 解释器快了!

设计原则

打开任意 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
}

EOSIO 的连续通胀率 4.879% 是怎么算出来的?

作者: UMU @ MEET.ONE 实验室

问题

eosio.contracts/eosio.system/src/producer_pay.cpp 中有这样一行代码:

1
const double   continuous_rate       = 0.04879;          // 5% annual rate

搜索一下,会得到这样的解释:

EOS是连续增发的模式,连续通胀率是 4.879%,年度通胀是 5%;

运用微积分的知识,可以推导出来,假设是增发的次数是无限多次,那么,连续通胀的情景下,所设置的连续通胀率就是 4.879%。

然而,并没有解释具体算法……

求解

  • 假设通胀率是“每日结算”的,记为 daily_rate,则:
1
2
3
(1 + daily_rate / 365) ^ 365 = 1 + annual_rate

注:这里的 ^ 表示幂,不是 XOR 运算。

那么计算 daily_rate 的公式为:

1
2
3
[365TH_ROOT(1 + annual_rate) - 1] * 365

注:365TH_ROOT 是开 365 次方

把 5% 带入,计算结果是:0.048793425246406,这个数值已经和 0.04879 基本一样了。

  • 但是“每日结算”并不够,接下来推到时时刻刻都在结算的情况。

问题本质:已知 annual_rate、(1 + continuous_rate / N) ^ N = 1 + annual_rate,求 continuous_rate 在 N 为无穷大时的解。

复习一下大学数学,马上就会发现 lim N->∞ (1 + x / N) ^ N 就是 e ^ x 的定义,所以:

1
continuous_rate = ln(1 + annual_rate)

把 5% 代入 annual_rate,continuous_rate = 0.048790164169432

参考

#1537 DAWN-651 ⁃ setting correct per-block “continuous inflation” so annual inflation is 5%

EOSIO 资源分配机制

本文为 MEET.ONE 发布的《EOSIO CPU 资源分配原理分析》 的个人版本,稣是其主要编辑之一。

1. 相关概念

您没看错,以下要介绍的几个概念,都是金融词汇。稣的柚子系列文章,又名《程序员转行做金融》,并兼职卖柚子……

  • 存款准备金率

    Deposit-reserve Ratio。存款准备金是指金融机构为保证客户提取存款和资金清算需要而准备的,是缴存在中央银行的存款,中央银行要求的存款准备金占其存款总额的比例就是存款准备金率。

    经常能听到的“降准不降息,等于装牛逼”里面的“降准”全称就是“降低法定存款准备金率”。

    举个外星的例子:如果存款准备金率为 1‰,就意味着金融机构每吸收 1000 元存款,要向央行缴存 1 元的存款准备金,用于发放贷款的资金为 999 元。

  • 挤兑

    Run on Banks。在银行券流通的条件下,银行券持有者争相到发行银行券的银行要求兑现贵金属货币的现象。当一家银行的信用发生动摇,准备金不足,银行券兑现发生困难,就会发生挤兑。挤兑可能使一家银行倒闭,甚至波及整个银行业。现在一般是指存款户集中地大量地到银行提取现钞。

  • 涨跌停板制度

    这个不解释了……就问一句:连续 5 天跌停和一天暴跌 41%,您喜欢那种?如果您喜欢没有板的,再多问一句:一天暴跌 99.9% 您觉得怎么样?

  • 峰谷电价

    又称“分时电价”,也很好理解。再举个类似的例子:下班高峰期打的,不加价基本打不到,因为别人加价优先接单。

2. 资源分配基本原则

EOS 账户可用资源与其抵押给资源的柚子数量的关系是:可用资源 = 总可用资源 * 本用户抵押数量 / 全体用户抵押数量。从这个关系上看,存在两个风险:

  • 大户挤兑散民;
  • 占着茅坑不拉屎。

第一个问题,用成本来解决,要通过加仓把别人的比例减少,按目前 3-4 亿的抵押量来说,需要付出的代价极高。

第二个问题,则通过引入一个放大因子来解决。之所以能放大是因为,某个时间点不拉屎的确实占大多数。只要把您拉屎的时间,除以一天的时间,就可以算出您一天拉屎的占用率。相信是很小的,笑……虽然您有柚子,就有拉屎的权利,但您自己不拉,让给需要拉的也是合理的,毕竟资源利用起来才是好事。

以 CPU 为例,计算公式为:

可用 CPU 微秒数 = max_block_cpu_usage * (account_cpu_usage_average_window_ms / block_interval_ms) * staked_cpu_count / total_staked_cpu_count

其中 max_block_cpu_usage 是可配置的,当前主网配置为默认值 default_max_block_cpu_usage = 200000

所以 max_block_cpu_usage * (account_cpu_usage_average_window_ms / block_interval_ms) = 34560000000

以主网 2018-10-19 为例,CPU 总质押量为 280053493.80756617 EOS,所以每个 EOS 可用 123.40 us。注意:这个数值是没有放大过的。

3. 堵车问题

EOS 定义了两种资源使用状态:拥堵、空闲,由过去一分钟每个块的平均使用量来界定。还是用 CPU 说事:大于 max_block_cpu_usage * target_block_cpu_usage_pct 则进入拥堵。

两个状态下的可用量本来应该有 1000 倍的差距,但因为有涨跌停板保护,并不会直上直下。每一分钟,只能跌到 99/100,只能涨到 1000/999。所以从拥堵开始到绝对拥堵,有 log(0.001) / log(0.99) = 687 分钟之长;从绝对拥堵完全恢复更慢,是 log(1000) / log(1000/999) = 6904 分钟。目前的 target_block_cpu_usage_pct 已经从 10% 调整到 20%,它提高了总使用量临界值,使拥堵状态更难触碰。

可用量的变化过程是可能随时改变方向的,类似多头和空头拉锯。比如拥堵时,可用量变少,能够使用资源的用户也随之减少,使用量降到阈值以下,可用量又会开始慢慢上升。

4. 阈值的设定

max_block_cpu_usage 和 target_block_cpu_usage_pct 都是可以配置的,为什么不一次性配高点呢?主要考虑的因素是,目前各个 BP 的机器性能参差不齐,如果冒然的把这两个值调高,可能会导致节点 replay 变慢,同时对于配置低的机器来说,同步区块也会很吃力。别忘了,我们的准备金率才 1‰,属于严重超发,提高可用率,虽然会使拥堵来得晚点,但真到拥堵的那刻,爆发的能量可是更大的哦!

总之,还是稳一点好,慢慢涨经验。目前来看昨天的调整,对节点之间的同步、CPU 使用率没有太大影响。

5. 参考

EOSIO 代码

EOS资源模型

RemoteFX 能否用于物理机的远程桌面服务?

用户故事

大学时期(2002-2006 年)经常在学校机房使用远程桌面(RDP)连自己宿舍的电脑,当时的校园网是 100Mpbs 的,但每次一开视频,还是卡成翔……

后来慢慢发现,远程桌面看视频已经不是事儿了,甚至可以玩游戏!

近几年,云游戏的概念越来越流行,曾经用远程桌面连到开启 RemoteFX 的虚拟机上玩过街霸,发现体验很好。于是有了一个疑问:稣有一台 PC,配了块 GeForce GTX 980 Ti 显卡,能不能开启 RemoteFX,然后在烂机器远程桌面上去愉快地玩耍?

调研结论

截止目前还不能在物理机上开启远程桌面的 RemoteFX 功能。其中原因是微软的商业策略,并不是技术问题。

参考链接

Windows 10 RDP with RemoteFX