搭建比特币测试环境

这是我正在写的一个电子书《比特币应用开发指南》,这是第一章的搭建测试环境的一小节。


搭建比特币节点

本书使用 BitcoinCore 0.16.3 和 Ubuntu 16.04 作为示例。

下载

运行下列命令下载 BitcoinCore 0.16.3 压缩包

1
2
3
4
5
6
7
8
9
10
11
$ wget https://bitcoincore.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-x86_64-linux-gnu.tar.gz
--2019-02-22 22:12:13-- https://bitcoincore.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-x86_64-linux-gnu.tar.gz
Resolving bitcoincore.org (bitcoincore.org)... 107.191.99.5, 198.251.83.116
Connecting to bitcoincore.org (bitcoincore.org)|107.191.99.5|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 26154766 (25M) [application/octet-stream]
Saving to: ‘bitcoin-0.16.3-x86_64-linux-gnu.tar.gz’

bitcoin-0.16.3-x86_64-linux-gnu.tar 100%[==============================================>] 24.94M 75.7KB/s in 10m 0s

--2019-02-22 22:22:13 (46.5 KB/s) - ‘bitcoin-0.16.3-x86_64-linux-gnu.tar.gz’ saved [26154766/26154766]

检验下载

下载校验和文件

1
2
3
4
5
6
7
8
9
10
11
$ wget https://bitcoincore.org/bin/bitcoin-core-0.16.3/SHA256SUMS.asc
--2019-02-22 22:13:13-- https://bitcoincore.org/bin/bitcoin-core-0.16.3/SHA256SUMS.asc
Resolving bitcoincore.org (bitcoincore.org)... 198.251.83.116, 107.191.99.5
Connecting to bitcoincore.org (bitcoincore.org)|198.251.83.116|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1957 (1.9K) [application/octet-stream]
Saving to: ‘SHA256SUMS.asc’

SHA256SUMS.asc 100%[=================================================>] 1.91K --.-KB/s in 0s

2019-02-22 22:13:16 (81.0 MB/s) - ‘SHA256SUMS.asc’ saved [1957/1957]

下载完成后使用以下命令验证 SHA256SUMS.asc 文件中是否列出了发布文件的校验和:

1
2
3
$ sha256sum --ignore-missing --check SHA256SUMS.asc
bitcoin-0.16.3-x86_64-linux-gnu.tar.gz: OK
sha256sum: WARNING: 20 lines are improperly formatted

在上述命令生成的输出中,必须确保输出内容中包含“OK”。

通过运行以下命令拉取发布签名密钥的副本:

1
2
3
4
5
6
7
$ gpg --recv-keys 01EA5486DE18A882D4C2684590C8019E36C2E964
gpg: requesting key 36C2E964 from hkp server keys.gnupg.net
gpg: /home/lishude/.gnupg/trustdb.gpg: trustdb created
gpg: key 36C2E964: public key "Wladimir J. van der Laan (Bitcoin Core binary release signing key) <laanwj@gmail.com>" imported
gpg: no ultimately trusted keys found
gpg: Total number processed: 1
gpg: imported: 1 (RSA: 1)

laanwj 是当前 BitcoinCore 的维护者和版本发布者,上面命令就是导入他的公钥。

然后就可以验证校验和文件是否签名正确:

1
2
3
4
5
6
$ gpg --verify SHA256SUMS.asc
gpg: Signature made Wed 19 Sep 2018 04:23:24 AM CST using RSA key ID 36C2E964
gpg: Good signature from "Wladimir J. van der Laan (Bitcoin Core binary release signing key) <laanwj@gmail.com>"
gpg: WARNING: This key is not certified with a trusted signature!
gpg: There is no indication that the signature belongs to the owner.
Primary key fingerprint: 01EA 5486 DE18 A882 D4C2 6845 90C8 019E 36C2 E964

检查输出中必须包含 gpg: Good signature 以及完整的一行 Primary key fingerprint: 01EA 5486 DE18 A882 D4C2 6845 90C8 019E 36C2 E964

全局安装

移动文件到 /usr/local 目录,这样就可以全局运行 bitcoin 客户端,这里你可能需要管理员权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ sudo tar zxvf bitcoin-0.16.3-x86_64-linux-gnu.tar.gz --strip-components=1 -C /usr/local
bitcoin-0.16.3/bin/
bitcoin-0.16.3/bin/bitcoin-cli
bitcoin-0.16.3/bin/bitcoind
bitcoin-0.16.3/bin/bitcoin-qt
bitcoin-0.16.3/bin/bitcoin-tx
bitcoin-0.16.3/bin/test_bitcoin
bitcoin-0.16.3/include/
bitcoin-0.16.3/include/bitcoinconsensus.h
bitcoin-0.16.3/lib/
bitcoin-0.16.3/lib/libbitcoinconsensus.so
bitcoin-0.16.3/lib/libbitcoinconsensus.so.0
bitcoin-0.16.3/lib/libbitcoinconsensus.so.0.0.0
bitcoin-0.16.3/share/
bitcoin-0.16.3/share/man/
bitcoin-0.16.3/share/man/man1/
bitcoin-0.16.3/share/man/man1/bitcoin-cli.1
bitcoin-0.16.3/share/man/man1/bitcoind.1
bitcoin-0.16.3/share/man/man1/bitcoin-qt.1
bitcoin-0.16.3/share/man/man1/bitcoin-tx.1

可以看到可执行文件有下面 4 个,其中前两个是本书介绍和讲解的内容。

  • bitcoind 守护进程,包含 RPC 服务和钱包
  • bitcoin-cli 客户端,用于与守护进程进行交互
  • bitcoin-qt 可视化界面钱包
  • bitcoin-tx 交易构建测试的辅助工具

通过打印版本号来查看是否正确安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ bitcoin-cli --version
Bitcoin Core RPC client version v0.16.3
$ bitcoind --version
Bitcoin Core Daemon version v0.16.3
Copyright (C) 2009-2018 The Bitcoin Core developers

Please contribute if you find Bitcoin Core useful. Visit
<https://bitcoincore.org> for further information about the software.
The source code is available from <https://github.com/bitcoin/bitcoin>.

This is experimental software.
Distributed under the MIT software license, see the accompanying file COPYING
or <https://opensource.org/licenses/MIT>

