@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
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';
|
|
@@ -114,6 +121,29 @@ export interface AccountStateVerificationResult {
|
|
|
114
121
|
onChainCommitment: string;
|
|
115
122
|
}
|
|
116
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
|
+
|
|
117
147
|
/**
|
|
118
148
|
* Represents a multisig account with GUARDIAN integration.
|
|
119
149
|
*/
|
|
@@ -143,6 +173,27 @@ function deserializeTransactionRequest(bytes: Uint8Array): TransactionRequest {
|
|
|
143
173
|
}
|
|
144
174
|
}
|
|
145
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
|
+
|
|
146
197
|
export class Multisig {
|
|
147
198
|
account: Account;
|
|
148
199
|
threshold: number;
|
|
@@ -253,6 +304,32 @@ export class Multisig {
|
|
|
253
304
|
return stored ?? this.account;
|
|
254
305
|
}
|
|
255
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);
|
|
331
|
+
}
|
|
332
|
+
|
|
256
333
|
/**
|
|
257
334
|
* Maps a proposal type to the procedure that determines its threshold.
|
|
258
335
|
*/
|
|
@@ -295,6 +372,43 @@ export class Multisig {
|
|
|
295
372
|
return this.procedureThresholds.get(procedure) ?? this.threshold;
|
|
296
373
|
}
|
|
297
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
|
+
|
|
298
412
|
/**
|
|
299
413
|
* Update the GUARDIAN client used by this Multisig instance.
|
|
300
414
|
*
|
|
@@ -486,12 +600,14 @@ export class Multisig {
|
|
|
486
600
|
|
|
487
601
|
try {
|
|
488
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);
|
|
489
607
|
this.account = account;
|
|
490
608
|
this.threshold = detected.threshold;
|
|
491
609
|
this.signerCommitments = detected.signerCommitments;
|
|
492
|
-
|
|
493
|
-
this.guardianCommitment = detected.guardianCommitment;
|
|
494
|
-
}
|
|
610
|
+
this.guardianCommitment = detected.guardianCommitment;
|
|
495
611
|
this.procedureThresholds = new Map(detected.procedureThresholds);
|
|
496
612
|
} catch (error) {
|
|
497
613
|
console.warn('Failed to refresh multisig config from account state', error);
|
|
@@ -599,17 +715,19 @@ export class Multisig {
|
|
|
599
715
|
* Create an "add signer" proposal.
|
|
600
716
|
*
|
|
601
717
|
* @param newCommitment - Commitment of the new signer (hex)
|
|
602
|
-
* @param
|
|
603
|
-
*
|
|
718
|
+
* @param options - Optional settings: `nonce`, `newThreshold` (defaults to
|
|
719
|
+
* current threshold)
|
|
604
720
|
*/
|
|
605
721
|
async createAddSignerProposal(
|
|
606
722
|
newCommitment: string,
|
|
607
|
-
|
|
608
|
-
|
|
723
|
+
options: CreateSignerProposalOptions = {},
|
|
724
|
+
...legacyArgs: never[]
|
|
609
725
|
): Promise<Proposal> {
|
|
726
|
+
const proposalNonce = resolveProposalNonce('createAddSignerProposal', options, legacyArgs);
|
|
610
727
|
const webClient = await this.getRawClient();
|
|
611
|
-
const targetThreshold = newThreshold ?? this.threshold;
|
|
728
|
+
const targetThreshold = options.newThreshold ?? this.threshold;
|
|
612
729
|
const targetSignerCommitments = [...this.signerCommitments, newCommitment];
|
|
730
|
+
this.warnOnOverrideDilution(targetSignerCommitments.length);
|
|
613
731
|
|
|
614
732
|
const { request, salt } = await buildUpdateSignersTransactionRequest(
|
|
615
733
|
webClient,
|
|
@@ -618,11 +736,13 @@ export class Multisig {
|
|
|
618
736
|
{ signatureScheme: this.signer.scheme },
|
|
619
737
|
);
|
|
620
738
|
|
|
621
|
-
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();
|
|
622
742
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
623
|
-
const proposalNonce = nonce ?? Date.now();
|
|
624
743
|
|
|
625
744
|
const metadata: ProposalMetadata = {
|
|
745
|
+
chainAnchor,
|
|
626
746
|
proposalType: 'add_signer',
|
|
627
747
|
targetThreshold,
|
|
628
748
|
targetSignerCommitments,
|
|
@@ -638,14 +758,15 @@ export class Multisig {
|
|
|
638
758
|
* Create a "remove signer" proposal by executing the update_signers script to summary.
|
|
639
759
|
*
|
|
640
760
|
* @param signerToRemove - Commitment of the signer to remove (hex)
|
|
641
|
-
* @param
|
|
642
|
-
*
|
|
761
|
+
* @param options - Optional settings: `nonce`, `newThreshold` (defaults to
|
|
762
|
+
* min of current threshold and new signer count)
|
|
643
763
|
*/
|
|
644
764
|
async createRemoveSignerProposal(
|
|
645
765
|
signerToRemove: string,
|
|
646
|
-
|
|
647
|
-
|
|
766
|
+
options: CreateSignerProposalOptions = {},
|
|
767
|
+
...legacyArgs: never[]
|
|
648
768
|
): Promise<Proposal> {
|
|
769
|
+
const proposalNonce = resolveProposalNonce('createRemoveSignerProposal', options, legacyArgs);
|
|
649
770
|
const webClient = await this.getRawClient();
|
|
650
771
|
const normalizedRemove = signerToRemove.toLowerCase();
|
|
651
772
|
const targetSignerCommitments = this.signerCommitments.filter(
|
|
@@ -659,7 +780,7 @@ export class Multisig {
|
|
|
659
780
|
throw new Error('Cannot remove the last signer');
|
|
660
781
|
}
|
|
661
782
|
|
|
662
|
-
const targetThreshold = newThreshold ?? Math.min(this.threshold, targetSignerCommitments.length);
|
|
783
|
+
const targetThreshold = options.newThreshold ?? Math.min(this.threshold, targetSignerCommitments.length);
|
|
663
784
|
|
|
664
785
|
if (targetThreshold < 1 || targetThreshold > targetSignerCommitments.length) {
|
|
665
786
|
throw new Error(
|
|
@@ -674,11 +795,13 @@ export class Multisig {
|
|
|
674
795
|
{ signatureScheme: this.signer.scheme },
|
|
675
796
|
);
|
|
676
797
|
|
|
677
|
-
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();
|
|
678
801
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
679
|
-
const proposalNonce = nonce ?? Date.now();
|
|
680
802
|
|
|
681
803
|
const metadata: ProposalMetadata = {
|
|
804
|
+
chainAnchor,
|
|
682
805
|
proposalType: 'remove_signer',
|
|
683
806
|
targetThreshold,
|
|
684
807
|
targetSignerCommitments,
|
|
@@ -694,12 +817,13 @@ export class Multisig {
|
|
|
694
817
|
* Create a "change threshold" proposal.
|
|
695
818
|
*
|
|
696
819
|
* @param newThreshold - The new threshold value
|
|
697
|
-
* @param
|
|
820
|
+
* @param options - Optional settings: `nonce`
|
|
698
821
|
*/
|
|
699
822
|
async createChangeThresholdProposal(
|
|
700
823
|
newThreshold: number,
|
|
701
|
-
|
|
824
|
+
options: CreateProposalOptions = {},
|
|
702
825
|
): Promise<Proposal> {
|
|
826
|
+
const proposalNonce = resolveProposalNonce('createChangeThresholdProposal', options);
|
|
703
827
|
const webClient = await this.getRawClient();
|
|
704
828
|
if (newThreshold < 1 || newThreshold > this.signerCommitments.length) {
|
|
705
829
|
throw new Error(
|
|
@@ -718,11 +842,13 @@ export class Multisig {
|
|
|
718
842
|
{ signatureScheme: this.signer.scheme },
|
|
719
843
|
);
|
|
720
844
|
|
|
721
|
-
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();
|
|
722
848
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
723
|
-
const proposalNonce = nonce ?? Date.now();
|
|
724
849
|
|
|
725
850
|
const metadata: ProposalMetadata = {
|
|
851
|
+
chainAnchor,
|
|
726
852
|
proposalType: 'change_threshold',
|
|
727
853
|
targetThreshold: newThreshold,
|
|
728
854
|
targetSignerCommitments: this.signerCommitments,
|
|
@@ -737,8 +863,9 @@ export class Multisig {
|
|
|
737
863
|
async createUpdateProcedureThresholdProposal(
|
|
738
864
|
targetProcedure: ProcedureName,
|
|
739
865
|
targetThreshold: number,
|
|
740
|
-
|
|
866
|
+
options: CreateProposalOptions = {},
|
|
741
867
|
): Promise<Proposal> {
|
|
868
|
+
const proposalNonce = resolveProposalNonce('createUpdateProcedureThresholdProposal', options);
|
|
742
869
|
const webClient = await this.getRawClient();
|
|
743
870
|
if (targetThreshold < 0 || targetThreshold > this.signerCommitments.length) {
|
|
744
871
|
throw new Error(
|
|
@@ -764,14 +891,16 @@ export class Multisig {
|
|
|
764
891
|
{ signatureScheme: this.signer.scheme },
|
|
765
892
|
);
|
|
766
893
|
|
|
767
|
-
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();
|
|
768
897
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
769
|
-
const proposalNonce = nonce ?? Date.now();
|
|
770
898
|
const action = targetThreshold === 0
|
|
771
899
|
? `Clear threshold override for ${targetProcedure}`
|
|
772
900
|
: `Set ${targetProcedure} threshold override to ${targetThreshold}`;
|
|
773
901
|
|
|
774
902
|
const metadata: ProposalMetadata = {
|
|
903
|
+
chainAnchor,
|
|
775
904
|
proposalType: 'update_procedure_threshold',
|
|
776
905
|
targetProcedure,
|
|
777
906
|
targetThreshold,
|
|
@@ -788,13 +917,14 @@ export class Multisig {
|
|
|
788
917
|
*
|
|
789
918
|
* @param newGuardianEndpoint - The new GUARDIAN server endpoint URL
|
|
790
919
|
* @param newGuardianPubkey - The new GUARDIAN server's public key commitment (hex)
|
|
791
|
-
* @param
|
|
920
|
+
* @param options - Optional settings: `nonce`
|
|
792
921
|
*/
|
|
793
922
|
async createSwitchGuardianProposal(
|
|
794
923
|
newGuardianEndpoint: string,
|
|
795
924
|
newGuardianPubkey: string,
|
|
796
|
-
|
|
925
|
+
options: CreateProposalOptions = {},
|
|
797
926
|
): Promise<Proposal> {
|
|
927
|
+
const proposalNonce = resolveProposalNonce('createSwitchGuardianProposal', options);
|
|
798
928
|
const webClient = await this.getRawClient();
|
|
799
929
|
await this.verifyGuardianEndpointCommitment(newGuardianEndpoint, newGuardianPubkey);
|
|
800
930
|
|
|
@@ -804,11 +934,13 @@ export class Multisig {
|
|
|
804
934
|
{ signatureScheme: this.signer.scheme },
|
|
805
935
|
);
|
|
806
936
|
|
|
807
|
-
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();
|
|
808
940
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
809
|
-
const proposalNonce = nonce ?? Date.now();
|
|
810
941
|
|
|
811
942
|
const metadata: ProposalMetadata = {
|
|
943
|
+
chainAnchor,
|
|
812
944
|
proposalType: 'switch_guardian',
|
|
813
945
|
saltHex: salt.toHex(),
|
|
814
946
|
requiredSignatures: this.getEffectiveThreshold('switch_guardian'),
|
|
@@ -826,12 +958,13 @@ export class Multisig {
|
|
|
826
958
|
* Create a "consume notes" proposal to consume notes sent to the multisig account.
|
|
827
959
|
*
|
|
828
960
|
* @param noteIds - IDs of the notes to consume (hex strings)
|
|
829
|
-
* @param
|
|
961
|
+
* @param options - Optional settings: `nonce`
|
|
830
962
|
*/
|
|
831
963
|
async createConsumeNotesProposal(
|
|
832
964
|
noteIds: string[],
|
|
833
|
-
|
|
965
|
+
options: CreateProposalOptions = {},
|
|
834
966
|
): Promise<Proposal> {
|
|
967
|
+
const proposalNonce = resolveProposalNonce('createConsumeNotesProposal', options);
|
|
835
968
|
const webClient = await this.getRawClient();
|
|
836
969
|
if (noteIds.length === 0) {
|
|
837
970
|
throw new Error('At least one note ID is required');
|
|
@@ -851,11 +984,13 @@ export class Multisig {
|
|
|
851
984
|
|
|
852
985
|
const { request, salt } = buildConsumeNotesTransactionRequestFromNotes(fetchedNotes);
|
|
853
986
|
|
|
854
|
-
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();
|
|
855
990
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
856
|
-
const proposalNonce = nonce ?? Date.now();
|
|
857
991
|
|
|
858
992
|
const metadata: ProposalMetadata = {
|
|
993
|
+
chainAnchor,
|
|
859
994
|
proposalType: 'consume_notes',
|
|
860
995
|
noteIds,
|
|
861
996
|
metadataVersion: CONSUME_NOTES_METADATA_VERSION_V2,
|
|
@@ -884,38 +1019,41 @@ export class Multisig {
|
|
|
884
1019
|
* @param recipientId - Account ID of the recipient (hex string)
|
|
885
1020
|
* @param faucetId - Faucet/token account ID (hex string)
|
|
886
1021
|
* @param amount - Amount to send
|
|
887
|
-
* @param
|
|
888
|
-
*
|
|
889
|
-
*
|
|
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)
|
|
890
1025
|
*/
|
|
891
1026
|
async createP2idProposal(
|
|
892
1027
|
recipientId: string,
|
|
893
1028
|
faucetId: string,
|
|
894
1029
|
amount: bigint,
|
|
895
|
-
|
|
896
|
-
|
|
1030
|
+
options: CreateP2idProposalOptions = {},
|
|
1031
|
+
...legacyArgs: never[]
|
|
897
1032
|
): Promise<Proposal> {
|
|
1033
|
+
const proposalNonce = resolveProposalNonce('createP2idProposal', options, legacyArgs);
|
|
898
1034
|
const webClient = await this.getRawClient();
|
|
899
1035
|
if (amount <= 0n) {
|
|
900
1036
|
throw new Error('Amount must be greater than 0');
|
|
901
1037
|
}
|
|
902
1038
|
|
|
903
|
-
|
|
904
|
-
|
|
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;
|
|
905
1042
|
const { request, salt } = buildP2idTransactionRequest(
|
|
906
1043
|
this._accountId,
|
|
907
1044
|
recipientId,
|
|
908
1045
|
faucetId,
|
|
909
1046
|
amount,
|
|
910
|
-
|
|
911
|
-
{ noteType: options.noteType },
|
|
1047
|
+
noteOptions,
|
|
912
1048
|
);
|
|
913
1049
|
|
|
914
|
-
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();
|
|
915
1053
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
916
|
-
const proposalNonce = nonce ?? Date.now();
|
|
917
1054
|
|
|
918
1055
|
const metadata: ProposalMetadata = {
|
|
1056
|
+
chainAnchor,
|
|
919
1057
|
proposalType: 'p2id',
|
|
920
1058
|
saltHex: salt.toHex(),
|
|
921
1059
|
requiredSignatures: this.getEffectiveThreshold('p2id'),
|
|
@@ -924,6 +1062,9 @@ export class Multisig {
|
|
|
924
1062
|
amount: amount.toString(),
|
|
925
1063
|
// Omitted for public notes so the wire shape matches pre-#322 proposals.
|
|
926
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,
|
|
927
1068
|
description: `Send ${amount} of asset ${faucetId.slice(0, 10)}... to ${recipientId.slice(0, 10)}...`,
|
|
928
1069
|
};
|
|
929
1070
|
|
|
@@ -983,7 +1124,7 @@ export class Multisig {
|
|
|
983
1124
|
|
|
984
1125
|
/**
|
|
985
1126
|
* Export a note created by this multisig account as serialized note-file
|
|
986
|
-
* bytes for out-of-band delivery
|
|
1127
|
+
* bytes for out-of-band delivery.
|
|
987
1128
|
*
|
|
988
1129
|
* A private note publishes only its commitment on chain, so the recipient
|
|
989
1130
|
* can never learn its contents via sync; the sender must hand them the
|
|
@@ -1026,7 +1167,7 @@ export class Multisig {
|
|
|
1026
1167
|
|
|
1027
1168
|
/**
|
|
1028
1169
|
* Export a note created by this multisig account as a note file downloaded
|
|
1029
|
-
* by the browser
|
|
1170
|
+
* by the browser. Browser-only convenience over
|
|
1030
1171
|
* {@link exportNoteToBytes}; use that method directly in non-DOM
|
|
1031
1172
|
* environments.
|
|
1032
1173
|
*
|
|
@@ -1054,7 +1195,7 @@ export class Multisig {
|
|
|
1054
1195
|
}
|
|
1055
1196
|
|
|
1056
1197
|
/**
|
|
1057
|
-
* Import a note file received out-of-band
|
|
1198
|
+
* Import a note file received out-of-band so the note can be
|
|
1058
1199
|
* consumed by this multisig account.
|
|
1059
1200
|
*
|
|
1060
1201
|
* Sync the Miden client with the network afterwards so the note's on-chain
|
|
@@ -1081,7 +1222,7 @@ export class Multisig {
|
|
|
1081
1222
|
}
|
|
1082
1223
|
|
|
1083
1224
|
/**
|
|
1084
|
-
* Import a note file received out-of-band
|
|
1225
|
+
* Import a note file received out-of-band from a browser
|
|
1085
1226
|
* `File`/`Blob` (e.g. a file-input selection). See
|
|
1086
1227
|
* {@link importNoteFromBytes} for the returned identifier semantics.
|
|
1087
1228
|
*/
|
|
@@ -1096,10 +1237,9 @@ export class Multisig {
|
|
|
1096
1237
|
* The P2ID note is rebuilt deterministically from the proposal salt, so the
|
|
1097
1238
|
* ID is known ahead of execution. For a private P2ID this is the ID to pass
|
|
1098
1239
|
* to {@link exportNoteToBytes} after executing, so the note file can be delivered
|
|
1099
|
-
* to the recipient out-of-band
|
|
1240
|
+
* to the recipient out-of-band.
|
|
1100
1241
|
*
|
|
1101
|
-
*
|
|
1102
|
-
* current vault state, which execution itself changes.
|
|
1242
|
+
* The note ID remains deterministic from the proposal metadata and salt.
|
|
1103
1243
|
*/
|
|
1104
1244
|
async getP2idNoteId(proposal: Proposal): Promise<string> {
|
|
1105
1245
|
const metadata = proposal.metadata;
|
|
@@ -1113,15 +1253,14 @@ export class Multisig {
|
|
|
1113
1253
|
throw new Error('getP2idNoteId requires a P2ID proposal with recipient, faucet, amount, and salt metadata');
|
|
1114
1254
|
}
|
|
1115
1255
|
|
|
1116
|
-
const account = await this.getStoreAccount();
|
|
1117
1256
|
const note = buildP2idNoteFromMetadata(
|
|
1118
1257
|
this._accountId,
|
|
1119
1258
|
metadata.recipientId,
|
|
1120
1259
|
metadata.faucetId,
|
|
1121
1260
|
BigInt(metadata.amount),
|
|
1122
|
-
account,
|
|
1123
1261
|
parseP2idNoteType(metadata.noteType),
|
|
1124
1262
|
metadata.saltHex,
|
|
1263
|
+
{ reclaimHeight: metadata.reclaimHeight, timelockHeight: metadata.timelockHeight },
|
|
1125
1264
|
);
|
|
1126
1265
|
return note.id().toString();
|
|
1127
1266
|
}
|
|
@@ -1166,6 +1305,19 @@ export class Multisig {
|
|
|
1166
1305
|
return this.guardian.abandonStatus(this._accountId, nonce);
|
|
1167
1306
|
}
|
|
1168
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
|
+
|
|
1169
1321
|
async signProposal(proposalId: string): Promise<Proposal> {
|
|
1170
1322
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1171
1323
|
const existingProposal = await this.getProposalForSigning(proposalId, normalizedProposalId);
|
|
@@ -1227,8 +1379,16 @@ export class Multisig {
|
|
|
1227
1379
|
async executeProposal(proposalId: string): Promise<void> {
|
|
1228
1380
|
const { metadata, finalRequest, proposal } = await this.prepareProposalExecution(proposalId);
|
|
1229
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.
|
|
1230
1385
|
const accountId = AccountId.fromHex(this._accountId);
|
|
1231
|
-
|
|
1386
|
+
const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
|
|
1387
|
+
try {
|
|
1388
|
+
await this.proverWorkflow.submitAt(accountId, finalRequest, anchor);
|
|
1389
|
+
} finally {
|
|
1390
|
+
anchor.free();
|
|
1391
|
+
}
|
|
1232
1392
|
|
|
1233
1393
|
if (metadata.proposalType === 'switch_guardian') {
|
|
1234
1394
|
if (!metadata.newGuardianEndpoint || !metadata.newGuardianPubkey) {
|
|
@@ -1295,14 +1455,42 @@ export class Multisig {
|
|
|
1295
1455
|
* Submit an integration-built transaction (advice already injected). Mirrors
|
|
1296
1456
|
* the Rust `submit_transaction`; used by the custom proposal producer flow
|
|
1297
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.
|
|
1298
1460
|
*/
|
|
1299
|
-
async submitTransaction(request: TransactionRequest): Promise<void> {
|
|
1300
|
-
|
|
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
|
+
}
|
|
1301
1489
|
}
|
|
1302
1490
|
|
|
1303
1491
|
/**
|
|
1304
|
-
* Create a proposal from a producer-built transaction the SDK does not model
|
|
1305
|
-
*
|
|
1492
|
+
* Create a proposal from a producer-built transaction the SDK does not model.
|
|
1493
|
+
* `transactionRequestBytes` is a serialized TransactionRequest;
|
|
1306
1494
|
* `proposalType` is a free-form, non-empty label that must not collide with a
|
|
1307
1495
|
* built-in type. The integration keeps its own recipe to execute later via
|
|
1308
1496
|
* `prepareCustomExecution`.
|
|
@@ -1310,8 +1498,9 @@ export class Multisig {
|
|
|
1310
1498
|
async createCustomProposal(
|
|
1311
1499
|
transactionRequestBytes: Uint8Array,
|
|
1312
1500
|
proposalType: string,
|
|
1313
|
-
|
|
1501
|
+
options: CreateProposalOptions = {},
|
|
1314
1502
|
): Promise<Proposal> {
|
|
1503
|
+
const proposalNonce = resolveProposalNonce('createCustomProposal', options);
|
|
1315
1504
|
const label = proposalType.trim().toLowerCase();
|
|
1316
1505
|
if (label.length === 0) {
|
|
1317
1506
|
throw new Error('proposalType must not be empty');
|
|
@@ -1329,11 +1518,13 @@ export class Multisig {
|
|
|
1329
1518
|
|
|
1330
1519
|
const webClient = await this.getRawClient();
|
|
1331
1520
|
const request = deserializeTransactionRequest(transactionRequestBytes);
|
|
1332
|
-
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();
|
|
1333
1524
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
1334
|
-
const proposalNonce = nonce ?? Date.now();
|
|
1335
1525
|
|
|
1336
1526
|
const metadata: ProposalMetadata = {
|
|
1527
|
+
chainAnchor,
|
|
1337
1528
|
proposalType: 'custom',
|
|
1338
1529
|
description: '',
|
|
1339
1530
|
rawProposalType: label,
|
|
@@ -1346,7 +1537,7 @@ export class Multisig {
|
|
|
1346
1537
|
/**
|
|
1347
1538
|
* Assemble the validated execution advice (cosigner signatures + GUARDIAN
|
|
1348
1539
|
* acknowledgment) for a ready custom proposal, so an integration can rebuild
|
|
1349
|
-
* its transaction with its own recipe and submit
|
|
1540
|
+
* its transaction with its own recipe and submit.
|
|
1350
1541
|
*
|
|
1351
1542
|
* `transactionRequestBytes` is the serialized transaction request; it is used only to verify
|
|
1352
1543
|
* (binding check) that it reproduces the signed commitment, before the
|
|
@@ -1392,9 +1583,27 @@ export class Multisig {
|
|
|
1392
1583
|
|
|
1393
1584
|
const bindingRequest = deserializeTransactionRequest(transactionRequestBytes);
|
|
1394
1585
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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
|
+
}
|
|
1398
1607
|
if (derivedCommitmentHex !== signedCommitmentHex) {
|
|
1399
1608
|
throw new Error(
|
|
1400
1609
|
`Custom proposal binding mismatch: expected ${signedCommitmentHex}, got ${derivedCommitmentHex}`,
|
|
@@ -1450,12 +1659,17 @@ export class Multisig {
|
|
|
1450
1659
|
cosignerSig.signature.scheme,
|
|
1451
1660
|
);
|
|
1452
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
|
+
}
|
|
1453
1669
|
const { key, values } = buildSignatureAdviceEntry(
|
|
1454
1670
|
signerCommitment,
|
|
1455
1671
|
createTxCommitmentWord(),
|
|
1456
1672
|
signature,
|
|
1457
|
-
ecdsaPublicKey,
|
|
1458
|
-
cosignerSig.signature.scheme === 'ecdsa' ? cosignerSig.signature.signature : undefined,
|
|
1459
1673
|
);
|
|
1460
1674
|
const keyHex = normalizeHexWord(key.toHex());
|
|
1461
1675
|
if (adviceMapKeys.has(keyHex)) {
|
|
@@ -1486,12 +1700,13 @@ export class Multisig {
|
|
|
1486
1700
|
}
|
|
1487
1701
|
const ackSigBytes = signatureHexToBytes(ackSigHex, ackScheme);
|
|
1488
1702
|
const ackSignature = Signature.deserialize(ackSigBytes);
|
|
1703
|
+
if (ackScheme === 'ecdsa' && ackPubkey) {
|
|
1704
|
+
assertEcdsaSignatureRecoverable(ackSigHex, normalizedTxCommitmentHex, ackPubkey);
|
|
1705
|
+
}
|
|
1489
1706
|
const { key: ackKey, values: ackValues } = buildSignatureAdviceEntry(
|
|
1490
1707
|
guardianCommitment,
|
|
1491
1708
|
createTxCommitmentWord(),
|
|
1492
1709
|
ackSignature,
|
|
1493
|
-
ackScheme === 'ecdsa' ? ackPubkey : undefined,
|
|
1494
|
-
ackScheme === 'ecdsa' ? ackSigHex : undefined,
|
|
1495
1710
|
);
|
|
1496
1711
|
const ackKeyHex = normalizeHexWord(ackKey.toHex());
|
|
1497
1712
|
if (adviceMapKeys.has(ackKeyHex)) {
|
|
@@ -1558,7 +1773,7 @@ export class Multisig {
|
|
|
1558
1773
|
|
|
1559
1774
|
const txSummaryBytes = base64ToUint8Array(txSummaryBase64);
|
|
1560
1775
|
const txSummary = TransactionSummary.deserialize(txSummaryBytes);
|
|
1561
|
-
const saltHex = txSummary
|
|
1776
|
+
const saltHex = summarySalt(txSummary).toHex();
|
|
1562
1777
|
const txCommitmentHex = txSummary.toCommitment().toHex();
|
|
1563
1778
|
const normalizedTxCommitmentHex = normalizeHexWord(txCommitmentHex);
|
|
1564
1779
|
const normalizedSignerCommitments = new Set(
|
|
@@ -1599,14 +1814,17 @@ export class Multisig {
|
|
|
1599
1814
|
cosignerSig.signature.scheme,
|
|
1600
1815
|
);
|
|
1601
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
|
+
}
|
|
1602
1824
|
const { key, values } = buildSignatureAdviceEntry(
|
|
1603
1825
|
signerCommitment,
|
|
1604
1826
|
createTxCommitmentWord(),
|
|
1605
1827
|
signature,
|
|
1606
|
-
ecdsaPublicKey,
|
|
1607
|
-
cosignerSig.signature.scheme === 'ecdsa'
|
|
1608
|
-
? cosignerSig.signature.signature
|
|
1609
|
-
: undefined,
|
|
1610
1828
|
);
|
|
1611
1829
|
const keyHex = normalizeHexWord(key.toHex());
|
|
1612
1830
|
if (adviceMapKeys.has(keyHex)) {
|
|
@@ -1642,12 +1860,13 @@ export class Multisig {
|
|
|
1642
1860
|
}
|
|
1643
1861
|
const ackSigBytes = signatureHexToBytes(ackSigHex, ackScheme);
|
|
1644
1862
|
const ackSignature = Signature.deserialize(ackSigBytes);
|
|
1863
|
+
if (ackScheme === 'ecdsa' && ackPubkey) {
|
|
1864
|
+
assertEcdsaSignatureRecoverable(ackSigHex, normalizedTxCommitmentHex, ackPubkey);
|
|
1865
|
+
}
|
|
1645
1866
|
const { key: ackKey, values: ackValues } = buildSignatureAdviceEntry(
|
|
1646
1867
|
guardianCommitment,
|
|
1647
1868
|
createTxCommitmentWord(),
|
|
1648
1869
|
ackSignature,
|
|
1649
|
-
ackScheme === 'ecdsa' ? ackPubkey : undefined,
|
|
1650
|
-
ackScheme === 'ecdsa' ? ackSigHex : undefined,
|
|
1651
1870
|
);
|
|
1652
1871
|
const ackKeyHex = normalizeHexWord(ackKey.toHex());
|
|
1653
1872
|
if (adviceMapKeys.has(ackKeyHex)) {
|
|
@@ -1836,39 +2055,70 @@ export class Multisig {
|
|
|
1836
2055
|
|
|
1837
2056
|
private async verifyProposalMetadataBinding(proposal: Proposal): Promise<string> {
|
|
1838
2057
|
const txSummaryCommitment = this.ensureProposalCommitmentMatchesSummary(proposal);
|
|
1839
|
-
if (proposal.metadata.proposalType === 'custom') {
|
|
1840
|
-
// Custom proposals (issue #266) have no per-type reconstruction recipe;
|
|
1841
|
-
// the id ↔ tx_summary commitment match above is the only available
|
|
1842
|
-
// integrity guarantee for an opaque proposal.
|
|
1843
|
-
return txSummaryCommitment;
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
if (proposal.metadata.proposalType === 'switch_guardian') {
|
|
1847
|
-
// Exempt from binding re-execution (mirrors the `custom` exemption above).
|
|
1848
|
-
// The WASM `executeForSummary` leaves the guardian-disabling side effect
|
|
1849
|
-
// applied to the in-session account, so re-execution reconstructs a smaller
|
|
1850
|
-
// delta and falsely rejects with "metadata does not match tx_summary". The
|
|
1851
|
-
// native Rust client does not mutate, so this is an intentional divergence.
|
|
1852
|
-
// The id ↔ tx_summary match above plus `verifyGuardianEndpointCommitment`
|
|
1853
|
-
// at propose/execute time still bind the proposal.
|
|
1854
|
-
return txSummaryCommitment;
|
|
1855
|
-
}
|
|
1856
2058
|
|
|
1857
2059
|
const summary = TransactionSummary.deserialize(base64ToUint8Array(proposal.txSummary));
|
|
1858
|
-
const salt = proposal.metadata.saltHex
|
|
1859
|
-
? Word.fromHex(normalizeHexWord(proposal.metadata.saltHex))
|
|
1860
|
-
: summary.salt();
|
|
1861
2060
|
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
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);
|
|
2091
|
+
|
|
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());
|
|
1866
2096
|
|
|
1867
|
-
|
|
1868
|
-
|
|
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();
|
|
1869
2104
|
}
|
|
2105
|
+
}
|
|
1870
2106
|
|
|
1871
|
-
|
|
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);
|
|
1872
2122
|
}
|
|
1873
2123
|
|
|
1874
2124
|
private async buildTransactionRequestFromMetadata(
|
|
@@ -1952,14 +2202,18 @@ export class Multisig {
|
|
|
1952
2202
|
throw new UnsupportedMetadataVersionError(version);
|
|
1953
2203
|
}
|
|
1954
2204
|
case 'p2id': {
|
|
1955
|
-
const account = await this.getStoreAccount();
|
|
1956
2205
|
const { request } = buildP2idTransactionRequest(
|
|
1957
2206
|
this._accountId,
|
|
1958
2207
|
metadata.recipientId,
|
|
1959
2208
|
metadata.faucetId,
|
|
1960
2209
|
BigInt(metadata.amount),
|
|
1961
|
-
|
|
1962
|
-
|
|
2210
|
+
{
|
|
2211
|
+
salt,
|
|
2212
|
+
signatureAdviceMap,
|
|
2213
|
+
noteType: parseP2idNoteType(metadata.noteType),
|
|
2214
|
+
reclaimHeight: metadata.reclaimHeight,
|
|
2215
|
+
timelockHeight: metadata.timelockHeight,
|
|
2216
|
+
}
|
|
1963
2217
|
);
|
|
1964
2218
|
return request;
|
|
1965
2219
|
}
|