@openzeppelin/miden-multisig-client 0.17.0 → 0.18.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +159 -46
  2. package/dist/account/builder.d.ts +4 -4
  3. package/dist/account/builder.d.ts.map +1 -1
  4. package/dist/account/builder.js +17 -7
  5. package/dist/account/builder.js.map +1 -1
  6. package/dist/account/layout.d.ts +5 -5
  7. package/dist/account/layout.d.ts.map +1 -1
  8. package/dist/account/layout.js +5 -5
  9. package/dist/account/layout.js.map +1 -1
  10. package/dist/client.d.ts +18 -1
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +79 -6
  13. package/dist/client.js.map +1 -1
  14. package/dist/index.d.ts +6 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +5 -5
  17. package/dist/index.js.map +1 -1
  18. package/dist/multisig/authArgErrors.d.ts +14 -31
  19. package/dist/multisig/authArgErrors.d.ts.map +1 -1
  20. package/dist/multisig/authArgErrors.js +22 -46
  21. package/dist/multisig/authArgErrors.js.map +1 -1
  22. package/dist/multisig/consumeNotesErrors.d.ts +13 -1
  23. package/dist/multisig/consumeNotesErrors.d.ts.map +1 -1
  24. package/dist/multisig/consumeNotesErrors.js +17 -0
  25. package/dist/multisig/consumeNotesErrors.js.map +1 -1
  26. package/dist/multisig/signing.d.ts +1 -1
  27. package/dist/multisig/signing.d.ts.map +1 -1
  28. package/dist/multisig/signing.js +8 -3
  29. package/dist/multisig/signing.js.map +1 -1
  30. package/dist/multisig.d.ts +118 -11
  31. package/dist/multisig.d.ts.map +1 -1
  32. package/dist/multisig.js +421 -141
  33. package/dist/multisig.js.map +1 -1
  34. package/dist/procedures.d.ts +7 -7
  35. package/dist/procedures.js +7 -7
  36. package/dist/proposal/factory.d.ts.map +1 -1
  37. package/dist/proposal/factory.js +7 -0
  38. package/dist/proposal/factory.js.map +1 -1
  39. package/dist/raw-client.d.ts +1 -0
  40. package/dist/raw-client.d.ts.map +1 -1
  41. package/dist/raw-client.js +10 -2
  42. package/dist/raw-client.js.map +1 -1
  43. package/dist/recovery/publicNoteBackfill.js +1 -1
  44. package/dist/recovery/publicNoteBackfill.js.map +1 -1
  45. package/dist/retry/classify.d.ts +3 -0
  46. package/dist/retry/classify.d.ts.map +1 -1
  47. package/dist/retry/classify.js +2 -2
  48. package/dist/retry/classify.js.map +1 -1
  49. package/dist/signer.d.ts +1 -0
  50. package/dist/signer.d.ts.map +1 -1
  51. package/dist/signer.js +1 -0
  52. package/dist/signer.js.map +1 -1
  53. package/dist/signers/index.d.ts +1 -0
  54. package/dist/signers/index.d.ts.map +1 -1
  55. package/dist/signers/index.js +1 -0
  56. package/dist/signers/index.js.map +1 -1
  57. package/dist/signers/ledger.d.ts +25 -0
  58. package/dist/signers/ledger.d.ts.map +1 -0
  59. package/dist/signers/ledger.js +96 -0
  60. package/dist/signers/ledger.js.map +1 -0
  61. package/dist/state/adopt.d.ts +45 -0
  62. package/dist/state/adopt.d.ts.map +1 -0
  63. package/dist/state/adopt.js +101 -0
  64. package/dist/state/adopt.js.map +1 -0
  65. package/dist/transaction/authArgs.d.ts +57 -0
  66. package/dist/transaction/authArgs.d.ts.map +1 -0
  67. package/dist/transaction/authArgs.js +108 -0
  68. package/dist/transaction/authArgs.js.map +1 -0
  69. package/dist/transaction/consumeNotes.d.ts +9 -5
  70. package/dist/transaction/consumeNotes.d.ts.map +1 -1
  71. package/dist/transaction/consumeNotes.js +8 -23
  72. package/dist/transaction/consumeNotes.js.map +1 -1
  73. package/dist/transaction/noteAuthentication.d.ts +39 -0
  74. package/dist/transaction/noteAuthentication.d.ts.map +1 -0
  75. package/dist/transaction/noteAuthentication.js +94 -0
  76. package/dist/transaction/noteAuthentication.js.map +1 -0
  77. package/dist/transaction/options.d.ts +25 -0
  78. package/dist/transaction/options.d.ts.map +1 -1
  79. package/dist/transaction/p2id.d.ts +3 -2
  80. package/dist/transaction/p2id.d.ts.map +1 -1
  81. package/dist/transaction/p2id.js +36 -27
  82. package/dist/transaction/p2id.js.map +1 -1
  83. package/dist/transaction/summary.d.ts +41 -15
  84. package/dist/transaction/summary.d.ts.map +1 -1
  85. package/dist/transaction/summary.js +71 -19
  86. package/dist/transaction/summary.js.map +1 -1
  87. package/dist/transaction/updateGuardian.d.ts +3 -3
  88. package/dist/transaction/updateGuardian.d.ts.map +1 -1
  89. package/dist/transaction/updateGuardian.js +5 -16
  90. package/dist/transaction/updateGuardian.js.map +1 -1
  91. package/dist/transaction/updateProcedureThreshold.d.ts +3 -3
  92. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
  93. package/dist/transaction/updateProcedureThreshold.js +6 -16
  94. package/dist/transaction/updateProcedureThreshold.js.map +1 -1
  95. package/dist/transaction/updateSigners.d.ts +3 -3
  96. package/dist/transaction/updateSigners.d.ts.map +1 -1
  97. package/dist/transaction/updateSigners.js +9 -16
  98. package/dist/transaction/updateSigners.js.map +1 -1
  99. package/dist/transaction.d.ts +3 -2
  100. package/dist/transaction.d.ts.map +1 -1
  101. package/dist/transaction.js +3 -2
  102. package/dist/transaction.js.map +1 -1
  103. package/dist/types/proposal.d.ts +31 -0
  104. package/dist/types/proposal.d.ts.map +1 -1
  105. package/dist/types/proposal.js +8 -0
  106. package/dist/types/proposal.js.map +1 -1
  107. package/dist/utils/eip712.d.ts +80 -0
  108. package/dist/utils/eip712.d.ts.map +1 -0
  109. package/dist/utils/eip712.js +49 -0
  110. package/dist/utils/eip712.js.map +1 -0
  111. package/dist/utils/signature.d.ts +4 -0
  112. package/dist/utils/signature.d.ts.map +1 -1
  113. package/dist/utils/signature.js +49 -1
  114. package/dist/utils/signature.js.map +1 -1
  115. package/package.json +11 -6
  116. package/src/account/builder.ts +18 -7
  117. package/src/account/layout.ts +5 -5
  118. package/src/client.ts +94 -6
  119. package/src/index.ts +21 -3
  120. package/src/multisig/authArgErrors.ts +23 -56
  121. package/src/multisig/consumeNotesErrors.ts +20 -1
  122. package/src/multisig/signing.ts +8 -2
  123. package/src/multisig.ts +530 -183
  124. package/src/procedures.ts +7 -7
  125. package/src/proposal/factory.ts +7 -0
  126. package/src/raw-client.ts +11 -7
  127. package/src/recovery/publicNoteBackfill.ts +1 -1
  128. package/src/retry/classify.ts +3 -3
  129. package/src/signer.ts +1 -0
  130. package/src/signers/index.ts +1 -0
  131. package/src/signers/ledger.ts +122 -0
  132. package/src/state/adopt.ts +132 -0
  133. package/src/transaction/authArgs.ts +142 -0
  134. package/src/transaction/consumeNotes.ts +23 -30
  135. package/src/transaction/noteAuthentication.ts +136 -0
  136. package/src/transaction/options.ts +27 -0
  137. package/src/transaction/p2id.ts +45 -34
  138. package/src/transaction/summary.ts +86 -22
  139. package/src/transaction/updateGuardian.ts +8 -22
  140. package/src/transaction/updateProcedureThreshold.ts +8 -20
  141. package/src/transaction/updateSigners.ts +11 -22
  142. package/src/transaction.ts +12 -1
  143. package/src/types/proposal.ts +30 -0
  144. package/src/utils/eip712.ts +57 -0
  145. package/src/utils/signature.ts +57 -0
  146. package/src/prover/test-node.d.ts +0 -6
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Canonical consumption mode for `consume_notes` proposals: authenticated.
3
+ *
4
+ * miden-client decides per input note, at execution time and from the local
5
+ * store alone, whether it is consumed as *authenticated* (the store holds
6
+ * its inclusion proof) or *unauthenticated* (anything else). The two modes
7
+ * commit differently into the transaction summary,
8
+ * `hash(nullifier || note_id_or_ZERO)`, so the proposer and every verifier
9
+ * must be in the same mode or the summary commitment, and with it the
10
+ * proposal id, differs. That was issue #409's live shape: a cosigner whose
11
+ * fresh store had never seen the notes failed with "metadata does not match
12
+ * tx_summary". Proposal creation and every rebuild call
13
+ * {@link ensureNotesAuthenticated} first, so the executed transaction is the
14
+ * one the cosigners signed regardless of what each store held before.
15
+ */
16
+ import { Endpoint, type Note, type NoteInclusionProof, RpcClient } from '@miden-sdk/miden-sdk';
17
+
18
+ import { ConsumeNoteNotAuthenticatedError } from '../multisig/consumeNotesErrors.js';
19
+ import type { RawClientSource } from '../raw-client.js';
20
+ import { getRawMidenClient } from '../raw-client.js';
21
+ import { importNoteWithProof } from '../recovery/proposalNoteImport.js';
22
+ import { resolveRpcConfig, type RpcConfig } from '../rpc/config.js';
23
+ import { retryRpcRead } from '../rpc/retry.js';
24
+ import { normalizeHexWord } from '../utils/encoding.js';
25
+
26
+ export interface EnsureNotesAuthenticatedOptions {
27
+ /** Miden node RPC endpoint the inclusion proofs are fetched from. */
28
+ midenRpcEndpoint: string;
29
+ /** Retry policy for the proof fetch; defaults to the SDK's RPC defaults. */
30
+ rpc?: RpcConfig;
31
+ }
32
+
33
+ /**
34
+ * Makes every note in `notes` an authenticated input note in the client's
35
+ * local store: present, with its on-chain inclusion proof and block header.
36
+ *
37
+ * Notes already authenticated locally are left alone. For the rest the
38
+ * inclusion proofs come from the node in one round trip (the node serves
39
+ * proofs for private notes too) and each note is imported as committed,
40
+ * which also upgrades a proof-less record the store already tracked.
41
+ *
42
+ * @throws {ConsumeNoteNotAuthenticatedError} when a note is not committed on
43
+ * chain yet, the node does not serve its proof, the import fails, or the
44
+ * record still lacks authentication after a sync.
45
+ */
46
+ export async function ensureNotesAuthenticated(
47
+ midenClient: RawClientSource,
48
+ notes: readonly Note[],
49
+ options: EnsureNotesAuthenticatedOptions,
50
+ ): Promise<void> {
51
+ const webClient = await getRawMidenClient(midenClient, options.midenRpcEndpoint);
52
+ const pending = await unauthenticatedNotes(webClient, notes);
53
+ if (pending.length === 0) {
54
+ return;
55
+ }
56
+
57
+ const proofs = new Map<string, NoteInclusionProof>();
58
+ try {
59
+ const rpcClient = new RpcClient(new Endpoint(options.midenRpcEndpoint));
60
+ const fetched = await retryRpcRead(
61
+ () => rpcClient.getNotesById(pending.map((note) => note.id())),
62
+ resolveRpcConfig(options.rpc),
63
+ );
64
+ for (const entry of fetched) {
65
+ proofs.set(normalizeHexWord(entry.noteId.toString()), entry.inclusionProof);
66
+ }
67
+ } catch (error) {
68
+ throw new ConsumeNoteNotAuthenticatedError(
69
+ noteIdHex(pending[0]),
70
+ `failed to fetch inclusion proofs from the node: ${errorDetail(error)}`,
71
+ );
72
+ }
73
+
74
+ for (const note of pending) {
75
+ const idHex = noteIdHex(note);
76
+ const proof = proofs.get(idHex);
77
+ if (!proof) {
78
+ throw new ConsumeNoteNotAuthenticatedError(
79
+ idHex,
80
+ 'the node has no inclusion proof for it (not committed on chain yet)',
81
+ );
82
+ }
83
+ const { outcome, wasImported } = await importNoteWithProof(
84
+ webClient,
85
+ 'proposal',
86
+ idHex,
87
+ note,
88
+ proof,
89
+ );
90
+ if (!wasImported) {
91
+ throw new ConsumeNoteNotAuthenticatedError(
92
+ idHex,
93
+ `failed to import it with its inclusion proof: ${outcome.reason ?? 'unknown error'}`,
94
+ );
95
+ }
96
+ }
97
+
98
+ // The import authenticates a note committed at or below the client's sync
99
+ // height; a newer one lands unverified until a sync fetches its block
100
+ // header. A cosigner that just loaded the account is typically behind the
101
+ // note's block, so sync once and re-check before failing.
102
+ if ((await unauthenticatedNotes(webClient, notes)).length > 0) {
103
+ await webClient.syncState();
104
+ }
105
+ const still = await unauthenticatedNotes(webClient, notes);
106
+ if (still.length > 0) {
107
+ throw new ConsumeNoteNotAuthenticatedError(
108
+ noteIdHex(still[0]),
109
+ 'its inclusion proof was imported but the local store could not verify it ' +
110
+ 'against the chain even after a sync',
111
+ );
112
+ }
113
+ }
114
+
115
+ /** The subset of `notes` whose local record is missing or carries no proof. */
116
+ async function unauthenticatedNotes(
117
+ webClient: Awaited<ReturnType<typeof getRawMidenClient>>,
118
+ notes: readonly Note[],
119
+ ): Promise<Note[]> {
120
+ const pending: Note[] = [];
121
+ for (const note of notes) {
122
+ const record = await webClient.getInputNote(noteIdHex(note));
123
+ if (!record || !record.isAuthenticated()) {
124
+ pending.push(note);
125
+ }
126
+ }
127
+ return pending;
128
+ }
129
+
130
+ function noteIdHex(note: Note): string {
131
+ return normalizeHexWord(note.id().toString());
132
+ }
133
+
134
+ function errorDetail(error: unknown): string {
135
+ return error instanceof Error ? error.message : String(error);
136
+ }
@@ -6,8 +6,35 @@ export interface SignatureOptions {
6
6
  signatureAdviceMap?: AdviceMap;
7
7
  signatureScheme?: SignatureScheme;
8
8
  midenRpcEndpoint?: string;
9
+ /**
10
+ * The block the transaction summary binds. Omitted, the store's sync height,
11
+ * which is right for the party creating a proposal. A cosigner or executor
12
+ * rebuilding a proposal pins it to the proposal's anchor block, or the rebuilt
13
+ * summary can never match the signed one.
14
+ */
15
+ boundBlockNum?: number;
16
+ /**
17
+ * Blocks after the bound block at which the approvers' signatures stop
18
+ * authorizing the transaction, so it must be included by then. Bound by the
19
+ * summary, so a rebuild must pass the same value. At most 65535 blocks, the
20
+ * furthest a transaction can expire after its reference block; omitted, the
21
+ * approval never expires, which is the upstream default.
22
+ */
23
+ approvalExpirationDelta?: number;
9
24
  }
