@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.
- package/README.md +144 -7
- package/dist/account/builder.d.ts.map +1 -1
- package/dist/account/builder.js +32 -25
- package/dist/account/builder.js.map +1 -1
- package/dist/account/builder.test.js +73 -21
- package/dist/account/builder.test.js.map +1 -1
- package/dist/account/layout.d.ts +31 -0
- package/dist/account/layout.d.ts.map +1 -0
- package/dist/account/layout.js +31 -0
- package/dist/account/layout.js.map +1 -0
- package/dist/account/masm/account-components/auth.d.ts +1 -4
- package/dist/account/masm/account-components/auth.d.ts.map +1 -1
- package/dist/account/masm/account-components/auth.js +36 -53
- package/dist/account/masm/account-components/auth.js.map +1 -1
- package/dist/account/masm/index.d.ts +0 -1
- package/dist/account/masm/index.d.ts.map +1 -1
- package/dist/account/masm/index.js +0 -1
- package/dist/account/masm/index.js.map +1 -1
- package/dist/account/storage.d.ts +4 -0
- package/dist/account/storage.d.ts.map +1 -1
- package/dist/account/storage.js +9 -20
- package/dist/account/storage.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +5 -3
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +68 -15
- package/dist/client.test.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/inspector.d.ts +59 -1
- package/dist/inspector.d.ts.map +1 -1
- package/dist/inspector.js +157 -29
- package/dist/inspector.js.map +1 -1
- package/dist/inspector.test.js +246 -37
- package/dist/inspector.test.js.map +1 -1
- package/dist/multisig.d.ts +106 -32
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +289 -95
- package/dist/multisig.js.map +1 -1
- package/dist/multisig.test.js +581 -58
- package/dist/multisig.test.js.map +1 -1
- package/dist/procedures.d.ts +6 -7
- package/dist/procedures.d.ts.map +1 -1
- package/dist/procedures.js +6 -7
- package/dist/procedures.js.map +1 -1
- package/dist/proposal/metadata.d.ts.map +1 -1
- package/dist/proposal/metadata.js +13 -2
- package/dist/proposal/metadata.js.map +1 -1
- package/dist/proposal/metadata.test.js +57 -0
- package/dist/proposal/metadata.test.js.map +1 -1
- package/dist/prover/workflow.d.ts +3 -2
- package/dist/prover/workflow.d.ts.map +1 -1
- package/dist/prover/workflow.js +5 -2
- package/dist/prover/workflow.js.map +1 -1
- package/dist/prover/workflow.test.js +6 -4
- package/dist/prover/workflow.test.js.map +1 -1
- package/dist/transaction/index.d.ts +1 -1
- package/dist/transaction/index.d.ts.map +1 -1
- package/dist/transaction/index.js +1 -1
- package/dist/transaction/index.js.map +1 -1
- package/dist/transaction/p2id.d.ts +17 -6
- package/dist/transaction/p2id.d.ts.map +1 -1
- package/dist/transaction/p2id.js +22 -31
- package/dist/transaction/p2id.js.map +1 -1
- package/dist/transaction/p2id.test.js +62 -24
- package/dist/transaction/p2id.test.js.map +1 -1
- package/dist/transaction/summary.d.ts +45 -2
- package/dist/transaction/summary.d.ts.map +1 -1
- package/dist/transaction/summary.js +42 -2
- package/dist/transaction/summary.js.map +1 -1
- package/dist/transaction/summary.test.d.ts +2 -0
- package/dist/transaction/summary.test.d.ts.map +1 -0
- package/dist/transaction/summary.test.js +26 -0
- package/dist/transaction/summary.test.js.map +1 -0
- package/dist/transaction/updateGuardian.d.ts.map +1 -1
- package/dist/transaction/updateGuardian.js +16 -18
- package/dist/transaction/updateGuardian.js.map +1 -1
- package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
- package/dist/transaction/updateProcedureThreshold.js +15 -17
- package/dist/transaction/updateProcedureThreshold.js.map +1 -1
- package/dist/transaction/updateSigners.d.ts +6 -1
- package/dist/transaction/updateSigners.d.ts.map +1 -1
- package/dist/transaction/updateSigners.js +20 -17
- package/dist/transaction/updateSigners.js.map +1 -1
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +1 -1
- package/dist/transaction.js.map +1 -1
- package/dist/types/proposal.d.ts +28 -4
- package/dist/types/proposal.d.ts.map +1 -1
- package/dist/types/proposal.js +18 -0
- package/dist/types/proposal.js.map +1 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/signature.d.ts +11 -7
- package/dist/utils/signature.d.ts.map +1 -1
- package/dist/utils/signature.js +24 -58
- package/dist/utils/signature.js.map +1 -1
- package/dist/utils/word.d.ts +7 -0
- package/dist/utils/word.d.ts.map +1 -1
- package/dist/utils/word.js +15 -0
- package/dist/utils/word.js.map +1 -1
- package/masm/account_components/auth/guarded_multisig.masm +42 -0
- package/package.json +7 -4
- package/src/account/builder.test.ts +111 -45
- package/src/account/builder.ts +45 -33
- package/src/account/layout.ts +33 -0
- package/src/account/masm/account-components/auth.ts +36 -56
- package/src/account/masm/index.ts +0 -1
- package/src/account/storage.ts +9 -22
- package/src/client.test.ts +80 -15
- package/src/client.ts +5 -3
- package/src/index.ts +26 -1
- package/src/inspector.test.ts +330 -38
- package/src/inspector.ts +196 -33
- package/src/multisig.test.ts +679 -63
- package/src/multisig.ts +361 -107
- package/src/procedures.ts +6 -7
- package/src/proposal/metadata.test.ts +76 -0
- package/src/proposal/metadata.ts +13 -2
- package/src/prover/workflow.test.ts +9 -4
- package/src/prover/workflow.ts +15 -3
- package/src/transaction/index.ts +7 -1
- package/src/transaction/p2id.test.ts +112 -31
- package/src/transaction/p2id.ts +39 -38
- package/src/transaction/summary.test.ts +32 -0
- package/src/transaction/summary.ts +83 -4
- package/src/transaction/updateGuardian.ts +15 -25
- package/src/transaction/updateProcedureThreshold.ts +13 -29
- package/src/transaction/updateSigners.ts +38 -30
- package/src/transaction.ts +8 -1
- package/src/types/proposal.ts +43 -4
- package/src/types.ts +0 -1
- package/src/utils/signature.ts +32 -65
- package/src/utils/word.ts +17 -0
- package/dist/account/masm/auth.d.ts +0 -5
- package/dist/account/masm/auth.d.ts.map +0 -1
- package/dist/account/masm/auth.js +0 -1509
- package/dist/account/masm/auth.js.map +0 -1
- package/masm/account_components/auth/multisig.masm +0 -12
- package/masm/account_components/auth/multisig_ecdsa.masm +0 -12
- package/masm/account_components/auth/multisig_guardian.masm +0 -16
- package/masm/account_components/auth/multisig_guardian_ecdsa.masm +0 -16
- package/masm/auth/guardian.masm +0 -199
- package/masm/auth/guardian_ecdsa.masm +0 -195
- package/masm/auth/multisig.masm +0 -554
- package/masm/auth/multisig_ecdsa.masm +0 -554
- 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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
+
GUARDED_MULTISIG_ACCOUNT_COMPONENT_MASM,
|
|
130
125
|
);
|
|
131
126
|
expect(webClient.accounts.insert).toHaveBeenCalledTimes(1);
|
|
132
127
|
});
|
|
133
128
|
|
|
134
|
-
it('uses
|
|
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).
|
|
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
|
-
|
|
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
|
+
});
|
package/src/account/builder.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
37
|
+
dupw
|
|
38
|
+
# => [TX_SUMMARY_COMMITMENT, TX_SUMMARY_COMMITMENT]
|
|
39
|
+
|
|
40
|
+
exec.guardian::verify_signature
|
|
41
|
+
# => [TX_SUMMARY_COMMITMENT]
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
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
|
`;
|
package/src/account/storage.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
75
|
+
const slot1 = StorageSlot.map(GUARDIAN_SLOT_NAMES.SCHEME_ID, guardianSchemeMap);
|
|
89
76
|
|
|
90
|
-
return [slot0, slot1
|
|
77
|
+
return [slot0, slot1];
|
|
91
78
|
}
|
|
92
79
|
}
|
|
93
80
|
|
package/src/client.test.ts
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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: '
|
|
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
|
),
|