feat: withdrawals/slashing implemented on interep

This commit is contained in:
rymnc 2022-12-02 12:49:11 +05:30
parent ac21cfd698
commit 430b040043
No known key found for this signature in database
GPG Key ID: C740033EE3F41EBD
2 changed files with 256 additions and 80 deletions

View File

@ -10,15 +10,16 @@ contract RLN {
uint256 public immutable DEPTH; uint256 public immutable DEPTH;
uint256 public immutable SET_SIZE; uint256 public immutable SET_SIZE;
uint256 public pubkeyIndex = 0; uint256 public idCommitmentIndex;
mapping(uint256 => uint256) public members; mapping(uint256 => uint256) public stakedAmounts;
mapping(uint256 => bool) public members;
IPoseidonHasher public poseidonHasher; IPoseidonHasher public poseidonHasher;
IValidGroupStorage public validGroupStorage; IValidGroupStorage public validGroupStorage;
IInterep public interep; IInterep public interep;
event MemberRegistered(uint256 pubkey, uint256 index); event MemberRegistered(uint256 idCommitment, uint256 index);
event MemberWithdrawn(uint256 pubkey, uint256 index); event MemberWithdrawn(uint256 idCommitment);
constructor( constructor(
uint256 membershipDeposit, uint256 membershipDeposit,
@ -34,13 +35,12 @@ contract RLN {
interep = IInterep(validGroupStorage.interep()); interep = IInterep(validGroupStorage.interep());
} }
function register(uint256 pubkey) external payable { function register(uint256 idCommitment) external payable {
require(pubkeyIndex < SET_SIZE, "RLN, register: set is full");
require( require(
msg.value == MEMBERSHIP_DEPOSIT, msg.value == MEMBERSHIP_DEPOSIT,
"RLN, register: membership deposit is not satisfied" "RLN, register: membership deposit is not satisfied"
); );
_register(pubkey); _register(idCommitment, msg.value);
} }
/// @dev Registers a member via a valid Interep Semaphore group. /// @dev Registers a member via a valid Interep Semaphore group.
@ -49,20 +49,19 @@ contract RLN {
/// @param nullifierHash: Nullifier hash. /// @param nullifierHash: Nullifier hash.
/// @param externalNullifier: External nullifier. /// @param externalNullifier: External nullifier.
/// @param proof: Zero-knowledge proof. /// @param proof: Zero-knowledge proof.
/// @param pubkey: Public key of the member. /// @param idCommitment: ID Commitment of the member.
function register( function register(
uint256 groupId, uint256 groupId,
bytes32 signal, bytes32 signal,
uint256 nullifierHash, uint256 nullifierHash,
uint256 externalNullifier, uint256 externalNullifier,
uint256[8] calldata proof, uint256[8] calldata proof,
uint256 pubkey uint256 idCommitment
) external { ) external {
require( require(
validGroupStorage.isValidGroup(groupId), validGroupStorage.isValidGroup(groupId),
"RLN, register: invalid interep group" "RLN, register: invalid interep group"
); );
require(pubkeyIndex < SET_SIZE, "RLN, register: set is full");
interep.verifyProof( interep.verifyProof(
groupId, groupId,
signal, signal,
@ -70,93 +69,110 @@ contract RLN {
externalNullifier, externalNullifier,
proof proof
); );
_register(pubkey); _register(idCommitment, 0);
} }
function registerBatch(uint256[] calldata pubkeys) external payable { function registerBatch(uint256[] calldata idCommitments) external payable {
uint256 pubkeylen = pubkeys.length; uint256 idCommitmentlen = idCommitments.length;
require( require(
pubkeyIndex + pubkeylen <= SET_SIZE, idCommitmentIndex + idCommitmentlen <= SET_SIZE,
"RLN, registerBatch: set is full" "RLN, registerBatch: set is full"
); );
require( require(
msg.value == MEMBERSHIP_DEPOSIT * pubkeylen, msg.value == MEMBERSHIP_DEPOSIT * idCommitmentlen,
"RLN, registerBatch: membership deposit is not satisfied" "RLN, registerBatch: membership deposit is not satisfied"
); );
for (uint256 i = 0; i < pubkeylen; i++) { for (uint256 i = 0; i < idCommitmentlen; i++) {
_register(pubkeys[i]); _register(idCommitments[i], msg.value / idCommitmentlen);
} }
} }
function _register(uint256 pubkey) internal { function _register(uint256 idCommitment, uint256 stake) internal {
members[pubkeyIndex] = pubkey; require(
emit MemberRegistered(pubkey, pubkeyIndex); !members[idCommitment],
pubkeyIndex += 1; "RLN, _register: member already registered"
);
require(idCommitmentIndex < SET_SIZE, "RLN, register: set is full");
if (stake != 0) {
members[idCommitment] = true;
stakedAmounts[idCommitment] = stake;
} else {
members[idCommitment] = true;
stakedAmounts[idCommitment] = 0;
}
emit MemberRegistered(idCommitment, idCommitmentIndex);
idCommitmentIndex += 1;
} }
function withdrawBatch( function withdrawBatch(
uint256[] calldata secrets, uint256[] calldata secrets,
uint256[] calldata pubkeyIndexes,
address payable[] calldata receivers address payable[] calldata receivers
) external { ) external {
uint256 batchSize = secrets.length; uint256 batchSize = secrets.length;
require(batchSize != 0, "RLN, withdrawBatch: batch size zero"); require(batchSize != 0, "RLN, withdrawBatch: batch size zero");
require( require(
batchSize == pubkeyIndexes.length, batchSize == secrets.length,
"RLN, withdrawBatch: batch size mismatch pubkey indexes" "RLN, withdrawBatch: batch size mismatch secrets"
); );
require( require(
batchSize == receivers.length, batchSize == receivers.length,
"RLN, withdrawBatch: batch size mismatch receivers" "RLN, withdrawBatch: batch size mismatch receivers"
); );
for (uint256 i = 0; i < batchSize; i++) { for (uint256 i = 0; i < batchSize; i++) {
_withdraw(secrets[i], pubkeyIndexes[i], receivers[i]); _withdraw(secrets[i], receivers[i]);
} }
} }
function withdraw( function withdraw(uint256 secret, address payable receiver) external {
uint256 secret, _withdraw(secret, receiver);
uint256 _pubkeyIndex,
address payable receiver
) external {
_withdraw(secret, _pubkeyIndex, receiver);
} }
function _withdraw( function withdraw(uint256 secret) external {
uint256 secret, _withdraw(secret);
uint256 _pubkeyIndex, }
address payable receiver
) internal { function _withdraw(uint256 secret, address payable receiver) internal {
// derive idCommitment
uint256 idCommitment = hash(secret);
// check if member is registered
require(members[idCommitment], "RLN, _withdraw: member not registered");
// check if member has stake
require( require(
_pubkeyIndex < SET_SIZE, stakedAmounts[idCommitment] != 0,
"RLN, _withdraw: invalid pubkey index" "RLN, _withdraw: member has no stake"
);
require(
members[_pubkeyIndex] != 0,
"RLN, _withdraw: member doesn't exist"
); );
require( require(
receiver != address(0), receiver != address(0),
"RLN, _withdraw: empty receiver address" "RLN, _withdraw: empty receiver address"
); );
// derive public key
uint256 pubkey = hash(secret);
require(
members[_pubkeyIndex] == pubkey,
"RLN, _withdraw: not verified"
);
// delete member
members[_pubkeyIndex] = 0;
// refund deposit // refund deposit
(bool sent, bytes memory data) = receiver.call{ (bool sent, ) = receiver.call{value: stakedAmounts[idCommitment]}("");
value: MEMBERSHIP_DEPOSIT
}("");
require(sent, "transfer failed"); require(sent, "transfer failed");
emit MemberWithdrawn(pubkey, _pubkeyIndex); // delete member
members[idCommitment] = false;
stakedAmounts[idCommitment] = 0;
emit MemberWithdrawn(idCommitment);
}
function _withdraw(uint256 secret) internal {
// derive idCommitment
uint256 idCommitment = hash(secret);
// check if member is registered
require(members[idCommitment], "RLN, _withdraw: member not registered");
require(stakedAmounts[idCommitment] == 0, "RLN, _withdraw: staked");
// delete member
members[idCommitment] = false;
emit MemberWithdrawn(idCommitment);
} }
function hash(uint256 input) internal view returns (uint256) { function hash(uint256 input) internal view returns (uint256) {

View File

@ -30,7 +30,7 @@ describe("RLN", () => {
}); });
const txRegisterReceipt = await registerTx.wait(); const txRegisterReceipt = await registerTx.wait();
const pubkey = txRegisterReceipt.events[0].args.pubkey; const pubkey = txRegisterReceipt.events[0].args.idCommitment;
// We ensure the registered id_commitment is the one we passed // We ensure the registered id_commitment is the one we passed
expect( expect(
@ -53,31 +53,85 @@ describe("RLN", () => {
const registerTx = await rln["register(uint256)"](idCommitment, { const registerTx = await rln["register(uint256)"](idCommitment, {
value: price, value: price,
}); });
const txRegisterReceipt = await registerTx.wait(); await registerTx.wait();
const treeIndex = txRegisterReceipt.events[0].args.index;
// We withdraw our id_commitment // We withdraw our id_commitment
const receiverAddress = "0x000000000000000000000000000000000000dead"; const receiverAddress = "0x000000000000000000000000000000000000dead";
const withdrawTx = await rln.withdraw(idSecret, treeIndex, receiverAddress); const withdrawTx = await rln["withdraw(uint256,address)"](
idSecret,
receiverAddress
);
const txWithdrawReceipt = await withdrawTx.wait(); const txWithdrawReceipt = await withdrawTx.wait();
const withdrawalPk = txWithdrawReceipt.events[0].args.pubkey; const withdrawalPk = txWithdrawReceipt.events[0].args.idCommitment;
const withdrawalTreeIndex = txWithdrawReceipt.events[0].args.index;
// We ensure the registered id_commitment is the one we passed and that the index is the same // We ensure the registered id_commitment is the one we passed and that the index is the same
expect( expect(
withdrawalPk.toHexString() === idCommitment, withdrawalPk.toHexString() === idCommitment,
"withdraw commitment doesn't match registered commitment" "withdraw commitment doesn't match registered commitment"
); );
expect( });
withdrawalTreeIndex.toHexString() === treeIndex.toHexString(),
"withdraw index doesn't match registered index" it("should not withdraw stake without address", async () => {
const rln = await ethers.getContract("RLN", ethers.provider.getSigner(0));
const price = await rln.MEMBERSHIP_DEPOSIT();
// A valid pair of (id_secret, id_commitment) generated in rust
const idSecret =
"0x2a09a9fd93c590c26b91effbb2499f07e8f7aa12e2b4940a3aed2411cb65e11c";
const idCommitment =
"0x0c3ac305f6a4fe9bfeb3eba978bc876e2a99208b8b56c80160cfb54ba8f02368";
const registerTx = await rln["register(uint256)"](idCommitment, {
value: price,
});
await registerTx.wait();
// We withdraw our id_commitment
const withdrawTx = rln["withdraw(uint256)"](idSecret);
await expect(withdrawTx).to.be.revertedWith("RLN, _withdraw: staked");
});
it("should not withdraw withdraw stake if no stake exists", async () => {
const rln = await ethers.getContract("RLN", ethers.provider.getSigner(0));
const validGroupId = createGroupId("github", "silver");
const dummySignal = sToBytes32("foo");
const dummyNullifierHash = BigNumber.from(0);
const dummyExternalNullifier = BigNumber.from(0);
const dummyProof = Array(8).fill(BigNumber.from(0));
const idCommitment = BigNumber.from(
"0x0c3ac305f6a4fe9bfeb3eba978bc876e2a99208b8b56c80160cfb54ba8f02368"
);
const secret =
"0x2a09a9fd93c590c26b91effbb2499f07e8f7aa12e2b4940a3aed2411cb65e11c";
const registerTx = await rln[
"register(uint256,bytes32,uint256,uint256,uint256[8],uint256)"
](
validGroupId,
dummySignal,
dummyNullifierHash,
dummyExternalNullifier,
dummyProof,
idCommitment
);
await registerTx.wait();
const address = "0x000000000000000000000000000000000000dead";
const withdrawTx = rln["withdraw(uint256,address)"](secret, address);
await expect(withdrawTx).to.be.revertedWith(
"RLN, _withdraw: member has no stake"
); );
}); });
it.skip("should not allow multiple registrations with same pubkey", async () => { it("should not allow multiple registrations with same pubkey", async () => {
const rln = await ethers.getContract("RLN", ethers.provider.getSigner(0)); const rln = await ethers.getContract("RLN", ethers.provider.getSigner(0));
const price = await rln.MEMBERSHIP_DEPOSIT(); const price = await rln.MEMBERSHIP_DEPOSIT();
@ -89,22 +143,16 @@ describe("RLN", () => {
const registerTx = await rln["register(uint256)"](idCommitment, { const registerTx = await rln["register(uint256)"](idCommitment, {
value: price, value: price,
}); });
const txRegisterReceipt = await registerTx.wait(); await registerTx.wait();
const index1 = txRegisterReceipt.events[0].args.index;
// Send the same tx again // Send the same tx again
const registerTx2 = await rln["register(uint256)"](idCommitment, { const registerTx2 = rln["register(uint256)"](idCommitment, {
value: price, value: price,
}); });
const txRegisterReceipt2 = await registerTx2.wait();
const index2 = txRegisterReceipt2.events[0].args.index;
const pk1 = await rln.members(index1); await expect(registerTx2).to.be.revertedWith(
const pk2 = await rln.members(index2); "RLN, _register: member already registered"
const samePk = pk1.toHexString() === pk2.toHexString(); );
if (samePk) {
assert(false, "same pubkey registered twice");
}
}); });
it("[interep] should register new memberships", async () => { it("[interep] should register new memberships", async () => {
@ -138,7 +186,7 @@ describe("RLN", () => {
); );
expect(event.args.signal).to.equal(dummySignal); expect(event.args.signal).to.equal(dummySignal);
const pubkey = txRegisterReceipt.events[1].args.pubkey; const pubkey = txRegisterReceipt.events[1].args.idCommitment;
expect(pubkey.toHexString() === dummyPubkey.toHexString()); expect(pubkey.toHexString() === dummyPubkey.toHexString());
}); });
@ -254,8 +302,120 @@ describe("RLN", () => {
const txRegisterReceipt = await registerTx.wait(); const txRegisterReceipt = await registerTx.wait();
expect(txRegisterReceipt.events[1].args.pubkey.toHexString()).to.eql( expect(txRegisterReceipt.events[1].args.idCommitment.toHexString()).to.eql(
BigNumber.from(identity.getCommitment()).toHexString() BigNumber.from(identity.getCommitment()).toHexString()
); );
}); });
it("[interep] should revert with invalid proof", async () => {
// need to create new fixtures for this test
const { PoseidonHasher } = await deployments.fixture("PoseidonHasher");
const verifier20Factory = await ethers.getContractFactory("Verifier20");
const verifier20 = await verifier20Factory.deploy();
await verifier20.deployed();
const interepFactory = await ethers.getContractFactory(
"Interep",
ethers.provider.getSigner(0)
);
const interep = await interepFactory.deploy([
{
contractAddress: verifier20.address,
merkleTreeDepth: merkleTreeDepth,
},
]);
await interep.deployed();
const groupTx = await interep.updateGroups(getGroups());
await groupTx.wait();
const validGroupStorageFactory = await ethers.getContractFactory(
"ValidGroupStorage"
);
const validGroupStorage = await validGroupStorageFactory.deploy(
interep.address,
getValidGroups()
);
await validGroupStorage.deployed();
const rlnFactory = await ethers.getContractFactory(
"RLN",
ethers.provider.getSigner(0)
);
const rln = await rlnFactory.deploy(
1000000000000000,
20,
PoseidonHasher.address,
validGroupStorage.address
);
await rln.deployed();
const identity = await createInterepIdentity(
ethers.provider.getSigner(0),
"github"
);
// create a proof to test
const proof = await createInterepProof({
identity,
members: [identity.getCommitment()],
groupProvider: "github",
groupTier: "silver",
signal: sToBytes32("foo"),
externalNullifier: 1,
snarkArtifacts: {
wasmFilePath: "./test/snarkArtifacts/semaphore.wasm",
zkeyFilePath: "./test/snarkArtifacts/semaphore.zkey",
},
});
// do not update root of group
const registerTx = rln[
"register(uint256,bytes32,uint256,uint256,uint256[8],uint256)"
](
proof.groupId,
proof.signal,
proof.publicSignals.nullifierHash,
proof.publicSignals.externalNullifier,
proof.solidityProof,
identity.getCommitment()
);
await expect(registerTx).to.be.revertedWith("InvalidProof()");
});
it("[interep] should withdraw a registration", async () => {
const rln = await ethers.getContract("RLN", ethers.provider.getSigner(0));
const validGroupId = createGroupId("github", "silver");
const dummySignal = sToBytes32("foo");
const dummyNullifierHash = BigNumber.from(0);
const dummyExternalNullifier = BigNumber.from(0);
const dummyProof = Array(8).fill(BigNumber.from(0));
const idCommitment = BigNumber.from(
"0x0c3ac305f6a4fe9bfeb3eba978bc876e2a99208b8b56c80160cfb54ba8f02368"
);
const secret =
"0x2a09a9fd93c590c26b91effbb2499f07e8f7aa12e2b4940a3aed2411cb65e11c";
const registerTx = await rln[
"register(uint256,bytes32,uint256,uint256,uint256[8],uint256)"
](
validGroupId,
dummySignal,
dummyNullifierHash,
dummyExternalNullifier,
dummyProof,
idCommitment
);
await registerTx.wait();
const withdrawTx = await rln["withdraw(uint256)"](secret);
const txWithdrawReceipt = await withdrawTx.wait();
expect(txWithdrawReceipt.events[0].args.idCommitment).to.eql(idCommitment);
});
}); });