feat(standards): add foundation for Multisig Smart Authentication Component#2806
feat(standards): add foundation for Multisig Smart Authentication Component#2806onurinanc wants to merge 17 commits into0xMiden:nextfrom
Conversation
partylikeits1983
left a comment
There was a problem hiding this comment.
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!
|
|
||
|
|
There was a problem hiding this comment.
Nit: double empty line
| #! | ||
| #! Invocation: exec | ||
| @locals(3) | ||
| pub proc set_procedure_policy |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
Is it not possible to reuse the existing setup_keys_and_authenticators_with_scheme function which we use in the existing mutlisig?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
PhilippGackstatter
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Nit: Should we add a constant for this (e.g. the variant which encodes to 3) to avoid the magic number?
| # => [is_valid_note_restrictions, note_restrictions] | ||
|
|
||
| assert.err=ERR_INVALID_NOTE_RESTRICTIONS | ||
| # => [note_restrictions] | ||
|
|
||
| drop | ||
| # => [] |
There was a problem hiding this comment.
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.
| proc compute_tx_threshold_smart(policy_threshold: u32, default_threshold: u32) -> u32 | ||
| swap | ||
| # => [default_threshold, policy_threshold] |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Nit: Do we need the _smart suffix when the module already contains that in its name?
| 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)] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I think the code works correctly but the comments are incorrect. Any case, it's obviously better to use u32max. Thanks for the suggestion.
| # => [OLD_THRESHOLD_CONFIG, pad(12)] | ||
|
|
||
| drop loc_store.1 drop drop | ||
| # => [pad(12)] |
There was a problem hiding this comment.
This auto-pads back to 16.
| drop loc_store.1 drop drop | ||
| # => [pad(12)] | ||
|
|
||
| loc_load.0 | ||
| # => [num_approvers] |
There was a problem hiding this comment.
The pad comment is missing.
| # ------ 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] |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
Should we make this error a constant?
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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-5receive_asset: 1-of-5update_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.
There was a problem hiding this comment.
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:
protocol/crates/miden-standards/asm/standards/auth/multisig.masm
Lines 427 to 432 in e1a92ce
I still think this approach has problems though, for example:
- Multisig configured with
default_threshold = 3andreceive_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
There was a problem hiding this comment.
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_calledwhich means you can only call one procedure at a time,receive_assetorupdate_signers_and_thresholds. - or, configure
update_signers_and_signatures, 4, so that it will effectively choose 4 not 1 for thereceive_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
There was a problem hiding this comment.
This is why we need to configure the
update_signers_and_thresholdsto 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:
- Determine the final threshold using
max(default_threshold, max(policy_threshold, spending_threshold))rather than potentially using a lower combined threshold than the default. - 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.
- 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.
There was a problem hiding this comment.
Before diving into the comments, there are also related discussions you might be interested to taking a look:
- Spending Limits OpenZeppelin/miden-confidential-contracts#35
- Amount-based Thresholds OpenZeppelin/miden-confidential-contracts#36
- Authentication Component Combining Spending Limits + Amount Based Thresholds OpenZeppelin/miden-confidential-contracts#37
- Timelocked Account OpenZeppelin/guardian#61
- 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_asset2-of-5: send at most $10k3-of-5: send at most $100k4-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
There was a problem hiding this comment.
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
maxfor 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).
|
Addressed the issues, suggestions, and corrections, ready for review. |
| #[derive(Debug, Clone, PartialEq, Eq)] | ||
| pub struct AuthMultisigSmartConfig { | ||
| approvers: Vec<(PublicKeyCommitment, AuthScheme)>, | ||
| default_threshold: u32, | ||
| procedure_policies: Vec<(Word, ProcedurePolicy)>, | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
| 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") | ||
| }); |
There was a problem hiding this comment.
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.
| 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(), | ||
| ), | ||
| ) | ||
| } |
There was a problem hiding this comment.
| 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])); |
There was a problem hiding this comment.
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.
| 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") | ||
| ); |
There was a problem hiding this comment.
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)]; |
There was a problem hiding this comment.
| 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( |
There was a problem hiding this comment.
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.
| /// 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<()> { |
There was a problem hiding this comment.
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.
| push.0 # note_restrictions | ||
| push.0 # delayed_threshold | ||
| push.1 # immediate_threshold | ||
| call.::miden::standards::components::auth::multisig_smart::set_procedure_policy |
There was a problem hiding this comment.
Could we change this to use the Rust definitions, e.g.:
| 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.
| // 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])); |
There was a problem hiding this comment.
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]));
This is a follow-up PR for: #2670
It's a foundation for MultisigSmart Authentication Component will have the following properties:
The completed version of the PR having the above features is a big PR, so, decided to separate these PRs into 3:
I believe this PR is self-explanatory since it doesn't include any features but prepares a core component for MultisigSmart.