Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,12 @@ Datasets with CDN support have three payment rails: a **PDP rail** for storage p

Both CDN rails have `paymentRate = 0` and use fixed lockup for one-time payments based on usage. At dataset creation the cache-miss rail is seeded with **0.3 USDFC** and the CDN rail with **0.7 USDFC**. Both CDN rails use a **5-day lockup period**, which sets the settle window FilBeam has after dataset deletion to claim any remaining fixed lockup.

### Shared bandwidth rail (CDN subscriptions)

The bandwidth rail can be shared across multiple data sets of the same payer so CDN is bought once even when a piece is stored in several data sets (for example multi-copy upload across providers). The `withCDN` metadata value carries an optional group id, and FWSS keys a shared bandwidth rail by `keccak256(abi.encode(payer, groupId))`. When a data set is created with a group id whose shared bandwidth rail already exists and is active, the data set joins it, no second bandwidth rail or 0.7 USDFC lockup is created. The shared `cdnRailId` is the subscription identity: every member data set resolves to the same rail, so the FilBeam controller meters and settles bandwidth once per rail via `settleCDNBandwidthRail(cdnRailId, cdnAmount)`.

The cache-miss rail stays per data set, its payee is the data set's SP, which differs per copy. `cdnRailRefCount` counts the data sets referencing each shared bandwidth rail, and the rail is terminated only when the last member is torn down (via `terminateCDNService`, `dataSetDeleted`, or abandonment). An empty group id keeps the legacy one-rail-per-data-set behavior.

### Payment Models

