Talking is cheap, show me the code. -- Linus Torvalds
废话少说,有种把你的代码亮出来。

上几篇 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 文件也全部删除,然后再重启集群就好了。不过这种处理方式有几个副作用:

  1. 扇区或者任务有时候删除不掉,大量删除或者终止任务或者扇区可能会导致状态机出问题。
  2. 任务或者任务虽然已经删除,但是会留下很多垃圾数据在 Miner 的 datastore 文件中。

第二个解决思路就是把你当前 Miner 的 ID Counter 设置为你已经上链的最大扇区 Number 就行了,然而官方目前的代码中是没有这个 API 可以让你直接把当前的 ID Counter 强行设置到某个数值。所有你得自己添加一个这样的功能。

第二种解决方案正是我们今天要讨论的内容。

首先你需要知道,所有 Lotus 相关程序启动之后,在本地都会启动一个 JSONRPC 的服务端,Lotus 的所有客户端的命令,本质上都是调用远程的 JSONRPC API 实现的。所以通常你的 Lotus daemon 或者 Miner 没有启动的时候, 你在运行 lotus/lotus-miner 命令的时候都会出现类似下面的错误:

ERROR: could not get API info for FullNode: could not get api endpoint: API not running (no endpoint)

意思是获取不到相关的 API 信息, API 服务没有运行。

所以我们如果需要新增一个客户端命令实现某个功能的话,就需要做 2 件事情:

  1. 增加一个该命令的 CMD 入口指令
  2. 为这个功能添加一个 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 等:

drwxrwxr-x 2 4096 6月   7 16:11 chain-noise/
drwxrwxr-x 2 4096 10月 16 21:39 lotus/
drwxrwxr-x 2 4096 10月 16 21:39 lotus-bench/
drwxrwxr-x 3 4096 6月   7 16:11 lotus-fountain/
drwxrwxr-x 2 4096 6月  27 10:48 lotus-gateway/
drwxrwxr-x 2 4096 6月   7 16:11 lotus-health/
drwxrwxr-x 2 4096 6月   7 16:11 lotus-keygen/
drwxrwxr-x 2 4096 10月 16 21:39 lotus-miner/
drwxrwxr-x 5 4096 10月 16 21:39 lotus-monitor/
drwxrwxr-x 2 4096 6月   7 16:11 lotus-pcr/
drwxrwxr-x 2 4096 10月 16 21:39 lotus-seal-worker/
drwxrwxr-x 3 4096 10月 16 21:39 lotus-seed/
drwxrwxr-x 2 4096 10月 16 21:39 lotus-shed/
drwxrwxr-x 3 4096 6月  27 13:08 lotus-sim/
drwxrwxr-x 2 4096 10月 16 21:39 lotus-stats/
drwxrwxr-x 2 4096 10月 16 21:39 lotus-wallet/
drwxrwxr-x 2 4096 10月 16 21:37 qark/
drwxrwxr-x 2 4096 10月 16 21:39 tvx/

这次我们添加功能是给 SectorNumber 添加 setget 功能,那么显然我们需要把这个功能添加到 lotus-miner 这个模块。

lotus-miner 是一个复合的命令,它本身包含了很多个子命令:

lotus-miner
NAME:
   lotus-miner - Filecoin decentralized storage network miner

USAGE:
   lotus-miner [global options] command [command options] [arguments...]

VERSION:
   1.12.0+2k+git.7f545944e

COMMANDS:
   init     Initialize a lotus miner repo
   run      Start a lotus miner process
   stop     Stop a running lotus miner
   config   Manage node config
   backup   Create node metadata backup
   version  Print version
   help, h  Shows a list of commands or help for one command
   CHAIN:
     actor  manipulate the miner actor
     info   Print miner info
   DEVELOPER:
     auth          Manage RPC permissions
     log           Manage logging
     wait-api      Wait for lotus api to come online
     fetch-params  Fetch proving parameters
   MARKET:
     storage-deals    Manage storage deals and related configuration
     retrieval-deals  Manage retrieval deals and related configuration
     data-transfers   Manage data transfers
     dagstore         Manage the dagstore on the markets subsystem
   NETWORK:
     net  Manage P2P Network
   RETRIEVAL:
     pieces  interact with the piecestore
   STORAGE:
     sectors  interact with sector store
     proving  View proving information
     storage  manage sector storage
     sealing  interact with sealing pipeline
     worker   interact with worker pipeline
     dworker  interact with device worker pipeline

几乎每个子命令都对应者一个单独实现文件,我们看下 lotus-miner 的目录结构:

-rw-rw-r-- 1 27166 10月 16 21:39 actor.go
-rw-rw-r-- 1  2678 10月 16 21:39 actor_test.go
-rw-rw-r-- 1  1416 10月 16 21:39 allinfo_test.go
-rw-rw-r-- 1   369 10月 16 21:39 backup.go
-rw-rw-r-- 1  1736 10月 16 21:39 config.go
-rw-rw-r-- 1  6089 10月 16 21:39 dagstore.go
-rw-rw-r-- 1  2330 10月 16 21:39 dworker.go
-rw-rw-r-- 1  3635 10月 16 21:39 info_all.go
-rw-rw-r-- 1 18836 10月 16 21:39 info.go
-rw-rw-r-- 1 22106 10月 16 21:39 init.go
-rw-rw-r-- 1  7685 10月 16 21:39 init_restore.go
-rw-rw-r-- 1  4309 10月 16 21:39 init_service.go
-rw-rw-r-- 1  5067 10月 16 21:39 main.go
-rw-rw-r-- 1 21935 10月 16 21:39 market.go
-rw-rw-r-- 1  3086 10月 16 21:39 pieces.go
-rw-rw-r-- 1 11372 10月 16 21:39 proving.go
-rw-rw-r-- 1  6395 10月 16 21:39 retrieval-deals.go
-rw-rw-r-- 1 10863 10月 16 21:39 run.go
-rw-rw-r-- 1 11441 10月 16 21:39 sealing.go
-rw-rw-r-- 1 52331 10月 16 21:39 sectors.go
-rw-rw-r-- 1   461 10月 16 21:39 stop.go
-rw-rw-r-- 1 29440 10月 16 21:39 storage.go
-rw-rw-r-- 1  8181 10月 16 21:39 worker.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 个子命令:

  1. lotus-miner sectors counter get: 获取当前 SectorNumber Counter 的值。
  2. lotus-miner sectors counter set <value> 重置当前 SectorNumber Counter 的值为某个指定的值。
  3. lotus-miner sectors counter next 将当前 SectorNumber Counter 的值 +1。

4.1 修改 sectors.go 文件,新增相关命令

  1. 我们首先要在 Subcommands 中增加 sectorsCounter 子命令:

     Subcommands: []*cli.Command{
         sectorsStatusCmd,
         sectorsListCmd,
         sectorsRefsCmd,
         sectorsUpdateCmd,
         sectorsPledgeCmd,
         sectorsCheckExpireCmd,
         sectorsExpiredCmd,
         sectorsRenewCmd,
         sectorsExtendCmd,
         sectorsTerminateCmd,
         sectorsRemoveCmd,
         sectorsMarkForUpgradeCmd,
         sectorsStartSealCmd,
         sectorsSealDelayCmd,
         sectorsCapacityCollateralCmd,
         sectorsBatching,
         sectorsCounter, // 这里增加我们需要的子命令
     },
    
  2. 定义 sectorsCounter 变量,我们给 sectorsCounter 再拆分成三个子命令实现:

     var sectorsCounter = &cli.Command{
     	Name:  "counter",
     	Usage: "manage sector number counter",
     	Subcommands: []*cli.Command{
     		sectorsCounterGet,
     		sectorsCounterSet,
     		sectorsCounterNext,
     	},
     }
    
  3. 实现 sectorsCounterGet 子命令:

     var 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
     	},
     }
    
  4. 实现 sectorsCounterSet 子命令,修改 SectorNumber Counter 属于高级操作,所以需要加上 --really-do-it 参数:

     var 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
     	},
     }
    
  5. 实现 sectorsCounterNext 子命令:

     var 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 中:

