Skip to content

feat(standards): add foundation for Multisig Smart Authentication Component#2806

Open
onurinanc wants to merge 17 commits into0xMiden:nextfrom
onurinanc:multisig-smart-foundation
Open

feat(standards): add foundation for Multisig Smart Authentication Component#2806
onurinanc wants to merge 17 commits into0xMiden:nextfrom
onurinanc:multisig-smart-foundation

Conversation

@onurinanc
Copy link
Copy Markdown
Collaborator

This is a follow-up PR for: #2670

It's a foundation for MultisigSmart Authentication Component will have the following properties:

  • Timelocked Accounts
  • Amount-Based Thresholds
  • Spending Limits with Oracle Integration
  • Guardian Integration to Smart Account

The completed version of the PR having the above features is a big PR, so, decided to separate these PRs into 3:

  • First PR is this PR implementing a core foundation for MultisigSmart
  • The follow-up PR will be adding the above features (if it is again a big PR, will try to split it up to 2 PRs)
  • Final PR is the Presets Configurations for Multisig Accounts with the Security Best Practices

I believe this PR is self-explanatory since it doesn't include any features but prepares a core component for MultisigSmart.

@mmagician mmagician added standards Related to standard note scripts or account components pr-from-maintainers PRs that come from internal contributors or integration partners. They should be given priority labels Apr 29, 2026
Copy link
Copy Markdown
Contributor

@partylikeits1983 partylikeits1983 left a comment

Choose a reason for hiding this comment

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

Looks good! Just a few comments during my first review pass.

I saw some MASM that I saw could be cycle count optimized so I went to check how many cycles could be shaved off in the set_procedure_policy procedure, but then realized that the tests do not call it.

I'd push for adding tests which execute all MASM which is added. I understand this is a foundational PR for follow up PRs, but even minimal testing would be better than no tests. I think once this has a bit more test coverage it will be ready to merge, because everything else looks good!

Comment on lines +13 to +14


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: double empty line

Suggested change

#!
#! Invocation: exec
@locals(3)
pub proc set_procedure_policy
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I could not find a test which calls set_procedure_policy. Could we add a test which calls it just so we don't add code which is never tested.

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

Choose a reason for hiding this comment

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

Same here, can we add a test which calls this?

(Vec<AuthSecretKey>, Vec<AuthScheme>, Vec<PublicKey>, Vec<BasicAuthenticator>);

