@openzeppelin/miden-multisig-client 0.12.6 → 0.12.7

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 (89) hide show
  1. package/README.md +70 -75
  2. package/dist/account/builder.d.ts +0 -18
  3. package/dist/account/builder.d.ts.map +1 -1
  4. package/dist/account/builder.js +16 -24
  5. package/dist/account/builder.js.map +1 -1
  6. package/dist/account/masm.d.ts +2 -0
  7. package/dist/account/masm.d.ts.map +1 -1
  8. package/dist/account/masm.js +428 -1
  9. package/dist/account/masm.js.map +1 -1
  10. package/dist/account/storage.d.ts.map +1 -1
  11. package/dist/account/storage.js +5 -6
  12. package/dist/account/storage.js.map +1 -1
  13. package/dist/client.d.ts +5 -57
  14. package/dist/client.d.ts.map +1 -1
  15. package/dist/client.js +7 -53
  16. package/dist/client.js.map +1 -1
  17. package/dist/client.test.js +28 -0
  18. package/dist/client.test.js.map +1 -1
  19. package/dist/index.d.ts +3 -46
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -45
  22. package/dist/index.js.map +1 -1
  23. package/dist/inspector.d.ts +4 -30
  24. package/dist/inspector.d.ts.map +1 -1
  25. package/dist/inspector.js +13 -43
  26. package/dist/inspector.js.map +1 -1
  27. package/dist/multisig/helpers.d.ts.map +1 -1
  28. package/dist/multisig/helpers.js.map +1 -1
  29. package/dist/multisig.d.ts +39 -161
  30. package/dist/multisig.d.ts.map +1 -1
  31. package/dist/multisig.js +190 -241
  32. package/dist/multisig.js.map +1 -1
  33. package/dist/multisig.test.js +41 -31
  34. package/dist/multisig.test.js.map +1 -1
  35. package/dist/procedures.d.ts +20 -47
  36. package/dist/procedures.d.ts.map +1 -1
  37. package/dist/procedures.js +17 -50
  38. package/dist/procedures.js.map +1 -1
  39. package/dist/signer.d.ts +11 -26
  40. package/dist/signer.d.ts.map +1 -1
  41. package/dist/signer.js +41 -26
  42. package/dist/signer.js.map +1 -1
  43. package/dist/transaction/options.d.ts +2 -0
  44. package/dist/transaction/options.d.ts.map +1 -1
  45. package/dist/transaction/updatePsm.d.ts.map +1 -1
  46. package/dist/transaction/updatePsm.js +21 -3
  47. package/dist/transaction/updatePsm.js.map +1 -1
  48. package/dist/transaction/updateSigners.d.ts.map +1 -1
  49. package/dist/transaction/updateSigners.js +21 -3
  50. package/dist/transaction/updateSigners.js.map +1 -1
  51. package/dist/transaction.d.ts.map +1 -1
  52. package/dist/transaction.js +0 -2
  53. package/dist/transaction.js.map +1 -1
  54. package/dist/transaction.test.js +5 -0
  55. package/dist/transaction.test.js.map +1 -1
  56. package/dist/types/proposal.d.ts +16 -8
  57. package/dist/types/proposal.d.ts.map +1 -1
  58. package/dist/types.d.ts +18 -14
  59. package/dist/types.d.ts.map +1 -1
  60. package/dist/utils/signature.d.ts +10 -2
  61. package/dist/utils/signature.d.ts.map +1 -1
  62. package/dist/utils/signature.js +77 -5
  63. package/dist/utils/signature.js.map +1 -1
  64. package/dist/utils/signature.test.js +16 -1
  65. package/dist/utils/signature.test.js.map +1 -1
  66. package/masm/multisig_ecdsa.masm +424 -0
  67. package/masm/psm_ecdsa.masm +179 -0
  68. package/package.json +2 -2
  69. package/src/account/builder.ts +18 -25
  70. package/src/account/masm.ts +433 -2
  71. package/src/account/storage.ts +5 -6
  72. package/src/client.test.ts +36 -0
  73. package/src/client.ts +10 -59
  74. package/src/index.ts +13 -62
  75. package/src/inspector.ts +15 -44
  76. package/src/multisig/helpers.ts +1 -2
  77. package/src/multisig.test.ts +45 -34
  78. package/src/multisig.ts +246 -273
  79. package/src/procedures.ts +21 -56
  80. package/src/signer.ts +48 -28
  81. package/src/transaction/options.ts +2 -0
  82. package/src/transaction/updatePsm.ts +24 -3
  83. package/src/transaction/updateSigners.ts +25 -3
  84. package/src/transaction.test.ts +6 -0
  85. package/src/transaction.ts +0 -2
  86. package/src/types/proposal.ts +15 -8
  87. package/src/types.ts +26 -16
  88. package/src/utils/signature.test.ts +24 -1
  89. package/src/utils/signature.ts +86 -4
@@ -1,9 +1,3 @@
1
- /**
2
- * Account builder for creating multisig accounts with PSM authentication.
3
- *
4
- * This module provides functionality to create multisig accounts.
5
- */
6
-
7
1
  import {
8
2
  AccountBuilder,
9
3
  AccountComponent,
@@ -13,37 +7,33 @@ import {
13
7
  } from '@demox-labs/miden-sdk';
14
8
  import type { MultisigConfig, CreateAccountResult } from '../types.js';
15
9
  import { buildMultisigStorageSlots, buildPsmStorageSlots } from './storage.js';
16
- import { MULTISIG_MASM, PSM_MASM } from './masm.js';
10
+ import { MULTISIG_MASM, MULTISIG_ECDSA_MASM, PSM_MASM, PSM_ECDSA_MASM } from './masm.js';
17
11
 
18
- /**
19
- * Creates a multisig account with PSM authentication.
20
- *
21
- * @param webClient - Initialized Miden WebClient
22
- * @param config - Multisig configuration
23
- * @returns The created account and seed
24
- */
25
12
  export async function createMultisigAccount(
26
13
  webClient: WebClient,
27
14
  config: MultisigConfig
28
15
  ): Promise<CreateAccountResult> {
29
16
  validateMultisigConfig(config);
30
17
 
18
+ const signatureScheme = config.signatureScheme ?? 'falcon';
31
19
  const multisigSlots = buildMultisigStorageSlots(config);
32
20
  const psmSlots = buildPsmStorageSlots(config);
33
21
 
34
22
  const psmBuilder = webClient.createScriptBuilder();
23
+ const psmMasm = signatureScheme === 'ecdsa' ? PSM_ECDSA_MASM : PSM_MASM;
35
24
  const psmComponent = AccountComponent
36
- .compile(PSM_MASM, psmBuilder, psmSlots)
25
+ .compile(psmMasm, psmBuilder, psmSlots)
37
26
  .withSupportsAllTypes();
38
27
 
28
+ const multisigMasm = signatureScheme === 'ecdsa' ? MULTISIG_ECDSA_MASM : MULTISIG_MASM;
29
+ const psmLibraryPath = signatureScheme === 'ecdsa' ? 'openzeppelin::psm_ecdsa' : 'openzeppelin::psm';
39
30
  const multisigBuilder = webClient.createScriptBuilder();
40
- const psmLib = multisigBuilder.buildLibrary('openzeppelin::psm', PSM_MASM);
31
+ const psmLib = multisigBuilder.buildLibrary(psmLibraryPath, psmMasm);
41
32
  multisigBuilder.linkStaticLibrary(psmLib);
42
33
  const multisigComponent = AccountComponent
43
- .compile(MULTISIG_MASM, multisigBuilder, multisigSlots)
34
+ .compile(multisigMasm, multisigBuilder, multisigSlots)
44
35
  .withSupportsAllTypes();
45
36
 
46
- // Generate random seed
47
37
  const seed = new Uint8Array(32);
48
38
  crypto.getRandomValues(seed);
49
39
 
@@ -68,12 +58,6 @@ export async function createMultisigAccount(
68
58
  };
69
59
  }
70
60
 
71
- /**
72
- * Validates a multisig configuration.
73
- *
74
- * @param config - The configuration to validate
75
- * @throws Error if configuration is invalid
76
- */
77
61
  export function validateMultisigConfig(config: MultisigConfig): void {
78
62
  if (config.threshold === 0) {
79
63
  throw new Error('threshold must be greater than 0');
@@ -81,6 +65,16 @@ export function validateMultisigConfig(config: MultisigConfig): void {
81
65
  if (config.signerCommitments.length === 0) {
82
66
  throw new Error('at least one signer commitment is required');
83
67
  }
68
+ for (const commitment of config.signerCommitments) {
69
+ const stripped = commitment.startsWith('0x') || commitment.startsWith('0X')
70
+ ? commitment.slice(2)
71
+ : commitment;
72
+ if (stripped.length > 64) {
73
+ throw new Error(
74
+ `signerCommitments must be 32-byte commitment hex (64 chars), got ${stripped.length} chars`
75
+ );
76
+ }
77
+ }
84
78
  if (config.threshold > config.signerCommitments.length) {
85
79
  throw new Error(
86
80
  `threshold (${config.threshold}) cannot exceed number of signers (${config.signerCommitments.length})`
@@ -90,7 +84,6 @@ export function validateMultisigConfig(config: MultisigConfig): void {
90
84
  throw new Error('PSM commitment is required');
91
85
  }
92
86
 
93
- // Validate procedure thresholds if provided
94
87
  if (config.procedureThresholds) {
95
88
  const seen = new Set<string>();
96
89
  for (const pt of config.procedureThresholds) {
@@ -1,5 +1,3 @@
1
- // Embedded MASM constants - content from masm/*.masm files
2
-
3
1
  export const MULTISIG_MASM = `# Multi-Signature RPO Falcon 512 Authentication Component
4
2
  #
5
3
  # This component provides multi-signature authentication for accounts.
@@ -425,6 +423,432 @@ pub proc auth_tx_rpo_falcon512_multisig.1(salt: BeWord)
425
423
  end
426
424
  `;
427
425
 
426
+ export const MULTISIG_ECDSA_MASM = `# Multi-Signature ECDSA secp256k1 Authentication Component
427
+ #
428
+ # This component provides multi-signature authentication for accounts using
429
+ # ECDSA secp256k1 (k256/keccak) signatures instead of RPO Falcon 512.
430
+ # It integrates with the PSM component for optional PSM signature verification.
431
+
432
+ use miden::active_account
433
+ use miden::native_account
434
+ use miden::auth
435
+ use openzeppelin::psm_ecdsa
436
+
437
+ # Type definitions for v0.12 syntax
438
+ type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt }
439
+
440
+ # CONSTANTS
441
+ # =================================================================================================
442
+
443
+ # Auth Request Constants
444
+
445
+ # The event emitted when a signature is not found for a required signer.
446
+ const AUTH_UNAUTHORIZED_EVENT = event("miden::auth::unauthorized")
447
+
448
+ # Storage Layout Constants
449
+ #
450
+ # +-------------------------------+----------+--------------+-------------------+
451
+ # | THRESHOLD & APPROVERS CONFIG | PUB KEYS | EXECUTED TXS | PROC THRESHOLDS |
452
+ # | (slot) | (map) | (map) | (map) |
453
+ # +-------------------------------+----------+--------------+-------------------+
454
+ # | 0 | 1 | 2 | 3 |
455
+ # +-------------------------------+----------+--------------+-------------------+
456
+
457
+ # The slot in this component's storage layout where the default signature threshold and
458
+ # number of approvers are stored as:
459
+ # [default_threshold, num_approvers, 0, 0].
460
+ # The threshold is guaranteed to be less than or equal to num_approvers.
461
+ const THRESHOLD_CONFIG_SLOT = 0
462
+
463
+ # The slot in this component's storage layout where the public keys map is stored.
464
+ # Map entries: [key_index, 0, 0, 0] => APPROVER_PUBLIC_KEY
465
+ const PUBLIC_KEYS_MAP_SLOT = 1
466
+
467
+ # The slot in this component's storage layout where executed transactions are stored.
468
+ # Map entries: transaction_message => [is_executed, 0, 0, 0]
469
+ const EXECUTED_TXS_SLOT = 2
470
+
471
+ # The slot in this component's storage layout where procedure thresholds are stored.
472
+ # Map entries: PROC_ROOT => [proc_threshold, 0, 0, 0]
473
+ const PROC_THRESHOLD_ROOTS_SLOT = 3
474
+
475
+ # Executed Transaction Flag Constant
476
+ const IS_EXECUTED_FLAG = [1, 0, 0, 0]
477
+
478
+ # ERRORS
479
+ const ERR_TX_ALREADY_EXECUTED = "failed to approve multisig transaction as it was already executed"
480
+
481
+ const ERR_MALFORMED_MULTISIG_CONFIG = "number of approvers must be equal to or greater than threshold"
482
+
483
+ const ERR_ZERO_IN_MULTISIG_CONFIG = "number of approvers or threshold must not be zero"
484
+
485
+ # MULTISIG PROCEDURES
486
+ # =================================================================================================
487
+
488
+ #! Check if transaction has already been executed and add it to executed transactions for replay protection.
489
+ #!
490
+ #! Inputs: [MSG]
491
+ #! Outputs: []
492
+ #!
493
+ #! Panics if:
494
+ #! - the same transaction has already been executed
495
+ proc assert_new_tx(msg: BeWord)
496
+ push.IS_EXECUTED_FLAG
497
+ # => [[0, 0, 0, is_executed], MSG]
498
+
499
+ swapw
500
+ # => [MSG, IS_EXECUTED_FLAG]
501
+
502
+ push.EXECUTED_TXS_SLOT
503
+ # => [index, MSG, IS_EXECUTED_FLAG]
504
+
505
+ # Set the key value pair in the map to mark transaction as executed
506
+ exec.native_account::set_map_item
507
+ # => [OLD_MAP_ROOT, [0, 0, 0, is_executed]]
508
+
509
+ dropw drop drop drop
510
+ # => [is_executed]
511
+
512
+ assertz.err=ERR_TX_ALREADY_EXECUTED
513
+ # => []
514
+ end
515
+
516
+ #! Remove old approver public keys from the approver public key mapping.
517
+ #!
518
+ #! This procedure cleans up the storage by removing public keys of approvers that are no longer
519
+ #! part of the multisig configuration. This procedure assumes that init_num_of_approvers and
520
+ #! new_num_of_approvers are u32 values.
521
+ #!
522
+ #! Inputs: [init_num_of_approvers, new_num_of_approvers]
523
+ #! Outputs: []
524
+ #!
525
+ #! Where:
526
+ #! - init_num_of_approvers is the original number of approvers before the update
527
+ #! - new_num_of_approvers is the new number of approvers after the update
528
+ proc cleanup_pubkey_mapping(init_num_of_approvers: u32, new_num_of_approvers: u32)
529
+ dup.1 dup.1
530
+ u32assert2 u32lt
531
+ # => [should_loop, i = init_num_of_approvers, new_num_of_approvers]
532
+
533
+ while.true
534
+ # => [i, new_num_of_approvers]
535
+
536
+ sub.1
537
+ # => [i-1, new_num_of_approvers]
538
+
539
+ dup
540
+ # => [i-1, i-1, new_num_of_approvers]
541
+
542
+ push.0.0.0
543
+ # => [[0, 0, 0, i-1], i-1, new_num_of_approvers]
544
+
545
+ padw swapw
546
+ # => [[0, 0, 0, i-1], EMPTY_WORD, i-1, new_num_of_approvers]
547
+
548
+ push.PUBLIC_KEYS_MAP_SLOT
549
+ # => [pub_key_slot_idx, [0, 0, 0, i-1], EMPTY_WORD, i-1, new_num_of_approvers]
550
+
551
+ exec.native_account::set_map_item
552
+ # => [OLD_MAP_ROOT, OLD_MAP_VALUE, i-1, new_num_of_approvers]
553
+
554
+ dropw dropw
555
+ # => [i-1, new_num_of_approvers]
556
+
557
+ dup.1 dup.1
558
+ u32lt
559
+ # => [should_loop, i-1, new_num_of_approvers]
560
+ end
561
+
562
+ drop drop
563
+ # => []
564
+ end
565
+
566
+ #! Update threshold config and add / remove approvers
567
+ #!
568
+ #! Inputs:
569
+ #! Operand stack: [MULTISIG_CONFIG_HASH, pad(12)]
570
+ #! Advice map: {
571
+ #! MULTISIG_CONFIG_HASH => [CONFIG, PUB_KEY_N, PUB_KEY_N-1, ..., PUB_KEY_0]
572
+ #! }
573
+ #! Outputs:
574
+ #! Operand stack: []
575
+ #!
576
+ #! Where:
577
+ #! - MULTISIG_CONFIG_HASH is the hash of the threshold and new public key vector
578
+ #! - MULTISIG_CONFIG is [threshold, num_approvers, 0, 0]
579
+ #! - PUB_KEY_i is the public key of the i-th signer
580
+ #!
581
+ #! Locals:
582
+ #! 0: new_num_of_approvers
583
+ #! 1: init_num_of_approvers
584
+ pub proc update_signers_and_threshold.2(multisig_config_hash: BeWord)
585
+ adv.push_mapval
586
+ # => [MULTISIG_CONFIG_HASH, pad(12)]
587
+
588
+ adv_loadw
589
+ # => [MULTISIG_CONFIG, pad(12)]
590
+
591
+ # store new_num_of_approvers for later
592
+ dup.2 loc_store.0
593
+ # => [MULTISIG_CONFIG, pad(12)]
594
+
595
+ dup.3 dup.3
596
+ # => [num_approvers, threshold, MULTISIG_CONFIG, pad(12)]
597
+
598
+ # make sure that the threshold is smaller than the number of approvers
599
+ u32assert2.err=ERR_MALFORMED_MULTISIG_CONFIG
600
+ u32gt assertz.err=ERR_MALFORMED_MULTISIG_CONFIG
601
+ # => [MULTISIG_CONFIG, pad(12)]
602
+
603
+ dup.3 dup.3
604
+ # => [num_approvers, threshold, MULTISIG_CONFIG, pad(12)]
605
+
606
+ # make sure that threshold or num_approvers are not zero
607
+ eq.0 assertz.err=ERR_ZERO_IN_MULTISIG_CONFIG
608
+ eq.0 assertz.err=ERR_ZERO_IN_MULTISIG_CONFIG
609
+ # => [MULTISIG_CONFIG, pad(12)]
610
+
611
+ push.THRESHOLD_CONFIG_SLOT
612
+ # => [slot, MULTISIG_CONFIG, pad(12)]
613
+
614
+ exec.native_account::set_item
615
+ # => [OLD_THRESHOLD_CONFIG, pad(12)]
616
+
617
+ # store init_num_of_approvers for later
618
+ drop drop loc_store.1 drop
619
+ # => [pad(12)]
620
+
621
+ loc_load.0
622
+ # => [num_approvers]
623
+
624
+ dup neq.0
625
+ while.true
626
+ sub.1
627
+ # => [i-1, pad(12)]
628
+
629
+ dup push.0.0.0
630
+ # => [[0, 0, 0, i-1], i-1, pad(12)]
631
+
632
+ padw adv_loadw
633
+ # => [PUB_KEY, [0, 0, 0, i-1], i-1, pad(12)]
634
+
635
+ swapw
636
+ # => [[0, 0, 0, i-1], PUB_KEY, i-1, pad(12)]
637
+
638
+ push.PUBLIC_KEYS_MAP_SLOT
639
+ # => [pub_key_slot_idx, [0, 0, 0, i-1], PUB_KEY, i-1, pad(12)]
640
+
641
+ exec.native_account::set_map_item
642
+ # => [OLD_MAP_ROOT, OLD_MAP_VALUE, i-1, pad(12)]
643
+
644
+ dropw dropw
645
+ # => [i-1, pad(12)]
646
+
647
+ dup neq.0
648
+ # => [is_non_zero, i-1, pad(12)]
649
+ end
650
+ # => [pad(13)]
651
+
652
+ drop
653
+ # => [pad(12)]
654
+
655
+ # compare initial vs current multisig config
656
+
657
+ # load init_num_of_approvers & new_num_of_approvers
658
+ loc_load.0 loc_load.1
659
+ # => [init_num_of_approvers, new_num_of_approvers, pad(12)]
660
+
661
+ exec.cleanup_pubkey_mapping
662
+ # => [pad(12)]
663
+ end
664
+
665
+ # Computes the effective transaction threshold based on called procedures and per-procedure
666
+ # overrides stored in PROC_THRESHOLD_ROOTS_SLOT. Falls back to default_threshold if no
667
+ # overrides apply.
668
+ #
669
+ #! Inputs: [default_threshold]
670
+ #! Outputs: [transaction_threshold]
671
+ proc compute_transaction_threshold.1(default_threshold: u32) -> u32
672
+ # 1. initialize transaction_threshold = 0
673
+ # 2. iterate through all account procedures
674
+ # a. check if the procedure was called during the transaction
675
+ # b. if called, get the override threshold of that procedure from the config map
676
+ # c. if proc_threshold > transaction_threshold, set transaction_threshold = proc_threshold
677
+ # 3. if transaction_threshold == 0 at the end, revert to using default_threshold
678
+
679
+ # store default_threshold for later
680
+ loc_store.0
681
+ # => []
682
+
683
+ # 1. initialize transaction_threshold = 0
684
+ push.0
685
+ # => [transaction_threshold]
686
+
687
+ # get the number of account procedures
688
+ exec.active_account::get_num_procedures
689
+ # => [num_procedures, transaction_threshold]
690
+
691
+ # 2. iterate through all account procedures
692
+ dup neq.0
693
+ # => [should_continue, num_procedures, transaction_threshold]
694
+ while.true
695
+ sub.1 dup
696
+ # => [num_procedures-1, num_procedures-1, transaction_threshold]
697
+
698
+ # get procedure root of the procedure with index i
699
+ exec.active_account::get_procedure_root dupw
700
+ # => [PROC_ROOT, PROC_ROOT, num_procedures-1, transaction_threshold]
701
+
702
+ # 2a. check if this procedure has been called in the transaction
703
+ exec.native_account::was_procedure_called
704
+ # => [was_called, PROC_ROOT, num_procedures-1, transaction_threshold]
705
+
706
+ # if it has been called, get the override threshold of that procedure
707
+ if.true
708
+ # => [PROC_ROOT, num_procedures-1, transaction_threshold]
709
+
710
+ push.PROC_THRESHOLD_ROOTS_SLOT
711
+ # => [PROC_THRESHOLD_ROOTS_SLOT, PROC_ROOT, num_procedures-1, transaction_threshold]
712
+
713
+ # 2b. get the override proc_threshold of that procedure
714
+ # if the procedure has no override threshold, the returned map item will be [0, 0, 0, 0]
715
+ exec.active_account::get_initial_map_item
716
+ # => [[0, 0, 0, proc_threshold], num_procedures-1, transaction_threshold]
717
+
718
+ drop drop drop dup dup.3
719
+ # => [transaction_threshold, proc_threshold, proc_threshold, num_procedures-1, transaction_threshold]
720
+
721
+ u32assert2.err="transaction threshold or procedure threshold are not u32"
722
+ u32gt
723
+ # => [is_gt, proc_threshold, num_procedures-1, transaction_threshold]
724
+ # 2c. if proc_threshold > transaction_threshold, update transaction_threshold
725
+ movup.2 movdn.3
726
+ # => [is_gt, proc_threshold, transaction_threshold, num_procedures-1]
727
+ cdrop
728
+ # => [updated_transaction_threshold, num_procedures-1]
729
+ swap
730
+ # => [num_procedures-1, updated_transaction_threshold]
731
+ # if it has not been called during this transaction, nothing to do, move to the next procedure
732
+ else
733
+ dropw
734
+ # => [num_procedures-1, transaction_threshold]
735
+ end
736
+
737
+ dup neq.0
738
+ # => [should_continue, num_procedures-1, transaction_threshold]
739
+ end
740
+
741
+ drop
742
+ # => [transaction_threshold]
743
+
744
+ loc_load.0
745
+ # => [default_threshold, transaction_threshold]
746
+
747
+ # 3. if transaction_threshold == 0 at the end, revert to using default_threshold
748
+ dup.1 eq.0
749
+ # => [is_zero, default_threshold, transaction_threshold]
750
+
751
+ cdrop
752
+ # => [effective_transaction_threshold]
753
+ end
754
+
755
+ #! Authenticate a transaction using ECDSA secp256k1 with multi-signature support.
756
+ #!
757
+ #! This procedure implements multi-signature authentication by:
758
+ #! 1. Computing the transaction summary message that needs to be signed
759
+ #! 2. Verifying ECDSA signatures from multiple required signers against their public keys
760
+ #! 3. Ensuring the minimum threshold of valid signatures is met
761
+ #! 4. Implementing replay protection by tracking executed transactions
762
+ #! 5. Verifying PSM signature if PSM selector is enabled (via PSM component)
763
+ #!
764
+ #! Inputs:
765
+ #! Operand stack: [SALT]
766
+ #! Advice map: {
767
+ #! h(PUB_KEY_0, MSG): SIG_0,
768
+ #! h(PUB_KEY_1, MSG): SIG_1,
769
+ #! h(PUB_KEY_n, MSG): SIG_n
770
+ #! }
771
+ #! Outputs:
772
+ #! Operand stack: []
773
+ #!
774
+ #! Where:
775
+ #! - SALT is a cryptographically random nonce that enables multiple concurrent
776
+ #! multisig transactions while maintaining replay protection. Each transaction
777
+ #! must use a unique SALT value to ensure transaction uniqueness.
778
+ #! - SIG_i is the ECDSA signature from the i-th signer.
779
+ #! - MSG is the transaction message being signed.
780
+ #! - h(PUB_KEY_i, MSG) is the hash of the public key and message used as the advice map key.
781
+ #!
782
+ #! Panics if:
783
+ #! - insufficient number of valid signatures (below threshold).
784
+ #! - the same transaction has already been executed (replay protection).
785
+ #! - PSM signature verification fails (if PSM is enabled).
786
+ #!
787
+ #! Invocation: call
788
+ pub proc auth_tx_ecdsa_multisig.1(salt: BeWord)
789
+ exec.native_account::incr_nonce drop
790
+ # => [SALT]
791
+
792
+ # ------ Computing transaction summary ------
793
+
794
+ exec.auth::create_tx_summary
795
+ # => [SALT, OUTPUT_NOTES_COMMITMENT, INPUT_NOTES_COMMITMENT, ACCOUNT_DELTA_COMMITMENT]
796
+
797
+ # to build a tx_summary in the host, we need these four words in the advice provider
798
+ exec.auth::adv_insert_hqword
799
+ # => [SALT, OUTPUT_NOTES_COMMITMENT, INPUT_NOTES_COMMITMENT, ACCOUNT_DELTA_COMMITMENT]
800
+
801
+ # the commitment to the tx summary is the message that is signed
802
+ exec.auth::hash_tx_summary
803
+ # => [TX_SUMMARY_COMMITMENT]
804
+
805
+ # ------ Verifying approver signatures ------
806
+
807
+ push.THRESHOLD_CONFIG_SLOT
808
+ # => [index, TX_SUMMARY_COMMITMENT]
809
+
810
+ exec.active_account::get_initial_item
811
+ # => [0, 0, num_of_approvers, default_threshold, TX_SUMMARY_COMMITMENT]
812
+
813
+ drop drop
814
+ # => [num_of_approvers, default_threshold, TX_SUMMARY_COMMITMENT]
815
+
816
+ swap movdn.5
817
+ # => [num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold]
818
+
819
+ push.PUBLIC_KEYS_MAP_SLOT
820
+ # => [pub_key_slot_idx, num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold]
821
+
822
+ exec.::miden::auth::ecdsa_k256_keccak::verify_signatures
823
+ # => [num_verified_signatures, TX_SUMMARY_COMMITMENT, default_threshold]
824
+
825
+ # ------ Checking threshold is >= num_verified_signatures ------
826
+
827
+ movup.5
828
+ # => [default_threshold, num_verified_signatures, TX_SUMMARY_COMMITMENT]
829
+
830
+ exec.compute_transaction_threshold
831
+ # => [transaction_threshold, num_verified_signatures, TX_SUMMARY_COMMITMENT]
832
+
833
+ u32assert2 u32lt
834
+ # => [is_unauthorized, TX_SUMMARY_COMMITMENT]
835
+
836
+ # If signatures are non-existent the tx will fail here.
837
+ if.true
838
+ emit.AUTH_UNAUTHORIZED_EVENT
839
+ push.0 assert.err="insufficient number of signatures"
840
+ end
841
+
842
+ # ------ Verifying PSM Signature ------
843
+ # => [TX_SUMMARY_COMMITMENT]
844
+ call.psm_ecdsa::verify_psm_signature
845
+
846
+ # ------ Writing executed transaction MSG to map ------
847
+ # => [TX_SUMMARY_COMMITMENT]
848
+ exec.assert_new_tx
849
+ end
850
+ `;
851
+
428
852
  export const PSM_MASM = `# Private State Manager (PSM) Authentication Component
429
853
  #
430
854
  # This component provides PSM signature verification for accounts.
@@ -604,3 +1028,10 @@ pub proc verify_psm_signature(msg: BeWord)
604
1028
  exec.enable_psm
605
1029
  end
606
1030
  `;
1031
+
1032
+ export const PSM_ECDSA_MASM = PSM_MASM
1033
+ .replaceAll('RPO Falcon 512', 'ECDSA secp256k1')
1034
+ .replace(
1035
+ 'exec.::miden::auth::rpo_falcon512::verify_signatures',
1036
+ 'exec.::miden::auth::ecdsa_k256_keccak::verify_signatures',
1037
+ );
@@ -1,6 +1,6 @@
1
1
  import type { MultisigConfig } from '../types.js';
2
2
  import { StorageSlot, StorageMap, Word } from '@demox-labs/miden-sdk';
3
- import { ensureHexPrefix } from '../utils/encoding.js';
3
+ import { normalizeHexWord } from '../utils/encoding.js';
4
4
  import { getProcedureRoot } from '../procedures.js';
5
5
 
6
6
  export class StorageLayoutBuilder {
@@ -19,19 +19,18 @@ export class StorageLayoutBuilder {
19
19
  const signersMap = new StorageMap();
20
20
  config.signerCommitments.forEach((commitment, index) => {
21
21
  const key = new Word(new BigUint64Array([BigInt(index), 0n, 0n, 0n]));
22
- const value = Word.fromHex(ensureHexPrefix(commitment));
22
+ const value = Word.fromHex(normalizeHexWord(commitment));
23
23
  signersMap.insert(key, value);
24
24
  });
25
25
  const slot1 = StorageSlot.map(signersMap);
26
26
 
27
27
  const slot2 = StorageSlot.map(new StorageMap());
28
28
 
29
- // Map entries: PROC_ROOT => [proc_threshold, 0, 0, 0]
30
- // Use SDK's Word.fromHex to match how account code procedure roots are represented
31
29
  const procThresholdMap = new StorageMap();
32
30
  if (config.procedureThresholds) {
31
+ const signatureScheme = config.signatureScheme ?? 'falcon';
33
32
  for (const pt of config.procedureThresholds) {
34
- const rootHex = getProcedureRoot(pt.procedure);
33
+ const rootHex = getProcedureRoot(pt.procedure, signatureScheme);
35
34
  const key = Word.fromHex(rootHex);
36
35
  const value = new Word(new BigUint64Array([BigInt(pt.threshold), 0n, 0n, 0n]));
37
36
  procThresholdMap.insert(key, value);
@@ -49,7 +48,7 @@ export class StorageLayoutBuilder {
49
48
 
50
49
  const psmKeyMap = new StorageMap();
51
50
  const zeroKey = new Word(new BigUint64Array([0n, 0n, 0n, 0n]));
52
- const psmKey = Word.fromHex(ensureHexPrefix(config.psmCommitment));
51
+ const psmKey = Word.fromHex(normalizeHexWord(config.psmCommitment));
53
52
  psmKeyMap.insert(zeroKey, psmKey);
54
53
  const slot1 = StorageSlot.map(psmKeyMap);
55
54
 
@@ -73,6 +73,7 @@ describe('MultisigClient', () => {
73
73
  };
74
74
 
75
75
  mockSigner = {
76
+ scheme: 'falcon',
76
77
  commitment: '0x' + '1'.repeat(64),
77
78
  publicKey: '0x' + '2'.repeat(64),
78
79
  signAccountIdWithTimestamp: vi.fn().mockReturnValue('0x' + 'a'.repeat(128)),
@@ -174,4 +175,39 @@ describe('MultisigClient', () => {
174
175
  ).rejects.toThrow();
175
176
  });
176
177
  });
178
+
179
+ describe('initialize', () => {
180
+ it('should fetch PSM pubkey and return commitment', async () => {
181
+ const client = new MultisigClient(webClient);
182
+
183
+ mockFetch.mockResolvedValueOnce({
184
+ ok: true,
185
+ json: async () => ({
186
+ commitment: '0x' + 'f'.repeat(64),
187
+ pubkey: '0x' + 'e'.repeat(64),
188
+ }),
189
+ });
190
+
191
+ const result = await client.initialize('falcon');
192
+
193
+ expect(result.psmCommitment).toBe('0x' + 'f'.repeat(64));
194
+ expect(result.psmPublicKey).toBe('0x' + 'e'.repeat(64));
195
+ });
196
+
197
+ it('should work without specifying scheme', async () => {
198
+ const client = new MultisigClient(webClient);
199
+
200
+ mockFetch.mockResolvedValueOnce({
201
+ ok: true,
202
+ json: async () => ({
203
+ commitment: '0x' + 'f'.repeat(64),
204
+ }),
205
+ });
206
+
207
+ const result = await client.initialize();
208
+
209
+ expect(result.psmCommitment).toBe('0x' + 'f'.repeat(64));
210
+ expect(result.psmPublicKey).toBeUndefined();
211
+ });
212
+ });
177
213
  });