Update lib

- Remove eff_ecdsa prover and verifier
- Add support for both addr_membership and pubkey_membership in MembershipProver
- Expose an initWasm method from the MembershipProver class and the Poseidon class that takes in a SpartanWasm class instance
- Export default configuration for both addr_membership and pubkey_membership
This commit is contained in:
Daniel Tehrani
2023-01-27 16:20:01 +09:00
parent e3fbcce396
commit 47d71c1e06
13 changed files with 255 additions and 278 deletions

View File

@@ -0,0 +1,73 @@
const wasm_tester = require("circom_tester").wasm;
var EC = require("elliptic").ec;
import * as path from "path";
const ec = new EC("secp256k1");
import { Poseidon, Tree, SpartanWasm, defaultWasmConfig } from "spartan-ecdsa";
import { getEffEcdsaCircuitInput } from "./test_utils";
import { privateToAddress } from "@ethereumjs/util";
describe("membership", () => {
it("should verify correct signature and merkle proof", async () => {
// Compile the circuit
const circuit = await wasm_tester(
path.join(__dirname, "./circuits/addr_membership_test.circom"),
{
prime: "secq256k1" // Specify to use the option --prime secq256k1 when compiling with circom
}
);
const wasm = new SpartanWasm(defaultWasmConfig);
// Construct the tree
const poseidon = new Poseidon();
await poseidon.initWasm(wasm);
const nLevels = 10;
const tree = new Tree(nLevels, poseidon);
const privKeys = [
Buffer.from("".padStart(16, "🧙"), "utf16le"),
Buffer.from("".padStart(16, "🪄"), "utf16le"),
Buffer.from("".padStart(16, "🔮"), "utf16le")
];
// Store addresses hashes
const addresses: bigint[] = [];
// Compute public key hashes
for (const privKey of privKeys) {
const address = privateToAddress(privKey);
addresses.push(BigInt("0x" + address.toString("hex")));
}
// Insert the pubkey hashes into the tree
for (const address of addresses) {
tree.insert(address);
}
// Sanity check (check that there are not duplicate members)
expect(new Set(addresses).size === addresses.length).toBeTruthy();
// Sign
const index = 0; // Use privKeys[0] for proving
const privKey = privKeys[index];
const msg = Buffer.from("hello world");
// Prepare signature proof input
const effEcdsaInput = getEffEcdsaCircuitInput(privKey, msg);
const merkleProof = tree.createProof(index);
const input = {
...effEcdsaInput,
siblings: merkleProof.siblings,
pathIndices: merkleProof.pathIndices,
root: tree.root()
};
// Generate witness
const w = await circuit.calculateWitness(input, true);
await circuit.checkConstraints(w);
});
});

View File

@@ -1,23 +1,26 @@
const wasm_tester = require("circom_tester").wasm;
var EC = require("elliptic").ec;
import * as path from "path";
const ec = new EC("secp256k1");
import { Poseidon, Tree } from "spartan-ecdsa";
import { Poseidon, Tree, SpartanWasm, defaultWasmConfig } from "spartan-ecdsa";
import { privateToPublic } from "@ethereumjs/util";
import { getEffEcdsaCircuitInput } from "./test_utils";
describe("membership", () => {
describe("pubkey_membership", () => {
it("should verify correct signature and merkle proof", async () => {
// Compile the circuit
const circuit = await wasm_tester(
path.join(__dirname, "./circuits/membership_test.circom"),
path.join(__dirname, "./circuits/pubkey_membership_test.circom"),
{
prime: "secq256k1" // Specify to use the option --prime secq256k1 when compiling with circom
}
);
const wasm = new SpartanWasm(defaultWasmConfig);
// Construct the tree
const poseidon = new Poseidon();
await poseidon.init();
await poseidon.initWasm(wasm);
const nLevels = 10;
const tree = new Tree(nLevels, poseidon);
@@ -32,10 +35,8 @@ describe("membership", () => {
// Compute public key hashes
for (const privKey of privKeys) {
const pubKey = ec.keyFromPrivate(privKey).getPublic();
const pubKeyX = BigInt(pubKey.x.toString());
const pubKeyY = BigInt(pubKey.y.toString());
const pubKeyHash = poseidon.hash([pubKeyX, pubKeyY]);
const pubKey = privateToPublic(privKey);
const pubKeyHash = poseidon.hashPubKey(pubKey);
pubKeyHashes.push(pubKeyHash);
}

View File

@@ -1,22 +1,45 @@
import * as path from "path";
const isWeb = typeof window !== "undefined";
import { LeafType, ProverConfig, WasmConfig } from "./types";
export const DEFAULT_SPARTAN_WASM = isWeb
? "https://storage.googleapis.com/personae-proving_keys/spartan_wasm_bg.wasm"
: path.join(__dirname, "wasm/build/spartan_wasm_bg.wasm");
export const defaultWasmConfig: WasmConfig = {
pathOrUrl: isWeb
? "https://storage.googleapis.com/personae-proving_keys/spartan_wasm_bg.wasm"
: path.join(__dirname, "wasm/build/spartan_wasm_bg.wasm")
};
export const DEFAULT_EFF_ECDSA_WITNESS_GEN_WASM = isWeb
? "https://storage.googleapis.com/personae-proving_keys/eff_ecdsa/eff_ecdsa.wasm"
: path.join(__dirname, "circuits/eff_ecdsa.wasm");
// Default configs for MembershipProver
export const DEFAULT_EFF_ECDSA_CIRCUIT = isWeb
? "https://storage.googleapis.com/personae-proving_keys/eff_ecdsa/eff_ecdsa.circuit"
: path.join(__dirname, "circuits/eff_ecdsa.circuit");
// Default configs for pubkey membership proving
export const defaultPubkeyMembershipConfig: ProverConfig = {
spartanWasm: isWeb
? "https://storage.googleapis.com/personae-proving_keys/spartan_wasm_bg.wasm"
: path.join(__dirname, "wasm/build/spartan_wasm_bg.wasm"),
export const DEFAULT_MEMBERSHIP_WITNESS_GEN_WASM = isWeb
? "https://storage.googleapis.com/personae-proving-keys/membership/membeship.wasm"
: path.join(__dirname, "circuits/membership.wasm");
witnessGenWasm: isWeb
? "https://storage.googleapis.com/personae-proving-keys/membership/pubkey_membership.wasm"
: path.join(__dirname, "circuits/pubkey_membership.wasm"),
export const DEFAULT_MEMBERSHIP_CIRCUIT = isWeb
? "https://storage.googleapis.com/personae-proving_keys/membership/membership.circuit"
: path.join(__dirname, "circuits/membership.circuit");
circuit: isWeb
? "https://storage.googleapis.com/personae-proving_keys/membership/pubkey_membership.circuit"
: path.join(__dirname, "circuits/pubkey_membership.circuit"),
leafType: LeafType.PubKeyHash
};
// Default configs for address membership proving
export const defaultAddressMembershipConfig: ProverConfig = {
spartanWasm: isWeb
? "https://storage.googleapis.com/personae-proving_keys/spartan_wasm_bg.wasm"
: path.join(__dirname, "wasm/build/spartan_wasm_bg.wasm"),
witnessGenWasm: isWeb
? "https://storage.googleapis.com/personae-proving-keys/membership/addr_membership.wasm"
: path.join(__dirname, "circuits/addr_membership.wasm"),
circuit: isWeb
? "https://storage.googleapis.com/personae-proving_keys/membership/addr_membership.circuit"
: path.join(__dirname, "circuits/addr_membership.circuit"),
leafType: LeafType.Address
};

View File

@@ -1,76 +0,0 @@
import {
DEFAULT_EFF_ECDSA_CIRCUIT,
DEFAULT_EFF_ECDSA_WITNESS_GEN_WASM
} from "../config";
import { loadCircuit, snarkJsWitnessGen, fromSig } from "../helpers/utils";
import {
EffEcdsaPubInput,
EffEcdsaCircuitPubInput
} from "../helpers/efficient_ecdsa";
import { SpartanWasm } from "../wasm";
import { hashPersonalMessage } from "@ethereumjs/util";
import { ProverOptions, IProver, NIZK } from "../types";
import { Profiler } from "../helpers/profiler";
export class EffECDSAProver extends Profiler implements IProver {
spartanWasm: SpartanWasm;
circuit: string;
witnessGenWasm: string;
constructor(options?: ProverOptions) {
super({ enabled: options?.enableProfiler });
this.spartanWasm = new SpartanWasm({ spartanWasm: options?.spartanWasm });
this.circuit = options?.circuit || DEFAULT_EFF_ECDSA_CIRCUIT;
this.witnessGenWasm =
options?.witnessGenWasm || DEFAULT_EFF_ECDSA_WITNESS_GEN_WASM;
}
// sig: format of the `eth_sign` RPC method
// https://ethereum.github.io/execution-apis/api-documentation
async prove(sig: string, msg: Buffer): Promise<NIZK> {
const { r, s, v } = fromSig(sig);
const msgHash = hashPersonalMessage(msg);
const circuitPubInput = EffEcdsaCircuitPubInput.computeFromSig(
r,
v,
msgHash
);
const effEcdsaPubInput = new EffEcdsaPubInput(
r,
v,
msgHash,
circuitPubInput
);
const witnessGenInput = {
s,
...circuitPubInput
};
this.time("Generate witness");
const witness = await snarkJsWitnessGen(
witnessGenInput,
this.witnessGenWasm
);
this.timeEnd("Generate witness");
this.time("Load circuit");
const circuitBin = await loadCircuit(this.circuit);
this.timeEnd("Load circuit");
await this.spartanWasm.init();
this.time("Prove");
let proof = await this.spartanWasm.prove(
circuitBin,
witness.data,
effEcdsaPubInput.circuitPubInput.serialize()
);
this.timeEnd("Prove");
return { proof, publicInput: effEcdsaPubInput.serialize() };
}
}

View File

@@ -1,51 +0,0 @@
import { DEFAULT_EFF_ECDSA_CIRCUIT } from "../config";
import { loadCircuit } from "../helpers/utils";
import { SpartanWasm } from "../wasm";
import {
EffEcdsaPubInput,
verifyEffEcdsaPubInput
} from "../helpers/efficient_ecdsa";
import { Profiler } from "../helpers/profiler";
import { VerifyOptions, IVerifier } from "../types";
export class EffECDSAVerifier extends Profiler implements IVerifier {
spartanWasm: SpartanWasm;
circuit: string;
constructor(options?: VerifyOptions) {
super({ enabled: options?.enableProfiler });
this.circuit = options?.circuit || DEFAULT_EFF_ECDSA_CIRCUIT;
this.spartanWasm = new SpartanWasm({ spartanWasm: options?.spartanWasm });
}
async verify(
proof: Uint8Array,
publicInputSer: Uint8Array
): Promise<boolean> {
this.time("Load circuit");
const circuitBin = await loadCircuit(this.circuit);
this.timeEnd("Load circuit");
this.time("Verify public input");
const publicInput = EffEcdsaPubInput.deserialize(publicInputSer);
const isPubInputValid = verifyEffEcdsaPubInput(publicInput);
this.timeEnd("Verify public input");
this.time("Verify NIZK");
await this.spartanWasm.init();
let nizkValid;
try {
nizkValid = await this.spartanWasm.verify(
circuitBin,
proof,
publicInput.circuitPubInput.serialize()
);
} catch (e) {
return false;
}
this.timeEnd("Verify NIZK");
return isPubInputValid && nizkValid;
}
}

View File

@@ -1,5 +1,5 @@
import { Profiler } from "../helpers/profiler";
import { IProver, MerkleProof, NIZK, ProverOptions } from "../types";
import { IProver, MerkleProof, NIZK, ProverConfig, LeafType } from "../types";
import { SpartanWasm } from "../wasm";
import {
bigIntToBytes,
@@ -11,27 +11,27 @@ import {
EffEcdsaPubInput,
EffEcdsaCircuitPubInput
} from "../helpers/efficient_ecdsa";
import {
DEFAULT_MEMBERSHIP_CIRCUIT,
DEFAULT_MEMBERSHIP_WITNESS_GEN_WASM
} from "../config";
/**
* ECDSA Membership Prover
*/
export class MembershipProver extends Profiler implements IProver {
spartanWasm: SpartanWasm;
spartanWasm!: SpartanWasm;
circuit: string;
witnessGenWasm: string;
leafType: LeafType;
constructor(options?: ProverOptions) {
constructor(options: ProverConfig) {
super({ enabled: options?.enableProfiler });
const spartanWasm = new SpartanWasm({ spartanWasm: options?.spartanWasm });
this.spartanWasm = spartanWasm;
this.circuit = options?.circuit || DEFAULT_MEMBERSHIP_CIRCUIT;
this.witnessGenWasm =
options?.witnessGenWasm || DEFAULT_MEMBERSHIP_WITNESS_GEN_WASM;
this.leafType = options.leafType;
this.circuit = options.circuit;
this.witnessGenWasm = options.witnessGenWasm;
}
async initWasm(wasm: SpartanWasm) {
this.spartanWasm = wasm;
this.spartanWasm.init();
}
// @ts-ignore
@@ -81,7 +81,6 @@ export class MembershipProver extends Profiler implements IProver {
const circuitBin = await loadCircuit(this.circuit);
this.timeEnd("Load circuit");
await this.spartanWasm.init();
this.time("Prove");
let proof = await this.spartanWasm.prove(
circuitBin,

View File

@@ -2,16 +2,11 @@ import { SpartanWasm } from "../wasm";
import { bigIntToLeBytes, bytesLeToBigInt } from "./utils";
export class Poseidon {
wasm: SpartanWasm;
constructor(wasm?: SpartanWasm) {
if (typeof wasm === "undefined") {
this.wasm = new SpartanWasm();
} else {
this.wasm = wasm;
}
}
wasm!: SpartanWasm;
constructor() {}
async init() {
async initWasm(wasm: SpartanWasm) {
this.wasm = wasm;
await this.wasm.init();
}
@@ -24,4 +19,12 @@ export class Poseidon {
const result = this.wasm.poseidon(inputsBytes);
return bytesLeToBigInt(result);
}
hashPubKey(pubKey: Buffer): bigint {
const pubKeyX = BigInt("0x" + pubKey.toString("hex").slice(0, 64));
const pubKeyY = BigInt("0x" + pubKey.toString("hex").slice(64, 128));
const pubKeyHash = this.hash([pubKeyX, pubKeyY]);
return pubKeyHash;
}
}

View File

@@ -16,18 +16,6 @@ export class Tree {
this.treeInner = new IncrementalMerkleTree(hash, this.depth, BigInt(0));
}
private hashPubKey(pubKey: Buffer): bigint {
const pubKeyX = BigInt("0x" + pubKey.toString("hex").slice(0, 64));
const pubKeyY = BigInt("0x" + pubKey.toString("hex").slice(64, 128));
const pubKeyHash = this.poseidon.hash([pubKeyX, pubKeyY]);
return pubKeyHash;
}
hashAndInsert(pubKey: Buffer) {
this.insert(this.hashPubKey(pubKey));
}
insert(leaf: bigint) {
this.treeInner.insert(leaf);
}
@@ -36,8 +24,8 @@ export class Tree {
return this.treeInner.root;
}
indexOf(pubKey: Buffer): number {
return this.treeInner.indexOf(this.hashPubKey(pubKey));
indexOf(leaf: bigint): number {
return this.treeInner.indexOf(leaf);
}
createProof(index: number): MerkleProof {

View File

@@ -1,7 +1,7 @@
export * from "./types";
export * from "./helpers/efficient_ecdsa";
export * from "./core/eff_ecdsa_prover";
export * from "./core/membership_prover";
export * from "./core/eff_ecdsa_verifier";
export * from "./helpers/tree";
export * from "./helpers/poseidon";
export * from "./wasm/index";
export * from "./config";

View File

@@ -14,13 +14,17 @@ export interface NIZK {
publicInput: Uint8Array;
}
export interface ProverOptions {
export interface ProverConfig {
proverWasm?: string;
witnessGenWasm?: string;
circuit?: string;
spartanWasm?: string;
witnessGenWasm: string;
circuit: string;
spartanWasm: string;
enableProfiler?: boolean;
leafType: LeafType;
}
export interface WasmConfig {
pathOrUrl: string;
}
export interface VerifyOptions {
@@ -45,5 +49,10 @@ export interface IVerifier {
}
export interface SpartanWasmOptions {
spartanWasm?: string;
spartanWasm: string;
}
export enum LeafType {
PubKeyHash,
Address
}

View File

@@ -2,20 +2,14 @@ import * as wasm from "./wasm";
import _initWeb from "./wasm.js";
import fs from "fs";
import path from "path";
import { SpartanWasmOptions } from "../types";
import { DEFAULT_SPARTAN_WASM } from "../config";
import { WasmConfig } from "../types";
// TODO: Rename this to just Wasm since it includes not only Spartan but also Poseidon
export class SpartanWasm {
private spartanWasmPathOrUrl: any;
constructor(options?: SpartanWasmOptions) {
const defaultWasmPath =
typeof window === "undefined"
? path.join(__dirname, "./build/spartan_wasm_bg.wasm")
: DEFAULT_SPARTAN_WASM;
this.spartanWasmPathOrUrl = options?.spartanWasm || defaultWasmPath;
constructor(config: WasmConfig) {
this.spartanWasmPathOrUrl = config.pathOrUrl;
}
async init() {

View File

@@ -1,47 +0,0 @@
import { EffECDSAProver, EffECDSAVerifier } from "../src/lib";
import { hashPersonalMessage, ecsign } from "@ethereumjs/util";
import * as path from "path";
describe("eff_ecdsa prove and verify", () => {
// Sign message
const privKey = Buffer.from(
"f5b552f608f5b552f608f5b552f6082ff5b552f608f5b552f608f5b552f6082f",
"hex"
);
let msg = Buffer.from("harry potter");
const msgHash = hashPersonalMessage(msg);
const { v, r, s } = ecsign(msgHash, privKey);
const sig = `0x${r.toString("hex")}${s.toString("hex")}${v.toString(16)}`;
// Init prover and verifier
let prover = new EffECDSAProver({
enableProfiler: false
});
let verifier = new EffECDSAVerifier({
enableProfiler: false
});
it("should prove and verify valid signature", async () => {
const { proof, publicInput } = await prover.prove(sig, msg);
const result = await verifier.verify(proof, publicInput);
expect(result).toBe(true);
});
it("verifier should return false when the proof is invalid", async () => {
const { proof, publicInput } = await prover.prove(sig, msg);
proof[0] = proof[0] + 1;
const result = await verifier.verify(proof, publicInput);
expect(result).toBe(false);
});
it("verifier should return false when the public input is invalid", async () => {
const { proof, publicInput } = await prover.prove(sig, msg);
publicInput[0] = publicInput[0] + 1;
const result = await verifier.verify(proof, publicInput);
expect(result).toBe(false);
});
});

View File

@@ -1,13 +1,24 @@
import * as path from "path";
import { MembershipProver, Tree, Poseidon } from "../src/lib";
import { hashPersonalMessage, ecsign, privateToPublic } from "@ethereumjs/util";
import {
MembershipProver,
Tree,
Poseidon,
defaultAddressMembershipConfig,
defaultPubkeyMembershipConfig,
SpartanWasm,
defaultWasmConfig
} from "../src/lib";
import {
hashPersonalMessage,
ecsign,
privateToAddress,
privateToPublic
} from "@ethereumjs/util";
var EC = require("elliptic").ec;
const ec = new EC("secp256k1");
//! Still doesn't pass. Need to fix.
describe("membership prove and verify", () => {
// Init prover
const treeDepth = 10;
const treeDepth = 20;
const privKeys = ["1", "a", "bb", "ccc", "dddd", "ffff"].map(val =>
Buffer.from(val.padStart(64, "0"), "hex")
@@ -16,6 +27,7 @@ describe("membership prove and verify", () => {
// Sign (Use privKeys[0] for proving)
const proverIndex = 0;
const proverPrivKey = privKeys[proverIndex];
let proverAddress: bigint;
let msg = Buffer.from("harry potter");
const msgHash = hashPersonalMessage(msg);
@@ -24,34 +36,83 @@ describe("membership prove and verify", () => {
const sig = `0x${r.toString("hex")}${s.toString("hex")}${v.toString(16)}`;
let poseidon: Poseidon;
let tree: Tree;
let prover: MembershipProver;
let wasm: SpartanWasm;
beforeAll(async () => {
// Init Wasm
wasm = new SpartanWasm(defaultWasmConfig);
// Init Poseidon
poseidon = new Poseidon();
await poseidon.init();
tree = new Tree(treeDepth, poseidon);
prover = new MembershipProver();
// Insert the members into the tree
for (const privKey of privKeys) {
const pubKey = privateToPublic(privKey);
tree.hashAndInsert(pubKey);
}
await poseidon.initWasm(wasm);
});
it("should prove and verify valid signature and merkle proof", async () => {
const index = tree.indexOf(privateToPublic(proverPrivKey));
const merkleProof = tree.createProof(proverIndex);
describe("pubkey_membership prover and verify", () => {
it("should prove and verify valid signature and merkle proof", async () => {
const pubKeyTree = new Tree(treeDepth, poseidon);
const { proof, publicInput } = await prover.prove(
sig,
msgHash,
merkleProof
);
let proverPubKeyHash;
// Insert the members into the tree
for (const privKey of privKeys) {
const pubKey = privateToPublic(privKey);
const pubKeyHash = poseidon.hashPubKey(pubKey);
pubKeyTree.insert(pubKeyHash);
// TODO: Verify the proof
// Set prover's public key hash for the reference below
if (proverPrivKey === privKey) proverPubKeyHash = pubKeyHash;
}
const pubKeyMembershipProver = new MembershipProver(
defaultPubkeyMembershipConfig
);
await pubKeyMembershipProver.initWasm(wasm);
const index = pubKeyTree.indexOf(proverPubKeyHash as bigint);
const merkleProof = pubKeyTree.createProof(index);
const { proof, publicInput } = await pubKeyMembershipProver.prove(
sig,
msgHash,
merkleProof
);
// TODO: Verify the proof
});
});
describe("adddr_membership prover and verify", () => {
it("should prove and verify valid signature and merkle proof", async () => {
const addressTree = new Tree(treeDepth, poseidon);
let proverAddress;
// Insert the members into the tree
for (const privKey of privKeys) {
const address = BigInt(
"0x" + privateToAddress(privKey).toString("hex")
);
addressTree.insert(address);
// Set prover's public key hash for the reference below
if (proverPrivKey === privKey) proverAddress = address;
}
const addressMembershipProver = new MembershipProver(
defaultAddressMembershipConfig
);
await addressMembershipProver.initWasm(wasm);
const index = addressTree.indexOf(proverAddress as bigint);
const merkleProof = addressTree.createProof(index);
const { proof, publicInput } = await addressMembershipProver.prove(
sig,
msgHash,
merkleProof
);
// TODO: Verify the proof
});
});
});