Skip to content

feat: introduce shared procedure policies for multisig auth#2670

Open
onurinanc wants to merge 15 commits into0xMiden:nextfrom
onurinanc:procedure-policies
Open

feat: introduce shared procedure policies for multisig auth#2670
onurinanc wants to merge 15 commits into0xMiden:nextfrom
onurinanc:procedure-policies

Conversation

@onurinanc
Copy link
Copy Markdown
Collaborator

Related Issue: #2669

@onurinanc onurinanc changed the title refactor: Introduce procedure_policies rather than proc_thresholds for AuthMultisig feat: introduce shared procedure policies for multisig auth Mar 23, 2026
Copy link
Copy Markdown
Contributor

@PhilippGackstatter PhilippGackstatter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I think the overall logic makes sense.

I left a few comments about structure, validation and readability. Overall:

  • There is a lot of terminology that would be good to explain in a bit more detail (I think ProcedurePolicy type would be a good place):
    • immediate_threshold
    • delay_threshold
    • lane / mode
    • note restrictions
    • multisig runtime (I'd try to avoid this term if possible)
    • transaction-shape constraint / note-shape constraint
  • We should ensure we construct only valid instances of types. Applies to AuthMultisigConfig and ProcedurePolicyMode.
  • In general, it would be good to avoid making fields in structs public because that makes it harder to evolve these structs without breaking the API.
  • Afaict, we use a couple of terms interchangebly and it'd be great to pick one and use it consistently:
    • "lane" and "mode" (I like the term lane in this context)
    • "constraint" and "restriction"
    • "direct execution lane" and "immediate execution"

Comment on lines +93 to +94

const MAX_PROC_POLICY_NOTE_RESTRICTIONS = 3
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This constant should probably go into the constants section above rather than the errors section. Would also be good to add a short doc comment.

Comment on lines +194 to +203
pub proc get_procedure_policy(proc_root: word)
push.PROC_POLICY_ROOTS_SLOT[0..2]
exec.active_account::get_initial_map_item
end

#! Returns the current number of approvers after any in-transaction signer update has been applied.
#!
#! Inputs: []
#! Outputs: [num_approvers]
pub proc get_current_num_approvers
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these procedures do not need to be public, I'd keep them private.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed to keep pub proc get_current_num_approvers it private but get_procedure_policy will be used by the smart multisig.

Comment on lines +192 to +194
#! Inputs: [proc_root]
#! Outputs: [immediate_threshold, delayed_threshold, note_restrictions, 0]
pub proc get_procedure_policy(proc_root: word)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#! Inputs: [proc_root]
#! Outputs: [immediate_threshold, delayed_threshold, note_restrictions, 0]
pub proc get_procedure_policy(proc_root: word)
#! Inputs: [PROC_ROOT]
#! Outputs: [immediate_threshold, delayed_threshold, note_restrictions, 0]
pub proc get_procedure_policy(proc_root: word)

Nit: This is a word, right?

Also, the type signature is different from the doc-comment signature.

Comment on lines +243 to +248

loc_store.3
loc_store.2
loc_store.1
loc_store.0
# => [proc_index, num_approvers]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to store these in that order or would the reverse also work? If so, I'd change it to use loc_storew_le.

Comment on lines +253 to +255
loc_load.3
swap
# => [num_approvers, immediate_threshold, proc_index, num_approvers]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to introduce local constants for this procedure, so it reads like loc_load.IMMEDIATE_THRESHOLD_LOC.

Comment on lines +180 to +194
pub fn new(config: AuthMultisigConfig) -> Result<Self, AccountError> {
for (_, policy) in config.procedure_policies() {
if !matches!(policy.mode(), ProcedurePolicyMode::ImmediateOnly { .. }) {
return Err(AccountError::other(
"basic multisig procedure policies must be immediate-only",
));
}

if policy.constraints() != ProcedurePolicyConstraints::none() {
return Err(AccountError::other(
"basic multisig procedure policies cannot set constraints",
));
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we couldn't validate this in AuthMultisigConfig already? iiuc, this currently allows constructing an invalid config (e.g. using ProcedurePolicyMode::DelayOnly).

Comment on lines +299 to +308
loc_load.3 eq.0
if.false
loc_load.2
loc_load.3
dup.1 swap
u32assert2.err=ERR_PROC_THRESHOLD_NOT_U32
u32gt assertz.err=ERR_DELAYED_THRESHOLD_EXCEEDS_IMMEDIATE
drop
# => [proc_index, num_approvers]
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could we add some intermediate stack comments here to make this easier to read?

#! - delayed threshold exceeds immediate threshold.
#! - note_restrictions is not in the range 0..=3.
#! - constraints are configured without any threshold.
@locals(5)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we only use 4 locals, but would be good to double-check.

Comment on lines +492 to +496
exec.get_procedure_policy
# => [immediate_threshold, delayed_threshold, note_restrictions, 0, num_procedures-1, max_called_proc_immediate_threshold]

movdn.3 drop drop drop dup dup.3
# => [transaction_threshold, proc_threshold, proc_threshold, num_procedures-1, transaction_threshold]
# => [max_called_proc_immediate_threshold, immediate_threshold, immediate_threshold, num_procedures-1, max_called_proc_immediate_threshold]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This procedure has quite a bit of logic in it. It might make sense to call a wrapper like get_immediate_threshold that extracts the immediate threshold internally instead of get_procedure_policy and extracting the threshold here.

Comment on lines +499 to +505
u32gt
# => [is_gt, proc_threshold, num_procedures-1, transaction_threshold]
# 2c. if proc_threshold > transaction_threshold, update transaction_threshold
# => [is_gt, immediate_threshold, num_procedures-1, max_called_proc_immediate_threshold]

movup.2 movdn.3
# => [is_gt, proc_threshold, transaction_threshold, num_procedures-1]
# => [is_gt, immediate_threshold, max_called_proc_immediate_threshold, num_procedures-1]
cdrop
# => [updated_transaction_threshold, num_procedures-1]
# => [updated_max_called_proc_immediate_threshold, num_procedures-1]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit/Optional: Another thing that would make this procedure easier to read is add a small u32_max procedure that does the logic internally.

Comment on lines +162 to +164
pub fn with_proc_thresholds(
self,
proc_thresholds: Vec<(Word, u32)>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We don't need a vec, so we could take impl IntoIterator<...> instead.

Comment on lines 65 to 69
pub struct AuthMultisigConfig {
approvers: Vec<(PublicKeyCommitment, AuthScheme)>,
default_threshold: u32,
proc_thresholds: Vec<(Word, u32)>,
procedure_policies: Vec<(Word, ProcedurePolicy)>,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on validate_procedure_policies_auth_multisig, AuthMultisigConfig only supports ProcedurePolicyExecutionMode::ImmediateOnly and ProcedurePolicyNoteRestrictions::None, right?

If so, I would not change this to ProcedurePolicy because this allows representing invalid states in the first place. If we keep it as is, we do not need validate_procedure_policies_auth_multisig at all. We should also remove all constructors that take ProcedurePolicy as input for the same reason - it's an invalid state.

We can still make use of ProcedurePolicy in From<AuthMultisig> for AccountComponent to initialize storage, but it would be an implementation detail rather than a part of the public contract.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this comment!

  • I initially approached this with the idea of having a shared base AuthMultisigConfig for all multisig components, but I now think that this ended up creating a structure and helper procedures that do not add much meaning.
  • The other reason of having procedure_policies was to keep a common structure for the MASM templates, because that would allow the items using this config slot in multisig.masm (PROC_THRESHOLD_ROOTS_SLOT) to remain common. If we do not do this, then the common procedures currently implemented for AuthMultisigSmart would most likely need to be reintroduced under similar names, such as for these procedures: assert_proc_thresholds_lte_num_approvers, compute_transaction_threshold, and set_procedure_threshold. I had also made this choice with the next PR (AuthMultisigSmart) in mind, but at this point I agree with you. I think proc_thresholds: Vec<(Word, u32)> should remain as it is.

/// or create output notes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(u8)]
pub enum ProcedurePolicyNoteRestrictions {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub enum ProcedurePolicyNoteRestrictions {
pub enum ProcedurePolicyNoteRestriction {

Nit: It's a single restriction (that may encapsulate multiple restrictions) we set and not multiple ones, so I'd still suggest using singular.

Comment on lines +28 to +31
None = 0,
NoInputNotes = 1,
NoOutputNotes = 2,
NoInputOutputNotes = 3,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be worth making the last variant more precise because it could be read as "no input and output notes" or "no input or output notes", and I think it's the latter.

Comment on lines +775 to +781
dup.1 swap
# => [num_approvers, proc_threshold, proc_threshold, PROC_ROOT]

u32assert2.err=ERR_NUM_APPROVERS_OR_PROC_THRESHOLD_NOT_U32
u32gt assertz.err=ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS
# => [proc_threshold, PROC_ROOT]

# Store [proc_threshold, 0, 0, 0] = PROC_THRESHOLD_WORD, where proc_threshold == 0 acts as clear.
push.0.0.0
movup.3
swapw
# => [PROC_ROOT, PROC_THRESHOLD_WORD]
drop
# => [PROC_ROOT]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't need the proc_threshold, we shouldn't duplicate it a couple of lines above and remove the drop.

#!
#! Invocation: call
@locals(2)
pub proc set_procedure_threshold
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have both of these?

  • set_procedure_threshold(proc_threshold, PROC_ROOT)
  • set_procedure_policy(immediate_threshold, delayed_threshold, note_restrictions, PROC_ROOT)

We can do everything with set_procedure_policy and we don't need set_procedure_threshold anymore, right?

If we do need the latter, we should rename it to make it clear that it sets an immediate threshold, e.g. set_immediate_procedure_threshold.

# => [PROC_ROOT, proc_threshold, 0, 0, 0]

push.PROC_POLICY_ROOTS_SLOT[0..2]
exec.native_account::set_map_item
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This overwrites any previously set delay threshold or note restrictions with 0 - is that intended? Related to a previous comment about whether we even need this procedure anymore.


/// Configuration for [`AuthMultisig`] component.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthMultisigConfig {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a question to better understand the direction this is going to take, so I can better review the PR.

We plan to eventually have three multisig components, AuthMultisig, AuthMultisigSmart and AuthGuardedMultisig, right? And they all share the same code (crates/miden-standards/asm/standards/auth/multisig.masm)?

If so, would we also pay the execution price for the more advanced multisig features (note restrictions, delayed execution) for the basic AuthMultisig? And if so, it seems it would make sense to just have two components AuthMultisigSmart and AuthGuardedMultisig, since the smart version can just be configured to be like the "basic" one, right?

We could still define struct AuthMultisig(AuthMultisigSmart) (i.e. a simpler wrapper that configures the smart version without advanced features), but at the MASM level we'd only have to think about two versions, which would be helpful in terms of complexity.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this comment! I think in this PR the path is not much clear, so related to with this comment: #2670 (comment) I've removed the procedure_policies for Multisig and the GuardedMultisig versions, and also for the AuthMultisigConfig.

But, I've kept the procedure_policies in this PR rather than the following MultisigSmart PR. (Or, do you want me to combine this PR with the following one?)

u32assert2.err=ERR_PROC_THRESHOLD_NOT_U32
u32gt assertz.err=ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS
# => [immediate_threshold, proc_index, num_approvers]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this assertion makes sense to have in this procedure, but the validation starting from here (threshold and note restriction validation) seems unnecessary because we assert the same things in set_procedure_policy already, right?

If we do somehow need it, it'd be awesome if we could deduplicate the validation code into a helper procedure, because we can maintain it in one place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants