Created
June 24, 2023 17:01
-
-
Save djma/40b5e6bb02bf43468de4528c973ac649 to your computer and use it in GitHub Desktop.
In-browser proof of group membership with spartan-ecdsa
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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