diff --git a/contracts/base/SemaphoreGroups.sol b/contracts/base/SemaphoreGroups.sol index 61bad826d..df9154477 100644 --- a/contracts/base/SemaphoreGroups.sol +++ b/contracts/base/SemaphoreGroups.sol @@ -48,8 +48,9 @@ abstract contract SemaphoreGroups is Context, ISemaphoreGroups { groups[groupId].insert(identityCommitment); uint256 merkleTreeRoot = getMerkleTreeRoot(groupId); + uint256 index = getNumberOfMerkleTreeLeaves(groupId) - 1; - emit MemberAdded(groupId, identityCommitment, merkleTreeRoot); + emit MemberAdded(groupId, index, identityCommitment, merkleTreeRoot); } /// @dev Updates an identity commitment of an existing group. A proof of membership is @@ -73,8 +74,9 @@ abstract contract SemaphoreGroups is Context, ISemaphoreGroups { groups[groupId].update(identityCommitment, newIdentityCommitment, proofSiblings, proofPathIndices); uint256 merkleTreeRoot = getMerkleTreeRoot(groupId); + uint256 index = proofPathIndicesToMemberIndex(proofPathIndices); - emit MemberUpdated(groupId, identityCommitment, newIdentityCommitment, merkleTreeRoot); + emit MemberUpdated(groupId, index, identityCommitment, newIdentityCommitment, merkleTreeRoot); } /// @dev Removes an identity commitment from an existing group. A proof of membership is @@ -96,8 +98,9 @@ abstract contract SemaphoreGroups is Context, ISemaphoreGroups { groups[groupId].remove(identityCommitment, proofSiblings, proofPathIndices); uint256 merkleTreeRoot = getMerkleTreeRoot(groupId); + uint256 index = proofPathIndicesToMemberIndex(proofPathIndices); - emit MemberRemoved(groupId, identityCommitment, merkleTreeRoot); + emit MemberRemoved(groupId, index, identityCommitment, merkleTreeRoot); } /// @dev See {ISemaphoreGroups-getMerkleTreeRoot}. @@ -114,4 +117,27 @@ abstract contract SemaphoreGroups is Context, ISemaphoreGroups { function getNumberOfMerkleTreeLeaves(uint256 groupId) public view virtual override returns (uint256) { return groups[groupId].numberOfLeaves; } + + /// @dev Converts the path indices of a Merkle proof to the identity commitment index in the tree. + /// @param proofPathIndices: Path of the proof of membership. + /// @return Index of a group member. + function proofPathIndicesToMemberIndex(uint8[] calldata proofPathIndices) private pure returns (uint256) { + uint256 memberIndex = 0; + + for (uint8 i = uint8(proofPathIndices.length); i > 0; ) { + if (memberIndex > 0 || proofPathIndices[i - 1] != 0) { + memberIndex *= 2; + + if (proofPathIndices[i - 1] == 1) { + memberIndex += 1; + } + } + + unchecked { + --i; + } + } + + return memberIndex; + } } diff --git a/contracts/interfaces/ISemaphoreGroups.sol b/contracts/interfaces/ISemaphoreGroups.sol index 346358fcb..797d4e5a0 100644 --- a/contracts/interfaces/ISemaphoreGroups.sol +++ b/contracts/interfaces/ISemaphoreGroups.sol @@ -16,17 +16,20 @@ interface ISemaphoreGroups { /// @dev Emitted when a new identity commitment is added. /// @param groupId: Group id of the group. + /// @param index: Identity commitment index. /// @param identityCommitment: New identity commitment. /// @param merkleTreeRoot: New root hash of the tree. - event MemberAdded(uint256 indexed groupId, uint256 identityCommitment, uint256 merkleTreeRoot); + event MemberAdded(uint256 indexed groupId, uint256 index, uint256 identityCommitment, uint256 merkleTreeRoot); /// @dev Emitted when an identity commitment is updated. /// @param groupId: Group id of the group. - /// @param identityCommitment: New identity commitment. + /// @param index: Identity commitment index. + /// @param identityCommitment: Existing identity commitment to be updated. /// @param newIdentityCommitment: New identity commitment. /// @param merkleTreeRoot: New root hash of the tree. event MemberUpdated( uint256 indexed groupId, + uint256 index, uint256 identityCommitment, uint256 newIdentityCommitment, uint256 merkleTreeRoot @@ -34,9 +37,10 @@ interface ISemaphoreGroups { /// @dev Emitted when a new identity commitment is removed. /// @param groupId: Group id of the group. + /// @param index: Identity commitment index. /// @param identityCommitment: Existing identity commitment to be removed. /// @param merkleTreeRoot: New root hash of the tree. - event MemberRemoved(uint256 indexed groupId, uint256 identityCommitment, uint256 merkleTreeRoot); + event MemberRemoved(uint256 indexed groupId, uint256 index, uint256 identityCommitment, uint256 merkleTreeRoot); /// @dev Returns the last root hash of a group. /// @param groupId: Id of the group. diff --git a/test/Semaphore.ts b/test/Semaphore.ts index 969dd1460..2fef9c47f 100644 --- a/test/Semaphore.ts +++ b/test/Semaphore.ts @@ -101,6 +101,7 @@ describe("Semaphore", () => { .to.emit(contract, "MemberAdded") .withArgs( groupId, + 0, members[0], "18951329906296061785889394467312334959162736293275411745101070722914184798221" ) @@ -125,7 +126,7 @@ describe("Semaphore", () => { const transaction = contract.addMembers(groupId, members) - await expect(transaction).to.emit(contract, "MemberAdded").withArgs(groupId, BigInt(3), group.root) + await expect(transaction).to.emit(contract, "MemberAdded").withArgs(groupId, 2, BigInt(3), group.root) }) }) @@ -152,7 +153,9 @@ describe("Semaphore", () => { const transaction = contract.updateMember(groupId, BigInt(1), BigInt(4), siblings, pathIndices) - await expect(transaction).to.emit(contract, "MemberUpdated").withArgs(groupId, BigInt(1), BigInt(4), root) + await expect(transaction) + .to.emit(contract, "MemberUpdated") + .withArgs(groupId, 0, BigInt(1), BigInt(4), root) }) }) @@ -170,16 +173,16 @@ describe("Semaphore", () => { group.addMembers(members) - group.removeMember(0) + group.removeMember(2) await contract["createGroup(uint256,uint256,uint256,address)"](groupId, treeDepth, 0, accounts[0]) await contract.addMembers(groupId, members) - const { siblings, pathIndices, root } = group.generateProofOfMembership(0) + const { siblings, pathIndices, root } = group.generateProofOfMembership(2) - const transaction = contract.removeMember(groupId, BigInt(1), siblings, pathIndices) + const transaction = contract.removeMember(groupId, BigInt(3), siblings, pathIndices) - await expect(transaction).to.emit(contract, "MemberRemoved").withArgs(groupId, BigInt(1), root) + await expect(transaction).to.emit(contract, "MemberRemoved").withArgs(groupId, 2, BigInt(3), root) }) }) diff --git a/test/SemaphoreVoting.ts b/test/SemaphoreVoting.ts index 73e40cfef..474ed7e95 100644 --- a/test/SemaphoreVoting.ts +++ b/test/SemaphoreVoting.ts @@ -120,6 +120,7 @@ describe("SemaphoreVoting", () => { .to.emit(contract, "MemberAdded") .withArgs( pollIds[1], + 0, identityCommitment, "14787813191318312920980352979830075893203307366494541177071234930769373297362" ) diff --git a/test/SemaphoreWhistleblowing.ts b/test/SemaphoreWhistleblowing.ts index b1bd1115a..7a2d0a5a2 100644 --- a/test/SemaphoreWhistleblowing.ts +++ b/test/SemaphoreWhistleblowing.ts @@ -85,6 +85,7 @@ describe("SemaphoreWhistleblowing", () => { .to.emit(contract, "MemberAdded") .withArgs( entityIds[0], + 0, identityCommitment, "14787813191318312920980352979830075893203307366494541177071234930769373297362" ) @@ -129,6 +130,7 @@ describe("SemaphoreWhistleblowing", () => { .to.emit(contract, "MemberRemoved") .withArgs( entityIds[0], + 0, identityCommitment, "15019797232609675441998260052101280400536945603062888308240081994073687793470" ) diff --git a/yarn.lock b/yarn.lock index 3528084d5..cfd9fda4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6483,7 +6483,7 @@ __metadata: dependencies: bn.js: ^4.11.8 ethereumjs-util: ^6.0.0 - checksum: ae074be0bb012857ab5d3ae644d1163b908a48dd724b7d2567cfde309dc72222d460438f2411936a70dc949dc604ce1ef7118f7273bd525815579143c907e336 + checksum: 03127d09960e5f8a44167463faf25b2894db2f746376dbb8195b789ed11762f93db9c574eaa7c498c400063508e9dfc1c80de2edf5f0e1406b25c87d860ff2f1 languageName: node linkType: hard