Alternative Architecture
TL;DR: Reuse as much of the existing PDP components as possible. Conceptually, Retrievability Checking should be just a boolean flag added to PDP deals (âyes, I also want retrievabilityâ). The retrieval verifier can be very closely tied to the PDPVerifier contract and reuse its state to keep track of what should be checked for retrievability.
I am also arguing that this boolean should be actually set to true by default - whatâs the point of storing data if you cannot retrieve it?
- Discussion with FilOz: https://space-meridian.slack.com/archives/C0838QS1CVC/p1747824258480759
High-level overview
In PDP, there is a concept of âproving periodâ (24h on mainnet, 30min on testnet). Every proving period, SP is required to post N âchallenges per proof setâ (hard-coded to 5 in Curio) every proving period. A challenge is an inclusion proof for a block inside the proofset thatâs chosen in (deterministically) randomly way.
Letâs do the same for Retrievability Checking:
- Every proving period, the checkers should perform N retrieval requests for each proof set.
- Each retrieval request should pick a random root from the proofset.
- The service contract will check whether aggregated RSR meets the minimum required SLA.
- Suggested parameters:
- Retrievals per period (hardcoded for June 6): 100 for mainnet, 2 for testnet.
- SLA requirement: 90% for mainnet (10 out of 100 requests can fail), 50% for testnet (1 out of 2 requests can fail)
Components
- HotStorageService - combines PDP+Retrievability, will be based on PDPSimpleServiceWithPayments, needs to be built
- PDPVerifier - existing smart contract
- RetrievabilityVerifier - new smart contract we need to build
- RetrievabilityVerifier is tied to PDPVerifier. (I.e. RetrievabilityVerifierâs constructor requires the addresss of PDPVerifier contract).
- What does it mean:
- Itâs not possible to schedule retrievability checks for content not stored via PDP.
- In the simplest form, VerifiabilityVerifier will have a hardcoded link to exactly on PDPVerifier contract.
- The first point is an inherent part of this design.
- The second point can be easily lifted, if HotStorageService.createProofSet() can forward to our verifier not only the proofset id, but also the address of the PDPVerifier contract owning this proofset. We can add this after June 6.
Workflows
A new deal is created
IIUC, this means creating a new PDP ProofSet with the attached payment rails.
- The client or SP calls HotStorageService to create a new deal and indicate whether retrievability is required.
- The SP calls
PDPVerifier.createProofSet(potentially via the service contract), which:- calls
HotStorageService.createProofSet
- emits
ProofSetCreatedevent
- calls
HotStorageService.createProofSetwill callRetrievabilityVerifier.createProofSetBUT only if the proofset is expected to be retrievable (the contract includes retrievability SLA).- This needs to be implemented (by FilOz).
RetrievabilityVerifier.createProofSetwill initialise the internal state tracking retrievability of this proofset and emintProofSetCreatedevent.
- The Checker node listens for the event
RetrievabilityVerifier.ProofSetCreatedand updates its off-chain state to prepare for retrievability checking.
sequenceDiagram
SP->>HotStorageService: Create a new deal (retrievable=y/n)
SP->>PDPVerifier: createProofSet() (potentially via service contract):
PDPVerifier->>HotStorageService: createProofSet()
HotStorageService->>RetrievabilityVerifier: createProofSet() (ONLY if retrievable)
RetrievabilityVerifier->>RetrievabilityVerifier: emit ProofSetCreated
PDPVerifier->>PDPVerifier: emit ProofSetCreated
loop on RetrievabilityVerifier.ProofSetCreated
CheckerNode->>CheckerNode: Initialise off-chain state
end
A new proving period starts
For each proofset, once a day on mainnet (every 30 min on testnet), the SP (Curio) sends a TX calling PDPVerifier.nextProvingPeriod(proofSetId,...).
- The smart contract function calls
HotStorageService.nextProvingPeriod(proofSetId,...).
We need to modify the HotStorageService as follows:
HotStorageService.nextProvingPeriod(...args)must check whether the proofset is expected to be retrievable.
- If the proofset should be retrievable, then the service must call
RetrievabilityVerifier.nextProvingPeriod(...args)
The RetrievabilityVerifier contract updates its internal state thatâs recording retrieval results and emits NextProvingPeriod event.
The Checker node listens for this event (RetrievabilityVerifier.NextProvingPeriod) to start a new round of retrieval checks for a proofset. (Note: this event is emitted only for proofsets that are retrievable according to HotStorageService internal state.)
- Add this proofset to the off-chain list of proofsets to check (unless the proofset was already added before).
- Next, the node schedules M retrieval checks for this proofset in the next T minutes.
- M can be initially set to N (number of retrieval requests per proving period). Later, we should use a random value so that SPâs donât know how many retrieval checks to expect.
- T is the same as the length of the proving period.
- When the time comes to run a retrieval check for a proofset:
- The node picks a random root CID in the proofset by reading the on-chain state of the linked PDPVerifier contract.
- The node attempts to retrieve that root CID from the provider
- The result is reported to the
RetrievabilityVerifier.- I think we may need to batch these writes to save gas fee, but thatâs beyond the minimal MVP scope.
sequenceDiagram
loop On proofset period start
SP->>PDPVerifier: nextProvingPeriod(proofSetId, ...)
PDPVerifier->>HotStorageService: nextProvingPeriod(proofSetId, ...)
HotStorageService->>RetrievabilityVerifier: nextProvingPeriod(...) (ONLY if retrievable)
RetrievabilityVerifier->>RetrievabilityVerifier: emit NextProvingPeriod()
end
loop on RetrievabilityVerifier.NextProvingPeriod
CheckerNode->>CheckerNode: Schedule retrieval checks
end
loop On schedule
CheckerNode->>PDPVerifier: Pick random root CID
CheckerNode->>SP: Attempt retrieval
CheckerNode->>RetrievabilityVerifier: Report result
endA deal ends
This is out of the scope for June 6.
- At some point,
PDPVerifier.deleteProofSet()is called to clean up the on-chain state and reduce amount od data stored.
PDPVerifier.deleteProofSet()callsHotStorageService.deleteProofSet()
HotStorageService.deleteProofSet()callsRetrievabilityVerifier.deleteProofSet()but ONLY if the proofset was retrievable
RetrievabilityVerifier.deleteProofSet()cleans up the on-chain state
SLA evaluation
Inside HotStorageService.nextProvingPeriod(...args), the service contract will ask RetrievabilityVerifier for the retrieval results reported for the proofset in the previous proving period and decide whether the SLA was met.
RetrievabilityVerifier interface (draft)
// This interface needs to be implemented by FilOz's service contract
interface RetrievabilityListener {
function setIsRetrievable(uint256 setId, uint256 rootId, bool isRetrievable) external;
}
contract RetrievabilityVerifier is Initializable, Upgradeable {
// Discussion point about pdpVerifier. Is this simplication worth it?
// Would it be better to track pdpVerifier address on a per-proofset basis
// instead? That will make this contract much more generic and future-proof.
function initialize(address _pdpVerifier) {
pdpVerifier = _pdpVerifier;
}
// Emitted when a new proofset with retrievability SLA was created
event ProofSetCreated(uint256 indexed setId);
// Emitted when a proofset was deleted (after a deal ended?)
event ProofSetDeleted(uint256 indexed setId);
// Emitted when a new proving period (a new round of retrieval checks)
// starts.
event NextProvingPeriod(uint256 indexed setId, uint256 challengeEpoch);
// HotStorageService is required to call this method
// but only for proofsets subject to retrievability SLA.
//
// Emits ProofSetCreated event.
function createProofSet(uint256 setId);
// HotStorageService is required to call this method
// but only for proofsets subject to retrievability SLA
//
// Emits ProofSetDeleted event.
function deleteProofSet(uint256 setId);
// HotStorageService is required to call this method
// but only for proofsets subject to retrievability SLA
//
// Emits NextProvingPeriod event.
function nextProvingPeriod(uint256 setId, uint256 challengeEpoch)
// Discussion points: maybe we don't need these wrappers and we should
// let the checker node to interact with the PDPVerifier contract directly
// These wrappers would become more important in case we wanted to
// support more than one PDPVerifier contract on a per-proof-set basis
// How many files are stored in this proof set.
// Caveat: when files (roots) are deleted, their indices are not recycled.
// Checker nodes must check if re-run the sampling if they hit a deleted file
// (deleted = not live, see below)
function getRootCount(uint256 setId) {
return PDPVerifier(pdpVerifier).getNextRootId();
}
// Returns false if the proof set is not live or if the root id is 1) not yet created 2) deleted
function isRootLive(uint256 setId, uint256 rootId) public view returns (bool) {
return PDPVerifier(pdpVerifier).rootLive(setId, rootId)
}
function getRootCid(uint256 setId, uint256 rootId)
public view returns (Cids.Cid memory)
{
require(isRootLive(setId, rootId), "Root is not live");
return PDPVerifier(pdpVerifier).getRootCid(setId, rootId);
}
function setIsRetrievable(
address retrievabilityListener,
uint256 setId,
uint256 rootId,
bool isRetrievable
)
public onlyChecker
{
return RetrievabilityListener(retrievabilityListener)
.setIsRetrievable(setId, rootId, isRetrievable);
}
}Checker algorithm
Checker algorithm for a random retrieval check for proof-set setId using the API above:
count = RetrievabilityVerifier.getRootCount(setId)
ix = random(0, C)
isLive = RetrievabilityVerifier.isRootLive(setId, ix)
if not isLiveâgoto 2
cid = RetrievabilityVerifier.getRootCid(setId, ix)
- check retrievability of
cid
Integration with HotStorageService
We expect the FilOz team to build this integration.
The proposal is based on SimplePDPServiceWithPayments as proposed in https://github.com/FilOzone/pdp/pull/146/
contract SimplePDPServiceWithPayments is
PDPListener,
IArbiter,
Initializable,
UUPSUpgradeable,
OwnableUpgradeable
{
// ...
function initialize(
address _pdpVerifierAddress,
address _retrievabilityVerifierAddress, // NEW
address _paymentsContractAddress,
address _usdFcTokenAddress,
uint256 _initialOperatorCommissionBps
) public initializer {
// ...(existing code)...
// TODO: make this required
retrievabilityVerifierAddress = _retrievabilityVerifierAddress;
}
function proofSetCreated(
uint256 proofSetId,
address creator,
bytes calldata extraData
) external {
// ...(existing code)...
RetrievabilityVerifier(retrievabilityVerifierAddress).proofSetCreated(proofSetId)
}
// TODO: Payment rail termination; not needed in MVP
// TODO: call RetrievabilityVerifier.proofSetDeleted
function proofSetDeleted(
uint256 proofSetId,
uint256 deletedLeafCount,
bytes calldata
) external {}
function nextProvingPeriod(
uint256 proofSetId,
uint256 challengeEpoch,
uint256 leafCount,
bytes calldata
) external {
// ...(existing code)...
RetrievabilityVerifier(retrievabilityVerifierAddress).nextProvingPeriod(proofSetId, challengeEpoch);
}
}