10
25
 
11
26
  export interface MidenClientSignatureOptions extends SignatureOptions {
12
27
  midenRpcEndpoint: string;
13
28
  }
29
+
30
+ /**
31
+ * Options for a request a multisig account executes. The account decides the
32
+ * auth args the request has to carry, so every multisig builder needs it.
33
+ */
34
+ export interface MultisigRequestOptions extends SignatureOptions {
35
+ accountId: string;
36
+ }
37
+
38
+ export interface MidenClientMultisigRequestOptions extends MultisigRequestOptions {
39
+ midenRpcEndpoint: string;
40
+ }
@@ -14,10 +14,10 @@ import {
14
14
  NoteTag,
15
15
  NoteType,
16
16
  Poseidon2,
17
- TransactionRequestBuilder,
18
17
  Word as WordType,
19
18
  } from '@miden-sdk/miden-sdk';
20
- import { randomWord } from '../utils/random.js';
19
+ import type { RawClientSource } from '../raw-client.js';
20
+ import { buildMultisigRequest, multisigRequestBuilder } from './authArgs.js';
21
21
  import { normalizeHexWord } from '../utils/encoding.js';
22
22
  import type { SignatureOptions } from './options.js';
23
23
  import type { P2idNoteVisibility } from '../types/proposal.js';
