package storageadapter import ( "bytes" "context" "sync" "github.com/ipfs/go-cid" "golang.org/x/xerrors" "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-fil-markets/storagemarket" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/chain/actors/builtin/market" "github.com/filecoin-project/lotus/chain/actors/builtin/miner" "github.com/filecoin-project/lotus/chain/events" "github.com/filecoin-project/lotus/chain/types" ) type sectorCommittedEventsAPI interface { Called(check events.CheckFunc, msgHnd events.MsgHandler, rev events.RevertHandler, confidence int, timeout abi.ChainEpoch, mf events.MsgMatchFunc) error } func OnDealSectorPreCommitted(ctx context.Context, api getCurrentDealInfoAPI, eventsApi sectorCommittedEventsAPI, provider address.Address, dealID abi.DealID, proposal market.DealProposal, publishCid *cid.Cid, callback storagemarket.DealSectorPreCommittedCallback) error { // Ensure callback is only called once var once sync.Once cb := func(sectorNumber abi.SectorNumber, isActive bool, err error) { once.Do(func() { callback(sectorNumber, isActive, err) }) } // First check if the deal is already active, and if so, bail out checkFunc := func(ts *types.TipSet) (done bool, more bool, err error) { di, isActive, publishTs, err := checkIfDealAlreadyActive(ctx, api, ts, dealID, proposal, publishCid) if err != nil { // Note: the error returned from here will end up being returned // from OnDealSectorPreCommitted so no need to call the callback // with the error return false, false, err } if isActive { // Deal is already active, bail out cb(0, true, nil) return true, false, nil } // Check that precommits which landed between when the deal was published // and now don't already contain the deal we care about. // (this can happen when the precommit lands vary quickly (in tests), or // when the client node was down after the deal was published, and when // the precommit containing it landed on chain) if publishTs == types.EmptyTSK { lookup, err := api.StateSearchMsg(ctx, *publishCid) if err != nil { return false, false, err } if lookup != nil { // can be nil in tests publishTs = lookup.TipSet } } diff, err := api.diffPreCommits(ctx, provider, publishTs, ts.Key()) if err != nil { return false, false, err } for _, info := range diff.Added { for _, d := range info.Info.DealIDs { if d == di { cb(info.Info.SectorNumber, false, nil) return true, false, nil } } } // Not yet active, start matching against incoming messages return false, true, nil } // Watch for a pre-commit message to the provider. matchEvent := func(msg *types.Message) (bool, error) { matched := msg.To == provider && msg.Method == miner.Methods.PreCommitSector return matched, nil } // The deal must be accepted by the deal proposal start epoch, so timeout // if the chain reaches that epoch timeoutEpoch := proposal.StartEpoch + 1 // Check if the message params included the deal ID we're looking for. called := func(msg *types.Message, rec *types.MessageReceipt, ts *types.TipSet, curH abi.ChainEpoch) (more bool, err error) { defer func() { if err != nil { cb(0, false, xerrors.Errorf("handling applied event: %w", err)) } }() // If the deal hasn't been activated by the proposed start epoch, the // deal will timeout (when msg == nil it means the timeout epoch was reached) if msg == nil { err = xerrors.Errorf("deal %d was not activated by proposed deal start epoch %d", dealID, proposal.StartEpoch) return false, err } // Ignore the pre-commit message if it was not executed successfully if rec.ExitCode != 0 { return true, nil } // Extract the message parameters var params miner.SectorPreCommitInfo if err := params.UnmarshalCBOR(bytes.NewReader(msg.Params)); err != nil { return false, xerrors.Errorf("unmarshal pre commit: %w", err) } // When the deal is published, the deal ID may change, so get the // current deal ID from the publish message CID dealID, _, _, err = GetCurrentDealInfo(ctx, ts, api, dealID, proposal, publishCid) if err != nil { return false, err } // Check through the deal IDs associated with this message for _, did := range params.DealIDs { if did == dealID { // Found the deal ID in this message. Callback with the sector ID. cb(params.SectorNumber, false, nil) return false, nil } } // Didn't find the deal ID in this message, so keep looking return true, nil } revert := func(ctx context.Context, ts *types.TipSet) error { log.Warn("deal pre-commit reverted; TODO: actually handle this!") // TODO: Just go back to DealSealing? return nil } if err := eventsApi.Called(checkFunc, called, revert, int(build.MessageConfidence+1), timeoutEpoch, matchEvent); err != nil { return xerrors.Errorf("failed to set up called handler: %w", err) } return nil } func OnDealSectorCommitted(ctx context.Context, api getCurrentDealInfoAPI, eventsApi sectorCommittedEventsAPI, provider address.Address, dealID abi.DealID, sectorNumber abi.SectorNumber, proposal market.DealProposal, publishCid *cid.Cid, callback storagemarket.DealSectorCommittedCallback) error { // Ensure callback is only called once var once sync.Once cb := func(err error) { once.Do(func() { callback(err) }) } // First check if the deal is already active, and if so, bail out checkFunc := func(ts *types.TipSet) (done bool, more bool, err error) { _, isActive, _, err := checkIfDealAlreadyActive(ctx, api, ts, dealID, proposal, publishCid) if err != nil { // Note: the error returned from here will end up being returned // from OnDealSectorCommitted so no need to call the callback // with the error return false, false, err } if isActive { // Deal is already active, bail out cb(nil) return true, false, nil } // Not yet active, start matching against incoming messages return false, true, nil } // Match a prove-commit sent to the provider with the given sector number matchEvent := func(msg *types.Message) (matched bool, err error) { if msg.To != provider || msg.Method != miner.Methods.ProveCommitSector { return false, nil } var params miner.ProveCommitSectorParams if err := params.UnmarshalCBOR(bytes.NewReader(msg.Params)); err != nil { return false, xerrors.Errorf("failed to unmarshal prove commit sector params: %w", err) } return params.SectorNumber == sectorNumber, nil } // The deal must be accepted by the deal proposal start epoch, so timeout // if the chain reaches that epoch timeoutEpoch := proposal.StartEpoch + 1 called := func(msg *types.Message, rec *types.MessageReceipt, ts *types.TipSet, curH abi.ChainEpoch) (more bool, err error) { defer func() { if err != nil { cb(xerrors.Errorf("handling applied event: %w", err)) } }() // If the deal hasn't been activated by the proposed start epoch, the // deal will timeout (when msg == nil it means the timeout epoch was reached) if msg == nil { err := xerrors.Errorf("deal %d was not activated by proposed deal start epoch %d", dealID, proposal.StartEpoch) return false, err } // Ignore the prove-commit message if it was not executed successfully if rec.ExitCode != 0 { return true, nil } // Get the deal info _, sd, _, err := GetCurrentDealInfo(ctx, ts, api, dealID, proposal, publishCid) if err != nil { return false, xerrors.Errorf("failed to look up deal on chain: %w", err) } // Make sure the deal is active if sd.State.SectorStartEpoch < 1 { return false, xerrors.Errorf("deal wasn't active: deal=%d, parentState=%s, h=%d", dealID, ts.ParentState(), ts.Height()) } log.Infof("Storage deal %d activated at epoch %d", dealID, sd.State.SectorStartEpoch) cb(nil) return false, nil } revert := func(ctx context.Context, ts *types.TipSet) error { log.Warn("deal activation reverted; TODO: actually handle this!") // TODO: Just go back to DealSealing? return nil } if err := eventsApi.Called(checkFunc, called, revert, int(build.MessageConfidence+1), timeoutEpoch, matchEvent); err != nil { return xerrors.Errorf("failed to set up called handler: %w", err) } return nil } func checkIfDealAlreadyActive(ctx context.Context, api getCurrentDealInfoAPI, ts *types.TipSet, dealID abi.DealID, proposal market.DealProposal, publishCid *cid.Cid) (abi.DealID, bool, types.TipSetKey, error) { di, sd, publishTs, err := GetCurrentDealInfo(ctx, ts, api, dealID, proposal, publishCid) if err != nil { // TODO: This may be fine for some errors return 0, false, types.EmptyTSK, xerrors.Errorf("failed to look up deal on chain: %w", err) } // Sector with deal is already active if sd.State.SectorStartEpoch > 0 { return 0, true, publishTs, nil } // Sector was slashed if sd.State.SlashEpoch > 0 { return 0, false, types.EmptyTSK, xerrors.Errorf("deal %d was slashed at epoch %d", dealID, sd.State.SlashEpoch) } return di, false, publishTs, nil }