@openzeppelin/miden-multisig-client 0.17.0-rc.2 → 0.17.0-rc.3
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 +167 -42
- package/dist/account/builder.d.ts +2 -2
- package/dist/account/builder.d.ts.map +1 -1
- package/dist/account/builder.js +49 -24
- package/dist/account/builder.js.map +1 -1
- package/dist/connectivity.d.ts +2 -0
- package/dist/connectivity.d.ts.map +1 -1
- package/dist/connectivity.js +7 -4
- package/dist/connectivity.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/multisig/authArgErrors.d.ts +59 -0
- package/dist/multisig/authArgErrors.d.ts.map +1 -0
- package/dist/multisig/authArgErrors.js +115 -0
- package/dist/multisig/authArgErrors.js.map +1 -0
- package/dist/multisig/helpers.d.ts +5 -0
- package/dist/multisig/helpers.d.ts.map +1 -1
- package/dist/multisig/helpers.js +7 -2
- package/dist/multisig/helpers.js.map +1 -1
- package/dist/multisig.d.ts +140 -0
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +419 -12
- package/dist/multisig.js.map +1 -1
- package/dist/procedures.d.ts +1 -1
- package/dist/procedures.js +1 -1
- package/dist/recovery/proposalNoteImport.d.ts +173 -0
- package/dist/recovery/proposalNoteImport.d.ts.map +1 -0
- package/dist/recovery/proposalNoteImport.js +411 -0
- package/dist/recovery/proposalNoteImport.js.map +1 -0
- package/dist/recovery/publicNoteBackfill.d.ts +168 -0
- package/dist/recovery/publicNoteBackfill.d.ts.map +1 -0
- package/dist/recovery/publicNoteBackfill.js +368 -0
- package/dist/recovery/publicNoteBackfill.js.map +1 -0
- package/dist/recovery/recoverNotes.d.ts +147 -0
- package/dist/recovery/recoverNotes.d.ts.map +1 -0
- package/dist/recovery/recoverNotes.js +163 -0
- package/dist/recovery/recoverNotes.js.map +1 -0
- package/dist/recovery/transportDrain.d.ts +124 -0
- package/dist/recovery/transportDrain.d.ts.map +1 -0
- package/dist/recovery/transportDrain.js +262 -0
- package/dist/recovery/transportDrain.js.map +1 -0
- package/dist/transaction/consumeNotes.d.ts.map +1 -1
- package/dist/transaction/consumeNotes.js +4 -1
- package/dist/transaction/consumeNotes.js.map +1 -1
- package/dist/transaction/p2id.d.ts.map +1 -1
- package/dist/transaction/p2id.js +4 -1
- package/dist/transaction/p2id.js.map +1 -1
- package/dist/transaction/summary.d.ts +10 -4
- package/dist/transaction/summary.d.ts.map +1 -1
- package/dist/transaction/summary.js +13 -7
- package/dist/transaction/summary.js.map +1 -1
- package/dist/transaction/updateGuardian.js +4 -1
- package/dist/transaction/updateGuardian.js.map +1 -1
- package/dist/transaction/updateProcedureThreshold.js +4 -1
- package/dist/transaction/updateProcedureThreshold.js.map +1 -1
- package/dist/transaction/updateSigners.js +4 -1
- package/dist/transaction/updateSigners.js.map +1 -1
- package/dist/transaction.d.ts +1 -1
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +1 -1
- package/dist/transaction.js.map +1 -1
- package/package.json +14 -10
- package/src/account/builder.ts +87 -35
- package/src/connectivity.ts +9 -4
- package/src/index.ts +34 -1
- package/src/multisig/authArgErrors.ts +138 -0
- package/src/multisig/helpers.ts +7 -2
- package/src/multisig.ts +513 -20
- package/src/procedures.ts +1 -1
- package/src/recovery/proposalNoteImport.ts +548 -0
- package/src/recovery/publicNoteBackfill.ts +502 -0
- package/src/recovery/recoverNotes.ts +288 -0
- package/src/recovery/transportDrain.ts +330 -0
- package/src/transaction/consumeNotes.ts +4 -1
- package/src/transaction/p2id.ts +4 -1
- package/src/transaction/summary.ts +13 -7
- package/src/transaction/updateGuardian.ts +4 -1
- package/src/transaction/updateProcedureThreshold.ts +4 -1
- package/src/transaction/updateSigners.ts +4 -1
- package/src/transaction.ts +1 -1
- package/dist/account/builder.test.d.ts +0 -2
- package/dist/account/builder.test.d.ts.map +0 -1
- package/dist/account/builder.test.js +0 -166
- package/dist/account/builder.test.js.map +0 -1
- package/dist/account/masm/account-components/auth.d.ts +0 -2
- package/dist/account/masm/account-components/auth.d.ts.map +0 -1
- package/dist/account/masm/account-components/auth.js +0 -46
- package/dist/account/masm/account-components/auth.js.map +0 -1
- package/dist/account/masm/index.d.ts +0 -2
- package/dist/account/masm/index.d.ts.map +0 -1
- package/dist/account/masm/index.js +0 -4
- package/dist/account/masm/index.js.map +0 -1
- package/dist/account/masm.d.ts +0 -2
- package/dist/account/masm.d.ts.map +0 -1
- package/dist/account/masm.js +0 -4
- package/dist/account/masm.js.map +0 -1
- package/dist/account/storage.test.d.ts +0 -2
- package/dist/account/storage.test.d.ts.map +0 -1
- package/dist/account/storage.test.js +0 -73
- package/dist/account/storage.test.js.map +0 -1
- package/dist/client.test.d.ts +0 -2
- package/dist/client.test.d.ts.map +0 -1
- package/dist/client.test.js +0 -380
- package/dist/client.test.js.map +0 -1
- package/dist/connectivity.test.d.ts +0 -2
- package/dist/connectivity.test.d.ts.map +0 -1
- package/dist/connectivity.test.js +0 -61
- package/dist/connectivity.test.js.map +0 -1
- package/dist/inspector.test.d.ts +0 -2
- package/dist/inspector.test.d.ts.map +0 -1
- package/dist/inspector.test.js +0 -425
- package/dist/inspector.test.js.map +0 -1
- package/dist/lookupAuth.test.d.ts +0 -2
- package/dist/lookupAuth.test.d.ts.map +0 -1
- package/dist/lookupAuth.test.js +0 -138
- package/dist/lookupAuth.test.js.map +0 -1
- package/dist/multisig/consumeNotesErrors.test.d.ts +0 -2
- package/dist/multisig/consumeNotesErrors.test.d.ts.map +0 -1
- package/dist/multisig/consumeNotesErrors.test.js +0 -28
- package/dist/multisig/consumeNotesErrors.test.js.map +0 -1
- package/dist/multisig/helpers.test.d.ts +0 -2
- package/dist/multisig/helpers.test.d.ts.map +0 -1
- package/dist/multisig/helpers.test.js +0 -94
- package/dist/multisig/helpers.test.js.map +0 -1
- package/dist/multisig.test.d.ts +0 -2
- package/dist/multisig.test.d.ts.map +0 -1
- package/dist/multisig.test.js +0 -4112
- package/dist/multisig.test.js.map +0 -1
- package/dist/proposal/factory.test.d.ts +0 -2
- package/dist/proposal/factory.test.d.ts.map +0 -1
- package/dist/proposal/factory.test.js +0 -32
- package/dist/proposal/factory.test.js.map +0 -1
- package/dist/proposal/metadata.test.d.ts +0 -2
- package/dist/proposal/metadata.test.d.ts.map +0 -1
- package/dist/proposal/metadata.test.js +0 -193
- package/dist/proposal/metadata.test.js.map +0 -1
- package/dist/prover/config.test.d.ts +0 -2
- package/dist/prover/config.test.d.ts.map +0 -1
- package/dist/prover/config.test.js +0 -54
- package/dist/prover/config.test.js.map +0 -1
- package/dist/prover/errors.test.d.ts +0 -2
- package/dist/prover/errors.test.d.ts.map +0 -1
- package/dist/prover/errors.test.js +0 -29
- package/dist/prover/errors.test.js.map +0 -1
- package/dist/prover/retry.test.d.ts +0 -2
- package/dist/prover/retry.test.d.ts.map +0 -1
- package/dist/prover/retry.test.js +0 -20
- package/dist/prover/retry.test.js.map +0 -1
- package/dist/prover/workflow.test.d.ts +0 -2
- package/dist/prover/workflow.test.d.ts.map +0 -1
- package/dist/prover/workflow.test.js +0 -105
- package/dist/prover/workflow.test.js.map +0 -1
- package/dist/raw-client.test.d.ts +0 -2
- package/dist/raw-client.test.d.ts.map +0 -1
- package/dist/raw-client.test.js +0 -111
- package/dist/raw-client.test.js.map +0 -1
- package/dist/rpc/config.test.d.ts +0 -2
- package/dist/rpc/config.test.d.ts.map +0 -1
- package/dist/rpc/config.test.js +0 -24
- package/dist/rpc/config.test.js.map +0 -1
- package/dist/rpc/errors.test.d.ts +0 -2
- package/dist/rpc/errors.test.d.ts.map +0 -1
- package/dist/rpc/errors.test.js +0 -34
- package/dist/rpc/errors.test.js.map +0 -1
- package/dist/rpc/retry.test.d.ts +0 -2
- package/dist/rpc/retry.test.d.ts.map +0 -1
- package/dist/rpc/retry.test.js +0 -98
- package/dist/rpc/retry.test.js.map +0 -1
- package/dist/signers/ecdsa.test.d.ts +0 -2
- package/dist/signers/ecdsa.test.d.ts.map +0 -1
- package/dist/signers/ecdsa.test.js +0 -88
- package/dist/signers/ecdsa.test.js.map +0 -1
- package/dist/signers/falcon.test.d.ts +0 -2
- package/dist/signers/falcon.test.d.ts.map +0 -1
- package/dist/signers/falcon.test.js +0 -161
- package/dist/signers/falcon.test.js.map +0 -1
- package/dist/signers/miden-wallet.ecdsa-recovery.test.d.ts +0 -2
- package/dist/signers/miden-wallet.ecdsa-recovery.test.d.ts.map +0 -1
- package/dist/signers/miden-wallet.ecdsa-recovery.test.js +0 -148
- package/dist/signers/miden-wallet.ecdsa-recovery.test.js.map +0 -1
- package/dist/signers/miden-wallet.test.d.ts +0 -2
- package/dist/signers/miden-wallet.test.d.ts.map +0 -1
- package/dist/signers/miden-wallet.test.js +0 -164
- package/dist/signers/miden-wallet.test.js.map +0 -1
- package/dist/signers/para.test.d.ts +0 -2
- package/dist/signers/para.test.d.ts.map +0 -1
- package/dist/signers/para.test.js +0 -146
- package/dist/signers/para.test.js.map +0 -1
- package/dist/transaction/index.d.ts +0 -8
- package/dist/transaction/index.d.ts.map +0 -1
- package/dist/transaction/index.js +0 -7
- package/dist/transaction/index.js.map +0 -1
- package/dist/transaction/p2id.test.d.ts +0 -2
- package/dist/transaction/p2id.test.d.ts.map +0 -1
- package/dist/transaction/p2id.test.js +0 -246
- package/dist/transaction/p2id.test.js.map +0 -1
- package/dist/transaction/rpoRandomCoin.test.d.ts +0 -2
- package/dist/transaction/rpoRandomCoin.test.d.ts.map +0 -1
- package/dist/transaction/rpoRandomCoin.test.js +0 -52
- package/dist/transaction/rpoRandomCoin.test.js.map +0 -1
- package/dist/transaction/summary.test.d.ts +0 -2
- package/dist/transaction/summary.test.d.ts.map +0 -1
- package/dist/transaction/summary.test.js +0 -26
- package/dist/transaction/summary.test.js.map +0 -1
- package/dist/transaction.test.d.ts +0 -2
- package/dist/transaction.test.d.ts.map +0 -1
- package/dist/transaction.test.js +0 -122
- package/dist/transaction.test.js.map +0 -1
- package/dist/types/proposal.test.d.ts +0 -2
- package/dist/types/proposal.test.d.ts.map +0 -1
- package/dist/types/proposal.test.js +0 -56
- package/dist/types/proposal.test.js.map +0 -1
- package/dist/utils/digest.test.d.ts +0 -2
- package/dist/utils/digest.test.d.ts.map +0 -1
- package/dist/utils/digest.test.js +0 -48
- package/dist/utils/digest.test.js.map +0 -1
- package/dist/utils/ecdsa.test.d.ts +0 -2
- package/dist/utils/ecdsa.test.d.ts.map +0 -1
- package/dist/utils/ecdsa.test.js +0 -131
- package/dist/utils/ecdsa.test.js.map +0 -1
- package/dist/utils/encoding.test.d.ts +0 -2
- package/dist/utils/encoding.test.d.ts.map +0 -1
- package/dist/utils/encoding.test.js +0 -185
- package/dist/utils/encoding.test.js.map +0 -1
- package/dist/utils/key.test.d.ts +0 -2
- package/dist/utils/key.test.d.ts.map +0 -1
- package/dist/utils/key.test.js +0 -82
- package/dist/utils/key.test.js.map +0 -1
- package/dist/utils/signature.test.d.ts +0 -2
- package/dist/utils/signature.test.d.ts.map +0 -1
- package/dist/utils/signature.test.js +0 -164
- package/dist/utils/signature.test.js.map +0 -1
- package/dist/utils/word.test.d.ts +0 -2
- package/dist/utils/word.test.d.ts.map +0 -1
- package/dist/utils/word.test.js +0 -54
- package/dist/utils/word.test.js.map +0 -1
- package/masm/account_components/auth/guarded_multisig.masm +0 -42
- package/src/account/builder.test.ts +0 -238
- package/src/account/masm/account-components/auth.ts +0 -46
- package/src/account/masm/index.ts +0 -4
- package/src/account/masm.ts +0 -4
- package/src/account/storage.test.ts +0 -90
- package/src/client.test.ts +0 -473
- package/src/connectivity.test.ts +0 -67
- package/src/inspector.test.ts +0 -542
- package/src/lookupAuth.test.ts +0 -185
- package/src/multisig/consumeNotesErrors.test.ts +0 -43
- package/src/multisig/helpers.test.ts +0 -114
- package/src/multisig.test.ts +0 -5008
- package/src/proposal/factory.test.ts +0 -40
- package/src/proposal/metadata.test.ts +0 -235
- package/src/prover/config.test.ts +0 -90
- package/src/prover/errors.test.ts +0 -61
- package/src/prover/retry.test.ts +0 -38
- package/src/prover/workflow.test.ts +0 -145
- package/src/raw-client.test.ts +0 -171
- package/src/rpc/config.test.ts +0 -45
- package/src/rpc/errors.test.ts +0 -69
- package/src/rpc/retry.test.ts +0 -144
- package/src/signers/ecdsa.test.ts +0 -111
- package/src/signers/falcon.test.ts +0 -197
- package/src/signers/miden-wallet.ecdsa-recovery.test.ts +0 -201
- package/src/signers/miden-wallet.test.ts +0 -206
- package/src/signers/para.test.ts +0 -186
- package/src/transaction/index.ts +0 -13
- package/src/transaction/p2id.test.ts +0 -371
- package/src/transaction/rpoRandomCoin.test.ts +0 -64
- package/src/transaction/summary.test.ts +0 -32
- package/src/transaction.test.ts +0 -142
- package/src/types/proposal.test.ts +0 -68
- package/src/utils/digest.test.ts +0 -55
- package/src/utils/ecdsa.test.ts +0 -164
- package/src/utils/encoding.test.ts +0 -233
- package/src/utils/key.test.ts +0 -91
- package/src/utils/signature.test.ts +0 -210
- package/src/utils/word.test.ts +0 -64
package/dist/multisig.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* for proposal management.
|
|
6
6
|
*/
|
|
7
7
|
import { GuardianHttpClient } from '@openzeppelin/guardian-client';
|
|
8
|
+
import { ProposalSaltMalformedError } from './multisig/authArgErrors.js';
|
|
8
9
|
import { Account, AccountId, AdviceMap, Endpoint, FeltArray, Note, NoteExportFormat, NoteFile, RpcClient, Signature, TransactionRequest, TransactionSummary, Word, } from '@miden-sdk/miden-sdk';
|
|
9
|
-
import { chainAnchorFromBase64, chainAnchorToBase64, executeForSummary, executeForSummaryAt,
|
|
10
|
+
import { chainAnchorFromBase64, chainAnchorToBase64, executeForSummary, executeForSummaryAt, buildUpdateSignersTransactionRequest, buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildConsumeNotesTransactionRequest, buildP2idNoteFromMetadata, buildP2idTransactionRequest, parseP2idNoteType, p2idNoteTypeToMetadata, } from './transaction.js';
|
|
10
11
|
import { buildConsumeNotesTransactionRequestFromNotes } from './transaction/consumeNotes.js';
|
|
11
12
|
import { CONSUME_NOTES_METADATA_VERSION_V2, MAX_CONSUME_NOTES_METADATA_BYTES, } from './types/proposal.js';
|
|
12
13
|
import { LEGACY_CONSUME_NOTES_ENABLED } from './multisig/config.js';
|
|
@@ -20,6 +21,10 @@ import { AccountInspector, assertCompleteDetectedConfig } from './inspector.js';
|
|
|
20
21
|
import { ProposalFactory } from './proposal/factory.js';
|
|
21
22
|
import { ProposalMetadataCodec } from './proposal/metadata.js';
|
|
22
23
|
import { ProposalSignatures } from './proposal/signatures.js';
|
|
24
|
+
import { importNotesFromProposals as importNotesFromProposalsStandalone, throwIfCancelled, } from './recovery/proposalNoteImport.js';
|
|
25
|
+
import { backfillPublicNotesByTag as backfillPublicNotesByTagStandalone, } from './recovery/publicNoteBackfill.js';
|
|
26
|
+
import { drainPrivateNoteBacklog } from './recovery/transportDrain.js';
|
|
27
|
+
import { GUARDIAN_SWITCH_RECOVERY_OPTIONS, runNoteRecovery, } from './recovery/recoverNotes.js';
|
|
23
28
|
import { getRawMidenClient, getTransactionProver, requireMidenRpcEndpoint, } from './raw-client.js';
|
|
24
29
|
import { resolveProverConfig, } from './prover/config.js';
|
|
25
30
|
import { ProverWorkflow } from './prover/workflow.js';
|
|
@@ -67,6 +72,22 @@ function resolveProposalNonce(method, options, legacyArgs = []) {
|
|
|
67
72
|
}
|
|
68
73
|
return options.nonce ?? Date.now();
|
|
69
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Deadline for `Multisig.preservePreSwitchProposalNotes`: no client in the
|
|
77
|
+
* stack applies request deadlines, and a half-dead old GUARDIAN must not
|
|
78
|
+
* stall the switch. Mirror of the Rust SDK's `PRE_SWITCH_IMPORT_TIMEOUT`.
|
|
79
|
+
*/
|
|
80
|
+
const PRE_SWITCH_IMPORT_TIMEOUT_MS = 30_000;
|
|
81
|
+
/** Race sentinel for the pre-switch import timeout. */
|
|
82
|
+
const PRE_SWITCH_IMPORT_TIMED_OUT = Symbol('pre-switch proposal-note import timed out');
|
|
83
|
+
/**
|
|
84
|
+
* How long the switch waits after the timeout for the cancelled flow's one
|
|
85
|
+
* uninterruptible in-flight operation to settle, so it cannot overlap the
|
|
86
|
+
* switch transaction on the shared client.
|
|
87
|
+
*/
|
|
88
|
+
const PRE_SWITCH_SETTLE_GRACE_MS = 5_000;
|
|
89
|
+
/** A `Word` is four field elements: 64 hex digits. Anything longer is not a salt. */
|
|
90
|
+
const MAX_SALT_HEX_DIGITS = 64;
|
|
70
91
|
export class Multisig {
|
|
71
92
|
account;
|
|
72
93
|
threshold;
|
|
@@ -241,6 +262,11 @@ export class Multisig {
|
|
|
241
262
|
/**
|
|
242
263
|
* Update the GUARDIAN client used by this Multisig instance.
|
|
243
264
|
*
|
|
265
|
+
* When repointing to a different GUARDIAN provider after a switch, call
|
|
266
|
+
* {@link preservePreSwitchProposalNotes} first: pending proposals do not
|
|
267
|
+
* survive a switch, and the notes embedded in them can only be imported
|
|
268
|
+
* while the old GUARDIAN is still the current client.
|
|
269
|
+
*
|
|
244
270
|
* @param guardianClient - The new GUARDIAN HTTP client
|
|
245
271
|
*/
|
|
246
272
|
setGuardianClient(guardianClient) {
|
|
@@ -379,6 +405,23 @@ export class Multisig {
|
|
|
379
405
|
throw error;
|
|
380
406
|
}
|
|
381
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Sync the local store with the Miden node, then reload the cached account
|
|
410
|
+
* and multisig config from it (mirrors the Rust `sync_network_only`).
|
|
411
|
+
* Without the refresh, a summary would be built against the freshly synced
|
|
412
|
+
* store while readiness thresholds and signature validation still read the
|
|
413
|
+
* stale cached config. Returns the store-backed account, or `null` when the
|
|
414
|
+
* store has no record for this account (the cached config is then kept, as
|
|
415
|
+
* in `getStoreAccount`); callers that require the account decide how to
|
|
416
|
+
* fail.
|
|
417
|
+
*/
|
|
418
|
+
async syncNetworkOnly() {
|
|
419
|
+
const webClient = await this.getRawClient();
|
|
420
|
+
await retryRpcRead(() => webClient.syncState(), this.rpcConfig);
|
|
421
|
+
const account = await retryRpcRead(() => webClient.getAccount(AccountId.fromHex(this._accountId)), this.rpcConfig);
|
|
422
|
+
this.refreshConfigFromAccount(account ?? null);
|
|
423
|
+
return account ?? null;
|
|
424
|
+
}
|
|
382
425
|
refreshConfigFromAccount(account) {
|
|
383
426
|
if (!account) {
|
|
384
427
|
return;
|
|
@@ -445,6 +488,56 @@ export class Multisig {
|
|
|
445
488
|
}
|
|
446
489
|
return Array.from(this.proposals.values());
|
|
447
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* {@link syncProposals} variant for the recovery flow: per-proposal
|
|
493
|
+
* failures (a payload that does not parse, a metadata binding that does
|
|
494
|
+
* not verify) are isolated as skip reasons instead of failing the whole
|
|
495
|
+
* listing, so one corrupt proposal cannot block recovering notes from the
|
|
496
|
+
* healthy ones. The strict listing stays the signing-path behavior, where
|
|
497
|
+
* a malformed proposal must surface loudly. Proposals at or below the
|
|
498
|
+
* account's committed nonce are dropped (already executed or superseded,
|
|
499
|
+
* matching the Rust SDK's listing), and the shared proposal cache is left
|
|
500
|
+
* untouched. GUARDIAN being unreachable still throws — there is nothing
|
|
501
|
+
* to isolate without a listing.
|
|
502
|
+
*/
|
|
503
|
+
async syncProposalsIsolatingFailures(cancelled) {
|
|
504
|
+
const deltas = await this.guardian.getDeltaProposals(this._accountId);
|
|
505
|
+
const factory = this.proposalFactory();
|
|
506
|
+
let currentNonce;
|
|
507
|
+
try {
|
|
508
|
+
currentNonce = this.account.nonce().asInt();
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
currentNonce = undefined;
|
|
512
|
+
}
|
|
513
|
+
const proposals = [];
|
|
514
|
+
const skipped = [];
|
|
515
|
+
for (let position = 0; position < deltas.length; position += 1) {
|
|
516
|
+
throwIfCancelled(cancelled);
|
|
517
|
+
const delta = deltas[position];
|
|
518
|
+
const identifier = `proposal at nonce ${delta.nonce} (#${position})`;
|
|
519
|
+
try {
|
|
520
|
+
const proposalId = normalizeHexWord(computeCommitmentFromTxSummary(delta.deltaPayload.txSummary.data));
|
|
521
|
+
const existingProposal = this.proposals.get(proposalId);
|
|
522
|
+
const proposal = factory.fromDelta(delta, proposalId, existingProposal?.metadata, existingProposal?.signatures ?? []);
|
|
523
|
+
// Stale-nonce check before the binding verification (matching the
|
|
524
|
+
// Rust listing): the verification re-executes consume proposals in
|
|
525
|
+
// the VM, which is wasted on proposals already executed/superseded.
|
|
526
|
+
if (currentNonce !== undefined && BigInt(proposal.nonce) <= currentNonce) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
await this.verifyProposalMetadataBinding(proposal);
|
|
530
|
+
proposals.push(proposal);
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
skipped.push({
|
|
534
|
+
identifier,
|
|
535
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return { proposals, skipped };
|
|
540
|
+
}
|
|
448
541
|
/**
|
|
449
542
|
* List all known proposals
|
|
450
543
|
*/
|
|
@@ -613,6 +706,19 @@ export class Multisig {
|
|
|
613
706
|
*/
|
|
614
707
|
async createSwitchGuardianProposal(newGuardianEndpoint, newGuardianPubkey, options = {}) {
|
|
615
708
|
const proposalNonce = resolveProposalNonce('createSwitchGuardianProposal', options);
|
|
709
|
+
const { summaryBase64, metadata } = await this.buildSwitchGuardianSummary(newGuardianEndpoint, newGuardianPubkey);
|
|
710
|
+
// SwitchGuardian is a regular delta proposal; push it to GUARDIAN so
|
|
711
|
+
// sign/execute (which fetch from GUARDIAN) can find it. To leave an
|
|
712
|
+
// unreachable GUARDIAN, use createSwitchGuardianProposalOffline instead.
|
|
713
|
+
return this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Shared build step for both switch-GUARDIAN creation paths: verify the new
|
|
717
|
+
* endpoint's `/pubkey` commitment, then execute the update-guardian request
|
|
718
|
+
* for its summary and metadata. Kept in one place so the online and offline
|
|
719
|
+
* proposals for the same operation can never drift apart.
|
|
720
|
+
*/
|
|
721
|
+
async buildSwitchGuardianSummary(newGuardianEndpoint, newGuardianPubkey) {
|
|
616
722
|
const webClient = await this.getRawClient();
|
|
617
723
|
await this.verifyGuardianEndpointCommitment(newGuardianEndpoint, newGuardianPubkey);
|
|
618
724
|
const { request, salt } = await buildUpdateGuardianTransactionRequest(webClient, newGuardianPubkey, { signatureScheme: this.signer.scheme });
|
|
@@ -629,9 +735,56 @@ export class Multisig {
|
|
|
629
735
|
newGuardianEndpoint,
|
|
630
736
|
description: `Switch GUARDIAN to ${newGuardianEndpoint}`,
|
|
631
737
|
};
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
738
|
+
return { summaryBase64, metadata };
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Create a "switch GUARDIAN" proposal fully offline — nothing is pushed to
|
|
742
|
+
* the current GUARDIAN, so an account can leave an unreachable operator
|
|
743
|
+
* (issue #433; mirrors the Rust `create_proposal_offline`).
|
|
744
|
+
*
|
|
745
|
+
* The transaction summary is built and signed locally, the proposal is
|
|
746
|
+
* cached for `signProposalOffline` / `executeProposal`, and the returned
|
|
747
|
+
* `ExportedProposal` (which already includes the proposer's signature) can
|
|
748
|
+
* be `JSON.stringify`-ed and shared with cosigners for `importProposal`.
|
|
749
|
+
*
|
|
750
|
+
* Only switch-GUARDIAN proposals can be created offline: every other
|
|
751
|
+
* proposal type requires a GUARDIAN acknowledgment at execution, so a
|
|
752
|
+
* proposal the GUARDIAN never saw could collect signatures but never
|
|
753
|
+
* execute. The new endpoint must be reachable — its `/pubkey` commitment
|
|
754
|
+
* is verified before anything is built or signed.
|
|
755
|
+
*
|
|
756
|
+
* Note: executeProposal's best-effort canonicalization push cannot reach a
|
|
757
|
+
* proposal the current GUARDIAN never received, so even if that GUARDIAN is
|
|
758
|
+
* back up at execution time it keeps serving the account until background
|
|
759
|
+
* reconciliation (issue #305) — same outcome as executing while it is down.
|
|
760
|
+
*
|
|
761
|
+
* @param newGuardianEndpoint - The new GUARDIAN server endpoint URL
|
|
762
|
+
* @param newGuardianPubkey - The new GUARDIAN server's public key commitment (hex)
|
|
763
|
+
* @param options - Optional settings: `nonce`
|
|
764
|
+
*/
|
|
765
|
+
async createSwitchGuardianProposalOffline(newGuardianEndpoint, newGuardianPubkey, options = {}) {
|
|
766
|
+
const proposalNonce = resolveProposalNonce('createSwitchGuardianProposalOffline', options);
|
|
767
|
+
// Sync with the Miden node and refresh the cached account/config before
|
|
768
|
+
// building (mirrors the Rust `sync_network_only`): with no GUARDIAN push
|
|
769
|
+
// to reject a stale delta at creation, a summary built from stale local
|
|
770
|
+
// state — or a readiness threshold read from stale config — would only
|
|
771
|
+
// fail at execution, after the whole side-channel cosigning ceremony.
|
|
772
|
+
await this.syncNetworkOnly();
|
|
773
|
+
const { summaryBase64, metadata } = await this.buildSwitchGuardianSummary(newGuardianEndpoint, newGuardianPubkey);
|
|
774
|
+
const exported = {
|
|
775
|
+
accountId: this._accountId,
|
|
776
|
+
nonce: proposalNonce,
|
|
777
|
+
commitment: computeCommitmentFromTxSummary(summaryBase64),
|
|
778
|
+
txSummaryBase64: summaryBase64,
|
|
779
|
+
signatures: [],
|
|
780
|
+
metadata,
|
|
781
|
+
};
|
|
782
|
+
// Reuse the cosigner-side machinery end to end: importProposal validates
|
|
783
|
+
// and caches exactly as it would on a cosigner's client, and
|
|
784
|
+
// signProposalOffline adds the proposer's signature and re-exports.
|
|
785
|
+
const proposal = await this.importProposal(JSON.stringify(exported));
|
|
786
|
+
const signedJson = await this.signProposalOffline(proposal.id);
|
|
787
|
+
return JSON.parse(signedJson);
|
|
635
788
|
}
|
|
636
789
|
/**
|
|
637
790
|
* Create a "consume notes" proposal to consume notes sent to the multisig account.
|
|
@@ -862,6 +1015,189 @@ export class Multisig {
|
|
|
862
1015
|
const noteBytes = new Uint8Array(await file.arrayBuffer());
|
|
863
1016
|
return this.importNoteFromBytes(noteBytes);
|
|
864
1017
|
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Proposal-import strategy of {@link recoverNotes}: import the notes
|
|
1020
|
+
* embedded in the given v2 consume-notes proposals into the local Miden
|
|
1021
|
+
* store, reusing this client's Miden RPC endpoint and retry
|
|
1022
|
+
* configuration.
|
|
1023
|
+
*/
|
|
1024
|
+
async importNotesFromProposals(proposals, cancelled) {
|
|
1025
|
+
return importNotesFromProposalsStandalone(this.midenClient, proposals, {
|
|
1026
|
+
midenRpcEndpoint: this.getMidenRpcEndpoint(),
|
|
1027
|
+
rpc: { retry: { maxAttempts: this.rpcConfig.maxAttempts } },
|
|
1028
|
+
cancelled,
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Public-backfill strategy of {@link recoverNotes}: scan a historical
|
|
1033
|
+
* block range for public notes addressed at this account's standard note
|
|
1034
|
+
* tag and import them with their on-chain inclusion proofs, reusing this
|
|
1035
|
+
* client's Miden RPC endpoint and retry configuration.
|
|
1036
|
+
*/
|
|
1037
|
+
async backfillPublicNotesByTag(options = {}) {
|
|
1038
|
+
return backfillPublicNotesByTagStandalone(this.midenClient, {
|
|
1039
|
+
accountId: this._accountId,
|
|
1040
|
+
midenRpcEndpoint: this.getMidenRpcEndpoint(),
|
|
1041
|
+
rpc: { retry: { maxAttempts: this.rpcConfig.maxAttempts } },
|
|
1042
|
+
...(options.fromBlock !== undefined ? { fromBlock: options.fromBlock } : {}),
|
|
1043
|
+
...(options.toBlock !== undefined ? { toBlock: options.toBlock } : {}),
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Run the note-recovery strategies as a single wallet-facing flow,
|
|
1048
|
+
* typically right after key-based recovery loaded the account — the TS
|
|
1049
|
+
* counterpart of the Rust SDK's `MultisigClient::recover_notes`.
|
|
1050
|
+
*
|
|
1051
|
+
* By default every strategy runs — the private-note transport backlog
|
|
1052
|
+
* drain, the proposal-embedded note import, and the historical
|
|
1053
|
+
* public-note backfill over the whole chain — followed by a normal sync
|
|
1054
|
+
* (chain sync plus GUARDIAN state sync) that verifies whatever was
|
|
1055
|
+
* imported. Pass {@link RecoverNotesOptions} to choose strategies, bound
|
|
1056
|
+
* the backfill's block range, or skip the final sync.
|
|
1057
|
+
*
|
|
1058
|
+
* No strategy failure aborts the flow: each primitive already reports
|
|
1059
|
+
* per-note and per-source problems instead of throwing, and a strategy
|
|
1060
|
+
* that cannot run at all (GUARDIAN unreachable while listing proposals,
|
|
1061
|
+
* chain tip unresolvable, a broken local store) becomes a
|
|
1062
|
+
* `RecoveryStepProblem` entry in the report while the remaining
|
|
1063
|
+
* strategies still run. The flow is idempotent — rerunning re-imports
|
|
1064
|
+
* nothing that already arrived — so a report with `retryable: true` can
|
|
1065
|
+
* simply be retried. Throws only for an inverted backfill range.
|
|
1066
|
+
*/
|
|
1067
|
+
async recoverNotes(options = {}) {
|
|
1068
|
+
return runNoteRecovery(options, this.buildNoteRecoverySteps(options));
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* The strategy implementations {@link recoverNotes} hands to
|
|
1072
|
+
* `runNoteRecovery`. The optional `cancelled` token is the pre-switch
|
|
1073
|
+
* import's cooperative cancellation: `runNoteRecovery` checks it before
|
|
1074
|
+
* starting each step, and the proposal-import step threads it into its
|
|
1075
|
+
* listing and import loops so they stop at their next checkpoint too.
|
|
1076
|
+
*/
|
|
1077
|
+
buildNoteRecoverySteps(options, cancelled) {
|
|
1078
|
+
return {
|
|
1079
|
+
transportDrain: () => drainPrivateNoteBacklog(this.midenClient),
|
|
1080
|
+
proposalImport: async () => {
|
|
1081
|
+
// The lenient listing isolates per-proposal parse/binding failures
|
|
1082
|
+
// as skip reasons, so one corrupt proposal cannot block recovering
|
|
1083
|
+
// notes from the healthy ones; those skips surface as `invalid`
|
|
1084
|
+
// outcomes alongside the per-note ones.
|
|
1085
|
+
const { proposals, skipped } = await this.syncProposalsIsolatingFailures(cancelled);
|
|
1086
|
+
const outcomes = skipped.map(({ identifier, reason }) => ({
|
|
1087
|
+
identifier,
|
|
1088
|
+
source: 'proposal',
|
|
1089
|
+
status: 'invalid',
|
|
1090
|
+
reason,
|
|
1091
|
+
}));
|
|
1092
|
+
throwIfCancelled(cancelled);
|
|
1093
|
+
outcomes.push(...(await this.importNotesFromProposals(proposals, cancelled)));
|
|
1094
|
+
return outcomes;
|
|
1095
|
+
},
|
|
1096
|
+
publicBackfill: async () => {
|
|
1097
|
+
// Importing a proof into a store that has never seen the chain
|
|
1098
|
+
// fails, and neither key-based recovery nor `load()` syncs on its
|
|
1099
|
+
// own — so sync the chain state first. Incremental, so cheap when
|
|
1100
|
+
// the store is already synced.
|
|
1101
|
+
try {
|
|
1102
|
+
await this.midenClient.syncChain();
|
|
1103
|
+
}
|
|
1104
|
+
catch (error) {
|
|
1105
|
+
throw new Error(`failed to sync the chain state the backfill imports against: ${error instanceof Error ? error.message : String(error)}`);
|
|
1106
|
+
}
|
|
1107
|
+
return this.backfillPublicNotesByTag({
|
|
1108
|
+
...(options.fromBlock !== undefined ? { fromBlock: options.fromBlock } : {}),
|
|
1109
|
+
...(options.toBlock !== undefined ? { toBlock: options.toBlock } : {}),
|
|
1110
|
+
});
|
|
1111
|
+
},
|
|
1112
|
+
sync: async () => {
|
|
1113
|
+
// Parity with the Rust flow's `sync()`: the transport fetch plus the
|
|
1114
|
+
// chain sync (`MidenClient.sync()` runs both, fail-fast), then the
|
|
1115
|
+
// GUARDIAN state sync.
|
|
1116
|
+
await this.midenClient.sync();
|
|
1117
|
+
await this.syncState();
|
|
1118
|
+
},
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Import the notes embedded in the old GUARDIAN's pending consume-notes
|
|
1123
|
+
* proposals while they are still reachable: pending proposals do not
|
|
1124
|
+
* survive a guardian switch, making them the one recovery source
|
|
1125
|
+
* {@link recoverNotes} loses once the client repoints (issue #417).
|
|
1126
|
+
*
|
|
1127
|
+
* Run automatically by {@link executeProposal} on the switch path, before
|
|
1128
|
+
* the switch transaction executes; call it yourself before repointing a
|
|
1129
|
+
* client by hand via {@link setGuardianClient}. Best-effort by contract:
|
|
1130
|
+
* problems are warned, never thrown, and the flow runs under a timeout
|
|
1131
|
+
* with cooperative cancellation plus a bounded settle grace, so a hung
|
|
1132
|
+
* old GUARDIAN can neither block the switch nor overlap it on the shared
|
|
1133
|
+
* client. Returns the recovery report, or `undefined` when the flow could
|
|
1134
|
+
* not run or timed out. Mirror of the Rust SDK's
|
|
1135
|
+
* `preserve_pre_switch_proposal_notes`; full rationale and semantics in
|
|
1136
|
+
* "Preserving Notes Across a Guardian Switch" (docs/MULTISIG_SDK.md).
|
|
1137
|
+
*/
|
|
1138
|
+
async preservePreSwitchProposalNotes() {
|
|
1139
|
+
let timer;
|
|
1140
|
+
let graceTimer;
|
|
1141
|
+
let timedOut = false;
|
|
1142
|
+
try {
|
|
1143
|
+
const cancelled = () => timedOut;
|
|
1144
|
+
const flow = runNoteRecovery(GUARDIAN_SWITCH_RECOVERY_OPTIONS, this.buildNoteRecoverySteps(GUARDIAN_SWITCH_RECOVERY_OPTIONS, cancelled), cancelled);
|
|
1145
|
+
const outcome = await Promise.race([
|
|
1146
|
+
flow,
|
|
1147
|
+
new Promise((resolve) => {
|
|
1148
|
+
timer = setTimeout(() => {
|
|
1149
|
+
timedOut = true;
|
|
1150
|
+
resolve(PRE_SWITCH_IMPORT_TIMED_OUT);
|
|
1151
|
+
}, PRE_SWITCH_IMPORT_TIMEOUT_MS);
|
|
1152
|
+
}),
|
|
1153
|
+
]);
|
|
1154
|
+
if (outcome === PRE_SWITCH_IMPORT_TIMED_OUT) {
|
|
1155
|
+
console.warn(`Pre-switch proposal-note import timed out after ${PRE_SWITCH_IMPORT_TIMEOUT_MS}ms ` +
|
|
1156
|
+
'and was cancelled; notes embedded in pending proposals may be ' +
|
|
1157
|
+
'unrecoverable after the GUARDIAN switch');
|
|
1158
|
+
// The token stops all new work; give the one uninterruptible
|
|
1159
|
+
// in-flight operation a bounded grace to settle. The result is
|
|
1160
|
+
// unobserved, and a rejection must not go unhandled.
|
|
1161
|
+
await Promise.race([
|
|
1162
|
+
flow.catch(() => { }),
|
|
1163
|
+
new Promise((resolve) => {
|
|
1164
|
+
graceTimer = setTimeout(resolve, PRE_SWITCH_SETTLE_GRACE_MS);
|
|
1165
|
+
}),
|
|
1166
|
+
]);
|
|
1167
|
+
return undefined;
|
|
1168
|
+
}
|
|
1169
|
+
for (const problem of outcome.problems) {
|
|
1170
|
+
console.warn(`Pre-switch proposal-note import step '${problem.step}' did not finish; ` +
|
|
1171
|
+
'notes embedded in pending proposals may be unrecoverable after the ' +
|
|
1172
|
+
'GUARDIAN switch', problem.reason);
|
|
1173
|
+
}
|
|
1174
|
+
// Per-note failures never reach `problems` — and this is the last
|
|
1175
|
+
// moment the notes are reachable, so "retryable" cannot help: the
|
|
1176
|
+
// source is gone once the client repoints. They must be observable now.
|
|
1177
|
+
for (const noteOutcome of outcome.proposalImport ?? []) {
|
|
1178
|
+
if (noteOutcome.status === 'invalid' || noteOutcome.status === 'failed') {
|
|
1179
|
+
console.warn(`Pre-switch import could not preserve embedded note ${noteOutcome.identifier} ` +
|
|
1180
|
+
`(${noteOutcome.status}); it may be unrecoverable after the GUARDIAN switch`, noteOutcome.reason);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return outcome;
|
|
1184
|
+
}
|
|
1185
|
+
catch (error) {
|
|
1186
|
+
// runNoteRecovery only throws for caller errors (inverted backfill
|
|
1187
|
+
// range, never passed here), but the switch must survive anything.
|
|
1188
|
+
console.warn('Pre-switch proposal-note import could not run; notes embedded in pending ' +
|
|
1189
|
+
'proposals may be unrecoverable after the GUARDIAN switch', error);
|
|
1190
|
+
return undefined;
|
|
1191
|
+
}
|
|
1192
|
+
finally {
|
|
1193
|
+
if (timer !== undefined) {
|
|
1194
|
+
clearTimeout(timer);
|
|
1195
|
+
}
|
|
1196
|
+
if (graceTimer !== undefined) {
|
|
1197
|
+
clearTimeout(graceTimer);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
865
1201
|
/**
|
|
866
1202
|
* Compute the ID of the note a P2ID proposal will create when executed.
|
|
867
1203
|
*
|
|
@@ -881,6 +1217,11 @@ export class Multisig {
|
|
|
881
1217
|
!metadata.saltHex) {
|
|
882
1218
|
throw new Error('getP2idNoteId requires a P2ID proposal with recipient, faucet, amount, and salt metadata');
|
|
883
1219
|
}
|
|
1220
|
+
// Validate before deriving. Unlike the rebuild paths there is no commitment check
|
|
1221
|
+
// downstream of this note id -- it goes straight out to a recipient — so a salt that
|
|
1222
|
+
// silently padded to the zero word would produce a plausible id for a note that does
|
|
1223
|
+
// not exist, with nothing to catch it.
|
|
1224
|
+
this.requireProposalSaltHex(proposal.id, metadata);
|
|
884
1225
|
const note = buildP2idNoteFromMetadata(this._accountId, metadata.recipientId, metadata.faucetId, BigInt(metadata.amount), parseP2idNoteType(metadata.noteType), metadata.saltHex, { reclaimHeight: metadata.reclaimHeight, timelockHeight: metadata.timelockHeight });
|
|
885
1226
|
return note.id().toString();
|
|
886
1227
|
}
|
|
@@ -974,6 +1315,12 @@ export class Multisig {
|
|
|
974
1315
|
*/
|
|
975
1316
|
async executeProposal(proposalId) {
|
|
976
1317
|
const { metadata, finalRequest, proposal } = await this.prepareProposalExecution(proposalId);
|
|
1318
|
+
if (metadata.proposalType === 'switch_guardian') {
|
|
1319
|
+
// #417: import notes embedded in pending proposals from the old
|
|
1320
|
+
// GUARDIAN. Must run before the switch executes and repoints;
|
|
1321
|
+
// best-effort and bounded — see preservePreSwitchProposalNotes.
|
|
1322
|
+
await this.preservePreSwitchProposalNotes();
|
|
1323
|
+
}
|
|
977
1324
|
// Execute at the proposal's anchored reference block, so the summary the
|
|
978
1325
|
// cosigners signed reproduces exactly. The anchor was already checked
|
|
979
1326
|
// against the summary's block commitment during binding verification.
|
|
@@ -1009,9 +1356,7 @@ export class Multisig {
|
|
|
1009
1356
|
'will keep serving this account until reconciliation', error);
|
|
1010
1357
|
}
|
|
1011
1358
|
try {
|
|
1012
|
-
const
|
|
1013
|
-
await retryRpcRead(() => webClient.syncState(), this.rpcConfig);
|
|
1014
|
-
const updatedAccount = await retryRpcRead(() => webClient.getAccount(accountId), this.rpcConfig);
|
|
1359
|
+
const updatedAccount = await this.syncNetworkOnly();
|
|
1015
1360
|
if (!updatedAccount) {
|
|
1016
1361
|
throw new Error(`Updated account ${this._accountId} is missing from local client`);
|
|
1017
1362
|
}
|
|
@@ -1243,9 +1588,19 @@ export class Multisig {
|
|
|
1243
1588
|
}
|
|
1244
1589
|
const txSummaryBytes = base64ToUint8Array(txSummaryBase64);
|
|
1245
1590
|
const txSummary = TransactionSummary.deserialize(txSummaryBytes);
|
|
1246
|
-
const saltHex =
|
|
1591
|
+
const saltHex = this.requireProposalSaltHex(proposalId, metadata);
|
|
1247
1592
|
const txCommitmentHex = txSummary.toCommitment().toHex();
|
|
1248
1593
|
const normalizedTxCommitmentHex = normalizeHexWord(txCommitmentHex);
|
|
1594
|
+
// This summary was re-fetched from GUARDIAN, not taken from the verified proposal:
|
|
1595
|
+
// `ensureProposalCommitmentMatchesSummary` pins the CACHED `proposal.txSummary` to the
|
|
1596
|
+
// id, and nothing pinned this one. It goes on to key the advice map and drive the
|
|
1597
|
+
// rebuild, so an unrelated summary served here would have signatures collected against
|
|
1598
|
+
// one transaction and advice assembled for another. On an ECDSA roster the
|
|
1599
|
+
// recoverability check would notice; on a Falcon roster nothing else compares them.
|
|
1600
|
+
if (normalizedTxCommitmentHex !== normalizeHexWord(proposalId)) {
|
|
1601
|
+
throw new Error(`Proposal ${proposalId} tx_summary commitment ${normalizedTxCommitmentHex} ` +
|
|
1602
|
+
'does not match the proposal id it belongs to');
|
|
1603
|
+
}
|
|
1249
1604
|
const normalizedSignerCommitments = new Set(this.signerCommitments.map((commitment) => normalizeHexWord(commitment)));
|
|
1250
1605
|
const adviceMap = new AdviceMap();
|
|
1251
1606
|
const adviceMapKeys = new Set();
|
|
@@ -1319,8 +1674,16 @@ export class Multisig {
|
|
|
1319
1674
|
if (metadata.proposalType === 'switch_guardian') {
|
|
1320
1675
|
await this.verifyGuardianEndpointCommitment(metadata.newGuardianEndpoint, metadata.newGuardianPubkey);
|
|
1321
1676
|
}
|
|
1677
|
+
// The builders read `.toHex()` and allocate their own Word, so this handle stays
|
|
1678
|
+
// ours; without the release it leaks once per execute.
|
|
1322
1679
|
const executionSalt = Word.fromHex(normalizeHexWord(saltHex));
|
|
1323
|
-
|
|
1680
|
+
let finalRequest;
|
|
1681
|
+
try {
|
|
1682
|
+
finalRequest = await this.buildTransactionRequestFromMetadata(metadata, executionSalt, adviceMap);
|
|
1683
|
+
}
|
|
1684
|
+
finally {
|
|
1685
|
+
executionSalt.free?.();
|
|
1686
|
+
}
|
|
1324
1687
|
return { finalRequest, metadata, proposal };
|
|
1325
1688
|
}
|
|
1326
1689
|
/**
|
|
@@ -1475,9 +1838,7 @@ export class Multisig {
|
|
|
1475
1838
|
// guardian endpoint commitment provide the binding checks for this type.
|
|
1476
1839
|
return txSummaryCommitment;
|
|
1477
1840
|
}
|
|
1478
|
-
const salt = proposal.metadata
|
|
1479
|
-
? Word.fromHex(normalizeHexWord(proposal.metadata.saltHex))
|
|
1480
|
-
: summarySalt(summary);
|
|
1841
|
+
const salt = Word.fromHex(normalizeHexWord(this.requireProposalSaltHex(proposal.id, proposal.metadata)));
|
|
1481
1842
|
const request = await this.buildTransactionRequestFromMetadata(proposal.metadata, salt);
|
|
1482
1843
|
const webClient = await this.getRawClient();
|
|
1483
1844
|
const reconstructed = await executeForSummaryAt(webClient, this._accountId, request, anchor);
|
|
@@ -1497,6 +1858,52 @@ export class Multisig {
|
|
|
1497
1858
|
* cannot be reproduced, verified, or executed. The caller owns the returned
|
|
1498
1859
|
* anchor and must `free()` it once done.
|
|
1499
1860
|
*/
|
|
1861
|
+
/**
|
|
1862
|
+
* Reads a proposal's salt. Throws when absent, because there is nothing to fall
|
|
1863
|
+
* back to.
|
|
1864
|
+
*
|
|
1865
|
+
* The request declares this salt through `withFeeConversionSalt`, and miden-client
|
|
1866
|
+
* commits `hash(CONVERSION_INFO || SALT)` into the auth arg from it. The summary
|
|
1867
|
+
* therefore carries the COMMITMENT, and a commitment is not invertible to the salt
|
|
1868
|
+
* it was built from -- so `summaryAuthArg(summary)` cannot stand in here. It used
|
|
1869
|
+
* to: before the request declared a salt the auth arg WAS the bare salt, which is
|
|
1870
|
+
* why the fallback this replaces was correct when it was written.
|
|
1871
|
+
*
|
|
1872
|
+
* A declared salt also bypasses miden-client's zero-fee early return, so this holds
|
|
1873
|
+
* on a chain that charges nothing exactly as on one that charges.
|
|
1874
|
+
*/
|
|
1875
|
+
requireProposalSaltHex(proposalId, metadata) {
|
|
1876
|
+
const saltHex = metadata.saltHex;
|
|
1877
|
+
if (saltHex === undefined || saltHex === null || saltHex === '') {
|
|
1878
|
+
throw new Error(`Proposal ${proposalId} has no salt; its request cannot be rebuilt because ` +
|
|
1879
|
+
'the auth arg commits hash(CONVERSION_INFO || SALT) and is not invertible ' +
|
|
1880
|
+
'to the salt');
|
|
1881
|
+
}
|
|
1882
|
+
// GUARDIAN serves this field and the response is cast, not parsed, so everything
|
|
1883
|
+
// below is untrusted input. A truthiness test is not enough: `normalizeHexWord`
|
|
1884
|
+
// left-pads, so `'0x'` and `'0X'` are truthy and pad to the ZERO word -- a salt
|
|
1885
|
+
// nobody chose, which rebuilds a different request and reports itself as a summary
|
|
1886
|
+
// mismatch. A non-string throws out of the hex helpers instead, and an unbounded
|
|
1887
|
+
// string is a logging hazard the error type already guards against.
|
|
1888
|
+
if (typeof saltHex !== 'string') {
|
|
1889
|
+
throw new ProposalSaltMalformedError({
|
|
1890
|
+
proposalId,
|
|
1891
|
+
saltHex,
|
|
1892
|
+
reason: `expected a hex string, got ${typeof saltHex}`,
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
const digits = saltHex.replace(/^0[xX]/, '');
|
|
1896
|
+
if (digits.length === 0 || digits.length > MAX_SALT_HEX_DIGITS || !/^[0-9a-fA-F]+$/.test(digits)) {
|
|
1897
|
+
throw new ProposalSaltMalformedError({
|
|
1898
|
+
proposalId,
|
|
1899
|
+
saltHex,
|
|
1900
|
+
reason: digits.length === 0
|
|
1901
|
+
? 'a hex prefix with no digits is the zero word, not a salt'
|
|
1902
|
+
: `expected 1 to ${MAX_SALT_HEX_DIGITS} hex digits`,
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
return saltHex;
|
|
1906
|
+
}
|
|
1500
1907
|
requireProposalAnchor(proposalId, metadata) {
|
|
1501
1908
|
if (!metadata.chainAnchor) {
|
|
1502
1909
|
throw new Error(`Proposal ${proposalId} has no chain anchor; it was created without ` +
|