Skip to main content

SAAS under the hood - How does it work?

caution

Welcome to this technical reference guide on the inner workings of Sealing-as-a-Service (SAAS). This page serves purely as a reference point and is specifically crafted for those who already possess a deep understanding of Filecoin. If your objective is merely to begin your journey with SAAS, we recommend you to consult the tailored documentation for your intended role. Are you a Sealer or a Storage Provider? If you're uncertain, please revisit the introductory chapter to clarify your position!

tip

The information detailed on this page is universal to all Sealing-as-a-Service (SAAS) implementations, providing a comprehensive view of the foundational inner workings of lotus and lotus-miner, which SAAS is built upon. Specific details related to web3mine's implementation, be it software designed for Sealers or for Storage Providers, are addressed on their respective dedicated pages.

A part of this page is copied from Sealing-as-a-service workgroup.

If you need more info, please join our Discord, or join us on Filecoin slack, #fil-sealing-as-a-service

Web3mine's sealing-as-a-service implementation extends on Lotus APIs that enable importing partially-sealed sectors to a miner node.

Example banner

The process of sealing a sector consists from a number of relatively simple steps:

  • (non-cc) Gather deal data
  • Assign a sector number
  • Get seal randomness (Ticket)
  • Perform PreCommit1 (TreeD + Generating SDR Labels)
  • Perform PreCommit2 (Encode Sealed/TreeRLast + TreeC)
  • Send a PreCommit message to the on-chain miner actor
  • Wait for PreCommitChallengeDelay epochs (150 on mainnet)
  • Generate interactive challenge seed (Seed)
  • Perform Commit1 (Read vanilla PoRep proof)
  • Perform Commit2 (Generate PoRep snark of the generated vanilla proof)
  • Send a Commit message to the on-chain miner actor

Gathering deal data

In this step the unsealed sector file is created. This file contains fr32 padded data to be sealed

fr32 is a 32-bit representation of a field element (which, in our case, is the arithmetic field of BLS12-381). To be well-formed, a value of type Fr32 must actually fit within that field, but this is not enforced by the type system. It is an invariant which must be preserved by correct usage. In the case of so-called Fr32 padding, two zero bits are inserted ‘after’ a number requiring at most 254 bits to represent. This guarantees that the result will be Fr32, regardless of the value of the initial 254 bits. This is a ‘conservative’ technique, since for some initial values, only one bit of zero-padding would actually be required.

A sector can contain zero or more pieces (deals). Pieces must be power-of-two size after fr32 padding, and they must be naturally aligned in the sector.

CC sectors are sectors where the unsealed file data is all null-bytes (0x00)

Assigning sector number

In filecoin each sector is also referred to as a “replica”, a unique copy of data. The process of sealing (replication) is exists to convince the network that the replica was created correctly.

Replicas are unique by “Replica ID”, which is created from:

  • Proved ID (on-chain miner actor number)
  • Sector ID (sector number)
  • Ticket
  • CommD (top of the merkle-tree built over unsealed data)
  • PoRep Type

Sector Numbers essentially are used to enforce that each sector created by a miner is a unique copy. A side-effect of this is that SPs now need to manage this number space.

Part of the sealing process is to select a number to assign to a sector.

It’s also worth noting that having sector numbers be assigned in blocks in highly preferred as some on-chain operations process sectors “in bulk”, by performing operations directly on RLE representations of lists of sectors - doing so results in significant chain gas savings

Generating sealing ticket

The sealing ticket “anchors” the sector to a chain at specific point in time:

  • Sector with ticket X is guaranteed to have been created after the tipset which was used to generate the ticket was mined
  • Sector with ticket X from a given chain commits to that chain - it can’t be committed to a fork

Ticket is just 32 bytes of randomness (technically fr32, so 254 bits)

Example ticket can be generated with lotus chain node API like so:

ts, _ := client.ChainHead(ctx)

var maddr address.Address = [miner address]

buf := new(bytes.Buffer)
maddr.MarshalCBOR(buf)

ticketEpoch := ts.Height() - policy.SealRandomnessLookback
rand, err := client.StateGetRandomnessFromTickets(ctx, crypto.DomainSeparationTag_SealRandomness, ticketEpoch, buf.Bytes(), ts.Key())

PreCommit1

Call seal_pre_commit_phase1 on rust-filecoin-proofs, or PreCommit1 on filecoin-ffi or one of lotus sealing abstraction layers.

PreCommit2

