FileCoin Lotus复制证明 PoRep 源码梳理

Posted nirao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FileCoin Lotus复制证明 PoRep 源码梳理相关的知识,希望对你有一定的参考价值。

流程图
技术图片

Incoming

lotus-miner-storage,首先调用 PledgeSector 通过类似微服务的方式调用

在 cmd/lotus-storage-miner/sectors.go 发出生成扇区的命令,通过微服务的方式调用

    var pledgeSectorCmd = &cli.Command{
    Name:  "pledge-sector",
    Usage: "store random data in a sector",
    Action: func(cctx *cli.Context) error {
        // 获取miner网关地址
        nodeApi, closer, err := lcli.GetStorageMinerAPI(cctx)
        if err != nil {
            return err
        }
        defer closer()
        ctx := lcli.ReqContext(cctx)

        return nodeApi.PledgeSector(ctx)
    },
}

在 storage/garbage.go 生成新的扇区,获取分片数组大小,扇区id,该过程关键在调用内部方法 m.pledgeSector产生数据,填满扇区数据。

func (m *Miner) PledgeSector() error {
    go func() {
        ctx := context.TODO() // we can't use the context from command which invokes
        // this, as we run everything here async, and it's cancelled when the
        // command exits

        // 一共多少个分片,是否跟生成默克尔书的分块对应?
        size := sectorbuilder.UserBytesForSectorSize(m.sb.SectorSize())

        // 扇区id
        sid, err := m.sb.AcquireSectorId()
        if err != nil {
            log.Errorf("%+v", err)
            return
        }
        // 产生分片数组,该方法中会将生成的签名信息提交到链上,重点方法
        pieces, err := m.pledgeSector(ctx, sid, []uint64{}, size)
        if err != nil {
            log.Errorf("%+v", err)
            return
        }
        // 产生新的扇区
        if err := m.newSector(context.TODO(), sid, pieces[0].DealID, pieces[0].ppi()); err != nil {
            log.Errorf("%+v", err)
            return
        }
    }()
    return nil
}

在重点查看m.pledgeSector,该方法主要作用是为每隔扇区生成一个凭据,并把每隔凭据封装成一个交易信息,提交到链上,并解析出链上的提交信息进行判断交易id是否一致,存储数据;返回信息为分片信息数组

func (m *Miner) pledgeSector(ctx context.Context, sectorID uint64, existingPieceSizes []uint64, sizes ...uint64) ([]Piece, error) {
    ...
    //  将交易信息提交到链上
    params, aerr := actors.SerializeParams(&actors.PublishStorageDealsParams{
        Deals: deals,
    })
    ...
    //等待链上反馈消息
    r, err := m.api.StateWaitMsg(ctx, smsg.Cid())
    if err != nil {
        return nil, err
    }
    ...
    //从链上消息中解析出DealID,看是否一致
    var resp actors.PublishStorageDealResponse
    if err := resp.UnmarshalCBOR(bytes.NewReader(r.Receipt.Return)); err != nil {
        return nil, err
    }
    if len(resp.DealIDs) != len(sizes) {
        return nil, xerrors.New("got unexpected number of DealIDs from PublishStorageDeals")
    }
    ....
    out := make([]Piece, len(sizes))
  
    //根据链上确认的结果,首先将piece的信息存入到sector里
    for i, size := range sizes {
        //填充数据
        ppi, err := m.sb.AddPiece(size, sectorID, io.LimitReader(rand.New(rand.NewSource(42)), int64(size)), existingPieceSizes)
        if err != nil {
            return nil, err
        }

        existingPieceSizes = append(existingPieceSizes, size)
        out[i] = Piece{
            DealID: resp.DealIDs[i],
            Size:   ppi.Size,
            CommP:  ppi.CommP[:],
        }
    }
    return out, nil
}

扇区信息生成之后调用 /storage/secotrs.go

//扇区信息生成之后,调用该方法
func (m *Miner) onSectorIncoming(sector *SectorInfo) {
    // 判断id是否存在
    has, err := m.sectors.Has(sector.SectorID)
    if err != nil {
        return
    }
    if has {
        log.Warnf("SealPiece called more than once for sector %d", sector.SectorID)
        return
    }

    // 把数据写入 扇区 硬盘中
    if err := m.sectors.Begin(sector.SectorID, sector); err != nil {
        log.Errorf("sector tracking failed: %s", err)
        return
    }

    go func() {
        select {
        case m.sectorUpdated <- sectorUpdate{  //更改状态
            newState: api.Packing,
            id:       sector.SectorID,
        }:
        case <-m.stop:
            log.Warn("failed to send incoming sector update, miner shutting down")
        }
    }()
}    

