Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Changes

- [BREAKING] Renamed `ProvenBatch::new` to `new_unchecked` ([#2687](https://github.com/0xMiden/miden-base/issues/2687)).
- Added shared `ProcedurePolicy` for AuthMultisig ([#2670](https://github.com/0xMiden/protocol/pull/2670)).
- [BREAKING] Changed `NoteType` encoding from 2 bits to 1 and makes `NoteType::Private` the default ([#2691](https://github.com/0xMiden/miden-base/issues/2691)).

## 0.14.0 (2026-03-23)
Expand Down
30 changes: 30 additions & 0 deletions crates/miden-standards/asm/standards/auth/tx_policy.masm
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use miden::protocol::native_account
use miden::protocol::tx

const ERR_AUTH_PROCEDURE_MUST_BE_CALLED_ALONE = "procedure must be called alone"
const ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_INPUT_NOTES = "transaction must not include input notes"
const ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_OUTPUT_NOTES = "transaction must not include output notes"
const ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_INPUT_OR_OUTPUT_NOTES = "transaction must not include input or output notes"

#! Asserts that exactly one non-auth account procedure was called in the current transaction.
Expand Down Expand Up @@ -59,6 +61,34 @@ pub proc assert_only_one_non_auth_procedure_called
# => []
end

#! Asserts that the current transaction does not consume input notes.
#!
#! Inputs: []
#! Outputs: []
#!
#! Invocation: exec
pub proc assert_no_input_notes
exec.tx::get_num_input_notes
# => [num_input_notes]

assertz.err=ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_INPUT_NOTES
# => []
end

#! Asserts that the current transaction does not create output notes.
#!
#! Inputs: []
#! Outputs: []
#!
#! Invocation: exec
pub proc assert_no_output_notes
exec.tx::get_num_output_notes
# => [num_output_notes]

assertz.err=ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_OUTPUT_NOTES
# => []
end

#! Asserts that the current transaction does not consume input notes or create output notes.
#!
#! Inputs: []
Expand Down
2 changes: 1 addition & 1 deletion crates/miden-standards/src/account/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pub use singlesig::AuthSingleSig;
mod singlesig_acl;
pub use singlesig_acl::{AuthSingleSigAcl, AuthSingleSigAclConfig};

mod multisig;
pub mod multisig;
pub use multisig::{AuthMultisig, AuthMultisigConfig};
Comment on lines +10 to 11
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 usually export the necessary types instead of making modules public.


mod multisig_psm;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod procedure_policies;

use alloc::collections::BTreeSet;
use alloc::vec::Vec;

Expand Down
246 changes: 246 additions & 0 deletions crates/miden-standards/src/account/auth/multisig/procedure_policies.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
use miden_protocol::Word;
use miden_protocol::errors::AccountError;

/// Defines which execution modes a procedure policy supports and the corresponding threshold
/// values for each mode.
///
/// A procedure can require the immediate threshold, the delayed threshold, or support both.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcedurePolicyExecutionMode {
ImmediateOnly {
immediate_threshold: u32,
},
DelayOnly {
delay_threshold: u32,
},
ImmediateOrDelay {
immediate_threshold: u32,
delay_threshold: u32,
},
}

/// Note Restrictions on whether transactions that call a procedure may consume input notes
/// or create output notes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(u8)]
pub enum ProcedurePolicyNoteRestriction {
#[default]
None = 0,
NoInputNotes = 1,
NoOutputNotes = 2,
NoInputOrOutputNotes = 3,
}

/// Defines a per-procedure multisig policy.
///
/// A procedure policy can override the default multisig requirements for a specific procedure.
/// It specifies:
/// - an execution mode, which determines whether the procedure can be executed immediately, after a
/// delay, or both
/// - note restrictions, which limit whether a transaction invoking the procedure may consume input
/// notes or create output notes
///
/// Execution modes:
/// - Immediate execution: the action is authorized and executed within the current transaction.
/// - Delayed execution: the action is proposed first, and can only be executed after a required
/// time delay has elapsed.
///
/// Thresholds:
/// - Immediate threshold: the number of signatures required to authorize immediate execution.
/// - Delayed threshold: the number of signatures required to authorize a delayed action.
///
/// The thresholds for immediate and delayed execution may differ.
///
/// The policy is encoded into the procedure-policy storage word as:
/// `[immediate_threshold, delayed_threshold, note_restrictions, 0]`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProcedurePolicy {
execution_mode: ProcedurePolicyExecutionMode,
note_restrictions: ProcedurePolicyNoteRestriction,
}

impl ProcedurePolicy {
/// Creates an explicit procedure policy from an execution mode and note restriction pair.
///
/// Common multisig cases should generally prefer the `with_*_threshold...` helpers and
/// configure note restrictions afterwards via [`ProcedurePolicy::with_note_restrictions`].
pub fn new(
execution_mode: ProcedurePolicyExecutionMode,
note_restrictions: ProcedurePolicyNoteRestriction,
) -> Result<Self, AccountError> {
Self::validate_execution_mode(execution_mode)?;
Ok(Self { execution_mode, note_restrictions })
}

pub fn with_immediate_threshold(immediate_threshold: u32) -> Result<Self, AccountError> {
Self::new(
ProcedurePolicyExecutionMode::ImmediateOnly { immediate_threshold },
ProcedurePolicyNoteRestriction::None,
)
}

pub fn with_delay_threshold(delay_threshold: u32) -> Result<Self, AccountError> {
Self::new(
ProcedurePolicyExecutionMode::DelayOnly { delay_threshold },
ProcedurePolicyNoteRestriction::None,
)
}

pub fn with_immediate_and_delay_thresholds(
immediate_threshold: u32,
delay_threshold: u32,
) -> Result<Self, AccountError> {
Self::new(
ProcedurePolicyExecutionMode::ImmediateOrDelay { immediate_threshold, delay_threshold },
ProcedurePolicyNoteRestriction::None,
)
}

pub const fn with_note_restrictions(
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 const fn with_note_restrictions(
pub const fn with_note_restriction(

Nit

mut self,
note_restrictions: ProcedurePolicyNoteRestriction,
) -> Self {
self.note_restrictions = note_restrictions;
self
}

pub const fn execution_mode(&self) -> ProcedurePolicyExecutionMode {
self.execution_mode
}

pub const fn note_restrictions(&self) -> ProcedurePolicyNoteRestriction {
self.note_restrictions
}

pub const fn immediate_threshold(&self) -> Option<u32> {
match self.execution_mode {
ProcedurePolicyExecutionMode::ImmediateOnly { immediate_threshold } => {
Some(immediate_threshold)
},
ProcedurePolicyExecutionMode::DelayOnly { .. } => None,
ProcedurePolicyExecutionMode::ImmediateOrDelay { immediate_threshold, .. } => {
Some(immediate_threshold)
},
}
}

pub const fn delay_threshold(&self) -> Option<u32> {
match self.execution_mode {
ProcedurePolicyExecutionMode::ImmediateOnly { .. } => None,
ProcedurePolicyExecutionMode::DelayOnly { delay_threshold } => Some(delay_threshold),
ProcedurePolicyExecutionMode::ImmediateOrDelay { delay_threshold, .. } => {
Some(delay_threshold)
},
}
}

fn validate_execution_mode(
execution_mode: ProcedurePolicyExecutionMode,
) -> Result<(), AccountError> {
match execution_mode {
ProcedurePolicyExecutionMode::ImmediateOnly { immediate_threshold } => {
if immediate_threshold == 0 {
return Err(AccountError::other(
"procedure policy immediate threshold must be at least 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.

Ideally, what this function validates should be enforced in ProcedurePolicyMode. We should not be able to construct a ProcedurePolicyMode with immediate threshold = 0 if this is never a valid state.

If the non-zero conditions is the only constraint we think we're going to have, I'd suggest redefining the enum as:

pub enum ProcedurePolicyMode {
    ImmediateOnly {
        immediate_threshold: NonZero<u32>,
    },
    DelayOnly {
        delay_threshold: NonZero<u32>,
    },
    ImmediateOrDelay {
        immediate_threshold: NonZero<u32>,
        delay_threshold: NonZero<u32>,
    },
}

If we think we'll have more things we need to restrict about the mode, I'd probably consider replacing ProcedurePolicyMode with ProcedurePolicyThresholds.


Not from this PR and more of a general question (no change requested), but do thresholds need to be u32s? It seems a u8 or at most a u16 should be sufficient. We'd have to make sure that num_approvers is consistent with this in any case.

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.

Not from this PR and more of a general question (no change requested), but do thresholds need to be u32s? It seems a u8 or at most a u16 should be sufficient. We'd have to make sure that num_approvers is consistent with this in any case.

I think this question makes sense, the general usage is 3 of 5 at some cases there are 11 of 19 multisig for example, but the question is more broader that would there be a case in the future a multisig is managed by u32_max or u16_max number of people, I don't think it will the case but keeping it u32 also makes sense.

}
},
ProcedurePolicyExecutionMode::DelayOnly { delay_threshold } => {
if delay_threshold == 0 {
return Err(AccountError::other(
"procedure policy delay threshold must be at least 1",
));
}
},
ProcedurePolicyExecutionMode::ImmediateOrDelay {
immediate_threshold,
delay_threshold,
} => {
if immediate_threshold == 0 || delay_threshold == 0 {
return Err(AccountError::other(
"immediate and delayed thresholds must both be at least 1",
));
}
// Delayed execution is the lower-quorum option while immediate execution is
// higher-quorum path. If the delay threshold were greater than the
// immediate threshold, the "fast" path would be easier to satisfy
// than the delayed path, which contradicts that model.
if delay_threshold > immediate_threshold {
return Err(AccountError::other(
"delay threshold cannot exceed immediate threshold",
));
}
},
}

Ok(())
}

pub fn to_word(&self) -> Word {
let immediate_threshold = self.immediate_threshold().unwrap_or(0);
let delay_threshold = self.delay_threshold().unwrap_or(0);

Word::from([immediate_threshold, delay_threshold, self.note_restrictions as u32, 0])
}
}

#[cfg(test)]
mod tests {
use alloc::string::ToString;

use super::{ProcedurePolicy, ProcedurePolicyNoteRestriction};

#[test]
fn procedure_policy_word_encoding_matches_storage_layout() {
let policy = ProcedurePolicy::with_immediate_and_delay_thresholds(4, 3)
.unwrap()
.with_note_restrictions(ProcedurePolicyNoteRestriction::NoInputOrOutputNotes);

assert_eq!(policy.to_word(), [4u32, 3, 3, 0].into());
}

#[test]
fn procedure_policy_construction_rejects_invalid_combinations() {
assert!(
ProcedurePolicy::with_immediate_threshold(0)
.unwrap_err()
.to_string()
.contains("procedure policy immediate threshold must be at least 1")
);

assert!(
ProcedurePolicy::with_immediate_and_delay_thresholds(1, 0)
.unwrap_err()
.to_string()
.contains("immediate and delayed thresholds must both be at least 1")
);

assert!(
ProcedurePolicy::with_immediate_and_delay_thresholds(1, 2)
.unwrap_err()
.to_string()
.contains("delay threshold cannot exceed immediate threshold")
);
}

#[test]
fn procedure_policy_thresholds_are_exposed_with_getters() {
let procedure_policy = ProcedurePolicy::with_delay_threshold(2).unwrap();

assert_eq!(procedure_policy.immediate_threshold(), None);
assert_eq!(procedure_policy.delay_threshold(), Some(2));
}

#[test]
fn procedure_policy_note_restrictions_are_exposed_with_getters() {
let procedure_policy = ProcedurePolicy::with_immediate_threshold(2)
.unwrap()
.with_note_restrictions(ProcedurePolicyNoteRestriction::NoInputNotes);

assert_eq!(ProcedurePolicyNoteRestriction::default(), ProcedurePolicyNoteRestriction::None);
assert_eq!(
procedure_policy.note_restrictions(),
ProcedurePolicyNoteRestriction::NoInputNotes
);
}
}
Loading