@openzeppelin/miden-multisig-client 0.16.2 → 0.17.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/dist/multisig.js
CHANGED
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { GuardianHttpClient } from '@openzeppelin/guardian-client';
|
|
8
8
|
import { Account, AccountId, AdviceMap, Endpoint, FeltArray, Note, NoteExportFormat, NoteFile, RpcClient, Signature, TransactionRequest, TransactionSummary, Word, } from '@miden-sdk/miden-sdk';
|
|
9
|
-
import { executeForSummary, buildUpdateSignersTransactionRequest, buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildConsumeNotesTransactionRequest, buildP2idNoteFromMetadata, buildP2idTransactionRequest, parseP2idNoteType, p2idNoteTypeToMetadata, } from './transaction.js';
|
|
9
|
+
import { chainAnchorFromBase64, chainAnchorToBase64, executeForSummary, executeForSummaryAt, summarySalt, buildUpdateSignersTransactionRequest, buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildConsumeNotesTransactionRequest, buildP2idNoteFromMetadata, buildP2idTransactionRequest, parseP2idNoteType, p2idNoteTypeToMetadata, } from './transaction.js';
|
|
10
10
|
import { buildConsumeNotesTransactionRequestFromNotes } from './transaction/consumeNotes.js';
|
|
11
11
|
import { CONSUME_NOTES_METADATA_VERSION_V2, MAX_CONSUME_NOTES_METADATA_BYTES, } from './types/proposal.js';
|
|
12
12
|
import { LEGACY_CONSUME_NOTES_ENABLED } from './multisig/config.js';
|
|
13
13
|
import { ConsumeNotesMetadataOversizeError, LegacyConsumeNotesNoteMissingError, NoteBindingMismatchError, UnsupportedMetadataVersionError, } from './multisig/consumeNotesErrors.js';
|
|
14
14
|
import { noteFromBase64, noteToBase64 } from './utils/encoding.js';
|
|
15
15
|
import { base64ToUint8Array, uint8ArrayToBase64, normalizeHexWord, } from './utils/encoding.js';
|
|
16
|
-
import { buildSignatureAdviceEntry, normalizeSignerCommitment, signatureHexToBytes, tryComputeEcdsaCommitmentHex, } from './utils/signature.js';
|
|
16
|
+
import { assertEcdsaSignatureRecoverable, buildSignatureAdviceEntry, normalizeSignerCommitment, signatureHexToBytes, tryComputeEcdsaCommitmentHex, } from './utils/signature.js';
|
|
17
17
|
import { computeCommitmentFromTxSummary, accountIdToHex } from './multisig/helpers.js';
|
|
18
18
|
import { buildGuardianSignatureFromSigner } from './multisig/signing.js';
|
|
19
|
-
import { AccountInspector } from './inspector.js';
|
|
19
|
+
import { AccountInspector, assertCompleteDetectedConfig } from './inspector.js';
|
|
20
20
|
import { ProposalFactory } from './proposal/factory.js';
|
|
21
21
|
import { ProposalMetadataCodec } from './proposal/metadata.js';
|
|
22
22
|
import { ProposalSignatures } from './proposal/signatures.js';
|
|
@@ -53,6 +53,20 @@ function deserializeTransactionRequest(bytes) {
|
|
|
53
53
|
throw new Error(`failed to decode transaction request: ${detail}`);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Single home for the proposal-nonce default, plus a runtime guard for
|
|
58
|
+
* pre-#387 positional callers. Untyped JS passing the old `nonce` number (or
|
|
59
|
+
* a legacy trailing argument) would otherwise bind it as the options bag and
|
|
60
|
+
* silently fall back to every default — a public note instead of a private
|
|
61
|
+
* one, or the current threshold instead of the requested one — so it must
|
|
62
|
+
* fail loudly instead.
|
|
63
|
+
*/
|
|
64
|
+
function resolveProposalNonce(method, options, legacyArgs = []) {
|
|
65
|
+
if (typeof options !== 'object' || options === null || legacyArgs.length > 0) {
|
|
66
|
+
throw new Error(`${method}: positional optional parameters were replaced by a trailing options object (issue #387); pass { nonce, ... } instead`);
|
|
67
|
+
}
|
|
68
|
+
return options.nonce ?? Date.now();
|
|
69
|
+
}
|
|
56
70
|
export class Multisig {
|
|
57
71
|
account;
|
|
58
72
|
threshold;
|
|
@@ -131,6 +145,30 @@ export class Multisig {
|
|
|
131
145
|
const stored = await retryRpcRead(() => webClient.getAccount(AccountId.fromHex(this._accountId)), this.rpcConfig);
|
|
132
146
|
return stored ?? this.account;
|
|
133
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Read the current ordered signer public-key commitments from account
|
|
150
|
+
* storage (store-backed state, falling back to the snapshot).
|
|
151
|
+
*
|
|
152
|
+
* Commitments are ordered by signer index as currently stored; indices
|
|
153
|
+
* re-pack when signers are removed, so index 0 is the creation-time first
|
|
154
|
+
* key only until the first membership change. Unlike the
|
|
155
|
+
* `signerCommitments` field, which reflects the config detected at
|
|
156
|
+
* construction / last sync, this reads the account state directly.
|
|
157
|
+
* See `AccountInspector.getSignerPublicKeyCommitments` (issue #306).
|
|
158
|
+
*/
|
|
159
|
+
async getSignerPublicKeyCommitments() {
|
|
160
|
+
const account = await this.getStoreAccount();
|
|
161
|
+
return AccountInspector.getSignerPublicKeyCommitments(account);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read the current guardian public-key commitment from account storage.
|
|
165
|
+
* The guarded-multisig always includes a guardian, so this throws (rather
|
|
166
|
+
* than returning null) when the entry is missing.
|
|
167
|
+
*/
|
|
168
|
+
async getGuardianPublicKeyCommitment() {
|
|
169
|
+
const account = await this.getStoreAccount();
|
|
170
|
+
return AccountInspector.getGuardianPublicKeyCommitment(account);
|
|
171
|
+
}
|
|
134
172
|
/**
|
|
135
173
|
* Maps a proposal type to the procedure that determines its threshold.
|
|
136
174
|
*/
|
|
@@ -169,6 +207,37 @@ export class Multisig {
|
|
|
169
207
|
}
|
|
170
208
|
return this.procedureThresholds.get(procedure) ?? this.threshold;
|
|
171
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Per-procedure threshold overrides whose effective signing ratio is diluted
|
|
212
|
+
* by growing the signer set to `newNumSigners`.
|
|
213
|
+
*
|
|
214
|
+
* Overrides are absolute signature counts, not ratios, and the on-chain
|
|
215
|
+
* `update_signers_and_threshold` procedure does not re-scale them: growing
|
|
216
|
+
* the approver set silently lowers every override's effective signing ratio
|
|
217
|
+
* (a 2-of-2 override becomes 2-of-n). Callers creating a proposal that grows
|
|
218
|
+
* the signer set should surface these overrides and suggest raising them via
|
|
219
|
+
* an update-procedure-threshold proposal alongside the growth.
|
|
220
|
+
*
|
|
221
|
+
* @param newNumSigners - Signer-set size the proposal produces
|
|
222
|
+
* @returns The configured overrides, or an empty list when the set does not grow
|
|
223
|
+
*/
|
|
224
|
+
overridesDilutedBySignerGrowth(newNumSigners) {
|
|
225
|
+
if (newNumSigners <= this.signerCommitments.length) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
return Array.from(this.procedureThresholds.entries()).map(([procedure, threshold]) => ({
|
|
229
|
+
procedure,
|
|
230
|
+
threshold,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
warnOnOverrideDilution(newNumSigners) {
|
|
234
|
+
const current = this.signerCommitments.length;
|
|
235
|
+
for (const { procedure, threshold } of this.overridesDilutedBySignerGrowth(newNumSigners)) {
|
|
236
|
+
console.warn(`growing the signer set dilutes the ${procedure} threshold override ` +
|
|
237
|
+
`(${threshold}-of-${current} becomes ${threshold}-of-${newNumSigners}); consider raising it ` +
|
|
238
|
+
`via an update-procedure-threshold proposal alongside the signer update`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
172
241
|
/**
|
|
173
242
|
* Update the GUARDIAN client used by this Multisig instance.
|
|
174
243
|
*
|
|
@@ -316,12 +385,14 @@ export class Multisig {
|
|
|
316
385
|
}
|
|
317
386
|
try {
|
|
318
387
|
const detected = AccountInspector.fromAccount(account);
|
|
388
|
+
// Fail closed on a partial read: adopting a truncated signer set would
|
|
389
|
+
// let membership proposals rewrite the account without the omitted
|
|
390
|
+
// keys. The catch below keeps the previously validated config instead.
|
|
391
|
+
assertCompleteDetectedConfig(detected);
|
|
319
392
|
this.account = account;
|
|
320
393
|
this.threshold = detected.threshold;
|
|
321
394
|
this.signerCommitments = detected.signerCommitments;
|
|
322
|
-
|
|
323
|
-
this.guardianCommitment = detected.guardianCommitment;
|
|
324
|
-
}
|
|
395
|
+
this.guardianCommitment = detected.guardianCommitment;
|
|
325
396
|
this.procedureThresholds = new Map(detected.procedureThresholds);
|
|
326
397
|
}
|
|
327
398
|
catch (error) {
|
|
@@ -407,18 +478,22 @@ export class Multisig {
|
|
|
407
478
|
* Create an "add signer" proposal.
|
|
408
479
|
*
|
|
409
480
|
* @param newCommitment - Commitment of the new signer (hex)
|
|
410
|
-
* @param
|
|
411
|
-
*
|
|
481
|
+
* @param options - Optional settings: `nonce`, `newThreshold` (defaults to
|
|
482
|
+
* current threshold)
|
|
412
483
|
*/
|
|
413
|
-
async createAddSignerProposal(newCommitment,
|
|
484
|
+
async createAddSignerProposal(newCommitment, options = {}, ...legacyArgs) {
|
|
485
|
+
const proposalNonce = resolveProposalNonce('createAddSignerProposal', options, legacyArgs);
|
|
414
486
|
const webClient = await this.getRawClient();
|
|
415
|
-
const targetThreshold = newThreshold ?? this.threshold;
|
|
487
|
+
const targetThreshold = options.newThreshold ?? this.threshold;
|
|
416
488
|
const targetSignerCommitments = [...this.signerCommitments, newCommitment];
|
|
489
|
+
this.warnOnOverrideDilution(targetSignerCommitments.length);
|
|
417
490
|
const { request, salt } = await buildUpdateSignersTransactionRequest(webClient, targetThreshold, targetSignerCommitments, { signatureScheme: this.signer.scheme });
|
|
418
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
491
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
492
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
493
|
+
anchor.free();
|
|
419
494
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
420
|
-
const proposalNonce = nonce ?? Date.now();
|
|
421
495
|
const metadata = {
|
|
496
|
+
chainAnchor,
|
|
422
497
|
proposalType: 'add_signer',
|
|
423
498
|
targetThreshold,
|
|
424
499
|
targetSignerCommitments,
|
|
@@ -432,10 +507,11 @@ export class Multisig {
|
|
|
432
507
|
* Create a "remove signer" proposal by executing the update_signers script to summary.
|
|
433
508
|
*
|
|
434
509
|
* @param signerToRemove - Commitment of the signer to remove (hex)
|
|
435
|
-
* @param
|
|
436
|
-
*
|
|
510
|
+
* @param options - Optional settings: `nonce`, `newThreshold` (defaults to
|
|
511
|
+
* min of current threshold and new signer count)
|
|
437
512
|
*/
|
|
438
|
-
async createRemoveSignerProposal(signerToRemove,
|
|
513
|
+
async createRemoveSignerProposal(signerToRemove, options = {}, ...legacyArgs) {
|
|
514
|
+
const proposalNonce = resolveProposalNonce('createRemoveSignerProposal', options, legacyArgs);
|
|
439
515
|
const webClient = await this.getRawClient();
|
|
440
516
|
const normalizedRemove = signerToRemove.toLowerCase();
|
|
441
517
|
const targetSignerCommitments = this.signerCommitments.filter((c) => c.toLowerCase() !== normalizedRemove);
|
|
@@ -445,15 +521,17 @@ export class Multisig {
|
|
|
445
521
|
if (targetSignerCommitments.length === 0) {
|
|
446
522
|
throw new Error('Cannot remove the last signer');
|
|
447
523
|
}
|
|
448
|
-
const targetThreshold = newThreshold ?? Math.min(this.threshold, targetSignerCommitments.length);
|
|
524
|
+
const targetThreshold = options.newThreshold ?? Math.min(this.threshold, targetSignerCommitments.length);
|
|
449
525
|
if (targetThreshold < 1 || targetThreshold > targetSignerCommitments.length) {
|
|
450
526
|
throw new Error(`Invalid threshold ${targetThreshold}. Must be between 1 and ${targetSignerCommitments.length}`);
|
|
451
527
|
}
|
|
452
528
|
const { request, salt } = await buildUpdateSignersTransactionRequest(webClient, targetThreshold, targetSignerCommitments, { signatureScheme: this.signer.scheme });
|
|
453
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
529
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
530
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
531
|
+
anchor.free();
|
|
454
532
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
455
|
-
const proposalNonce = nonce ?? Date.now();
|
|
456
533
|
const metadata = {
|
|
534
|
+
chainAnchor,
|
|
457
535
|
proposalType: 'remove_signer',
|
|
458
536
|
targetThreshold,
|
|
459
537
|
targetSignerCommitments,
|
|
@@ -467,9 +545,10 @@ export class Multisig {
|
|
|
467
545
|
* Create a "change threshold" proposal.
|
|
468
546
|
*
|
|
469
547
|
* @param newThreshold - The new threshold value
|
|
470
|
-
* @param
|
|
548
|
+
* @param options - Optional settings: `nonce`
|
|
471
549
|
*/
|
|
472
|
-
async createChangeThresholdProposal(newThreshold,
|
|
550
|
+
async createChangeThresholdProposal(newThreshold, options = {}) {
|
|
551
|
+
const proposalNonce = resolveProposalNonce('createChangeThresholdProposal', options);
|
|
473
552
|
const webClient = await this.getRawClient();
|
|
474
553
|
if (newThreshold < 1 || newThreshold > this.signerCommitments.length) {
|
|
475
554
|
throw new Error(`Invalid threshold ${newThreshold}. Must be between 1 and ${this.signerCommitments.length}`);
|
|
@@ -478,10 +557,12 @@ export class Multisig {
|
|
|
478
557
|
throw new Error('New threshold is the same as current threshold');
|
|
479
558
|
}
|
|
480
559
|
const { request, salt } = await buildUpdateSignersTransactionRequest(webClient, newThreshold, this.signerCommitments, { signatureScheme: this.signer.scheme });
|
|
481
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
560
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
561
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
562
|
+
anchor.free();
|
|
482
563
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
483
|
-
const proposalNonce = nonce ?? Date.now();
|
|
484
564
|
const metadata = {
|
|
565
|
+
chainAnchor,
|
|
485
566
|
proposalType: 'change_threshold',
|
|
486
567
|
targetThreshold: newThreshold,
|
|
487
568
|
targetSignerCommitments: this.signerCommitments,
|
|
@@ -491,7 +572,8 @@ export class Multisig {
|
|
|
491
572
|
};
|
|
492
573
|
return this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
493
574
|
}
|
|
494
|
-
async createUpdateProcedureThresholdProposal(targetProcedure, targetThreshold,
|
|
575
|
+
async createUpdateProcedureThresholdProposal(targetProcedure, targetThreshold, options = {}) {
|
|
576
|
+
const proposalNonce = resolveProposalNonce('createUpdateProcedureThresholdProposal', options);
|
|
495
577
|
const webClient = await this.getRawClient();
|
|
496
578
|
if (targetThreshold < 0 || targetThreshold > this.signerCommitments.length) {
|
|
497
579
|
throw new Error(`Invalid threshold ${targetThreshold}. Must be between 0 and ${this.signerCommitments.length}`);
|
|
@@ -504,13 +586,15 @@ export class Multisig {
|
|
|
504
586
|
throw new Error(`Procedure ${targetProcedure} already has threshold override ${targetThreshold}`);
|
|
505
587
|
}
|
|
506
588
|
const { request, salt } = await buildUpdateProcedureThresholdTransactionRequest(webClient, targetProcedure, targetThreshold, { signatureScheme: this.signer.scheme });
|
|
507
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
589
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
590
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
591
|
+
anchor.free();
|
|
508
592
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
509
|
-
const proposalNonce = nonce ?? Date.now();
|
|
510
593
|
const action = targetThreshold === 0
|
|
511
594
|
? `Clear threshold override for ${targetProcedure}`
|
|
512
595
|
: `Set ${targetProcedure} threshold override to ${targetThreshold}`;
|
|
513
596
|
const metadata = {
|
|
597
|
+
chainAnchor,
|
|
514
598
|
proposalType: 'update_procedure_threshold',
|
|
515
599
|
targetProcedure,
|
|
516
600
|
targetThreshold,
|
|
@@ -525,16 +609,19 @@ export class Multisig {
|
|
|
525
609
|
*
|
|
526
610
|
* @param newGuardianEndpoint - The new GUARDIAN server endpoint URL
|
|
527
611
|
* @param newGuardianPubkey - The new GUARDIAN server's public key commitment (hex)
|
|
528
|
-
* @param
|
|
612
|
+
* @param options - Optional settings: `nonce`
|
|
529
613
|
*/
|
|
530
|
-
async createSwitchGuardianProposal(newGuardianEndpoint, newGuardianPubkey,
|
|
614
|
+
async createSwitchGuardianProposal(newGuardianEndpoint, newGuardianPubkey, options = {}) {
|
|
615
|
+
const proposalNonce = resolveProposalNonce('createSwitchGuardianProposal', options);
|
|
531
616
|
const webClient = await this.getRawClient();
|
|
532
617
|
await this.verifyGuardianEndpointCommitment(newGuardianEndpoint, newGuardianPubkey);
|
|
533
618
|
const { request, salt } = await buildUpdateGuardianTransactionRequest(webClient, newGuardianPubkey, { signatureScheme: this.signer.scheme });
|
|
534
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
619
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
620
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
621
|
+
anchor.free();
|
|
535
622
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
536
|
-
const proposalNonce = nonce ?? Date.now();
|
|
537
623
|
const metadata = {
|
|
624
|
+
chainAnchor,
|
|
538
625
|
proposalType: 'switch_guardian',
|
|
539
626
|
saltHex: salt.toHex(),
|
|
540
627
|
requiredSignatures: this.getEffectiveThreshold('switch_guardian'),
|
|
@@ -550,9 +637,10 @@ export class Multisig {
|
|
|
550
637
|
* Create a "consume notes" proposal to consume notes sent to the multisig account.
|
|
551
638
|
*
|
|
552
639
|
* @param noteIds - IDs of the notes to consume (hex strings)
|
|
553
|
-
* @param
|
|
640
|
+
* @param options - Optional settings: `nonce`
|
|
554
641
|
*/
|
|
555
|
-
async createConsumeNotesProposal(noteIds,
|
|
642
|
+
async createConsumeNotesProposal(noteIds, options = {}) {
|
|
643
|
+
const proposalNonce = resolveProposalNonce('createConsumeNotesProposal', options);
|
|
556
644
|
const webClient = await this.getRawClient();
|
|
557
645
|
if (noteIds.length === 0) {
|
|
558
646
|
throw new Error('At least one note ID is required');
|
|
@@ -569,10 +657,12 @@ export class Multisig {
|
|
|
569
657
|
}
|
|
570
658
|
const embeddedNotes = fetchedNotes.map((n) => noteToBase64(n));
|
|
571
659
|
const { request, salt } = buildConsumeNotesTransactionRequestFromNotes(fetchedNotes);
|
|
572
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
660
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
661
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
662
|
+
anchor.free();
|
|
573
663
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
574
|
-
const proposalNonce = nonce ?? Date.now();
|
|
575
664
|
const metadata = {
|
|
665
|
+
chainAnchor,
|
|
576
666
|
proposalType: 'consume_notes',
|
|
577
667
|
noteIds,
|
|
578
668
|
metadataVersion: CONSUME_NOTES_METADATA_VERSION_V2,
|
|
@@ -598,21 +688,26 @@ export class Multisig {
|
|
|
598
688
|
* @param recipientId - Account ID of the recipient (hex string)
|
|
599
689
|
* @param faucetId - Faucet/token account ID (hex string)
|
|
600
690
|
* @param amount - Amount to send
|
|
601
|
-
* @param
|
|
602
|
-
*
|
|
603
|
-
*
|
|
691
|
+
* @param options - Optional settings: `nonce`; `noteType` selects the created
|
|
692
|
+
* note's visibility (defaults to `NoteType.Public`, issue #322);
|
|
693
|
+
* `reclaimHeight`/`timelockHeight` build a P2IDE note (issue #366)
|
|
604
694
|
*/
|
|
605
|
-
async createP2idProposal(recipientId, faucetId, amount,
|
|
695
|
+
async createP2idProposal(recipientId, faucetId, amount, options = {}, ...legacyArgs) {
|
|
696
|
+
const proposalNonce = resolveProposalNonce('createP2idProposal', options, legacyArgs);
|
|
606
697
|
const webClient = await this.getRawClient();
|
|
607
698
|
if (amount <= 0n) {
|
|
608
699
|
throw new Error('Amount must be greater than 0');
|
|
609
700
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const
|
|
701
|
+
// Forward everything but the nonce, so a note option added to
|
|
702
|
+
// CreateP2idProposalOptions can't be silently dropped before the builder.
|
|
703
|
+
const { nonce: _nonce, ...noteOptions } = options;
|
|
704
|
+
const { request, salt } = buildP2idTransactionRequest(this._accountId, recipientId, faucetId, amount, noteOptions);
|
|
705
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
706
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
707
|
+
anchor.free();
|
|
613
708
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
614
|
-
const proposalNonce = nonce ?? Date.now();
|
|
615
709
|
const metadata = {
|
|
710
|
+
chainAnchor,
|
|
616
711
|
proposalType: 'p2id',
|
|
617
712
|
saltHex: salt.toHex(),
|
|
618
713
|
requiredSignatures: this.getEffectiveThreshold('p2id'),
|
|
@@ -621,6 +716,9 @@ export class Multisig {
|
|
|
621
716
|
amount: amount.toString(),
|
|
622
717
|
// Omitted for public notes so the wire shape matches pre-#322 proposals.
|
|
623
718
|
noteType: p2idNoteTypeToMetadata(options.noteType),
|
|
719
|
+
// Omitted when absent so plain-P2ID payloads keep the pre-#366 wire shape.
|
|
720
|
+
reclaimHeight: options.reclaimHeight,
|
|
721
|
+
timelockHeight: options.timelockHeight,
|
|
624
722
|
description: `Send ${amount} of asset ${faucetId.slice(0, 10)}... to ${recipientId.slice(0, 10)}...`,
|
|
625
723
|
};
|
|
626
724
|
return this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
@@ -668,7 +766,7 @@ export class Multisig {
|
|
|
668
766
|
}
|
|
669
767
|
/**
|
|
670
768
|
* Export a note created by this multisig account as serialized note-file
|
|
671
|
-
* bytes for out-of-band delivery
|
|
769
|
+
* bytes for out-of-band delivery.
|
|
672
770
|
*
|
|
673
771
|
* A private note publishes only its commitment on chain, so the recipient
|
|
674
772
|
* can never learn its contents via sync; the sender must hand them the
|
|
@@ -705,7 +803,7 @@ export class Multisig {
|
|
|
705
803
|
}
|
|
706
804
|
/**
|
|
707
805
|
* Export a note created by this multisig account as a note file downloaded
|
|
708
|
-
* by the browser
|
|
806
|
+
* by the browser. Browser-only convenience over
|
|
709
807
|
* {@link exportNoteToBytes}; use that method directly in non-DOM
|
|
710
808
|
* environments.
|
|
711
809
|
*
|
|
@@ -731,7 +829,7 @@ export class Multisig {
|
|
|
731
829
|
}
|
|
732
830
|
}
|
|
733
831
|
/**
|
|
734
|
-
* Import a note file received out-of-band
|
|
832
|
+
* Import a note file received out-of-band so the note can be
|
|
735
833
|
* consumed by this multisig account.
|
|
736
834
|
*
|
|
737
835
|
* Sync the Miden client with the network afterwards so the note's on-chain
|
|
@@ -756,7 +854,7 @@ export class Multisig {
|
|
|
756
854
|
return webClient.importNoteFile(noteFile);
|
|
757
855
|
}
|
|
758
856
|
/**
|
|
759
|
-
* Import a note file received out-of-band
|
|
857
|
+
* Import a note file received out-of-band from a browser
|
|
760
858
|
* `File`/`Blob` (e.g. a file-input selection). See
|
|
761
859
|
* {@link importNoteFromBytes} for the returned identifier semantics.
|
|
762
860
|
*/
|
|
@@ -770,10 +868,9 @@ export class Multisig {
|
|
|
770
868
|
* The P2ID note is rebuilt deterministically from the proposal salt, so the
|
|
771
869
|
* ID is known ahead of execution. For a private P2ID this is the ID to pass
|
|
772
870
|
* to {@link exportNoteToBytes} after executing, so the note file can be delivered
|
|
773
|
-
* to the recipient out-of-band
|
|
871
|
+
* to the recipient out-of-band.
|
|
774
872
|
*
|
|
775
|
-
*
|
|
776
|
-
* current vault state, which execution itself changes.
|
|
873
|
+
* The note ID remains deterministic from the proposal metadata and salt.
|
|
777
874
|
*/
|
|
778
875
|
async getP2idNoteId(proposal) {
|
|
779
876
|
const metadata = proposal.metadata;
|
|
@@ -784,8 +881,7 @@ export class Multisig {
|
|
|
784
881
|
!metadata.saltHex) {
|
|
785
882
|
throw new Error('getP2idNoteId requires a P2ID proposal with recipient, faucet, amount, and salt metadata');
|
|
786
883
|
}
|
|
787
|
-
const
|
|
788
|
-
const note = buildP2idNoteFromMetadata(this._accountId, metadata.recipientId, metadata.faucetId, BigInt(metadata.amount), account, parseP2idNoteType(metadata.noteType), metadata.saltHex);
|
|
884
|
+
const note = buildP2idNoteFromMetadata(this._accountId, metadata.recipientId, metadata.faucetId, BigInt(metadata.amount), parseP2idNoteType(metadata.noteType), metadata.saltHex, { reclaimHeight: metadata.reclaimHeight, timelockHeight: metadata.timelockHeight });
|
|
789
885
|
return note.id().toString();
|
|
790
886
|
}
|
|
791
887
|
/**
|
|
@@ -826,6 +922,18 @@ export class Multisig {
|
|
|
826
922
|
async abandonStatus(nonce) {
|
|
827
923
|
return this.guardian.abandonStatus(this._accountId, nonce);
|
|
828
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* Fetch one page of this account's canonical delta history
|
|
927
|
+
* from GUARDIAN (issue #413), newest-first by nonce, with decoded
|
|
928
|
+
* input/output note summaries. Pass `options.cursor` from a previous
|
|
929
|
+
* page's `nextCursor` to resume; an absent `nextCursor` means the
|
|
930
|
+
* feed is exhausted. Served while the account is paused. Only
|
|
931
|
+
* transactions pushed through GUARDIAN appear — history of
|
|
932
|
+
* transactions executed elsewhere is not visible to it.
|
|
933
|
+
*/
|
|
934
|
+
async deltaHistory(options = {}) {
|
|
935
|
+
return this.guardian.getDeltaHistory(this._accountId, options);
|
|
936
|
+
}
|
|
829
937
|
async signProposal(proposalId) {
|
|
830
938
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
831
939
|
const existingProposal = await this.getProposalForSigning(proposalId, normalizedProposalId);
|
|
@@ -866,8 +974,17 @@ export class Multisig {
|
|
|
866
974
|
*/
|
|
867
975
|
async executeProposal(proposalId) {
|
|
868
976
|
const { metadata, finalRequest, proposal } = await this.prepareProposalExecution(proposalId);
|
|
977
|
+
// Execute at the proposal's anchored reference block, so the summary the
|
|
978
|
+
// cosigners signed reproduces exactly. The anchor was already checked
|
|
979
|
+
// against the summary's block commitment during binding verification.
|
|
869
980
|
const accountId = AccountId.fromHex(this._accountId);
|
|
870
|
-
|
|
981
|
+
const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
|
|
982
|
+
try {
|
|
983
|
+
await this.proverWorkflow.submitAt(accountId, finalRequest, anchor);
|
|
984
|
+
}
|
|
985
|
+
finally {
|
|
986
|
+
anchor.free();
|
|
987
|
+
}
|
|
871
988
|
if (metadata.proposalType === 'switch_guardian') {
|
|
872
989
|
if (!metadata.newGuardianEndpoint || !metadata.newGuardianPubkey) {
|
|
873
990
|
throw new Error('Switch GUARDIAN proposal metadata is incomplete after execution');
|
|
@@ -915,18 +1032,37 @@ export class Multisig {
|
|
|
915
1032
|
* Submit an integration-built transaction (advice already injected). Mirrors
|
|
916
1033
|
* the Rust `submit_transaction`; used by the custom proposal producer flow
|
|
917
1034
|
* after `prepareCustomExecution` rebuilds its request with the returned advice.
|
|
1035
|
+
* The transaction is executed at the proposal's anchored reference block,
|
|
1036
|
+
* since the collected signatures only authorize the summary produced there.
|
|
918
1037
|
*/
|
|
919
|
-
async submitTransaction(request) {
|
|
920
|
-
|
|
1038
|
+
async submitTransaction(proposalId, request) {
|
|
1039
|
+
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1040
|
+
const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
|
|
1041
|
+
const existing = this.getLocalProposal(proposalId);
|
|
1042
|
+
const proposal = this.proposalFactory().fromDelta(delta, normalizedProposalId, existing?.metadata, existing?.signatures ?? []);
|
|
1043
|
+
const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
|
|
1044
|
+
try {
|
|
1045
|
+
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
1046
|
+
const txSummary = TransactionSummary.deserialize(base64ToUint8Array(delta.deltaPayload.txSummary.data));
|
|
1047
|
+
const summaryBlockCommitment = normalizeHexWord(txSummary.blockCommitment().toHex());
|
|
1048
|
+
if (anchorCommitment !== summaryBlockCommitment) {
|
|
1049
|
+
throw new Error(`Proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`);
|
|
1050
|
+
}
|
|
1051
|
+
await this.proverWorkflow.submitAt(AccountId.fromHex(this._accountId), request, anchor);
|
|
1052
|
+
}
|
|
1053
|
+
finally {
|
|
1054
|
+
anchor.free();
|
|
1055
|
+
}
|
|
921
1056
|
}
|
|
922
1057
|
/**
|
|
923
|
-
* Create a proposal from a producer-built transaction the SDK does not model
|
|
924
|
-
*
|
|
1058
|
+
* Create a proposal from a producer-built transaction the SDK does not model.
|
|
1059
|
+
* `transactionRequestBytes` is a serialized TransactionRequest;
|
|
925
1060
|
* `proposalType` is a free-form, non-empty label that must not collide with a
|
|
926
1061
|
* built-in type. The integration keeps its own recipe to execute later via
|
|
927
1062
|
* `prepareCustomExecution`.
|
|
928
1063
|
*/
|
|
929
|
-
async createCustomProposal(transactionRequestBytes, proposalType,
|
|
1064
|
+
async createCustomProposal(transactionRequestBytes, proposalType, options = {}) {
|
|
1065
|
+
const proposalNonce = resolveProposalNonce('createCustomProposal', options);
|
|
930
1066
|
const label = proposalType.trim().toLowerCase();
|
|
931
1067
|
if (label.length === 0) {
|
|
932
1068
|
throw new Error('proposalType must not be empty');
|
|
@@ -939,10 +1075,12 @@ export class Multisig {
|
|
|
939
1075
|
}
|
|
940
1076
|
const webClient = await this.getRawClient();
|
|
941
1077
|
const request = deserializeTransactionRequest(transactionRequestBytes);
|
|
942
|
-
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
1078
|
+
const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
|
|
1079
|
+
const chainAnchor = chainAnchorToBase64(anchor);
|
|
1080
|
+
anchor.free();
|
|
943
1081
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
944
|
-
const proposalNonce = nonce ?? Date.now();
|
|
945
1082
|
const metadata = {
|
|
1083
|
+
chainAnchor,
|
|
946
1084
|
proposalType: 'custom',
|
|
947
1085
|
description: '',
|
|
948
1086
|
rawProposalType: label,
|
|
@@ -953,7 +1091,7 @@ export class Multisig {
|
|
|
953
1091
|
/**
|
|
954
1092
|
* Assemble the validated execution advice (cosigner signatures + GUARDIAN
|
|
955
1093
|
* acknowledgment) for a ready custom proposal, so an integration can rebuild
|
|
956
|
-
* its transaction with its own recipe and submit
|
|
1094
|
+
* its transaction with its own recipe and submit.
|
|
957
1095
|
*
|
|
958
1096
|
* `transactionRequestBytes` is the serialized transaction request; it is used only to verify
|
|
959
1097
|
* (binding check) that it reproduces the signed commitment, before the
|
|
@@ -976,9 +1114,26 @@ export class Multisig {
|
|
|
976
1114
|
const txSummary = TransactionSummary.deserialize(base64ToUint8Array(delta.deltaPayload.txSummary.data));
|
|
977
1115
|
const signedCommitmentHex = normalizeHexWord(txSummary.toCommitment().toHex());
|
|
978
1116
|
const bindingRequest = deserializeTransactionRequest(transactionRequestBytes);
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1117
|
+
// Probe at the proposal's anchored reference block: the signed summary
|
|
1118
|
+
// binds that block's commitment, so probing at the local sync height would
|
|
1119
|
+
// never reproduce it. The anchor arrives from an untrusted party via
|
|
1120
|
+
// GUARDIAN, so its block commitment is checked against the signed summary
|
|
1121
|
+
// before executing against it.
|
|
1122
|
+
const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
|
|
1123
|
+
let derivedCommitmentHex;
|
|
1124
|
+
try {
|
|
1125
|
+
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
1126
|
+
const summaryBlockCommitment = normalizeHexWord(txSummary.blockCommitment().toHex());
|
|
1127
|
+
if (anchorCommitment !== summaryBlockCommitment) {
|
|
1128
|
+
throw new Error(`Custom proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`);
|
|
1129
|
+
}
|
|
1130
|
+
const webClient = await this.getRawClient();
|
|
1131
|
+
const derived = await executeForSummaryAt(webClient, this._accountId, bindingRequest, anchor);
|
|
1132
|
+
derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
|
|
1133
|
+
}
|
|
1134
|
+
finally {
|
|
1135
|
+
anchor.free();
|
|
1136
|
+
}
|
|
982
1137
|
if (derivedCommitmentHex !== signedCommitmentHex) {
|
|
983
1138
|
throw new Error(`Custom proposal binding mismatch: expected ${signedCommitmentHex}, got ${derivedCommitmentHex}`);
|
|
984
1139
|
}
|
|
@@ -1007,7 +1162,10 @@ export class Multisig {
|
|
|
1007
1162
|
const signerCommitment = Word.fromHex(signerCommitmentHex);
|
|
1008
1163
|
const sigBytes = signatureHexToBytes(cosignerSig.signature.signature, cosignerSig.signature.scheme);
|
|
1009
1164
|
const signature = Signature.deserialize(sigBytes);
|
|
1010
|
-
|
|
1165
|
+
if (cosignerSig.signature.scheme === 'ecdsa' && ecdsaPublicKey) {
|
|
1166
|
+
assertEcdsaSignatureRecoverable(cosignerSig.signature.signature, normalizedTxCommitmentHex, ecdsaPublicKey);
|
|
1167
|
+
}
|
|
1168
|
+
const { key, values } = buildSignatureAdviceEntry(signerCommitment, createTxCommitmentWord(), signature);
|
|
1011
1169
|
const keyHex = normalizeHexWord(key.toHex());
|
|
1012
1170
|
if (adviceMapKeys.has(keyHex)) {
|
|
1013
1171
|
throw new Error(`Duplicate advice-map key detected for proposal ${proposalId}`);
|
|
@@ -1035,7 +1193,10 @@ export class Multisig {
|
|
|
1035
1193
|
}
|
|
1036
1194
|
const ackSigBytes = signatureHexToBytes(ackSigHex, ackScheme);
|
|
1037
1195
|
const ackSignature = Signature.deserialize(ackSigBytes);
|
|
1038
|
-
|
|
1196
|
+
if (ackScheme === 'ecdsa' && ackPubkey) {
|
|
1197
|
+
assertEcdsaSignatureRecoverable(ackSigHex, normalizedTxCommitmentHex, ackPubkey);
|
|
1198
|
+
}
|
|
1199
|
+
const { key: ackKey, values: ackValues } = buildSignatureAdviceEntry(guardianCommitment, createTxCommitmentWord(), ackSignature);
|
|
1039
1200
|
const ackKeyHex = normalizeHexWord(ackKey.toHex());
|
|
1040
1201
|
if (adviceMapKeys.has(ackKeyHex)) {
|
|
1041
1202
|
throw new Error(`Duplicate advice-map key detected for GUARDIAN acknowledgment in proposal ${proposalId}`);
|
|
@@ -1082,7 +1243,7 @@ export class Multisig {
|
|
|
1082
1243
|
}
|
|
1083
1244
|
const txSummaryBytes = base64ToUint8Array(txSummaryBase64);
|
|
1084
1245
|
const txSummary = TransactionSummary.deserialize(txSummaryBytes);
|
|
1085
|
-
const saltHex = txSummary
|
|
1246
|
+
const saltHex = summarySalt(txSummary).toHex();
|
|
1086
1247
|
const txCommitmentHex = txSummary.toCommitment().toHex();
|
|
1087
1248
|
const normalizedTxCommitmentHex = normalizeHexWord(txCommitmentHex);
|
|
1088
1249
|
const normalizedSignerCommitments = new Set(this.signerCommitments.map((commitment) => normalizeHexWord(commitment)));
|
|
@@ -1109,9 +1270,10 @@ export class Multisig {
|
|
|
1109
1270
|
const signerCommitment = Word.fromHex(signerCommitmentHex);
|
|
1110
1271
|
const sigBytes = signatureHexToBytes(cosignerSig.signature.signature, cosignerSig.signature.scheme);
|
|
1111
1272
|
const signature = Signature.deserialize(sigBytes);
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1273
|
+
if (cosignerSig.signature.scheme === 'ecdsa' && ecdsaPublicKey) {
|
|
1274
|
+
assertEcdsaSignatureRecoverable(cosignerSig.signature.signature, normalizedTxCommitmentHex, ecdsaPublicKey);
|
|
1275
|
+
}
|
|
1276
|
+
const { key, values } = buildSignatureAdviceEntry(signerCommitment, createTxCommitmentWord(), signature);
|
|
1115
1277
|
const keyHex = normalizeHexWord(key.toHex());
|
|
1116
1278
|
if (adviceMapKeys.has(keyHex)) {
|
|
1117
1279
|
throw new Error(`Duplicate advice-map key detected for proposal ${proposalId}`);
|
|
@@ -1143,7 +1305,10 @@ export class Multisig {
|
|
|
1143
1305
|
}
|
|
1144
1306
|
const ackSigBytes = signatureHexToBytes(ackSigHex, ackScheme);
|
|
1145
1307
|
const ackSignature = Signature.deserialize(ackSigBytes);
|
|
1146
|
-
|
|
1308
|
+
if (ackScheme === 'ecdsa' && ackPubkey) {
|
|
1309
|
+
assertEcdsaSignatureRecoverable(ackSigHex, normalizedTxCommitmentHex, ackPubkey);
|
|
1310
|
+
}
|
|
1311
|
+
const { key: ackKey, values: ackValues } = buildSignatureAdviceEntry(guardianCommitment, createTxCommitmentWord(), ackSignature);
|
|
1147
1312
|
const ackKeyHex = normalizeHexWord(ackKey.toHex());
|
|
1148
1313
|
if (adviceMapKeys.has(ackKeyHex)) {
|
|
1149
1314
|
throw new Error(`Duplicate advice-map key detected for GUARDIAN acknowledgment in proposal ${proposalId}`);
|
|
@@ -1287,34 +1452,58 @@ export class Multisig {
|
|
|
1287
1452
|
}
|
|
1288
1453
|
async verifyProposalMetadataBinding(proposal) {
|
|
1289
1454
|
const txSummaryCommitment = this.ensureProposalCommitmentMatchesSummary(proposal);
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1455
|
+
const summary = TransactionSummary.deserialize(base64ToUint8Array(proposal.txSummary));
|
|
1456
|
+
// The anchor arrives from an untrusted party via GUARDIAN, so check its
|
|
1457
|
+
// block commitment against the one bound into the signed summary before
|
|
1458
|
+
// anything executes against it. `ChainAnchor.deserialize` already enforced
|
|
1459
|
+
// internal header/chain consistency.
|
|
1460
|
+
const anchor = this.requireProposalAnchor(proposal.id, proposal.metadata);
|
|
1461
|
+
try {
|
|
1462
|
+
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
1463
|
+
const summaryBlockCommitment = normalizeHexWord(summary.blockCommitment().toHex());
|
|
1464
|
+
if (anchorCommitment !== summaryBlockCommitment) {
|
|
1465
|
+
throw new Error(`Invalid proposal: chain anchor does not match the block commitment bound into the tx_summary for ${proposal.id}`);
|
|
1466
|
+
}
|
|
1467
|
+
if (proposal.metadata.proposalType === 'custom') {
|
|
1468
|
+
// Custom proposals have no per-type reconstruction recipe;
|
|
1469
|
+
// the id ↔ tx_summary commitment match above is the only available
|
|
1470
|
+
// integrity guarantee for an opaque proposal.
|
|
1471
|
+
return txSummaryCommitment;
|
|
1472
|
+
}
|
|
1473
|
+
if (proposal.metadata.proposalType === 'switch_guardian') {
|
|
1474
|
+
// Re-execution would mutate the WASM account twice. The proposal ID and
|
|
1475
|
+
// guardian endpoint commitment provide the binding checks for this type.
|
|
1476
|
+
return txSummaryCommitment;
|
|
1477
|
+
}
|
|
1478
|
+
const salt = proposal.metadata.saltHex
|
|
1479
|
+
? Word.fromHex(normalizeHexWord(proposal.metadata.saltHex))
|
|
1480
|
+
: summarySalt(summary);
|
|
1481
|
+
const request = await this.buildTransactionRequestFromMetadata(proposal.metadata, salt);
|
|
1482
|
+
const webClient = await this.getRawClient();
|
|
1483
|
+
const reconstructed = await executeForSummaryAt(webClient, this._accountId, request, anchor);
|
|
1484
|
+
const reconstructedCommitment = normalizeHexWord(reconstructed.toCommitment().toHex());
|
|
1485
|
+
if (reconstructedCommitment !== txSummaryCommitment) {
|
|
1486
|
+
throw new Error(`Invalid proposal: metadata does not match tx_summary for ${proposal.id}`);
|
|
1487
|
+
}
|
|
1294
1488
|
return txSummaryCommitment;
|
|
1295
1489
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
// The WASM `executeForSummary` leaves the guardian-disabling side effect
|
|
1299
|
-
// applied to the in-session account, so re-execution reconstructs a smaller
|
|
1300
|
-
// delta and falsely rejects with "metadata does not match tx_summary". The
|
|
1301
|
-
// native Rust client does not mutate, so this is an intentional divergence.
|
|
1302
|
-
// The id ↔ tx_summary match above plus `verifyGuardianEndpointCommitment`
|
|
1303
|
-
// at propose/execute time still bind the proposal.
|
|
1304
|
-
return txSummaryCommitment;
|
|
1490
|
+
finally {
|
|
1491
|
+
anchor.free();
|
|
1305
1492
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
if (
|
|
1315
|
-
throw new Error(`
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Decodes a proposal's chain anchor. Throws when absent: a proposal without
|
|
1496
|
+
* an anchor was created at an unknown reference block, so its signed summary
|
|
1497
|
+
* cannot be reproduced, verified, or executed. The caller owns the returned
|
|
1498
|
+
* anchor and must `free()` it once done.
|
|
1499
|
+
*/
|
|
1500
|
+
requireProposalAnchor(proposalId, metadata) {
|
|
1501
|
+
if (!metadata.chainAnchor) {
|
|
1502
|
+
throw new Error(`Proposal ${proposalId} has no chain anchor; it was created without ` +
|
|
1503
|
+
'chain-anchored execution and its signed summary cannot be reproduced ' +
|
|
1504
|
+
'at the original reference block');
|
|
1316
1505
|
}
|
|
1317
|
-
return
|
|
1506
|
+
return chainAnchorFromBase64(metadata.chainAnchor);
|
|
1318
1507
|
}
|
|
1319
1508
|
async buildTransactionRequestFromMetadata(metadata, salt, signatureAdviceMap) {
|
|
1320
1509
|
const webClient = await this.getRawClient();
|
|
@@ -1370,8 +1559,13 @@ export class Multisig {
|
|
|
1370
1559
|
throw new UnsupportedMetadataVersionError(version);
|
|
1371
1560
|
}
|
|
1372
1561
|
case 'p2id': {
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1562
|
+
const { request } = buildP2idTransactionRequest(this._accountId, metadata.recipientId, metadata.faucetId, BigInt(metadata.amount), {
|
|
1563
|
+
salt,
|
|
1564
|
+
signatureAdviceMap,
|
|
1565
|
+
noteType: parseP2idNoteType(metadata.noteType),
|
|
1566
|
+
reclaimHeight: metadata.reclaimHeight,
|
|
1567
|
+
timelockHeight: metadata.timelockHeight,
|
|
1568
|
+
});
|
|
1375
1569
|
return request;
|
|
1376
1570
|
}
|
|
1377
1571
|
case 'custom':
|