@openzeppelin/miden-multisig-client 0.16.2 → 0.17.0-rc.2

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 (150) hide show
  1. package/README.md +144 -7
  2. package/dist/account/builder.d.ts.map +1 -1
  3. package/dist/account/builder.js +32 -25
  4. package/dist/account/builder.js.map +1 -1
  5. package/dist/account/builder.test.js +73 -21
  6. package/dist/account/builder.test.js.map +1 -1
  7. package/dist/account/layout.d.ts +31 -0
  8. package/dist/account/layout.d.ts.map +1 -0
  9. package/dist/account/layout.js +31 -0
  10. package/dist/account/layout.js.map +1 -0
  11. package/dist/account/masm/account-components/auth.d.ts +1 -4
  12. package/dist/account/masm/account-components/auth.d.ts.map +1 -1
  13. package/dist/account/masm/account-components/auth.js +36 -53
  14. package/dist/account/masm/account-components/auth.js.map +1 -1
  15. package/dist/account/masm/index.d.ts +0 -1
  16. package/dist/account/masm/index.d.ts.map +1 -1
  17. package/dist/account/masm/index.js +0 -1
  18. package/dist/account/masm/index.js.map +1 -1
  19. package/dist/account/storage.d.ts +4 -0
  20. package/dist/account/storage.d.ts.map +1 -1
  21. package/dist/account/storage.js +9 -20
  22. package/dist/account/storage.js.map +1 -1
  23. package/dist/client.d.ts.map +1 -1
  24. package/dist/client.js +5 -3
  25. package/dist/client.js.map +1 -1
  26. package/dist/client.test.js +68 -15
  27. package/dist/client.test.js.map +1 -1
  28. package/dist/index.d.ts +4 -3
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +3 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/inspector.d.ts +59 -1
  33. package/dist/inspector.d.ts.map +1 -1
  34. package/dist/inspector.js +157 -29
  35. package/dist/inspector.js.map +1 -1
  36. package/dist/inspector.test.js +246 -37
  37. package/dist/inspector.test.js.map +1 -1
  38. package/dist/multisig.d.ts +106 -32
  39. package/dist/multisig.d.ts.map +1 -1
  40. package/dist/multisig.js +289 -95
  41. package/dist/multisig.js.map +1 -1
  42. package/dist/multisig.test.js +581 -58
  43. package/dist/multisig.test.js.map +1 -1
  44. package/dist/procedures.d.ts +6 -7
  45. package/dist/procedures.d.ts.map +1 -1
  46. package/dist/procedures.js +6 -7
  47. package/dist/procedures.js.map +1 -1
  48. package/dist/proposal/metadata.d.ts.map +1 -1
  49. package/dist/proposal/metadata.js +13 -2
  50. package/dist/proposal/metadata.js.map +1 -1
  51. package/dist/proposal/metadata.test.js +57 -0
  52. package/dist/proposal/metadata.test.js.map +1 -1
  53. package/dist/prover/workflow.d.ts +3 -2
  54. package/dist/prover/workflow.d.ts.map +1 -1
  55. package/dist/prover/workflow.js +5 -2
  56. package/dist/prover/workflow.js.map +1 -1
  57. package/dist/prover/workflow.test.js +6 -4
  58. package/dist/prover/workflow.test.js.map +1 -1
  59. package/dist/transaction/index.d.ts +1 -1
  60. package/dist/transaction/index.d.ts.map +1 -1
  61. package/dist/transaction/index.js +1 -1
  62. package/dist/transaction/index.js.map +1 -1
  63. package/dist/transaction/p2id.d.ts +17 -6
  64. package/dist/transaction/p2id.d.ts.map +1 -1
  65. package/dist/transaction/p2id.js +22 -31
  66. package/dist/transaction/p2id.js.map +1 -1
  67. package/dist/transaction/p2id.test.js +62 -24
  68. package/dist/transaction/p2id.test.js.map +1 -1
  69. package/dist/transaction/summary.d.ts +45 -2
  70. package/dist/transaction/summary.d.ts.map +1 -1
  71. package/dist/transaction/summary.js +42 -2
  72. package/dist/transaction/summary.js.map +1 -1
  73. package/dist/transaction/summary.test.d.ts +2 -0
  74. package/dist/transaction/summary.test.d.ts.map +1 -0
  75. package/dist/transaction/summary.test.js +26 -0
  76. package/dist/transaction/summary.test.js.map +1 -0
  77. package/dist/transaction/updateGuardian.d.ts.map +1 -1
  78. package/dist/transaction/updateGuardian.js +16 -18
  79. package/dist/transaction/updateGuardian.js.map +1 -1
  80. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
  81. package/dist/transaction/updateProcedureThreshold.js +15 -17
  82. package/dist/transaction/updateProcedureThreshold.js.map +1 -1
  83. package/dist/transaction/updateSigners.d.ts +6 -1
  84. package/dist/transaction/updateSigners.d.ts.map +1 -1
  85. package/dist/transaction/updateSigners.js +20 -17
  86. package/dist/transaction/updateSigners.js.map +1 -1
  87. package/dist/transaction.d.ts +2 -2
  88. package/dist/transaction.d.ts.map +1 -1
  89. package/dist/transaction.js +1 -1
  90. package/dist/transaction.js.map +1 -1
  91. package/dist/types/proposal.d.ts +28 -4
  92. package/dist/types/proposal.d.ts.map +1 -1
  93. package/dist/types/proposal.js +18 -0
  94. package/dist/types/proposal.js.map +1 -1
  95. package/dist/types.d.ts +0 -1
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/utils/signature.d.ts +11 -7
  98. package/dist/utils/signature.d.ts.map +1 -1
  99. package/dist/utils/signature.js +24 -58
  100. package/dist/utils/signature.js.map +1 -1
  101. package/dist/utils/word.d.ts +7 -0
  102. package/dist/utils/word.d.ts.map +1 -1
  103. package/dist/utils/word.js +15 -0
  104. package/dist/utils/word.js.map +1 -1
  105. package/masm/account_components/auth/guarded_multisig.masm +42 -0
  106. package/package.json +7 -4
  107. package/src/account/builder.test.ts +111 -45
  108. package/src/account/builder.ts +45 -33
  109. package/src/account/layout.ts +33 -0
  110. package/src/account/masm/account-components/auth.ts +36 -56
  111. package/src/account/masm/index.ts +0 -1
  112. package/src/account/storage.ts +9 -22
  113. package/src/client.test.ts +80 -15
  114. package/src/client.ts +5 -3
  115. package/src/index.ts +26 -1
  116. package/src/inspector.test.ts +330 -38
  117. package/src/inspector.ts +196 -33
  118. package/src/multisig.test.ts +679 -63
  119. package/src/multisig.ts +361 -107
  120. package/src/procedures.ts +6 -7
  121. package/src/proposal/metadata.test.ts +76 -0
  122. package/src/proposal/metadata.ts +13 -2
  123. package/src/prover/workflow.test.ts +9 -4
  124. package/src/prover/workflow.ts +15 -3
  125. package/src/transaction/index.ts +7 -1
  126. package/src/transaction/p2id.test.ts +112 -31
  127. package/src/transaction/p2id.ts +39 -38
  128. package/src/transaction/summary.test.ts +32 -0
  129. package/src/transaction/summary.ts +83 -4
  130. package/src/transaction/updateGuardian.ts +15 -25
  131. package/src/transaction/updateProcedureThreshold.ts +13 -29
  132. package/src/transaction/updateSigners.ts +38 -30
  133. package/src/transaction.ts +8 -1
  134. package/src/types/proposal.ts +43 -4
  135. package/src/types.ts +0 -1
  136. package/src/utils/signature.ts +32 -65
  137. package/src/utils/word.ts +17 -0
  138. package/dist/account/masm/auth.d.ts +0 -5
  139. package/dist/account/masm/auth.d.ts.map +0 -1
  140. package/dist/account/masm/auth.js +0 -1509
  141. package/dist/account/masm/auth.js.map +0 -1
  142. package/masm/account_components/auth/multisig.masm +0 -12
  143. package/masm/account_components/auth/multisig_ecdsa.masm +0 -12
  144. package/masm/account_components/auth/multisig_guardian.masm +0 -16
  145. package/masm/account_components/auth/multisig_guardian_ecdsa.masm +0 -16
  146. package/masm/auth/guardian.masm +0 -199
  147. package/masm/auth/guardian_ecdsa.masm +0 -195
  148. package/masm/auth/multisig.masm +0 -554
  149. package/masm/auth/multisig_ecdsa.masm +0 -554
  150. package/src/account/masm/auth.ts +0 -1512