@@ -74,6 +74,36 @@ export function deriveP2idSerialNumber(salt: Word): Word {
74
74
  ]));
75
75
  }
76
76
 
77
+ /**
78
+ * P2ID storage since protocol 0.17 rc.5: target account, then a two-felt salt.
79
+ * Zero salt is the upstream default. A secret salt hides the target from
80
+ * guesses against the storage commitment; this builder keeps the note
81
+ * deterministic in the proposal salt, which already derives the serial number.
82
+ */
83
+ function p2idStorage(recipient: AccountId): Felt[] {
84
+ return [recipient.suffix(), recipient.prefix(), new Felt(0n), new Felt(0n)];
85
+ }
86
+
87
+ /**
88
+ * P2IDE storage: reclaimer (the sender), target, then reclaim and timelock
89
+ * heights. Zero encodes an unset height. The script requires all six items.
90
+ */
91
+ function p2ideStorage(
92
+ sender: AccountId,
93
+ recipient: AccountId,
94
+ reclaimHeight: number,
95
+ timelockHeight: number,
96
+ ): Felt[] {
97
+ return [
98
+ sender.suffix(),
99
+ sender.prefix(),
100
+ recipient.suffix(),
101
+ recipient.prefix(),
102
+ new Felt(BigInt(reclaimHeight)),
103
+ new Felt(BigInt(timelockHeight)),
104
+ ];
105
+ }
106
+
77
107
  function buildP2idNote(
78
108
  sender: AccountId,
79
109
  recipient: AccountId,
@@ -89,19 +119,10 @@ function buildP2idNote(
89
119
  const timelockHeight = parseP2ideHeight('timelockHeight', heights.timelockHeight);
90
120
  const isP2ide = reclaimHeight !== undefined || timelockHeight !== undefined;
91
121
 
92
- // P2IDE storage layout (miden-standards `P2ideNoteStorage`): the P2ID
93
- // storage plus reclaim/timelock heights as felts, 0 encoding "unset".
94
122
  const noteScript = isP2ide ? NoteScript.p2ide() : NoteScript.p2id();
95
- const storageFelts = [
96
- recipient.suffix(),
97
- recipient.prefix(),
98
- ];
99
- if (isP2ide) {
100
- storageFelts.push(
101
- new Felt(BigInt(reclaimHeight ?? 0)),
102
- new Felt(BigInt(timelockHeight ?? 0)),
103
- );
104
- }
123
+ const storageFelts = isP2ide
124
+ ? p2ideStorage(sender, recipient, reclaimHeight ?? 0, timelockHeight ?? 0)
125
+ : p2idStorage(recipient);
105
126
  const noteStorage = new NoteStorage(new FeltArray(storageFelts));
106
127
 
107
128
  const noteRecipient = new NoteRecipient(serialNum, noteScript, noteStorage);
@@ -141,14 +162,18 @@ export function buildP2idNoteFromMetadata(
141
162
  return buildP2idNote(sender, recipient, noteAssets, noteType, saltHex, heights);
142
163
  }
143
164
 
144
- export function buildP2idTransactionRequest(
165
+ export async function buildP2idTransactionRequest(
166
+ client: RawClientSource,
145
167
  senderId: string,
146
168
  recipientId: string,
147
169
  faucetId: string,
148
170
  amount: bigint,
149
171
  options: P2idTransactionOptions = {},
150
- ): { request: TransactionRequest; salt: Word } {
151
- const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
172
+ ): Promise<{ request: TransactionRequest; salt: Word }> {
173
+ const { builder, saltHex } = await multisigRequestBuilder(client, {
174
+ ...options,
175
+ accountId: senderId,
176
+ });
152
177
 
153
178
  const note = buildP2idNoteFromMetadata(
154
179
  senderId,
@@ -156,29 +181,15 @@ export function buildP2idTransactionRequest(
156
181
  faucetId,
157
182
  amount,
158
183
  options.noteType ?? NoteType.Public,
159
- authSaltHex,
184
+ saltHex,
160
185
  { reclaimHeight: options.reclaimHeight, timelockHeight: options.timelockHeight },
161
186
  );
162
187
 
163
- const outputNotes = new MidenArrays.NoteArray([note]);
164
-
165
- const authSaltForBuilder = WordType.fromHex(normalizeHexWord(authSaltHex));
166
-
167
- let txBuilder = new TransactionRequestBuilder();
168
- txBuilder = txBuilder.withOwnOutputNotes(outputNotes);
169
- txBuilder = txBuilder.withFeeConversionSalt(authSaltForBuilder);
170
- // Borrows rather than consumes: the glue passes `__wbg_ptr` without taking it,
171
- // so the handle stays ours to release once the builder has read it.
172
- authSaltForBuilder.free?.();
188
+ let txBuilder = builder.withOwnOutputNotes(new MidenArrays.NoteArray([note]));
173
189
 
174
190
  if (options.signatureAdviceMap) {
175
191
  txBuilder = txBuilder.extendAdviceMap(options.signatureAdviceMap);
176
192
  }
177
193
 
178
- const authSaltForReturn = WordType.fromHex(normalizeHexWord(authSaltHex));
179
-
180
- return {
181
- request: txBuilder.build(),
182
- salt: authSaltForReturn,
183
- };
194
+ return buildMultisigRequest(txBuilder, saltHex, senderId);
184
195
  }
@@ -6,22 +6,48 @@ import type {
6
6
  } from '@miden-sdk/miden-sdk';
7
7
  import { AccountId, ChainAnchor, Word } from '@miden-sdk/miden-sdk';
8
8
  import { getRawMidenClient } from '../raw-client.js';
9
- import { base64ToUint8Array, uint8ArrayToBase64 } from '../utils/encoding.js';
9
+ import { base64ToUint8Array, normalizeHexWord, uint8ArrayToBase64 } from '../utils/encoding.js';
10
10
 
11
11
  /**
12
- * Index of the first user param carrying the auth args. The guarded-multisig
13
- * auth component zeroes user params 0-2 and fills 3-6 with the auth args, matching
14
- * `push.0.0.0` ahead of `multisig::auth_tx` in `guarded_multisig.masm`.
12
+ * Layout of the six user params a multisig auth component binds into the
13
+ * transaction summary since protocol 0.17: the approval expiration block (or
14
+ * zero for an approval that never expires), a zero, then the four salt felts.
15
15
  */
16
- const AUTH_ARG_USER_PARAM_OFFSET = 3;
16
+ const APPROVAL_EXPIRATION_USER_PARAM_INDEX = 0;
17
+ const SALT_USER_PARAM_OFFSET = 2;
18
+
19
+ /**
20
+ * The summary binds the block the request's auth args name, and the anchor
21
+ * the store's sync height at capture. A sync landing between the build and the
22
+ * capture leaves them one block apart, and every cosigner's anchor check would
23
+ * then fail on a proposal nothing else is wrong with. Caught here, before the
24
+ * proposal is pushed, so the proposer rebuilds instead.
25
+ */
26
+ export class SummaryAnchorMismatchError extends Error {
27
+ readonly retryable = true;
28
+
29
+ constructor(details: { anchorCommitmentHex: string; summaryBlockCommitmentHex: string }) {
30
+ super(
31
+ `the transaction summary binds block commitment ${details.summaryBlockCommitmentHex} but ` +
32
+ `the captured chain anchor is ${details.anchorCommitmentHex}; a sync landed between ` +
33
+ 'building the request and capturing its anchor, so rebuild the request and retry',
34
+ );
35
+ this.name = 'SummaryAnchorMismatchError';
36
+ }
37
+ }
17
38
 
18
39
  /**
19
40
  * Captures a `ChainAnchor` for the request at the current sync height and
20
41
  * executes the transaction against it to obtain the summary awaiting
21
42
  * authorization. The anchor is returned alongside the summary so the proposer
22
43
  * can ship it with the signed data; cosigners and the executor then reproduce
23
- * the summary — which binds the reference block commitment since protocol
24
- * 0.16 — with {@link executeForSummaryAt} regardless of their own sync height.
44
+ * the summary with {@link executeForSummaryAt} regardless of their own sync
45
+ * height.
46
+ *
47
+ * The request's auth args bind the block its summary commits to, and this
48
+ * package pins that block to the anchor: a proposer builds at the sync height
49
+ * the anchor is captured at, and a rebuild passes the anchor's block number.
50
+ * The check below is what makes the first half hold.
25
51
  */
26
52
  export function executeForSummary(
27
53
  client: MidenClient,
@@ -44,13 +70,30 @@ export async function executeForSummary(
44
70
  const acc = AccountId.fromHex(accountId);
45
71
  const rawClient = await getRawMidenClient(client, midenRpcEndpoint);
46
72
  const anchor = await rawClient.chainAnchorForRequest(txRequest);
47
- const summary = await rawClient.executeForSummaryAt(acc, txRequest, anchor);
73
+ let summary: TransactionSummary;
74
+ try {
75
+ summary = await rawClient.executeForSummaryAt(acc, txRequest, anchor);
76
+ } catch (error) {
77
+ anchor.free();
78
+ throw error;
79
+ }
80
+
81
+ const anchorCommitment = anchor.commitment();
82
+ const summaryBlockCommitment = summary.blockCommitment();
83
+ const anchorCommitmentHex = normalizeHexWord(anchorCommitment.toHex());
84
+ const summaryBlockCommitmentHex = normalizeHexWord(summaryBlockCommitment.toHex());
85
+ anchorCommitment.free?.();
86
+ summaryBlockCommitment.free?.();
87
+ if (anchorCommitmentHex !== summaryBlockCommitmentHex) {
88
+ anchor.free();
89
+ throw new SummaryAnchorMismatchError({ anchorCommitmentHex, summaryBlockCommitmentHex });
90
+ }
48
91
  return { summary, anchor };
49
92
  }
50
93
 
51
94
  /**
52
95
  * Executes a transaction at the given `ChainAnchor`'s reference block to
53
- * obtain the summary awaiting authorization — the anchored counterpart of
96
+ * obtain the summary awaiting authorization: the anchored counterpart of
54
97
  * {@link executeForSummary} for cosigners and executors holding a proposal's
55
98
  * anchor.
56
99
  */
@@ -98,19 +141,40 @@ export function chainAnchorFromBase64(anchorBase64: string): ChainAnchor {
98
141
  }
99
142
 
100
143
  /**
101
- * Reads the auth args back out of a transaction summary.
102
- *
103
- * Since miden-protocol 0.16-rc the summary binds seven user-defined elements
104
- * instead of a dedicated salt word. The guarded-multisig auth component zeroes
105
- * the leading three and passes the auth args as the trailing four, so the auth
106
- * args are the tail of `userParams()`.
144
+ * The block a proposal's `chainAnchor` names, which is the block its summary
145
+ * binds: a custom producer rebuilds its request at this block. Decodes the
146
+ * anchor for the one number and frees it.
147
+ */
148
+ export function chainAnchorBlockNum(anchorBase64: string): number {
149
+ const anchor = chainAnchorFromBase64(anchorBase64);
150
+ try {
151
+ return anchor.blockNum();
152
+ } finally {
153
+ anchor.free();
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Reads the salt a multisig transaction summary binds.
107
159
  *
108
- * This is the auth-arg word, *not* the proposal salt. When the request declares
109
- * a fee conversion salt, miden-client uses it to commit the native conversion
110
- * info under `hash(CONVERSION_INFO || SALT)`. That commitment is not invertible
111
- * to the salt. Keep the salt alongside the proposal — `ProposalMetadata.saltHex`
112
- * — rather than trying to recover it from the summary.
160
+ * Since protocol 0.17 the multisig auth components bind the salt itself into
161
+ * the summary's user params rather than a commitment derived from it, so the
162
+ * value cosigners signed over is readable again. A proposal still carries the
163
+ * salt in its metadata, because a request has to be rebuilt before any summary
164
+ * exists; this reader is the cross-check that the two agree.
165
+ */
166
+ export function summarySalt(summary: TransactionSummary): Word {
167
+ return Word.newFromFelts(
168
+ summary.userParams().slice(SALT_USER_PARAM_OFFSET, SALT_USER_PARAM_OFFSET + 4),
169
+ );
170
+ }
171
+
172
+ /**
173
+ * Reads the block at which the approvers' signatures stop authorizing the
174
+ * transaction, or `undefined` for an approval that never expires, which is
175
+ * what this package's builders produce.
113
176
  */
114
- export function summaryAuthArg(summary: TransactionSummary): Word {
115
- return Word.newFromFelts(summary.userParams().slice(AUTH_ARG_USER_PARAM_OFFSET));
177
+ export function summaryApprovalExpirationBlockNum(summary: TransactionSummary): number | undefined {
178
+ const value = summary.userParams()[APPROVAL_EXPIRATION_USER_PARAM_INDEX].asInt();
179
+ return value === 0n ? undefined : Number(value);
116
180
  }
@@ -1,17 +1,15 @@
1
1
  import {
2
2
  type MidenClient,
3
3
  TransactionRequest,
4
- TransactionRequestBuilder,
5
4
  TransactionScript,
6
5
  type WasmWebClient,
7
6
  Word,
8
- Word as WordType,
9
7
  } from '@miden-sdk/miden-sdk';
10
8
  import { compileTxScript } from '../raw-client.js';
11
9
  import { normalizeHexWord } from '../utils/encoding.js';
12
- import { randomWord } from '../utils/random.js';
13
10
  import { authSchemeId } from '../utils/signature.js';
14
- import type { MidenClientSignatureOptions, SignatureOptions } from './options.js';
11
+ import { buildMultisigRequest, multisigRequestBuilder } from './authArgs.js';
12
+ import type { MidenClientMultisigRequestOptions, MultisigRequestOptions } from './options.js';
15
13
  import type { SignatureScheme } from '../types.js';
16
14
 
17
15
  async function buildUpdateGuardianScript(
@@ -44,17 +42,17 @@ end
44
42
  export function buildUpdateGuardianTransactionRequest(
45
43
  client: MidenClient,
46
44
  newGuardianPubkey: string,
47
- options: MidenClientSignatureOptions,
45
+ options: MidenClientMultisigRequestOptions,
48
46
  ): Promise<{ request: TransactionRequest; salt: Word }>;
49
47
  export function buildUpdateGuardianTransactionRequest(
50
48
  client: WasmWebClient,
51
49
  newGuardianPubkey: string,
52
- options?: SignatureOptions,
50
+ options: MultisigRequestOptions,
53
51
  ): Promise<{ request: TransactionRequest; salt: Word }>;
54
52
  export async function buildUpdateGuardianTransactionRequest(
55
53
  client: MidenClient | WasmWebClient,
56
54
  newGuardianPubkey: string,
57
- options: SignatureOptions = {},
55
+ options: MultisigRequestOptions,
58
56
  ): Promise<{ request: TransactionRequest; salt: Word }> {
59
57
  const signatureScheme = options.signatureScheme ?? 'falcon';
60
58
  const script = await buildUpdateGuardianScript(
@@ -64,24 +62,12 @@ export async function buildUpdateGuardianTransactionRequest(
64
62
  options.midenRpcEndpoint,
65
63
  );
66
64
 
67
- const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
68
- const authSaltForBuilder = WordType.fromHex(normalizeHexWord(authSaltHex));
69
-
70
- let txBuilder = new TransactionRequestBuilder();
71
- txBuilder = txBuilder.withCustomScript(script);
72
- txBuilder = txBuilder.withFeeConversionSalt(authSaltForBuilder);
73
- // Borrows rather than consumes: the glue passes `__wbg_ptr` without taking it,
74
- // so the handle stays ours to release once the builder has read it.
75
- authSaltForBuilder.free?.();
65
+ const { builder, saltHex } = await multisigRequestBuilder(client, options);
66
+ let txBuilder = builder.withCustomScript(script);
76
67
 
77
68
  if (options.signatureAdviceMap) {
78
69
  txBuilder = txBuilder.extendAdviceMap(options.signatureAdviceMap);
79
70
  }
80
71
 
81
- const authSaltForReturn = WordType.fromHex(normalizeHexWord(authSaltHex));
82
-
83
- return {
84
- request: txBuilder.build(),
85
- salt: authSaltForReturn,
86
- };
72
+ return buildMultisigRequest(txBuilder, saltHex, options.accountId);
87
73
  }
@@ -4,7 +4,6 @@ import {
4
4
  type MidenClient,
5
5
  Poseidon2,
6
6
  TransactionRequest,
7
- TransactionRequestBuilder,
8
7
  TransactionScript,
9
8
  type WasmWebClient,
10
9
  Word,
@@ -13,8 +12,8 @@ import {
13
12
  import { getProcedureRoot, type ProcedureName } from '../procedures.js';
14
13
  import { compileTxScript } from '../raw-client.js';
15
14
  import { normalizeHexWord } from '../utils/encoding.js';
16
- import { randomWord } from '../utils/random.js';
17
- import type { MidenClientSignatureOptions, SignatureOptions } from './options.js';
15
+ import { buildMultisigRequest, multisigRequestBuilder } from './authArgs.js';
16
+ import type { MidenClientMultisigRequestOptions, MultisigRequestOptions } from './options.js';
18
17
 
19
18
  function buildProcedureThresholdFelts(procedure: ProcedureName, threshold: number): Felt[] {
20
19
  const procedureRoot = WordType.fromHex(normalizeHexWord(getProcedureRoot(procedure)));
@@ -66,19 +65,19 @@ export function buildUpdateProcedureThresholdTransactionRequest(
66
65
  client: MidenClient,
67
66
  procedure: ProcedureName,
68
67
  threshold: number,
69
- options: MidenClientSignatureOptions,
68
+ options: MidenClientMultisigRequestOptions,
70
69
  ): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }>;
71
70
  export function buildUpdateProcedureThresholdTransactionRequest(
72
71
  client: WasmWebClient,
73
72
  procedure: ProcedureName,
74
73
  threshold: number,
75
- options?: SignatureOptions,
74
+ options: MultisigRequestOptions,
76
75
  ): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }>;
77
76
  export async function buildUpdateProcedureThresholdTransactionRequest(
78
77
  client: MidenClient | WasmWebClient,
79
78
  procedure: ProcedureName,
80
79
  threshold: number,
81
- options: SignatureOptions = {},
80
+ options: MultisigRequestOptions,
82
81
  ): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }> {
83
82
  const configHash = buildProcedureThresholdConfigHash(procedure, threshold);
84
83
 
@@ -88,23 +87,12 @@ export async function buildUpdateProcedureThresholdTransactionRequest(
88
87
  threshold,
89
88
  options.midenRpcEndpoint,
90
89
  );
91
- const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
92
- const authSalt = WordType.fromHex(normalizeHexWord(authSaltHex));
93
-
94
- let txBuilder = new TransactionRequestBuilder();
95
- txBuilder = txBuilder.withCustomScript(script);
96
- txBuilder = txBuilder.withFeeConversionSalt(authSalt);
97
- // Borrows rather than consumes: the glue passes `__wbg_ptr` without taking it,
98
- // so the handle stays ours to release once the builder has read it.
99
- authSalt.free?.();
90
+ const { builder, saltHex } = await multisigRequestBuilder(client, options);
91
+ let txBuilder = builder.withCustomScript(script);
100
92
 
101
93
  if (options.signatureAdviceMap) {
102
94
  txBuilder = txBuilder.extendAdviceMap(options.signatureAdviceMap);
103
95
  }
104
96
 
105
- return {
106
- request: txBuilder.build(),
107
- salt: WordType.fromHex(normalizeHexWord(authSaltHex)),
108
- configHash,
109
- };
97
+ return { ...buildMultisigRequest(txBuilder, saltHex, options.accountId), configHash };
110
98
  }
@@ -5,7 +5,6 @@ import {
5
5
  type MidenClient,
6
6
  Poseidon2,
7
7
  TransactionRequest,
8
- TransactionRequestBuilder,
9
8
  TransactionScript,
10
9
  type WasmWebClient,
11
10
  Word,
@@ -13,9 +12,9 @@ import {
13
12
  } from '@miden-sdk/miden-sdk';
14
13
  import { compileTxScript } from '../raw-client.js';
15
14
  import { normalizeHexWord } from '../utils/encoding.js';
16
- import { randomWord } from '../utils/random.js';
17
15
  import { authSchemeId } from '../utils/signature.js';
18
- import type { MidenClientSignatureOptions, SignatureOptions } from './options.js';
16
+ import { buildMultisigRequest, multisigRequestBuilder } from './authArgs.js';
17
+ import type { MidenClientMultisigRequestOptions, MultisigRequestOptions } from './options.js';
19
18
  import type { SignatureScheme } from '../types.js';
20
19
 
21
20
  function buildMultisigConfigFelts(
@@ -77,19 +76,19 @@ export function buildUpdateSignersTransactionRequest(
77
76
  client: MidenClient,
78
77
  threshold: number,
79
78
  signerCommitments: string[],
80
- options: MidenClientSignatureOptions,
79
+ options: MidenClientMultisigRequestOptions,
81
80
  ): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }>;
82
81
  export function buildUpdateSignersTransactionRequest(
83
82
  client: WasmWebClient,
84
83
  threshold: number,
85
84
  signerCommitments: string[],
86
- options?: SignatureOptions,
85
+ options: MultisigRequestOptions,
87
86
  ): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }>;
88
87
  export async function buildUpdateSignersTransactionRequest(
89
88
  client: MidenClient | WasmWebClient,
90
89
  threshold: number,
91
90
  signerCommitments: string[],
92
- options: SignatureOptions = {},
91
+ options: MultisigRequestOptions,
93
92
  ): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }> {
94
93
  const signatureScheme = options.signatureScheme ?? 'falcon';
95
94
  const { configHash: configHashForAdvice, payload } = buildMultisigConfigAdvice(
@@ -115,28 +114,18 @@ export async function buildUpdateSignersTransactionRequest(
115
114
 
116
115
  const script = await buildUpdateSignersScript(client, options.midenRpcEndpoint);
117
116
 
118
- const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
119
-
120
- const authSaltForBuilder = WordType.fromHex(normalizeHexWord(authSaltHex));
121
-
122
- let txBuilder = new TransactionRequestBuilder();
123
- txBuilder = txBuilder.withCustomScript(script);
124
- txBuilder = txBuilder.withScriptArg(configHashForScript);
125
- txBuilder = txBuilder.extendAdviceMap(advice);
126
- txBuilder = txBuilder.withFeeConversionSalt(authSaltForBuilder);
127
- // Borrows rather than consumes: the glue passes `__wbg_ptr` without taking it,
128
- // so the handle stays ours to release once the builder has read it.
129
- authSaltForBuilder.free?.();
117
+ const { builder, saltHex } = await multisigRequestBuilder(client, options);
118
+ let txBuilder = builder
119
+ .withCustomScript(script)
120
+ .withScriptArg(configHashForScript)
121
+ .extendAdviceMap(advice);
130
122
 
131
123
  if (options.signatureAdviceMap) {
132
124
  txBuilder = txBuilder.extendAdviceMap(options.signatureAdviceMap);
133
125
  }
134
126
 
135
- const authSaltForReturn = WordType.fromHex(normalizeHexWord(authSaltHex));
136
-
137
127
  return {
138
- request: txBuilder.build(),
139
- salt: authSaltForReturn,
128
+ ...buildMultisigRequest(txBuilder, saltHex, options.accountId),
140
129
  configHash: configHashForReturn,
141
130
  };
142
131
  }