Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/synapse-core/src/mocks/jsonrpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ export const presets = {
getDataSetLeafCount: () => [0n],
getScheduledRemovals: () => [[]],
getNextChallengeEpoch: () => [5000n],
findPieceIdsByCid: () => [[0n]],
},
serviceRegistry: {
registerProvider: () => [1n],
Expand Down
11 changes: 11 additions & 0 deletions packages/synapse-core/src/mocks/jsonrpc/pdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type getDataSetStorageProvider = ExtractAbiFunction<typeof Abis.pdp, 'get
export type getDataSetLeafCount = ExtractAbiFunction<typeof Abis.pdp, 'getDataSetLeafCount'>
export type getScheduledRemovals = ExtractAbiFunction<typeof Abis.pdp, 'getScheduledRemovals'>
export type getNextChallengeEpoch = ExtractAbiFunction<typeof Abis.pdp, 'getNextChallengeEpoch'>
export type findPieceIdsByCid = ExtractAbiFunction<typeof Abis.pdp, 'findPieceIdsByCid'>

export interface PDPVerifierOptions {
dataSetLive?: (args: AbiToType<dataSetLive['inputs']>) => AbiToType<dataSetLive['outputs']>
Expand All @@ -29,6 +30,7 @@ export interface PDPVerifierOptions {
getNextChallengeEpoch?: (
args: AbiToType<getNextChallengeEpoch['inputs']>
) => AbiToType<getNextChallengeEpoch['outputs']>
findPieceIdsByCid?: (args: AbiToType<findPieceIdsByCid['inputs']>) => AbiToType<findPieceIdsByCid['outputs']>
}

/**
Expand Down Expand Up @@ -124,6 +126,15 @@ export function pdpVerifierCallHandler(data: Hex, options: JSONRPCOptions): Hex
options.pdpVerifier.getNextChallengeEpoch(args)
)
}
case 'findPieceIdsByCid': {
if (!options.pdpVerifier?.findPieceIdsByCid) {
throw new Error('PDP Verifier: findPieceIdsByCid is not defined')
}
return encodeAbiParameters(
Abis.pdp.find((abi) => abi.type === 'function' && abi.name === 'findPieceIdsByCid')!.outputs,
options.pdpVerifier.findPieceIdsByCid(args)
)
}
default: {
throw new Error(`PDP Verifier: unknown function: ${functionName} with args: ${args}`)
}
Expand Down
135 changes: 135 additions & 0 deletions packages/synapse-core/src/pdp-verifier/find-piece-ids-by-cid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import type { Simplify } from 'type-fest'
import {
type Address,
type Chain,
type Client,
type ContractFunctionParameters,
type ContractFunctionReturnType,
type ReadContractErrorType,
type Transport,
toHex,
} from 'viem'
import { readContract } from 'viem/actions'
import type { pdpVerifierAbi } from '../abis/generated.ts'
import { asChain } from '../chains.ts'
import type { PieceCID } from '../piece/piece.ts'
import type { ActionCallChain } from '../types.ts'

export namespace findPieceIdsByCid {
export type OptionsType = {
/** The ID of the data set to search in. */
dataSetId: bigint
/** The PieceCID to search for. */
pieceCid: PieceCID
/** The starting piece ID for the search. @default 0n */
startPieceId?: bigint
/** The maximum number of results to return. @default 1n */
limit?: bigint
/** PDP Verifier contract address. If not provided, the default is the PDP Verifier contract address for the chain. */
contractAddress?: Address
}

export type OutputType = readonly bigint[]

/**
* `uint256[]` - Array of piece IDs matching the given CID
*/
export type ContractOutputType = ContractFunctionReturnType<
typeof pdpVerifierAbi,
'pure' | 'view',
'findPieceIdsByCid'
>

export type ErrorType = asChain.ErrorType | ReadContractErrorType
}

/**
* Find piece IDs for a given PieceCID in a data set.
*
* Uses the on-chain `findPieceIdsByCid` function for efficient CID→ID lookup.
*
* @example
* ```ts
* import { findPieceIdsByCid } from '@filoz/synapse-core/pdp-verifier'
* import { calibration } from '@filoz/synapse-core/chains'
* import { createPublicClient, http } from 'viem'
* import * as Piece from '@filoz/synapse-core/piece'
*
* const client = createPublicClient({
* chain: calibration,
* transport: http(),
* })
*
* const pieceCid = Piece.parse('bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbze7rxt7vqn7veigmy')
* const pieceIds = await findPieceIdsByCid(client, {
* dataSetId: 1n,
* pieceCid,
* })
* // pieceIds is an array of bigint IDs matching the CID
* ```
*
* @param client - The client to use to find piece IDs.
* @param options - {@link findPieceIdsByCid.OptionsType}
* @returns Array of piece IDs matching the CID {@link findPieceIdsByCid.OutputType}
* @throws Errors {@link findPieceIdsByCid.ErrorType}
*/
export async function findPieceIdsByCid(
client: Client<Transport, Chain>,
options: findPieceIdsByCid.OptionsType
): Promise<findPieceIdsByCid.OutputType> {
return await readContract(
client,
findPieceIdsByCidCall({
chain: client.chain,
dataSetId: options.dataSetId,
pieceCid: options.pieceCid,
startPieceId: options.startPieceId,
limit: options.limit,
contractAddress: options.contractAddress,
})
)
}

