diff --git a/CHANGELOG.md b/CHANGELOG.md index 85da5da4..e33f252e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] ### Added +- Dataset lifecycle tracking with `DataSetStatusChanged` event ([#169](https://github.com/FilOzone/filecoin-services/issues/169)) +- Convenience functions `isDataSetActive()` and `getDataSetStatusDetails()` for status checking +- Comprehensive documentation: dataset lifecycle guide and integration guide +- Subgraph status history tracking with `DataSetStatusHistory` entity ### Changed +- **BREAKING**: Simplified `DataSetStatus` enum from 3 states to 2 states ([#169](https://github.com/FilOzone/filecoin-services/issues/169)) + - **Old values**: `NotFound (0)`, `Active (1)`, `Terminating (2)` + - **New values**: `Inactive (0)`, `Active (1)` + - **Migration**: + - `NotFound` → `Inactive` (non-existent datasets) + - `Terminating` → `Active` (terminated datasets with pieces are still Active) + - Use `pdpEndEpoch` to check if a dataset is terminated + - **Details**: `Inactive` represents non-existent datasets or datasets with no pieces yet. `Active` represents all datasets with pieces, including terminated ones. + - Use `getDataSetStatusDetails()` to check termination status separately from Active/Inactive status +- Subgraph schema updated with status enum and history tracking ## [0.3.0] - 2025-10-08 - M3.1 Calibration Network Deployment diff --git a/service_contracts/README.md b/service_contracts/README.md index 003afb20..0e4e6813 100644 --- a/service_contracts/README.md +++ b/service_contracts/README.md @@ -29,6 +29,46 @@ This directory contains the smart contracts for different Filecoin services usin - `pdp` - PDP verifier contract (from main branch) +## Dataset Status & Lifecycle + +Datasets have a simplified two-state lifecycle system to track their operational status: + +### Status States + +- **Inactive**: Dataset doesn't exist or has no pieces added yet (rate = 0, no proving) +- **Active**: Dataset has pieces and proving history (including terminated datasets) + +**Important**: Terminated datasets remain **Active** because they still have data. Status reflects data existence, not operational state. Use `pdpEndEpoch` to check if a dataset is terminated. + +### Querying Status + +**From Solidity:** +```solidity +import {FilecoinWarmStorageServiceStateLibrary} from "./lib/FilecoinWarmStorageServiceStateLibrary.sol"; + +// Get status +DataSetStatus status = FilecoinWarmStorageServiceStateLibrary.getDataSetStatus(service, dataSetId); +bool isActive = (status == FilecoinWarmStorageService.DataSetStatus.Active); +``` + +**From Subgraph:** +```graphql +{ + dataSet(id: "0x...") { + setId + status # "ACTIVE" or "INACTIVE" + totalPieces + pdpEndEpoch + } +} +``` + +**Via View Contract:** +```solidity +DataSetStatus status = viewContract.getDataSetStatus(dataSetId); +// 0 = Inactive, 1 = Active +``` + ### Extsload The allow for many view methods within the 24 KiB contract size constraint, viewing is done with `extsload` and `extsloadStruct`. There are three recommended ways to access `view` methods. diff --git a/service_contracts/abi/Errors.abi.json b/service_contracts/abi/Errors.abi.json index 04764905..b65ad4a8 100644 --- a/service_contracts/abi/Errors.abi.json +++ b/service_contracts/abi/Errors.abi.json @@ -239,6 +239,115 @@ } ] }, + { + "type": "error", + "name": "InsufficientFundsForMinimumRate", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "minimumRequired", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "available", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InsufficientLockupAllowance", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "lockupAllowance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "lockupUsage", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minimumLockupRequired", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InsufficientMaxLockupPeriod", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "maxLockupPeriod", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "requiredLockupPeriod", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InsufficientRateAllowance", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "rateAllowance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rateUsage", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minimumRateRequired", + "type": "uint256", + "internalType": "uint256" + } + ] + }, { "type": "error", "name": "InvalidChallengeCount", @@ -574,6 +683,22 @@ } ] }, + { + "type": "error", + "name": "OperatorNotApproved", + "inputs": [ + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + } + ] + }, { "type": "error", "name": "PaymentRailsNotFinalized", diff --git a/service_contracts/src/FilecoinWarmStorageService.sol b/service_contracts/src/FilecoinWarmStorageService.sol index 08becbb9..84d62697 100644 --- a/service_contracts/src/FilecoinWarmStorageService.sol +++ b/service_contracts/src/FilecoinWarmStorageService.sol @@ -130,12 +130,11 @@ contract FilecoinWarmStorageService is } enum DataSetStatus { - // Data set doesn't yet exist or has been deleted - NotFound, - // Data set is active - Active, - // Data set is in the process of being terminated - Terminating + // Dataset is inactive: non-existent (pdpRailId==0) or no pieces added yet (rate==0, no proving) + Inactive, + // Dataset has pieces and proving history (includes datasets in process of being terminated) + // Note: Datasets being terminated remain Active - they become Inactive after deletion when data is wiped + Active } // Decode structure for data set creation extra data diff --git a/service_contracts/src/lib/FilecoinWarmStorageServiceStateInternalLibrary.sol b/service_contracts/src/lib/FilecoinWarmStorageServiceStateInternalLibrary.sol index e9185262..117708eb 100644 --- a/service_contracts/src/lib/FilecoinWarmStorageServiceStateInternalLibrary.sol +++ b/service_contracts/src/lib/FilecoinWarmStorageServiceStateInternalLibrary.sol @@ -119,18 +119,33 @@ library FilecoinWarmStorageServiceStateInternalLibrary { info.dataSetId = dataSetId; } + /** + * @notice Get the current status of a dataset + * @dev A dataset is Active when it has pieces and proving history (including terminated datasets) + * @dev A dataset is Inactive when: non-existent or no pieces added yet + * @param service The service contract + * @param dataSetId The ID of the dataset + * @return status The current status + */ function getDataSetStatus(FilecoinWarmStorageService service, uint256 dataSetId) internal view returns (FilecoinWarmStorageService.DataSetStatus status) { FilecoinWarmStorageService.DataSetInfoView memory info = getDataSet(service, dataSetId); + + // Non-existent datasets are inactive if (info.pdpRailId == 0) { - return FilecoinWarmStorageService.DataSetStatus.NotFound; + return FilecoinWarmStorageService.DataSetStatus.Inactive; } - if (info.pdpEndEpoch != 0) { - return FilecoinWarmStorageService.DataSetStatus.Terminating; + + // Check if proving is activated (has pieces) + // Inactive only if no proving has started, everything else is Active + uint256 activationEpoch = provingActivationEpoch(service, dataSetId); + if (activationEpoch == 0) { + return FilecoinWarmStorageService.DataSetStatus.Inactive; } + return FilecoinWarmStorageService.DataSetStatus.Active; } diff --git a/service_contracts/src/lib/FilecoinWarmStorageServiceStateLibrary.sol b/service_contracts/src/lib/FilecoinWarmStorageServiceStateLibrary.sol index e8d70b79..9e5e0344 100644 --- a/service_contracts/src/lib/FilecoinWarmStorageServiceStateLibrary.sol +++ b/service_contracts/src/lib/FilecoinWarmStorageServiceStateLibrary.sol @@ -115,18 +115,33 @@ library FilecoinWarmStorageServiceStateLibrary { info.dataSetId = dataSetId; } + /** + * @notice Get the current status of a dataset + * @dev A dataset is Active when it has pieces and proving history (including terminated datasets) + * @dev A dataset is Inactive when: non-existent or no pieces added yet + * @param service The service contract + * @param dataSetId The ID of the dataset + * @return status The current status + */ function getDataSetStatus(FilecoinWarmStorageService service, uint256 dataSetId) public view returns (FilecoinWarmStorageService.DataSetStatus status) { FilecoinWarmStorageService.DataSetInfoView memory info = getDataSet(service, dataSetId); + + // Non-existent datasets are inactive if (info.pdpRailId == 0) { - return FilecoinWarmStorageService.DataSetStatus.NotFound; + return FilecoinWarmStorageService.DataSetStatus.Inactive; } - if (info.pdpEndEpoch != 0) { - return FilecoinWarmStorageService.DataSetStatus.Terminating; + + // Check if proving is activated (has pieces) + // Inactive only if no proving has started, everything else is Active + uint256 activationEpoch = provingActivationEpoch(service, dataSetId); + if (activationEpoch == 0) { + return FilecoinWarmStorageService.DataSetStatus.Inactive; } + return FilecoinWarmStorageService.DataSetStatus.Active; } diff --git a/service_contracts/test/FilecoinWarmStorageService.t.sol b/service_contracts/test/FilecoinWarmStorageService.t.sol index 220475da..a7d71f50 100644 --- a/service_contracts/test/FilecoinWarmStorageService.t.sol +++ b/service_contracts/test/FilecoinWarmStorageService.t.sol @@ -1782,7 +1782,7 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { // 0. Verify that DataSet with ID 1 is not found FilecoinWarmStorageService.DataSetStatus status = viewContract.getDataSetStatus(1); - assertEq(uint256(status), uint256(FilecoinWarmStorageService.DataSetStatus.NotFound), "expected NotFound"); + assertEq(uint256(status), uint256(FilecoinWarmStorageService.DataSetStatus.Inactive), "expected Inactive"); // 1. Setup: Create a dataset with CDN enabled. console.log("1. Setting up: Creating dataset with service provider"); @@ -1828,7 +1828,11 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { console.log("Created data set with ID:", dataSetId); status = viewContract.getDataSetStatus(dataSetId); - assertEq(uint256(status), uint256(FilecoinWarmStorageService.DataSetStatus.Active), "expected Active"); + assertEq( + uint256(status), + uint256(FilecoinWarmStorageService.DataSetStatus.Inactive), + "expected Inactive (no pieces yet)" + ); // 2. Submit a valid proof. console.log("\n2. Starting proving period and submitting proof"); @@ -1880,9 +1884,11 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { assertFalse(exists, "withCDN metadata should not exist after termination"); assertEq(withCDN, "", "withCDN value should be cleared for dataset"); - // check status is terminating + // check status remains active (terminated datasets are still Active) status = viewContract.getDataSetStatus(dataSetId); - assertEq(uint256(status), uint256(FilecoinWarmStorageService.DataSetStatus.Terminating), "expected Terminating"); + assertEq( + uint256(status), uint256(FilecoinWarmStorageService.DataSetStatus.Active), "expected Active (terminating)" + ); // Ensure piecesAdded reverts console.log("\n4. Testing operations after termination"); @@ -1909,9 +1915,11 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { console.log("Rolling to block:", info.pdpEndEpoch + 1); vm.roll(info.pdpEndEpoch + 1); - // check status is still Terminating as data set is not yet deleted from PDP + // check status is still Active as data set is not yet deleted from PDP status = viewContract.getDataSetStatus(dataSetId); - assertEq(uint256(status), uint256(FilecoinWarmStorageService.DataSetStatus.Terminating), "expected Terminating"); + assertEq( + uint256(status), uint256(FilecoinWarmStorageService.DataSetStatus.Active), "expected Active (terminating)" + ); // Ensure other functions also revert now console.log("\n6. Testing operations after payment end epoch"); @@ -1956,7 +1964,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { pdpServiceWithPayments.dataSetDeleted(dataSetId, 10, bytes("")); status = viewContract.getDataSetStatus(dataSetId); - assertEq(uint256(status), uint256(FilecoinWarmStorageService.DataSetStatus.NotFound), "expected NotFound"); + assertEq( + uint256(status), uint256(FilecoinWarmStorageService.DataSetStatus.Inactive), "expected Inactive (deleted)" + ); console.log("\n=== Test completed successfully! ==="); } diff --git a/subgraph/schemas/schema.v1.graphql b/subgraph/schemas/schema.v1.graphql index a8a35aac..a9da5e85 100644 --- a/subgraph/schemas/schema.v1.graphql +++ b/subgraph/schemas/schema.v1.graphql @@ -1,3 +1,8 @@ +enum DataSetStatus { + INACTIVE + ACTIVE +} + type DataSet @entity(immutable: false) { id: Bytes! # setId setId: BigInt! # uint256 @@ -10,7 +15,8 @@ type DataSet @entity(immutable: false) { pdpEndEpoch: BigInt! leafCount: BigInt! # uint256 challengeRange: BigInt! # uint256 - isActive: Boolean! + isActive: Boolean! # Deprecated: use status field instead + status: DataSetStatus! # Current lifecycle status lastProvenEpoch: BigInt! # uint256 nextChallengeEpoch: BigInt! # uint256 totalPieces: BigInt! # uint256 diff --git a/subgraph/src/filecoin-warm-storage-service.ts b/subgraph/src/filecoin-warm-storage-service.ts index a6bbef54..4347c476 100644 --- a/subgraph/src/filecoin-warm-storage-service.ts +++ b/subgraph/src/filecoin-warm-storage-service.ts @@ -284,7 +284,8 @@ export function handleDataSetCreated(event: DataSetCreatedEvent): void { dataSet.payee = payee; dataSet.serviceProvider = serviceProvider; dataSet.withCDN = withCDN; - dataSet.isActive = true; + dataSet.isActive = true; // Deprecated: kept for backward compatibility + dataSet.status = "INACTIVE"; // Initially inactive (no pieces yet) dataSet.pdpEndEpoch = BIGINT_ZERO; dataSet.leafCount = BIGINT_ZERO; dataSet.challengeRange = BIGINT_ZERO; @@ -496,6 +497,7 @@ export function handlePDPPaymentTerminated(event: PDPPaymentTerminatedEvent): vo } if (dataSet) { dataSet.isActive = false; + dataSet.status = "INACTIVE"; dataSet.pdpEndEpoch = endEpoch; dataSet.save(); } @@ -531,6 +533,8 @@ export function handleCDNPaymentTerminated(event: CDNPaymentTerminatedEvent): vo } if (dataSet) { dataSet.isActive = false; + dataSet.status = "INACTIVE"; dataSet.save(); } } +