/// Sets up secret keys, auth schemes, public keys, and authenticators for a specific scheme.
fn setup_keys_and_authenticators_with_scheme(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it not possible to reuse the existing setup_keys_and_authenticators_with_scheme function which we use in the existing mutlisig?

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.

Good point! There are 3 different versions of multisig tests use this setup, and would be good to use a common helpers. However, not to increase the size of this PR, we can do this refactor in a separate PR.

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.

Afaict these are exactly the same, so we can simply make the one in multisig.rs pub(super) and reuse it and remove this function. This would decrease the size of this PR.

Copy link
Copy Markdown
Contributor

@PhilippGackstatter PhilippGackstatter left a comment

Choose a reason for hiding this comment

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

Looked at the MASM code and left a few suggestions for optimization, simplification, correctness, etc., but overall looks good. Agree with @partylikeits1983 that we need tests for all procedures (sooner or later at least).

u32assert.err=ERR_INVALID_NOTE_RESTRICTIONS
# => [note_restrictions]

dup u32lte.3
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: Should we add a constant for this (e.g. the variant which encodes to 3) to avoid the magic number?

Comment on lines +86 to +92
# => [is_valid_note_restrictions, note_restrictions]

assert.err=ERR_INVALID_NOTE_RESTRICTIONS
# => [note_restrictions]

drop
# => []
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.

We duplicate the restrictions unnecessarily further above and then drop it here. This pattern comes up every now and then. If you use an LLM for code generation, it may be worth adding this to CLAUDE.md, or similar files.

Comment on lines +137 to +139
proc compute_tx_threshold_smart(policy_threshold: u32, default_threshold: u32) -> u32
swap
# => [default_threshold, policy_threshold]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: This procedure could take [default_threshold, policy_threshold] as input so it doesn't have to swap, since the caller could easily load the arguments in reverse.

#! Amount-Based Thresholds features land, it will accept an additional `spending_threshold`
#! parameter and pick max(policy_threshold, spending_threshold) before falling back to
#! default_threshold when both are zero.
proc compute_tx_threshold_smart(policy_threshold: u32, default_threshold: u32) -> u32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: Do we need the _smart suffix when the module already contains that in its name?

Comment on lines +159 to +170
proc max_threshold_pair
dup.1 dup.1
# => [lhs, rhs, lhs, rhs]

swap
# => [rhs, lhs, lhs, rhs]

u32gt
# => [is_rhs_gt, lhs, rhs]

cdrop
# => [max(lhs, rhs)]
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.

The docs don't match the code here, iiuc. u32gt pushes is_lhs_gt on the stack, and if so, cdrop leaves lhs on the stack, rhs otherwise.

But, more generally, we should be able to remove this procedure and use u32max instead.

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.

I think the code works correctly but the comments are incorrect. Any case, it's obviously better to use u32max. Thanks for the suggestion.

Comment on lines +664 to +667
# => [OLD_THRESHOLD_CONFIG, pad(12)]

drop loc_store.1 drop drop
# => [pad(12)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This auto-pads back to 16.

Comment on lines +666 to +670
drop loc_store.1 drop drop
# => [pad(12)]

loc_load.0
# => [num_approvers]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The pad comment is missing.

Comment on lines +763 to +772
# ------ Computing procedure policy ------
exec.compute_procedure_policy_context
# => [policy_threshold, note_restrictions, TX_SUMMARY_COMMITMENT]

loc_store.0
# => [note_restrictions, TX_SUMMARY_COMMITMENT]

# note_restrictions are already enforced inside compute_procedure_policy_context.
drop
# => [TX_SUMMARY_COMMITMENT]
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.

Should we make it more obvious that compute_procedure_policy_context enforces things? I think the compute part is probably less important so I'd suggest just enforce_procedure_policy.


if.true
emit.AUTH_UNAUTHORIZED_EVENT
push.0 assert.err="insufficient number of signatures"
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.

Should we make this error a constant?

Comment on lines +137 to +146
proc compute_tx_threshold_smart(policy_threshold: u32, default_threshold: u32) -> u32
swap
# => [default_threshold, policy_threshold]

dup.1 eq.0
# => [is_policy_zero, default_threshold, policy_threshold]

cdrop
# => [effective_transaction_threshold]
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why does a non-zero policy threshold override the default threshold? If the latter is higher, we'd drop down to a lower one which seems potentially problematic. miden::standards::auth::multisig treats the default threshold as the minimum (miden::standards::auth::multisig::compute_transaction_threshold picks the higher one) which seems like a more secure approach.

If we'd want to do the same, we could remove this procedure and use u32max directly in auth_tx.

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.

Why does a non-zero policy threshold override the default threshold? If the latter is higher, we'd drop down to a lower one which seems potentially problematic. miden::standards::auth::multisig treats the default threshold as the minimum (miden::standards::auth::multisig::compute_transaction_threshold picks the higher one) which seems like a more secure approach.

I think it is similar how it works in multisig. Let's say the default threshold is 3 for the multisig, and we would like to have 1-of-5 can use receive_asset.

I've also put this procedure temporarily, and will be changed after the spending limits and amount based thresholds are included, the procedure will override the default threshold to be able to use a specific procedure as an example:

  • default_threshold: 3-of-5
  • receive_asset: 1-of-5
  • update_signers_and_thresholds : 4-of-5

Then, if just the receive_asset is called, it is 1-of-5, if update_signers_and_thresholds is called, it is 4-of-5, and if both of them are called in the same tx, then 4-of-5 is valid.

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.

Ah, you are right. I erroneously looked at how the multisig updates the transaction_threshold in the loop (which is essentially using the max function). After the loop, it picks between the tx threshold and default threshold as you describe:

# 3. if transaction_threshold == 0 at the end, revert to using default_threshold
dup.1 eq.0
# => [is_zero, default_threshold, transaction_threshold]
cdrop
# => [effective_transaction_threshold]

I still think this approach has problems though, for example:

  • Multisig configured with default_threshold = 3 and receive_asset_threshold = 1.
  • I create a tx to update the public key in the account and I consequently need to meet the default threshold of 3 because nothing else is configured.
  • I create a tx to update the public key and I receive an asset. Now, according to this logic, the tx_threshold comes out at 1, so the above logic will pick it over the default threshold. So, I just need the receive_asset threshold of 1.

That doesn't seem right - I can update a public key with one signature. I should always need at least the default threshold, otherwise I can escalate privileges by calling the procedure with the lowest threshold (here receive_asset).

If we change to this approach, then on the configuration level (in set_procedure_threshold and set_procedure_policy respectively), we should check that the procedure thresholds are strictly greater than the default threshold, otherwise there is no point in configuring them.

cc @mmagician or @partylikeits1983 in case I got something wrong about the multisig thresholds

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.

I create a tx to update the public key in the account and I consequently need to meet the default threshold of 3 because nothing else is configured.

This is why we need to configure the update_signers_and_thresholds to 4 for example. Not in this PR, but in the one PR after the following PR, we'll have security best consideration presets for the default configuration. There is obviously critical issue with the current state of the multisig.masm configurations as you have stated but this will be fixed the configurations such as:

  • using tx_policy::assert_only_one_non_auth_procedure_called which means you can only call one procedure at a time, receive_asset or update_signers_and_thresholds.
  • or, configure update_signers_and_signatures, 4, so that it will effectively choose 4 not 1 for the receive_asset

The completed version of the PR having the above features is a big PR, so, decided to separate these PRs into 3:

  • First PR is this PR implementing a core foundation for MultisigSmart
  • The follow-up PR will be adding the above features (if it is again a big PR, will try to split it up to 2 PRs)
  • Final PR is the Presets Configurations for Multisig Accounts with the Security Best Practices

Please see above also the 3rd part.

In addition to that, in the follow-up PR, I'll be changing the above procedure to something else, which is similar to:

#! Computes the effective transaction threshold for multisig smart policy.
#!
#! Selects the highest threshold among policy_threshold and spending_threshold, then
#! falls back to default_threshold when the combined result is zero.
#!
#! Inputs:  [policy_threshold, spending_threshold, default_threshold]
#! Outputs: [transaction_threshold]
#!
#! Where:
#! - policy_threshold is the threshold derived from the called procedure policies.
#! - spending_threshold is the threshold derived from spending limits.
#! - default_threshold is the account's configured default multisig threshold.
#! - transaction_threshold is the effective minimum number of signatures required.
#!
#! Invocation: exec
#!
#! Locals:
#! 0: policy_threshold
#! 1: spending_threshold
#! 2: default_threshold
@locals(3)
proc compute_tx_threshold_smart(policy_threshold: u32, spending_threshold: u32, default_threshold: u32) -> u32
    loc_store.0
    # => [spending_threshold, default_threshold]

    loc_store.1
    # => [default_threshold]

    loc_store.2
    # => []

    loc_load.0
    loc_load.1
    # => [spending_threshold, policy_threshold]

    dup.1 dup.1
    # => [spending_threshold, policy_threshold, spending_threshold, policy_threshold]

    swap
    # => [policy_threshold, spending_threshold, spending_threshold, policy_threshold]

    u32gt
    # => [is_policy_gt, spending_threshold, policy_threshold]

    cdrop
    # => [combined_threshold]

    loc_load.2
    # => [default_threshold, combined_threshold]

    dup.1 eq.0
    # => [is_zero, default_threshold, combined_threshold]

    cdrop
    # => [effective_transaction_threshold]
end

Copy link
Copy Markdown
Contributor

@PhilippGackstatter PhilippGackstatter May 7, 2026

Choose a reason for hiding this comment

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

This is why we need to configure the update_signers_and_thresholds to 4 for example

Right, so we effectively require all critical procedures to have a threshold configured that must be strictly greater than the default threshold (logically, otherwise why configure it at all). All other less critical functionality that has no special configuration would automatically use at least the default threshold. Here I can see the usefulness of the default threshold: It avoids having to configure a threshold for each procedure individually.

If we continue to allow procedure thresholds to be configured that are less than the default threshold, we can still run into the situation I described earlier where I call receive_asset which gives me the configured threshold of 1, and I can additionally invoke "less critical functionality" with a lower threshold than the default, which is likely not intended by the one who configured the multisig. Even if this is "less critical", it's still suboptimal.

A tx policy like tx_policy::assert_only_one_non_auth_procedure_called would mitigate this, but the whole system seems very complex to setup correctly at that point, and I think we should close this hole regardless, so one doesn't have to opt-in to such tx policies for security.

This seems security critical to me, so I would still suggest doing one of these:

  1. Determine the final threshold using max(default_threshold, max(policy_threshold, spending_threshold)) rather than potentially using a lower combined threshold than the default.
  2. Disallow spending and procedure thresholds that are lower than the default threshold. That way, by construction, it is impossible to get an overall threshold that is lower than the default.
  3. Remove the default threshold altogether and require each procedure to be configured individually. Maximal explicitness, maximal configuration burden and maximal safety.

If we do 1, we should also do 2, because a lower procedure threshold would never take effect.

I would suggest going with 1, because it doesn't require a ton of configuration and seems safe to me: We have a default threshold that can be selectively increased for certain procedures by using procedure or spending thresholds.

Copy link
Copy Markdown
Collaborator Author

@onurinanc onurinanc May 7, 2026

Choose a reason for hiding this comment

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

Before diving into the comments, there are also related discussions you might be interested to taking a look:

  1. Spending Limits OpenZeppelin/miden-confidential-contracts#35
  2. Amount-based Thresholds OpenZeppelin/miden-confidential-contracts#36
  3. Authentication Component Combining Spending Limits + Amount Based Thresholds OpenZeppelin/miden-confidential-contracts#37
  4. Timelocked Account OpenZeppelin/guardian#61
  1. Determine the final threshold using max(default_threshold, max(policy_threshold, spending_threshold)) rather than potentially using a lower combined threshold than the default.

I'm not sure we can do this if we would like to support "amount-based thresholds" which is a contradicting idea having the max(default_threshold) unless we have the default_threshold is configuring to 1. The main reason is that we would like to have the following as an example configuration:

  • 1-of-5: receive_asset
  • 2-of-5: send at most $10k
  • 3-of-5: send at most $100k
  • 4-of-5: send at most $1M, rotate key, for example.

So, having max(default_threshold) doesn't let us to do that.

This is why we would like to have preset configurations for "high-impact procedures".

CC: @bobbinth for any comments

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'll have to take some time to check the discussions, but to respond to your comment:

So, having max(default_threshold) doesn't let us to do that.

I think it would work as long as we set default_threshold = 1. This is not great, but still better than the current situation because at least it makes the "low" default explicit, and that any procedure that should require a higher threshold needs to set that procedure/spending/... threshold explicitly.

But at this point, the default threshold is no longer useful, at least in the scenarios you mention. So, we could instead go with the third option I mentioned:

Remove the default threshold altogether and require each procedure to be configured individually. Maximal explicitness, maximal configuration burden and maximal safety.

You'd then want to make sure that each account procedure has an explicit threshold defined. With this, we get the same overall behavior as if we kept the default threshold and used max(default_threshold, combined_threshold), because procedure thresholds are combined using max.

Overall, I think:

  • For the regular multisig, keeping the default threshold probably makes sense, but changing it to use max for combination.
  • For the smart multisig, the default threshold seems to do more harm than good, so I'd consider removing it, thereby requiring that all procedures are explicitly configured. If a procedure isn't configured, the auth procedure should abort (which is tricky to enforce in component constructor in Rust itself, but in MASM, we can check that during updates in assert_proc_policies_lte_num_approvers).

@onurinanc
Copy link
Copy Markdown
Collaborator Author

onurinanc commented May 6, 2026

Addressed the issues, suggestions, and corrections, ready for review.

Comment on lines +58 to +63
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthMultisigSmartConfig {
approvers: Vec<(PublicKeyCommitment, AuthScheme)>,
default_threshold: u32,
procedure_policies: Vec<(Word, ProcedurePolicy)>,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The config is quite similar to AuthMultisigConfig. We may want to consider implementing AuthMultisig by configuring AuthMultisigSmart with immediate mode and without note restrictions. If we eventually also change guarded_multisig to use multisig_smart as the underlying implementation, this would allow us to maintain just one multisig codebase that can do everything, and is just configured differently by the concrete AuthMultisig, AuthGuardedMultisig and AuthMultisigSmart. So we could remove quite a bit of almost duplicated code (i.e. multisig::update_signers_and_threshold and multisig_smart::update_signers_and_threshold, etc.).

This probably needs to be re-evaluated once we are done with multisig smart, to see what the overhead of the smart procedures is vs the plain multisig ones. So, no changes requested for this PR, just a thought for now.

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.

This probably needs to be re-evaluated once we are done with multisig smart, to see what the overhead of the smart procedures is vs the plain multisig ones. So, no changes requested for this PR, just a thought for now.

Agreed!

Comment on lines +29 to +47
static THRESHOLD_CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::multisig::threshold_config")
.expect("storage slot name should be valid")
});