以上为 Incomeing 过程,主要作用是计算piece大小,产生扇区id信息;把每个piece的大小产生凭据(包含交易信息等),提交到链上,进行验证;之后用piece数组,产生扇区信息;然后把扇区的信息写入磁盘,将状态更改 Packing状态,此过程将消耗大量的 cpu 和内存

Packing

后续的操作主要在 /storage/sector_states.go 文件中
主要是判断扇区数据是否完整,将没填满的扇区填充完整,之后将状态更改为 Unsealed状态

// 打包的状态,将没哟填满数据的扇区填满
func (m *Miner) handlePacking(ctx context.Context, sector SectorInfo) *sectorUpdate {
    log.Infow("performing filling up rest of the sector...", "sector", sector.SectorID)

    var allocated uint64
    for _, piece := range sector.Pieces {
        allocated += piece.Size
    }

    ubytes := sectorbuilder.UserBytesForSectorSize(m.sb.SectorSize())

    if allocated > ubytes {
        return sector.upd().fatal(xerrors.Errorf("too much data in sector: %d > %d", allocated, ubytes))
    }

    //fillers From Remaining
    fillerSizes, err := fillersFromRem(ubytes - allocated)
    if err != nil {
        return sector.upd().fatal(err)
    }

    if len(fillerSizes) > 0 {
        log.Warnf("Creating %d filler pieces for sector %d", len(fillerSizes), sector.SectorID)
    }
    //此处调用  pledgeSector将扇区填满
    pieces, err := m.pledgeSector(ctx, sector.SectorID, sector.existingPieces(), fillerSizes...)
    if err != nil {
        return sector.upd().fatal(xerrors.Errorf("filling up the sector (%v): %w", fillerSizes, err))
    }

    //数据填充完毕后,扇区的状态转换到了Unsealed状态
    return sector.upd().to(api.Unsealed).state(func(info *SectorInfo) {
        info.Pieces = append(info.Pieces, pieces...)
    })
}

Unsealed

func (m *Miner) handleUnsealed(ctx context.Context, sector SectorInfo) *sectorUpdate {
    log.Infow("performing sector replication...", "sector", sector.SectorID)
    // 调用随机函数返回一个随机选票(包含区块高度,和票据)
    // 随机函数在初始化矿工生成的,运用的反射,具体需要详细查看 ?
    ticket, err := m.tktFn(ctx)
    if err != nil {
        return sector.upd().fatal(err)
    }

    // 开始进行密封的操作,主要根据源数据产生加密数据,产生一份副本
    rspco, err := m.sb.SealPreCommit(sector.SectorID, *ticket, sector.pieceInfos())
    if err != nil {
        return sector.upd().to(api.SealFailed).error(xerrors.Errorf("seal pre commit failed: %w", err))
    }
    // 更改状态,把数据的唯一复制凭据信息,和随机数相关更新
    return sector.upd().to(api.PreCommitting).state(func(info *SectorInfo) {
        info.CommD = rspco.CommD[:]
        info.CommR = rspco.CommR[:]
        info.Ticket = SealTicket{
            BlockHeight: ticket.BlockHeight,
            TicketBytes: ticket.TicketBytes[:],
        }
    })
}

/lib/sectorbuilder/sectorbuilder.go 文件中
判断在 .lotusstorage 文件下几个目录是存在 cache,staged,sealed;调用rust库的代码生成相关的凭据

func (sb *SectorBuilder) SealPreCommit(sectorID uint64, ticket SealTicket, pieces []PublicPieceInfo) (RawSealPreCommitOutput, error) {
   
    ...
        // 底层是rust部分的代码生成凭据信息
        rspco, err := sectorbuilder.SealPreCommit(
        sb.ssize,
        PoRepProofPartitions,
        cacheDir,
        stagedPath,
        sealedPath,
        sectorID,
        addressToProverID(sb.Miner),
        ticket.TicketBytes,
        pieces,
    )

    log.Warn(xerrors.Errorf("[qz2.4]: time to precommit %v at :%v", sectorID, time.Since(start).Milliseconds()))
    start = time.Now()
    if err != nil {
        return RawSealPreCommitOutput{}, xerrors.Errorf("presealing sector %d (%s): %w", sectorID, stagedPath, err)
    }
    // 返会相关的凭据信息
    return RawSealPreCommitOutput(rspco), nil
    
}

