飞行的蜗牛

vuePress-theme-reco 极客学长    2013 - 2025
飞行的蜗牛 飞行的蜗牛

Choose mode

  • dark
  • auto
  • light
首页
分类
  • 技术杂谈
  • Database
  • Docker
  • PHP
  • 随笔杂谈
  • 前端开发
  • FunnyTools
  • Jekyll
  • 读书笔记
  • Java
  • SpringBoot
  • 区块链技术
  • IPFS
  • C/C++
  • Filecoin
  • Golang
  • Sharding-JDBC
  • 分布式存储
  • Lotus-源码系列
  • Lotus
  • 框架源码系列
  • Spring-源码系列
  • AI
  • ChatGPT
  • Stable Diffusion
  • DeepSeek-R1
  • DeepSeek-V3
标签
时间抽
关于作者
开源项目
GeekAI (opens new window)
author-avatar

极客学长

154

文章

151

标签

首页
分类
  • 技术杂谈
  • Database
  • Docker
  • PHP
  • 随笔杂谈
  • 前端开发
  • FunnyTools
  • Jekyll
  • 读书笔记
  • Java
  • SpringBoot
  • 区块链技术
  • IPFS
  • C/C++
  • Filecoin
  • Golang
  • Sharding-JDBC
  • 分布式存储
  • Lotus-源码系列
  • Lotus
  • 框架源码系列
  • Spring-源码系列
  • AI
  • ChatGPT
  • Stable Diffusion
  • DeepSeek-R1
  • DeepSeek-V3