static APPROVER_PUBKEYS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::multisig::approver_public_keys")
.expect("storage slot name should be valid")
});

static APPROVER_SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::multisig::approver_schemes")
.expect("storage slot name should be valid")
});

static EXECUTED_TRANSACTIONS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::auth::multisig::executed_transactions")
.expect("storage slot name should be valid")
});
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.

These are identical to the ones defined in crates/miden-standards/src/account/auth/multisig.rs. Could we reuse those by making them pub(super)? This also makes it easier to see that these are shared.

Comment on lines +193 to +202
pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::approver_public_keys_slot().clone(),
StorageSlotSchema::map(
"Approver public keys",
SchemaType::u32(),
SchemaType::pub_key(),
),
)
}
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 fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::approver_public_keys_slot().clone(),
StorageSlotSchema::map(
"Approver public keys",
SchemaType::u32(),
SchemaType::pub_key(),
),
)
}
pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
AuthMultisig::approver_public_keys_slot_schema()
}

Since these are the same, I'd reuse the schema definition from there. Applies to all schema functions here.

.storage()
.get_item(AuthMultisigSmart::threshold_config_slot())
.expect("threshold config should be present");
assert_eq!(threshold_config, Word::from([2u32, 2u32, 0, 0]));
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.

Suggestion: Further above, define:

