Skip to content

Commit 12a5bf7

Browse files
virajbhartiyarvagg
andauthored
feat(pdp): implement ping validation for storage providers in selection process (#119)
* feat: implement ping validation for storage providers in selection process * refactor(pdp): update PDPServer to handle null authHelper and improve provider selection logic in StorageService * fix(storage): remove unnecessary blank line in StorageService * fix(pdp): allow empty API and retrieval endpoints for null authHelper in PDPServer * refactor(storage): clarify comments in StorageService regarding proof set ID and ping validation parameter * refactor(storage): remove unused PDPServer instances and simplify provider selection logic in StorageService * refactor(storage): streamline provider selection logic by removing unused parameters and enhancing inline sorting * refactor(storage): update comments for clarity and enhance test coverage for provider selection logic * refactor(pdp): simplify error handling for API and retrieval endpoints in PDPServer * refactor(storage): remove unused parameters from selectProviderWithPing and update related tests for clarity * fix(pdp): update ping endpoint in PDPServer to include 'pdp' in the URL * test(pdp): update urls to avoid duplicate suffixes --------- Co-authored-by: Rod Vagg <[email protected]>
1 parent 3f48808 commit 12a5bf7

File tree

4 files changed

+684
-85
lines changed

4 files changed

+684
-85
lines changed

src/pdp/server.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export interface RootAdditionStatusResponse {
117117
export class PDPServer {
118118
private readonly _apiEndpoint: string
119119
private readonly _retrievalEndpoint: string
120-
private readonly _authHelper: PDPAuthHelper
120+
private readonly _authHelper: PDPAuthHelper | null
121121
private readonly _serviceName: string
122122

123123
/**
@@ -128,7 +128,7 @@ export class PDPServer {
128128
* @param serviceName - Service name for uploads (defaults to 'public')
129129
*/
130130
constructor (
131-
authHelper: PDPAuthHelper,
131+
authHelper: PDPAuthHelper | null,
132132
apiEndpoint: string,
133133
retrievalEndpoint: string,
134134
serviceName: string = 'public'
@@ -161,13 +161,13 @@ export class PDPServer {
161161
recordKeeper: string
162162
): Promise<CreateProofSetResponse> {
163163
// Generate the EIP-712 signature for proof set creation
164-
const authData = await this._authHelper.signCreateProofSet(clientDataSetId, payee, withCDN)
164+
const authData = await this.getAuthHelper().signCreateProofSet(clientDataSetId, payee, withCDN)
165165

166166
// Prepare the extra data for the contract call
167167
// This needs to match the ProofSetCreateData struct in Pandora contract
168168
const extraData = this._encodeProofSetCreateData({
169169
metadata: '', // Empty metadata for now
170-
payer: await this._authHelper.getSignerAddress(),
170+
payer: await this.getAuthHelper().getSignerAddress(),
171171
withCDN,
172172
signature: authData.signature
173173
})
@@ -251,7 +251,7 @@ export class PDPServer {
251251
}
252252

253253
// Generate the EIP-712 signature for adding roots
254-
const authData = await this._authHelper.signAddRoots(
254+
const authData = await this.getAuthHelper().signAddRoots(
255255
clientDataSetId,
256256
nextRootId,
257257
rootDataArray // Pass RootData[] directly to auth helper
@@ -578,7 +578,7 @@ export class PDPServer {
578578
* @throws Error if provider is not reachable or returns non-200 status
579579
*/
580580
async ping (): Promise<void> {
581-
const response = await fetch(`${this._apiEndpoint}/ping`, {
581+
const response = await fetch(`${this._apiEndpoint}/pdp/ping`, {
582582
method: 'GET',
583583
headers: {}
584584
})
@@ -594,6 +594,9 @@ export class PDPServer {
594594
}
595595

596596
getAuthHelper (): PDPAuthHelper {
597+
if (this._authHelper == null) {
598+
throw new Error('AuthHelper is not available for an operation that requires signing')
599+
}
597600
return this._authHelper
598601
}
599602
}

src/storage/service.ts

Lines changed: 109 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -584,24 +584,44 @@ export class StorageService {
584584
if (b.currentRootCount > 0 && a.currentRootCount === 0) return 1
585585
return a.pdpVerifierProofSetId - b.pdpVerifierProofSetId
586586
})
587-
const selected = sorted[0]
588587

589-
// Get the provider for this proof set
590-
const providerId = await pandoraService.getProviderIdByAddress(selected.payee)
591-
if (providerId === 0) {
592-
throw createError(
593-
'StorageService',
594-
'smartSelectProvider',
595-
`Provider ${selected.payee} for proof set ${selected.pdpVerifierProofSetId} is not currently approved`
596-
)
588+
// Convert sorted proof sets to providers and use shared selection logic
589+
const existingProviders: ApprovedProviderInfo[] = []
590+
for (const proofSet of sorted) {
591+
const providerAddress = proofSet.payee.toLowerCase()
592+
if (existingProviders.some(p => p.owner.toLowerCase() === providerAddress)) {
593+
continue
594+
}
595+
const providerId = await pandoraService.getProviderIdByAddress(proofSet.payee)
596+
if (providerId === 0) {
597+
console.warn(`Provider ${proofSet.payee} for proof set ${proofSet.pdpVerifierProofSetId} is not currently approved, skipping`)
598+
continue
599+
}
600+
const provider = await pandoraService.getApprovedProvider(providerId)
601+
existingProviders.push(provider)
597602
}
598603

599-
const provider = await pandoraService.getApprovedProvider(providerId)
604+
if (existingProviders.length > 0) {
605+
const selectedProvider = await StorageService.selectProviderWithPing(existingProviders)
600606

601-
return {
602-
provider,
603-
proofSetId: selected.pdpVerifierProofSetId,
604-
isExisting: true
607+
// Find the first matching proof set ID for this provider
608+
const matchingProofSet = sorted.find(ps =>
609+
ps.payee.toLowerCase() === selectedProvider.owner.toLowerCase()
610+
)
611+
612+
if (matchingProofSet == null) {
613+
throw createError(
614+
'StorageService',
615+
'smartSelectProvider',
616+
'Selected provider not found in proof sets'
617+
)
618+
}
619+
620+
return {
621+
provider: selectedProvider,
622+
proofSetId: matchingProofSet.pdpVerifierProofSetId,
623+
isExisting: true
624+
}
605625
}
606626
}
607627

@@ -627,33 +647,90 @@ export class StorageService {
627647
}
628648

629649
/**
630-
* Select a random provider from the given list
650+
* Select a random provider from the given list with ping validation
651+
* @param providers - List of available providers
652+
* @param signer - Signer for entropy generation
653+
* @returns A provider that responds to ping
654+
* @throws Error if no providers are reachable
631655
*/
632656
private static async selectRandomProvider (
633657
providers: ApprovedProviderInfo[],
634658
signer: ethers.Signer
635659
): Promise<ApprovedProviderInfo> {
636-
let randomIndex: number
660+
if (providers.length === 0) {
661+
throw createError(
662+
'StorageService',
663+
'selectRandomProvider',
664+
'No providers available'
665+
)
666+
}
637667

638-
// Try crypto.getRandomValues if available (HTTPS contexts)
639-
if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues != null) {
640-
const randomBytes = new Uint8Array(1)
641-
globalThis.crypto.getRandomValues(randomBytes)
642-
randomIndex = randomBytes[0] % providers.length
643-
} else {
644-
// Fallback for HTTP contexts - use multiple entropy sources
645-
const timestamp = Date.now()
646-
const random = Math.random()
647-
// Use wallet address as additional entropy
648-
const addressBytes = await signer.getAddress()
649-
const addressSum = addressBytes.split('').reduce((a, c) => a + c.charCodeAt(0), 0)
668+
// Randomly sort providers inline
669+
const randomlySorted: ApprovedProviderInfo[] = []
670+
const remaining = [...providers]
671+
672+
while (remaining.length > 0) {
673+
let randomIndex: number
674+
675+
// Try crypto.getRandomValues if available (HTTPS contexts)
676+
if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues != null) {
677+
const randomBytes = new Uint8Array(1)
678+
globalThis.crypto.getRandomValues(randomBytes)
679+
randomIndex = randomBytes[0] % remaining.length
680+
} else {
681+
// Fallback for HTTP contexts - use multiple entropy sources
682+
const timestamp = Date.now()
683+
const random = Math.random()
684+
// Use wallet address as additional entropy
685+
const addressBytes = await signer.getAddress()
686+
const addressSum = addressBytes.split('').reduce((a, c) => a + c.charCodeAt(0), 0)
687+
688+
// Combine sources for better distribution
689+
const combined = (timestamp * random * addressSum) % remaining.length
690+
randomIndex = Math.floor(Math.abs(combined))
691+
}
692+
693+
randomlySorted.push(remaining.splice(randomIndex, 1)[0])
694+
}
695+
696+
return await StorageService.selectProviderWithPing(randomlySorted)
697+
}
698+
699+
/**
700+
* Select a provider from a sorted list with ping validation.
701+
* This is shared logic used by both smart selection and random selection.
702+
* @param sortedProviders - List of providers to try in order
703+
* @returns A provider that responds to ping
704+
* @throws Error if no providers are reachable
705+
*/
706+
private static async selectProviderWithPing (sortedProviders: ApprovedProviderInfo[]): Promise<ApprovedProviderInfo> {
707+
if (sortedProviders.length === 0) {
708+
throw createError(
709+
'StorageService',
710+
'selectProviderWithPing',
711+
'No reachable storage providers available after ping validation'
712+
)
713+
}
650714

651-
// Combine sources for better distribution
652-
const combined = (timestamp * random * addressSum) % providers.length
653-
randomIndex = Math.floor(Math.abs(combined))
715+
// Try providers in order until we find one that responds to ping
716+
for (const provider of sortedProviders) {
717+
try {
718+
// Create a temporary PDPServer for this specific provider's endpoint
719+
const providerPdpServer = new PDPServer(null, provider.pdpUrl, provider.pieceRetrievalUrl)
720+
await providerPdpServer.ping()
721+
return provider
722+
} catch (error) {
723+
console.warn(`Provider ${provider.owner} failed ping test:`, error instanceof Error ? error.message : String(error))
724+
// Continue to next provider
725+
}
654726
}
655727

656-
return providers[randomIndex]
728+
// All providers failed ping test
729+
throw createError(
730+
'StorageService',
731+
'selectProviderWithPing',
732+
`All ${sortedProviders.length} available storage providers failed ping validation`
733+
)
657734
}
658735

659736
/**

0 commit comments

Comments
 (0)