diff --git a/.gas-snapshot b/.gas-snapshot index 4629d29..a67299a 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1 +1,13 @@ -WakuRlnV2Test:test__ValidRegistration() (gas: 108661) \ No newline at end of file +WakuRlnV2Test:test__IdCommitmentToMetadata__DoesntExist() (gas: 8299) +WakuRlnV2Test:test__InvalidPaginationQuery__EndIndexGTIdCommitmentIndex() (gas: 13351) +WakuRlnV2Test:test__InvalidPaginationQuery__StartIndexGTEndIndex() (gas: 11184) +WakuRlnV2Test:test__InvalidRegistration__DuplicateIdCommitment() (gas: 111313) +WakuRlnV2Test:test__InvalidRegistration__FullTree() (gas: 97043) +WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__LargerThanField() (gas: 9926) +WakuRlnV2Test:test__InvalidRegistration__InvalidIdCommitment__Zero() (gas: 9139) +WakuRlnV2Test:test__InvalidRegistration__InvalidUserMessageLimit__LargerThanMax() (gas: 10147) +WakuRlnV2Test:test__InvalidRegistration__InvalidUserMessageLimit__Zero() (gas: 9242) +WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1002, μ: 403497, ~: 136269) +WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 128461) +WakuRlnV2Test:test__ValidRegistration(uint256,uint32) (runs: 1001, μ: 133518, ~: 133518) +WakuRlnV2Test:test__ValidRegistration__kats() (gas: 108902) \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 1c87703..1df6815 100644 --- a/foundry.toml +++ b/foundry.toml @@ -17,6 +17,9 @@ src = "src" test = "test" +[fuzz] +max_test_rejects = 128_000 + [profile.ci] fuzz = { runs = 10_000 } verbosity = 4 diff --git a/src/WakuRlnV2.sol b/src/WakuRlnV2.sol index ad03200..ba9a3d8 100644 --- a/src/WakuRlnV2.sol +++ b/src/WakuRlnV2.sol @@ -37,6 +37,7 @@ contract WakuRlnV2 { /// @notice The index of the next member to be registered uint32 public idCommitmentIndex = 0; + /// @notice the membership metadata of the member struct MembershipInfo { /// @notice the user message limit of each member uint32 userMessageLimit; @@ -59,17 +60,21 @@ contract WakuRlnV2 { /// @param index The index of the member in the set event MemberRegistered(uint256 idCommitment, uint32 userMessageLimit, uint32 index); + /// @notice the modifier to check if the idCommitment is valid + /// @param idCommitment The idCommitment of the member modifier onlyValidIdCommitment(uint256 idCommitment) { if (!isValidCommitment(idCommitment)) revert InvalidIdCommitment(idCommitment); _; } - modifier onlyValidUserMessageLimit(uint32 messageLimit) { - if (messageLimit > MAX_MESSAGE_LIMIT) revert InvalidUserMessageLimit(messageLimit); - if (messageLimit == 0) revert InvalidUserMessageLimit(messageLimit); + /// @notice the modifier to check if the userMessageLimit is valid + /// @param userMessageLimit The user message limit + modifier onlyValidUserMessageLimit(uint32 userMessageLimit) { + if (!isValidUserMessageLimit(userMessageLimit)) revert InvalidUserMessageLimit(userMessageLimit); _; } + /// @notice the constructor of the contract constructor(uint32 maxMessageLimit) { MAX_MESSAGE_LIMIT = maxMessageLimit; SET_SIZE = uint32(1 << DEPTH); @@ -77,9 +82,45 @@ contract WakuRlnV2 { LazyIMT.init(imtData, DEPTH); } - function memberExists(uint256 idCommitment) public view returns (bool) { + /// @notice Checks if a commitment is valid + /// @param idCommitment The idCommitment of the member + /// @return true if the commitment is valid, false otherwise + function isValidCommitment(uint256 idCommitment) public pure returns (bool) { + return idCommitment != 0 && idCommitment < Q; + } + + /// @notice Checks if a user message limit is valid + /// @param userMessageLimit The user message limit + /// @return true if the user message limit is valid, false otherwise + function isValidUserMessageLimit(uint32 userMessageLimit) public view returns (bool) { + return userMessageLimit > 0 && userMessageLimit <= MAX_MESSAGE_LIMIT; + } + + /// @notice Returns the rateCommitment of a member + /// @param index The index of the member + /// @return The rateCommitment of the member + function indexToCommitment(uint32 index) internal view returns (uint256) { + return imtData.elements[LazyIMT.indexForElement(0, index)]; + } + + /// @notice Returns the metadata of a member + /// @param idCommitment The idCommitment of the member + /// @return The metadata of the member (userMessageLimit, index, rateCommitment) + function idCommitmentToMetadata(uint256 idCommitment) public view returns (uint32, uint32, uint256) { MembershipInfo memory member = memberInfo[idCommitment]; - return member.userMessageLimit > 0 && member.index >= 0; + // we cannot call indexToCommitment for 0 index if the member doesn't exist + if (member.userMessageLimit == 0) { + return (0, 0, 0); + } + return (member.userMessageLimit, member.index, indexToCommitment(member.index)); + } + + /// @notice Checks if a member exists + /// @param idCommitment The idCommitment of the member + /// @return true if the member exists, false otherwise + function memberExists(uint256 idCommitment) public view returns (bool) { + (,, uint256 rateCommitment) = idCommitmentToMetadata(idCommitment); + return rateCommitment != 0; } /// Allows a user to register as a member @@ -112,29 +153,30 @@ contract WakuRlnV2 { idCommitmentIndex += 1; } - function isValidCommitment(uint256 idCommitment) public pure returns (bool) { - return idCommitment != 0 && idCommitment < Q; - } - - function indexToCommitment(uint32 index) public view returns (uint256) { - return imtData.elements[LazyIMT.indexForElement(0, index)]; - } - + /// @notice Returns the commitments of a range of members + /// @param startIndex The start index of the range + /// @param endIndex The end index of the range + /// @return The commitments of the members function getCommitments(uint32 startIndex, uint32 endIndex) public view returns (uint256[] memory) { - if (startIndex >= endIndex) revert InvalidPaginationQuery(startIndex, endIndex); + if (startIndex > endIndex) revert InvalidPaginationQuery(startIndex, endIndex); if (endIndex > idCommitmentIndex) revert InvalidPaginationQuery(startIndex, endIndex); - uint256[] memory commitments = new uint256[](endIndex - startIndex); - for (uint32 i = startIndex; i < endIndex; i++) { + uint256[] memory commitments = new uint256[](endIndex - startIndex + 1); + for (uint32 i = startIndex; i <= endIndex; i++) { commitments[i - startIndex] = indexToCommitment(i); } return commitments; } + /// @notice Returns the root of the IMT + /// @return The root of the IMT function root() external view returns (uint256) { return LazyIMT.root(imtData, DEPTH); } + /// @notice Returns the merkle proof elements of a given membership + /// @param index The index of the member + /// @return The merkle proof elements of the member function merkleProofElements(uint40 index) public view returns (uint256[] memory) { return LazyIMT.merkleProofElements(imtData, index, DEPTH); } diff --git a/test/WakuRlnV2.t.sol b/test/WakuRlnV2.t.sol index 4d86250..280ed01 100644 --- a/test/WakuRlnV2.t.sol +++ b/test/WakuRlnV2.t.sol @@ -1,15 +1,18 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.19 <0.9.0; -import { Test, console } from "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/Test.sol"; import { Deploy } from "../script/Deploy.s.sol"; import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; -import { WakuRlnV2 } from "../src/WakuRlnV2.sol"; +import "../src/WakuRlnV2.sol"; import { PoseidonT3 } from "poseidon-solidity/PoseidonT3.sol"; import { LazyIMT } from "@zk-kit/imt.sol/LazyIMT.sol"; contract WakuRlnV2Test is Test { + using stdStorage for StdStorage; + WakuRlnV2 internal w; DeploymentConfig internal deploymentConfig; @@ -20,7 +23,7 @@ contract WakuRlnV2Test is Test { (w, deploymentConfig) = deployment.run(); } - function test__ValidRegistration() external { + function test__ValidRegistration__kats() external { vm.pauseGasMetering(); uint256 idCommitment = 2; uint32 userMessageLimit = 2; @@ -35,14 +38,119 @@ contract WakuRlnV2Test is Test { // kats from zerokit uint256 rateCommitment = 4_699_387_056_273_519_054_140_667_386_511_343_037_709_699_938_246_587_880_795_929_666_834_307_503_001; - assertEq(w.indexToCommitment(0), rateCommitment); - uint256[] memory commitments = w.getCommitments(0, 1); - assertEq(commitments.length, 1); - assertEq(commitments[index], rateCommitment); assertEq( w.root(), 13_801_897_483_540_040_307_162_267_952_866_411_686_127_372_014_953_358_983_481_592_640_000_001_877_295 ); + (uint32 fetchedUserMessageLimit2, uint32 index2, uint256 rateCommitment2) = + w.idCommitmentToMetadata(idCommitment); + assertEq(fetchedUserMessageLimit2, userMessageLimit); + assertEq(index2, 0); + assertEq(rateCommitment2, rateCommitment); vm.resumeGasMetering(); } + + function test__ValidRegistration(uint256 idCommitment, uint32 userMessageLimit) external { + vm.assume(w.isValidCommitment(idCommitment) && w.isValidUserMessageLimit(userMessageLimit)); + + assertEq(w.memberExists(idCommitment), false); + w.register(idCommitment, userMessageLimit); + uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); + + (uint32 fetchedUserMessageLimit, uint32 index, uint256 fetchedRateCommitment) = + w.idCommitmentToMetadata(idCommitment); + assertEq(fetchedUserMessageLimit, userMessageLimit); + assertEq(index, 0); + assertEq(fetchedRateCommitment, rateCommitment); + } + + function test__IdCommitmentToMetadata__DoesntExist() external { + uint256 idCommitment = 2; + (uint32 userMessageLimit, uint32 index, uint256 rateCommitment) = w.idCommitmentToMetadata(idCommitment); + assertEq(userMessageLimit, 0); + assertEq(index, 0); + assertEq(rateCommitment, 0); + } + + function test__InvalidRegistration__InvalidIdCommitment__Zero() external { + uint256 idCommitment = 0; + uint32 userMessageLimit = 2; + vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, 0)); + w.register(idCommitment, userMessageLimit); + } + + function test__InvalidRegistration__InvalidIdCommitment__LargerThanField() external { + uint256 idCommitment = w.Q() + 1; + uint32 userMessageLimit = 2; + vm.expectRevert(abi.encodeWithSelector(InvalidIdCommitment.selector, idCommitment)); + w.register(idCommitment, userMessageLimit); + } + + function test__InvalidRegistration__InvalidUserMessageLimit__Zero() external { + uint256 idCommitment = 2; + uint32 userMessageLimit = 0; + vm.expectRevert(abi.encodeWithSelector(InvalidUserMessageLimit.selector, 0)); + w.register(idCommitment, userMessageLimit); + } + + function test__InvalidRegistration__InvalidUserMessageLimit__LargerThanMax() external { + uint256 idCommitment = 2; + uint32 userMessageLimit = w.MAX_MESSAGE_LIMIT() + 1; + vm.expectRevert(abi.encodeWithSelector(InvalidUserMessageLimit.selector, userMessageLimit)); + w.register(idCommitment, userMessageLimit); + } + + function test__InvalidRegistration__DuplicateIdCommitment() external { + uint256 idCommitment = 2; + uint32 userMessageLimit = 2; + w.register(idCommitment, userMessageLimit); + vm.expectRevert(DuplicateIdCommitment.selector); + w.register(idCommitment, userMessageLimit); + } + + function test__InvalidRegistration__FullTree() external { + uint32 userMessageLimit = 2; + // we progress the tree to the last leaf + stdstore.target(address(w)).sig("idCommitmentIndex()").checked_write(1 << w.DEPTH()); + vm.expectRevert(FullTree.selector); + w.register(1, userMessageLimit); + } + + function test__InvalidPaginationQuery__StartIndexGTEndIndex() external { + vm.expectRevert(abi.encodeWithSelector(InvalidPaginationQuery.selector, 1, 0)); + w.getCommitments(1, 0); + } + + function test__InvalidPaginationQuery__EndIndexGTIdCommitmentIndex() external { + vm.expectRevert(abi.encodeWithSelector(InvalidPaginationQuery.selector, 0, 2)); + w.getCommitments(0, 2); + } + + function test__ValidPaginationQuery__OneElement() external { + uint32 userMessageLimit = 2; + uint256 idCommitment = 1; + w.register(idCommitment, userMessageLimit); + uint256[] memory commitments = w.getCommitments(0, 0); + assertEq(commitments.length, 1); + uint256 rateCommitment = PoseidonT3.hash([idCommitment, userMessageLimit]); + assertEq(commitments[0], rateCommitment); + } + + function test__ValidPaginationQuery(uint32 idCommitmentsLength) external { + vm.assume(idCommitmentsLength > 0 && idCommitmentsLength <= 100); + uint32 userMessageLimit = 2; + + vm.pauseGasMetering(); + for (uint256 i = 0; i < idCommitmentsLength; i++) { + w.register(i + 1, userMessageLimit); + } + vm.resumeGasMetering(); + + uint256[] memory commitments = w.getCommitments(0, idCommitmentsLength); + assertEq(commitments.length, idCommitmentsLength + 1); + for (uint256 i = 0; i < idCommitmentsLength; i++) { + uint256 rateCommitment = PoseidonT3.hash([i + 1, userMessageLimit]); + assertEq(commitments[i], rateCommitment); + } + } }