feat: create function to remove identity commitments

This commit is contained in:
cedoor
2021-11-08 16:28:56 +01:00
parent 02f57ed988
commit ed1634ef52
6 changed files with 2780 additions and 152 deletions

View File

@@ -17,13 +17,23 @@ contract Groups is OwnableUpgradeable {
/// @param provider: The provider of the group.
/// @param name: The name of the group.
/// @param identityCommitment: The new identity commitment.
/// @param index: The index of the identity commitment in the tree.
/// @param root: The new root hash of the tree.
event NewIdentityCommitment(
bytes32 indexed provider,
bytes32 indexed name,
uint256 identityCommitment,
uint256 index,
uint256 root
);
/// @dev Emitted when a new identity commitment is deleted.
/// @param provider: The provider of the group.
/// @param name: The name of the group.
/// @param identityCommitment: The new identity commitment.
/// @param root: The new root hash of the tree.
event DeleteIdentityCommitment(
bytes32 indexed provider,
bytes32 indexed name,
uint256 identityCommitment,
uint256 root
);
@@ -37,26 +47,29 @@ contract Groups is OwnableUpgradeable {
__Ownable_init();
}
/// @dev ...
/// @dev Creates a new group by initializing the associated Merkle tree.
/// @param provider: The provider of the group.
/// @param name: The name of the group.
/// @param depth: Depth of the tree.
function createOwnableGroup(
/// @param admin: Admin of the group.
function createGroup(
bytes32 provider,
bytes32 name,
uint8 depth,
address admin
) external {
createGroup(provider, name, depth);
require(admin != owner(), "Groups: group admin cannot be the contract owner");
) external onlyOwner {
bytes32 groupId = getGroupId(provider, name);
require(groups[groupId].depth == 0, "Groups: group already exists");
groups[groupId].init(depth, 0);
groupAdmins[groupId] = admin;
emit NewGroup(provider, name, depth);
}
/// @dev ...
/// @dev Batch function to add multiple identity commitments.
/// @param provider: The provider of the group.
/// @param names: The names of the group.
/// @param identityCommitments: Identity commitments.
@@ -72,7 +85,7 @@ contract Groups is OwnableUpgradeable {
}
}
/// @dev Gets a group provider and a group name and returns the last root hash of the group.
/// @dev Returns the last root hash of the group.
/// @return The root hash.
function getRoot(bytes32 provider, bytes32 name) external view returns (uint256) {
bytes32 groupId = getGroupId(provider, name);
@@ -80,7 +93,7 @@ contract Groups is OwnableUpgradeable {
return groups[groupId].root;
}
/// @dev Gets a group provider and a group name and returns the size of the group.
/// @dev Returns the size of the group.
/// @return The root hash.
function getSize(bytes32 provider, bytes32 name) external view returns (uint256) {
bytes32 groupId = getGroupId(provider, name);
@@ -88,25 +101,7 @@ contract Groups is OwnableUpgradeable {
return groups[groupId].numberOfLeaves;
}
/// @dev ...
/// @param provider: The provider of the group.
/// @param name: The name of the group.
/// @param depth: Depth of the tree.
function createGroup(
bytes32 provider,
bytes32 name,
uint8 depth
) public onlyOwner {
bytes32 groupId = getGroupId(provider, name);
require(groups[groupId].depth == 0, "Groups: group already exists");
groups[groupId].init(depth, 0);
emit NewGroup(provider, name, depth);
}
/// @dev ...
/// @dev Adds an identity commitment to an existing group.
/// @param provider: The provider of the group.
/// @param name: The name of the group.
/// @param identityCommitment: The new identity commitment.
@@ -117,23 +112,40 @@ contract Groups is OwnableUpgradeable {
) public {
bytes32 groupId = getGroupId(provider, name);
require(
(owner() == _msgSender() && groupAdmins[groupId] == address(0)) || groupAdmins[groupId] == _msgSender(),
"Groups: caller is not the contract owner or the group admin"
);
require(groups[groupId].depth != 0, "Groups: group does not exist");
require(groupAdmins[groupId] == _msgSender(), "Groups: caller is not the group admin");
groups[groupId].insert(identityCommitment);
emit NewIdentityCommitment(
provider,
name,
identityCommitment,
groups[groupId].numberOfLeaves - 1,
groups[groupId].root
);
emit NewIdentityCommitment(provider, name, identityCommitment, groups[groupId].root);
}
/// @dev Deletes an identity commitment from an existing group.
/// @param provider: The provider of the group.
/// @param name: The name of the group.
/// @param identityCommitment: The new identity commitment.
/// @param path: The new identity commitment.
/// @param siblingNodes: The new identity commitment.
function deleteIdentityCommitment(
bytes32 provider,
bytes32 name,
uint256 identityCommitment,
uint8[] memory path,
uint256[] memory siblingNodes
) public {
bytes32 groupId = getGroupId(provider, name);
require(groups[groupId].depth != 0, "Groups: group does not exist");
require(groupAdmins[groupId] == _msgSender(), "Groups: caller is not the group admin");
groups[groupId].remove(identityCommitment, path, siblingNodes);
emit DeleteIdentityCommitment(provider, name, identityCommitment, groups[groupId].root);
}
/// @dev Returns the group id.
/// @param provider: The provider of the group.
/// @param name: The name of the group.
function getGroupId(bytes32 provider, bytes32 name) private pure returns (bytes32) {
return keccak256(abi.encodePacked(provider, name));
}

View File

@@ -52,4 +52,60 @@ library IncrementalTree {
self.root = hash;
self.numberOfLeaves += 1;
}
function remove(
TreeData storage self,
uint256 leaf,
uint8[] memory path,
uint256[] memory siblingNodes
) public {
require(verify(self, leaf, path, siblingNodes), "IncrementalTree: leaf is not part of the tree");
uint256 hash = self.zeroes[0];
for (uint8 i = 0; i < self.depth; i++) {
if (path[i] % 2 == 0) {
if (siblingNodes[i] == self.lastNodes[i][1]) {
self.lastNodes[i][0] = hash;
}
hash = Hash.poseidon([hash, siblingNodes[i]]);
} else {
if (siblingNodes[i] == self.lastNodes[i][0]) {
self.lastNodes[i][1] = hash;
}
hash = Hash.poseidon([siblingNodes[i], hash]);
}
}
self.root = hash;
}
function verify(
TreeData storage self,
uint256 leaf,
uint8[] memory path,
uint256[] memory siblingNodes
) private view returns (bool) {
require(leaf < SNARK_SCALAR_FIELD, "IncrementalTree: leaf must be < SNARK_SCALAR_FIELD");
require(
path.length == self.depth && siblingNodes.length == self.depth,
"IncrementalTree: length of path is not correct"
);
uint256 hash = leaf;
for (uint8 i = 0; i < self.depth; i++) {
require(siblingNodes[i] < SNARK_SCALAR_FIELD, "IncrementalTree: sibling node must be < SNARK_SCALAR_FIELD");
if (path[i] % 2 == 0) {
hash = Hash.poseidon([hash, siblingNodes[i]]);
} else {
hash = Hash.poseidon([siblingNodes[i], hash]);
}
}
return hash == self.root;
}
}

View File

@@ -76,6 +76,7 @@
"hardhat": "^2.3.0",
"hardhat-gas-reporter": "^1.0.4",
"husky": "^6.0.0",
"incrementalquintree": "^1.0.7",
"lint-staged": "^11.0.0",
"mocha": "^8.4.0",
"prettier": "^2.3.0",

View File

@@ -1,44 +1,39 @@
import { expect } from "chai"
import { Signer } from "ethers"
import { buildPoseidon } from "circomlibjs"
import { ethers, run } from "hardhat"
import { IncrementalQuinTree } from "incrementalquintree"
import { Groups } from "../typechain"
describe("Groups", () => {
let contract: Groups
let signers: Signer[]
let accounts: string[]
const provider = ethers.utils.formatBytes32String("twitter")
const name = ethers.utils.formatBytes32String("gold")
const identityCommitment = BigInt(1)
before(async () => {
contract = await run("deploy:groups", { logs: false })
signers = await run("accounts", { logs: false })
accounts = await Promise.all(signers.map((signer: Signer) => signer.getAddress()))
})
it("Should create a group", async () => {
const fun = () => contract.createGroup(provider, name, 16)
const fun = () => contract.createGroup(provider, name, 16, accounts[0])
await expect(fun()).to.emit(contract, "NewGroup").withArgs(provider, name, 16)
})
it("Should not create a group with an existing id", async () => {
const fun = () => contract.createGroup(provider, name, 16)
const fun = () => contract.createGroup(provider, name, 16, accounts[0])
await expect(fun()).to.be.revertedWith("Groups: group already exists")
})
it("Should create an ownable group", async () => {
const dao = ethers.utils.formatBytes32String("DAO")
const name = ethers.utils.formatBytes32String("HelloWorld")
const admin = await signers[1].getAddress()
const fun = () => contract.createOwnableGroup(dao, name, 16, admin)
await expect(fun()).to.emit(contract, "NewGroup").withArgs(dao, name, 16)
})
it("Should not add an identity commitment if the group does not exist", async () => {
const identityCommitment = BigInt(2)
const otherName = ethers.utils.formatBytes32String("silver")
const fun = () => contract.addIdentityCommitment(provider, otherName, identityCommitment)
@@ -46,20 +41,15 @@ describe("Groups", () => {
await expect(fun()).to.be.revertedWith("Groups: group does not exist")
})
it("Should not add an identity commitment if the caller is not the contract owner or the group admin", async () => {
it("Should not add an identity commitment if the caller is not the group admin", async () => {
const identityCommitment = BigInt(2)
const otherName = ethers.utils.formatBytes32String("silver")
const fun = () => contract.connect(signers[2]).addIdentityCommitment(provider, otherName, identityCommitment)
const fun = () => contract.connect(signers[1]).addIdentityCommitment(provider, name, identityCommitment)
await expect(fun()).to.be.revertedWith("Groups: caller is not the contract owner or the group admin")
await expect(fun()).to.be.revertedWith("Groups: caller is not the group admin")
})
it("Should add an identity commitment in a group", async () => {
const identityCommitment = BigInt(
"2825646560483793878176284075509449079260676404272675066033690163469311186662"
)
const fun = () => contract.addIdentityCommitment(provider, name, identityCommitment)
await expect(fun())
@@ -68,21 +58,10 @@ describe("Groups", () => {
provider,
name,
identityCommitment,
0,
"13636421308146043413489220009267735248703575391714290025204419877115892930915"
"16211261537006706331557500769845541584780950636316907182067421710925347020533"
)
})
it("Should add an identity commitment in a ownable group", async () => {
const provider = ethers.utils.formatBytes32String("DAO")
const name = ethers.utils.formatBytes32String("HelloWorld")
const identityCommitment = BigInt(2)
const fun = () => contract.connect(signers[1]).addIdentityCommitment(provider, name, identityCommitment)
await expect(fun()).to.emit(contract, "NewIdentityCommitment")
})
it("Should throw an error if the length of the array parameters is not the same", async () => {
const names = ["gold", "silver", "bronze"].map(ethers.utils.formatBytes32String)
const identityCommitments = [1, 2].map(BigInt)
@@ -96,11 +75,35 @@ describe("Groups", () => {
const names = ["gold", "silver", "bronze"].map(ethers.utils.formatBytes32String)
const identityCommitments = [1, 2, 3].map(BigInt)
await contract.createGroup(provider, names[1], 16)
await contract.createGroup(provider, names[2], 16)
await contract.createGroup(provider, names[1], 16, accounts[0])
await contract.createGroup(provider, names[2], 16, accounts[0])
const fun = () => contract.batchAddIdentityCommitment(provider, names, identityCommitments)
await expect(fun()).to.emit(contract, "NewIdentityCommitment")
})
it("Should remove an identity commitment in a group", async () => {
const name = ethers.utils.formatBytes32String("RM")
const poseidon = await buildPoseidon()
const tree = new IncrementalQuinTree(16, 0, 2, (inputs: BigInt[]) => poseidon.F.toObject(poseidon(inputs)))
tree.insert(BigInt(1))
tree.insert(BigInt(2))
tree.insert(BigInt(3))
await contract.createGroup(provider, name, 16, accounts[0])
await contract.addIdentityCommitment(provider, name, BigInt(1))
await contract.addIdentityCommitment(provider, name, BigInt(2))
await contract.addIdentityCommitment(provider, name, BigInt(3))
tree.update(1, 0)
const path = tree.genMerklePath(1)
const siblingNodes = path.pathElements.map((e: BigInt[]) => e[0])
const fun = () => contract.deleteIdentityCommitment(provider, name, BigInt(2), path.indices, siblingNodes)
await expect(fun()).to.emit(contract, "DeleteIdentityCommitment").withArgs(provider, name, BigInt(2), path.root)
})
})

47
types/incrementalquintree/index.d.ts vendored Normal file
View File

@@ -0,0 +1,47 @@
/** Declaration file generated by dts-gen */
declare module "incrementalquintree" {
export class IncrementalQuinTree {
constructor(_depth: any, _zeroValue: any, _leavesPerNode: any, _hashFunc: any)
copy(): any
equals(t: any): any
genMerklePath(_index: any): any
genMerkleSubrootPath(_startIndex: any, _endIndex: any): any
getLeaf(_index: any): any
hash(_leaves: any): any
insert(_value: any): void
update(_index: any, _value: any): void
static verifyMerklePath(_proof: any, _hashFunc: any): any
}
export class MultiIncrementalQuinTree {
constructor(_depth: any, _zeroValue: any, _leavesPerNode: any, _hashFunc: any)
copy(): any
equals(t: any): any
genMerklePath(_absoluteIndex: any): any
genMerkleSubrootPath(_absoluteStartIndex: any, _absoluteEndIndex: any): any
getLeaf(_index: any): any
hash(_leaves: any): any
insert(_value: any): void
update(_absoluteIndex: any, _value: any): void
static verifyMerklePath(_proof: any, _hashFunc: any): any
}
}

2659
yarn.lock

File diff suppressed because it is too large Load Diff