Skip to content

feat: deprecate datacap for nv29 (FIP-0118 / Solstice) - #2132

Merged
snail8501 merged 24 commits into
mainfrom
feat/solstice
Oct 17, 2026
Merged

snail8501 merged 24 commits into
mainfrom
feat/solstice

Conversation

@snail8501

@snail8501 snail8501 commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Deprecate datacap for nv29 (FIP-0118 / Solstice), plus two DoS fixes

Pins lotus to v1.37.0-rc1, the release carrying nv29, and makes Boost behave
correctly once the network reaches it, where FIP-0118 freezes datacap and the
verified registry. It also carries two independent robustness fixes for
client-controlled fields that could crash boostd.

What nv29 changes

  • Datacap and verifreg are frozen. No allocation can be created or claimed,
    and no claim is written for a sector that activates at the upgrade or after.
  • Deal ids leave the pre-commit; deals do not leave the sector. FIP-0118
    removes deal_ids from SectorPreCommitInfo, so the miner actor's
    "sector has deal ids" rejection is never reached and a plain, unverified
    market deal still onboards
    . What changes is how the deal is linked: the
    commit carries a piece activation manifest instead, and at nv29
    processPieces drops only its VerifiedAllocationKey while keeping the
    Notify entry that wakes the market actor, whose SectorContentChanged
    still activates the deal.
  • Every sector gets 10× QAP from nv29 on, through the FULL_QA_POWER flag
    the actor sets at activation.
  • What does not survive is the verified half. Past nv29 an allocation can
    never become a claim, so a verified deal seals a sector whose client, term
    and datacap mean nothing on chain. Boost turns it away at proposal time
    (below) rather than let it seal.
  • The DDO (direct data onboarding) flow is FIL+ by construction, so from
    nv29 on there is no direct-data onboarding for Boost to serve at all.

Deal protocol

  • validateDealProposal refuses a verified proposal at nv29, after the
    signature and transfer validation and quoting FIP-0118. Unverified proposals
    are unaffected — they still onboard. The check sits inside the
    proposal.VerifiedDeal branch deliberately: the datacap actor survives the
    upgrade, so a client holding a leftover balance would otherwise pass every
    check below and be sealed at the verified ask, for power that no longer
    arrives. Flipping the CLI default alone would miss --verified and older
    clients.
  • boost deal --verified now defaults to false, with the deprecation in its
    usage string. Below nv29 it still works unchanged.

Direct deals

  • RejectedAtNv29(nv) is the shared predicate and DirectDealRejectionAtNv29
    the shared message, so the provider, the CLI and the deal filter all give one
    answer. The same constant names the provider's rejection.
  • Accept rejects every direct deal at nv29, explicitly, rather than falling
    through to an "allocation not found". Import reaches the same rejection
    through Accept before it reads anything from chain.
  • Below nv29 nothing changes: an allocation still decides everything, Import
    still reads it for the miner address, and the deal's VerifiedAllocationKey
    is still built into its manifest.
  • watchSealingUpdates now asks one function, DirectDealStatus
    (storagemarket/directdeal_onchain.go), whether the piece landed. It returns
    one of: sealing, landed, gone, not visible on this node, no claim, or claim on
    another sector. The gql sealing state and the migrate-curio migration both go
    through the same answer instead of re-deriving it.
  • The deal is dated by the sector's own FULL_QA_POWER flag (SectorOnboarded),
    never by the chain's current version and never by
    miner.SectorIsFullQaPower — that one derives full QA power from
    VerifiedDealWeight alone and so also calls a pre-nv29 datacap sector full QA
    power. FULL_QA_POWER is set on empty sectors too, so the flag is read
    together with a non-zero piece weight.
  • A non-zero entry.SectorID is itself the proof the piece reached a sector —
    Boost writes it only after addPieceWithRetry returns — so the sealer's piece
    list is no longer re-read to confirm it.
  • There is no timeout on the wait. A sealer only reports a final state after it
    has confirmed the sector on chain itself, so the chain read that follows is
    final by construction; a clock on top of that could only turn node lag into a
    verdict. A read failure or a sector this node cannot see is retryable and
    never becomes a verdict on the deal.
  • ErrNoClaimFound now means "a piece onboarded before nv29 has no claim", and
    is unreachable past nv29. Its wording is pinned by a test, because migrate-curio
    matches a deal's stored error as a string.

Migration to Curio

cmd/migrate-curio/migrate.go no longer trusts a deal's own fatal error as
evidence it failed: Boost used to write a deal off when it found no claim, which
after nv29 is every deal FIP-0118 handled. Exactly one fatal error is excused
— ErrNoClaimFound — and only for a sector that onboarded at or after nv29.
Sector dating is one StateMinerSectors for the whole run. If the chain cannot
be read the run aborts, rather than reading everything as pre-nv29 and silently
skipping the deals this change exists to rescue.

