极路由和 newifi 刷 BREED

极路由 1S

1S 有两款硬件:hc5661 和 hc5661a,刷错变砖,而且要刷的文件名有点迷惑性,意不意外?

原版 bootloader 有 DHCP 功能,自身 IP 是 192.168.2.1,比较特殊。

这里以 hc5661 为例,版本是:HC5661 - 1.4.11.21001s,破解式 root 或开发者解锁后,可以直接 mtd unlock u-boot,开不开心?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
BusyBox v1.22.1 (2018-05-10 05:32:57 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.

***********************************************************
__ __ _ _ ____ _ TM
/ / / / (_) _ __ (_) / __/ (_)
/ /_/ / / / | | /| / / / / / /_ / /
/ __ / / / | |/ |/ / / / / __/ / /
/_/ /_/ /_/ |__/|__/ /_/ /_/ /_/
http://www.hiwifi.com/ rooted by UMU
***********************************************************
root@HiUMU:~# cat /proc/mtd
dev: size erasesize name
mtd0: 00030000 00010000 "u-boot"
mtd1: 00010000 00010000 "hw_panic"
mtd2: 00010000 00010000 "Factory"
mtd3: 00020000 00010000 "oem"
mtd4: 00010000 00010000 "bdinfo"
mtd5: 00010000 00010000 "backup"
mtd6: 00f70000 00010000 "firmware"
mtd7: 00120000 00010000 "kernel"
mtd8: 00e50000 00010000 "rootfs"
mtd9: 00400000 00010000 "rootfs_data"
root@HiUMU:~# mtd unlock u-boot
Unlocking u-boot ...
root@HiUMU:~# cd /tmp
root@HiUMU:/tmp# mtd write ./breed-mt7620-hiwifi-hc5761.bin u-boot
Unlocking u-boot ...

Writing from ./breed-mt7620-hiwifi-hc5761.bin to u-boot ...
[e:0] [w0]
[e:1] [w1]

刷完,捅菊花进入的信息是:

CPU MediaTek MT7620A ver 2, eco 3
内存 128MB DDR2
Flash Winbond W25Q128 @ 24MHz (16MB)
以太网 MediaTek MT7620A built-in 5-port 10/100M switch
时钟频率 CPU: 580MHz, Bus: 193MHz
编译日期 2018-12-29 [git-135bed9]
版本 1.1 (r1266)

极路由 3

版本 HC5861 - 1.4.10.20837s,一样能开发者解锁后直接刷。这种不保护 bootloader 的 ROM,也是醉了,真香……已经过保,也不想恢复官方 ROM,直接刷 BREED,再刷 OpenWRT 18.06.5。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
BusyBox v1.22.1 (2018-03-10 04:32:13 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.

***********************************************************
__ __ _ _ ____ _ TM
/ / / / (_) _ __ (_) / __/ (_)
/ /_/ / / / | | /| / / / / / /_ / /
/ __ / / / | |/ |/ / / / / __/ / /
/_/ /_/ /_/ |__/|__/ /_/ /_/ /_/
http://www.hiwifi.com/
***********************************************************
root@Hiwifi:~# cat /proc/mtd
dev: size erasesize name
mtd0: 00030000 00010000 "u-boot"
mtd1: 00010000 00010000 "hw_panic"
mtd2: 00010000 00010000 "Factory"
mtd3: 00020000 00010000 "oem"
mtd4: 00010000 00010000 "bdinfo"
mtd5: 00010000 00010000 "backup"
mtd6: 00f70000 00010000 "firmware"
mtd7: 00120000 00010000 "kernel"
mtd8: 00e50000 00010000 "rootfs"
mtd9: 00380000 00010000 "rootfs_data"
root@Hiwifi:~# mtd unlock u-boot
Unlocking u-boot ...
root@Hiwifi:~# cd /tmp
root@Hiwifi:/tmp# mtd write breed-mt7620-hiwifi-hc5861.bin u-boot
Unlocking u-boot ...

Writing from breed-mt7620-hiwifi-hc5861.bin to u-boot ...
[e:0] [w0]
[e:1] [w1]

BREED 信息:

CPU MediaTek MT7620A ver 2, eco 6
内存 128MB DDR2
Flash Winbond W25Q128 @ 24MHz (16MB)
以太网 MediaTek MT7620A built-in 5-port 10/100M switch
时钟频率 CPU: 580MHz, Bus: 193MHz
编译日期 2018-12-29 [git-135bed9]
版本 1.1 (r1266)

嗯……这款路由器就是骗钱的。

Lenovo Y1S

这款最简单,直接捅菊花进官方恢复模式,刷这个文件:https://breed.hackpascal.net/breed-mt7620-lenovo-y1s.bin

Newifi D1

先官方 ROM 降级:xCloudOS_newifi-d1_Build20150922_v0.0.4.3500_beta_sign.bin 或更早的 xCloudOS_newifi-d1_Build_v0.0.4.2100_beta_sign.bin,自寻下载。

较老的版本可能和 Y1S 一样可以直接在恢复模式下刷,不过 UMU 忘记这样尝试。

0.0.4.3500, r33798 的版本,布局如下:

1
2
3
4
5
6
7
8
9
10
11
# cat /proc/mtd
dev: size erasesize name
mtd0: 00030000 00010000 "u-boot"
mtd1: 00010000 00010000 "u-boot-env"
mtd2: 00010000 00010000 "Factory"
mtd3: 02000000 00010000 "fullflash"
mtd4: 01fb0000 00010000 "firmware"
mtd5: 0012a926 00010000 "kernel"
mtd6: 01e656da 00010000 "rootfs"
mtd7: 01080000 00010000 "rootfs_data"
mtd8: 00020000 00010000 "panic_oops"

网上的刷机教程如下:

1
2
3
4
5
6
cd /tmp
wget https://breed.hackpascal.net/breed-mt7621-newifi-d1.bin --no-check-certificate
dd if=/dev/zero bs=1024 count=192 | tr "\000" "\377" >breed_192.bin
dd if=breed-mt7621-newifi-d1.bin of=breed_192.bin conv=notrunc
cat /tmp/breed_192.bin /dev/mtd1 /dev/mtd2 /dev/mtd4 >fullflash_with_breed.bin
mtd write fullflash_with_breed.bin fullflash

原理:u-boot 分区不能直接刷,但 fullflash 分区可以刷,fullflash 其实包含了 u-boot。

fullflash = u-boot + u-boot-env + firmware

以上命令就是组合成一个适合刷到 fullflash 的文件,而且是用 BREED 覆盖了 u-boot,然后刷到 fullflash,这样 u-boot 就被覆盖了,其它分区还是原来的内容。

值得注意的是:刷完 BREED,捅菊花的位置变了,不再是捅原来的菊花……而是天线下方那个红十字按钮,这原是用于 WPS 的。

Newifi D2

访问 http://192.168.99.1/newifi/ifiwen_hss.html 开启 SSHD,查看分区布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
BusyBox v1.24.1 (2018-02-27 16:23:44 CST) built-in shell (ash)


_______________________________________________________________
| ____ _ ____ |
| | _ \ __ _ _ __ __| | ___ _ __ __ _| __ ) _____ __ |
| | |_) / _` | '_ \ / _` |/ _ \| '__/ _` | _ \ / _ \ \/ / |
| | __/ (_| | | | | (_| | (_) | | | (_| | |_) | (_) > < |
| |_| \__,_|_| |_|\__,_|\___/|_| \__,_|____/ \___/_/\_\ |
| |
| PandoraBox SDK Platform |
| The Core of SmartRouter |
| Copyright 2013-2016 D-Team Technology Co.,Ltd.SZ |
| http://www.pandorabox.org.cn |
|______________________________________________________________|
Base on OpenWrt BARRIER BREAKER (3.2.1.7418, 2018-03-06-git-bbccda9)
#cat /proc/mtd
dev: size erasesize name
mtd0: 00010000 00010000 "u-boot-env"
mtd1: 00010000 00010000 "Factory"
mtd2: 01fb0000 00010000 "firmware"
mtd3: 00146bf9 00010000 "kernel"
mtd4: 01e49407 00010000 "rootfs"
mtd5: 00d40000 00010000 "rootfs_data"
mtd6: 00020000 00010000 "panic_oops"
mtd7: 00010000 00010000 "nvram"

这是被隐藏掉两个分区的!

高端刷法,是用一个内核模块来刷的,自寻 newifi-d2-jail-break.ko,参考文章:https://www.right.com.cn/forum/thread-365936-1-1.html

按以上链接刷好是 1.1 (r1237) 版,进 BREED 刷最新 BREED:https://breed.hackpascal.net/breed-mt7621-newifi-d2.bin

信息为:

CPU MediaTek MT7621A ver 1, eco 3
内存 512MB DDR3
Flash Winbond W25Q256 @ 48MHz (32MB)
以太网 MediaTek MT7530 Gigabit switch
时钟频率 CPU: 880MHz, DDR: 1066MHz, Bus: 293MHz, Ref: 40MHz
编译日期 2018-12-29 [git-135bed9]
版本 1.1 (r1266)

How to empty an array in JavaScript?

When I write pathfinding codes in JavaScript, I ran into this problem: how to empty the array?

Methods

  • Method 1
1
array = []
  • Method 2
1
array.length = 0
  • Method 3
1
array.splice(0, array.length)
  • Method 4
1
2
3
while (array.length > 0) {
array.pop()
}

Difference between Method 1 and the others

Method 1 only reassign a:

1
2
3
4
5
6
7
8
let a = [1, 2, 3]
let b = a

// empty a, but doesn't affect b
a = []

console.log('a =', a)
console.log('b =', b)

Method 2, 3, 4, will empty multiple variables referencing the same object:

1
2
3
4
5
6
7
8
let a = [1, 2, 3]
let b = a

// empty a and b
a.length = 0

console.log('a =', a)
console.log('b =', b)

More

See https://github.com/UMU618/js-empty-array#example

跟 UMU 一起玩 OpenWRT(入门篇15):ip-tiny 和 ip-full 的区别

起源

今天看到有网文《iptables+tproxy实现ss-redir的UDP转发的方法》说:“OpenWrt 做 UDP 转发需要的依赖是:iptables-mod-tproxy, kmod-ipt-tproxy 和 ip-full”。使用 opkg install ip 安装的默认是 ip-tiny,一般情况下都是够用的,不禁想弄明白两者有何区别。

探索

ip 命令对比测试:

  • ip-tiny
1
2
3
OBJECT := { link | address | route | rule | neigh | tunnel | maddress |
mroute | mrule | monitor | netns | macsec | token | ila |
vrf | sr }
  • ip-full
1
2
3
4
OBJECT := { link | address | addrlabel | route | rule | neigh | ntable |
tunnel | tuntap | maddress | mroute | mrule | monitor | xfrm |
netns | l2tp | fou | macsec | tcp_metrics | token | netconf | ila |
vrf | sr }

即 ip-full 多了这些对象: addrlabel | ntable | tuntap | xfrm | l2tp | fou | tcp_metrics | netconf。举个例子,只安装 ip-tiny 时,运行 ip xfrm 报错如下:

Object “xfrm” is unknown, try “ip help”.

相关知识:

xfrm is an IP framework for transforming packets (such as encrypting
their payloads). This framework is used to implement the IPsec
protocol suite (with the state object operating on the Security
Association Database, and the policy object operating on the Security
Policy Database). It is also used for the IP Payload Compression
Protocol and features of Mobile IPv6.

结论

实际上,转发普通 UDP 包,并不需要 ip-full,ip-tiny 即可。

强制 body-parser 解析无 Content-Type 请求

需求

原版 EOS 历史 API 插件将数据都保存在内存,随着历史数据越来越多,内存消耗高达 T 级以上,使得这个插件失去实用性。于是出现很多替代产品,比如把数据同步到 MongoDB,然后用 Nodejs 对接 MongoDB 来实现 API 服务。

2019 年 3 月份,MEET.ONE 实现了一个基于 MongoDB 的 EOS 历史 API 服务。劣者将去掉 MongoDB 交互部分的框架开源于 UMU618/eos-history-api-service

测试

这个 API 服务的开发者是公司另一名 Web 全栈开发,他测试通过之后,劣者用 cleos 一试,立马 bug!调试后端代码,发现 req.body 不是一个 JSON 对象。

劣者立刻用 tcpdump 抓包,发现 cleos 发出去的包并无异常,body 就是一段 JSON 数据。

交流后,发现测试工具的差异:劣者是 C++ 开发,自然而然使用 cleos 测试,而 Web 全栈开发对 cleos 比较陌生,他们会选择 postman 或者自己写测试性客户端。比如:

1
2
3
4
5
6
7
const request = require('request-json')
const client = request.createClient('http://127.0.0.1:8888/')

const json = {"id": "d5245026c757532ea3dd5b3a02a07620eb7238113d0a49cae5ebb93921a34135"};
client.post('/v1/history/get_transaction', json, function(err, res, body) {
console.log(res.statusCode, body)
})

后来劣者写了一个简易的 Web 服务器,显示请求头。

1
2
3
4
5
6
7
8
9
10
11
12
13
const http = require('http')

http.createServer(function (req, res) {
let buffer = req.method + ' ' + req.url + ' ' + req.httpVersion
console.log(buffer)
res.write(buffer + '\r\n')
for (let i = 0; i < req.rawHeaders.length; i += 2) {
buffer = req.rawHeaders[i] + ': ' + req.rawHeaders[i + 1]
console.log(buffer)
res.write(buffer + '\r\n')
}
res.end()
}).listen(8888)

经对比,cleos 发的请求不带 Content-Type。cleos 是 EOSIO 的官方工具,使用者众多,若不支持它是不合理的,后端也不能要求客户端都带上 Content-Type。

调试

检查后端代码,其对 body 的解析是用 body-parser 完成的:

1
app.use(bodyParser.json())

使用以下命令启动服务:

1
DEBUG=body-parser:* node app.js

发现有这样 2 行关键的调试信息:

1
2
body-parser:json content-type undefined +0ms
body-parser:json skip parsing +1ms

原来 body-parser 会检查 Content-Type,不符合它的预期,就不解析,于是 body 就不是 JSON 对象。

开发

暴力的解决方案

参考《Express 解析 json 格式 post 数据》后,我们这样解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 显式调用 JSON.parse 强行解析
app.use((req, res, next) => {
req.rawBody = ''
req.on('data', (chunk) => {
req.rawBody += chunk
})
req.on('end', () => {
try {
req.body = JSON.parse(req.rawBody)
} catch (err) {
req.body = null
}
next()
})
})

但劣者认为以上方案比较不优雅,JavaScript 作为一门高级语言,我们希望更多专注于业务逻辑,尽量复用现有代码,少自己写工具性代码。下面探讨使用 body-parser 的解法。

优雅的解决方案

劣者希望能告诉 body-parser 遇到不传 Content-Type 依然当它是 JSON 去解析。于是这就得去看它的代码!我们从上面关键的调试信息入手,可以很快发现:

1
2
3
4
5
6
// determine if request should be parsed
if (!shouldParse(req)) {
debug('skip parsing')
next()
return
}

而 shouldParse 是可以由传入的选项影响的:

1
2
3
4
5
6
7
8
9
10
11
12
function json (options) {
var opts = options || {}
// 省略部分无关代码
var type = opts.type || 'application/json'
// 省略部分无关代码

// create the appropriate type checking function
var shouldParse = typeof type !== 'function'
? typeChecker(type)
: type
// 省略部分无关代码
}

于是最终的解决方案是:如果不传 Content-Type,当做 application/json;但如果有传,那得传对,否则也是不理。效果上,比之前无脑地当成 application/json,稍微好一些。实现上,则更优雅。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// Force body-parser to parse data as JSON
const bodyParser = require('body-parser')
const typeis = require('type-is')

app.use(bodyParser.json({ type: function(req) {
if (undefined === req.headers['content-type']) {
// cleos POST data without content-type
return true
} else {
return Boolean(typeis(req, 'application/json'))
}
}}))

(完)

ECC Node.js

前情

上篇《基于 ECC 的私钥转为公钥的过程》讲到求椭圆曲线上的点时,用的是基于 Python 的 SAGE。为了方便 Node.js 程序员理解和实现完整流程代码,本篇用 Node.js 库实现椭圆曲线点的计算。

用 Node.js 求椭圆曲线的点

库的选型考虑 eosjs 用的 ecurveelliptic

1. ecurve

1
2
3
4
5
6
7
8
const ecurve = require('ecurve')
const BigInteger = require('bigi')

const k1 = ecurve.getCurveByName('secp256k1')
const pk = BigInteger.fromHex('d2653ff7cbb2d8ff129ac27ef5781ce68b2558c41a74af1f2ddca635cbeef07d')
const pub = k1.G.multiply(pk)
pub.affineX.toHex()
pub.affineY.isEven()

得到 x 值为 c0ded2bc1f1305fb0faac5e6c03ee3a1924234985427b6167ca569d13df435cf,y 为偶数,和 SAGE 计算结果一样。

2. elliptic

流程基本一样,所以这里给出完整转换代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @author UMU618 <umu618@hotmail.com>
* @copyright MEET.ONE 2019
* @description Use block-always-using-brace npm-coding-style.
*/


'use strict'


const bsc = require('bs58check')
const ripemd160 = require('ripemd160')
const bs = require('bs58')
const elliptic = require('elliptic')
const BN = require('bn.js')

function encodePublicKey(payload) {
const checksum = new ripemd160().update(payload).digest()

return bs.encode(Buffer.concat([
payload,
checksum.slice(0, 4)
]))
}

function privateKeyToPublicKey(privateKey) {
const buf = bsc.decode(privateKey)
if (0x80 !== buf[0]) {
throw new Error('Not a private key.')
}
const k1 = elliptic.curves.secp256k1
const pvt = new BN(buf.slice(1), 'be')
const pub = k1.g.mul(pvt)
const y = pub.getY().isEven() ? 2 : 3
return 'EOS' + encodePublicKey(Buffer.from([y].concat(pub.getX().toArray())))
}

const privateKey = '5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3'
const publicKey = privateKeyToPublicKey(privateKey)

console.log(privateKey)
console.log(publicKey)

3. 对比

elliptic 比较好用,比较快。

代码

https://github.com/UMU618/secp256k1-tools

基于 ECC 的私钥转为公钥的过程

基本知识

ECC 体系中,私钥是一个大型随机数,而公钥则是私钥乘以椭圆曲线上的基点后对应的点。

meet-one/private-to-public 是 MEET.ONE 开发的私钥转公钥工具。

EOS 支持的 EC 有两种:secp256k1(以下简称 k1)、secp256r1,下面以 k1 为例,结合 Node.js 和 Python 代码介绍转换过程。

涉及算法

  • BASE58:编解码私钥、公钥。
  • SHA-256:校验私钥,本文忽略此步。
  • ECC, secp256k1:计算公钥。
  • RIPEMD-160:校验公钥。

转换过程

1. 解码私钥

假设私钥为:5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3,这是 base58 编码,用 bs58 库解码:

1
2
3
const bs = require('bs58')

bs.decode('5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3')

得到 <Buffer 80 d2 65 3f f7 cb b2 d8 ff 12 9a c2 7e f5 78 1c e6 8b 25 58 c4 1a 74 af 1f 2d dc a6 35 cb ee f0 7d aa 08 64 4a>,其中第 1 个字节 0x80 是类型,末尾的 4 字节是校验码。

这里我们不关心校验码,也可以直接用 bs58check 解码:

1
2
3
const bsc = require('bs58check')

bsc.decode('5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3')

得到 <Buffer 80 d2 65 3f f7 cb b2 d8 ff 12 9a c2 7e f5 78 1c e6 8b 25 58 c4 1a 74 af 1f 2d dc a6 35 cb ee f0 7d>,去掉首字节后为:

d2 65 3f f7 cb b2 d8 ff 12 9a c2 7e f5 78 1c e6 8b 25 58 c4 1a 74 af 1f 2d dc a6 35 cb ee f0 7d

这是 256bit 整数的 Big endian 字节流表示,转为 16 进制整形为 0xd2653ff7cbb2d8ff129ac27ef5781ce68b2558c41a74af1f2ddca635cbeef07d,记为 pk

2. 构造椭圆曲线,求公钥的坐标

根据 SECG 规定的 k1 的参数,我们用基于 Python 的 SAGE 构造 k1 对应的椭圆曲线,然后计算 pk * G

1
2
3
4
5
6
7
8
9
a = 0
b = 7
p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
E = EllipticCurve (GF(p), [a, b])
pk = 0xd2653ff7cbb2d8ff129ac27ef5781ce68b2558c41a74af1f2ddca635cbeef07d
G = E(Gx, Gy)
pk * G

结果为:(87237761414843254130560834629777710286905276524352264071298714336416392033743 : 108016191455113306196371645921919775466659772908675410052799661524790827329728 : 1)

注:这里的 (x : y : z) 是射影坐标,一般采用笛卡尔坐标系表示,为 (x/z, y/z)。

3. 编码公钥

取 x 值:87237761414843254130560834629777710286905276524352264071298714336416392033743

16 进制为:0xc0ded2bc1f1305fb0faac5e6c03ee3a1924234985427b6167ca569d13df435cf

Big endian 字节流表示为:[0xc0, 0xde, 0xd2, 0xbc, 0x1f, 0x13, 0x05, 0xfb, 0x0f, 0xaa, 0xc5, 0xe6, 0xc0, 0x3e, 0xe3, 0xa1, 0x92, 0x42, 0x34, 0x98, 0x54, 0x27, 0xb6, 0x16, 0x7c, 0xa5, 0x69, 0xd1, 0x3d, 0xf4, 0x35, 0xcf]

由于 y 值是偶数,所以添加一个前缀 2,得到:[2, 0xc0, 0xde, 0xd2, 0xbc, 0x1f, 0x13, 0x05, 0xfb, 0x0f, 0xaa, 0xc5, 0xe6, 0xc0, 0x3e, 0xe3, 0xa1, 0x92, 0x42, 0x34, 0x98, 0x54, 0x27, 0xb6, 0x16, 0x7c, 0xa5, 0x69, 0xd1, 0x3d, 0xf4, 0x35, 0xcf]

注:若 y 为奇数,则前缀为 3。

注:为什么这么规定?只用 x 值和 y 值的奇偶性表示一个点,这叫公钥的压缩格式。因为只要有 x,就可以通过 k1 的椭圆曲线方程式 $y^2 = x^3 + 7 \mod p$ 求出 y,但此时 y 会有两个解,又由于 p 是一个大质数,必定为奇数,故两个 y 解的和 mod p 一定等于 0,即一奇一偶,所以用 x 和 y 的奇偶性标志即可代表这个点。

接着求校验码:

1
2
3
const ripemd160 = require('ripemd160')

new ripemd160().update(Buffer.from([2, 0xc0, 0xde, 0xd2, 0xbc, 0x1f, 0x13, 0x05, 0xfb, 0x0f, 0xaa, 0xc5, 0xe6, 0xc0, 0x3e, 0xe3, 0xa1, 0x92, 0x42, 0x34, 0x98, 0x54, 0x27, 0xb6, 0x16, 0x7c, 0xa5, 0x69, 0xd1, 0x3d, 0xf4, 0x35, 0xcf])).digest()

得到:<Buffer eb 05 f9 d2 c6 dd 62 f7 f2 a0 f7 61 ea 1d 8c 0b 84 4a 3b 52>,取前 4 字节,添加到末尾,得到:[2, 0xc0, 0xde, 0xd2, 0xbc, 0x1f, 0x13, 0x05, 0xfb, 0x0f, 0xaa, 0xc5, 0xe6, 0xc0, 0x3e, 0xe3, 0xa1, 0x92, 0x42, 0x34, 0x98, 0x54, 0x27, 0xb6, 0x16, 0x7c, 0xa5, 0x69, 0xd1, 0x3d, 0xf4, 0x35, 0xcf, 0xeb, 0x05, 0xf9, 0xd2]

然后,base58 编码:

1
2
3
const bs = require('bs58')

bs.encode(Buffer.from([2, 0xc0, 0xde, 0xd2, 0xbc, 0x1f, 0x13, 0x05, 0xfb, 0x0f, 0xaa, 0xc5, 0xe6, 0xc0, 0x3e, 0xe3, 0xa1, 0x92, 0x42, 0x34, 0x98, 0x54, 0x27, 0xb6, 0x16, 0x7c, 0xa5, 0x69, 0xd1, 0x3d, 0xf4, 0x35, 0xcf, 0xeb, 0x05, 0xf9, 0xd2]))

得到:6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV,加上前缀 EOS,即为公钥。

优化思维【4】Bash for 循环

常规教材

Bash 循环有三种写法。

1. 类 C 语言语法

1
2
3
4
5

for ((i = 1; i <= 10000000; ++i))
do
echo $i
done

2. for in 语法

1
2
3
4
for i in {1..10000000}
do
echo $i
done

3. 使用外部命令 seq

1
2
3
4
for i in `seq 1 10000000`
do
echo $i
done

分析

这三种方法中,性能最好的是第一种,最差的是第三种。

方法 1 是语言层面的循环语法,循环会立刻开始,而后两种,作为对比,并不会立刻开始!

方法 2 中的 {1..10000000} 会产生一个 1 到 10000000 的序列,然后再开始循环。

方法 3 中的 seq 1 10000000 是调用 seq 产生一个 1 到 10000000 的序列,然后再开始循环。涉及到外部进程调用和管道传递,所以比方法 2 更慢。

总结

  • 方法 1 写起来最麻烦,性能却是最好的。“做一件事,有很多种方式”,有时候不是好事,有对比,就有伤害……优化往往是和人性作对!

  • 语言原生的方法一般比调用外部命令好。

优化思维【3】消除没必要步骤

故事

四月底给 EOSIO / eos 提了一个优化 MongoDB 插件性能的 PR,被连续感谢好几个 Release

分析

原先的流程:fc::variant -> JSON string -> BSON,实现起来很简单,因为 JSON 是很常见的,fc::variant 和 BSON 都有到 JSON 的转化,所以实现代码很简单,一行两个函数。

但数据大时,性能问题就暴露了,这个过程先把 fc::variant 对象序列化为 JSON 字符串,然后反序列化到 BSON 对象。两步都是 CPU 密集型操作,由于 nodeos 及其插件暂时对多核支持不好,导致单核跑爆。

两个过程都要用递归实现,调用栈可能很深。调用函数可能有入栈出栈的消耗,有一种优化思路正是用 inline 减少函数的频繁调用

回归到本质,fc::variant 和 BSON 都是对象,应该直接转化才对。只是实现起来就不是一行能搞定的。先挑简单的方式实现,后期再优化,这是一种挺常规的做法。

SQLite Node.js

选型

mapbox / sqlite3: Asynchronous, non-blocking SQLite3 bindings for Node.js

安装:

1
yarn add sqlite3

常见操作

创建数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const path = require('path')
const sqlite = require('sqlite3')

const dbPath = path.join(__dirname, 'test.db')
const db = new sqlite.Database(dbPath)

const sqls = [`CREATE TABLE test(
id CHAR(64) NOT NULL PRIMARY KEY CHECK(LENGTH(id) == 64),
timeStamp INTEGER NOT NULL,
state INTEGER NOT NULL DEFAULT 0)`

, 'CREATE INDEX index_id ON txs(id)'
, 'CREATE INDEX index_timeStamp ON txs(timeStamp)'
, 'CREATE INDEX index_state ON txs(state)'
]

db.serialize(() => {
for (let sql of sqls) {
db.run(sql, (err) => {
if (err) {
console.error(err)
} else {
console.log('SQL executed.')
}
})
}
})

db.close()

插入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const items = [
{ hash: 'D141925E39814FB5256615A1A94EC82B7043D983F68423D8C149A2AE360B623C'
, ts: 1563273661316, state: 0 }
, { hash: '2E9F26F5D0A73AE5DAFC8A1C22264725972AA997A22522A906D8CD7E225096ED'
, ts: 1563273661317, state: 1 }
, { hash: '0FF5F5F5E96664939D07D94975342D71F824747EFECE1D24FDDBB3B29DD91DCB'
, ts: 1563273661318, state: 0 }
]

db.serialize(() => {
const stmt = db.prepare("INSERT INTO test VALUES (?, ?, ?)")
for (const item of items) {
stmt.run(item.hash, item.ts, item.state, (err) => {
if (err) {
console.error(err)
} else {
console.log('INSERT', item.hash)
}
})
}
stmt.finalize()
})

查询

1
2
3
4
5
6
7
8
9
10
11
db.serialize(() => {
db.each("SELECT * FROM test WHERE state=0", (err, row) => {
if (err) {
console.error('SELECT state=0 error:', err)
} else {
// do something here
}
}, (err, count) => {
// do something here
})
})

更新

1
2
3
4
5
6
7
db.run("UPDATE test SET state=1 WHERE state=0", (err) => {
if (err) {
console.error('UPDATE txs error:', err)
} else {
console.log('UPDATE state to 1')
}
})

参考

http://www.sqlitetutorial.net/sqlite-nodejs/

pm2 运维经验

安装

比较多的文章推荐用 npm 安装,但 UMU 更推荐 yarn。

理由Visual Studio Code 脑残粉跟随 Microsoft/vscode 使用 yarn。

参考 yarn 安装,其中 Ubuntu 下命令为:

1
2
3
4
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list

sudo apt-get update && sudo apt-get install --no-install-recommends yarn

使用 yarn 安装 pm2:

1
yarn global add pm2

运行

不熟悉 yarn 的话,装完一头雾水,装到哪了?用以下命令显示:

1
yarn global bin

结果参考:

  • macOS:/usr/local/bin
  • Ubuntu:/home/ubuntu/.yarn/bin

启动脚本的命令为:

1
`yarn global bin`/pm2 start my-program.js

安全建议

root 身份能不用则不用。