@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/src/multisig.ts
CHANGED
|
@@ -18,10 +18,7 @@ import type {
|
|
|
18
18
|
} from './types.js';
|
|
19
19
|
import { ProposalSaltMalformedError } from './multisig/authArgErrors.js';
|
|
20
20
|
import type { ProcedureName } from './procedures.js';
|
|
21
|
-
import type {
|
|
22
|
-
MidenClient,
|
|
23
|
-
WasmWebClient,
|
|
24
|
-
} from '@miden-sdk/miden-sdk';
|
|
21
|
+
import type { MidenClient, OutputNoteRecord } from '@miden-sdk/miden-sdk';
|
|
25
22
|
import {
|
|
26
23
|
Account,
|
|
27
24
|
AccountId,
|
|
@@ -44,7 +41,10 @@ import {
|
|
|
44
41
|
chainAnchorFromBase64,
|
|
45
42
|
chainAnchorToBase64,
|
|
46
43
|
executeForSummary,
|
|
47
|
-
|
|
44
|
+
executeForSummaryAtTip,
|
|
45
|
+
isStaleChainError,
|
|
46
|
+
prepareTipExecution,
|
|
47
|
+
syncToBoundBlock,
|
|
48
48
|
summaryApprovalExpirationBlockNum,
|
|
49
49
|
summarySalt,
|
|
50
50
|
buildUpdateSignersTransactionRequest,
|
|
@@ -110,11 +110,7 @@ import {
|
|
|
110
110
|
type NoteRecoverySteps,
|
|
111
111
|
type RecoverNotesOptions,
|
|
112
112
|
} from './recovery/recoverNotes.js';
|
|
113
|
-
import {
|
|
114
|
-
getRawMidenClient,
|
|
115
|
-
getTransactionProver,
|
|
116
|
-
requireMidenRpcEndpoint,
|
|
117
|
-
} from './raw-client.js';
|
|
113
|
+
import { requireMidenRpcEndpoint } from './config.js';
|
|
118
114
|
import {
|
|
119
115
|
resolveProverConfig,
|
|
120
116
|
type ResolvedProverConfig,
|
|
@@ -142,6 +138,20 @@ export interface AccountState {
|
|
|
142
138
|
updatedAt: string;
|
|
143
139
|
}
|
|
144
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Outcome of {@link Multisig.syncState}.
|
|
143
|
+
*
|
|
144
|
+
* `'guardian'`: GUARDIAN's canonical nonce was above the local nonce (or the
|
|
145
|
+
* local store had no account, or the same nonce carried a different
|
|
146
|
+
* commitment), so the state was fetched and reconciled; `state` is what
|
|
147
|
+
* GUARDIAN served. `'local'`: GUARDIAN's canonical nonce was below the local
|
|
148
|
+
* nonce, or equal to it at the same commitment, so the state fetch was
|
|
149
|
+
* skipped and the local account stands.
|
|
150
|
+
*/
|
|
151
|
+
export type SyncStateResult =
|
|
152
|
+
| { source: 'guardian'; state: AccountState }
|
|
153
|
+
| { source: 'local'; localNonce: bigint; guardianNonce: bigint };
|
|
154
|
+
|
|
145
155
|
export interface AccountStateVerificationResult {
|
|
146
156
|
accountId: string;
|
|
147
157
|
localCommitment: string;
|
|
@@ -157,7 +167,7 @@ export interface CreateProposalOptions {
|
|
|
157
167
|
/** Proposal nonce; defaults to `Date.now()`. */
|
|
158
168
|
nonce?: number;
|
|
159
169
|
/**
|
|
160
|
-
* Blocks after the proposal
|
|
170
|
+
* Blocks after the block the proposal binds by which the transaction must be
|
|
161
171
|
* included; past that the approvers' signatures no longer authorize it. The
|
|
162
172
|
* summary binds it, so the executing party can neither shorten nor extend it.
|
|
163
173
|
* Omitted, the approval never expires (the upstream default).
|
|
@@ -324,7 +334,6 @@ export class Multisig {
|
|
|
324
334
|
private guardian: GuardianHttpClient;
|
|
325
335
|
private readonly signer: Signer;
|
|
326
336
|
private readonly midenClient: MidenClient;
|
|
327
|
-
private readonly rawClientPromise: Promise<WasmWebClient>;
|
|
328
337
|
private readonly proverWorkflow: ProverWorkflow;
|
|
329
338
|
private readonly rpcConfig: ResolvedRpcConfig;
|
|
330
339
|
private readonly _accountId: string;
|
|
@@ -373,10 +382,9 @@ export class Multisig {
|
|
|
373
382
|
this.midenClient = midenClient;
|
|
374
383
|
this._accountId = accountId ?? (account ? accountIdToHex(account) : '');
|
|
375
384
|
this.midenRpcEndpoint = requireMidenRpcEndpoint(midenRpcEndpoint);
|
|
376
|
-
this.rawClientPromise = getRawMidenClient(midenClient, this.midenRpcEndpoint);
|
|
377
385
|
this.proverWorkflow = new ProverWorkflow(
|
|
378
386
|
this.midenClient,
|
|
379
|
-
proverConfig ?? resolveProverConfig(undefined,
|
|
387
|
+
proverConfig ?? resolveProverConfig(undefined, midenClient.defaultProver),
|
|
380
388
|
);
|
|
381
389
|
this.rpcConfig = rpcConfig ?? resolveRpcConfig(undefined);
|
|
382
390
|
}
|
|
@@ -385,10 +393,6 @@ export class Multisig {
|
|
|
385
393
|
return this.midenRpcEndpoint;
|
|
386
394
|
}
|
|
387
395
|
|
|
388
|
-
private async getRawClient(): Promise<WasmWebClient> {
|
|
389
|
-
return this.rawClientPromise;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
396
|
private proposalFactory(): ProposalFactory {
|
|
393
397
|
return new ProposalFactory({
|
|
394
398
|
accountId: this._accountId,
|
|
@@ -425,7 +429,7 @@ export class Multisig {
|
|
|
425
429
|
}
|
|
426
430
|
|
|
427
431
|
/**
|
|
428
|
-
* Resolve the account from the
|
|
432
|
+
* Resolve the account from the Miden client's store, falling back to the
|
|
429
433
|
* `account` snapshot when the store has no record.
|
|
430
434
|
*
|
|
431
435
|
* Transaction execution reads the store, and other flows (e.g. consume-notes
|
|
@@ -433,12 +437,11 @@ export class Multisig {
|
|
|
433
437
|
* source from the store to see the same state execution will.
|
|
434
438
|
*/
|
|
435
439
|
async getStoreAccount(): Promise<Account> {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
);
|
|
441
|
-
return stored ?? this.account;
|
|
440
|
+
return (await this.readStoreAccount()) ?? this.account;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private readStoreAccount(): Promise<Account | null> {
|
|
444
|
+
return retryRpcRead(() => this.midenClient.accounts.get(this._accountId), this.rpcConfig);
|
|
442
445
|
}
|
|
443
446
|
|
|
444
447
|
/**
|
|
@@ -607,24 +610,37 @@ export class Multisig {
|
|
|
607
610
|
/**
|
|
608
611
|
* Sync account state from GUARDIAN into the local Miden client store.
|
|
609
612
|
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
* is
|
|
613
|
+
* Starts with the canonical-nonce pre-check (OpenZeppelin/guardian#191):
|
|
614
|
+
* GUARDIAN reports the nonce and commitment of its canonical state, and
|
|
615
|
+
* when that nonce is not above the local account's — at a matching
|
|
616
|
+
* commitment when equal — nothing newer exists to pull, so the state
|
|
617
|
+
* fetch is skipped and the local account stands (`source: 'local'`).
|
|
618
|
+
*
|
|
619
|
+
* Otherwise the state is fetched (`source: 'guardian'`). If the GUARDIAN
|
|
620
|
+
* commitment differs from the local commitment (or the account is missing
|
|
621
|
+
* locally) and the GUARDIAN state is safe to import, the local store is
|
|
622
|
+
* overwritten with the GUARDIAN state. When the GUARDIAN is merely *behind*
|
|
613
623
|
* local — e.g. the pushed execution delta has not been canonicalized yet
|
|
614
624
|
* (see OpenZeppelin/guardian#316) — the local state is already ahead and
|
|
615
625
|
* on-chain-verifiable, so it is kept as authoritative. Either way, config is
|
|
616
626
|
* refreshed from the resulting account so callers reading `Multisig.account`
|
|
617
627
|
* (e.g. the UI) observe the current state instead of a stale snapshot.
|
|
618
628
|
*/
|
|
619
|
-
async syncState(): Promise<
|
|
629
|
+
async syncState(): Promise<SyncStateResult> {
|
|
630
|
+
const localAccount = await this.readStoreAccount();
|
|
631
|
+
|
|
632
|
+
if (localAccount) {
|
|
633
|
+
const head = await this.guardian.getCanonicalNonce(this._accountId);
|
|
634
|
+
const localNonce = localAccount.nonce().asInt();
|
|
635
|
+
const guardianNonce = BigInt(head.nonce);
|
|
636
|
+
if (this.guardianIsNotAhead(localAccount, localNonce, head.commitment, guardianNonce)) {
|
|
637
|
+
this.refreshConfigFromAccount(localAccount);
|
|
638
|
+
return { source: 'local', localNonce, guardianNonce };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
620
642
|
const state = await this.fetchState();
|
|
621
|
-
|
|
622
|
-
const webClient = await this.getRawClient();
|
|
623
|
-
const localAccount = await retryRpcRead(
|
|
624
|
-
() => webClient.getAccount(accountId),
|
|
625
|
-
this.rpcConfig,
|
|
626
|
-
);
|
|
627
|
-
let accountForConfigRefresh: Account | null = localAccount ?? null;
|
|
643
|
+
let accountForConfigRefresh: Account | null = localAccount;
|
|
628
644
|
|
|
629
645
|
const guardianCommitment = normalizeHexWord(state.commitment);
|
|
630
646
|
const localCommitment = localAccount
|
|
@@ -634,24 +650,44 @@ export class Multisig {
|
|
|
634
650
|
if (!localAccount || localCommitment !== guardianCommitment) {
|
|
635
651
|
const accountBytes = base64ToUint8Array(state.stateDataBase64);
|
|
636
652
|
const incomingAccount = Account.deserialize(accountBytes);
|
|
637
|
-
if (await this.isSafeToOverwriteLocalState(incomingAccount, localAccount)) {
|
|
638
|
-
await
|
|
653
|
+
if (await this.isSafeToOverwriteLocalState(incomingAccount, localAccount ?? undefined)) {
|
|
654
|
+
await this.midenClient.accounts.insert({ account: incomingAccount, overwrite: true });
|
|
639
655
|
accountForConfigRefresh = incomingAccount;
|
|
640
656
|
}
|
|
641
657
|
}
|
|
642
658
|
|
|
643
659
|
this.refreshConfigFromAccount(accountForConfigRefresh);
|
|
644
660
|
|
|
645
|
-
return state;
|
|
661
|
+
return { source: 'guardian', state };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* The pre-check decision: GUARDIAN has nothing newer when its canonical
|
|
666
|
+
* nonce is below the local nonce, or equal to it at the same commitment.
|
|
667
|
+
* An equal nonce with a different commitment is divergence, which the
|
|
668
|
+
* full fetch reports (see `isSafeToOverwriteLocalState`).
|
|
669
|
+
*/
|
|
670
|
+
private guardianIsNotAhead(
|
|
671
|
+
localAccount: Account,
|
|
672
|
+
localNonce: bigint,
|
|
673
|
+
guardianCommitment: string,
|
|
674
|
+
guardianNonce: bigint,
|
|
675
|
+
): boolean {
|
|
676
|
+
if (guardianNonce < localNonce) {
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
if (guardianNonce > localNonce) {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
return (
|
|
683
|
+
normalizeHexWord(guardianCommitment) ===
|
|
684
|
+
normalizeHexWord(localAccount.to_commitment().toHex())
|
|
685
|
+
);
|
|
646
686
|
}
|
|
647
687
|
|
|
648
688
|
async verifyStateCommitment(): Promise<AccountStateVerificationResult> {
|
|
649
689
|
const accountId = AccountId.fromHex(this._accountId);
|
|
650
|
-
const
|
|
651
|
-
const localAccount = await retryRpcRead(
|
|
652
|
-
() => webClient.getAccount(accountId),
|
|
653
|
-
this.rpcConfig,
|
|
654
|
-
);
|
|
690
|
+
const localAccount = await this.readStoreAccount();
|
|
655
691
|
|
|
656
692
|
if (!localAccount) {
|
|
657
693
|
throw new Error(
|
|
@@ -722,14 +758,10 @@ export class Multisig {
|
|
|
722
758
|
* fail.
|
|
723
759
|
*/
|
|
724
760
|
private async syncNetworkOnly(): Promise<Account | null> {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
this.rpcConfig,
|
|
730
|
-
);
|
|
731
|
-
this.refreshConfigFromAccount(account ?? null);
|
|
732
|
-
return account ?? null;
|
|
761
|
+
await this.syncChain();
|
|
762
|
+
const account = await this.readStoreAccount();
|
|
763
|
+
this.refreshConfigFromAccount(account);
|
|
764
|
+
return account;
|
|
733
765
|
}
|
|
734
766
|
|
|
735
767
|
private refreshConfigFromAccount(account: Account | null): void {
|
|
@@ -808,11 +840,12 @@ export class Multisig {
|
|
|
808
840
|
* Every synced proposal's metadata is checked against its signed summary
|
|
809
841
|
* and the outcome is recorded in {@link Proposal.verification}. One that
|
|
810
842
|
* fails is still cached and returned, so a single stale or corrupt
|
|
811
|
-
* proposal cannot hide the others (issue #462
|
|
812
|
-
* proposal
|
|
813
|
-
* with `retryable: true` means a transient node error
|
|
814
|
-
*
|
|
815
|
-
*
|
|
843
|
+
* proposal cannot hide the others (issue #462). Verification re-executes
|
|
844
|
+
* each proposal at the chain tip, so the Miden client is synced once first.
|
|
845
|
+
* `failed` with `retryable: true` means a transient node error or chain
|
|
846
|
+
* state this client had not caught up with, worth syncing again;
|
|
847
|
+
* `retryable: false` means the proposal cannot be reproduced and must be
|
|
848
|
+
* re-proposed. `signProposal` and `executeProposal` re-verify and
|
|
816
849
|
* refuse a failed proposal. A failed proposal still counts as reported, so
|
|
817
850
|
* it is pruned like any other once GUARDIAN stops listing it.
|
|
818
851
|
*
|
|
@@ -852,6 +885,14 @@ export class Multisig {
|
|
|
852
885
|
const factory = this.proposalFactory();
|
|
853
886
|
|
|
854
887
|
const reported = new Map<string, { delta: (typeof deltas)[number]; verified: Proposal }>();
|
|
888
|
+
if (deltas.length > 0) {
|
|
889
|
+
// Verification re-executes at the store's sync height; bring it to the
|
|
890
|
+
// tip once for the whole listing. Best effort: a node outage then shows
|
|
891
|
+
// up on each proposal as a retryable failure instead of failing the sync.
|
|
892
|
+
await this.syncChain().catch((error) =>
|
|
893
|
+
console.warn('Could not sync the Miden client before verifying proposals', error),
|
|
894
|
+
);
|
|
895
|
+
}
|
|
855
896
|
for (const delta of deltas) {
|
|
856
897
|
const proposalId = normalizeHexWord(
|
|
857
898
|
computeCommitmentFromTxSummary(delta.deltaPayload.txSummary.data)
|
|
@@ -1035,7 +1076,6 @@ export class Multisig {
|
|
|
1035
1076
|
...legacyArgs: never[]
|
|
1036
1077
|
): Promise<Proposal> {
|
|
1037
1078
|
const proposalNonce = resolveProposalNonce('createAddSignerProposal', options, legacyArgs);
|
|
1038
|
-
const webClient = await this.getRawClient();
|
|
1039
1079
|
const targetThreshold = options.newThreshold ?? this.threshold;
|
|
1040
1080
|
const targetSignerCommitments = [...this.signerCommitments, newCommitment];
|
|
1041
1081
|
// What `update_signers_and_threshold` rejects on-chain, and what the auth
|
|
@@ -1049,13 +1089,13 @@ export class Multisig {
|
|
|
1049
1089
|
this.warnOnOverrideDilution(targetSignerCommitments.length);
|
|
1050
1090
|
|
|
1051
1091
|
const { request, salt } = await buildUpdateSignersTransactionRequest(
|
|
1052
|
-
|
|
1092
|
+
this.midenClient,
|
|
1053
1093
|
targetThreshold,
|
|
1054
1094
|
targetSignerCommitments,
|
|
1055
1095
|
this.proposalRequestOptions(options),
|
|
1056
1096
|
);
|
|
1057
1097
|
|
|
1058
|
-
const { summary, anchor } = await executeForSummary(
|
|
1098
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
1059
1099
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
1060
1100
|
anchor.free();
|
|
1061
1101
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -1086,7 +1126,6 @@ export class Multisig {
|
|
|
1086
1126
|
...legacyArgs: never[]
|
|
1087
1127
|
): Promise<Proposal> {
|
|
1088
1128
|
const proposalNonce = resolveProposalNonce('createRemoveSignerProposal', options, legacyArgs);
|
|
1089
|
-
const webClient = await this.getRawClient();
|
|
1090
1129
|
const normalizedRemove = signerToRemove.toLowerCase();
|
|
1091
1130
|
const targetSignerCommitments = this.signerCommitments.filter(
|
|
1092
1131
|
(c) => c.toLowerCase() !== normalizedRemove
|
|
@@ -1108,13 +1147,13 @@ export class Multisig {
|
|
|
1108
1147
|
}
|
|
1109
1148
|
|
|
1110
1149
|
const { request, salt } = await buildUpdateSignersTransactionRequest(
|
|
1111
|
-
|
|
1150
|
+
this.midenClient,
|
|
1112
1151
|
targetThreshold,
|
|
1113
1152
|
targetSignerCommitments,
|
|
1114
1153
|
this.proposalRequestOptions(options),
|
|
1115
1154
|
);
|
|
1116
1155
|
|
|
1117
|
-
const { summary, anchor } = await executeForSummary(
|
|
1156
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
1118
1157
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
1119
1158
|
anchor.free();
|
|
1120
1159
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -1143,7 +1182,6 @@ export class Multisig {
|
|
|
1143
1182
|
options: CreateProposalOptions = {},
|
|
1144
1183
|
): Promise<Proposal> {
|
|
1145
1184
|
const proposalNonce = resolveProposalNonce('createChangeThresholdProposal', options);
|
|
1146
|
-
const webClient = await this.getRawClient();
|
|
1147
1185
|
if (newThreshold < 1 || newThreshold > this.signerCommitments.length) {
|
|
1148
1186
|
throw new Error(
|
|
1149
1187
|
`Invalid threshold ${newThreshold}. Must be between 1 and ${this.signerCommitments.length}`
|
|
@@ -1155,13 +1193,13 @@ export class Multisig {
|
|
|
1155
1193
|
}
|
|
1156
1194
|
|
|
1157
1195
|
const { request, salt } = await buildUpdateSignersTransactionRequest(
|
|
1158
|
-
|
|
1196
|
+
this.midenClient,
|
|
1159
1197
|
newThreshold,
|
|
1160
1198
|
this.signerCommitments,
|
|
1161
1199
|
this.proposalRequestOptions(options),
|
|
1162
1200
|
);
|
|
1163
1201
|
|
|
1164
|
-
const { summary, anchor } = await executeForSummary(
|
|
1202
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
1165
1203
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
1166
1204
|
anchor.free();
|
|
1167
1205
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -1185,7 +1223,6 @@ export class Multisig {
|
|
|
1185
1223
|
options: CreateProposalOptions = {},
|
|
1186
1224
|
): Promise<Proposal> {
|
|
1187
1225
|
const proposalNonce = resolveProposalNonce('createUpdateProcedureThresholdProposal', options);
|
|
1188
|
-
const webClient = await this.getRawClient();
|
|
1189
1226
|
if (targetThreshold < 0 || targetThreshold > this.signerCommitments.length) {
|
|
1190
1227
|
throw new Error(
|
|
1191
1228
|
`Invalid threshold ${targetThreshold}. Must be between 0 and ${this.signerCommitments.length}`
|
|
@@ -1204,13 +1241,13 @@ export class Multisig {
|
|
|
1204
1241
|
}
|
|
1205
1242
|
|
|
1206
1243
|
const { request, salt } = await buildUpdateProcedureThresholdTransactionRequest(
|
|
1207
|
-
|
|
1244
|
+
this.midenClient,
|
|
1208
1245
|
targetProcedure,
|
|
1209
1246
|
targetThreshold,
|
|
1210
1247
|
this.proposalRequestOptions(options),
|
|
1211
1248
|
);
|
|
1212
1249
|
|
|
1213
|
-
const { summary, anchor } = await executeForSummary(
|
|
1250
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
1214
1251
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
1215
1252
|
anchor.free();
|
|
1216
1253
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -1267,7 +1304,6 @@ export class Multisig {
|
|
|
1267
1304
|
newGuardianPubkey: string,
|
|
1268
1305
|
approvalExpirationDelta: number | undefined,
|
|
1269
1306
|
): Promise<{ summaryBase64: string; metadata: ProposalMetadata }> {
|
|
1270
|
-
const webClient = await this.getRawClient();
|
|
1271
1307
|
// What `auth_tx_guarded_multisig` asserts after a guardian rotation, checked
|
|
1272
1308
|
// before any signature is collected.
|
|
1273
1309
|
validateMultisigConfig({
|
|
@@ -1278,7 +1314,7 @@ export class Multisig {
|
|
|
1278
1314
|
await this.verifyGuardianEndpointCommitment(newGuardianEndpoint, newGuardianPubkey);
|
|
1279
1315
|
|
|
1280
1316
|
const { request, salt } = await buildUpdateGuardianTransactionRequest(
|
|
1281
|
-
|
|
1317
|
+
this.midenClient,
|
|
1282
1318
|
newGuardianPubkey,
|
|
1283
1319
|
{
|
|
1284
1320
|
accountId: this._accountId,
|
|
@@ -1287,7 +1323,7 @@ export class Multisig {
|
|
|
1287
1323
|
},
|
|
1288
1324
|
);
|
|
1289
1325
|
|
|
1290
|
-
const { summary, anchor } = await executeForSummary(
|
|
1326
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
1291
1327
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
1292
1328
|
anchor.free();
|
|
1293
1329
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -1378,16 +1414,14 @@ export class Multisig {
|
|
|
1378
1414
|
options: CreateProposalOptions = {},
|
|
1379
1415
|
): Promise<Proposal> {
|
|
1380
1416
|
const proposalNonce = resolveProposalNonce('createConsumeNotesProposal', options);
|
|
1381
|
-
const webClient = await this.getRawClient();
|
|
1382
1417
|
if (noteIds.length === 0) {
|
|
1383
1418
|
throw new Error('At least one note ID is required');
|
|
1384
1419
|
}
|
|
1385
1420
|
|
|
1386
1421
|
// Fetch notes locally (proposer has them per FR-012); embed for v2 verification.
|
|
1387
|
-
const rawClient = await getRawMidenClient(webClient);
|
|
1388
1422
|
const fetchedNotes: Note[] = [];
|
|
1389
1423
|
for (const noteIdHex of noteIds) {
|
|
1390
|
-
const inputNoteRecord = await
|
|
1424
|
+
const inputNoteRecord = await this.midenClient.notes.get(noteIdHex);
|
|
1391
1425
|
if (!inputNoteRecord) {
|
|
1392
1426
|
throw new LegacyConsumeNotesNoteMissingError(noteIdHex);
|
|
1393
1427
|
}
|
|
@@ -1400,12 +1434,12 @@ export class Multisig {
|
|
|
1400
1434
|
const embeddedNotes = fetchedNotes.map((n) => noteToBase64(n));
|
|
1401
1435
|
|
|
1402
1436
|
const { request, salt } = await buildConsumeNotesTransactionRequestFromNotes(
|
|
1403
|
-
|
|
1437
|
+
this.midenClient,
|
|
1404
1438
|
fetchedNotes,
|
|
1405
1439
|
this.proposalRequestOptions(options),
|
|
1406
1440
|
);
|
|
1407
1441
|
|
|
1408
|
-
const { summary, anchor } = await executeForSummary(
|
|
1442
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
1409
1443
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
1410
1444
|
anchor.free();
|
|
1411
1445
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -1452,7 +1486,6 @@ export class Multisig {
|
|
|
1452
1486
|
...legacyArgs: never[]
|
|
1453
1487
|
): Promise<Proposal> {
|
|
1454
1488
|
const proposalNonce = resolveProposalNonce('createP2idProposal', options, legacyArgs);
|
|
1455
|
-
const webClient = await this.getRawClient();
|
|
1456
1489
|
if (amount <= 0n) {
|
|
1457
1490
|
throw new Error('Amount must be greater than 0');
|
|
1458
1491
|
}
|
|
@@ -1461,7 +1494,7 @@ export class Multisig {
|
|
|
1461
1494
|
// CreateP2idProposalOptions can't be silently dropped before the builder.
|
|
1462
1495
|
const { nonce: _nonce, ...noteOptions } = options;
|
|
1463
1496
|
const { request, salt } = await buildP2idTransactionRequest(
|
|
1464
|
-
|
|
1497
|
+
this.midenClient,
|
|
1465
1498
|
this._accountId,
|
|
1466
1499
|
recipientId,
|
|
1467
1500
|
faucetId,
|
|
@@ -1469,7 +1502,7 @@ export class Multisig {
|
|
|
1469
1502
|
noteOptions,
|
|
1470
1503
|
);
|
|
1471
1504
|
|
|
1472
|
-
const { summary, anchor } = await executeForSummary(
|
|
1505
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
1473
1506
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
1474
1507
|
anchor.free();
|
|
1475
1508
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -1496,49 +1529,25 @@ export class Multisig {
|
|
|
1496
1529
|
/**
|
|
1497
1530
|
* Get notes that can be consumed by this multisig account.
|
|
1498
1531
|
*
|
|
1499
|
-
* Returns
|
|
1500
|
-
*
|
|
1532
|
+
* Returns the notes the Miden client's last sync screened as consumable by
|
|
1533
|
+
* this account right now; block-locked notes are left out.
|
|
1501
1534
|
*/
|
|
1502
1535
|
async getConsumableNotes(): Promise<ConsumableNote[]> {
|
|
1503
|
-
const
|
|
1504
|
-
const webClient = await this.getRawClient();
|
|
1536
|
+
const records = await this.midenClient.notes.listAvailable({ account: this._accountId });
|
|
1505
1537
|
|
|
1506
|
-
// Get consumable notes for this account
|
|
1507
|
-
const consumableRecords = await webClient.getConsumableNotes(accountId);
|
|
1508
|
-
|
|
1509
|
-
// Convert to our simplified ConsumableNote type
|
|
1510
1538
|
const notes: ConsumableNote[] = [];
|
|
1511
|
-
for (const
|
|
1512
|
-
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
const canConsumeNow = consumability.some(
|
|
1517
|
-
(c) => c.accountId().toString().toLowerCase() === this._accountId.toLowerCase() &&
|
|
1518
|
-
c.consumptionStatus().consumableAfterBlock() === undefined
|
|
1519
|
-
);
|
|
1520
|
-
|
|
1521
|
-
if (canConsumeNow) {
|
|
1522
|
-
// Miden 0.15: InputNoteRecord.id() is `NoteId | undefined`; skip id-less records.
|
|
1523
|
-
const id = inputNote.id();
|
|
1524
|
-
if (id === undefined) {
|
|
1525
|
-
continue;
|
|
1526
|
-
}
|
|
1527
|
-
const noteId = id.toString();
|
|
1528
|
-
const details = inputNote.details();
|
|
1529
|
-
const fungibleAssets = details.assets().fungibleAssets();
|
|
1530
|
-
|
|
1531
|
-
// Extract assets
|
|
1532
|
-
const assets: NoteAsset[] = [];
|
|
1533
|
-
for (const asset of fungibleAssets) {
|
|
1534
|
-
assets.push({
|
|
1535
|
-
faucetId: asset.faucetId().toString(),
|
|
1536
|
-
amount: asset.amount(),
|
|
1537
|
-
});
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
notes.push({ id: noteId, assets });
|
|
1539
|
+
for (const inputNote of records) {
|
|
1540
|
+
// Miden 0.15: InputNoteRecord.id() is `NoteId | undefined`; skip id-less records.
|
|
1541
|
+
const id = inputNote.id();
|
|
1542
|
+
if (id === undefined) {
|
|
1543
|
+
continue;
|
|
1541
1544
|
}
|
|
1545
|
+
const assets: NoteAsset[] = inputNote
|
|
1546
|
+
.details()
|
|
1547
|
+
.assets()
|
|
1548
|
+
.fungibleAssets()
|
|
1549
|
+
.map((asset) => ({ faucetId: asset.faucetId().toString(), amount: asset.amount() }));
|
|
1550
|
+
notes.push({ id: id.toString(), assets });
|
|
1542
1551
|
}
|
|
1543
1552
|
|
|
1544
1553
|
return notes;
|
|
@@ -1562,12 +1571,11 @@ export class Multisig {
|
|
|
1562
1571
|
* @returns Serialized note file bytes
|
|
1563
1572
|
*/
|
|
1564
1573
|
async exportNoteToBytes(noteId: string): Promise<Uint8Array> {
|
|
1565
|
-
const webClient = await this.getRawClient();
|
|
1566
1574
|
const trimmedNoteId = noteId.trim();
|
|
1567
1575
|
|
|
1568
|
-
let record;
|
|
1576
|
+
let record: OutputNoteRecord | undefined;
|
|
1569
1577
|
try {
|
|
1570
|
-
record = await
|
|
1578
|
+
[record] = await this.midenClient.notes.listSent({ ids: [trimmedNoteId] });
|
|
1571
1579
|
} catch (err) {
|
|
1572
1580
|
const detail = err instanceof Error ? err.message : String(err);
|
|
1573
1581
|
throw new Error(
|
|
@@ -1583,7 +1591,7 @@ export class Multisig {
|
|
|
1583
1591
|
const format = record.inclusionProof()
|
|
1584
1592
|
? NoteExportFormat.Full
|
|
1585
1593
|
: NoteExportFormat.Details;
|
|
1586
|
-
const noteFile = await
|
|
1594
|
+
const noteFile = await this.midenClient.notes.export(trimmedNoteId, { format });
|
|
1587
1595
|
return noteFile.serialize();
|
|
1588
1596
|
}
|
|
1589
1597
|
|
|
@@ -1630,8 +1638,6 @@ export class Multisig {
|
|
|
1630
1638
|
* commitment for a details-only file
|
|
1631
1639
|
*/
|
|
1632
1640
|
async importNoteFromBytes(noteBytes: Uint8Array): Promise<string> {
|
|
1633
|
-
const webClient = await this.getRawClient();
|
|
1634
|
-
|
|
1635
1641
|
let noteFile: NoteFile;
|
|
1636
1642
|
try {
|
|
1637
1643
|
noteFile = NoteFile.deserialize(noteBytes);
|
|
@@ -1640,7 +1646,7 @@ export class Multisig {
|
|
|
1640
1646
|
throw new Error(`failed to decode note file: ${detail}`);
|
|
1641
1647
|
}
|
|
1642
1648
|
|
|
1643
|
-
return
|
|
1649
|
+
return this.midenClient.notes.import(noteFile);
|
|
1644
1650
|
}
|
|
1645
1651
|
|
|
1646
1652
|
/**
|
|
@@ -1972,6 +1978,8 @@ export class Multisig {
|
|
|
1972
1978
|
|
|
1973
1979
|
async signProposal(proposalId: string): Promise<Proposal> {
|
|
1974
1980
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1981
|
+
// Verification re-executes the proposal at the store's sync height.
|
|
1982
|
+
await this.syncChain();
|
|
1975
1983
|
const existingProposal = await this.getProposalForSigning(proposalId, normalizedProposalId);
|
|
1976
1984
|
if (!existingProposal) {
|
|
1977
1985
|
throw new Error(`Proposal not found: ${proposalId}`);
|
|
@@ -2019,9 +2027,14 @@ export class Multisig {
|
|
|
2019
2027
|
return this.proposals.get(proposalId) ?? this.proposals.get(normalizedProposalId);
|
|
2020
2028
|
}
|
|
2021
2029
|
|
|
2030
|
+
/**
|
|
2031
|
+
* Builds the final, fully signed request for a ready proposal, for a caller
|
|
2032
|
+
* that proves and submits it with its own pipeline. The request declares the
|
|
2033
|
+
* block its summary binds, so execute it at the chain tip, without an
|
|
2034
|
+
* anchor, on a client synced to at least that block.
|
|
2035
|
+
*/
|
|
2022
2036
|
async createTransactionProposalRequest(proposalId: string): Promise<TransactionRequest> {
|
|
2023
|
-
const { finalRequest
|
|
2024
|
-
anchor.free();
|
|
2037
|
+
const { finalRequest } = await this.prepareProposalExecution(proposalId);
|
|
2025
2038
|
return finalRequest;
|
|
2026
2039
|
}
|
|
2027
2040
|
|
|
@@ -2031,25 +2044,19 @@ export class Multisig {
|
|
|
2031
2044
|
* @param proposalId - The proposal commitment/ID
|
|
2032
2045
|
*/
|
|
2033
2046
|
async executeProposal(proposalId: string): Promise<void> {
|
|
2034
|
-
const { metadata, finalRequest, proposal
|
|
2035
|
-
await this.prepareProposalExecution(proposalId);
|
|
2036
|
-
|
|
2037
|
-
try {
|
|
2038
|
-
if (metadata.proposalType === 'switch_guardian') {
|
|
2039
|
-
// #417: import notes embedded in pending proposals from the old
|
|
2040
|
-
// GUARDIAN. Must run before the switch executes and repoints;
|
|
2041
|
-
// best-effort and bounded — see preservePreSwitchProposalNotes.
|
|
2042
|
-
await this.preservePreSwitchProposalNotes();
|
|
2043
|
-
}
|
|
2047
|
+
const { metadata, finalRequest, proposal } = await this.prepareProposalExecution(proposalId);
|
|
2044
2048
|
|
|
2045
|
-
|
|
2046
|
-
//
|
|
2047
|
-
//
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
anchor.free();
|
|
2049
|
+
if (metadata.proposalType === 'switch_guardian') {
|
|
2050
|
+
// #417: import notes embedded in pending proposals from the old
|
|
2051
|
+
// GUARDIAN. Must run before the switch executes and repoints;
|
|
2052
|
+
// best-effort and bounded — see preservePreSwitchProposalNotes.
|
|
2053
|
+
await this.preservePreSwitchProposalNotes();
|
|
2051
2054
|
}
|
|
2052
2055
|
|
|
2056
|
+
// Execute at the chain tip. The request declares the block the signed
|
|
2057
|
+
// summary binds, so the summary the cosigners signed reproduces there.
|
|
2058
|
+
await this.submitAtTip(finalRequest);
|
|
2059
|
+
|
|
2053
2060
|
if (metadata.proposalType === 'switch_guardian') {
|
|
2054
2061
|
if (!metadata.newGuardianEndpoint || !metadata.newGuardianPubkey) {
|
|
2055
2062
|
throw new Error('Switch GUARDIAN proposal metadata is incomplete after execution');
|
|
@@ -2109,11 +2116,12 @@ export class Multisig {
|
|
|
2109
2116
|
* Submit an integration-built transaction (advice already injected). Mirrors
|
|
2110
2117
|
* the Rust `submit_transaction`; used by the custom proposal producer flow
|
|
2111
2118
|
* after `prepareCustomExecution` rebuilds its request with the returned advice.
|
|
2112
|
-
* The transaction is executed at the
|
|
2113
|
-
*
|
|
2119
|
+
* The transaction is executed at the chain tip, so the request has to declare
|
|
2120
|
+
* the block its auth args bind (`feeAwareTransactionRequestBuilder` does).
|
|
2114
2121
|
*/
|
|
2115
2122
|
async submitTransaction(proposalId: string, request: TransactionRequest): Promise<void> {
|
|
2116
2123
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
2124
|
+
await this.syncChain();
|
|
2117
2125
|
const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
|
|
2118
2126
|
const existing = this.getLocalProposal(proposalId);
|
|
2119
2127
|
const proposal = this.proposalFactory().fromDelta(
|
|
@@ -2135,11 +2143,33 @@ export class Multisig {
|
|
|
2135
2143
|
`Proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`,
|
|
2136
2144
|
);
|
|
2137
2145
|
}
|
|
2138
|
-
|
|
2139
|
-
await this.proverWorkflow.submitAt(AccountId.fromHex(this._accountId), request, anchor);
|
|
2140
2146
|
} finally {
|
|
2141
2147
|
anchor.free();
|
|
2142
2148
|
}
|
|
2149
|
+
await this.submitAtTip(request);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
/**
|
|
2153
|
+
* Executes `request` at the chain tip, then proves, submits and applies it,
|
|
2154
|
+
* syncing first when this client is still below the block the request binds.
|
|
2155
|
+
*/
|
|
2156
|
+
private async submitAtTip(request: TransactionRequest): Promise<void> {
|
|
2157
|
+
await prepareTipExecution(this.midenClient, request, () => this.syncChain());
|
|
2158
|
+
await this.proverWorkflow.submit(AccountId.fromHex(this._accountId), request);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
/**
|
|
2162
|
+
* Brings the Miden client to the chain tip before a proposal is re-executed:
|
|
2163
|
+
* an execution loads foreign accounts, the fee faucet among them, at the
|
|
2164
|
+
* store's sync height, which a node prunes about 50 blocks later.
|
|
2165
|
+
*/
|
|
2166
|
+
private async syncChain(): Promise<void> {
|
|
2167
|
+
await retryRpcRead(() => this.midenClient.syncChain(), this.rpcConfig);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/** {@link syncToBoundBlock} with this client's RPC retry policy. */
|
|
2171
|
+
private async syncToBoundBlock(boundBlockNum: number): Promise<void> {
|
|
2172
|
+
await syncToBoundBlock(this.midenClient, boundBlockNum, () => this.syncChain());
|
|
2143
2173
|
}
|
|
2144
2174
|
|
|
2145
2175
|
/**
|
|
@@ -2170,9 +2200,8 @@ export class Multisig {
|
|
|
2170
2200
|
);
|
|
2171
2201
|
}
|
|
2172
2202
|
|
|
2173
|
-
const webClient = await this.getRawClient();
|
|
2174
2203
|
const request = deserializeTransactionRequest(transactionRequestBytes);
|
|
2175
|
-
const { summary, anchor } = await executeForSummary(
|
|
2204
|
+
const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
|
|
2176
2205
|
const chainAnchor = chainAnchorToBase64(anchor);
|
|
2177
2206
|
anchor.free();
|
|
2178
2207
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -2203,6 +2232,8 @@ export class Multisig {
|
|
|
2203
2232
|
transactionRequestBytes: Uint8Array,
|
|
2204
2233
|
): Promise<AdviceMap> {
|
|
2205
2234
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
2235
|
+
// The binding probe re-executes the producer's request at the store's sync height.
|
|
2236
|
+
await this.syncChain();
|
|
2206
2237
|
const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
|
|
2207
2238
|
const existing = this.getLocalProposal(proposalId);
|
|
2208
2239
|
const proposal = this.proposalFactory().fromDelta(
|
|
@@ -2237,13 +2268,11 @@ export class Multisig {
|
|
|
2237
2268
|
|
|
2238
2269
|
const bindingRequest = deserializeTransactionRequest(transactionRequestBytes);
|
|
2239
2270
|
|
|
2240
|
-
//
|
|
2241
|
-
//
|
|
2242
|
-
//
|
|
2243
|
-
//
|
|
2244
|
-
// before executing against it.
|
|
2271
|
+
// The anchor arrives from an untrusted party via GUARDIAN, so its block
|
|
2272
|
+
// commitment is checked against the signed summary: it has to name the
|
|
2273
|
+
// block the summary binds. The probe itself runs at the chain tip, where
|
|
2274
|
+
// the request's declared bound block reproduces the signed summary.
|
|
2245
2275
|
const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
|
|
2246
|
-
let derivedCommitmentHex: string;
|
|
2247
2276
|
try {
|
|
2248
2277
|
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
2249
2278
|
const summaryBlockCommitment = normalizeHexWord(txSummary.blockCommitment().toHex());
|
|
@@ -2252,12 +2281,11 @@ export class Multisig {
|
|
|
2252
2281
|
`Custom proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`,
|
|
2253
2282
|
);
|
|
2254
2283
|
}
|
|
2255
|
-
const webClient = await this.getRawClient();
|
|
2256
|
-
const derived = await executeForSummaryAt(webClient, this._accountId, bindingRequest, anchor);
|
|
2257
|
-
derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
|
|
2258
2284
|
} finally {
|
|
2259
2285
|
anchor.free();
|
|
2260
2286
|
}
|
|
2287
|
+
const derived = await executeForSummaryAtTip(this.midenClient, this._accountId, bindingRequest);
|
|
2288
|
+
const derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
|
|
2261
2289
|
if (derivedCommitmentHex !== signedCommitmentHex) {
|
|
2262
2290
|
throw new Error(
|
|
2263
2291
|
`Custom proposal binding mismatch: expected ${signedCommitmentHex}, got ${derivedCommitmentHex}`,
|
|
@@ -2396,14 +2424,13 @@ export class Multisig {
|
|
|
2396
2424
|
}
|
|
2397
2425
|
|
|
2398
2426
|
/**
|
|
2399
|
-
* The returned
|
|
2400
|
-
*
|
|
2427
|
+
* The returned request declares the block the signed summary binds, and
|
|
2428
|
+
* executes at the chain tip.
|
|
2401
2429
|
*/
|
|
2402
2430
|
private async prepareProposalExecution(proposalId: string): Promise<{
|
|
2403
2431
|
finalRequest: TransactionRequest;
|
|
2404
2432
|
metadata: ProposalMetadata;
|
|
2405
2433
|
proposal: Proposal;
|
|
2406
|
-
anchor: ChainAnchor;
|
|
2407
2434
|
}> {
|
|
2408
2435
|
const proposal = this.getLocalProposal(proposalId);
|
|
2409
2436
|
if (!proposal) {
|
|
@@ -2411,6 +2438,8 @@ export class Multisig {
|
|
|
2411
2438
|
}
|
|
2412
2439
|
|
|
2413
2440
|
this.proposalFactory().assertAccountId(proposal.accountId);
|
|
2441
|
+
// Verification and execution run at the store's sync height.
|
|
2442
|
+
await this.syncChain();
|
|
2414
2443
|
await this.verifyProposalMetadataBinding(proposal);
|
|
2415
2444
|
|
|
2416
2445
|
const metadata = proposal.metadata;
|
|
@@ -2560,17 +2589,21 @@ export class Multisig {
|
|
|
2560
2589
|
}
|
|
2561
2590
|
|
|
2562
2591
|
const anchor = this.requireProposalAnchor(proposalId, metadata);
|
|
2592
|
+
let binding: ProposalRequestBinding;
|
|
2563
2593
|
try {
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
proposalRequestBinding(txSummary, anchor, saltHex),
|
|
2567
|
-
adviceMap,
|
|
2568
|
-
);
|
|
2569
|
-
return { finalRequest, metadata, proposal, anchor };
|
|
2570
|
-
} catch (error) {
|
|
2594
|
+
binding = proposalRequestBinding(txSummary, anchor, saltHex);
|
|
2595
|
+
} finally {
|
|
2571
2596
|
anchor.free();
|
|
2572
|
-
throw error;
|
|
2573
2597
|
}
|
|
2598
|
+
// A switch_guardian proposal is verified without a rebuild, so this may be
|
|
2599
|
+
// the first time this client needs the chain at the bound block.
|
|
2600
|
+
await this.syncToBoundBlock(binding.boundBlockNum);
|
|
2601
|
+
const finalRequest = await this.buildTransactionRequestFromMetadata(
|
|
2602
|
+
metadata,
|
|
2603
|
+
binding,
|
|
2604
|
+
adviceMap,
|
|
2605
|
+
);
|
|
2606
|
+
return { finalRequest, metadata, proposal };
|
|
2574
2607
|
}
|
|
2575
2608
|
|
|
2576
2609
|
/**
|
|
@@ -2655,6 +2688,10 @@ export class Multisig {
|
|
|
2655
2688
|
|
|
2656
2689
|
const proposal = this.proposalFactory().fromExported(exported);
|
|
2657
2690
|
|
|
2691
|
+
// Verification re-executes every built-in type at the store's sync height.
|
|
2692
|
+
if (proposal.metadata.proposalType !== 'custom') {
|
|
2693
|
+
await this.syncChain();
|
|
2694
|
+
}
|
|
2658
2695
|
await this.verifyProposalMetadataBinding(proposal);
|
|
2659
2696
|
this.proposals.set(proposal.id, proposal);
|
|
2660
2697
|
|
|
@@ -2695,6 +2732,10 @@ export class Multisig {
|
|
|
2695
2732
|
throw new Error('You have already signed this proposal');
|
|
2696
2733
|
}
|
|
2697
2734
|
|
|
2735
|
+
// Verification re-executes every built-in type at the store's sync height.
|
|
2736
|
+
if (proposal.metadata?.proposalType !== 'custom') {
|
|
2737
|
+
await this.syncChain();
|
|
2738
|
+
}
|
|
2698
2739
|
const commitmentToSign = await this.verifyProposalMetadataBinding(proposal);
|
|
2699
2740
|
|
|
2700
2741
|
// Sign the commitment
|
|
@@ -2758,7 +2799,7 @@ export class Multisig {
|
|
|
2758
2799
|
} catch (error) {
|
|
2759
2800
|
proposal.verification = {
|
|
2760
2801
|
status: 'failed',
|
|
2761
|
-
retryable: isTransientRpcError(error),
|
|
2802
|
+
retryable: isTransientRpcError(error) || isStaleChainError(error),
|
|
2762
2803
|
message: error instanceof Error ? error.message : String(error),
|
|
2763
2804
|
};
|
|
2764
2805
|
throw error;
|
|
@@ -2770,10 +2811,11 @@ export class Multisig {
|
|
|
2770
2811
|
|
|
2771
2812
|
const summary = TransactionSummary.deserialize(base64ToUint8Array(proposal.txSummary));
|
|
2772
2813
|
|
|
2773
|
-
// The anchor arrives from an untrusted party via GUARDIAN, so check
|
|
2774
|
-
//
|
|
2775
|
-
//
|
|
2776
|
-
//
|
|
2814
|
+
// The anchor arrives from an untrusted party via GUARDIAN, so check that it
|
|
2815
|
+
// names the block the signed summary binds: the rebuild below binds the
|
|
2816
|
+
// block it names. Nothing executes against it; the rebuild runs at the
|
|
2817
|
+
// chain tip. `ChainAnchor.deserialize` already enforced internal
|
|
2818
|
+
// header/chain consistency.
|
|
2777
2819
|
const anchor = this.requireProposalAnchor(proposal.id, proposal.metadata);
|
|
2778
2820
|
try {
|
|
2779
2821
|
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
@@ -2812,6 +2854,12 @@ export class Multisig {
|
|
|
2812
2854
|
return txSummaryCommitment;
|
|
2813
2855
|
}
|
|
2814
2856
|
|
|
2857
|
+
// The rebuild reads the chain's fee faucet from the synced protocol
|
|
2858
|
+
// configuration and executes at the tip, so the store has to have synced
|
|
2859
|
+
// to the block the summary binds; a cosigner that has only just loaded
|
|
2860
|
+
// the account has not.
|
|
2861
|
+
await this.syncToBoundBlock(binding.boundBlockNum);
|
|
2862
|
+
|
|
2815
2863
|
// A consume-notes summary commits to *authenticated* consumption (see
|
|
2816
2864
|
// ensureNotesAuthenticated), which miden-client decides from this store
|
|
2817
2865
|
// alone. Put the store in that mode before the rebuild, or a cosigner
|
|
@@ -2824,8 +2872,7 @@ export class Multisig {
|
|
|
2824
2872
|
}
|
|
2825
2873
|
|
|
2826
2874
|
const request = await this.buildTransactionRequestFromMetadata(proposal.metadata, binding);
|
|
2827
|
-
const
|
|
2828
|
-
const reconstructed = await executeForSummaryAt(webClient, this._accountId, request, anchor);
|
|
2875
|
+
const reconstructed = await executeForSummaryAtTip(this.midenClient, this._accountId, request);
|
|
2829
2876
|
const reconstructedCommitment = normalizeHexWord(reconstructed.toCommitment().toHex());
|
|
2830
2877
|
|
|
2831
2878
|
if (reconstructedCommitment !== txSummaryCommitment) {
|
|
@@ -2838,12 +2885,6 @@ export class Multisig {
|
|
|
2838
2885
|
}
|
|
2839
2886
|
}
|
|
2840
2887
|
|
|
2841
|
-
/**
|
|
2842
|
-
* Decodes a proposal's chain anchor. Throws when absent: a proposal without
|
|
2843
|
-
* an anchor was created at an unknown reference block, so its signed summary
|
|
2844
|
-
* cannot be reproduced, verified, or executed. The caller owns the returned
|
|
2845
|
-
* anchor and must `free()` it once done.
|
|
2846
|
-
*/
|
|
2847
2888
|
/**
|
|
2848
2889
|
* Reads a proposal's salt. Throws when absent, because there is nothing to fall
|
|
2849
2890
|
* back to.
|
|
@@ -2892,12 +2933,17 @@ export class Multisig {
|
|
|
2892
2933
|
return saltHex;
|
|
2893
2934
|
}
|
|
2894
2935
|
|
|
2936
|
+
/**
|
|
2937
|
+
* Decodes a proposal's chain anchor, which names the block its signed summary
|
|
2938
|
+
* binds. Throws when absent: every proposal carries one, so a proposal
|
|
2939
|
+
* without it is malformed and is neither verified nor executed. The caller
|
|
2940
|
+
* owns the returned anchor and must `free()` it once done.
|
|
2941
|
+
*/
|
|
2895
2942
|
private requireProposalAnchor(proposalId: string, metadata: ProposalMetadata): ChainAnchor {
|
|
2896
2943
|
if (!metadata.chainAnchor) {
|
|
2897
2944
|
throw new Error(
|
|
2898
|
-
`Proposal ${proposalId} has no chain anchor
|
|
2899
|
-
'
|
|
2900
|
-
'at the original reference block',
|
|
2945
|
+
`Proposal ${proposalId} has no chain anchor, which names the block its signed ` +
|
|
2946
|
+
'summary binds; it cannot be verified or executed',
|
|
2901
2947
|
);
|
|
2902
2948
|
}
|
|
2903
2949
|
return chainAnchorFromBase64(metadata.chainAnchor);
|
|
@@ -2916,8 +2962,7 @@ export class Multisig {
|
|
|
2916
2962
|
if (expirationBlockNum === undefined) {
|
|
2917
2963
|
return;
|
|
2918
2964
|
}
|
|
2919
|
-
const
|
|
2920
|
-
const syncHeight = await webClient.getSyncHeight();
|
|
2965
|
+
const syncHeight = await this.midenClient.getSyncHeight();
|
|
2921
2966
|
if (syncHeight >= expirationBlockNum) {
|
|
2922
2967
|
throw new Error(
|
|
2923
2968
|
`Proposal ${proposalId} approval expired at block ${expirationBlockNum}; the chain is at ` +
|
|
@@ -2956,13 +3001,12 @@ export class Multisig {
|
|
|
2956
3001
|
metadata: ProposalMetadata,
|
|
2957
3002
|
requestOptions: MultisigRequestOptions,
|
|
2958
3003
|
): Promise<TransactionRequest> {
|
|
2959
|
-
const webClient = await this.getRawClient();
|
|
2960
3004
|
switch (metadata.proposalType) {
|
|
2961
3005
|
case 'add_signer':
|
|
2962
3006
|
case 'remove_signer':
|
|
2963
3007
|
case 'change_threshold': {
|
|
2964
3008
|
const { request } = await buildUpdateSignersTransactionRequest(
|
|
2965
|
-
|
|
3009
|
+
this.midenClient,
|
|
2966
3010
|
metadata.targetThreshold,
|
|
2967
3011
|
metadata.targetSignerCommitments,
|
|
2968
3012
|
requestOptions,
|
|
@@ -2971,7 +3015,7 @@ export class Multisig {
|
|
|
2971
3015
|
}
|
|
2972
3016
|
case 'switch_guardian': {
|
|
2973
3017
|
const { request } = await buildUpdateGuardianTransactionRequest(
|
|
2974
|
-
|
|
3018
|
+
this.midenClient,
|
|
2975
3019
|
metadata.newGuardianPubkey,
|
|
2976
3020
|
requestOptions,
|
|
2977
3021
|
);
|
|
@@ -2979,7 +3023,7 @@ export class Multisig {
|
|
|
2979
3023
|
}
|
|
2980
3024
|
case 'update_procedure_threshold': {
|
|
2981
3025
|
const { request } = await buildUpdateProcedureThresholdTransactionRequest(
|
|
2982
|
-
|
|
3026
|
+
this.midenClient,
|
|
2983
3027
|
metadata.targetProcedure,
|
|
2984
3028
|
metadata.targetThreshold,
|
|
2985
3029
|
requestOptions,
|
|
@@ -2992,7 +3036,7 @@ export class Multisig {
|
|
|
2992
3036
|
if (version === CONSUME_NOTES_METADATA_VERSION_V2) {
|
|
2993
3037
|
const decoded = decodeEmbeddedConsumeNotes(metadata);
|
|
2994
3038
|
const { request } = await buildConsumeNotesTransactionRequestFromNotes(
|
|
2995
|
-
|
|
3039
|
+
this.midenClient,
|
|
2996
3040
|
decoded,
|
|
2997
3041
|
requestOptions,
|
|
2998
3042
|
);
|
|
@@ -3005,7 +3049,7 @@ export class Multisig {
|
|
|
3005
3049
|
throw new UnsupportedMetadataVersionError(version);
|
|
3006
3050
|
}
|
|
3007
3051
|
const { request } = await buildConsumeNotesTransactionRequest(
|
|
3008
|
-
|
|
3052
|
+
this.midenClient,
|
|
3009
3053
|
metadata.noteIds,
|
|
3010
3054
|
requestOptions,
|
|
3011
3055
|
);
|
|
@@ -3015,7 +3059,7 @@ export class Multisig {
|
|
|
3015
3059
|
}
|
|
3016
3060
|
case 'p2id': {
|
|
3017
3061
|
const { request } = await buildP2idTransactionRequest(
|
|
3018
|
-
|
|
3062
|
+
this.midenClient,
|
|
3019
3063
|
this._accountId,
|
|
3020
3064
|
metadata.recipientId,
|
|
3021
3065
|
metadata.faucetId,
|