diff --git a/service_contracts/src/FilecoinWarmStorageService.sol b/service_contracts/src/FilecoinWarmStorageService.sol index 01ab88f8..1ac62651 100644 --- a/service_contracts/src/FilecoinWarmStorageService.sol +++ b/service_contracts/src/FilecoinWarmStorageService.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.20; import {PDPListener} from "@pdp/PDPVerifier.sol"; +import {IPDPVerifier} from "@pdp/interfaces/IPDPVerifier.sol"; import {Cids} from "@pdp/Cids.sol"; import {SessionKeyRegistry} from "@session-key-registry/SessionKeyRegistry.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; @@ -663,7 +664,7 @@ contract FilecoinWarmStorageService is // Validate payer has sufficient funds and operator approvals for minimum pricing // If CDN is enabled, validation must account for the additional fixed lockup amounts - validatePayerOperatorApprovalAndFunds(payments, createData.payer, hasCDN); + validatePayerOperatorApprovalAndFunds(payments, createData.payer, hasCDN, 0, new Cids.Cid[](0)); uint256 pdpRailId = payments.createRail( usdfcTokenAddress, // token address @@ -834,6 +835,13 @@ contract FilecoinWarmStorageService is // Verify the signature verifyAddPiecesSignature(payer, info.clientDataSetId, pieceData, nonce, metadataKeys, metadataValues, signature); + // Validate payer/operator approvals and available funds for the new pieces + // This checks the payer has sufficient available funds and operator allowances + // to cover the increased per-epoch rate and the 30-day lockup implied by the + // new total leaf count after adding these pieces. + FilecoinPayV1 payments = FilecoinPayV1(paymentsContractAddress); + validatePayerOperatorApprovalAndFunds(payments, payer, false, dataSetId, pieceData); + // Store metadata for each new piece for (uint256 i = 0; i < pieceData.length; i++) { uint256 pieceId = firstAdded + i; @@ -1177,14 +1185,22 @@ contract FilecoinWarmStorageService is /// @notice Validates that the payer has sufficient funds and operator approvals for minimum pricing /// @param payments The FilecoinPayV1 contract instance /// @param payer The address of the payer - function validatePayerOperatorApprovalAndFunds(FilecoinPayV1 payments, address payer, bool includeCDN) - internal - view - { - // Calculate required lockup for minimum pricing - uint256 minimumLockupRequired = (minimumStorageRatePerMonth * DEFAULT_LOCKUP_PERIOD) / EPOCHS_PER_MONTH; + function validatePayerOperatorApprovalAndFunds( + FilecoinPayV1 payments, + address payer, + bool includeCDN, + uint256 dataSetId, + Cids.Cid[] memory pieceData + ) internal view { + uint256 totalBytes = 0; + if (dataSetId != 0 && pieceData.length != 0) { + totalBytes = IPDPVerifier(pdpVerifierAddress).getDataSetLeafCount(dataSetId) * BYTES_PER_LEAF; + } - // If CDN is enabled, include the fixed cache-miss and CDN lockup amounts + // Calculate the minimum storage rate per epoch based on total bytes + uint256 minimumStorageRatePerEpoch = _calculateStorageRate(totalBytes); + // Calculate the minimum lockup required for the payer + uint256 minimumLockupRequired = minimumStorageRatePerEpoch * DEFAULT_LOCKUP_PERIOD; if (includeCDN) { minimumLockupRequired += DEFAULT_CACHE_MISS_LOCKUP_AMOUNT + DEFAULT_CDN_LOCKUP_AMOUNT; } @@ -1208,14 +1224,10 @@ contract FilecoinWarmStorageService is // Verify operator is approved require(isApproved, Errors.OperatorNotApproved(payer, address(this))); - - // Calculate minimum rate per epoch - uint256 minimumRatePerEpoch = minimumStorageRatePerMonth / EPOCHS_PER_MONTH; - // Verify rate allowance is sufficient require( - rateAllowance >= rateUsage + minimumRatePerEpoch, - Errors.InsufficientRateAllowance(payer, address(this), rateAllowance, rateUsage, minimumRatePerEpoch) + rateAllowance >= rateUsage + minimumStorageRatePerEpoch, + Errors.InsufficientRateAllowance(payer, address(this), rateAllowance, rateUsage, minimumStorageRatePerEpoch) ); // Verify lockup allowance is sufficient (include CDN extras when applicable) diff --git a/service_contracts/test/FilecoinWarmStorageService.t.sol b/service_contracts/test/FilecoinWarmStorageService.t.sol index ff8d8216..8aaa1b54 100644 --- a/service_contracts/test/FilecoinWarmStorageService.t.sol +++ b/service_contracts/test/FilecoinWarmStorageService.t.sol @@ -803,9 +803,9 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { // First batch (3 pieces) with key "meta" => metadataShort Cids.Cid[] memory pieceData1 = new Cids.Cid[](3); - pieceData1[0].data = bytes("1_0:1111"); - pieceData1[1].data = bytes("1_1:111100000"); - pieceData1[2].data = bytes("1_2:11110000000000"); + pieceData1[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("1_0:1111"))); + pieceData1[1] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("1_1:111100000"))); + pieceData1[2] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("1_2:11110000000000"))); string[] memory keys1 = new string[](1); string[] memory values1 = new string[](1); keys1[0] = "meta"; @@ -817,8 +817,10 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { // Second batch (2 pieces) with key "meta" => metadataLong Cids.Cid[] memory pieceData2 = new Cids.Cid[](2); - pieceData2[0].data = bytes("2_0:22222222222222222222"); - pieceData2[1].data = bytes("2_1:222222222222222222220000000000000000000000000000000000000000"); + pieceData2[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("2_0:22222222222222222222"))); + pieceData2[1] = Cids.CommPv2FromDigest( + 0, 4, keccak256(abi.encodePacked("2_1:222222222222222222220000000000000000000000000000000000000000000")) + ); string[] memory keys2 = new string[](1); string[] memory values2 = new string[](1); keys2[0] = "meta"; @@ -1066,7 +1068,8 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { ); // Expected minimum: (0.06 USDFC * 86400) / 86400 = 0.06 USDFC = 6e16 - uint256 minimumRequired = 6e16; + uint256 minimumRequiredPerEpoch = (uint256(6e16) / (2880 * 30)); + uint256 minimumRequired = minimumRequiredPerEpoch * (2880 * 30); // Expect revert with InsufficientLockupFunds error makeSignaturePass(insufficientClient); @@ -1161,6 +1164,194 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { assertEq(dataSetId, 1, "Dataset should be created with above-minimum funds"); } + function testAddPiecesFundingValidation() public { + uint256 minimumAmount = 6e16; // 0.06 USDFC + + // Test 1: Insufficient funds with small piece (below minimum rate) + address client1 = makeAddr("client1"); + mockUSDFC.safeTransfer(client1, minimumAmount); + + vm.startPrank(client1); + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + mockUSDFC.approve(address(payments), minimumAmount); + payments.deposit(mockUSDFC, client1, minimumAmount); + vm.stopPrank(); + + (string[] memory dsKeys, string[] memory dsValues) = _getSingleMetadataKV("label", "Test"); + FilecoinWarmStorageService.DataSetCreateData memory createData = FilecoinWarmStorageService.DataSetCreateData({ + payer: client1, + clientDataSetId: 1001, + metadataKeys: dsKeys, + metadataValues: dsValues, + signature: FAKE_SIGNATURE + }); + + bytes memory encodedCreateData = abi.encode( + createData.payer, + createData.clientDataSetId, + createData.metadataKeys, + createData.metadataValues, + createData.signature + ); + + makeSignaturePass(client1); + vm.prank(serviceProvider); + uint256 dataSetId1 = mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData); + + vm.prank(client1); + payments.withdraw(mockUSDFC, 59999999999961600); // Withdraw almost all funds + + Cids.Cid[] memory pieceData1 = new Cids.Cid[](1); + pieceData1[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("piece1"))); + string[] memory emptyKeys = new string[](0); + string[] memory emptyValues = new string[](0); + + makeSignaturePass(client1); + uint256 leafCount1 = 0; + for (uint256 i = 0; i < pieceData1.length; i++) { + (uint256 padding, uint8 height,) = Cids.validateCommPv2(pieceData1[i]); + leafCount1 += Cids.leafCount(padding, height); + } + uint256 lockupRequired1 = pdpServiceWithPayments.calculateRatePerEpoch(leafCount1 * 32) * 2880 * 30; + (uint256 availableFunds1,) = getAccountInfo(mockUSDFC, client1); + + vm.expectRevert( + abi.encodeWithSelector(Errors.InsufficientLockupFunds.selector, client1, lockupRequired1, availableFunds1) + ); + vm.prank(serviceProvider); + mockPDPVerifier.addPieces( + pdpServiceWithPayments, dataSetId1, 0, pieceData1, 0, FAKE_SIGNATURE, emptyKeys, emptyValues + ); + + // Test 2: Sufficient funds with small piece (below minimum rate) + address client2 = makeAddr("client2"); + mockUSDFC.safeTransfer(client2, minimumAmount); + + vm.startPrank(client2); + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + mockUSDFC.approve(address(payments), minimumAmount); + payments.deposit(mockUSDFC, client2, minimumAmount); + vm.stopPrank(); + + createData.payer = client2; + createData.clientDataSetId = 1002; + encodedCreateData = abi.encode( + createData.payer, + createData.clientDataSetId, + createData.metadataKeys, + createData.metadataValues, + createData.signature + ); + + makeSignaturePass(client2); + vm.prank(serviceProvider); + uint256 dataSetId2 = mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData); + + Cids.Cid[] memory pieceData2 = new Cids.Cid[](1); + pieceData2[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("piece2"))); + + vm.prank(serviceProvider); + mockPDPVerifier.addPieces( + pdpServiceWithPayments, dataSetId2, 0, pieceData2, 0, FAKE_SIGNATURE, emptyKeys, emptyValues + ); + + // Test 3: Insufficient funds with large pieces (above minimum rate) + address client3 = makeAddr("client3"); + mockUSDFC.safeTransfer(client3, minimumAmount); + + vm.startPrank(client3); + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + mockUSDFC.approve(address(payments), minimumAmount); + payments.deposit(mockUSDFC, client3, minimumAmount); + vm.stopPrank(); + + createData.payer = client3; + createData.clientDataSetId = 1003; + encodedCreateData = abi.encode( + createData.payer, + createData.clientDataSetId, + createData.metadataKeys, + createData.metadataValues, + createData.signature + ); + + makeSignaturePass(client3); + vm.prank(serviceProvider); + uint256 dataSetId3 = mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData); + + // Construct 5 large pieces + uint256 totalPieces = 5; + Cids.Cid[] memory pieceData3 = new Cids.Cid[](totalPieces); + string[] memory keys3 = new string[](MAX_KEYS_PER_PIECE); + string[] memory values3 = new string[](MAX_KEYS_PER_PIECE); + + for (uint256 p = 0; p < totalPieces; p++) { + pieceData3[p] = Cids.CommPv2FromDigest(0, 28, keccak256(abi.encodePacked("file", Strings.toString(p)))); + } + for (uint256 k = 0; k < MAX_KEYS_PER_PIECE; k++) { + keys3[k] = _generateKey(k); + values3[k] = _makeStringOfLength(128); + } + + makeSignaturePass(client3); + + uint256 leafCount3 = 0; + for (uint256 i = 0; i < pieceData3.length; i++) { + (uint256 padding, uint8 height,) = Cids.validateCommPv2(pieceData3[i]); + leafCount3 += Cids.leafCount(padding, height); + } + + uint256 lockupRequired3 = pdpServiceWithPayments.calculateRatePerEpoch(leafCount3 * 32) * 2880 * 30; + (uint256 availableFunds3,) = getAccountInfo(mockUSDFC, client3); + + vm.expectRevert( + abi.encodeWithSelector(Errors.InsufficientLockupFunds.selector, client3, lockupRequired3, availableFunds3) + ); + vm.prank(serviceProvider); + mockPDPVerifier.addPieces(pdpServiceWithPayments, dataSetId3, 0, pieceData3, 0, FAKE_SIGNATURE, keys3, values3); + + // Test 4: Sufficient funds with large pieces (above minimum rate) + address client4 = makeAddr("client4"); + uint256 sufficientAmount = 10e16; // 0.10 USDFC + mockUSDFC.safeTransfer(client4, sufficientAmount); + + vm.startPrank(client4); + payments.setOperatorApproval(mockUSDFC, address(pdpServiceWithPayments), true, 1000e18, 1000e18, 365 days); + mockUSDFC.approve(address(payments), sufficientAmount); + payments.deposit(mockUSDFC, client4, sufficientAmount); + vm.stopPrank(); + + createData.payer = client4; + createData.clientDataSetId = 1004; + encodedCreateData = abi.encode( + createData.payer, + createData.clientDataSetId, + createData.metadataKeys, + createData.metadataValues, + createData.signature + ); + + makeSignaturePass(client4); + vm.prank(serviceProvider); + uint256 dataSetId4 = mockPDPVerifier.createDataSet(pdpServiceWithPayments, encodedCreateData); + + Cids.Cid[] memory pieceData4 = new Cids.Cid[](totalPieces); + string[] memory keys4 = new string[](MAX_KEYS_PER_PIECE); + string[] memory values4 = new string[](MAX_KEYS_PER_PIECE); + + for (uint256 p = 0; p < totalPieces; p++) { + pieceData4[p] = Cids.CommPv2FromDigest(0, 28, keccak256(abi.encodePacked("file", Strings.toString(p)))); + } + for (uint256 k = 0; k < MAX_KEYS_PER_PIECE; k++) { + keys4[k] = _generateKey(k); + values4[k] = _makeStringOfLength(128); + } + + makeSignaturePass(client4); + vm.prank(serviceProvider); + mockPDPVerifier.addPieces(pdpServiceWithPayments, dataSetId4, 0, pieceData4, 0, FAKE_SIGNATURE, keys4, values4); + } + // Operator Approval Validation Tests function testOperatorApproval_NotApproved() public { // Setup: Client with sufficient funds but no operator approval @@ -1277,7 +1468,8 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { // DEFAULT_LOCKUP_PERIOD = 86400 // EPOCHS_PER_MONTH = 86400 // minimumLockupRequired = (6e16 * 86400) / 86400 = 6e16 - uint256 minimumLockupRequired = 6e16; + uint256 minimumLockupRatePerEpoch = (uint256(6e16) / (2880 * 30)); + uint256 minimumLockupRequired = minimumLockupRatePerEpoch * (2880 * 30); uint256 insufficientLockupAllowance = minimumLockupRequired - 1; // Just below minimum // Transfer tokens and set up approvals @@ -4558,7 +4750,7 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { // Prepare piece data Cids.Cid[] memory pieceData = new Cids.Cid[](1); - pieceData[0].data = bytes("test_piece_1"); + pieceData[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("test_piece_1"))); string[] memory keys = new string[](0); string[] memory values = new string[](0); @@ -4604,7 +4796,7 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { // Prepare piece data Cids.Cid[] memory pieceData = new Cids.Cid[](1); - pieceData[0].data = bytes("test_piece"); + pieceData[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("test_piece_1"))); string[] memory keys = new string[](0); string[] memory values = new string[](0); @@ -4678,7 +4870,7 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { // Prepare piece data Cids.Cid[] memory pieceData = new Cids.Cid[](1); - pieceData[0].data = bytes("test"); + pieceData[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("test_piece_1"))); string[] memory keys = new string[](0); string[] memory values = new string[](0); @@ -4725,7 +4917,7 @@ contract FilecoinWarmStorageServiceTest is MockFVMTest { // Prepare piece data Cids.Cid[] memory pieceData = new Cids.Cid[](1); - pieceData[0].data = bytes("test"); + pieceData[0] = Cids.CommPv2FromDigest(0, 4, keccak256(abi.encodePacked("test_piece_1"))); string[] memory keys = new string[](0); string[] memory values = new string[](0); diff --git a/service_contracts/test/mocks/SharedMocks.sol b/service_contracts/test/mocks/SharedMocks.sol index 39e0b267..594c2f5a 100644 --- a/service_contracts/test/mocks/SharedMocks.sol +++ b/service_contracts/test/mocks/SharedMocks.sol @@ -99,6 +99,8 @@ contract MockPDPVerifier { // Track data set service providers for testing mapping(uint256 => address) public dataSetServiceProviders; + // Track simple leaf counts per data set for tests (approximate via bytes length) + mapping(uint256 => uint256) public dataSetLeafCount; event DataSetCreated(uint256 indexed setId, address indexed owner); event DataSetServiceProviderChanged( @@ -118,6 +120,9 @@ contract MockPDPVerifier { // Track service provider dataSetServiceProviders[setId] = msg.sender; + // initialize leaf count to 0 + dataSetLeafCount[setId] = 0; + emit DataSetCreated(setId, msg.sender); return setId; } @@ -128,6 +133,7 @@ contract MockPDPVerifier { } delete dataSetServiceProviders[setId]; + delete dataSetLeafCount[setId]; emit DataSetDeleted(setId, 0); } @@ -150,9 +156,22 @@ contract MockPDPVerifier { } bytes memory extraData = abi.encode(nonce, allKeys, allValues, signature); + + uint256 leafCount = 0; + for (uint256 i = 0; i < pieceData.length; i++) { + (uint256 padding, uint8 height,) = Cids.validateCommPv2(pieceData[i]); + leafCount += Cids.leafCount(padding, height); + } + dataSetLeafCount[dataSetId] += leafCount; + listenerAddr.piecesAdded(dataSetId, firstAdded, pieceData, extraData); } + // Expose leaf count similar to real PDPVerifier + function getDataSetLeafCount(uint256 setId) external view returns (uint256) { + return dataSetLeafCount[setId]; + } + /** * @notice Simulates service provider change for testing purposes * @dev This function mimics the PDPVerifier's claimDataSetOwnership functionality