Skip to content
Merged
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 @@ -91,6 +91,7 @@
- Fixed the fungible and non-fungible MINT note scripts assuming their `exec` callers provide blank stack slot ([#3668](https://github.com/0xMiden/protocol/pull/3668)).
- [BREAKING] Bounded the multisig approver set to 64 signers, enforced both by `ApproverSet::MAX_APPROVERS` at account creation and by `MAX_NUM_APPROVERS` in the `multisig` and `multisig_smart` `update_signers_and_threshold` procedures ([#3723](https://github.com/0xMiden/protocol/pull/3723)).
- The PSWAP note script now rejects a `PswapAttachment` that does not consist of exactly one word, instead of letting the attachment write past the four locals of `get_current_depth` ([#3761](https://github.com/0xMiden/protocol/pull/3761)).
- [BREAKING] `multisig_smart` now rejects delay-only procedure policies ([#3781](https://github.com/0xMiden/protocol/pull/3781)).
- The standard config note scripts now reject a non-public note [#3779](https://github.com/0xMiden/protocol/pull/3779).
- Role symbol validation now computes its upper bound with `exp.u4`, whose unique exponent decomposition stops a prover from widening the bound and slipping a non-canonical symbol through ([#3774](https://github.com/0xMiden/protocol/pull/3774)).

Expand Down
28 changes: 17 additions & 11 deletions crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ const ERR_PROC_POLICY_INVALID_MODE = "called procedures do not support the selec

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_DELAY_ONLY_POLICY_UNSUPPORTED = "delayed threshold requires an immediate threshold"

const ERR_NOTE_RESTRICTIONS_REQUIRE_THRESHOLD = "procedure policy note restrictions require an immediate threshold"

const ERR_NUM_APPROVERS_OR_PROC_THRESHOLD_NOT_U32 = "number of approvers and procedure threshold must be u32"

Expand Down Expand Up @@ -89,16 +91,22 @@ const INIT_NUM_OF_APPROVERS_LOC = 1
#!
#! 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.
#! - delayed_threshold is the threshold for delayed execution, or 0 when disabled. It requires a
#! non-zero immediate_threshold: delayed execution is not implemented yet, so a delay-only
#! policy would make the procedure permanently uncallable.
#! - 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.
#!
#! Passing a zero immediate_threshold together with a zero delayed_threshold and no note
#! restrictions clears the procedure's policy.
#!
#! 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.
#! - delayed_threshold is non-zero while immediate_threshold is zero.
#! - note_restrictions is non-zero while immediate_threshold is zero.
Comment on lines +108 to +109

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
#! - delayed_threshold is non-zero while immediate_threshold is zero.
#! - note_restrictions is non-zero while immediate_threshold is zero.
#! - delayed_threshold or note_restrictions is non-zero while immediate_threshold is zero.

nit: We could merge these to reduce redundancy

#! - PROC_ROOT is not one of the account's procedures.
#!
#! Invocation: call
Expand Down Expand Up @@ -146,15 +154,13 @@ pub proc set_procedure_policy
# => [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]
# immediate is zero, so the entry can only clear the policy.
loc_load.DELAYED_THRESHOLD_LOC eq.0 assert.err=ERR_DELAY_ONLY_POLICY_UNSUPPORTED
# => [PROC_ROOT]

if.true
loc_load.NOTE_RESTRICTIONS_LOC eq.0 assert.err=ERR_NOTE_RESTRICTIONS_REQUIRE_THRESHOLD
# => [PROC_ROOT]
end
# both thresholds are zero, so note_restrictions must be zero as well.
loc_load.NOTE_RESTRICTIONS_LOC eq.0 assert.err=ERR_NOTE_RESTRICTIONS_REQUIRE_THRESHOLD
# => [PROC_ROOT]
else
# immediate is non-zero. Validate delayed_threshold <= immediate_threshold.
loc_load.DELAYED_THRESHOLD_LOC loc_load.IMMEDIATE_THRESHOLD_LOC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use miden_protocol::account::component::{
};
use miden_protocol::account::{
AccountComponent,
AccountProcedureRoot,
StorageMap,
StorageMapKey,
StorageSlot,
Expand All @@ -30,9 +31,26 @@ use super::super::multisig::{
use super::ProcedurePolicy;
use crate::account::account_component_code;
use crate::account::auth::{Approver, ApproverSet, AuthMultisig};
use crate::procedure_root;

account_component_code!(MULTISIG_SMART_CODE, "miden-standards-auth-multisig-smart.masp");

// PROCEDURE ROOTS
// ================================================================================================

/// MASL library namespace used for procedure-root lookups. Distinct from
/// [`AuthMultisigSmart::NAME`], which mirrors the standards-side MASM module path.
const MULTISIG_SMART_LIBRARY_PATH: &str = "miden::standards::components::auth::multisig_smart";

// Initialize the procedure root of the `set_procedure_policy` procedure only once. It is the only
// procedure that writes the policy map, so callers configuring policies commonly need its root.
procedure_root!(
MULTISIG_SMART_SET_PROCEDURE_POLICY,
MULTISIG_SMART_LIBRARY_PATH,
AuthMultisigSmart::SET_PROCEDURE_POLICY_PROC_NAME,
AuthMultisigSmart::code()
);

// CONSTANTS
// ================================================================================================

Expand Down Expand Up @@ -106,9 +124,7 @@ fn validate_proc_policies(
}

for (_, policy) in proc_policies {
if let Some(immediate_threshold) = policy.immediate_threshold()
&& immediate_threshold > num_approvers
{
if policy.immediate_threshold() > num_approvers {
return Err(AccountError::other(
"procedure policy immediate threshold cannot exceed number of approvers",
));
Expand Down Expand Up @@ -140,11 +156,19 @@ impl AuthMultisigSmart {
/// The name of the component.
pub const NAME: &'static str = "miden::standards::auth::multisig_smart";

/// The name of the procedure that edits per-procedure policies.
const SET_PROCEDURE_POLICY_PROC_NAME: &'static str = "set_procedure_policy";

/// Returns the [`AccountComponentCode`] of this component.
pub fn code() -> &'static AccountComponentCode {
&MULTISIG_SMART_CODE
}

/// Returns the procedure root of the `set_procedure_policy` account procedure.
pub fn set_procedure_policy_root() -> AccountProcedureRoot {
*MULTISIG_SMART_SET_PROCEDURE_POLICY
}

/// Creates a new [`AuthMultisigSmart`] component from the provided configuration.
pub fn new(config: AuthMultisigSmartConfig) -> Result<Self, AccountError> {
validate_proc_policies(config.approvers().len() as u32, config.procedure_policies())?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ 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.
/// A procedure can require the immediate threshold only, or support both the immediate and the
/// delayed threshold. There is deliberately no delay-only mode: delayed execution is not
/// implemented yet and policy enforcement always runs in immediate mode, so a delay-only policy
/// would make its procedure permanently uncallable. A delay-only mode can be added once the
/// delayed execution path exists.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcedurePolicyExecutionMode {
ImmediateOnly {
immediate_threshold: u32,
},
DelayOnly {
delay_threshold: u32,
},
ImmediateOrDelay {
immediate_threshold: u32,
delay_threshold: u32,
Expand All @@ -35,8 +36,8 @@ pub enum ProcedurePolicyNoteRestriction {
///
/// 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
/// - an execution mode, which determines whether the procedure can be executed immediately only, or
/// immediately and after a delay
/// - note restrictions, which limit whether a transaction invoking the procedure may consume input
/// notes or create output notes
///
Expand All @@ -49,7 +50,8 @@ pub enum ProcedurePolicyNoteRestriction {
/// - 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 thresholds for immediate and delayed execution may differ. Every policy must define an
/// immediate threshold, see [`ProcedurePolicyExecutionMode`].
///
/// The policy is encoded into the procedure-policy storage word as:
/// `[immediate_threshold, delayed_threshold, note_restrictions, 0]`.
Expand Down Expand Up @@ -79,13 +81,6 @@ impl ProcedurePolicy {
)
}

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,
Expand All @@ -112,22 +107,20 @@ impl ProcedurePolicy {
self.note_restrictions
}

pub const fn immediate_threshold(&self) -> Option<u32> {
pub const fn immediate_threshold(&self) -> u32 {
match self.execution_mode {
ProcedurePolicyExecutionMode::ImmediateOnly { immediate_threshold } => {
Some(immediate_threshold)
immediate_threshold
},
ProcedurePolicyExecutionMode::DelayOnly { .. } => None,
ProcedurePolicyExecutionMode::ImmediateOrDelay { immediate_threshold, .. } => {
Some(immediate_threshold)
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)
},
Expand All @@ -145,13 +138,6 @@ impl ProcedurePolicy {
));
}
},
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,
Expand All @@ -177,7 +163,7 @@ impl ProcedurePolicy {
}

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

Word::from([immediate_threshold, delay_threshold, self.note_restrictions as u32, 0])
Expand Down Expand Up @@ -221,14 +207,27 @@ mod tests {
.to_string()
.contains("delay threshold cannot exceed immediate threshold")
);

// A delay-only policy would be enforced in immediate mode and brick its procedure.
assert!(
ProcedurePolicy::with_immediate_and_delay_thresholds(0, 2)
.unwrap_err()
.to_string()
.contains("immediate and delayed thresholds must both be at least 1")
);
}

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

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

let immediate_only_policy = ProcedurePolicy::with_immediate_threshold(3).unwrap();

assert_eq!(immediate_only_policy.immediate_threshold(), 3);
assert_eq!(immediate_only_policy.delay_threshold(), None);
}

#[test]
Expand Down
46 changes: 46 additions & 0 deletions crates/miden-testing/tests/auth/multisig_smart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use miden_standards::code_builder::CodeBuilder;
use miden_standards::errors::standards::{
ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_INPUT_NOTES,
ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_OUTPUT_NOTES,
ERR_DELAY_ONLY_POLICY_UNSUPPORTED,
ERR_DUPLICATE_APPROVER_PUBLIC_KEY,
ERR_MULTISIG_APPROVAL_EXPIRED,
ERR_PROC_ROOT_NOT_IN_ACCOUNT,
Expand Down Expand Up @@ -646,6 +647,51 @@ async fn test_multisig_smart_set_procedure_policy_rejects_foreign_root() -> anyh
Ok(())
}

/// `set_procedure_policy` must reject a delay-only policy.
#[tokio::test]
async fn test_multisig_smart_set_procedure_policy_rejects_delay_only_policy() -> anyhow::Result<()>
{
let auth_scheme = AuthScheme::EcdsaK256Keccak;
let (_secret_keys, _auth_schemes, public_keys, _authenticators) =
setup_keys_and_authenticators_with_scheme(2, 2, auth_scheme)?;

let multisig_account = create_multisig_smart_account(2, &public_keys, 100, vec![])?;
let mock_chain =
MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap().build()?;

let set_policy_root = AuthMultisigSmart::set_procedure_policy_root().as_word();

let set_policy_script = compile_multisig_smart_tx_script(format!(
"
@transaction_script
pub proc main
push.{root}
push.0 # note_restrictions
push.1 # delayed_threshold
push.0 # immediate_threshold
call.::miden::standards::components::auth::multisig_smart::set_procedure_policy
end
",
root = set_policy_root,
))?;

let salt = Word::from([Felt::new_unchecked(8); 4]);
let result = mock_chain
.build_transaction(multisig_account.id())
.tx_script(set_policy_script)
.multisig_auth_args(MultisigAuthArgs::new(
mock_chain.latest_block_header().block_num(),
salt,
))
.build()?
.execute()
.await;

assert_transaction_executor_error!(result, ERR_DELAY_ONLY_POLICY_UNSUPPORTED);

Ok(())
}

/// Regression test for the per-procedure contribution semantic of `compute_called_proc_policy`:
/// a transaction that mixes a low-policy procedure (receive_asset = 1) with an unpolicied
/// procedure (set_procedure_policy) must require `max(policy, default) = default` signatures,
Expand Down
Loading