This product includes software developed by the OpenSSL Project for use in the
OpenSSL Toolkit <https://www.openssl.org> and cryptographic software written by
Eric Young and UPnP software written by Thomas Bernard.

如果有以上类似的输出,则证明全局安装完成。

配置

主网节点

现在可以直接运行 bitcoind 直接启动守护进程,当然默认不是以守护态启动的,需要加上 bitcoind -daemon.

1
2
$ bitcoind -daemon
Bitcoind server starting

这样我们就以主网节点的方式启动,先看一下检查节点运行状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ bitcoin-cli getblockchaininfo
{
"chain": "main",
"blocks": 0,
"headers": 311998,
"bestblockhash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
"difficulty": 1,
"mediantime": 1231006505,
"verificationprogress": 2.411095771117041e-09,
"initialblockdownload": true,
"chainwork": "0000000000000000000000000000000000000000000000000000000100010001",
"size_on_disk": 293,
"pruned": false,
"softforks": []
}

如果出现类型输出则说明运行正常,这个时候我们检查家目录会自动生成一个文件夹 .bitcoin。这个文件是配置文件、本地钱包、区块数据的所在目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ls -al .bitcoin
total 800
drwxrwxr-x 5 lishude lishude 4096 Feb 22 22:45 .
drwxr-xr-x 6 lishude lishude 4096 Feb 22 22:43 ..
-rw------- 1 lishude lishude 37 Feb 22 22:43 banlist.dat
drwx------ 3 lishude lishude 4096 Feb 22 22:44 blocks
drwx------ 2 lishude lishude 4096 Feb 22 22:45 chainstate
-rw------- 1 lishude lishude 13806 Feb 22 22:45 debug.log
-rw------- 1 lishude lishude 247985 Feb 22 22:45 fee_estimates.dat
-rw------- 1 lishude lishude 0 Feb 22 22:43 .lock
-rw------- 1 lishude lishude 17 Feb 22 22:45 mempool.dat
-rw------- 1 lishude lishude 522270 Feb 22 22:45 peers.dat
drwxrwxr-x 2 lishude lishude 4096 Feb 22 22:45 wallets

如果不想每次启动时候加 -daemon 参数,那么可以创建一个配置文件,配置文件默认路径为 ~/.bitcoin/bitcoin.conf

1
2
3
4
5
daemon=1

server=1
rpcuser=test
rpcpassword=test

除了 -daemon 配置了守护态启动之外,我们还加入了其它配置:

  • server 字段设置是否开启 HTTP-JOSONRPC 功能。
  • rpcuserrpcpassword 设置 JSONRPC 用户名和密码。

这样子我们就还启动了 HTTP-JOSNRPC 功能,可以使用 HTTP Request 的形式和比特币节点进行交互。默认监听端口号是 8332,这个可以通过 rpcport 来修改。

现在就使用了配置文件的内容,不用在跟 -daemon 等命令行参数了,直接就可以运行 bitcoind。运行成功,现在以上面获取区块状态为例,我们使用 cURL 测试下 HTTP-JSONRPC 功能。

1
2
3
4
5
6
curl
--request POST \
--user test:test \
--data-binary '{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }' \
-H 'content-type: applicaiton/json;' \
http://127.0.0.1:8332/

内容除了没有格式化外,和使用 bitcoin-cli 一致。另外 JSONRPC 和其它命令的内容会在后续章节继续说明。

测试节点

我们通过配置成功运行了主网节点,为了开发方便一般我们还会配置“回归测试节点”,也就是 regtest 网络模式。

修改配置文件添加下列配置:

  • regtest 字段说明我们将使用 regtest 私有网络。
  • txindex 字段是否对所有的交易进行索引,默认只对本地钱包地址的交易进行索引,开启后相对的会增加磁盘使用。
  • discover 字段说明是否进行寻找 P2P 对等节点,开发测试我们关闭就行。
  • rpcallowip 设置允许的 RPC 访问 IP,一般都是在虚拟机运行节点,本机进行编码,设置成 0.0.0.0/0 即允许所有 IP 访问 RPC。
  • rpcport 配置 RPC 端口号,由于测试网端口号为 18443,为了编码方便,这里和主网 RPC 配置端口设置一致。

空行后为新增配置,完整配置如下:

1
2
3
4
5
6
7
8
9
10
daemon=1
server=1
rpcuser=test
rpcpassword=test

regtest=1
txindex=1
rpcallowip=0.0.0.0/0
rpcport=8332
discover=0

继续运行 bitcoind,这个时候会发现 .bitcoin 文件夹比之前多了一个 regtest 的文件夹,这个文件是本地测试时所有的数据,内容基本和上一层主网下的一样。另外删除这个文件夹之后就相当于还原测试环境,这个相当友好。

Nodejs 多线程以及线程池教程

Nodejs v11.7.0 发布后,woker_threads API 就进入基本稳定状态,不需要在运行时候加上 --experimental-worker 标识了。

worker 对于执行 CPU 密集型操作非常有用,但对 I/O 密集型操作没有多大帮助,使用内置异步 I/O 操作效率更高。

API 简介

创建一个 worker 线程很简单:

1
2
3
// index.js
const { Worker } = require("worker_threads");
const worker = new Worker(`${__dirname}/worker.js`);

这里是在相同的目录创建一个文件 worker.js,通过加载这个文件来创建线程,这个和前端 worker 基本一致,不过 node.js 也提供了一个同文件创建线程的方式:

1
2
3
4
5
6
7
8
const { Worker, isMainThread } = require("worker_threads");

if (isMainThread) {
// 主线程处理逻辑
const worker = new Worker(__filename);
} else {
// worker 线程处理逻辑
}

如果要向 worker 线程传递数据的话,可以通过事件通知的方式来传递数据:

1
worker.postMessage(value);

value 可以是合法地 JS 数据,不过这里会拷贝数据,并且拷贝数据的逻辑要遵循 HTML 结构化克隆算法。

结构化克隆算法是由 HTML5 规范定义的用于复制复杂 JavaScript 对象的算法。通过来自 Workers 的 postMessage() 或使用 IndexedDB 存储对象时在内部使用。它通过递归输入对象来构建克隆,同时保持先前访问过的引用的映射,以避免无限遍历循环。结构化克隆所不能做到的: Error 以及 Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。2、企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERROR 异常。3、对象的某些特定参数也不会被保留,RegExp 对象的 lastIndex 字段不会被保留;属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下;原形链上的属性也不会被追踪以及复制。https://developer.mozilla.org/zh-CN/docs/Web/Guide/API/DOM/The_structured_clone_algorithm

