@openzeppelin/miden-multisig-client 0.16.1 → 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.
- package/README.md +192 -11
- 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 +4 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -5
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +74 -16
- package/dist/client.test.js.map +1 -1
- package/dist/index.d.ts +5 -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 +109 -33
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +307 -104
- package/dist/multisig.js.map +1 -1
- package/dist/multisig.test.js +582 -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/errors.d.ts +5 -0
- package/dist/prover/errors.d.ts.map +1 -1
- package/dist/prover/errors.js +7 -155
- package/dist/prover/errors.js.map +1 -1
- package/dist/prover/errors.test.js +5 -0
- package/dist/prover/errors.test.js.map +1 -1
- package/dist/prover/retry.d.ts +1 -6
- package/dist/prover/retry.d.ts.map +1 -1
- package/dist/prover/retry.js +5 -28
- package/dist/prover/retry.js.map +1 -1
- package/dist/prover/retry.test.js +1 -1
- package/dist/prover/retry.test.js.map +1 -1
- package/dist/prover/workflow.d.ts +4 -3
- 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 +28 -3
- package/dist/prover/workflow.test.js.map +1 -1
- package/dist/retry/classify.d.ts +16 -0
- package/dist/retry/classify.d.ts.map +1 -0
- package/dist/retry/classify.js +187 -0
- package/dist/retry/classify.js.map +1 -0
- package/dist/retry/runtime.d.ts +13 -0
- package/dist/retry/runtime.d.ts.map +1 -0
- package/dist/retry/runtime.js +33 -0
- package/dist/retry/runtime.js.map +1 -0
- package/dist/rpc/config.d.ts +11 -0
- package/dist/rpc/config.d.ts.map +1 -0
- package/dist/rpc/config.js +17 -0
- package/dist/rpc/config.js.map +1 -0
- package/dist/rpc/config.test.d.ts +2 -0
- package/dist/rpc/config.test.d.ts.map +1 -0
- package/dist/rpc/config.test.js +24 -0
- package/dist/rpc/config.test.js.map +1 -0
- package/dist/rpc/errors.d.ts +2 -0
- package/dist/rpc/errors.d.ts.map +1 -0
- package/dist/rpc/errors.js +11 -0
- package/dist/rpc/errors.js.map +1 -0
- package/dist/rpc/errors.test.d.ts +2 -0
- package/dist/rpc/errors.test.d.ts.map +1 -0
- package/dist/rpc/errors.test.js +34 -0
- package/dist/rpc/errors.test.js.map +1 -0
- package/dist/rpc/retry.d.ts +4 -0
- package/dist/rpc/retry.d.ts.map +1 -0
- package/dist/rpc/retry.js +6 -0
- package/dist/rpc/retry.js.map +1 -0
- package/dist/rpc/retry.test.d.ts +2 -0
- package/dist/rpc/retry.test.d.ts.map +1 -0
- package/dist/rpc/retry.test.js +98 -0
- package/dist/rpc/retry.test.js.map +1 -0
- 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 +87 -16
- package/src/client.ts +16 -3
- package/src/index.ts +27 -1
- package/src/inspector.test.ts +330 -38
- package/src/inspector.ts +196 -33
- package/src/multisig.test.ts +680 -63
- package/src/multisig.ts +400 -115
- package/src/procedures.ts +6 -7
- package/src/proposal/metadata.test.ts +76 -0
- package/src/proposal/metadata.ts +13 -2
- package/src/prover/errors.test.ts +8 -0
- package/src/prover/errors.ts +7 -175
- package/src/prover/retry.test.ts +1 -1
- package/src/prover/retry.ts +10 -35
- package/src/prover/workflow.test.ts +40 -4
- package/src/prover/workflow.ts +16 -4
- package/src/retry/classify.ts +220 -0
- package/src/retry/runtime.ts +45 -0
- package/src/rpc/config.test.ts +45 -0
- package/src/rpc/config.ts +30 -0
- package/src/rpc/errors.test.ts +69 -0
- package/src/rpc/errors.ts +12 -0
- package/src/rpc/retry.test.ts +144 -0
- package/src/rpc/retry.ts +12 -0
- 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
package/src/multisig.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* for proposal management.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { GuardianHttpClient, type AbandonCandidateResponse, type AbandonStatus, type DeltaObject, type ProposalSignature, type Signer, type AuthConfig, type StateObject } from '@openzeppelin/guardian-client';
|
|
8
|
+
import { GuardianHttpClient, type AbandonCandidateResponse, type AbandonStatus, type DeltaObject, type HistoryOptions, type HistoryPage, type ProposalSignature, type Signer, type AuthConfig, type StateObject } from '@openzeppelin/guardian-client';
|
|
9
9
|
import type {
|
|
10
10
|
ConsumableNote,
|
|
11
11
|
ExportedProposal,
|
|
@@ -36,9 +36,14 @@ import {
|
|
|
36
36
|
TransactionRequest,
|
|
37
37
|
TransactionSummary,
|
|
38
38
|
Word,
|
|
39
|
+
type ChainAnchor,
|
|
39
40
|
} from '@miden-sdk/miden-sdk';
|
|
40
41
|
import {
|
|
42
|
+
chainAnchorFromBase64,
|
|
43
|
+
chainAnchorToBase64,
|
|
41
44
|
executeForSummary,
|
|
45
|
+
executeForSummaryAt,
|
|
46
|
+
summarySalt,
|
|
42
47
|
buildUpdateSignersTransactionRequest,
|
|
43
48
|
buildUpdateProcedureThresholdTransactionRequest,
|
|
44
49
|
buildUpdateGuardianTransactionRequest,
|
|
@@ -47,6 +52,7 @@ import {
|
|
|
47
52
|
buildP2idTransactionRequest,
|
|
48
53
|
parseP2idNoteType,
|
|
49
54
|
p2idNoteTypeToMetadata,
|
|
55
|
+
type P2ideHeightOptions,
|
|
50
56
|
} from './transaction.js';
|
|
51
57
|
import { buildConsumeNotesTransactionRequestFromNotes } from './transaction/consumeNotes.js';
|
|
52
58
|
import {
|
|
@@ -67,6 +73,7 @@ import {
|
|
|
67
73
|
normalizeHexWord,
|
|
68
74
|
} from './utils/encoding.js';
|
|
69
75
|
import {
|
|
76
|
+
assertEcdsaSignatureRecoverable,
|
|
70
77
|
buildSignatureAdviceEntry,
|
|
71
78
|
normalizeSignerCommitment,
|
|
72
79
|
signatureHexToBytes,
|
|
@@ -74,7 +81,7 @@ import {
|
|
|
74
81
|
} from './utils/signature.js';
|
|
75
82
|
import { computeCommitmentFromTxSummary, accountIdToHex } from './multisig/helpers.js';
|
|
76
83
|
import { buildGuardianSignatureFromSigner } from './multisig/signing.js';
|
|
77
|
-
import { AccountInspector } from './inspector.js';
|
|
84
|
+
import { AccountInspector, assertCompleteDetectedConfig } from './inspector.js';
|
|
78
85
|
import { ProposalFactory } from './proposal/factory.js';
|
|
79
86
|
import { ProposalMetadataCodec } from './proposal/metadata.js';
|
|
80
87
|
import { ProposalSignatures } from './proposal/signatures.js';
|
|
@@ -88,6 +95,11 @@ import {
|
|
|
88
95
|
type ResolvedProverConfig,
|
|
89
96
|
} from './prover/config.js';
|
|
90
97
|
import { ProverWorkflow } from './prover/workflow.js';
|
|
98
|
+
import {
|
|
99
|
+
resolveRpcConfig,
|
|
100
|
+
type ResolvedRpcConfig,
|
|
101
|
+
} from './rpc/config.js';
|
|
102
|
+
import { retryRpcRead } from './rpc/retry.js';
|
|
91
103
|
|
|
92
104
|
/**
|
|
93
105
|
* Result of fetching account state from GUARDIAN.
|
|
@@ -109,6 +121,29 @@ export interface AccountStateVerificationResult {
|
|
|
109
121
|
onChainCommitment: string;
|
|
110
122
|
}
|
|
111
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Options shared by the `create*Proposal` family (issue #387). Every optional
|
|
126
|
+
* knob lives in a single trailing options bag, so call sites never need
|
|
127
|
+
* positional `undefined` holes to reach a later option.
|
|
128
|
+
*/
|
|
129
|
+
export interface CreateProposalOptions {
|
|
130
|
+
/** Proposal nonce; defaults to `Date.now()`. */
|
|
131
|
+
nonce?: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface CreateSignerProposalOptions extends CreateProposalOptions {
|
|
135
|
+
/**
|
|
136
|
+
* New signing threshold. Defaults to the current threshold on add, and to
|
|
137
|
+
* `min(current threshold, remaining signer count)` on remove.
|
|
138
|
+
*/
|
|
139
|
+
newThreshold?: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface CreateP2idProposalOptions extends CreateProposalOptions, P2ideHeightOptions {
|
|
143
|
+
/** Visibility of the created note. Defaults to `NoteType.Public` (issue #322). */
|
|
144
|
+
noteType?: NoteType;
|
|
145
|
+
}
|
|
146
|
+
|
|
112
147
|
/**
|
|
113
148
|
* Represents a multisig account with GUARDIAN integration.
|
|
114
149
|
*/
|
|
@@ -138,6 +173,27 @@ function deserializeTransactionRequest(bytes: Uint8Array): TransactionRequest {
|
|
|
138
173
|
}
|
|
139
174
|
}
|
|
140
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Single home for the proposal-nonce default, plus a runtime guard for
|
|
178
|
+
* pre-#387 positional callers. Untyped JS passing the old `nonce` number (or
|
|
179
|
+
* a legacy trailing argument) would otherwise bind it as the options bag and
|
|
180
|
+
* silently fall back to every default — a public note instead of a private
|
|
181
|
+
* one, or the current threshold instead of the requested one — so it must
|
|
182
|
+
* fail loudly instead.
|
|
183
|
+
*/
|
|
184
|
+
function resolveProposalNonce(
|
|
185
|
+
method: string,
|
|
186
|
+
options: CreateProposalOptions,
|
|
187
|
+
legacyArgs: readonly unknown[] = [],
|
|
188
|
+
): number {
|
|
189
|
+
if (typeof options !== 'object' || options === null || legacyArgs.length > 0) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`${method}: positional optional parameters were replaced by a trailing options object (issue #387); pass { nonce, ... } instead`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return options.nonce ?? Date.now();
|
|
195
|
+
}
|
|
196
|
+
|
|
141
197
|
export class Multisig {
|
|
142
198
|
account: Account;
|
|
143
199
|
threshold: number;
|
|
@@ -151,6 +207,7 @@ export class Multisig {
|
|
|
151
207
|
private readonly midenClient: MidenClient;
|
|
152
208
|
private readonly rawClientPromise: Promise<WasmWebClient>;
|
|
153
209
|
private readonly proverWorkflow: ProverWorkflow;
|
|
210
|
+
private readonly rpcConfig: ResolvedRpcConfig;
|
|
154
211
|
private readonly _accountId: string;
|
|
155
212
|
private readonly midenRpcEndpoint: string;
|
|
156
213
|
private proposals: Map<string, Proposal> = new Map();
|
|
@@ -164,6 +221,7 @@ export class Multisig {
|
|
|
164
221
|
accountId: string | undefined,
|
|
165
222
|
midenRpcEndpoint: string,
|
|
166
223
|
proverConfig?: ResolvedProverConfig,
|
|
224
|
+
rpcConfig?: ResolvedRpcConfig,
|
|
167
225
|
) {
|
|
168
226
|
this.account = account;
|
|
169
227
|
this.threshold = config.threshold;
|
|
@@ -183,6 +241,7 @@ export class Multisig {
|
|
|
183
241
|
this.midenClient,
|
|
184
242
|
proverConfig ?? resolveProverConfig(undefined, getTransactionProver(midenClient)),
|
|
185
243
|
);
|
|
244
|
+
this.rpcConfig = rpcConfig ?? resolveRpcConfig(undefined);
|
|
186
245
|
}
|
|
187
246
|
|
|
188
247
|
private getMidenRpcEndpoint(): string {
|
|
@@ -238,7 +297,37 @@ export class Multisig {
|
|
|
238
297
|
*/
|
|
239
298
|
async getStoreAccount(): Promise<Account> {
|
|
240
299
|
const webClient = await this.getRawClient();
|
|
241
|
-
|
|
300
|
+
const stored = await retryRpcRead(
|
|
301
|
+
() => webClient.getAccount(AccountId.fromHex(this._accountId)),
|
|
302
|
+
this.rpcConfig,
|
|
303
|
+
);
|
|
304
|
+
return stored ?? this.account;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Read the current ordered signer public-key commitments from account
|
|
309
|
+
* storage (store-backed state, falling back to the snapshot).
|
|
310
|
+
*
|
|
311
|
+
* Commitments are ordered by signer index as currently stored; indices
|
|
312
|
+
* re-pack when signers are removed, so index 0 is the creation-time first
|
|
313
|
+
* key only until the first membership change. Unlike the
|
|
314
|
+
* `signerCommitments` field, which reflects the config detected at
|
|
315
|
+
* construction / last sync, this reads the account state directly.
|
|
316
|
+
* See `AccountInspector.getSignerPublicKeyCommitments` (issue #306).
|
|
317
|
+
*/
|
|
318
|
+
async getSignerPublicKeyCommitments(): Promise<string[]> {
|
|
319
|
+
const account = await this.getStoreAccount();
|
|
320
|
+
return AccountInspector.getSignerPublicKeyCommitments(account);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Read the current guardian public-key commitment from account storage.
|
|
325
|
+
* The guarded-multisig always includes a guardian, so this throws (rather
|
|
326
|
+
* than returning null) when the entry is missing.
|
|
327
|
+
*/
|
|
328
|
+
async getGuardianPublicKeyCommitment(): Promise<string> {
|
|
329
|
+
const account = await this.getStoreAccount();
|
|
330
|
+
return AccountInspector.getGuardianPublicKeyCommitment(account);
|
|
242
331
|
}
|
|
243
332
|
|
|
244
333
|
/**
|
|
@@ -283,6 +372,43 @@ export class Multisig {
|
|
|
283
372
|
return this.procedureThresholds.get(procedure) ?? this.threshold;
|
|
284
373
|
}
|
|
285
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Per-procedure threshold overrides whose effective signing ratio is diluted
|
|
377
|
+
* by growing the signer set to `newNumSigners`.
|
|
378
|
+
*
|
|
379
|
+
* Overrides are absolute signature counts, not ratios, and the on-chain
|
|
380
|
+
* `update_signers_and_threshold` procedure does not re-scale them: growing
|
|
381
|
+
* the approver set silently lowers every override's effective signing ratio
|
|
382
|
+
* (a 2-of-2 override becomes 2-of-n). Callers creating a proposal that grows
|
|
383
|
+
* the signer set should surface these overrides and suggest raising them via
|
|
384
|
+
* an update-procedure-threshold proposal alongside the growth.
|
|
385
|
+
*
|
|
386
|
+
* @param newNumSigners - Signer-set size the proposal produces
|
|
387
|
+
* @returns The configured overrides, or an empty list when the set does not grow
|
|
388
|
+
*/
|
|
389
|
+
overridesDilutedBySignerGrowth(
|
|
390
|
+
newNumSigners: number,
|
|
391
|
+
): Array<{ procedure: ProcedureName; threshold: number }> {
|
|
392
|
+
if (newNumSigners <= this.signerCommitments.length) {
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
return Array.from(this.procedureThresholds.entries()).map(([procedure, threshold]) => ({
|
|
396
|
+
procedure,
|
|
397
|
+
threshold,
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private warnOnOverrideDilution(newNumSigners: number): void {
|
|
402
|
+
const current = this.signerCommitments.length;
|
|
403
|
+
for (const { procedure, threshold } of this.overridesDilutedBySignerGrowth(newNumSigners)) {
|
|
404
|
+
console.warn(
|
|
405
|
+
`growing the signer set dilutes the ${procedure} threshold override ` +
|
|
406
|
+
`(${threshold}-of-${current} becomes ${threshold}-of-${newNumSigners}); consider raising it ` +
|
|
407
|
+
`via an update-procedure-threshold proposal alongside the signer update`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
286
412
|
/**
|
|
287
413
|
* Update the GUARDIAN client used by this Multisig instance.
|
|
288
414
|
*
|
|
@@ -326,7 +452,10 @@ export class Multisig {
|
|
|
326
452
|
const state = await this.fetchState();
|
|
327
453
|
const accountId = AccountId.fromHex(this._accountId);
|
|
328
454
|
const webClient = await this.getRawClient();
|
|
329
|
-
const localAccount = await
|
|
455
|
+
const localAccount = await retryRpcRead(
|
|
456
|
+
() => webClient.getAccount(accountId),
|
|
457
|
+
this.rpcConfig,
|
|
458
|
+
);
|
|
330
459
|
let accountForConfigRefresh: Account | null = localAccount ?? null;
|
|
331
460
|
|
|
332
461
|
const guardianCommitment = normalizeHexWord(state.commitment);
|
|
@@ -351,7 +480,10 @@ export class Multisig {
|
|
|
351
480
|
async verifyStateCommitment(): Promise<AccountStateVerificationResult> {
|
|
352
481
|
const accountId = AccountId.fromHex(this._accountId);
|
|
353
482
|
const webClient = await this.getRawClient();
|
|
354
|
-
const localAccount = await
|
|
483
|
+
const localAccount = await retryRpcRead(
|
|
484
|
+
() => webClient.getAccount(accountId),
|
|
485
|
+
this.rpcConfig,
|
|
486
|
+
);
|
|
355
487
|
|
|
356
488
|
if (!localAccount) {
|
|
357
489
|
throw new Error(
|
|
@@ -434,7 +566,10 @@ export class Multisig {
|
|
|
434
566
|
const rpcClient = new RpcClient(new Endpoint(this.getMidenRpcEndpoint()));
|
|
435
567
|
|
|
436
568
|
try {
|
|
437
|
-
const accountDetails = await
|
|
569
|
+
const accountDetails = await retryRpcRead(
|
|
570
|
+
() => rpcClient.getAccountDetails(accountId),
|
|
571
|
+
this.rpcConfig,
|
|
572
|
+
);
|
|
438
573
|
// If the account is not found or its commitment is zero, means that the account is not deployed yet
|
|
439
574
|
if (!accountDetails) {
|
|
440
575
|
return null;
|
|
@@ -465,12 +600,14 @@ export class Multisig {
|
|
|
465
600
|
|
|
466
601
|
try {
|
|
467
602
|
const detected = AccountInspector.fromAccount(account);
|
|
603
|
+
// Fail closed on a partial read: adopting a truncated signer set would
|
|
604
|
+
// let membership proposals rewrite the account without the omitted
|
|
605
|
+
// keys. The catch below keeps the previously validated config instead.
|
|
606
|
+
assertCompleteDetectedConfig(detected);
|
|
468
607
|
this.account = account;
|
|
469
608
|
this.threshold = detected.threshold;
|
|
470
609
|
this.signerCommitments = detected.signerCommitments;
|
|
471
|
-
|
|
472
|
-
this.guardianCommitment = detected.guardianCommitment;
|
|
473
|
-
}
|
|
610
|
+
this.guardianCommitment = detected.guardianCommitment;
|
|
474
611
|
this.procedureThresholds = new Map(detected.procedureThresholds);
|
|
475
612
|
} catch (error) {
|
|
476
613
|
console.warn('Failed to refresh multisig config from account state', error);
|
|
@@ -578,17 +715,19 @@ export class Multisig {
|
|
|
578
715
|
* Create an "add signer" proposal.
|
|
579
716
|
*
|
|
580
717
|
* @param newCommitment - Commitment of the new signer (hex)
|
|
581
|
-
* @param
|
|
582
|
-
*
|
|
718
|
+
* @param options - Optional settings: `nonce`, `newThreshold` (defaults to
|
|
719
|
+
* current threshold)
|
|
583
720
|
*/
|
|
584
721
|
async createAddSignerProposal(
|
|
585
722
|
newCommitment: string,
|
|
586
|
-
|
|
587
|
-
|
|
723
|
+
options: CreateSignerProposalOptions = {},
|
|
724
|
+
...legacyArgs: never[]
|
|
588
725
|
): Promise<Proposal> {
|
|
726
|
+
const proposalNonce = resolveProposalNonce('createAddSignerProposal', options, legacyArgs);
|
|
589
727
|
const webClient = await this.getRawClient();
|
|
590
|
-
const targetThreshold = newThreshold ?? this.threshold;
|
|
728
|
+
const targetThreshold = options.newThreshold ?? this.threshold;
|
|
591
729
|
const targetSignerCommitments = [...this.signerCommitments, newCommitment];
|
|
730
|
+
this.warnOnOverrideDilution(targetSignerCommitments.length);
|
|
592
731
|
|
|
593
732
|
const { request, salt } = await buildUpdateSignersTransactionRequest(
|
|
594
733
|
webClient,
|
|
@@ -597,11 +736,13 @@ export class Multisig {
|
|
|
597
736
|
{ signatureScheme: this.signer.scheme },
|
|
598
737
|
);
|
|
599
738
|
|
|
600
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
739
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
740
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
741
|
+
anchor.free();
|
|
601
742
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
602
|
-
const proposalNonce = nonce ?? Date.now();
|
|
603
743
|
|
|
604
744
|
const metadata: ProposalMetadata = {
|
|
745
|
+
chainAnchor,
|
|
605
746
|
proposalType: 'add_signer',
|
|
606
747
|
targetThreshold,
|
|
607
748
|
targetSignerCommitments,
|
|
@@ -617,14 +758,15 @@ export class Multisig {
|
|
|
617
758
|
* Create a "remove signer" proposal by executing the update_signers script to summary.
|
|
618
759
|
*
|
|
619
760
|
* @param signerToRemove - Commitment of the signer to remove (hex)
|
|
620
|
-
* @param
|
|
621
|
-
*
|
|
761
|
+
* @param options - Optional settings: `nonce`, `newThreshold` (defaults to
|
|
762
|
+
* min of current threshold and new signer count)
|
|
622
763
|
*/
|
|
623
764
|
async createRemoveSignerProposal(
|
|
624
765
|
signerToRemove: string,
|
|
625
|
-
|
|
626
|
-
|
|
766
|
+
options: CreateSignerProposalOptions = {},
|
|
767
|
+
...legacyArgs: never[]
|
|
627
768
|
): Promise<Proposal> {
|
|
769
|
+
const proposalNonce = resolveProposalNonce('createRemoveSignerProposal', options, legacyArgs);
|
|
628
770
|
const webClient = await this.getRawClient();
|
|
629
771
|
const normalizedRemove = signerToRemove.toLowerCase();
|
|
630
772
|
const targetSignerCommitments = this.signerCommitments.filter(
|
|
@@ -638,7 +780,7 @@ export class Multisig {
|
|
|
638
780
|
throw new Error('Cannot remove the last signer');
|
|
639
781
|
}
|
|
640
782
|
|
|
641
|
-
const targetThreshold = newThreshold ?? Math.min(this.threshold, targetSignerCommitments.length);
|
|
783
|
+
const targetThreshold = options.newThreshold ?? Math.min(this.threshold, targetSignerCommitments.length);
|
|
642
784
|
|
|
643
785
|
if (targetThreshold < 1 || targetThreshold > targetSignerCommitments.length) {
|
|
644
786
|
throw new Error(
|
|
@@ -653,11 +795,13 @@ export class Multisig {
|
|
|
653
795
|
{ signatureScheme: this.signer.scheme },
|
|
654
796
|
);
|
|
655
797
|
|
|
656
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
798
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
799
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
800
|
+
anchor.free();
|
|
657
801
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
658
|
-
const proposalNonce = nonce ?? Date.now();
|
|
659
802
|
|
|
660
803
|
const metadata: ProposalMetadata = {
|
|
804
|
+
chainAnchor,
|
|
661
805
|
proposalType: 'remove_signer',
|
|
662
806
|
targetThreshold,
|
|
663
807
|
targetSignerCommitments,
|
|
@@ -673,12 +817,13 @@ export class Multisig {
|
|
|
673
817
|
* Create a "change threshold" proposal.
|
|
674
818
|
*
|
|
675
819
|
* @param newThreshold - The new threshold value
|
|
676
|
-
* @param
|
|
820
|
+
* @param options - Optional settings: `nonce`
|
|
677
821
|
*/
|
|
678
822
|
async createChangeThresholdProposal(
|
|
679
823
|
newThreshold: number,
|
|
680
|
-
|
|
824
|
+
options: CreateProposalOptions = {},
|
|
681
825
|
): Promise<Proposal> {
|
|
826
|
+
const proposalNonce = resolveProposalNonce('createChangeThresholdProposal', options);
|
|
682
827
|
const webClient = await this.getRawClient();
|
|
683
828
|
if (newThreshold < 1 || newThreshold > this.signerCommitments.length) {
|
|
684
829
|
throw new Error(
|
|
@@ -697,11 +842,13 @@ export class Multisig {
|
|
|
697
842
|
{ signatureScheme: this.signer.scheme },
|
|
698
843
|
);
|
|
699
844
|
|
|
700
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
845
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
846
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
847
|
+
anchor.free();
|
|
701
848
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
702
|
-
const proposalNonce = nonce ?? Date.now();
|
|
703
849
|
|
|
704
850
|
const metadata: ProposalMetadata = {
|
|
851
|
+
chainAnchor,
|
|
705
852
|
proposalType: 'change_threshold',
|
|
706
853
|
targetThreshold: newThreshold,
|
|
707
854
|
targetSignerCommitments: this.signerCommitments,
|
|
@@ -716,8 +863,9 @@ export class Multisig {
|
|
|
716
863
|
async createUpdateProcedureThresholdProposal(
|
|
717
864
|
targetProcedure: ProcedureName,
|
|
718
865
|
targetThreshold: number,
|
|
719
|
-
|
|
866
|
+
options: CreateProposalOptions = {},
|
|
720
867
|
): Promise<Proposal> {
|
|
868
|
+
const proposalNonce = resolveProposalNonce('createUpdateProcedureThresholdProposal', options);
|
|
721
869
|
const webClient = await this.getRawClient();
|
|
722
870
|
if (targetThreshold < 0 || targetThreshold > this.signerCommitments.length) {
|
|
723
871
|
throw new Error(
|
|
@@ -743,14 +891,16 @@ export class Multisig {
|
|
|
743
891
|
{ signatureScheme: this.signer.scheme },
|
|
744
892
|
);
|
|
745
893
|
|
|
746
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
894
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
895
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
896
|
+
anchor.free();
|
|
747
897
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
748
|
-
const proposalNonce = nonce ?? Date.now();
|
|
749
898
|
const action = targetThreshold === 0
|
|
750
899
|
? `Clear threshold override for ${targetProcedure}`
|
|
751
900
|
: `Set ${targetProcedure} threshold override to ${targetThreshold}`;
|
|
752
901
|
|
|
753
902
|
const metadata: ProposalMetadata = {
|
|
903
|
+
chainAnchor,
|
|
754
904
|
proposalType: 'update_procedure_threshold',
|
|
755
905
|
targetProcedure,
|
|
756
906
|
targetThreshold,
|
|
@@ -767,13 +917,14 @@ export class Multisig {
|
|
|
767
917
|
*
|
|
768
918
|
* @param newGuardianEndpoint - The new GUARDIAN server endpoint URL
|
|
769
919
|
* @param newGuardianPubkey - The new GUARDIAN server's public key commitment (hex)
|
|
770
|
-
* @param
|
|
920
|
+
* @param options - Optional settings: `nonce`
|
|
771
921
|
*/
|
|
772
922
|
async createSwitchGuardianProposal(
|
|
773
923
|
newGuardianEndpoint: string,
|
|
774
924
|
newGuardianPubkey: string,
|
|
775
|
-
|
|
925
|
+
options: CreateProposalOptions = {},
|
|
776
926
|
): Promise<Proposal> {
|
|
927
|
+
const proposalNonce = resolveProposalNonce('createSwitchGuardianProposal', options);
|
|
777
928
|
const webClient = await this.getRawClient();
|
|
778
929
|
await this.verifyGuardianEndpointCommitment(newGuardianEndpoint, newGuardianPubkey);
|
|
779
930
|
|
|
@@ -783,11 +934,13 @@ export class Multisig {
|
|
|
783
934
|
{ signatureScheme: this.signer.scheme },
|
|
784
935
|
);
|
|
785
936
|
|
|
786
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
937
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
938
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
939
|
+
anchor.free();
|
|
787
940
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
788
|
-
const proposalNonce = nonce ?? Date.now();
|
|
789
941
|
|
|
790
942
|
const metadata: ProposalMetadata = {
|
|
943
|
+
chainAnchor,
|
|
791
944
|
proposalType: 'switch_guardian',
|
|
792
945
|
saltHex: salt.toHex(),
|
|
793
946
|
requiredSignatures: this.getEffectiveThreshold('switch_guardian'),
|
|
@@ -805,12 +958,13 @@ export class Multisig {
|
|
|
805
958
|
* Create a "consume notes" proposal to consume notes sent to the multisig account.
|
|
806
959
|
*
|
|
807
960
|
* @param noteIds - IDs of the notes to consume (hex strings)
|
|
808
|
-
* @param
|
|
961
|
+
* @param options - Optional settings: `nonce`
|
|
809
962
|
*/
|
|
810
963
|
async createConsumeNotesProposal(
|
|
811
964
|
noteIds: string[],
|
|
812
|
-
|
|
965
|
+
options: CreateProposalOptions = {},
|
|
813
966
|
): Promise<Proposal> {
|
|
967
|
+
const proposalNonce = resolveProposalNonce('createConsumeNotesProposal', options);
|
|
814
968
|
const webClient = await this.getRawClient();
|
|
815
969
|
if (noteIds.length === 0) {
|
|
816
970
|
throw new Error('At least one note ID is required');
|
|
@@ -830,11 +984,13 @@ export class Multisig {
|
|
|
830
984
|
|
|
831
985
|
const { request, salt } = buildConsumeNotesTransactionRequestFromNotes(fetchedNotes);
|
|
832
986
|
|
|
833
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
987
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
988
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
989
|
+
anchor.free();
|
|
834
990
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
835
|
-
const proposalNonce = nonce ?? Date.now();
|
|
836
991
|
|
|
837
992
|
const metadata: ProposalMetadata = {
|
|
993
|
+
chainAnchor,
|
|
838
994
|
proposalType: 'consume_notes',
|
|
839
995
|
noteIds,
|
|
840
996
|
metadataVersion: CONSUME_NOTES_METADATA_VERSION_V2,
|
|
@@ -863,38 +1019,41 @@ export class Multisig {
|
|
|
863
1019
|
* @param recipientId - Account ID of the recipient (hex string)
|
|
864
1020
|
* @param faucetId - Faucet/token account ID (hex string)
|
|
865
1021
|
* @param amount - Amount to send
|
|
866
|
-
* @param
|
|
867
|
-
*
|
|
868
|
-
*
|
|
1022
|
+
* @param options - Optional settings: `nonce`; `noteType` selects the created
|
|
1023
|
+
* note's visibility (defaults to `NoteType.Public`, issue #322);
|
|
1024
|
+
* `reclaimHeight`/`timelockHeight` build a P2IDE note (issue #366)
|
|
869
1025
|
*/
|
|
870
1026
|
async createP2idProposal(
|
|
871
1027
|
recipientId: string,
|
|
872
1028
|
faucetId: string,
|
|
873
1029
|
amount: bigint,
|
|
874
|
-
|
|
875
|
-
|
|
1030
|
+
options: CreateP2idProposalOptions = {},
|
|
1031
|
+
...legacyArgs: never[]
|
|
876
1032
|
): Promise<Proposal> {
|
|
1033
|
+
const proposalNonce = resolveProposalNonce('createP2idProposal', options, legacyArgs);
|
|
877
1034
|
const webClient = await this.getRawClient();
|
|
878
1035
|
if (amount <= 0n) {
|
|
879
1036
|
throw new Error('Amount must be greater than 0');
|
|
880
1037
|
}
|
|
881
1038
|
|
|
882
|
-
|
|
883
|
-
|
|
1039
|
+
// Forward everything but the nonce, so a note option added to
|
|
1040
|
+
// CreateP2idProposalOptions can't be silently dropped before the builder.
|
|
1041
|
+
const { nonce: _nonce, ...noteOptions } = options;
|
|
884
1042
|
const { request, salt } = buildP2idTransactionRequest(
|
|
885
1043
|
this._accountId,
|
|
886
1044
|
recipientId,
|
|
887
1045
|
faucetId,
|
|
888
1046
|
amount,
|
|
889
|
-
|
|
890
|
-
{ noteType: options.noteType },
|
|
1047
|
+
noteOptions,
|
|
891
1048
|
);
|
|
892
1049
|
|
|
893
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
1050
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
1051
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
1052
|
+
anchor.free();
|
|
894
1053
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
895
|
-
const proposalNonce = nonce ?? Date.now();
|
|
896
1054
|
|
|
897
1055
|
const metadata: ProposalMetadata = {
|
|
1056
|
+
chainAnchor,
|
|
898
1057
|
proposalType: 'p2id',
|
|
899
1058
|
saltHex: salt.toHex(),
|
|
900
1059
|
requiredSignatures: this.getEffectiveThreshold('p2id'),
|
|
@@ -903,6 +1062,9 @@ export class Multisig {
|
|
|
903
1062
|
amount: amount.toString(),
|
|
904
1063
|
// Omitted for public notes so the wire shape matches pre-#322 proposals.
|
|
905
1064
|
noteType: p2idNoteTypeToMetadata(options.noteType),
|
|
1065
|
+
// Omitted when absent so plain-P2ID payloads keep the pre-#366 wire shape.
|
|
1066
|
+
reclaimHeight: options.reclaimHeight,
|
|
1067
|
+
timelockHeight: options.timelockHeight,
|
|
906
1068
|
description: `Send ${amount} of asset ${faucetId.slice(0, 10)}... to ${recipientId.slice(0, 10)}...`,
|
|
907
1069
|
};
|
|
908
1070
|
|
|
@@ -962,7 +1124,7 @@ export class Multisig {
|
|
|
962
1124
|
|
|
963
1125
|
/**
|
|
964
1126
|
* Export a note created by this multisig account as serialized note-file
|
|
965
|
-
* bytes for out-of-band delivery
|
|
1127
|
+
* bytes for out-of-band delivery.
|
|
966
1128
|
*
|
|
967
1129
|
* A private note publishes only its commitment on chain, so the recipient
|
|
968
1130
|
* can never learn its contents via sync; the sender must hand them the
|
|
@@ -1005,7 +1167,7 @@ export class Multisig {
|
|
|
1005
1167
|
|
|
1006
1168
|
/**
|
|
1007
1169
|
* Export a note created by this multisig account as a note file downloaded
|
|
1008
|
-
* by the browser
|
|
1170
|
+
* by the browser. Browser-only convenience over
|
|
1009
1171
|
* {@link exportNoteToBytes}; use that method directly in non-DOM
|
|
1010
1172
|
* environments.
|
|
1011
1173
|
*
|
|
@@ -1033,7 +1195,7 @@ export class Multisig {
|
|
|
1033
1195
|
}
|
|
1034
1196
|
|
|
1035
1197
|
/**
|
|
1036
|
-
* Import a note file received out-of-band
|
|
1198
|
+
* Import a note file received out-of-band so the note can be
|
|
1037
1199
|
* consumed by this multisig account.
|
|
1038
1200
|
*
|
|
1039
1201
|
* Sync the Miden client with the network afterwards so the note's on-chain
|
|
@@ -1060,7 +1222,7 @@ export class Multisig {
|
|
|
1060
1222
|
}
|
|
1061
1223
|
|
|
1062
1224
|
/**
|
|
1063
|
-
* Import a note file received out-of-band
|
|
1225
|
+
* Import a note file received out-of-band from a browser
|
|
1064
1226
|
* `File`/`Blob` (e.g. a file-input selection). See
|
|
1065
1227
|
* {@link importNoteFromBytes} for the returned identifier semantics.
|
|
1066
1228
|
*/
|
|
@@ -1075,10 +1237,9 @@ export class Multisig {
|
|
|
1075
1237
|
* The P2ID note is rebuilt deterministically from the proposal salt, so the
|
|
1076
1238
|
* ID is known ahead of execution. For a private P2ID this is the ID to pass
|
|
1077
1239
|
* to {@link exportNoteToBytes} after executing, so the note file can be delivered
|
|
1078
|
-
* to the recipient out-of-band
|
|
1240
|
+
* to the recipient out-of-band.
|
|
1079
1241
|
*
|
|
1080
|
-
*
|
|
1081
|
-
* current vault state, which execution itself changes.
|
|
1242
|
+
* The note ID remains deterministic from the proposal metadata and salt.
|
|
1082
1243
|
*/
|
|
1083
1244
|
async getP2idNoteId(proposal: Proposal): Promise<string> {
|
|
1084
1245
|
const metadata = proposal.metadata;
|
|
@@ -1092,15 +1253,14 @@ export class Multisig {
|
|
|
1092
1253
|
throw new Error('getP2idNoteId requires a P2ID proposal with recipient, faucet, amount, and salt metadata');
|
|
1093
1254
|
}
|
|
1094
1255
|
|
|
1095
|
-
const account = await this.getStoreAccount();
|
|
1096
1256
|
const note = buildP2idNoteFromMetadata(
|
|
1097
1257
|
this._accountId,
|
|
1098
1258
|
metadata.recipientId,
|
|
1099
1259
|
metadata.faucetId,
|
|
1100
1260
|
BigInt(metadata.amount),
|
|
1101
|
-
account,
|
|
1102
1261
|
parseP2idNoteType(metadata.noteType),
|
|
1103
1262
|
metadata.saltHex,
|
|
1263
|
+
{ reclaimHeight: metadata.reclaimHeight, timelockHeight: metadata.timelockHeight },
|
|
1104
1264
|
);
|
|
1105
1265
|
return note.id().toString();
|
|
1106
1266
|
}
|
|
@@ -1145,6 +1305,19 @@ export class Multisig {
|
|
|
1145
1305
|
return this.guardian.abandonStatus(this._accountId, nonce);
|
|
1146
1306
|
}
|
|
1147
1307
|
|
|
1308
|
+
/**
|
|
1309
|
+
* Fetch one page of this account's canonical delta history
|
|
1310
|
+
* from GUARDIAN (issue #413), newest-first by nonce, with decoded
|
|
1311
|
+
* input/output note summaries. Pass `options.cursor` from a previous
|
|
1312
|
+
* page's `nextCursor` to resume; an absent `nextCursor` means the
|
|
1313
|
+
* feed is exhausted. Served while the account is paused. Only
|
|
1314
|
+
* transactions pushed through GUARDIAN appear — history of
|
|
1315
|
+
* transactions executed elsewhere is not visible to it.
|
|
1316
|
+
*/
|
|
1317
|
+
async deltaHistory(options: HistoryOptions = {}): Promise<HistoryPage> {
|
|
1318
|
+
return this.guardian.getDeltaHistory(this._accountId, options);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1148
1321
|
async signProposal(proposalId: string): Promise<Proposal> {
|
|
1149
1322
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1150
1323
|
const existingProposal = await this.getProposalForSigning(proposalId, normalizedProposalId);
|
|
@@ -1206,8 +1379,16 @@ export class Multisig {
|
|
|
1206
1379
|
async executeProposal(proposalId: string): Promise<void> {
|
|
1207
1380
|
const { metadata, finalRequest, proposal } = await this.prepareProposalExecution(proposalId);
|
|
1208
1381
|
|
|
1382
|
+
// Execute at the proposal's anchored reference block, so the summary the
|
|
1383
|
+
// cosigners signed reproduces exactly. The anchor was already checked
|
|
1384
|
+
// against the summary's block commitment during binding verification.
|
|
1209
1385
|
const accountId = AccountId.fromHex(this._accountId);
|
|
1210
|
-
|
|
1386
|
+
const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
|
|
1387
|
+
try {
|
|
1388
|
+
await this.proverWorkflow.submitAt(accountId, finalRequest, anchor);
|
|
1389
|
+
} finally {
|
|
1390
|
+
anchor.free();
|
|
1391
|
+
}
|
|
1211
1392
|
|
|
1212
1393
|
if (metadata.proposalType === 'switch_guardian') {
|
|
1213
1394
|
if (!metadata.newGuardianEndpoint || !metadata.newGuardianPubkey) {
|
|
@@ -1228,15 +1409,25 @@ export class Multisig {
|
|
|
1228
1409
|
...switchDelta,
|
|
1229
1410
|
deltaPayload: switchDelta.deltaPayload.txSummary,
|
|
1230
1411
|
});
|
|
1231
|
-
} catch {
|
|
1232
|
-
//
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
// Best-effort — see above — but the failure must be visible: a
|
|
1414
|
+
// silently lost push leaves the pre-switch GUARDIAN serving this
|
|
1415
|
+
// account (split-brain, issue #305) with nothing to diagnose by.
|
|
1416
|
+
console.warn(
|
|
1417
|
+
'SwitchGuardian delta push to the pre-switch GUARDIAN failed; it ' +
|
|
1418
|
+
'will keep serving this account until reconciliation',
|
|
1419
|
+
error,
|
|
1420
|
+
);
|
|
1233
1421
|
}
|
|
1234
1422
|
|
|
1235
1423
|
try {
|
|
1236
1424
|
const webClient = await this.getRawClient();
|
|
1237
|
-
await webClient.syncState();
|
|
1425
|
+
await retryRpcRead(() => webClient.syncState(), this.rpcConfig);
|
|
1238
1426
|
|
|
1239
|
-
const updatedAccount = await
|
|
1427
|
+
const updatedAccount = await retryRpcRead(
|
|
1428
|
+
() => webClient.getAccount(accountId),
|
|
1429
|
+
this.rpcConfig,
|
|
1430
|
+
);
|
|
1240
1431
|
if (!updatedAccount) {
|
|
1241
1432
|
throw new Error(
|
|
1242
1433
|
`Updated account ${this._accountId} is missing from local client`
|
|
@@ -1264,14 +1455,42 @@ export class Multisig {
|
|
|
1264
1455
|
* Submit an integration-built transaction (advice already injected). Mirrors
|
|
1265
1456
|
* the Rust `submit_transaction`; used by the custom proposal producer flow
|
|
1266
1457
|
* after `prepareCustomExecution` rebuilds its request with the returned advice.
|
|
1458
|
+
* The transaction is executed at the proposal's anchored reference block,
|
|
1459
|
+
* since the collected signatures only authorize the summary produced there.
|
|
1267
1460
|
*/
|
|
1268
|
-
async submitTransaction(request: TransactionRequest): Promise<void> {
|
|
1269
|
-
|
|
1461
|
+
async submitTransaction(proposalId: string, request: TransactionRequest): Promise<void> {
|
|
1462
|
+
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1463
|
+
const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
|
|
1464
|
+
const existing = this.getLocalProposal(proposalId);
|
|
1465
|
+
const proposal = this.proposalFactory().fromDelta(
|
|
1466
|
+
delta,
|
|
1467
|
+
normalizedProposalId,
|
|
1468
|
+
existing?.metadata,
|
|
1469
|
+
existing?.signatures ?? [],
|
|
1470
|
+
);
|
|
1471
|
+
|
|
1472
|
+
const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
|
|
1473
|
+
try {
|
|
1474
|
+
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
1475
|
+
const txSummary = TransactionSummary.deserialize(
|
|
1476
|
+
base64ToUint8Array(delta.deltaPayload.txSummary.data),
|
|
1477
|
+
);
|
|
1478
|
+
const summaryBlockCommitment = normalizeHexWord(txSummary.blockCommitment().toHex());
|
|
1479
|
+
if (anchorCommitment !== summaryBlockCommitment) {
|
|
1480
|
+
throw new Error(
|
|
1481
|
+
`Proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`,
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
await this.proverWorkflow.submitAt(AccountId.fromHex(this._accountId), request, anchor);
|
|
1486
|
+
} finally {
|
|
1487
|
+
anchor.free();
|
|
1488
|
+
}
|
|
1270
1489
|
}
|
|
1271
1490
|
|
|
1272
1491
|
/**
|
|
1273
|
-
* Create a proposal from a producer-built transaction the SDK does not model
|
|
1274
|
-
*
|
|
1492
|
+
* Create a proposal from a producer-built transaction the SDK does not model.
|
|
1493
|
+
* `transactionRequestBytes` is a serialized TransactionRequest;
|
|
1275
1494
|
* `proposalType` is a free-form, non-empty label that must not collide with a
|
|
1276
1495
|
* built-in type. The integration keeps its own recipe to execute later via
|
|
1277
1496
|
* `prepareCustomExecution`.
|
|
@@ -1279,8 +1498,9 @@ export class Multisig {
|
|
|
1279
1498
|
async createCustomProposal(
|
|
1280
1499
|
transactionRequestBytes: Uint8Array,
|
|
1281
1500
|
proposalType: string,
|
|
1282
|
-
|
|
1501
|
+
options: CreateProposalOptions = {},
|
|
1283
1502
|
): Promise<Proposal> {
|
|
1503
|
+
const proposalNonce = resolveProposalNonce('createCustomProposal', options);
|
|
1284
1504
|
const label = proposalType.trim().toLowerCase();
|
|
1285
1505
|
if (label.length === 0) {
|
|
1286
1506
|
throw new Error('proposalType must not be empty');
|
|
@@ -1298,11 +1518,13 @@ export class Multisig {
|
|
|
1298
1518
|
|
|
1299
1519
|
const webClient = await this.getRawClient();
|
|
1300
1520
|
const request = deserializeTransactionRequest(transactionRequestBytes);
|
|
1301
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
1521
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
1522
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
1523
|
+
anchor.free();
|
|
1302
1524
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
1303
|
-
const proposalNonce = nonce ?? Date.now();
|
|
1304
1525
|
|
|
1305
1526
|
const metadata: ProposalMetadata = {
|
|
1527
|
+
chainAnchor,
|
|
1306
1528
|
proposalType: 'custom',
|
|
1307
1529
|
description: '',
|
|
1308
1530
|
rawProposalType: label,
|
|
@@ -1315,7 +1537,7 @@ export class Multisig {
|
|
|
1315
1537
|
/**
|
|
1316
1538
|
* Assemble the validated execution advice (cosigner signatures + GUARDIAN
|
|
1317
1539
|
* acknowledgment) for a ready custom proposal, so an integration can rebuild
|
|
1318
|
-
* its transaction with its own recipe and submit
|
|
1540
|
+
* its transaction with its own recipe and submit.
|
|
1319
1541
|
*
|
|
1320
1542
|
* `transactionRequestBytes` is the serialized transaction request; it is used only to verify
|
|
1321
1543
|
* (binding check) that it reproduces the signed commitment, before the
|
|
@@ -1361,9 +1583,27 @@ export class Multisig {
|
|
|
1361
1583
|
|
|
1362
1584
|
const bindingRequest = deserializeTransactionRequest(transactionRequestBytes);
|
|
1363
1585
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1586
|
+
// Probe at the proposal's anchored reference block: the signed summary
|
|
1587
|
+
// binds that block's commitment, so probing at the local sync height would
|
|
1588
|
+
// never reproduce it. The anchor arrives from an untrusted party via
|
|
1589
|
+
// GUARDIAN, so its block commitment is checked against the signed summary
|
|
1590
|
+
// before executing against it.
|
|
1591
|
+
const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
|
|
1592
|
+
let derivedCommitmentHex: string;
|
|
1593
|
+
try {
|
|
1594
|
+
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
1595
|
+
const summaryBlockCommitment = normalizeHexWord(txSummary.blockCommitment().toHex());
|
|
1596
|
+
if (anchorCommitment !== summaryBlockCommitment) {
|
|
1597
|
+
throw new Error(
|
|
1598
|
+
`Custom proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`,
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
const webClient = await this.getRawClient();
|
|
1602
|
+
const derived = await executeForSummaryAt(webClient, this._accountId, bindingRequest, anchor);
|
|
1603
|
+
derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
|
|
1604
|
+
} finally {
|
|
1605
|
+
anchor.free();
|
|
1606
|
+
}
|
|
1367
1607
|
if (derivedCommitmentHex !== signedCommitmentHex) {
|
|
1368
1608
|
throw new Error(
|
|
1369
1609
|
`Custom proposal binding mismatch: expected ${signedCommitmentHex}, got ${derivedCommitmentHex}`,
|
|
@@ -1419,12 +1659,17 @@ export class Multisig {
|
|
|
1419
1659
|
cosignerSig.signature.scheme,
|
|
1420
1660
|
);
|
|
1421
1661
|
const signature = Signature.deserialize(sigBytes);
|
|
1662
|
+
if (cosignerSig.signature.scheme === 'ecdsa' && ecdsaPublicKey) {
|
|
1663
|
+
assertEcdsaSignatureRecoverable(
|
|
1664
|
+
cosignerSig.signature.signature,
|
|
1665
|
+
normalizedTxCommitmentHex,
|
|
1666
|
+
ecdsaPublicKey,
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1422
1669
|
const { key, values } = buildSignatureAdviceEntry(
|
|
1423
1670
|
signerCommitment,
|
|
1424
1671
|
createTxCommitmentWord(),
|
|
1425
1672
|
signature,
|
|
1426
|
-
ecdsaPublicKey,
|
|
1427
|
-
cosignerSig.signature.scheme === 'ecdsa' ? cosignerSig.signature.signature : undefined,
|
|
1428
1673
|
);
|
|
1429
1674
|
const keyHex = normalizeHexWord(key.toHex());
|
|
1430
1675
|
if (adviceMapKeys.has(keyHex)) {
|
|
@@ -1455,12 +1700,13 @@ export class Multisig {
|
|
|
1455
1700
|
}
|
|
1456
1701
|
const ackSigBytes = signatureHexToBytes(ackSigHex, ackScheme);
|
|
1457
1702
|
const ackSignature = Signature.deserialize(ackSigBytes);
|
|
1703
|
+
if (ackScheme === 'ecdsa' && ackPubkey) {
|
|
1704
|
+
assertEcdsaSignatureRecoverable(ackSigHex, normalizedTxCommitmentHex, ackPubkey);
|
|
1705
|
+
}
|
|
1458
1706
|
const { key: ackKey, values: ackValues } = buildSignatureAdviceEntry(
|
|
1459
1707
|
guardianCommitment,
|
|
1460
1708
|
createTxCommitmentWord(),
|
|
1461
1709
|
ackSignature,
|
|
1462
|
-
ackScheme === 'ecdsa' ? ackPubkey : undefined,
|
|
1463
|
-
ackScheme === 'ecdsa' ? ackSigHex : undefined,
|
|
1464
1710
|
);
|
|
1465
1711
|
const ackKeyHex = normalizeHexWord(ackKey.toHex());
|
|
1466
1712
|
if (adviceMapKeys.has(ackKeyHex)) {
|
|
@@ -1527,7 +1773,7 @@ export class Multisig {
|
|
|
1527
1773
|
|
|
1528
1774
|
const txSummaryBytes = base64ToUint8Array(txSummaryBase64);
|
|
1529
1775
|
const txSummary = TransactionSummary.deserialize(txSummaryBytes);
|
|
1530
|
-
const saltHex = txSummary
|
|
1776
|
+
const saltHex = summarySalt(txSummary).toHex();
|
|
1531
1777
|
const txCommitmentHex = txSummary.toCommitment().toHex();
|
|
1532
1778
|
const normalizedTxCommitmentHex = normalizeHexWord(txCommitmentHex);
|
|
1533
1779
|
const normalizedSignerCommitments = new Set(
|
|
@@ -1568,14 +1814,17 @@ export class Multisig {
|
|
|
1568
1814
|
cosignerSig.signature.scheme,
|
|
1569
1815
|
);
|
|
1570
1816
|
const signature = Signature.deserialize(sigBytes);
|
|
1817
|
+
if (cosignerSig.signature.scheme === 'ecdsa' && ecdsaPublicKey) {
|
|
1818
|
+
assertEcdsaSignatureRecoverable(
|
|
1819
|
+
cosignerSig.signature.signature,
|
|
1820
|
+
normalizedTxCommitmentHex,
|
|
1821
|
+
ecdsaPublicKey,
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1571
1824
|
const { key, values } = buildSignatureAdviceEntry(
|
|
1572
1825
|
signerCommitment,
|
|
1573
1826
|
createTxCommitmentWord(),
|
|
1574
1827
|
signature,
|
|
1575
|
-
ecdsaPublicKey,
|
|
1576
|
-
cosignerSig.signature.scheme === 'ecdsa'
|
|
1577
|
-
? cosignerSig.signature.signature
|
|
1578
|
-
: undefined,
|
|
1579
1828
|
);
|
|
1580
1829
|
const keyHex = normalizeHexWord(key.toHex());
|
|
1581
1830
|
if (adviceMapKeys.has(keyHex)) {
|
|
@@ -1611,12 +1860,13 @@ export class Multisig {
|
|
|
1611
1860
|
}
|
|
1612
1861
|
const ackSigBytes = signatureHexToBytes(ackSigHex, ackScheme);
|
|
1613
1862
|
const ackSignature = Signature.deserialize(ackSigBytes);
|
|
1863
|
+
if (ackScheme === 'ecdsa' && ackPubkey) {
|
|
1864
|
+
assertEcdsaSignatureRecoverable(ackSigHex, normalizedTxCommitmentHex, ackPubkey);
|
|
1865
|
+
}
|
|
1614
1866
|
const { key: ackKey, values: ackValues } = buildSignatureAdviceEntry(
|
|
1615
1867
|
guardianCommitment,
|
|
1616
1868
|
createTxCommitmentWord(),
|
|
1617
1869
|
ackSignature,
|
|
1618
|
-
ackScheme === 'ecdsa' ? ackPubkey : undefined,
|
|
1619
|
-
ackScheme === 'ecdsa' ? ackSigHex : undefined,
|
|
1620
1870
|
);
|
|
1621
1871
|
const ackKeyHex = normalizeHexWord(ackKey.toHex());
|
|
1622
1872
|
if (adviceMapKeys.has(ackKeyHex)) {
|
|
@@ -1805,39 +2055,70 @@ export class Multisig {
|
|
|
1805
2055
|
|
|
1806
2056
|
private async verifyProposalMetadataBinding(proposal: Proposal): Promise<string> {
|
|
1807
2057
|
const txSummaryCommitment = this.ensureProposalCommitmentMatchesSummary(proposal);
|
|
1808
|
-
if (proposal.metadata.proposalType === 'custom') {
|
|
1809
|
-
// Custom proposals (issue #266) have no per-type reconstruction recipe;
|
|
1810
|
-
// the id ↔ tx_summary commitment match above is the only available
|
|
1811
|
-
// integrity guarantee for an opaque proposal.
|
|
1812
|
-
return txSummaryCommitment;
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
if (proposal.metadata.proposalType === 'switch_guardian') {
|
|
1816
|
-
// Exempt from binding re-execution (mirrors the `custom` exemption above).
|
|
1817
|
-
// The WASM `executeForSummary` leaves the guardian-disabling side effect
|
|
1818
|
-
// applied to the in-session account, so re-execution reconstructs a smaller
|
|
1819
|
-
// delta and falsely rejects with "metadata does not match tx_summary". The
|
|
1820
|
-
// native Rust client does not mutate, so this is an intentional divergence.
|
|
1821
|
-
// The id ↔ tx_summary match above plus `verifyGuardianEndpointCommitment`
|
|
1822
|
-
// at propose/execute time still bind the proposal.
|
|
1823
|
-
return txSummaryCommitment;
|
|
1824
|
-
}
|
|
1825
2058
|
|
|
1826
2059
|
const summary = TransactionSummary.deserialize(base64ToUint8Array(proposal.txSummary));
|
|
1827
|
-
const salt = proposal.metadata.saltHex
|
|
1828
|
-
? Word.fromHex(normalizeHexWord(proposal.metadata.saltHex))
|
|
1829
|
-
: summary.salt();
|
|
1830
2060
|
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2061
|
+
// The anchor arrives from an untrusted party via GUARDIAN, so check its
|
|
2062
|
+
// block commitment against the one bound into the signed summary before
|
|
2063
|
+
// anything executes against it. `ChainAnchor.deserialize` already enforced
|
|
2064
|
+
// internal header/chain consistency.
|
|
2065
|
+
const anchor = this.requireProposalAnchor(proposal.id, proposal.metadata);
|
|
2066
|
+
try {
|
|
2067
|
+
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
2068
|
+
const summaryBlockCommitment = normalizeHexWord(summary.blockCommitment().toHex());
|
|
2069
|
+
if (anchorCommitment !== summaryBlockCommitment) {
|
|
2070
|
+
throw new Error(
|
|
2071
|
+
`Invalid proposal: chain anchor does not match the block commitment bound into the tx_summary for ${proposal.id}`,
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
if (proposal.metadata.proposalType === 'custom') {
|
|
2076
|
+
// Custom proposals have no per-type reconstruction recipe;
|
|
2077
|
+
// the id ↔ tx_summary commitment match above is the only available
|
|
2078
|
+
// integrity guarantee for an opaque proposal.
|
|
2079
|
+
return txSummaryCommitment;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
if (proposal.metadata.proposalType === 'switch_guardian') {
|
|
2083
|
+
// Re-execution would mutate the WASM account twice. The proposal ID and
|
|
2084
|
+
// guardian endpoint commitment provide the binding checks for this type.
|
|
2085
|
+
return txSummaryCommitment;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
const salt = proposal.metadata.saltHex
|
|
2089
|
+
? Word.fromHex(normalizeHexWord(proposal.metadata.saltHex))
|
|
2090
|
+
: summarySalt(summary);
|
|
1835
2091
|
|
|
1836
|
-
|
|
1837
|
-
|
|
2092
|
+
const request = await this.buildTransactionRequestFromMetadata(proposal.metadata, salt);
|
|
2093
|
+
const webClient = await this.getRawClient();
|
|
2094
|
+
const reconstructed = await executeForSummaryAt(webClient, this._accountId, request, anchor);
|
|
2095
|
+
const reconstructedCommitment = normalizeHexWord(reconstructed.toCommitment().toHex());
|
|
2096
|
+
|
|
2097
|
+
if (reconstructedCommitment !== txSummaryCommitment) {
|
|
2098
|
+
throw new Error(`Invalid proposal: metadata does not match tx_summary for ${proposal.id}`);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
return txSummaryCommitment;
|
|
2102
|
+
} finally {
|
|
2103
|
+
anchor.free();
|
|
1838
2104
|
}
|
|
2105
|
+
}
|
|
1839
2106
|
|
|
1840
|
-
|
|
2107
|
+
/**
|
|
2108
|
+
* Decodes a proposal's chain anchor. Throws when absent: a proposal without
|
|
2109
|
+
* an anchor was created at an unknown reference block, so its signed summary
|
|
2110
|
+
* cannot be reproduced, verified, or executed. The caller owns the returned
|
|
2111
|
+
* anchor and must `free()` it once done.
|
|
2112
|
+
*/
|
|
2113
|
+
private requireProposalAnchor(proposalId: string, metadata: ProposalMetadata): ChainAnchor {
|
|
2114
|
+
if (!metadata.chainAnchor) {
|
|
2115
|
+
throw new Error(
|
|
2116
|
+
`Proposal ${proposalId} has no chain anchor; it was created without ` +
|
|
2117
|
+
'chain-anchored execution and its signed summary cannot be reproduced ' +
|
|
2118
|
+
'at the original reference block',
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
return chainAnchorFromBase64(metadata.chainAnchor);
|
|
1841
2122
|
}
|
|
1842
2123
|
|
|
1843
2124
|
private async buildTransactionRequestFromMetadata(
|
|
@@ -1921,14 +2202,18 @@ export class Multisig {
|
|
|
1921
2202
|
throw new UnsupportedMetadataVersionError(version);
|
|
1922
2203
|
}
|
|
1923
2204
|
case 'p2id': {
|
|
1924
|
-
const account = await this.getStoreAccount();
|
|
1925
2205
|
const { request } = buildP2idTransactionRequest(
|
|
1926
2206
|
this._accountId,
|
|
1927
2207
|
metadata.recipientId,
|
|
1928
2208
|
metadata.faucetId,
|
|
1929
2209
|
BigInt(metadata.amount),
|
|
1930
|
-
|
|
1931
|
-
|
|
2210
|
+
{
|
|
2211
|
+
salt,
|
|
2212
|
+
signatureAdviceMap,
|
|
2213
|
+
noteType: parseP2idNoteType(metadata.noteType),
|
|
2214
|
+
reclaimHeight: metadata.reclaimHeight,
|
|
2215
|
+
timelockHeight: metadata.timelockHeight,
|
|
2216
|
+
}
|
|
1932
2217
|
);
|
|
1933
2218
|
return request;
|
|
1934
2219
|
}
|