Where a claim no longer appears

Everything that told a successful seal from a failed one by the presence of a
claim now dates the sector instead, through SectorOnboarded:

  • piecedirectory/doctor.go: a missing claim for such a piece no longer reads
    as "gone from chain", which would otherwise skip the checks on every pass.
  • cmd/migrate-curio: as above.
  • gql: the direct-deal sealing state reports DirectDealStatus rather than
    "(No claim found)".
  • sectorstatemgr: reads both weight fields, since from nv29 the pieces land in
    VerifiedDealWeight.
  • direct deal rows in booster-http are still labelled by allocation id and
    sector; a direct deal always has a real allocation id, since nv29 deals are
    refused before one could be absent.

CLI

boostd import-direct refuses the import up front at nv29, with the provider's
own reason string, before it reads the CAR path or the allocation — the same
rejection, one step earlier, so an operator is told why before anything moves.
--allocation-id is unchanged.

boost direct-deal allocate and extend-claim refuse to build messages at
nv29; list-allocations still lists and warns on stderr, so its table stays
usable from a script.

Deal filtering

  • ConsiderVerifiedStorageDeals and ConsiderUnverifiedStorageDeals collapse
    into one switch at nv29, where no deal is verified: read separately,
    verified-on with unverified-off would reject every deal.
  • The external storage-deal filter document goes to 2.3.0 and carries
    NetworkVersion, so a script sorting FIL+ traffic can tell the chain moving
    from a client dropping datacap. The retrieval document stays 2.2.0: a
    version bump that arrives with no shape change only misleads. A document with
    no network version is refused rather than handed to a script, where a zero
    reads as a very old chain.

Robustness fix: reject retrieval requests with a nil selector

The graphsync wire schema marks the request selector optional, so a peer can
omit it and it decodes to a nil ipld.Node. dagcbor.Encode dereferences it
and panics, taking down the daemon. Two guards:

  • handleRetrievalDeal rejects the request before it is tracked as a transfer,
    so it is never registered as an active unpaid retrieval. The incoming-request
    hook then sees intercept == false and hands it to the legacy path.
  • requestValidator.acceptDeal rejects it immediately before dagcbor.Encode,
    as defense in depth.

Robustness fix: reject zero-size deal transfers

Transfer is not covered by the client's signature and had no validation at
all. With Transfer.Size == 0 the mismatch check in the HTTP transport is
skipped (it is guarded by DealSize != 0), dealSize falls through as 0 from
the HEAD response, chunkSize becomes 0, and outputStats.Size() / chunkSize
panics in a detached goroutine with no recover — crashing boostd. Fixed at
three layers:

  • validateDealProposal requires a positive Transfer.Size for online deals.
  • transfer.execute rejects a non-positive dealSize before computing
    chunkSize.
  • The transfer goroutine in Execute recovers panics and converts them into a
    transfer error, mirroring the existing recover() in
    DirectDealsProvider.execDeal.

Dependencies

lotus v1.37.0-rc1 (no replace pin: the release carries nv29 directly) and
go-state-types v0.19.1. Routine bumps come along for the ride: boxo 0.42.2,
go-libp2p-kad-dht 0.42.2, testify 1.12.1, x/crypto 0.55.0, x/text 0.41.0,
protobuf 1.36.12. go.sum is regenerated and go mod tidy -diff is clean.

Testing

  • Unit tests pass across every touched package: storagemarket, cmd/boostd,
    cmd/migrate-curio, db, gql, node/modules, sectorstatemgr,
    piecedirectory, transport/httptransport, storagemarket/dealfilter.
  • gofmt and go vet clean on the same set; go build ./... clean both in
    workspace mode and with GOWORK=off.
  • New direct-deal coverage: the nv29 accept/import rejection, the sector-flag
    read (including the pre-nv29 datacap sector that
    miner.SectorIsFullQaPower gets wrong), the empty-sector guard, the
    no-clock wait, the claim-mismatch and not-visible branches, the
    ErrNoClaimFound wording, and the migrate-curio migratableDirectDeal table
    from both sides of nv29.
  • itests: TestDirectDealSealingAcrossNv29 (accepted below the upgrade, sealed
    after it), TestDirectDealRejectedAtNv29, and TestLIDCleanup, which now
    crosses the upgrade mid-test and asserts both that the chain crossed and that
    the claim really is absent. The DDO ensembles set the upgrade height with
    kit.LatestActorsAt, because the default buildconstants.TestNetworkVersion
    is nv29 in this lotus and the verifreg setup the tests need cannot run there.
  • The piece-doctor tests sit behind the test_lid build tag and nothing in the
    CI matrix compiled them. A test-piece-doctor entry now runs the leveldb half
    of ./piecedirectory/... with --tags=debug,test_lid.
  • CI is green on this branch, including lid-docker-compose.