export namespace findPieceIdsByCidCall {
export type OptionsType = Simplify<findPieceIdsByCid.OptionsType & ActionCallChain>
export type ErrorType = asChain.ErrorType
export type OutputType = ContractFunctionParameters<typeof pdpVerifierAbi, 'pure' | 'view', 'findPieceIdsByCid'>
}

/**
* Create a call to the {@link findPieceIdsByCid} function for use with the multicall or readContract function.
*
* @example
* ```ts
* import { findPieceIdsByCidCall } from '@filoz/synapse-core/pdp-verifier'
* import { calibration } from '@filoz/synapse-core/chains'
* import { createPublicClient, http } from 'viem'
* import { readContract } from 'viem/actions'
* import * as Piece from '@filoz/synapse-core/piece'
*
* const client = createPublicClient({
* chain: calibration,
* transport: http(),
* })
*
* const pieceCid = Piece.parse('bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbze7rxt7vqn7veigmy')
* const result = await readContract(client, findPieceIdsByCidCall({
* chain: calibration,
* dataSetId: 1n,
* pieceCid,
* }))
* ```
*
* @param options - {@link findPieceIdsByCidCall.OptionsType}
* @returns The call to the findPieceIdsByCid function {@link findPieceIdsByCidCall.OutputType}
* @throws Errors {@link findPieceIdsByCidCall.ErrorType}
*/
export function findPieceIdsByCidCall(options: findPieceIdsByCidCall.OptionsType) {
const chain = asChain(options.chain)
return {
abi: chain.contracts.pdp.abi,
address: options.contractAddress ?? chain.contracts.pdp.address,
functionName: 'findPieceIdsByCid',
args: [options.dataSetId, { data: toHex(options.pieceCid.bytes) }, options.startPieceId ?? 0n, options.limit ?? 1n],
} satisfies findPieceIdsByCidCall.OutputType
}
1 change: 1 addition & 0 deletions packages/synapse-core/src/pdp-verifier/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { asChain } from '../chains.ts'

export * from './data-set-live.ts'
export * from './find-piece-ids-by-cid.ts'
export * from './get-active-piece-count.ts'
export * from './get-active-pieces.ts'
export * from './get-data-set-leaf-count.ts'
Expand Down
143 changes: 143 additions & 0 deletions packages/synapse-core/test/find-piece-ids-by-cid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import assert from 'assert'
import { setup } from 'iso-web/msw'
import { createPublicClient, http, toHex } from 'viem'
import { calibration, mainnet } from '../src/chains.ts'
import { JSONRPC, presets } from '../src/mocks/jsonrpc/index.ts'
import { findPieceIdsByCid, findPieceIdsByCidCall } from '../src/pdp-verifier/find-piece-ids-by-cid.ts'
import * as Piece from '../src/piece/piece.ts'