相应的主线程接收数据则可以通过 worker.on("message", ...) 接收数据:

1
2
3
worker.on("message", resp => {
// ...
});

相应的 Worker 线程收发数据和主线程基本一致,不过这里的事件对象换成了 parentPort,而这个对象可以直接从 worker 包获取

那么 worker 线程可以使用 parentPort.on("message", ...) 接收数据:

1
2
3
4
5
6
7
8
const { parentPort } = require("worker_threads");

// receive from main thread
parentPort.on("message", buf => {
// post to main thread
parentPort.postMessage(buf);
});
});

主线程除了可以直接 postMessage 传递数据,另外也可以创建 worker 的时候传递。

1
2
3
const worker = new Worker(__filename, {
workerData: "data"
});

相应的 worker 线程接受则换成了 workerData

1
const { workerData } = require("worker_threads");

最基本的 API 介绍完毕后就可以写一个简单的 demo ,从 worker 线程中获取随机数据。

index.js 暴露随机数据接口

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
const { Worker } = require("worker_threads");

const random = (size /** number */) => {
return new Promise((resolve, reject) => {
const worker = new Worker(`${__dirname}/worker.js`);

// copy
worker.postMessage(size);

worker.on("message", (resp /** { error: Error; data: Buffer } */) => {
const { data, error } = resp;
if (error) {
reject(err);
} else {
resolve(data);
}
// exit thread
worker.terminate();
});

// The 'error' event is emitted if the worker thread throws an uncaught exception.
// In that case, the worker will be terminated.
worker.on("error", (err /* Error*/) => {
reject(err);
// exit thread
worker.terminate();
});
});
};

module.exports = random;

worker.js worker 线程逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const { parentPort } = require("worker_threads");
const { randomBytes } = require("crypto");
parentPort.on("message", size => {
const response = {
error: null,
data: null
};

randomBytes(size, (err, buf) => {
if (err) {
response.err = err;
} else {
response.data = buf;
}

parentPort.postMessage(response);
});
});

test.js 测试调用

1
2
3
4
5
6
7
const random = require("./thread");
(async () => {
console.time("Worker mode");
const result = await random(32);
result.toString("hex");
console.timeEnd("Worker mode");
})().catch(console.error);

结果数据:Worker mode: 63.023ms

使用正常的模式看看耗时

1
2
3
4
5
6
7
8
9
10
const { randomBytes } = require("crypto");
console.time("Normal mode");
randomBytes(32, (err, buf) => {
if (err) {
console.error(err);
} else {
buf.toString("hex");
}
console.timeEnd("Normal mode");
});

结果数据:Normal mode: 0.383ms

耗时挺严重的,这是上面我们的例子中每次调用都会创建销毁一个线程,这个耗时最大了,而且线程间拷贝传递数据也会耗时。

所以官方推荐使用线程池模式以及使用 SharedArrayBuffer 传递数据避免拷贝。

线程池 Demo

这里提供一个我写的一个线程池 demo 并不是很完善。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
const EventEmitter = require("events");
const { Worker } = require("worker_threads");

// 线程状态
const WorkerStates = {
TODO: 0,
READY: 1,
DOING: 2,
OFF: 3
};

// 线程池状态
const WorkerPoolStates = {
TODO: 0,
READY: 1,
OFF: 2
};

class SHA256 {
constructor() {
this.size = 10;
this.workers = [];
this.state = WorkerPoolStates.TODO;
}

// 每次使用线程池都必须运行
// `await init()` 初始化
init() {
return new Promise((resolve, reject) => {
if (this.state == WorkerPoolStates.READY) {
resolve();
return;
}

let successCount = 0;
let failedCount = 0;

const event = new EventEmitter();
event.on("spawning", (isSuccess, ErrorReason) => {
if (isSuccess) {
++successCount;
} else {
++failedCount;
}

// 如果所有线程都创建失败,那么直接抛出
if (failedCount == this.size) {
this.state = WorkerPoolStates.OFF;
reject(new Error(ErrorReason));
}
// 至少一个线程创建成功即可
else if (successCount != 0 && successCount + failedCount == this.size) {
this.state = WorkerPoolStates.READY;
resolve();
}
});

for (let i = 0; i < this.size; ++i) {
const worker = new Worker(`${__dirname}/worker.js`);
this.workers.push({
state: WorkerStates.TODO,
instance: worker
});

// 当线程执行代码后悔触发 online 事件
worker.on(
"online",
(index => () => {
// 线程执行完代码后再更改线程状态
this.workers[index].state = WorkerStates.READY;
this.workers[index].instance.removeAllListeners();
event.emit("spawning", true);
})(i)
);

worker.on(
"error",
(index => ErrorReason => {
this.workers[index].state = WorkerStates.OFF;
this.workers[index].instance.removeAllListeners();
event.emit("spawning", false, ErrorReason);
})(i)
);
}
});
}

digest(data = "") {
return new Promise((resolve, reject) => {
if (this.state != WorkerPoolStates.READY) {
reject(new Error("Create threads failed or not ready yet"));
}

let curAvaWorker = null;
let curAvaWorkerIndex = 0;

// 这里有 bug
// 如果所有线程都是空闲的返回结果了
// 处理方式应该使用 promise 进行回调
for (let i = 0; i < this.size; ++i) {
const curWorker = this.workers[i];
if (curWorker.state == WorkerStates.OFF) {
recreate(i);
}
if (curAvaWorker == null && curWorker.state == WorkerStates.READY) {
curWorker.state = WorkerStates.DOING;
curAvaWorker = curWorker.instance;
curAvaWorkerIndex = i;
}
}

if (curAvaWorker == null) {
return;
}

curAvaWorker.on("message", msg => {
this.free(curAvaWorkerIndex, false);
if (!msg.error) {
resolve(msg.data);
return;
}
reject(msg.error);
});

curAvaWorker.once("error", error => {
this.free(curAvaWorkerIndex, true);
reject(error);
});

curAvaWorker.postMessage(data);
});
}

// 重新恢复线程
recreate(i) {
const worker = new Worker(`${__dirname}/worker.js`);
const deadWorker = this.workers[i];
deadWorker.state = WorkerStates.TODO;
deadWorker.instance = worker;

worker.once("online", () =>
process.nextTick(() => {
deadWorker.state = WorkerStates.READY;
worker.removeAllListeners();
})
);

worker.once("error", error => {
console.error(error);
deadWorker.state = WorkerStates.OFF;
worker.removeAllListeners();
});
}

// 切换线程状态
free(i, hasError) {
this.workers[i].status = hasError ? WorkerStates.READY : WorkerStates.OFF;
this.workers[i].instance.removeAllListeners();
}

// 停止所有线程
terminate() {
this.state = WorkerPoolStates.OFF;
return new Promise((resolve, reject) => {
for (let i = 0; i < this.size; ++i) {
this.workers[i].instance.terminate(err => {
if (!err && i == this.size) {
resolve();
} else {
reject(err);
}
});
}
});
}
}

module.exports = new SHA256();

node.js Buffer 与 BigInt 类型互转

node.js 中的 Buffer 是使用 Uint8Array 实现的,所以可以认为是 Golang 中的 []byte 类型。

不过在 Golang math 包中 []byte 和 math.BigInt 类型是可以互转的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"bytes"
"crypto/rand"
"fmt"
"math/big"
)

func main() {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
panic(err)
}
num := new(big.Int).SetBytes(buf)
fmt.Println(bytes.Equal(buf, num.Bytes()))
// Output:
// true
}

在之前的 BigInt 尝鲜 一文中,介绍了 BigInt 的用法,虽然 BigInt 和 TypedArray 没有直接互转的接口,但是提供了 BigInt(param: string) 的初始化方式,其中这里的 param 和 Math.Number 参数对应,可以传类如 “0xa” 16 进制字符串等。

1
2
3
var x = BigInt("0xa");
x.toString(16);
// 'a'

那么这里就可以通过 16 进制字符串这个中间过程做 BigInt 和 Buffer 的互转。

1
2
3
4
5
6
7
function BigIntToBuffer(bn: bigint): string {
return "0x" + bn.toString(16);
}

function BufferToBigInt(buf: Buffer): bigint {
return BigInt("0x" + buf.toString("hex"));
}

在 TypeScript 中 BigInt 和 Number 类型的表示方式一样,不是类的那种首字母大写,而是使用 bigint

而这个转换逻辑在 base58 包也有用到

https://github.com/isLishude/bs58js/blob/master/index.ts#L5-L11

1
2
3
4
5
6
let x: bigint =
hex.length === 0
? zero
: hex.startsWith("0x") || hex.startsWith("0X")
? BigInt(hex)
: BigInt("0x" + hex);

本文作为铺垫,接下来的文章会写一篇 Base58 相关的内容。

分享:我是如何为公司追回价值 8 百万人民币的虚拟货币的

几个月之前,钱包部门的同事和我说有一个 ERC20 Token 无法进行转账,转账交易发出去后是交易成功了但是余额没有变化,然后给我发了交易的 txid 让我来看看有没有解决办法。在 etherscan 上查看这个交易状态是成功的,但是并没有发出 ERC20 Transfer 事件,而是一个 RejectedPaymentFromLockedUpWallet 事件。然后我查看了合约代码,不得不说实在太长了,还有一些非英文的注释,发现了 transfer 调用时存在锁定验证,如下所示,transfer 调用时会检查限制地址收付款。

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
mapping (address => LockedInfo) public lockedWalletInfo;

struct LockedInfo {
uint timeLockUpEnd;
bool sendLock;
bool receiveLock;
}

event RejectedPaymentToLockedUpWallet (address indexed from, address indexed to, uint256 value);
event RejectedPaymentFromLockedUpWallet (address indexed from, address indexed to, uint256 value);

// token transfer
function transfer(address _to, uint256 _value) public returns (bool) {
require(_to != address(this));
// 这里验证转出是否已经被锁定
if(lockedWalletInfo[msg.sender].timeLockUpEnd > now && lockedWalletInfo[msg.sender].sendLock == true) {
emit RejectedPaymentFromLockedUpWallet(msg.sender, _to, _value);
return false;
}
// 这里验证收款是否已经被锁定
else if(lockedWalletInfo[_to].timeLockUpEnd > now && lockedWalletInfo[_to].receiveLock == true) {
emit RejectedPaymentToLockedUpWallet(msg.sender, _to, _value);
return false;
}
else {
// 验证通过后可直接调用标准 ERC20 转账
return super.transfer(_to, _value);
}
}

按照 ERC20 的规范,转账失败是不能返回 true 而且不能没有报错的。而这个合约验证锁定失败后直接发出一个调用失败的事件没有报错,这导致同事发现问题时候账户都已经被锁定快一个月了。之前这个合约是被专人审核过的,当时的 checklist 基本都是看其是否有溢出漏洞、权限漏洞等,这样的锁定漏洞没有被重视。

根据上述代码,存在一个 lockedWalletInfo 合约状态,根据名称可以看出来和锁定有一定关系,其可见性是公开的,然后我根据根据被锁定的地址来获取这个状态,返回的结果是收款没有被锁定而转账被锁定了了几千年。之后只能看看锁定调用有没有其它漏洞,代码如下所示,锁定和解锁调用只能被管理员调用。这个情况下技术上几乎是没有解决方式了,只能通过商务谈判。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
event Locked (address indexed target, uint timeLockUpEnd, bool sendLock, bool receiveLock);
event Unlocked (address indexed target);

function walletLock(address _targetWallet, uint _timeLockEnd, bool _sendLock, bool _receiveLock) onlyOwner public {
require(_targetWallet != 0x0);

// 如果设置 _sendLock 和 _receiveLock 则代表解锁
if(_sendLock == false && _receiveLock == false) {
_timeLockEnd = 0;
}

lockedWalletInfo[_targetWallet].timeLockUpEnd = _timeLockEnd;
lockedWalletInfo[_targetWallet].sendLock = _sendLock;
lockedWalletInfo[_targetWallet].receiveLock = _receiveLock;

if(_timeLockEnd > 0) {
emit Locked(_targetWallet, _timeLockEnd, _sendLock, _receiveLock);
} else {
emit Unlocked(_targetWallet);
}
}

不过过了一个晚上,我发现其代码的 transferFrom 和 approve 代码是有漏洞的,如下所示可以看到合约程序员已经偷懒了,估计是复制粘贴了其它合约的代码,这两个地方没有进行任何权限限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mapping (address => mapping (address => uint256)) internal allowed;

