Skip to content

Commit 9ee7f5b

Browse files
authored
feat: implement SynapseStorage#pieceStatus(commp) (#127)
Closes: #16
1 parent 579ef07 commit 9ee7f5b

File tree

12 files changed

+1101
-4
lines changed

12 files changed

+1101
-4
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,12 @@ const downloaded = await storage.providerDownload(result.commp)
384384
// Get the list of root CIDs in the current proof set by querying the provider
385385
const rootCids = await storage.getProofSetRoots()
386386
console.log(`Root CIDs: ${rootCids.map(cid => cid.toString()).join(', ')}`)
387+
388+
// Check the status of a piece on the storage provider
389+
const status = await storage.pieceStatus(result.commp)
390+
console.log(`Piece exists: ${status.exists}`)
391+
console.log(`Proof set last proven: ${status.proofSetLastProven}`)
392+
console.log(`Proof set next proof due: ${status.proofSetNextProofDue}`)
387393
```
388394

389395
**Storage Service Methods:**
@@ -392,6 +398,7 @@ console.log(`Root CIDs: ${rootCids.map(cid => cid.toString()).join(', ')}`)
392398
- `preflightUpload(dataSize)` - Check if an upload is possible before attempting it
393399
- `getProviderInfo()` - Get detailed information about the selected storage provider
394400
- `getProofSetRoots()` - Get the list of root CIDs in the proof set by querying the provider
401+
- `pieceStatus(commp)` - Get the status of a piece including proof set timing information
395402

396403
##### Size Constraints
397404

src/pandora/service.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,4 +879,75 @@ export class PandoraService {
879879
epochsPerMonth: result.epochsPerMonth
880880
}
881881
}
882+
883+
// ========== Proving Period Operations ==========
884+
885+
/**
886+
* Get the maximum proving period in epochs
887+
* This is the maximum time allowed between proofs before a fault is recorded
888+
* @returns Maximum proving period in epochs
889+
*/
890+
async getMaxProvingPeriod (): Promise<number> {
891+
const contract = this._getPandoraContract()
892+
const maxProvingPeriod = await contract.getMaxProvingPeriod()
893+
return Number(maxProvingPeriod)
894+
}
895+
896+
/**
897+
* Get the challenge window size in epochs
898+
* This is the window at the end of each proving period where proofs can be submitted
899+
* @returns Challenge window size in epochs
900+
*/
901+
async getChallengeWindow (): Promise<number> {
902+
const contract = this._getPandoraContract()
903+
const challengeWindow = await contract.challengeWindow()
904+
return Number(challengeWindow)
905+
}
906+
907+
/**
908+
* Get the maximum proving period in hours
909+
* Convenience method that converts epochs to hours
910+
* @returns Maximum proving period in hours
911+
*/
912+
async getProvingPeriodInHours (): Promise<number> {
913+
const maxProvingPeriod = await this.getMaxProvingPeriod()
914+
// Convert epochs to hours: epochs * 30 seconds / 3600 seconds per hour
915+
return (maxProvingPeriod * 30) / 3600
916+
}
917+
918+
/**
919+
* Get the challenge window in minutes
920+
* Convenience method that converts epochs to minutes
921+
* @returns Challenge window in minutes
922+
*/
923+
async getChallengeWindowInMinutes (): Promise<number> {
924+
const challengeWindow = await this.getChallengeWindow()
925+
// Convert epochs to minutes: epochs * 30 seconds / 60 seconds per minute
926+
return (challengeWindow * 30) / 60
927+
}
928+
929+
/**
930+
* Get comprehensive proving period information
931+
* @returns Object with all proving period timing information
932+
*/
933+
async getProvingPeriodInfo (): Promise<{
934+
maxProvingPeriodEpochs: number
935+
challengeWindowEpochs: number
936+
maxProvingPeriodHours: number
937+
challengeWindowMinutes: number
938+
epochDurationSeconds: number
939+
}> {
940+
const [maxProvingPeriod, challengeWindow] = await Promise.all([
941+
this.getMaxProvingPeriod(),
942+
this.getChallengeWindow()
943+
])
944+
945+
return {
946+
maxProvingPeriodEpochs: maxProvingPeriod,
947+
challengeWindowEpochs: challengeWindow,
948+
maxProvingPeriodHours: (maxProvingPeriod * 30) / 3600,
949+
challengeWindowMinutes: (challengeWindow * 30) / 60,
950+
epochDurationSeconds: 30
951+
}
952+
}
882953
}

src/storage/service.ts

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@ import type {
1919
UploadCallbacks,
2020
UploadResult,
2121
RootData,
22-
CommP
22+
CommP,
23+
PieceStatus
2324
} from '../types.js'
2425
import type { Synapse } from '../synapse.js'
2526
import type { PandoraService } from '../pandora/service.js'
2627
import { PDPServer } from '../pdp/server.js'
2728
import { PDPAuthHelper } from '../pdp/auth.js'
28-
import { createError } from '../utils/index.js'
29+
import { createError, epochToDate, calculateLastProofDate, timeUntilEpoch } from '../utils/index.js'
2930
import { SIZE_CONSTANTS, TIMING_CONSTANTS } from '../utils/constants.js'
31+
import { asCommP } from '../commp/index.js'
3032

3133
export class StorageService {
3234
private readonly _synapse: Synapse
@@ -1028,4 +1030,131 @@ export class StorageService {
10281030
const proofSetData = await this._pdpServer.getProofSet(this._proofSetId)
10291031
return proofSetData.roots.map(root => root.rootCid)
10301032
}
1033+
1034+
/**
1035+
* Get the status of a piece on this storage provider
1036+
* This method checks if the piece exists on the provider and provides proof timing information
1037+
* for the proof set containing this piece.
1038+
*
1039+
* Note: Proofs are submitted for entire proof sets, not individual pieces. The timing information
1040+
* returned reflects when the proof set (containing this piece) was last proven and when the next
1041+
* proof is due.
1042+
*
1043+
* @param commp - The CommP (piece CID) to check
1044+
* @returns Status information including existence, proof set timing, and retrieval URL
1045+
*/
1046+
async pieceStatus (commp: string | CommP): Promise<PieceStatus> {
1047+
const parsedCommP = asCommP(commp)
1048+
if (parsedCommP == null) {
1049+
throw createError('StorageService', 'pieceStatus', 'Invalid CommP provided')
1050+
}
1051+
1052+
// Run multiple operations in parallel for better performance
1053+
const [pieceCheckResult, proofSetData, currentEpoch] = await Promise.all([
1054+
// Check if piece exists on provider
1055+
this._pdpServer.findPiece(parsedCommP, 0).then(() => true).catch(() => false),
1056+
// Get proof set data
1057+
this._pdpServer.getProofSet(this._proofSetId).catch((error) => {
1058+
console.debug('Failed to get proof set data:', error)
1059+
return null
1060+
}),
1061+
// Get current epoch
1062+
this._synapse.payments.getCurrentEpoch()
1063+
])
1064+
1065+
const exists = pieceCheckResult
1066+
const network = this._synapse.getNetwork()
1067+
1068+
// Initialize return values
1069+
let retrievalUrl: string | null = null
1070+
let rootId: number | undefined
1071+
let lastProven: Date | null = null
1072+
let nextProofDue: Date | null = null
1073+
let inChallengeWindow = false
1074+
let hoursUntilChallengeWindow = 0
1075+
let isProofOverdue = false
1076+
1077+
// If piece exists, get provider info for retrieval URL and proving params in parallel
1078+
if (exists) {
1079+
const [providerInfo, provingParams] = await Promise.all([
1080+
// Get provider info for retrieval URL
1081+
this.getProviderInfo().catch(() => null),
1082+
// Get proving period configuration (only if we have proof set data)
1083+
proofSetData != null
1084+
? Promise.all([
1085+
this._pandoraService.getMaxProvingPeriod(),
1086+
this._pandoraService.getChallengeWindow()
1087+
]).then(([maxProvingPeriod, challengeWindow]) => ({ maxProvingPeriod, challengeWindow }))
1088+
.catch(() => null)
1089+
: Promise.resolve(null)
1090+
])
1091+
1092+
// Set retrieval URL if we have provider info
1093+
if (providerInfo != null) {
1094+
// Remove trailing slash from pieceRetrievalUrl to avoid double slashes
1095+
retrievalUrl = `${providerInfo.pieceRetrievalUrl.replace(/\/$/, '')}/piece/${parsedCommP.toString()}`
1096+
}
1097+
1098+
// Process proof timing data if we have proof set data and proving params
1099+
if (proofSetData != null && provingParams != null) {
1100+
// Check if this CommP is in the proof set
1101+
const rootData = proofSetData.roots.find(root => root.rootCid.toString() === parsedCommP.toString())
1102+
1103+
if (rootData != null) {
1104+
rootId = rootData.rootId
1105+
1106+
// Calculate timing based on nextChallengeEpoch
1107+
if (proofSetData.nextChallengeEpoch > 0) {
1108+
// nextChallengeEpoch is when the challenge window STARTS, not ends!
1109+
// The proving deadline is nextChallengeEpoch + challengeWindow
1110+
const challengeWindowStart = proofSetData.nextChallengeEpoch
1111+
const provingDeadline = challengeWindowStart + provingParams.challengeWindow
1112+
1113+
// Calculate when the next proof is due (end of challenge window)
1114+
nextProofDue = epochToDate(provingDeadline, network)
1115+
1116+
// Calculate last proven date (one proving period before next challenge)
1117+
const lastProvenDate = calculateLastProofDate(
1118+
proofSetData.nextChallengeEpoch,
1119+
provingParams.maxProvingPeriod,
1120+
network
1121+
)
1122+
if (lastProvenDate != null) {
1123+
lastProven = lastProvenDate
1124+
}
1125+
1126+
// Check if we're in the challenge window
1127+
inChallengeWindow = currentEpoch >= challengeWindowStart && currentEpoch < provingDeadline
1128+
1129+
// Check if proof is overdue (past the proving deadline)
1130+
isProofOverdue = currentEpoch >= provingDeadline
1131+
1132+
// Calculate hours until challenge window starts (only if before challenge window)
1133+
if (currentEpoch < challengeWindowStart) {
1134+
const timeUntil = timeUntilEpoch(challengeWindowStart, Number(currentEpoch))
1135+
hoursUntilChallengeWindow = timeUntil.hours
1136+
}
1137+
} else {
1138+
// If nextChallengeEpoch is 0, it might mean:
1139+
// 1. Proof was just submitted and system is updating
1140+
// 2. Proof set is not active
1141+
// In case 1, we might have just proven, so set lastProven to very recent
1142+
// This is a temporary state and should resolve quickly
1143+
console.debug('Proof set has nextChallengeEpoch=0, may have just been proven')
1144+
}
1145+
}
1146+
}
1147+
}
1148+
1149+
return {
1150+
exists,
1151+
proofSetLastProven: lastProven,
1152+
proofSetNextProofDue: nextProofDue,
1153+
retrievalUrl,
1154+
rootId,
1155+
inChallengeWindow,
1156+
hoursUntilChallengeWindow,
1157+
isProofOverdue
1158+
}
1159+
}
10311160
}

src/test/epoch.test.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/* globals describe it */
2+
import { assert } from 'chai'
3+
import {
4+
epochToDate,
5+
dateToEpoch,
6+
getGenesisTimestamp,
7+
timeUntilEpoch,
8+
calculateLastProofDate
9+
} from '../utils/epoch.js'
10+
import { GENESIS_TIMESTAMPS, TIME_CONSTANTS } from '../utils/constants.js'
11+
12+
describe('Epoch Utilities', () => {
13+
describe('epochToDate', () => {
14+
it('should convert epoch 0 to genesis timestamp for mainnet', () => {
15+
const date = epochToDate(0, 'mainnet')
16+
assert.equal(date.getTime(), GENESIS_TIMESTAMPS.mainnet * 1000)
17+
})
18+
19+
it('should convert epoch 0 to genesis timestamp for calibration', () => {
20+
const date = epochToDate(0, 'calibration')
21+
assert.equal(date.getTime(), GENESIS_TIMESTAMPS.calibration * 1000)
22+
})
23+
24+
it('should calculate correct date for future epochs', () => {
25+
const epochsPerDay = 24 * 60 * 2 // 2880 epochs per day
26+
const date = epochToDate(epochsPerDay, 'mainnet')
27+
const expectedTime = (GENESIS_TIMESTAMPS.mainnet + epochsPerDay * TIME_CONSTANTS.EPOCH_DURATION) * 1000
28+
assert.equal(date.getTime(), expectedTime)
29+
})
30+
31+
it('should handle large epoch numbers', () => {
32+
const largeEpoch = 1000000
33+
const date = epochToDate(largeEpoch, 'calibration')
34+
const expectedTime = (GENESIS_TIMESTAMPS.calibration + largeEpoch * TIME_CONSTANTS.EPOCH_DURATION) * 1000
35+
assert.equal(date.getTime(), expectedTime)
36+
})
37+
})
38+
39+
describe('dateToEpoch', () => {
40+
it('should convert genesis date to epoch 0 for mainnet', () => {
41+
const genesisDate = new Date(GENESIS_TIMESTAMPS.mainnet * 1000)
42+
const epoch = dateToEpoch(genesisDate, 'mainnet')
43+
assert.equal(epoch, 0)
44+
})
45+
46+
it('should convert genesis date to epoch 0 for calibration', () => {
47+
const genesisDate = new Date(GENESIS_TIMESTAMPS.calibration * 1000)
48+
const epoch = dateToEpoch(genesisDate, 'calibration')
49+
assert.equal(epoch, 0)
50+
})
51+
52+
it('should calculate correct epoch for future dates', () => {
53+
const futureDate = new Date((GENESIS_TIMESTAMPS.mainnet + 3600) * 1000) // 1 hour after genesis
54+
const epoch = dateToEpoch(futureDate, 'mainnet')
55+
assert.equal(epoch, 120) // 3600 seconds / 30 seconds per epoch
56+
})
57+
58+
it('should round down to nearest epoch', () => {
59+
const partialEpochDate = new Date((GENESIS_TIMESTAMPS.calibration + 45) * 1000) // 1.5 epochs
60+
const epoch = dateToEpoch(partialEpochDate, 'calibration')
61+
assert.equal(epoch, 1) // Should round down
62+
})
63+
})
64+
65+
describe('getGenesisTimestamp', () => {
66+
it('should return correct timestamp for mainnet', () => {
67+
const timestamp = getGenesisTimestamp('mainnet')
68+
assert.equal(timestamp, GENESIS_TIMESTAMPS.mainnet)
69+
})
70+
71+
it('should return correct timestamp for calibration', () => {
72+
const timestamp = getGenesisTimestamp('calibration')
73+
assert.equal(timestamp, GENESIS_TIMESTAMPS.calibration)
74+
})
75+
})
76+
77+
describe('timeUntilEpoch', () => {
78+
it('should calculate correct time difference', () => {
79+
const currentEpoch = 1000
80+
const futureEpoch = 1120 // 120 epochs in the future = 1 hour
81+
const result = timeUntilEpoch(futureEpoch, currentEpoch)
82+
83+
assert.equal(result.epochs, 120)
84+
assert.equal(result.seconds, 3600)
85+
assert.equal(result.minutes, 60)
86+
assert.equal(result.hours, 1)
87+
assert.equal(result.days, 1 / 24)
88+
})
89+
90+
it('should handle same epoch', () => {
91+
const result = timeUntilEpoch(1000, 1000)
92+
93+
assert.equal(result.epochs, 0)
94+
assert.equal(result.seconds, 0)
95+
assert.equal(result.minutes, 0)
96+
assert.equal(result.hours, 0)
97+
assert.equal(result.days, 0)
98+
})
99+
100+
it('should handle negative differences (past epochs)', () => {
101+
const result = timeUntilEpoch(1000, 1120)
102+
103+
assert.equal(result.epochs, -120)
104+
assert.equal(result.seconds, -3600)
105+
assert.equal(result.minutes, -60)
106+
assert.equal(result.hours, -1)
107+
assert.equal(result.days, -1 / 24)
108+
})
109+
})
110+
111+
describe('calculateLastProofDate', () => {
112+
it('should return null when nextChallengeEpoch is 0', () => {
113+
const result = calculateLastProofDate(0, 2880, 'mainnet')
114+
assert.isNull(result)
115+
})
116+
117+
it('should return null when in first proving period', () => {
118+
const result = calculateLastProofDate(100, 2880, 'mainnet')
119+
assert.isNull(result)
120+
})
121+
122+
it('should calculate correct last proof date', () => {
123+
const nextChallengeEpoch = 5760 // 2 days worth of epochs
124+
const maxProvingPeriod = 2880 // 1 day
125+
const result = calculateLastProofDate(nextChallengeEpoch, maxProvingPeriod, 'mainnet')
126+
127+
assert.isNotNull(result)
128+
// Last proof should be at epoch 2880 (5760 - 2880)
129+
const expectedDate = epochToDate(2880, 'mainnet')
130+
assert.equal(result?.getTime(), expectedDate.getTime())
131+
})
132+
133+
it('should handle edge case at proving period boundary', () => {
134+
const nextChallengeEpoch = 2880
135+
const maxProvingPeriod = 2880
136+
const result = calculateLastProofDate(nextChallengeEpoch, maxProvingPeriod, 'mainnet')
137+
138+
// Should return null since lastProofEpoch would be 0
139+
assert.isNull(result)
140+
})
141+
})
142+
})

0 commit comments

Comments
 (0)