Skip to content

Instantly share code, notes, and snippets.

@djma
Created June 24, 2023 17:01
Show Gist options
  • Save djma/40b5e6bb02bf43468de4528c973ac649 to your computer and use it in GitHub Desktop.
Save djma/40b5e6bb02bf43468de4528c973ac649 to your computer and use it in GitHub Desktop.
In-browser proof of group membership with spartan-ecdsa
This is example code to generate a zk-proof that you know the ecdsa private key to either
1) an ecdsa public key part of a merkle set
2) an ethereum address part of a merkle set
Public inputs:
- Merkle root
- Message hash
Private inputs:
- Merkle path
- Message signature
Expected data flow:
1. Server comes up with pubkey (or addr) set, merkle root (with poseidon hash), and a unique message to sign
2. Client signs the message with their browser wallet and finds the merkle path given the pubkey set
3. Client generates proof that they know a path and signature that satisfies the circuit.
4. Server verifies the proof, and public inputs (claims) match.
import styles from "../styles/Home.module.css";
import {
MembershipProver,
MembershipVerifier,
Poseidon,
Tree,
PublicInput,
} from "@personaelabs/spartan-ecdsa";
import { ecrecover, hashPersonalMessage, pubToAddress } from "@ethereumjs/util";
import { useState, useEffect } from "react";
import Web3 from "web3";
const addrMembershipConfig = {
circuit:
"https://storage.googleapis.com/personae-proving-keys/membership/addr_membership.circuit",
witnessGenWasm:
"https://storage.googleapis.com/personae-proving-keys/membership/addr_membership.wasm",
};
const pubkeyMembershipConfig = {
circuit:
"https://storage.googleapis.com/personae-proving-keys/membership/pubkey_membership.circuit",
witnessGenWasm:
"https://storage.googleapis.com/personae-proving-keys/membership/pubkey_membership.wasm",
};
export default function Home() {
const message = "random nonce: 123123";
const [addressesGroup, setAddressesGroup] = useState(
"0x45C2d45513a07C109bD29edc08Ba6Ecb2a2df58b\n0x3562284915C74A8cAa56a0edc656f8a538d6CE68"
);
const [pubkeysGroup, setPubkeysGroup] = useState(
"043b0be98d5c097bca4d06fcadb9386f59439b701e4ad23d06280ebdff3d2529e2589e7e83b5dc3213ffeb1468ea4f760ac0dbfa6e64998a78010d3112eaa3d7d4\n00339b2172392d72676f1cd99cdafd3cbadc8713f7e597f2e035b477d0eb79bf62cc24b896f602cd38431eb8284251601c5be02558d33c36f45bfd6aa70368aa"
);
const [pubkeyRoot, setPubkeyRoot] = useState("");
// const [privateKey, setPrivateKey] = useState("");
const [publicInputBox, setPublicInputBox] = useState("");
const [publicInput, setPublicInput] = useState<PublicInput | null>(null);
const [proof, setProof] = useState("");
const [web3, setWeb3] = useState(null);
const [account, setAccount] = useState(null);
const [signature, setSignature] = useState("");
const handleAddressesGroupChange = (e: any) => {
setAddressesGroup(e.target.value);
};
const handlePubkeysGroupChange = (e: any) => {
setPubkeysGroup(e.target.value);
};
// const handlePrivateKeyChange = (e) => {
// setPrivateKey(e.target.value);
// };
const handleSubmit = async (e: any) => {
e.preventDefault();
// Clear old output
setPublicInputBox("");
setProof("");
// Start the timer
const startTime = performance.now();
const { proof, publicInput } = (await groupProve(
pubkeysGroup,
addressesGroup,
// privateKey,
message,
signature
)) || { proof: "", publicInput: null };
// End the timer and calculate the elapsed time
const endTime = performance.now();
const elapsedTime = endTime - startTime;
setPublicInputBox(
"Elapsed time: " +
elapsedTime.toFixed(2) +
" milliseconds\n" +
`root: ${publicInput?.circuitPubInput.merkleRoot.toString(16)}\n` +
`msgHash: ${publicInput?.msgHash.toString("hex")}`
);
setPublicInput(publicInput);
setProof(Buffer.from(proof).toString("hex"));
};
const handleVerifySubmit = async (e: any) => {
e.preventDefault();
try {
// Init verifier
const verifier = new MembershipVerifier(pubkeyMembershipConfig);
await verifier.initWasm();
// Verify proof
await verifier.verify(
new Uint8Array(Buffer.from(proof, "hex")),
publicInput!.serialize()
);
alert("Proof verified!");
} catch (e) {
alert("Proof verification failed.");
}
};
const groupProve = async (
pubkeysGroup: string,
addressesGroup: string,
// privateKey: string,
message: string,
signature: string
) => {
if (!signature) {
alert("Please sign the message first.");
return;
}
// const msg = Buffer.from("harry potter");
// const msgHash = hashPersonalMessage(msg);
let msgHash = hashPersonalMessage(Buffer.from(message));
console.log("msghash: " + msgHash.toString("hex"));
// Init the Poseidon hash
const poseidon = new Poseidon();
await poseidon.initWasm();
const treeDepth = 20; // Provided circuits have tree depth = 20
const pubKeyTree = new Tree(treeDepth, poseidon);
const addressTree = new Tree(treeDepth, poseidon);
// Get the prover public key hash
let proverPubkey: Buffer;
let proverAddress: Buffer;
// Get the prover public key from the signed message
console.log("signedMessage length: " + signature.length);
const r = Buffer.from(signature.slice(2, 66), "hex");
const s = Buffer.from(signature.slice(66, 130), "hex");
const v = BigInt("0x" + signature.slice(130, 132));
console.log(r, s, v);
proverPubkey = ecrecover(msgHash, v, r, s);
// const recovered = new Web3().eth.accounts.recover(
// msgHash2.toString("hex"),
// v.toString(16),
// r.toString("hex"),
// s.toString("hex"),
// false
// );
// console.log("recovered: " + recovered);
console.log("Prover pubkey from signature: ", proverPubkey.toString("hex"));
// proverPubkey = privateToPublic(Buffer.from(privateKey, "hex"));
proverAddress = pubToAddress(proverPubkey);
console.log("Prover pubkey: ", proverPubkey.toString("hex"));
console.log("Prover address: ", proverAddress.toString("hex"));
// Insert other members into the tree
for (const member of pubkeysGroup.split("\n").filter((x) => x.length > 0)) {
pubKeyTree.insert(poseidon.hashPubKey(Buffer.from(member, "hex")));
}
for (const member of addressesGroup
.split("\n")
.filter((x) => x.length > 0)) {
addressTree.insert(BigInt(member));
}
setPubkeyRoot(pubKeyTree.root().toString(16));
// Compute the merkle proof
const pubKeyIndex = pubKeyTree.indexOf(poseidon.hashPubKey(proverPubkey));
const addressIndex = addressTree.indexOf(
BigInt("0x" + proverAddress.toString("hex"))
);
if (pubKeyIndex == -1 && addressIndex == -1) {
// Prover is not a member of the group
alert("Prover is not a member of the group");
return;
}
let prover;
let merkleProof;
if (pubKeyIndex >= 0) {
// Found the prover pubkey in the tree
console.log("Prover pubkey index: ", pubKeyIndex);
merkleProof = pubKeyTree.createProof(pubKeyIndex);
prover = new MembershipProver(pubkeyMembershipConfig);
} else {
// Found the prover address in the tree
console.log("Prover address index: ", addressIndex);
merkleProof = addressTree.createProof(addressIndex);
prover = new MembershipProver(addrMembershipConfig);
}
console.log("Merkle proof: ", merkleProof);
// Init the prover
await prover.initWasm();
// Sign a message with private key
let sig;
// const privateKeyBI = BigInt(
// privateKey.startsWith("0x") ? privateKey : "0x" + privateKey
// );
// const { r, s, v } = ecsign(
// msgHash,
// Buffer.from(privateKeyBI.toString(16), "hex")
// );
// sig = toRpcSig(v, r, s);
// console.log("r: ", r.toString("hex"));
// console.log("s: ", s.toString("hex"));
// console.log("v: ", v.toString(16));
sig = signature;
console.log("Signature: ", sig);
// const proverPubkey2 = ecrecover(msgHash, v, r, s);
// console.log("Prover pubkey 2: ", proverPubkey2.toString("hex"));
// Prove membership
return await prover.prove(sig, msgHash, merkleProof);
// return (
// "publicInput: " +
// JSON.stringify(
// {
// msgHash: publicInput.msgHash.toString("hex"),
// r: publicInput.r.toString(16),
// rV: publicInput.rV.toString(16),
// // circuitPubInput: JSON.stringify(publicInput.circuitPubInput),
// },
// null,
// 2
// ) +
// ",\nproof: " +
// Buffer.from(proof).toString("hex")
// );
// return (
// "addressesGroup: " +
// addressesGroup +
// ",\nprivateKey: " +
// privateKey +
// ",\nindex: " +
// index
// );
};
useEffect(() => {
if (window.ethereum) {
const web3Instance = new Web3(window.ethereum);
setWeb3(web3Instance);
} else {
alert("Please install MetaMask to use this dApp!");
}
}, []);
const connectMetaMask = async () => {
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
setAccount(accounts[0]);
};
const signMessage = async () => {
if (!web3 || !account) return;
const signature = await web3.eth.personal.sign(message, account, "");
// const typedData = { // WRONG
// types: {
// EIP712Domain: [
// { name: "name", type: "string" },
// { name: "version", type: "string" },
// ],
// Message: [{ name: "content", type: "string" }],
// },
// domain: {
// name: "Example DApp",
// version: "1",
// },
// primaryType: "Message",
// message: {
// content: message,
// },
// };
// const signature = await window.ethereum.request({
// method: "eth_signTypedData_v4",
// params: [account, JSON.stringify(typedData)],
// });
setSignature(signature);
};
return (
<div className={styles.container}>
<div style={{ margin: "0 auto", maxWidth: "600px", padding: "0 20px" }}>
<div style={{ padding: "2em" }}>
{account ? (
<>
<h3>Connected with: {account}</h3>
<button onClick={signMessage}>Sign Message</button>
{signature && (
<div>
<h3>Signature:</h3>
<textarea
readOnly
rows="4"
style={{ width: "100%" }}
value={signature}
/>
</div>
)}
</>
) : (
<button onClick={connectMetaMask}>Connect MetaMask</button>
)}
</div>
<h1>ZK address groups</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="pubkeys-group">Pubkeys Group:</label>
<textarea
id="pubkeys-group"
value={pubkeysGroup}
onChange={handlePubkeysGroupChange}
rows={4}
style={{ width: "100%" }}
></textarea>
</div>
<div style={{ marginTop: "20px" }}>
<label htmlFor="pubkey-root">Pubkeys Root:</label>
<textarea
id="pubkey-root"
value={pubkeyRoot}
readOnly
rows={1}
style={{ width: "100%" }}
></textarea>
</div>
<div>
<label htmlFor="addresses-group">Addresses Group:</label>
<textarea
id="addresses-group"
value={addressesGroup}
onChange={handleAddressesGroupChange}
rows={4}
style={{ width: "100%" }}
></textarea>
</div>
{/* <div>
<label htmlFor="private-key">Private Key:</label>
<input
type="text"
id="private-key"
value={privateKey}
onChange={handlePrivateKeyChange}
style={{ width: "100%" }}
/>
</div> */}
<div style={{ marginTop: "20px" }}>
<button type="submit">Submit</button>
</div>
</form>
<div style={{ marginTop: "20px" }}>
<label htmlFor="publicInput">Public Input:</label>
<textarea
id="publicInput"
value={publicInputBox}
readOnly
rows={4}
style={{ width: "100%" }}
></textarea>
</div>
<div style={{ marginTop: "20px" }}>
<label htmlFor="proof">Proof:</label>
<textarea
id="proof"
value={proof}
readOnly
rows={4}
style={{ width: "100%" }}
></textarea>
</div>
<form onSubmit={handleVerifySubmit}>
<div style={{ marginTop: "20px" }}>
<button type="submit">Verify</button>
</div>
</form>
</div>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment