From 5f32f4b44eb45e16e0f1eaeaa04a7c5968777cf6 Mon Sep 17 00:00:00 2001 From: Hany Date: Mon, 26 May 2025 19:57:43 +0300 Subject: [PATCH 1/2] test: Add invariant tests for SumTree (basic and advanced coverage) --- test/PDPVerifier.t.sol | 344 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 344 insertions(+) diff --git a/test/PDPVerifier.t.sol b/test/PDPVerifier.t.sol index dd8dbc3..85ef6ff 100644 --- a/test/PDPVerifier.t.sol +++ b/test/PDPVerifier.t.sol @@ -1322,6 +1322,133 @@ contract SumTreeAddTest is Test { // Assert final proof set leaf count assertEq(pdpVerifier.getProofSetLeafCount(testSetId), 820, "Incorrect final proof set leaf count"); } + function assertSumTreeInvariant(uint256 setId) internal view { + uint256 nextRootId = pdpVerifier.getNextRootId(setId); + for (uint256 index = 0; index < nextRootId; index++) { + uint256 height = pdpVerifier.getTestHeightFromIndex(index); + uint256 range = 1 << height; + + if (index + 1 < range) { + continue; // skip to avoid underflow + } + + uint256 expectedSum = 0; + for (uint256 j = index + 1 - range; j <= index; j++) { + expectedSum += pdpVerifier.getRootLeafCount(setId, j); + } + + uint256 actualSum = pdpVerifier.getSumTreeCounts(setId, index); + assertEq( + actualSum, + expectedSum, + string(abi.encodePacked("SumTree invariant failed at index ", vm.toString(index))) + ); + } +} + +function testSumTreeInvariantAfterAddsAndRemovals() public { + uint256[] memory counts = new uint256[](6); + counts[0] = 10; + counts[1] = 20; + counts[2] = 5; + counts[3] = 15; + counts[4] = 25; + counts[5] = 30; + + PDPVerifier.RootData[] memory rootDataArray = new PDPVerifier.RootData[](counts.length); + for (uint256 i = 0; i < counts.length; i++) { + Cids.Cid memory testCid = Cids.Cid(abi.encodePacked("test", i)); + rootDataArray[i] = PDPVerifier.RootData( + testCid, + counts[i] * pdpVerifier.LEAF_SIZE() + ); + } + pdpVerifier.addRoots(testSetId, rootDataArray, empty); + + uint256[] memory toRemove = new uint256[](2); + + toRemove[0] = 1; + toRemove[1] = 4; + pdpVerifier.scheduleRemovals(testSetId, toRemove, empty); + + pdpVerifier.nextProvingPeriod( + testSetId, + block.number + challengeFinalityDelay, + empty + ); + + assertSumTreeInvariant(testSetId); +} +function testAdvancedSumTreeInvariant() public { + uint256 numRoots = 16; + uint256[] memory counts = new uint256[](numRoots); + PDPVerifier.RootData[] memory rootDataArray = new PDPVerifier.RootData[](numRoots); + + // Round 1 Add roots with dynamic sizes + for (uint256 i = 0; i < numRoots; i++) { + uint256 size = ((i + 1) * 10) + (i % 3); // mild randomness + counts[i] = size; + Cids.Cid memory cid = Cids.Cid(abi.encodePacked("root", i)); + rootDataArray[i] = PDPVerifier.RootData(cid, size * pdpVerifier.LEAF_SIZE()); + } + + pdpVerifier.addRoots(testSetId, rootDataArray, empty); + pdpVerifier.nextProvingPeriod(testSetId, block.number + challengeFinalityDelay, empty); + + // Assert invariant after first round + assertSumTreeInvariant(testSetId); + + // Round 2 Remove a few roots and add new ones + uint256[] memory toRemove = new uint256[](4); // Initialize toRemove array + toRemove[0] = 2; + toRemove[1] = 5; + toRemove[2] = 7; + toRemove[3] = 12; + pdpVerifier.scheduleRemovals(testSetId, toRemove, empty); + + // Add 4 more roots to simulate continued updates + PDPVerifier.RootData[] memory round2Roots = new PDPVerifier.RootData[](4); // Initialize round2Roots array + for (uint256 i = 0; i < 4; i++) { + uint256 rootId = numRoots + i; + uint256 size = 50 + i * 11; + Cids.Cid memory cid = Cids.Cid(abi.encodePacked("newRoot", rootId)); + round2Roots[i] = PDPVerifier.RootData(cid, size * pdpVerifier.LEAF_SIZE()); + } + pdpVerifier.addRoots(testSetId, round2Roots, empty); + pdpVerifier.nextProvingPeriod(testSetId, block.number + 2 * challengeFinalityDelay, empty); + + // Final invariant check after multiple rounds + assertSumTreeInvariant(testSetId); + + // Additional sanity checks + bytes memory emptyBytes = new bytes(0); // Initialize emptyBytes + for (uint256 i = 0; i < toRemove.length; i++) { + assertEq(pdpVerifier.getRootLeafCount(testSetId, toRemove[i]), 0, "Leaf count should be zero for removed roots"); + assertEq(pdpVerifier.getRootCid(testSetId, toRemove[i]).data, emptyBytes, "CID should be cleared for removed root"); + } + + uint256 nextRootId = pdpVerifier.getNextRootId(testSetId); + assertGt(nextRootId, numRoots, "New roots not added properly"); + assertGt(pdpVerifier.getProofSetLeafCount(testSetId), 0, "Leaf count should be positive after add/remove"); +} +function testFuzzedSumTreeInvariant(uint256 seed) public { + vm.assume(seed > 100); + uint256 numRoots = 16; + PDPVerifier.RootData[] memory rootDataArray = new PDPVerifier.RootData[](numRoots); + + for (uint256 i = 0; i < numRoots; i++) { + uint256 dynamicSize = uint256(keccak256(abi.encode(seed, i))) % 100 + 1; + Cids.Cid memory cid = Cids.Cid(abi.encodePacked("fuzz", i)); + rootDataArray[i] = PDPVerifier.RootData(cid, dynamicSize * pdpVerifier.LEAF_SIZE()); + } + + pdpVerifier.addRoots(testSetId, rootDataArray, empty); + pdpVerifier.nextProvingPeriod(testSetId, block.number + challengeFinalityDelay, empty); + assertSumTreeInvariant(testSetId); +} + + + function testFindRootId() public { setUpTestingArray(); @@ -1804,4 +1931,221 @@ contract PDPVerifierMigrateTest is Test { vm.expectRevert("InvalidInitialization()"); UUPSUpgradeable(address(proxy)).upgradeToAndCall(address(newImplementation), migrationCall); } +} + +/// @title SumTreeEnhancedTest +/// @notice Advanced tests for SumTree data structure focusing on stress testing, performance, and edge cases +/// @dev Tagged with [ADVANCED] to distinguish from basic tests +contract SumTreeEnhancedTest is Test { + SumTreeInternalTestPDPVerifier pdpVerifier; + TestingRecordKeeperService listener; + uint256 testSetId; + uint256 challengeFinalityDelay = 100; + bytes empty = new bytes(0); + + function setUp() public { + PDPVerifier pdpVerifierImpl = new SumTreeInternalTestPDPVerifier(); + bytes memory initializeData = abi.encodeWithSelector( + PDPVerifier.initialize.selector, + challengeFinalityDelay + ); + MyERC1967Proxy proxy = new MyERC1967Proxy(address(pdpVerifierImpl), initializeData); + pdpVerifier = SumTreeInternalTestPDPVerifier(address(proxy)); + listener = new TestingRecordKeeperService(); + testSetId = pdpVerifier.createProofSet{value: PDPFees.sybilFee()}(address(listener), empty); + } + + /// @notice [ADVANCED] Tests SumTree invariants under heavy load with multiple rounds of adds and removes + function testSumTreeStressTest() public { + uint256 numRounds = 5; + uint256 rootsPerRound = 8; + + for (uint256 round = 0; round < numRounds; round++) { + // Add roots + PDPVerifier.RootData[] memory roots = new PDPVerifier.RootData[](rootsPerRound); + for (uint256 i = 0; i < rootsPerRound; i++) { + uint256 size = ((round + 1) * 100) + (i * 10) + (i % 3); + Cids.Cid memory cid = Cids.Cid(abi.encodePacked("stress", round, i)); + roots[i] = PDPVerifier.RootData(cid, size * pdpVerifier.LEAF_SIZE()); + } + pdpVerifier.addRoots(testSetId, roots, empty); + + // Remove some roots + uint256[] memory toRemove = new uint256[](rootsPerRound / 2); + for (uint256 i = 0; i < toRemove.length; i++) { + toRemove[i] = round * rootsPerRound + (i * 2); + } + pdpVerifier.scheduleRemovals(testSetId, toRemove, empty); + + // Process changes + pdpVerifier.nextProvingPeriod(testSetId, block.number + challengeFinalityDelay, empty); + + // Verify invariants + assertSumTreeInvariant(testSetId); + } + } + + /// @notice [ADVANCED] Tests SumTree behavior with edge case sizes and patterns + function testSumTreeEdgeCases() public { + // Test with very small sizes + testSumTreeWithValues([uint256(1), 2, 1]); + + // Test with very large sizes + testSumTreeWithValues([uint256(1000), 2000, 1000]); + + // Test with alternating patterns + testSumTreeWithPattern(10, true); // Alternating add/remove + testSumTreeWithPattern(10, false); // Sequential add/remove + } + + /// @notice [ADVANCED] Tests SumTree performance with large numbers of operations + function testSumTreePerformance() public { + uint256 numOperations = 100; + uint256 startGas = gasleft(); + + for (uint256 i = 0; i < numOperations; i++) { + // Add root + PDPVerifier.RootData[] memory root = new PDPVerifier.RootData[](1); + root[0] = PDPVerifier.RootData( + Cids.Cid(abi.encodePacked("perf", i)), + ((i + 1) * 10) * pdpVerifier.LEAF_SIZE() + ); + pdpVerifier.addRoots(testSetId, root, empty); + + // Remove every other root + if (i % 2 == 1) { + uint256[] memory toRemove = new uint256[](1); + toRemove[0] = i - 1; + pdpVerifier.scheduleRemovals(testSetId, toRemove, empty); + } + + pdpVerifier.nextProvingPeriod(testSetId, block.number + challengeFinalityDelay, empty); + } + + uint256 gasUsed = startGas - gasleft(); + console.log("Gas used for %d operations: %d", numOperations, gasUsed); + } + + /// @notice [ADVANCED] Tests SumTree with randomized operations + function testSumTreeRandomized(uint256 seed) public { + vm.assume(seed > 0); + uint256 numOperations = 20; + bool hasLeaves = false; + + for (uint256 i = 0; i < numOperations; i++) { + uint256 operation = uint256(keccak256(abi.encode(seed, i))) % 3; + + if (operation == 0) { + // Add root + PDPVerifier.RootData[] memory root = new PDPVerifier.RootData[](1); + uint256 size = uint256(keccak256(abi.encode(seed, i, "size"))) % 100 + 1; + root[0] = PDPVerifier.RootData( + Cids.Cid(abi.encodePacked("rand", i)), + size * pdpVerifier.LEAF_SIZE() + ); + pdpVerifier.addRoots(testSetId, root, empty); + hasLeaves = true; + } else if (operation == 1) { + // Remove root + uint256 nextRootId = pdpVerifier.getNextRootId(testSetId); + if (nextRootId > 0) { + uint256[] memory toRemove = new uint256[](1); + toRemove[0] = uint256(keccak256(abi.encode(seed, i, "remove"))) % nextRootId; + pdpVerifier.scheduleRemovals(testSetId, toRemove, empty); + } + } + + // Only call nextProvingPeriod if we have leaves + if (hasLeaves && + pdpVerifier.getScheduledRemovals(testSetId).length == 0 && + pdpVerifier.getProofSetLeafCount(testSetId) > 0) { + pdpVerifier.nextProvingPeriod(testSetId, block.number + challengeFinalityDelay, empty); + assertSumTreeInvariant(testSetId); + } + } + } + + // Helper functions + function testSumTreeWithSizes(uint256[] memory sizes, uint256[3] memory values) internal { + require(sizes.length == 3, "Sizes array must have length 3"); + PDPVerifier.RootData[] memory roots = new PDPVerifier.RootData[](3); + for (uint256 i = 0; i < 3; i++) { + Cids.Cid memory cid = Cids.Cid(abi.encodePacked("edge", i)); + roots[i] = PDPVerifier.RootData(cid, sizes[i] * values[i] * pdpVerifier.LEAF_SIZE()); + } + pdpVerifier.addRoots(testSetId, roots, empty); + pdpVerifier.nextProvingPeriod(testSetId, block.number + challengeFinalityDelay, empty); + assertSumTreeInvariant(testSetId); + } + + function testSumTreeWithPattern(uint256 numOperations, bool alternating) internal { + for (uint256 i = 0; i < numOperations; i++) { + if (alternating) { + // Add and remove in alternating pattern + if (i % 2 == 0) { + PDPVerifier.RootData[] memory root = new PDPVerifier.RootData[](1); + root[0] = PDPVerifier.RootData( + Cids.Cid(abi.encodePacked("alt", i)), + ((i + 1) * 10) * pdpVerifier.LEAF_SIZE() + ); + pdpVerifier.addRoots(testSetId, root, empty); + } else { + uint256[] memory toRemove = new uint256[](1); + toRemove[0] = i - 1; + pdpVerifier.scheduleRemovals(testSetId, toRemove, empty); + } + } else { + // Add sequentially then remove sequentially + if (i < numOperations / 2) { + PDPVerifier.RootData[] memory root = new PDPVerifier.RootData[](1); + root[0] = PDPVerifier.RootData( + Cids.Cid(abi.encodePacked("seq", i)), + ((i + 1) * 10) * pdpVerifier.LEAF_SIZE() + ); + pdpVerifier.addRoots(testSetId, root, empty); + } else { + uint256[] memory toRemove = new uint256[](1); + toRemove[0] = i - (numOperations / 2); + pdpVerifier.scheduleRemovals(testSetId, toRemove, empty); + } + } + pdpVerifier.nextProvingPeriod(testSetId, block.number + challengeFinalityDelay, empty); + assertSumTreeInvariant(testSetId); + } + } + + function assertSumTreeInvariant(uint256 setId) internal view { + uint256 nextRootId = pdpVerifier.getNextRootId(setId); + for (uint256 index = 0; index < nextRootId; index++) { + uint256 height = pdpVerifier.getTestHeightFromIndex(index); + uint256 range = 1 << height; + + if (index + 1 < range) { + continue; // skip to avoid underflow + } + + uint256 expectedSum = 0; + for (uint256 j = index + 1 - range; j <= index; j++) { + expectedSum += pdpVerifier.getRootLeafCount(setId, j); + } + + uint256 actualSum = pdpVerifier.getSumTreeCounts(setId, index); + assertEq( + actualSum, + expectedSum, + string(abi.encodePacked("SumTree invariant failed at index ", vm.toString(index))) + ); + } + } + + function testSumTreeWithValues(uint256[3] memory values) internal { + PDPVerifier.RootData[] memory roots = new PDPVerifier.RootData[](3); + for (uint256 i = 0; i < 3; i++) { + Cids.Cid memory cid = Cids.Cid(abi.encodePacked("edge", i)); + roots[i] = PDPVerifier.RootData(cid, values[i] * pdpVerifier.LEAF_SIZE()); + } + pdpVerifier.addRoots(testSetId, roots, empty); + pdpVerifier.nextProvingPeriod(testSetId, block.number + challengeFinalityDelay, empty); + assertSumTreeInvariant(testSetId); + } } \ No newline at end of file From d75d2de9f60deaba796c7c61397fef6c02152817 Mon Sep 17 00:00:00 2001 From: Hany Date: Thu, 4 Dec 2025 17:56:46 +0300 Subject: [PATCH 2/2] Enhance SumTree invariant tests with pattern coverage --- test/PDPVerifier.t.sol | 83 +++++++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 17 deletions(-) diff --git a/test/PDPVerifier.t.sol b/test/PDPVerifier.t.sol index 7a06542..dc29ce5 100644 --- a/test/PDPVerifier.t.sol +++ b/test/PDPVerifier.t.sol @@ -2199,16 +2199,54 @@ contract SumTreeEnhancedTest is MockFVMTest, PieceHelper { /// @notice [ADVANCED] Tests SumTree behavior with edge case sizes and patterns function testSumTreeEdgeCases() public { - // Test with very small sizes testSumTreeWithValues([uint256(1), 2, 1]); - - // Test with very large sizes testSumTreeWithValues([uint256(1000), 2000, 1000]); - - // Verify invariant holds after adding pieces with different sizes assertSumTreeInvariant(testSetId); } + /// @notice [ADVANCED] Tests SumTree with alternating add/remove pattern + function testSumTreeAlternatingPattern() public { + uint256 setId = createFreshDataSet(); + uint256 numOperations = 10; + + for (uint256 i = 0; i < numOperations; i++) { + Cids.Cid[] memory piece = new Cids.Cid[](1); + piece[0] = makeSamplePiece((i + 1) * 10); + pdpVerifier.addPieces(setId, address(0), piece, empty); + + if (i > 0 && i % 2 == 1) { + uint256[] memory toRemove = new uint256[](1); + toRemove[0] = i - 1; + pdpVerifier.schedulePieceDeletions(setId, toRemove, empty); + } + + pdpVerifier.nextProvingPeriod(setId, block.number + CHALLENGE_FINALITY_DELAY, empty); + assertSumTreeInvariant(setId); + } + } + + /// @notice [ADVANCED] Tests SumTree with sequential add-then-remove pattern + function testSumTreeSequentialPattern() public { + uint256 setId = createFreshDataSet(); + uint256 numPieces = 8; + + Cids.Cid[] memory pieces = new Cids.Cid[](numPieces); + for (uint256 i = 0; i < numPieces; i++) { + pieces[i] = makeSamplePiece((i + 1) * 15); + } + pdpVerifier.addPieces(setId, address(0), pieces, empty); + pdpVerifier.nextProvingPeriod(setId, block.number + CHALLENGE_FINALITY_DELAY, empty); + assertSumTreeInvariant(setId); + + for (uint256 i = 0; i < numPieces / 2; i++) { + uint256[] memory toRemove = new uint256[](1); + toRemove[0] = i; + pdpVerifier.schedulePieceDeletions(setId, toRemove, empty); + pdpVerifier.nextProvingPeriod(setId, block.number + CHALLENGE_FINALITY_DELAY, empty); + assertSumTreeInvariant(setId); + } + } + /// @notice [ADVANCED] Tests SumTree performance with large numbers of operations function testSumTreePerformance() public { uint256 numOperations = 100; @@ -2237,32 +2275,43 @@ contract SumTreeEnhancedTest is MockFVMTest, PieceHelper { /// @notice [ADVANCED] Tests SumTree with randomized operations function testSumTreeRandomized(uint256 seed) public { vm.assume(seed > 0); + uint256 setId = createFreshDataSet(); uint256 numOperations = 20; - uint256 piecesAdded = 0; + uint256 nextPieceToAdd = 0; + uint256 nextPieceToRemove = 0; + uint256 livePieces = 0; for (uint256 i = 0; i < numOperations; i++) { uint256 operation = uint256(keccak256(abi.encode(seed, i))) % 3; - if (operation == 0 || piecesAdded == 0) { - // Add piece (always add if no pieces yet) + if (operation == 0 || livePieces == 0) { Cids.Cid[] memory piece = new Cids.Cid[](1); uint256 size = uint256(keccak256(abi.encode(seed, i, "size"))) % 100 + 1; piece[0] = makeSamplePiece(size); - pdpVerifier.addPieces(testSetId, address(0), piece, empty); - piecesAdded++; + pdpVerifier.addPieces(setId, address(0), piece, empty); + nextPieceToAdd++; + livePieces++; + } else if (operation == 1 && nextPieceToRemove < nextPieceToAdd && livePieces > 1) { + uint256[] memory toRemove = new uint256[](1); + toRemove[0] = nextPieceToRemove; + pdpVerifier.schedulePieceDeletions(setId, toRemove, empty); + nextPieceToRemove++; + livePieces--; } - // Skip removal operations - they're tested in other tests - // and randomized removal is hard to get right without tracking state - // Periodically call nextProvingPeriod and verify invariant - if (i % 4 == 3 && pdpVerifier.getDataSetLeafCount(testSetId) > 0) { - pdpVerifier.nextProvingPeriod(testSetId, block.number + CHALLENGE_FINALITY_DELAY, empty); - assertSumTreeInvariant(testSetId); + if (i % 3 == 2 && pdpVerifier.getDataSetLeafCount(setId) > 0) { + pdpVerifier.nextProvingPeriod(setId, block.number + CHALLENGE_FINALITY_DELAY, empty); + assertSumTreeInvariant(setId); } } } - // Helper functions + function createFreshDataSet() internal returns (uint256) { + return pdpVerifier.addPieces{value: PDPFees.sybilFee()}( + NEW_DATA_SET_SENTINEL, address(listener), new Cids.Cid[](0), abi.encode(empty, empty) + ); + } + function assertSumTreeInvariant(uint256 setId) internal view { uint256 nextPieceId = pdpVerifier.getNextPieceId(setId); for (uint256 index = 0; index < nextPieceId; index++) {