标签
时间抽
关于作者
开源项目
GeekAI (opens new window)
  • Lotus 源码研究 06 - CC 扇区恢复功能的设计与实现

    • 1. 使用方式设计
      • 2. 扇区的计算过程
        • 3. 扇区状态和事件处理
          • 4. 扇区恢复的实现
            • 5. 扇区恢复命令

            Lotus 源码研究 06 - CC 扇区恢复功能的设计与实现

            vuePress-theme-reco 极客学长    2013 - 2025

            Lotus 源码研究 06 - CC 扇区恢复功能的设计与实现


            极客学长 2022-03-01 0 lotus 扇区恢复

            张爱玲

            人总是在接近幸福时倍感幸福,在幸福进行时却患得患失。

            转载声明

            本文转载自原语云公众号 Lotus CC 扇区恢复功能的设计与实现 (opens new window)。欢迎订阅,第一时间获取技术干货。

            目前 Lotus 主网大部分数据都是 CC 扇区,这些扇区里面存储的都是 Junk Data,其实呢都是 0x00。熟悉扇区计算过程的小伙伴都应该明白这些扇区即使丢了或者损坏了, 只要你还有封装机器就可以重新再计算回来,当然功能这存在的意义肯定是不再需要额外的抵押。原语云在最近的 1.14.1 版本中正式的增加了这个功能,接下来讲解下原语的“CC 扇区恢复”功能的设计和实现。

            # 1. 使用方式设计

            对于这个功能,个人觉得产品体验方式比编码实现更值得思考,因为扇区计算过程中的扇区流转状态比较多,而且完了之后还需要被下载到最终存储,如果产品体验方式还需要额外的配置或者服务器, 这无疑会增加运维的工作量,经过一番思考,原语云最终是通过如下的一个命令来实现一个扇区的恢复计算:

            lotus git:(yy_master) ./lotus-miner sectors recover --help
            NAME:
               lotus-miner sectors recover - recover the specified sector
            
            USAGE:
               lotus-miner sectors recover [command options] <sectorNum>
            
            DESCRIPTION:
               recover the specified sector. @Note:
               1, make sure the specified sector it really lost or unrecoverable before call this command.
               2, this ONLY workes for CC sectors for NOW (sectors without deals).
               3, remove the original sector data by calling sectors remove <sector> before invoke this command.
            
            OPTIONS:
               --ticket-epoch value     ticket epoch in the PreCommit submit (default: -1)
               --precommit-epoch value  sector precommit epoch for seed epoch and value define (default: -1)
               --debug                  pass this flag to print the ignore analysis log (will NOT start the recovery) (default: false)
               --really-do-it           pass this flag if you know what you are doing (default: false)
               --help, -h               show help (default: false)
            

            例如,如果扇区ID为2的 Proving 状态的扇区丢失或者损坏了,就可以通过如下命令来开始修复:

            lotus git:(yy_master) ./lotus-miner sectors recover --really-do-it=true 2
              State:  Proving
              Ignore: GetTicket/PreCommitting/WaitSeed/C2/Committing
              Ticket: {Value: 2UYaQlVzItyHuac4s4xhwxEr2K7IAwiw5tXLm60PX8M=, Epoch: -713}
              Seed:   {Value: 7tBu5ZPM5RuD1tVdjOssepUjpytNJUwC0rFFbPuyi6g=, Epoch: 202}
            

            这条命令运行以后,2 号扇区就会开始进入恢复计算,具体的计算过程会依据这个扇区本身的状态,例如这个 Proving 状态的2号扇区只要完成 AP/PC1/PC2 计算就可以了, 这个功能的核心是想做到整个恢复计算过程和普通的封装过程一样,现有的计算服务器架构,一样的资源分配和调度逻辑,无需任何额外的配置和管理工作。

            # 2. 扇区的计算过程

            恢复过程本质上就是一个封装过程,只是恢复过程中一些计算是可以省略的,例如对于一个 Proving 的扇区,恢复过程就没有必要再进行 C1/C2 计算, 也不再需要 PreCommit 和 Commit 两次上链了,要清楚的知道整个实现过程,我们得了解一个扇区的主要的计算过程,具体如下:

            1. AddPiece: 通过文件指针移位的方式得到一个扇区大小的空文件,然后再往里面写满 0x00,一般1~2分钟就完成了,这个过程也可以缓存加速。
            2. GetTicket: 获取随机数,以当前 actorID 和区块高度为主要参数去生成一个随机数,代码里面叫做 TicketValue。
            3. PreCommit1: 在 AP 生成的空扇区基础上结合得到的 Ticket 随机数就开始进行PC1计算了,一个32G的扇区需要2.5小时左右。
            4. PreCommit2: 在PC1的数据上,继续计算生成扇区的 r 和 c 层,一个32G的扇区使用 3080 GPU 计算通常 10 分钟左右完成。
            5. PreCommitSector: 前置上链,将 AP 后以及 PC2 后的 cid上链 (此处的 CID 就是 IPFS 的内容寻址 ID ),这个过程需要抵押。
            6. WaitSeed: 等待Seed随机数,这个随机数需要用来参与后续的 C1 计算,这个地方要强制等待指定的时间。
            7. Commit1: C1计算,生成扇区的 vanilla 证明。
            8. Commit2: C2计算,在 C1 的 vanilla 证明的基础上生成 zk-snark 证明。
            9. CommitSector: 证明上链,将 C2 计算得到的 zk-snark 结果提交上链,这个过程同样需要抵押。
            10. FinalizeSector: 将 PC2 后的数据做一些 triming 清理,然后调度存储worker下载数据写到最终(canStore = true)存储,用于后续的时空/爆块证明挑战读取。 封装计算过程,每个扇区都会经历上面的十个核心过程的转变,而且这个顺序是固定的,不能打乱,后面我们就会以前面的序号来代替这些计算过程。

            # 3. 扇区状态和事件处理

            恢复过程中还需要了解的一个核心点就是扇区的状态管理机制,Lotus 内部是通过一个 FSM 的状态机来管理一个扇区的状态以及该状态下的可处理事件, 具体的映射关系定义在:lotus/extern/storage-sealing/fsm.go 中的一个 fsmPlanners 的 map 中,例如我们截取其中的一小部分如下:

            var fsmPlanners = map[SectorState]func(events []statemachine.Event, state *SectorInfo) (uint64, error){
              ...
              PreCommit1: planOne(
                on(SectorPreCommit1{}, PreCommit2),
                on(SectorSealPreCommit1Failed{}, SealPreCommit1Failed),
                on(SectorDealsExpired{}, DealsExpired),
                on(SectorInvalidDealIDs{}, RecoverDealIDs),
                on(SectorOldTicket{}, GetTicket),
                apply(SectorRecover{}),
              ),
              ...
            }
            

            上述代码的第 3 行定义了 PreCommit1 状态的下的事件处理关系,这个表示在 PC1 状态下,状态状态机只能处理这 6 个事件,不然就会出现错误导致状态机协程退出, 然后就会出现大家都比较熟悉的 normal shutdown of statemachine 错误:

            1. SectorPreCommit1: 表示在 PC1 状态下 扇区已经完成了 PC1 计算,第二个参数的 PreCommit2 表述将该扇区的状态设置为 PreCommit2 。
            2. SectorSealPreCommit1Failed: 表示 PC1 计算出错了,将扇区状态设置为 SealPreCommit1Failed,状态机会在 60s 后继续设置状态为 PC1 。
            3. SectorDealsExpired: 扇区里面包含的订单已经过期,扇区状态会被设置为 DealsExpired。
            4. SectorInvalidDealIDs: 这个表示扇区信息里面的订单信息错误,将扇区状态设置为 RecoverDealIDs。
            5. SectorOldTicket: 这个表示最近一次获取的 Ticket 随机数过期了,将扇区状态设置为 GetTicket 去重新获取 Ticket 随机数 。
            6. SectorRecover: 这个状态就是原语云增加的用于将扇区转变到恢复状态以便开始进行扇区恢复计算,这里的 apply 没有第二个状态参数,所以也不会发生状态的改变,具体在状态会依据需要设置。

            整个事件的响应逻辑就是我们给指定扇区的状态机发送一个事件,然后状态机通过上述 fsmPlanners 定义的关系映射中找到扇区当前状态的事件集合,然后再通过事件名称找到具体的事件定义,再执行预先设定的事件处理函数 (apply) 并且将状态设置为第二个参数定义的状态 (如果有第二个参数定义)。

            # 4. 扇区恢复的实现

            了解上面的扇区的10个计算过程和扇区的状态管理后就可以开始本篇最核心的部分了 - 扇区恢复的设计和实现。 上面也提到过扇区恢复本质上一个选择性的重新计算的过程,具体的计算步骤还是上面描述的10个步骤,只是依据扇区的状态需要选择性的忽略某些计算,具体可以分成下面三种情况:

            1. PreCommit 上链前:这个状态前的扇区没有任何上链,所以恢复可以选择直接全部重新计算,包括 Ticket 也重新获取。
            2. PreCommit 上链后到 Commit 上链前:这个区间的状态的扇区已经完成了 PreCommitSector 上链,也就是如果要恢复需要保障前面的计算的出来的扇区的 sealed cid 是一样的, 这个时候你可以再翻看下前面的 AP/GetTicket/PC1/PC2 这 4 个计算过程,发现只要 Ticket 保持一致那重新计算的结果就是一样的, 而且因为已经完成了 PreCommitSector 上链,所以这里还要忽略 PreCommitSector 上链,这之后的 WaitSeed/C1/C2/Commit 上链/Finalize 计算过程和普通密封过程一样。
            3. CommitSector 上链后:这个状态的扇区已经完成了绝大部分的计算过程,最常见的就是 Proving,所以这里肯定不能再 PreCommitSector 和 CommitSector 上链了, 也不需要后面的证明过程,简单的说只要完成 AP/PC1/PC2/Finalize 计算就可以了。

            明白上面这个逻辑后,接下来编码上只需要做好下面三步工作就可以了:

            1. SectorInfo 里面定义一个字段用于存储需要忽略计算的任务,我在 lotus/extern/storage-sealing/types.go 定义的 SectorInfo 里面增加一个 SealTaskIgnore 字段来存储该扇区需要忽略的任务:

              type SealTask int64
              
              const (
                TaskNone            SealTask = 0x01 << 0
                TaskPacking         SealTask = 0x01 << 1
                TaskGetTicket       SealTask = 0x01 << 2
                TaskPreCommit1      SealTask = 0x01 << 3
                TaskPreCommit2      SealTask = 0x01 << 4
                TaskPreCommitSubmit SealTask = 0x01 << 5
                TaskWaitSeed        SealTask = 0x01 << 6
                TaskCommit          SealTask = 0x01 << 7
                TaskCommitSubmit    SealTask = 0x01 << 8
                TaskFinalize        SealTask = 0x01 << 9
              )
              
              type SectorInfo struct {
                // 密封中需要忽略的任务
                SealTaskIgnore SealTask
                ...
              }
              

              SealTaskIgnore 为一个 SealTask 类型,实际上是一个 int64 类型,这样我只要通过位运算就可以知道该扇区的密封计算是需要忽略哪些具体计算了。

            2. 增加一个扇区恢复事件:

              正如上面的 FSM 状态定义里面描述,我增加了一个 SectorRecover 事件用于告诉状态机让扇区进入恢复状态,恢复状态的初始化过程主要就是定义 SealTaskIgnore 和初始化 TicketValue/SeedValue 的过程, 扇区恢复初始化成功后就会进入正常的密封计算过程。SectorRecover 的定义以及他的 apply 事件处理实现如下:

              type SectorRecover struct {
                State       SectorState // starting state of recovery
                TaskIgnore  SealTask
                TicketEpoch abi.ChainEpoch
                TicketValue abi.SealRandomness
                SeedEpoch   abi.ChainEpoch
                SeedValue   abi.InteractiveSealRandomness
              }
              
              func (evt SectorRecover) apply(state *SectorInfo) {
                state.State = evt.State
                state.SealTaskIgnore = evt.TaskIgnore
                state.TicketValue = evt.TicketValue
                state.TicketEpoch = evt.TicketEpoch
                state.SeedValue = evt.SeedValue
                state.SeedEpoch = evt.SeedEpoch
              }
              
              

              核心字段就是 SealTaskIgnore 以及 PC1 和 C1 需要的 Ticket/Seed 随机数。从 SectorRecover 的 apply 函数可以看出给扇区推送了 SectorRecover 事件后, 状态会变成其 State 指定的值,通常这个状态通常是 Packing,也就是会从 AddPiece 重新开始,也就是我们通过 SectorRecover 事件让扇区的恢复变成了一个普通的封装计算,之后的过程和前面描述的一致。

            3. 响应 SealTaskIgnore 的设置:

              前面两步提到过,我们会依据扇区所在的状态来初始化 SealTaskIgnore 并且重置扇区到 Packing 开始重新计算,那接下来肯定就是需要响应 SealTaskIgnore 的设置了,也就是如果设置需要忽略 GetTicket, 就状态流转过程不一定不能再去执行 GetTicket 操作,这些需要修改 lotus/extern/storage-sealing/states_sealing.go 中定义的各种 handleXXX 的实现,例如 handleGetTicket 是 GetTicket 操作的具体的执行代码, 我们在函数最前面增加如下拦截判断 (注意看代码注释):

              func (m *Sealing) handleGetTicket(ctx statemachine.Context, sector SectorInfo) error {
              	// 如果 SealTaskIgnore 设置需要忽略 TaskGetTicket 计算
              	// 那就直接返回旧的 TicketValue 和 TicketEpoch
              	if (sector.SealTaskIgnore & TaskGetTicket) != 0 {
              	  log.Infof("Ignore handle %d.GetTicket", sector.SectorNumber)
              	  return ctx.Send(SectorTicket{
              		TicketValue: sector.TicketValue,
              		TicketEpoch: sector.TicketEpoch,
              	})
              
              	// 中间其他代码
              	...
              
              	return ctx.Send(SectorTicket{
              	  TicketValue: ticketValue,
              	  TicketEpoch: ticketEpoch,
              	})
              }
              

              将其他任务的 handleXXX 函数都增加类似的拦截和判断后,整个内部的恢复的计算流程就完整了,剩下的就是下面要描述的细节工作了。

            # 5. 扇区恢复命令

            内部的逻辑实现好了之后,剩下的就是提供一个 api 用于触发这个操作也就是 “向需要进行恢复的扇区推送一个 SectorRecover 事件”。 至于如何给添加一个 lotus api 和命令,我在之前的文章 Lotus 源码研究 04 - 小试牛刀 已经详细说明了,这里不再赘述。 这里我们重点描述 SealTaskIgnore 的定义以及 Ticket/Seed 的初始化。以下代码都是在 sectors recover 的命令行 api 里面判断的。

            1. SealTaskIgnore 的初始化:

              上面提到过我们需要依据扇区的状态来判断需要忽略哪些计算,具体三种情况也在上面仔细的描述了,具体代码实现如下 (注意看注释):

              var sectorsRecoverCmd = &cli.Command{
              	Name:  "recover",
              	Usage: "recover the specified sector",
              	Description: `...`,
              	ArgsUsage: "<sectorNum>",
              	Flags: []cli.Flag{
              	  ...
              	},
              	Action: func(cctx *cli.Context) error {
              	  ...
              
              	  // 查询扇区的信息
              	  sectorInfo, err = nodeApi.SectorsStatus(ctx, abi.SectorNumber(id), false)
              	  if err != nil {
              		return err
              	  }
              
              	  // 获取扇区的分配状态
              	  allocated, err := fullApi.StateMinerSectorAllocated(ctx, maddr, abi.SectorNumber(id), types.EmptyTSK)
              	  if err != nil {
              		return err
              	  }
              
              	  // 第一种情况:扇区在 PreCommitSector 上链前
              	  // 直接设置 SealTaskIgnore 为 SectorTaskNone,也就是不忽略任何任务
              	  if !allocated {
              		sealTaskIgnore = api.SectorTaskNone
              		goto RequestRecover
              	  }
              
              	  // 第三种情况:已经完成了 CommitSector 上链
              	  // 只需要完成 Packing/PreCommit1/PreCommit2/Finalize 计算
              	  // 忽略如下指定的其他任务
              	  if onChainInfo != nil {
              		sealTaskIgnore |= api.SectorTaskGetTicket       // ignore the getTicket
              		sealTaskIgnore |= api.SectorTaskPreCommitSubmit // Ignore the PreCommitSubmit
              		sealTaskIgnore |= api.SectorTaskWaitSeed        // Ignore the WaitSeed
              		sealTaskIgnore |= api.SectorTaskCommit          // Ignore the Commit
              		sealTaskIgnore |= api.SectorTaskCommitSubmit    // Ignore the CommitSubmit
              		goto RequestRecover
              	  }
              
              	  // 第二种情况:PreCommitSector 上链到 CommitSector 上链之间。
              	  // 这里需要忽略 GetTicket/PreCommitSubmit 任务
              	  if hasPreCommitInfo {
              		sealTaskIgnore |= api.SectorTaskGetTicket
              		sealTaskIgnore |= api.SectorTaskPreCommitSubmit
              		goto RequestRecover
              	  }
              	}
              }
              
            2. Ticket/Seed 的初始化:

              如果 Miner 的元数据没有损坏,Ticket/Seed 这两个值是可以直接拿到的,这个时候我们只需要简单的调用 sectors recover <sectorId> 就好,上面的 nodeApi.SectorsStatus 返回的 SectorInfo 就包含了之前的 Ticket/Seed 的值,我们直接使用即可,如果 Miner 的元数据损坏了,那就只能通过 ticket-epoch 和 precommit2-epoch 来分别来重新计算 Ticket 和 Seed 值了, 这两个值只能遍历区块数据才能得到 (区块浏览器可以看到),所以最好定期备份 Miner 的元数据,具体的获取代码如下:

              // 通过 ticketEpoch 重新计算 Ticket 随机数
              rand, err := fullApi.StateGetRandomnessFromTickets(ctx, crypto.DomainSeparationTag_SealRandomness, ticketEpoch, buf.Bytes(), tipSet.Key())
              if err != nil {
                return xerrors.Errorf("failed to get randomness from tickets: %w", err)
              }
              
              // 通过 Precommit2 上链的 epoch 来重新计算 Seed 随机数
              rand, err := fullApi.StateGetRandomnessFromBeacon(ctx, crypto.DomainSeparationTag_InteractiveSealChallengeSeed, seedEpoch, buf.Bytes(), tipSet.Key())
              if err != nil {
                return xerrors.Errorf("failed to get randomness from beacon: %w", err)
              }
              

              整个扇区恢复的原理,设计以及实现就到此结束,有问题可以到我们的电报群 原语云 lotus 交流群 (opens new window) 交流探讨。

            本站博文如非注明转载则均属作者原创文章,引用或转载无需申请版权或者注明出处,如需联系作者请加微信: geekmaster01

            ffmpeg 视频处理 Lotus snap-deal 功能体验报告