Known follow-ups

  • Curio's own direct-deal table has not been looked at here. The migration path
    was extended for post-nv29 deals, but no post-nv29 direct deal can be created,
    so this is a question about pre-existing rows rather than about this change.

@snail8501 snail8501 linked an issue Sep 19, 2026 that may be closed by this pull request
2 tasks
snail8501 added 5 commits September 19, 2026 18:43
verifreg/datacap is deprecated from nv29, so Boost stops offering the
features that depend on datacap. All gates branch on the on-chain
StateNetworkVersion, leaving pre-nv29 behaviour unchanged.

- add isNv29OrAbove() as the single nv29 decision point
- Accept() explicitly rejects DDO (FIL+ verified deals) at nv29+ instead of
  relying on an implicit allocation-not-found failure downstream
- Import() returns early when the deal was rejected, which also removes a
  nil allocation dereference
- CLI allocate errors out at nv29+: allocations can no longer be created
- CLI extend-claim errors out at nv29+: claims can no longer be extended
- CLI list-allocations marked [DEPRECATED at nv29]

Pins lotus to the feat/solstice branch (go.mod replace) and upgrades the
affected dependencies.

Refs #2106
The graphsync wire schema declares the selector optional, so a peer can
send a request that omits the field and have it decode to a nil selector.
That nil reaches dagcbor.Encode in acceptDeal and is dereferenced, panicking
on the responsemanager goroutine, which has no recover() and takes down the
boostd process.

An unauthenticated peer triggers this with a single ~470-byte retrieval
request: no deal, no escrowed funds and no stored piece are required.
Re-sending it after the supervised restart takes the process down again.

- acceptDeal() rejects a nil selector before encoding
- handleRetrievalDeal() intercepts it before the transfer is tracked, which
  also closes the secondary path where encoding.Encode would encode the nil
  as CBOR null
The Transfer descriptor in a DealProposal is not covered by the client
signature and is not validated anywhere. An attacker can submit a deal with
Transfer.Size=0 and a URL pointing at an HTTP server they control, making the
transport's chunkSize compute to 0 and triggering an integer divide-by-zero.
The panic happens on a transfer goroutine with no recover() and terminates
the boostd process.

The deal is already persisted with DealRetryAuto before the transfer runs, so
it re-executes and crashes again on every restart; the operator must delete
the deal from the database to recover.

- validateDealProposal() rejects Transfer.Size == 0 for online deals (size 0
  is legitimate for offline deals, preserved via !IsOffline)
- http_transport rejects a non-positive dealSize before the division, which
  also covers a peer answering with Content-Length: 0
- the transfer goroutine recovers panics, turning a process exit into a
  failed transfer
go.mod was updated without regenerating go.sum, so builds outside the
workspace failed with "missing go.sum entry" for lotus, go-state-types,
golang.org/x/crypto, boxo and mattn/go-isatty.

Regenerated with `GOWORK=off go mod tidy`; workspace mode does not write
go.sum, which is how the drift went unnoticed. go.work.sum is rebalanced
to match.
The itest ensemble starts at buildconstants.TestNetworkVersion, which is
nv29. With lotus pinned to the solstice line, nv29 now selects the real
solstice actors (manifest bafy2bzacedksulyk...) instead of aliasing the
v18 bundle, so verifreg enforces FIP-0118 and rejects AddVerifier. Both
TestDirectDeal and TestLIDCleanup go through
TestFramework.AddClientDataCap -> mkVerifier, so both failed with
ErrForbidden(18) "datacap is deprecated, adding new verifiers is no
longer supported".

Pin the ensemble to nv28, where nv maps to actors v18 (the pre-solstice
bundle), keeping the legacy datacap path these tests exercise. Verified
with a throwaway probe: AddVerifier exits 0 at nv28 and fails with the
same ErrForbidden(18) at nv29.

@LexLuthr LexLuthr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More changes are required. Please do a deep dive and thorough testing. Add more tests. I am also not sure about freezing NV28 for itests. It does not add any benefit to keep those tests around post NV29. Some of these tests need actual behaviour modification instead of freezing them in place. Some of these issue were probably not caught because of that.

  1. Boost deal defaults to verified. Let's flip that.
  2. Remove extend-claim command post NV29.
  3. A direct deal accepted before nv29 can finish sealing afterward without creating a claim. Piece doctor then clears its warning and skips checking whether its index or unsealed copy is missing.
  4. Migration skips a direct deal when it cannot find its claim. For deals sealed after nv29, that can leave the deal record and retrieval mapping out of Curio even though the data was successfully sealed.
  5. boostd import-direct needs the same early rejection.
  6. A successfully sealed deal can display Proving(No claim found) after nv29.