Call seal_pre_commit_phase2 on rust-filecoin-proofs, or PreCommit2 on filecoin-ffi or one of lotus sealing abstraction layers.

Sending PreCommit message

The PreCommit message is essentially the prover telling the chain that they will be able to prove that they have a replica with a certain CommD/R, and produce a PoRep for that sector in some amount of time. A small deposit is required for added security. This deposit will be burnt if the sector is not proven on time.

PreCommit deposit can be calculated with the StateMinerPreCommitDepositForPower method

Waiting for interactive challenge

After the PreCommit message executes on-chain, the prover needs to wait for PreCommitChallengeDelay epochs (150 on mainnet) for the interactive challenge (seed) to become available.

Generating interactive challenge randomness

The interactive challenge is randomness derived from drand, and can only be generated after PreCommitting to the sector on-chain.

Example interactive seed can be generated with lotus chain node API like so:

ts, _ := [precommit msg execution tipset + 150 epochs (or later)]

var maddr address.Address = [miner address]

buf := new(bytes.Buffer)
maddr.MarshalCBOR(buf)

randHeight := pci.PreCommitEpoch + policy.GetPreCommitChallengeDelay()

rand, err := client.StateGetRandomnessFromBeacon(ctx, crypto.DomainSeparationTag_InteractiveSealChallengeSeed, randHeight, buf.Bytes(), ts.Key())

Commit1

Call seal_commit_phase1 on rust-filecoin-proofs, or Commit1 on filecoin-ffi or one of lotus sealing abstraction layers.

Commit2

Call seal_commit_phase2 on rust-filecoin-proofs, or Commit2 on filecoin-ffi or one of lotus sealing abstraction layers.

Sending the Commit message

The commit message sends the snarked proof that a replica was created correctly to the on-chain miner actor.

A deposit may need to be included with the message if the available miner actor balance is not high enough to cover it.

Sector number management

By default lotus-miner will assign sector numbers sequentially to sectors it’s sealing. To prevent sector numbers from colliding with externally sealed sectors, it’s possible to reserve ranges of sector numbers, using either the ‘SectorNum*’ api methods, or with the ‘lotus-miner sectors numbers’ commands

  • SectorNum methods

    SectorNumFree

    SectorNumFree drops a sector reservation Perms: admin Inputs:
    [
    "reservation-name"
    ]

    Response: {}

    SectorNumReservations

    SectorNumReservations returns a list of sector number reservations Perms: read Inputs: null Response:
    {
    "reservation-name": [5, 3],
    "other-reservation-name": [12, 5]
    }

    SectorNumReserve

    SectorNumReserve creates a new sector number reservation. Will fail if any other reservation has colliding numbers or name. Set force to true to override safety checks. Valid characters for name: a-z, A-Z, 0-9, _, - Perms: admin Inputs:
    [
    "reservation-name", # name
    [5, 3], # to reserve (json-rle)
    false # force - set this to false in basically all cases
    ]

    Response: {}

    SectorNumReserveCount

    SectorNumReserveCount creates a new sector number reservation for count sector numbers. by default lotus will allocate lowest-available sector numbers to the reservation. For restrictions on name see SectorNumReserve Perms: admin Inputs:
    [
    "reservation-name", # name
    42 # number of SectorNumbers to reserve
    ]

    Response - json-rle
    [5, 42]

    On json-rle

    Sector number ranges are handled as bitfields encoded as an array of run-lengths, always starting with zeroes (absent values) E.g.: The set {0, 1, 2, 8, 9} is the bitfield 1110000011, and would be marshalled as [0, 3, 5, 2] In Go those lists can be unmarshalled with go-bitfield, in other languages it should be easy to transform this run-length list into any other format with a few map/reduce calls.

Partially sealed sector import

https://github.com/filecoin-project/lotus/pull/9210 adds "just" one new miner RPC method - SectorReceive

This method makes it possible to import sealed, or partially sealed sectors into lotus-miner instances

SectorReceive(ctx context.Context, meta RemoteSectorMeta) error //perm:admin

