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

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
@@ -1,15 +1,6 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { createMultisigAccount } from './builder.js';
3
- import {
4
- MULTISIG_ECDSA_MASM,
5
- MULTISIG_MASM,
6
- GUARDIAN_ECDSA_MASM,
7
- GUARDIAN_MASM,
8
- } from './masm/auth.js';
9
- import {
10
- MULTISIG_GUARDIAN_ACCOUNT_COMPONENT_MASM,
11
- MULTISIG_GUARDIAN_ECDSA_ACCOUNT_COMPONENT_MASM,
12
- } from './masm/account-components/auth.js';
2
+ import { createMultisigAccount, validateMultisigConfig } from './builder.js';
3
+ import { GUARDED_MULTISIG_ACCOUNT_COMPONENT_MASM } from './masm/account-components/auth.js';
13
4
 
14
5
  const {
15
6
  buildMultisigStorageSlots,
@@ -53,6 +44,12 @@ const {
53
44
  account: { id: () => ({ toString: () => '0x' + 'a'.repeat(30) }) },
54
45
  };
55
46
  }
47
+
48
+ buildWithoutSchemaCommitment() {
49
+ return {
50
+ account: { id: () => ({ toString: () => '0x' + 'a'.repeat(30) }) },
51
+ };
52
+ }
56
53
  }
57
54
 
58
55
  return {
@@ -93,7 +90,7 @@ describe('createMultisigAccount', () => {
93
90
  compileComponent.mockClear();
94
91
  });
95
92
 
96
- it('uses Falcon MASM by default', async () => {
93
+ function makeClient() {
97
94
  const authBuilder = {
98
95
  linkModule: vi.fn(),
99
96
  compileAccountComponentCode: vi.fn((source) => ({ source })),
@@ -104,6 +101,11 @@ describe('createMultisigAccount', () => {
104
101
  insert: vi.fn().mockResolvedValue(undefined),
105
102
  },
106
103
  };
104
+ return { authBuilder, webClient };
105
+ }
106
+
107
+ it('compiles the guarded component without re-linking SDK-provided modules (Falcon)', async () => {
108
+ const { authBuilder, webClient } = makeClient();
107
109
 
108
110
  await createMultisigAccount(
109
111
  webClient as never,
@@ -115,33 +117,17 @@ describe('createMultisigAccount', () => {
115
117
  'http://localhost:57291',
116
118
  );
117
119
 
118
- expect(authBuilder.linkModule).toHaveBeenNthCalledWith(
119
- 1,
120
- 'openzeppelin::auth::guardian',
121
- GUARDIAN_MASM,
122
- );
123
- expect(authBuilder.linkModule).toHaveBeenNthCalledWith(
124
- 2,
125
- 'openzeppelin::auth::multisig',
126
- MULTISIG_MASM,
127
- );
120
+ // The web SDK assembler already provides `miden::standards::auth::*`; re-linking would
121
+ // raise a duplicate-definition error, so the builder must NOT call linkModule.
122
+ expect(authBuilder.linkModule).not.toHaveBeenCalled();
128
123
  expect(authBuilder.compileAccountComponentCode).toHaveBeenCalledWith(
129
- MULTISIG_GUARDIAN_ACCOUNT_COMPONENT_MASM,
124
+ GUARDED_MULTISIG_ACCOUNT_COMPONENT_MASM,
130
125
  );
131
126
  expect(webClient.accounts.insert).toHaveBeenCalledTimes(1);
132
127
  });
133
128
 
134
- it('uses ECDSA MASM when requested', async () => {
135
- const authBuilder = {
136
- linkModule: vi.fn(),
137
- compileAccountComponentCode: vi.fn((source) => ({ source })),
138
- };
139
- const webClient = {
140
- createCodeBuilder: vi.fn().mockReturnValue(authBuilder),
141
- accounts: {
142
- insert: vi.fn().mockResolvedValue(undefined),
143
- },
144
- };
129
+ it('uses the same scheme-agnostic component for ECDSA', async () => {
130
+ const { authBuilder, webClient } = makeClient();
145
131
 
146
132
  await createMultisigAccount(
147
133
  webClient as never,
@@ -154,19 +140,99 @@ describe('createMultisigAccount', () => {
154
140
  'http://localhost:57291',
155
141
  );
156
142
 
157
- expect(authBuilder.linkModule).toHaveBeenNthCalledWith(
158
- 1,
159
- 'openzeppelin::auth::guardian_ecdsa',
160
- GUARDIAN_ECDSA_MASM,
161
- );
162
- expect(authBuilder.linkModule).toHaveBeenNthCalledWith(
163
- 2,
164
- 'openzeppelin::auth::multisig_ecdsa',
165
- MULTISIG_ECDSA_MASM,
166
- );
143
+ expect(authBuilder.linkModule).not.toHaveBeenCalled();
167
144
  expect(authBuilder.compileAccountComponentCode).toHaveBeenCalledWith(
168
- MULTISIG_GUARDIAN_ECDSA_ACCOUNT_COMPONENT_MASM,
145
+ GUARDED_MULTISIG_ACCOUNT_COMPONENT_MASM,
169
146
  );
170
147
  expect(webClient.accounts.insert).toHaveBeenCalledTimes(1);
171
148
  });
172
149
  });
150
+
151
+ describe('validateMultisigConfig', () => {
152
+ const signer = '0x' + '1'.repeat(64);
153
+
154
+ it('rejects a guardian commitment equal to a signer (matches upstream Rust invariant)', () => {
155
+ expect(() =>
156
+ validateMultisigConfig({
157
+ threshold: 1,
158
+ signerCommitments: [signer],
159
+ guardianCommitment: signer,
160
+ }),
161
+ ).toThrow(/different from all signer commitments/);
162
+ });
163
+
164
+ it('accepts a distinct guardian commitment', () => {
165
+ expect(() =>
166
+ validateMultisigConfig({
167
+ threshold: 1,
168
+ signerCommitments: [signer],
169
+ guardianCommitment: '0x' + '2'.repeat(64),
170
+ }),
171
+ ).not.toThrow();
172
+ });
173
+
174
+ describe('procedure threshold overrides vs update_procedure_threshold', () => {
175
+ const signers = Array.from({ length: 5 }, (_, i) => '0x' + String(i + 1).repeat(64));
176
+ const guardian = '0x' + '9'.repeat(64);
177
+
178
+ const config = (
179
+ threshold: number,
180
+ procedureThresholds: Array<{ procedure: string; threshold: number }>,
181
+ ) =>
182
+ ({
183
+ threshold,
184
+ signerCommitments: signers,
185
+ guardianCommitment: guardian,
186
+ procedureThresholds,
187
+ }) as Parameters<typeof validateMultisigConfig>[0];
188
+
189
+ it('rejects an override above the default threshold that guards the setter', () => {
190
+ expect(() =>
191
+ validateMultisigConfig(config(2, [{ procedure: 'send_asset', threshold: 4 }])),
192
+ ).toThrow(/exceeds the threshold of 2 that guards update_procedure_threshold/);
193
+ });
194
+
195
+ it('accepts the same override once the setter is raised to match', () => {
196
+ expect(() =>
197
+ validateMultisigConfig(
198
+ config(2, [
199
+ { procedure: 'send_asset', threshold: 4 },
200
+ { procedure: 'update_procedure_threshold', threshold: 4 },
201
+ ]),
202
+ ),
203
+ ).not.toThrow();
204
+ });
205
+
206
+ it('rejects an override above an explicitly raised setter', () => {
207
+ expect(() =>
208
+ validateMultisigConfig(
209
+ config(2, [
210
+ { procedure: 'send_asset', threshold: 4 },
211
+ { procedure: 'update_procedure_threshold', threshold: 3 },
212
+ ]),
213
+ ),
214
+ ).toThrow(/exceeds the threshold of 3 that guards update_procedure_threshold/);
215
+ });
216
+
217
+ it('accepts overrides at or below the default threshold', () => {
218
+ expect(() =>
219
+ validateMultisigConfig(
220
+ config(3, [
221
+ { procedure: 'send_asset', threshold: 3 },
222
+ { procedure: 'receive_asset', threshold: 1 },
223
+ ]),
224
+ ),
225
+ ).not.toThrow();
226
+ });
227
+
228
+ it('allows the setter override to exceed the default threshold', () => {
229
+ // Raising only the setter is always safe: it makes overrides harder to
230
+ // edit, never easier.
231
+ expect(() =>
232
+ validateMultisigConfig(
233
+ config(2, [{ procedure: 'update_procedure_threshold', threshold: 5 }]),
234
+ ),
235
+ ).not.toThrow();
236
+ });
237
+ });
238
+ });
@@ -9,22 +9,29 @@ import {
9
9
  AccountComponent,
10
10
  AccountStorageMode,
11
11
  type MidenClient,
12
+ type WasmWebClient,
12
13
  } from '@miden-sdk/miden-sdk';
13
14
  import type { MultisigConfig, CreateAccountResult } from '../types.js';
14
15
  import { getRawMidenClient } from '../raw-client.js';
15
16
  import { buildMultisigStorageSlots, buildGuardianStorageSlots } from './storage.js';
16
- import {
17
- MULTISIG_ECDSA_MASM,
18
- MULTISIG_MASM,
19
- GUARDIAN_ECDSA_MASM,
20
- GUARDIAN_MASM,
21
- } from './masm/auth.js';
22
- import {
23
- MULTISIG_GUARDIAN_ACCOUNT_COMPONENT_MASM,
24
- MULTISIG_GUARDIAN_ECDSA_ACCOUNT_COMPONENT_MASM,
25
- } from './masm/account-components/auth.js';
17
+ import { GUARDED_MULTISIG_ACCOUNT_COMPONENT_MASM } from './masm/account-components/auth.js';
26
18
  import { normalizeSignerCommitment } from '../utils/signature.js';
27
19
 
20
+ /** Builds the guarded-multisig component without relinking assembler-provided libraries. */
21
+ function buildGuardedMultisigComponent(
22
+ authBuilder: Awaited<ReturnType<WasmWebClient['createCodeBuilder']>>,
23
+ config: MultisigConfig,
24
+ ): AccountComponent {
25
+ const authSlots = [
26
+ ...buildMultisigStorageSlots(config),
27
+ ...buildGuardianStorageSlots(config),
28
+ ];
29
+ const authComponentCode = authBuilder.compileAccountComponentCode(
30
+ GUARDED_MULTISIG_ACCOUNT_COMPONENT_MASM,
31
+ );
32
+ return AccountComponent.compile(authComponentCode, authSlots).withSupportsAllTypes();
33
+ }
34
+
28
35
  /**
29
36
  * Creates a multisig account with GUARDIAN authentication.
30
37
  *
@@ -39,31 +46,10 @@ export async function createMultisigAccount(
39
46
  midenRpcEndpoint: string,
40
47
  ): Promise<CreateAccountResult> {
41
48
  validateMultisigConfig(config);
42
- const signatureScheme = config.signatureScheme ?? 'falcon';
43
49
  const rawClient = await getRawMidenClient(midenClient, midenRpcEndpoint);
44
- const authSlots = [
45
- ...buildMultisigStorageSlots(config),
46
- ...buildGuardianStorageSlots(config),
47
- ];
48
- const guardianMasm = signatureScheme === 'ecdsa' ? GUARDIAN_ECDSA_MASM : GUARDIAN_MASM;
49
- const multisigMasm = signatureScheme === 'ecdsa' ? MULTISIG_ECDSA_MASM : MULTISIG_MASM;
50
- const authComponentMasm = signatureScheme === 'ecdsa'
51
- ? MULTISIG_GUARDIAN_ECDSA_ACCOUNT_COMPONENT_MASM
52
- : MULTISIG_GUARDIAN_ACCOUNT_COMPONENT_MASM;
53
- const guardianLibraryPath = signatureScheme === 'ecdsa'
54
- ? 'openzeppelin::auth::guardian_ecdsa'
55
- : 'openzeppelin::auth::guardian';
56
- const multisigLibraryPath = signatureScheme === 'ecdsa'
57
- ? 'openzeppelin::auth::multisig_ecdsa'
58
- : 'openzeppelin::auth::multisig';
59
50
 
60
51
  const authBuilder = await rawClient.createCodeBuilder();
61
- authBuilder.linkModule(guardianLibraryPath, guardianMasm);
62
- authBuilder.linkModule(multisigLibraryPath, multisigMasm);
63
- const authComponentCode = authBuilder.compileAccountComponentCode(authComponentMasm);
64
- const authComponent = AccountComponent
65
- .compile(authComponentCode, authSlots)
66
- .withSupportsAllTypes();
52
+ const authComponent = buildGuardedMultisigComponent(authBuilder, config);
67
53
 
68
54
  let seed = config.seed;
69
55
  // Generate random seed if not provided
@@ -82,7 +68,7 @@ export async function createMultisigAccount(
82
68
  .withAuthComponent(authComponent)
83
69
  .withBasicWalletComponent();
84
70
 
85
- const result = accountBuilder.build();
71
+ const result = accountBuilder.buildWithoutSchemaCommitment();
86
72
 
87
73
  await midenClient.accounts.insert({ account: result.account, overwrite: false });
88
74
 
@@ -123,6 +109,9 @@ export function validateMultisigConfig(config: MultisigConfig): void {
123
109
  if (!config.guardianCommitment) {
124
110
  throw new Error('GUARDIAN commitment is required');
125
111
  }
112
+ if (signerCommitments.has(normalizeSignerCommitment(config.guardianCommitment))) {
113
+ throw new Error('GUARDIAN commitment must be different from all signer commitments');
114
+ }
126
115
 
127
116
  // Validate procedure thresholds if provided
128
117
  if (config.procedureThresholds) {
@@ -142,5 +131,28 @@ export function validateMultisigConfig(config: MultisigConfig): void {
142
131
  }
143
132
  seen.add(pt.procedure);
144
133
  }
134
+
135
+ // An override is only enforceable if lowering it costs at least as many
136
+ // signatures as the override itself demands. `update_procedure_threshold`
137
+ // is the procedure that edits overrides, so anything above its own
138
+ // effective threshold can be lowered by a smaller quorum and then used:
139
+ // a 2-of-5 with `send_asset: 4` is a 2-of-5 spend lock, not a 4-of-5 one.
140
+ // Mirrors `AuthMultisig::new` in miden-standards, which rejects the same
141
+ // shape, so Rust cannot build an account TypeScript would otherwise allow.
142
+ const setterOverride = config.procedureThresholds.find(
143
+ (pt) => pt.procedure === 'update_procedure_threshold'
144
+ )?.threshold;
145
+ const setterThreshold = setterOverride ?? config.threshold;
146
+
147
+ for (const pt of config.procedureThresholds) {
148
+ if (pt.threshold > setterThreshold) {
149
+ throw new Error(
150
+ `procedure threshold override for ${pt.procedure} (${pt.threshold}) exceeds the ` +
151
+ `threshold of ${setterThreshold} that guards update_procedure_threshold; such an ` +
152
+ `override can be removed by a smaller quorum. Raise the update_procedure_threshold ` +
153
+ `override to at least ${pt.threshold} to make it enforceable`
154
+ );
155
+ }
156
+ }
145
157
  }
146
158
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `AuthGuardedMultisig` storage slot names (`miden::standards::auth::*`),
3
+ * shared by the writer (`account/storage.ts`) and the readers
4
+ * (`inspector.ts`). Single source of truth so the two cannot drift. These
5
+ * must match the Rust `miden-standards` component exactly: account ID and
6
+ * commitment derive from the storage layout, so any divergence breaks
7
+ * cross-SDK determinism (guarded by the parity test).
8
+ *
9
+ * Deliberately not re-exported from the package index: consumers should use
10
+ * the `AccountInspector` accessors rather than reading storage directly
11
+ * (issue #306).
12
+ */
13
+
14
+ export const MULTISIG_SLOT_NAMES = {
15
+ THRESHOLD_CONFIG: 'miden::standards::auth::multisig::threshold_config',
16
+ SIGNER_PUBLIC_KEYS: 'miden::standards::auth::multisig::approver_public_keys',
17
+ SIGNER_SCHEME_IDS: 'miden::standards::auth::multisig::approver_schemes',
18
+ EXECUTED_TRANSACTIONS: 'miden::standards::auth::multisig::executed_transactions',
19
+ PROCEDURE_THRESHOLDS: 'miden::standards::auth::multisig::procedure_thresholds',
20
+ } as const;
21
+
22
+ export const GUARDIAN_SLOT_NAMES = {
23
+ PUBLIC_KEY: 'miden::standards::auth::guardian::pub_key',
24
+ SCHEME_ID: 'miden::standards::auth::guardian::scheme',
25
+ } as const;
26
+
27
+ /**
28
+ * Sanity ceiling for the signer count read from `threshold_config`. The
29
+ * contract has no on-chain maximum (the felt is only asserted to be a u32),
30
+ * so a corrupt or adversarial account could report an arbitrarily large
31
+ * count; readers bound their loops with this instead of trusting it.
32
+ */
33
+ export const MAX_SIGNERS = 1000;
@@ -1,66 +1,46 @@
1
1
  // This file is generated by scripts/generate-masm.mjs from the MASM contract sources.
2
2
  // Do not edit it by hand.
3
3
 
4
- export const MULTISIG_ACCOUNT_COMPONENT_MASM = `# Multi-Signature RPO Falcon 512 Authentication Component
5
-
6
- use openzeppelin::auth::multisig
7
-
8
- pub use multisig::update_signers_and_threshold
9
- pub use multisig::update_procedure_threshold
10
-
4
+ export const GUARDED_MULTISIG_ACCOUNT_COMPONENT_MASM = `# The MASM code of the Multi-Signature Authentication component integrated with a state guardian.
5
+ #
6
+ # See the \`AuthGuardedMultisig\` Rust type's documentation for more details.
7
+
8
+ use miden::standards::auth::multisig
9
+ use miden::standards::auth::guardian
10
+
11
+ pub use {update_signers_and_threshold} from miden::standards::auth::multisig
12
+ pub use {get_threshold_and_num_approvers} from miden::standards::auth::multisig
13
+ pub use {set_procedure_threshold} from miden::standards::auth::multisig
14
+ pub use {get_signer_at} from miden::standards::auth::multisig
15
+ pub use {is_signer} from miden::standards::auth::multisig
16
+
17
+ pub use {update_guardian_public_key} from miden::standards::auth::guardian
18
+
19
+ #! Authenticate a transaction with multi-signature support and optional guardian verification.
20
+ #!
21
+ #! Inputs:
22
+ #! Operand stack: [SALT]
23
+ #! Outputs:
24
+ #! Operand stack: []
25
+ #!
26
+ #! Invocation: call
11
27
  @auth_script
12
- pub proc auth_tx_multisig(salt: word)
13
- exec.multisig::auth_tx
14
- exec.multisig::assert_new_tx
15
- end
16
- `;
28
+ pub proc auth_tx_guarded_multisig(salt: word)
29
+ # zero the leading user params (not exposed through this component's interface); the SALT
30
+ # occupies the trailing four
31
+ push.0.0.0
32
+ # => [0, 0, 0, SALT]
17
33
 
18
- export const MULTISIG_GUARDIAN_ACCOUNT_COMPONENT_MASM = `# Multi-Signature RPO Falcon 512 Authentication Component With GUARDIAN
19
-
20
- use openzeppelin::auth::multisig
21
- use openzeppelin::auth::guardian
22
-
23
- pub use multisig::update_signers_and_threshold
24
- pub use multisig::update_procedure_threshold
25
- pub use guardian::update_guardian_public_key
26
- pub use guardian::verify_guardian_signature
27
-
28
- @auth_script
29
- pub proc auth_tx_multisig_guardian(salt: word)
30
34
  exec.multisig::auth_tx
31
- exec.guardian::verify_guardian_signature
32
- exec.multisig::assert_new_tx
33
- end
34
- `;
35
-
36
- export const MULTISIG_ECDSA_ACCOUNT_COMPONENT_MASM = `# Multi-Signature ECDSA secp256k1 Authentication Component
37
-
38
- use openzeppelin::auth::multisig_ecdsa
35
+ # => [TX_SUMMARY_COMMITMENT]
39
36
 
40
- pub use multisig_ecdsa::update_signers_and_threshold
41
- pub use multisig_ecdsa::update_procedure_threshold
37
+ dupw
38
+ # => [TX_SUMMARY_COMMITMENT, TX_SUMMARY_COMMITMENT]
39
+
40
+ exec.guardian::verify_signature
41
+ # => [TX_SUMMARY_COMMITMENT]
42
42
 
43
- @auth_script
44
- pub proc auth_tx_multisig_ecdsa(salt: word)
45
- exec.multisig_ecdsa::auth_tx
46
- exec.multisig_ecdsa::assert_new_tx
47
- end
48
- `;
49
-
50
- export const MULTISIG_GUARDIAN_ECDSA_ACCOUNT_COMPONENT_MASM = `# Multi-Signature ECDSA secp256k1 Authentication Component With GUARDIAN
51
-
52
- use openzeppelin::auth::multisig_ecdsa
53
- use openzeppelin::auth::guardian_ecdsa
54
-
55
- pub use multisig_ecdsa::update_signers_and_threshold
56
- pub use multisig_ecdsa::update_procedure_threshold
57
- pub use guardian_ecdsa::update_guardian_public_key
58
- pub use guardian_ecdsa::verify_guardian_signature
59
-
60
- @auth_script
61
- pub proc auth_tx_multisig_guardian_ecdsa(salt: word)
62
- exec.multisig_ecdsa::auth_tx
63
- exec.guardian_ecdsa::verify_guardian_signature
64
- exec.multisig_ecdsa::assert_new_tx
43
+ exec.multisig::record_and_assert_new_tx
44
+ # => []
65
45
  end
66
46
  `;
@@ -1,5 +1,4 @@
1
1
  // This file is generated by scripts/generate-masm.mjs from the MASM contract sources.
2
2
  // Do not edit it by hand.
3
3
 
4
- export * from './auth.js';
5
4
  export * from './account-components/auth.js';
@@ -2,21 +2,7 @@ import type { MultisigConfig } from '../types.js';
2
2
  import { StorageSlot, StorageMap, Word } from '@miden-sdk/miden-sdk';
3
3
  import { ensureHexPrefix } from '../utils/encoding.js';
4
4
  import { getProcedureRoot } from '../procedures.js';
5
-
6
- // Storage slot names matching the MASM definitions
7
- const MULTISIG_SLOT_NAMES = {
8
- THRESHOLD_CONFIG: 'openzeppelin::multisig::threshold_config',
9
- SIGNER_PUBLIC_KEYS: 'openzeppelin::multisig::signer_public_keys',
10
- SIGNER_SCHEME_IDS: 'openzeppelin::multisig::signer_scheme_ids',
11
- EXECUTED_TRANSACTIONS: 'openzeppelin::multisig::executed_transactions',
12
- PROCEDURE_THRESHOLDS: 'openzeppelin::multisig::procedure_thresholds',
13
- } as const;
14
-
15
- const GUARDIAN_SLOT_NAMES = {
16
- SELECTOR: 'openzeppelin::guardian::selector',
17
- PUBLIC_KEY: 'openzeppelin::guardian::public_key',
18
- SCHEME_ID: 'openzeppelin::guardian::scheme_id',
19
- } as const;
5
+ import { MULTISIG_SLOT_NAMES, GUARDIAN_SLOT_NAMES } from './layout.js';
20
6
 
21
7
  function signerMapKey(index: bigint): Word {
22
8
  return new Word(new BigUint64Array([index, 0n, 0n, 0n]));
@@ -70,24 +56,25 @@ export class StorageLayoutBuilder {
70
56
  return [slot0, slot1, slot2, slot3, slot4];
71
57
  }
72
58
 
59
+ /**
60
+ * The guarded-multisig has no enable/disable selector; the guardian is always present, so this
61
+ * returns just two slots: the guardian public-key map and its scheme map.
62
+ */
73
63
  buildGuardianSlots(config: MultisigConfig): StorageSlot[] {
74
- const selector = config.guardianEnabled !== false ? 1n : 0n;
75
64
  const schemeId = config.signatureScheme === 'ecdsa' ? 1n : 2n;
76
- const selectorWord = new Word(new BigUint64Array([selector, 0n, 0n, 0n]));
77
- const slot0 = StorageSlot.fromValue(GUARDIAN_SLOT_NAMES.SELECTOR, selectorWord);
65
+ const zeroKey = signerMapKey(0n);
78
66
 
79
67
  const guardianKeyMap = new StorageMap();
80
- const zeroKey = signerMapKey(0n);
81
68
  const guardianKey = Word.fromHex(ensureHexPrefix(config.guardianCommitment));
82
69
  guardianKeyMap.insert(zeroKey, guardianKey);
83
- const slot1 = StorageSlot.map(GUARDIAN_SLOT_NAMES.PUBLIC_KEY, guardianKeyMap);
70
+ const slot0 = StorageSlot.map(GUARDIAN_SLOT_NAMES.PUBLIC_KEY, guardianKeyMap);
84
71
 
85
72
  const guardianSchemeMap = new StorageMap();
86
73
  const guardianScheme = new Word(new BigUint64Array([schemeId, 0n, 0n, 0n]));
87
74
  guardianSchemeMap.insert(zeroKey, guardianScheme);
88
- const slot2 = StorageSlot.map(GUARDIAN_SLOT_NAMES.SCHEME_ID, guardianSchemeMap);
75
+ const slot1 = StorageSlot.map(GUARDIAN_SLOT_NAMES.SCHEME_ID, guardianSchemeMap);
89
76
 
90
- return [slot0, slot1, slot2];
77
+ return [slot0, slot1];
91
78
  }
92
79
  }
93
80
 
@@ -19,22 +19,27 @@ vi.mock('@miden-sdk/miden-sdk', () => ({
19
19
  vault: vi.fn(),
20
20
  })),
21
21
  },
22
+ Word: vi.fn(),
22
23
  }));
23
24
 
24
- // Mock the AccountInspector
25
- vi.mock('./inspector.js', () => ({
26
- AccountInspector: {
27
- fromAccount: vi.fn(() => ({
28
- threshold: 2,
29
- numSigners: 2,
30
- signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
31
- guardianEnabled: true,
32
- guardianCommitment: '0x' + 'c'.repeat(64),
33
- vaultBalances: [],
34
- procedureThresholds: new Map(),
35
- })),
36
- },
37
- }));
25
+ // Mock the AccountInspector, keeping the real assertCompleteDetectedConfig
26
+ // so load()'s fail-closed validation is exercised.
27
+ vi.mock('./inspector.js', async (importOriginal) => {
28
+ const actual = await importOriginal<typeof import('./inspector.js')>();
29
+ return {
30
+ ...actual,
31
+ AccountInspector: {
32
+ fromAccount: vi.fn(() => ({
33
+ threshold: 2,
34
+ numSigners: 2,
35
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
36
+ guardianCommitment: '0x' + 'c'.repeat(64),
37
+ vaultBalances: [],
38
+ procedureThresholds: new Map(),
39
+ })),
40
+ },
41
+ };
42
+ });
38
43
 
39
44
  // Mock the account creation module
40
45
  vi.mock('./account/index.js', () => ({
@@ -232,6 +237,66 @@ describe('MultisigClient', () => {
232
237
  expect(webClient.accounts.insert).toHaveBeenCalledTimes(1);
233
238
  });
234
239
 
240
+ it('fails closed when the detected signer set is incomplete (issue #306 review)', async () => {
241
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
242
+
243
+ const { AccountInspector } = await import('./inspector.js');
244
+ // Storage reports 3 signers but only 2 entries were readable — adopting
245
+ // this config would let membership proposals drop the missing key.
246
+ vi.mocked(AccountInspector.fromAccount).mockReturnValueOnce({
247
+ threshold: 2,
248
+ numSigners: 3,
249
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
250
+ guardianCommitment: '0x' + 'c'.repeat(64),
251
+ vaultBalances: [],
252
+ procedureThresholds: new Map(),
253
+ });
254
+
255
+ mockFetch.mockResolvedValueOnce({
256
+ ok: true,
257
+ json: async () => ({
258
+ account_id: '0x' + 'd'.repeat(30),
259
+ commitment: '0x' + 'e'.repeat(64),
260
+ state_json: { data: 'base64state' },
261
+ created_at: '2024-01-01T00:00:00Z',
262
+ updated_at: '2024-01-02T00:00:00Z',
263
+ }),
264
+ });
265
+
266
+ await expect(client.load('0x' + 'd'.repeat(30), mockSigner)).rejects.toThrow(
267
+ /incomplete signer set: storage reports 3 signers, read 2/,
268
+ );
269
+ });
270
+
271
+ it('fails closed when the guardian commitment is missing', async () => {
272
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
273
+
274
+ const { AccountInspector } = await import('./inspector.js');
275
+ vi.mocked(AccountInspector.fromAccount).mockReturnValueOnce({
276
+ threshold: 1,
277
+ numSigners: 1,
278
+ signerCommitments: ['0x' + 'a'.repeat(64)],
279
+ guardianCommitment: null,
280
+ vaultBalances: [],
281
+ procedureThresholds: new Map(),
282
+ });
283
+
284
+ mockFetch.mockResolvedValueOnce({
285
+ ok: true,
286
+ json: async () => ({
287
+ account_id: '0x' + 'd'.repeat(30),
288
+ commitment: '0x' + 'e'.repeat(64),
289
+ state_json: { data: 'base64state' },
290
+ created_at: '2024-01-01T00:00:00Z',
291
+ updated_at: '2024-01-02T00:00:00Z',
292
+ }),
293
+ });
294
+
295
+ await expect(client.load('0x' + 'd'.repeat(30), mockSigner)).rejects.toThrow(
296
+ /missing guardian commitment/,
297
+ );
298
+ });
299
+
235
300
  it('should throw if account not found on GUARDIAN', async () => {
236
301
  const client = new MultisigClient(webClient, CLIENT_CONFIG);
237
302
 
@@ -242,7 +307,7 @@ describe('MultisigClient', () => {
242
307
  headers: new Headers(),
243
308
  text: async () =>
244
309
  JSON.stringify({
245
- code: 'GUARDIAN_ACCOUNT_NOT_FOUND',
310
+ code: 'account_not_found',
246
311
  message: 'Account not found',
247
312
  meta: { retryable: false },
248
313
  }),
package/src/client.ts CHANGED
@@ -10,7 +10,7 @@ import { GuardianHttpClient } from '@openzeppelin/guardian-client';
10
10
  import type { StateObject } from '@openzeppelin/guardian-client';
11
11
  import { Multisig } from './multisig.js';
12
12
  import { createMultisigAccount } from './account/index.js';
13
- import { AccountInspector } from './inspector.js';
13
+ import { AccountInspector, assertCompleteDetectedConfig } from './inspector.js';
14
14
  import { getRawMidenClient, requireConfigValue, requireMidenRpcEndpoint } from './raw-client.js';
15
15
  import type { MultisigConfig, Signer } from './types.js';
16
16
  import {
@@ -214,11 +214,13 @@ export class MultisigClient {
214
214
  const account = Account.deserialize(accountBytes);
215
215
 
216
216
  const detected = AccountInspector.fromAccount(account);
217
+ // Fail closed on a partial read: the detected signer set becomes the
218
+ // authoritative config that membership proposals rewrite on-chain.
219
+ assertCompleteDetectedConfig(detected);
217
220
  const config: MultisigConfig = {
218
221
  threshold: detected.threshold,
219
222
  signerCommitments: detected.signerCommitments,
220
- guardianCommitment: detected.guardianCommitment ?? '',
221
- guardianEnabled: detected.guardianEnabled,
223
+ guardianCommitment: detected.guardianCommitment,
222
224
  procedureThresholds: Array.from(detected.procedureThresholds.entries()).map(
223
225
  ([procedure, threshold]) => ({ procedure, threshold })
224
226
  ),