Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ba25df6
wip bloom schema
wjmelements Oct 20, 2025
8a503aa
only shift by 8
wjmelements Oct 20, 2025
934f14e
parameterize K
wjmelements Oct 20, 2025
393a5a3
Merge remote-tracking branch 'origin/main' into bloom-schema
wjmelements Oct 21, 2025
c08fb7a
test BloomSet16
wjmelements Oct 21, 2025
35dd6c3
Errors.InsufficientCapabilitiesForProduct, and restore prior PDPOffer…
wjmelements Oct 21, 2025
62fd4b2
Merge remote-tracking branch 'origin/main' into bloom-schema
wjmelements Oct 22, 2025
2fb481e
must tag as dev
wjmelements Oct 22, 2025
1fdfd06
bytes[] capability values
wjmelements Oct 23, 2025
11dc0ba
tests build but do not yet pass
wjmelements Oct 23, 2025
e317e43
tests pass
wjmelements Oct 23, 2025
fc80b4b
mv PDPOffering.sol to test
wjmelements Oct 23, 2025
3d6682d
make update-abi
wjmelements Oct 23, 2025
17bd9ec
update subgraph: ProductUpdated and create product
wjmelements Oct 23, 2025
8061aa6
subgraph: bytes capabilityValues
wjmelements Oct 23, 2025
621f789
remove misleading exists bools
wjmelements Oct 23, 2025
f565a89
make ipni fields optional and document ipniPeerId
wjmelements Oct 23, 2025
891e3b6
getAllProductCapabilities, and add capability values to getActiveProv…
wjmelements Oct 23, 2025
7a572f1
make update-abi
wjmelements Oct 23, 2025
5a3224c
else if
wjmelements Oct 23, 2025
2709451
doc getAllProductCapabilities
wjmelements Oct 24, 2025
99d9cf9
ban empty capability value
wjmelements Oct 24, 2025
efc9773
forge lint
wjmelements Oct 24, 2025
a4d0062
feat(registry): provide more useful accessors (#328)
rvagg Oct 24, 2025
1628fde
Update service_contracts/src/lib/BloomSet.sol
wjmelements Oct 24, 2025
602b0dc
Merge remote-tracking branch 'origin/main' into bloom-schema
wjmelements Oct 24, 2025
3c28451
BigEndian library
wjmelements Oct 24, 2025
94eed74
use big endian encoding in test
wjmelements Oct 24, 2025
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
142 changes: 39 additions & 103 deletions service_contracts/src/ServiceProviderRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol";
import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";
import {BloomSet16} from "./lib/BloomSet.sol";
import {ServiceProviderRegistryStorage} from "./ServiceProviderRegistryStorage.sol";

uint256 constant REQUIRED_PDP_KEYS = 0x586f5ac60d105d3930cca32e603a0e938965087b7c1478068b04d25461c0a371;

/// @title ServiceProviderRegistry
/// @notice A registry contract for managing service providers across the Filecoin Services ecosystem
contract ServiceProviderRegistry is
Expand Down Expand Up @@ -43,7 +46,7 @@ contract ServiceProviderRegistry is
uint256 public constant MAX_CAPABILITY_VALUE_LENGTH = 128;

/// @notice Maximum number of capability key-value pairs per product
uint256 public constant MAX_CAPABILITIES = 10;
uint256 public constant MAX_CAPABILITIES = 24;

/// @notice Maximum length for location field
uint256 private constant MAX_LOCATION_LENGTH = 128;
Expand All @@ -59,7 +62,6 @@ contract ServiceProviderRegistry is
uint256 indexed providerId,
ProductType indexed productType,
address serviceProvider,
bytes productData,
string[] capabilityKeys,
string[] capabilityValues
);
Expand All @@ -69,7 +71,6 @@ contract ServiceProviderRegistry is
uint256 indexed providerId,
ProductType indexed productType,
address serviceProvider,
bytes productData,
string[] capabilityKeys,
string[] capabilityValues
);
Expand Down Expand Up @@ -125,7 +126,6 @@ contract ServiceProviderRegistry is
/// @param name Provider name (optional, max 128 chars)
/// @param description Provider description (max 256 chars)
/// @param productType The type of product to register
/// @param productData The encoded product configuration data
/// @param capabilityKeys Array of capability keys
/// @param capabilityValues Array of capability values
/// @return providerId The unique ID assigned to the provider
Expand All @@ -134,7 +134,6 @@ contract ServiceProviderRegistry is
string calldata name,
string calldata description,
ProductType productType,
bytes calldata productData,
string[] calldata capabilityKeys,
string[] calldata capabilityValues
) external payable returns (uint256 providerId) {
Expand Down Expand Up @@ -177,74 +176,64 @@ contract ServiceProviderRegistry is
emit ProviderRegistered(providerId, msg.sender, payee);

// Add the initial product using shared logic
_validateAndStoreProduct(providerId, productType, productData, capabilityKeys, capabilityValues);
_validateAndStoreProduct(providerId, productType, capabilityKeys, capabilityValues);

// msg.sender is also providers[providerId].serviceProvider
emit ProductAdded(providerId, productType, msg.sender, productData, capabilityKeys, capabilityValues);
emit ProductAdded(providerId, productType, msg.sender, capabilityKeys, capabilityValues);

// Burn the registration fee
require(FVMPay.burn(REGISTRATION_FEE), "Burn failed");
}

