@silvana-one/upgradable 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,497 @@
1
+ import {
2
+ Struct,
3
+ Field,
4
+ PublicKey,
5
+ Signature,
6
+ ZkProgram,
7
+ Poseidon,
8
+ SelfProof,
9
+ UInt32,
10
+ Experimental,
11
+ DynamicProof,
12
+ FeatureFlags,
13
+ Void,
14
+ Bool,
15
+ Nullifier,
16
+ PrivateKey,
17
+ Provable,
18
+ Encoding,
19
+ } from "o1js";
20
+ import { Storage } from "@silvana-one/storage";
21
+ import { PublicKeyOption } from "./upgradable.js";
22
+ import MinaSigner from "mina-signer";
23
+
24
+ export {
25
+ ValidatorsList,
26
+ UpgradeAuthorityDatabase,
27
+ ValidatorsState,
28
+ ValidatorsDecision,
29
+ ValidatorDecisionType,
30
+ ValidatorsDecisionState,
31
+ ValidatorsVoting,
32
+ ValidatorsVotingProof,
33
+ ValidatorsVotingNativeProof,
34
+ UpgradeDatabaseState,
35
+ UpgradeDatabaseStatePacked,
36
+ ChainId,
37
+ };
38
+
39
+ const { IndexedMerkleMap } = Experimental;
40
+ type IndexedMerkleMap = Experimental.IndexedMerkleMap;
41
+
42
+ const VALIDATORS_LIST_HEIGHT = 10;
43
+ const UPGRADE_AUTHORITY_DATABASE_HEIGHT = 20;
44
+
45
+ /**
46
+ * The `ValidatorsList` is an indexed Merkle map used to store the list of validators.
47
+ */
48
+ class ValidatorsList extends IndexedMerkleMap(VALIDATORS_LIST_HEIGHT) {}
49
+
50
+ /**
51
+ * The `UpgradeAuthorityDatabase` is an indexed Merkle map used to manage upgrade proposals.
52
+ */
53
+ class UpgradeAuthorityDatabase extends IndexedMerkleMap(
54
+ UPGRADE_AUTHORITY_DATABASE_HEIGHT
55
+ ) {}
56
+
57
+ /** Chain IDs following Auro Wallet naming conventions. */
58
+ const ChainId = {
59
+ "mina:mainnet": fieldFromString("mina:mainnet"),
60
+ "mina:devnet": fieldFromString("mina:devnet"),
61
+ "zeko:mainnet": fieldFromString("zeko:mainnet"),
62
+ "zeko:devnet": fieldFromString("zeko:devnet"),
63
+ };
64
+ type ChainId = keyof typeof ChainId;
65
+
66
+ /** Validator decision types for upgrade proposals. */
67
+ const ValidatorDecisionType = {
68
+ updateDatabase: fieldFromString("updateDatabase"),
69
+ updateValidatorsList: fieldFromString("updateValidatorsList"),
70
+ } as const;
71
+ type ValidatorDecisionType = keyof typeof ValidatorDecisionType;
72
+
73
+ /**
74
+ * Represents the state of the validators.
75
+ */
76
+ class ValidatorsState extends Struct({
77
+ /** Chain ID (e.g., 'mina:mainnet') */
78
+ chainId: Field,
79
+ /** Merkle root of the ValidatorsList */
80
+ root: Field,
81
+ /** Number of validators */
82
+ count: UInt32,
83
+ }) {
84
+ /**
85
+ * Asserts that two `ValidatorsState` instances are equal.
86
+ * @param a First `ValidatorsState` instance.
87
+ * @param b Second `ValidatorsState` instance.
88
+ */
89
+ static assertEquals(a: ValidatorsState, b: ValidatorsState) {
90
+ a.chainId.assertEquals(b.chainId);
91
+ a.root.assertEquals(b.root);
92
+ a.count.assertEquals(b.count);
93
+ }
94
+
95
+ /**
96
+ * Computes the hash of the validators state.
97
+ * @returns Hash of the current state.
98
+ */
99
+ hash() {
100
+ return Poseidon.hashPacked(ValidatorsState, this);
101
+ }
102
+
103
+ /**
104
+ * Returns an empty `ValidatorsState`.
105
+ * @returns An empty `ValidatorsState` instance.
106
+ */
107
+ static empty() {
108
+ return new ValidatorsState({
109
+ chainId: Field(0),
110
+ root: Field(0),
111
+ count: UInt32.zero,
112
+ });
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Represents the packed state of the upgrade database.
118
+ */
119
+ class UpgradeDatabaseStatePacked extends Struct({
120
+ /** Root of the UpgradeAuthority database */
121
+ root: Field,
122
+ /** Storage information (e.g., IPFS hash) */
123
+ storage: Storage,
124
+ /** X-coordinate of the next upgrade authority's public key */
125
+ nextUpgradeAuthorityX: Field,
126
+ /** Packed data containing version, validFrom, and flags */
127
+ data: Field,
128
+ }) {}
129
+
130
+ /**
131
+ * Represents the state of the upgrade database.
132
+ */
133
+ class UpgradeDatabaseState extends Struct({
134
+ /** Root of the UpgradeAuthority database */
135
+ root: Field,
136
+ /** Storage information (e.g., IPFS hash) */
137
+ storage: Storage,
138
+ /** Optional public key of the next upgrade authority */
139
+ nextUpgradeAuthority: PublicKeyOption,
140
+ /** Version of the UpgradeAuthorityDatabase */
141
+ version: UInt32,
142
+ /** Slot when the UpgradeAuthority is valid from */
143
+ validFrom: UInt32,
144
+ }) {
145
+ /**
146
+ * Asserts that two `UpgradeDatabaseState` instances are equal.
147
+ * @param a First `UpgradeDatabaseState` instance.
148
+ * @param b Second `UpgradeDatabaseState` instance.
149
+ */
150
+ static assertEquals(a: UpgradeDatabaseState, b: UpgradeDatabaseState) {
151
+ a.root.assertEquals(b.root);
152
+ Storage.assertEquals(a.storage, b.storage);
153
+ a.nextUpgradeAuthority.value.assertEquals(b.nextUpgradeAuthority.value);
154
+ a.nextUpgradeAuthority.isSome.assertEquals(b.nextUpgradeAuthority.isSome);
155
+ a.version.assertEquals(b.version);
156
+ }
157
+
158
+ /**
159
+ * Returns an empty `UpgradeDatabaseState`.
160
+ * @returns An empty `UpgradeDatabaseState` instance.
161
+ */
162
+ static empty() {
163
+ return new UpgradeDatabaseState({
164
+ root: new UpgradeAuthorityDatabase().root,
165
+ storage: Storage.empty(),
166
+ nextUpgradeAuthority: PublicKeyOption.none(),
167
+ version: UInt32.zero,
168
+ validFrom: UInt32.MAXINT(),
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Packs the `UpgradeDatabaseState` into a `UpgradeDatabaseStatePacked`.
174
+ * @returns A packed representation of the upgrade database state.
175
+ */
176
+ pack(): UpgradeDatabaseStatePacked {
177
+ const nextUpgradeAuthorityX = this.nextUpgradeAuthority.value.x;
178
+ const data = Field.fromBits([
179
+ ...this.version.value.toBits(32),
180
+ ...this.validFrom.value.toBits(32),
181
+ this.nextUpgradeAuthority.value.isOdd,
182
+ this.nextUpgradeAuthority.isSome,
183
+ ]);
184
+ return new UpgradeDatabaseStatePacked({
185
+ root: this.root,
186
+ storage: this.storage,
187
+ nextUpgradeAuthorityX,
188
+ data,
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Unpacks a `UpgradeDatabaseStatePacked` into a `UpgradeDatabaseState`.
194
+ * @param packed The packed upgrade database state.
195
+ * @returns An unpacked `UpgradeDatabaseState` instance.
196
+ */
197
+ static unpack(packed: UpgradeDatabaseStatePacked): UpgradeDatabaseState {
198
+ const bits = packed.data.toBits(66);
199
+ const versionBits = bits.slice(0, 32);
200
+ const validFromBits = bits.slice(32, 64);
201
+ const isOddBit = bits[64];
202
+ const isSomeBit = bits[65];
203
+ const version = UInt32.Unsafe.fromField(Field.fromBits(versionBits));
204
+ const validFrom = UInt32.Unsafe.fromField(Field.fromBits(validFromBits));
205
+ const nextUpgradeAuthority = PublicKeyOption.from(
206
+ PublicKey.from({ x: packed.nextUpgradeAuthorityX, isOdd: isOddBit })
207
+ );
208
+ nextUpgradeAuthority.isSome = isSomeBit;
209
+ return new UpgradeDatabaseState({
210
+ root: packed.root,
211
+ storage: packed.storage,
212
+ nextUpgradeAuthority,
213
+ version,
214
+ validFrom,
215
+ });
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Represents a decision made by the validators.
221
+ */
222
+ class ValidatorsDecision extends Struct({
223
+ /** Message to be signed when producing the nullifier, also serves as the nonce to prevent replay attacks */
224
+ message: Field,
225
+ /** Type of decision (e.g., 'updateDatabase') */
226
+ decisionType: Field,
227
+ /** UpgradeAuthority contract address */
228
+ contractAddress: PublicKey,
229
+ /** Chain ID */
230
+ chainId: Field,
231
+ /** Current validators state */
232
+ validators: ValidatorsState,
233
+ /** Current upgrade database state */
234
+ upgradeDatabase: UpgradeDatabaseState,
235
+ /** Proposed update to validators state */
236
+ updateValidatorsList: ValidatorsState,
237
+ /** Slot when decision expires */
238
+ expiry: UInt32,
239
+ }) {
240
+ /**
241
+ * Asserts that two `ValidatorsDecision` instances are equal.
242
+ * @param a First `ValidatorsDecision` instance.
243
+ * @param b Second `ValidatorsDecision` instance.
244
+ */
245
+ static assertEquals(a: ValidatorsDecision, b: ValidatorsDecision) {
246
+ a.message.assertEquals(b.message);
247
+ a.decisionType.assertEquals(b.decisionType);
248
+ a.contractAddress.assertEquals(b.contractAddress);
249
+ a.chainId.assertEquals(b.chainId);
250
+ ValidatorsState.assertEquals(a.validators, b.validators);
251
+ UpgradeDatabaseState.assertEquals(a.upgradeDatabase, b.upgradeDatabase);
252
+ a.expiry.assertEquals(b.expiry);
253
+ }
254
+
255
+ createNullifierMessage(): Field[] {
256
+ return [this.message, ...ValidatorsDecision.toFields(this)];
257
+ }
258
+
259
+ createJsonNullifier(params: {
260
+ network: "mainnet" | "testnet";
261
+ privateKey: PrivateKey;
262
+ }) {
263
+ const { network, privateKey } = params;
264
+ const minaSigner = new MinaSigner({ network });
265
+ const message = this.createNullifierMessage();
266
+ const nullifier = minaSigner.createNullifier(
267
+ message.map((field) => field.toBigInt()),
268
+ privateKey.toBase58()
269
+ );
270
+ return nullifier;
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Represents the state of a validators decision during the voting process.
276
+ */
277
+ class ValidatorsDecisionState extends Struct({
278
+ /** The validators' decision */
279
+ decision: ValidatorsDecision,
280
+ /** Indexed Merkle Map root of the validators who have voted */
281
+ alreadyVoted: Field,
282
+ /** Number of votes in favor of the decision */
283
+ yesVotes: UInt32,
284
+ /** Number of votes against the decision */
285
+ noVotes: UInt32,
286
+ /** Number of votes of abstention */
287
+ abstainVotes: UInt32,
288
+ }) {
289
+ static startVoting(decision: ValidatorsDecision) {
290
+ return new ValidatorsDecisionState({
291
+ decision,
292
+ alreadyVoted: new ValidatorsList().root,
293
+ yesVotes: UInt32.zero,
294
+ noVotes: UInt32.zero,
295
+ abstainVotes: UInt32.zero,
296
+ });
297
+ }
298
+ /**
299
+ * Records a vote
300
+ * @param validatorNullifier The nullifier of the validator.
301
+ * @param validatorsList The ValidatorsList containing authorized validators.
302
+ * @param votedList The ValidatorsList tracking who has already voted.
303
+ * @param yes Whether this is a "yes" vote.
304
+ * @param no Whether this is a "no" vote.
305
+ * @param abstain Whether this is an "abstain" vote.
306
+ * @param signature The signature of the validator.
307
+ * @returns A new `ValidatorsDecisionState` reflecting the vote.
308
+ */
309
+ vote(
310
+ validatorNullifier: Nullifier,
311
+ validatorsList: ValidatorsList,
312
+ votedList: ValidatorsList,
313
+ yes: Bool,
314
+ no: Bool,
315
+ abstain: Bool,
316
+ signature: Signature
317
+ ) {
318
+ const publicKey = validatorNullifier.getPublicKey();
319
+ const key = validatorNullifier.key();
320
+ validatorNullifier.verify(this.decision.createNullifierMessage());
321
+
322
+ const previousVotesCount = this.yesVotes
323
+ .add(this.noVotes)
324
+ .add(this.abstainVotes);
325
+ const yesVotes = this.yesVotes.add(
326
+ Provable.if(yes, UInt32.from(1), UInt32.from(0))
327
+ );
328
+ const noVotes = this.noVotes.add(
329
+ Provable.if(no, UInt32.from(1), UInt32.from(0))
330
+ );
331
+ const abstainVotes = this.abstainVotes.add(
332
+ Provable.if(abstain, UInt32.from(1), UInt32.from(0))
333
+ );
334
+ // Ensure exactly one vote type is selected
335
+ previousVotesCount
336
+ .add(UInt32.from(1))
337
+ .assertEquals(yesVotes.add(noVotes).add(abstainVotes));
338
+
339
+ const hash = Poseidon.hashPacked(PublicKey, publicKey);
340
+ validatorsList.root.assertEquals(this.decision.validators.root);
341
+ validatorsList
342
+ .get(hash)
343
+ .assertBool("Wrong ValidatorsList format")
344
+ .assertTrue("Validator doesn't have authority to sign");
345
+ signature
346
+ .verify(publicKey, ValidatorsDecision.toFields(this.decision))
347
+ .assertTrue("Wrong validator signature");
348
+ this.decision.validators.root.assertEquals(validatorsList.root);
349
+ votedList.root.assertEquals(this.alreadyVoted);
350
+ votedList.insert(key, Field(1));
351
+ return new ValidatorsDecisionState({
352
+ decision: this.decision,
353
+ alreadyVoted: votedList.root,
354
+ yesVotes,
355
+ noVotes,
356
+ abstainVotes,
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Asserts that two `ValidatorsDecisionState` instances are equal.
362
+ * @param a First `ValidatorsDecisionState` instance.
363
+ * @param b Second `ValidatorsDecisionState` instance.
364
+ */
365
+ static assertEquals(a: ValidatorsDecisionState, b: ValidatorsDecisionState) {
366
+ ValidatorsDecision.assertEquals(a.decision, b.decision);
367
+ a.alreadyVoted.assertEquals(b.alreadyVoted);
368
+ a.yesVotes.assertEquals(b.yesVotes);
369
+ a.noVotes.assertEquals(b.noVotes);
370
+ a.abstainVotes.assertEquals(b.abstainVotes);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * The `ValidatorsVoting` ZkProgram implements the voting logic for validators.
376
+ */
377
+ const ValidatorsVoting = ZkProgram({
378
+ name: "ValidatorsVoting",
379
+ publicInput: ValidatorsDecisionState,
380
+ publicOutput: ValidatorsDecisionState,
381
+
382
+ methods: {
383
+ /**
384
+ * Starts the voting process for a decision.
385
+ */
386
+ startVoting: {
387
+ privateInputs: [ValidatorsDecision],
388
+
389
+ async method(
390
+ state: ValidatorsDecisionState,
391
+ decision: ValidatorsDecision
392
+ ) {
393
+ const calculatedState = ValidatorsDecisionState.startVoting(decision);
394
+ ValidatorsDecisionState.assertEquals(state, calculatedState);
395
+ return { publicOutput: calculatedState };
396
+ },
397
+ },
398
+ /**
399
+ * Records a vote
400
+ */
401
+ vote: {
402
+ privateInputs: [
403
+ ValidatorsDecision,
404
+ Nullifier,
405
+ ValidatorsList,
406
+ ValidatorsList,
407
+ Bool,
408
+ Bool,
409
+ Bool,
410
+ Signature,
411
+ ],
412
+
413
+ async method(
414
+ state: ValidatorsDecisionState,
415
+ decision: ValidatorsDecision,
416
+ nullifier: Nullifier,
417
+ validatorsList: ValidatorsList,
418
+ votedList: ValidatorsList,
419
+ yes: Bool,
420
+ no: Bool,
421
+ abstain: Bool,
422
+ signature: Signature
423
+ ) {
424
+ const calculatedState = state.vote(
425
+ nullifier,
426
+ validatorsList,
427
+ votedList,
428
+ yes,
429
+ no,
430
+ abstain,
431
+ signature
432
+ );
433
+ return { publicOutput: calculatedState };
434
+ },
435
+ },
436
+
437
+ /**
438
+ * Merges two `ValidatorsDecisionState` proofs.
439
+ */
440
+ merge: {
441
+ privateInputs: [SelfProof, SelfProof],
442
+
443
+ async method(
444
+ state: ValidatorsDecisionState,
445
+ proof1: SelfProof<ValidatorsDecisionState, ValidatorsDecisionState>,
446
+ proof2: SelfProof<ValidatorsDecisionState, ValidatorsDecisionState>
447
+ ) {
448
+ proof1.verify();
449
+ proof2.verify();
450
+ ValidatorsDecisionState.assertEquals(state, proof1.publicInput);
451
+ ValidatorsDecisionState.assertEquals(
452
+ proof1.publicOutput,
453
+ proof2.publicInput
454
+ );
455
+ return { publicOutput: proof2.publicOutput };
456
+ },
457
+ },
458
+ },
459
+ });
460
+
461
+ /** Proof classes for the `ValidatorsVoting` ZkProgram. */
462
+ class ValidatorsVotingNativeProof extends ZkProgram.Proof(ValidatorsVoting) {}
463
+ class ValidatorsVotingProof extends DynamicProof<
464
+ ValidatorsDecisionState,
465
+ ValidatorsDecisionState
466
+ > {
467
+ static publicInputType = ValidatorsDecisionState;
468
+ static publicOutputType = ValidatorsDecisionState;
469
+ static maxProofsVerified = 2 as const;
470
+ static featureFlags = FeatureFlags.allMaybe;
471
+ }
472
+
473
+ /**
474
+ * Converts a `Field` element to a string representation.
475
+ * This is used for serializing `Field` values into strings suitable for storage or transmission.
476
+ *
477
+ * @param {Field} field - The `Field` element to convert.
478
+ * @returns {string} The string representation of the `Field`.
479
+ */
480
+ function fieldToString(field: Field): string {
481
+ return Encoding.stringFromFields([field]);
482
+ }
483
+
484
+ /**
485
+ * Reconstructs a `Field` element from its string representation.
486
+ * This function is essential for deserializing strings back into `Field` elements,
487
+ * which can then be used within the smart contract logic.
488
+ *
489
+ * @param {string} storage - The string representation of the `Field`.
490
+ * @returns {Field} The reconstructed `Field` element.
491
+ * @throws Will throw an error if the input string does not correspond to exactly one `Field`.
492
+ */
493
+ function fieldFromString(storage: string): Field {
494
+ const fields = Encoding.stringToFields(storage);
495
+ if (fields.length !== 1) throw new Error("String is too long");
496
+ return fields[0];
497
+ }