describe('findPieceIdsByCid', () => {
const server = setup()

before(async () => {
await server.start()
})

after(() => {
server.stop()
})

beforeEach(() => {
server.resetHandlers()
})

describe('findPieceIdsByCidCall', () => {
const pieceCid = Piece.parse('bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbze7rxt7vqn7veigmy')

it('should create call with calibration chain defaults', () => {
const call = findPieceIdsByCidCall({
chain: calibration,
dataSetId: 1n,
pieceCid,
})

assert.equal(call.functionName, 'findPieceIdsByCid')
assert.deepEqual(call.args, [1n, { data: toHex(pieceCid.bytes) }, 0n, 1n])
assert.equal(call.address, calibration.contracts.pdp.address)
assert.equal(call.abi, calibration.contracts.pdp.abi)
})

it('should create call with mainnet chain defaults', () => {
const call = findPieceIdsByCidCall({
chain: mainnet,
dataSetId: 1n,
pieceCid,
})

assert.equal(call.functionName, 'findPieceIdsByCid')
assert.deepEqual(call.args, [1n, { data: toHex(pieceCid.bytes) }, 0n, 1n])
assert.equal(call.address, mainnet.contracts.pdp.address)
assert.equal(call.abi, mainnet.contracts.pdp.abi)
})

it('should use provided startPieceId and limit', () => {
const call = findPieceIdsByCidCall({
chain: calibration,
dataSetId: 1n,
pieceCid,
startPieceId: 10n,
limit: 5n,
})

assert.deepEqual(call.args, [1n, { data: toHex(pieceCid.bytes) }, 10n, 5n])
})

it('should use custom address when provided', () => {
const customAddress = '0x1234567890123456789012345678901234567890'
const call = findPieceIdsByCidCall({
chain: calibration,
dataSetId: 1n,
pieceCid,
contractAddress: customAddress,
})

assert.equal(call.address, customAddress)
})
})

describe('findPieceIdsByCid (with mocked RPC)', () => {
it('should find piece IDs by CID', async () => {
server.use(JSONRPC(presets.basic))

const client = createPublicClient({
chain: calibration,
transport: http(),
})

const pieceCid = Piece.parse('bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbze7rxt7vqn7veigmy')
const result = await findPieceIdsByCid(client, { dataSetId: 1n, pieceCid })

assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0], 0n)
})

it('should return empty array when piece not found', async () => {
server.use(
JSONRPC({
...presets.basic,
pdpVerifier: {
...presets.basic.pdpVerifier,
findPieceIdsByCid: () => [[]],
},
})
)

const client = createPublicClient({
chain: calibration,
transport: http(),
})

const pieceCid = Piece.parse('bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbze7rxt7vqn7veigmy')
const result = await findPieceIdsByCid(client, { dataSetId: 1n, pieceCid })

assert.ok(Array.isArray(result))
assert.equal(result.length, 0)
})

it('should return multiple piece IDs', async () => {
server.use(
JSONRPC({
...presets.basic,
pdpVerifier: {
...presets.basic.pdpVerifier,
findPieceIdsByCid: () => [[42n, 99n]],
},
})
)

const client = createPublicClient({
chain: calibration,
transport: http(),
})

const pieceCid = Piece.parse('bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbze7rxt7vqn7veigmy')
const result = await findPieceIdsByCid(client, { dataSetId: 1n, pieceCid, limit: 10n })

assert.ok(Array.isArray(result))
assert.equal(result.length, 2)
assert.equal(result[0], 42n)
assert.equal(result[1], 99n)
})
})
})
25 changes: 13 additions & 12 deletions packages/synapse-sdk/src/storage/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1048,15 +1048,16 @@ export class StorageContext {
throw createError('StorageContext', 'deletePiece', 'Invalid PieceCID provided')
}

const dataSetData = await SP.getDataSet({
serviceURL: this._pdpEndpoint,
const pieceIds = await PDPVerifier.findPieceIdsByCid(this._client, {
dataSetId: this.dataSetId,
pieceCid: parsedPieceCID,
startPieceId: 0n,
limit: 1n,
})
const pieceData = dataSetData.pieces.find((piece) => piece.pieceCid.toString() === parsedPieceCID.toString())
if (pieceData == null) {
if (pieceIds.length === 0) {
throw createError('StorageContext', 'deletePiece', 'Piece not found in data set')
}
Comment on lines +1057 to 1059
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

This Piece not found in data set error is thrown from _getPieceIdByCID, but the createError context labels it as 'deletePiece'. Rename the method in the error context to 'getPieceIdByCID' so consumers can identify the real source of the failure.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

same applies here as per above comment

return pieceData.pieceId
return pieceIds[0]
}

/**
Expand Down Expand Up @@ -1108,9 +1109,12 @@ export class StorageContext {
}

// Run multiple operations in parallel for better performance
const [activePieces, nextChallengeEpoch, currentEpoch, pdpConfig, providerInfo] = await Promise.all([
PDPVerifier.getActivePieces(this._client, {
const [pieceIds, nextChallengeEpoch, currentEpoch, pdpConfig, providerInfo] = await Promise.all([
PDPVerifier.findPieceIdsByCid(this._client, {
dataSetId: this.dataSetId,
pieceCid: parsedPieceCID,
startPieceId: 0n,
limit: 1n,
}),
PDPVerifier.getNextChallengeEpoch(this._client, {
dataSetId: this.dataSetId,
Expand All @@ -1123,14 +1127,13 @@ export class StorageContext {
this.getProviderInfo().catch(() => null),
])

const pieceData = activePieces.pieces.find((piece) => piece.cid.equals(parsedPieceCID))
if (pieceData === undefined) {
if (pieceIds.length === 0) {
return null
}
const pieceId = pieceIds[0]

// Initialize return values
let retrievalUrl: string | null = null
let pieceId: bigint | undefined
let lastProven: Date | null = null
let nextProofDue: Date | null = null
let inChallengeWindow = false
Expand All @@ -1147,8 +1150,6 @@ export class StorageContext {

// Process proof timing data if we have data set data and PDP config
if (pdpConfig != null) {
pieceId = pieceData.id

// Calculate timing based on nextChallengeEpoch
if (nextChallengeEpoch > 0n) {
// nextChallengeEpoch is when the challenge window STARTS, not ends!
Expand Down
Loading