Comment thread storagemarket/direct_deals_provider.go
Comment thread transport/httptransport/http_transport.go Outdated
@BigLep BigLep added this to Solstice Sep 21, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in Solstice Sep 21, 2026
@github-project-automation github-project-automation Bot moved this from Backlog to In progress in Solstice Sep 21, 2026
@BigLep
BigLep requested a lite review from Copilot September 21, 2026 14:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

An unhandled nil allocation remains after a second state lookup, and requested regression coverage is missing.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

Updates Boost for Solstice/nv29 by deprecating datacap workflows and hardening retrieval and transfer handling.

Changes:

  • Reject deprecated datacap operations and direct deals at nv29+.
  • Validate selectors and transfer sizes, with transfer panic recovery.
  • Pin Solstice dependencies and refresh checksums and tests.
File Summary
transport/​httptransport/​http_transport.go Validates deal sizes and recovers transfer panics. Add coverage for zero Content-Length responses.
storagemarket/​direct_deals_provider.go Rejects nv29+ direct deals and avoids the initial nil allocation dereference. A second lookup still needs nil handling; add nv29 regression coverage.
storagemarket/​deal_acceptance.go Rejects zero-size online transfers. Add online and offline regression coverage.
retrievalmarket/​server/​validation.go Rejects nil selectors before encoding.
retrievalmarket/​server/​gsunpaidretrieval.go Rejects nil selectors before tracking transfers.
itests/​lid_cleanup_test.go Pins legacy cleanup tests to nv28.
itests/​ddo_test.go Pins direct-deal tests to nv28.
go.work.sum Updates workspace checksums.
go.sum Regenerates module checksums.
go.mod Pins Lotus to Solstice and updates dependencies.
cmd/​boost/​direct_deal.go Adds nv29 guards and deprecation messaging.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread transport/httptransport/http_transport.go
snail8501 added 2 commits September 22, 2026 23:39
FIP-0118 keeps verifreg out of sector activation, so a sector that proves at
nv29 is never given a claim. Boost read that absence as the deal having left
the chain, which failed healthy deals and skipped work on them in four places.
The fix is the same distinction everywhere: the chain's current version is the
right question for a new deal, and the sector's own activation height is the
right question for one that has already sealed.

- add RejectedAtNv29() and SealedAtOrAfterNv29() (plus Nv29UpgradeHeight())
  and keep the two questions apart
- resolveClaim() dates the deal's own sector, replacing two copies of the same
  lookup: a sector activated at or after nv29 completes the deal, one activated
  before it still owes its claim and still fails without one
- Accept() turns a direct deal away at nv29+ with an exported reason, and
  Import() returns on that rejection instead of dereferencing the nil allocation
  StateGetAllocation reports for an allocation that does not exist
- validateDealProposal() turns a verified deal away at nv29+, where flipping the
  client default is not enough because --verified and older clients still arrive
- the piece doctor no longer unflags a nv29 piece and returns before the
  unsealed-copy and index checks, which it did on every pass for a healthy deal
- the GQL sealing state no longer appends "(No claim found)" for a sector that
  activated at or after nv29
- migrateDDODeals no longer strands a deal sealed at nv29 outside Curio just
  because its claim is missing; it decides per deal, dating the deal's sector
- allocate and extend-claim refuse at nv29+, list-allocations is marked, and
  boostd import-direct rejects up front with the provider's own reason string
  rather than a paraphrase; --verified now defaults to false
- cover a zero Content-Length response in the http transport, and the source of
  the size rejection ahead of the division

Tests come with each path above, and the direct-deal itests now start at nv28
and cross to nv29 while the sector seals instead of being pinned to nv28 - that
crossing is the window these bugs live in. Both ends of it are asserted, so a
run that misses the window fails. A new CI job compiles and runs the piece
doctor tests, which sit behind the test_lid tag and were never built: that is
how doctor_test.go stopped compiling on main (13faaa8) unnoticed.
- drop sectorlessNode, left behind when the "sector not on chain" case moved to
  claimLookupNode.sectorMissing. Nothing referenced it, but `unused` in
  golangci-lint fails the lint job on it, and neither go vet nor the test run
  reports an unused type.
- give TestDirectDealRejectedAtNv29 an explicit proof mode. NewEnsemble now
  requires one, and RealProofs matches the framework default and the test next
  to it in the same file.