此过程会产生大量的缓存文件用于计算,产生加密后数据的唯一副本相关的凭据,此时并没有产生复制证明

PreCommitting

主要是讲消息广播到链上去,并把该消息cid存起来;主要是让区块到了指定的高度验证数据的有效性

func (m *Miner) handlePreCommitting(ctx context.Context, sector SectorInfo) *sectorUpdate {
    // 要发到链上的消息
    params := &actors.SectorPreCommitInfo{
        SectorNumber: sector.SectorID,

        CommR:     sector.CommR,
        SealEpoch: sector.Ticket.BlockHeight,
        DealIDs:   sector.deals(),
    }
    enc, aerr := actors.SerializeParams(params)
    if aerr != nil {
        return sector.upd().to(api.PreCommitFailed).error(xerrors.Errorf("could not serialize commit sector parameters: %w", aerr))
    }
    // 封装消息体
    msg := &types.Message{
        To:       m.maddr,
        From:     m.worker,
        Method:   actors.MAMethods.PreCommitSector,
        Params:   enc,
        Value:    types.NewInt(0), // TODO: need to ensure sufficient collateral
        GasLimit: types.NewInt(1000000 /* i dont know help */),
        GasPrice: types.NewInt(1),
    }

    log.Info("submitting precommit for sector: ", sector.SectorID)
    // 广播
    smsg, err := m.api.MpoolPushMessage(ctx, msg)
    if err != nil {
        return sector.upd().to(api.PreCommitFailed).error(xerrors.Errorf("pushing message to mpool: %w", err))
    }
    // 将受到消息cid 保存
    return sector.upd().to(api.PreCommitted).state(func(info *SectorInfo) {
        mcid := smsg.Cid()
        info.PreCommitMessage = &mcid
    })
}

PreCommitted

func (m *Miner) handlePreCommitted(ctx context.Context, sector SectorInfo) *sectorUpdate {
   // 等待链上的消息
    mw, err := m.api.StateWaitMsg(ctx, *sector.PreCommitMessage)
    if err != nil {
        return sector.upd().to(api.PreCommitFailed).error(err)
    }

    ...
    // 区块的高度+定义的延时量(8)
    randHeight := mw.TipSet.Height() + build.InteractivePoRepDelay - 1 // -1 because of how the messages are applied
    log.Infof("precommit for sector %d made it on chain, will start proof computation at height %d", sector.SectorID, randHeight)

    updateNonce := sector.Nonce
    // 一个是在区块到达一定的高度执行的方法和回滚的方法
    err = m.events.ChainAt(func(ctx context.Context, ts *types.TipSet, curH uint64) error {
        // 根据区块高度和ts key生成随机数
        rand, err := m.api.ChainGetRandomness(ctx, ts.Key(), int64(randHeight))
        if err != nil {
            err = xerrors.Errorf("failed to get randomness for computing seal proof: %w", err)

            m.sectorUpdated <- *sector.upd().fatal(err)
            return err
        }
        // 更改状态
        m.sectorUpdated <- *sector.upd().to(api.Committing).setNonce(updateNonce).state(func(info *SectorInfo) {
            // 将密封 seed更新
            info.Seed = SealSeed{
                BlockHeight: randHeight,
                TicketBytes: rand,
            }
        })

        updateNonce++

        return nil
    }, func(ctx context.Context, ts *types.TipSet) error {
        log.Warn("revert in interactive commit sector step")
        // TODO: need to cancel running process and restart...
        return nil
    }, build.InteractivePoRepConfidence, mw.TipSet.Height()+build.InteractivePoRepDelay)
    if err != nil {
        log.Warn("waitForPreCommitMessage ChainAt errored: ", err)
    }

    return nil
}

该过程主要是等待之前 生成扇区唯一副本和凭据广播到链上的消息,等待之后,根据当前的区块的高度加上一个延时变量(预估5分钟左右),生成在该区块时执行的方法,和回滚的方法。状态更改 Committing

Committing