// ERC20 标准 transferFrom
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);

balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
emmit Transfer(_from, _to, _value);
return true;
}

// ERC20 标准 approve
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}

那么这个时候就很简单了,让被锁定的地址调用 approve 给一个新地址所有的 Token 余额,然后新地址调用 transferFrom 就可以进行转账了,就这样被锁定的几亿个 Token 拿回来了,按照当时市价差不多接近八百万人民币。

开始使用 Go Module

开始使用 Go Module

Go 的包管理方式是逐渐演进的, 最初是 monorepo 模式,所有的包都放在 GOPATH 里面,使用类似命名空间的包路径区分包,不过这种包管理显然是有问题,由于包依赖可能会引入破坏性更新,生产环境和测试环境会出现运行不一致的问题。

从 v1.5 开始开始引入 vendor 包模式,如果项目目录下有 vendor 目录,那么 go 工具链会优先使用 vendor 内的包进行编译、测试等,这之后第三方的包管理思路都是通过这种方式来实现,比如说由社区维护准官方包管理工具 dep。

不过官方并不认同这种方式,在 v1.11 中加入了 Go Module 作为官方包管理形式,就这样 dep 无奈的结束了使命。最初的 Go Module 提案的名称叫做 vgo,下面为了介绍简称为 gomod。不过在 v1.11 和 v1.12 的 Go 版本中 gomod 是不能直接使用的。可以通过 go env 命令返回值的 GOMOD 字段是否为空来判断是否已经开启了 gomod,如果没有开启,可以通过设置环境变量 export GO111MODULE=on 开启。

目前 gomod 在 Go v1.12 功能基本稳定,到下一个版本 v1.13 将默认开启,是时候开始在项目中使用 gomod 了。

Hello,World

Go 维护者 Russ Cox 写一个简单的库,用于说明 gomod 的使用,下文我将使用这个库开始介绍。

首先在个人包命名空间目录新建一个文件夹,然后直接使用 go mod init 即可。

1
2
3
mkdir $GOPATH/github.com/islishude/gomodtest
cd $GOPATH/github.com/islishude/gomodtest
go mod init

更新:现在不允许在 GOPATH 下使用 gomod,需要更改成以下命令:

1
2
3
mkdir -p ~/gopher/gomodtest
cd ~/gopher/gomodtest
go mod init github.com/islishude/gomodtest

这时可看到目录内多了 go.mod 文件,内容很简单只有两行:

1
2
3
module github.com/islishude/gomodtest

go 1.12

首行为当前的模块名称,接下来是 go 的使用版本。这两行和 npm package.jsonnameengine 字段的功能很类似。

然后新建一个 main.go 写入以下内容,这里我们引用了 rsc.io/quote 包,注意我们现在还没有下载这个包。

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"

"rsc.io/quote"
)

func main() {
fmt.Println(quote.Hello())
}

如果是默认情况下,使用 go run main.go 肯定会提示找不到这个包的错误,但是当前 gomod 模式,如果没有此依赖回先下载这个依赖。

1
2
3
4
5
6
7
8
9
10
11
$ go run main.go
go: finding rsc.io/quote v1.5.2
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: downloading rsc.io/quote v1.5.2
go: extracting rsc.io/quote v1.5.2
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
Hello, world.

因为包含 golang.org 下的包,记得设置代理。这个时候当前包目录除了 go.mod 还有一个 go.sum 的文件,这个类似 npm package-lock.json

gomod 不会在 $GOPATH/src 目录下保存 rsc.io 包的源码,而是包源码和链接库保存在 $GOPATH/pkg/mod 目录下。

1
2
$ ls $GOPATH/pkg/mod
cache golang.org rsc.io

除了 go run 命令以外,go buildgo test 等命令也能自动下载相关依赖包。

包管理命令

当然我们平常都不会直接先写代码,写上引入的依赖名称和路径,然后在 build 的时候在下载。

安装依赖

如果要想先下载依赖,那么可以直接像以前那样 go get 即可,不过 gomod 下可以跟语义化版本号,比如 go get foo@v1.2.3,也可以跟 git 的分支或 tag,比如go get foo@master,当然也可以跟 git 提交哈希,比如 go get foo@e3702bed2。需要特别注意的是,gomod 除了遵循语义化版本原则外,还遵循最小版本选择原则,也就是说如果当前版本是 v1.1.0,只会下载不超过这个最大版本号。如果使用 go get foo@master,下次在下载只会和第一次的一样,无论 master 分支是否更新了代码,如下所示,使用包含当前最新提交哈希的虚拟版本号替代直接的 master 版本号。

1
2
3
4
5
6
7
8
9
10
11
12
$ go get golang.org/x/crypto/sha3@master
go: finding golang.org/x/crypto/sha3 latest
go: finding golang.org/x/crypto latest
$ cat go.mod
module github.com/adesight/test

go 1.12

require (
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a // indirect
rsc.io/quote v1.5.2
)

如果下载所有依赖可以使用 go mod download 命令。

升级依赖

查看所有以升级依赖版本:

1
2
3
4
5
6
7
8
9
$ go list -u -m all
go: finding golang.org/x/sys latest
go: finding golang.org/x/crypto latest
github.com/adesight/test
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a [v0.0.0-20190316082340-a2f829d7f35f]
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/sampler v1.99.99

升级次级或补丁版本号:

1
go get -u rsc.io/quote

仅升级补丁版本号:

1
go get -u=patch rscio/quote

升降级版本号,可以使用比较运算符控制:

1
go get foo@'<v1.6.2'

移除依赖

当前代码中不需要了某些包,删除相关代码片段后并没有在 go.mod 文件中自动移出。

运行下面命令可以移出所有代码中不需要的包:

1
go mod tidy

如果仅仅修改 go.mod 配置文件的内容,那么可以运行 go mod edit --droprequire=path,比如要移出 golang.org/x/crypto

1
go mod edit --droprequire=golang.org/x/crypto

查看依赖包