@snail8501

Copy link
Copy Markdown
Contributor Author

More changes are required. Please do a deep dive and thorough testing. Add more tests. I am also not sure about freezing NV28 for itests. It does not add any benefit to keep those tests around post NV29. Some of these tests need actual behaviour modification instead of freezing them in place. Some of these issue were probably not caught because of that.

  1. Boost deal defaults to verified. Let's flip that.
  2. Remove extend-claim command post NV29.
  3. A direct deal accepted before nv29 can finish sealing afterward without creating a claim. Piece doctor then clears its warning and skips checking whether its index or unsealed copy is missing.
  4. Migration skips a direct deal when it cannot find its claim. For deals sealed after nv29, that can leave the deal record and retrieval mapping out of Curio even though the data was successfully sealed.
  5. boostd import-direct needs the same early rejection.
  6. A successfully sealed deal can display Proving(No claim found) after nv29.

@LexLuthr All six are addressed. The nv28 freeze is gone — both itests now cross to nv29 while the sector seals instead of being pinned before it. --verified defaults to false, extend-claim is kept but gated (nv28 and nv29 have a compatibility window), the piece doctor no longer skips checks on a nv29 deal, migration no longer drops a deal whose claim is missing, import-direct rejects up front with the provider's own reason, and the UI no longer shows "(No claim found)" for a sector sealed at nv29+. All of it is covered by tests, and a new CI job runs the piece-doctor tests.

@snail8501
snail8501 requested a review from LexLuthr September 23, 2026 01:50

@LexLuthr LexLuthr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following need to be NV29 aware.

  1. sectorstatemgr.NewSectorStateMgr
  2. Deal filters

Comment thread storagemarket/direct_deals_provider.go Outdated
Comment thread storagemarket/direct_deals_provider.go Outdated
Comment thread cmd/migrate-curio/migrate.go Outdated
Comment thread cmd/migrate-curio/migrate.go Outdated
FIP-0118 takes verifreg out of sector activation, so a sector that proves at nv29
is never given a claim. Boost read that absence as the deal having left the
chain. Everything below answers one question instead -- where did this deal's
piece land, and when.

Dependencies:
- lotus moves to v1.37.0-rc1 and the feat/solstice replace is dropped, which is
  what makes api.ForkUpgradeParams.UpgradeSolsticeHeight available; the nv29
  height is read from the chain rather than inferred. go-state-types moves from a
  pseudo-version to v0.19.0 for network.Version29.

nv29 boundary and on-chain judgement (storagemarket):
- nv29UpgradeHeight() reads UpgradeSolsticeHeight. A zero height means a node too
  old to know about solstice and is an error; negative heights are how devnets
  schedule forks and are allowed.
- Nv29Boundary and ResolveNv29Boundary answer the boundary once.
  SectorOnboarded() is pure, so a miner's whole sector list dates in one pass,
  and it dates by si.Activation -- or si.PowerBaseEpoch when the sector is
  snapped, since a snap changes when the data it now holds arrived.
- SectorHoldsPiece() reads the sealer's piece list and separates "the piece is
  not there" from "the sealer cannot tell us".
- settle() no longer reads only the claim. A claim still wins when one exists.
  Without one, a piece onboarded at or after nv29 is settled from the sealer's own
  piece list: present completes the deal, absent fails it as ErrPieceNotOnboarded
  instead of waiting out a timeout for a claim that will never come. A sealer that
  reports no pieces at all leaves the piece unverifiable (ErrPieceUnverifiable,
  warned once per process and failed only after the same timeout).
- IsFailedSealingState() makes a final sealing state that destroys the data fatal
  as ErrSectorSealingFailed. Past nv29 the sealing state is the only thing
  separating a failed seal from a successful one, since neither writes a claim.
- The failures are exported sentinels, so migrate-curio can tell the one expected
  absence apart from a real fault.

Sector weights (sectorstatemgr):
- SectorCarriesData() reads both DealWeight and VerifiedDealWeight. From nv29
  every piece's spacetime lands in VerifiedDealWeight and DealWeight stays at
  zero, so reading DealWeight alone reports every post-nv29 sector as empty.

Deal filters:
- DealFilterParams carries NetworkVersion, and the storage-filter JSON document
  moves to 2.3.0 to carry it; the retrieval document stays at 2.2.0 rather than
  advertising a field it does not have. An unset version is refused, since a zero
  reads to a filter as a very old chain and would send it down the pre-nv29 path.
- BasicDealFilter reads ConsiderVerifiedStorageDeals and
  ConsiderUnverifiedStorageDeals as a single switch at nv29, where no deal is
  verified. Read separately, a miner with verified deals left on and unverified
  deals off rejected every deal. A once-per-process warning names both values.

