From 7eae9cc4ed2832d416c29773ab71fa3e2e1ff9a2 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Fri, 3 Jul 2026 14:43:10 +0200 Subject: [PATCH 01/18] fix multisig getters call API --- .../asm/standards/auth/multisig.masm | 48 ++++-- crates/miden-testing/tests/auth/multisig.rs | 149 ++++++++++++++++++ 2 files changed, 186 insertions(+), 11 deletions(-) diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index 5b7b457575..a2873bb434 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -474,12 +474,14 @@ end #! Returns the current threshold and num_approvers from `THRESHOLD_CONFIG_SLOT`. #! +#! This is the internal `exec` helper shared by internal callers and the public `call` +#! wrapper `get_threshold_and_num_approvers`. +#! #! Inputs: [] #! Outputs: [default_threshold, num_approvers] #! -#! Invocation: call -@account_procedure -pub proc get_threshold_and_num_approvers +#! Invocation: exec +proc get_current_threshold_and_num_approvers push.THRESHOLD_CONFIG_SLOT[0..2] exec.active_account::get_item # => [default_threshold, num_approvers, 0, 0] @@ -488,6 +490,22 @@ pub proc get_threshold_and_num_approvers # => [default_threshold, num_approvers] end +#! Returns the current threshold and num_approvers from `THRESHOLD_CONFIG_SLOT`. +#! +#! Inputs: [pad(16)] +#! Outputs: [default_threshold, num_approvers, pad(14)] +#! +#! Invocation: call +@account_procedure +pub proc get_threshold_and_num_approvers + exec.get_current_threshold_and_num_approvers + # => [default_threshold, num_approvers, pad(16)] + + # restore the call ABI stack depth of 16 + movup.2 drop movup.2 drop + # => [default_threshold, num_approvers, pad(14)] +end + #! Sets or clears a per-procedure threshold override. #! #! Inputs: [proc_threshold, PROC_ROOT] @@ -507,7 +525,7 @@ end #! Invocation: call @account_procedure pub proc set_procedure_threshold - exec.get_threshold_and_num_approvers + exec.get_current_threshold_and_num_approvers # => [default_threshold, num_approvers, proc_threshold, PROC_ROOT] drop @@ -538,8 +556,8 @@ end #! Returns signer public key at index i #! -#! Inputs: [index] -#! Outputs: [PUB_KEY, scheme_id] +#! Inputs: [index, pad(15)] +#! Outputs: [PUB_KEY, scheme_id, pad(11)] #! #! Panics if: #! - index is not a u32 value. @@ -578,14 +596,18 @@ pub proc get_signer_at # => [scheme_id, PUB_KEY] movdn.4 - # => [PUB_KEY, scheme_id] + # => [PUB_KEY, scheme_id, pad(15)] + + # restore the call ABI stack depth of 16 + movup.5 drop movup.5 drop movup.5 drop movup.5 drop + # => [PUB_KEY, scheme_id, pad(11)] end #! Returns 1 if PUB_KEY is a current signer, else 0. #! -#! Inputs: [PUB_KEY] -#! Outputs: [is_signer] +#! Inputs: [PUB_KEY, pad(12)] +#! Outputs: [is_signer, pad(15)] #! Locals: #! 0: is_signer_found #! 1: current_signer_index @@ -646,10 +668,14 @@ pub proc is_signer(pub_key: word) -> felt end drop dropw - # => [] + # => [pad(16)] loc_load.IS_SIGNER_FOUND_LOC - # => [is_signer] + # => [is_signer, pad(16)] + + # restore the call ABI stack depth of 16 + swap drop + # => [is_signer, pad(15)] end #! Check if transaction has already been executed and add it to executed transactions for replay protection, and diff --git a/crates/miden-testing/tests/auth/multisig.rs b/crates/miden-testing/tests/auth/multisig.rs index 4e0da43dfb..1700f537e2 100644 --- a/crates/miden-testing/tests/auth/multisig.rs +++ b/crates/miden-testing/tests/auth/multisig.rs @@ -1555,3 +1555,152 @@ async fn test_multisig_set_procedure_threshold_uses_current_num_approvers( Ok(()) } + +/// Executes a fully-signed 2-of-4 multisig transaction whose script `call`s one of the public +/// component getters, so the getter is exercised through the 16-felt `call` ABI (a getter that +/// returns at any operand-stack depth other than 16 aborts in `restore_context` with +/// `InvalidStackDepthOnReturn`). +async fn execute_multisig_getter_call(script_code: &str, make_args: F) -> anyhow::Result<()> +where + F: FnOnce(&[PublicKey]) -> Word, +{ + let (_secret_keys, auth_schemes, public_keys, authenticators) = + setup_keys_and_authenticators_with_scheme(4, 2, AuthScheme::EcdsaK256Keccak)?; + + let approvers = public_keys + .iter() + .zip(auth_schemes.iter()) + .map(|(pk, scheme)| (pk.clone(), *scheme)) + .collect::>(); + + let multisig_account = create_multisig_account(2, &approvers, 10, vec![])?; + + let mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()]) + .unwrap() + .build() + .unwrap(); + + let tx_script = CodeBuilder::default() + .with_dynamically_linked_library(AuthMultisig::code())? + .compile_tx_script(script_code)?; + + let tx_script_args = make_args(&public_keys); + let salt = Word::from([Felt::from_u8(77); 4]); + + let tx_context_builder = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(tx_script) + .tx_script_args(tx_script_args) + .auth_args(salt); + + // First pass without signatures: the getter `call` must return cleanly so the transaction + // reaches authentication and only fails there for missing signatures. + let tx_summary = tx_context_builder + .clone() + .build()? + .execute() + .await + .unwrap_err() + .unwrap_unauthorized_err(); + + let msg = tx_summary.as_ref().to_commitment(); + let tx_summary = SigningInputs::TransactionSummary(tx_summary); + + let sig_1 = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &tx_summary) + .await?; + let sig_2 = authenticators[1] + .get_signature(public_keys[1].to_commitment(), &tx_summary) + .await?; + + // Second pass with a valid quorum: the whole transaction, getter included, executes. + tx_context_builder + .add_signature(public_keys[0].to_commitment(), msg, sig_1) + .add_signature(public_keys[1].to_commitment(), msg, sig_2) + .build()? + .execute() + .await?; + + Ok(()) +} + +/// Regression test for the `call` ABI of `get_threshold_and_num_approvers`. +/// +/// The script asserts the returned `[default_threshold, num_approvers]` matches the 2-of-4 +/// configuration, which both proves the getter returns at operand-stack depth 16 and that it +/// yields the correct values. +#[tokio::test] +async fn test_get_threshold_and_num_approvers_call_abi() -> anyhow::Result<()> { + let script_code = " + begin + call.::miden::standards::components::auth::multisig::get_threshold_and_num_approvers + # => [default_threshold, num_approvers, pad(14)] + push.2 eq assert + # => [num_approvers, pad(14)] + push.4 eq assert + # => [pad(14)] + end + "; + + execute_multisig_getter_call(script_code, |_| Word::empty()).await +} + +/// Regression test for the `call` ABI of `get_signer_at`. +/// +/// The index `0` is supplied via `tx_script_args`. The script asserts the returned scheme id is +/// `1` (`EcdsaK256Keccak`), exercising the getter's five-felt output through the 16-felt `call` +/// ABI. +#[tokio::test] +async fn test_get_signer_at_call_abi() -> anyhow::Result<()> { + let script_code = " + begin + call.::miden::standards::components::auth::multisig::get_signer_at + # => [PUB_KEY, scheme_id, pad(11)] + movup.4 push.1 eq assert + # => [PUB_KEY, pad(11)] + dropw + # => [pad(12)] + end + "; + + // Index 0 as the sole input felt; the remaining felts of the word are ignored padding. + execute_multisig_getter_call(script_code, |_| Word::empty()).await +} + +/// Regression test for the `call` ABI of `is_signer` when the queried key is a signer. +/// +/// A real approver public key is supplied via `tx_script_args`; the script asserts the getter +/// returns `1`. +#[tokio::test] +async fn test_is_signer_true_call_abi() -> anyhow::Result<()> { + let script_code = " + begin + call.::miden::standards::components::auth::multisig::is_signer + # => [is_signer, pad(15)] + assert + # => [pad(15)] + end + "; + + execute_multisig_getter_call(script_code, |public_keys| public_keys[0].to_commitment().into()) + .await +} + +/// Regression test for the `call` ABI of `is_signer` when the queried key is not a signer. +/// +/// A key that is not part of the approver set is supplied via `tx_script_args`; the script asserts +/// the getter returns `0` after iterating over every approver. +#[tokio::test] +async fn test_is_signer_false_call_abi() -> anyhow::Result<()> { + let script_code = " + begin + call.::miden::standards::components::auth::multisig::is_signer + # => [is_signer, pad(15)] + assertz + # => [pad(15)] + end + "; + + // A word that is not any approver's public key commitment. + execute_multisig_getter_call(script_code, |_| Word::from([Felt::from_u8(0xff); 4])).await +} From f863f77129262d057a50d2e20f5ac6f7dc0f163d Mon Sep 17 00:00:00 2001 From: onurinanc Date: Fri, 3 Jul 2026 16:09:31 +0200 Subject: [PATCH 02/18] reduce cycle costs --- .../asm/standards/auth/multisig.masm | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index a2873bb434..eb0a3d7b78 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -165,7 +165,8 @@ proc assert_proc_thresholds_lte_num_approvers(num_approvers: u32) exec.active_account::get_num_procedures # => [num_procedures, num_approvers] - dup neq.0 + # num_procedures is always >= MIN_NUM_PROCEDURES (2), so enter the loop unconditionally + push.1 # => [should_continue, num_procedures, num_approvers] while.true sub.1 dup @@ -258,7 +259,8 @@ pub proc update_signers_and_threshold(multisig_config_hash: word) eq.0 assertz.err=ERR_ZERO_IN_MULTISIG_CONFIG # => [MULTISIG_CONFIG, pad(12)] - loc_load.NEW_NUM_OF_APPROVERS_LOC + # MULTISIG_CONFIG is [threshold, num_approvers, 0, 0], so num_approvers is at index 1 + dup.1 # => [num_approvers, MULTISIG_CONFIG, pad(12)] # make sure that all existing procedure threshold overrides remain reachable @@ -278,7 +280,8 @@ pub proc update_signers_and_threshold(multisig_config_hash: word) loc_load.NEW_NUM_OF_APPROVERS_LOC # => [num_approvers] - dup neq.0 + # num_approvers was already asserted non-zero above, so enter the loop unconditionally + push.1 while.true sub.1 # => [i-1, pad(12)] @@ -380,7 +383,8 @@ proc compute_transaction_threshold(default_threshold: u32) -> u32 # => [num_procedures, transaction_threshold] # 2. iterate through all account procedures - dup neq.0 + # num_procedures is always >= MIN_NUM_PROCEDURES (2), so enter the loop unconditionally + push.1 # => [should_continue, num_procedures, transaction_threshold] while.true sub.1 @@ -592,10 +596,8 @@ pub proc get_signer_at exec.active_account::get_initial_map_item # => [SCHEME_ID_WORD, PUB_KEY] - movdn.3 drop drop drop - # => [scheme_id, PUB_KEY] - - movdn.4 + # move scheme_id below the whole PUB_KEY word in one step, then drop the zero padding + movdn.7 drop drop drop # => [PUB_KEY, scheme_id, pad(15)] # restore the call ABI stack depth of 16 From 273f8d45b00f3f845a12f969c7dd25c2b0703afa Mon Sep 17 00:00:00 2001 From: onurinanc Date: Fri, 3 Jul 2026 16:18:30 +0200 Subject: [PATCH 03/18] correct stack-layout and advice-map comments --- .../asm/standards/auth/multisig.masm | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index eb0a3d7b78..7437fb32f0 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -208,11 +208,13 @@ end #! Inputs: #! Operand stack: [MULTISIG_CONFIG_HASH, pad(12)] #! Advice map: { -#! MULTISIG_CONFIG_HASH => -#! [ -#! CONFIG, -#! PUB_KEY_N, PUB_KEY_N-1, ..., PUB_KEY_0, -#! SCHEME_ID_N, SCHEME_ID_N-1, ..., SCHEME_ID_0 +#! MULTISIG_CONFIG_HASH => +#! [ +#! CONFIG, +#! PUB_KEY_N, SCHEME_ID_N, +#! PUB_KEY_N-1, SCHEME_ID_N-1, +#! ..., +#! PUB_KEY_0, SCHEME_ID_0 #! ] #! } #! Outputs: @@ -579,7 +581,7 @@ pub proc get_signer_at # => [APPROVER_MAP_KEY, index] push.APPROVER_PUBLIC_KEYS_SLOT[0..2] - # => [APPROVER_PUBLIC_KEYS_SLOT, APPROVER_MAP_KEY, index] + # => [pub_key_slot_suffix, pub_key_slot_prefix, APPROVER_MAP_KEY, index] exec.active_account::get_initial_map_item # => [PUB_KEY, index] @@ -591,7 +593,7 @@ pub proc get_signer_at # => [APPROVER_MAP_KEY, PUB_KEY] push.APPROVER_SCHEME_ID_SLOT[0..2] - # => [APPROVER_SCHEME_ID_SLOT, APPROVER_MAP_KEY, PUB_KEY] + # => [scheme_id_slot_suffix, scheme_id_slot_prefix, APPROVER_MAP_KEY, PUB_KEY] exec.active_account::get_initial_map_item # => [SCHEME_ID_WORD, PUB_KEY] @@ -692,7 +694,7 @@ end #! Invocation: exec pub proc assert_new_tx(msg: word) push.IS_EXECUTED_FLAG - # => [[0, 0, 0, is_executed], MSG] + # => [[is_executed, 0, 0, 0], MSG] swapw # => [TX_SUMMARY_COMMITMENT, IS_EXECUTED_FLAG] @@ -702,7 +704,7 @@ pub proc assert_new_tx(msg: word) # Set the key value pair in the map to mark transaction as executed exec.native_account::set_map_item - # => [[0, 0, 0, is_executed]] + # => [[is_executed, 0, 0, 0]] movdn.3 drop drop drop # => [is_executed] From 1a77fe569ff865193e1c4ebc8e7cbd64dff6b558 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Fri, 3 Jul 2026 16:38:49 +0200 Subject: [PATCH 04/18] warn that growing the signer set does not re-scale overrides --- .../miden-standards/asm/standards/auth/multisig.masm | 8 ++++++++ .../asm/standards/auth/multisig_smart/mod.masm | 7 +++++++ crates/miden-standards/src/account/auth/multisig.rs | 10 ++++++++++ 3 files changed, 25 insertions(+) diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index 7437fb32f0..514de2036c 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -227,6 +227,14 @@ end #! - PUB_KEY_i is the public key of the i-th signer #! - SCHEME_ID_i is the signature scheme id of the i-th signer #! +#! Note: per-procedure threshold overrides in `PROC_THRESHOLD_ROOTS_SLOT` are NOT re-scaled by this +#! procedure. They are absolute signature counts, not ratios, and the only cross-check is +#! `assert_proc_thresholds_lte_num_approvers`, which enforces `override <= num_approvers`. Growing +#! the signer set therefore leaves every override unchanged and silently lowers its effective signing +#! ratio (e.g. a 2-of-2 override becomes 2-of-N). To preserve the intended security level, operators +#! must re-evaluate and, where appropriate, raise the affected overrides via `set_procedure_threshold` +#! in the same transaction that grows the signer set; this procedure neither performs nor prompts it. +#! #! Locals: #! 0: new_num_of_approvers #! 1: init_num_of_approvers diff --git a/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm b/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm index 2e43d721d9..3e66aafe25 100644 --- a/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm +++ b/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm @@ -634,6 +634,13 @@ end #! - any existing smart procedure policy becomes unreachable under the new number of approvers. #! - any provided scheme identifier word is malformed. #! +#! Note: like [`multisig::update_signers_and_threshold`], this procedure does NOT re-scale existing +#! smart procedure policies. Their immediate/delayed thresholds are absolute signature counts, not +#! ratios, and `assert_proc_policies_lte_num_approvers` only enforces the reachability bound +#! (`threshold <= num_approvers`). Growing the signer set silently lowers the effective signing ratio +#! of every policy; operators must re-evaluate and, where appropriate, raise the affected policies in +#! the same transaction that grows the signer set. +#! #! Locals: #! 0: new_num_of_approvers #! 1: init_num_of_approvers diff --git a/crates/miden-standards/src/account/auth/multisig.rs b/crates/miden-standards/src/account/auth/multisig.rs index 914d92acec..ae4fe78972 100644 --- a/crates/miden-standards/src/account/auth/multisig.rs +++ b/crates/miden-standards/src/account/auth/multisig.rs @@ -147,6 +147,16 @@ impl AuthMultisigConfig { /// bound: on private accounts it rejects per-procedure thresholds below the default. /// /// [`create_multisig_wallet`]: crate::account::wallets::create_multisig_wallet +/// +/// # Security: growing the signer set does not re-scale overrides +/// +/// Per-procedure threshold overrides are absolute signature counts, not ratios. Updating the signer +/// set (via the `update_signers_and_threshold` account procedure) does not re-scale existing +/// overrides: the only cross-check is that each override stays `<= num_approvers`, which keeps it +/// reachable but never raises it. Growing the approver set therefore silently lowers the effective +/// signing ratio of every override (e.g. a `2`-of-`2` override becomes `2`-of-`n`). To preserve the +/// intended security level, re-evaluate the affected overrides and, where appropriate, raise them +/// via `set_procedure_threshold` in the same transaction that grows the signer set. #[derive(Debug)] pub struct AuthMultisig { config: AuthMultisigConfig, From 3207826d2cb19f7e7466ef083af2fb4bbe025cfa Mon Sep 17 00:00:00 2001 From: onurinanc Date: Fri, 3 Jul 2026 16:46:47 +0200 Subject: [PATCH 05/18] validate PROC_ROOT belongs to the account in set_procedure_threshold --- .../asm/standards/auth/multisig.masm | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index 514de2036c..5b4e6ec516 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -85,6 +85,8 @@ const ERR_NUM_APPROVERS_OR_PROC_THRESHOLD_NOT_U32 = "number of approvers and pro const ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS = "procedure threshold exceeds new number of approvers" +const ERR_PROC_ROOT_NOT_IN_ACCOUNT = "procedure root is not one of the account's procedures" + #! Remove old approver public keys and the corresponding scheme ids #! from the approver public key and scheme id mappings. #! @@ -535,6 +537,7 @@ end #! - proc_threshold is not a u32 value. #! - current num_approvers is not a u32 value. #! - proc_threshold > current num_approvers. +#! - PROC_ROOT is not one of the account's procedures. #! #! Invocation: call @account_procedure @@ -552,6 +555,18 @@ pub proc set_procedure_threshold u32gt assertz.err=ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS # => [proc_threshold, PROC_ROOT] + # Ensure PROC_ROOT is one of the account's procedures before storing an override for it, + # so a threshold cannot be silently written under a root the account does not have. + movdn.4 dupw + # => [PROC_ROOT, PROC_ROOT, proc_threshold] + + exec.active_account::has_procedure + assert.err=ERR_PROC_ROOT_NOT_IN_ACCOUNT + # => [PROC_ROOT, proc_threshold] + + movup.4 + # => [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 From e137464c20e3a6b14fa1cd2eada68c2d84ffeb20 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Fri, 3 Jul 2026 16:53:45 +0200 Subject: [PATCH 06/18] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ddbd2d4c9..d4b4cdf999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ - [BREAKING] Moved asset callback flag from asset vault key to account ID, making it immutable ([#3167](https://github.com/0xMiden/protocol/pull/3167)). - [BREAKING] Added `@account_procedure` attribute to mark which procedures should be included in the account component interface ([#3171](https://github.com/0xMiden/protocol/pull/3171)). - Documented that the `ecdsa_k256_keccak` authentication scheme discloses the signer's public key and signature at proving time via precompile calldata ([#3178](https://github.com/0xMiden/protocol/pull/3178)). +- Optimized the multisig auth component MASM (unconditional loop entry where the counter is guaranteed non-zero, single-step `scheme_id` extraction in `get_signer_at`, and a stack read instead of a local reload in `update_signers_and_threshold`), and documented that growing the signer set does not re-scale existing per-procedure threshold overrides ([#3211](https://github.com/0xMiden/protocol/pull/3211)). ### Fixes @@ -86,6 +87,7 @@ - [BREAKING] Fixed batch ID being serialized/deserialized and potentially not matching the serialized transaction headers ([#3061](https://github.com/0xMiden/protocol/pull/3061)). - Simplified the `ownable2step` ownership transitions ([#3170](https://github.com/0xMiden/protocol/pull/3170)). - Fixed `note_script_allowlist::assert_all_input_notes_allowed` and `tx_script_allowlist::assert_tx_script_allowed` to read the allowlist from the transaction's initial storage state via `active_account::get_initial_map_item` ([#3182](https://github.com/0xMiden/protocol/pull/3182)). +- Fixed `set_procedure_threshold` now asserts `PROC_ROOT` is one of the account's procedures (`ERR_PROC_ROOT_NOT_IN_ACCOUNT`) before storing an override, and corrected the inaccurate `assert_new_tx`, `update_signers_and_threshold`, and `get_signer_at` stack-layout and advice-map comments ([#3211](https://github.com/0xMiden/protocol/pull/3211)). ## v0.15.2 (2026-06-05) From a3a2caf3c19988b95385706623c60174ba8578d5 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 6 Jul 2026 11:48:35 +0200 Subject: [PATCH 07/18] apply suggestions --- .../asm/standards/auth/multisig.masm | 30 +++++++-------- .../standards/auth/multisig_smart/mod.masm | 14 +++---- crates/miden-testing/tests/auth/multisig.rs | 37 +++++++++++++------ 3 files changed, 47 insertions(+), 34 deletions(-) diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index 5b4e6ec516..3b977ea014 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -85,7 +85,7 @@ const ERR_NUM_APPROVERS_OR_PROC_THRESHOLD_NOT_U32 = "number of approvers and pro const ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS = "procedure threshold exceeds new number of approvers" -const ERR_PROC_ROOT_NOT_IN_ACCOUNT = "procedure root is not one of the account's procedures" +const ERR_PROC_ROOT_NOT_IN_ACCOUNT = "cannot set procedure threshold for procedure root that is not one of the account's procedures" #! Remove old approver public keys and the corresponding scheme ids #! from the approver public key and scheme id mappings. @@ -206,7 +206,15 @@ proc assert_proc_thresholds_lte_num_approvers(num_approvers: u32) end #! Update threshold config, add & remove approvers, and update the approver scheme ids -#! +#! +#! Note: per-procedure threshold overrides in `PROC_THRESHOLD_ROOTS_SLOT` are NOT re-scaled by this +#! procedure. They are absolute signature counts, not ratios, and the only cross-check is +#! `assert_proc_thresholds_lte_num_approvers`, which enforces `override <= num_approvers`. Growing +#! the signer set therefore leaves every override unchanged and silently lowers its effective signing +#! ratio (e.g. a 2-of-2 override becomes 2-of-N). To preserve the intended security level, operators +#! must re-evaluate and, where appropriate, raise the affected overrides via `set_procedure_threshold` +#! in the same transaction that grows the signer set; this procedure neither performs nor prompts it. +#! #! Inputs: #! Operand stack: [MULTISIG_CONFIG_HASH, pad(12)] #! Advice map: { @@ -223,20 +231,12 @@ end #! Operand stack: [] #! #! Where: -#! - MULTISIG_CONFIG_HASH is the hash of the threshold, +#! - MULTISIG_CONFIG_HASH is the hash of the threshold, #! new public key vector, and the corresponding scheme identifiers #! - MULTISIG_CONFIG is [threshold, num_approvers, 0, 0] #! - PUB_KEY_i is the public key of the i-th signer #! - SCHEME_ID_i is the signature scheme id of the i-th signer #! -#! Note: per-procedure threshold overrides in `PROC_THRESHOLD_ROOTS_SLOT` are NOT re-scaled by this -#! procedure. They are absolute signature counts, not ratios, and the only cross-check is -#! `assert_proc_thresholds_lte_num_approvers`, which enforces `override <= num_approvers`. Growing -#! the signer set therefore leaves every override unchanged and silently lowers its effective signing -#! ratio (e.g. a 2-of-2 override becomes 2-of-N). To preserve the intended security level, operators -#! must re-evaluate and, where appropriate, raise the affected overrides via `set_procedure_threshold` -#! in the same transaction that grows the signer set; this procedure neither performs nor prompts it. -#! #! Locals: #! 0: new_num_of_approvers #! 1: init_num_of_approvers @@ -517,7 +517,7 @@ pub proc get_threshold_and_num_approvers exec.get_current_threshold_and_num_approvers # => [default_threshold, num_approvers, pad(16)] - # restore the call ABI stack depth of 16 + # truncate the stack movup.2 drop movup.2 drop # => [default_threshold, num_approvers, pad(14)] end @@ -619,13 +619,13 @@ pub proc get_signer_at # => [scheme_id_slot_suffix, scheme_id_slot_prefix, APPROVER_MAP_KEY, PUB_KEY] exec.active_account::get_initial_map_item - # => [SCHEME_ID_WORD, PUB_KEY] + # => [[scheme_id, 0, 0, 0], PUB_KEY] # move scheme_id below the whole PUB_KEY word in one step, then drop the zero padding movdn.7 drop drop drop # => [PUB_KEY, scheme_id, pad(15)] - # restore the call ABI stack depth of 16 + # truncate the stack movup.5 drop movup.5 drop movup.5 drop movup.5 drop # => [PUB_KEY, scheme_id, pad(11)] end @@ -700,7 +700,7 @@ pub proc is_signer(pub_key: word) -> felt loc_load.IS_SIGNER_FOUND_LOC # => [is_signer, pad(16)] - # restore the call ABI stack depth of 16 + # truncate the stack swap drop # => [is_signer, pad(15)] end diff --git a/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm b/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm index 3e66aafe25..69f3e11079 100644 --- a/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm +++ b/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm @@ -623,6 +623,13 @@ end #! validating smart procedure policies ([`assert_proc_policies_lte_num_approvers`]) instead #! of per-procedure threshold overrides. #! +#! Note: like [`multisig::update_signers_and_threshold`], this procedure does NOT re-scale existing +#! smart procedure policies. Their immediate/delayed thresholds are absolute signature counts, not +#! ratios, and `assert_proc_policies_lte_num_approvers` only enforces the reachability bound +#! (`threshold <= num_approvers`). Growing the signer set silently lowers the effective signing ratio +#! of every policy; operators must re-evaluate and, where appropriate, raise the affected policies in +#! the same transaction that grows the signer set. +#! #! Inputs: #! Operand stack: [MULTISIG_CONFIG_COMMITMENT, pad(12)] #! Outputs: @@ -634,13 +641,6 @@ end #! - any existing smart procedure policy becomes unreachable under the new number of approvers. #! - any provided scheme identifier word is malformed. #! -#! Note: like [`multisig::update_signers_and_threshold`], this procedure does NOT re-scale existing -#! smart procedure policies. Their immediate/delayed thresholds are absolute signature counts, not -#! ratios, and `assert_proc_policies_lte_num_approvers` only enforces the reachability bound -#! (`threshold <= num_approvers`). Growing the signer set silently lowers the effective signing ratio -#! of every policy; operators must re-evaluate and, where appropriate, raise the affected policies in -#! the same transaction that grows the signer set. -#! #! Locals: #! 0: new_num_of_approvers #! 1: init_num_of_approvers diff --git a/crates/miden-testing/tests/auth/multisig.rs b/crates/miden-testing/tests/auth/multisig.rs index f293f6c5ac..e8acc8ee0a 100644 --- a/crates/miden-testing/tests/auth/multisig.rs +++ b/crates/miden-testing/tests/auth/multisig.rs @@ -1566,6 +1566,10 @@ async fn test_multisig_set_procedure_threshold_uses_current_num_approvers( /// component getters, so the getter is exercised through the 16-felt `call` ABI (a getter that /// returns at any operand-stack depth other than 16 aborts in `restore_context` with /// `InvalidStackDepthOnReturn`). +/// +/// The getter's inputs are supplied through `tx_script_args` (`make_args`), which fills the +/// 16-element `call` frame directly; pushing a full input word in the script instead would grow the +/// operand stack past 16 and break the getters' fixed output truncation. async fn execute_multisig_getter_call(script_code: &str, make_args: F) -> anyhow::Result<()> where F: FnOnce(&[PublicKey]) -> Word, @@ -1638,7 +1642,8 @@ where #[tokio::test] async fn test_get_threshold_and_num_approvers_call_abi() -> anyhow::Result<()> { let script_code = " - begin + @transaction_script + pub proc main call.::miden::standards::components::auth::multisig::get_threshold_and_num_approvers # => [default_threshold, num_approvers, pad(14)] push.2 eq assert @@ -1653,24 +1658,28 @@ async fn test_get_threshold_and_num_approvers_call_abi() -> anyhow::Result<()> { /// Regression test for the `call` ABI of `get_signer_at`. /// -/// The index `0` is supplied via `tx_script_args`. The script asserts the returned scheme id is -/// `1` (`EcdsaK256Keccak`), exercising the getter's five-felt output through the 16-felt `call` +/// The index `0` is supplied via `tx_script_args`. The script asserts the returned scheme id +/// matches `EcdsaK256Keccak`, exercising the getter's five-felt output through the 16-felt `call` /// ABI. #[tokio::test] async fn test_get_signer_at_call_abi() -> anyhow::Result<()> { - let script_code = " - begin + let expected_scheme_id = AuthScheme::EcdsaK256Keccak.as_u8(); + let script_code = format!( + r#" + @transaction_script + pub proc main call.::miden::standards::components::auth::multisig::get_signer_at # => [PUB_KEY, scheme_id, pad(11)] - movup.4 push.1 eq assert + movup.4 eq.{expected_scheme_id} assert.err="expected scheme ID {expected_scheme_id}" # => [PUB_KEY, pad(11)] dropw # => [pad(12)] end - "; + "# + ); // Index 0 as the sole input felt; the remaining felts of the word are ignored padding. - execute_multisig_getter_call(script_code, |_| Word::empty()).await + execute_multisig_getter_call(&script_code, |_| Word::empty()).await } /// Regression test for the `call` ABI of `is_signer` when the queried key is a signer. @@ -1680,7 +1689,8 @@ async fn test_get_signer_at_call_abi() -> anyhow::Result<()> { #[tokio::test] async fn test_is_signer_true_call_abi() -> anyhow::Result<()> { let script_code = " - begin + @transaction_script + pub proc main call.::miden::standards::components::auth::multisig::is_signer # => [is_signer, pad(15)] assert @@ -1688,8 +1698,10 @@ async fn test_is_signer_true_call_abi() -> anyhow::Result<()> { end "; - execute_multisig_getter_call(script_code, |public_keys| public_keys[0].to_commitment().into()) - .await + execute_multisig_getter_call(script_code, |public_keys| { + public_keys[0].to_commitment().into() + }) + .await } /// Regression test for the `call` ABI of `is_signer` when the queried key is not a signer. @@ -1699,7 +1711,8 @@ async fn test_is_signer_true_call_abi() -> anyhow::Result<()> { #[tokio::test] async fn test_is_signer_false_call_abi() -> anyhow::Result<()> { let script_code = " - begin + @transaction_script + pub proc main call.::miden::standards::components::auth::multisig::is_signer # => [is_signer, pad(15)] assertz From 1abe0bbb50fb05168fd5bf5045187a182b11d060 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 6 Jul 2026 12:23:40 +0200 Subject: [PATCH 08/18] lint --- crates/miden-testing/tests/auth/multisig.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/miden-testing/tests/auth/multisig.rs b/crates/miden-testing/tests/auth/multisig.rs index 5c6baf36b8..fe3e564c5a 100644 --- a/crates/miden-testing/tests/auth/multisig.rs +++ b/crates/miden-testing/tests/auth/multisig.rs @@ -1698,10 +1698,8 @@ async fn test_is_signer_true_call_abi() -> anyhow::Result<()> { end "; - execute_multisig_getter_call(script_code, |public_keys| { - public_keys[0].to_commitment().into() - }) - .await + execute_multisig_getter_call(script_code, |public_keys| public_keys[0].to_commitment().into()) + .await } /// Regression test for the `call` ABI of `is_signer` when the queried key is not a signer. From 4ce5509fec4dc33df03ac30d15a0b2fc23fa4b4d Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 6 Jul 2026 12:53:16 +0200 Subject: [PATCH 09/18] clarify auth_tx replay protection comes from assert_new_tx, not SALT --- crates/miden-standards/asm/standards/auth/multisig.masm | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index 3b977ea014..1d665e24a7 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -759,9 +759,11 @@ end #! Operand stack: [TX_SUMMARY_COMMITMENT] #! #! Where: -#! - SALT is a cryptographically random nonce that enables multiple concurrent -#! multisig transactions while maintaining replay protection. Each transaction -#! must use a unique SALT value to ensure transaction uniqueness. +#! - SALT is a cryptographically random nonce folded into the signed message, and thus into the +#! TX_SUMMARY_COMMITMENT, so that otherwise-identical transactions produce distinct commitments and +#! can run concurrently. That commitment's uniqueness is what `assert_new_tx` records and checks to +#! enforce replay protection; `auth_tx` itself records nothing, so a wrapper must call +#! `assert_new_tx` for replay protection to hold. #! - SIG_i is the signature from the i-th signer. #! - MSG is the transaction message being signed. #! - h(SIG_i, MSG) is the hash of the signature and message used as the advice map key. From 0b22e7a5138ff8baa741a8bd1bfc4cc2cf9c3abb Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 6 Jul 2026 13:22:47 +0200 Subject: [PATCH 10/18] remove unused @locals(1) from auth_tx --- crates/miden-standards/asm/standards/auth/multisig.masm | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index 1d665e24a7..40353db2f5 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -772,7 +772,6 @@ end #! - insufficient number of valid signatures (below threshold). #! #! Invocation: call -@locals(1) pub proc auth_tx(salt: word) exec.native_account::incr_nonce drop # => [SALT] From f92d4d429b4f1e3875b1679f54662cf79d17d177 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 6 Jul 2026 13:32:34 +0200 Subject: [PATCH 11/18] remove redundant dupw/dropw in tx_policy proc-call check --- .../miden-standards/asm/standards/auth/tx_policy.masm | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/miden-standards/asm/standards/auth/tx_policy.masm b/crates/miden-standards/asm/standards/auth/tx_policy.masm index 4201e44a86..96b1c9136b 100644 --- a/crates/miden-standards/asm/standards/auth/tx_policy.masm +++ b/crates/miden-standards/asm/standards/auth/tx_policy.masm @@ -25,14 +25,13 @@ pub proc assert_only_one_non_auth_procedure_called # => [should_continue, num_procedures] while.true sub.1 dup - exec.active_account::get_procedure_root dupw - # => [PROC_ROOT, PROC_ROOT] + exec.active_account::get_procedure_root + # => [PROC_ROOT, proc_index] exec.native_account::was_procedure_called - # => [was_called, PROC_ROOT] + # => [was_called, proc_index] if.true - dropw # => [proc_index] # The auth procedure is always at procedure index 0. @@ -43,9 +42,6 @@ pub proc assert_only_one_non_auth_procedure_called loc_load.0 add.1 loc_store.0 # => [proc_index] end - else - dropw - # => [proc_index] end dup neq.0 From 23f78ae493c2f9110ed2408682e34ffebf24c50b Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 6 Jul 2026 13:47:57 +0200 Subject: [PATCH 12/18] explain advice-loaded config is covered by ACCOUNT_DELTA_COMMITMENT --- crates/miden-standards/asm/standards/auth/multisig.masm | 5 +++++ .../asm/standards/auth/multisig_smart/mod.masm | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index 40353db2f5..bdf31fcf3e 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -245,6 +245,11 @@ end @locals(2) @account_procedure pub proc update_signers_and_threshold(multisig_config_hash: word) + # The signer configuration is read from the advice provider without an inline hash check. Every + # value written below via set_map_item is folded into ACCOUNT_DELTA_COMMITMENT, which is part of + # the TX_SUMMARY_COMMITMENT the current signers sign, so a substituted configuration produces a + # different delta commitment and thus a summary the current signers never signed. The same + # applies to the per-signer loads in the loop below. adv.push_mapval # => [MULTISIG_CONFIG_HASH, pad(12)] diff --git a/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm b/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm index 69f3e11079..e0e9e7c648 100644 --- a/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm +++ b/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm @@ -649,6 +649,11 @@ end @locals(2) @account_procedure pub proc update_signers_and_threshold(multisig_config_commitment: word) + # The signer configuration is read from the advice provider without an inline hash check. Every + # value written below via set_item / set_map_item is folded into ACCOUNT_DELTA_COMMITMENT, which + # is part of the TX_SUMMARY_COMMITMENT the current signers sign, so a substituted configuration + # produces a different delta commitment and thus a summary the current signers never signed. The + # same applies to the per-signer loads in the loop below. adv.push_mapval # => [MULTISIG_CONFIG_COMMITMENT, pad(12)] From 196cc119704eabad28af4fe74a916a509a656dfc Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 6 Jul 2026 14:17:52 +0200 Subject: [PATCH 13/18] rename assert_new_tx to record_and_assert_new_tx --- .../auth/guarded_multisig/guarded_multisig.masm | 2 +- .../asm/components/auth/multisig/multisig.masm | 2 +- .../auth/multisig_smart/multisig_smart.masm | 2 +- .../asm/standards/auth/multisig.masm | 13 ++++++++----- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/miden-standards/asm/components/auth/guarded_multisig/guarded_multisig.masm b/crates/miden-standards/asm/components/auth/guarded_multisig/guarded_multisig.masm index fd7c125609..6a93bdde61 100644 --- a/crates/miden-standards/asm/components/auth/guarded_multisig/guarded_multisig.masm +++ b/crates/miden-standards/asm/components/auth/guarded_multisig/guarded_multisig.masm @@ -32,6 +32,6 @@ pub proc auth_tx_guarded_multisig(salt: word) exec.guardian::verify_signature # => [TX_SUMMARY_COMMITMENT] - exec.multisig::assert_new_tx + exec.multisig::record_and_assert_new_tx # => [] end diff --git a/crates/miden-standards/asm/components/auth/multisig/multisig.masm b/crates/miden-standards/asm/components/auth/multisig/multisig.masm index 00c14866a1..2cef169ff3 100644 --- a/crates/miden-standards/asm/components/auth/multisig/multisig.masm +++ b/crates/miden-standards/asm/components/auth/multisig/multisig.masm @@ -23,6 +23,6 @@ pub proc auth_tx_multisig(salt: word) exec.multisig::auth_tx # => [TX_SUMMARY_COMMITMENT] - exec.multisig::assert_new_tx + exec.multisig::record_and_assert_new_tx # => [] end diff --git a/crates/miden-standards/asm/components/auth/multisig_smart/multisig_smart.masm b/crates/miden-standards/asm/components/auth/multisig_smart/multisig_smart.masm index bcfce77a96..90d2bbd28d 100644 --- a/crates/miden-standards/asm/components/auth/multisig_smart/multisig_smart.masm +++ b/crates/miden-standards/asm/components/auth/multisig_smart/multisig_smart.masm @@ -24,6 +24,6 @@ pub proc auth_tx_multisig_smart(salt: word) exec.multisig_smart::auth_tx # => [TX_SUMMARY_COMMITMENT] - exec.multisig::assert_new_tx + exec.multisig::record_and_assert_new_tx # => [] end diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index bdf31fcf3e..58fa94265c 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -710,8 +710,11 @@ pub proc is_signer(pub_key: word) -> felt # => [is_signer, pad(15)] end -#! Check if transaction has already been executed and add it to executed transactions for replay protection, and -#! finalizes multisig authentication. +#! Records the transaction as executed and asserts it was not already executed, providing replay +#! protection and finalizing multisig authentication. +#! +#! Writes IS_EXECUTED_FLAG into EXECUTED_TXS_SLOT keyed by MSG via set_map_item, then asserts the +#! slot did not already hold the flag. #! #! Inputs: [MSG] #! Outputs: [] @@ -720,7 +723,7 @@ end #! - the same transaction has already been executed #! #! Invocation: exec -pub proc assert_new_tx(msg: word) +pub proc record_and_assert_new_tx(msg: word) push.IS_EXECUTED_FLAG # => [[is_executed, 0, 0, 0], MSG] @@ -766,9 +769,9 @@ end #! Where: #! - SALT is a cryptographically random nonce folded into the signed message, and thus into the #! TX_SUMMARY_COMMITMENT, so that otherwise-identical transactions produce distinct commitments and -#! can run concurrently. That commitment's uniqueness is what `assert_new_tx` records and checks to +#! can run concurrently. That commitment's uniqueness is what `record_and_assert_new_tx` records and checks to #! enforce replay protection; `auth_tx` itself records nothing, so a wrapper must call -#! `assert_new_tx` for replay protection to hold. +#! `record_and_assert_new_tx` for replay protection to hold. #! - SIG_i is the signature from the i-th signer. #! - MSG is the transaction message being signed. #! - h(SIG_i, MSG) is the hash of the signature and message used as the advice map key. From e8f3a202b18dc563cd2549edd5e1598313d2d3d9 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 6 Jul 2026 14:33:41 +0200 Subject: [PATCH 14/18] fix inaccurate auth-component comments --- .../auth/network_account/network_account.masm | 2 +- .../asm/components/auth/no_auth/no_auth.masm | 6 ++- .../components/auth/singlesig/singlesig.masm | 6 +-- .../auth/singlesig_acl/singlesig_acl.masm | 6 +-- .../asm/standards/auth/guardian.masm | 10 ++-- .../asm/standards/auth/signature.masm | 48 +++++++++---------- 6 files changed, 40 insertions(+), 38 deletions(-) diff --git a/crates/miden-standards/asm/components/auth/network_account/network_account.masm b/crates/miden-standards/asm/components/auth/network_account/network_account.masm index 0237e8996e..4bdcdae78b 100644 --- a/crates/miden-standards/asm/components/auth/network_account/network_account.masm +++ b/crates/miden-standards/asm/components/auth/network_account/network_account.masm @@ -33,7 +33,7 @@ const ALLOWED_TX_SCRIPTS_SLOT = word("miden::standards::auth::network_account::a #! If both checks pass, the nonce is incremented when the account state changed or the account is #! new, matching the behavior of the NoAuth and SingleSig components. #! -#! Inputs: [pad(16)] +#! Inputs: [AUTH_ARGS, pad(12)] #! Outputs: [pad(16)] #! #! Invocation: call diff --git a/crates/miden-standards/asm/components/auth/no_auth/no_auth.masm b/crates/miden-standards/asm/components/auth/no_auth/no_auth.masm index d0b2c86f26..a46f8fde3e 100644 --- a/crates/miden-standards/asm/components/auth/no_auth/no_auth.masm +++ b/crates/miden-standards/asm/components/auth/no_auth/no_auth.masm @@ -10,8 +10,10 @@ use miden::core::word #! This avoids unnecessary nonce increments for transactions that don't modify #! the account state. #! -#! Inputs: [pad(16)] -#! Outputs: [pad(16)] +#! Inputs: [AUTH_ARGS, pad(12)] +#! Outputs: [AUTH_ARGS, pad(12)] +#! +#! Invocation: call @auth_script pub proc auth_no_auth # check if the account state has changed by comparing initial and final commitments diff --git a/crates/miden-standards/asm/components/auth/singlesig/singlesig.masm b/crates/miden-standards/asm/components/auth/singlesig/singlesig.masm index 5065a06b7e..774ab6accc 100644 --- a/crates/miden-standards/asm/components/auth/singlesig/singlesig.masm +++ b/crates/miden-standards/asm/components/auth/singlesig/singlesig.masm @@ -42,13 +42,13 @@ pub proc auth_tx(auth_args: word) # --------------------------------------------------------------------------------------------- push.PUBLIC_KEY_SLOT[0..2] exec.active_account::get_initial_item - # => [PUB_KEY, pad(16)] + # => [PK_COMM, pad(16)] push.SCHEME_ID_SLOT[0..2] exec.active_account::get_initial_item - # => [scheme_id, 0, 0, 0, PUB_KEY, pad(16)] + # => [scheme_id, 0, 0, 0, PK_COMM, pad(16)] movdn.7 drop drop drop - # => [PUB_KEY, scheme_id, pad(16)] + # => [PK_COMM, scheme_id, pad(16)] exec.signature::authenticate_transaction # => [pad(16)] diff --git a/crates/miden-standards/asm/components/auth/singlesig_acl/singlesig_acl.masm b/crates/miden-standards/asm/components/auth/singlesig_acl/singlesig_acl.masm index 887dc3fc9c..a2cd6e87c6 100644 --- a/crates/miden-standards/asm/components/auth/singlesig_acl/singlesig_acl.masm +++ b/crates/miden-standards/asm/components/auth/singlesig_acl/singlesig_acl.masm @@ -108,14 +108,14 @@ pub proc auth_tx_acl(auth_args: word) if.true # Fetch public key from storage. push.PUBLIC_KEY_SLOT[0..2] exec.active_account::get_initial_item - # => [PUB_KEY, pad(16)] + # => [PK_COMM, pad(16)] # Fetch scheme_id from storage push.SCHEME_ID_SLOT[0..2] exec.active_account::get_initial_item - # => [[scheme_id, 0, 0, 0], PUB_KEY, pad(16)] + # => [[scheme_id, 0, 0, 0], PK_COMM, pad(16)] movdn.7 drop drop drop - # => [PUB_KEY, scheme_id, pad(16)] + # => [PK_COMM, scheme_id, pad(16)] exec.signature::authenticate_transaction else diff --git a/crates/miden-standards/asm/standards/auth/guardian.masm b/crates/miden-standards/asm/standards/auth/guardian.masm index 4fff6934d2..222f1312ab 100644 --- a/crates/miden-standards/asm/standards/auth/guardian.masm +++ b/crates/miden-standards/asm/standards/auth/guardian.masm @@ -77,7 +77,7 @@ pub proc update_guardian_public_key(new_guardian_scheme_id: felt, new_guardian_p # => [GUARDIAN_MAP_KEY, NEW_GUARDIAN_PUBLIC_KEY] push.GUARDIAN_PUBLIC_KEYS_SLOT[0..2] - # => [guardian_pubkeys_slot_prefix, guardian_pubkeys_slot_suffix, + # => [guardian_pubkeys_slot_suffix, guardian_pubkeys_slot_prefix, # GUARDIAN_MAP_KEY, NEW_GUARDIAN_PUBLIC_KEY] exec.native_account::set_map_item @@ -97,7 +97,7 @@ pub proc update_guardian_public_key(new_guardian_scheme_id: felt, new_guardian_p # => [GUARDIAN_MAP_KEY, NEW_GUARDIAN_SCHEME_ID_WORD] push.GUARDIAN_SCHEME_ID_SLOT[0..2] - # => [guardian_scheme_slot_prefix, guardian_scheme_slot_suffix, + # => [guardian_scheme_slot_suffix, guardian_scheme_slot_prefix, # GUARDIAN_MAP_KEY, NEW_GUARDIAN_SCHEME_ID_WORD] exec.native_account::set_map_item @@ -140,11 +140,11 @@ pub proc verify_signature(msg: word) # => [1, MSG] push.GUARDIAN_PUBLIC_KEYS_SLOT[0..2] - # => [guardian_pubkeys_slot_prefix, guardian_pubkeys_slot_suffix, 1, MSG] + # => [guardian_pubkeys_slot_suffix, guardian_pubkeys_slot_prefix, 1, MSG] push.GUARDIAN_SCHEME_ID_SLOT[0..2] - # => [guardian_scheme_slot_prefix, guardian_scheme_slot_suffix, - # guardian_pubkeys_slot_prefix, guardian_pubkeys_slot_suffix, 1, MSG] + # => [guardian_scheme_slot_suffix, guardian_scheme_slot_prefix, + # guardian_pubkeys_slot_suffix, guardian_pubkeys_slot_prefix, 1, MSG] exec.signature::verify_signatures # => [num_verified_signatures, MSG] diff --git a/crates/miden-standards/asm/standards/auth/signature.masm b/crates/miden-standards/asm/standards/auth/signature.masm index f6bd82636f..9b1daa5e22 100644 --- a/crates/miden-standards/asm/standards/auth/signature.masm +++ b/crates/miden-standards/asm/standards/auth/signature.masm @@ -44,7 +44,7 @@ const ERR_INVALID_SCHEME_ID_WORD = "invalid scheme ID word format expected three #! included to commit to the transaction creator's intended reference block of the transaction #! which determines the fee parameters and therefore the fee amount that is deducted. #! -#! Inputs: [PUB_KEY, scheme_id] +#! Inputs: [PK_COMM, scheme_id] #! Outputs: [] #! #! Invocation: exec @@ -55,20 +55,20 @@ pub proc authenticate_transaction exec.native_account::incr_nonce exec.tx::get_block_number push.0.0 - # => [[0, 0, ref_block_num, final_nonce], PUB_KEY, scheme_id] + # => [[0, 0, ref_block_num, final_nonce], PK_COMM, scheme_id] # Compute the message that is signed. # --------------------------------------------------------------------------------------------- exec.auth::create_tx_summary - # => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT, PUB_KEY, scheme_id] + # => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT, PK_COMM, scheme_id] # insert tx summary into advice provider for extraction by the host adv.insert_hqword - # => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT, PUB_KEY, scheme_id] + # => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT, PK_COMM, scheme_id] # The commitment to the tx summary is the message that is signed exec.auth::hash_tx_summary - # OS => [MESSAGE, PUB_KEY, scheme_id] + # OS => [MESSAGE, PK_COMM, scheme_id] # AS => [] # Fetch signature from advice provider and verify. @@ -76,15 +76,15 @@ pub proc authenticate_transaction # Emit the authentication request event that pushes a signature for the message to the advice stack emit.AUTH_REQUEST_EVENT swapw - # OS => [PUB_KEY, MESSAGE, scheme_id] + # OS => [PK_COMM, MESSAGE, scheme_id] # AS => [SIGNATURE] movup.8 - # OS => [scheme_id, PUB_KEY, MESSAGE] + # OS => [scheme_id, PK_COMM, MESSAGE] # AS => [SIGNATURE] dup.0 exec.assert_supported_scheme - # OS => [scheme_id, PUB_KEY, MESSAGE] + # OS => [scheme_id, PK_COMM, MESSAGE] # AS => [SIGNATURE] # Verify the signature against the public key and the message. The procedure gets as inputs the @@ -99,26 +99,26 @@ end # 1 => ECDSA (ecdsa_k256_keccak) # 2 => Falcon (falcon512_poseidon2) # -# Inputs: [scheme_id, PUB_KEY, MSG] +# Inputs: [scheme_id, PK_COMM, MSG] # Outputs: [] proc verify_signature_by_scheme dup eq.ECDSA_K256_KECCAK_SCHEME_ID - # => [is_one, scheme_id PUB_KEY, MESSAGE] + # => [is_one, scheme_id, PK_COMM, MESSAGE] if.true drop - # OS => [PUB_KEY, MESSAGE] + # OS => [PK_COMM, MESSAGE] exec.ecdsa_k256_keccak::verify # OS => [] # AS => [] else dup eq.FALCON_512_POSEIDON2_SCHEME_ID - # => [is_2, scheme_id, PUB_KEY, MESSAGE] + # => [is_2, scheme_id, PK_COMM, MESSAGE] if.true drop - # OS => [PUB_KEY, MESSAGE] + # OS => [PK_COMM, MESSAGE] exec.falcon512_poseidon2::verify # OS => [] @@ -222,19 +222,19 @@ pub proc verify_signatures loc_load.APPROVER_PUB_KEY_SLOT_ID_PREFIX_LOC loc_load.APPROVER_PUB_KEY_SLOT_ID_SUFFIX_LOC exec.active_account::get_initial_map_item - # => [OWNER_PUB_KEY, i-1, MSG] + # => [PK_COMM, i-1, MSG] loc_storew_le.CURRENT_PK_LOC - # => [OWNER_PUB_KEY, i-1, MSG] + # => [PK_COMM, i-1, MSG] # Check if signature exists for this signer. # ----------------------------------------------------------------------------------------- movup.4 movdn.8 - # => [OWNER_PUB_KEY, MSG, i-1] + # => [PK_COMM, MSG, i-1] dupw.1 swapw - # => [OWNER_PUB_KEY, MSG, MSG, i-1] + # => [PK_COMM, MSG, MSG, i-1] exec.poseidon2::merge # => [SIG_KEY, MSG, i-1] @@ -259,30 +259,30 @@ pub proc verify_signatures # ----------------------------------------------------------------------------------------- loc_loadw_le.CURRENT_PK_LOC - # => [PK, MSG, MSG, i-1] + # => [PK_COMM, MSG, MSG, i-1] swapw - # => [MSG, PK, MSG, i-1] + # => [MSG, PK_COMM, MSG, i-1] # Emit the authentication request event that pushes a signature for the message to the advice stack. emit.AUTH_REQUEST_EVENT swapw - # => [PUB_KEY, MSG, MSG, i-1] + # => [PK_COMM, MSG, MSG, i-1] # Build map key from the current signer index. loc_load.SIGNER_INDEX_LOC exec.create_approver_map_key - # => [APPROVER_MAP_KEY, PUB_KEY, MSG, MSG, i-1] + # => [APPROVER_MAP_KEY, PK_COMM, MSG, MSG, i-1] loc_load.APPROVER_SCHEME_ID_SLOT_ID_PREFIX_LOC loc_load.APPROVER_SCHEME_ID_SLOT_ID_SUFFIX_LOC - # => [scheme_slot_id_suffix, scheme_slot_id_prefix, APPROVER_MAP_KEY, PUB_KEY, MSG, MSG, i-1] + # => [scheme_slot_id_suffix, scheme_slot_id_prefix, APPROVER_MAP_KEY, PK_COMM, MSG, MSG, i-1] # Get scheme_id for signer index i-1 from initial storage state. exec.active_account::get_initial_map_item - # => [[scheme_id, 0, 0, 0], PUB_KEY, MSG, MSG, i-1] + # => [[scheme_id, 0, 0, 0], PK_COMM, MSG, MSG, i-1] movdn.3 drop drop drop - # OS => [scheme_id, PUB_KEY, MSG, MSG, i-1] + # OS => [scheme_id, PK_COMM, MSG, MSG, i-1] # AS => [SIGNATURE] exec.verify_signature_by_scheme From 690e3d4e24c86364a685d85a9c0bfbbc4c906110 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 6 Jul 2026 14:50:18 +0200 Subject: [PATCH 15/18] assert reconciled threshold config before approver-map cleanup --- .../asm/standards/auth/multisig.masm | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index 58fa94265c..5f93b16628 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -87,11 +87,15 @@ const ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS = "procedure threshold exceeds ne const ERR_PROC_ROOT_NOT_IN_ACCOUNT = "cannot set procedure threshold for procedure root that is not one of the account's procedures" -#! Remove old approver public keys and the corresponding scheme ids -#! from the approver public key and scheme id mappings. +const ERR_CLEANUP_THRESHOLD_CONFIG_NOT_RECONCILED = "threshold config must be reconciled with the new approver count before cleaning up approver mappings" + +#! Remove old approver public keys and the corresponding scheme ids from the approver public key and +#! scheme id mappings. +#! +#! This procedure clears the approver entries in the range [new_num_of_approvers, +#! init_num_of_approvers) from APPROVER_PUBLIC_KEYS_SLOT and APPROVER_SCHEME_ID_SLOT. It does not +#! touch THRESHOLD_CONFIG_SLOT or PROC_THRESHOLD_ROOTS_SLOT. #! -#! This procedure cleans up the storage by removing public keys and signature scheme ids of approvers -#! that are no longer part of the multisig configuration. #! #! Inputs: [init_num_of_approvers, new_num_of_approvers] #! Outputs: [] @@ -101,9 +105,27 @@ const ERR_PROC_ROOT_NOT_IN_ACCOUNT = "cannot set procedure threshold for procedu #! - new_num_of_approvers is the new number of approvers after the update #! #! Panics if: -#! - init_num_of_approvers is not a u32 value. -#! - new_num_of_approvers is not a u32 value. +#! - init_num_of_approvers or new_num_of_approvers is not a u32 value. +#! - the stored threshold config has not been reconciled with new_num_of_approvers. pub proc cleanup_pubkey_and_scheme_id_mapping(init_num_of_approvers: u32, new_num_of_approvers: u32) + # Guard against callers that shrink the approver maps without reconciling THRESHOLD_CONFIG_SLOT. + exec.get_current_threshold_and_num_approvers + # => [threshold, num_approvers, init_num_of_approvers, new_num_of_approvers] + + # assert the stored threshold is reachable with the new signer count (threshold <= new count) + dup dup.4 + # => [new_num_of_approvers, threshold, threshold, num_approvers, init, new] + u32assert2.err=ERR_CLEANUP_THRESHOLD_CONFIG_NOT_RECONCILED + u32gt assertz.err=ERR_CLEANUP_THRESHOLD_CONFIG_NOT_RECONCILED + # => [threshold, num_approvers, init, new] + + # assert the stored approver count already reflects the reduced signer set + dup.1 dup.4 eq assert.err=ERR_CLEANUP_THRESHOLD_CONFIG_NOT_RECONCILED + # => [threshold, num_approvers, init, new] + + drop drop + # => [init_num_of_approvers, new_num_of_approvers] + dup.1 dup.1 u32assert2.err=ERR_APPROVER_COUNTS_NOT_U32 u32lt From b0271a73efc654b21f3717f4cc80d2af924ffb2c Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 6 Jul 2026 15:01:29 +0200 Subject: [PATCH 16/18] use truncate_stack in tests --- .../asm/standards/auth/multisig.masm | 1 - crates/miden-testing/tests/auth/multisig.rs | 82 +++++++++++-------- 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index 5f93b16628..07ffa7dcfd 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -96,7 +96,6 @@ const ERR_CLEANUP_THRESHOLD_CONFIG_NOT_RECONCILED = "threshold config must be re #! init_num_of_approvers) from APPROVER_PUBLIC_KEYS_SLOT and APPROVER_SCHEME_ID_SLOT. It does not #! touch THRESHOLD_CONFIG_SLOT or PROC_THRESHOLD_ROOTS_SLOT. #! -#! #! Inputs: [init_num_of_approvers, new_num_of_approvers] #! Outputs: [] #! diff --git a/crates/miden-testing/tests/auth/multisig.rs b/crates/miden-testing/tests/auth/multisig.rs index fe3e564c5a..045cf42a37 100644 --- a/crates/miden-testing/tests/auth/multisig.rs +++ b/crates/miden-testing/tests/auth/multisig.rs @@ -1566,13 +1566,9 @@ async fn test_multisig_set_procedure_threshold_uses_current_num_approvers( /// component getters, so the getter is exercised through the 16-felt `call` ABI (a getter that /// returns at any operand-stack depth other than 16 aborts in `restore_context` with /// `InvalidStackDepthOnReturn`). -/// -/// The getter's inputs are supplied through `tx_script_args` (`make_args`), which fills the -/// 16-element `call` frame directly; pushing a full input word in the script instead would grow the -/// operand stack past 16 and break the getters' fixed output truncation. -async fn execute_multisig_getter_call(script_code: &str, make_args: F) -> anyhow::Result<()> +async fn execute_multisig_getter_call(build_script: F) -> anyhow::Result<()> where - F: FnOnce(&[PublicKey]) -> Word, + F: FnOnce(&[PublicKey]) -> String, { let (_secret_keys, auth_schemes, public_keys, authenticators) = setup_keys_and_authenticators_with_scheme(4, 2, AuthScheme::EcdsaK256Keccak)?; @@ -1590,17 +1586,16 @@ where .build() .unwrap(); + let script_code = build_script(&public_keys); let tx_script = CodeBuilder::default() .with_dynamically_linked_library(AuthMultisig::code())? - .compile_tx_script(script_code)?; + .compile_tx_script(&script_code)?; - let tx_script_args = make_args(&public_keys); let salt = Word::from([Felt::from_u8(77); 4]); let tx_context_builder = mock_chain .build_tx_context(multisig_account.id(), &[], &[])? .tx_script(tx_script) - .tx_script_args(tx_script_args) .auth_args(salt); // First pass without signatures: the getter `call` must return cleanly so the transaction @@ -1641,7 +1636,8 @@ where /// yields the correct values. #[tokio::test] async fn test_get_threshold_and_num_approvers_call_abi() -> anyhow::Result<()> { - let script_code = " + execute_multisig_getter_call(|_| { + " @transaction_script pub proc main call.::miden::standards::components::auth::multisig::get_threshold_and_num_approvers @@ -1650,74 +1646,88 @@ async fn test_get_threshold_and_num_approvers_call_abi() -> anyhow::Result<()> { # => [num_approvers, pad(14)] push.4 eq assert # => [pad(14)] + exec.::miden::core::sys::truncate_stack end - "; - - execute_multisig_getter_call(script_code, |_| Word::empty()).await + " + .to_string() + }) + .await } /// Regression test for the `call` ABI of `get_signer_at`. /// -/// The index `0` is supplied via `tx_script_args`. The script asserts the returned scheme id -/// matches `EcdsaK256Keccak`, exercising the getter's five-felt output through the 16-felt `call` -/// ABI. +/// The signer at index `0` is queried and the script asserts the returned scheme id matches +/// `EcdsaK256Keccak`, exercising the getter's five-felt output through the 16-felt `call` ABI. #[tokio::test] async fn test_get_signer_at_call_abi() -> anyhow::Result<()> { let expected_scheme_id = AuthScheme::EcdsaK256Keccak.as_u8(); - let script_code = format!( - r#" + execute_multisig_getter_call(move |_| { + format!( + r#" @transaction_script pub proc main + # query the signer at index 0 + push.0 call.::miden::standards::components::auth::multisig::get_signer_at # => [PUB_KEY, scheme_id, pad(11)] movup.4 eq.{expected_scheme_id} assert.err="expected scheme ID {expected_scheme_id}" # => [PUB_KEY, pad(11)] dropw # => [pad(12)] + exec.::miden::core::sys::truncate_stack end "# - ); - - // Index 0 as the sole input felt; the remaining felts of the word are ignored padding. - execute_multisig_getter_call(&script_code, |_| Word::empty()).await + ) + }) + .await } /// Regression test for the `call` ABI of `is_signer` when the queried key is a signer. /// -/// A real approver public key is supplied via `tx_script_args`; the script asserts the getter -/// returns `1`. +/// A real approver public key is pushed by the script; the getter must return `1`. #[tokio::test] async fn test_is_signer_true_call_abi() -> anyhow::Result<()> { - let script_code = " + execute_multisig_getter_call(|public_keys| { + let pub_key = public_keys[0].to_commitment(); + format!( + r#" @transaction_script pub proc main + push.{pub_key} call.::miden::standards::components::auth::multisig::is_signer # => [is_signer, pad(15)] assert # => [pad(15)] + exec.::miden::core::sys::truncate_stack end - "; - - execute_multisig_getter_call(script_code, |public_keys| public_keys[0].to_commitment().into()) - .await + "# + ) + }) + .await } /// Regression test for the `call` ABI of `is_signer` when the queried key is not a signer. /// -/// A key that is not part of the approver set is supplied via `tx_script_args`; the script asserts -/// the getter returns `0` after iterating over every approver. +/// A key that is not part of the approver set is pushed by the script; the getter must return `0` +/// after iterating over every approver. #[tokio::test] async fn test_is_signer_false_call_abi() -> anyhow::Result<()> { - let script_code = " + execute_multisig_getter_call(|_| { + // A word that is not any approver's public key commitment. + let non_signer = Word::from([Felt::from_u8(0xff); 4]); + format!( + r#" @transaction_script pub proc main + push.{non_signer} call.::miden::standards::components::auth::multisig::is_signer # => [is_signer, pad(15)] assertz # => [pad(15)] + exec.::miden::core::sys::truncate_stack end - "; - - // A word that is not any approver's public key commitment. - execute_multisig_getter_call(script_code, |_| Word::from([Felt::from_u8(0xff); 4])).await + "# + ) + }) + .await } From 4603e804558872fefd7026140c4df641206f8f89 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 6 Jul 2026 15:18:46 +0200 Subject: [PATCH 17/18] fix --- crates/miden-standards/asm/standards/auth/multisig.masm | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index 07ffa7dcfd..4ce273e1ee 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -581,8 +581,6 @@ pub proc set_procedure_threshold u32gt assertz.err=ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS # => [proc_threshold, PROC_ROOT] - # Ensure PROC_ROOT is one of the account's procedures before storing an override for it, - # so a threshold cannot be silently written under a root the account does not have. movdn.4 dupw # => [PROC_ROOT, PROC_ROOT, proc_threshold] @@ -647,12 +645,11 @@ pub proc get_signer_at exec.active_account::get_initial_map_item # => [[scheme_id, 0, 0, 0], PUB_KEY] - # move scheme_id below the whole PUB_KEY word in one step, then drop the zero padding movdn.7 drop drop drop # => [PUB_KEY, scheme_id, pad(15)] # truncate the stack - movup.5 drop movup.5 drop movup.5 drop movup.5 drop + repeat.4 movup.5 drop end # => [PUB_KEY, scheme_id, pad(11)] end From 220e61fdf0f32d76bd10c7486a3b61428b90a1ac Mon Sep 17 00:00:00 2001 From: onurinanc Date: Tue, 7 Jul 2026 15:15:14 +0200 Subject: [PATCH 18/18] apply suggestions --- .../asm/components/auth/no_auth/no_auth.masm | 2 +- .../asm/standards/auth/multisig.masm | 46 +++++++++---------- .../standards/auth/multisig_smart/mod.masm | 6 +-- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/crates/miden-standards/asm/components/auth/no_auth/no_auth.masm b/crates/miden-standards/asm/components/auth/no_auth/no_auth.masm index a46f8fde3e..f5c6bd6681 100644 --- a/crates/miden-standards/asm/components/auth/no_auth/no_auth.masm +++ b/crates/miden-standards/asm/components/auth/no_auth/no_auth.masm @@ -11,7 +11,7 @@ use miden::core::word #! the account state. #! #! Inputs: [AUTH_ARGS, pad(12)] -#! Outputs: [AUTH_ARGS, pad(12)] +#! Outputs: [pad(16)] #! #! Invocation: call @auth_script diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm index 4ce273e1ee..705f7754dc 100644 --- a/crates/miden-standards/asm/standards/auth/multisig.masm +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -87,8 +87,6 @@ const ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS = "procedure threshold exceeds ne const ERR_PROC_ROOT_NOT_IN_ACCOUNT = "cannot set procedure threshold for procedure root that is not one of the account's procedures" -const ERR_CLEANUP_THRESHOLD_CONFIG_NOT_RECONCILED = "threshold config must be reconciled with the new approver count before cleaning up approver mappings" - #! Remove old approver public keys and the corresponding scheme ids from the approver public key and #! scheme id mappings. #! @@ -96,6 +94,12 @@ const ERR_CLEANUP_THRESHOLD_CONFIG_NOT_RECONCILED = "threshold config must be re #! init_num_of_approvers) from APPROVER_PUBLIC_KEYS_SLOT and APPROVER_SCHEME_ID_SLOT. It does not #! touch THRESHOLD_CONFIG_SLOT or PROC_THRESHOLD_ROOTS_SLOT. #! +#! Caller contract: before calling this, the caller must have already updated THRESHOLD_CONFIG_SLOT +#! so that the threshold is still reachable with the reduced signer set, i.e. threshold <= +#! new_num_of_approvers. Otherwise the account is left with a threshold that no remaining quorum can +#! reach, making it permanently unable to authenticate. In-tree this is guaranteed by +#! `update_signers_and_threshold`, which calls `assert_threshold_is_reachable` before this procedure. +#! #! Inputs: [init_num_of_approvers, new_num_of_approvers] #! Outputs: [] #! @@ -105,26 +109,7 @@ const ERR_CLEANUP_THRESHOLD_CONFIG_NOT_RECONCILED = "threshold config must be re #! #! Panics if: #! - init_num_of_approvers or new_num_of_approvers is not a u32 value. -#! - the stored threshold config has not been reconciled with new_num_of_approvers. pub proc cleanup_pubkey_and_scheme_id_mapping(init_num_of_approvers: u32, new_num_of_approvers: u32) - # Guard against callers that shrink the approver maps without reconciling THRESHOLD_CONFIG_SLOT. - exec.get_current_threshold_and_num_approvers - # => [threshold, num_approvers, init_num_of_approvers, new_num_of_approvers] - - # assert the stored threshold is reachable with the new signer count (threshold <= new count) - dup dup.4 - # => [new_num_of_approvers, threshold, threshold, num_approvers, init, new] - u32assert2.err=ERR_CLEANUP_THRESHOLD_CONFIG_NOT_RECONCILED - u32gt assertz.err=ERR_CLEANUP_THRESHOLD_CONFIG_NOT_RECONCILED - # => [threshold, num_approvers, init, new] - - # assert the stored approver count already reflects the reduced signer set - dup.1 dup.4 eq assert.err=ERR_CLEANUP_THRESHOLD_CONFIG_NOT_RECONCILED - # => [threshold, num_approvers, init, new] - - drop drop - # => [init_num_of_approvers, new_num_of_approvers] - dup.1 dup.1 u32assert2.err=ERR_APPROVER_COUNTS_NOT_U32 u32lt @@ -176,6 +161,20 @@ pub proc cleanup_pubkey_and_scheme_id_mapping(init_num_of_approvers: u32, new_nu drop drop end +#! Asserts the threshold is reachable with the given number of approvers, i.e. threshold is not +#! greater than num_approvers. +#! +#! Inputs: [num_approvers, threshold] +#! Outputs: [] +#! +#! Panics if: +#! - num_approvers or threshold is not a u32 value. +#! - threshold > num_approvers. +pub proc assert_threshold_is_reachable + u32assert2.err=ERR_MALFORMED_MULTISIG_CONFIG + u32gt assertz.err=ERR_MALFORMED_MULTISIG_CONFIG +end + #! Asserts that all configured per-procedure threshold overrides are less than or equal to #! number of approvers. #! @@ -284,9 +283,8 @@ pub proc update_signers_and_threshold(multisig_config_hash: word) dup dup.2 # => [num_approvers, threshold, MULTISIG_CONFIG, pad(12)] - # make sure that the threshold is smaller than the number of approvers - u32assert2.err=ERR_MALFORMED_MULTISIG_CONFIG - u32gt assertz.err=ERR_MALFORMED_MULTISIG_CONFIG + # make sure that the threshold is reachable with the number of approvers + exec.assert_threshold_is_reachable # => [MULTISIG_CONFIG, pad(12)] dup dup.2 diff --git a/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm b/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm index e0e9e7c648..6bd03b70b2 100644 --- a/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm +++ b/crates/miden-standards/asm/standards/auth/multisig_smart/mod.masm @@ -41,8 +41,6 @@ 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" @@ -667,8 +665,8 @@ pub proc update_signers_and_threshold(multisig_config_commitment: word) dup dup.2 # => [num_approvers, threshold, MULTISIG_CONFIG, pad(12)] - u32assert2.err=ERR_MALFORMED_MULTISIG_CONFIG - u32gt assertz.err=ERR_MALFORMED_MULTISIG_CONFIG + # make sure that the threshold is reachable with the number of approvers + exec.multisig::assert_threshold_is_reachable # => [MULTISIG_CONFIG, pad(12)] dup dup.2