@openzeppelin/miden-multisig-client 0.16.2 → 0.17.0-rc.1

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.
Files changed (150) hide show
  1. package/README.md +144 -7
  2. package/dist/account/builder.d.ts.map +1 -1
  3. package/dist/account/builder.js +32 -25
  4. package/dist/account/builder.js.map +1 -1
  5. package/dist/account/builder.test.js +73 -21
  6. package/dist/account/builder.test.js.map +1 -1
  7. package/dist/account/layout.d.ts +31 -0
  8. package/dist/account/layout.d.ts.map +1 -0
  9. package/dist/account/layout.js +31 -0
  10. package/dist/account/layout.js.map +1 -0
  11. package/dist/account/masm/account-components/auth.d.ts +1 -4
  12. package/dist/account/masm/account-components/auth.d.ts.map +1 -1
  13. package/dist/account/masm/account-components/auth.js +36 -53
  14. package/dist/account/masm/account-components/auth.js.map +1 -1
  15. package/dist/account/masm/index.d.ts +0 -1
  16. package/dist/account/masm/index.d.ts.map +1 -1
  17. package/dist/account/masm/index.js +0 -1
  18. package/dist/account/masm/index.js.map +1 -1
  19. package/dist/account/storage.d.ts +4 -0
  20. package/dist/account/storage.d.ts.map +1 -1
  21. package/dist/account/storage.js +9 -20
  22. package/dist/account/storage.js.map +1 -1
  23. package/dist/client.d.ts.map +1 -1
  24. package/dist/client.js +5 -3
  25. package/dist/client.js.map +1 -1
  26. package/dist/client.test.js +68 -15
  27. package/dist/client.test.js.map +1 -1
  28. package/dist/index.d.ts +4 -3
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +3 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/inspector.d.ts +59 -1
  33. package/dist/inspector.d.ts.map +1 -1
  34. package/dist/inspector.js +157 -29
  35. package/dist/inspector.js.map +1 -1
  36. package/dist/inspector.test.js +246 -37
  37. package/dist/inspector.test.js.map +1 -1
  38. package/dist/multisig.d.ts +106 -32
  39. package/dist/multisig.d.ts.map +1 -1
  40. package/dist/multisig.js +289 -95
  41. package/dist/multisig.js.map +1 -1
  42. package/dist/multisig.test.js +581 -58
  43. package/dist/multisig.test.js.map +1 -1
  44. package/dist/procedures.d.ts +6 -7
  45. package/dist/procedures.d.ts.map +1 -1
  46. package/dist/procedures.js +6 -7
  47. package/dist/procedures.js.map +1 -1
  48. package/dist/proposal/metadata.d.ts.map +1 -1
  49. package/dist/proposal/metadata.js +13 -2
  50. package/dist/proposal/metadata.js.map +1 -1
  51. package/dist/proposal/metadata.test.js +57 -0
  52. package/dist/proposal/metadata.test.js.map +1 -1
  53. package/dist/prover/workflow.d.ts +3 -2
  54. package/dist/prover/workflow.d.ts.map +1 -1
  55. package/dist/prover/workflow.js +5 -2
  56. package/dist/prover/workflow.js.map +1 -1
  57. package/dist/prover/workflow.test.js +6 -4
  58. package/dist/prover/workflow.test.js.map +1 -1
  59. package/dist/transaction/index.d.ts +1 -1
  60. package/dist/transaction/index.d.ts.map +1 -1
  61. package/dist/transaction/index.js +1 -1
  62. package/dist/transaction/index.js.map +1 -1
  63. package/dist/transaction/p2id.d.ts +17 -6
  64. package/dist/transaction/p2id.d.ts.map +1 -1
  65. package/dist/transaction/p2id.js +22 -31
  66. package/dist/transaction/p2id.js.map +1 -1
  67. package/dist/transaction/p2id.test.js +62 -24
  68. package/dist/transaction/p2id.test.js.map +1 -1
  69. package/dist/transaction/summary.d.ts +45 -2
  70. package/dist/transaction/summary.d.ts.map +1 -1
  71. package/dist/transaction/summary.js +42 -2
  72. package/dist/transaction/summary.js.map +1 -1
  73. package/dist/transaction/summary.test.d.ts +2 -0
  74. package/dist/transaction/summary.test.d.ts.map +1 -0
  75. package/dist/transaction/summary.test.js +26 -0
  76. package/dist/transaction/summary.test.js.map +1 -0
  77. package/dist/transaction/updateGuardian.d.ts.map +1 -1
  78. package/dist/transaction/updateGuardian.js +16 -18
  79. package/dist/transaction/updateGuardian.js.map +1 -1
  80. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
  81. package/dist/transaction/updateProcedureThreshold.js +15 -17
  82. package/dist/transaction/updateProcedureThreshold.js.map +1 -1
  83. package/dist/transaction/updateSigners.d.ts +6 -1
  84. package/dist/transaction/updateSigners.d.ts.map +1 -1
  85. package/dist/transaction/updateSigners.js +20 -17
  86. package/dist/transaction/updateSigners.js.map +1 -1
  87. package/dist/transaction.d.ts +2 -2
  88. package/dist/transaction.d.ts.map +1 -1
  89. package/dist/transaction.js +1 -1
  90. package/dist/transaction.js.map +1 -1
  91. package/dist/types/proposal.d.ts +28 -4
  92. package/dist/types/proposal.d.ts.map +1 -1
  93. package/dist/types/proposal.js +18 -0
  94. package/dist/types/proposal.js.map +1 -1
  95. package/dist/types.d.ts +0 -1
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/utils/signature.d.ts +11 -7
  98. package/dist/utils/signature.d.ts.map +1 -1
  99. package/dist/utils/signature.js +24 -58
  100. package/dist/utils/signature.js.map +1 -1
  101. package/dist/utils/word.d.ts +7 -0
  102. package/dist/utils/word.d.ts.map +1 -1
  103. package/dist/utils/word.js +15 -0
  104. package/dist/utils/word.js.map +1 -1
  105. package/masm/account_components/auth/guarded_multisig.masm +42 -0
  106. package/package.json +7 -4
  107. package/src/account/builder.test.ts +111 -45
  108. package/src/account/builder.ts +45 -33
  109. package/src/account/layout.ts +33 -0
  110. package/src/account/masm/account-components/auth.ts +36 -56
  111. package/src/account/masm/index.ts +0 -1
  112. package/src/account/storage.ts +9 -22
  113. package/src/client.test.ts +80 -15
  114. package/src/client.ts +5 -3
  115. package/src/index.ts +26 -1
  116. package/src/inspector.test.ts +330 -38
  117. package/src/inspector.ts +196 -33
  118. package/src/multisig.test.ts +679 -63
  119. package/src/multisig.ts +361 -107
  120. package/src/procedures.ts +6 -7
  121. package/src/proposal/metadata.test.ts +76 -0
  122. package/src/proposal/metadata.ts +13 -2
  123. package/src/prover/workflow.test.ts +9 -4
  124. package/src/prover/workflow.ts +15 -3
  125. package/src/transaction/index.ts +7 -1
  126. package/src/transaction/p2id.test.ts +112 -31
  127. package/src/transaction/p2id.ts +39 -38
  128. package/src/transaction/summary.test.ts +32 -0
  129. package/src/transaction/summary.ts +83 -4
  130. package/src/transaction/updateGuardian.ts +15 -25
  131. package/src/transaction/updateProcedureThreshold.ts +13 -29
  132. package/src/transaction/updateSigners.ts +38 -30
  133. package/src/transaction.ts +8 -1
  134. package/src/types/proposal.ts +43 -4
  135. package/src/types.ts +0 -1
  136. package/src/utils/signature.ts +32 -65
  137. package/src/utils/word.ts +17 -0
  138. package/dist/account/masm/auth.d.ts +0 -5
  139. package/dist/account/masm/auth.d.ts.map +0 -1
  140. package/dist/account/masm/auth.js +0 -1509
  141. package/dist/account/masm/auth.js.map +0 -1
  142. package/masm/account_components/auth/multisig.masm +0 -12
  143. package/masm/account_components/auth/multisig_ecdsa.masm +0 -12
  144. package/masm/account_components/auth/multisig_guardian.masm +0 -16
  145. package/masm/account_components/auth/multisig_guardian_ecdsa.masm +0 -16
  146. package/masm/auth/guardian.masm +0 -199
  147. package/masm/auth/guardian_ecdsa.masm +0 -195
  148. package/masm/auth/multisig.masm +0 -554
  149. package/masm/auth/multisig_ecdsa.masm +0 -554
  150. package/src/account/masm/auth.ts +0 -1512
