1- import { Experimental , Field , JsonProof , MerkleTree , Poseidon } from "o1js" ;
1+ import { Experimental , Field , JsonProof , MerkleTree , Poseidon , PrivateKey , AccountUpdate , Mina } from "o1js" ;
22import ProvePasswordInTreeProgram , { PASSWORD_TREE_HEIGHT , PasswordTreePublicInput , PasswordTreeWitness } from "./passwordTreeProgram" ;
33import { IMinAuthPlugin , IMinAuthPluginFactory , IMinAuthProver , IMinAuthProverFactory } from 'plugin/pluginType' ;
44import { RequestHandler } from "express" ;
55import { z } from "zod" ;
66import axios from "axios" ;
7+ import { TreeRootStorageContract } from "./treeRootStorageContract" ;
8+ import fs from 'fs/promises'
79
810const PasswordInTreeProofClass = Experimental . ZkProgram . Proof ( ProvePasswordInTreeProgram ) ;
911
1012abstract 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
1620class 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
45188export 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
115280SimplePasswordTreePlugin 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
122291export type SimplePasswordTreeProverConfiguration = {
123- apiServer : URL
292+ apiServer : URL ,
124293}
125294
126295export 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
0 commit comments