let num_approvers = approvers.len() as u32;
let default_threshold = 2u32;
let config = AuthMultisigSmartConfig::new(approvers.clone(), default_threshold)
// ...
assert_eq!(threshold_config, Word::from([default_threshold, num_approvers, 0, 0]));

Which makes this more robust against change and easier to verify.

Same idea for the other assertions in this module.

Comment on lines +381 to +402
let result = AuthMultisigSmartConfig::new(approvers.clone(), 2).and_then(|cfg| {
let policy = ProcedurePolicy::with_immediate_and_delay_thresholds(1, 2)?;
cfg.with_proc_policies(vec![(Word::from([1u32, 2, 3, 4]), policy)])
});
assert!(
result
.unwrap_err()
.to_string()
.contains("delay threshold cannot exceed immediate threshold")
);

let result = AuthMultisigSmartConfig::new(approvers, 2).and_then(|cfg| {
let policy = ProcedurePolicy::with_immediate_threshold(0)?
.with_note_restriction(ProcedurePolicyNoteRestriction::NoInputNotes);
cfg.with_proc_policies(vec![(Word::from([4u32, 3, 2, 1]), policy)])
});
assert!(
result
.unwrap_err()
.to_string()
.contains("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.

This effectively tests ProcedurePolicy, and these invariants are already tested in procedure_policy_construction_rejects_invalid_combinations. I would remove this here, because it doesn't test anything new.

Felt::new(0),
]);

