Skip to content

Commit f34c12f

Browse files
committed
use mina blockchain to storage the password tree root
1 parent e4afe58 commit f34c12f

File tree

2 files changed

+219
-35
lines changed

2 files changed

+219
-35
lines changed

src/plugins/passwordTree/plugin/index.ts

Lines changed: 204 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,25 @@
1-
import { Experimental, Field, JsonProof, MerkleTree, Poseidon } from "o1js";
1+
import { Experimental, Field, JsonProof, MerkleTree, Poseidon, PrivateKey, AccountUpdate, Mina } from "o1js";
22
import ProvePasswordInTreeProgram, { PASSWORD_TREE_HEIGHT, PasswordTreePublicInput, PasswordTreeWitness } from "./passwordTreeProgram";
33
import { IMinAuthPlugin, IMinAuthPluginFactory, IMinAuthProver, IMinAuthProverFactory } from 'plugin/pluginType';
44
import { RequestHandler } from "express";
55
import { z } from "zod";
66
import axios from "axios";
7+
import { TreeRootStorageContract } from "./treeRootStorageContract";
8+
import fs from 'fs/promises'
79

810
const PasswordInTreeProofClass = Experimental.ZkProgram.Proof(ProvePasswordInTreeProgram);
911

1012
abstract class TreeStorage {
1113
abstract getRoot(): Promise<Field>;
1214
abstract getWitness(uid: bigint): Promise<undefined | PasswordTreeWitness>;
1315
abstract getRole(uid: bigint): Promise<undefined | string>;
16+
abstract updateUser(uid: bigint, passwordHash: Field, role: string): Promise<void>;
17+
abstract hasUser(uid: bigint): Promise<boolean>;
1418
}
1519

1620
class InMemoryStorage implements TreeStorage {
17-
roles: Map<bigint, string>;
18-
merkleTree: MerkleTree;
19-
20-
constructor(roleMappings: Array<[bigint, Field, string]> = []) {
21-
this.roles = new Map();
22-
this.merkleTree = new MerkleTree(PASSWORD_TREE_HEIGHT);
23-
24-
roleMappings.forEach(([uid, password, role]) => {
25-
this.roles.set(uid, role);
26-
this.merkleTree.setLeaf(uid, Poseidon.hash([password]));
27-
})
28-
}
21+
roles: Map<bigint, string> = new Map();;
22+
merkleTree: MerkleTree = new MerkleTree(PASSWORD_TREE_HEIGHT);
2923

3024
async getRoot() { return this.merkleTree.getRoot(); }
3125

@@ -35,12 +29,161 @@ class InMemoryStorage implements TreeStorage {
3529
}
3630

3731
async getRole(uid: bigint) { return this.roles.get(uid); }
32+
33+
async updateUser(uid: bigint, passwordHash: Field, role: string): Promise<void> {
34+
this.roles.set(uid, role);
35+
this.merkleTree.setLeaf(uid, passwordHash);
36+
}
37+
38+
async hasUser(uid: bigint): Promise<boolean> {
39+
return this.roles.has(uid);
40+
}
41+
}
42+
43+
class PersistentInMemoryStorage extends InMemoryStorage {
44+
readonly file: fs.FileHandle;
45+
46+
async persist() {
47+
const emptyObj: Record<string, { passwordHash: string, role: string }> = {}
48+
const storageObj = Array.from(this.roles.entries())
49+
.reduce((prev, [uid, role]) => {
50+
const passwordHash =
51+
this.merkleTree.getNode(PASSWORD_TREE_HEIGHT, uid).toString();
52+
prev[uid.toString()] = { passwordHash, role };
53+
return prev;
54+
}, emptyObj);
55+
await this.file.write(JSON.stringify(storageObj), 0, 'utf-8');
56+
}
57+
58+
private constructor(
59+
file: fs.FileHandle,
60+
roles: Map<bigint, string>,
61+
merkleTree: MerkleTree) {
62+
super();
63+
64+
this.file = file;
65+
this.roles = roles;
66+
this.merkleTree = merkleTree;
67+
}
68+
69+
static async initialize(path: string): Promise<PersistentInMemoryStorage> {
70+
const handle = await fs.open(path, 'r+');
71+
const content = await handle.readFile('utf-8');
72+
const storageObj: Record<string, { passwordHash: string, role: string }> =
73+
JSON.parse(content);
74+
75+
const roles: Map<bigint, string> = new Map();
76+
const merkleTree: MerkleTree = new MerkleTree(PASSWORD_TREE_HEIGHT);
77+
78+
Object
79+
.entries(storageObj)
80+
.forEach((
81+
[uidStr, { passwordHash: passwordHashStr, role }]) => {
82+
const uid = BigInt(uidStr);
83+
const passwordHash = Field.from(passwordHashStr);
84+
roles.set(uid, role);
85+
merkleTree.setLeaf(uid, passwordHash);
86+
});
87+
88+
return new PersistentInMemoryStorage(handle, roles, merkleTree);
89+
}
90+
91+
async updateUser(uid: bigint, passwordHash: Field, role: string): Promise<void> {
92+
const prevRoot = this.merkleTree.getRoot();
93+
await super.updateUser(uid, passwordHash, role);
94+
const root = this.merkleTree.getRoot();
95+
if (prevRoot.equals(root).toBoolean()) return;
96+
await this.persist();
97+
}
3898
}
3999

40-
const storage = new InMemoryStorage([
41-
[BigInt(0), Field('7555220006856562833147743033256142154591945963958408607501861037584894828141'), 'admin'],
42-
[BigInt(1), Field('21565680844461314807147611702860246336805372493508489110556896454939225549736'), 'member']
43-
]);
100+
class GenericMinaBlockchainStorage<T extends TreeStorage> implements TreeStorage {
101+
private underlyingStorage: T;
102+
private contract: TreeRootStorageContract
103+
private mkTx: (txFn: () => void) => Promise<void>
104+
105+
constructor(
106+
storage: T,
107+
contract: TreeRootStorageContract,
108+
mkTx: (txFn: () => void) => Promise<void>) {
109+
this.underlyingStorage = storage;
110+
this.contract = contract;
111+
this.mkTx = mkTx;
112+
}
113+
114+
async updateTreeRootOnChainIfNecessary() {
115+
const onChain = await this.contract.treeRoot.fetch();
116+
const offChain = await this.underlyingStorage.getRoot();
117+
118+
if (!onChain)
119+
throw "tree root storage contract not deployed";
120+
121+
if (onChain.equals(offChain).toBoolean())
122+
return;
123+
124+
await this.mkTx(() => this.contract.treeRoot.set(offChain));
125+
}
126+
127+
async getRoot() { return this.underlyingStorage.getRoot(); }
128+
129+
async getWitness(uid: bigint) { return this.underlyingStorage.getWitness(uid); }
130+
131+
async getRole(uid: bigint) { return this.underlyingStorage.getRole(uid); }
132+
133+
async updateUser(uid: bigint, passwordHash: Field, role: string): Promise<void> {
134+
await this.underlyingStorage.updateUser(uid, passwordHash, role);
135+
await this.updateTreeRootOnChainIfNecessary();
136+
}
137+
138+
async hasUser(uid: bigint): Promise<boolean> {
139+
return this.underlyingStorage.hasUser(uid);
140+
}
141+
}
142+
143+
async function initializeGenericMinaBlockchainStorage<T extends TreeStorage>(
144+
storage: T,
145+
contractPrivateKey: PrivateKey,
146+
feePayerPrivateKey: PrivateKey
147+
): Promise<GenericMinaBlockchainStorage<T>> {
148+
await TreeRootStorageContract.compile();
149+
const contract = new TreeRootStorageContract(contractPrivateKey.toPublicKey());
150+
const feePayerPublicKey = feePayerPrivateKey.toPublicKey();
151+
152+
const mkTx = async (txFn: () => void): Promise<void> => {
153+
const txn = await Mina.transaction(feePayerPublicKey, txFn);
154+
await txn.prove();
155+
await txn.sign([feePayerPrivateKey, contractPrivateKey]).send();
156+
};
157+
158+
const blockchainStorage = new GenericMinaBlockchainStorage(storage, contract, mkTx);
159+
160+
if (contract.account.isNew.get()) {
161+
const treeRoot = await storage.getRoot();
162+
await mkTx(() => {
163+
AccountUpdate.fundNewAccount(feePayerPublicKey);
164+
contract.treeRoot.set(treeRoot);
165+
contract.deploy();
166+
});
167+
} else {
168+
await blockchainStorage.updateTreeRootOnChainIfNecessary();
169+
}
170+
171+
return blockchainStorage;
172+
}
173+
174+
class MinaBlockchainStorage
175+
extends GenericMinaBlockchainStorage<PersistentInMemoryStorage>{
176+
static async initialize(
177+
path: string,
178+
contractPrivateKey: PrivateKey,
179+
feePayerPrivateKey: PrivateKey) {
180+
const storage = await PersistentInMemoryStorage.initialize(path);
181+
return initializeGenericMinaBlockchainStorage(
182+
storage,
183+
contractPrivateKey,
184+
feePayerPrivateKey);
185+
}
186+
}
44187

45188
export class SimplePasswordTreePlugin implements IMinAuthPlugin<bigint, string>{
46189
readonly verificationKey: string;
@@ -54,7 +197,7 @@ export class SimplePasswordTreePlugin implements IMinAuthPlugin<bigint, string>{
54197
}
55198

56199
const uid = BigInt(req.params['uid']);
57-
const witness = await storage.getWitness(uid);
200+
const witness = await this.storage.getWitness(uid);
58201

59202
if (!witness) {
60203
resp
@@ -71,8 +214,17 @@ export class SimplePasswordTreePlugin implements IMinAuthPlugin<bigint, string>{
71214
return;
72215
}
73216

74-
const root = await storage.getRoot();
217+
const root = await this.storage.getRoot();
75218
return resp.status(200).json(root);
219+
},
220+
"/setPassword/:uid": async (req, resp) => {
221+
const uid = BigInt(req.params['uid']);
222+
const { passwordHashStr }: { passwordHashStr: string } = req.body;
223+
const passwordHash = Field.from(passwordHashStr);
224+
if (!await this.storage.hasUser(uid))
225+
throw "user doesn't exist";
226+
const role = await this.storage.getRole(uid);
227+
this.storage.updateUser(uid, passwordHash, role!);
76228
}
77229
};
78230

