Skip to content

Commit 3ac48a9

Browse files
feat: use PDPVerifier.findPieceIdsByCid for efficient CID→ID lookups (#718)
1 parent d2f14fb commit 3ac48a9

7 files changed

Lines changed: 308 additions & 12 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@ export const presets = {
526526
getDataSetLeafCount: () => [0n],
527527
getScheduledRemovals: () => [[]],
528528
getNextChallengeEpoch: () => [5000n],
529+
findPieceIdsByCid: () => [[0n]],
529530
},
530531
serviceRegistry: {
531532
registerProvider: () => [1n],

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type getDataSetStorageProvider = ExtractAbiFunction<typeof Abis.pdp, 'get
1414
export type getDataSetLeafCount = ExtractAbiFunction<typeof Abis.pdp, 'getDataSetLeafCount'>
1515
export type getScheduledRemovals = ExtractAbiFunction<typeof Abis.pdp, 'getScheduledRemovals'>
1616
export type getNextChallengeEpoch = ExtractAbiFunction<typeof Abis.pdp, 'getNextChallengeEpoch'>
17+
export type findPieceIdsByCid = ExtractAbiFunction<typeof Abis.pdp, 'findPieceIdsByCid'>
1718

1819
export interface PDPVerifierOptions {
1920
dataSetLive?: (args: AbiToType<dataSetLive['inputs']>) => AbiToType<dataSetLive['outputs']>
@@ -29,6 +30,7 @@ export interface PDPVerifierOptions {
2930
getNextChallengeEpoch?: (
3031
args: AbiToType<getNextChallengeEpoch['inputs']>
3132
) => AbiToType<getNextChallengeEpoch['outputs']>
33+
findPieceIdsByCid?: (args: AbiToType<findPieceIdsByCid['inputs']>) => AbiToType<findPieceIdsByCid['outputs']>
3234
}
3335

3436
/**
@@ -124,6 +126,15 @@ export function pdpVerifierCallHandler(data: Hex, options: JSONRPCOptions): Hex
124126
options.pdpVerifier.getNextChallengeEpoch(args)
125127
)
126128
}
129+
case 'findPieceIdsByCid': {
130+
if (!options.pdpVerifier?.findPieceIdsByCid) {
131+
throw new Error('PDP Verifier: findPieceIdsByCid is not defined')
132+
}
133+
return encodeAbiParameters(
134+
Abis.pdp.find((abi) => abi.type === 'function' && abi.name === 'findPieceIdsByCid')!.outputs,
135+
options.pdpVerifier.findPieceIdsByCid(args)
136+
)
137+
}
127138
default: {
128139
throw new Error(`PDP Verifier: unknown function: ${functionName} with args: ${args}`)
129140
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import type { Simplify } from 'type-fest'
2+
import {
3+
type Address,
4+
type Chain,
5+
type Client,
6+
type ContractFunctionParameters,
7+
type ContractFunctionReturnType,
8+
type ReadContractErrorType,
9+
type Transport,
10+
toHex,
11+
} from 'viem'
12+
import { readContract } from 'viem/actions'
13+
import type { pdpVerifierAbi } from '../abis/generated.ts'
14+
import { asChain } from '../chains.ts'
15+
import type { PieceCID } from '../piece/piece.ts'
16+
import type { ActionCallChain } from '../types.ts'
17+
18+
export namespace findPieceIdsByCid {
19+
export type OptionsType = {
20+
/** The ID of the data set to search in. */
21+
dataSetId: bigint
22+
/** The PieceCID to search for. */
23+
pieceCid: PieceCID
24+
/** The starting piece ID for the search. @default 0n */
25+
startPieceId?: bigint
26+
/** The maximum number of results to return. @default 1n */
27+
limit?: bigint
28+
/** PDP Verifier contract address. If not provided, the default is the PDP Verifier contract address for the chain. */
29+
contractAddress?: Address
30+
}
31+
32+
export type OutputType = readonly bigint[]
33+
34+
/**
35+
* `uint256[]` - Array of piece IDs matching the given CID
36+
*/
37+
export type ContractOutputType = ContractFunctionReturnType<
38+
typeof pdpVerifierAbi,
39+
'pure' | 'view',
40+
'findPieceIdsByCid'
41+
>
42+
43+
export type ErrorType = asChain.ErrorType | ReadContractErrorType
44+
}
45+
46+
/**
47+
* Find piece IDs for a given PieceCID in a data set.
48+
*
49+
* Uses the on-chain `findPieceIdsByCid` function for efficient CID→ID lookup.
50+
*
51+
* @example
52+
* ```ts
53+
* import { findPieceIdsByCid } from '@filoz/synapse-core/pdp-verifier'
54+
* import { calibration } from '@filoz/synapse-core/chains'
55+
* import { createPublicClient, http } from 'viem'
56+
* import * as Piece from '@filoz/synapse-core/piece'
57+
*
58+
* const client = createPublicClient({
59+
* chain: calibration,
60+
* transport: http(),
61+
* })
62+
*
63+
* const pieceCid = Piece.parse('bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbze7rxt7vqn7veigmy')
64+
* const pieceIds = await findPieceIdsByCid(client, {
65+
* dataSetId: 1n,
66+
* pieceCid,
67+
* })
68+
* // pieceIds is an array of bigint IDs matching the CID
69+
* ```
70+
*
71+
* @param client - The client to use to find piece IDs.
72+
* @param options - {@link findPieceIdsByCid.OptionsType}
73+
* @returns Array of piece IDs matching the CID {@link findPieceIdsByCid.OutputType}
74+
* @throws Errors {@link findPieceIdsByCid.ErrorType}
75+
*/
76+
export async function findPieceIdsByCid(
77+
client: Client<Transport, Chain>,
78+
options: findPieceIdsByCid.OptionsType
79+
): Promise<findPieceIdsByCid.OutputType> {
80+
return await readContract(
81+
client,
82+
findPieceIdsByCidCall({
83+
chain: client.chain,
84+
dataSetId: options.dataSetId,
85+
pieceCid: options.pieceCid,
86+
startPieceId: options.startPieceId,
87+
limit: options.limit,
88+
contractAddress: options.contractAddress,
89+
})
90+
)
91+
}
92+
93+
export namespace findPieceIdsByCidCall {
94+
export type OptionsType = Simplify<findPieceIdsByCid.OptionsType & ActionCallChain>
95+
export type ErrorType = asChain.ErrorType
96+
export type OutputType = ContractFunctionParameters<typeof pdpVerifierAbi, 'pure' | 'view', 'findPieceIdsByCid'>
97+
}
98+
99+
/**
100+
* Create a call to the {@link findPieceIdsByCid} function for use with the multicall or readContract function.
101+
*
102+
* @example
103+
* ```ts
104+
* import { findPieceIdsByCidCall } from '@filoz/synapse-core/pdp-verifier'
105+
* import { calibration } from '@filoz/synapse-core/chains'
106+
* import { createPublicClient, http } from 'viem'
107+
* import { readContract } from 'viem/actions'
108+
* import * as Piece from '@filoz/synapse-core/piece'
109+
*
110+
* const client = createPublicClient({
111+
* chain: calibration,
112+
* transport: http(),
113+
* })
114+
*
115+
* const pieceCid = Piece.parse('bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbze7rxt7vqn7veigmy')
116+
* const result = await readContract(client, findPieceIdsByCidCall({
117+
* chain: calibration,
118+
* dataSetId: 1n,
119+
* pieceCid,
120+
* }))
121+
* ```
122+
*
123+
* @param options - {@link findPieceIdsByCidCall.OptionsType}
124+
* @returns The call to the findPieceIdsByCid function {@link findPieceIdsByCidCall.OutputType}
125+
* @throws Errors {@link findPieceIdsByCidCall.ErrorType}
126+
*/
127+
export function findPieceIdsByCidCall(options: findPieceIdsByCidCall.OptionsType) {
128+
const chain = asChain(options.chain)
129+
return {
130+
abi: chain.contracts.pdp.abi,
131+
address: options.contractAddress ?? chain.contracts.pdp.address,
132+
functionName: 'findPieceIdsByCid',
133+
args: [options.dataSetId, { data: toHex(options.pieceCid.bytes) }, options.startPieceId ?? 0n, options.limit ?? 1n],
134+
} satisfies findPieceIdsByCidCall.OutputType
135+
}

packages/synapse-core/src/pdp-verifier/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { asChain } from '../chains.ts'
1111

1212
export * from './data-set-live.ts'
13+
export * from './find-piece-ids-by-cid.ts'
1314
export * from './get-active-piece-count.ts'
1415
export * from './get-active-pieces.ts'
1516
export * from './get-data-set-leaf-count.ts'
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import assert from 'assert'
2+
import { setup } from 'iso-web/msw'
3+
import { createPublicClient, http, toHex } from 'viem'
4+
import { calibration, mainnet } from '../src/chains.ts'
5+
import { JSONRPC, presets } from '../src/mocks/jsonrpc/index.ts'
6+
import { findPieceIdsByCid, findPieceIdsByCidCall } from '../src/pdp-verifier/find-piece-ids-by-cid.ts'
7+
import * as Piece from '../src/piece/piece.ts'
8+
9+
describe('findPieceIdsByCid', () => {
10+
const server = setup()
11+
12+
before(async () => {
13+
await server.start()
14+
})
15+
16+
after(() => {
17+
server.stop()
18+
})
19+
20+
beforeEach(() => {
21+
server.resetHandlers()
22+
})
23+
24+
describe('findPieceIdsByCidCall', () => {
25+
const pieceCid = Piece.parse('bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbze7rxt7vqn7veigmy')
26+
27+
it('should create call with calibration chain defaults', () => {
28+
const call = findPieceIdsByCidCall({
29+
chain: calibration,
30+
dataSetId: 1n,
31+
pieceCid,
32+
})
33+
34+
assert.equal(call.functionName, 'findPieceIdsByCid')
35+
assert.deepEqual(call.args, [1n, { data: toHex(pieceCid.bytes) }, 0n, 1n])
36+
assert.equal(call.address, calibration.contracts.pdp.address)
37+
assert.equal(call.abi, calibration.contracts.pdp.abi)
38+
})
39+
40+
it('should create call with mainnet chain defaults', () => {
41+
const call = findPieceIdsByCidCall({
42+
chain: mainnet,
43+
dataSetId: 1n,
44+
pieceCid,
45+
})
46+
47+
assert.equal(call.functionName, 'findPieceIdsByCid')
48+
assert.deepEqual(call.args, [1n, { data: toHex(pieceCid.bytes) }, 0n, 1n])
49+
assert.equal(call.address, mainnet.contracts.pdp.address)
50+
assert.equal(call.abi, mainnet.contracts.pdp.abi)
51+
})
52+
53+
it('should use provided startPieceId and limit', () => {
54+
const call = findPieceIdsByCidCall({
55+
chain: calibration,
56+
dataSetId: 1n,
57+
pieceCid,
58+
startPieceId: 10n,
59+
limit: 5n,
60+
})
61+
62+
assert.deepEqual(call.args, [1n, { data: toHex(pieceCid.bytes) }, 10n, 5n])
63+
})
64+
65+
it('should use custom address when provided', () => {
66+
const customAddress = '0x1234567890123456789012345678901234567890'
67+
const call = findPieceIdsByCidCall({
68+
chain: calibration,
69+
dataSetId: 1n,
70+
pieceCid,
71+
contractAddress: customAddress,
72+
})
73+
74+
assert.equal(call.address, customAddress)
75+
})
76+
})
77+
78+
describe('findPieceIdsByCid (with mocked RPC)', () => {
79+
it('should find piece IDs by CID', async () => {
80+
server.use(JSONRPC(presets.basic))
81+
82+
const client = createPublicClient({
83+
chain: calibration,
84+
transport: http(),
85+
})
86+
87+
const pieceCid = Piece.parse('bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbze7rxt7vqn7veigmy')
88+
const result = await findPieceIdsByCid(client, { dataSetId: 1n, pieceCid })
89+
90+
assert.ok(Array.isArray(result))
91+
assert.equal(result.length, 1)
92+
assert.equal(result[0], 0n)
93+
})
94+
95+
it('should return empty array when piece not found', async () => {
96+
server.use(
97+
JSONRPC({
98+
...presets.basic,
99+
pdpVerifier: {
100+
...presets.basic.pdpVerifier,
101+
findPieceIdsByCid: () => [[]],
102+
},
103+
})
104+
)
105+
106+
const client = createPublicClient({
107+
chain: calibration,
108+
transport: http(),
109+
})
110+
111+
const pieceCid = Piece.parse('bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbze7rxt7vqn7veigmy')
112+
const result = await findPieceIdsByCid(client, { dataSetId: 1n, pieceCid })
113+
114+
assert.ok(Array.isArray(result))
115+
assert.equal(result.length, 0)
116+
})
117+
118+
it('should return multiple piece IDs', async () => {
119+
server.use(
120+
JSONRPC({
121+
...presets.basic,
122+
pdpVerifier: {
123+
...presets.basic.pdpVerifier,
124+
findPieceIdsByCid: () => [[42n, 99n]],
125+
},
126+
})
127+
)
128+
129+
const client = createPublicClient({
130+
chain: calibration,
131+
transport: http(),
132+
})
133+
134+
const pieceCid = Piece.parse('bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbze7rxt7vqn7veigmy')
135+
const result = await findPieceIdsByCid(client, { dataSetId: 1n, pieceCid, limit: 10n })
136+
137+
assert.ok(Array.isArray(result))
138+
assert.equal(result.length, 2)
139+
assert.equal(result[0], 42n)
140+
assert.equal(result[1], 99n)
141+
})
142+
})
143+
})

packages/synapse-sdk/src/storage/context.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,15 +1048,16 @@ export class StorageContext {
10481048
throw createError('StorageContext', 'deletePiece', 'Invalid PieceCID provided')
10491049
}
10501050

1051-
const dataSetData = await SP.getDataSet({
1052-
serviceURL: this._pdpEndpoint,
1051+
const pieceIds = await PDPVerifier.findPieceIdsByCid(this._client, {
10531052
dataSetId: this.dataSetId,
1053+
pieceCid: parsedPieceCID,
1054+
startPieceId: 0n,
1055+
limit: 1n,
10541056
})
1055-
const pieceData = dataSetData.pieces.find((piece) => piece.pieceCid.toString() === parsedPieceCID.toString())
1056-
if (pieceData == null) {
1057+
if (pieceIds.length === 0) {
10571058
throw createError('StorageContext', 'deletePiece', 'Piece not found in data set')
10581059
}
1059-
return pieceData.pieceId
1060+
return pieceIds[0]
10601061
}
10611062

10621063
/**
@@ -1108,9 +1109,12 @@ export class StorageContext {
11081109
}
11091110

11101111
// Run multiple operations in parallel for better performance
1111-
const [activePieces, nextChallengeEpoch, currentEpoch, pdpConfig, providerInfo] = await Promise.all([
1112-
PDPVerifier.getActivePieces(this._client, {
1112+
const [pieceIds, nextChallengeEpoch, currentEpoch, pdpConfig, providerInfo] = await Promise.all([
1113+
PDPVerifier.findPieceIdsByCid(this._client, {
11131114
dataSetId: this.dataSetId,
1115+
pieceCid: parsedPieceCID,
1116+
startPieceId: 0n,
1117+
limit: 1n,
11141118
}),
11151119
PDPVerifier.getNextChallengeEpoch(this._client, {
11161120
dataSetId: this.dataSetId,
@@ -1123,14 +1127,13 @@ export class StorageContext {
11231127
this.getProviderInfo().catch(() => null),
11241128
])
11251129

1126-
const pieceData = activePieces.pieces.find((piece) => piece.cid.equals(parsedPieceCID))
1127-
if (pieceData === undefined) {
1130+
if (pieceIds.length === 0) {
11281131
return null
11291132
}
1133+
const pieceId = pieceIds[0]
11301134

11311135
// Initialize return values
11321136
let retrievalUrl: string | null = null
1133-
let pieceId: bigint | undefined
11341137
let lastProven: Date | null = null
11351138
let nextProofDue: Date | null = null
11361139
let inChallengeWindow = false
@@ -1147,8 +1150,6 @@ export class StorageContext {
11471150

11481151
// Process proof timing data if we have data set data and PDP config
11491152
if (pdpConfig != null) {
1150-
pieceId = pieceData.id
1151-
11521153
// Calculate timing based on nextChallengeEpoch
11531154
if (nextChallengeEpoch > 0n) {
11541155
// nextChallengeEpoch is when the challenge window STARTS, not ends!

0 commit comments

Comments
 (0)