可以直接查看 go.mod 文件,或者使用命令行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ go list -m all
github.com/adesight/test
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/sampler v1.99.99
$ go list -m -json all # json 格式输出
{
"Path": "golang.org/x/text",
"Version": "v0.3.0",
"Time": "2017-12-14T13:08:43Z",
"Indirect": true,
"Dir": "/Users/lishude/go/pkg/mod/golang.org/x/text@v0.3.0",
"GoMod": "/Users/lishude/go/pkg/mod/cache/download/golang.org/x/text/@v/v0.3.0.mod"
}
{
"Path": "rsc.io/quote",
"Version": "v1.5.2",
"Time": "2018-02-14T15:44:20Z",
"Dir": "/Users/lishude/go/pkg/mod/rsc.io/quote@v1.5.2",
"GoMod": "/Users/lishude/go/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.mod"
}

模块配置文本格式化

由于可手动修改 go.mod 文件,所以可能此文件并没有被格式化,使用下面命令进行文本格式化。

1
go mod edit -fmt

发布版本

发布包新版本和其它包管理工具基本一致,可以直接打标签,不过打标签之前需要在 go.mod 中写入相应的版本号:

1
2
3
4
5
6
7
8
9
10
$ go mod edit --module=github.com/islishude/gomodtest/v2
$ cat go.mod
module github.com/islishude/gomodtest/v2

go 1.12

require (
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a // indirect
rsc.io/quote v1.5.2
)

官方推荐将上述过程在一个新分支来避免混淆,那么类如上述例子可以创建一个 v2 分支,但这个不是强制要求的。

还有一种方式发布新版本,那就是在主线版本种加入 v2 文件夹,相应的也需要内置 go.mod 这个文件。

比如上述我们引入的 rsc.io/quote 包,其中 v3 版本是用内置文件夹,而 v2 使用的是 tag。

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
$ tree .
.
├── LICENSE
├── README.md
├── buggy
│ └── buggy_test.go
├── go.mod
├── go.sum
├── quote.go
├── quote_test.go
└── v3
├── go.mod
├── go.sum
└── quote.go
$ git tag -a
bad
v1.0.0
v1.1.0
v1.2.0
v1.2.1
v1.3.0
v1.4.0
v1.5.0
v1.5.1
v1.5.2
v1.5.3-pre1
v2.0.0
v2.0.1
v3.0.0
v3.1.0
(END)

根据上面的说明,想必你会看到一个问题,当我们升级主版本号的时候,要更改 module 名称,也就是上面所说的加上版本号,这就存在一个问题,如果我们要更新到主版本号的依赖就没有这么简单了,因为升级的依赖包路径都需要修改,这个在其它语言包管理以及 Go 第三方包管理工具都不存在的一点

如下所示,升级 rsc.io/quote 到 v3 版本。注意一点,作为例子这里包作者对函数也加上了版本,其实大部分人是不会加的。这个模式叫做 semantic import versioning,也是备受争议,大多数人认为这个没有特别大的作用,而维护者则认为这是为了 Go 下一个十年的必要条件。

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"

"rsc.io/quote/v3"
)

func main() {
fmt.Println(quote.HelloV3())
}

对于内部开发我觉得还挺好,让大家都了解,不要随意加入破坏性更新。

不过由于这个不讨喜功能,不同版本可以存在同一个包了。补充一句,对于 v0 和 v1 版本并不需要加入到 import path 内。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"

q1 "rsc.io/quote"
"rsc.io/quote/v3"
)

func main() {
fmt.Println(quote.HelloV3())
fmt.Println(q1.Hello())
}

从老项目迁移

从很多第三方的包管理工具直接迁移到 gomod 特别简单,直接运行 go mod init 即可。

如果没有使用任何第三方包管理工具,除了运行 go mod init 初始化以外,还要使用 go get ./... 下载安装所有依赖包,并更新 go.modgo.sum 文件。

默认情况下,go get 命令使用 @latest 版本控制符对所有依赖进行下载,如果想要更改某一个包的版本,可以使用 go mod edit --require 命令,比如要更新 rsc.io/quote 到 v3.1.0 版本。

1
go mod edit --require=rsc.io/quote@v3.1.0

阅读更多

上面说到 dep 这个社区维护的准官方管理工具无奈结束使命被 gomod 替代,关于这段故事,你可以阅读这篇文章:《关于 Go Module 的争吵》

更多介绍:

vscode-go 设置 lint 不强制检查对包公开类型是否注释

vscode-go 插件使用 golint 作为默认代码风格检查工具,而 golint 需要对包公开类型等需要注释说明用途,而且必须以类型名称开头。

所以经常会遇到风格检查下面的错误:

1
exported function ... should have comment or be unexported

但是对于国内的公司而言,不能只用英语注释吧,或者中英文混杂的注释,如下所示:

1
2
3
4
5
// Door is a door
type Door struct {}

// Dog is 狗的定义
type Dog struct {}

代码要定义一个们,所以我需要注释说明这是个门,想想都很滑稽。不过要具体说明用途的话,GoDoc 生成的文档对阅读代码的人而言还是很有帮助的,代码写完就是文档,以下图片是 context 包的文档,是不是特别清晰?

image

不过要关闭这个还是可以的,vscode-go 插件提供了很多个可选的 lint,只要改成 golangci-lint 就可以关闭强制 lint。

image

golangci-lint 还支持注释指令,比如说 golangci-lint 有个强制规则就是需要检查函数 error 返回值,但是有些函数只是为了实现某些接口,一直返回一个 nil error,这个时候再次检查就有些累赘了。

比如说 Hash.Write 函数为了实现 io.Writer 返回 (int, error),但是 error 永远为 nil 的。

以 sha256 为例,可以看到 error 就是默认的 nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (d *digest) Write(p []byte) (nn int, err error) {
nn = len(p)
d.len += uint64(nn)
if d.nx > 0 {
n := copy(d.x[d.nx:], p)
d.nx += n
if d.nx == chunk {
block(d, d.x[:])
d.nx = 0
}
p = p[n:]
}
if len(p) >= chunk {
n := len(p) &^ (chunk - 1)
block(d, p[:n])
p = p[n:]
}
if len(p) > 0 {
d.nx = copy(d.x[:], p)
}
return
}

这个时候就可以加上注释指令,忽略这个错误检查。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"crypto/sha256"
"fmt"
)

func main() {
h := sha256.New()
h.Write([]byte("hello world\n")) //nolint: errcheck
fmt.Printf("%x", h.Sum(nil))
}

在命令行工具中使用代理

Mac 和 Linux

