Skip to content

Commit 61169d1

Browse files
authored
feat: add batch piece deletion (#898)
1 parent 44ffc12 commit 61169d1

7 files changed

Lines changed: 307 additions & 57 deletions

File tree

examples/cli/src/commands/pieces-removal.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as p from '@clack/prompts'
2-
import { schedulePieceDeletion } from '@filoz/synapse-core/sp'
2+
import { schedulePieceDeletions } from '@filoz/synapse-core/sp'
33
import { getPdpDataSet } from '@filoz/synapse-core/warm-storage'
44
import { type Command, command } from 'cleye'
55
import { waitForTransactionReceipt } from 'viem/actions'
@@ -42,10 +42,10 @@ export const piecesRemoval: Command = command(
4242
: await selectPiece(client, dataSet, argv.flags)
4343

4444
p.log.info(`Removing piece ${pieceId} from data set ${dataSetId}...`)
45-
const result = await schedulePieceDeletion(client, {
45+
const result = await schedulePieceDeletions(client, {
4646
dataSetId,
4747
clientDataSetId: dataSet.clientDataSetId,
48-
pieceId,
48+
pieceIds: [pieceId],
4949
serviceURL: dataSet.provider.pdp.serviceURL,
5050
})
5151

packages/synapse-core/src/errors/pdp.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,20 @@ export class DeletePieceError extends SynapseError {
179179
}
180180
}
181181

182+
export class TooManyPiecesQueuedError extends SynapseError {
183+
override name: 'TooManyPiecesQueuedError' = 'TooManyPiecesQueuedError'
184+
185+
constructor() {
186+
super(`Too many pieces queued.`, {
187+
details: `The data set already has ${SIZE_CONSTANTS.MAX_DELETE_PIECES_BATCH_SIZE} or more scheduled removals queued on-chain; retry after the next proving period flushes the queue.`,
188+
})
189+
}
190+
191+
static override is(value: unknown): value is TooManyPiecesQueuedError {
192+
return isSynapseError(value) && value.name === 'TooManyPiecesQueuedError'
193+
}
194+
}
195+
182196
export class TerminateServiceError extends SynapseError {
183197
override name: 'TerminateServiceError' = 'TerminateServiceError'
184198

Lines changed: 122 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
import { HttpError, type RequestJsonErrors, request } from 'iso-web/http'
1+
import { HttpError, type RequestErrors, request } from 'iso-web/http'
22
import type { Account, Chain, Client, Hex, Transport } from 'viem'
3-
import { DeletePieceError } from '../errors/pdp.ts'
3+
import { DeletePieceError, TooManyPiecesQueuedError } from '../errors/pdp.ts'
4+
import { AtLeastOnePieceRequiredError, TooManyPiecesError } from '../errors/warm-storage.ts'
45
import { signSchedulePieceRemovals } from '../typed-data/sign-schedule-piece-removals.ts'
5-
import { RETRY_CONSTANTS } from '../utils/constants.ts'
6+
import { RETRY_CONSTANTS, SIZE_CONSTANTS } from '../utils/constants.ts'
67

7-
export namespace deletePiece {
8+
const MAX_CURIO_PIECE_ID = (1n << 63n) - 1n
9+
10+
export namespace deletePieces {
811
export type OptionsType = {
912
serviceURL: string
1013
dataSetId: bigint
11-
pieceId: bigint
14+
pieceIds: bigint[]
1215
extraData: Hex
1316
/** The number of retries. Defaults to 2. */
1417
retryCount?: number
@@ -18,47 +21,86 @@ export namespace deletePiece {
1821
export type OutputType = {
1922
hash: Hex
2023
}
21-
export type ErrorType = DeletePieceError | RequestJsonErrors
24+
export type ErrorType =
25+
| AtLeastOnePieceRequiredError
26+
| TooManyPiecesError
27+
| RangeError
28+
| DeletePieceError
29+
| RequestErrors
2230
}
2331

2432
/**
25-
* Delete a piece from a data set on the PDP API.
33+
* Delete pieces from a data set on the PDP API in one transaction.
2634
*
2735
* DELETE /pdp/data-sets/{dataSetId}/pieces/{pieceId}
2836
*
29-
* @param options - {@link deletePiece.OptionsType}
30-
* @returns Hash of the delete operation {@link deletePiece.OutputType}
31-
* @throws Errors {@link deletePiece.ErrorType}
37+
* Curio uses the first piece ID in the URL for backwards-compatible routing and
38+
* the pieceIds request field as the authoritative list when it is non-empty.
39+
*
40+
* @param options - {@link deletePieces.OptionsType}
41+
* @returns Hash of the delete operation {@link deletePieces.OutputType}
42+
* @throws Errors {@link deletePieces.ErrorType}
3243
*/
33-
export async function deletePiece(options: deletePiece.OptionsType): Promise<deletePiece.OutputType> {
34-
const { serviceURL, dataSetId, pieceId, extraData } = options
35-
const response = await request.json.delete<{ txHash: Hex }>(
36-
new URL(`pdp/data-sets/${dataSetId}/pieces/${pieceId}`, serviceURL),
37-
{
38-
body: { extraData },
39-
timeout: RETRY_CONSTANTS.TIMEOUT,
40-
retry: {
41-
retries: options.retryCount,
42-
minTimeout: options.retryDelay ?? RETRY_CONSTANTS.RETRY_DELAY,
43-
shouldRetry: (ctx) => HttpError.is(ctx.error) && ctx.error.code === 429,
44-
},
45-
}
46-
)
44+
export async function deletePieces(options: deletePieces.OptionsType): Promise<deletePieces.OutputType> {
45+
const { serviceURL, dataSetId, extraData } = options
46+
const pieceIds = normalizeDeletePieceIds(options.pieceIds)
47+
48+
// Curio accepts uint64 JSON numbers. Construct the array from bigint decimal
49+
// strings so IDs above Number.MAX_SAFE_INTEGER are not rounded by JSON.stringify.
50+
const body = `{"extraData":${JSON.stringify(extraData)},"pieceIds":[${pieceIds.join(',')}]}`
51+
const response = await request.delete(new URL(`pdp/data-sets/${dataSetId}/pieces/${pieceIds[0]}`, serviceURL), {
52+
body,
53+
headers: { 'content-type': 'application/json' },
54+
timeout: RETRY_CONSTANTS.TIMEOUT,
55+
retry: {
56+
retries: options.retryCount,
57+
minTimeout: options.retryDelay ?? RETRY_CONSTANTS.RETRY_DELAY,
58+
},
59+
})
4760

4861
if (response.error) {
4962
if (HttpError.is(response.error)) {
63+
if (response.error.code === 429) {
64+
throw new TooManyPiecesQueuedError()
65+
}
5066
throw new DeletePieceError(await response.error.response.text())
5167
}
5268
throw response.error
5369
}
5470

55-
return { hash: response.result.txHash }
71+
const result = (await response.result.json()) as { txHash: Hex }
72+
return { hash: result.txHash }
5673
}
5774

58-
export namespace schedulePieceDeletion {
75+
/**
76+
* Validate a delete-pieces batch before signing or sending it.
77+
*/
78+
export function validateDeletePiecesBatch(pieceCount: number): void {
79+
if (!Number.isInteger(pieceCount) || pieceCount < 1) {
80+
throw new AtLeastOnePieceRequiredError()
81+
}
82+
if (pieceCount > SIZE_CONSTANTS.MAX_DELETE_PIECES_BATCH_SIZE) {
83+
throw new TooManyPiecesError(pieceCount, SIZE_CONSTANTS.MAX_DELETE_PIECES_BATCH_SIZE)
84+
}
85+
}
86+
87+
function normalizeDeletePieceIds(pieceIds: bigint[]): bigint[] {
88+
const normalized = [...new Set(pieceIds)]
89+
validateDeletePiecesBatch(normalized.length)
90+
91+
for (const pieceId of normalized) {
92+
if (pieceId < 0n || pieceId > MAX_CURIO_PIECE_ID) {
93+
throw new RangeError(`Piece ID ${pieceId} is outside Curio's supported range of 0 to ${MAX_CURIO_PIECE_ID}`)
94+
}
95+
}
96+
97+
return normalized
98+
}
99+
100+
export namespace schedulePieceDeletions {
59101
export type OptionsType = {
60-
/** The piece ID to delete. */
61-
pieceId: bigint
102+
/** The piece IDs to delete. Duplicate IDs are removed before signing. */
103+
pieceIds: bigint[]
62104
/** The data set ID to delete the piece from. */
63105
dataSetId: bigint
64106
/** The client data set id (nonce) to use for the signature. Must be unique for each data set. */
@@ -70,23 +112,23 @@ export namespace schedulePieceDeletion {
70112
/** The delay with exponential backoff between retries in milliseconds. Defaults to {@link RETRY_CONSTANTS.RETRY_DELAY}. */
71113
retryDelay?: number
72114
}
73-
export type OutputType = deletePiece.OutputType
74-
export type ErrorType = deletePiece.ErrorType
115+
export type OutputType = deletePieces.OutputType
116+
export type ErrorType = deletePieces.ErrorType
75117
}
76118

77119
/**
78-
* Schedule a piece deletion
120+
* Schedule piece deletions in one transaction.
79121
*
80122
* Call the Service Provider API to schedule the piece deletion.
81123
*
82124
* @param client - The client to use to schedule the piece deletion.
83-
* @param options - {@link schedulePieceDeletion.OptionsType}
84-
* @returns schedule piece deletion operation hash {@link schedulePieceDeletion.OutputType}
85-
* @throws Errors {@link schedulePieceDeletion.ErrorType}
125+
* @param options - {@link schedulePieceDeletions.OptionsType}
126+
* @returns Schedule piece deletions operation hash {@link schedulePieceDeletions.OutputType}
127+
* @throws Errors {@link schedulePieceDeletions.ErrorType}
86128
*
87129
* @example
88130
* ```ts
89-
* import { schedulePieceDeletion } from '@filoz/synapse-core/sp'
131+
* import { schedulePieceDeletions } from '@filoz/synapse-core/sp'
90132
* import { createWalletClient, http } from 'viem'
91133
* import { privateKeyToAccount } from 'viem/accounts'
92134
* import { calibration } from '@filoz/synapse-core/chains'
@@ -98,8 +140,8 @@ export namespace schedulePieceDeletion {
98140
* transport: http(),
99141
* })
100142
*
101-
* const result = await schedulePieceDeletion(client, {
102-
* pieceId: 1n,
143+
* const result = await schedulePieceDeletions(client, {
144+
* pieceIds: [1n, 2n],
103145
* dataSetId: 1n,
104146
* clientDataSetId: 1n,
105147
* serviceURL: 'https://pdp.example.com',
@@ -108,19 +150,56 @@ export namespace schedulePieceDeletion {
108150
* console.log(result.hash)
109151
* ```
110152
*/
111-
export async function schedulePieceDeletion(
153+
export async function schedulePieceDeletions(
112154
client: Client<Transport, Chain, Account>,
113-
options: schedulePieceDeletion.OptionsType
114-
): Promise<schedulePieceDeletion.OutputType> {
115-
return deletePiece({
155+
options: schedulePieceDeletions.OptionsType
156+
): Promise<schedulePieceDeletions.OutputType> {
157+
const pieceIds = normalizeDeletePieceIds(options.pieceIds)
158+
159+
return deletePieces({
116160
serviceURL: options.serviceURL,
117161
dataSetId: options.dataSetId,
118-
pieceId: options.pieceId,
162+
pieceIds,
119163
extraData: await signSchedulePieceRemovals(client, {
120164
clientDataSetId: options.clientDataSetId,
121-
pieceIds: [options.pieceId],
165+
pieceIds,
122166
}),
123167
retryCount: options.retryCount,
124168
retryDelay: options.retryDelay,
125169
})
126170
}
171+
172+
export namespace deletePiece {
173+
export type OptionsType = Omit<deletePieces.OptionsType, 'pieceIds'> & { pieceId: bigint }
174+
export type OutputType = deletePieces.OutputType
175+
export type ErrorType = deletePieces.ErrorType
176+
}
177+
178+
/**
179+
* Delete one piece from a data set on the PDP API.
180+
*
181+
* @deprecated Use {@link deletePieces} instead.
182+
*/
183+
export function deletePiece(options: deletePiece.OptionsType): Promise<deletePiece.OutputType> {
184+
const { pieceId, ...rest } = options
185+
return deletePieces({ ...rest, pieceIds: [pieceId] })
186+
}
187+
188+
export namespace schedulePieceDeletion {
189+
export type OptionsType = Omit<schedulePieceDeletions.OptionsType, 'pieceIds'> & { pieceId: bigint }
190+
export type OutputType = schedulePieceDeletions.OutputType
191+
export type ErrorType = schedulePieceDeletions.ErrorType
192+
}
193+
194+
/**
195+
* Schedule one piece deletion.
196+
*
197+
* @deprecated Use {@link schedulePieceDeletions} instead.
198+
*/
199+
export function schedulePieceDeletion(
200+
client: Client<Transport, Chain, Account>,
201+
options: schedulePieceDeletion.OptionsType
202+
): Promise<schedulePieceDeletion.OutputType> {
203+
const { pieceId, ...rest } = options
204+
return schedulePieceDeletions(client, { ...rest, pieceIds: [pieceId] })
205+
}

packages/synapse-core/src/utils/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ export const SIZE_CONSTANTS = {
9999
*/
100100
MAX_ADD_PIECES_BATCH_SIZE: 40,
101101

102+
/**
103+
* Maximum pieces per schedulePieceDeletions call accepted by the Curio PDP API.
104+
*
105+
* Curio also rejects requests (429) when the data set already has 200 or more
106+
* removals queued on-chain; the queue only drains at the next proving period.
107+
*/
108+
MAX_DELETE_PIECES_BATCH_SIZE: 35,
109+
102110
/**
103111
* Bytes per leaf in the PDP merkle tree.
104112
* The FWSS contract converts leaf counts to bytes via `totalBytes = leafCount * BYTES_PER_LEAF`.

0 commit comments

Comments
 (0)