From b7b54ea1117b1a9e201ea60d914589d0a6070796 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Wed, 22 Oct 2025 14:51:37 -0500 Subject: [PATCH 1/8] allow forward progress through unsettled periods --- service_contracts/src/FilecoinWarmStorageService.sol | 10 +++------- .../test/FilecoinWarmStorageService.t.sol | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/service_contracts/src/FilecoinWarmStorageService.sol b/service_contracts/src/FilecoinWarmStorageService.sol index 90914c19..6a857299 100644 --- a/service_contracts/src/FilecoinWarmStorageService.sol +++ b/service_contracts/src/FilecoinWarmStorageService.sol @@ -1443,14 +1443,14 @@ contract FilecoinWarmStorageService is } // Count proven epochs and find the last proven epoch - (uint256 provenEpochCount, uint256 lastProvenEpoch) = + (uint256 provenEpochCount, uint256 settledUpTo) = _findProvenEpochs(dataSetId, fromEpoch, toEpoch, activationEpoch); // If no epochs are proven, we can't settle anything if (provenEpochCount == 0) { return ValidationResult({ modifiedAmount: 0, - settleUpto: fromEpoch, + settleUpto: settledUpTo, note: "No proven epochs in the requested range" }); } @@ -1458,11 +1458,7 @@ contract FilecoinWarmStorageService is // Calculate the modified amount based on proven epochs uint256 modifiedAmount = (proposedAmount * provenEpochCount) / totalEpochsRequested; - return ValidationResult({ - modifiedAmount: modifiedAmount, - settleUpto: lastProvenEpoch, // Settle up to the last proven epoch - note: "" - }); + return ValidationResult({modifiedAmount: modifiedAmount, settleUpto: settledUpTo, note: ""}); } function _findProvenEpochs(uint256 dataSetId, uint256 fromEpoch, uint256 toEpoch, uint256 activationEpoch) diff --git a/service_contracts/test/FilecoinWarmStorageService.t.sol b/service_contracts/test/FilecoinWarmStorageService.t.sol index 67cb612d..5442be04 100644 --- a/service_contracts/test/FilecoinWarmStorageService.t.sol +++ b/service_contracts/test/FilecoinWarmStorageService.t.sol @@ -4321,7 +4321,7 @@ contract ValidatePaymentTest is FilecoinWarmStorageServiceTest { // Should pay nothing assertEq(result.modifiedAmount, 0, "Should pay nothing"); - assertEq(result.settleUpto, fromEpoch, "Should not settle"); + assertEq(result.settleUpto, activationEpoch + (maxProvingPeriod * 2), "Should not settle last period"); assertEq(result.note, "No proven epochs in the requested range"); } From 29cf3b2f04a7204813dfbdfc08e6a1bc30332aac Mon Sep 17 00:00:00 2001 From: William Morriss Date: Wed, 22 Oct 2025 15:54:39 -0500 Subject: [PATCH 2/8] add failing test --- .../test/FilecoinWarmStorageService.t.sol | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/service_contracts/test/FilecoinWarmStorageService.t.sol b/service_contracts/test/FilecoinWarmStorageService.t.sol index 5442be04..831c27a6 100644 --- a/service_contracts/test/FilecoinWarmStorageService.t.sol +++ b/service_contracts/test/FilecoinWarmStorageService.t.sol @@ -4312,10 +4312,9 @@ contract ValidatePaymentTest is FilecoinWarmStorageServiceTest { // Validate payment FilecoinWarmStorageService.DataSetInfoView memory info = viewContract.getDataSet(dataSetId); uint256 fromEpoch = activationEpoch - 1; // exclusive - uint256 toEpoch = activationEpoch + (maxProvingPeriod * 3) - 1; + uint256 toEpoch = vm.getBlockNumber() - 1; uint256 proposedAmount = 1000e6; - vm.prank(address(payments)); IValidator.ValidationResult memory result = pdpServiceWithPayments.validatePayment(info.pdpRailId, proposedAmount, fromEpoch, toEpoch, 0); @@ -4323,6 +4322,33 @@ contract ValidatePaymentTest is FilecoinWarmStorageServiceTest { assertEq(result.modifiedAmount, 0, "Should pay nothing"); assertEq(result.settleUpto, activationEpoch + (maxProvingPeriod * 2), "Should not settle last period"); assertEq(result.note, "No proven epochs in the requested range"); + + vm.prank(address(mockPDPVerifier)); + pdpServiceWithPayments.nextProvingPeriod(dataSetId, challengeEpoch + maxProvingPeriod * 2, 100, ""); + + result = pdpServiceWithPayments.validatePayment(info.pdpRailId, proposedAmount, activationEpoch, toEpoch, 0); + assertEq(result.modifiedAmount, 0, "Should pay nothing"); + assertEq(result.settleUpto, activationEpoch + (maxProvingPeriod * 2), "Should not settle last period"); + assertEq(result.note, "No proven epochs in the requested range"); + + toEpoch = activationEpoch + 1; + result = pdpServiceWithPayments.validatePayment(info.pdpRailId, proposedAmount, activationEpoch, toEpoch, 0); + assertEq(result.modifiedAmount, 0, "Should pay nothing"); + assertEq(result.settleUpto, activationEpoch, "Should not settle"); + assertEq(result.note, "No proven epochs in the requested range"); + + fromEpoch = activationEpoch + maxProvingPeriod * 2 - 1; + toEpoch = activationEpoch + maxProvingPeriod * 2 + 1; + result = pdpServiceWithPayments.validatePayment(info.pdpRailId, proposedAmount, fromEpoch, toEpoch, 0); + assertEq(result.modifiedAmount, 0, "Should pay nothing"); + assertEq(result.settleUpto, fromEpoch, "Should not settle"); + assertEq(result.note, "No proven epochs in the requested range"); + + fromEpoch = activationEpoch + maxProvingPeriod * 2 - 2; + result = pdpServiceWithPayments.validatePayment(info.pdpRailId, proposedAmount, fromEpoch, toEpoch, 0); + assertEq(result.modifiedAmount, 0, "Should pay nothing"); + assertEq(result.settleUpto, activationEpoch + maxProvingPeriod * 2, "Should not settle into last period"); + assertEq(result.note, "No proven epochs in the requested range"); } /** From c448c413c6824255cc773eb33994ea73366228a6 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Wed, 22 Oct 2025 15:54:53 -0500 Subject: [PATCH 3/8] fix test --- service_contracts/src/FilecoinWarmStorageService.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/service_contracts/src/FilecoinWarmStorageService.sol b/service_contracts/src/FilecoinWarmStorageService.sol index 6a857299..3f78565a 100644 --- a/service_contracts/src/FilecoinWarmStorageService.sol +++ b/service_contracts/src/FilecoinWarmStorageService.sol @@ -1482,6 +1482,8 @@ contract FilecoinWarmStorageService is if (_isPeriodProven(dataSetId, startingPeriod, currentPeriod)) { provenEpochCount = toEpoch - fromEpoch; settledUpTo = toEpoch; + } else { + settledUpTo = fromEpoch; } } else { if (_isPeriodProven(dataSetId, startingPeriod, currentPeriod)) { From 91f66ec3c501456d05d9c21f5b9616dc66ba8c5b Mon Sep 17 00:00:00 2001 From: William Morriss Date: Thu, 23 Oct 2025 13:21:46 -0500 Subject: [PATCH 4/8] settleUpTo --- .../src/FilecoinWarmStorageService.sol | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/service_contracts/src/FilecoinWarmStorageService.sol b/service_contracts/src/FilecoinWarmStorageService.sol index 3f78565a..73ae63dc 100644 --- a/service_contracts/src/FilecoinWarmStorageService.sol +++ b/service_contracts/src/FilecoinWarmStorageService.sol @@ -1442,15 +1442,15 @@ contract FilecoinWarmStorageService is }); } - // Count proven epochs and find the last proven epoch - (uint256 provenEpochCount, uint256 settledUpTo) = + // Count proven epochs up to toEpoch, possibly stopping earlier if unresolved + (uint256 provenEpochCount, uint256 settleUpTo) = _findProvenEpochs(dataSetId, fromEpoch, toEpoch, activationEpoch); // If no epochs are proven, we can't settle anything if (provenEpochCount == 0) { return ValidationResult({ modifiedAmount: 0, - settleUpto: settledUpTo, + settleUpto: settleUpTo, note: "No proven epochs in the requested range" }); } @@ -1458,13 +1458,13 @@ contract FilecoinWarmStorageService is // Calculate the modified amount based on proven epochs uint256 modifiedAmount = (proposedAmount * provenEpochCount) / totalEpochsRequested; - return ValidationResult({modifiedAmount: modifiedAmount, settleUpto: settledUpTo, note: ""}); + return ValidationResult({modifiedAmount: modifiedAmount, settleUpto: settleUpTo, note: ""}); } function _findProvenEpochs(uint256 dataSetId, uint256 fromEpoch, uint256 toEpoch, uint256 activationEpoch) internal view - returns (uint256 provenEpochCount, uint256 settledUpTo) + returns (uint256 provenEpochCount, uint256 settleUpTo) { require(toEpoch >= activationEpoch && toEpoch <= block.number, Errors.InvalidEpochRange(fromEpoch, toEpoch)); uint256 currentPeriod = getProvingPeriodForEpoch(dataSetId, block.number); @@ -1481,9 +1481,9 @@ contract FilecoinWarmStorageService is if (toEpoch < startingPeriodDeadline) { if (_isPeriodProven(dataSetId, startingPeriod, currentPeriod)) { provenEpochCount = toEpoch - fromEpoch; - settledUpTo = toEpoch; + settleUpTo = toEpoch; } else { - settledUpTo = fromEpoch; + settleUpTo = fromEpoch; } } else { if (_isPeriodProven(dataSetId, startingPeriod, currentPeriod)) { @@ -1497,15 +1497,15 @@ contract FilecoinWarmStorageService is provenEpochCount += maxProvingPeriod; } } - settledUpTo = _calcPeriodDeadline(activationEpoch, endingPeriod - 1); + settleUpTo = _calcPeriodDeadline(activationEpoch, endingPeriod - 1); // handle the last period separately if (_isPeriodProven(dataSetId, endingPeriod, currentPeriod)) { - provenEpochCount += (toEpoch - settledUpTo); - settledUpTo = toEpoch; + provenEpochCount += (toEpoch - settleUpTo); + settleUpTo = toEpoch; } } - return (provenEpochCount, settledUpTo); + return (provenEpochCount, settleUpTo); } function _isPeriodProven(uint256 dataSetId, uint256 periodId, uint256 currentPeriod) private view returns (bool) { From a14e411b6a606cd361693ae3e09c43ff758ec269 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Thu, 23 Oct 2025 13:39:37 -0500 Subject: [PATCH 5/8] _provingPeriodForEpoch --- service_contracts/src/FilecoinWarmStorageService.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/service_contracts/src/FilecoinWarmStorageService.sol b/service_contracts/src/FilecoinWarmStorageService.sol index 73ae63dc..6da786b9 100644 --- a/service_contracts/src/FilecoinWarmStorageService.sol +++ b/service_contracts/src/FilecoinWarmStorageService.sol @@ -1126,8 +1126,10 @@ contract FilecoinWarmStorageService is * @return The period ID this epoch belongs to, or type(uint256).max if before activation */ function getProvingPeriodForEpoch(uint256 dataSetId, uint256 epoch) public view returns (uint256) { - uint256 activationEpoch = provingActivationEpoch[dataSetId]; + return _provingPeriodForEpoch(provingActivationEpoch[dataSetId], epoch); + } + function _provingPeriodForEpoch(uint256 activationEpoch, uint256 epoch) internal view returns (uint256) { // If proving wasn't activated or epoch is before activation if (activationEpoch == 0 || epoch < activationEpoch) { return type(uint256).max; // Invalid period @@ -1467,13 +1469,13 @@ contract FilecoinWarmStorageService is returns (uint256 provenEpochCount, uint256 settleUpTo) { require(toEpoch >= activationEpoch && toEpoch <= block.number, Errors.InvalidEpochRange(fromEpoch, toEpoch)); - uint256 currentPeriod = getProvingPeriodForEpoch(dataSetId, block.number); + uint256 currentPeriod = _provingPeriodForEpoch(activationEpoch, block.number); if (fromEpoch < activationEpoch - 1) { fromEpoch = activationEpoch - 1; } - uint256 startingPeriod = getProvingPeriodForEpoch(dataSetId, fromEpoch + 1); + uint256 startingPeriod = _provingPeriodForEpoch(activationEpoch, fromEpoch + 1); // handle first period separately uint256 startingPeriodDeadline = _calcPeriodDeadline(activationEpoch, startingPeriod); @@ -1490,7 +1492,7 @@ contract FilecoinWarmStorageService is provenEpochCount += (startingPeriodDeadline - fromEpoch); } - uint256 endingPeriod = getProvingPeriodForEpoch(dataSetId, toEpoch); + uint256 endingPeriod = _provingPeriodForEpoch(activationEpoch, toEpoch); // loop through the proving periods between startingPeriod and endingPeriod for (uint256 period = startingPeriod + 1; period < endingPeriod; period++) { if (_isPeriodProven(dataSetId, period, currentPeriod)) { From 4c75bb638fdca70f85937dfa9f0c4f9c150c2517 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Thu, 23 Oct 2025 13:43:56 -0500 Subject: [PATCH 6/8] rm redundancy related to provenThisPeriod --- .../src/FilecoinWarmStorageService.sol | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/service_contracts/src/FilecoinWarmStorageService.sol b/service_contracts/src/FilecoinWarmStorageService.sol index 6da786b9..a5e792bc 100644 --- a/service_contracts/src/FilecoinWarmStorageService.sol +++ b/service_contracts/src/FilecoinWarmStorageService.sol @@ -860,7 +860,6 @@ contract FilecoinWarmStorageService is revert Errors.InvalidChallengeEpoch(dataSetId, minWindow, maxWindow, challengeEpoch); } provingDeadlines[dataSetId] = firstDeadline; - provenThisPeriod[dataSetId] = false; // Initialize the activation epoch when proving first starts // This marks when the data set became active for proving @@ -910,15 +909,6 @@ contract FilecoinWarmStorageService is emit FaultRecord(dataSetId, faultPeriods, provingDeadlines[dataSetId]); } - // Record the status of the current/previous proving period that's ending - if (provingDeadlines[dataSetId] != NO_PROVING_DEADLINE && provenThisPeriod[dataSetId]) { - // Determine the period ID that just completed - uint256 completedPeriodId = getProvingPeriodForEpoch(dataSetId, provingDeadlines[dataSetId] - 1); - - // Record whether this period was proven - provenPeriods[dataSetId][completedPeriodId >> 8] |= 1 << (completedPeriodId & 255); - } - provingDeadlines[dataSetId] = nextDeadline; provenThisPeriod[dataSetId] = false; @@ -1469,8 +1459,6 @@ contract FilecoinWarmStorageService is returns (uint256 provenEpochCount, uint256 settleUpTo) { require(toEpoch >= activationEpoch && toEpoch <= block.number, Errors.InvalidEpochRange(fromEpoch, toEpoch)); - uint256 currentPeriod = _provingPeriodForEpoch(activationEpoch, block.number); - if (fromEpoch < activationEpoch - 1) { fromEpoch = activationEpoch - 1; } @@ -1481,28 +1469,28 @@ contract FilecoinWarmStorageService is uint256 startingPeriodDeadline = _calcPeriodDeadline(activationEpoch, startingPeriod); if (toEpoch < startingPeriodDeadline) { - if (_isPeriodProven(dataSetId, startingPeriod, currentPeriod)) { + if (_isPeriodProven(dataSetId, startingPeriod)) { provenEpochCount = toEpoch - fromEpoch; settleUpTo = toEpoch; } else { settleUpTo = fromEpoch; } } else { - if (_isPeriodProven(dataSetId, startingPeriod, currentPeriod)) { + if (_isPeriodProven(dataSetId, startingPeriod)) { provenEpochCount += (startingPeriodDeadline - fromEpoch); } uint256 endingPeriod = _provingPeriodForEpoch(activationEpoch, toEpoch); // loop through the proving periods between startingPeriod and endingPeriod for (uint256 period = startingPeriod + 1; period < endingPeriod; period++) { - if (_isPeriodProven(dataSetId, period, currentPeriod)) { + if (_isPeriodProven(dataSetId, period)) { provenEpochCount += maxProvingPeriod; } } settleUpTo = _calcPeriodDeadline(activationEpoch, endingPeriod - 1); // handle the last period separately - if (_isPeriodProven(dataSetId, endingPeriod, currentPeriod)) { + if (_isPeriodProven(dataSetId, endingPeriod)) { provenEpochCount += (toEpoch - settleUpTo); settleUpTo = toEpoch; } @@ -1510,10 +1498,7 @@ contract FilecoinWarmStorageService is return (provenEpochCount, settleUpTo); } - function _isPeriodProven(uint256 dataSetId, uint256 periodId, uint256 currentPeriod) private view returns (bool) { - if (periodId == currentPeriod) { - return provenThisPeriod[dataSetId]; - } + function _isPeriodProven(uint256 dataSetId, uint256 periodId) private view returns (bool) { uint256 isProven = provenPeriods[dataSetId][periodId >> 8] & (1 << (periodId & 255)); return isProven != 0; } From 93b682df20be9c8e7249d45706042ac25ef1ad45 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Fri, 24 Oct 2025 13:40:24 -0500 Subject: [PATCH 7/8] document test cases --- .../test/FilecoinWarmStorageService.t.sol | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/service_contracts/test/FilecoinWarmStorageService.t.sol b/service_contracts/test/FilecoinWarmStorageService.t.sol index 831c27a6..4516a9e1 100644 --- a/service_contracts/test/FilecoinWarmStorageService.t.sol +++ b/service_contracts/test/FilecoinWarmStorageService.t.sol @@ -4318,7 +4318,7 @@ contract ValidatePaymentTest is FilecoinWarmStorageServiceTest { IValidator.ValidationResult memory result = pdpServiceWithPayments.validatePayment(info.pdpRailId, proposedAmount, fromEpoch, toEpoch, 0); - // Should pay nothing + // Should settle two unproven periods assertEq(result.modifiedAmount, 0, "Should pay nothing"); assertEq(result.settleUpto, activationEpoch + (maxProvingPeriod * 2), "Should not settle last period"); assertEq(result.note, "No proven epochs in the requested range"); @@ -4326,17 +4326,20 @@ contract ValidatePaymentTest is FilecoinWarmStorageServiceTest { vm.prank(address(mockPDPVerifier)); pdpServiceWithPayments.nextProvingPeriod(dataSetId, challengeEpoch + maxProvingPeriod * 2, 100, ""); + // Should settle up to start of current period result = pdpServiceWithPayments.validatePayment(info.pdpRailId, proposedAmount, activationEpoch, toEpoch, 0); assertEq(result.modifiedAmount, 0, "Should pay nothing"); assertEq(result.settleUpto, activationEpoch + (maxProvingPeriod * 2), "Should not settle last period"); assertEq(result.note, "No proven epochs in the requested range"); + // Never settle less than 1 proving period when that period is unproven toEpoch = activationEpoch + 1; result = pdpServiceWithPayments.validatePayment(info.pdpRailId, proposedAmount, activationEpoch, toEpoch, 0); assertEq(result.modifiedAmount, 0, "Should pay nothing"); assertEq(result.settleUpto, activationEpoch, "Should not settle"); assertEq(result.note, "No proven epochs in the requested range"); + // Never settle less than 1 proving period when that period is unproven fromEpoch = activationEpoch + maxProvingPeriod * 2 - 1; toEpoch = activationEpoch + maxProvingPeriod * 2 + 1; result = pdpServiceWithPayments.validatePayment(info.pdpRailId, proposedAmount, fromEpoch, toEpoch, 0); @@ -4344,11 +4347,19 @@ contract ValidatePaymentTest is FilecoinWarmStorageServiceTest { assertEq(result.settleUpto, fromEpoch, "Should not settle"); assertEq(result.note, "No proven epochs in the requested range"); + // Settle only up to the start of current period fromEpoch = activationEpoch + maxProvingPeriod * 2 - 2; result = pdpServiceWithPayments.validatePayment(info.pdpRailId, proposedAmount, fromEpoch, toEpoch, 0); assertEq(result.modifiedAmount, 0, "Should pay nothing"); assertEq(result.settleUpto, activationEpoch + maxProvingPeriod * 2, "Should not settle into last period"); assertEq(result.note, "No proven epochs in the requested range"); + + // Settle only up to the start of current period + fromEpoch = activationEpoch + maxProvingPeriod / 2; + result = pdpServiceWithPayments.validatePayment(info.pdpRailId, proposedAmount, fromEpoch, toEpoch, 0); + assertEq(result.modifiedAmount, 0, "Should pay nothing"); + assertEq(result.settleUpto, activationEpoch + maxProvingPeriod * 2, "Should not settle into last period"); + assertEq(result.note, "No proven epochs in the requested range"); } /** From a62e6a7fd1ae9219b0fd51d5be9ca441eaa73cb4 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Fri, 24 Oct 2025 13:43:43 -0500 Subject: [PATCH 8/8] document why the first period is special --- service_contracts/src/FilecoinWarmStorageService.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service_contracts/src/FilecoinWarmStorageService.sol b/service_contracts/src/FilecoinWarmStorageService.sol index a5e792bc..b2d76cf8 100644 --- a/service_contracts/src/FilecoinWarmStorageService.sol +++ b/service_contracts/src/FilecoinWarmStorageService.sol @@ -1465,7 +1465,7 @@ contract FilecoinWarmStorageService is uint256 startingPeriod = _provingPeriodForEpoch(activationEpoch, fromEpoch + 1); - // handle first period separately + // handle first period separately; it may be partially settled already uint256 startingPeriodDeadline = _calcPeriodDeadline(activationEpoch, startingPeriod); if (toEpoch < startingPeriodDeadline) {