Skip to content

Commit 89b0fd4

Browse files
authored
Add cursor-based pagination and safer data set piece detection (#914)
* feat: add cursor-based pagination for data set and piece queries * docs: Document active piece field migration * fix: derive hasActivePieces from leaf count Use getDataSetLeafCount as an O(1) presence proxy instead of a one-item cursor scan, which still walks to nextPieceId on drained data sets. * fix: replace SDK activePieceCount with hasActivePieces Listing paths now report piece presence from leaf count, and getActivePieceCount paginates the cursor API instead of the linear-scan getter.
1 parent 3c9b08f commit 89b0fd4

20 files changed

Lines changed: 223 additions & 89 deletions

docs/src/content/docs/developer-guides/migration-guide.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,44 @@ for await (const piece of paginate(({ cursor }) =>
104104

105105
Raw `*Call` helpers in `@filoz/synapse-core` remain ABI-oriented: provide their required contract-facing `offset` or `startPieceId` and `limit` fields explicitly when constructing multicalls.
106106

107+
### Action: Replace core `activePieceCount` with `hasActivePieces`
108+
109+
The enriched `PdpDataSet` values returned by `getPdpDataSet()` and `getPdpDataSets()` no longer include an exact `activePieceCount`. They now expose `hasActivePieces`, derived from a non-zero `getDataSetLeafCount` read. Leaf count is an O(1) storage lookup, unlike `getActivePiecesByCursor` and `getActivePieceCount`, which scan piece IDs and can run out of gas on large or fully drained data sets:
110+
111+
```ts
112+
// before
113+
const dataSet = await getPdpDataSet(client, { dataSetId })
114+
if (dataSet && dataSet.activePieceCount > 0n) {
115+
// the data set has pieces
116+
}
117+
118+
// after
119+
const dataSet = await getPdpDataSet(client, { dataSetId })
120+
if (dataSet?.hasActivePieces) {
121+
// the data set has pieces
122+
}
123+
```
124+
125+
`WarmStorageService.hasActivePieces()` keeps the same public API, but now uses the same leaf-count proxy instead of calculating an exact count. `EnhancedDataSetInfo` from `getClientDataSetsWithDetails()` / `findDataSets()` also exposes `hasActivePieces` instead of `activePieceCount`.
126+
127+
The core `getActivePieceCount()` action remains available, but the underlying contract getter scans the data set's piece-ID range and can fail for large data sets. `WarmStorageService.getActivePieceCount()` now paginates `getActivePiecesByCursor` to derive an exact count:
128+
129+
```ts
130+
const activePieceCount = await warmStorageService.getActivePieceCount({ dataSetId })
131+
```
132+
133+
To paginate explicitly in core:
134+
135+
```ts
136+
let activePieceCount = 0n
137+
138+
for await (const _piece of paginate(({ cursor }) =>
139+
getActivePiecesByCursor(client, { dataSetId, cursor })
140+
)) {
141+
activePieceCount++
142+
}
143+
```
144+
107145
---
108146

109147
## 1.0.0

docs/src/content/docs/developer-guides/storage/storage-operations.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ These APIs are useful when you want to inspect existing data sets, query stored
196196

197197
### Getting All Data Sets
198198

199-
Retrieve all data sets owned by your account to inspect piece counts, CDN status, and metadata:
199+
Retrieve all data sets owned by your account to inspect piece presence, CDN status, and metadata:
200200

201201
```ts twoslash
202202
// @lib: esnext,dom
@@ -210,7 +210,7 @@ for (const ds of dataSets) {
210210
console.log(`Dataset ${ds.pdpVerifierDataSetId}:`, {
211211
live: ds.isLive,
212212
cdn: ds.withCDN,
213-
pieces: ds.activePieceCount,
213+
hasActivePieces: ds.hasActivePieces,
214214
metadata: ds.metadata
215215
});
216216
}

packages/synapse-core/src/mocks/jsonrpc/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ export const presets = {
570570
false,
571571
],
572572
getDataSetStorageProvider: () => [ADDRESSES.serviceProvider1, ADDRESSES.zero],
573-
getDataSetLeafCount: () => [0n],
573+
getDataSetLeafCount: () => [2n],
574574
getScheduledRemovals: () => [[]],
575575
getNextChallengeEpoch: () => [5000n],
576576
findPieceIdsByCid: () => [[0n]],

packages/synapse-core/src/pdp-verifier/get-active-piece-count.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { asChain } from '../chains.ts'
66
import type { ActionCallChain } from '../types.ts'
77
import { STRING_ERRORS, stringErrorEquals } from '../utils/contract-errors.ts'
88
import { toReadClient } from '../utils/read-client.ts'
9+
import type { getActivePiecesByCursor } from './get-active-pieces-by-cursor.ts'
910

1011
export namespace getActivePieceCount {
1112
export type OptionsType = {
@@ -23,6 +24,10 @@ export namespace getActivePieceCount {
2324
/**
2425
* Get the active piece count for a data set (non-zero leaf count)
2526
*
27+
* This contract method scans the complete historical piece-ID range and can
28+
* run out of gas for large data sets. Prefer explicitly traversing
29+
* {@link getActivePiecesByCursor} when reliability for large sets is required.
30+
*
2631
* @example
2732
* ```ts
2833
* import { getActivePieceCount } from '@filoz/synapse-core/pdp-verifier'

packages/synapse-core/src/warm-storage/find-matching-data-sets.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function metadataMatches(dataSetMetadata: MetadataObject, requestedMetada
4141
* Only active datasets are considered (live, managed, pdpEndEpoch === 0n).
4242
*
4343
* Sort order:
44-
* 1. Datasets with pieces (activePieceCount > 0) before empty datasets
44+
* 1. Datasets with pieces before empty datasets
4545
* 2. Within each group, older datasets (lower ID) first
4646
*
4747
* @param dataSets - Datasets to search (typically filtered to a single provider)
@@ -54,10 +54,8 @@ export function findMatchingDataSets(dataSets: SelectionDataSet[], metadata: Met
5454
)
5555

5656
return matching.sort((a, b) => {
57-
// Datasets with pieces sort before empty ones
58-
if (a.activePieceCount > 0n && b.activePieceCount === 0n) return -1
59-
if (b.activePieceCount > 0n && a.activePieceCount === 0n) return 1
60-
// Within same group, oldest dataset first (lower ID)
57+
if (a.hasActivePieces && !b.hasActivePieces) return -1
58+
if (b.hasActivePieces && !a.hasActivePieces) return 1
6159
return Number(a.dataSetId - b.dataSetId)
6260
})
6361
}

packages/synapse-core/src/warm-storage/get-pdp-data-set.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { type Address, type Chain, type Client, isAddressEqual, type ReadContrac
22
import { multicall } from 'viem/actions'
33
import { asChain } from '../chains.ts'
44
import { dataSetLiveCall } from '../pdp-verifier/data-set-live.ts'
5-
import { getActivePieceCountCall } from '../pdp-verifier/get-active-piece-count.ts'
5+
import type { getActivePiecesByCursor } from '../pdp-verifier/get-active-pieces-by-cursor.ts'
6+
import { type getDataSetLeafCount, getDataSetLeafCountCall } from '../pdp-verifier/get-data-set-leaf-count.ts'
67
import { getDataSetListenerCall } from '../pdp-verifier/get-data-set-listener.ts'
78
import { getPDPProviderCall, parsePDPProvider } from '../sp-registry/get-pdp-provider.ts'
89
import { toReadClient } from '../utils/read-client.ts'
@@ -25,7 +26,14 @@ export namespace getPdpDataSet {
2526
}
2627

2728
/**
28-
* Get a PDP data set by ID
29+
* Get a PDP data set by ID.
30+
*
31+
* The result reports piece presence from a non-zero {@link getDataSetLeafCount}
32+
* read, which is an O(1) storage lookup, rather than scanning piece IDs. Exact
33+
* active-piece counts are omitted because the contract's count getter performs
34+
* a linear scan and can fail for large data sets. To derive an exact count
35+
* explicitly, traverse {@link getActivePiecesByCursor} with `paginate` and
36+
* count the yielded pieces.
2937
*
3038
* @param client - The client to use to get the PDP data set.
3139
* @param options - {@link getPdpDataSet.OptionsType}
@@ -75,7 +83,7 @@ export async function getPdpDataSet(
7583
}
7684

7785
/**
78-
* Read the PDP data set info.
86+
* Read PDP data set information.
7987
*
8088
* @param client - The client to use to read the PDP data set info.
8189
* @param options
@@ -89,7 +97,7 @@ export async function readPdpDataSetInfo(
8997
}
9098
): Promise<PdpDataSetInfo> {
9199
const chain = asChain(client.chain)
92-
const [live, listener, _metadata, _pdpProvider, activePieceCount] = await multicall(toReadClient(client), {
100+
const [live, listener, _metadata, _pdpProvider, leafCount] = await multicall(toReadClient(client), {
93101
allowFailure: false,
94102
contracts: [
95103
dataSetLiveCall({
@@ -108,7 +116,7 @@ export async function readPdpDataSetInfo(
108116
chain: client.chain,
109117
providerId: options.providerId,
110118
}),
111-
getActivePieceCountCall({
119+
getDataSetLeafCountCall({
112120
chain: client.chain,
113121
dataSetId: options.dataSetInfo.dataSetId,
114122
}),
@@ -124,6 +132,6 @@ export async function readPdpDataSetInfo(
124132
cdn: options.dataSetInfo.cdnRailId > 0n && 'withCDN' in metadata,
125133
metadata,
126134
provider: pdpProvider,
127-
activePieceCount,
135+
hasActivePieces: leafCount > 0n,
128136
}
129137
}

packages/synapse-core/src/warm-storage/get-pdp-data-sets.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { multicall } from 'viem/actions'
33
import { asChain } from '../chains.ts'
44
import type { Page, paginate } from '../pagination.ts'
55
import { type dataSetLive, dataSetLiveCall } from '../pdp-verifier/data-set-live.ts'
6-
import { type getActivePieceCount, getActivePieceCountCall } from '../pdp-verifier/get-active-piece-count.ts'
6+
import type { getActivePiecesByCursor } from '../pdp-verifier/get-active-pieces-by-cursor.ts'
7+
import { type getDataSetLeafCount, getDataSetLeafCountCall } from '../pdp-verifier/get-data-set-leaf-count.ts'
78
import { type getDataSetListener, getDataSetListenerCall } from '../pdp-verifier/get-data-set-listener.ts'
89
import { type getPDPProvider, getPDPProviderCall, parsePDPProvider } from '../sp-registry/get-pdp-provider.ts'
910
import type { PDPProvider } from '../sp-registry/types.ts'
@@ -24,7 +25,7 @@ type DataSetEnrichmentResults = [
2425
dataSetLive.OutputType,
2526
getDataSetListener.ContractOutputType,
2627
getAllDataSetMetadata.ContractOutputType,
27-
getActivePieceCount.OutputType,
28+
getDataSetLeafCount.OutputType,
2829
]
2930

3031
export namespace getPdpDataSets {
@@ -41,7 +42,12 @@ export namespace getPdpDataSets {
4142
*
4243
* Only the current source page is enriched, in bounded batches with source
4344
* order preserved. Pass `nextCursor` back as `cursor`; treat it as
44-
* opaque. Use {@link paginate} to traverse every page.
45+
* opaque. Use {@link paginate} to traverse every page. Results report piece
46+
* presence from a non-zero {@link getDataSetLeafCount} read, which is an O(1)
47+
* storage lookup, rather than scanning piece IDs. Exact active-piece counts are
48+
* omitted because the contract's count getter performs a linear scan and can
49+
* fail for large data sets. To derive a count explicitly, traverse
50+
* {@link getActivePiecesByCursor} and count the yielded pieces.
4551
*
4652
* @param client - The client to use to get data sets for a client address.
4753
* @param options - {@link getPdpDataSets.OptionsType}
@@ -97,7 +103,7 @@ export async function getPdpDataSets(
97103

98104
/**
99105
* Enrich one bounded batch of source data sets with their PDP state, metadata,
100-
* provider details, and active-piece counts.
106+
* provider details, and active-piece presence.
101107
*
102108
* The four data-set-specific reads are flattened into one Viem multicall. PDP
103109
* provider reads are deduplicated by provider ID and cached in `providers`, so
@@ -128,7 +134,7 @@ async function enrichDataSetBatch(
128134
dataSetLiveCall({ chain: client.chain, dataSetId }),
129135
getDataSetListenerCall({ chain: client.chain, dataSetId }),
130136
getAllDataSetMetadataCall({ chain: client.chain, dataSetId }),
131-
getActivePieceCountCall({ chain: client.chain, dataSetId }),
137+
getDataSetLeafCountCall({ chain: client.chain, dataSetId }),
132138
])
133139
const providerCalls = missingProviderIds.map((providerId) => getPDPProviderCall({ chain: client.chain, providerId }))
134140
const results = await multicall(toReadClient(client), {
@@ -149,7 +155,7 @@ async function enrichDataSetBatch(
149155

150156
return dataSets.map((dataSet, index) => {
151157
const resultOffset = index * DATA_SET_CALL_COUNT
152-
const [live, listener, rawMetadata, activePieceCount] = results.slice(
158+
const [live, listener, rawMetadata, leafCount] = results.slice(
153159
resultOffset,
154160
resultOffset + DATA_SET_CALL_COUNT
155161
) as DataSetEnrichmentResults
@@ -166,7 +172,7 @@ async function enrichDataSetBatch(
166172
cdn: dataSet.cdnRailId > 0n && 'withCDN' in metadata,
167173
metadata,
168174
provider,
169-
activePieceCount,
175+
hasActivePieces: leafCount > 0n,
170176
}
171177
})
172178
}

packages/synapse-core/src/warm-storage/location-types.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import type { PDPProvider } from '../sp-registry/types.ts'
22
import type { MetadataObject } from '../utils/metadata.ts'
33

44
/**
5-
* Dataset with piece count, for provider selection.
5+
* Dataset with piece-presence information used for provider selection.
66
*
77
* Picks the fields that selectProviders() and findMatchingDataSets()
8-
* need, plus activePieceCount which is fetched separately via multicall.
8+
* need, plus whether the data set contains at least one active piece.
99
*
10-
* Core callers can spread a PdpDataSet directly: `{ ...ds, activePieceCount }`.
10+
* Core callers can spread a PdpDataSet directly.
1111
* SDK callers map from EnhancedDataSetInfo (different field names).
1212
*/
1313
export interface SelectionDataSet {
@@ -17,8 +17,8 @@ export interface SelectionDataSet {
1717
providerId: bigint
1818
/** Data set metadata (key-value pairs) */
1919
metadata: MetadataObject
20-
/** Number of active pieces in the dataset (0 = empty) */
21-
activePieceCount: bigint
20+
/** Whether the data set contains at least one active piece */
21+
hasActivePieces: boolean
2222
/** End epoch for PDP service (0 = active) */
2323
pdpEndEpoch: bigint
2424
/** Whether the data set is live in the PDP Verifier */
@@ -44,7 +44,7 @@ export interface ProviderSelectionInput {
4444
/** Array of endorsed provider IDs (from endorsements.getProviderIds).
4545
* Non-empty = restrict to endorsed only. Empty = use all providers. */
4646
endorsedIds: bigint[]
47-
/** Client's existing datasets with metadata and piece counts */
47+
/** Client's existing datasets with metadata and piece-presence information */
4848
clientDataSets: SelectionDataSet[]
4949
}
5050

packages/synapse-core/src/warm-storage/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ export type PdpDataSetInfo = {
4444
metadata: MetadataObject
4545
/** PDP provider associated with the data set. */
4646
provider: PDPProvider
47-
/** Number of active (non-zero) pieces in the data set. */
48-
activePieceCount: bigint
47+
/** Whether the data set contains at least one active piece (non-zero leaf count). */
48+
hasActivePieces: boolean
4949
}
5050

5151
export interface PdpDataSet extends DataSetInfo, PdpDataSetInfo {}

packages/synapse-core/test/find-matching-data-sets.test.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ function makeDataSet(
88
): SelectionDataSet {
99
return {
1010
metadata: {},
11-
activePieceCount: 0n,
11+
hasActivePieces: false,
1212
pdpEndEpoch: 0n,
1313
live: true,
1414
managed: true,
@@ -71,18 +71,18 @@ describe('findMatchingDataSets', () => {
7171

7272
it('sorts datasets with pieces before empty ones', () => {
7373
const dataSets = [
74-
makeDataSet({ dataSetId: 1n, providerId: 1n, metadata: { source: 'app' }, activePieceCount: 0n }),
75-
makeDataSet({ dataSetId: 2n, providerId: 2n, metadata: { source: 'app' }, activePieceCount: 5n }),
74+
makeDataSet({ dataSetId: 1n, providerId: 1n, metadata: { source: 'app' }, hasActivePieces: false }),
75+
makeDataSet({ dataSetId: 2n, providerId: 2n, metadata: { source: 'app' }, hasActivePieces: true }),
7676
]
7777
const result = findMatchingDataSets(dataSets, { source: 'app' })
7878
assert.equal(result[0].dataSetId, 2n)
7979
assert.equal(result[1].dataSetId, 1n)
8080
})
8181

82-
it('sorts by ID ascending within same piece group', () => {
82+
it('sorts by ID ascending', () => {
8383
const dataSets = [
84-
makeDataSet({ dataSetId: 10n, providerId: 1n, metadata: { source: 'app' }, activePieceCount: 3n }),
85-
makeDataSet({ dataSetId: 5n, providerId: 2n, metadata: { source: 'app' }, activePieceCount: 3n }),
84+
makeDataSet({ dataSetId: 10n, providerId: 1n, metadata: { source: 'app' }, hasActivePieces: true }),
85+
makeDataSet({ dataSetId: 5n, providerId: 2n, metadata: { source: 'app' }, hasActivePieces: true }),
8686
]
8787
const result = findMatchingDataSets(dataSets, { source: 'app' })
8888
assert.equal(result[0].dataSetId, 5n)
@@ -134,15 +134,14 @@ describe('findMatchingDataSets', () => {
134134
assert.equal(result.length, 0)
135135
})
136136

137-
it('full sorting: pieces first, then by ID within groups', () => {
137+
it('sorts by piece presence and then by ID', () => {
138138
const dataSets = [
139-
makeDataSet({ dataSetId: 10n, providerId: 1n, metadata: { source: 'app' }, activePieceCount: 0n }),
140-
makeDataSet({ dataSetId: 5n, providerId: 2n, metadata: { source: 'app' }, activePieceCount: 3n }),
141-
makeDataSet({ dataSetId: 3n, providerId: 3n, metadata: { source: 'app' }, activePieceCount: 0n }),
142-
makeDataSet({ dataSetId: 8n, providerId: 4n, metadata: { source: 'app' }, activePieceCount: 7n }),
139+
makeDataSet({ dataSetId: 10n, providerId: 1n, metadata: { source: 'app' }, hasActivePieces: false }),
140+
makeDataSet({ dataSetId: 5n, providerId: 2n, metadata: { source: 'app' }, hasActivePieces: true }),
141+
makeDataSet({ dataSetId: 3n, providerId: 3n, metadata: { source: 'app' }, hasActivePieces: false }),
142+
makeDataSet({ dataSetId: 8n, providerId: 4n, metadata: { source: 'app' }, hasActivePieces: true }),
143143
]
144144
const result = findMatchingDataSets(dataSets, { source: 'app' })
145-
// Pieces first (5n, 8n by ID ascending), then empty (3n, 10n by ID ascending)
146145
assert.deepEqual(
147146
result.map((ds) => ds.dataSetId),
148147
[5n, 8n, 3n, 10n]

0 commit comments

Comments
 (0)