::: motto Linus Torvalds
Talking is cheap, show me the code.
废话少说,有种把你的代码亮出来。
:::
上几篇 Lotus 源码研究文章我们但是其实都没有涉及到如何真正动手修改或者添加 Lotus 功能,对于你不是很了解的东西,你想太多的是没有用了,是该动手的时候了。
这篇文章我们就从一个小功能开始,演示一下如何上手 Lotus 开发。
1. 功能需求分析
今天我们开发的一个小功能是大家平时不常用,但是关键时候有非常好用的功能。经常有矿工朋友问我:“我的 Miner 的元数据不小心被删了,而有没有备份,重新初始化矿工之后矿工 ID 又从 0 开始了,这个该怎么办?”
对于这种情况其实即使你备份了,只要在备份之后你还有继续封装新的扇区,恢复之后扇区 ID 照样接不上。如果你强行封装,那么新封装的扇区数据会把你前面封装的数据
覆盖掉,比如你 Sector Number 为 100 的扇区已经 Proving 了,这个时候你再创建一个 Sector Nuber 为 100 的扇区,那么原来的扇区就被覆盖了,这样掉算力是必然的。
那么怎么处理这种问题呢?
第一个解决解决思路很简单,miner 的这个 ID Counter 是一个自增的计数器,你每执行一次 lotus-miner sectors pledge 命令,它就会加 1。所以笨一点的办法就是用 shell 脚本写个循环,想要 ID Counter 增加到多少,
就调用多少次就行了,然后在把生成的 AddPieces 任务全部删掉,unsealed 文件也全部删除,然后再重启集群就好了。不过这种处理方式有几个副作用:
- 扇区或者任务有时候删除不掉,大量删除或者终止任务或者扇区可能会导致状态机出问题。
- 任务或者任务虽然已经删除,但是会留下很多垃圾数据在 Miner 的
datastore文件中。
第二个解决思路就是把你当前 Miner 的 ID Counter 设置为你已经上链的最大扇区 Number 就行了,然而官方目前的代码中是没有这个 API 可以让你直接把当前的 ID Counter 强行设置到某个数值。所有你得自己添加一个这样的功能。
第二种解决方案正是我们今天要讨论的内容。
首先你需要知道,所有 Lotus 相关程序启动之后,在本地都会启动一个 JSONRPC 的服务端,Lotus 的所有客户端的命令,本质上都是调用远程的 JSONRPC API 实现的。所以通常你的 Lotus daemon 或者 Miner 没有启动的时候,
你在运行 lotus/lotus-miner 命令的时候都会出现类似下面的错误:
1 | ERROR: could not get API info for FullNode: could not get api endpoint: API not running (no endpoint) |
意思是获取不到相关的 API 信息, API 服务没有运行。
所以我们如果需要新增一个客户端命令实现某个功能的话,就需要做 2 件事情:
- 增加一个该命令的 CMD 入口指令
- 为这个功能添加一个 API 和实现
2. 源码版本
本文所涉及的源码版本为:https://github.com/filecoin-project/lotus/releases/tag/v1.11.3
最后一次提交的 Commit ID 为:a0ddb10deb9f4966c2fd766543a97eae62b6ec82
3. CLI API
在我们上一篇源码研究的文章中我们就提到: lotus 所有命令的入口都在 cmd 这目录下。cmd 目录下有很多模块,比如 lotus, lotus-miner, lotus-bench 等:
1 | drwxrwxr-x 2 4096 6月 7 16:11 chain-noise/ |
这次我们添加功能是给 SectorNumber 添加 set 和 get 功能,那么显然我们需要把这个功能添加到 lotus-miner 这个模块。
lotus-miner 是一个复合的命令,它本身包含了很多个子命令:
1 | lotus-miner |
几乎每个子命令都对应者一个单独实现文件,我们看下 lotus-miner 的目录结构:
1 | -rw-rw-r-- 1 27166 10月 16 21:39 actor.go |
- actor.go: 对应
lotus-miner actor命令 - info.go: 对应
lotus-miner info命令 - init.go: 对应
lotus-miner init命令 - sectors.go: 对应
lotus-miner sectors命令 - …
咱们这次既然是给 SectorNumber 添加 API,那么显然把这个命令加到 sectors.go 是个不错的选择。
4. API 的设计与实现
这次我们打算设置并实现下面 3 个子命令:
lotus-miner sectors counter get: 获取当前 SectorNumber Counter 的值。lotus-miner sectors counter set <value>重置当前 SectorNumber Counter 的值为某个指定的值。lotus-miner sectors counter next将当前 SectorNumber Counter 的值 +1。
4.1 新增相关命令
我们首先要在
sectors.go文件中的Subcommands中增加sectorsCounter子命令:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19Subcommands: []*cli.Command{
sectorsStatusCmd,
sectorsListCmd,
sectorsRefsCmd,
sectorsUpdateCmd,
sectorsPledgeCmd,
sectorsCheckExpireCmd,
sectorsExpiredCmd,
sectorsRenewCmd,
sectorsExtendCmd,
sectorsTerminateCmd,
sectorsRemoveCmd,
sectorsMarkForUpgradeCmd,
sectorsStartSealCmd,
sectorsSealDelayCmd,
sectorsCapacityCollateralCmd,
sectorsBatching,
sectorsCounter, // 这里增加我们需要的子命令
},定义
sectorsCounter变量,我们给sectorsCounter再拆分成三个子命令实现:1
2
3
4
5
6
7
8
9var sectorsCounter = &cli.Command{
Name: "counter",
Usage: "manage sector number counter",
Subcommands: []*cli.Command{
sectorsCounterGet,
sectorsCounterSet,
sectorsCounterNext,
},
}实现
sectorsCounterGet子命令:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19var sectorsCounterGet = &cli.Command{
Name: "get",
Usage: "get the current sector number.",
Action: func(cctx *cli.Context) error {
nodeApi, closer, err := lcli.GetStorageMinerAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := lcli.ReqContext(cctx)
sectorNum, err := nodeApi.SectorCounterGet(ctx)
if err != nil {
return err
}
fmt.Println("Current sector counter number: ", sectorNum)
return nil
},
}实现
sectorsCounterSet子命令,修改 SectorNumber Counter 属于高级操作,所以需要加上--really-do-it参数: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
36var sectorsCounterSet = &cli.Command{
Name: "set",
Usage: "ADVANCED: manually set the next sector number",
ArgsUsage: "<sectorNum>",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "really-do-it",
Usage: "pass this flag if you know what you are doing",
},
},
Action: func(cctx *cli.Context) error {
if !cctx.Bool("really-do-it") {
return xerrors.Errorf("this is a command for advanced users, only use it if you are sure of what you are doing")
}
nodeApi, closer, err := lcli.GetStorageMinerAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := lcli.ReqContext(cctx)
if cctx.Args().Len() != 1 {
return xerrors.Errorf("must pass sector number")
}
id, err := strconv.ParseUint(cctx.Args().Get(0), 10, 64)
if err != nil {
return xerrors.Errorf("could not parse sector number: %w", err)
}
err = nodeApi.SectorCounterSet(ctx, abi.SectorNumber(id))
if err == nil {
fmt.Println("OK, current sector number is set to : ", id)
}
return nil
},
}实现
sectorsCounterNext子命令: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
30var sectorsCounterNext = &cli.Command{
Name: "next",
Usage: "ADVANCED: Increase the sector number by 1",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "really-do-it",
Usage: "pass this flag if you know what you are doing",
},
},
Action: func(cctx *cli.Context) error {
if !cctx.Bool("really-do-it") {
return xerrors.Errorf("this is a command for advanced users, only use it if you are sure of what you are doing")
}
nodeApi, closer, err := lcli.GetStorageMinerAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := lcli.ReqContext(cctx)
sectorNum, err := nodeApi.SectorCounterNext(ctx)
if err != nil {
return nil
}
fmt.Println("Set sector number + 1: ", sectorNum)
return nil
},
}
4.2 添加 RPC API
大部分的 API 都在 api 这个 package 中:
1 | -rw-rw-r-- 1 2052 10月 16 21:39 api_common.go # 通用 API |
关于 Sector 命令的 JSONRPC API 基本都在 api_storage.go 文件中,所以我们也把 API 放在其中,其中第一个是只读权限,后面两个都需要 admin 权限:
1 | // @Added by xxxx 2021-10-18 for lotus-miner sectors counter cmd |
::: warning 注意:
多人合作的项目,一个良好的习惯是: 每次修改代码需要注明你在什么时候加上的,该代码的作用是什么,以便其他人一眼便能知道你加的这些代码的意图。 如果后面该功能删除了,
其他工程师能及时删除这些代码,不至于变成 僵尸代码。
:::
4.3 添加 RPC 实现
在添加完 API 之后我们需要给相应的 API 添加实现代码。lotus 的 API 实现封装的层级比较深,这里给你简单屡一下:
api_storage.go的实现在node/impl/storminer.go中,是通过StorageMinerAPI对象实现的,StorageMinerAPI又是通过调用Miner(storage/miner_sealing.go) 对象的 API 实现:1
2
3
4
5
6
7
8
9
10
11func (sm *StorageMinerAPI) SectorRemove(ctx context.Context, id abi.SectorNumber) error {
return sm.Miner.RemoveSector(ctx, id)
}
func (sm *StorageMinerAPI) SectorCounterGet(ctx context.Context) (abi.SectorNumber, error) {
return sm.Miner.GetSectorNumber(ctx)
}
func (sm *StorageMinerAPI) SectorCounterSet(ctx context.Context, id abi.SectorNumber) error {
return sm.Miner.SetSectorNumber(ctx, id)
}Miner又是通过调用Sealing(extern/storage-sealing/sealing.go) 对象的 API 实现:1
2
3
4
5
6
7
8
9
10
11func (m *Miner) GetSectorNumber(ctx context.Context) (abi.SectorNumber, error) {
return m.sealing.GetSectorNumber(ctx)
}
func (m *Miner) SetSectorNumber(ctx context.Context, id abi.SectorNumber) error {
return m.sealing.SetSectorNumber(ctx, id)
}
func (m *Miner) NextSectorNumber(ctx context.Context) (abi.SectorNumber, error) {
return m.sealing.NextSectorNumber(ctx)
}Sealing又是通过调用SectorIDCounter(extern/storage-sealing/types.go) 对象的 API 实现:1
2
3
4
5
6
7
8
9
10
11func (m *Sealing) GetSectorNumber(ctx context.Context) (abi.SectorNumber, error) {
return m.sc.Get()
}
func (m *Sealing) SetSectorNumber(ctx context.Context, sid abi.SectorNumber) error {
return m.sc.Set(sid)
}
func (m *Sealing) NextSectorNumber(ctx context.Context) (abi.SectorNumber, error) {
return m.sc.Next()
}由于
SectorIDCounter目前只有Next()一个 API,所以我们需要给它加上其他两个:1
2
3
4
5type SectorIDCounter interface {
Get() (abi.SectorNumber, error) //新增
Set(abi.SectorNumber) error //新增
Next() (abi.SectorNumber, error)
}而这个
SectorIDCounter的实现其实又是在node/modules/storageminer.go文件中的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17type sidsc struct {
sc *storedcounter.StoredCounter
}
func (s *sidsc) Get() (abi.SectorNumber, error) {
i, err := s.sc.Get()
return abi.SectorNumber(i), err
}
func (s *sidsc) Set(number abi.SectorNumber) error {
return s.sc.Set(uint64(number))
}
func (s *sidsc) Next() (abi.SectorNumber, error) {
i, err := s.sc.Next()
return abi.SectorNumber(i), err
}我们可以看到,最终的实现其实是由
storedcounter.StoredCounter这个对象实现的。但是这个结构体其实定义在另一项目中(filecoin-project/go-storedcounter),所以我们不能直接改。
我们需要到原项目中去改,打开这个项目一看你会发现只有一个文件:
所以干脆把这个项目放到 extern 目录中去托管:
1
2
3cd extern/
git clone https://github.com/filecoin-project/go-storedcounter.git
rm -rf go-storedcounter/.git然后修改
go.mod文件,删除go-storedcounter远程依赖:
然后在文件末尾增加下面的代码,替换为本地依赖:
1
replace github.com/filecoin-project/go-storedcounter => ./extern/go-storedcounter
此时我们就可以开始编辑
extern/go-storedcounter/storedcounter.go了,为StoredCounter对象加上Get()和Set()API: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// Get current Sector Number
func (sc *StoredCounter) Get() (uint64, error) {
sc.lock.Lock()
defer sc.lock.Unlock()
has, err := sc.ds.Has(sc.name)
if err != nil {
return 0, err
}
if ! has {
return 0, nil
}
curBytes, err := sc.ds.Get(sc.name)
if err != nil {
return 0, err
}
cur, _ := binary.Uvarint(curBytes);
return cur,nil
}
// Set the next counter value, updating it on disk in the process
func (sc *StoredCounter) Set(number uint64) error {
sc.lock.Lock()
defer sc.lock.Unlock()
has, err := sc.ds.Has(sc.name)
if err != nil {
return err
}
if has {
curBytes, err := sc.ds.Get(sc.name)
if err != nil {
return err
}
cur, _ := binary.Uvarint(curBytes)
if cur > number {
return xerrors.Errorf("Number %d should not less than current SectorID %d", number, cur)
}
}
buf := make([]byte, binary.MaxVarintLen64)
size := binary.PutUvarint(buf, number)
return sc.ds.Put(sc.name, buf[:size])
}至此,我们的功能代码都添加完了。
测试
完成编码之后我们接下来就要开始测试了,首先我们来编译一下,由于我们修改了 JSONRPC 的 API,所以我们需要先生成 API 的代理实现文件 proxy_gen.go:
1 | make gen |
执行完之后最后会打印出下面的日志:
1 | >>> IF YOU'VE MODIFIED THE CLI, REMEMBER TO ALSO MAKE docsgen-cli |
意思是,如果你修改了 CLI,你还需要执行:
1 | make docsgen-cli |
这些都执行完了之后,你就可以开始编译了:
1 | make lotus-miner |
编译完成之后你可以执行 ./lotus-miner sectors --help 会看到新的命令菜单里面多了 counter 子命令:

接下来我们可以在本地网络测试一下上述我们新增的功能,首先我们在本地跑一个 2K 网络,如果还不知道如何搭建本地 2K 网络的话,
请参考 本地搭建 2K 测试网入门教程(写的非常详细,小白专用)。

总结
今天用一个很小的 Demo 去给大家演示了新手如何去从零开始为 lotus 添加一个小的功能。相信你实操完这个功能之后,应该对 lotus 的基本架构有个大概的了解了。
其实整个 Lotus 软件设计的非常人性化,也非常专业,模块划分一目了然,本身就降低了源码阅读的难度。总结一下今天这篇文章的内容:
- Lotus 客户端和守护进程之间的交互都是通过 JSONRPC 模式进行。
- 添加一个客户端命令只要三步:1.添加 CLI API 以及实现,2.添加 JSONRPC API,3.实现 JSONRPC API。
- 你并不需要在完全了解 Lotus 源码架构之后才开始动手去修改代码,而是应该在修改功能的过程中去了解源码架构。
更多 Lotus 相关的技术交流,可以加入 TG 电报群 原语云 Lotus 技术交流。