From 3f4800e082305e4173500d5eedd55975973d7580 Mon Sep 17 00:00:00 2001 From: Chaitu-Tatipamula <107246959+Chaitu-Tatipamula@users.noreply.github.com> Date: Fri, 30 Jan 2026 03:16:53 +0530 Subject: [PATCH 1/2] feat: add getActivePiecesByCursor for O(limit) pagination --- src/PDPVerifier.sol | 70 ++++++++++++++++++++++++++ test/PDPVerifier.t.sol | 110 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/src/PDPVerifier.sol b/src/PDPVerifier.sol index 160f148..79566bf 100644 --- a/src/PDPVerifier.sol +++ b/src/PDPVerifier.sol @@ -331,6 +331,8 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable { /** * @notice Returns active pieces (non-zero leaf count) for a data set with pagination + * @dev WARNING: This function has O(offset) gas complexity because it always iterates from + * piece ID 0. for large data sets with high offsets, consider using getActivePiecesByCursor() * @param setId The data set ID * @param offset Starting index for pagination (0-based) * @param limit Maximum number of pieces to return @@ -391,6 +393,74 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable { } } + /** + * @notice Returns active pieces using cursor-based pagination for O(limit) gas complexity + * @dev This function is more gas-efficient than getActivePieces() for large data sets because + * it starts iteration from startPieceId instead of always from 0. Use this for paginating + * through large data sets. + * @param setId The data set ID + * @param startPieceId The piece ID to start from (use 0 for first page, then last returned pieceId + 1) + * @param limit Maximum number of pieces to return + * @return pieces Array of active piece CIDs + * @return pieceIds Array of corresponding piece IDs + * @return hasMore True if there are more pieces beyond this page + */ + function getActivePiecesByCursor(uint256 setId, uint256 startPieceId, uint256 limit) + public + view + returns (Cids.Cid[] memory pieces, uint256[] memory pieceIds, bool hasMore) + { + require(dataSetLive(setId), "Data set not live"); + require(limit > 0, "Limit must be greater than 0"); + + uint256 maxPieceId = nextPieceId[setId]; + + // if startPieceId is beyond all pieces, return empty + if (startPieceId >= maxPieceId) { + return (new Cids.Cid[](0), new uint256[](0), false); + } + + // Over-allocate arrays to limit size + Cids.Cid[] memory tempPieces = new Cids.Cid[](limit); + uint256[] memory tempPieceIds = new uint256[](limit); + uint256 resultIndex = 0; + + // Start fron startPieceId and collect up to limit + for (uint256 i = startPieceId; i < maxPieceId && resultIndex < limit; i++) { + if (pieceLeafCounts[setId][i] > 0) { + tempPieces[resultIndex] = pieceCids[setId][i]; + tempPieceIds[resultIndex] = i; + resultIndex++; + } + } + + // Check if there are more active pieces after last one we found + if (resultIndex > 0) { + uint256 lastFound = tempPieceIds[resultIndex - 1]; + for (uint256 i = lastFound + 1; i < maxPieceId; i++) { + if (pieceLeafCounts[setId][i] > 0) { + hasMore = true; + break; + } + } + } + + // Handle case where we found fewer items than limit + if (resultIndex == 0) { + return (new Cids.Cid[](0), new uint256[](0), false); + } else if (resultIndex < limit) { + pieces = new Cids.Cid[](resultIndex); + pieceIds = new uint256[](resultIndex); + for (uint256 i = 0; i < resultIndex; i++) { + pieces[i] = tempPieces[i]; + pieceIds[i] = tempPieceIds[i]; + } + } else { + pieces = tempPieces; + pieceIds = tempPieceIds; + } + } + // storage provider proposes new storage provider. If the storage provider proposes themself delete any outstanding proposed storage provider function proposeDataSetStorageProvider(uint256 setId, address newStorageProvider) public { require(dataSetLive(setId), "Data set not live"); diff --git a/test/PDPVerifier.t.sol b/test/PDPVerifier.t.sol index 8e132c1..bac9855 100644 --- a/test/PDPVerifier.t.sol +++ b/test/PDPVerifier.t.sol @@ -1132,6 +1132,116 @@ contract PDPVerifierPaginationTest is MockFVMTest, PieceHelper { assertEq(totalRetrieved, 100, "Should have retrieved all 100 pieces"); } + + function testGetActivePiecesByCursorBasic() public { + uint256 setId = pdpVerifier.addPieces{value: PDPFees.sybilFee()}( + NEW_DATA_SET_SENTINEL, address(listener), new Cids.Cid[](0), abi.encode(empty, empty) + ); + + // Add 10 pieces + Cids.Cid[] memory testPieces = new Cids.Cid[](10); + for (uint256 i = 0; i < 10; i++) { + testPieces[i] = makeSamplePiece(1024 / 32); + } + pdpVerifier.addPieces(setId, address(0), testPieces, empty); + + // Test first page + (Cids.Cid[] memory pieces1, uint256[] memory ids1, bool hasMore1) = + pdpVerifier.getActivePiecesByCursor(setId, 0, 5); + assertEq(pieces1.length, 5, "Should return 5 pieces"); + assertEq(ids1[0], 0, "First ID should be 0"); + assertEq(ids1[4], 4, "Last ID should be 4"); + assertEq(hasMore1, true, "Should have more"); + + // Test second page using cursor (last ID + 1) + (Cids.Cid[] memory pieces2, uint256[] memory ids2, bool hasMore2) = + pdpVerifier.getActivePiecesByCursor(setId, ids1[4] + 1, 5); + assertEq(pieces2.length, 5, "Should return 5 pieces"); + assertEq(ids2[0], 5, "First ID should be 5"); + assertEq(ids2[4], 9, "Last ID should be 9"); + assertEq(hasMore2, false, "Should not have more"); + } + + function testGetActivePiecesByCursorWithDeleted() public { + uint256 setId = pdpVerifier.addPieces{value: PDPFees.sybilFee()}( + NEW_DATA_SET_SENTINEL, address(listener), new Cids.Cid[](0), abi.encode(empty, empty) + ); + + // Add 10 pieces + Cids.Cid[] memory testPieces = new Cids.Cid[](10); + for (uint256 i = 0; i < 10; i++) { + testPieces[i] = makeSamplePiece(1024 / 32); + } + pdpVerifier.addPieces(setId, address(0), testPieces, empty); + + // Delete pieces 2, 4, 6 + uint256[] memory toRemove = new uint256[](3); + toRemove[0] = 2; + toRemove[1] = 4; + toRemove[2] = 6; + pdpVerifier.schedulePieceDeletions(setId, toRemove, empty); + uint256 challengeFinality = pdpVerifier.getChallengeFinality(); + pdpVerifier.nextProvingPeriod(setId, vm.getBlockNumber() + challengeFinality, empty); + + // Cursor at piece 3 should skip deleted pieces and return 3, 5, 7, 8, 9 + (Cids.Cid[] memory pieces, uint256[] memory ids, bool hasMore) = + pdpVerifier.getActivePiecesByCursor(setId, 3, 10); + + assertEq(pieces.length, 5, "Should return 5 active pieces (3,5,7,8,9)"); + assertEq(ids[0], 3, "First should be 3"); + assertEq(ids[1], 5, "Second should be 5 (skipped 4)"); + assertEq(ids[2], 7, "Third should be 7 (skipped 6)"); + assertEq(ids[3], 8, "Fourth should be 8"); + assertEq(ids[4], 9, "Fifth should be 9"); + assertEq(hasMore, false, "No more pieces"); + } + + function testGetActivePiecesByCursorBeyondRange() public { + uint256 setId = pdpVerifier.addPieces{value: PDPFees.sybilFee()}( + NEW_DATA_SET_SENTINEL, address(listener), new Cids.Cid[](0), abi.encode(empty, empty) + ); + + Cids.Cid[] memory testPieces = new Cids.Cid[](5); + for (uint256 i = 0; i < 5; i++) { + testPieces[i] = makeSamplePiece(1024 / 32); + } + pdpVerifier.addPieces(setId, address(0), testPieces, empty); + + // Cursor beyond all pieces + (Cids.Cid[] memory pieces, uint256[] memory ids, bool hasMore) = + pdpVerifier.getActivePiecesByCursor(setId, 100, 10); + + assertEq(pieces.length, 0, "Should return empty"); + assertEq(ids.length, 0, "Should return empty IDs"); + assertEq(hasMore, false, "No more pieces"); + } + + function testGetActivePiecesByCursorEmpty() public { + uint256 setId = pdpVerifier.addPieces{value: PDPFees.sybilFee()}( + NEW_DATA_SET_SENTINEL, address(listener), new Cids.Cid[](0), abi.encode(empty, empty) + ); + + (Cids.Cid[] memory pieces, uint256[] memory ids, bool hasMore) = + pdpVerifier.getActivePiecesByCursor(setId, 0, 10); + + assertEq(pieces.length, 0, "Should return empty for empty dataset"); + assertEq(ids.length, 0, "Should return empty IDs"); + assertEq(hasMore, false, "No more pieces"); + } + + function testGetActivePiecesByCursorNotLive() public { + vm.expectRevert("Data set not live"); + pdpVerifier.getActivePiecesByCursor(999, 0, 10); + } + + function testGetActivePiecesByCursorZeroLimit() public { + uint256 setId = pdpVerifier.addPieces{value: PDPFees.sybilFee()}( + NEW_DATA_SET_SENTINEL, address(listener), new Cids.Cid[](0), abi.encode(empty, empty) + ); + + vm.expectRevert("Limit must be greater than 0"); + pdpVerifier.getActivePiecesByCursor(setId, 0, 0); + } } // TestingRecordKeeperService is a PDPListener that allows any amount of proof challenges From 4f1999a8df085681c4adfad2ea08d3b3b97ec818 Mon Sep 17 00:00:00 2001 From: Chaitu-Tatipamula <107246959+Chaitu-Tatipamula@users.noreply.github.com> Date: Sat, 31 Jan 2026 03:21:29 +0530 Subject: [PATCH 2/2] refactor: use assembly truncation instead of copy loops, fixes typos --- src/PDPVerifier.sol | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/PDPVerifier.sol b/src/PDPVerifier.sol index 79566bf..fde7f40 100644 --- a/src/PDPVerifier.sol +++ b/src/PDPVerifier.sol @@ -332,7 +332,7 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable { /** * @notice Returns active pieces (non-zero leaf count) for a data set with pagination * @dev WARNING: This function has O(offset) gas complexity because it always iterates from - * piece ID 0. for large data sets with high offsets, consider using getActivePiecesByCursor() + * piece ID 0. For large data sets with high offsets, consider using getActivePiecesByCursor() * @param setId The data set ID * @param offset Starting index for pagination (0-based) * @param limit Maximum number of pieces to return @@ -379,12 +379,11 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable { return (new Cids.Cid[](0), new uint256[](0), false); } else if (resultIndex < limit) { // Found fewer items than limit - need to resize arrays - pieces = new Cids.Cid[](resultIndex); - pieceIds = new uint256[](resultIndex); - - for (uint256 i = 0; i < resultIndex; i++) { - pieces[i] = tempPieces[i]; - pieceIds[i] = tempPieceIds[i]; + pieces = tempPieces; + pieceIds = tempPieceIds; + assembly ("memory-safe") { + mstore(pieces, resultIndex) + mstore(pieceIds, resultIndex) } } else { // Found exactly limit items - use temp arrays directly @@ -425,7 +424,7 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable { uint256[] memory tempPieceIds = new uint256[](limit); uint256 resultIndex = 0; - // Start fron startPieceId and collect up to limit + // Start from startPieceId and collect up to limit for (uint256 i = startPieceId; i < maxPieceId && resultIndex < limit; i++) { if (pieceLeafCounts[setId][i] > 0) { tempPieces[resultIndex] = pieceCids[setId][i]; @@ -449,11 +448,11 @@ contract PDPVerifier is Initializable, UUPSUpgradeable, OwnableUpgradeable { if (resultIndex == 0) { return (new Cids.Cid[](0), new uint256[](0), false); } else if (resultIndex < limit) { - pieces = new Cids.Cid[](resultIndex); - pieceIds = new uint256[](resultIndex); - for (uint256 i = 0; i < resultIndex; i++) { - pieces[i] = tempPieces[i]; - pieceIds[i] = tempPieceIds[i]; + pieces = tempPieces; + pieceIds = tempPieceIds; + assembly ("memory-safe") { + mstore(pieces, resultIndex) + mstore(pieceIds, resultIndex) } } else { pieces = tempPieces;