@@ -4,28 +4,107 @@ import type {
4
4
  TransactionSummary,
5
5
  WasmWebClient,
6
6
  } from '@miden-sdk/miden-sdk';
7
- import { AccountId } from '@miden-sdk/miden-sdk';
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
10
 
11
+ /**
12
+ * Index of the first user param carrying the auth-arg salt. 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`.
15
+ */
16
+ const SALT_USER_PARAM_OFFSET = 3;
17
+
18
+ /**
19
+ * Captures a `ChainAnchor` for the request at the current sync height and
20
+ * executes the transaction against it to obtain the summary awaiting
21
+ * authorization. The anchor is returned alongside the summary so the proposer
22
+ * 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.
25
+ */
10
26
  export function executeForSummary(
11
27
  client: MidenClient,
12
28
  accountId: string,
13
29
  txRequest: TransactionRequest,
14
30
  midenRpcEndpoint: string,
15
- ): Promise<TransactionSummary>;
31
+ ): Promise<{ summary: TransactionSummary; anchor: ChainAnchor }>;
16
32
  export function executeForSummary(
17
33
  client: WasmWebClient,
18
34
  accountId: string,
19
35
  txRequest: TransactionRequest,
20
36
  midenRpcEndpoint?: string,
21
- ): Promise<TransactionSummary>;
37
+ ): Promise<{ summary: TransactionSummary; anchor: ChainAnchor }>;
22
38
  export async function executeForSummary(
23
39
  client: MidenClient | WasmWebClient,
24
40
  accountId: string,
25
41
  txRequest: TransactionRequest,
26
42
  midenRpcEndpoint?: string,
43
+ ): Promise<{ summary: TransactionSummary; anchor: ChainAnchor }> {
44
+ const acc = AccountId.fromHex(accountId);
45
+ const rawClient = await getRawMidenClient(client, midenRpcEndpoint);
46
+ const anchor = await rawClient.chainAnchorForRequest(txRequest);
47
+ const summary = await rawClient.executeForSummaryAt(acc, txRequest, anchor);
48
+ return { summary, anchor };
49
+ }
50
+
51
+ /**
52
+ * Executes a transaction at the given `ChainAnchor`'s reference block to
53
+ * obtain the summary awaiting authorization — the anchored counterpart of
54
+ * {@link executeForSummary} for cosigners and executors holding a proposal's
55
+ * anchor.
56
+ */
57
+ export function executeForSummaryAt(
58
+ client: MidenClient,
59
+ accountId: string,
60
+ txRequest: TransactionRequest,
61
+ anchor: ChainAnchor,
62
+ midenRpcEndpoint: string,
63
+ ): Promise<TransactionSummary>;
64
+ export function executeForSummaryAt(
65
+ client: WasmWebClient,
66
+ accountId: string,
67
+ txRequest: TransactionRequest,
68
+ anchor: ChainAnchor,
69
+ midenRpcEndpoint?: string,
70
+ ): Promise<TransactionSummary>;
71
+ export async function executeForSummaryAt(
72
+ client: MidenClient | WasmWebClient,
73
+ accountId: string,
74
+ txRequest: TransactionRequest,
75
+ anchor: ChainAnchor,
76
+ midenRpcEndpoint?: string,
27
77
  ): Promise<TransactionSummary> {
28
78
  const acc = AccountId.fromHex(accountId);
29
79
  const rawClient = await getRawMidenClient(client, midenRpcEndpoint);
30
- return rawClient.executeForSummary(acc, txRequest);
80
+ return rawClient.executeForSummaryAt(acc, txRequest, anchor);
81
+ }
82
+
83
+ /**
84
+ * Serializes a `ChainAnchor` to base64 for the proposal wire payload.
85
+ */
86
+ export function chainAnchorToBase64(anchor: ChainAnchor): string {
87
+ return uint8ArrayToBase64(anchor.serialize());
88
+ }
89
+
90
+ /**
91
+ * Deserializes a `ChainAnchor` from its base64 wire form. `ChainAnchor`
92
+ * deserialization validates the header/chain consistency internally, so a
93
+ * decoded anchor only needs its block commitment checked against the signed
94
+ * transaction summary before it is safe to execute against.
95
+ */
96
+ export function chainAnchorFromBase64(anchorBase64: string): ChainAnchor {
97
+ return ChainAnchor.deserialize(base64ToUint8Array(anchorBase64));
98
+ }
99
+
100
+ /**
101
+ * Reads the auth-arg salt 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 salt
106
+ * is the tail of `userParams()`.
107
+ */
108
+ export function summarySalt(summary: TransactionSummary): Word {
109
+ return Word.newFromFelts(summary.userParams().slice(SALT_USER_PARAM_OFFSET));
31
110
  }
@@ -1,6 +1,4 @@
1
1
  import {
2
- AdviceMap,
3
- FeltArray,
4
2
  type MidenClient,
5
3
  TransactionRequest,
6
4
  TransactionRequestBuilder,
@@ -9,37 +7,38 @@ import {
9
7
  Word,
10
8
  Word as WordType,
11
9
  } from '@miden-sdk/miden-sdk';
12
- import { GUARDIAN_ECDSA_MASM, GUARDIAN_MASM } from '../account/masm/auth.js';
13
10
  import { compileTxScript } from '../raw-client.js';
14
11
  import { normalizeHexWord } from '../utils/encoding.js';
15
12
  import { randomWord } from '../utils/random.js';
13
+ import { authSchemeId } from '../utils/signature.js';
16
14
  import type { MidenClientSignatureOptions, SignatureOptions } from './options.js';
17
15
  import type { SignatureScheme } from '../types.js';
18
16
 
19
17
  async function buildUpdateGuardianScript(
20
18
  client: MidenClient | WasmWebClient,
19
+ newGuardianPubkey: string,
21
20
  signatureScheme: SignatureScheme,
22
21
  midenRpcEndpoint?: string,
23
22
  ): Promise<TransactionScript> {
24
- const guardianLibraryPath = 'oz_guardian::guardian';
25
- const guardianMasm = signatureScheme === 'ecdsa' ? GUARDIAN_ECDSA_MASM : GUARDIAN_MASM;
23
+ // A word literal preserves the key's element order on the operand stack.
24
+ const keyLiteral = normalizeHexWord(newGuardianPubkey);
25
+ const schemeId = authSchemeId(signatureScheme);
26
26
 
27
+ // Calling the origin procedure yields the same MAST root as its component re-export.
27
28
  const scriptSource = `
28
- use oz_guardian::guardian
29
+ use miden::standards::auth::guardian
29
30
 
30
- begin
31
- adv.push_mapval
32
- dropw
31
+ @transaction_script
32
+ pub proc main
33
+ push.${keyLiteral}
34
+ push.${schemeId}
33
35
  call.guardian::update_guardian_public_key
36
+ drop
37
+ dropw
34
38
  end
35
39
  `;
36
40
 
37
- return compileTxScript(
38
- client,
39
- scriptSource,
40
- [{ namespace: guardianLibraryPath, code: guardianMasm }],
41
- midenRpcEndpoint,
42
- );
41
+ return compileTxScript(client, scriptSource, [], midenRpcEndpoint);
43
42
  }
44
43
 
45
44
  export function buildUpdateGuardianTransactionRequest(
@@ -60,25 +59,16 @@ export async function buildUpdateGuardianTransactionRequest(
60
59
  const signatureScheme = options.signatureScheme ?? 'falcon';
61
60
  const script = await buildUpdateGuardianScript(
62
61
  client,
62
+ newGuardianPubkey,
63
63
  signatureScheme,
64
64
  options.midenRpcEndpoint,
65
65
  );
66
66
 
67
67
  const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
68
-
69
- const pubkeyWordForAdvice = WordType.fromHex(normalizeHexWord(newGuardianPubkey));
70
- const pubkeyWordForFelts = WordType.fromHex(normalizeHexWord(newGuardianPubkey));
71
- const pubkeyWordForScript = WordType.fromHex(normalizeHexWord(newGuardianPubkey));
72
-
73
- const advice = new AdviceMap();
74
- advice.insert(pubkeyWordForAdvice, new FeltArray(pubkeyWordForFelts.toFelts()));
75
-
76
68
  const authSaltForBuilder = WordType.fromHex(normalizeHexWord(authSaltHex));
77
69
 
78
70
  let txBuilder = new TransactionRequestBuilder();
79
71
  txBuilder = txBuilder.withCustomScript(script);
80
- txBuilder = txBuilder.withScriptArg(pubkeyWordForScript);
81
- txBuilder = txBuilder.extendAdviceMap(advice);
82
72
  txBuilder = txBuilder.withAuthArg(authSaltForBuilder);
83
73
 
84
74
  if (options.signatureAdviceMap) {
@@ -10,16 +10,11 @@ import {
10
10
  Word,
11
11
  Word as WordType,
12
12
  } from '@miden-sdk/miden-sdk';
13
- import {
14
- MULTISIG_ECDSA_MASM,
15
- MULTISIG_MASM,
16
- } from '../account/masm/auth.js';
17
13
  import { getProcedureRoot, type ProcedureName } from '../procedures.js';
18
14
  import { compileTxScript } from '../raw-client.js';
19
15
  import { normalizeHexWord } from '../utils/encoding.js';
20
16
  import { randomWord } from '../utils/random.js';
21
17
  import type { MidenClientSignatureOptions, SignatureOptions } from './options.js';
22
- import type { SignatureScheme } from '../types.js';
23
18
 
24
19
  function buildProcedureThresholdFelts(procedure: ProcedureName, threshold: number): Felt[] {
25
20
  const procedureRoot = WordType.fromHex(normalizeHexWord(getProcedureRoot(procedure)));
@@ -32,48 +27,39 @@ function buildProcedureThresholdFelts(procedure: ProcedureName, threshold: numbe
32
27
  ];
33
28
  }
34
29
 
35
- function buildProcedureThresholdAdvice(
36
- procedure: ProcedureName,
37
- threshold: number,
38
- ): { configHash: Word; payload: FeltArray } {
39
- // `Poseidon2.hashElements` consumes (frees) its `FeltArray` by value, so the
40
- // advice payload must be a freshly built one — reusing the hashed array
41
- // surfaces as "null pointer passed to rust" at the later `advice.insert`.
42
- const configHash = Poseidon2.hashElements(
30
+ /**
31
+ * `set_procedure_threshold` reads its `[proc_threshold, PROC_ROOT]` inputs from the operand stack
32
+ * (pushed by the script), so no advice-map entry is attached; this hash is returned only for
33
+ * caller bookkeeping.
34
+ */
35
+ function buildProcedureThresholdConfigHash(procedure: ProcedureName, threshold: number): Word {
36
+ return Poseidon2.hashElements(
43
37
  new FeltArray(buildProcedureThresholdFelts(procedure, threshold)),
44
38
  );
45
- const payload = new FeltArray(buildProcedureThresholdFelts(procedure, threshold));
46
- return { configHash, payload };
47
39
  }
48
40
 
49
41
  async function buildUpdateProcedureThresholdScript(
50
42
  client: MidenClient | WasmWebClient,
51
43
  procedure: ProcedureName,
52
44
  threshold: number,
53
- signatureScheme: SignatureScheme,
54
45
  midenRpcEndpoint?: string,
55
46
  ): Promise<TransactionScript> {
56
- const multisigMasm = signatureScheme === 'ecdsa' ? MULTISIG_ECDSA_MASM : MULTISIG_MASM;
57
47
  const procedureRoot = normalizeHexWord(getProcedureRoot(procedure));
58
48
 
59
49
  const scriptSource = `
60
- use oz_multisig::multisig
50
+ use miden::standards::auth::multisig
61
51
 
62
- begin
52
+ @transaction_script
53
+ pub proc main
63
54
  push.${procedureRoot}
64
55
  push.${threshold}
65
- call.multisig::update_procedure_threshold
56
+ call.multisig::set_procedure_threshold
66
57
  dropw
67
58
  drop
68
59
  end
69
60
  `;
70
61
 
71
- return compileTxScript(
72
- client,
73
- scriptSource,
74
- [{ namespace: 'oz_multisig::multisig', code: multisigMasm }],
75
- midenRpcEndpoint,
76
- );
62
+ return compileTxScript(client, scriptSource, [], midenRpcEndpoint);
77
63
  }
78
64
 
79
65
  export function buildUpdateProcedureThresholdTransactionRequest(
@@ -94,14 +80,12 @@ export async function buildUpdateProcedureThresholdTransactionRequest(
94
80
  threshold: number,
95
81
  options: SignatureOptions = {},
96
82
  ): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }> {
97
- const signatureScheme = options.signatureScheme ?? 'falcon';
98
- const { configHash } = buildProcedureThresholdAdvice(procedure, threshold);
83
+ const configHash = buildProcedureThresholdConfigHash(procedure, threshold);
99
84
 
100
85
  const script = await buildUpdateProcedureThresholdScript(
101
86
  client,
102
87
  procedure,
103
88
  threshold,
104
- signatureScheme,
105
89
  options.midenRpcEndpoint,
106
90
  );
107
91
  const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
@@ -11,66 +11,66 @@ import {
11
11
  Word,
12
12
  Word as WordType,
13
13
  } from '@miden-sdk/miden-sdk';
14
- import {
15
- MULTISIG_ECDSA_MASM,
16
- MULTISIG_MASM,
17
- } from '../account/masm/auth.js';
18
14
  import { compileTxScript } from '../raw-client.js';
19
15
  import { normalizeHexWord } from '../utils/encoding.js';
20
16
  import { randomWord } from '../utils/random.js';
17
+ import { authSchemeId } from '../utils/signature.js';
21
18
  import type { MidenClientSignatureOptions, SignatureOptions } from './options.js';
22
19
  import type { SignatureScheme } from '../types.js';
23
20
 
24
- function buildMultisigConfigFelts(threshold: number, signerCommitments: string[]): Felt[] {
21
+ function buildMultisigConfigFelts(
22
+ threshold: number,
23
+ signerCommitments: string[],
24
+ signatureScheme: SignatureScheme,
25
+ ): Felt[] {
25
26
  const numApprovers = signerCommitments.length;
27
+ const schemeId = authSchemeId(signatureScheme);
26
28
  const felts: Felt[] = [
27
29
  new Felt(BigInt(threshold)),
28
30
  new Felt(BigInt(numApprovers)),
29
31
  new Felt(0n),
30
32
  new Felt(0n),
31
33
  ];
34
+ // Interleave [PUB_KEY, SCHEME_ID] per approver, in reverse index order.
32
35
  for (const commitment of [...signerCommitments].reverse()) {
33
36
  const word = WordType.fromHex(normalizeHexWord(commitment));
34
37
  felts.push(...word.toFelts());
38
+ felts.push(new Felt(BigInt(schemeId)), new Felt(0n), new Felt(0n), new Felt(0n));
35
39
  }
36
40
  return felts;
37
41
  }
38
42
 
39
- function buildMultisigConfigAdvice(
43
+ export function buildMultisigConfigAdvice(
40
44
  threshold: number,
41
45
  signerCommitments: string[],
46
+ signatureScheme: SignatureScheme,
42
47
  ): { configHash: Word; payload: FeltArray } {
43
- // `Poseidon2.hashElements` consumes (frees) its `FeltArray` by value, so the
44
- // advice payload must be a freshly built one — reusing the hashed array
45
- // surfaces as "null pointer passed to rust" at the later `advice.insert`.
48
+ // `Poseidon2.hashElements` consumes (frees) its `FeltArray` by value, so the advice payload
49
+ // must be a separately built array — reusing the hashed array surfaces as "null pointer
50
+ // passed to rust" at the later `advice.insert`.
46
51
  const configHash = Poseidon2.hashElements(
47
- new FeltArray(buildMultisigConfigFelts(threshold, signerCommitments)),
52
+ new FeltArray(buildMultisigConfigFelts(threshold, signerCommitments, signatureScheme)),
53
+ );
54
+ const payload = new FeltArray(
55
+ buildMultisigConfigFelts(threshold, signerCommitments, signatureScheme),
48
56
  );
49
- const payload = new FeltArray(buildMultisigConfigFelts(threshold, signerCommitments));
50
57
  return { configHash, payload };
51
58
  }
52
59
 
53
60
  async function buildUpdateSignersScript(
54
61
  client: MidenClient | WasmWebClient,
55
- signatureScheme: SignatureScheme,
56
62
  midenRpcEndpoint?: string,
57
63
  ): Promise<TransactionScript> {
58
- const multisigMasm = signatureScheme === 'ecdsa' ? MULTISIG_ECDSA_MASM : MULTISIG_MASM;
59
-
60
64
  const scriptSource = `
61
- use oz_multisig::multisig
65
+ use miden::standards::auth::multisig
62
66
 
63
- begin
67
+ @transaction_script
68
+ pub proc main
64
69
  call.multisig::update_signers_and_threshold
65
70
  end
66
71
  `;
67
72
 
68
- return compileTxScript(
69
- client,
70
- scriptSource,
71
- [{ namespace: 'oz_multisig::multisig', code: multisigMasm }],
72
- midenRpcEndpoint,
73
- );
73
+ return compileTxScript(client, scriptSource, [], midenRpcEndpoint);
74
74
  }
75
75
 
76
76
  export function buildUpdateSignersTransactionRequest(
@@ -92,20 +92,28 @@ export async function buildUpdateSignersTransactionRequest(
92
92
  options: SignatureOptions = {},
93
93
  ): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }> {
94
94
  const signatureScheme = options.signatureScheme ?? 'falcon';
95
- const { configHash: configHashForAdvice, payload } = buildMultisigConfigAdvice(threshold, signerCommitments);
95
+ const { configHash: configHashForAdvice, payload } = buildMultisigConfigAdvice(
96
+ threshold,
97
+ signerCommitments,
98
+ signatureScheme,
99
+ );
96
100
 
97
- const { configHash: configHashForScript } = buildMultisigConfigAdvice(threshold, signerCommitments);
101
+ const { configHash: configHashForScript } = buildMultisigConfigAdvice(
102
+ threshold,
103
+ signerCommitments,
104
+ signatureScheme,
105
+ );
98
106
 
99
- const { configHash: configHashForReturn } = buildMultisigConfigAdvice(threshold, signerCommitments);
107
+ const { configHash: configHashForReturn } = buildMultisigConfigAdvice(
108
+ threshold,
109
+ signerCommitments,
110
+ signatureScheme,
111
+ );
100
112
 
101
113
  const advice = new AdviceMap();
102
114
  advice.insert(configHashForAdvice, payload);
103
115
 
104
- const script = await buildUpdateSignersScript(
105
- client,
106
- signatureScheme,
107
- options.midenRpcEndpoint,
108
- );
116
+ const script = await buildUpdateSignersScript(client, options.midenRpcEndpoint);
109
117
 
110
118
  const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
111
119
 
@@ -1,13 +1,20 @@
1
1
  export {
2
2
  buildConsumeNotesTransactionRequest,
3
3
  } from './transaction/consumeNotes.js';
4
- export { executeForSummary } from './transaction/summary.js';
4
+ export {
5
+ chainAnchorFromBase64,
6
+ chainAnchorToBase64,
7
+ executeForSummary,
8
+ executeForSummaryAt,
9
+ summarySalt,
10
+ } from './transaction/summary.js';
5
11
  export {
6
12
  buildP2idNoteFromMetadata,
7
13
  buildP2idTransactionRequest,
8
14
  parseP2idNoteType,
9
15
  p2idNoteTypeToMetadata,
10
16
  type P2idTransactionOptions,
17
+ type P2ideHeightOptions,
11
18
  } from './transaction/p2id.js';
12
19
  export {
13
20
  buildUpdateGuardianTransactionRequest,
@@ -37,6 +37,14 @@ interface BaseProposalMetadata {
37
37
  description: string;
38
38
  saltHex?: string;
39
39
  requiredSignatures?: number;
40
+ /**
41
+ * Base64-serialized Miden `ChainAnchor` pinning the reference block the
42
+ * proposal's transaction summary was built at. Required to verify or
43
+ * execute the proposal: since protocol 0.16 the signed summary binds the
44
+ * reference block commitment, so it only reproduces when re-executed at
45
+ * that block.
46
+ */
47
+ chainAnchor?: string;
40
48
  }
41
49
 
42
50
  export interface UpdateSignersProposalMetadata extends BaseProposalMetadata {
@@ -89,6 +97,31 @@ export function isP2idNoteVisibility(value: string): value is P2idNoteVisibility
89
97
  return value === 'public' || value === 'private';
90
98
  }
91
99
 
100
+ /** Maximum P2IDE block height: heights are `u32` on-chain (`BlockNumber`). */
101
+ export const MAX_P2IDE_BLOCK_HEIGHT = 0xffff_ffff;
102
+
103
+ /**
104
+ * Validates a P2IDE reclaim/timelock height (issue #366). Heights are `u32`
105
+ * block numbers; `0` is rejected because it is the on-chain encoding for "no
106
+ * constraint", so accepting it would silently build an unconstrained note.
107
+ * An invalid value throws rather than being dropped, which would rebuild a
108
+ * note that could never match the signed tx_summary commitment.
109
+ */
110
+ export function parseP2ideHeight(
111
+ field: 'reclaimHeight' | 'timelockHeight',
112
+ value: number | undefined,
113
+ ): number | undefined {
114
+ if (value === undefined) {
115
+ return undefined;
116
+ }
117
+ if (!Number.isInteger(value) || value < 1 || value > MAX_P2IDE_BLOCK_HEIGHT) {
118
+ throw new Error(
119
+ `unsupported ${field} '${value}': expected an integer between 1 and ${MAX_P2IDE_BLOCK_HEIGHT}`,
120
+ );
121
+ }
122
+ return value;
123
+ }
124
+
92
125
  export interface P2IdProposalMetadata extends BaseProposalMetadata {
93
126
  proposalType: 'p2id';
94
127
  recipientId: string;
@@ -96,14 +129,20 @@ export interface P2IdProposalMetadata extends BaseProposalMetadata {
96
129
  amount: string;
97
130
  /** Visibility of the created note. Absent on the wire => 'public' (pre-#322 proposals). */
98
131
  noteType?: P2idNoteVisibility;
132
+ /**
133
+ * Absolute block height at which the sender may reclaim the note (issue
134
+ * #366). Presence of either height means the proposal creates a P2IDE note
135
+ * instead of a plain P2ID note; both absent => plain P2ID (pre-#366
136
+ * proposals).
137
+ */
138
+ reclaimHeight?: number;
139
+ /** Absolute block height before which the note cannot be consumed. */
140
+ timelockHeight?: number;
99
141
  }
100
142
 
101
143
  export interface CustomProposalMetadata extends BaseProposalMetadata {
102
144
  proposalType: 'custom';
103
- /** Original server-defined proposal label, e.g. "b2agg" (issue #266). Mirrors
104
- * Rust `ProposalMetadata.proposal_type`; it is what lets a custom proposal
105
- * round-trip back to GUARDIAN/export, so it is required in the domain model.
106
- * Any wire-level optionality is resolved in the parser/codec boundary. */
145
+ /** Original server-defined proposal label, preserved during round trips. */
107
146
  rawProposalType: string;
108
147
  }
109
148
 
package/src/types.ts CHANGED
@@ -80,7 +80,6 @@ export interface MultisigConfig {
80
80
  signerCommitments: string[];
81
81
  guardianCommitment: string;
82
82
  guardianPublicKey?: string;
83
- guardianEnabled?: boolean;
84
83
  storageMode?: 'private' | 'public';
85
84
  procedureThresholds?: ProcedureThreshold[];
86
85
  signatureScheme?: SignatureScheme;
@@ -1,12 +1,13 @@
1
1
  import { AdviceMap, Felt, FeltArray, Poseidon2, Signature, Word } from '@miden-sdk/miden-sdk';
2
2
  import * as midenSdk from '@miden-sdk/miden-sdk';
3
+ import { EcdsaFormat } from './ecdsa.js';
3
4
  import { hexToBytes, normalizeHexWord } from './encoding.js';
4
5
  import type { ProposalSignatureEntry, SignatureScheme } from '../types.js';
5
6
 
6
7
  export const ECDSA_AUTH_SCHEME_ID = 1;
7
8
  export const FALCON_AUTH_SCHEME_ID = 2;
8
9
 
9
- function authSchemeId(scheme: SignatureScheme): number {
10
+ export function authSchemeId(scheme: SignatureScheme): number {
10
11
  return scheme === 'ecdsa' ? ECDSA_AUTH_SCHEME_ID : FALCON_AUTH_SCHEME_ID;
11
12
  }
12
13
 
@@ -21,31 +22,17 @@ export function signatureHexToBytes(
21
22
  return withPrefix;
22
23
  }
23
24
 
24
- function bytesToPackedU32Felts(bytes: Uint8Array): Felt[] {
25
- const felts: Felt[] = [];
26
- for (let i = 0; i < bytes.length; i += 4) {
27
- let packed = 0;
28
- for (let j = 0; j < 4 && i + j < bytes.length; j += 1) {
29
- packed |= bytes[i + j] << (j * 8);
30
- }
31
- felts.push(new Felt(BigInt(packed >>> 0)));
32
- }
33
- return felts;
34
- }
35
-
36
- function encodeEcdsaSignatureFelts(pubkeyBytes: Uint8Array, sigBytes: Uint8Array): Felt[] {
37
- const pkFelts = bytesToPackedU32Felts(pubkeyBytes);
38
- const sigFelts = bytesToPackedU32Felts(sigBytes);
39
- return [...pkFelts, ...sigFelts];
40
-
41
- }
42
-
25
+ /**
26
+ * `toPreparedSignature` is the SDK binding for the Rust
27
+ * `Signature::to_encoded_signature`, so both Falcon and ECDSA advice payloads
28
+ * come from upstream rather than being packed here. For ECDSA it emits
29
+ * `QX[8] || QY[8] || SIG_R[8] || SIG_S[8]` and recovers the public key from the
30
+ * message, which is why the signature must carry its recovery byte.
31
+ */
43
32
  export function buildSignatureAdviceEntry(
44
33
  pubkeyCommitment: Word,
45
34
  message: Word,
46
35
  signature: Signature,
47
- ecdsaPubkeyHex?: string,
48
- ecdsaSigHex?: string,
49
36
  ): { key: Word; values: Felt[] } {
50
37
  const elements = new FeltArray([
51
38
  ...pubkeyCommitment.toFelts(),
@@ -53,16 +40,31 @@ export function buildSignatureAdviceEntry(
53
40
  ]);
54
41
  const key = Poseidon2.hashElements(elements);
55
42
 
56
- let values: Felt[];
57
- if (ecdsaPubkeyHex && ecdsaSigHex) {
58
- const pkBytes = hexToBytes(ecdsaPubkeyHex);
59
- const sigBytes = hexToBytes(ecdsaSigHex);
60
- values = encodeEcdsaSignatureFelts(pkBytes, sigBytes);
61
- } else {
62
- values = signature.toPreparedSignature(message);
43
+ return { key, values: signature.toPreparedSignature(message) };
44
+ }
45
+
46
+ /** Rejects unrecoverable ECDSA signatures before entering WASM. */
47
+ export function assertEcdsaSignatureRecoverable(
48
+ signatureHex: string,
49
+ messageHex: string,
50
+ expectedPublicKeyHex: string,
51
+ ): void {
52
+ let recovered: string;
53
+ try {
54
+ recovered = EcdsaFormat.recoverCompressedPublicKeyHex(
55
+ hexToBytes(messageHex),
56
+ hexToBytes(signatureHex),
57
+ );
58
+ } catch (error) {
59
+ throw new Error(`ECDSA signature does not recover a public key: ${String(error)}`);
63
60
  }
64
61
 
65
- return { key, values };
62
+ const expected = EcdsaFormat.compressPublicKey(expectedPublicKeyHex);
63
+ if (recovered.toLowerCase() !== expected.toLowerCase()) {
64
+ throw new Error(
65
+ `ECDSA signature recovers public key ${recovered}, which does not match the expected ${expected}`,
66
+ );
67
+ }
66
68
  }
67
69
 
68
70
  export function tryComputeEcdsaCommitmentHex(pubkeyHex: string): string | null {
@@ -87,41 +89,6 @@ export function tryComputeCommitmentHex(
87
89
  }
88
90
  }
89
91
 
90
- export function verifyEcdsaCommitment(
91
- pubkeyHex: string,
92
- expectedCommitmentHex: string,
93
- ): { match: boolean; computedHex: string; packedFelts: string[]; error?: string } {
94
- try {
95
- const bytes = hexToBytes(pubkeyHex);
96
- const packedU32Values: number[] = [];
97
- for (let i = 0; i < bytes.length; i += 4) {
98
- let packed = 0;
99
- for (let j = 0; j < 4 && i + j < bytes.length; j += 1) {
100
- packed |= bytes[i + j] << (j * 8);
101
- }
102
- packedU32Values.push(packed >>> 0);
103
- }
104
-
105
- const packedFelts = bytesToPackedU32Felts(bytes);
106
- const feltArray = new FeltArray(packedFelts);
107
- const computed = Poseidon2.hashElements(feltArray);
108
- const computedHex = normalizeHexWord(computed.toHex());
109
- const expectedNorm = normalizeHexWord(expectedCommitmentHex);
110
- return {
111
- match: computedHex === expectedNorm,
112
- computedHex,
113
- packedFelts: packedU32Values.map((value) => value.toString()),
114
- };
115
- } catch (error) {
116
- return {
117
- match: false,
118
- computedHex: `ERROR: ${error}`,
119
- packedFelts: [],
120
- error: String(error),
121
- };
122
- }
123
- }
124
-
125
92
  export function mergeSignatureAdviceMaps(
126
93
  advice: AdviceMap,
127
94
  entries: Array<{ key: Word; values: Felt[] }>,
package/src/utils/word.ts CHANGED
@@ -15,6 +15,23 @@ export function wordElementToBigInt(word: Word, index: number): bigint {
15
15
  return index < elements.length ? elements[index] : 0n;
16
16
  }
17
17
 
18
+ /**
19
+ * True when every element of the word is zero. The SDK's storage-map reads
20
+ * return `Word::empty()` for a key with no entry (`StorageMap::get` is
21
+ * `unwrap_or_default()` in miden-protocol), so readers use this to detect
22
+ * absent entries.
23
+ */
24
+ export function isEmptyWord(word: Word): boolean {
25
+ const elements: BigUint64Array | bigint[] =
26
+ typeof word.toU64s === 'function' ? word.toU64s() : word.toFelts().map(f => f.asInt());
27
+ for (const element of elements) {
28
+ if (element !== 0n) {
29
+ return false;
30
+ }
31
+ }
32
+ return true;
33
+ }
34
+
18
35
  export function wordToBytes(word: { toFelts: () => Array<{ asInt: () => bigint }> }): Uint8Array {
19
36
  const felts = word.toFelts();
20
37
  const buf = new Uint8Array(32);