Skip to content

Commit c469484

Browse files
committed
Pure AI draft
1 parent da6f7c9 commit c469484

15 files changed

Lines changed: 1732 additions & 162 deletions
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
-- PDPv0 scheduled-removal draining (filecoin-project/curio#1422).
2+
--
3+
-- PDPVerifier no longer applies scheduled piece removals inside
4+
-- nextProvingPeriod; the storage provider drains the queue explicitly with
5+
-- processPieceDeletions, and nextProvingPeriod reverts while the queue is
6+
-- non-empty. See https://github.com/FilOzone/pdp/pull/297.
7+
--
8+
-- This table is the work queue the drain watcher selects from. Rows are
9+
-- candidates rather than confirmed work: the task's first action is an on-chain
10+
-- queue read, and a row whose data set has an empty queue is simply dropped. So
11+
-- the seed below can be indiscriminate and needs no chain access at migration
12+
-- time.
13+
--
14+
-- Two writers: this one-time seed, which picks up data sets already carrying a
15+
-- removal queue at upgrade time (including any stuck by FilOzone/pdp#283), and
16+
-- the DeletePiece intake path, which inserts a row alongside every
17+
-- schedulePieceDeletions send from here on.
18+
19+
CREATE TABLE IF NOT EXISTS pdpv0_deletion_drain (
20+
data_set BIGINT PRIMARY KEY REFERENCES pdp_data_sets(id) ON DELETE CASCADE,
21+
22+
-- ON DELETE SET NULL so an abandoned or exhausted harmony task releases its
23+
-- claim automatically, the same way pdp_data_sets.challenge_request_task_id
24+
-- works. Without it a row lost mid-task would never be re-claimed.
25+
task_id BIGINT REFERENCES harmony_task(id) ON DELETE SET NULL,
26+
27+
-- In-flight processPieceDeletions transaction. At most one per data set:
28+
-- drains must be sequential because each one re-reads the queue length.
29+
msg_hash TEXT DEFAULT NULL,
30+
31+
-- Bumped when a drain send fails, bounding retries so a permanently failing
32+
-- data set cannot spin forever.
33+
failures BIGINT NOT NULL DEFAULT 0,
34+
35+
-- Set when the task claimed the row but could not act on it yet (for
36+
-- example the challenge window has not closed). Keeps the watcher from
37+
-- re-claiming and re-reading chain state on every tipset.
38+
blocked_at TIMESTAMPTZ DEFAULT NULL,
39+
40+
created_at TIMESTAMPTZ NOT NULL DEFAULT TIMEZONE('UTC', NOW())
41+
);
42+
43+
COMMENT ON TABLE pdpv0_deletion_drain IS
44+
'Data sets that may have a non-empty PDPVerifier scheduled-removal queue to drain via processPieceDeletions.';
45+
46+
-- The watcher only ever looks for unclaimed rows with no drain in flight.
47+
CREATE INDEX IF NOT EXISTS idx_pdpv0_deletion_drain_pending
48+
ON pdpv0_deletion_drain (data_set)
49+
WHERE task_id IS NULL AND msg_hash IS NULL;
50+
51+
-- The confirmation watcher scans by in-flight message.
52+
CREATE INDEX IF NOT EXISTS idx_pdpv0_deletion_drain_msg_hash
53+
ON pdpv0_deletion_drain (msg_hash)
54+
WHERE msg_hash IS NOT NULL;
55+
56+
-- Reclaiming abandoned rows scans by task_id.
57+
CREATE INDEX IF NOT EXISTS idx_pdpv0_deletion_drain_task_id
58+
ON pdpv0_deletion_drain (task_id)
59+
WHERE task_id IS NOT NULL;
60+
61+
INSERT INTO pdpv0_deletion_drain (data_set)
62+
SELECT id FROM pdp_data_sets
63+
ON CONFLICT (data_set) DO NOTHING;

pdp-process-piece-deletions-findings.md

Lines changed: 514 additions & 0 deletions
Large diffs are not rendered by default.

pdp/contract/removals.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package contract
2+
3+
import (
4+
"context"
5+
"math/big"
6+
"strconv"
7+
"strings"
8+
9+
"github.com/ethereum/go-ethereum/accounts/abi"
10+
"golang.org/x/xerrors"
11+
)
12+
13+
// This file is hand-maintained, unlike the generated PDPVerifier bindings. It
14+
// carries the removal-queue entrypoint and custom errors added by
15+
// https://github.com/FilOzone/pdp/pull/297, which are not yet in the checked-in
16+
// ABI. Delete it and use the generated bindings once PDPVerifier.abi is
17+
// regenerated against a release containing processPieceDeletions.
18+
const removalQueueABIJSON = `[
19+
{"type":"function","name":"processPieceDeletions","inputs":[{"name":"setId","type":"uint256","internalType":"uint256"},{"name":"removalCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},
20+
{"type":"error","name":"OnlyStorageProvider","inputs":[]},
21+
{"type":"error","name":"InvalidPieceDeletionBatch","inputs":[]},
22+
{"type":"error","name":"EmptyRemovalBatch","inputs":[]},
23+
{"type":"error","name":"PendingPieceDeletions","inputs":[{"name":"count","type":"uint256","internalType":"uint256"}]},
24+
{"type":"error","name":"NoPiecesToProve","inputs":[]}
25+
]`
26+
27+
var removalQueueABI abi.ABI
28+
29+
func init() {
30+
parsed, err := abi.JSON(strings.NewReader(removalQueueABIJSON))
31+
if err != nil {
32+
panic("failed to parse PDPVerifier removal-queue ABI fragment: " + err.Error())
33+
}
34+
removalQueueABI = parsed
35+
}
36+
37+
// RemovalQueueABI exposes the hand-maintained fragment so callers can resolve
38+
// the new custom errors for revert classification.
39+
func RemovalQueueABI() *abi.ABI {
40+
return &removalQueueABI
41+
}
42+
43+
// PackProcessPieceDeletions builds calldata draining removalCount entries from
44+
// the tail of a data set's scheduled-removal queue.
45+
func PackProcessPieceDeletions(setID, removalCount *big.Int) ([]byte, error) {
46+
data, err := removalQueueABI.Pack("processPieceDeletions", setID, removalCount)
47+
if err != nil {
48+
return nil, xerrors.Errorf("packing processPieceDeletions: %w", err)
49+
}
50+
return data, nil
51+
}
52+
53+
// PieceDeletionProcessingMinVersion is the lowest PDPVerifier VERSION that
54+
// exposes processPieceDeletions. Below it, nextProvingPeriod still drains the
55+
// removal queue itself and Curio must not attempt to drain it separately.
56+
//
57+
// TODO: confirm against the release that lands FilOzone/pdp#297. The currently
58+
// deployed contract reports "3.4.0"; this is a placeholder for the next bump.
59+
const PieceDeletionProcessingMinVersion = "3.5.0"
60+
61+
// SupportsPieceDeletionProcessing reports whether the deployed PDPVerifier
62+
// exposes processPieceDeletions.
63+
//
64+
// The value is read rather than cached for the process lifetime: PDPVerifier is
65+
// a UUPS proxy and can be upgraded in place underneath a running Curio.
66+
func SupportsPieceDeletionProcessing(ctx context.Context, verifier *PDPVerifier) (bool, error) {
67+
version, err := verifier.VERSION(EthCallOpts(ctx))
68+
if err != nil {
69+
return false, xerrors.Errorf("reading PDPVerifier VERSION: %w", err)
70+
}
71+
72+
cmp, err := compareSemver(version, PieceDeletionProcessingMinVersion)
73+
if err != nil {
74+
return false, xerrors.Errorf("comparing PDPVerifier version %q: %w", version, err)
75+
}
76+
return cmp >= 0, nil
77+
}
78+
79+
// compareSemver orders two dotted version strings, returning -1, 0 or 1. Any
80+
// pre-release or build suffix is ignored: "3.5.0-rc1" compares equal to "3.5.0",
81+
// which is the conservative choice for a feature gate on a release candidate.
82+
func compareSemver(a, b string) (int, error) {
83+
aParts, err := parseSemver(a)
84+
if err != nil {
85+
return 0, err
86+
}
87+
bParts, err := parseSemver(b)
88+
if err != nil {
89+
return 0, err
90+
}
91+
92+
for i := range aParts {
93+
switch {
94+
case aParts[i] < bParts[i]:
95+
return -1, nil
96+
case aParts[i] > bParts[i]:
97+
return 1, nil
98+
}
99+
}
100+
return 0, nil
101+
}
102+
103+
func parseSemver(v string) ([3]int, error) {
104+
var out [3]int
105+
106+
v = strings.TrimSpace(v)
107+
v = strings.TrimPrefix(v, "v")
108+
if idx := strings.IndexAny(v, "-+"); idx >= 0 {
109+
v = v[:idx]
110+
}
111+
112+
fields := strings.Split(v, ".")
113+
if len(fields) == 0 || len(fields) > 3 {
114+
return out, xerrors.Errorf("malformed version %q", v)
115+
}
116+
117+
for i, field := range fields {
118+
n, err := strconv.Atoi(field)
119+
if err != nil {
120+
return out, xerrors.Errorf("malformed version %q: component %q is not a number", v, field)
121+
}
122+
if n < 0 {
123+
return out, xerrors.Errorf("malformed version %q: negative component", v)
124+
}
125+
out[i] = n
126+
}
127+
return out, nil
128+
}

pdp/handlers.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,43 @@ func (p *PDPService) handleDeleteDataSetPiece(w http.ResponseWriter, r *http.Req
11141114
return
11151115
}
11161116

1117+
// Removals can only be drained once the data set has a proving schedule: the
1118+
// drain waits for the challenge window to close, and there is no window
1119+
// without one. Accepting a removal before then would build a queue that can
1120+
// never be drained, while blocking initProvingPeriod from ever succeeding.
1121+
var proveAtEpoch sql.NullInt64
1122+
err = p.db.QueryRow(ctx, `SELECT prove_at_epoch FROM pdp_data_sets WHERE id = $1`, dataSetId).Scan(&proveAtEpoch)
1123+
if err != nil {
1124+
httpServerError(w, http.StatusInternalServerError, "Failed to read data set proving schedule", err)
1125+
return
1126+
}
1127+
if !proveAtEpoch.Valid {
1128+
http.Error(w, fmt.Sprintf("data set %d has not started proving yet; retry once its first proving period is initialized", dataSetId),
1129+
http.StatusTooManyRequests)
1130+
return
1131+
}
1132+
1133+
// A drain transaction in flight means the queue is mid-processing. Adding to
1134+
// it now re-fills what is being drained and pushes the proving-period
1135+
// rollover further out, so refuse until the drain settles.
1136+
//
1137+
// Keyed on an in-flight message rather than the mere presence of a drain
1138+
// row: the migration seeds a row for every data set, so row existence would
1139+
// refuse every deletion until that sweep completed.
1140+
var draining bool
1141+
err = p.db.QueryRow(ctx, `
1142+
SELECT EXISTS (SELECT 1 FROM pdpv0_deletion_drain WHERE data_set = $1 AND msg_hash IS NOT NULL)
1143+
`, dataSetId).Scan(&draining)
1144+
if err != nil {
1145+
httpServerError(w, http.StatusInternalServerError, "Failed to read removal drain state", err)
1146+
return
1147+
}
1148+
if draining {
1149+
http.Error(w, fmt.Sprintf("data set %d is draining previously scheduled removals; retry once they are processed", dataSetId),
1150+
http.StatusTooManyRequests)
1151+
return
1152+
}
1153+
11171154
// Soft gate: refuse if the data set's on-chain removal queue is already at our
11181155
// conservative ceiling. This keeps us well clear of the on-chain MAX_ENQUEUED_REMOVALS.
11191156
pdpVerifier, err := contract.NewPDPVerifier(contract.ContractAddresses().PDPVerifier, p.ethClient)
@@ -1127,7 +1164,7 @@ func (p *PDPService) handleDeleteDataSetPiece(w http.ResponseWriter, r *http.Req
11271164
return
11281165
}
11291166
if len(queued) >= contract.ConservativeEnqueuedRemovalsLimit {
1130-
http.Error(w, fmt.Sprintf("data set %d already has %d scheduled removals queued (limit %d); retry after the next proving period flushes the queue",
1167+
http.Error(w, fmt.Sprintf("data set %d already has %d scheduled removals queued (limit %d); retry once they have been processed",
11311168
dataSetId, len(queued), contract.ConservativeEnqueuedRemovalsLimit), http.StatusTooManyRequests)
11321169
return
11331170
}
@@ -1206,6 +1243,19 @@ func (p *PDPService) handleDeleteDataSetPiece(w http.ResponseWriter, r *http.Req
12061243
log.Errorw("Failed to update rm_message_hash in pdp_data_set_pieces", "dataSetId", dataSetId, "pieceIDs", pieceIDsI64, "error", err)
12071244
return false, err
12081245
}
1246+
1247+
// PDPVerifier no longer applies scheduled removals inside
1248+
// nextProvingPeriod, so the data set needs an explicit drain before it
1249+
// can roll over. Enqueue it in the same transaction as the send.
1250+
_, err = tx.Exec(`
1251+
INSERT INTO pdpv0_deletion_drain (data_set)
1252+
VALUES ($1)
1253+
ON CONFLICT (data_set) DO UPDATE SET blocked_at = NULL`, dataSetId)
1254+
if err != nil {
1255+
log.Errorw("Failed to enqueue removal drain", "dataSetId", dataSetId, "error", err)
1256+
return false, err
1257+
}
1258+
12091259
log.Infow("scheduled user requested deletion", "dataSetId", dataSetId, "pieceIDs", pieceIDsI64, "txHash", txHashLower)
12101260

12111261
return true, nil

pdpnode/tasks.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,15 @@ func buildPDPTasks(ctx context.Context, d *Deps, chainSched *chainsched.CurioCha
7979
pay.NewSettleWatcher(w)
8080
pdpv0.NewDataSetDeleteWatcher(w)
8181
pdpv0.NewCleanupPiecesWatcher(w)
82+
pdpv0.NewProcessDeletionsWatcher(w)
8283
pdpv0.NewProvingPeriodWatcher(w)
8384
pdpv0.NewTerminateServiceWatcher(w)
8485

8586
tasks = append(tasks,
8687
pdpv0.NewProveTask(db, ethClient, d.Chain, w, senderEth, d.CachedPieceReader, d.IndexStore),
8788
pdpv0.NewNextProvingPeriodTask(db, ethClient, d.Chain, w, senderEth),
8889
pdpv0.NewInitProvingPeriodTask(db, ethClient, d.Chain, w, senderEth),
90+
pdpv0.NewProcessDeletionsTask(db, ethClient, d.Chain, w, senderEth),
8991
pdpv0.NewPDPNotifyTask(ctx, db),
9092
pdpv0.NewPDPPullPieceTask(ctx, db, d.PieceIO, cfg.Subsystems.PDPPullPieceMaxTasks),
9193
pdpv0.NewTerminateServiceTask(db, ethClient, senderEth),

tasks/pdpv0/error_detection.go

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ var (
3131
ErrPDPVerifierDataSetNotLive abi.Error
3232
ErrPDPVerifierInsufficientChallengeDelay abi.Error
3333

34+
// Removal-queue errors from FilOzone/pdp#297. These resolve against the
35+
// hand-maintained ABI fragment in pdp/contract/removals.go until the
36+
// generated PDPVerifier bindings carry them.
37+
ErrPDPVerifierPendingPieceDeletions abi.Error
38+
ErrPDPVerifierInvalidPieceDeletionBatch abi.Error
39+
ErrPDPVerifierEmptyRemovalBatch abi.Error
40+
ErrPDPVerifierOnlyStorageProvider abi.Error
41+
ErrPDPVerifierNoPiecesToProve abi.Error
42+
3443
// Unexpected proving invariant errors. Curio should not produce these in
3544
// normal PDPv0 initPP/nextPP/prove flow; classify them explicitly so they
3645
// alert and require investigation instead of entering recovery/backoff paths.
@@ -77,6 +86,33 @@ func init() {
7786
panic("PDPVerifier ABI missing ExcessiveChallengeDelay error")
7887
}
7988

89+
removalQueue := contract.RemovalQueueABI()
90+
91+
ErrPDPVerifierPendingPieceDeletions, ok = removalQueue.Errors["PendingPieceDeletions"]
92+
if !ok {
93+
panic("PDPVerifier removal ABI missing PendingPieceDeletions error")
94+
}
95+
96+
ErrPDPVerifierInvalidPieceDeletionBatch, ok = removalQueue.Errors["InvalidPieceDeletionBatch"]
97+
if !ok {
98+
panic("PDPVerifier removal ABI missing InvalidPieceDeletionBatch error")
99+
}
100+
101+
ErrPDPVerifierEmptyRemovalBatch, ok = removalQueue.Errors["EmptyRemovalBatch"]
102+
if !ok {
103+
panic("PDPVerifier removal ABI missing EmptyRemovalBatch error")
104+
}
105+
106+
ErrPDPVerifierOnlyStorageProvider, ok = removalQueue.Errors["OnlyStorageProvider"]
107+
if !ok {
108+
panic("PDPVerifier removal ABI missing OnlyStorageProvider error")
109+
}
110+
111+
ErrPDPVerifierNoPiecesToProve, ok = removalQueue.Errors["NoPiecesToProve"]
112+
if !ok {
113+
panic("PDPVerifier removal ABI missing NoPiecesToProve error")
114+
}
115+
80116
parsedFWSS, err := FWSS.FilecoinWarmStorageServiceMetaData.GetAbi()
81117
if err != nil {
82118
panic("failed to parse FWSS ABI: " + err.Error())
@@ -214,11 +250,63 @@ func IsProvingPeriodNotInitializedError(err error) bool {
214250

215251
// IsNextProvingPeriodEmptyDatasetError returns true when PDPVerifier refuses to
216252
// start the next proving period because the current proving set has no leaves.
253+
//
254+
// Both encodings are matched because one Curio build spans two contract
255+
// versions: the condition is a string revert before FilOzone/pdp#297 and the
256+
// NoPiecesToProve custom error afterwards. The underlying requirement --
257+
// dataSetLeafCount > 0 -- is the same, so neither form can be dropped until no
258+
// deployment runs the older contract.
217259
func IsNextProvingPeriodEmptyDatasetError(err error) bool {
218260
if err == nil {
219261
return false
220262
}
221-
return strings.Contains(strings.ToLower(err.Error()), strings.ToLower(provingRevertNoLeavesForProvingPeriod))
263+
errStr := strings.ToLower(err.Error())
264+
return strings.Contains(errStr, strings.ToLower(provingRevertNoLeavesForProvingPeriod)) ||
265+
strings.Contains(errStr, contractErrorSelector(ErrPDPVerifierNoPiecesToProve))
266+
}
267+
268+
// IsPendingPieceDeletionsError returns true when nextProvingPeriod (or initPP)
269+
// refuses to roll over because the data set still has scheduled removals
270+
// queued. This is recoverable: the drain task processes the queue and the
271+
// proving-period task retries.
272+
func IsPendingPieceDeletionsError(err error) bool {
273+
if err == nil {
274+
return false
275+
}
276+
return strings.Contains(strings.ToLower(err.Error()), contractErrorSelector(ErrPDPVerifierPendingPieceDeletions))
277+
}
278+
279+
// IsStaleRemovalQueueViewError returns true when processPieceDeletions rejects
280+
// the requested batch because Curio's view of the queue is out of date -- the
281+
// queue shrank, or emptied, between the read and the send. Re-reading the queue
282+
// and retrying is the correct response.
283+
func IsStaleRemovalQueueViewError(err error) bool {
284+
if err == nil {
285+
return false
286+
}
287+
errStr := strings.ToLower(err.Error())
288+
return strings.Contains(errStr, contractErrorSelector(ErrPDPVerifierInvalidPieceDeletionBatch)) ||
289+
strings.Contains(errStr, contractErrorSelector(ErrPDPVerifierEmptyRemovalBatch))
290+
}
291+
292+
// IsOnlyStorageProviderError returns true when PDPVerifier rejects a removal
293+
// call because the sender is not the data set's storage provider. This needs
294+
// operator attention rather than a retry.
295+
func IsOnlyStorageProviderError(err error) bool {
296+
if err == nil {
297+
return false
298+
}
299+
return strings.Contains(strings.ToLower(err.Error()), contractErrorSelector(ErrPDPVerifierOnlyStorageProvider))
300+
}
301+
302+
// IsPDPVerifierDataSetNotLive returns true when PDPVerifier reports that a data
303+
// set is no longer live. In the removal pipeline this means the data set is
304+
// being deleted or cleaned up, so its removal queue no longer matters.
305+
func IsPDPVerifierDataSetNotLive(err error) bool {
306+
if err == nil {
307+
return false
308+
}
309+
return strings.Contains(strings.ToLower(err.Error()), contractErrorSelector(ErrPDPVerifierDataSetNotLive))
222310
}
223311

224312
// IsRefreshProvingStateError returns true when initPP/nextPP selected a

0 commit comments

Comments
 (0)