package/src/inspector.ts CHANGED
@@ -4,21 +4,11 @@
4
4
 
5
5
  import { Account, Word } from '@miden-sdk/miden-sdk';
6
6
  import { base64ToUint8Array } from './utils/encoding.js';
7
- import { wordElementToBigInt, wordToHex } from './utils/word.js';
7
+ import { isEmptyWord, wordElementToBigInt, wordToHex } from './utils/word.js';
8
8
  import { getProcedureRoot, getProcedureNames, type ProcedureName } from './procedures.js';
9
+ import { MULTISIG_SLOT_NAMES, GUARDIAN_SLOT_NAMES, MAX_SIGNERS } from './account/layout.js';
9
10
 
10
- // Storage slot names matching the MASM definitions
11
- const MULTISIG_SLOT_NAMES = {
12
- THRESHOLD_CONFIG: 'openzeppelin::multisig::threshold_config',
13
- SIGNER_PUBLIC_KEYS: 'openzeppelin::multisig::signer_public_keys',
14
- EXECUTED_TRANSACTIONS: 'openzeppelin::multisig::executed_transactions',
15
- PROCEDURE_THRESHOLDS: 'openzeppelin::multisig::procedure_thresholds',
16
- } as const;
17
-
18
- const GUARDIAN_SLOT_NAMES = {
19
- SELECTOR: 'openzeppelin::guardian::selector',
20
- PUBLIC_KEY: 'openzeppelin::guardian::public_key',
21
- } as const;
11
+ type AccountStorageLike = ReturnType<Account['storage']>;
22
12
 