let scheme_word = [Felt::new(auth_scheme as u64), Felt::new(0), Felt::new(0), Felt::new(0)];
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
let scheme_word = [Felt::new(auth_scheme as u64), Felt::new(0), Felt::new(0), Felt::new(0)];
let scheme_word = [Felt::from(auth_scheme.as_u8()), Felt::ZERO, Felt::ZERO, Felt::ZERO];

nit

/// Layout expected by `update_signers_and_threshold` when looking up the new multisig config in
/// the advice map: `[threshold, num_approvers, 0, 0, (PUB_KEY, SCHEME_WORD) for each approver]`.
/// Public keys are appended in reverse so the procedure pops them in ascending index order.
fn build_update_signers_config_vector(
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.

Thanks for pulling this out into a helper! It might be relatively easy to reuse this function in the multisig.rs tests, where we duplicate this logic four times. If so, would be great to include this in this PR.

Comment on lines +213 to +221
/// A procedure policy with `NoInputOrOutputNotes` restriction must abort any transaction that
/// reaches that procedure while carrying input or output notes.
#[rstest]
#[case::ecdsa(AuthScheme::EcdsaK256Keccak)]
#[case::falcon(AuthScheme::Falcon512Poseidon2)]
#[tokio::test]
async fn test_multisig_smart_proc_policy_no_notes_constraint_is_enforced(
#[case] auth_scheme: AuthScheme,
) -> anyhow::Result<()> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should ideally cover all possible ProcedurePolicyNoteRestriction variants.

I think we could do this by replacing this test with two tests and rstest parameterization:

#[rstest]
#[case::no_restriction(ProcedurePolicyNoteRestriction::None, None)]
#[case::no_input_notes(
    ProcedurePolicyNoteRestriction::NoInputNotes,
    Some(ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_INPUT_NOTES)
)]
#[case::no_output_notes(
    ProcedurePolicyNoteRestriction::NoOutputNotes,
    None,
)]
#[case::no_input_or_output_notes(
    ProcedurePolicyNoteRestriction::NoInputOrOutputNotes,
    Some(ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_INPUT_NOTES)
)]
#[tokio::test]
async fn test_multisig_smart_enforces_note_restrictions_on_tx_with_input_notes(
    #[case] restriction: ProcedurePolicyNoteRestriction,
    #[case] expected_error: Option<MasmError>,
) -> anyhow::Result<()> {
    todo!("setup tx with one input note")
}