@@ -81,46 +233,63 @@ export class SimplePasswordTreePlugin implements IMinAuthPlugin<bigint, string>{
81233
async verifyAndGetOutput(uid: bigint, jsonProof: JsonProof):
82234
Promise<string> {
83235
const proof = PasswordInTreeProofClass.fromJSON(jsonProof);
84-
const expectedWitness = await storage.getWitness(uid);
85-
const expectedRoot = await storage.getRoot();
236+
const expectedWitness = await this.storage.getWitness(uid);
237+
const expectedRoot = await this.storage.getRoot();
86238
if (proof.publicInput.witness != expectedWitness ||
87239
proof.publicInput.root != expectedRoot) {
88240
throw 'public input invalid';
89241
}
90-
const role = await storage.getRole(uid);
242+
const role = await this.storage.getRole(uid);
91243
if (!role) { throw 'unknown public input'; }
92244
return role;
93245
};
94246

95-
constructor(verificationKey: string, roles: Array<[bigint, Field, string]>) {
247+
constructor(verificationKey: string, storage: MinaBlockchainStorage) {
96248
this.verificationKey = verificationKey;
97-
this.storage = new InMemoryStorage(roles);
249+
this.storage = storage;
98250
}
99251

100-
static async initialize(configuration: { roles: Array<[bigint, Field, string]> })
101-
: Promise<SimplePasswordTreePlugin> {
252+
static async initialize(configuration: {
253+
storageFile: string,
254+
contractPrivateKey: string,
255+
feePayerPrivateKey: string
256+
}): Promise<SimplePasswordTreePlugin> {
102257
const { verificationKey } = await ProvePasswordInTreeProgram.compile();
103-
return new SimplePasswordTreePlugin(verificationKey, configuration.roles);
258+
const storage = await MinaBlockchainStorage
259+
.initialize(
260+
configuration.storageFile,
261+
PrivateKey.fromBase58(configuration.contractPrivateKey),
262+
PrivateKey.fromBase58(configuration.feePayerPrivateKey)
263+
)
264+
return new SimplePasswordTreePlugin(verificationKey, storage);
104265
}
105266

106-
static readonly configurationSchema: z.ZodType<{ roles: Array<[bigint, Field, string]> }> =
267+
static readonly configurationSchema:
268+
z.ZodType<{
269+
storageFile: string,
270+
contractPrivateKey: string,
271+
feePayerPrivateKey: string
272+
}> =
107273
z.object({
108-
roles: z.array(z.tuple([
109-
z.bigint(),
110-
z.custom<Field>((val) => typeof val === "string" ? /^[0-9]+$/.test(val) : false),
111-
z.string()]))
274+
storageFile: z.string(),
275+
contractPrivateKey: z.string(),
276+
feePayerPrivateKey: z.string()
112277
})
113278
}
114279

115280
SimplePasswordTreePlugin satisfies
116281
IMinAuthPluginFactory<
117282
IMinAuthPlugin<bigint, string>,
118-
{ roles: Array<[bigint, Field, string]> },
283+
{
284+
storageFile: string,
285+
contractPrivateKey: string,
286+
feePayerPrivateKey: string
287+
},
119288
bigint,
120289
string>;
121290

122291
export type SimplePasswordTreeProverConfiguration = {
123-
apiServer: URL
292+
apiServer: URL,
124293
}
125294

126295
export class SimplePasswordTreeProver implements
@@ -131,7 +300,7 @@ export class SimplePasswordTreeProver implements
131300
async prove(publicInput: PasswordTreePublicInput, secretInput: Field)
132301
: Promise<JsonProof> {
133302
const proof = await ProvePasswordInTreeProgram.baseCase(
134-
publicInput, Field(secretInput));
303+
publicInput, Field.from(secretInput));
135304
return proof.toJSON();
136305
}
137306

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Permissions, DeployArgs, Experimental, Field, JsonProof, MerkleTree, Poseidon, PrivateKey, PublicKey, SmartContract, State, method, state, verify, Mina, AccountUpdate } from "o1js";
2+
3+
export class TreeRootStorageContract extends SmartContract {
4+
@state(Field) treeRoot = State<Field>();
5+
6+
deploy(args?: DeployArgs) {
7+
super.deploy(args);
8+
9+
this.account.permissions.set({
10+
...Permissions.allImpossible(),
11+
editState: Permissions.signature(),
12+
access: Permissions.signature(),
13+
});
14+
}
15+
}

0 commit comments

Comments
 (0)