From ee07450b65f0d68ebc347b6f3b8fd6e0e36ab2e6 Mon Sep 17 00:00:00 2001 From: stubbsta Date: Mon, 24 Nov 2025 11:29:36 +0200 Subject: [PATCH] Add root cache tests and emit root storage event --- src/WakuRlnV2.sol | 7 +++ test/WakuRlnV2.t.sol | 132 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/src/WakuRlnV2.sol b/src/WakuRlnV2.sol index 4b73a7e..f27c557 100644 --- a/src/WakuRlnV2.sol +++ b/src/WakuRlnV2.sol @@ -37,6 +37,10 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member /// @notice The Merkle tree that stores rate commitments of memberships LazyIMTData public merkleTree; + /// @notice Emitted whenever a new Merkle tree root is stored + /// @param newRoot The newly stored Merkle tree root + event RootStored(uint256 newRoot); + /// @notice Сheck if the idCommitment is valid /// @param idCommitment The idCommitment of the membership modifier onlyValidIdCommitment(uint256 idCommitment) { @@ -59,6 +63,7 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member // Fixed-size circular buffer for recent roots uint8 public constant HISTORY_SIZE = 5; + /// @notice Fixed-size circular buffer storing the most recent HISTORY_SIZE roots /// @dev Organized as a ring buffer where rootIndex points to the next write position uint256[HISTORY_SIZE] private recentRoots; @@ -203,6 +208,8 @@ contract WakuRlnV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable, Member recentRoots[rootIndex] = newRoot; rootIndex = (rootIndex + 1) % HISTORY_SIZE; } + + emit RootStored(newRoot); } /// @notice Returns the list of recent roots, newest first diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index d482f1d..ac4b80e 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -1279,6 +1279,24 @@ contract WakuRlnV2Test is Test { assertEq(token.balanceOf(address(w)), newPrice); } + function test__RootStoredEvent_OnRegister() external { + uint32 rateLimit = w.minMembershipRateLimit(); + (, uint256 price) = w.priceCalculator().calculate(rateLimit); + + token.approve(address(w), price); + uint256 idCommitment = 42; + + // First: expect RootStored (emitted from _upsertInTree via _storeNewRoot) + vm.expectEmit(false, false, false, false); // only match event signature + emit WakuRlnV2.RootStored(0); // value ignored because all data=false + + // Then: expect MembershipRegistered (emitted afterward) + vm.expectEmit(true, true, false, false); // check idCommitment + rateLimit + emit MembershipUpgradeable.MembershipRegistered(idCommitment, rateLimit, 0); + + w.register(idCommitment, rateLimit, noIdCommitmentsToErase); + } + function test__ZeroPriceEdgeCase() external { MockPriceCalculator zeroPriceCalc = new MockPriceCalculator(address(token), 0); @@ -1699,4 +1717,118 @@ contract WakuRlnV2Test is Test { assertEq(recent[4], r1); } } + + function test__RecentRoots_BatchFullEraseNoWrap() external { + uint256 initialRoot = w.root(); + uint32 rate = w.minMembershipRateLimit(); + (, uint256 price) = w.priceCalculator().calculate(rate); + + // Register 2 memberships to avoid wrap-around (2 registers + 2 erases = 4 < 5) + uint256 root1; + token.approve(address(w), price); + w.register(1, rate, noIdCommitmentsToErase); + root1 = w.root(); + + uint256 root2; + token.approve(address(w), price); + w.register(2, rate, noIdCommitmentsToErase); + root2 = w.root(); + + // Warp to expire all (assuming same registration time, all expire together) + (,, uint256 graceStart,,,,,) = w.memberships(1); + vm.warp(graceStart + w.gracePeriodDurationForNewMemberships() + 1); + + // Prepare ids to erase + uint256[] memory ids = new uint256[](2); + ids[0] = 1; + ids[1] = 2; + + // Full erase all 2 in one tx + w.eraseMemberships(ids, true); + + // After erases, roots added for each update + uint256 finalRoot = w.root(); // Final root after all erases (should be initial empty tree root) + assertEq(finalRoot, initialRoot); + + // History should have the 2 new roots from erases + previous 2, with one 0 + uint256[HISTORY] memory recent = w.getRecentRoots(); + assertEq(recent[0], finalRoot); // Newest: after last erase (initialRoot) + assertEq(recent[2], root1); // After second register + assertEq(recent[3], 0); // After first register + assertEq(recent[4], 0); // Unused slot + // recent[1] = after first erase (intermediate, non-zero, different) + assertNotEq(recent[1], 0); + assertNotEq(recent[1], recent[0]); + assertNotEq(recent[1], recent[2]); + } + + function test__RecentRoots_BatchFullEraseWithWrap() external { + uint256 initialRoot = w.root(); + uint32 rate = w.minMembershipRateLimit(); + (, uint256 price) = w.priceCalculator().calculate(rate); + + // Register 5 memberships to fill history + for (uint256 i = 1; i <= 5; i++) { + token.approve(address(w), price); + w.register(i, rate, noIdCommitmentsToErase); + } + uint256 root5 = w.root(); + + // Warp to expire all + (,, uint256 graceStart,,,,,) = w.memberships(1); + vm.warp(graceStart + w.gracePeriodDurationForNewMemberships() + 1); + + // Erase ids one at a time to update root each time + uint256[] memory ids = new uint256[](1); + for (uint256 i = 0; i < 5; i++) { + ids[0] = i + 1; + w.eraseMemberships(ids, true); + } + + uint256 finalRoot = w.root(); // Should be initial empty tree root + assertEq(finalRoot, initialRoot); + + // History should now contain the 5 new roots from the erases, newest first + uint256[HISTORY] memory recent = w.getRecentRoots(); + assertEq(recent[0], finalRoot); + + // Assert no old roots remain (e.g., root5 not in history) + for (uint8 i = 0; i < HISTORY; i++) { + assertNotEq(recent[i], root5); + assertNotEq(recent[i], 0); + } + + // Check getRootAt + assertEq(w.getRootAt(0), finalRoot); + } + + function test__RecentRoots_UpgradePreservesHistory() external { + uint32 rate = w.minMembershipRateLimit(); + (, uint256 price) = w.priceCalculator().calculate(rate); + + // Fill history with 6 registers to have wrap + for (uint256 i = 1; i <= 6; i++) { + token.approve(address(w), price); + w.register(i, rate, noIdCommitmentsToErase); + } + uint256[HISTORY] memory preUpgradeRecent = w.getRecentRoots(); + + // Deploy new implementation (assuming same contract for simplicity, or a new version) + address newImpl = address(new WakuRlnV2()); + + // Upgrade as owner + vm.prank(w.owner()); + w.upgradeTo(newImpl); + + // Check history unchanged + uint256[HISTORY] memory postUpgradeRecent = w.getRecentRoots(); + for (uint8 i = 0; i < HISTORY; i++) { + assertEq(postUpgradeRecent[i], preUpgradeRecent[i]); + } + + // Further operations work + token.approve(address(w), price); + w.register(7, rate, noIdCommitmentsToErase); + assertNotEq(w.getRootAt(0), preUpgradeRecent[0]); + } }