@openzeppelin/miden-multisig-client 0.18.0-rc.1 → 0.18.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 +71 -17
- package/dist/account/builder.d.ts +3 -4
- package/dist/account/builder.d.ts.map +1 -1
- package/dist/account/builder.js +6 -13
- package/dist/account/builder.js.map +1 -1
- package/dist/client.d.ts +11 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +8 -3
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/multisig/authArgErrors.d.ts +15 -1
- package/dist/multisig/authArgErrors.d.ts.map +1 -1
- package/dist/multisig/authArgErrors.js +20 -0
- package/dist/multisig/authArgErrors.js.map +1 -1
- package/dist/multisig.d.ts +76 -25
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +196 -156
- package/dist/multisig.js.map +1 -1
- package/dist/procedures.d.ts +5 -5
- package/dist/procedures.js +5 -5
- package/dist/prover/workflow.d.ts +7 -3
- package/dist/prover/workflow.d.ts.map +1 -1
- package/dist/prover/workflow.js +7 -5
- package/dist/prover/workflow.js.map +1 -1
- package/dist/recovery/proposalNoteImport.d.ts +5 -6
- package/dist/recovery/proposalNoteImport.d.ts.map +1 -1
- package/dist/recovery/proposalNoteImport.js +12 -13
- package/dist/recovery/proposalNoteImport.js.map +1 -1
- package/dist/recovery/publicNoteBackfill.d.ts +2 -3
- package/dist/recovery/publicNoteBackfill.d.ts.map +1 -1
- package/dist/recovery/publicNoteBackfill.js +4 -5
- package/dist/recovery/publicNoteBackfill.js.map +1 -1
- package/dist/transaction/authArgs.d.ts +2 -3
- package/dist/transaction/authArgs.d.ts.map +1 -1
- package/dist/transaction/authArgs.js +7 -14
- package/dist/transaction/authArgs.js.map +1 -1
- package/dist/transaction/consumeNotes.d.ts +4 -12
- package/dist/transaction/consumeNotes.d.ts.map +1 -1
- package/dist/transaction/consumeNotes.js +10 -4
- package/dist/transaction/consumeNotes.js.map +1 -1
- package/dist/transaction/noteAuthentication.d.ts +2 -3
- package/dist/transaction/noteAuthentication.d.ts.map +1 -1
- package/dist/transaction/noteAuthentication.js +11 -11
- package/dist/transaction/noteAuthentication.js.map +1 -1
- package/dist/transaction/options.d.ts +0 -7
- package/dist/transaction/options.d.ts.map +1 -1
- package/dist/transaction/p2id.d.ts +2 -3
- package/dist/transaction/p2id.d.ts.map +1 -1
- package/dist/transaction/p2id.js.map +1 -1
- package/dist/transaction/summary.d.ts +94 -22
- package/dist/transaction/summary.d.ts.map +1 -1
- package/dist/transaction/summary.js +146 -12
- package/dist/transaction/summary.js.map +1 -1
- package/dist/transaction/updateGuardian.d.ts +3 -7
- package/dist/transaction/updateGuardian.d.ts.map +1 -1
- package/dist/transaction/updateGuardian.js +3 -4
- package/dist/transaction/updateGuardian.js.map +1 -1
- package/dist/transaction/updateProcedureThreshold.d.ts +3 -8
- package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
- package/dist/transaction/updateProcedureThreshold.js +3 -4
- package/dist/transaction/updateProcedureThreshold.js.map +1 -1
- package/dist/transaction/updateSigners.d.ts +3 -8
- package/dist/transaction/updateSigners.d.ts.map +1 -1
- package/dist/transaction/updateSigners.js +3 -4
- 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/dist/types/proposal.d.ts +9 -8
- package/dist/types/proposal.d.ts.map +1 -1
- package/dist/types/proposal.js.map +1 -1
- package/package.json +4 -4
- package/src/account/builder.ts +6 -13
- package/src/client.ts +13 -10
- package/src/config.ts +14 -0
- package/src/index.ts +5 -3
- package/src/multisig/authArgErrors.ts +28 -1
- package/src/multisig.ts +238 -194
- package/src/procedures.ts +5 -5
- package/src/prover/workflow.ts +7 -10
- package/src/recovery/proposalNoteImport.ts +13 -15
- package/src/recovery/publicNoteBackfill.ts +6 -6
- package/src/transaction/authArgs.ts +15 -21
- package/src/transaction/consumeNotes.ts +6 -34
- package/src/transaction/noteAuthentication.ts +18 -13
- package/src/transaction/options.ts +0 -9
- package/src/transaction/p2id.ts +2 -3
- package/src/transaction/summary.ts +160 -57
- package/src/transaction/updateGuardian.ts +5 -23
- package/src/transaction/updateProcedureThreshold.ts +5 -25
- package/src/transaction/updateSigners.ts +5 -22
- package/src/transaction.ts +6 -0
- package/src/types/proposal.ts +9 -8
- package/dist/raw-client.d.ts +0 -14
- package/dist/raw-client.d.ts.map +0 -1
- package/dist/raw-client.js +0 -60
- package/dist/raw-client.js.map +0 -1
- package/src/raw-client.ts +0 -89
package/dist/multisig.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { GuardianHttpClient } from '@openzeppelin/guardian-client';
|
|
8
8
|
import { ProposalSaltMalformedError } from './multisig/authArgErrors.js';
|
|
9
9
|
import { Account, AccountId, AdviceMap, FeltArray, Note, NoteExportFormat, NoteFile, Signature, TransactionRequest, TransactionSummary, Word, } from '@miden-sdk/miden-sdk';
|
|
10
|
-
import { chainAnchorFromBase64, chainAnchorToBase64, executeForSummary,
|
|
10
|
+
import { chainAnchorFromBase64, chainAnchorToBase64, executeForSummary, executeForSummaryAtTip, isStaleChainError, prepareTipExecution, syncToBoundBlock, summaryApprovalExpirationBlockNum, summarySalt, buildUpdateSignersTransactionRequest, buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildConsumeNotesTransactionRequest, buildP2idNoteFromMetadata, buildP2idTransactionRequest, parseP2idNoteType, p2idNoteTypeToMetadata, } from './transaction.js';
|
|
11
11
|
import { buildConsumeNotesTransactionRequestFromNotes } from './transaction/consumeNotes.js';
|
|
12
12
|
import { validateMultisigConfig } from './account/builder.js';
|
|
13
13
|
import { ensureNotesAuthenticated } from './transaction/noteAuthentication.js';
|
|
@@ -27,7 +27,7 @@ import { importNotesFromProposals as importNotesFromProposalsStandalone, throwIf
|
|
|
27
27
|
import { backfillPublicNotesByTag as backfillPublicNotesByTagStandalone, } from './recovery/publicNoteBackfill.js';
|
|
28
28
|
import { drainPrivateNoteBacklog } from './recovery/transportDrain.js';
|
|
29
29
|
import { GUARDIAN_SWITCH_RECOVERY_OPTIONS, runNoteRecovery, } from './recovery/recoverNotes.js';
|
|
30
|
-
import {
|
|
30
|
+
import { requireMidenRpcEndpoint } from './config.js';
|
|
31
31
|
import { resolveProverConfig, } from './prover/config.js';
|
|
32
32
|
import { ProverWorkflow } from './prover/workflow.js';
|
|
33
33
|
import { resolveRpcConfig, } from './rpc/config.js';
|
|
@@ -141,7 +141,6 @@ export class Multisig {
|
|
|
141
141
|
guardian;
|
|
142
142
|
signer;
|
|
143
143
|
midenClient;
|
|
144
|
-
rawClientPromise;
|
|
145
144
|
proverWorkflow;
|
|
146
145
|
rpcConfig;
|
|
147
146
|
_accountId;
|
|
@@ -177,16 +176,12 @@ export class Multisig {
|
|
|
177
176
|
this.midenClient = midenClient;
|
|
178
177
|
this._accountId = accountId ?? (account ? accountIdToHex(account) : '');
|
|
179
178
|
this.midenRpcEndpoint = requireMidenRpcEndpoint(midenRpcEndpoint);
|
|
180
|
-
this.
|
|
181
|
-
this.proverWorkflow = new ProverWorkflow(this.midenClient, proverConfig ?? resolveProverConfig(undefined, getTransactionProver(midenClient)));
|
|
179
|
+
this.proverWorkflow = new ProverWorkflow(this.midenClient, proverConfig ?? resolveProverConfig(undefined, midenClient.defaultProver));
|
|
182
180
|
this.rpcConfig = rpcConfig ?? resolveRpcConfig(undefined);
|
|
183
181
|
}
|
|
184
182
|
getMidenRpcEndpoint() {
|
|
185
183
|
return this.midenRpcEndpoint;
|
|
186
184
|
}
|
|
187
|
-
async getRawClient() {
|
|
188
|
-
return this.rawClientPromise;
|
|
189
|
-
}
|
|
190
185
|
proposalFactory() {
|
|
191
186
|
return new ProposalFactory({
|
|
192
187
|
accountId: this._accountId,
|
|
@@ -215,7 +210,7 @@ export class Multisig {
|
|
|
215
210
|
return this.signer.commitment;
|
|
216
211
|
}
|
|
217
212
|
/**
|
|
218
|
-
* Resolve the account from the
|
|
213
|
+
* Resolve the account from the Miden client's store, falling back to the
|
|
219
214
|
* `account` snapshot when the store has no record.
|
|
220
215
|
*
|
|
221
216
|
* Transaction execution reads the store, and other flows (e.g. consume-notes
|
|
@@ -223,9 +218,10 @@ export class Multisig {
|
|
|
223
218
|
* source from the store to see the same state execution will.
|
|
224
219
|
*/
|
|
225
220
|
async getStoreAccount() {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
return (await this.readStoreAccount()) ?? this.account;
|
|
222
|
+
}
|
|
223
|
+
readStoreAccount() {
|
|
224
|
+
return retryRpcRead(() => this.midenClient.accounts.get(this._accountId), this.rpcConfig);
|
|
229
225
|
}
|
|
230
226
|
/**
|
|
231
227
|
* Read the current ordered signer public-key commitments from account
|
|
@@ -375,9 +371,16 @@ export class Multisig {
|
|
|
375
371
|
/**
|
|
376
372
|
* Sync account state from GUARDIAN into the local Miden client store.
|
|
377
373
|
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
* is
|
|
374
|
+
* Starts with the canonical-nonce pre-check (OpenZeppelin/guardian#191):
|
|
375
|
+
* GUARDIAN reports the nonce and commitment of its canonical state, and
|
|
376
|
+
* when that nonce is not above the local account's — at a matching
|
|
377
|
+
* commitment when equal — nothing newer exists to pull, so the state
|
|
378
|
+
* fetch is skipped and the local account stands (`source: 'local'`).
|
|
379
|
+
*
|
|
380
|
+
* Otherwise the state is fetched (`source: 'guardian'`). If the GUARDIAN
|
|
381
|
+
* commitment differs from the local commitment (or the account is missing
|
|
382
|
+
* locally) and the GUARDIAN state is safe to import, the local store is
|
|
383
|
+
* overwritten with the GUARDIAN state. When the GUARDIAN is merely *behind*
|
|
381
384
|
* local — e.g. the pushed execution delta has not been canonicalized yet
|
|
382
385
|
* (see OpenZeppelin/guardian#316) — the local state is already ahead and
|
|
383
386
|
* on-chain-verifiable, so it is kept as authoritative. Either way, config is
|
|
@@ -385,11 +388,18 @@ export class Multisig {
|
|
|
385
388
|
* (e.g. the UI) observe the current state instead of a stale snapshot.
|
|
386
389
|
*/
|
|
387
390
|
async syncState() {
|
|
391
|
+
const localAccount = await this.readStoreAccount();
|
|
392
|
+
if (localAccount) {
|
|
393
|
+
const head = await this.guardian.getCanonicalNonce(this._accountId);
|
|
394
|
+
const localNonce = localAccount.nonce().asInt();
|
|
395
|
+
const guardianNonce = BigInt(head.nonce);
|
|
396
|
+
if (this.guardianIsNotAhead(localAccount, localNonce, head.commitment, guardianNonce)) {
|
|
397
|
+
this.refreshConfigFromAccount(localAccount);
|
|
398
|
+
return { source: 'local', localNonce, guardianNonce };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
388
401
|
const state = await this.fetchState();
|
|
389
|
-
|
|
390
|
-
const webClient = await this.getRawClient();
|
|
391
|
-
const localAccount = await retryRpcRead(() => webClient.getAccount(accountId), this.rpcConfig);
|
|
392
|
-
let accountForConfigRefresh = localAccount ?? null;
|
|
402
|
+
let accountForConfigRefresh = localAccount;
|
|
393
403
|
const guardianCommitment = normalizeHexWord(state.commitment);
|
|
394
404
|
const localCommitment = localAccount
|
|
395
405
|
? normalizeHexWord(localAccount.to_commitment().toHex())
|
|
@@ -397,18 +407,33 @@ export class Multisig {
|
|
|
397
407
|
if (!localAccount || localCommitment !== guardianCommitment) {
|
|
398
408
|
const accountBytes = base64ToUint8Array(state.stateDataBase64);
|
|
399
409
|
const incomingAccount = Account.deserialize(accountBytes);
|
|
400
|
-
if (await this.isSafeToOverwriteLocalState(incomingAccount, localAccount)) {
|
|
401
|
-
await
|
|
410
|
+
if (await this.isSafeToOverwriteLocalState(incomingAccount, localAccount ?? undefined)) {
|
|
411
|
+
await this.midenClient.accounts.insert({ account: incomingAccount, overwrite: true });
|
|
402
412
|
accountForConfigRefresh = incomingAccount;
|
|
403
413
|
}
|
|
404
414
|
}
|
|
405
415
|
this.refreshConfigFromAccount(accountForConfigRefresh);
|
|
406
|
-
return state;
|
|
416
|
+
return { source: 'guardian', state };
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* The pre-check decision: GUARDIAN has nothing newer when its canonical
|
|
420
|
+
* nonce is below the local nonce, or equal to it at the same commitment.
|
|
421
|
+
* An equal nonce with a different commitment is divergence, which the
|
|
422
|
+
* full fetch reports (see `isSafeToOverwriteLocalState`).
|
|
423
|
+
*/
|
|
424
|
+
guardianIsNotAhead(localAccount, localNonce, guardianCommitment, guardianNonce) {
|
|
425
|
+
if (guardianNonce < localNonce) {
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
if (guardianNonce > localNonce) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
return (normalizeHexWord(guardianCommitment) ===
|
|
432
|
+
normalizeHexWord(localAccount.to_commitment().toHex()));
|
|
407
433
|
}
|
|
408
434
|
async verifyStateCommitment() {
|
|
409
435
|
const accountId = AccountId.fromHex(this._accountId);
|
|
410
|
-
const
|
|
411
|
-
const localAccount = await retryRpcRead(() => webClient.getAccount(accountId), this.rpcConfig);
|
|
436
|
+
const localAccount = await this.readStoreAccount();
|
|
412
437
|
if (!localAccount) {
|
|
413
438
|
throw new Error(`Local account state not found for account ${this._accountId}. Sync the account before verifying.`);
|
|
414
439
|
}
|
|
@@ -464,11 +489,10 @@ export class Multisig {
|
|
|
464
489
|
* fail.
|
|
465
490
|
*/
|
|
466
491
|
async syncNetworkOnly() {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
return account ?? null;
|
|
492
|
+
await this.syncChain();
|
|
493
|
+
const account = await this.readStoreAccount();
|
|
494
|
+
this.refreshConfigFromAccount(account);
|
|
495
|
+
return account;
|
|
472
496
|
}
|
|
473
497
|
refreshConfigFromAccount(account) {
|
|
474
498
|
if (!account) {
|
|
@@ -539,11 +563,12 @@ export class Multisig {
|
|
|
539
563
|
* Every synced proposal's metadata is checked against its signed summary
|
|
540
564
|
* and the outcome is recorded in {@link Proposal.verification}. One that
|
|
541
565
|
* fails is still cached and returned, so a single stale or corrupt
|
|
542
|
-
* proposal cannot hide the others (issue #462
|
|
543
|
-
* proposal
|
|
544
|
-
* with `retryable: true` means a transient node error
|
|
545
|
-
*
|
|
546
|
-
*
|
|
566
|
+
* proposal cannot hide the others (issue #462). Verification re-executes
|
|
567
|
+
* each proposal at the chain tip, so the Miden client is synced once first.
|
|
568
|
+
* `failed` with `retryable: true` means a transient node error or chain
|
|
569
|
+
* state this client had not caught up with, worth syncing again;
|
|
570
|
+
* `retryable: false` means the proposal cannot be reproduced and must be
|
|
571
|
+
* re-proposed. `signProposal` and `executeProposal` re-verify and
|
|
547
572
|
* refuse a failed proposal. A failed proposal still counts as reported, so
|
|
548
573
|
* it is pruned like any other once GUARDIAN stops listing it.
|
|
549
574
|
*
|
|
@@ -581,6 +606,12 @@ export class Multisig {
|
|
|
581
606
|
const deltas = await this.guardian.getDeltaProposals(this._accountId);
|
|
582
607
|
const factory = this.proposalFactory();
|
|
583
608
|
const reported = new Map();
|
|
609
|
+
if (deltas.length > 0) {
|
|
610
|
+
// Verification re-executes at the store's sync height; bring it to the
|
|
611
|
+
// tip once for the whole listing. Best effort: a node outage then shows
|
|
612
|
+
// up on each proposal as a retryable failure instead of failing the sync.
|
|
613
|
+
await this.syncChain().catch((error) => console.warn('Could not sync the Miden client before verifying proposals', error));
|
|
614
|
+
}
|
|
584
615
|
for (const delta of deltas) {
|
|
585
616
|
const proposalId = normalizeHexWord(computeCommitmentFromTxSummary(delta.deltaPayload.txSummary.data));
|
|
586
617
|
const existingProposal = this.proposals.get(proposalId);
|
|
@@ -729,7 +760,6 @@ export class Multisig {
|
|
|
729
760
|
*/
|
|
730
761
|
async createAddSignerProposal(newCommitment, options = {}, ...legacyArgs) {
|
|
731
762
|
const proposalNonce = resolveProposalNonce('createAddSignerProposal', options, legacyArgs);
|
|
732
|
-
const webClient = await this.getRawClient();
|
|
733
763
|
const targetThreshold = options.newThreshold ?? this.threshold;
|
|
734
764
|
const targetSignerCommitments = [...this.signerCommitments, newCommitment];
|
|
735
765
|
// What `update_signers_and_threshold` rejects on-chain, and what the auth
|
|
@@ -741,8 +771,8 @@ export class Multisig {
|
|
|
741
771
|
guardianCommitment: this.guardianCommitment,
|
|
742
772
|
});
|
|
743
773
|
this.warnOnOverrideDilution(targetSignerCommitments.length);
|
|
744
|
-
const { request, salt } = await buildUpdateSignersTransactionRequest(
|
|
745
|
-
const { summary, anchor } = await executeForSummary(
|
|
774
|
+
const { request, salt } = await buildUpdateSignersTransactionRequest(this.midenClient, targetThreshold, targetSignerCommitments, this.proposalRequestOptions(options));
|
|
775
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
746
776
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
747
777
|
anchor.free();
|
|
748
778
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -766,7 +796,6 @@ export class Multisig {
|
|
|
766
796
|
*/
|
|
767
797
|
async createRemoveSignerProposal(signerToRemove, options = {}, ...legacyArgs) {
|
|
768
798
|
const proposalNonce = resolveProposalNonce('createRemoveSignerProposal', options, legacyArgs);
|
|
769
|
-
const webClient = await this.getRawClient();
|
|
770
799
|
const normalizedRemove = signerToRemove.toLowerCase();
|
|
771
800
|
const targetSignerCommitments = this.signerCommitments.filter((c) => c.toLowerCase() !== normalizedRemove);
|
|
772
801
|
if (targetSignerCommitments.length === this.signerCommitments.length) {
|
|
@@ -779,8 +808,8 @@ export class Multisig {
|
|
|
779
808
|
if (targetThreshold < 1 || targetThreshold > targetSignerCommitments.length) {
|
|
780
809
|
throw new Error(`Invalid threshold ${targetThreshold}. Must be between 1 and ${targetSignerCommitments.length}`);
|
|
781
810
|
}
|
|
782
|
-
const { request, salt } = await buildUpdateSignersTransactionRequest(
|
|
783
|
-
const { summary, anchor } = await executeForSummary(
|
|
811
|
+
const { request, salt } = await buildUpdateSignersTransactionRequest(this.midenClient, targetThreshold, targetSignerCommitments, this.proposalRequestOptions(options));
|
|
812
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
784
813
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
785
814
|
anchor.free();
|
|
786
815
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -803,15 +832,14 @@ export class Multisig {
|
|
|
803
832
|
*/
|
|
804
833
|
async createChangeThresholdProposal(newThreshold, options = {}) {
|
|
805
834
|
const proposalNonce = resolveProposalNonce('createChangeThresholdProposal', options);
|
|
806
|
-
const webClient = await this.getRawClient();
|
|
807
835
|
if (newThreshold < 1 || newThreshold > this.signerCommitments.length) {
|
|
808
836
|
throw new Error(`Invalid threshold ${newThreshold}. Must be between 1 and ${this.signerCommitments.length}`);
|
|
809
837
|
}
|
|
810
838
|
if (newThreshold === this.threshold) {
|
|
811
839
|
throw new Error('New threshold is the same as current threshold');
|
|
812
840
|
}
|
|
813
|
-
const { request, salt } = await buildUpdateSignersTransactionRequest(
|
|
814
|
-
const { summary, anchor } = await executeForSummary(
|
|
841
|
+
const { request, salt } = await buildUpdateSignersTransactionRequest(this.midenClient, newThreshold, this.signerCommitments, this.proposalRequestOptions(options));
|
|
842
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
815
843
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
816
844
|
anchor.free();
|
|
817
845
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -828,7 +856,6 @@ export class Multisig {
|
|
|
828
856
|
}
|
|
829
857
|
async createUpdateProcedureThresholdProposal(targetProcedure, targetThreshold, options = {}) {
|
|
830
858
|
const proposalNonce = resolveProposalNonce('createUpdateProcedureThresholdProposal', options);
|
|
831
|
-
const webClient = await this.getRawClient();
|
|
832
859
|
if (targetThreshold < 0 || targetThreshold > this.signerCommitments.length) {
|
|
833
860
|
throw new Error(`Invalid threshold ${targetThreshold}. Must be between 0 and ${this.signerCommitments.length}`);
|
|
834
861
|
}
|
|
@@ -839,8 +866,8 @@ export class Multisig {
|
|
|
839
866
|
if (currentOverride !== undefined && currentOverride === targetThreshold) {
|
|
840
867
|
throw new Error(`Procedure ${targetProcedure} already has threshold override ${targetThreshold}`);
|
|
841
868
|
}
|
|
842
|
-
const { request, salt } = await buildUpdateProcedureThresholdTransactionRequest(
|
|
843
|
-
const { summary, anchor } = await executeForSummary(
|
|
869
|
+
const { request, salt } = await buildUpdateProcedureThresholdTransactionRequest(this.midenClient, targetProcedure, targetThreshold, this.proposalRequestOptions(options));
|
|
870
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
844
871
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
845
872
|
anchor.free();
|
|
846
873
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -880,7 +907,6 @@ export class Multisig {
|
|
|
880
907
|
* proposals for the same operation can never drift apart.
|
|
881
908
|
*/
|
|
882
909
|
async buildSwitchGuardianSummary(newGuardianEndpoint, newGuardianPubkey, approvalExpirationDelta) {
|
|
883
|
-
const webClient = await this.getRawClient();
|
|
884
910
|
// What `auth_tx_guarded_multisig` asserts after a guardian rotation, checked
|
|
885
911
|
// before any signature is collected.
|
|
886
912
|
validateMultisigConfig({
|
|
@@ -889,12 +915,12 @@ export class Multisig {
|
|
|
889
915
|
guardianCommitment: newGuardianPubkey,
|
|
890
916
|
});
|
|
891
917
|
await this.verifyGuardianEndpointCommitment(newGuardianEndpoint, newGuardianPubkey);
|
|
892
|
-
const { request, salt } = await buildUpdateGuardianTransactionRequest(
|
|
918
|
+
const { request, salt } = await buildUpdateGuardianTransactionRequest(this.midenClient, newGuardianPubkey, {
|
|
893
919
|
accountId: this._accountId,
|
|
894
920
|
approvalExpirationDelta,
|
|
895
921
|
signatureScheme: this.signer.scheme,
|
|
896
922
|
});
|
|
897
|
-
const { summary, anchor } = await executeForSummary(
|
|
923
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
898
924
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
899
925
|
anchor.free();
|
|
900
926
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -966,15 +992,13 @@ export class Multisig {
|
|
|
966
992
|
*/
|
|
967
993
|
async createConsumeNotesProposal(noteIds, options = {}) {
|
|
968
994
|
const proposalNonce = resolveProposalNonce('createConsumeNotesProposal', options);
|
|
969
|
-
const webClient = await this.getRawClient();
|
|
970
995
|
if (noteIds.length === 0) {
|
|
971
996
|
throw new Error('At least one note ID is required');
|
|
972
997
|
}
|
|
973
998
|
// Fetch notes locally (proposer has them per FR-012); embed for v2 verification.
|
|
974
|
-
const rawClient = await getRawMidenClient(webClient);
|
|
975
999
|
const fetchedNotes = [];
|
|
976
1000
|
for (const noteIdHex of noteIds) {
|
|
977
|
-
const inputNoteRecord = await
|
|
1001
|
+
const inputNoteRecord = await this.midenClient.notes.get(noteIdHex);
|
|
978
1002
|
if (!inputNoteRecord) {
|
|
979
1003
|
throw new LegacyConsumeNotesNoteMissingError(noteIdHex);
|
|
980
1004
|
}
|
|
@@ -985,8 +1009,8 @@ export class Multisig {
|
|
|
985
1009
|
// the notes are authenticated here first, before the anchor is captured.
|
|
986
1010
|
await this.ensureNotesAuthenticated(fetchedNotes);
|
|
987
1011
|
const embeddedNotes = fetchedNotes.map((n) => noteToBase64(n));
|
|
988
|
-
const { request, salt } = await buildConsumeNotesTransactionRequestFromNotes(
|
|
989
|
-
const { summary, anchor } = await executeForSummary(
|
|
1012
|
+
const { request, salt } = await buildConsumeNotesTransactionRequestFromNotes(this.midenClient, fetchedNotes, this.proposalRequestOptions(options));
|
|
1013
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
990
1014
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
991
1015
|
anchor.free();
|
|
992
1016
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -1023,15 +1047,14 @@ export class Multisig {
|
|
|
1023
1047
|
*/
|
|
1024
1048
|
async createP2idProposal(recipientId, faucetId, amount, options = {}, ...legacyArgs) {
|
|
1025
1049
|
const proposalNonce = resolveProposalNonce('createP2idProposal', options, legacyArgs);
|
|
1026
|
-
const webClient = await this.getRawClient();
|
|
1027
1050
|
if (amount <= 0n) {
|
|
1028
1051
|
throw new Error('Amount must be greater than 0');
|
|
1029
1052
|
}
|
|
1030
1053
|
// Forward everything but the nonce, so a note option added to
|
|
1031
1054
|
// CreateP2idProposalOptions can't be silently dropped before the builder.
|
|
1032
1055
|
const { nonce: _nonce, ...noteOptions } = options;
|
|
1033
|
-
const { request, salt } = await buildP2idTransactionRequest(
|
|
1034
|
-
const { summary, anchor } = await executeForSummary(
|
|
1056
|
+
const { request, salt } = await buildP2idTransactionRequest(this.midenClient, this._accountId, recipientId, faucetId, amount, noteOptions);
|
|
1057
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
1035
1058
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
1036
1059
|
anchor.free();
|
|
1037
1060
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -1055,41 +1078,24 @@ export class Multisig {
|
|
|
1055
1078
|
/**
|
|
1056
1079
|
* Get notes that can be consumed by this multisig account.
|
|
1057
1080
|
*
|
|
1058
|
-
* Returns
|
|
1059
|
-
*
|
|
1081
|
+
* Returns the notes the Miden client's last sync screened as consumable by
|
|
1082
|
+
* this account right now; block-locked notes are left out.
|
|
1060
1083
|
*/
|
|
1061
1084
|
async getConsumableNotes() {
|
|
1062
|
-
const
|
|
1063
|
-
const webClient = await this.getRawClient();
|
|
1064
|
-
// Get consumable notes for this account
|
|
1065
|
-
const consumableRecords = await webClient.getConsumableNotes(accountId);
|
|
1066
|
-
// Convert to our simplified ConsumableNote type
|
|
1085
|
+
const records = await this.midenClient.notes.listAvailable({ account: this._accountId });
|
|
1067
1086
|
const notes = [];
|
|
1068
|
-
for (const
|
|
1069
|
-
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
c.consumptionStatus().consumableAfterBlock() === undefined);
|
|
1074
|
-
if (canConsumeNow) {
|
|
1075
|
-
// Miden 0.15: InputNoteRecord.id() is `NoteId | undefined`; skip id-less records.
|
|
1076
|
-
const id = inputNote.id();
|
|
1077
|
-
if (id === undefined) {
|
|
1078
|
-
continue;
|
|
1079
|
-
}
|
|
1080
|
-
const noteId = id.toString();
|
|
1081
|
-
const details = inputNote.details();
|
|
1082
|
-
const fungibleAssets = details.assets().fungibleAssets();
|
|
1083
|
-
// Extract assets
|
|
1084
|
-
const assets = [];
|
|
1085
|
-
for (const asset of fungibleAssets) {
|
|
1086
|
-
assets.push({
|
|
1087
|
-
faucetId: asset.faucetId().toString(),
|
|
1088
|
-
amount: asset.amount(),
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
notes.push({ id: noteId, assets });
|
|
1087
|
+
for (const inputNote of records) {
|
|
1088
|
+
// Miden 0.15: InputNoteRecord.id() is `NoteId | undefined`; skip id-less records.
|
|
1089
|
+
const id = inputNote.id();
|
|
1090
|
+
if (id === undefined) {
|
|
1091
|
+
continue;
|
|
1092
1092
|
}
|
|
1093
|
+
const assets = inputNote
|
|
1094
|
+
.details()
|
|
1095
|
+
.assets()
|
|
1096
|
+
.fungibleAssets()
|
|
1097
|
+
.map((asset) => ({ faucetId: asset.faucetId().toString(), amount: asset.amount() }));
|
|
1098
|
+
notes.push({ id: id.toString(), assets });
|
|
1093
1099
|
}
|
|
1094
1100
|
return notes;
|
|
1095
1101
|
}
|
|
@@ -1111,11 +1117,10 @@ export class Multisig {
|
|
|
1111
1117
|
* @returns Serialized note file bytes
|
|
1112
1118
|
*/
|
|
1113
1119
|
async exportNoteToBytes(noteId) {
|
|
1114
|
-
const webClient = await this.getRawClient();
|
|
1115
1120
|
const trimmedNoteId = noteId.trim();
|
|
1116
1121
|
let record;
|
|
1117
1122
|
try {
|
|
1118
|
-
record = await
|
|
1123
|
+
[record] = await this.midenClient.notes.listSent({ ids: [trimmedNoteId] });
|
|
1119
1124
|
}
|
|
1120
1125
|
catch (err) {
|
|
1121
1126
|
const detail = err instanceof Error ? err.message : String(err);
|
|
@@ -1127,7 +1132,7 @@ export class Multisig {
|
|
|
1127
1132
|
const format = record.inclusionProof()
|
|
1128
1133
|
? NoteExportFormat.Full
|
|
1129
1134
|
: NoteExportFormat.Details;
|
|
1130
|
-
const noteFile = await
|
|
1135
|
+
const noteFile = await this.midenClient.notes.export(trimmedNoteId, { format });
|
|
1131
1136
|
return noteFile.serialize();
|
|
1132
1137
|
}
|
|
1133
1138
|
/**
|
|
@@ -1171,7 +1176,6 @@ export class Multisig {
|
|
|
1171
1176
|
* commitment for a details-only file
|
|
1172
1177
|
*/
|
|
1173
1178
|
async importNoteFromBytes(noteBytes) {
|
|
1174
|
-
const webClient = await this.getRawClient();
|
|
1175
1179
|
let noteFile;
|
|
1176
1180
|
try {
|
|
1177
1181
|
noteFile = NoteFile.deserialize(noteBytes);
|
|
@@ -1180,7 +1184,7 @@ export class Multisig {
|
|
|
1180
1184
|
const detail = err instanceof Error ? err.message : String(err);
|
|
1181
1185
|
throw new Error(`failed to decode note file: ${detail}`);
|
|
1182
1186
|
}
|
|
1183
|
-
return
|
|
1187
|
+
return this.midenClient.notes.import(noteFile);
|
|
1184
1188
|
}
|
|
1185
1189
|
/**
|
|
1186
1190
|
* Import a note file received out-of-band from a browser
|
|
@@ -1464,6 +1468,8 @@ export class Multisig {
|
|
|
1464
1468
|
}
|
|
1465
1469
|
async signProposal(proposalId) {
|
|
1466
1470
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1471
|
+
// Verification re-executes the proposal at the store's sync height.
|
|
1472
|
+
await this.syncChain();
|
|
1467
1473
|
const existingProposal = await this.getProposalForSigning(proposalId, normalizedProposalId);
|
|
1468
1474
|
if (!existingProposal) {
|
|
1469
1475
|
throw new Error(`Proposal not found: ${proposalId}`);
|
|
@@ -1492,9 +1498,14 @@ export class Multisig {
|
|
|
1492
1498
|
await this.syncProposals();
|
|
1493
1499
|
return this.proposals.get(proposalId) ?? this.proposals.get(normalizedProposalId);
|
|
1494
1500
|
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Builds the final, fully signed request for a ready proposal, for a caller
|
|
1503
|
+
* that proves and submits it with its own pipeline. The request declares the
|
|
1504
|
+
* block its summary binds, so execute it at the chain tip, without an
|
|
1505
|
+
* anchor, on a client synced to at least that block.
|
|
1506
|
+
*/
|
|
1495
1507
|
async createTransactionProposalRequest(proposalId) {
|
|
1496
|
-
const { finalRequest
|
|
1497
|
-
anchor.free();
|
|
1508
|
+
const { finalRequest } = await this.prepareProposalExecution(proposalId);
|
|
1498
1509
|
return finalRequest;
|
|
1499
1510
|
}
|
|
1500
1511
|
/**
|
|
@@ -1503,22 +1514,16 @@ export class Multisig {
|
|
|
1503
1514
|
* @param proposalId - The proposal commitment/ID
|
|
1504
1515
|
*/
|
|
1505
1516
|
async executeProposal(proposalId) {
|
|
1506
|
-
const { metadata, finalRequest, proposal
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
// against the summary's block commitment during binding verification.
|
|
1517
|
-
await this.proverWorkflow.submitAt(AccountId.fromHex(this._accountId), finalRequest, anchor);
|
|
1518
|
-
}
|
|
1519
|
-
finally {
|
|
1520
|
-
anchor.free();
|
|
1521
|
-
}
|
|
1517
|
+
const { metadata, finalRequest, proposal } = await this.prepareProposalExecution(proposalId);
|
|
1518
|
+
if (metadata.proposalType === 'switch_guardian') {
|
|
1519
|
+
// #417: import notes embedded in pending proposals from the old
|
|
1520
|
+
// GUARDIAN. Must run before the switch executes and repoints;
|
|
1521
|
+
// best-effort and bounded — see preservePreSwitchProposalNotes.
|
|
1522
|
+
await this.preservePreSwitchProposalNotes();
|
|
1523
|
+
}
|
|
1524
|
+
// Execute at the chain tip. The request declares the block the signed
|
|
1525
|
+
// summary binds, so the summary the cosigners signed reproduces there.
|
|
1526
|
+
await this.submitAtTip(finalRequest);
|
|
1522
1527
|
if (metadata.proposalType === 'switch_guardian') {
|
|
1523
1528
|
if (!metadata.newGuardianEndpoint || !metadata.newGuardianPubkey) {
|
|
1524
1529
|
throw new Error('Switch GUARDIAN proposal metadata is incomplete after execution');
|
|
@@ -1564,11 +1569,12 @@ export class Multisig {
|
|
|
1564
1569
|
* Submit an integration-built transaction (advice already injected). Mirrors
|
|
1565
1570
|
* the Rust `submit_transaction`; used by the custom proposal producer flow
|
|
1566
1571
|
* after `prepareCustomExecution` rebuilds its request with the returned advice.
|
|
1567
|
-
* The transaction is executed at the
|
|
1568
|
-
*
|
|
1572
|
+
* The transaction is executed at the chain tip, so the request has to declare
|
|
1573
|
+
* the block its auth args bind (`feeAwareTransactionRequestBuilder` does).
|
|
1569
1574
|
*/
|
|
1570
1575
|
async submitTransaction(proposalId, request) {
|
|
1571
1576
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1577
|
+
await this.syncChain();
|
|
1572
1578
|
const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
|
|
1573
1579
|
const existing = this.getLocalProposal(proposalId);
|
|
1574
1580
|
const proposal = this.proposalFactory().fromDelta(delta, normalizedProposalId, existing?.metadata, existing?.signatures ?? []);
|
|
@@ -1580,11 +1586,31 @@ export class Multisig {
|
|
|
1580
1586
|
if (anchorCommitment !== summaryBlockCommitment) {
|
|
1581
1587
|
throw new Error(`Proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`);
|
|
1582
1588
|
}
|
|
1583
|
-
await this.proverWorkflow.submitAt(AccountId.fromHex(this._accountId), request, anchor);
|
|
1584
1589
|
}
|
|
1585
1590
|
finally {
|
|
1586
1591
|
anchor.free();
|
|
1587
1592
|
}
|
|
1593
|
+
await this.submitAtTip(request);
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Executes `request` at the chain tip, then proves, submits and applies it,
|
|
1597
|
+
* syncing first when this client is still below the block the request binds.
|
|
1598
|
+
*/
|
|
1599
|
+
async submitAtTip(request) {
|
|
1600
|
+
await prepareTipExecution(this.midenClient, request, () => this.syncChain());
|
|
1601
|
+
await this.proverWorkflow.submit(AccountId.fromHex(this._accountId), request);
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Brings the Miden client to the chain tip before a proposal is re-executed:
|
|
1605
|
+
* an execution loads foreign accounts, the fee faucet among them, at the
|
|
1606
|
+
* store's sync height, which a node prunes about 50 blocks later.
|
|
1607
|
+
*/
|
|
1608
|
+
async syncChain() {
|
|
1609
|
+
await retryRpcRead(() => this.midenClient.syncChain(), this.rpcConfig);
|
|
1610
|
+
}
|
|
1611
|
+
/** {@link syncToBoundBlock} with this client's RPC retry policy. */
|
|
1612
|
+
async syncToBoundBlock(boundBlockNum) {
|
|
1613
|
+
await syncToBoundBlock(this.midenClient, boundBlockNum, () => this.syncChain());
|
|
1588
1614
|
}
|
|
1589
1615
|
/**
|
|
1590
1616
|
* Create a proposal from a producer-built transaction the SDK does not model.
|
|
@@ -1605,9 +1631,8 @@ export class Multisig {
|
|
|
1605
1631
|
if (BUILTIN_PROPOSAL_TYPES.has(label)) {
|
|
1606
1632
|
throw new Error(`'${label}' is a built-in proposal type; use the typed proposal API instead`);
|
|
1607
1633
|
}
|
|
1608
|
-
const webClient = await this.getRawClient();
|
|
1609
1634
|
const request = deserializeTransactionRequest(transactionRequestBytes);
|
|
1610
|
-
const { summary, anchor } = await executeForSummary(
|
|
1635
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
1611
1636
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
1612
1637
|
anchor.free();
|
|
1613
1638
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -1632,6 +1657,8 @@ export class Multisig {
|
|
|
1632
1657
|
*/
|
|
1633
1658
|
async prepareCustomExecution(proposalId, transactionRequestBytes) {
|
|
1634
1659
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1660
|
+
// The binding probe re-executes the producer's request at the store's sync height.
|
|
1661
|
+
await this.syncChain();
|
|
1635
1662
|
const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
|
|
1636
1663
|
const existing = this.getLocalProposal(proposalId);
|
|
1637
1664
|
const proposal = this.proposalFactory().fromDelta(delta, normalizedProposalId, existing?.metadata, existing?.signatures ?? []);
|
|
@@ -1646,26 +1673,23 @@ export class Multisig {
|
|
|
1646
1673
|
const txSummary = TransactionSummary.deserialize(base64ToUint8Array(delta.deltaPayload.txSummary.data));
|
|
1647
1674
|
const signedCommitmentHex = normalizeHexWord(txSummary.toCommitment().toHex());
|
|
1648
1675
|
const bindingRequest = deserializeTransactionRequest(transactionRequestBytes);
|
|
1649
|
-
//
|
|
1650
|
-
//
|
|
1651
|
-
//
|
|
1652
|
-
//
|
|
1653
|
-
// before executing against it.
|
|
1676
|
+
// The anchor arrives from an untrusted party via GUARDIAN, so its block
|
|
1677
|
+
// commitment is checked against the signed summary: it has to name the
|
|
1678
|
+
// block the summary binds. The probe itself runs at the chain tip, where
|
|
1679
|
+
// the request's declared bound block reproduces the signed summary.
|
|
1654
1680
|
const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
|
|
1655
|
-
let derivedCommitmentHex;
|
|
1656
1681
|
try {
|
|
1657
1682
|
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
1658
1683
|
const summaryBlockCommitment = normalizeHexWord(txSummary.blockCommitment().toHex());
|
|
1659
1684
|
if (anchorCommitment !== summaryBlockCommitment) {
|
|
1660
1685
|
throw new Error(`Custom proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`);
|
|
1661
1686
|
}
|
|
1662
|
-
const webClient = await this.getRawClient();
|
|
1663
|
-
const derived = await executeForSummaryAt(webClient, this._accountId, bindingRequest, anchor);
|
|
1664
|
-
derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
|
|
1665
1687
|
}
|
|
1666
1688
|
finally {
|
|
1667
1689
|
anchor.free();
|
|
1668
1690
|
}
|
|
1691
|
+
const derived = await executeForSummaryAtTip(this.midenClient, this._accountId, bindingRequest);
|
|
1692
|
+
const derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
|
|
1669
1693
|
if (derivedCommitmentHex !== signedCommitmentHex) {
|
|
1670
1694
|
throw new Error(`Custom proposal binding mismatch: expected ${signedCommitmentHex}, got ${derivedCommitmentHex}`);
|
|
1671
1695
|
}
|
|
@@ -1753,8 +1777,8 @@ export class Multisig {
|
|
|
1753
1777
|
return this.proposals.get(proposalId) ?? this.proposals.get(normalizedProposalId);
|
|
1754
1778
|
}
|
|
1755
1779
|
/**
|
|
1756
|
-
* The returned
|
|
1757
|
-
*
|
|
1780
|
+
* The returned request declares the block the signed summary binds, and
|
|
1781
|
+
* executes at the chain tip.
|
|
1758
1782
|
*/
|
|
1759
1783
|
async prepareProposalExecution(proposalId) {
|
|
1760
1784
|
const proposal = this.getLocalProposal(proposalId);
|
|
@@ -1762,6 +1786,8 @@ export class Multisig {
|
|
|
1762
1786
|
throw new Error(`Proposal not found: ${proposalId}`);
|
|
1763
1787
|
}
|
|
1764
1788
|
this.proposalFactory().assertAccountId(proposal.accountId);
|
|
1789
|
+
// Verification and execution run at the store's sync height.
|
|
1790
|
+
await this.syncChain();
|
|
1765
1791
|
await this.verifyProposalMetadataBinding(proposal);
|
|
1766
1792
|
const metadata = proposal.metadata;
|
|
1767
1793
|
// Reject custom proposals before any advice assembly or GUARDIAN ack push:
|
|
@@ -1873,14 +1899,18 @@ export class Multisig {
|
|
|
1873
1899
|
await this.verifyGuardianEndpointCommitment(metadata.newGuardianEndpoint, metadata.newGuardianPubkey);
|
|
1874
1900
|
}
|
|
1875
1901
|
const anchor = this.requireProposalAnchor(proposalId, metadata);
|
|
1902
|
+
let binding;
|
|
1876
1903
|
try {
|
|
1877
|
-
|
|
1878
|
-
return { finalRequest, metadata, proposal, anchor };
|
|
1904
|
+
binding = proposalRequestBinding(txSummary, anchor, saltHex);
|
|
1879
1905
|
}
|
|
1880
|
-
|
|
1906
|
+
finally {
|
|
1881
1907
|
anchor.free();
|
|
1882
|
-
throw error;
|
|
1883
1908
|
}
|
|
1909
|
+
// A switch_guardian proposal is verified without a rebuild, so this may be
|
|
1910
|
+
// the first time this client needs the chain at the bound block.
|
|
1911
|
+
await this.syncToBoundBlock(binding.boundBlockNum);
|
|
1912
|
+
const finalRequest = await this.buildTransactionRequestFromMetadata(metadata, binding, adviceMap);
|
|
1913
|
+
return { finalRequest, metadata, proposal };
|
|
1884
1914
|
}
|
|
1885
1915
|
/**
|
|
1886
1916
|
* Export a proposal for offline signing
|
|
@@ -1950,6 +1980,10 @@ export class Multisig {
|
|
|
1950
1980
|
throw new Error('Invalid proposal JSON: missing required fields');
|
|
1951
1981
|
}
|
|
1952
1982
|
const proposal = this.proposalFactory().fromExported(exported);
|
|
1983
|
+
// Verification re-executes every built-in type at the store's sync height.
|
|
1984
|
+
if (proposal.metadata.proposalType !== 'custom') {
|
|
1985
|
+
await this.syncChain();
|
|
1986
|
+
}
|
|
1953
1987
|
await this.verifyProposalMetadataBinding(proposal);
|
|
1954
1988
|
this.proposals.set(proposal.id, proposal);
|
|
1955
1989
|
return proposal;
|
|
@@ -1982,6 +2016,10 @@ export class Multisig {
|
|
|
1982
2016
|
if (alreadySigned) {
|
|
1983
2017
|
throw new Error('You have already signed this proposal');
|
|
1984
2018
|
}
|
|
2019
|
+
// Verification re-executes every built-in type at the store's sync height.
|
|
2020
|
+
if (proposal.metadata?.proposalType !== 'custom') {
|
|
2021
|
+
await this.syncChain();
|
|
2022
|
+
}
|
|
1985
2023
|
const commitmentToSign = await this.verifyProposalMetadataBinding(proposal);
|
|
1986
2024
|
// Sign the commitment
|
|
1987
2025
|
const signature = await buildGuardianSignatureFromSigner(this.signer, commitmentToSign);
|
|
@@ -2033,7 +2071,7 @@ export class Multisig {
|
|
|
2033
2071
|
catch (error) {
|
|
2034
2072
|
proposal.verification = {
|
|
2035
2073
|
status: 'failed',
|
|
2036
|
-
retryable: isTransientRpcError(error),
|
|
2074
|
+
retryable: isTransientRpcError(error) || isStaleChainError(error),
|
|
2037
2075
|
message: error instanceof Error ? error.message : String(error),
|
|
2038
2076
|
};
|
|
2039
2077
|
throw error;
|
|
@@ -2042,10 +2080,11 @@ export class Multisig {
|
|
|
2042
2080
|
async checkProposalMetadataBinding(proposal) {
|
|
2043
2081
|
const txSummaryCommitment = this.ensureProposalCommitmentMatchesSummary(proposal);
|
|
2044
2082
|
const summary = TransactionSummary.deserialize(base64ToUint8Array(proposal.txSummary));
|
|
2045
|
-
// The anchor arrives from an untrusted party via GUARDIAN, so check
|
|
2046
|
-
//
|
|
2047
|
-
//
|
|
2048
|
-
//
|
|
2083
|
+
// The anchor arrives from an untrusted party via GUARDIAN, so check that it
|
|
2084
|
+
// names the block the signed summary binds: the rebuild below binds the
|
|
2085
|
+
// block it names. Nothing executes against it; the rebuild runs at the
|
|
2086
|
+
// chain tip. `ChainAnchor.deserialize` already enforced internal
|
|
2087
|
+
// header/chain consistency.
|
|
2049
2088
|
const anchor = this.requireProposalAnchor(proposal.id, proposal.metadata);
|
|
2050
2089
|
try {
|
|
2051
2090
|
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
@@ -2072,6 +2111,11 @@ export class Multisig {
|
|
|
2072
2111
|
// binding checks for this type.
|
|
2073
2112
|
return txSummaryCommitment;
|
|
2074
2113
|
}
|
|
2114
|
+
// The rebuild reads the chain's fee faucet from the synced protocol
|
|
2115
|
+
// configuration and executes at the tip, so the store has to have synced
|
|
2116
|
+
// to the block the summary binds; a cosigner that has only just loaded
|
|
2117
|
+
// the account has not.
|
|
2118
|
+
await this.syncToBoundBlock(binding.boundBlockNum);
|
|
2075
2119
|
// A consume-notes summary commits to *authenticated* consumption (see
|
|
2076
2120
|
// ensureNotesAuthenticated), which miden-client decides from this store
|
|
2077
2121
|
// alone. Put the store in that mode before the rebuild, or a cosigner
|
|
@@ -2081,8 +2125,7 @@ export class Multisig {
|
|
|
2081
2125
|
await this.ensureNotesAuthenticated(decodeEmbeddedConsumeNotes(proposal.metadata));
|
|
2082
2126
|
}
|
|
2083
2127
|
const request = await this.buildTransactionRequestFromMetadata(proposal.metadata, binding);
|
|
2084
|
-
const
|
|
2085
|
-
const reconstructed = await executeForSummaryAt(webClient, this._accountId, request, anchor);
|
|
2128
|
+
const reconstructed = await executeForSummaryAtTip(this.midenClient, this._accountId, request);
|
|
2086
2129
|
const reconstructedCommitment = normalizeHexWord(reconstructed.toCommitment().toHex());
|
|
2087
2130
|
if (reconstructedCommitment !== txSummaryCommitment) {
|
|
2088
2131
|
throw new Error(`Invalid proposal: metadata does not match tx_summary for ${proposal.id}`);
|
|
@@ -2093,12 +2136,6 @@ export class Multisig {
|
|
|
2093
2136
|
anchor.free();
|
|
2094
2137
|
}
|
|
2095
2138
|
}
|
|
2096
|
-
/**
|
|
2097
|
-
* Decodes a proposal's chain anchor. Throws when absent: a proposal without
|
|
2098
|
-
* an anchor was created at an unknown reference block, so its signed summary
|
|
2099
|
-
* cannot be reproduced, verified, or executed. The caller owns the returned
|
|
2100
|
-
* anchor and must `free()` it once done.
|
|
2101
|
-
*/
|
|
2102
2139
|
/**
|
|
2103
2140
|
* Reads a proposal's salt. Throws when absent, because there is nothing to fall
|
|
2104
2141
|
* back to.
|
|
@@ -2140,11 +2177,16 @@ export class Multisig {
|
|
|
2140
2177
|
}
|
|
2141
2178
|
return saltHex;
|
|
2142
2179
|
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Decodes a proposal's chain anchor, which names the block its signed summary
|
|
2182
|
+
* binds. Throws when absent: every proposal carries one, so a proposal
|
|
2183
|
+
* without it is malformed and is neither verified nor executed. The caller
|
|
2184
|
+
* owns the returned anchor and must `free()` it once done.
|
|
2185
|
+
*/
|
|
2143
2186
|
requireProposalAnchor(proposalId, metadata) {
|
|
2144
2187
|
if (!metadata.chainAnchor) {
|
|
2145
|
-
throw new Error(`Proposal ${proposalId} has no chain anchor
|
|
2146
|
-
'
|
|
2147
|
-
'at the original reference block');
|
|
2188
|
+
throw new Error(`Proposal ${proposalId} has no chain anchor, which names the block its signed ` +
|
|
2189
|
+
'summary binds; it cannot be verified or executed');
|
|
2148
2190
|
}
|
|
2149
2191
|
return chainAnchorFromBase64(metadata.chainAnchor);
|
|
2150
2192
|
}
|
|
@@ -2158,8 +2200,7 @@ export class Multisig {
|
|
|
2158
2200
|
if (expirationBlockNum === undefined) {
|
|
2159
2201
|
return;
|
|
2160
2202
|
}
|
|
2161
|
-
const
|
|
2162
|
-
const syncHeight = await webClient.getSyncHeight();
|
|
2203
|
+
const syncHeight = await this.midenClient.getSyncHeight();
|
|
2163
2204
|
if (syncHeight >= expirationBlockNum) {
|
|
2164
2205
|
throw new Error(`Proposal ${proposalId} approval expired at block ${expirationBlockNum}; the chain is at ` +
|
|
2165
2206
|
`block ${syncHeight}, so the collected signatures no longer authorize it`);
|
|
@@ -2188,20 +2229,19 @@ export class Multisig {
|
|
|
2188
2229
|
}
|
|
2189
2230
|
}
|
|
2190
2231
|
async buildTransactionRequestWithOptions(metadata, requestOptions) {
|
|
2191
|
-
const webClient = await this.getRawClient();
|
|
2192
2232
|
switch (metadata.proposalType) {
|
|
2193
2233
|
case 'add_signer':
|
|
2194
2234
|
case 'remove_signer':
|
|
2195
2235
|
case 'change_threshold': {
|
|
2196
|
-
const { request } = await buildUpdateSignersTransactionRequest(
|
|
2236
|
+
const { request } = await buildUpdateSignersTransactionRequest(this.midenClient, metadata.targetThreshold, metadata.targetSignerCommitments, requestOptions);
|
|
2197
2237
|
return request;
|
|
2198
2238
|
}
|
|
2199
2239
|
case 'switch_guardian': {
|
|
2200
|
-
const { request } = await buildUpdateGuardianTransactionRequest(
|
|
2240
|
+
const { request } = await buildUpdateGuardianTransactionRequest(this.midenClient, metadata.newGuardianPubkey, requestOptions);
|
|
2201
2241
|
return request;
|
|
2202
2242
|
}
|
|
2203
2243
|
case 'update_procedure_threshold': {
|
|
2204
|
-
const { request } = await buildUpdateProcedureThresholdTransactionRequest(
|
|
2244
|
+
const { request } = await buildUpdateProcedureThresholdTransactionRequest(this.midenClient, metadata.targetProcedure, metadata.targetThreshold, requestOptions);
|
|
2205
2245
|
return request;
|
|
2206
2246
|
}
|
|
2207
2247
|
case 'consume_notes': {
|
|
@@ -2209,7 +2249,7 @@ export class Multisig {
|
|
|
2209
2249
|
const version = metadata.metadataVersion;
|
|
2210
2250
|
if (version === CONSUME_NOTES_METADATA_VERSION_V2) {
|
|
2211
2251
|
const decoded = decodeEmbeddedConsumeNotes(metadata);
|
|
2212
|
-
const { request } = await buildConsumeNotesTransactionRequestFromNotes(
|
|
2252
|
+
const { request } = await buildConsumeNotesTransactionRequestFromNotes(this.midenClient, decoded, requestOptions);
|
|
2213
2253
|
return request;
|
|
2214
2254
|
}
|
|
2215
2255
|
if (version === undefined || version === 1) {
|
|
@@ -2218,13 +2258,13 @@ export class Multisig {
|
|
|
2218
2258
|
// operator which legacy shape was rejected.
|
|
2219
2259
|
throw new UnsupportedMetadataVersionError(version);
|
|
2220
2260
|
}
|
|
2221
|
-
const { request } = await buildConsumeNotesTransactionRequest(
|
|
2261
|
+
const { request } = await buildConsumeNotesTransactionRequest(this.midenClient, metadata.noteIds, requestOptions);
|
|
2222
2262
|
return request;
|
|
2223
2263
|
}
|
|
2224
2264
|
throw new UnsupportedMetadataVersionError(version);
|
|
2225
2265
|
}
|
|
2226
2266
|
case 'p2id': {
|
|
2227
|
-
const { request } = await buildP2idTransactionRequest(
|
|
2267
|
+
const { request } = await buildP2idTransactionRequest(this.midenClient, this._accountId, metadata.recipientId, metadata.faucetId, BigInt(metadata.amount), {
|
|
2228
2268
|
...requestOptions,
|
|
2229
2269
|
noteType: parseP2idNoteType(metadata.noteType),
|
|
2230
2270
|
reclaimHeight: metadata.reclaimHeight,
|