Migration (cmd/migrate-curio):
- migratableDirectDeal() excuses exactly one fatal error, ErrNoClaimFound, and
  only for a piece onboarded at or after nv29. ErrPieceNotOnboarded and
  ErrPieceUnverifiable are not excused: they say the data could not be
  established, which is not the same as the claim being gone.
- dateDealsAgainstNv29() dates every deal's sector from one ChainHead plus one
  StateMinerSectors for the whole run, instead of a chain read per deal. A deal's
  answer is its sector's, and a stale set can only read true as false.
- A chain that will not answer now aborts the run. The previous version read a
  failed lookup as pre-nv29, silently skipping the deals this change exists to
  rescue, on a node that was merely unreachable.
- A dead sector is checked before the claim, so its skip reason names the sector
  rather than a claim it never had.

UI (gql):
- sealingState now asks what the provider asks -- is this deal's piece onboarded
  in that sector -- instead of whether the sector activated at or after nv29,
  which suppressed "(No claim found)" for any such sector including one that does
  not hold the deal's data.
- newDirectDealResolver() gives both call sites one constructor; the single-deal
  query built one inline without a full node and panicked.

Tests:
- The direct-deal itest asserted only the chain's side of the crossing -- sector
  on chain, no claim, piece still in the sector -- so the provider changes above
  were never exercised end to end. assertDirectDealComplete reads the node's own
  direct-deal database and requires the deal to reach Complete with no error;
  with no claim to find, the only way it completes is by the sealer reporting the
  piece in the sector. TestFramework exposes RepoDir and DirectDealsDB() to read
  it.
- Checked by disabling the nv29 path in settle(): the assertion fails after ~197s
  with "boost never completed the direct deal", and the suite passes with it
  enabled. The itests need -tags=debug, since untagged builds use
  UpgradeSolsticeHeight=999999999999999 and cannot mine the test chain.
@snail8501

Copy link
Copy Markdown
Contributor Author

@LexLuthr Both points from the second review are in 8e4b6a4.

sectorstatemgr — refreshState now reads both spacetime weights, via SectorCarriesData(), and needs no network-version check: the weights themselves are the correct signal. From nv29 every piece's spacetime lands in VerifiedDealWeight and DealWeight stays at zero, so reading DealWeight alone reported every post-nv29 sector as holding nothing. That is what made those sectors look empty both to the deal filters and to the "sectors with deals" list. Covered by sectorstatemgr/sector_weights_test.go.

Deal filters — DealFilterParams now carries NetworkVersion, and BasicDealFilter reads ConsiderVerifiedStorageDeals and ConsiderUnverifiedStorageDeals as a single switch at nv29, where no deal is verified. Read separately, a miner with verified deals left on and unverified deals off rejected every deal. A once-per-process warning names both configured values. External filters receive the version through the storage JSON document, now 2.3.0; the retrieval document deliberately stays at 2.2.0 rather than advertising a field it does not have, and an unset version is refused rather than read as nv0.

One correction to my earlier replies, now that 8e4b6a4 has replaced that approach: my answer on direct_deals_provider.go:715 said the deal's own sector is dated by Activation. That is no longer what the code does — after the point you raised at line 681, an epoch comparison cannot separate a successful snap from a failed one, so a claimless deal is settled from the sealer's piece list instead. The same applies to the migration and UI items in my 09-22 summary: the conclusions still hold, but the mechanism is piece-based rather than activation-based.

scripts/fiximports reorders two imports in the test files added this round:
testify/require sorts after ipfs/go-cid, and the go-state-types market import
belongs in the go-state-types group rather than with the lotus ones. gen-check
runs that script and fails if the tree comes back dirty.
@BigLep

BigLep commented Sep 28, 2026

Copy link
Copy Markdown
Member

@snail8501 : I want to make sure you saw #2134

@snail8501
snail8501 requested a review from LexLuthr October 11, 2026 12:09
@BigLep BigLep removed this from Boost Oct 11, 2026
@BigLep
BigLep requested a lite review from Copilot October 11, 2026 23:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread cmd/migrate-curio/migrate.go
Comment thread storagemarket/direct_deals_provider.go Outdated
Comment thread gql/resolver_directdeals.go Outdated
Comment thread piecedirectory/doctor.go Outdated
Comment thread retrievalmarket/server/gsunpaidretrieval.go Outdated
Comment thread storagemarket/dealfilter/types.go
snail8501 and others added 3 commits October 12, 2026 10:59
- the storage deal filter never serialized DealFilterParams.NetworkVersion, so
  a filter branching on VerifiedDeal could not see it; the storage payload
  version goes to 2.3.0 and the retrieval payload stays at 2.2.0