类 Unix 环境下设置代理特别简单,只要在用户家目录下的 .bashrc.bash_profile (ZSH 对应 .zshrc 以及 .zsh_profile)设置 http_proxyhttps_proxy 环境变量到代理地址即可,如下所示,其中 $url 就是代理地址。

1
2
3
4
5
6
# 代理设置
url=http://127.0.0.1:1080
# 如果代理失效的话直接运行 poff 即可断开 proxy
alias poff='unset http_proxy;unset https_proxy'
# 快捷方式打开
alias pon='export http_proxy=$url; export https_proxy=$url'

这里我设置了别名,需要使用代理的时候直接运行 pon 即可打开代理,而运行 poff 则关闭代理。

或者你可以直接可以设置只要打开 Termial 就使用代理的话去掉 alias 命令即可。

1
2
3
url=http://127.0.0.1:1080
export http_proxy=$url;
export https_proxy=$url

这里我使用的是 Shadowsocks ,所以这里的 urlhttp://127.0.0.1:1080。如果你需要自定义端口可以打开 Shadowsocks 的偏好设置,http 选项卡设置 http 代理监听端口即可。

注意:Mac 下的 ShadowsocksX-NG 的 HTTP 代理服务器端口默认 1087 而 socks5 的则是 1086 端口。

在 Linux 下有个比设置 http 和 https 代理有个更简单的,可以直接设置 export ALL_PROXY=socks5://127.0.0.1:1080 就行了。

最后测试一下连接,如果出现以下情况即说明设置成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ curl -I https://google.com
HTTP/1.1 200 Connection established

HTTP/2 301
location: https://www.google.com/
content-type: text/html; charset=UTF-8
date: Thu, 28 Feb 2019 03:37:48 GMT
expires: Sat, 30 Mar 2019 03:37:48 GMT
cache-control: public, max-age=2592000
server: gws
content-length: 220
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
alt-svc: quic=":443"; ma=2592000; v="44,43,39"

Windows

如果你使用 Windows PowerShell 的话,那么需要使用下面的方式:

1
2
3
4
5
6
7
# 查看
set http_proxy
# 设置
set http_proxy=YOUR-PROXY
set https_proxy=YOUR-PROXY
# 删除
set http_proxy=

在 Windows 下使用 Git Bash 则配置方式和 Mac/Linux 一致,不过需要注意的需要在 .bash_profile 而不是 .bashrc 中进行配置。

因为如果你留意的话在每次打开 Git Bash 的时候回一闪而过 login 的字样,而 .bashrc 在每次打开命令行工具时就加载,而 .bash_profile 仅在用户登录时候加载一次,所以每次打开 GitBash 的行为就是登录用户。

VSCode

打开设置,选择 Applicaion-Proxy,注意这里保持 Proxy Strict SSL,关闭可能导致某些插件不能正常工作。

image

在 VSCode Windows 下默认使用 PowerShell 或者 CMD 命令工具,当然也可以设置成 GitBash 的。方法很简单,使用快捷键 Ctrl + Shift + p,然后输入 shell 选择默认 shell 命令即可。

Git

1
2
3
4
5
6
7
8
9
10
11
# 设置 git 的代理相关设置
git config –global http.proxy http://127.0.0.1:1080
git config –global https.proxy http://127.0.0.1:1080
# 或者使用 sock5
git config --global http.proxy socks5://127.0.0.1:1080
git config --global https.proxy socks5://127.0.0.1:1080

# 取消 git 的代理相关设置
git config –global –unset http.proxy
# 取消 git 的代理相关设置
git config –global –unset https.proxy

从随机数到比特币地址

从随机数到私钥

比特币是去中心化的,生成地址更是和传统银行中心化方式不同。

地址不需要中心化组织确认是否有重复,使用者也不需要担心会与别人地址重复,因为地址空间的数量达到 2256 个,用十进制表示的话,大约是 1077 ,而可见宇宙被估计只含有 1080 个原子。更通俗话的讲地址重复的可能性比被陨石砸到的的几率还要低。

上述我提到 256 这个数字就是比特币的使用随机熵的比特位数,简单说就是一个 256 bits 的随机数,有时候还会被称作为种子。

一个种子可以直接作为比特币的私钥,不过为了更加安全,通常会再使用 sha256 进行哈希一次后作为私钥。

读到这里,你已经看到一些一些名词,如果你了解非对称加密以及哈希算法的话,接下来就不会太难理解。

如果使用 Node.js 进行生成密钥的话,就像是如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
const crypto = require("crypto");

// 32字节即256位的种子
const seed = crypto.randomBytes(32);
// 生成私钥
const priKey = crypto
.createHash("sha256")
.update(seed)
.digest();

// 打印可读的16 进制字符串
console.log(priKey.toString("hex"));

到了这里还很简单,就只遇到一个安全的随机数生成器,还有一个 sha256 的哈希函数。其中随机数生成器都是使用密码学安全的算法或者从随机源中获取,一些自己实现的很少,这里的 crypto.randomBytes 就是从计算机内部各种事件生成的。

当然为了随机,你还可以投硬币,只要 256 次即可。还有一些网页的场景,通过随机滑动鼠标进行生成随机种子。

如果使用 Bitcoin core 的钱包的话,可使用下面 RPC 命令进行。

以下为具体命令和步骤。

bitcoind getnewaddress

这个命令会返回地址,因为私钥不能默认返回,地址为 13smYdDuR5S1zy1Ypu8C65bJmZENooC5hm,我们还需要进行一步。

dumpprivkey 13smYdDuR5S1zy1Ypu8C65bJmZENooC5hm

这样就返回了我们需要的私钥,KxmRvNP7j7ZXGsspiWJi4prmaYYCuNUEh1NCPQTSht3uSHwxyfrc

从私钥到公钥

假设上一节中我们生成了一个私钥:

1
1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD

比特币使用 secp256k1 椭圆曲线加密算法(ECC)来生成公钥。椭圆曲线加密算法比较难理解,这里就不详述,比较简单的理解是可以私钥可以看做曲线上的一个点,私钥可以根据某个特定点进行计算来获取公钥,而这个过程基本是不可逆的。

image

node.js crypto 模块已经封装了接下来需要的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const priKey = Buffer.from(
"1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD",
"hex"
);

// 获取曲线
const secp256k1 = crypto.createECDH("secp256k1");
// 设置私钥
secp256k1.setPrivateKey(priKey);
// 获取非压缩公钥
const pubKey = secp256k1.getPublicKey(undefined, "uncompressed");
console.log(pubKey.toString("hex"));