And another test test_multisig_smart_enforces_note_restrictions_on_tx_with_output_notes that has an analogous setup, just with an output note rather than an input note.

Here I don't think the parameterization over different auth schemes is super important, so I'd rather use the rstest parameters for this.

Comment on lines +393 to +396
push.0 # note_restrictions
push.0 # delayed_threshold
push.1 # immediate_threshold
call.::miden::standards::components::auth::multisig_smart::set_procedure_policy
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.

Could we change this to use the Rust definitions, e.g.:

Suggested change
push.0 # note_restrictions
push.0 # delayed_threshold
push.1 # immediate_threshold
call.::miden::standards::components::auth::multisig_smart::set_procedure_policy
push.{note_restrictions} # note_restrictions
push.0 # delayed_threshold
push.1 # immediate_threshold
call.::miden::standards::components::auth::multisig_smart::set_procedure_policy
// ...
note_restrictions = ProcedurePolicyNoteRestriction::NoInputNotes as u8,
// ... similarly for the others

Better for maintainability.

Comment on lines +441 to +446
// Policy word layout: [immediate, delayed, note_restrictions, 0]
let stored_policy = multisig_account
.storage()
.get_map_item(AuthMultisigSmart::procedure_policies_slot(), receive_asset_root)
.expect("procedure policies slot should be present");
assert_eq!(stored_policy, Word::from([1u32, 0, 0, 0]));
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.

Similar comment as before: Would be good to define the value once and then reuse it, e.g.:

let note_restrictions = ProcedurePolicyNoteRestriction::NoInputNotes;

// ...
begin
    push.{root}
    push.{note_restrictions}    # note_restrictions

// ...
assert_eq!(stored_policy, Word::from([..., ..., note_restrictions, 0]));

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

Labels

pr-from-maintainers PRs that come from internal contributors or integration partners. They should be given priority standards Related to standard note scripts or account components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants