From f188b9f491a213f2f491b7f44aeef240f1d73082 Mon Sep 17 00:00:00 2001 From: ronantakizawa Date: Wed, 26 Feb 2025 15:01:05 -0700 Subject: [PATCH] feat: add blinding commitment and merkle proofs --- src/imported/blinding-commitment.ts | 122 ++++++++++++++ src/imported/merkle-proof.ts | 252 ++++++++++++++++++++++++++++ src/index.ts | 2 + 3 files changed, 376 insertions(+) create mode 100644 src/imported/blinding-commitment.ts create mode 100644 src/imported/merkle-proof.ts diff --git a/src/imported/blinding-commitment.ts b/src/imported/blinding-commitment.ts new file mode 100644 index 0000000..d7f8c38 --- /dev/null +++ b/src/imported/blinding-commitment.ts @@ -0,0 +1,122 @@ +import { Field, Poseidon, Provable, Struct, ZkProgram } from 'o1js'; +import { Credential } from '../credential-index.js'; +import { DynamicRecord } from '../dynamic/dynamic-record.js'; + +/** + * BlindingCommitment implementation + * + * Allows a user to commit to any data without revealing it, + * with optional blinding randomness for stronger hiding properties. + */ +export const BlindingCommitment = { + /** + * Create a credential specification for blinding commitments + * + * @param options.maxEntries Maximum number of entries in the data record + * @returns A credential spec for blinding commitments + */ + async Credential({ maxEntries = 100 }: { maxEntries?: number } = {}) { + // Define the program for creating the commitment + const CommitmentProgram = ZkProgram({ + name: 'BlindingCommitment', + publicInput: { + ownerPublicKey: Provable.PublicKey, + blinding: Field, + }, + publicOutput: Struct({ + owner: Provable.PublicKey, + data: { + commitment: Field, + }, + }), + methods: { + create: { + privateInputs: [Provable.Any], + method(publicInput: { ownerPublicKey: Provable.PublicKey; blinding: Field }, privateData: any) { + // Create commitment as hash of data and blinding factor + const commitment = Poseidon.hash([ + ...Provable.toFields(privateData), + publicInput.blinding, + ]); + + return { + publicOutput: { + owner: publicInput.ownerPublicKey, + data: { + commitment, + }, + }, + }; + }, + }, + }, + }); + + // Create imported credential specification from program + const ImportedCred = await Credential.Imported.fromProgram(CommitmentProgram); + + // Compile the verification key + const verificationKey = await ImportedCred.compile(); + + return { + spec: ImportedCred.spec, + verificationKey, + + /** + * Create a blinding commitment credential + * + * @param params.owner Owner of the credential + * @param params.data Data to commit to + * @param params.blinding Optional blinding factor (defaults to random) + * @returns A credential containing the commitment + */ + async create(params: { + owner: { toPublicKey(): any }; + data: any; + blinding?: Field; + }) { + const { owner, data, blinding = Field.random() } = params; + + // Dynamically handle any data structure + const Dynamic = DynamicRecord(data, { maxEntries }); + const dynamicData = Dynamic.from(data); + + // Generate the proof + const { proof } = await CommitmentProgram.create( + { + ownerPublicKey: owner.toPublicKey(), + blinding, + }, + dynamicData + ); + + return ImportedCred.fromProof(proof, verificationKey); + }, + + /** + * Verify a commitment against original data and blinding + * + * @param credential The commitment credential + * @param data Original data to verify + * @param blinding Original blinding factor + * @returns true if the commitment matches the data and blinding + */ + async verify(credential: any, data: any, blinding: Field): Promise { + await Credential.validate(credential); + + // Recreate the commitment + const Dynamic = DynamicRecord(data, { maxEntries }); + const dynamicData = Dynamic.from(data); + + const expectedCommitment = Poseidon.hash([ + ...Provable.toFields(dynamicData), + blinding, + ]); + + // Compare with the commitment in the credential + const actualCommitment = credential.credential.data.commitment; + return expectedCommitment.equals(actualCommitment).toBoolean(); + } + }; + } +}; \ No newline at end of file diff --git a/src/imported/merkle-proof.ts b/src/imported/merkle-proof.ts new file mode 100644 index 0000000..4b3b0ea --- /dev/null +++ b/src/imported/merkle-proof.ts @@ -0,0 +1,252 @@ +import { + Field, + MerkleMap, + MerkleTree, + MerkleWitness, + Poseidon, + Provable, + Struct, + ZkProgram, + } from 'o1js'; + import { Credential } from '../credential-index.js'; + import { DynamicRecord } from '../dynamic/dynamic-record.js'; + + /** + * Create a MerkleWitness class with the specified height + */ + function createMerkleWitness(height: number) { + return MerkleWitness(height); + } + + /** + * MerkleProof implementation for proving membership in Merkle trees and maps + */ + export const MerkleProof = { + /** + * Create a credential specification for MerkleTree proofs + * + * @param options.treeHeight Height of the Merkle tree + * @param options.maxEntries Maximum number of entries in the data record + * @returns A credential spec for MerkleTree proofs + */ + async Tree({ + treeHeight = 20, + maxEntries = 100 + }: { + treeHeight?: number; + maxEntries?: number; + } = {}) { + // Create witness for the data's position in the tree + const Witness = createMerkleWitness(treeHeight); + + // Define the program for creating the Merkle proof + const MerkleProofProgram = ZkProgram({ + name: 'MerkleTreeProof', + publicInput: { + ownerPublicKey: Provable.PublicKey, + root: Field, + }, + publicOutput: Struct({ + owner: Provable.PublicKey, + data: { + root: Field, + }, + }), + methods: { + prove: { + privateInputs: [Witness, Provable.Any], + method(publicInput: { ownerPublicKey: Provable.PublicKey; root: Field }, witness: any, data: any) { + // Hash the data to get the leaf value + const dataHash = Poseidon.hash(Provable.toFields(data)); + + // Verify the witness path leads to the claimed root + const computedRoot = witness.calculateRoot(dataHash); + computedRoot.assertEquals(publicInput.root); + + return { + publicOutput: { + owner: publicInput.ownerPublicKey, + data: { + root: publicInput.root, + }, + }, + }; + }, + }, + }, + }); + + // Create imported credential specification from program + const ImportedCred = await Credential.Imported.fromProgram(MerkleProofProgram); + + // Compile the verification key + const verificationKey = await ImportedCred.compile(); + + return { + spec: ImportedCred.spec, + verificationKey, + + /** + * Create a credential proving inclusion in a Merkle tree + * + * @param params.owner Credential owner + * @param params.tree MerkleTree containing the data + * @param params.data The data to prove inclusion for + * @param params.index Index of the data in the tree + * @returns A credential containing the Merkle root and proof + */ + async create(params: { + owner: { toPublicKey(): any }; + tree: MerkleTree; + data: any; + index: bigint; + }) { + const { owner, tree, data, index } = params; + + // Verify that the tree height matches our expected height + if (tree.height !== treeHeight) { + throw new Error(`Tree height mismatch: expected ${treeHeight}, got ${tree.height}`); + } + + // Create witness for the data's position in the tree + const witness = tree.getWitness(index); + + // Dynamically handle any data structure + const Dynamic = DynamicRecord(data, { maxEntries }); + const dynamicData = Dynamic.from(data); + + // Hash the data to get the leaf value + const dataHash = Poseidon.hash(Provable.toFields(dynamicData)); + + // Verify the leaf matches what's in the tree + const root = tree.getRoot(); + const WitnessClass = Witness; // Use the class directly + const pathWitness = new WitnessClass(witness); + const expectedRoot = pathWitness.calculateRoot(dataHash); + + if (!root.equals(expectedRoot).toBoolean()) { + throw new Error('Data hash does not match the tree leaf at the given index'); + } + + // Generate the proof + const { proof } = await MerkleProofProgram.prove( + { + ownerPublicKey: owner.toPublicKey(), + root, + }, + pathWitness, + dynamicData + ); + + return ImportedCred.fromProof(proof, verificationKey); + } + }; + }, + + /** + * Create a credential specification for MerkleMap proofs + * + * @param options.maxEntries Maximum number of entries in the data record + * @returns A credential spec for MerkleMap proofs + */ + async Map({ maxEntries = 100 }: { maxEntries?: number } = {}) { + // Define the program for creating the Merkle map proof + const MerkleMapProofProgram = ZkProgram({ + name: 'MerkleMapProof', + publicInput: { + ownerPublicKey: Provable.PublicKey, + root: Field, + key: Field, + }, + publicOutput: Struct({ + owner: Provable.PublicKey, + data: { + root: Field, + key: Field, + }, + }), + methods: { + prove: { + privateInputs: [Provable.Any, Provable.Witness], + method(publicInput: { ownerPublicKey: Provable.PublicKey; root: Field; key: Field }, data: any, witness: any) { + // Hash the data to get the value + const dataHash = Poseidon.hash(Provable.toFields(data)); + + // Verify the witness path leads to the claimed root + const [computedRoot, computedKey] = witness.computeRootAndKey(dataHash); + computedRoot.assertEquals(publicInput.root); + computedKey.assertEquals(publicInput.key); + + return { + publicOutput: { + owner: publicInput.ownerPublicKey, + data: { + root: publicInput.root, + key: publicInput.key, + }, + }, + }; + }, + }, + }, + }); + + // Create imported credential specification from program + const ImportedCred = await Credential.Imported.fromProgram(MerkleMapProofProgram); + + // Compile the verification key + const verificationKey = await ImportedCred.compile(); + + return { + spec: ImportedCred.spec, + verificationKey, + + /** + * Create a credential proving inclusion in a Merkle map + * + * @param params.owner Credential owner + * @param params.map MerkleMap containing the data + * @param params.data The data to prove inclusion for + * @param params.key Key of the data in the map + * @returns A credential containing the Merkle root and proof + */ + async create(params: { + owner: { toPublicKey(): any }; + map: MerkleMap; + data: any; + key: Field; + }) { + const { owner, map, data, key } = params; + + // Get witness for the data's position in the map + const witness = map.getWitness(key); + + // Dynamically handle any data structure + const Dynamic = DynamicRecord(data, { maxEntries }); + const dynamicData = Dynamic.from(data); + + // Hash the data to get the value + const dataHash = Poseidon.hash(Provable.toFields(dynamicData)); + + // Verify the data matches what's in the map + const value = map.get(key); + if (!value.equals(dataHash).toBoolean()) { + throw new Error('Data hash does not match the map value at the given key'); + } + + // Generate the proof + const { proof } = await MerkleMapProofProgram.prove( + { + ownerPublicKey: owner.toPublicKey(), + root: map.getRoot(), + key, + }, + dynamicData, + witness + ); + + return ImportedCred.fromProof(proof, verificationKey); + } + }; + } + }; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1117eaf..eff2348 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,8 @@ export { export { Schema } from './dynamic/schema.ts'; export { PrettyPrinter } from './pretty-printer.ts'; export { Numeric } from './dynamic/gadgets-numeric.ts'; +export { BlindingCommitment } from './imported/blinding-commitment.ts'; +export { MerkleProof } from './imported/merkle-proof.ts'; export type { StoredCredentialJSON,