type RemoteSectorMeta struct {
////////
// BASIC SECTOR INFORMATION

// State specifies the first state the sector will enter after being imported
// Must be one of the following states:
// * Packing
// * GetTicket
// * PreCommitting
// * SubmitCommit
// * Proving/Available
State SectorState

Sector abi.SectorID
Type abi.RegisteredSealProof

////////
// SEALING METADATA
// (allows lotus to continue the sealing process)

// Required in Packing and later
Pieces []SectorPiece // todo better type?

// Required in PreCommitting and later
TicketValue abi.SealRandomness
TicketEpoch abi.ChainEpoch
PreCommit1Out storiface.PreCommit1Out // todo specify better

CommD *cid.Cid
CommR *cid.Cid // SectorKey

// Required in SubmitCommit and later
PreCommitInfo *miner.SectorPreCommitInfo
PreCommitDeposit *big.Int
PreCommitMessage *cid.Cid
PreCommitTipSet types.TipSetKey

SeedValue abi.InteractiveSealRandomness
SeedEpoch abi.ChainEpoch

CommitProof []byte

// Required in Proving/Available
CommitMessage *cid.Cid

// Optional sector metadata to import
Log []SectorLog

////////
// SECTOR DATA SOURCE

// Sector urls - lotus will use those for fetching files into local storage

// Required in all states
DataUnsealed *storiface.SectorLocation

// Required in PreCommitting and later
DataSealed *storiface.SectorLocation
DataCache *storiface.SectorLocation

////////
// SEALING SERVICE HOOKS

// URL
// RemoteCommit1Endpoint is an URL of POST endpoint which lotus will call requesting Commit1 (seal_commit_phase1)
// request body will be json-serialized RemoteCommit1Params struct
RemoteCommit1Endpoint string

// RemoteCommit2Endpoint is an URL of POST endpoint which lotus will call requesting Commit2 (seal_commit_phase2)
// request body will be json-serialized RemoteCommit2Params struct
RemoteCommit2Endpoint string

// RemoteSealingDoneEndpoint is called after the sector exists the sealing pipeline
// request body will be json-serialized RemoteSealingDoneParams struct
RemoteSealingDoneEndpoint string
}

type SectorPiece struct {
Piece abi.PieceInfo
DealInfo *PieceDealInfo // nil for pieces which do not appear in deals (e.g. filler pieces)
}

type PieceDealInfo struct {
PublishCid *cid.Cid
DealID abi.DealID
DealProposal *market.DealProposal
DealSchedule DealSchedule
KeepUnsealed bool
}

// DealSchedule communicates the time interval of a storage deal. The deal must
// appear in a sealed (proven) sector no later than StartEpoch, otherwise it
// is invalid.
type DealSchedule struct {
StartEpoch abi.ChainEpoch
EndEpoch abi.ChainEpoch
}