- the piece doctor stopped reading claims at nv29+, which leaves the claims of
  the pre-nv29 sectors a piece lists on the sector-weight arm alone; a failed
  read no longer aborts the batch either
- the direct deal panel reported a carrying sector as "(Data in sector)", a
  verdict on the deal from a read that answers for the whole sector
The nil-selector guard added for the acceptDeal panic ran before the paid
check, so a paid retrieval with no selector returned "intercepted, error" and
the queued hook never called the legacy path. The request was dropped with an
error log instead of reaching the validator, which rejects a nil selector on
its own. The guard now applies to an unpaid retrieval only; block order and
the panic path are unchanged.
The watch still had a Curio-shaped branch and a pair of holes through which
a deal could poll forever.

Curio leaves the watch entirely. `DDPConfig.Curio` is gone, along with the
`sps.Version` sniff that set it and the `trackCurioSealing` wait it gated in
`indexAndAnnounce` -- a second, coarser copy of the wait the watch already
does. `sectorPiecesKnown` reads `si.Pieces` from `sps.SectorsStatus`, the
same call and field for every sealer, and treats an empty list as "not
reported" rather than "no pieces": a sealed sector always lists its padding,
so an empty answer cannot mean absence. The Curio-specific reading stays in
cmd/migrate-curio, where the recorded rows are, since no full-node API
exposes a sector's pieces.

Two outcomes now terminate instead of polling:

- A removed sector. `Removing`/`Terminating` and friends stay settled --
  those sectors are still on chain carrying weight, so the chain decides
  them -- but `Removed` is the one shape that cannot resolve itself: a
  sector deleted before activation never appears on chain, so waiting for
  its data waits for something that cannot happen. That is
  `ErrSectorRemovedByProvider`, distinct from `ErrSectorSealingFailed`
  because nothing failed to seal. The check is `isSectorRemoved`, local to
  this file: `IsFinalSealingState` is shared with the market-deal path,
  where "final" means the pipeline is done rather than the data is going
  away.
- A sector carrying another piece's spacetime. The sealer's list is the only
  thing that can tell "something landed here" from "this deal landed here",
  so a readable list that does not hold our (PieceCID, Size, offset) is now
  `sealingPieceMissing`, fatal. The arm sits after the claim arm and needs
  the readable list: before nv29 a claim on this sector settles the deal
  whether or not the list agrees.

`pieceInSector` moves to `sectorstatemgr.PieceInSector` so the migration asks
the same question of Curio's recorded list with the same offset
accumulation, and `verdictForSealedSector`'s weight-present arm drops its
stale note that a fresh seal across the upgrade was unreachable: a fresh
seal and a snap differ only in how the data arrived, which the chain does
not record.
Comment thread storagemarket/direct_deals_provider.go Outdated
Comment thread piecedirectory/doctor.go
Comment thread itests/ddo_test.go Outdated
Comment thread storagemarket/provider.go Outdated
Comment thread storagemarket/direct_deals_provider.go Outdated
@github-project-automation github-project-automation Bot moved this from In review to In progress in Solstice Oct 14, 2026
A sector with weight, no claim and no reported piece list no longer
completes the deal: the weight says something landed in the sector, not
that this deal did. That case polls now, like the other cases where the
list is unreadable, so a missing piece is never read out of a list that
was never reported.

Also drop the per-poll read of the deal row. The row is this thread's
output, not its input: the caller already checked it is incomplete, one
thread runs per deal, FailPausedDeal refuses running deals, and Complete
is written only after the watch returns. The tests pin the complete row
as unreachable instead of re-reading it.

@LexLuthr LexLuthr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last change needed to clean up Curio.

File: node/modules/lp2p.go:18 - This calls the sealer’s Version API solely to choose a boost-curio user agent. Please remove the lookup and use the normal Boost user agent.

node/modules/lp2p.go called the sealer's Version API only to decide
between a boost-curio and a boost user agent. Drop the lookup and the
branch: the user agent is always boost-<build version> now.
go.mod: lotus v1.37.0-rc1 -> v1.38.1, go-state-types v0.19.0 -> v0.19.1,
go-jsonrpc v0.10.2 -> v0.10.3. lotus v1.38.1 requires go 1.26.8, so the
go directive and go.work move up with it.

lotus v1.38.1 removed blockstore.WrapIDStore along with the identity
blockstore that resolved identity CIDs inline; its replacement is
blockstore.RejectIdentityCids, which is how lotus migrated its own three
call sites. Writes and deletes of identity CIDs are still dropped, so the
CAR produced by booster-bitswap fetch is unchanged; reads now fail with
blockstore.ErrIdentityCid instead of being served from the CID itself.
@snail8501