func (m *Miner) handleCommitting(ctx context.Context, sector SectorInfo) *sectorUpdate {
    ...
    // 产生复制证明凭据
    proof, err := m.sb.SealCommit(sector.SectorID, sector.Ticket.SB(), sector.Seed.SB(), sector.pieceInfos(), sector.rspco())
    if err != nil {
        return sector.upd().to(api.SealCommitFailed).error(xerrors.Errorf("computing seal proof failed: %w", err))
    }
    ...
    // 把包含证明文件的消息广播
    smsg, err := m.api.MpoolPushMessage(ctx, msg)
    if err != nil {
        return sector.upd().to(api.CommitFailed).error(xerrors.Errorf("pushing message to mpool: %w", err))
    }

    // 更改状态
    return sector.upd().to(api.CommitWait).state(func(info *SectorInfo) {
        mcid := smsg.Cid()
        info.CommitMessage = &mcid
        info.Proof = proof
    })
}


// 这个是重点关注的方法,产生复制证明的证明凭据
func (sb *SectorBuilder) SealCommit(sectorID uint64, ticket SealTicket, seed SealSeed, pieces []PublicPieceInfo, rspco RawSealPreCommitOutput) (proof []byte, err error) {
    // 产生一个工作任务
    call := workerCall{
        task: WorkerTask{
            Type:       WorkerCommit,
            TaskID:     atomic.AddUint64(&sb.taskCtr, 1),
            SectorID:   sectorID,
            SealTicket: ticket,
            Pieces:     pieces,

            SealSeed: seed,
            Rspco:    rspco,
        },
        ret: make(chan SealRes),
    }

    atomic.AddInt32(&sb.commitWait, 1)

    select { // prefer remote
    case sb.commitTasks <- call:
        proof, err = sb.sealCommitRemote(call)
    default:
        sb.checkRateLimit()

        rl := sb.rateLimit
        if sb.noCommit {
            rl = make(chan struct{})
        }
        start := time.Now()
        log.Warn(xerrors.Errorf("[qz2.6]: start to commit :%v", start))

        select { // use whichever is available
        case sb.commitTasks <- call: // 远程work产生复制证明凭据
            proof, err = sb.sealCommitRemote(call)
            log.Warn(xerrors.Errorf("[qz2.7]: remote commit :%v", time.Since(start).Milliseconds()))

        case rl <- struct{}{}:  // 默认本地work产生复制证明的凭据,内部主要是调用 rust部分的代码
            proof, err = sb.sealCommitLocal(sectorID, ticket, seed, pieces, rspco)
            log.Warn(xerrors.Errorf("[qz2.8]: local commit time :%v", time.Since(start).Milliseconds()))

        }
    }
    if err != nil {
        return nil, xerrors.Errorf("commit: %w", err)
    }

    return proof, nil
}

等待链上的消息,之后产生复制证明的凭据,并广播到链上去

CommitWait

主要是接受链上的消息,判断状态,将扇区状态更改为 proving,存储成功

func (m *Miner) handleCommitWait(ctx context.Context, sector SectorInfo) *sectorUpdate {
    ...
    // 等待链上广播来的消息
    mw, err := m.api.StateWaitMsg(ctx, *sector.CommitMessage)
    if err != nil {
        return sector.upd().to(api.CommitFailed).error(xerrors.Errorf("failed to wait for porep inclusion: %w", err))
    }

   // 判断状态
    if mw.Receipt.ExitCode != 0 {
        log.Errorf("UNHANDLED: submitting sector proof failed (exit=%d, msg=%s) (t:%x; s:%x(%d); p:%x)", mw.Receipt.ExitCode, sector.CommitMessage, sector.Ticket.TicketBytes, sector.Seed.TicketBytes, sector.Seed.BlockHeight, sector.Proof)
        return sector.upd().fatal(xerrors.Errorf("UNHANDLED: submitting sector proof failed (exit: %d)", mw.Receipt.ExitCode))
    }
    // 最终产生算力,更改扇区状态
    return sector.upd().to(api.Proving).state(func(info *SectorInfo) {
    })
}

以上是关于FileCoin Lotus复制证明 PoRep 源码梳理的主要内容,如果未能解决你的问题,请参考以下文章

FileCoin 挖矿教程 Lotus 源码大致的结构目录

盘古开源详细解读Filecoin复制证明与时空证明机制

Filecoin官方信息|Lotus节点的演变

FileCoin 挖矿教程之四:日常维护

盘古开源解析:智能合约在Filecoin中真正价值

Filecoin系列 - golang实现版本Louts