type SectorLocation struct {
// Local when set to true indicates to lotus that sector data is already
// available locally; When set lotus will skip fetching sector data, and
// only check that sector data exists in sector storage
Local bool

// URL to the sector data
// For sealed/unsealed sector, lotus expects octet-stream
// For cache, lotus expects a tar archive with cache files
// Valid schemas:
// - http:// / https://
URL string

// optional http headers to use when requesting sector data
Headers []SecDataHttpHeader
}
  • Example parameters as json (values are not all valid):
    [
    {
    # State specifies the first state the sector will enter after being imported
    # Must be one of the following states:
    # * Packing
    # * GetTicket
    # * PreCommitting
    # * SubmitCommit
    # * Proving/Available
    "State": "PreCommitting",
    "Sector": {
    "Miner": 1000,
    "Number": 9
    },
    "Type": 8, # RegisteredSealProof

    # Required in Packing and later
    "Pieces": [
    {
    "Piece": {
    "Size": 1032,
    "PieceCID": {"/": "baga6ea4seaqao7s73y24kcutaosvacpdjgfe5pw76ooefnyqw4ynr3d2y6x2mpq"}, # CommP
    },
    "DealInfo": {
    "PublishCid": null,
    "DealID": 5432,
    "DealProposal": {
    "PieceCID": {"/": "baga6ea4seaqao7s73y24kcutaosvacpdjgfe5pw76ooefnyqw4ynr3d2y6x2mpq"}, # CommP
    "PieceSize": 1032,
    "VerifiedDeal": true,
    "Client": "f01234",
    "Provider": "f01000",
    "Label": "",
    "StartEpoch": 10101,
    "EndEpoch": 1010100,
    "StoragePricePerEpoch": "0",
    "ProviderCollateral": "0",
    "ClientCollateral": "0"
    },
    "DealSchedule": {
    "StartEpoch": 10101,
    "EndEpoch": 1010100
    },
    "KeepUnsealed": true
    }
    }
    ],

    # Required in PreCommitting and later
    "TicketValue": "Bw==", # base64, 32 bytes of ticket randomness
    "TicketEpoch": 10000,
    "PreCommit1Out": "Bw==", # see (1) below
    "CommD": {"/": "baga6ea4seaqao7s73y24kcutaosvacpdjgfe5pw76ooefnyqw4ynr3d2y6x2mpq"},
    "CommR": {"/": "bagboea4b5abcbwhjwgl3vo54uefgs6gq5qkjmar3xg5st56ippg7vzy4nwkxubs3"},

    # Required in SubmitCommit and later
    "PreCommitInfo": {
    "SealProof": 8,
    "SectorNumber": 9,
    "SealedCID": {"/": "bagboea4b5abcbwhjwgl3vo54uefgs6gq5qkjmar3xg5st56ippg7vzy4nwkxubs3"},
    "SealRandEpoch": 10101,
    "DealIDs": [
    5432
    ],
    "Expiration": 10101,
    "UnsealedCid": null
    },
    "PreCommitDeposit": "0",
    "PreCommitMessage": null,
    "PreCommitTipSet": [{"/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4"}, {"/":"bafy2bzacebp3shtrn43k7g3unredz7fxn4gj533d3o43tqn2p2ipxxhrvchve"}],
    "SeedValue": "Bw==", # base64, 32 bytes of ticket randomness
    "SeedEpoch": 10101,
    "CommitProof": "Ynl0ZSBhcnJheQ==", # see (2) below

    # Required in Proving/Available
    "CommitMessage": {"/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4"},

    # Optional sector metadata to import
    "Log": [
    {
    "Kind": "string value",
    "Timestamp": 42,
    "Trace": "string value",
    "Message": "string value"
    }
    ],

    # Sector data references - lotus will use those for fetching files into local storage
    # * When Local is false, lotus will fetch sector data from provided URLs
    # * When Local is true, lotus will assume that sector data is available in the lotus storage system already (e.g. through a SaaS sidecar fetching and declaring sector data to the sector index before calling SectorReceive). This is useful for SaaS implementations which want to use more advanced file transport methods.
    "DataUnsealed": {
    "Local": false,
    "URL": "<http://example.com/something/something/miner-567/sector-unsealed-123>", # http GET, will be executed by lotus-miner or one of lotus-workers when the DownloadSector task type is enabled; Expects octet-stream with the "unsealed" sector file
    "Headers": [
    {
    "Key": "X-my-header",
    "Value": "my-header-value"
    }
    ]
    },
    "DataSealed": {
    "Local": false,
    "URL": "<http://example.com/something/something/miner-567/sector-sealed-123>", # http GET, will be executed by lotus-miner or one of lotus-workers when the DownloadSector task type is enabled; Expects octet-stream with the "sealed" sector file; File names and sizes must match constraints defined in <https://github.com/filecoin-project/lotus/blob/6b663d33854a8797435a27de9fa278560d9a56ca/storage/sealer/tarutil/systar.go#L17-L72>
    "Headers": []
    },
    "DataCache": {
    "Local": false,
    "URL": "<http://example.com/something/something/miner-567/sector-cache-123>", # http GET, will be executed by lotus-miner or one of lotus-workers when the DownloadSector task type is enabled; returned data must be a TAR of the "cache" sector directory
    "Headers": []
    },

    # Sealing service hooks (all are optional)
    "RemoteCommit1Endpoint": "<http://example.com/something/something/miner-567/c1>", # http POST, request body is json-ified <https://github.com/filecoin-project/lotus/blob/6b663d33854a8797435a27de9fa278560d9a56ca/api/api_storage.go#L584-L591>, return value is json of <https://github.com/filecoin-project/rust-filecoin-proofs-api/blob/11126c5b91dca942712cf632917a0027634dfddf/src/seal.rs#L154-L162> (same as in (2) below)
    "RemoteCommit2Endpoint": "<http://example.com/something/something/miner-567/c2>", # http POST, request body is json <https://github.com/filecoin-project/lotus/blob/6b663d33854a8797435a27de9fa278560d9a56ca/api/api_storage.go#L593-L599>, Commit1Out is described in (2) below
    "RemoteSealingDoneEndpoint": "<http://example.com/something/something/miner-567/done>" # http POST, request body is <https://github.com/filecoin-project/lotus/blob/6b663d33854a8797435a27de9fa278560d9a56ca/api/api_storage.go#L601-L611>
    }
    ]

Example external sector sealing flow:

First connect to lotus and lotus-miner RPC:

client := ...
miner := ...

Reserve some sector numbers:

snums, err := miner.SectorNumReserveCount(ctx, "test-reservation-0001", 16)

Load on-chain miner info

    maddr, err := miner.ActorAddress(ctx)
require.NoError(t, err)

mid, err := address.IDFromAddress(maddr)
require.NoError(t, err)

mi, err := client.StateMinerInfo(ctx, maddr, types.EmptyTSK)
require.NoError(t, err)
ver, err := client.StateNetworkVersion(ctx, types.EmptyTSK)
require.NoError(t, err)
spt, err := lminer.PreferredSealProofTypeFromWindowPoStType(ver, mi.WindowPoStProofType)
require.NoError(t, err)

ssize, err := spt.SectorSize()
require.NoError(t, err)

Get a sector number from the reservation

sn, err := snums.First()

Create sector identifiers which we’ll use in next steps

    snum := abi.SectorNumber(sn)
sid := abi.SectorID{Miner: abi.ActorID(mid), Number: snum}
sref := storiface.SectorRef{ID: sid, ProofType: spt}

Make a sealer instance

sealer, err := ffiwrapper.New(&basicfs.Provider{
Root: sectorDir,
})

Get a data reader (in this case all-zero data for CC sector)

dataReader := bytes.NewReader(bytes.Repeat([]byte{0}, int(pieceSize.Unpadded())))

Create the unsealed sector file from the data

pieceInfo, err := sealer.AddPiece(ctx, sref, nil, pieceSize.Unpadded(), dataReader)

Create a sealing ticket

    // get most recent valid ticket epoch
ts, err := client.ChainHead(ctx)
require.NoError(t, err)
ticketEpoch := ts.Height() - policy.SealRandomnessLookback

// ticket entropy is cbor-seriasized miner addr
buf := new(bytes.Buffer)
require.NoError(t, maddr.MarshalCBOR(buf))

// generate ticket randomness
rand, err := client.StateGetRandomnessFromTickets(ctx, crypto.DomainSeparationTag_SealRandomness, ticketEpoch, buf.Bytes(), ts.Key())
require.NoError(t, err)

Do PreCommit1 / PreCommit2

    // run PC1
pc1out, err := sealer.SealPreCommit1(ctx, sref, abi.SealRandomness(rand), []abi.PieceInfo{pieceInfo})
require.NoError(t, err)

// run pc2
scids, err := sealer.SealPreCommit2(ctx, sref, pc1out)
require.NoError(t, err)

Make finalized cache

    // make finalized cache, put it in [sectorDir]/fin-cache while keeping the large cache for remote C1
finDst := filepath.Join(sectorDir, "fin-cache", fmt.Sprintf("s-t01000-%d", snum))
require.NoError(t, os.MkdirAll(finDst, 0777))
require.NoError(t, sealer.FinalizeSectorInto(ctx, sref, finDst))

Now we need to make sure that the sealed sector files can be fetched by the miner instance we will be sending the sector to, for that we can setup a very simple http service with a few endpoints:

m := mux.NewRouter()
m.HandleFunc("/sectors/{type}/{id}", remoteGetSector(sectorDir)).Methods("GET")
m.HandleFunc("/sectors/{id}/commit1", remoteCommit1(sealer)).Methods("POST")

Endpoint serving sector data:

func remoteGetSector(sectorRoot string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {

vars := mux.Vars(r)

// validate sector id
id, err := storiface.ParseSectorID(vars["id"])
if err != nil {
w.WriteHeader(500)
return
}

// validate type
_, err = spaths.FileTypeFromString(vars["type"])
if err != nil {
w.WriteHeader(500)
return
}

typ := vars["type"]
if typ == "cache" {
// if cache is requested, send the finalized cache we've created above
typ = "fin-cache"
}

path := filepath.Join(sectorRoot, typ, vars["id"])

stat, err := os.Stat(path)
if err != nil {
w.WriteHeader(500)
return
}

if stat.IsDir() {
if _, has := r.Header["Range"]; has {
w.WriteHeader(500)
return
}

w.Header().Set("Content-Type", "application/x-tar")
w.WriteHeader(200)

err := tarutil.TarDirectory(path, w, make([]byte, 1<<20))
if err != nil {
return
}
} else {
w.Header().Set("Content-Type", "application/octet-stream")
// will do a ranged read over the file at the given path if the caller has asked for a ranged read in the request headers.
http.ServeFile(w, r, path)
}

fmt.Printf("served sector file/dir, sectorID=%+v, fileType=%s, path=%s\n", id, vars["type"], path)
}
}

Endpoint responding to Commit1 requests:

func remoteCommit1(s *ffiwrapper.Sealer) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)

// validate sector id
id, err := storiface.ParseSectorID(vars["id"])
if err != nil {
w.WriteHeader(500)
return
}

var params api.RemoteCommit1Params
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
w.WriteHeader(500)
return
}

sref := storiface.SectorRef{
ID: id,
ProofType: params.ProofType,
}

ssize, err := params.ProofType.SectorSize()
if err != nil {
w.WriteHeader(500)
return
}

p, err := s.SealCommit1(r.Context(), sref, params.Ticket, params.Seed, []abi.PieceInfo{
{
Size: abi.PaddedPieceSize(ssize),
PieceCID: params.Unsealed,
},
}, storiface.SectorCids{
Unsealed: params.Unsealed,
Sealed: params.Sealed,
})
if err != nil {
w.WriteHeader(500)
return
}

if _, err := w.Write(p); err != nil {
fmt.Println("c1 write error")
}
}
}

We also can, but don’t have to, create some additional endpoints for remote commit2, and for sealing-done notification:

m.HandleFunc("/sectors/{id}/sealed", remoteDone(doneResp)).Methods("POST")
m.HandleFunc("/commit2", remoteCommit2(sealer)).Methods("POST")

Commit2 handler:

func remoteCommit2(s *ffiwrapper.Sealer) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var params api.RemoteCommit2Params
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
w.WriteHeader(500)
return
}

sref := storiface.SectorRef{
ID: params.Sector,
ProofType: params.ProofType,
}

p, err := s.SealCommit2(r.Context(), sref, params.Commit1Out)
if err != nil {
fmt.Println("c2 error: ", err)
w.WriteHeader(500)
return
}

if _, err := w.Write(p); err != nil {
fmt.Println("c2 write error")
}
}
}

Sealing done notification:

func remoteDone(rs **api.RemoteSealingDoneParams) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
*rs = new(api.RemoteSealingDoneParams)
if err := json.NewDecoder(r.Body).Decode(*rs); err != nil {
w.WriteHeader(500)
return
}

w.WriteHeader(200)
}
}

After the endpoints are taken care of, and we have sealed the sector to a point at which we want to send it to a storage provider we can start building remote sector metadata

First we’ll get a list of http endpoints for access to sector data:

    unsealedURL := fmt.Sprintf("%s/sectors/unsealed/s-t0%d-%d", srv.URL, mid, snum)
sealedURL := fmt.Sprintf("%s/sectors/sealed/s-t0%d-%d", srv.URL, mid, snum)
cacheURL := fmt.Sprintf("%s/sectors/cache/s-t0%d-%d", srv.URL, mid, snum)
remoteC1URL := fmt.Sprintf("%s/sectors/s-t0%d-%d/commit1", srv.URL, mid, snum)
remoteC2URL := fmt.Sprintf("%s/commit2", srv.URL)
doneURL := fmt.Sprintf("%s/sectors/s-t0%d-%d/sealed", srv.URL, mid, snum)

Next we’ll build the remote sector metadata:

sectorMeta := api.RemoteSectorMeta{
// sector state
State: "PreCommitting",

// sector id
Sector: sid,
Type: spt,

// piece information
Pieces: []api.SectorPiece{
{
Piece: pieceInfo,
DealInfo: nil,
},
},

// ticket
TicketValue: abi.SealRandomness(rand),
TicketEpoch: ticketEpoch,

// pc1 metadata
PreCommit1Out: pc1out,

// Data/Replica CIDs
CommD: &scids.Unsealed,
CommR: &scids.Sealed,

// Pointers to sector data
DataUnsealed: &storiface.SectorLocation{
Local: false,
URL: unsealedURL,
},
DataSealed: &storiface.SectorLocation{
Local: false,
URL: sealedURL,
},
DataCache: &storiface.SectorLocation{
Local: false,
URL: cacheURL,
},

// Remote Commit endpoints
RemoteCommit1Endpoint: remoteC1URL,
RemoteCommit2Endpoint: remoteC2URL,

// Done notification endpoint
RemoteSealingDoneEndpoint: doneURL,
}

Last, we call miner.SectorReceive(ctx, meta), and see the sector be imported, then continue going through remaining sealing steps.