Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
08f17e9
fix(scripts): deploy warm storage only (#348)
wjmelements Nov 13, 2025
32e7dc5
refactor: replaced filfox with blockscout as default block explorer (…
akronim26 Nov 15, 2025
55e6e67
feat: add announcePlannedUpgrade flow for ServiceProviderRegistry (#356)
Chaitu-Tatipamula Nov 26, 2025
7146943
feat: automatic deployment address management via deployments.json (#…
Chaitu-Tatipamula Dec 16, 2025
467be3e
feat: add spec.md file with pricing model (#366)
rjan90 Dec 17, 2025
f0ac0de
fix!: remove redundant PDP config getters from view contract (#372)
rvagg Dec 20, 2025
8dbf1b2
feat: modify rail rate when piecesAdded is called (#365)
DarkLord017 Dec 20, 2025
df899b9
fix: cleanup piece metadata on scheduled removals (#343)
wjmelements Jan 5, 2026
7f0b401
chore(docs): additional clarity and justification regarding rate chan…
rvagg Jan 6, 2026
a6f51e8
chore(docs): clarify rate change semantics after termination
rvagg Jan 6, 2026
25f987e
fix(fwss)!: require rail settlement before dataset deletion (#377)
rvagg Jan 8, 2026
72f7ac8
docs(fwss): document precision loss in minimum rate calculation (#378)
rvagg Jan 8, 2026
7b87a78
refactor(fwss): defer rate recalculation to piece operations (#381)
rvagg Jan 10, 2026
8e44651
chore!: consistent contract naming, add SignatureVerificationLib (#388)
rvagg Jan 13, 2026
2edbe0e
feat: ProviderIdSet (#386)
wjmelements Jan 13, 2026
3f13281
chore(abi): add ProviderIdSet to `update-abi` (#389)
wjmelements Jan 13, 2026
35d458f
Merge remote-tracking branch 'upstream/main' into ash/chore/sync-upst…
alanshaw Jan 14, 2026
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# VIM
*.swp
*.idea/
*keystore/
*keystore/

# git worktrees
.trees/
96 changes: 48 additions & 48 deletions CHANGELOG.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ This repository contains smart contracts and services for the Filecoin ecosystem
- **Payment Integration**: Built on top of the [Filecoin Services Payments](https://github.com/FilOzone/filecoin-services-payments) framework
- **Data Verification**: Uses [PDP verifiers](https://github.com/FilOzone/pdp) for cryptographic proof of data possession

## Pricing

The service uses static global pricing set by the contract owner (default: 2.5 USDFC per TiB/month). Rail payment rates are calculated based on data size with a minimum floor. See [SPEC.md](SPEC.md) for details on rate calculation, pricing updates, and top-up/renewal behavior.

## 🚀 Quick Start

### Prerequisites
Expand Down
141 changes: 141 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Filecoin Services Specification

## Pricing

### Pricing Model

FilecoinWarmStorageService uses **static global pricing**. All payment rails use the same price regardless of which provider stores the data. The default storage price is 2.5 USDFC per TiB/month.

Providers may advertise their own prices in the ServiceProviderRegistry, but these are informational for other services, and does not affect actual payments in FilecoinWarmStorageService.

### Rate Calculation

The payment rate per epoch is calculated from the total data size in bytes:

```
# Constants
EPOCHS_PER_MONTH = 86400 # 2880 epochs/day × 30 days
TiB = 1099511627776 # bytes

# Default pricing (owner-adjustable)
pricePerTiBPerMonth = 2.5 USDFC
minimumStorageRatePerMonth = 0.06 USDFC

# Per-epoch rate calculation
sizeBasedRate = totalBytes × pricePerTiBPerMonth ÷ TiB ÷ EPOCHS_PER_MONTH
minimumRate = minimumStorageRatePerMonth ÷ EPOCHS_PER_MONTH
finalRate = max(sizeBasedRate, minimumRate)
```

The default minimum floor ensures datasets below ~24.58 GiB still generate the minimum payment of 0.06 USDFC/month.

**Precision note**: Integer division when computing `minimumRate` causes minor precision loss. The actual monthly payment (`minimumRate × EPOCHS_PER_MONTH`) is slightly less than `minimumStorageRatePerMonth`—under 0.0001% for typical floor prices. This is acceptable; see the lockup section below for how pre-flight checks handle this.

### Pricing Updates

Only the contract owner can update pricing by calling `updatePricing(newStoragePrice, newMinimumRate)`. Maximum allowed values are 10 USDFC for storage price and 0.24 USDFC for minimum rate.

**Effect on existing datasets**: Pricing changes do not immediately update rates for existing datasets. New rates take effect when pieces are next added or removed. This avoids gas-expensive rate recalculations across all active datasets while ensuring new pricing applies to all future storage operations.

### Rate Update Timing

Rate recalculation timing differs for additions and deletions due to proving semantics:

- **Adding pieces**: The rate updates immediately when `piecesAdded()` is called. The client begins paying for new pieces right away, even though those pieces won't be included in proof challenges until the next proving period. This fail-fast behavior protects providers: if the client lacks sufficient funds for the new lockup, the transaction fails before the provider commits resources.

- **Removing pieces**: Deletions are scheduled and take effect at the next proving boundary (`nextProvingPeriod()`). The client continues paying the existing rate until the removal is finalized. This deferral is required because proofs may challenge any portion of the current data set during the proving period—the provider must continue storing and proving all existing data until the period ends.

**Why the asymmetry?**

During each proving period, proofs are generated over a fixed data set. The prover must maintain the complete data set because challenges can target any leaf:

- **Additions expand the proof space** but don't affect existing challenges. New pieces simply won't be challenged until the next period. Payment starts immediately because storage resources are committed.

- **Deletions would shrink the proof space** mid-period, potentially invalidating challenges. The data must remain intact until `nextProvingPeriod()` finalizes the removal. Only then does the rate decrease.

This ensures proof integrity while providing fair payment semantics: you pay when you add, and continue paying for deletions until the proving period boundary.

### Rate Changes After Termination

When a service is terminated (by client or provider), the payment rail enters a lockup period during which funds continue flowing to the provider. Rate change behavior differs from active rails:

- **Additions are blocked**: `piecesAdded()` reverts after termination. No new pieces can be added to a terminated dataset.

- **Deletions are allowed**: Piece removals can still be scheduled during the lockup window via `piecesScheduledRemove()`, and take effect at the next proving boundary.

- **Rate can only decrease or stay the same**: Since additions are blocked, the only size changes come from deletions. FilecoinPay enforces `newRate <= oldRate` on terminated rails—rate increases are rejected with `RateChangeNotAllowedOnTerminatedRail`.

This design ensures the provider receives payment at or above the rate established before termination. The lockup period guarantees payment for the agreed service level, while still allowing the client to reduce their data footprint (and rate) through deletions.

### Funding and Top-Up

Clients pay for storage by depositing USDFC into the Filecoin Pay contract. These funds flow to providers over time based on the storage rate.

**Lockup**: To protect providers from non-payment, FWSS requires clients to maintain a 30-day reserve of funds. This "lockup" guarantees the provider will be paid for at least 30 days even if the client stops adding funds. The lockup is not a pre-payment—funds still flow to the provider gradually—but it cannot be withdrawn while the storage agreement is active.

```
lockupRequired = finalRate × EPOCHS_PER_MONTH
```

At minimum pricing, this equals `minimumStorageRatePerMonth` (0.06 USDFC at default settings). For larger datasets, the lockup equals one month's storage cost.

**Pre-flight check precision**: The pre-flight validation uses a multiply-first formula `(minimumStorageRatePerMonth × EPOCHS_PER_MONTH) ÷ EPOCHS_PER_MONTH` which preserves the exact monthly value. This produces cleaner error messages (the configured floor price rather than a value with precision loss artifacts) and is slightly more conservative than the actual rail lockup. The difference is under 0.0001% and always in the user's favor—they are never required to have less than needed.

**Storage duration** extends as clients deposit additional funds:

```
storageDuration = availableFunds ÷ finalRate
```

Deposits extend the duration without changing the rate (unless adding pieces triggers an immediate rate recalculation, or scheduled deletions take effect at the next proving boundary).

**Delinquency**: When a client's funded epoch falls below the current epoch, the payment rail can no longer be settled—no further payments flow to the provider. The provider may terminate the service to claim payment from the locked funds, guaranteeing up to 30 days of payment from the last funded epoch.

## Settlement and Payment Validation

### Proving Period Deadlines and Settlement

Settlement progress (`settledUpTo`) tracks the epoch up to which payments have been processed. The validator callback `validatePayment()` determines how far settlement can advance and how much payment is due.

**Key principle**: Settlement advancement is decoupled from payment amount.

- **Proven periods**: Both settlement advancement and payment proceed normally.
- **Unproven periods with open deadline**: Settlement is blocked until the period is resolved (proven or deadline passes).
- **Unproven periods with passed deadline**: Settlement advances (the SP can never prove this period), but payment for those epochs is zero.

This design ensures:
1. Clients are not stuck waiting indefinitely for a provider who has abandoned the service
2. Providers are not paid for periods they failed to prove
3. Settlement can complete even if the provider disappears after termination

### Settlement During Lockup

After termination, the payment rail enters a lockup period. Settlement continues normally during this time:

- If the provider proves all periods, they receive full payment
- If the provider fails to prove some periods, those epochs receive zero payment
- If the provider abandons entirely, settlement advances with zero payment once all deadlines pass

The client's locked funds are released proportionally as settlement progresses. Unproven epochs result in funds returning to the client rather than flowing to the provider.

### Dataset Deletion Requirements

Dataset deletion (`dataSetDeleted`) requires the payment rail to be fully settled before the dataset can be removed:

```
require(settledUpTo >= endEpoch, RailNotFullySettled)
```

**Rationale**: The `validatePayment()` callback reads dataset state (proving status, periods proven) to calculate payment amounts. If the dataset is deleted before settlement completes, `validatePayment()` cannot function, forcing clients to use `settleTerminatedRailWithoutValidation()` which pays the full amount regardless of proof status.

**Implications**:

- Providers must wait for settlement to complete before deleting datasets
- Clients can always settle rails (with zero payment for unproven periods) once deadlines pass
- Dataset deletion timing is controlled by proving period deadlines, not just the lockup period

**Timing**: To delete a dataset after termination:
1. Wait for `block.number > pdpEndEpoch` (lockup period elapsed)
2. Wait for all proving period deadlines within the lockup to pass
3. Call `settleRail()` to complete settlement (rail may auto-finalize)
4. Call `deleteDataSet()` to remove the dataset
1 change: 1 addition & 0 deletions service_contracts/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ ABI_CONTRACTS := \
FilecoinWarmStorageServiceStateView \
FilecoinPayV1 \
PDPVerifier \
ProviderIdSet \
ServiceProviderRegistry \
ServiceProviderRegistryStorage \
SessionKeyRegistry \
Expand Down
21 changes: 21 additions & 0 deletions service_contracts/abi/Errors.abi.json
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,27 @@
}
]
},
{
"type": "error",
"name": "RailNotFullySettled",
"inputs": [
{
"name": "railId",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "settledUpTo",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "endEpoch",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "ServiceContractMustTerminateRail",
Expand Down
21 changes: 21 additions & 0 deletions service_contracts/abi/FilecoinWarmStorageService.abi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2291,6 +2291,27 @@
}
]
},
{
"type": "error",
"name": "RailNotFullySettled",
"inputs": [
{
"name": "railId",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "settledUpTo",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "endEpoch",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "ServiceContractMustTerminateRail",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,4 @@
[
{
"type": "function",
"name": "challengeWindow",
"inputs": [
{
"name": "service",
"type": "FilecoinWarmStorageService",
"internalType": "contract FilecoinWarmStorageService"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "clientDataSets",
Expand Down Expand Up @@ -201,19 +182,6 @@
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getChallengesPerProof",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint64",
"internalType": "uint64"
}
],
"stateMutability": "pure"
},
{
"type": "function",
"name": "getClientDataSets",
Expand Down Expand Up @@ -477,25 +445,6 @@
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getMaxProvingPeriod",
"inputs": [
{
"name": "service",
"type": "FilecoinWarmStorageService",
"internalType": "contract FilecoinWarmStorageService"
}
],
"outputs": [
{
"name": "",
"type": "uint64",
"internalType": "uint64"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getPDPConfig",
Expand Down
39 changes: 0 additions & 39 deletions service_contracts/abi/FilecoinWarmStorageServiceStateView.abi.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,6 @@
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "challengeWindow",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "clientDataSets",
Expand Down Expand Up @@ -169,19 +156,6 @@
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getChallengesPerProof",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint64",
"internalType": "uint64"
}
],
"stateMutability": "pure"
},
{
"type": "function",
"name": "getClientDataSets",
Expand Down Expand Up @@ -419,19 +393,6 @@
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getMaxProvingPeriod",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint64",
"internalType": "uint64"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getPDPConfig",
Expand Down
Loading
Loading