Skip to content

Instantly share code, notes, and snippets.

@kindgracekind
Last active October 4, 2024 19:16
Show Gist options
  • Save kindgracekind/13cb7b82e0c87aa408e5b103dce602d4 to your computer and use it in GitHub Desktop.
Save kindgracekind/13cb7b82e0c87aa408e5b103dce602d4 to your computer and use it in GitHub Desktop.
Multiple pseudonymous accounts with Semaphore

Multiple Pseudonymous Accounts with Zero-Knowledge Proofs

As AI gets better at imitating humans, proof of personhood schemes are becoming increasingly important. These systems set out to accomplish the following goals:

  • Verify that a given user is a human
  • Verify that a given user is unique

The latter goal, in particular, can have drawbacks. For example, users are unable to create multiple pseudonymous accounts, which may be a desired feature in something like a microblogging platform.

In order to solve this problem, we can use Semaphore's external nullifiers to allow up to N pseudonymous accounts while ensuring accounts cannot be linked to each other. This is in contrast to the typical use of external nullifiers, in which a single app uses a single nullifier to prevent duplicate signups.

Note that this is just one candidate scheme- there may be others with different advantages and drawbacks.

Enabling Multiple Accounts

  • An app (Y) maintains its own list of users, in a publicly accessible Merkle tree
  • Users may add an identity to the Y user group by proving their membership in an external user group, such as Worldcoin
  • A fixed number of registration scopes are available, so that the same Worldcoin user may create multiple Y users
  • Nullifiers are used per-scope to prevent users from re-using the same identity slot
  • All in-app actions are performed by proving membership in the Y user group

Enabling Account Deletion

Instead of N accounts total, let's say we want to allow users to have up-to-N active accounts - if they delete an account, they should be able to create a new one in its place. Furthermore, we would like these new accounts to be unlinkable to their predecessors.

We can accomplish this by using a successor system, in which each account has a "successor" account that takes its place.

  • A Y user creates a new account commitment locally
  • The user proves their membership in the Y user group, and provides the commitment of the new account
  • The original account profile is marked as deactivated

Notably, this completely revokes access to the original account- this is necessary to prevent users from re-using the same identity slot.

import { generateProof, verifyProof } from "@semaphore-protocol/proof";
import { Identity } from "@semaphore-protocol/identity";
import { Group } from "@semaphore-protocol/group";
import { decodeMessage } from "@semaphore-protocol/utils";
const hash = (str) => {
return crypto.createHash("md5").update(str).digest("hex");
};
// Dummy worldcoin user pool
class WorldCoin {
constructor() {
this.group = new Group();
}
// would require iris hash irl
registerUser(commitment) {
this.group.addMember(commitment);
}
getRoot() {
return this.group.root;
}
}
class TwitterClone {
constructor(worldCoin) {
this.worldCoin = worldCoin;
this.worldCoinMap = {};
this.users = new Group();
this.userProfiles = {};
this.tweets = [];
}
async registerUser(proof) {
const scope = decodeMessage(proof.scope);
const scopeNames = [0, 1, 2].map((i) => `tc-register-${i}`);
if (!scopeNames.includes(scope)) {
throw new Error("Invalid scope");
}
if (proof.merkleTreeRoot !== String(this.worldCoin.getRoot())) {
throw new Error("Wrong group");
}
await verifyProof(proof);
const existingUser = this.worldCoinMap[proof.nullifier];
if (existingUser) {
this.users.removeMember(this.users.indexOf(existingUser));
}
const commitment = proof.message;
this.users.addMember(commitment);
this.worldCoinMap[proof.nullifier] = commitment;
}
async authorizeUser(proof) {
if (decodeMessage(proof.scope) !== "tc-auth") {
throw new Error("Invalid scope");
}
if (proof.merkleTreeRoot !== String(this.users.root)) {
throw new Error("Wrong group");
}
await verifyProof(proof);
const userProfile = this.userProfiles[proof.nullifier] ?? {
username: "Anonymous",
};
this.userProfiles[proof.nullifier] = userProfile;
return userProfile;
}
async postTweet(proof) {
const userProfile = await this.authorizeUser(proof);
if (userProfile.deactivated) {
throw new Error("User is deactivated");
}
const username = userProfile.username;
const content = decodeMessage(proof.message);
this.tweets.push({
username,
content,
});
console.log("Tweet: ", username, content);
}
async deactivateUser(proof) {
const userProfile = await this.authorizeUser(proof);
userProfile.deactivated = true;
}
async activateUser(proof) {
const userProfile = await this.authorizeUser(proof);
if (userProfile.successor) {
throw new Error("Cannot reactivate user with successor");
}
userProfile.deactivated = false;
}
async setSuccessor(proof) {
const userProfile = await this.authorizeUser(proof);
if (!userProfile.deactivated) {
throw new Error("User must be deactivated before setting successor");
}
userProfile.successor = proof.message;
const successor = proof.message;
this.users.addMember(successor);
}
}
const worldCoin = new WorldCoin();
const twitterClone = new TwitterClone(worldCoin);
const worldCoinIdentity = new Identity();
worldCoin.registerUser(worldCoinIdentity.commitment);
// Derive app-specific identity
const twitterIdentity = new Identity(hash(String(worldCoinIdentity.publicKey)));
const registerProof = await generateProof(
worldCoinIdentity,
worldCoin.group,
twitterIdentity.commitment,
"tc-register-0"
);
await twitterClone.registerUser(registerProof);
const postProof = await generateProof(
twitterIdentity,
twitterClone.users,
"Hello world",
"tc-auth"
);
await twitterClone.postTweet(postProof);
const deactivateProof = await generateProof(
twitterIdentity,
twitterClone.users,
0,
"tc-auth"
);
await twitterClone.deactivateUser(deactivateProof);
const successorIdentity = new Identity(hash(String(twitterIdentity.publicKey)));
const successorProof = await generateProof(
twitterIdentity,
twitterClone.users,
successorIdentity.commitment,
"tc-auth"
);
await twitterClone.setSuccessor(successorProof);
const postProof2 = await generateProof(
twitterIdentity,
twitterClone.users,
"New post",
"tc-auth"
);
let error = false;
try {
await twitterClone.postTweet(postProof2);
} catch (e) {
error = true;
}
if (!error) {
throw new Error("Post should fail, since user is deactivated");
}
const postProof3 = await generateProof(
successorIdentity,
twitterClone.users,
"Hello world from new identity",
"tc-auth"
);
await twitterClone.postTweet(postProof3);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment