-
Notifications
You must be signed in to change notification settings - Fork 166
Expand file tree
/
Copy pathmod.masm
More file actions
877 lines (719 loc) · 33.7 KB
/
Copy pathmod.masm
File metadata and controls
877 lines (719 loc) · 33.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
use miden::protocol::active_account
use miden::protocol::native_account
use miden::protocol::auth::AUTH_UNAUTHORIZED_EVENT
use miden::standards::auth
use miden::standards::auth::multisig
use miden::standards::auth::multisig::APPROVER_PUBLIC_KEYS_SLOT
use miden::standards::auth::multisig::APPROVER_SCHEME_ID_SLOT
use miden::standards::auth::multisig::THRESHOLD_CONFIG_SLOT
use miden::standards::auth::multisig_smart::spending_limits
use miden::standards::auth::multisig_smart::timelock_controller
use miden::standards::auth::signature
use miden::standards::auth::tx_policy
# STORAGE SLOTS
# =================================================================================================
# Map: PROC_ROOT => smart per-procedure policy word.
const PROCEDURE_POLICIES_SLOT = word("miden::standards::auth::multisig_smart::procedure_policies")
# EXECUTION MODES
# =================================================================================================
# Execution mode value passed to procedure-policy helpers when the transaction runs the immediate
# (direct call) path.
const IMMEDIATE_EXECUTION_MODE = 0
# Execution mode value when the transaction runs the delayed (timelocked execute) path.
const DELAYED_EXECUTION_MODE = 1
# NOTE RESTRICTIONS
# =================================================================================================
# Bit-encoded ProcedurePolicyNoteRestriction enum:
# 0b00 = None, 0b01 = NoInputNotes, 0b10 = NoOutputNotes, 0b11 = NoInputOrOutputNotes.
# The bit-encoding lets us OR per-procedure restrictions together to compute their union.
const NOTE_RESTRICTION_INPUT_NOTES_MASK = 1
const NOTE_RESTRICTION_OUTPUT_NOTES_MASK = 2
# Highest valid note restriction enum value (NoInputOrOutputNotes = 0b11).
const NOTE_RESTRICTION_MAX = 3
# ERRORS
# =================================================================================================
const ERR_MALFORMED_MULTISIG_CONFIG = "number of approvers must be equal to or greater than threshold"
const ERR_ZERO_IN_MULTISIG_CONFIG = "number of approvers or threshold must not be zero"
const ERR_PROC_POLICY_INVALID_MODE = "called procedures do not support the selected execution mode"
const ERR_DELAYED_THRESHOLD_EXCEEDS_IMMEDIATE = "delayed threshold cannot exceed immediate threshold"
const ERR_NOTE_RESTRICTIONS_REQUIRE_THRESHOLD = "procedure policy note restrictions require an immediate or delayed threshold"
const ERR_NUM_APPROVERS_OR_PROC_THRESHOLD_NOT_U32 = "number of approvers and procedure threshold must be u32"
const ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS = "procedure threshold exceeds new number of approvers"
const ERR_INVALID_NOTE_RESTRICTIONS = "procedure policy note restrictions must be between 0 and 3"
const ERR_INSUFFICIENT_SIGNATURES = "insufficient number of signatures"
const ERR_EXECUTE_PATH_MISMATCH = "execute path must match delay requirement"
# LOCAL ADDRESSES (set_procedure_policy)
# =================================================================================================
const IMMEDIATE_THRESHOLD_LOC = 0
const DELAYED_THRESHOLD_LOC = 1
const NOTE_RESTRICTIONS_LOC = 2
# LOCAL ADDRESSES (compute_called_proc_policy)
# =================================================================================================
const EXECUTION_MODE_LOC = 0
const DEFAULT_THRESHOLD_LOC = 1
#! Gets the procedure policy entry for PROC_ROOT from the account's initial state.
#!
#! Inputs: [PROC_ROOT]
#! Outputs: [immediate_threshold, delayed_threshold, note_restrictions]
#!
#! Where:
#! - PROC_ROOT is the root of the account procedure whose smart policy is being read.
#! - immediate_threshold is the threshold for direct execution, or 0 when disabled.
#! - delayed_threshold is the threshold for delayed execution, or 0 when disabled.
#! - note_restrictions is the note restriction enum value in the 0..=NOTE_RESTRICTION_MAX range.
#!
#! Invocation: exec
pub proc get_procedure_policy
push.PROCEDURE_POLICIES_SLOT[0..2]
exec.active_account::get_initial_map_item
# => [immediate_threshold, delayed_threshold, note_restrictions, 0]
movup.3 drop
# => [immediate_threshold, delayed_threshold, note_restrictions]
end
#! Validates that note_restrictions is within the supported range.
#!
#! Inputs: [note_restrictions]
#! Outputs: []
#!
#! Where:
#! - note_restrictions is the policy enum value to validate.
#!
#! Panics if:
#! - note_restrictions is not a u32 value.
#! - note_restrictions is greater than NOTE_RESTRICTION_MAX.
#!
#! Invocation: exec
proc assert_valid_note_restrictions
u32assert.err=ERR_INVALID_NOTE_RESTRICTIONS
# => [note_restrictions]
u32lte.NOTE_RESTRICTION_MAX
# => [is_valid_note_restrictions]
assert.err=ERR_INVALID_NOTE_RESTRICTIONS
# => []
end
# HELPER PROCEDURES
# =================================================================================================
#! Returns the current number of approvers after any in-transaction signer update has been applied.
#!
#! Inputs: []
#! Outputs: [num_approvers]
#!
#! Where:
#! - num_approvers is the current number of signers configured in the threshold config.
#!
#! Invocation: exec
proc get_current_num_approvers
push.THRESHOLD_CONFIG_SLOT[0..2]
exec.active_account::get_item
# => [threshold, num_approvers, 0, 0]
movup.2 drop movup.2 drop
# => [threshold, num_approvers]
drop
# => [num_approvers]
end
#! Extracts the `num_approvers` field out of a MULTISIG_CONFIG word.
#!
#! MULTISIG_CONFIG layout: [threshold, num_approvers, 0, 0].
#!
#! Inputs: [MULTISIG_CONFIG]
#! Outputs: [num_approvers, MULTISIG_CONFIG]
#!
#! Where:
#! - MULTISIG_CONFIG is the multisig configuration word.
#! - num_approvers is the second felt of MULTISIG_CONFIG.
#!
#! Invocation: exec
proc multisig_config_to_num_approvers
dup.1
# => [num_approvers, MULTISIG_CONFIG]
end
#! Computes the effective transaction threshold.
#!
#! Used as a fallback layer on top of `compute_called_proc_policy`: when no non-auth procedure was
#! called (policy_threshold is 0), the tx still needs to require at least `default_threshold`
#! signatures. When any non-auth procedure was called, `compute_called_proc_policy` has already
#! folded `default_threshold` into the max for every unpolicied procedure, so `policy_threshold`
#! alone reflects the correct requirement and is returned as-is.
#!
#! Inputs: [default_threshold, policy_threshold]
#! Outputs: [transaction_threshold]
#!
#! Where:
#! - policy_threshold is the max contribution across all called non-auth procedures (each
#! contributes either its policy-selected threshold or `default_threshold` when unpolicied);
#! it is 0 only when no non-auth procedure was called.
#! - default_threshold is the account's configured default multisig threshold.
#! - transaction_threshold is the effective minimum number of signatures required.
#!
#! Invocation: exec
proc compute_tx_threshold(default_threshold: u32, policy_threshold: u32) -> u32
dup.1 eq.0
# => [is_policy_zero, default_threshold, policy_threshold]
cdrop
# => [effective_transaction_threshold]
end
#! Computes a single procedure's contribution to the threshold max-accumulator and the
#! `requires_delay` flag, given that procedure's policy thresholds and the active execution mode.
#!
#! Inputs: [immediate, delayed, execution_mode, default_threshold]
#! Outputs: [contribution, requires_delay]
#!
#! Where:
#! - immediate is the procedure's immediate-mode policy threshold (0 = not configured).
#! - delayed is the procedure's delayed-mode policy threshold (0 = not configured).
#! - execution_mode is IMMEDIATE_EXECUTION_MODE (0) or DELAYED_EXECUTION_MODE (1).
#! - default_threshold is the account's configured default multisig threshold.
#! - contribution is the policy-selected threshold when the procedure has a policy for the active
#! mode, or default_threshold when it has no policy at all.
#! - requires_delay is execution_mode when the contribution came from policy, or 0 when it came
#! from default_threshold (a default contribution does not flip the delay flag).
#!
#! Panics if:
#! - the procedure's policy exists but is configured for the opposite execution mode.
#!
#! Invocation: exec
proc compute_proc_policy_contribution
# selected = (execution_mode == DELAYED_EXECUTION_MODE) ? delayed : immediate
dup dup.2
# => [delayed, immediate, immediate, delayed, execution_mode, default_threshold]
dup.4
# => [execution_mode, delayed, immediate, immediate, delayed, execution_mode, default_threshold]
# delayed mode (1) keeps `delayed`; immediate mode (0) keeps `immediate`.
cdrop
# => [selected, immediate, delayed, execution_mode, default_threshold]
dup eq.0
# => [is_selected_zero, selected, immediate, delayed, execution_mode, default_threshold]
if.true
# Selected threshold is zero. Either there is no policy at all (both thresholds are zero)
# or the policy is configured for the opposite execution mode (the other threshold is
# non-zero). The two cases are distinguished by inspecting `immediate OR delayed`.
drop
# => [immediate, delayed, execution_mode, default_threshold]
dup dup.2 u32or eq.0
# => [is_no_policy, immediate, delayed, execution_mode, default_threshold]
if.true
# No policy for this procedure → contribute default_threshold; requires_delay = 0.
drop drop drop
# => [default_threshold]
push.0 swap
# => [default_threshold, 0]
else
# Mode misuse: policy exists but is configured for the opposite execution mode.
# Panic so the caller learns about the misconfiguration explicitly.
push.0 assert.err=ERR_PROC_POLICY_INVALID_MODE
end
else
# Selected threshold is non-zero → contribute it; requires_delay = execution_mode.
# => [selected, immediate, delayed, execution_mode, default_threshold]
movdn.2 drop drop
# => [selected, execution_mode, default_threshold]
movup.2 drop
# => [selected, execution_mode]
end
end
#! Computes the effective per-procedure policy for all called procedures.
#!
#! Iterates over all account procedures, and for those that were called in this transaction
#! accumulates:
#! - the highest required threshold (max),
#! - the union of their note restrictions (so if one proc forbids input notes and another forbids
#! output notes, the transaction ends up forbidding both),
#! - whether any called procedure requires the delayed execution mode.
#!
#! Inputs: [execution_mode, default_threshold]
#! Outputs: [policy_threshold, policy_requires_delay, note_restrictions]
#!
#! Where:
#! - execution_mode is IMMEDIATE_EXECUTION_MODE (0) or DELAYED_EXECUTION_MODE (1).
#! - default_threshold is the account's configured default multisig threshold; used as the
#! per-procedure contribution for any *called* procedure that has no stored policy. This makes
#! procedures-without-a-policy visible in the max accumulation, which closes a privilege
#! escalation where a tx mixing a low-policy proc (e.g. receive_asset = 1) with an unpolicied
#! high-impact proc (e.g. update_signers) could otherwise be authorized at the lower threshold.
#! - policy_threshold is the max across all called non-auth procedures, where each called
#! procedure contributes either its policy-selected threshold or default_threshold (no policy).
#! - policy_requires_delay is 1 when any called procedure's policy explicitly requires the
#! delayed execution mode (a default contribution does not flip this).
#! - note_restrictions is the combined (union) note restriction enum across all called procedures.
#!
#! The auth procedure (procedure index 0) is excluded — it is the auth flow itself, not a
#! user-callable account procedure, and counting it would make `policy_threshold` always include
#! `default_threshold`, breaking the override-down semantic for single-proc transactions.
#!
#! Panics if:
#! - any called procedure's policy does not support the active execution mode.
#!
#! Example (default_threshold = 3, immediate execution mode):
#!
#! receive_asset → ProcedurePolicy { immediate_threshold = 1 }
#! update_signers_and_threshold → (no policy)
#!
#! A. Only `receive_asset` is called:
#! - receive_asset: policied → contribute 1
#! - update_signers: not called → no contribution
#! - threshold_acc = max(0, 1) = 1
#! - Result: 1 signature required (override-down works for single-proc tx).
#!
#! B. Both `receive_asset` and `update_signers_and_threshold` are called:
#! - receive_asset: policied → contribute 1
#! - update_signers: called, no policy → contribute default_threshold = 3
#! - threshold_acc = max(0, 1, 3) = 3
#! - Result: 3 signatures required (privilege escalation prevented).
#!
#! Invocation: exec
#!
#! Locals:
#! - EXECUTION_MODE_LOC: execution_mode
#! - DEFAULT_THRESHOLD_LOC: default_threshold
@locals(2)
proc compute_called_proc_policy(execution_mode: u32, default_threshold: u32)
loc_store.EXECUTION_MODE_LOC
loc_store.DEFAULT_THRESHOLD_LOC
# => []
push.0 push.0 push.0 exec.active_account::get_num_procedures
# => [proc_index, threshold_acc=0, requires_delay_acc=0, restrictions_acc=0]
dup neq.0
# => [should_continue, proc_index, threshold_acc, requires_delay_acc, restrictions_acc]
while.true
sub.1
# => [proc_index, threshold_acc, requires_delay_acc, restrictions_acc]
# Procedure index 0 is the auth procedure and must never contribute to the policy
# threshold, even if it appears as a called procedure during the auth flow itself.
dup neq.0
# => [is_non_auth, proc_index, threshold_acc, requires_delay_acc, restrictions_acc]
dup.1 exec.active_account::get_procedure_root
# => [PROC_ROOT, is_non_auth, proc_index, threshold_acc, requires_delay_acc, restrictions_acc]
exec.native_account::was_procedure_called
# => [was_called, is_non_auth, proc_index, threshold_acc, requires_delay_acc, restrictions_acc]
and
# => [should_process, proc_index, threshold_acc, requires_delay_acc, restrictions_acc]
if.true
dup exec.active_account::get_procedure_root
# => [PROC_ROOT, proc_index, threshold_acc, requires_delay_acc, restrictions_acc]
exec.get_procedure_policy
# => [immediate, delayed, restr_proc, proc_index, threshold_acc, requires_delay_acc, restrictions_acc]
# Fold per-proc note_restrictions into the accumulator via OR.
# ProcedurePolicyNoteRestriction is bit-encoded (0=None, 1=NoInput, 2=NoOutput, 3=Both),
# so bitwise OR is the union of constraints: e.g. NoInput (0b01) ∪ NoOutput (0b10) = Both (0b11).
movup.6 movup.3 u32or movdn.5
# => [immediate, delayed, proc_index, threshold_acc, requires_delay_acc, new_restrictions]
loc_load.EXECUTION_MODE_LOC movdn.2
loc_load.DEFAULT_THRESHOLD_LOC movdn.3
# => [immediate, delayed, execution_mode, default_threshold, proc_index, threshold_acc, requires_delay_acc, new_restrictions]
exec.compute_proc_policy_contribution
# => [contribution, requires_delay, proc_index, threshold_acc, requires_delay_acc, new_restrictions]
# threshold_acc' = max(threshold_acc, contribution)
movup.3 u32max
# => [new_threshold, requires_delay, proc_index, requires_delay_acc, new_restrictions]
# requires_delay_acc' = requires_delay_acc OR requires_delay
swap movup.3 or
# => [new_requires_delay, new_threshold, proc_index, new_restrictions]
swap movup.2
# => [proc_index, new_threshold, new_requires_delay, new_restrictions]
end
dup neq.0
# => [should_continue, proc_index, threshold_acc, requires_delay_acc, restrictions_acc]
end
drop
# => [threshold_acc, requires_delay_acc, restrictions_acc]
end
#! Enforces note_restrictions against the current transaction.
#!
#! `note_restrictions` is treated as a bit set (see `NOTE_RESTRICTION_*_MASK`):
#! - bit 0 (mask 1) → forbid input notes
#! - bit 1 (mask 2) → forbid output notes
#!
#! Inputs: [note_restrictions]
#! Outputs: []
#!
#! Invocation: exec
pub proc enforce_note_restrictions
dup u32and.NOTE_RESTRICTION_INPUT_NOTES_MASK eq.NOTE_RESTRICTION_INPUT_NOTES_MASK
# => [has_input_note_restriction, note_restrictions]
if.true
exec.tx_policy::assert_no_input_notes
end
# => [note_restrictions]
u32and.NOTE_RESTRICTION_OUTPUT_NOTES_MASK eq.NOTE_RESTRICTION_OUTPUT_NOTES_MASK
# => [has_output_note_restriction]
if.true
exec.tx_policy::assert_no_output_notes
end
# => []
end
#! Enforces the procedure-policy for the current transaction:
#! - asserts each called procedure supports the active execution mode,
#! - asserts the union of note restrictions against the transaction's input/output notes,
#! - returns the effective threshold required by the called procedure policies and
#! whether any policy required the delayed-execution mode.
#!
#! The active execution mode is read from [`timelock_controller::is_execute_path`]: when
#! the timelock controller's `PENDING_EXECUTE` slot is set this transaction is on the
#! delayed-execute path (mode = 1); otherwise it is on the immediate path (mode = 0).
#!
#! Inputs: [default_threshold]
#! Outputs: [policy_threshold, policy_requires_delay]
#!
#! Where:
#! - default_threshold is forwarded to [`compute_called_proc_policy`] as the per-procedure
#! contribution for any called procedure without an explicit policy.
#! - policy_requires_delay is 1 when any called procedure's policy explicitly requires the
#! delayed execution mode (used downstream to enforce [`ERR_EXECUTE_PATH_MISMATCH`]).
#!
#! Invocation: exec
proc enforce_procedure_policy(default_threshold: u32)
exec.timelock_controller::is_execute_path
# => [execution_mode, default_threshold]
exec.compute_called_proc_policy
# => [policy_threshold, policy_requires_delay, note_restrictions]
movup.2
# => [note_restrictions, policy_threshold, policy_requires_delay]
exec.enforce_note_restrictions
# => [policy_threshold, policy_requires_delay]
end
#! Asserts that all configured smart per-procedure policies are valid for num_approvers.
#!
#! Inputs: [num_approvers]
#! Outputs: []
#!
#! Where:
#! - num_approvers is the number of approvers that all stored policies must remain reachable with.
#!
#! Panics if:
#! - any stored immediate or delayed threshold is not a u32 value.
#! - any stored immediate or delayed threshold exceeds num_approvers.
#!
#! Invocation: exec
proc assert_proc_policies_lte_num_approvers
exec.active_account::get_num_procedures
# => [num_procedures, num_approvers]
dup neq.0
# => [should_continue, num_procedures, num_approvers]
while.true
sub.1 dup
# => [proc_index, proc_index, num_approvers]
exec.active_account::get_procedure_root
# => [PROC_ROOT, proc_index, num_approvers]
push.PROCEDURE_POLICIES_SLOT[0..2]
# => [policy_slot_suffix, policy_slot_prefix, PROC_ROOT, proc_index, num_approvers]
# Use the *current* policy state, not the initial one. A `set_procedure_policy` earlier
# in this transaction that raised a threshold above the new num_approvers would otherwise
# be missed and the multisig could end up with an unreachable threshold.
exec.active_account::get_map_item
# => [immediate_threshold, delayed_threshold, note_restrictions, 0, proc_index, num_approvers]
# Drop the trailing 0 (depth 3) without disturbing the three policy fields above it.
movup.3 drop
# => [immediate_threshold, delayed_threshold, note_restrictions, proc_index, num_approvers]
# immediate_threshold <= num_approvers
dup.4
# => [num_approvers, immediate_threshold, delayed_threshold, note_restrictions, proc_index, num_approvers]
u32assert2.err=ERR_NUM_APPROVERS_OR_PROC_THRESHOLD_NOT_U32
u32gt assertz.err=ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS
# => [delayed_threshold, note_restrictions, proc_index, num_approvers]
# delayed_threshold <= num_approvers
dup.3
# => [num_approvers, delayed_threshold, note_restrictions, proc_index, num_approvers]
u32assert2.err=ERR_NUM_APPROVERS_OR_PROC_THRESHOLD_NOT_U32
u32gt assertz.err=ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS
# => [note_restrictions, proc_index, num_approvers]
drop
# => [proc_index, num_approvers]
dup neq.0
# => [should_continue, proc_index, num_approvers]
end
drop drop
# => []
end
# PUBLIC INTERFACE
# =================================================================================================
#! Sets or clears a smart per-procedure policy.
#!
#! Inputs: [immediate_threshold, delayed_threshold, note_restrictions, PROC_ROOT]
#! Outputs: []
#!
#! Where:
#! - immediate_threshold is the threshold for direct execution, or 0 when disabled.
#! - delayed_threshold is the threshold for delayed execution, or 0 when disabled.
#! - note_restrictions is the note restriction enum value in the 0..=NOTE_RESTRICTION_MAX range.
#! - PROC_ROOT is the root of the account procedure whose policy is being updated.
#!
#! Panics if:
#! - immediate_threshold or delayed_threshold is not a u32 value.
#! - note_restrictions is outside the supported range.
#! - either threshold exceeds the current number of approvers.
#! - delayed_threshold exceeds immediate_threshold when immediate_threshold is non-zero.
#! - note_restrictions is non-zero while both thresholds are zero.
#!
#! Invocation: call
@locals(3)
pub proc set_procedure_policy
loc_store.IMMEDIATE_THRESHOLD_LOC
# => [delayed_threshold, note_restrictions, PROC_ROOT]
loc_store.DELAYED_THRESHOLD_LOC
# => [note_restrictions, PROC_ROOT]
loc_store.NOTE_RESTRICTIONS_LOC
# => [PROC_ROOT]
# ----- Validate immediate_threshold <= num_approvers (preserving num_approvers for the
# delayed check that follows). -----
exec.get_current_num_approvers
# => [num_approvers, PROC_ROOT]
dup loc_load.IMMEDIATE_THRESHOLD_LOC swap
# => [num_approvers, immediate_threshold, num_approvers, PROC_ROOT]
u32assert2.err=ERR_NUM_APPROVERS_OR_PROC_THRESHOLD_NOT_U32
u32gt assertz.err=ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS
# => [num_approvers, PROC_ROOT]
# ----- Validate delayed_threshold <= num_approvers (consumes num_approvers). -----
loc_load.DELAYED_THRESHOLD_LOC swap
# => [num_approvers, delayed_threshold, PROC_ROOT]
u32assert2.err=ERR_NUM_APPROVERS_OR_PROC_THRESHOLD_NOT_U32
u32gt assertz.err=ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS
# => [PROC_ROOT]
# ----- Validate note_restrictions is in 0..=NOTE_RESTRICTION_MAX. -----
loc_load.NOTE_RESTRICTIONS_LOC
exec.assert_valid_note_restrictions
# => [PROC_ROOT]
# ----- Validate (immediate, delayed, note_restrictions) shape. -----
# `if.true` consumes its boolean condition, so the body branches operate on `[PROC_ROOT]`
# without any leading `drop`.
loc_load.IMMEDIATE_THRESHOLD_LOC eq.0
# => [is_immediate_threshold_zero, PROC_ROOT]
if.true
# immediate is zero. If delayed is also zero, note_restrictions must be zero, otherwise
# the policy would forbid notes for a procedure that has no threshold to authorize them.
loc_load.DELAYED_THRESHOLD_LOC eq.0
# => [is_delayed_threshold_zero, PROC_ROOT]
if.true
# `eq.0 assert` produces the proper error when note_restrictions is non-zero;
# `assertz` would surface a generic "binary value expected" error for values 2 or 3.
loc_load.NOTE_RESTRICTIONS_LOC eq.0 assert.err=ERR_NOTE_RESTRICTIONS_REQUIRE_THRESHOLD
# => [PROC_ROOT]
end
else
# immediate is non-zero. Validate delayed_threshold <= immediate_threshold.
loc_load.DELAYED_THRESHOLD_LOC loc_load.IMMEDIATE_THRESHOLD_LOC
# => [immediate_threshold, delayed_threshold, PROC_ROOT]
u32assert2.err=ERR_NUM_APPROVERS_OR_PROC_THRESHOLD_NOT_U32
u32gt assertz.err=ERR_DELAYED_THRESHOLD_EXCEEDS_IMMEDIATE
# => [PROC_ROOT]
end
# ----- Write [immediate, delayed, note_restrictions, 0] to PROCEDURE_POLICIES_SLOT[PROC_ROOT].
push.0
loc_load.NOTE_RESTRICTIONS_LOC
loc_load.DELAYED_THRESHOLD_LOC
loc_load.IMMEDIATE_THRESHOLD_LOC
# => [immediate_threshold, delayed_threshold, note_restrictions, 0, PROC_ROOT]
swapw
# => [PROC_ROOT, immediate_threshold, delayed_threshold, note_restrictions, 0]
push.PROCEDURE_POLICIES_SLOT[0..2]
# => [procedure_policies_slot_suffix, procedure_policies_slot_prefix, PROC_ROOT, POLICY_WORD]
exec.native_account::set_map_item
# => [OLD_POLICY_WORD]
dropw
# => []
end
#! Updates threshold config, approvers, and approver scheme ids for smart multisig accounts.
#!
#! Same advice map and config layout as [`multisig::update_signers_and_threshold`]. Differs by
#! validating smart procedure policies ([`assert_proc_policies_lte_num_approvers`]) instead
#! of per-procedure threshold overrides.
#!
#! Inputs:
#! Operand stack: [MULTISIG_CONFIG_COMMITMENT, pad(12)]
#! Outputs:
#! Operand stack: []
#!
#! Panics if:
#! - the new threshold exceeds the new number of approvers.
#! - the new threshold or number of approvers is zero.
#! - any existing smart procedure policy becomes unreachable under the new number of approvers.
#! - any provided scheme identifier word is malformed.
#!
#! Locals:
#! 0: new_num_of_approvers
#! 1: init_num_of_approvers
#!
#! Invocation: call
@locals(2)
pub proc update_signers_and_threshold(multisig_config_commitment: word)
adv.push_mapval
# => [MULTISIG_CONFIG_COMMITMENT, pad(12)]
adv_loadw
# => [MULTISIG_CONFIG, pad(12)]
# store new_num_of_approvers for later
exec.multisig_config_to_num_approvers loc_store.0
# => [MULTISIG_CONFIG, pad(12)]
dup dup.2
# => [num_approvers, threshold, MULTISIG_CONFIG, pad(12)]
u32assert2.err=ERR_MALFORMED_MULTISIG_CONFIG
u32gt assertz.err=ERR_MALFORMED_MULTISIG_CONFIG
# => [MULTISIG_CONFIG, pad(12)]
dup dup.2
# => [num_approvers, threshold, MULTISIG_CONFIG, pad(12)]
eq.0 assertz.err=ERR_ZERO_IN_MULTISIG_CONFIG
eq.0 assertz.err=ERR_ZERO_IN_MULTISIG_CONFIG
# => [MULTISIG_CONFIG, pad(12)]
loc_load.0
# => [num_approvers, MULTISIG_CONFIG, pad(12)]
exec.assert_proc_policies_lte_num_approvers
# => [MULTISIG_CONFIG, pad(12)]
push.THRESHOLD_CONFIG_SLOT[0..2]
# => [config_slot_suffix, config_slot_prefix, MULTISIG_CONFIG, pad(12)]
exec.native_account::set_item
# => [OLD_THRESHOLD_CONFIG, pad(12)]
# Save the old num_of_approvers for the post-loop scheme/pubkey cleanup, then drop the rest
# of OLD_THRESHOLD_CONFIG.
drop loc_store.1 drop drop
# => [pad(12)]
loc_load.0
# => [num_approvers, pad(12)]
dup neq.0
while.true
sub.1
# => [i-1, pad(12)]
dup exec.signature::create_approver_map_key
# => [APPROVER_MAP_KEY, i-1, pad(12)]
padw adv_loadw
# => [PUB_KEY, APPROVER_MAP_KEY, i-1, pad(12)]
swapw
# => [APPROVER_MAP_KEY, PUB_KEY, i-1, pad(12)]
push.APPROVER_PUBLIC_KEYS_SLOT[0..2]
# => [pub_key_slot_suffix, pub_key_slot_prefix, APPROVER_MAP_KEY, PUB_KEY, i-1, pad(12)]
exec.native_account::set_map_item
# => [OLD_VALUE, i-1, pad(12)]
adv_loadw
# => [SCHEME_ID_WORD, i-1, pad(12)]
exec.auth::signature::assert_supported_scheme_word
# => [SCHEME_ID_WORD, i-1, pad(12)]
dup.4 exec.signature::create_approver_map_key
# => [APPROVER_MAP_KEY, SCHEME_ID_WORD, i-1, pad(12)]
push.APPROVER_SCHEME_ID_SLOT[0..2]
# => [scheme_id_slot_id_suffix, scheme_id_slot_id_prefix, APPROVER_MAP_KEY, SCHEME_ID_WORD, i-1, pad(12)]
exec.native_account::set_map_item
# => [OLD_VALUE, i-1, pad(12)]
dropw
# => [i-1, pad(12)]
dup neq.0
# => [is_non_zero, i-1, pad(12)]
end
# => [pad(13)]
drop
# => [pad(12)]
loc_load.0 loc_load.1
# => [init_num_of_approvers, new_num_of_approvers, pad(12)]
exec.multisig::cleanup_pubkey_and_scheme_id_mapping
# => [pad(12)]
end
#! Authenticate a transaction using multisig smart-policy rules.
#!
#! Inputs:
#! Operand stack: [SALT]
#! Outputs:
#! Operand stack: [TX_SUMMARY_COMMITMENT]
#!
#! Locals:
#! 0: policy_threshold
#! 1: default_threshold
#! 2: policy_requires_delay
#! 3: spending_threshold
#! 4: spending_requires_delay
#! 5: num_verified_signatures
#!
#! Flow:
#! 1. Compute spending policy (amount/tier-derived threshold + requires_delay flag).
#! 2. Build the tx summary commitment used for signing and timelock proposals.
#! 3. Enforce per-procedure policy (note restrictions + policy threshold + delay flag).
#! 4. Verify approver signatures.
#! 5. Combine the policy threshold with the spending threshold via `u32max`, fall back to
#! `default_threshold` when both are zero, and assert `num_verified_signatures` meets it.
#! 6. After signature verification, enforce that the active execute-path matches whether any
#! consumed policy (procedure or spending) required the delayed mode. Running this check
#! only after verification lets a caller still produce the TX_SUMMARY_COMMITMENT needed for
#! a propose/execute round-trip from an unauthorized dry-run.
#! 7. Finalize any pending timelock propose/cancel/execute slots against the verified sig count
#! and the union of `policy_requires_delay`/`spending_requires_delay`.
#!
#! Invocation: call
@locals(6)
pub proc auth_tx(salt: word)
exec.native_account::incr_nonce drop
# => [SALT]
# ------ Computing spending policy ------
exec.spending_limits::compute_spending_policy
# => [spending_threshold, spending_requires_delay, SALT]
loc_store.3
# => [spending_requires_delay, SALT]
loc_store.4
# => [SALT]
# ------ Computing transaction summary ------
exec.auth::create_tx_summary
# => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT]
adv.insert_hqword
# => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT]
exec.auth::hash_tx_summary
# => [TX_SUMMARY_COMMITMENT]
# ------ Reading threshold config (default + num_approvers) ------
exec.multisig::get_initial_threshold_and_num_approvers
# => [default_threshold, num_of_approvers, TX_SUMMARY_COMMITMENT]
# Save default_threshold for the procedure-policy enforcement and the final tx-threshold
# fallback below.
dup loc_store.1
# => [default_threshold, num_of_approvers, TX_SUMMARY_COMMITMENT]
# ------ Enforcing procedure policy (consumes default_threshold) ------
exec.enforce_procedure_policy
# => [policy_threshold, policy_requires_delay, num_of_approvers, TX_SUMMARY_COMMITMENT]
loc_store.0
# => [policy_requires_delay, num_of_approvers, TX_SUMMARY_COMMITMENT]
loc_store.2
# => [num_of_approvers, TX_SUMMARY_COMMITMENT]
# ------ Verifying approver signatures ------
push.APPROVER_PUBLIC_KEYS_SLOT[0..2]
push.APPROVER_SCHEME_ID_SLOT[0..2]
exec.::miden::standards::auth::signature::verify_signatures
# => [num_verified_signatures, TX_SUMMARY_COMMITMENT]
dup loc_store.5
# => [num_verified_signatures, TX_SUMMARY_COMMITMENT]
# ------ Computing final transaction threshold ------
# The per-procedure pass (`compute_called_proc_policy`) has already folded `default_threshold`
# into every called procedure that lacked an explicit policy, so `policy_threshold` carries
# the procedure-side contribution. Spending limits add an independent amount-derived bound;
# combine the two via `u32max`. When both are zero (no called procedures and no spending),
# `compute_tx_threshold` falls back to `default_threshold`.
loc_load.0
loc_load.3 u32max
# => [policy_or_spending_threshold, num_verified_signatures, TX_SUMMARY_COMMITMENT]
# compute_tx_threshold expects default_threshold on top, policy_threshold underneath.
loc_load.1
# => [default_threshold, policy_or_spending_threshold, num_verified_signatures, TX_SUMMARY_COMMITMENT]
exec.compute_tx_threshold
# => [transaction_threshold, num_verified_signatures, TX_SUMMARY_COMMITMENT]
u32assert2 u32lt
# => [is_unauthorized, TX_SUMMARY_COMMITMENT]
if.true
emit.AUTH_UNAUTHORIZED_EVENT
push.0 assert.err=ERR_INSUFFICIENT_SIGNATURES
end
# ------ Enforcing execute-path consistency ------
# If a *procedure* policy demands the delayed execution mode, the transaction must already be
# on the execute path (proposed earlier, now being executed). Spending limits use their own
# `requires_delay` flag to decide whether to advance timelock proposals (below) but do not
# by themselves veto an immediate-mode transaction — that distinction belongs to the policy
# layer and is consistent with the pre-foundation behavior we are preserving here.
loc_load.2
# => [policy_requires_delay, TX_SUMMARY_COMMITMENT]
exec.timelock_controller::is_execute_path
# => [is_execute_path, policy_requires_delay, TX_SUMMARY_COMMITMENT]
# is_valid = NOT(policy_requires_delay) OR is_execute_path
swap not or
# => [is_valid_execute_path, TX_SUMMARY_COMMITMENT]
assert.err=ERR_EXECUTE_PATH_MISMATCH
# => [TX_SUMMARY_COMMITMENT]
# ------ Finalizing timelock proposals ------
# The timelock controller consumes the union of policy- and spending-derived delay flags so
# that high-spending transactions still advance any pending propose/cancel/execute slots.
loc_load.2 loc_load.4 or
# => [requires_delay, TX_SUMMARY_COMMITMENT]
loc_load.5
# => [num_verified_signatures, requires_delay, TX_SUMMARY_COMMITMENT]
exec.timelock_controller::finalize_timelock_proposals
# => [TX_SUMMARY_COMMITMENT]
end