// 输出结果
// 04 +
// f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a +
// 07cf33da18bd734c600b96a72bbc4749d5141c90ec8ac328ae52ddfe2e505bdb +

对于输出结果对应椭圆曲线上的坐标点,先忽略 04 那么首先是 x 轴坐标,接着就是 y 轴坐标。

在获取公钥的时候,第二个参数传递 uncompressed 表明公钥取非压缩版本,那么 04 就代表非压缩标识符。

那么相应的还有压缩版本,把获取公钥步骤时第二个参数换成 compressed,那么输出结果就成了: 03 + f028892bad7ed57d2fb57bf33081d5cfcf6f9ed3d3d7f159c2e2fff579dc341a

数据量上少了一半。03 不是代表压缩版本的意思,而是代表非压缩压缩版本的对应数值是奇数,也就是上面非压缩公钥的最后一个字母 b 也就是 11,那么肯定是奇数,所以是 03。那如果是偶数呢?就换成 02

从公钥到地址

之后我们需要计算 hash160(pubkey),也就是 RIPEMD160(SHA256(K))

1
2
3
4
5
6
7
8
const sha256_result = crypto
.createHash("sha256")
.update(pubKey)
.digest();
const ripemed160_result = crypto
.createHash("ripemd160")
.update(sha256_result)
.digest();

到了这一步的结果 ripemed160_result 基本就代表了比特币地址了,但是并不是日常见到的地址,可以称之为地址公钥。

为了把地址公钥转换成可打印形式,我们需要对它进行 base58check 操作。

Base58 不含 Base64 中的 0(数字 0)、O(大写字母 o)、l(小写字母 L)、I(大写字母 i),以及“+”和“/”两个字符。简而言之,Base58 就是由不包括(0,O,l,I)的大小写字母和数字组成,这样使得地址长度变小而且更容易辨认手写体。

base58 其实和 base64 之类的操作相似。举个都知道的例子,十进制转换成二进制的时候是除 2 取余,逆序排列 的方式,那么 base58 也差不多,不过换成了 除 58 取余,逆序排列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const table = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const zero = 0n;
const base = 58n;

const bs58 = (buf: Buffer) => {
const hex = buf.toString("hex");
let x = hex.length === 0 ? zero : BigInt("0x" + hex);
let res = "";

while (x > zero) {
res = table[Number(x % base)] + res;
x = x / base;
}

for (let i = 0; i < hex.length; i += 2) {
if (hex[i] === "0" && hex[i + 1] === "0") {
res = "1" + res;
} else {
break;
}
}
return res;
};

这一步得到就是 base58 之后的结果。不过我们需要 base58check。而 base58check 就是在 base58 之前对数据加上校验和,防止数据出错。

1
2
3
4
5
6
// 原始数据加上 version prefix 进行两次 sha256 运算
// 取前 4 字节作为校验和
checksum := SHA256x2(version + pubKeyHash)[:4]
// 然后进行拼接数据后再进行 base58 就是比特币地址了
payload := version + pubKeyHash + checksum
address := bs58(payload)

就此比特币地址生成完毕,更具体的流程可参见《精通比特币》第四章《密钥和地址》。

Nginx 请求连接限制配置

通过 APT 等包管理工具安装的 nginx 默认也安装了请求限制模块 limit_req_modulelimit_conn_module ,这两个官方模块可以配置客户端请求和者连接的数量。

那请求和连接有什么区别呢?

对于 HTTP 协议来说,其建立在 TCP 之上,1.0 版本一个 HTTP 请求就会产生一个 TCP 连接,对于1.1版本来说加入了 keep-alive 头部关键字可以串行复用了 TCP 请求,在 HTTP/2(包括SPDY)中,每个并发请求都被视为一个单独的连接,完全支持多路复用 TCP 连接。

所以说一个连接必定有一个请求,多个请求可能来源同一个连接。

设置语法

设置连接限制可以使用 limit_req_zone 以及 limit_req 两个指令来设置。

请求限制

limmit_req_zone 相当于初始化一个连接限制行为。

1
2
3
4
# limit_req_zone
Syntax: limit_req_zone key zone=name:size rate=rate;
Default: —
Context: http

key 是指一个客户端的特征值,比如说 $remote_addr 指定以客户端IP来作为限制手段。

size 是指使用内存存储这些请求/连接池的大小限制,一般设置为10M即可。

rate 是指速率,比如说 1r/s 是每秒限制为一个请求。

例如:limit_req_zone $binary_remote_addr zone=azone:10m rate=1r/s;

定义好之后,就可以使用 limit_req 来对具体场景做限制,可以使用在 http,server 以及 location 区域。

1
2
3
4
# limit_req
Syntax: limit_req zone=name [burst=number] [nodelay];
Default: —
Context: http, server, location

burst 设置最多有多少个请求可以延迟到队列,如果加上nodelay就会立即丢弃其它的都会被立即丢弃并返回503状态码。

连接限制

而 limit_conn_zone 和 limit_conn 则和上述配置差不都。

limit_conn_zone 和 limit_req_zone 的区别就是没有了 rate 速率的设置,相应的连接限制数量要在 limit_conn 指令下完成。

1
2
3
4
# limit_conn_zone
Syntax: limit_conn_zone key zone=name:size;
Default: —
Context: http
1
2
3
4
# limit_conn
Syntax: limit_conn zone number;
Default: —
Context: http, server, location

例如:

1
2
3
4
5
6
7
8
9
10
limit_conn_zone $binary_remote_addr zone=test:10m;
server {
listen 80;
server_name _;

location / {
limit_conn test 1;
limit_rate 300k;
}
}

一些常用 github 协作术语

1
2
3
4
5
6
7
8
LGTM  —  looks good to me
ACK  —  acknowledgement, i.e. agreed/accepted change
NACK/NAK — negative acknowledgement, i.e. disagree with change and/or concept
RFC  —  request for comments, i.e. I think this is a good idea, lets discuss
WIP  —  work in progress, do not merge yet
AFAIK/AFAICT  —  as far as I know / can tell
IIRC  —  if I recall correctly
IANAL  — “ I am not a lawyer ”, but I smell licensing issues

ref: https://medium.freecodecamp.org/what-do-cryptic-github-comments-mean-9c1912bcc0a4