Copy link
Copy Markdown
Contributor Author

One last change needed to clean up Curio.

File: node/modules/lp2p.go:18 - This calls the sealer’s Version API solely to choose a boost-curio user agent. Please remove the lookup and use the normal Boost user agent.

@LexLuthr Done in 5766645. UserAgent() no longer takes the sealing pipeline API and no longer calls Version(): it always sets boost-. The strings.Contains(v, "curio") branch, the boost-curio- prefix and the sealer injection at node/builder.go:442 are all gone. That was the last sealer-version probe in the running node — the previous round removed the provider's one.

One consequence worth stating: boostx stats used the boost-curio- user agent to classify a node as Curio. Nodes now report as plain boost through that channel. Say the word if you want that classification kept some other way; otherwise it goes away with the lookup.

@snail8501
snail8501 requested a review from LexLuthr October 15, 2026 17:10
@LexLuthr

Copy link
Copy Markdown
Collaborator

One last change needed to clean up Curio.
File: node/modules/lp2p.go:18 - This calls the sealer’s Version API solely to choose a boost-curio user agent. Please remove the lookup and use the normal Boost user agent.

@LexLuthr Done in 5766645. UserAgent() no longer takes the sealing pipeline API and no longer calls Version(): it always sets boost-. The strings.Contains(v, "curio") branch, the boost-curio- prefix and the sealer injection at node/builder.go:442 are all gone. That was the last sealer-version probe in the running node — the previous round removed the provider's one.

One consequence worth stating: boostx stats used the boost-curio- user agent to classify a node as Curio. Nodes now report as plain boost through that channel. Say the word if you want that classification kept some other way; otherwise it goes away with the lookup.

This is expected consequence. Curio stopped being Boost compatible long time ago. We never cleaned up on Boost side.

@LexLuthr LexLuthr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix the failing tests

make gen output for two stale bits on this branch:
- node/config/doc_gen.go was missing StateCallGasLimit, the config field
  lotus v1.38.1 added and boost embeds through lotus_config.StorageMiner
- fiximports regrouped imports in five touched files
Both tests opened their first stream before the proxy had registered the
service node's route. The proxy learns routes from libp2p's connection
notifiee, which the swarm fires from its listener goroutine
(swarm_listen.go -> s.refs.Go(addConn)), so Connect returning does not
mean the route is active. handleIncoming then finds no route and resets
the stream, which surfaces as "stream reset" on the client's read, or as
a Rejected response to an outbound request.

Reproduced with spare CPUs busy: 2 failures in 500 runs without -race,
3 in 200 with it, failing on protocolproxy_test.go:237 exactly as CI
does, with the proxy logging "received protocol request ... with no
active peer" right before each one.

- wait for the routes after connecting, in both tests
- accept the reset at any step of the round trip in the cases that
  expect a failure, since it can land before negotiation finishes
- connect the service node explicitly in TestOutboundForwarding, so the
  route exists before the first request

300 runs with -race and 1000 without, both under CPU load, are green.
Breaking Connected to register no routes still fails 8 subtests, so the
tests keep their teeth.
@snail8501

Copy link
Copy Markdown
Contributor Author

Fix the failing tests

@LexLuthr gen-check was stale generated output. doc_gen.go was missing StateCallGasLimit, the field lotus v1.38.1 adds and boost picks up through lotus_config.StorageMiner, and fiximports wanted imports regrouped in five files. c0aba65 is what make gen writes.

test-all was protocolproxy opening its stream before the proxy had the service node's route. Routes are written in Connected, which the swarm fires from the listener goroutine, so Connect returning does not mean the route is active yet, and handleIncoming resets the stream. ba438a7 waits for the routes after connecting, and in the cases that expect a failure it accepts the reset wherever it lands. 300 runs with -race and 1000 without, under load, are green.

@BigLep

BigLep commented Oct 16, 2026

Copy link
Copy Markdown
Member

@snail8501 @LexLuthr : given the network upgrade is Monday 2026-10-19, we really should be getting this release out. Is there anything blocking merging and cutting the release? I assume @filecoin-project/storswift-miner-maintainers will handle this, but please raise if you have questions or need help.

@snail8501
snail8501 merged commit e3ec646 into main Oct 17, 2026
22 checks passed
@snail8501
snail8501 deleted the feat/solstice branch October 17, 2026 01:20
@github-project-automation github-project-automation Bot moved this from In progress to Done in Solstice Oct 17, 2026
This was referenced Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Solstice] datacap deprecation

4 participants