PDP and CDN rails use fundamentally different payment models:
Expand Down
11 changes: 11 additions & 0 deletions service_contracts/abi/Errors.abi.json
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,17 @@
}
]
},
{
"type": "error",
"name": "UnknownCDNBandwidthRail",
"inputs": [
{
"name": "cdnRailId",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "UnsupportedSignatureV",
Expand Down
29 changes: 29 additions & 0 deletions service_contracts/abi/FilecoinWarmStorageService.abi.json
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,24 @@
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "settleCDNBandwidthRail",
"inputs": [
{
"name": "cdnRailId",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "cdnAmount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "settleFilBeamPaymentRails",
Expand Down Expand Up @@ -2081,6 +2099,17 @@
}
]
},
{
"type": "error",
"name": "UnknownCDNBandwidthRail",
"inputs": [
{
"name": "cdnRailId",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "ZeroAddress",
Expand Down
4 changes: 4 additions & 0 deletions service_contracts/src/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@ library Errors {
/// @param dataSetId The data set ID
error FilBeamServiceNotConfigured(uint256 dataSetId);

/// @notice The rail id is not a CDN bandwidth rail managed by this contract
/// @param cdnRailId The CDN bandwidth rail ID
error UnknownCDNBandwidthRail(uint256 cdnRailId);

/// @notice Only the FilBeam controller address can call this function
/// @param expected The expected FilBeam controller address
/// @param actual The caller address
Expand Down
91 changes: 85 additions & 6 deletions service_contracts/src/FilecoinWarmStorageService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,14 @@ contract FilecoinWarmStorageService is
// Piece IDs awaiting metadata cleanup; cleared each nextProvingPeriod call
mapping(uint256 dataSetId => uint256[] pieceIds) internal scheduledPieceMetadataRemovals;

// Shared CDN bandwidth rail per (payer, CDN group). The shared rail id is the CDN subscription
// identity: every data set in the group resolves to the same cdnRailId. Keyed by
// keccak256(abi.encode(payer, groupId)), where groupId is the value of the `withCDN` metadata
// entry. An empty group id leaves this unused and the data set keeps a bandwidth rail of its own.
mapping(bytes32 cdnGroupKey => uint256 cdnRailId) internal cdnGroupRail;
// Number of data sets referencing each shared CDN bandwidth rail; the rail is torn down at zero.
mapping(uint256 cdnRailId => uint256 refCount) internal cdnRailRefCount;

event UpgradeAnnounced(PlannedUpgrade plannedUpgrade);

// =========================================================================
Expand Down Expand Up @@ -599,11 +607,21 @@ contract FilecoinWarmStorageService is
// Create the payment rails using the FilecoinPayV1 contract
FilecoinPayV1 payments = FilecoinPayV1(paymentsContractAddress);

// Determine once whether CDN is enabled in metadata and reuse the result
bool hasCDN = hasCDNMetadataKey(createData.metadataKeys);
// Determine once whether CDN is enabled and, if so, which shared subscription it joins.
// The group key is derived from the payer and the `withCDN` metadata value, an empty value
// means the data set is its own subscription (legacy one-rail-per-data-set behavior).
(bool hasCDN, bytes32 cdnGroupKey) =
cdnMetadata(createData.payer, createData.metadataKeys, createData.metadataValues);

(uint256 pdpRailId, uint256 cacheMissRailId, uint256 cdnRailId) = payments.createRails(
dataSetId, usdfcTokenAddress, createData.payer, payee, hasCDN ? filBeamBeneficiaryAddress : address(0)
dataSetId,
usdfcTokenAddress,
createData.payer,
payee,
hasCDN ? filBeamBeneficiaryAddress : address(0),
cdnGroupKey,
cdnGroupRail,
cdnRailRefCount
);

railToDataSet[pdpRailId] = dataSetId;
Expand Down Expand Up @@ -654,9 +672,14 @@ contract FilecoinWarmStorageService is
// Abandonment path: rail was never terminated via terminateService.
// SP forfeits pending op-fees; lifecycle reserve returns to the payer.
_verifyInactivity(dataSetId);
// abandonRails also terminates CDN rails and clears the proving activation epoch
// abandonRails also terminates CDN rails and clears the proving activation epoch.
// The bandwidth rail is only torn down when this is its last referencing data set.
payments.abandonRails(
provingActivationEpoch, dataSetId, info.pdpRailId, info.cacheMissRailId, info.cdnRailId
provingActivationEpoch,
dataSetId,
info.pdpRailId,
info.cacheMissRailId,
_bandwidthRailToTeardown(info.cdnRailId)
);
} else {
// Normal path: terminateService was already called.
Expand Down Expand Up @@ -1094,6 +1117,22 @@ contract FilecoinWarmStorageService is
);
}

/**
* @notice Settles a shared CDN bandwidth rail once for its whole subscription.
* @dev Only callable by the FilBeam controller. The shared bandwidth rail id is the CDN
* subscription identity, so a single call covers every data set in the group. Cache-miss is
* still settled per data set via `settleFilBeamPaymentRails` (with `cdnAmount == 0` for
* grouped data sets, so the bandwidth portion is only ever settled through this path).
* @param cdnRailId The shared CDN bandwidth rail id
* @param cdnAmount Amount to settle for the bandwidth rail
*/
function settleCDNBandwidthRail(uint256 cdnRailId, uint256 cdnAmount) external onlyFilBeamController {
require(cdnRailRefCount[cdnRailId] != 0, Errors.UnknownCDNBandwidthRail(cdnRailId));
if (cdnAmount > 0) {
FilecoinPayV1(paymentsContractAddress).modifyRailPayment(cdnRailId, 0, cdnAmount);
}
}

/**
* @notice Allows users to add funds to their CDN-related payment rails
* @param dataSetId The ID of the data set
Expand Down Expand Up @@ -1162,7 +1201,23 @@ contract FilecoinWarmStorageService is
/// Ideally we would catch only specific error types, but contract size constraint prevents
/// us from implementing error handling.
function _terminateCDNRails(uint256 dataSetId, DataSetInfo storage info, FilecoinPayV1 payments) internal {
payments.terminateCDNRails(dataSetId, info.cacheMissRailId, info.cdnRailId);
payments.terminateCDNRails(dataSetId, info.cacheMissRailId, _bandwidthRailToTeardown(info.cdnRailId));
}

/// @notice Decrements the reference count for a shared CDN bandwidth rail.
/// @dev Returns the rail id to tear down (only when this was the last reference), or 0 when the
/// rail is still shared by sibling data sets and must stay alive.
function _bandwidthRailToTeardown(uint256 cdnRailId) internal returns (uint256) {
if (cdnRailId == 0) {
return 0;
}
uint256 refs = cdnRailRefCount[cdnRailId];
if (refs <= 1) {
cdnRailRefCount[cdnRailId] = 0;
return cdnRailId;
}
cdnRailRefCount[cdnRailId] = refs - 1;
return 0;
}

function updatePaymentRates(
Expand Down Expand Up @@ -1294,6 +1349,30 @@ contract FilecoinWarmStorageService is
return false;
}

/// @notice Reads the `withCDN` metadata entry, returning whether CDN is enabled and the CDN
/// subscription key the data set joins.
/// @dev The subscription key is keccak256(abi.encode(payer, groupId)) where groupId is the value
/// of the `withCDN` entry. An empty value yields a zero key, meaning the data set is its own
/// subscription. Keying by payer guarantees the shared rail's `from` matches every member,
/// so different payers can never share a rail. `keys` and `values` are equal length here,
/// validated by the caller before this is reached.
function cdnMetadata(address payer, string[] memory keys, string[] memory values)
internal
pure
returns (bool hasCDN, bytes32 cdnGroupKey)
{
for (uint256 i = 0; i < keys.length; i++) {
bytes memory keyBytes = bytes(keys[i]);
if (keyBytes.length == METADATA_KEY_WITH_CDN_SIZE && keccak256(keyBytes) == METADATA_KEY_WITH_CDN_HASH) {
hasCDN = true;
if (bytes(values[i]).length != 0) {
cdnGroupKey = keccak256(abi.encode(payer, values[i]));
}
break;
}
}
}

/**
* @notice Returns true if key `withCDN` exists in the metadata keys of the data set.
* @param dataSetId The sequential data set identifier
Expand Down
42 changes: 42 additions & 0 deletions service_contracts/src/lib/FilecoinWarmStorageServiceLayout.json
Original file line number Diff line number Diff line change
Expand Up @@ -620,5 +620,47 @@
}
}
}
},
{
"label": "cdnGroupRail",
"slot": "23",
"offset": 0,
"type": "mapping(bytes32 => uint256)",
"typeDetails": {
"label": "mapping(bytes32 => uint256)",
"encoding": "mapping",
"numberOfBytes": "32",
"key": {
"label": "bytes32",
"encoding": "inplace",
"numberOfBytes": "32"
},
"value": {
"label": "uint256",
"encoding": "inplace",
"numberOfBytes": "32"
}
}
},
{
"label": "cdnRailRefCount",
"slot": "24",
"offset": 0,
"type": "mapping(uint256 => uint256)",
"typeDetails": {
"label": "mapping(uint256 => uint256)",
"encoding": "mapping",
"numberOfBytes": "32",
"key": {
"label": "uint256",
"encoding": "inplace",
"numberOfBytes": "32"
},
"value": {
"label": "uint256",
"encoding": "inplace",
"numberOfBytes": "32"
}
}
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ bytes32 constant NEXT_UPGRADE_SLOT = bytes32(uint256(19));
bytes32 constant DEPRECATED_STORAGE_PRICE_PER_TIB_PER_MONTH_SLOT = bytes32(uint256(20));
bytes32 constant DEPRECATED_MINIMUM_STORAGE_RATE_PER_MONTH_SLOT = bytes32(uint256(21));
bytes32 constant SCHEDULED_PIECE_METADATA_REMOVALS_SLOT = bytes32(uint256(22));
bytes32 constant CDN_GROUP_RAIL_SLOT = bytes32(uint256(23));
bytes32 constant CDN_RAIL_REF_COUNT_SLOT = bytes32(uint256(24));
Loading
Loading