23
13
  export interface VaultBalance {
24
14
  faucetId: string;
@@ -29,12 +19,90 @@ export interface DetectedMultisigConfig {
29
19
  threshold: number;
30
20
  numSigners: number;
31
21
  signerCommitments: string[];
32
- guardianEnabled: boolean;
33
22
  guardianCommitment: string | null;
34
23
  vaultBalances: VaultBalance[];
35
24
  procedureThresholds: Map<ProcedureName, number>;
36
25
  }
37
26
 
27
+ /**
28
+ * Fail-closed validation for consuming a lenient `fromAccount()` result on a
29
+ * mutation path. `fromAccount` tolerates partial reads (absent entries are
30
+ * skipped), which is fine for inspection but not for callers that store the
31
+ * result as the authoritative config: membership proposals treat the signer
32
+ * set as the complete on-chain set, so adopting a truncated read could
33
+ * rewrite the account without the omitted keys.
34
+ */
35
+ export function assertCompleteDetectedConfig(
36
+ detected: DetectedMultisigConfig,
37
+ ): asserts detected is DetectedMultisigConfig & { guardianCommitment: string } {
38
+ if (detected.numSigners === 0 || detected.signerCommitments.length !== detected.numSigners) {
39
+ throw new Error(
40
+ `incomplete signer set: storage reports ${detected.numSigners} signers, read ${detected.signerCommitments.length}`,
41
+ );
42
+ }
43
+ if (!detected.guardianCommitment) {
44
+ throw new Error(
45
+ 'missing guardian commitment: the guarded-multisig always includes a guardian',
46
+ );
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Rejects accounts built from a different contract version before any
52
+ * layout-dependent read: against such an account the slot names and
53
+ * procedure-root-keyed maps below describe a different component, so reads
54
+ * would silently miss its state.
55
+ */
56
+ function assertPinnedContractVersion(account: Account): void {
57
+ if (!account.code().hasProcedure(Word.fromHex(getProcedureRoot('auth_tx')))) {
58
+ throw new Error(
59
+ 'unsupported contract version: the account\'s code does not carry this ' +
60
+ "SDK's pinned guarded-multisig auth procedure; use the SDK release " +
61
+ 'matching the contract version the account was created with ' +
62
+ '(see docs/MULTISIG_SDK.md, "Contract version pinning")',
63
+ );
64
+ }
65
+ }
66
+
67
+ function indexMapKey(index: number): Word {
68
+ return new Word(new BigUint64Array([BigInt(index), 0n, 0n, 0n]));
69
+ }
70
+
71
+ /**
72
+ * Read one entry from a storage map, treating "no entry" uniformly.
73
+ *
74
+ * The SDK returns `undefined` only when the slot itself is absent or not a
75
+ * map; a key with no entry in an existing map comes back as `Word::empty()`
76
+ * (`StorageMap::get` is `unwrap_or_default()` in miden-protocol). The
77
+ * contract also zeroes removed approver entries, so an empty word always
78
+ * means "no entry" and is mapped to `undefined` here — matching the Rust
79
+ * readers.
80
+ */
81
+ function readMapWord(
82
+ storage: AccountStorageLike,
83
+ slotName: string,
84
+ key: Word,
85
+ ): Word | undefined {
86
+ let value: Word | undefined;
87
+ try {
88
+ value = storage.getMapItem(slotName, key) as Word | undefined;
89
+ } catch (error) {
90
+ // The SDK's storage rejects Word instances constructed by a different
91
+ // bundled copy of @miden-sdk/miden-sdk (wasm-bindgen instance check).
92
+ if (error instanceof Error && error.message.includes('expected instance of Word')) {
93
+ throw new Error(
94
+ `cannot read ${slotName}: the account object comes from a different copy of @miden-sdk/miden-sdk than the one this package links; pass an Account created by the same SDK instance`,
95
+ { cause: error },
96
+ );
97
+ }
98
+ throw error;
99
+ }
100
+ if (value === undefined || isEmptyWord(value)) {
101
+ return undefined;
102
+ }
103
+ return value;
104
+ }
105
+
38
106
  /**
39
107
  * Inspects an account to detect its multisig configuration.
40
108
  *
@@ -51,6 +119,98 @@ export interface DetectedMultisigConfig {
51
119
  export class AccountInspector {
52
120
  private constructor() {}
53
121
 
122
+ /**
123
+ * Read the ordered approver (signer) public-key commitments of a
124
+ * guarded-multisig account from its
125
+ * `miden::standards::auth::multisig::approver_public_keys` storage map.
126
+ *
127
+ * Since the account uses the upstream `AuthGuardedMultisig` component,
128
+ * `Account.getPublicKeyCommitments()` also returns these commitments;
129
+ * this accessor is the strict, layout-insulated alternative: it validates
130
+ * the complete set against the configured signer count and throws instead
131
+ * of silently omitting unreadable entries, and it shields consumers from
132
+ * storage-layout changes across contract versions.
133
+ *
134
+ * The returned array is ordered by signer index as currently stored.
135
+ * Index 0 is the key listed first at creation only until the first
136
+ * membership change: removing a signer re-packs the indices. Hot/cold
137
+ * roles are a consumer-side convention, not part of on-chain state.
138
+ *
139
+ * Unlike `fromAccount()`, which tolerates partial reads, this throws if
140
+ * the account was built from a different contract version or any signer
141
+ * entry is absent — it never silently returns a truncated or empty list.
142
+ *
143
+ * The `account` must come from the same copy of `@miden-sdk/miden-sdk`
144
+ * that this package links; an account from a separately bundled SDK is
145
+ * rejected by the SDK's own instance checks (a descriptive error is
146
+ * thrown).
147
+ *
148
+ * @param account - The Account object from the Miden SDK
149
+ * @returns Signer public-key commitments as 0x-prefixed hex, ordered by signer index
150
+ */
151
+ static getSignerPublicKeyCommitments(account: Account): string[] {
152
+ assertPinnedContractVersion(account);
153
+ const storage = account.storage();
154
+
155
+ const thresholdConfig = storage.getItem(MULTISIG_SLOT_NAMES.THRESHOLD_CONFIG) as
156
+ | Word
157
+ | undefined;
158
+ if (!thresholdConfig) {
159
+ throw new Error(
160
+ `account has no ${MULTISIG_SLOT_NAMES.THRESHOLD_CONFIG} storage slot: not a guarded-multisig account`,
161
+ );
162
+ }
163
+
164
+ const numSigners = Number(wordElementToBigInt(thresholdConfig, 1));
165
+ if (numSigners === 0) {
166
+ throw new Error(
167
+ `${MULTISIG_SLOT_NAMES.THRESHOLD_CONFIG} reports zero signers: not a guarded-multisig account`,
168
+ );
169
+ }
170
+ if (numSigners > MAX_SIGNERS) {
171
+ throw new Error(
172
+ `${MULTISIG_SLOT_NAMES.THRESHOLD_CONFIG} reports ${numSigners} signers, exceeding the sanity limit of ${MAX_SIGNERS}: account storage is corrupt`,
173
+ );
174
+ }
175
+
176
+ const commitments: string[] = [];
177
+ for (let i = 0; i < numSigners; i++) {
178
+ const commitment = readMapWord(storage, MULTISIG_SLOT_NAMES.SIGNER_PUBLIC_KEYS, indexMapKey(i));
179
+ if (!commitment) {
180
+ throw new Error(
181
+ `missing signer public key at index ${i} in ${MULTISIG_SLOT_NAMES.SIGNER_PUBLIC_KEYS} (expected ${numSigners} signers)`,
182
+ );
183
+ }
184
+ commitments.push(wordToHex(commitment));
185
+ }
186
+ return commitments;
187
+ }
188
+
189
+ /**
190
+ * Read the guardian public-key commitment of a guarded-multisig account
191
+ * from its `miden::standards::auth::guardian::pub_key` storage map.
192
+ *
193
+ * The guarded-multisig component always includes a guardian, so this
194
+ * returns the commitment or throws: on an account from a different
195
+ * contract version, or when the guardian key entry is missing
196
+ * (inconsistent account state). Genuine storage read failures propagate.
197
+ *
198
+ * @param account - The Account object from the Miden SDK
199
+ * @returns The guardian commitment as 0x-prefixed hex
200
+ */
201
+ static getGuardianPublicKeyCommitment(account: Account): string {
202
+ assertPinnedContractVersion(account);
203
+ const storage = account.storage();
204
+
205
+ const commitment = readMapWord(storage, GUARDIAN_SLOT_NAMES.PUBLIC_KEY, indexMapKey(0));
206
+ if (!commitment) {
207
+ throw new Error(
208
+ `${GUARDIAN_SLOT_NAMES.PUBLIC_KEY} has no entry: inconsistent account state (the guarded-multisig always includes a guardian)`,
209
+ );
210
+ }
211
+ return wordToHex(commitment);
212
+ }
213
+
54
214
  /**
55
215
  * Inspect a base64-encoded serialized account.
56
216
  *
@@ -66,22 +226,32 @@ export class AccountInspector {
66
226
  /**
67
227
  * Inspect a Miden SDK Account object.
68
228
  *
229
+ * Lenient by design (skips unreadable parts): `MultisigClient.load` uses
230
+ * it to reconstruct config from accounts it already trusts. Consumers that
231
+ * need a guarantee should use `getSignerPublicKeyCommitments` /
232
+ * `getGuardianPublicKeyCommitment`, which throw instead of degrading.
233
+ *
69
234
  * @param account - The Account object from Miden SDK
70
235
  * @returns Detected multisig configuration
71
236
  */
72
237
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
238
  static fromAccount(account: Account): DetectedMultisigConfig {
239
+ // Reject accounts built from a different contract version before any
240
+ // procedure-root-keyed read: against such an account the reads below would
241
+ // silently miss its stored overrides (its `procedure_thresholds` map is
242
+ // keyed by *its* roots, not this SDK's) and report wrong thresholds.
243
+ assertPinnedContractVersion(account);
244
+
74
245
  const storage = account.storage();
75
246
 
76
- const slot0 = storage.getItem(MULTISIG_SLOT_NAMES.THRESHOLD_CONFIG) as Word;
77
- const threshold = Number(wordElementToBigInt(slot0, 0));
78
- const numSigners = Number(wordElementToBigInt(slot0, 1));
247
+ const slot0 = storage.getItem(MULTISIG_SLOT_NAMES.THRESHOLD_CONFIG) as Word | undefined;
248
+ const threshold = slot0 ? Number(wordElementToBigInt(slot0, 0)) : 0;
249
+ const numSigners = slot0 ? Number(wordElementToBigInt(slot0, 1)) : 0;
79
250
 
80
251
  const signerCommitments: string[] = [];
81
- for (let i = 0; i < numSigners; i++) {
252
+ for (let i = 0; i < Math.min(numSigners, MAX_SIGNERS); i++) {
82
253
  try {
83
- const key = new Word(new BigUint64Array([BigInt(i), 0n, 0n, 0n]));
84
- const commitment = storage.getMapItem(MULTISIG_SLOT_NAMES.SIGNER_PUBLIC_KEYS, key) as Word;
254
+ const commitment = readMapWord(storage, MULTISIG_SLOT_NAMES.SIGNER_PUBLIC_KEYS, indexMapKey(i));
85
255
  if (commitment) {
86
256
  signerCommitments.push(wordToHex(commitment));
87
257
  }
@@ -90,20 +260,14 @@ export class AccountInspector {
90
260
  }
91
261
  }
92
262
 
93
- let guardianEnabled = false;
263
+ // The guarded-multisig has no enable/disable selector; the guardian is always present.
264
+ // Read its public key directly from the guardian pub_key slot.
94
265
  let guardianCommitment: string | null = null;
95
266
 
96
267
  try {
97
- const guardianSlot0 = storage.getItem(GUARDIAN_SLOT_NAMES.SELECTOR) as Word;
98
- const selector = Number(wordElementToBigInt(guardianSlot0, 0));
99
- guardianEnabled = selector === 1;
100
-
101
- if (guardianEnabled) {
102
- const zeroKey = new Word(new BigUint64Array([0n, 0n, 0n, 0n]));
103
- const guardianKey = storage.getMapItem(GUARDIAN_SLOT_NAMES.PUBLIC_KEY, zeroKey) as Word;
104
- if (guardianKey) {
105
- guardianCommitment = wordToHex(guardianKey);
106
- }
268
+ const guardianKey = readMapWord(storage, GUARDIAN_SLOT_NAMES.PUBLIC_KEY, indexMapKey(0));
269
+ if (guardianKey) {
270
+ guardianCommitment = wordToHex(guardianKey);
107
271
  }
108
272
  } catch (error) {
109
273
  console.warn(error);
@@ -130,7 +294,7 @@ export class AccountInspector {
130
294
  try {
131
295
  const rootHex = getProcedureRoot(procName);
132
296
  const rootWord = Word.fromHex(rootHex);
133
- const value = storage.getMapItem(MULTISIG_SLOT_NAMES.PROCEDURE_THRESHOLDS, rootWord) as Word;
297
+ const value = readMapWord(storage, MULTISIG_SLOT_NAMES.PROCEDURE_THRESHOLDS, rootWord);
134
298
  if (value) {
135
299
  const procThreshold = Number(wordElementToBigInt(value, 0));
136
300
  if (procThreshold > 0) {
@@ -146,7 +310,6 @@ export class AccountInspector {
146
310
  threshold,
147
311
  numSigners,
148
312
  signerCommitments,
149
- guardianEnabled,
150
313
  guardianCommitment,
151
314
  vaultBalances,
152
315
  procedureThresholds,