/// @notice Add a new product to an existing provider
/// @param productType The type of product to add
/// @param productData The encoded product configuration data
/// @param capabilityKeys Array of capability keys (max 32 chars each, max 10 keys)
/// @param capabilityValues Array of capability values (max 128 chars each, max 10 values)
function addProduct(
ProductType productType,
bytes calldata productData,
string[] calldata capabilityKeys,
string[] calldata capabilityValues
) external {
function addProduct(ProductType productType, string[] calldata capabilityKeys, string[] calldata capabilityValues)
external
{
// Only support PDP for now
require(productType == ProductType.PDP, "Only PDP product type currently supported");

uint256 providerId = addressToProviderId[msg.sender];
require(providerId != 0, "Provider not registered");

_addProduct(providerId, productType, productData, capabilityKeys, capabilityValues);
_addProduct(providerId, productType, capabilityKeys, capabilityValues);
}

/// @notice Internal function to add a product with validation
function _addProduct(
uint256 providerId,
ProductType productType,
bytes memory productData,
string[] memory capabilityKeys,
string[] memory capabilityValues
) private providerExists(providerId) providerActive(providerId) onlyServiceProvider(providerId) {
// Check product doesn't already exist
require(!providerProducts[providerId][productType].isActive, "Product already exists for this provider");

// Validate and store product
_validateAndStoreProduct(providerId, productType, productData, capabilityKeys, capabilityValues);
_validateAndStoreProduct(providerId, productType, capabilityKeys, capabilityValues);

// msg.sender is providers[providerId].serviceProvider, because onlyServiceProvider
emit ProductAdded(providerId, productType, msg.sender, productData, capabilityKeys, capabilityValues);
emit ProductAdded(providerId, productType, msg.sender, capabilityKeys, capabilityValues);
}

/// @notice Internal function to validate and store a product (used by both register and add)
function _validateAndStoreProduct(
uint256 providerId,
ProductType productType,
bytes memory productData,
string[] memory capabilityKeys,
string[] memory capabilityValues
) private {
// Validate product data
_validateProductData(productType, productData);
_validateProductKeys(productType, capabilityKeys);

// Validate capability k/v pairs
_validateCapabilities(capabilityKeys, capabilityValues);

// Store product
providerProducts[providerId][productType] = ServiceProduct({
productType: productType,
productData: productData,
capabilityKeys: capabilityKeys,
isActive: true
});
providerProducts[providerId][productType] =
ServiceProduct({productType: productType, capabilityKeys: capabilityKeys, isActive: true});

// Store capability values in mapping
mapping(string => string) storage capabilities = productCapabilities[providerId][productType];
Expand All @@ -259,12 +248,10 @@ contract ServiceProviderRegistry is

/// @notice Update an existing product configuration
/// @param productType The type of product to update
/// @param productData The new encoded product configuration data
/// @param capabilityKeys Array of capability keys (max 32 chars each, max 10 keys)
/// @param capabilityValues Array of capability values (max 128 chars each, max 10 values)
function updateProduct(
ProductType productType,
bytes calldata productData,
string[] calldata capabilityKeys,
string[] calldata capabilityValues
) external {
Expand All @@ -274,14 +261,13 @@ contract ServiceProviderRegistry is
uint256 providerId = addressToProviderId[msg.sender];
require(providerId != 0, "Provider not registered");

_updateProduct(providerId, productType, productData, capabilityKeys, capabilityValues);
_updateProduct(providerId, productType, capabilityKeys, capabilityValues);
}

/// @notice Internal function to update a product
function _updateProduct(
uint256 providerId,
ProductType productType,
bytes memory productData,
string[] memory capabilityKeys,
string[] memory capabilityValues
) private providerExists(providerId) providerActive(providerId) onlyServiceProvider(providerId) {
Expand All @@ -292,7 +278,7 @@ contract ServiceProviderRegistry is
require(product.isActive, "Product does not exist for this provider");

// Validate product data
_validateProductData(productType, productData);
_validateProductKeys(productType, capabilityKeys);

// Validate capability k/v pairs
_validateCapabilities(capabilityKeys, capabilityValues);
Expand All @@ -305,7 +291,6 @@ contract ServiceProviderRegistry is

// Update product
product.productType = productType;
product.productData = productData;
product.capabilityKeys = capabilityKeys;
product.isActive = true;

Expand All @@ -315,7 +300,7 @@ contract ServiceProviderRegistry is
}

// msg.sender is also providers[providerId].serviceProvider, because onlyServiceProvider
emit ProductUpdated(providerId, productType, msg.sender, productData, capabilityKeys, capabilityValues);
emit ProductUpdated(providerId, productType, msg.sender, capabilityKeys, capabilityValues);
}

/// @notice Remove a product from a provider
Expand Down Expand Up @@ -359,22 +344,6 @@ contract ServiceProviderRegistry is
emit ProductRemoved(providerId, productType);
}

/// @notice Update PDP service configuration with capabilities
/// @param pdpOffering The new PDP service configuration
/// @param capabilityKeys Array of capability keys (max 32 chars each, max 10 keys)
/// @param capabilityValues Array of capability values (max 128 chars each, max 10 values)
function updatePDPServiceWithCapabilities(
PDPOffering memory pdpOffering,
string[] memory capabilityKeys,
string[] memory capabilityValues
) external {
uint256 providerId = addressToProviderId[msg.sender];
require(providerId != 0, "Provider not registered");

bytes memory encodedData = abi.encode(pdpOffering);
_updateProduct(providerId, ProductType.PDP, encodedData, capabilityKeys, capabilityValues);
}

/// @notice Update provider information
/// @param name New provider name (optional, max 128 chars)
/// @param description New provider description (max 256 chars)
Expand Down Expand Up @@ -421,20 +390,19 @@ contract ServiceProviderRegistry is

// Mark all products as inactive and clear capabilities
// For now just PDP, but this is extensible
if (providerProducts[providerId][ProductType.PDP].productData.length > 0) {
ServiceProduct storage product = providerProducts[providerId][ProductType.PDP];

ServiceProduct storage product = providerProducts[providerId][ProductType.PDP];
if (product.isActive) {
// Decrement active count if product was active
if (product.isActive) {
activeProductTypeProviderCount[ProductType.PDP]--;
}
activeProductTypeProviderCount[ProductType.PDP]--;

// Clear capabilities from mapping
mapping(string => string) storage capabilities = productCapabilities[providerId][ProductType.PDP];
for (uint256 i = 0; i < product.capabilityKeys.length; i++) {
delete capabilities[product.capabilityKeys[i]];
}
product.isActive = false;
delete product.productType;
delete product.capabilityKeys;
delete product.isActive;
}

// Clear address mapping
Expand Down Expand Up @@ -467,37 +435,16 @@ contract ServiceProviderRegistry is
/// @notice Get product data for a specific product type
/// @param providerId The ID of the provider
/// @param productType The type of product to retrieve
/// @return productData The encoded product data
/// @return capabilityKeys Array of capability keys
/// @return isActive Whether the product is active
function getProduct(uint256 providerId, ProductType productType)
external
view
providerExists(providerId)
returns (bytes memory productData, string[] memory capabilityKeys, bool isActive)
returns (string[] memory capabilityKeys, bool isActive)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we just return the keys and values here and be done with it? this is one of the most awkward spots - if we don't use key-existence as a signal then we're always going to want the values too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key existence is a signal

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about we just return ProviderWithProduct here? so this is the single version of getProvidersByProductType

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key existence is a signal

you're the one arguing to make value necessary even for booleans; in that world I can't think of a case where just getting the keys is useful to me, I just want all of it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you still don't understand. If a capability is a boolean, its existence is sufficient. But that existence cannot be the signaled with the empty string because it is indistinguishable when doing a single key lookup in a solidity mapping.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a key's absence unambiguously signals the capability is not supported, then the key's presence can unambiguously signal the capability is supported. Any nonzero length is thus truthy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need many of these methods. I agree they aren't useful on or off chain. I will check how we are using them tomorrow.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a key's absence unambiguously signals the capability is not supported, then the key's presence can unambiguously signal the capability is supported. Any nonzero length is thus truthy.

This is what I've been arguing for here and which is why I want an exists boolean return any time I want to ask for a specific key. I don't want to have to put a value in the value map, I just want to know the key exists and then not care about the value, and to work around the limitations of not having a null or non-zero sentinel in solidity. But we agree that the current implementation of doing that is broken - it should do it properly by iterating over keys that it has and figuring out whether it exists or not. But I also now think we can just do away with that entirely. There may be a case for "tell me the value for this key" or "tell me if you have this key", but with the way this is shaping up, I think all I really ever want out of this is be able to get the full product, keys and values, and deal with it on the client side.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use case for querying a single key is for onchain lookup. It should not loop over all of the keys to do that.

{
ServiceProduct memory product = providerProducts[providerId][productType];
return (product.productData, product.capabilityKeys, product.isActive);
}

/// @notice Get PDP service configuration for a provider (convenience function)
/// @param providerId The ID of the provider
/// @return pdpOffering The decoded PDP service data
/// @return capabilityKeys Array of capability keys
/// @return isActive Whether the PDP service is active
function getPDPService(uint256 providerId)
external
view
providerExists(providerId)
returns (PDPOffering memory pdpOffering, string[] memory capabilityKeys, bool isActive)
{
ServiceProduct memory product = providerProducts[providerId][ProductType.PDP];

if (product.productData.length > 0) {
pdpOffering = abi.decode(product.productData, (PDPOffering));
capabilityKeys = product.capabilityKeys;
isActive = product.isActive;
}
return (product.capabilityKeys, product.isActive);
}

/// @notice Get all providers that offer a specific product type with pagination
Expand Down Expand Up @@ -532,7 +479,7 @@ contract ServiceProviderRegistry is
uint256 resultIndex = 0;

for (uint256 i = 1; i <= numProviders && resultIndex < limit; i++) {
if (providerProducts[i][productType].productData.length > 0) {
if (providerProducts[i][productType].isActive) {
if (currentIndex >= offset && currentIndex < offset + limit) {
ServiceProviderInfo storage provider = providers[i];
result.providers[resultIndex] = ProviderWithProduct({
Expand Down Expand Up @@ -579,10 +526,7 @@ contract ServiceProviderRegistry is
uint256 resultIndex = 0;

for (uint256 i = 1; i <= numProviders && resultIndex < limit; i++) {
if (
providers[i].isActive && providerProducts[i][productType].isActive
&& providerProducts[i][productType].productData.length > 0
) {
if (providers[i].isActive && providerProducts[i][productType].isActive) {
if (currentIndex >= offset && currentIndex < offset + limit) {
ServiceProviderInfo storage provider = providers[i];
result.providers[resultIndex] = ProviderWithProduct({
Expand Down Expand Up @@ -795,29 +739,21 @@ contract ServiceProviderRegistry is

/// @notice Validate product data based on product type
/// @param productType The type of product
/// @param productData The encoded product data
function _validateProductData(ProductType productType, bytes memory productData) private pure {
function _validateProductKeys(ProductType productType, string[] memory capabilityKeys) private pure {
uint256 requiredKeys;
if (productType == ProductType.PDP) {
PDPOffering memory pdpOffering = abi.decode(productData, (PDPOffering));
_validatePDPOffering(pdpOffering);
requiredKeys = REQUIRED_PDP_KEYS;
} else {
revert("Unsupported product type");
}
}

/// @notice Validate PDP offering
function _validatePDPOffering(PDPOffering memory pdpOffering) private pure {
require(bytes(pdpOffering.serviceURL).length > 0, "Service URL cannot be empty");
require(bytes(pdpOffering.serviceURL).length <= MAX_SERVICE_URL_LENGTH, "Service URL too long");
require(pdpOffering.minPieceSizeInBytes > 0, "Min piece size must be greater than 0");
require(
pdpOffering.maxPieceSizeInBytes >= pdpOffering.minPieceSizeInBytes,
"Max piece size must be >= min piece size"
);
// Validate new fields
require(pdpOffering.minProvingPeriodInEpochs > 0, "Min proving period must be greater than 0");
require(bytes(pdpOffering.location).length > 0, "Location cannot be empty");
require(bytes(pdpOffering.location).length <= MAX_LOCATION_LENGTH, "Location too long");
uint256 foundKeys = 0;
for (uint256 i = 0; i < capabilityKeys.length; i++) {
uint256 key = BloomSet16.compressed(capabilityKeys[i]);
if (BloomSet16.mayContain(requiredKeys, key)) {
foundKeys |= key;
}
}
require(BloomSet16.mayContain(foundKeys, requiredKeys));
}

/// @notice Validate capability key-value pairs
Expand Down
14 changes: 0 additions & 14 deletions service_contracts/src/ServiceProviderRegistryStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,10 @@ contract ServiceProviderRegistryStorage {
/// @notice Product offering of the Service Provider
struct ServiceProduct {
ProductType productType;
bytes productData; // ABI-encoded service-specific data
string[] capabilityKeys; // Max MAX_CAPABILITY_KEY_LENGTH chars each
bool isActive;
}

/// @notice PDP-specific service data
struct PDPOffering {
string serviceURL; // HTTP API endpoint
uint256 minPieceSizeInBytes; // Minimum piece size accepted in bytes
uint256 maxPieceSizeInBytes; // Maximum piece size accepted in bytes
bool ipniPiece; // Supports IPNI piece CID indexing
bool ipniIpfs; // Supports IPNI IPFS CID indexing
uint256 storagePricePerTibPerMonth; // Storage price per TiB per month (in token's smallest unit)
uint256 minProvingPeriodInEpochs; // Minimum proving period in epochs
string location; // Geographic location of the service provider
IERC20 paymentTokenAddress; // Token contract for payment (IERC20(address(0)) for FIL)
}

/// @notice Combined provider and product information for detailed queries
struct ProviderWithProduct {
uint256 providerId;
Expand Down
Loading
Loading