@openzeppelin/miden-multisig-client 0.17.0-rc.2 → 0.17.0
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 +172 -47
- 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/src/multisig.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
ProposalSignatureEntry,
|
|
17
17
|
ProposalType,
|
|
18
18
|
} from './types.js';
|
|
19
|
+
import { ProposalSaltMalformedError } from './multisig/authArgErrors.js';
|
|
19
20
|
import type { ProcedureName } from './procedures.js';
|
|
20
21
|
import type {
|
|
21
22
|
MidenClient,
|
|
@@ -43,7 +44,6 @@ import {
|
|
|
43
44
|
chainAnchorToBase64,
|
|
44
45
|
executeForSummary,
|
|
45
46
|
executeForSummaryAt,
|
|
46
|
-
summarySalt,
|
|
47
47
|
buildUpdateSignersTransactionRequest,
|
|
48
48
|
buildUpdateProcedureThresholdTransactionRequest,
|
|
49
49
|
buildUpdateGuardianTransactionRequest,
|
|
@@ -85,6 +85,23 @@ import { AccountInspector, assertCompleteDetectedConfig } from './inspector.js';
|
|
|
85
85
|
import { ProposalFactory } from './proposal/factory.js';
|
|
86
86
|
import { ProposalMetadataCodec } from './proposal/metadata.js';
|
|
87
87
|
import { ProposalSignatures } from './proposal/signatures.js';
|
|
88
|
+
import {
|
|
89
|
+
importNotesFromProposals as importNotesFromProposalsStandalone,
|
|
90
|
+
throwIfCancelled,
|
|
91
|
+
type NoteImportOutcome,
|
|
92
|
+
} from './recovery/proposalNoteImport.js';
|
|
93
|
+
import {
|
|
94
|
+
backfillPublicNotesByTag as backfillPublicNotesByTagStandalone,
|
|
95
|
+
type PublicBackfillReport,
|
|
96
|
+
} from './recovery/publicNoteBackfill.js';
|
|
97
|
+
import { drainPrivateNoteBacklog } from './recovery/transportDrain.js';
|
|
98
|
+
import {
|
|
99
|
+
GUARDIAN_SWITCH_RECOVERY_OPTIONS,
|
|
100
|
+
runNoteRecovery,
|
|
101
|
+
type NoteRecoveryReport,
|
|
102
|
+
type NoteRecoverySteps,
|
|
103
|
+
type RecoverNotesOptions,
|
|
104
|
+
} from './recovery/recoverNotes.js';
|
|
88
105
|
import {
|
|
89
106
|
getRawMidenClient,
|
|
90
107
|
getTransactionProver,
|
|
@@ -194,6 +211,26 @@ function resolveProposalNonce(
|
|
|
194
211
|
return options.nonce ?? Date.now();
|
|
195
212
|
}
|
|
196
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Deadline for `Multisig.preservePreSwitchProposalNotes`: no client in the
|
|
216
|
+
* stack applies request deadlines, and a half-dead old GUARDIAN must not
|
|
217
|
+
* stall the switch. Mirror of the Rust SDK's `PRE_SWITCH_IMPORT_TIMEOUT`.
|
|
218
|
+
*/
|
|
219
|
+
const PRE_SWITCH_IMPORT_TIMEOUT_MS = 30_000;
|
|
220
|
+
|
|
221
|
+
/** Race sentinel for the pre-switch import timeout. */
|
|
222
|
+
const PRE_SWITCH_IMPORT_TIMED_OUT = Symbol('pre-switch proposal-note import timed out');
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* How long the switch waits after the timeout for the cancelled flow's one
|
|
226
|
+
* uninterruptible in-flight operation to settle, so it cannot overlap the
|
|
227
|
+
* switch transaction on the shared client.
|
|
228
|
+
*/
|
|
229
|
+
const PRE_SWITCH_SETTLE_GRACE_MS = 5_000;
|
|
230
|
+
|
|
231
|
+
/** A `Word` is four field elements: 64 hex digits. Anything longer is not a salt. */
|
|
232
|
+
const MAX_SALT_HEX_DIGITS = 64;
|
|
233
|
+
|
|
197
234
|
export class Multisig {
|
|
198
235
|
account: Account;
|
|
199
236
|
threshold: number;
|
|
@@ -412,6 +449,11 @@ export class Multisig {
|
|
|
412
449
|
/**
|
|
413
450
|
* Update the GUARDIAN client used by this Multisig instance.
|
|
414
451
|
*
|
|
452
|
+
* When repointing to a different GUARDIAN provider after a switch, call
|
|
453
|
+
* {@link preservePreSwitchProposalNotes} first: pending proposals do not
|
|
454
|
+
* survive a switch, and the notes embedded in them can only be imported
|
|
455
|
+
* while the old GUARDIAN is still the current client.
|
|
456
|
+
*
|
|
415
457
|
* @param guardianClient - The new GUARDIAN HTTP client
|
|
416
458
|
*/
|
|
417
459
|
setGuardianClient(guardianClient: GuardianHttpClient): void {
|
|
@@ -593,6 +635,27 @@ export class Multisig {
|
|
|
593
635
|
}
|
|
594
636
|
}
|
|
595
637
|
|
|
638
|
+
/**
|
|
639
|
+
* Sync the local store with the Miden node, then reload the cached account
|
|
640
|
+
* and multisig config from it (mirrors the Rust `sync_network_only`).
|
|
641
|
+
* Without the refresh, a summary would be built against the freshly synced
|
|
642
|
+
* store while readiness thresholds and signature validation still read the
|
|
643
|
+
* stale cached config. Returns the store-backed account, or `null` when the
|
|
644
|
+
* store has no record for this account (the cached config is then kept, as
|
|
645
|
+
* in `getStoreAccount`); callers that require the account decide how to
|
|
646
|
+
* fail.
|
|
647
|
+
*/
|
|
648
|
+
private async syncNetworkOnly(): Promise<Account | null> {
|
|
649
|
+
const webClient = await this.getRawClient();
|
|
650
|
+
await retryRpcRead(() => webClient.syncState(), this.rpcConfig);
|
|
651
|
+
const account = await retryRpcRead(
|
|
652
|
+
() => webClient.getAccount(AccountId.fromHex(this._accountId)),
|
|
653
|
+
this.rpcConfig,
|
|
654
|
+
);
|
|
655
|
+
this.refreshConfigFromAccount(account ?? null);
|
|
656
|
+
return account ?? null;
|
|
657
|
+
}
|
|
658
|
+
|
|
596
659
|
private refreshConfigFromAccount(account: Account | null): void {
|
|
597
660
|
if (!account) {
|
|
598
661
|
return;
|
|
@@ -677,6 +740,67 @@ export class Multisig {
|
|
|
677
740
|
return Array.from(this.proposals.values());
|
|
678
741
|
}
|
|
679
742
|
|
|
743
|
+
/**
|
|
744
|
+
* {@link syncProposals} variant for the recovery flow: per-proposal
|
|
745
|
+
* failures (a payload that does not parse, a metadata binding that does
|
|
746
|
+
* not verify) are isolated as skip reasons instead of failing the whole
|
|
747
|
+
* listing, so one corrupt proposal cannot block recovering notes from the
|
|
748
|
+
* healthy ones. The strict listing stays the signing-path behavior, where
|
|
749
|
+
* a malformed proposal must surface loudly. Proposals at or below the
|
|
750
|
+
* account's committed nonce are dropped (already executed or superseded,
|
|
751
|
+
* matching the Rust SDK's listing), and the shared proposal cache is left
|
|
752
|
+
* untouched. GUARDIAN being unreachable still throws — there is nothing
|
|
753
|
+
* to isolate without a listing.
|
|
754
|
+
*/
|
|
755
|
+
private async syncProposalsIsolatingFailures(cancelled?: () => boolean): Promise<{
|
|
756
|
+
proposals: Proposal[];
|
|
757
|
+
skipped: Array<{ identifier: string; reason: string }>;
|
|
758
|
+
}> {
|
|
759
|
+
const deltas = await this.guardian.getDeltaProposals(this._accountId);
|
|
760
|
+
const factory = this.proposalFactory();
|
|
761
|
+
|
|
762
|
+
let currentNonce: bigint | undefined;
|
|
763
|
+
try {
|
|
764
|
+
currentNonce = this.account.nonce().asInt();
|
|
765
|
+
} catch {
|
|
766
|
+
currentNonce = undefined;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const proposals: Proposal[] = [];
|
|
770
|
+
const skipped: Array<{ identifier: string; reason: string }> = [];
|
|
771
|
+
for (let position = 0; position < deltas.length; position += 1) {
|
|
772
|
+
throwIfCancelled(cancelled);
|
|
773
|
+
const delta = deltas[position];
|
|
774
|
+
const identifier = `proposal at nonce ${delta.nonce} (#${position})`;
|
|
775
|
+
try {
|
|
776
|
+
const proposalId = normalizeHexWord(
|
|
777
|
+
computeCommitmentFromTxSummary(delta.deltaPayload.txSummary.data)
|
|
778
|
+
);
|
|
779
|
+
const existingProposal = this.proposals.get(proposalId);
|
|
780
|
+
const proposal = factory.fromDelta(
|
|
781
|
+
delta,
|
|
782
|
+
proposalId,
|
|
783
|
+
existingProposal?.metadata,
|
|
784
|
+
existingProposal?.signatures ?? [],
|
|
785
|
+
);
|
|
786
|
+
// Stale-nonce check before the binding verification (matching the
|
|
787
|
+
// Rust listing): the verification re-executes consume proposals in
|
|
788
|
+
// the VM, which is wasted on proposals already executed/superseded.
|
|
789
|
+
if (currentNonce !== undefined && BigInt(proposal.nonce) <= currentNonce) {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
await this.verifyProposalMetadataBinding(proposal);
|
|
793
|
+
proposals.push(proposal);
|
|
794
|
+
} catch (error) {
|
|
795
|
+
skipped.push({
|
|
796
|
+
identifier,
|
|
797
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return { proposals, skipped };
|
|
802
|
+
}
|
|
803
|
+
|
|
680
804
|
/**
|
|
681
805
|
* List all known proposals
|
|
682
806
|
*/
|
|
@@ -925,6 +1049,27 @@ export class Multisig {
|
|
|
925
1049
|
options: CreateProposalOptions = {},
|
|
926
1050
|
): Promise<Proposal> {
|
|
927
1051
|
const proposalNonce = resolveProposalNonce('createSwitchGuardianProposal', options);
|
|
1052
|
+
const { summaryBase64, metadata } = await this.buildSwitchGuardianSummary(
|
|
1053
|
+
newGuardianEndpoint,
|
|
1054
|
+
newGuardianPubkey,
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
// SwitchGuardian is a regular delta proposal; push it to GUARDIAN so
|
|
1058
|
+
// sign/execute (which fetch from GUARDIAN) can find it. To leave an
|
|
1059
|
+
// unreachable GUARDIAN, use createSwitchGuardianProposalOffline instead.
|
|
1060
|
+
return this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Shared build step for both switch-GUARDIAN creation paths: verify the new
|
|
1065
|
+
* endpoint's `/pubkey` commitment, then execute the update-guardian request
|
|
1066
|
+
* for its summary and metadata. Kept in one place so the online and offline
|
|
1067
|
+
* proposals for the same operation can never drift apart.
|
|
1068
|
+
*/
|
|
1069
|
+
private async buildSwitchGuardianSummary(
|
|
1070
|
+
newGuardianEndpoint: string,
|
|
1071
|
+
newGuardianPubkey: string,
|
|
1072
|
+
): Promise<{ summaryBase64: string; metadata: ProposalMetadata }> {
|
|
928
1073
|
const webClient = await this.getRawClient();
|
|
929
1074
|
await this.verifyGuardianEndpointCommitment(newGuardianEndpoint, newGuardianPubkey);
|
|
930
1075
|
|
|
@@ -949,9 +1094,68 @@ export class Multisig {
|
|
|
949
1094
|
description: `Switch GUARDIAN to ${newGuardianEndpoint}`,
|
|
950
1095
|
};
|
|
951
1096
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1097
|
+
return { summaryBase64, metadata };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Create a "switch GUARDIAN" proposal fully offline — nothing is pushed to
|
|
1102
|
+
* the current GUARDIAN, so an account can leave an unreachable operator
|
|
1103
|
+
* (issue #433; mirrors the Rust `create_proposal_offline`).
|
|
1104
|
+
*
|
|
1105
|
+
* The transaction summary is built and signed locally, the proposal is
|
|
1106
|
+
* cached for `signProposalOffline` / `executeProposal`, and the returned
|
|
1107
|
+
* `ExportedProposal` (which already includes the proposer's signature) can
|
|
1108
|
+
* be `JSON.stringify`-ed and shared with cosigners for `importProposal`.
|
|
1109
|
+
*
|
|
1110
|
+
* Only switch-GUARDIAN proposals can be created offline: every other
|
|
1111
|
+
* proposal type requires a GUARDIAN acknowledgment at execution, so a
|
|
1112
|
+
* proposal the GUARDIAN never saw could collect signatures but never
|
|
1113
|
+
* execute. The new endpoint must be reachable — its `/pubkey` commitment
|
|
1114
|
+
* is verified before anything is built or signed.
|
|
1115
|
+
*
|
|
1116
|
+
* Note: executeProposal's best-effort canonicalization push cannot reach a
|
|
1117
|
+
* proposal the current GUARDIAN never received, so even if that GUARDIAN is
|
|
1118
|
+
* back up at execution time it keeps serving the account until background
|
|
1119
|
+
* reconciliation (issue #305) — same outcome as executing while it is down.
|
|
1120
|
+
*
|
|
1121
|
+
* @param newGuardianEndpoint - The new GUARDIAN server endpoint URL
|
|
1122
|
+
* @param newGuardianPubkey - The new GUARDIAN server's public key commitment (hex)
|
|
1123
|
+
* @param options - Optional settings: `nonce`
|
|
1124
|
+
*/
|
|
1125
|
+
async createSwitchGuardianProposalOffline(
|
|
1126
|
+
newGuardianEndpoint: string,
|
|
1127
|
+
newGuardianPubkey: string,
|
|
1128
|
+
options: CreateProposalOptions = {},
|
|
1129
|
+
): Promise<ExportedProposal> {
|
|
1130
|
+
const proposalNonce = resolveProposalNonce('createSwitchGuardianProposalOffline', options);
|
|
1131
|
+
|
|
1132
|
+
// Sync with the Miden node and refresh the cached account/config before
|
|
1133
|
+
// building (mirrors the Rust `sync_network_only`): with no GUARDIAN push
|
|
1134
|
+
// to reject a stale delta at creation, a summary built from stale local
|
|
1135
|
+
// state — or a readiness threshold read from stale config — would only
|
|
1136
|
+
// fail at execution, after the whole side-channel cosigning ceremony.
|
|
1137
|
+
await this.syncNetworkOnly();
|
|
1138
|
+
|
|
1139
|
+
const { summaryBase64, metadata } = await this.buildSwitchGuardianSummary(
|
|
1140
|
+
newGuardianEndpoint,
|
|
1141
|
+
newGuardianPubkey,
|
|
1142
|
+
);
|
|
1143
|
+
|
|
1144
|
+
const exported: ExportedProposal = {
|
|
1145
|
+
accountId: this._accountId,
|
|
1146
|
+
nonce: proposalNonce,
|
|
1147
|
+
commitment: computeCommitmentFromTxSummary(summaryBase64),
|
|
1148
|
+
txSummaryBase64: summaryBase64,
|
|
1149
|
+
signatures: [],
|
|
1150
|
+
metadata,
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
// Reuse the cosigner-side machinery end to end: importProposal validates
|
|
1154
|
+
// and caches exactly as it would on a cosigner's client, and
|
|
1155
|
+
// signProposalOffline adds the proposer's signature and re-exports.
|
|
1156
|
+
const proposal = await this.importProposal(JSON.stringify(exported));
|
|
1157
|
+
const signedJson = await this.signProposalOffline(proposal.id);
|
|
1158
|
+
return JSON.parse(signedJson) as ExportedProposal;
|
|
955
1159
|
}
|
|
956
1160
|
|
|
957
1161
|
/**
|
|
@@ -1231,6 +1435,218 @@ export class Multisig {
|
|
|
1231
1435
|
return this.importNoteFromBytes(noteBytes);
|
|
1232
1436
|
}
|
|
1233
1437
|
|
|
1438
|
+
/**
|
|
1439
|
+
* Proposal-import strategy of {@link recoverNotes}: import the notes
|
|
1440
|
+
* embedded in the given v2 consume-notes proposals into the local Miden
|
|
1441
|
+
* store, reusing this client's Miden RPC endpoint and retry
|
|
1442
|
+
* configuration.
|
|
1443
|
+
*/
|
|
1444
|
+
private async importNotesFromProposals(
|
|
1445
|
+
proposals: ReadonlyArray<Pick<Proposal, 'id' | 'metadata'>>,
|
|
1446
|
+
cancelled?: () => boolean,
|
|
1447
|
+
): Promise<NoteImportOutcome[]> {
|
|
1448
|
+
return importNotesFromProposalsStandalone(this.midenClient, proposals, {
|
|
1449
|
+
midenRpcEndpoint: this.getMidenRpcEndpoint(),
|
|
1450
|
+
rpc: { retry: { maxAttempts: this.rpcConfig.maxAttempts } },
|
|
1451
|
+
cancelled,
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* Public-backfill strategy of {@link recoverNotes}: scan a historical
|
|
1457
|
+
* block range for public notes addressed at this account's standard note
|
|
1458
|
+
* tag and import them with their on-chain inclusion proofs, reusing this
|
|
1459
|
+
* client's Miden RPC endpoint and retry configuration.
|
|
1460
|
+
*/
|
|
1461
|
+
private async backfillPublicNotesByTag(
|
|
1462
|
+
options: { fromBlock?: number; toBlock?: number } = {},
|
|
1463
|
+
): Promise<PublicBackfillReport> {
|
|
1464
|
+
return backfillPublicNotesByTagStandalone(this.midenClient, {
|
|
1465
|
+
accountId: this._accountId,
|
|
1466
|
+
midenRpcEndpoint: this.getMidenRpcEndpoint(),
|
|
1467
|
+
rpc: { retry: { maxAttempts: this.rpcConfig.maxAttempts } },
|
|
1468
|
+
...(options.fromBlock !== undefined ? { fromBlock: options.fromBlock } : {}),
|
|
1469
|
+
...(options.toBlock !== undefined ? { toBlock: options.toBlock } : {}),
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* Run the note-recovery strategies as a single wallet-facing flow,
|
|
1475
|
+
* typically right after key-based recovery loaded the account — the TS
|
|
1476
|
+
* counterpart of the Rust SDK's `MultisigClient::recover_notes`.
|
|
1477
|
+
*
|
|
1478
|
+
* By default every strategy runs — the private-note transport backlog
|
|
1479
|
+
* drain, the proposal-embedded note import, and the historical
|
|
1480
|
+
* public-note backfill over the whole chain — followed by a normal sync
|
|
1481
|
+
* (chain sync plus GUARDIAN state sync) that verifies whatever was
|
|
1482
|
+
* imported. Pass {@link RecoverNotesOptions} to choose strategies, bound
|
|
1483
|
+
* the backfill's block range, or skip the final sync.
|
|
1484
|
+
*
|
|
1485
|
+
* No strategy failure aborts the flow: each primitive already reports
|
|
1486
|
+
* per-note and per-source problems instead of throwing, and a strategy
|
|
1487
|
+
* that cannot run at all (GUARDIAN unreachable while listing proposals,
|
|
1488
|
+
* chain tip unresolvable, a broken local store) becomes a
|
|
1489
|
+
* `RecoveryStepProblem` entry in the report while the remaining
|
|
1490
|
+
* strategies still run. The flow is idempotent — rerunning re-imports
|
|
1491
|
+
* nothing that already arrived — so a report with `retryable: true` can
|
|
1492
|
+
* simply be retried. Throws only for an inverted backfill range.
|
|
1493
|
+
*/
|
|
1494
|
+
async recoverNotes(options: RecoverNotesOptions = {}): Promise<NoteRecoveryReport> {
|
|
1495
|
+
return runNoteRecovery(options, this.buildNoteRecoverySteps(options));
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* The strategy implementations {@link recoverNotes} hands to
|
|
1500
|
+
* `runNoteRecovery`. The optional `cancelled` token is the pre-switch
|
|
1501
|
+
* import's cooperative cancellation: `runNoteRecovery` checks it before
|
|
1502
|
+
* starting each step, and the proposal-import step threads it into its
|
|
1503
|
+
* listing and import loops so they stop at their next checkpoint too.
|
|
1504
|
+
*/
|
|
1505
|
+
private buildNoteRecoverySteps(
|
|
1506
|
+
options: RecoverNotesOptions,
|
|
1507
|
+
cancelled?: () => boolean,
|
|
1508
|
+
): NoteRecoverySteps {
|
|
1509
|
+
return {
|
|
1510
|
+
transportDrain: () => drainPrivateNoteBacklog(this.midenClient),
|
|
1511
|
+
proposalImport: async () => {
|
|
1512
|
+
// The lenient listing isolates per-proposal parse/binding failures
|
|
1513
|
+
// as skip reasons, so one corrupt proposal cannot block recovering
|
|
1514
|
+
// notes from the healthy ones; those skips surface as `invalid`
|
|
1515
|
+
// outcomes alongside the per-note ones.
|
|
1516
|
+
const { proposals, skipped } = await this.syncProposalsIsolatingFailures(cancelled);
|
|
1517
|
+
const outcomes: NoteImportOutcome[] = skipped.map(({ identifier, reason }) => ({
|
|
1518
|
+
identifier,
|
|
1519
|
+
source: 'proposal',
|
|
1520
|
+
status: 'invalid',
|
|
1521
|
+
reason,
|
|
1522
|
+
}));
|
|
1523
|
+
throwIfCancelled(cancelled);
|
|
1524
|
+
outcomes.push(...(await this.importNotesFromProposals(proposals, cancelled)));
|
|
1525
|
+
return outcomes;
|
|
1526
|
+
},
|
|
1527
|
+
publicBackfill: async () => {
|
|
1528
|
+
// Importing a proof into a store that has never seen the chain
|
|
1529
|
+
// fails, and neither key-based recovery nor `load()` syncs on its
|
|
1530
|
+
// own — so sync the chain state first. Incremental, so cheap when
|
|
1531
|
+
// the store is already synced.
|
|
1532
|
+
try {
|
|
1533
|
+
await this.midenClient.syncChain();
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
throw new Error(
|
|
1536
|
+
`failed to sync the chain state the backfill imports against: ${
|
|
1537
|
+
error instanceof Error ? error.message : String(error)
|
|
1538
|
+
}`,
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
return this.backfillPublicNotesByTag({
|
|
1542
|
+
...(options.fromBlock !== undefined ? { fromBlock: options.fromBlock } : {}),
|
|
1543
|
+
...(options.toBlock !== undefined ? { toBlock: options.toBlock } : {}),
|
|
1544
|
+
});
|
|
1545
|
+
},
|
|
1546
|
+
sync: async () => {
|
|
1547
|
+
// Parity with the Rust flow's `sync()`: the transport fetch plus the
|
|
1548
|
+
// chain sync (`MidenClient.sync()` runs both, fail-fast), then the
|
|
1549
|
+
// GUARDIAN state sync.
|
|
1550
|
+
await this.midenClient.sync();
|
|
1551
|
+
await this.syncState();
|
|
1552
|
+
},
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* Import the notes embedded in the old GUARDIAN's pending consume-notes
|
|
1558
|
+
* proposals while they are still reachable: pending proposals do not
|
|
1559
|
+
* survive a guardian switch, making them the one recovery source
|
|
1560
|
+
* {@link recoverNotes} loses once the client repoints (issue #417).
|
|
1561
|
+
*
|
|
1562
|
+
* Run automatically by {@link executeProposal} on the switch path, before
|
|
1563
|
+
* the switch transaction executes; call it yourself before repointing a
|
|
1564
|
+
* client by hand via {@link setGuardianClient}. Best-effort by contract:
|
|
1565
|
+
* problems are warned, never thrown, and the flow runs under a timeout
|
|
1566
|
+
* with cooperative cancellation plus a bounded settle grace, so a hung
|
|
1567
|
+
* old GUARDIAN can neither block the switch nor overlap it on the shared
|
|
1568
|
+
* client. Returns the recovery report, or `undefined` when the flow could
|
|
1569
|
+
* not run or timed out. Mirror of the Rust SDK's
|
|
1570
|
+
* `preserve_pre_switch_proposal_notes`; full rationale and semantics in
|
|
1571
|
+
* "Preserving Notes Across a Guardian Switch" (docs/MULTISIG_SDK.md).
|
|
1572
|
+
*/
|
|
1573
|
+
async preservePreSwitchProposalNotes(): Promise<NoteRecoveryReport | undefined> {
|
|
1574
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1575
|
+
let graceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1576
|
+
let timedOut = false;
|
|
1577
|
+
try {
|
|
1578
|
+
const cancelled = () => timedOut;
|
|
1579
|
+
const flow = runNoteRecovery(
|
|
1580
|
+
GUARDIAN_SWITCH_RECOVERY_OPTIONS,
|
|
1581
|
+
this.buildNoteRecoverySteps(GUARDIAN_SWITCH_RECOVERY_OPTIONS, cancelled),
|
|
1582
|
+
cancelled,
|
|
1583
|
+
);
|
|
1584
|
+
const outcome = await Promise.race([
|
|
1585
|
+
flow,
|
|
1586
|
+
new Promise<typeof PRE_SWITCH_IMPORT_TIMED_OUT>((resolve) => {
|
|
1587
|
+
timer = setTimeout(() => {
|
|
1588
|
+
timedOut = true;
|
|
1589
|
+
resolve(PRE_SWITCH_IMPORT_TIMED_OUT);
|
|
1590
|
+
}, PRE_SWITCH_IMPORT_TIMEOUT_MS);
|
|
1591
|
+
}),
|
|
1592
|
+
]);
|
|
1593
|
+
if (outcome === PRE_SWITCH_IMPORT_TIMED_OUT) {
|
|
1594
|
+
console.warn(
|
|
1595
|
+
`Pre-switch proposal-note import timed out after ${PRE_SWITCH_IMPORT_TIMEOUT_MS}ms ` +
|
|
1596
|
+
'and was cancelled; notes embedded in pending proposals may be ' +
|
|
1597
|
+
'unrecoverable after the GUARDIAN switch',
|
|
1598
|
+
);
|
|
1599
|
+
// The token stops all new work; give the one uninterruptible
|
|
1600
|
+
// in-flight operation a bounded grace to settle. The result is
|
|
1601
|
+
// unobserved, and a rejection must not go unhandled.
|
|
1602
|
+
await Promise.race([
|
|
1603
|
+
flow.catch(() => {}),
|
|
1604
|
+
new Promise<void>((resolve) => {
|
|
1605
|
+
graceTimer = setTimeout(resolve, PRE_SWITCH_SETTLE_GRACE_MS);
|
|
1606
|
+
}),
|
|
1607
|
+
]);
|
|
1608
|
+
return undefined;
|
|
1609
|
+
}
|
|
1610
|
+
for (const problem of outcome.problems) {
|
|
1611
|
+
console.warn(
|
|
1612
|
+
`Pre-switch proposal-note import step '${problem.step}' did not finish; ` +
|
|
1613
|
+
'notes embedded in pending proposals may be unrecoverable after the ' +
|
|
1614
|
+
'GUARDIAN switch',
|
|
1615
|
+
problem.reason,
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
// Per-note failures never reach `problems` — and this is the last
|
|
1619
|
+
// moment the notes are reachable, so "retryable" cannot help: the
|
|
1620
|
+
// source is gone once the client repoints. They must be observable now.
|
|
1621
|
+
for (const noteOutcome of outcome.proposalImport ?? []) {
|
|
1622
|
+
if (noteOutcome.status === 'invalid' || noteOutcome.status === 'failed') {
|
|
1623
|
+
console.warn(
|
|
1624
|
+
`Pre-switch import could not preserve embedded note ${noteOutcome.identifier} ` +
|
|
1625
|
+
`(${noteOutcome.status}); it may be unrecoverable after the GUARDIAN switch`,
|
|
1626
|
+
noteOutcome.reason,
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return outcome;
|
|
1631
|
+
} catch (error) {
|
|
1632
|
+
// runNoteRecovery only throws for caller errors (inverted backfill
|
|
1633
|
+
// range, never passed here), but the switch must survive anything.
|
|
1634
|
+
console.warn(
|
|
1635
|
+
'Pre-switch proposal-note import could not run; notes embedded in pending ' +
|
|
1636
|
+
'proposals may be unrecoverable after the GUARDIAN switch',
|
|
1637
|
+
error,
|
|
1638
|
+
);
|
|
1639
|
+
return undefined;
|
|
1640
|
+
} finally {
|
|
1641
|
+
if (timer !== undefined) {
|
|
1642
|
+
clearTimeout(timer);
|
|
1643
|
+
}
|
|
1644
|
+
if (graceTimer !== undefined) {
|
|
1645
|
+
clearTimeout(graceTimer);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1234
1650
|
/**
|
|
1235
1651
|
* Compute the ID of the note a P2ID proposal will create when executed.
|
|
1236
1652
|
*
|
|
@@ -1253,6 +1669,12 @@ export class Multisig {
|
|
|
1253
1669
|
throw new Error('getP2idNoteId requires a P2ID proposal with recipient, faucet, amount, and salt metadata');
|
|
1254
1670
|
}
|
|
1255
1671
|
|
|
1672
|
+
// Validate before deriving. Unlike the rebuild paths there is no commitment check
|
|
1673
|
+
// downstream of this note id -- it goes straight out to a recipient — so a salt that
|
|
1674
|
+
// silently padded to the zero word would produce a plausible id for a note that does
|
|
1675
|
+
// not exist, with nothing to catch it.
|
|
1676
|
+
this.requireProposalSaltHex(proposal.id, metadata);
|
|
1677
|
+
|
|
1256
1678
|
const note = buildP2idNoteFromMetadata(
|
|
1257
1679
|
this._accountId,
|
|
1258
1680
|
metadata.recipientId,
|
|
@@ -1379,6 +1801,13 @@ export class Multisig {
|
|
|
1379
1801
|
async executeProposal(proposalId: string): Promise<void> {
|
|
1380
1802
|
const { metadata, finalRequest, proposal } = await this.prepareProposalExecution(proposalId);
|
|
1381
1803
|
|
|
1804
|
+
if (metadata.proposalType === 'switch_guardian') {
|
|
1805
|
+
// #417: import notes embedded in pending proposals from the old
|
|
1806
|
+
// GUARDIAN. Must run before the switch executes and repoints;
|
|
1807
|
+
// best-effort and bounded — see preservePreSwitchProposalNotes.
|
|
1808
|
+
await this.preservePreSwitchProposalNotes();
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1382
1811
|
// Execute at the proposal's anchored reference block, so the summary the
|
|
1383
1812
|
// cosigners signed reproduces exactly. The anchor was already checked
|
|
1384
1813
|
// against the summary's block commitment during binding verification.
|
|
@@ -1421,13 +1850,7 @@ export class Multisig {
|
|
|
1421
1850
|
}
|
|
1422
1851
|
|
|
1423
1852
|
try {
|
|
1424
|
-
const
|
|
1425
|
-
await retryRpcRead(() => webClient.syncState(), this.rpcConfig);
|
|
1426
|
-
|
|
1427
|
-
const updatedAccount = await retryRpcRead(
|
|
1428
|
-
() => webClient.getAccount(accountId),
|
|
1429
|
-
this.rpcConfig,
|
|
1430
|
-
);
|
|
1853
|
+
const updatedAccount = await this.syncNetworkOnly();
|
|
1431
1854
|
if (!updatedAccount) {
|
|
1432
1855
|
throw new Error(
|
|
1433
1856
|
`Updated account ${this._accountId} is missing from local client`
|
|
@@ -1773,9 +2196,23 @@ export class Multisig {
|
|
|
1773
2196
|
|
|
1774
2197
|
const txSummaryBytes = base64ToUint8Array(txSummaryBase64);
|
|
1775
2198
|
const txSummary = TransactionSummary.deserialize(txSummaryBytes);
|
|
1776
|
-
const saltHex =
|
|
2199
|
+
const saltHex = this.requireProposalSaltHex(proposalId, metadata);
|
|
1777
2200
|
const txCommitmentHex = txSummary.toCommitment().toHex();
|
|
1778
2201
|
const normalizedTxCommitmentHex = normalizeHexWord(txCommitmentHex);
|
|
2202
|
+
|
|
2203
|
+
// This summary was re-fetched from GUARDIAN, not taken from the verified proposal:
|
|
2204
|
+
// `ensureProposalCommitmentMatchesSummary` pins the CACHED `proposal.txSummary` to the
|
|
2205
|
+
// id, and nothing pinned this one. It goes on to key the advice map and drive the
|
|
2206
|
+
// rebuild, so an unrelated summary served here would have signatures collected against
|
|
2207
|
+
// one transaction and advice assembled for another. On an ECDSA roster the
|
|
2208
|
+
// recoverability check would notice; on a Falcon roster nothing else compares them.
|
|
2209
|
+
if (normalizedTxCommitmentHex !== normalizeHexWord(proposalId)) {
|
|
2210
|
+
throw new Error(
|
|
2211
|
+
`Proposal ${proposalId} tx_summary commitment ${normalizedTxCommitmentHex} ` +
|
|
2212
|
+
'does not match the proposal id it belongs to',
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
1779
2216
|
const normalizedSignerCommitments = new Set(
|
|
1780
2217
|
this.signerCommitments.map((commitment) => normalizeHexWord(commitment)),
|
|
1781
2218
|
);
|
|
@@ -1880,12 +2317,15 @@ export class Multisig {
|
|
|
1880
2317
|
await this.verifyGuardianEndpointCommitment(metadata.newGuardianEndpoint, metadata.newGuardianPubkey);
|
|
1881
2318
|
}
|
|
1882
2319
|
|
|
2320
|
+
// The builders read `.toHex()` and allocate their own Word, so this handle stays
|
|
2321
|
+
// ours; without the release it leaks once per execute.
|
|
1883
2322
|
const executionSalt = Word.fromHex(normalizeHexWord(saltHex));
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
executionSalt,
|
|
1887
|
-
|
|
1888
|
-
|
|
2323
|
+
let finalRequest;
|
|
2324
|
+
try {
|
|
2325
|
+
finalRequest = await this.buildTransactionRequestFromMetadata(metadata, executionSalt, adviceMap);
|
|
2326
|
+
} finally {
|
|
2327
|
+
executionSalt.free?.();
|
|
2328
|
+
}
|
|
1889
2329
|
|
|
1890
2330
|
return { finalRequest, metadata, proposal };
|
|
1891
2331
|
}
|
|
@@ -2085,9 +2525,9 @@ export class Multisig {
|
|
|
2085
2525
|
return txSummaryCommitment;
|
|
2086
2526
|
}
|
|
2087
2527
|
|
|
2088
|
-
const salt =
|
|
2089
|
-
|
|
2090
|
-
|
|
2528
|
+
const salt = Word.fromHex(
|
|
2529
|
+
normalizeHexWord(this.requireProposalSaltHex(proposal.id, proposal.metadata)),
|
|
2530
|
+
);
|
|
2091
2531
|
|
|
2092
2532
|
const request = await this.buildTransactionRequestFromMetadata(proposal.metadata, salt);
|
|
2093
2533
|
const webClient = await this.getRawClient();
|
|
@@ -2110,6 +2550,59 @@ export class Multisig {
|
|
|
2110
2550
|
* cannot be reproduced, verified, or executed. The caller owns the returned
|
|
2111
2551
|
* anchor and must `free()` it once done.
|
|
2112
2552
|
*/
|
|
2553
|
+
/**
|
|
2554
|
+
* Reads a proposal's salt. Throws when absent, because there is nothing to fall
|
|
2555
|
+
* back to.
|
|
2556
|
+
*
|
|
2557
|
+
* The request declares this salt through `withFeeConversionSalt`, and miden-client
|
|
2558
|
+
* commits `hash(CONVERSION_INFO || SALT)` into the auth arg from it. The summary
|
|
2559
|
+
* therefore carries the COMMITMENT, and a commitment is not invertible to the salt
|
|
2560
|
+
* it was built from -- so `summaryAuthArg(summary)` cannot stand in here. It used
|
|
2561
|
+
* to: before the request declared a salt the auth arg WAS the bare salt, which is
|
|
2562
|
+
* why the fallback this replaces was correct when it was written.
|
|
2563
|
+
*
|
|
2564
|
+
* A declared salt also bypasses miden-client's zero-fee early return, so this holds
|
|
2565
|
+
* on a chain that charges nothing exactly as on one that charges.
|
|
2566
|
+
*/
|
|
2567
|
+
private requireProposalSaltHex(proposalId: string, metadata: ProposalMetadata): string {
|
|
2568
|
+
const saltHex: unknown = metadata.saltHex;
|
|
2569
|
+
|
|
2570
|
+
if (saltHex === undefined || saltHex === null || saltHex === '') {
|
|
2571
|
+
throw new Error(
|
|
2572
|
+
`Proposal ${proposalId} has no salt; its request cannot be rebuilt because ` +
|
|
2573
|
+
'the auth arg commits hash(CONVERSION_INFO || SALT) and is not invertible ' +
|
|
2574
|
+
'to the salt',
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// GUARDIAN serves this field and the response is cast, not parsed, so everything
|
|
2579
|
+
// below is untrusted input. A truthiness test is not enough: `normalizeHexWord`
|
|
2580
|
+
// left-pads, so `'0x'` and `'0X'` are truthy and pad to the ZERO word -- a salt
|
|
2581
|
+
// nobody chose, which rebuilds a different request and reports itself as a summary
|
|
2582
|
+
// mismatch. A non-string throws out of the hex helpers instead, and an unbounded
|
|
2583
|
+
// string is a logging hazard the error type already guards against.
|
|
2584
|
+
if (typeof saltHex !== 'string') {
|
|
2585
|
+
throw new ProposalSaltMalformedError({
|
|
2586
|
+
proposalId,
|
|
2587
|
+
saltHex,
|
|
2588
|
+
reason: `expected a hex string, got ${typeof saltHex}`,
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
const digits = saltHex.replace(/^0[xX]/, '');
|
|
2592
|
+
if (digits.length === 0 || digits.length > MAX_SALT_HEX_DIGITS || !/^[0-9a-fA-F]+$/.test(digits)) {
|
|
2593
|
+
throw new ProposalSaltMalformedError({
|
|
2594
|
+
proposalId,
|
|
2595
|
+
saltHex,
|
|
2596
|
+
reason:
|
|
2597
|
+
digits.length === 0
|
|
2598
|
+
? 'a hex prefix with no digits is the zero word, not a salt'
|
|
2599
|
+
: `expected 1 to ${MAX_SALT_HEX_DIGITS} hex digits`,
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
return saltHex;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2113
2606
|
private requireProposalAnchor(proposalId: string, metadata: ProposalMetadata): ChainAnchor {
|
|
2114
2607
|
if (!metadata.chainAnchor) {
|
|
2115
2608
|
throw new Error(
|
package/src/procedures.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
export const PROCEDURE_ROOTS = {
|
|
14
14
|
update_signers: '0xa261cfd3c8791ac5abe1e78e14eade2f20789d73ab1c23c430418de59bc3380e',
|
|
15
15
|
update_procedure_threshold: '0x97587c61d49313b1d5a3c8b7437e0080e67ed9bd9d3e7206bcae562f934ccd03',
|
|
16
|
-
auth_tx: '
|
|
16
|
+
auth_tx: '0x43fb07d62ed26993b7b13c7b411db62c5b5acffa2813e989608c41a72d7185ec',
|
|
17
17
|
update_guardian: '0x0a614ff7c81a561cbd2a4c2d9482031a7a841ca5de33349daed23a9d871b3675',
|
|
18
18
|
send_asset: '0x595bc83258726a66bd904912cfd5186c07cbd902dfbc115b7d6bc8105efc57e3',
|
|
19
19
|
receive_asset: '0x34a56dd18f6fe5aab63198b9dcfc6467e793ebabb37d56b994b902504635da13',
|