-rw-rw-r-- 1   2052 10月 16 21:39 api_common.go # 通用 API
-rw-rw-r-- 1  56368 10月 16 21:39 api_full.go # 全节点 API
-rw-rw-r-- 1   4058 10月 16 21:39 api_gateway.go # 网关 API
-rw-rw-r-- 1   2553 10月 16 21:39 api_net.go
-rw-rw-r-- 1  23745 10月 16 21:39 api_storage.go # StorageMiner API
-rw-rw-r-- 1   3185 10月 16 21:39 api_test.go
-rw-rw-r-- 1   1422 6月  27 10:48 api_wallet.go # 钱包 API
-rw-rw-r-- 1   4559 9月   3 10:59 api_worker.go # Worker API

关于 Sector 命令的 JSONRPC API 基本都在 api_storage.go 文件中,所以我们也把 API 放在其中,其中第一个是只读权限,后面两个都需要 admin 权限:

// @Added by xxxx 2021-10-18 for lotus-miner sectors counter cmd
SectorCounterGet(context.Context) (abi.SectorNumber, error)  //perm:read
SectorCounterSet(context.Context, abi.SectorNumber) error    //perm:admin
SectorCounterNext(context.Context) (abi.SectorNumber, error) //perm:admin

注意:多人合作的项目,一个良好的习惯是: 每次修改代码需要注明你在什么时候加上的,该代码的作用是什么,以便其他人一眼便能知道你加的这些代码的意图。 如果后面该功能删除了, 其他工程师能及时删除这些代码,不至于变成 僵尸代码

在添加完 API 之后我们需要给相应的 API 添加实现代码。lotus 的 API 实现封装的层级比较深,这里给你简单屡一下:

  1. api_storage.go 的实现在 node/impl/storminer.go 中,是通过 StorageMinerAPI 对象实现的,StorageMinerAPI 又是通过调用 Miner(storage/miner_sealing.go) 对象的 API 实现:

     func (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)
     }
    
  2. Miner 又是通过调用 Sealing(extern/storage-sealing/sealing.go) 对象的 API 实现:

     func (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)
     }
    
  3. Sealing 又是通过调用 SectorIDCounter(extern/storage-sealing/types.go) 对象的 API 实现:

     func (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,所以我们需要给它加上其他两个:

     type SectorIDCounter interface {
     	Get() (abi.SectorNumber, error) //新增
     	Set(abi.SectorNumber) error //新增
     	Next() (abi.SectorNumber, error)
     }
    

    而这个 SectorIDCounter 的实现其实又是在 node/modules/storageminer.go 文件中的:

     type 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 目录中去托管:

     cd extern/
     git clone https://github.com/filecoin-project/go-storedcounter.git
     rm -rf go-storedcounter/.git
    

    然后修改 go.mod 文件,删除 go-storedcounter 远程依赖:

    然后在文件末尾增加下面的代码,替换为本地依赖:

     replace github.com/filecoin-project/go-storedcounter => ./extern/go-storedcounter
    

    此时我们就可以开始编辑 extern/go-storedcounter/storedcounter.go 了,为 StoredCounter 对象加上 Get()Set() API:

     // 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

make gen

执行完之后最后会打印出下面的日志:

>>> IF YOU'VE MODIFIED THE CLI, REMEMBER TO ALSO MAKE docsgen-cli

意思是,如果你修改了 CLI,你还需要执行:

make docsgen-cli

这些都执行完了之后,你就可以开始编译了:

make lotus-miner

编译完成之后你可以执行 ./lotus-miner sectors --help 会看到新的命令菜单里面多了 counter 子命令:

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

总结

今天用一个很小的 Demo 去给大家演示了新手如何去从零开始为 lotus 添加一个小的功能。相信你实操完这个功能之后,应该对 lotus 的基本架构有个大概的了解了。 其实整个 Lotus 软件设计的非常人性化,也非常专业,模块划分一目了然,本身就降低了源码阅读的难度。总结一下今天这篇文章的内容:

  1. Lotus 客户端和守护进程之间的交互都是通过 JSONRPC 模式进行。
  2. 添加一个客户端命令只要三步:1.添加 CLI API 以及实现,2.添加 JSONRPC API,3.实现 JSONRPC API。
  3. 你并不需要在完全了解 Lotus 源码架构之后才开始动手去修改代码,而是应该在修改功能的过程中去了解源码架构。

更多 Lotus 相关的技术交流,可以加入 TG 电报群 原语云 Lotus 技术交流